From 8608c5277d5d8fb0ba35d00de0915c1856ab271e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Wi=C5=9Bniewski?= Date: Tue, 25 Mar 2025 06:14:07 +0100 Subject: [PATCH 01/26] Add up down message history --- src/gui/components/TextInput.zig | 33 +++++++++++---- src/gui/windows/change_name.zig | 2 +- src/gui/windows/chat.zig | 69 ++++++++++++++++++++++++++++++- src/gui/windows/invite.zig | 2 +- src/gui/windows/multiplayer.zig | 2 +- src/gui/windows/save_creation.zig | 2 +- src/utils.zig | 5 +++ 7 files changed, 103 insertions(+), 12 deletions(-) diff --git a/src/gui/components/TextInput.zig b/src/gui/components/TextInput.zig index 80638baad..ddc00ee33 100644 --- a/src/gui/components/TextInput.zig +++ b/src/gui/components/TextInput.zig @@ -33,6 +33,8 @@ maxHeight: f32, textSize: Vec2f = undefined, scrollBar: *ScrollBar, onNewline: gui.Callback, +onUp: ?gui.Callback, +onDown: ?gui.Callback, pub fn __init() void { texture = Texture.initFromFile("assets/cubyz/ui/text_input.png"); @@ -42,7 +44,7 @@ pub fn __deinit() void { texture.deinit(); } -pub fn init(pos: Vec2f, maxWidth: f32, maxHeight: f32, text: []const u8, onNewline: gui.Callback) *TextInput { +pub fn init(pos: Vec2f, maxWidth: f32, maxHeight: f32, text: []const u8, onNewline: gui.Callback, onUp: ?gui.Callback, onDown: ?gui.Callback) *TextInput { const scrollBar = ScrollBar.init(undefined, scrollBarWidth, maxHeight - 2*border, 0); const self = main.globalAllocator.create(TextInput); self.* = TextInput{ @@ -54,6 +56,8 @@ pub fn init(pos: Vec2f, maxWidth: f32, maxHeight: f32, text: []const u8, onNewli .maxHeight = maxHeight, .scrollBar = scrollBar, .onNewline = onNewline, + .onUp = onUp, + .onDown = onDown, }; self.currentString.appendSlice(text); self.textSize = self.textBuffer.calculateLineBreaks(fontSize, maxWidth - 2*border - scrollBarWidth); @@ -239,8 +243,11 @@ 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) bool { + const newCursor = self.textBuffer.mousePosToIndex(self.textBuffer.indexToCursorPos(self.cursor.?) + Vec2f{0, 16*relativeLines}, self.currentString.items.len); + const changed = self.cursor != newCursor; + self.cursor = newCursor; + return changed; } pub fn down(self: *TextInput, mods: main.Window.Key.Modifiers) void { @@ -249,7 +256,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; } @@ -258,10 +265,16 @@ 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)) { + if(self.onDown) |cb| cb.run(); + } } } self.ensureCursorVisibility(); + } else { + if(!mods.shift) { + if(self.onDown) |cb| cb.run(); + } } } @@ -271,7 +284,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; } @@ -280,10 +293,16 @@ 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)) { + if(self.onUp) |cb| cb.run(); + } } } self.ensureCursorVisibility(); + } else { + if(!mods.shift) { + if(self.onUp) |cb| cb.run(); + } } } diff --git a/src/gui/windows/change_name.zig b/src/gui/windows/change_name.zig index 1b19d1f69..419fafb28 100644 --- a/src/gui/windows/change_name.zig +++ b/src/gui/windows/change_name.zig @@ -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}, null, null); list.add(textComponent); list.add(Button.initText(.{0, 0}, 100, "Apply", .{.callback = &apply})); list.finish(.center); diff --git a/src/gui/windows/chat.zig b/src/gui/windows/chat.zig index 1f916bfde..c140c8783 100644 --- a/src/gui/windows/chat.zig +++ b/src/gui/windows/chat.zig @@ -1,6 +1,7 @@ const std = @import("std"); const main = @import("root"); +// const main = @import("../../main.zig"); const Vec2f = main.vec.Vec2f; const gui = @import("../gui.zig"); @@ -11,6 +12,7 @@ const Label = GuiComponent.Label; const MutexComponent = GuiComponent.MutexComponent; const TextInput = GuiComponent.TextInput; const VerticalList = @import("../components/VerticalList.zig"); +const CircularBufferQueue = main.utils.CircularBufferQueue; pub var window: GuiWindow = GuiWindow{ .relativePosition = .{ @@ -29,6 +31,7 @@ pub var window: GuiWindow = GuiWindow{ const padding: f32 = 8; const messageTimeout: i32 = 10000; const messageFade = 1000; +const reusableHistoryMaxSize = 128; var history: main.List(*Label) = undefined; var messageQueue: main.utils.ConcurrentQueue([]const u8) = undefined; @@ -37,9 +40,13 @@ var historyStart: u32 = 0; var fadeOutEnd: u32 = 0; pub var input: *TextInput = undefined; var hideInput: bool = true; +var upHistory: CircularBufferQueue([]const u8) = undefined; +var downHistory: CircularBufferQueue([]const u8) = undefined; pub fn init() void { history = .init(main.globalAllocator); + upHistory = .init(main.globalAllocator, reusableHistoryMaxSize); + downHistory = .init(main.globalAllocator, reusableHistoryMaxSize); expirationTime = .init(main.globalAllocator); messageQueue = .init(main.globalAllocator, 16); } @@ -52,6 +59,14 @@ pub fn deinit() void { while(messageQueue.dequeue()) |msg| { main.globalAllocator.free(msg); } + while(upHistory.dequeue()) |msg| { + main.globalAllocator.free(msg); + } + upHistory.deinit(); + while(downHistory.dequeue()) |msg| { + main.globalAllocator.free(msg); + } + downHistory.deinit(); messageQueue.deinit(); expirationTime.deinit(); } @@ -87,10 +102,44 @@ 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}, .{.callback = loadNextHistoryEntry}, .{.callback = loadPreviousHistoryEntry}); refresh(); } +pub fn loadNextHistoryEntry(_: usize) void { + transferBetweenQueues(input.currentString.items, &upHistory, &downHistory); +} +fn transferBetweenQueues(current: []const u8, inQueue: *CircularBufferQueue([]const u8), outQueue: *CircularBufferQueue([]const u8)) void { + if(inQueue.empty()) { + return; + } + + if(current.len > 0 and (outQueue.empty() or !std.mem.eql(u8, outQueue.peek_front().?, current))) { + if(outQueue.reachedCapacity()) { + main.globalAllocator.free(outQueue.dequeue().?); + } + outQueue.enqueue(main.globalAllocator.dupe(u8, current)); + } + + var msg = inQueue.dequeue_front().?; + if(std.mem.eql(u8, msg, current)) { + main.globalAllocator.free(msg); + if(inQueue.dequeue_front()) |m| { msg = m; } + else return; + } + + input.clear(); + for(msg) |c| input.inputCharacter(c); + + if(outQueue.reachedCapacity()) { + main.globalAllocator.free(outQueue.dequeue().?); + } + outQueue.enqueue(msg); +} +pub fn loadPreviousHistoryEntry(_: usize) void { + transferBetweenQueues(input.currentString.items, &downHistory, &upHistory); +} + pub fn onClose() void { while(history.popOrNull()) |label| { label.deinit(); @@ -98,6 +147,12 @@ pub fn onClose() void { while(messageQueue.dequeue()) |msg| { main.globalAllocator.free(msg); } + while(upHistory.dequeue()) |msg| { + main.globalAllocator.free(msg); + } + while(downHistory.dequeue()) |msg| { + main.globalAllocator.free(msg); + } expirationTime.clearRetainingCapacity(); historyStart = 0; fadeOutEnd = 0; @@ -156,6 +211,18 @@ 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 { + while(downHistory.dequeue_front()) |msg| { + if(upHistory.reachedCapacity()) { + main.globalAllocator.free(upHistory.dequeue().?); + } + upHistory.enqueue(msg); + } + + if(upHistory.reachedCapacity()) { + main.globalAllocator.free(upHistory.dequeue().?); + } + upHistory.enqueue(main.globalAllocator.dupe(u8, data)); + main.network.Protocols.chat.send(main.game.world.?.conn, data); input.clear(); } diff --git a/src/gui/windows/invite.zig b/src/gui/windows/invite.zig index 47487cfa9..6771680fe 100644 --- a/src/gui/windows/invite.zig +++ b/src/gui/windows/invite.zig @@ -81,7 +81,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 = ©Ip})); - ipAddressEntry = TextInput.init(.{0, 0}, width, 32, settings.lastUsedIPAddress, .{.callback = &invite}); + ipAddressEntry = TextInput.init(.{0, 0}, width, 32, settings.lastUsedIPAddress, .{.callback = &invite}, null, null); 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"))); diff --git a/src/gui/windows/multiplayer.zig b/src/gui/windows/multiplayer.zig index cc0fe2d04..5bce99911 100644 --- a/src/gui/windows/multiplayer.zig +++ b/src/gui/windows/multiplayer.zig @@ -93,7 +93,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 = ©Ip})); - ipAddressEntry = TextInput.init(.{0, 0}, width, 32, settings.lastUsedIPAddress, .{.callback = &join}); + ipAddressEntry = TextInput.init(.{0, 0}, width, 32, settings.lastUsedIPAddress, .{.callback = &join}, null, null); list.add(ipAddressEntry); list.add(Button.initText(.{0, 0}, 100, "Join", .{.callback = &join})); list.finish(.center); diff --git a/src/gui/windows/save_creation.zig b/src/gui/windows/save_creation.zig index fd1a6062a..fde8512e7 100644 --- a/src/gui/windows/save_creation.zig +++ b/src/gui/windows/save_creation.zig @@ -107,7 +107,7 @@ pub fn onOpen() void { num += 1; } const name = std.fmt.bufPrint(&buf, "Save{}", .{num}) catch unreachable; - textInput = TextInput.init(.{0, 0}, 128, 22, name, .{.callback = &createWorld}); + textInput = TextInput.init(.{0, 0}, 128, 22, name, .{.callback = &createWorld}, null, null); list.add(textInput); gamemodeInput = Button.initText(.{0, 0}, 128, @tagName(gamemode), .{.callback = &gamemodeCallback}); diff --git a/src/utils.zig b/src/utils.zig index 933b00e5a..d7f736f58 100644 --- a/src/utils.zig +++ b/src/utils.zig @@ -388,6 +388,11 @@ pub fn CircularBufferQueue(comptime T: type) type { // MARK: CircularBufferQueue return self.mem[self.startIndex]; } + pub fn peek_front(self: *Self) ?T { + if(self.empty()) return null; + return self.mem[(self.endIndex - 1) & self.mask]; + } + pub fn empty(self: *Self) bool { return self.startIndex == self.endIndex; } From 3f6fbde18612076bbda83686ebee0d43ab641f4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Wi=C5=9Bniewski?= Date: Wed, 26 Mar 2025 06:39:25 +0100 Subject: [PATCH 02/26] Deduplicate messages when inserting into history --- src/gui/windows/chat.zig | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/gui/windows/chat.zig b/src/gui/windows/chat.zig index c140c8783..248200ca0 100644 --- a/src/gui/windows/chat.zig +++ b/src/gui/windows/chat.zig @@ -113,13 +113,7 @@ fn transferBetweenQueues(current: []const u8, inQueue: *CircularBufferQueue([]co if(inQueue.empty()) { return; } - - if(current.len > 0 and (outQueue.empty() or !std.mem.eql(u8, outQueue.peek_front().?, current))) { - if(outQueue.reachedCapacity()) { - main.globalAllocator.free(outQueue.dequeue().?); - } - outQueue.enqueue(main.globalAllocator.dupe(u8, current)); - } + addToHistory(current, outQueue); var msg = inQueue.dequeue_front().?; if(std.mem.eql(u8, msg, current)) { @@ -136,6 +130,14 @@ fn transferBetweenQueues(current: []const u8, inQueue: *CircularBufferQueue([]co } outQueue.enqueue(msg); } +fn addToHistory(current: []const u8, queue: *CircularBufferQueue([]const u8)) void { + if(current.len > 0 and (queue.empty() or !std.mem.eql(u8, queue.peek_front().?, current))) { + if(queue.reachedCapacity()) { + main.globalAllocator.free(queue.dequeue().?); + } + queue.enqueue(main.globalAllocator.dupe(u8, current)); + } +} pub fn loadPreviousHistoryEntry(_: usize) void { transferBetweenQueues(input.currentString.items, &downHistory, &upHistory); } @@ -217,11 +219,7 @@ pub fn sendMessage(_: usize) void { } upHistory.enqueue(msg); } - - if(upHistory.reachedCapacity()) { - main.globalAllocator.free(upHistory.dequeue().?); - } - upHistory.enqueue(main.globalAllocator.dupe(u8, data)); + addToHistory(data, &upHistory); main.network.Protocols.chat.send(main.game.world.?.conn, data); input.clear(); From eb064b1e293a5f18c12f521f90226b7926489aae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Wi=C5=9Bniewski?= Date: Wed, 26 Mar 2025 06:52:58 +0100 Subject: [PATCH 03/26] Add dedicated function for inserting strings into TextInput --- src/gui/components/TextInput.zig | 10 ++++++++++ src/gui/windows/chat.zig | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/gui/components/TextInput.zig b/src/gui/components/TextInput.zig index ddc00ee33..c9655521b 100644 --- a/src/gui/components/TextInput.zig +++ b/src/gui/components/TextInput.zig @@ -412,6 +412,16 @@ pub fn inputCharacter(self: *TextInput, character: u21) void { } } +pub fn inputString(self: *TextInput, utf8EncodedString: []const u8) void { + if(self.cursor) |*cursor| { + self.deleteSelection(); + self.currentString.insertSlice(cursor.*, utf8EncodedString); + self.reloadText(); + cursor.* += @intCast(utf8EncodedString.len); + self.ensureCursorVisibility(); + } +} + pub fn selectAll(self: *TextInput, mods: main.Window.Key.Modifiers) void { if(mods.control) { self.selectionStart = 0; diff --git a/src/gui/windows/chat.zig b/src/gui/windows/chat.zig index 248200ca0..749e7ce05 100644 --- a/src/gui/windows/chat.zig +++ b/src/gui/windows/chat.zig @@ -123,7 +123,7 @@ fn transferBetweenQueues(current: []const u8, inQueue: *CircularBufferQueue([]co } input.clear(); - for(msg) |c| input.inputCharacter(c); + input.inputString(msg); if(outQueue.reachedCapacity()) { main.globalAllocator.free(outQueue.dequeue().?); From 5c0c4d277946ade731c132ad6f088c09449987b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Wi=C5=9Bniewski?= Date: Wed, 26 Mar 2025 06:54:27 +0100 Subject: [PATCH 04/26] Fix formatting issues --- src/gui/windows/chat.zig | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/gui/windows/chat.zig b/src/gui/windows/chat.zig index 749e7ce05..9ca67a65f 100644 --- a/src/gui/windows/chat.zig +++ b/src/gui/windows/chat.zig @@ -118,8 +118,9 @@ fn transferBetweenQueues(current: []const u8, inQueue: *CircularBufferQueue([]co var msg = inQueue.dequeue_front().?; if(std.mem.eql(u8, msg, current)) { main.globalAllocator.free(msg); - if(inQueue.dequeue_front()) |m| { msg = m; } - else return; + if(inQueue.dequeue_front()) |m| { + msg = m; + } else return; } input.clear(); From 14f7b81be41bf229a7795070f5182772d3c4568b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Wi=C5=9Bniewski?= Date: Fri, 28 Mar 2025 02:43:27 +0100 Subject: [PATCH 05/26] Change history behavior --- src/gui/windows/chat.zig | 175 +++++++++++++++++++++++++++------------ src/utils.zig | 8 ++ 2 files changed, 129 insertions(+), 54 deletions(-) diff --git a/src/gui/windows/chat.zig b/src/gui/windows/chat.zig index 5b247d831..2563b8bd3 100644 --- a/src/gui/windows/chat.zig +++ b/src/gui/windows/chat.zig @@ -30,7 +30,7 @@ pub var window: GuiWindow = GuiWindow{ const padding: f32 = 8; const messageTimeout: i32 = 10000; const messageFade = 1000; -const reusableHistoryMaxSize = 128; +const reusableHistoryMaxSize = 8192; var history: main.List(*Label) = undefined; var messageQueue: main.utils.ConcurrentQueue([]const u8) = undefined; @@ -39,13 +39,108 @@ var historyStart: u32 = 0; var fadeOutEnd: u32 = 0; pub var input: *TextInput = undefined; var hideInput: bool = true; -var upHistory: CircularBufferQueue([]const u8) = undefined; -var downHistory: CircularBufferQueue([]const u8) = undefined; +var messageHistory: History = undefined; + +pub const History = struct { + up: CircularBufferQueue([]const u8), + current: ?[]const u8, + down: CircularBufferQueue([]const u8), + + fn init(maxSize: u32) History { + return .{ + .up = .init(main.globalAllocator, maxSize), + .current = null, + .down = .init(main.globalAllocator, maxSize), + }; + } + fn deinit(self: *History) void { + self.clear(); + self.up.deinit(); + self.down.deinit(); + } + fn clear(self: *History) void { + while(self.up.dequeue()) |msg| { + main.globalAllocator.free(msg); + } + if(self.current) |msg| { + main.globalAllocator.free(msg); + self.current = null; + } + while(self.down.dequeue()) |msg| { + main.globalAllocator.free(msg); + } + } + fn canMoveUp(self: *History) bool { + return self.up.empty() == false; + } + fn moveUp(self: *History) void { + std.debug.assert(self.canMoveUp()); + // This can not overflow because we are moving items between two queues of same size. + if(self.current) |current| { + self.down.enqueue_front(current); + } + self.current = self.up.dequeue_front(); + std.debug.assert(self.current != null); + } + fn canMoveDown(self: *History) bool { + return self.down.empty() == false; + } + fn moveDown(self: *History) void { + std.debug.assert(self.canMoveDown()); + // This can not overflow because we are moving items between two queues of same size. + if(self.current) |current| { + self.up.enqueue_front(current); + } + self.current = self.down.dequeue_front(); + std.debug.assert(self.current != null); + } + fn flush(self: *History) void { + if(self.current) |msg| { + self.up.enqueue_front(msg); + self.current = null; + } + while(self.down.dequeue_front()) |msg| { + self.up.enqueue_front(msg); + } + } + fn insertIfUnique(self: *History, new: []const u8) bool { + if(new.len == 0) return false; + if(self.current) |current| { + if(std.mem.eql(u8, current, new)) return false; + } + if(self.down.peek_front()) |msg| { + if(std.mem.eql(u8, msg, new)) return false; + } + if(self.up.peek_front()) |msg| { + if(std.mem.eql(u8, msg, new)) return false; + } + if(self.down.reachedCapacity()) { + main.globalAllocator.free(self.down.dequeue_back().?); + } + self.down.enqueue_front(main.globalAllocator.dupe(u8, new)); + return true; + } + fn pushIfUnique(self: *History, new: []const u8) void { + if(new.len == 0) return; + if(self.current) |current| { + if(std.mem.eql(u8, current, new)) return; + } + if(self.down.peek_front()) |msg| { + if(std.mem.eql(u8, msg, new)) return; + } + if(self.up.peek_front()) |msg| { + if(std.mem.eql(u8, msg, new)) return; + } + if(self.up.reachedCapacity()) { + main.globalAllocator.free(self.up.dequeue_back().?); + } + self.up.enqueue_front(main.globalAllocator.dupe(u8, new)); + } +}; pub fn init() void { history = .init(main.globalAllocator); - upHistory = .init(main.globalAllocator, reusableHistoryMaxSize); - downHistory = .init(main.globalAllocator, reusableHistoryMaxSize); + messageHistory = .init(reusableHistoryMaxSize); expirationTime = .init(main.globalAllocator); messageQueue = .init(main.globalAllocator, 16); } @@ -58,14 +153,7 @@ pub fn deinit() void { while(messageQueue.dequeue()) |msg| { main.globalAllocator.free(msg); } - while(upHistory.dequeue()) |msg| { - main.globalAllocator.free(msg); - } - upHistory.deinit(); - while(downHistory.dequeue()) |msg| { - main.globalAllocator.free(msg); - } - downHistory.deinit(); + messageHistory.deinit(); messageQueue.deinit(); expirationTime.deinit(); } @@ -106,41 +194,30 @@ pub fn onOpen() void { } pub fn loadNextHistoryEntry(_: usize) void { - transferBetweenQueues(input.currentString.items, &upHistory, &downHistory); -} -fn transferBetweenQueues(current: []const u8, inQueue: *CircularBufferQueue([]const u8), outQueue: *CircularBufferQueue([]const u8)) void { - if(inQueue.empty()) { - return; - } - addToHistory(current, outQueue); + if(!messageHistory.canMoveUp()) return; - var msg = inQueue.dequeue_front().?; - if(std.mem.eql(u8, msg, current)) { - main.globalAllocator.free(msg); - if(inQueue.dequeue_front()) |m| { - msg = m; - } else return; + _ = messageHistory.insertIfUnique(input.currentString.items); + messageHistory.moveUp(); + + if(messageHistory.current) |msg| { + input.clear(); + input.inputString(msg); } +} - input.clear(); - input.inputString(msg); +pub fn loadPreviousHistoryEntry(_: usize) void { + if(!messageHistory.canMoveDown()) return; - if(outQueue.reachedCapacity()) { - main.globalAllocator.free(outQueue.dequeue().?); + if(messageHistory.insertIfUnique(input.currentString.items)) { + messageHistory.moveDown(); } - outQueue.enqueue(msg); -} -fn addToHistory(current: []const u8, queue: *CircularBufferQueue([]const u8)) void { - if(current.len > 0 and (queue.empty() or !std.mem.eql(u8, queue.peek_front().?, current))) { - if(queue.reachedCapacity()) { - main.globalAllocator.free(queue.dequeue().?); - } - queue.enqueue(main.globalAllocator.dupe(u8, current)); + messageHistory.moveDown(); + + if(messageHistory.current) |msg| { + input.clear(); + input.inputString(msg); } } -pub fn loadPreviousHistoryEntry(_: usize) void { - transferBetweenQueues(input.currentString.items, &downHistory, &upHistory); -} pub fn onClose() void { while(history.popOrNull()) |label| { @@ -149,12 +226,7 @@ pub fn onClose() void { while(messageQueue.dequeue()) |msg| { main.globalAllocator.free(msg); } - while(upHistory.dequeue()) |msg| { - main.globalAllocator.free(msg); - } - while(downHistory.dequeue()) |msg| { - main.globalAllocator.free(msg); - } + messageHistory.clear(); expirationTime.clearRetainingCapacity(); historyStart = 0; fadeOutEnd = 0; @@ -213,13 +285,8 @@ 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 { - while(downHistory.dequeue_front()) |msg| { - if(upHistory.reachedCapacity()) { - main.globalAllocator.free(upHistory.dequeue().?); - } - upHistory.enqueue(msg); - } - addToHistory(data, &upHistory); + messageHistory.flush(); + messageHistory.pushIfUnique(data); main.network.Protocols.chat.send(main.game.world.?.conn, data); input.clear(); diff --git a/src/utils.zig b/src/utils.zig index e829d319e..d97b21db4 100644 --- a/src/utils.zig +++ b/src/utils.zig @@ -362,6 +362,10 @@ pub fn CircularBufferQueue(comptime T: type) type { // MARK: CircularBufferQueue } } + pub inline fn enqueue_front(self: *Self, elem: T) void { + self.enqueue(elem); + } + pub fn enqueue_back(self: *Self, elem: T) void { self.startIndex = (self.startIndex -% 1) & self.mask; self.mem[self.startIndex] = elem; @@ -377,6 +381,10 @@ pub fn CircularBufferQueue(comptime T: type) type { // MARK: CircularBufferQueue return result; } + pub inline fn dequeue_back(self: *Self) ?T { + return self.dequeue(); + } + pub fn dequeue_front(self: *Self) ?T { if(self.empty()) return null; self.endIndex = (self.endIndex -% 1) & self.mask; From ad154fc55df2dc7033bcfcb80413f774cabfb2d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Wi=C5=9Bniewski?= Date: Fri, 28 Mar 2025 02:51:47 +0100 Subject: [PATCH 06/26] Rename inputString to setString --- src/gui/components/TextInput.zig | 10 +--------- src/gui/windows/chat.zig | 4 ++-- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/gui/components/TextInput.zig b/src/gui/components/TextInput.zig index daa7012ec..4590922c8 100644 --- a/src/gui/components/TextInput.zig +++ b/src/gui/components/TextInput.zig @@ -271,10 +271,6 @@ pub fn down(self: *TextInput, mods: main.Window.Key.Modifiers) void { } } self.ensureCursorVisibility(); - } else { - if(!mods.shift) { - if(self.onDown) |cb| cb.run(); - } } } @@ -299,10 +295,6 @@ pub fn up(self: *TextInput, mods: main.Window.Key.Modifiers) void { } } self.ensureCursorVisibility(); - } else { - if(!mods.shift) { - if(self.onUp) |cb| cb.run(); - } } } @@ -412,7 +404,7 @@ pub fn inputCharacter(self: *TextInput, character: u21) void { } } -pub fn inputString(self: *TextInput, utf8EncodedString: []const u8) void { +pub fn setString(self: *TextInput, utf8EncodedString: []const u8) void { if(self.cursor) |*cursor| { self.deleteSelection(); self.currentString.insertSlice(cursor.*, utf8EncodedString); diff --git a/src/gui/windows/chat.zig b/src/gui/windows/chat.zig index 2563b8bd3..2dbf05cf1 100644 --- a/src/gui/windows/chat.zig +++ b/src/gui/windows/chat.zig @@ -201,7 +201,7 @@ pub fn loadNextHistoryEntry(_: usize) void { if(messageHistory.current) |msg| { input.clear(); - input.inputString(msg); + input.setString(msg); } } @@ -215,7 +215,7 @@ pub fn loadPreviousHistoryEntry(_: usize) void { if(messageHistory.current) |msg| { input.clear(); - input.inputString(msg); + input.setString(msg); } } From 93c014a5bf6e404a2d946ffa98e9d1f6ee5319f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Wi=C5=9Bniewski?= Date: Fri, 28 Mar 2025 22:49:35 +0100 Subject: [PATCH 07/26] Move clearing to setString --- src/gui/components/TextInput.zig | 2 +- src/gui/windows/chat.zig | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/gui/components/TextInput.zig b/src/gui/components/TextInput.zig index 4590922c8..a351e5df0 100644 --- a/src/gui/components/TextInput.zig +++ b/src/gui/components/TextInput.zig @@ -406,7 +406,7 @@ pub fn inputCharacter(self: *TextInput, character: u21) void { pub fn setString(self: *TextInput, utf8EncodedString: []const u8) void { if(self.cursor) |*cursor| { - self.deleteSelection(); + self.clear(); self.currentString.insertSlice(cursor.*, utf8EncodedString); self.reloadText(); cursor.* += @intCast(utf8EncodedString.len); diff --git a/src/gui/windows/chat.zig b/src/gui/windows/chat.zig index 2dbf05cf1..eaceb0d86 100644 --- a/src/gui/windows/chat.zig +++ b/src/gui/windows/chat.zig @@ -200,7 +200,6 @@ pub fn loadNextHistoryEntry(_: usize) void { messageHistory.moveUp(); if(messageHistory.current) |msg| { - input.clear(); input.setString(msg); } } @@ -214,7 +213,6 @@ pub fn loadPreviousHistoryEntry(_: usize) void { messageHistory.moveDown(); if(messageHistory.current) |msg| { - input.clear(); input.setString(msg); } } From a7325ebb5169ce73979cd1d77f02865a68fd39a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Wi=C5=9Bniewski?= Date: Sat, 19 Apr 2025 18:50:20 +0200 Subject: [PATCH 08/26] Apply suggestions from code review Co-authored-by: IntegratedQuantum <43880493+IntegratedQuantum@users.noreply.github.com> --- src/gui/components/TextInput.zig | 4 ++-- src/gui/windows/chat.zig | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/gui/components/TextInput.zig b/src/gui/components/TextInput.zig index a351e5df0..e2d448ff1 100644 --- a/src/gui/components/TextInput.zig +++ b/src/gui/components/TextInput.zig @@ -407,9 +407,9 @@ pub fn inputCharacter(self: *TextInput, character: u21) void { pub fn setString(self: *TextInput, utf8EncodedString: []const u8) void { if(self.cursor) |*cursor| { self.clear(); - self.currentString.insertSlice(cursor.*, utf8EncodedString); + self.currentString.insertSlice(0, utf8EncodedString); self.reloadText(); - cursor.* += @intCast(utf8EncodedString.len); + if(self.cursor != null) self.cursor = @intCast(utf8EncodedString.len); self.ensureCursorVisibility(); } } diff --git a/src/gui/windows/chat.zig b/src/gui/windows/chat.zig index eaceb0d86..16de256a1 100644 --- a/src/gui/windows/chat.zig +++ b/src/gui/windows/chat.zig @@ -71,7 +71,7 @@ pub const History = struct { } } fn canMoveUp(self: *History) bool { - return self.up.empty() == false; + return !self.up.empty(); } fn moveUp(self: *History) void { std.debug.assert(self.canMoveUp()); @@ -83,7 +83,7 @@ pub const History = struct { std.debug.assert(self.current != null); } fn canMoveDown(self: *History) bool { - return self.down.empty() == false; + return !self.down.empty(); } fn moveDown(self: *History) void { std.debug.assert(self.canMoveDown()); From 9d574a27c307df09aeabafcb77d31659d9dcaf11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Wi=C5=9Bniewski?= Date: Wed, 23 Apr 2025 01:44:10 +0200 Subject: [PATCH 09/26] Remove unused cursor capture --- src/gui/components/TextInput.zig | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/gui/components/TextInput.zig b/src/gui/components/TextInput.zig index e2d448ff1..b1e8b7583 100644 --- a/src/gui/components/TextInput.zig +++ b/src/gui/components/TextInput.zig @@ -405,13 +405,11 @@ pub fn inputCharacter(self: *TextInput, character: u21) void { } pub fn setString(self: *TextInput, utf8EncodedString: []const u8) void { - if(self.cursor) |*cursor| { - self.clear(); - self.currentString.insertSlice(0, utf8EncodedString); - self.reloadText(); - if(self.cursor != null) self.cursor = @intCast(utf8EncodedString.len); - self.ensureCursorVisibility(); - } + 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 { From fe5ce818fd590f2cdffb325b5c68527dd3d7b6f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Wi=C5=9Bniewski?= Date: Fri, 25 Apr 2025 12:17:45 +0200 Subject: [PATCH 10/26] Use FixedSizeCircularBuffer for history --- src/gui/windows/chat.zig | 82 +++++++++++++++++++++------------------- src/utils.zig | 68 +++++++++++++++++++++++++++++---- 2 files changed, 105 insertions(+), 45 deletions(-) diff --git a/src/gui/windows/chat.zig b/src/gui/windows/chat.zig index 16de256a1..40ef4788f 100644 --- a/src/gui/windows/chat.zig +++ b/src/gui/windows/chat.zig @@ -11,7 +11,7 @@ const Label = GuiComponent.Label; const MutexComponent = GuiComponent.MutexComponent; const TextInput = GuiComponent.TextInput; const VerticalList = @import("../components/VerticalList.zig"); -const CircularBufferQueue = main.utils.CircularBufferQueue; +const FixedSizeCircularBuffer = main.utils.FixedSizeCircularBuffer; pub var window: GuiWindow = GuiWindow{ .relativePosition = .{ @@ -30,7 +30,7 @@ pub var window: GuiWindow = GuiWindow{ const padding: f32 = 8; const messageTimeout: i32 = 10000; const messageFade = 1000; -const reusableHistoryMaxSize = 8192; +const reusableHistoryMaxSize = 4; var history: main.List(*Label) = undefined; var messageQueue: main.utils.ConcurrentQueue([]const u8) = undefined; @@ -42,21 +42,21 @@ var hideInput: bool = true; var messageHistory: History = undefined; pub const History = struct { - up: CircularBufferQueue([]const u8), + up: FixedSizeCircularBuffer([]const u8, reusableHistoryMaxSize), current: ?[]const u8, - down: CircularBufferQueue([]const u8), + down: FixedSizeCircularBuffer([]const u8, reusableHistoryMaxSize), - fn init(maxSize: u32) History { + fn init() History { return .{ - .up = .init(main.globalAllocator, maxSize), + .up = .init(main.globalAllocator), .current = null, - .down = .init(main.globalAllocator, maxSize), + .down = .init(main.globalAllocator), }; } fn deinit(self: *History) void { self.clear(); - self.up.deinit(); - self.down.deinit(); + self.up.deinit(main.globalAllocator); + self.down.deinit(main.globalAllocator); } fn clear(self: *History) void { while(self.up.dequeue()) |msg| { @@ -71,76 +71,82 @@ pub const History = struct { } } fn canMoveUp(self: *History) bool { - return !self.up.empty(); + return !self.up.isEmpty(); } fn moveUp(self: *History) void { std.debug.assert(self.canMoveUp()); - // This can not overflow because we are moving items between two queues of same size. - if(self.current) |current| { - self.down.enqueue_front(current); + + if(self.current) |msg| { + if(self.down.forceEnqueueFront(msg)) |old| { + main.globalAllocator.free(old); + } } - self.current = self.up.dequeue_front(); + self.current = self.up.dequeueFront(); std.debug.assert(self.current != null); } fn canMoveDown(self: *History) bool { - return !self.down.empty(); + return !self.down.isEmpty(); } fn moveDown(self: *History) void { std.debug.assert(self.canMoveDown()); - // This can not overflow because we are moving items between two queues of same size. - if(self.current) |current| { - self.up.enqueue_front(current); + + if(self.current) |msg| { + if(self.up.forceEnqueueFront(msg)) |old| { + main.globalAllocator.free(old); + } } - self.current = self.down.dequeue_front(); + self.current = self.down.dequeueFront(); std.debug.assert(self.current != null); } fn flush(self: *History) void { if(self.current) |msg| { - self.up.enqueue_front(msg); + if(self.up.forceEnqueueFront(msg)) |old| { + main.globalAllocator.free(old); + } self.current = null; } - while(self.down.dequeue_front()) |msg| { - self.up.enqueue_front(msg); + while(self.down.dequeueFront()) |msg| { + if(self.up.forceEnqueueFront(msg)) |old| { + main.globalAllocator.free(old); + } } } fn insertIfUnique(self: *History, new: []const u8) bool { - if(new.len == 0) return false; - if(self.current) |current| { - if(std.mem.eql(u8, current, new)) return false; + if(new.len == 0) return self.down.isEmpty(); + if(self.current) |msg| { + if(std.mem.eql(u8, msg, new)) return false; } - if(self.down.peek_front()) |msg| { + if(self.down.peekFront()) |msg| { if(std.mem.eql(u8, msg, new)) return false; } - if(self.up.peek_front()) |msg| { + if(self.up.peekFront()) |msg| { if(std.mem.eql(u8, msg, new)) return false; } - if(self.down.reachedCapacity()) { - main.globalAllocator.free(self.down.dequeue_back().?); + if(self.down.forceEnqueueFront(main.globalAllocator.dupe(u8, new))) |old| { + main.globalAllocator.free(old); } - self.down.enqueue_front(main.globalAllocator.dupe(u8, new)); return true; } fn pushIfUnique(self: *History, new: []const u8) void { if(new.len == 0) return; - if(self.current) |current| { - if(std.mem.eql(u8, current, new)) return; + if(self.current) |msg| { + if(std.mem.eql(u8, msg, new)) return; } - if(self.down.peek_front()) |msg| { + if(self.down.peekFront()) |msg| { if(std.mem.eql(u8, msg, new)) return; } - if(self.up.peek_front()) |msg| { + if(self.up.peekFront()) |msg| { if(std.mem.eql(u8, msg, new)) return; } - if(self.up.reachedCapacity()) { - main.globalAllocator.free(self.up.dequeue_back().?); + if(self.up.forceEnqueueFront(main.globalAllocator.dupe(u8, new))) |old| { + main.globalAllocator.free(old); } - self.up.enqueue_front(main.globalAllocator.dupe(u8, new)); } }; pub fn init() void { history = .init(main.globalAllocator); - messageHistory = .init(reusableHistoryMaxSize); + messageHistory = .init(); expirationTime = .init(main.globalAllocator); messageQueue = .init(main.globalAllocator, 16); } diff --git a/src/utils.zig b/src/utils.zig index 85d5224ae..9bdd742f7 100644 --- a/src/utils.zig +++ b/src/utils.zig @@ -338,12 +338,53 @@ pub fn FixedSizeCircularBuffer(T: type, capacity: comptime_int) type { // MARK: allocator.destroy(self.mem); } + pub fn peekFront(self: Self) ?T { + if(self.isEmpty()) return null; + return self.mem[self.startIndex]; + } + + pub fn peekBack(self: Self) ?T { + if(self.isEmpty()) return null; + return self.mem[self.startIndex + self.len - 1 & mask]; + } + + pub fn enqueueFront(self: *Self, elem: T) !void { + if(self.isFull()) return error.OutOfMemory; + self.enqueueFrontAssumeCapacity(elem); + } + + pub fn forceEnqueueFront(self: *Self, elem: T) ?T { + const result = if(self.isFull()) self.dequeueBack() else null; + self.enqueueFrontAssumeCapacity(elem); + return result; + } + + pub fn enqueueFrontAssumeCapacity(self: *Self, elem: T) void { + self.startIndex = (self.startIndex -% 1) & mask; + self.mem[self.startIndex] = elem; + self.len += 1; + } + pub fn enqueue(self: *Self, elem: T) !void { - if(self.len >= capacity) return error.OutOfMemory; + return self.enqueueBack(elem); + } + + pub fn enqueueBack(self: *Self, elem: T) !void { + if(self.isFull()) return error.OutOfMemory; + self.enqueueBackAssumeCapacity(elem); + } + + pub fn enqueueBackAssumeCapacity(self: *Self, elem: T) void { self.mem[self.startIndex + self.len & mask] = elem; self.len += 1; } + pub fn forceEnqueueBack(self: *Self, elem: T) ?T { + const result = if(self.isFull()) self.dequeueFront() else null; + self.enqueueBackAssumeCapacity(elem); + return result; + } + pub fn enqueueSlice(self: *Self, elems: []const T) !void { if(elems.len + self.len > capacity) { return error.OutOfMemory; @@ -376,14 +417,24 @@ pub fn FixedSizeCircularBuffer(T: type, capacity: comptime_int) type { // MARK: } } + pub fn dequeueFront(self: *Self) ?T { + return self.dequeue(); + } + pub fn dequeue(self: *Self) ?T { - if(self.len == 0) return null; + if(self.isEmpty()) return null; const result = self.mem[self.startIndex]; self.startIndex = (self.startIndex + 1) & mask; self.len -= 1; return result; } + pub fn dequeueBack(self: *Self) ?T { + if(self.isEmpty()) return null; + self.len -= 1; + return self.mem[self.startIndex + self.len & mask]; + } + pub fn dequeueSlice(self: *Self, out: []T) !void { if(out.len > self.len) return error.OutOfBounds; const start = self.startIndex; @@ -408,6 +459,14 @@ pub fn FixedSizeCircularBuffer(T: type, capacity: comptime_int) type { // MARK: if(i >= self.len) return null; return self.mem[(self.startIndex + i) & mask]; } + + pub fn isEmpty(self: Self) bool { + return self.len == 0; + } + + pub fn isFull(self: Self) bool { + return self.len >= capacity; + } }; } @@ -516,11 +575,6 @@ pub fn CircularBufferQueue(comptime T: type) type { // MARK: CircularBufferQueue return self.mem[self.startIndex]; } - pub fn peek_front(self: *Self) ?T { - if(self.empty()) return null; - return self.mem[(self.endIndex - 1) & self.mask]; - } - pub fn getSliceAtOffset(self: Self, offset: usize, result: []T) !void { if(offset + result.len > self.len) return error.OutOfBounds; const start = self.startIndex + offset & self.mask; From cf1850793ec8dc224627020f418f7d151cf69022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Wi=C5=9Bniewski?= Date: Fri, 25 Apr 2025 12:25:17 +0200 Subject: [PATCH 11/26] Restore large queue size --- src/gui/windows/chat.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/windows/chat.zig b/src/gui/windows/chat.zig index 40ef4788f..803f426cb 100644 --- a/src/gui/windows/chat.zig +++ b/src/gui/windows/chat.zig @@ -30,7 +30,7 @@ pub var window: GuiWindow = GuiWindow{ const padding: f32 = 8; const messageTimeout: i32 = 10000; const messageFade = 1000; -const reusableHistoryMaxSize = 4; +const reusableHistoryMaxSize = 8192; var history: main.List(*Label) = undefined; var messageQueue: main.utils.ConcurrentQueue([]const u8) = undefined; From a69839bb576edb0d93c83941592470fde371d9ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Wi=C5=9Bniewski?= Date: Fri, 25 Apr 2025 12:36:37 +0200 Subject: [PATCH 12/26] Allow navigation to empty entry --- src/gui/windows/chat.zig | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/gui/windows/chat.zig b/src/gui/windows/chat.zig index 803f426cb..248a02a04 100644 --- a/src/gui/windows/chat.zig +++ b/src/gui/windows/chat.zig @@ -100,19 +100,23 @@ pub const History = struct { } fn flush(self: *History) void { if(self.current) |msg| { - if(self.up.forceEnqueueFront(msg)) |old| { - main.globalAllocator.free(old); + if(msg.len != 0) { + if(self.up.forceEnqueueFront(msg)) |old| { + main.globalAllocator.free(old); + } } self.current = null; } while(self.down.dequeueFront()) |msg| { - if(self.up.forceEnqueueFront(msg)) |old| { - main.globalAllocator.free(old); + if(msg.len != 0) { + if(self.up.forceEnqueueFront(msg)) |old| { + main.globalAllocator.free(old); + } } } } fn insertIfUnique(self: *History, new: []const u8) bool { - if(new.len == 0) return self.down.isEmpty(); + if(new.len == 0 and !self.down.isEmpty()) return false; if(self.current) |msg| { if(std.mem.eql(u8, msg, new)) return false; } @@ -128,7 +132,7 @@ pub const History = struct { return true; } fn pushIfUnique(self: *History, new: []const u8) void { - if(new.len == 0) return; + if(new.len == 0 and !self.down.isEmpty()) return; if(self.current) |msg| { if(std.mem.eql(u8, msg, new)) return; } From 85d6549377cf976c766b6f3d5c8442ed5eca1e59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Wi=C5=9Bniewski?= Date: Sun, 27 Apr 2025 17:28:28 +0200 Subject: [PATCH 13/26] self.len must never be bigger than capacity --- src/utils.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils.zig b/src/utils.zig index 9bdd742f7..321fc92c1 100644 --- a/src/utils.zig +++ b/src/utils.zig @@ -465,7 +465,8 @@ pub fn FixedSizeCircularBuffer(T: type, capacity: comptime_int) type { // MARK: } pub fn isFull(self: Self) bool { - return self.len >= capacity; + std.debug.assert(self.len <= capacity); + return self.len == capacity; } }; } From 7edd1e396973cf1346c9fafd823c8a1c27dbeb6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Wi=C5=9Bniewski?= Date: Sun, 27 Apr 2025 17:46:32 +0200 Subject: [PATCH 14/26] Move optional callbacks into struct --- src/gui/components/TextInput.zig | 17 ++++++++++------- src/gui/windows/change_name.zig | 2 +- src/gui/windows/chat.zig | 2 +- src/gui/windows/invite.zig | 2 +- src/gui/windows/multiplayer.zig | 2 +- src/gui/windows/save_creation.zig | 2 +- 6 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/gui/components/TextInput.zig b/src/gui/components/TextInput.zig index b1e8b7583..a9f9c0a78 100644 --- a/src/gui/components/TextInput.zig +++ b/src/gui/components/TextInput.zig @@ -33,8 +33,7 @@ maxHeight: f32, textSize: Vec2f = undefined, scrollBar: *ScrollBar, onNewline: gui.Callback, -onUp: ?gui.Callback, -onDown: ?gui.Callback, +optional: OptionalCallbacks, pub fn __init() void { texture = Texture.initFromFile("assets/cubyz/ui/text_input.png"); @@ -44,7 +43,12 @@ pub fn __deinit() void { texture.deinit(); } -pub fn init(pos: Vec2f, maxWidth: f32, maxHeight: f32, text: []const u8, onNewline: gui.Callback, onUp: ?gui.Callback, onDown: ?gui.Callback) *TextInput { +const OptionalCallbacks = struct { + onUp: ?gui.Callback = null, + onDown: ?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{ @@ -56,8 +60,7 @@ pub fn init(pos: Vec2f, maxWidth: f32, maxHeight: f32, text: []const u8, onNewli .maxHeight = maxHeight, .scrollBar = scrollBar, .onNewline = onNewline, - .onUp = onUp, - .onDown = onDown, + .optional = optional, }; self.currentString.appendSlice(text); self.textSize = self.textBuffer.calculateLineBreaks(fontSize, maxWidth - 2*border - scrollBarWidth); @@ -266,7 +269,7 @@ pub fn down(self: *TextInput, mods: main.Window.Key.Modifiers) void { self.selectionStart = null; } else { if(!self.moveCursorVertically(1)) { - if(self.onDown) |cb| cb.run(); + if(self.optional.onDown) |cb| cb.run(); } } } @@ -290,7 +293,7 @@ pub fn up(self: *TextInput, mods: main.Window.Key.Modifiers) void { self.selectionStart = null; } else { if(!self.moveCursorVertically(-1)) { - if(self.onUp) |cb| cb.run(); + if(self.optional.onUp) |cb| cb.run(); } } } diff --git a/src/gui/windows/change_name.zig b/src/gui/windows/change_name.zig index b65a88d5e..127b866e8 100644 --- a/src/gui/windows/change_name.zig +++ b/src/gui/windows/change_name.zig @@ -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}, null, null); + 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); diff --git a/src/gui/windows/chat.zig b/src/gui/windows/chat.zig index 248a02a04..96e3c964b 100644 --- a/src/gui/windows/chat.zig +++ b/src/gui/windows/chat.zig @@ -199,7 +199,7 @@ fn refresh() void { } pub fn onOpen() void { - input = TextInput.init(.{0, 0}, 256, 32, "", .{.callback = &sendMessage}, .{.callback = loadNextHistoryEntry}, .{.callback = loadPreviousHistoryEntry}); + input = TextInput.init(.{0, 0}, 256, 32, "", .{.callback = &sendMessage}, .{.onUp = .{.callback = loadNextHistoryEntry}, .onDown = .{.callback = loadPreviousHistoryEntry}}); refresh(); } diff --git a/src/gui/windows/invite.zig b/src/gui/windows/invite.zig index 742dfb1e2..4bb5257d1 100644 --- a/src/gui/windows/invite.zig +++ b/src/gui/windows/invite.zig @@ -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 = ©Ip})); - ipAddressEntry = TextInput.init(.{0, 0}, width, 32, settings.lastUsedIPAddress, .{.callback = &invite}, null, null); + 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"))); diff --git a/src/gui/windows/multiplayer.zig b/src/gui/windows/multiplayer.zig index 117bb912a..c2fa59153 100644 --- a/src/gui/windows/multiplayer.zig +++ b/src/gui/windows/multiplayer.zig @@ -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 = ©Ip})); - ipAddressEntry = TextInput.init(.{0, 0}, width, 32, settings.lastUsedIPAddress, .{.callback = &join}, null, null); + 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); diff --git a/src/gui/windows/save_creation.zig b/src/gui/windows/save_creation.zig index 60c8e0871..7076e2d2c 100644 --- a/src/gui/windows/save_creation.zig +++ b/src/gui/windows/save_creation.zig @@ -107,7 +107,7 @@ pub fn onOpen() void { num += 1; } const name = std.fmt.bufPrint(&buf, "Save{}", .{num}) catch unreachable; - textInput = TextInput.init(.{0, 0}, 128, 22, name, .{.callback = &createWorld}, null, null); + textInput = TextInput.init(.{0, 0}, 128, 22, name, .{.callback = &createWorld}, .{}); list.add(textInput); gamemodeInput = Button.initText(.{0, 0}, 128, @tagName(gamemode), .{.callback = &gamemodeCallback}); From cfc507949fbbfffd17c8230203177d81116546c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Wi=C5=9Bniewski?= Date: Sun, 27 Apr 2025 17:59:11 +0200 Subject: [PATCH 15/26] Use enum for moveCursorVertically return value --- src/gui/components/TextInput.zig | 12 +++++++----- src/gui/windows/chat.zig | 25 ++++++++++--------------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/gui/components/TextInput.zig b/src/gui/components/TextInput.zig index a9f9c0a78..318dc4cd4 100644 --- a/src/gui/components/TextInput.zig +++ b/src/gui/components/TextInput.zig @@ -246,11 +246,13 @@ pub fn right(self: *TextInput, mods: main.Window.Key.Modifiers) void { } } -fn moveCursorVertically(self: *TextInput, relativeLines: f32) bool { +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); - const changed = self.cursor != newCursor; self.cursor = newCursor; - return changed; + if(self.cursor != newCursor) { + return .changed; + } + return .same; } pub fn down(self: *TextInput, mods: main.Window.Key.Modifiers) void { @@ -268,7 +270,7 @@ pub fn down(self: *TextInput, mods: main.Window.Key.Modifiers) void { cursor.* = @max(cursor.*, selectionStart); self.selectionStart = null; } else { - if(!self.moveCursorVertically(1)) { + if(self.moveCursorVertically(1) == .same) { if(self.optional.onDown) |cb| cb.run(); } } @@ -292,7 +294,7 @@ pub fn up(self: *TextInput, mods: main.Window.Key.Modifiers) void { cursor.* = @min(cursor.*, selectionStart); self.selectionStart = null; } else { - if(!self.moveCursorVertically(-1)) { + if(self.moveCursorVertically(-1) == .same) { if(self.optional.onUp) |cb| cb.run(); } } diff --git a/src/gui/windows/chat.zig b/src/gui/windows/chat.zig index 96e3c964b..f2dbe8ebe 100644 --- a/src/gui/windows/chat.zig +++ b/src/gui/windows/chat.zig @@ -115,33 +115,28 @@ pub const History = struct { } } } - fn insertIfUnique(self: *History, new: []const u8) bool { - if(new.len == 0 and !self.down.isEmpty()) return false; + fn isDuplicate(self: *History, new: []const u8) bool { + if(new.len == 0 and !self.down.isEmpty()) return true; if(self.current) |msg| { - if(std.mem.eql(u8, msg, new)) return false; + if(std.mem.eql(u8, msg, new)) return true; } if(self.down.peekFront()) |msg| { - if(std.mem.eql(u8, msg, new)) return false; + if(std.mem.eql(u8, msg, new)) return true; } if(self.up.peekFront()) |msg| { - if(std.mem.eql(u8, msg, new)) return false; + if(std.mem.eql(u8, msg, new)) return true; } + return false; + } + fn insertIfUnique(self: *History, new: []const u8) bool { + if(isDuplicate(self, new)) return false; if(self.down.forceEnqueueFront(main.globalAllocator.dupe(u8, new))) |old| { main.globalAllocator.free(old); } return true; } fn pushIfUnique(self: *History, new: []const u8) void { - if(new.len == 0 and !self.down.isEmpty()) return; - if(self.current) |msg| { - if(std.mem.eql(u8, msg, new)) return; - } - if(self.down.peekFront()) |msg| { - if(std.mem.eql(u8, msg, new)) return; - } - if(self.up.peekFront()) |msg| { - if(std.mem.eql(u8, msg, new)) return; - } + if(isDuplicate(self, new)) return; if(self.up.forceEnqueueFront(main.globalAllocator.dupe(u8, new))) |old| { main.globalAllocator.free(old); } From 205e2c2be956474da017bb2266e165d2de976d26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Wi=C5=9Bniewski?= Date: Sun, 27 Apr 2025 21:58:26 +0200 Subject: [PATCH 16/26] WA attempt #1 --- src/gui/windows/chat.zig | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/gui/windows/chat.zig b/src/gui/windows/chat.zig index f2dbe8ebe..f9f1e9584 100644 --- a/src/gui/windows/chat.zig +++ b/src/gui/windows/chat.zig @@ -199,7 +199,11 @@ pub fn onOpen() void { } pub fn loadNextHistoryEntry(_: usize) void { - if(!messageHistory.canMoveUp()) return; + if(!messageHistory.canMoveUp()) { + loadPreviousHistoryEntry(0); + if(messageHistory.canMoveUp()) loadNextHistoryEntry(0); + return; + } _ = messageHistory.insertIfUnique(input.currentString.items); messageHistory.moveUp(); From f63e19f2d963764ba95c079c1f3f6125189659e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Wi=C5=9Bniewski?= Date: Mon, 28 Apr 2025 22:05:32 +0200 Subject: [PATCH 17/26] Fix edge case from review --- src/gui/windows/chat.zig | 105 ++++++++++++++++----------------------- 1 file changed, 42 insertions(+), 63 deletions(-) diff --git a/src/gui/windows/chat.zig b/src/gui/windows/chat.zig index f9f1e9584..358b5e9ad 100644 --- a/src/gui/windows/chat.zig +++ b/src/gui/windows/chat.zig @@ -43,13 +43,11 @@ var messageHistory: History = undefined; pub const History = struct { up: FixedSizeCircularBuffer([]const u8, reusableHistoryMaxSize), - current: ?[]const u8, down: FixedSizeCircularBuffer([]const u8, reusableHistoryMaxSize), fn init() History { return .{ .up = .init(main.globalAllocator), - .current = null, .down = .init(main.globalAllocator), }; } @@ -62,64 +60,38 @@ pub const History = struct { while(self.up.dequeue()) |msg| { main.globalAllocator.free(msg); } - if(self.current) |msg| { - main.globalAllocator.free(msg); - self.current = null; - } while(self.down.dequeue()) |msg| { main.globalAllocator.free(msg); } } - fn canMoveUp(self: *History) bool { - return !self.up.isEmpty(); - } fn moveUp(self: *History) void { - std.debug.assert(self.canMoveUp()); - - if(self.current) |msg| { + if(self.up.dequeueFront()) |msg| { if(self.down.forceEnqueueFront(msg)) |old| { main.globalAllocator.free(old); } } - self.current = self.up.dequeueFront(); - std.debug.assert(self.current != null); - } - fn canMoveDown(self: *History) bool { - return !self.down.isEmpty(); } fn moveDown(self: *History) void { - std.debug.assert(self.canMoveDown()); - - if(self.current) |msg| { + if(self.down.dequeueFront()) |msg| { if(self.up.forceEnqueueFront(msg)) |old| { main.globalAllocator.free(old); } } - self.current = self.down.dequeueFront(); - std.debug.assert(self.current != null); } fn flush(self: *History) void { - if(self.current) |msg| { - if(msg.len != 0) { - if(self.up.forceEnqueueFront(msg)) |old| { - main.globalAllocator.free(old); - } - } - self.current = null; - } while(self.down.dequeueFront()) |msg| { - if(msg.len != 0) { - if(self.up.forceEnqueueFront(msg)) |old| { - main.globalAllocator.free(old); - } + if(msg.len == 0) { + main.globalAllocator.free(msg); + continue; + } + + if(self.up.forceEnqueueFront(msg)) |old| { + main.globalAllocator.free(old); } } } - fn isDuplicate(self: *History, new: []const u8) bool { - if(new.len == 0 and !self.down.isEmpty()) return true; - if(self.current) |msg| { - if(std.mem.eql(u8, msg, new)) return true; - } + 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; } @@ -128,16 +100,13 @@ pub const History = struct { } return false; } - fn insertIfUnique(self: *History, new: []const u8) bool { - if(isDuplicate(self, new)) return false; - if(self.down.forceEnqueueFront(main.globalAllocator.dupe(u8, new))) |old| { + pub fn pushDown(self: *History, new: []const u8) void { + if(self.down.forceEnqueueFront(new)) |old| { main.globalAllocator.free(old); } - return true; } - fn pushIfUnique(self: *History, new: []const u8) void { - if(isDuplicate(self, new)) return; - if(self.up.forceEnqueueFront(main.globalAllocator.dupe(u8, new))) |old| { + fn pushUp(self: *History, new: []const u8) void { + if(self.up.forceEnqueueFront(new)) |old| { main.globalAllocator.free(old); } } @@ -199,29 +168,36 @@ pub fn onOpen() void { } pub fn loadNextHistoryEntry(_: usize) void { - if(!messageHistory.canMoveUp()) { - loadPreviousHistoryEntry(0); - if(messageHistory.canMoveUp()) loadNextHistoryEntry(0); - return; + if(messageHistory.isDuplicate(input.currentString.items)) { + if(messageHistory.up.dequeueFront()) |msg| { + messageHistory.pushDown(msg); + } + } else { + if(messageHistory.down.dequeueFront()) |msg| { + messageHistory.pushUp(msg); + } + messageHistory.pushDown(main.globalAllocator.dupe(u8, input.currentString.items)); + if(messageHistory.up.dequeueFront()) |msg| { + messageHistory.pushDown(msg); + } } - - _ = messageHistory.insertIfUnique(input.currentString.items); - messageHistory.moveUp(); - - if(messageHistory.current) |msg| { + if(messageHistory.down.peekFront()) |msg| { input.setString(msg); } } pub fn loadPreviousHistoryEntry(_: usize) void { - if(!messageHistory.canMoveDown()) return; - - if(messageHistory.insertIfUnique(input.currentString.items)) { - messageHistory.moveDown(); + if(messageHistory.isDuplicate(input.currentString.items)) { + if(messageHistory.down.dequeueFront()) |msg| { + messageHistory.pushUp(msg); + } + } else { + if(messageHistory.down.dequeueFront()) |msg| { + messageHistory.pushUp(msg); + } + messageHistory.pushUp(main.globalAllocator.dupe(u8, input.currentString.items)); } - messageHistory.moveDown(); - - if(messageHistory.current) |msg| { + if(messageHistory.down.peekFront()) |msg| { input.setString(msg); } } @@ -292,8 +268,11 @@ 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 { + const isDuplicate = messageHistory.isDuplicate(data); messageHistory.flush(); - messageHistory.pushIfUnique(data); + if(!isDuplicate and !messageHistory.isDuplicate(data)) { + messageHistory.pushUp(main.globalAllocator.dupe(u8, data)); + } main.network.Protocols.chat.send(main.game.world.?.conn, data); input.clear(); From 04ac810d59bb3578f2ead74d76c99abec277c2a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Wi=C5=9Bniewski?= Date: Tue, 29 Apr 2025 18:42:51 +0200 Subject: [PATCH 18/26] Update src/gui/windows/chat.zig Co-authored-by: IntegratedQuantum <43880493+IntegratedQuantum@users.noreply.github.com> --- src/gui/windows/chat.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/src/gui/windows/chat.zig b/src/gui/windows/chat.zig index 358b5e9ad..789f83d13 100644 --- a/src/gui/windows/chat.zig +++ b/src/gui/windows/chat.zig @@ -81,7 +81,6 @@ pub const History = struct { fn flush(self: *History) void { while(self.down.dequeueFront()) |msg| { if(msg.len == 0) { - main.globalAllocator.free(msg); continue; } From affb12305e1a9a3007cc4b72b41356870e972fab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Wi=C5=9Bniewski?= Date: Tue, 29 Apr 2025 19:04:50 +0200 Subject: [PATCH 19/26] Remove isEmpty and isFull --- src/items.zig | 2 +- src/utils.zig | 25 ++++++++----------------- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/src/items.zig b/src/items.zig index 4c159d6af..ccb8dc238 100644 --- a/src/items.zig +++ b/src/items.zig @@ -463,7 +463,7 @@ pub const Tool = struct { // MARK: Tool pub fn deinit(self: *const Tool) void { // TODO: This is leaking textures! //if(self.texture) |texture| { - //texture.deinit(); + //texture.deinit(); //} self.image.deinit(main.globalAllocator); self.tooltip.deinit(); diff --git a/src/utils.zig b/src/utils.zig index 321fc92c1..ec73e1f11 100644 --- a/src/utils.zig +++ b/src/utils.zig @@ -339,22 +339,22 @@ pub fn FixedSizeCircularBuffer(T: type, capacity: comptime_int) type { // MARK: } pub fn peekFront(self: Self) ?T { - if(self.isEmpty()) return null; + if(self.len == 0) return null; return self.mem[self.startIndex]; } pub fn peekBack(self: Self) ?T { - if(self.isEmpty()) return null; + if(self.len == 0) return null; return self.mem[self.startIndex + self.len - 1 & mask]; } pub fn enqueueFront(self: *Self, elem: T) !void { - if(self.isFull()) return error.OutOfMemory; + if(self.len == capacity) return error.OutOfMemory; self.enqueueFrontAssumeCapacity(elem); } pub fn forceEnqueueFront(self: *Self, elem: T) ?T { - const result = if(self.isFull()) self.dequeueBack() else null; + const result = if(self.len == capacity) self.dequeueBack() else null; self.enqueueFrontAssumeCapacity(elem); return result; } @@ -370,7 +370,7 @@ pub fn FixedSizeCircularBuffer(T: type, capacity: comptime_int) type { // MARK: } pub fn enqueueBack(self: *Self, elem: T) !void { - if(self.isFull()) return error.OutOfMemory; + if(self.len == capacity) return error.OutOfMemory; self.enqueueBackAssumeCapacity(elem); } @@ -380,7 +380,7 @@ pub fn FixedSizeCircularBuffer(T: type, capacity: comptime_int) type { // MARK: } pub fn forceEnqueueBack(self: *Self, elem: T) ?T { - const result = if(self.isFull()) self.dequeueFront() else null; + const result = if(self.len == capacity) self.dequeueFront() else null; self.enqueueBackAssumeCapacity(elem); return result; } @@ -422,7 +422,7 @@ pub fn FixedSizeCircularBuffer(T: type, capacity: comptime_int) type { // MARK: } pub fn dequeue(self: *Self) ?T { - if(self.isEmpty()) return null; + if(self.len == 0) return null; const result = self.mem[self.startIndex]; self.startIndex = (self.startIndex + 1) & mask; self.len -= 1; @@ -430,7 +430,7 @@ pub fn FixedSizeCircularBuffer(T: type, capacity: comptime_int) type { // MARK: } pub fn dequeueBack(self: *Self) ?T { - if(self.isEmpty()) return null; + if(self.len == 0) return null; self.len -= 1; return self.mem[self.startIndex + self.len & mask]; } @@ -459,15 +459,6 @@ pub fn FixedSizeCircularBuffer(T: type, capacity: comptime_int) type { // MARK: if(i >= self.len) return null; return self.mem[(self.startIndex + i) & mask]; } - - pub fn isEmpty(self: Self) bool { - return self.len == 0; - } - - pub fn isFull(self: Self) bool { - std.debug.assert(self.len <= capacity); - return self.len == capacity; - } }; } From 7d89f85b6aede9503163bb1df9bfb0aa28d2a0fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Wi=C5=9Bniewski?= Date: Wed, 30 Apr 2025 17:30:27 +0200 Subject: [PATCH 20/26] Allow for empty history entry --- src/gui/windows/chat.zig | 14 ++++++++++---- src/utils.zig | 8 ++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/gui/windows/chat.zig b/src/gui/windows/chat.zig index 789f83d13..6909ee83c 100644 --- a/src/gui/windows/chat.zig +++ b/src/gui/windows/chat.zig @@ -66,6 +66,9 @@ pub const History = struct { } fn moveUp(self: *History) void { if(self.up.dequeueFront()) |msg| { + if(msg.len == 0) { + return self.moveUp(); + } if(self.down.forceEnqueueFront(msg)) |old| { main.globalAllocator.free(old); } @@ -73,12 +76,15 @@ pub const History = struct { } fn moveDown(self: *History) void { if(self.down.dequeueFront()) |msg| { + if(msg.len == 0) { + return self.moveDown(); + } if(self.up.forceEnqueueFront(msg)) |old| { main.globalAllocator.free(old); } } } - fn flush(self: *History) void { + fn flushUp(self: *History) void { while(self.down.dequeueFront()) |msg| { if(msg.len == 0) { continue; @@ -90,7 +96,7 @@ pub const History = struct { } } pub fn isDuplicate(self: *History, new: []const u8) bool { - if(new.len == 0) return true; + if(new.len == 0 and self.down.len != 0) return true; if(self.down.peekFront()) |msg| { if(std.mem.eql(u8, msg, new)) return true; } @@ -104,7 +110,7 @@ pub const History = struct { main.globalAllocator.free(old); } } - fn pushUp(self: *History, new: []const u8) void { + pub fn pushUp(self: *History, new: []const u8) void { if(self.up.forceEnqueueFront(new)) |old| { main.globalAllocator.free(old); } @@ -268,7 +274,7 @@ pub fn sendMessage(_: usize) void { std.log.err("Chat message is too long with {}/{} characters. Limits are 1000/10000", .{main.graphics.TextBuffer.Parser.countVisibleCharacters(data), data.len}); } else { const isDuplicate = messageHistory.isDuplicate(data); - messageHistory.flush(); + messageHistory.flushUp(); if(!isDuplicate and !messageHistory.isDuplicate(data)) { messageHistory.pushUp(main.globalAllocator.dupe(u8, data)); } diff --git a/src/utils.zig b/src/utils.zig index ec73e1f11..484341b56 100644 --- a/src/utils.zig +++ b/src/utils.zig @@ -349,12 +349,12 @@ pub fn FixedSizeCircularBuffer(T: type, capacity: comptime_int) type { // MARK: } pub fn enqueueFront(self: *Self, elem: T) !void { - if(self.len == capacity) return error.OutOfMemory; + if(self.len >= capacity) return error.OutOfMemory; self.enqueueFrontAssumeCapacity(elem); } pub fn forceEnqueueFront(self: *Self, elem: T) ?T { - const result = if(self.len == capacity) self.dequeueBack() else null; + const result = if(self.len >= capacity) self.dequeueBack() else null; self.enqueueFrontAssumeCapacity(elem); return result; } @@ -370,7 +370,7 @@ pub fn FixedSizeCircularBuffer(T: type, capacity: comptime_int) type { // MARK: } pub fn enqueueBack(self: *Self, elem: T) !void { - if(self.len == capacity) return error.OutOfMemory; + if(self.len >= capacity) return error.OutOfMemory; self.enqueueBackAssumeCapacity(elem); } @@ -380,7 +380,7 @@ pub fn FixedSizeCircularBuffer(T: type, capacity: comptime_int) type { // MARK: } pub fn forceEnqueueBack(self: *Self, elem: T) ?T { - const result = if(self.len == capacity) self.dequeueFront() else null; + const result = if(self.len >= capacity) self.dequeueFront() else null; self.enqueueBackAssumeCapacity(elem); return result; } From bcb37435367129c1f3989d56b7357fa880de1598 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Wi=C5=9Bniewski?= Date: Wed, 30 Apr 2025 22:47:18 +0200 Subject: [PATCH 21/26] Change empty message handling some more --- src/gui/windows/chat.zig | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/gui/windows/chat.zig b/src/gui/windows/chat.zig index 6909ee83c..c5fd69cdb 100644 --- a/src/gui/windows/chat.zig +++ b/src/gui/windows/chat.zig @@ -96,7 +96,7 @@ pub const History = struct { } } pub fn isDuplicate(self: *History, new: []const u8) bool { - if(new.len == 0 and self.down.len != 0) return true; + if(new.len == 0) return true; if(self.down.peekFront()) |msg| { if(std.mem.eql(u8, msg, new)) return true; } @@ -186,9 +186,8 @@ pub fn loadNextHistoryEntry(_: usize) void { messageHistory.pushDown(msg); } } - if(messageHistory.down.peekFront()) |msg| { - input.setString(msg); - } + const msg = messageHistory.down.peekFront() orelse ""; + input.setString(msg); } pub fn loadPreviousHistoryEntry(_: usize) void { @@ -202,9 +201,8 @@ pub fn loadPreviousHistoryEntry(_: usize) void { } messageHistory.pushUp(main.globalAllocator.dupe(u8, input.currentString.items)); } - if(messageHistory.down.peekFront()) |msg| { - input.setString(msg); - } + const msg = messageHistory.down.peekFront() orelse ""; + input.setString(msg); } pub fn onClose() void { @@ -273,9 +271,8 @@ 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 { - const isDuplicate = messageHistory.isDuplicate(data); messageHistory.flushUp(); - if(!isDuplicate and !messageHistory.isDuplicate(data)) { + if(!messageHistory.isDuplicate(data)) { messageHistory.pushUp(main.globalAllocator.dupe(u8, data)); } From 2ff79aff4dad2d2f266b6d980514083bef0982e2 Mon Sep 17 00:00:00 2001 From: Krzysztof Wisniewski Date: Thu, 1 May 2025 16:25:30 +0200 Subject: [PATCH 22/26] Remove unused methods --- src/gui/windows/chat.zig | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/gui/windows/chat.zig b/src/gui/windows/chat.zig index c5fd69cdb..6525abda7 100644 --- a/src/gui/windows/chat.zig +++ b/src/gui/windows/chat.zig @@ -64,26 +64,6 @@ pub const History = struct { main.globalAllocator.free(msg); } } - fn moveUp(self: *History) void { - if(self.up.dequeueFront()) |msg| { - if(msg.len == 0) { - return self.moveUp(); - } - if(self.down.forceEnqueueFront(msg)) |old| { - main.globalAllocator.free(old); - } - } - } - fn moveDown(self: *History) void { - if(self.down.dequeueFront()) |msg| { - if(msg.len == 0) { - return self.moveDown(); - } - if(self.up.forceEnqueueFront(msg)) |old| { - main.globalAllocator.free(old); - } - } - } fn flushUp(self: *History) void { while(self.down.dequeueFront()) |msg| { if(msg.len == 0) { From 4d1cabe65c810a44dd886cc19787175238d669d7 Mon Sep 17 00:00:00 2001 From: Krzysztof Wisniewski Date: Fri, 2 May 2025 18:25:34 +0200 Subject: [PATCH 23/26] Go to hell with all of those edge cases <3 --- src/gui/windows/chat.zig | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/gui/windows/chat.zig b/src/gui/windows/chat.zig index 6525abda7..046c46e58 100644 --- a/src/gui/windows/chat.zig +++ b/src/gui/windows/chat.zig @@ -95,6 +95,20 @@ pub const History = struct { 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) bool { + if(self.up.dequeueFront()) |msg| { + self.pushDown(msg); + return true; + } + return false; + } }; pub fn init() void { @@ -153,18 +167,13 @@ pub fn onOpen() void { } pub fn loadNextHistoryEntry(_: usize) void { + const isSuccess = messageHistory.cycleUp(); if(messageHistory.isDuplicate(input.currentString.items)) { - if(messageHistory.up.dequeueFront()) |msg| { - messageHistory.pushDown(msg); - } + if(isSuccess) _ = messageHistory.cycleDown(); + _ = messageHistory.cycleDown(); } else { - if(messageHistory.down.dequeueFront()) |msg| { - messageHistory.pushUp(msg); - } messageHistory.pushDown(main.globalAllocator.dupe(u8, input.currentString.items)); - if(messageHistory.up.dequeueFront()) |msg| { - messageHistory.pushDown(msg); - } + _ = messageHistory.cycleDown(); } const msg = messageHistory.down.peekFront() orelse ""; input.setString(msg); @@ -172,13 +181,9 @@ pub fn loadNextHistoryEntry(_: usize) void { pub fn loadPreviousHistoryEntry(_: usize) void { if(messageHistory.isDuplicate(input.currentString.items)) { - if(messageHistory.down.dequeueFront()) |msg| { - messageHistory.pushUp(msg); - } + _ = messageHistory.cycleUp(); } else { - if(messageHistory.down.dequeueFront()) |msg| { - messageHistory.pushUp(msg); - } + _ = messageHistory.cycleUp(); messageHistory.pushUp(main.globalAllocator.dupe(u8, input.currentString.items)); } const msg = messageHistory.down.peekFront() orelse ""; From b965c0a60b38a53699a73140164276bc092a81f4 Mon Sep 17 00:00:00 2001 From: Krzysztof Wisniewski Date: Fri, 2 May 2025 18:39:07 +0200 Subject: [PATCH 24/26] WA for 4b --- src/gui/windows/chat.zig | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/gui/windows/chat.zig b/src/gui/windows/chat.zig index 046c46e58..04d1399a2 100644 --- a/src/gui/windows/chat.zig +++ b/src/gui/windows/chat.zig @@ -102,12 +102,10 @@ pub const History = struct { } return false; } - pub fn cycleDown(self: *History) bool { + pub fn cycleDown(self: *History) void { if(self.up.dequeueFront()) |msg| { self.pushDown(msg); - return true; } - return false; } }; @@ -169,21 +167,19 @@ pub fn onOpen() void { pub fn loadNextHistoryEntry(_: usize) void { const isSuccess = messageHistory.cycleUp(); if(messageHistory.isDuplicate(input.currentString.items)) { - if(isSuccess) _ = messageHistory.cycleDown(); - _ = messageHistory.cycleDown(); + if(isSuccess) messageHistory.cycleDown(); + messageHistory.cycleDown(); } else { messageHistory.pushDown(main.globalAllocator.dupe(u8, input.currentString.items)); - _ = messageHistory.cycleDown(); + messageHistory.cycleDown(); } const msg = messageHistory.down.peekFront() orelse ""; input.setString(msg); } pub fn loadPreviousHistoryEntry(_: usize) void { - if(messageHistory.isDuplicate(input.currentString.items)) { - _ = messageHistory.cycleUp(); - } else { - _ = messageHistory.cycleUp(); + _ = messageHistory.cycleUp(); + if(messageHistory.isDuplicate(input.currentString.items)) {} else { messageHistory.pushUp(main.globalAllocator.dupe(u8, input.currentString.items)); } const msg = messageHistory.down.peekFront() orelse ""; From ac3b486fac51e58fcb759e9e8dd0eda3ff47033c Mon Sep 17 00:00:00 2001 From: Krzysztof Wisniewski Date: Fri, 2 May 2025 19:58:53 +0200 Subject: [PATCH 25/26] Add tab actions to TextInput --- src/gui/components/TextInput.zig | 9 +++++++++ src/gui/gui.zig | 5 +++++ src/gui/windows/chat.zig | 5 +++++ src/main.zig | 1 + 4 files changed, 20 insertions(+) diff --git a/src/gui/components/TextInput.zig b/src/gui/components/TextInput.zig index 318dc4cd4..054e7a6c6 100644 --- a/src/gui/components/TextInput.zig +++ b/src/gui/components/TextInput.zig @@ -46,6 +46,7 @@ pub fn __deinit() void { const OptionalCallbacks = struct { onUp: ?gui.Callback = null, onDown: ?gui.Callback = null, + onTab: ?gui.Callback = null, }; pub fn init(pos: Vec2f, maxWidth: f32, maxHeight: f32, text: []const u8, onNewline: gui.Callback, optional: OptionalCallbacks) *TextInput { @@ -467,6 +468,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; diff --git a/src/gui/gui.zig b/src/gui/gui.zig index b23cfc650..84fa9d214 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -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 { diff --git a/src/gui/windows/chat.zig b/src/gui/windows/chat.zig index 04d1399a2..887460811 100644 --- a/src/gui/windows/chat.zig +++ b/src/gui/windows/chat.zig @@ -262,3 +262,8 @@ pub fn sendMessage(_: usize) void { } } } + +pub fn autocomplete(_: usize) void { + if(input.currentString.items.len == 0) return; + if(input.currentString.items[0] != '/') return; +} \ No newline at end of file diff --git a/src/main.zig b/src/main.zig index f717545b9..7baf6238d 100644 --- a/src/main.zig +++ b/src/main.zig @@ -403,6 +403,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)}, From 5e1c4c1e9840adfbc0433f4d833a556fa4f31be0 Mon Sep 17 00:00:00 2001 From: Krzysztof Wisniewski Date: Fri, 2 May 2025 21:06:35 +0200 Subject: [PATCH 26/26] Implement autocompletion cycling --- src/gui/components/TextInput.zig | 2 ++ src/gui/windows/chat.zig | 39 ++++++++++++++++++++++++++++---- src/main.zig | 4 ++++ src/server/command/_command.zig | 30 ++++++++++++++++++++++++ src/server/server.zig | 2 -- 5 files changed, 71 insertions(+), 6 deletions(-) diff --git a/src/gui/components/TextInput.zig b/src/gui/components/TextInput.zig index 054e7a6c6..dca6a4d45 100644 --- a/src/gui/components/TextInput.zig +++ b/src/gui/components/TextInput.zig @@ -47,6 +47,7 @@ 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 { @@ -407,6 +408,7 @@ pub fn inputCharacter(self: *TextInput, character: u21) void { self.reloadText(); cursor.* += @intCast(utf8.len); self.ensureCursorVisibility(); + if(self.optional.onCharacter) |cb| cb.run(); } } diff --git a/src/gui/windows/chat.zig b/src/gui/windows/chat.zig index 887460811..46b75c2a7 100644 --- a/src/gui/windows/chat.zig +++ b/src/gui/windows/chat.zig @@ -12,6 +12,7 @@ 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 = .{ @@ -40,6 +41,8 @@ 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), @@ -127,6 +130,16 @@ pub fn deinit() void { 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 { @@ -160,7 +173,12 @@ fn refresh() void { } pub fn onOpen() void { - input = TextInput.init(.{0, 0}, 256, 32, "", .{.callback = &sendMessage}, .{.onUp = .{.callback = loadNextHistoryEntry}, .onDown = .{.callback = loadPreviousHistoryEntry}}); + input = TextInput.init(.{0, 0}, 256, 32, "", .{.callback = &sendMessage}, .{ + .onUp = .{.callback = loadNextHistoryEntry}, + .onDown = .{.callback = loadPreviousHistoryEntry}, + .onTab = .{.callback = autocomplete}, + .onCharacter = .{.callback = onCharacter}, + }); refresh(); } @@ -264,6 +282,19 @@ pub fn sendMessage(_: usize) void { } pub fn autocomplete(_: usize) void { - if(input.currentString.items.len == 0) return; - if(input.currentString.items[0] != '/') return; -} \ No newline at end of file + 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(); +} diff --git a/src/main.zig b/src/main.zig index 7baf6238d..6ccc68ac8 100644 --- a/src/main.zig +++ b/src/main.zig @@ -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"); @@ -574,6 +575,9 @@ pub fn main() void { // MARK: main() } } else |_| {} + server_commands.init(); + defer server_commands.deinit(); + gui.initWindowList(); defer gui.deinitWindowList(); diff --git a/src/server/command/_command.zig b/src/server/command/_command.zig index 7ed24e513..5c5735cb4 100644 --- a/src/server/command/_command.zig +++ b/src/server/command/_command.zig @@ -40,3 +40,33 @@ pub fn execute(msg: []const u8, source: *User) void { source.sendMessage("#ff0000Unrecognized Command \"{s}\"", .{command}); } } + +pub fn autocomplete(msg: []const u8, allocator: main.heap.NeverFailingAllocator) main.ListUnmanaged([]const u8) { + if(std.mem.indexOfScalar(u8, msg, ' ') != null) return .{}; + + var matches: main.ListUnmanaged([]const u8) = .{}; + + if(commands.contains(msg)) { + const newKey = std.fmt.allocPrint(allocator.allocator, "/{s}", .{msg}) catch unreachable; + matches.append(allocator, newKey); + } else { + var maxLen: usize = 0; + + var iterator = commands.keyIterator(); + while(iterator.next()) |key| { + if(std.mem.startsWith(u8, key.*, msg)) { + const newKey = std.fmt.allocPrint(allocator.allocator, "/{s}", .{key.*}) catch unreachable; + matches.append(allocator, newKey); + + if(key.len > maxLen) { + const second = matches.items[0]; + const first = matches.items[matches.items.len - 1]; + matches.items[0] = first; + matches.items[matches.items.len - 1] = second; + maxLen = key.len; + } + } + } + } + return matches; +} diff --git a/src/server/server.zig b/src/server/server.zig index 6252e8459..80aa85835 100644 --- a/src/server/server.zig +++ b/src/server/server.zig @@ -288,7 +288,6 @@ pub var thread: ?std.Thread = null; fn init(name: []const u8, singlePlayerPort: ?u16) void { // MARK: init() std.debug.assert(world == null); // There can only be one world. - command.init(); users = .init(main.globalAllocator); userDeinitList = .init(main.globalAllocator, 16); userConnectList = .init(main.globalAllocator, 16); @@ -339,7 +338,6 @@ fn deinit() void { _world.deinit(); } world = null; - command.deinit(); } pub fn getUserListAndIncreaseRefCount(allocator: main.heap.NeverFailingAllocator) []*User {