Skip to content

Command name autocompletion #1380

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 31 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
8608c52
Add up down message history
Argmaster Mar 25, 2025
3f6fbde
Deduplicate messages when inserting into history
Argmaster Mar 26, 2025
eb064b1
Add dedicated function for inserting strings into TextInput
Argmaster Mar 26, 2025
5c0c4d2
Fix formatting issues
Argmaster Mar 26, 2025
bb1cc73
Merge remote-tracking branch 'origin/master' into feature/up-down-his…
Argmaster Mar 28, 2025
14f7b81
Change history behavior
Argmaster Mar 28, 2025
ad154fc
Rename inputString to setString
Argmaster Mar 28, 2025
93c014a
Move clearing to setString
Argmaster Mar 28, 2025
a7325eb
Apply suggestions from code review
Argmaster Apr 19, 2025
ea373d5
Merge remote-tracking branch 'origin/master' into feature/up-down-his…
Argmaster Apr 22, 2025
9d574a2
Remove unused cursor capture
Argmaster Apr 22, 2025
fe5ce81
Use FixedSizeCircularBuffer for history
Argmaster Apr 25, 2025
cf18507
Restore large queue size
Argmaster Apr 25, 2025
a69839b
Allow navigation to empty entry
Argmaster Apr 25, 2025
85d6549
self.len must never be bigger than capacity
Argmaster Apr 27, 2025
7edd1e3
Move optional callbacks into struct
Argmaster Apr 27, 2025
cfc5079
Use enum for moveCursorVertically return value
Argmaster Apr 27, 2025
205e2c2
WA attempt #1
Argmaster Apr 27, 2025
f63e19f
Fix edge case from review
Argmaster Apr 28, 2025
c9db0aa
Merge remote-tracking branch 'origin/master' into feature/up-down-his…
Argmaster Apr 28, 2025
04ac810
Update src/gui/windows/chat.zig
Argmaster Apr 29, 2025
1d199cc
Merge remote-tracking branch 'upstream/master' into feature/up-down-h…
Argmaster Apr 29, 2025
affb123
Remove isEmpty and isFull
Argmaster Apr 29, 2025
7d89f85
Allow for empty history entry
Argmaster Apr 30, 2025
bcb3743
Change empty message handling some more
Argmaster Apr 30, 2025
2ff79af
Remove unused methods
Argmaster May 1, 2025
4d1cabe
Go to hell with all of those edge cases <3
Argmaster May 2, 2025
b965c0a
WA for 4b
Argmaster May 2, 2025
68afd40
Merge branch 'feature/up-down-history' into feature/autocomplete
Argmaster May 2, 2025
ac3b486
Add tab actions to TextInput
Argmaster May 2, 2025
5e1c4c1
Implement autocompletion cycling
Argmaster May 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 42 additions & 7 deletions src/gui/components/TextInput.zig
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ maxHeight: f32,
textSize: Vec2f = undefined,
scrollBar: *ScrollBar,
onNewline: gui.Callback,
optional: OptionalCallbacks,

pub fn __init() void {
texture = Texture.initFromFile("assets/cubyz/ui/text_input.png");
Expand All @@ -42,7 +43,14 @@ pub fn __deinit() void {
texture.deinit();
}

pub fn init(pos: Vec2f, maxWidth: f32, maxHeight: f32, text: []const u8, onNewline: gui.Callback) *TextInput {
const OptionalCallbacks = struct {
onUp: ?gui.Callback = null,
onDown: ?gui.Callback = null,
onTab: ?gui.Callback = null,
onCharacter: ?gui.Callback = null,
};

pub fn init(pos: Vec2f, maxWidth: f32, maxHeight: f32, text: []const u8, onNewline: gui.Callback, optional: OptionalCallbacks) *TextInput {
const scrollBar = ScrollBar.init(undefined, scrollBarWidth, maxHeight - 2*border, 0);
const self = main.globalAllocator.create(TextInput);
self.* = TextInput{
Expand All @@ -54,6 +62,7 @@ pub fn init(pos: Vec2f, maxWidth: f32, maxHeight: f32, text: []const u8, onNewli
.maxHeight = maxHeight,
.scrollBar = scrollBar,
.onNewline = onNewline,
.optional = optional,
};
self.currentString.appendSlice(text);
self.textSize = self.textBuffer.calculateLineBreaks(fontSize, maxWidth - 2*border - scrollBarWidth);
Expand Down Expand Up @@ -239,8 +248,13 @@ pub fn right(self: *TextInput, mods: main.Window.Key.Modifiers) void {
}
}

fn moveCursorVertically(self: *TextInput, relativeLines: f32) void {
self.cursor = self.textBuffer.mousePosToIndex(self.textBuffer.indexToCursorPos(self.cursor.?) + Vec2f{0, 16*relativeLines}, self.currentString.items.len);
fn moveCursorVertically(self: *TextInput, relativeLines: f32) enum {changed, same} {
const newCursor = self.textBuffer.mousePosToIndex(self.textBuffer.indexToCursorPos(self.cursor.?) + Vec2f{0, 16*relativeLines}, self.currentString.items.len);
self.cursor = newCursor;
if(self.cursor != newCursor) {
return .changed;
}
return .same;
}

pub fn down(self: *TextInput, mods: main.Window.Key.Modifiers) void {
Expand All @@ -249,7 +263,7 @@ pub fn down(self: *TextInput, mods: main.Window.Key.Modifiers) void {
if(self.selectionStart == null) {
self.selectionStart = cursor.*;
}
self.moveCursorVertically(1);
_ = self.moveCursorVertically(1);
if(self.selectionStart == self.cursor) {
self.selectionStart = null;
}
Expand All @@ -258,7 +272,9 @@ pub fn down(self: *TextInput, mods: main.Window.Key.Modifiers) void {
cursor.* = @max(cursor.*, selectionStart);
self.selectionStart = null;
} else {
self.moveCursorVertically(1);
if(self.moveCursorVertically(1) == .same) {
if(self.optional.onDown) |cb| cb.run();
}
}
}
self.ensureCursorVisibility();
Expand All @@ -271,7 +287,7 @@ pub fn up(self: *TextInput, mods: main.Window.Key.Modifiers) void {
if(self.selectionStart == null) {
self.selectionStart = cursor.*;
}
self.moveCursorVertically(-1);
_ = self.moveCursorVertically(-1);
if(self.selectionStart == self.cursor) {
self.selectionStart = null;
}
Expand All @@ -280,7 +296,9 @@ pub fn up(self: *TextInput, mods: main.Window.Key.Modifiers) void {
cursor.* = @min(cursor.*, selectionStart);
self.selectionStart = null;
} else {
self.moveCursorVertically(-1);
if(self.moveCursorVertically(-1) == .same) {
if(self.optional.onUp) |cb| cb.run();
}
}
}
self.ensureCursorVisibility();
Expand Down Expand Up @@ -390,9 +408,18 @@ pub fn inputCharacter(self: *TextInput, character: u21) void {
self.reloadText();
cursor.* += @intCast(utf8.len);
self.ensureCursorVisibility();
if(self.optional.onCharacter) |cb| cb.run();
}
}

pub fn setString(self: *TextInput, utf8EncodedString: []const u8) void {
self.clear();
self.currentString.insertSlice(0, utf8EncodedString);
self.reloadText();
if(self.cursor != null) self.cursor = @intCast(utf8EncodedString.len);
self.ensureCursorVisibility();
}

pub fn selectAll(self: *TextInput, mods: main.Window.Key.Modifiers) void {
if(mods.control) {
self.selectionStart = 0;
Expand Down Expand Up @@ -443,6 +470,14 @@ pub fn newline(self: *TextInput, mods: main.Window.Key.Modifiers) void {
self.ensureCursorVisibility();
}

pub fn tab(self: *TextInput, mods: main.Window.Key.Modifiers) void {
if(!mods.shift and self.optional.onTab != null) {
self.optional.onTab.?.run();
return;
}
self.ensureCursorVisibility();
}

fn ensureCursorVisibility(self: *TextInput) void {
if(self.textSize[1] > self.maxHeight - 2*border) {
var y: f32 = 0;
Expand Down
5 changes: 5 additions & 0 deletions src/gui/gui.zig
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,11 @@ pub const textCallbacks = struct {
current.newline(mods);
}
}
pub fn tab(mods: main.Window.Key.Modifiers) void {
if(selectedTextInput) |current| {
current.tab(mods);
}
}
};

pub fn mainButtonPressed() void {
Expand Down
2 changes: 1 addition & 1 deletion src/gui/windows/change_name.zig
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ pub fn onOpen() void {
list.add(Label.init(.{0, 0}, width, "\\**italic*\\* \\*\\***bold**\\*\\* \\_\\___underlined__\\_\\_ \\~\\~~~strike-through~~\\~\\~", .center));
list.add(Label.init(.{0, 0}, width, "Even colors are possible, using the hexadecimal color code:", .center));
list.add(Label.init(.{0, 0}, width, "\\##ff0000ff#ffffff00#ffffff00#ff0000red#ffffff \\##ff0000ff#00770077#ffffff00#ff7700orange#ffffff \\##ffffff00#00ff00ff#ffffff00#00ff00green#ffffff \\##ffffff00#ffffff00#0000ffff#0000ffblue", .center));
textComponent = TextInput.init(.{0, 0}, width, 32, if(settings.playerName.len == 0) "quanturmdoelvloper" else settings.playerName, .{.callback = &apply});
textComponent = TextInput.init(.{0, 0}, width, 32, if(settings.playerName.len == 0) "quanturmdoelvloper" else settings.playerName, .{.callback = &apply}, .{});
list.add(textComponent);
list.add(Button.initText(.{0, 0}, 100, "Apply", .{.callback = &apply}));
list.finish(.center);
Expand Down
139 changes: 138 additions & 1 deletion src/gui/windows/chat.zig
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const Label = GuiComponent.Label;
const MutexComponent = GuiComponent.MutexComponent;
const TextInput = GuiComponent.TextInput;
const VerticalList = @import("../components/VerticalList.zig");
const FixedSizeCircularBuffer = main.utils.FixedSizeCircularBuffer;
const server_commands = main.server_commands;

pub var window: GuiWindow = GuiWindow{
.relativePosition = .{
Expand All @@ -29,6 +31,7 @@ pub var window: GuiWindow = GuiWindow{
const padding: f32 = 8;
const messageTimeout: i32 = 10000;
const messageFade = 1000;
const reusableHistoryMaxSize = 8192;

var history: main.List(*Label) = undefined;
var messageQueue: main.utils.ConcurrentQueue([]const u8) = undefined;
Expand All @@ -37,9 +40,81 @@ var historyStart: u32 = 0;
var fadeOutEnd: u32 = 0;
pub var input: *TextInput = undefined;
var hideInput: bool = true;
var messageHistory: History = undefined;
var autocompleteList: main.ListUnmanaged([]const u8) = .{};
var autocompleteListPointer: usize = 0;

pub const History = struct {
up: FixedSizeCircularBuffer([]const u8, reusableHistoryMaxSize),
down: FixedSizeCircularBuffer([]const u8, reusableHistoryMaxSize),

fn init() History {
return .{
.up = .init(main.globalAllocator),
.down = .init(main.globalAllocator),
};
}
fn deinit(self: *History) void {
self.clear();
self.up.deinit(main.globalAllocator);
self.down.deinit(main.globalAllocator);
}
fn clear(self: *History) void {
while(self.up.dequeue()) |msg| {
main.globalAllocator.free(msg);
}
while(self.down.dequeue()) |msg| {
main.globalAllocator.free(msg);
}
}
fn flushUp(self: *History) void {
while(self.down.dequeueFront()) |msg| {
if(msg.len == 0) {
continue;
}

if(self.up.forceEnqueueFront(msg)) |old| {
main.globalAllocator.free(old);
}
}
}
pub fn isDuplicate(self: *History, new: []const u8) bool {
if(new.len == 0) return true;
if(self.down.peekFront()) |msg| {
if(std.mem.eql(u8, msg, new)) return true;
}
if(self.up.peekFront()) |msg| {
if(std.mem.eql(u8, msg, new)) return true;
}
return false;
}
pub fn pushDown(self: *History, new: []const u8) void {
if(self.down.forceEnqueueFront(new)) |old| {
main.globalAllocator.free(old);
}
}
pub fn pushUp(self: *History, new: []const u8) void {
if(self.up.forceEnqueueFront(new)) |old| {
main.globalAllocator.free(old);
}
}
pub fn cycleUp(self: *History) bool {
if(self.down.dequeueFront()) |msg| {
self.pushUp(msg);
return true;
}
return false;
}
pub fn cycleDown(self: *History) void {
if(self.up.dequeueFront()) |msg| {
self.pushDown(msg);
}
}
};

pub fn init() void {
history = .init(main.globalAllocator);
messageHistory = .init();
expirationTime = .init(main.globalAllocator);
messageQueue = .init(main.globalAllocator, 16);
}
Expand All @@ -52,8 +127,19 @@ pub fn deinit() void {
while(messageQueue.dequeue()) |msg| {
main.globalAllocator.free(msg);
}
messageHistory.deinit();
messageQueue.deinit();
expirationTime.deinit();

discardAutocompleteList();
}

fn discardAutocompleteList() void {
for(autocompleteList.items) |match| {
main.globalAllocator.free(match);
}
autocompleteList.clearAndFree(main.globalAllocator);
autocompleteListPointer = 0;
}

fn refresh() void {
Expand Down Expand Up @@ -87,17 +173,45 @@ fn refresh() void {
}

pub fn onOpen() void {
input = TextInput.init(.{0, 0}, 256, 32, "", .{.callback = &sendMessage});
input = TextInput.init(.{0, 0}, 256, 32, "", .{.callback = &sendMessage}, .{
.onUp = .{.callback = loadNextHistoryEntry},
.onDown = .{.callback = loadPreviousHistoryEntry},
.onTab = .{.callback = autocomplete},
.onCharacter = .{.callback = onCharacter},
});
refresh();
}

pub fn loadNextHistoryEntry(_: usize) void {
const isSuccess = messageHistory.cycleUp();
if(messageHistory.isDuplicate(input.currentString.items)) {
if(isSuccess) messageHistory.cycleDown();
messageHistory.cycleDown();
} else {
messageHistory.pushDown(main.globalAllocator.dupe(u8, input.currentString.items));
messageHistory.cycleDown();
}
const msg = messageHistory.down.peekFront() orelse "";
input.setString(msg);
}

pub fn loadPreviousHistoryEntry(_: usize) void {
_ = messageHistory.cycleUp();
if(messageHistory.isDuplicate(input.currentString.items)) {} else {
messageHistory.pushUp(main.globalAllocator.dupe(u8, input.currentString.items));
}
const msg = messageHistory.down.peekFront() orelse "";
input.setString(msg);
}

pub fn onClose() void {
while(history.popOrNull()) |label| {
label.deinit();
}
while(messageQueue.dequeue()) |msg| {
main.globalAllocator.free(msg);
}
messageHistory.clear();
expirationTime.clearRetainingCapacity();
historyStart = 0;
fadeOutEnd = 0;
Expand Down Expand Up @@ -156,8 +270,31 @@ pub fn sendMessage(_: usize) void {
if(data.len > 10000 or main.graphics.TextBuffer.Parser.countVisibleCharacters(data) > 1000) {
std.log.err("Chat message is too long with {}/{} characters. Limits are 1000/10000", .{main.graphics.TextBuffer.Parser.countVisibleCharacters(data), data.len});
} else {
messageHistory.flushUp();
if(!messageHistory.isDuplicate(data)) {
messageHistory.pushUp(main.globalAllocator.dupe(u8, data));
}

main.network.Protocols.chat.send(main.game.world.?.conn, data);
input.clear();
}
}
}

pub fn autocomplete(_: usize) void {
const msg = input.currentString.items;
if(msg.len <= 1) return;
if(msg[0] != '/') return;

if(autocompleteList.items.len == 0) {
autocompleteList = server_commands.autocomplete(msg[1..msg.len], main.globalAllocator);
autocompleteListPointer = 0;
}
if(autocompleteList.items.len == 0) return;
input.setString(autocompleteList.items[autocompleteListPointer%autocompleteList.items.len]);
autocompleteListPointer += 1;
}

pub fn onCharacter(_: usize) void {
discardAutocompleteList();
}
2 changes: 1 addition & 1 deletion src/gui/windows/invite.zig
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ pub fn onOpen() void {
ipAddressLabel = Label.init(.{0, 0}, width, " ", .center);
list.add(ipAddressLabel);
list.add(Button.initText(.{0, 0}, 100, "Copy IP", .{.callback = &copyIp}));
ipAddressEntry = TextInput.init(.{0, 0}, width, 32, settings.lastUsedIPAddress, .{.callback = &invite});
ipAddressEntry = TextInput.init(.{0, 0}, width, 32, settings.lastUsedIPAddress, .{.callback = &invite}, .{});
list.add(ipAddressEntry);
list.add(Button.initText(.{0, 0}, 100, "Invite", .{.callback = &invite}));
list.add(Button.initText(.{0, 0}, 100, "Manage Players", gui.openWindowCallback("manage_players")));
Expand Down
2 changes: 1 addition & 1 deletion src/gui/windows/multiplayer.zig
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ pub fn onOpen() void {
ipAddressLabel = Label.init(.{0, 0}, width, " ", .center);
list.add(ipAddressLabel);
list.add(Button.initText(.{0, 0}, 100, "Copy IP", .{.callback = &copyIp}));
ipAddressEntry = TextInput.init(.{0, 0}, width, 32, settings.lastUsedIPAddress, .{.callback = &join});
ipAddressEntry = TextInput.init(.{0, 0}, width, 32, settings.lastUsedIPAddress, .{.callback = &join}, .{});
list.add(ipAddressEntry);
list.add(Button.initText(.{0, 0}, 100, "Join", .{.callback = &join}));
list.finish(.center);
Expand Down
2 changes: 1 addition & 1 deletion src/gui/windows/save_creation.zig
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ pub fn onOpen() void {
}
const name = std.fmt.allocPrint(main.stackAllocator.allocator, "Save{}", .{num}) catch unreachable;
defer main.stackAllocator.free(name);
textInput = TextInput.init(.{0, 0}, 128, 22, name, .{.callback = &createWorld});
textInput = TextInput.init(.{0, 0}, 128, 22, name, .{.callback = &createWorld}, .{});
list.add(textInput);

gamemodeInput = Button.initText(.{0, 0}, 128, @tagName(gamemode), .{.callback = &gamemodeCallback});
Expand Down
5 changes: 5 additions & 0 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub const Tag = tag.Tag;
pub const utils = @import("utils.zig");
pub const vec = @import("vec.zig");
pub const ZonElement = @import("zon.zig").ZonElement;
pub const server_commands = @import("server/command/_command.zig");

pub const Window = @import("graphics/Window.zig");

Expand Down Expand Up @@ -403,6 +404,7 @@ pub const KeyBoard = struct { // MARK: KeyBoard
.{.name = "textPaste", .key = c.GLFW_KEY_V, .repeatAction = &gui.textCallbacks.paste},
.{.name = "textCut", .key = c.GLFW_KEY_X, .repeatAction = &gui.textCallbacks.cut},
.{.name = "textNewline", .key = c.GLFW_KEY_ENTER, .repeatAction = &gui.textCallbacks.newline},
.{.name = "textTab", .key = c.GLFW_KEY_TAB, .repeatAction = &gui.textCallbacks.tab},

// Hotbar shortcuts:
.{.name = "Hotbar 1", .key = c.GLFW_KEY_1, .pressAction = setHotbarSlot(1)},
Expand Down Expand Up @@ -573,6 +575,9 @@ pub fn main() void { // MARK: main()
}
} else |_| {}

server_commands.init();
defer server_commands.deinit();

gui.initWindowList();
defer gui.deinitWindowList();

Expand Down
Loading
Loading