mirror of
https://github.com/PixelGuys/Cubyz.git
synced 2025-08-03 11:17:05 -04:00
Add chat history accessible with up/down arrows (#1244)
* Add up down message history * Deduplicate messages when inserting into history * Add dedicated function for inserting strings into TextInput * Fix formatting issues * Change history behavior * Rename inputString to setString * Move clearing to setString * Apply suggestions from code review Co-authored-by: IntegratedQuantum <43880493+IntegratedQuantum@users.noreply.github.com> * Remove unused cursor capture * Use FixedSizeCircularBuffer for history * Restore large queue size * Allow navigation to empty entry * self.len must never be bigger than capacity * Move optional callbacks into struct * Use enum for moveCursorVertically return value * WA attempt #1 * Fix edge case from review * Update src/gui/windows/chat.zig Co-authored-by: IntegratedQuantum <43880493+IntegratedQuantum@users.noreply.github.com> * Remove isEmpty and isFull * Allow for empty history entry * Change empty message handling some more * Remove unused methods * Go to hell with all of those edge cases <3 * WA for 4b * Remove CircularBufferQueue methods * Fix queue thing --------- Co-authored-by: IntegratedQuantum <43880493+IntegratedQuantum@users.noreply.github.com>
This commit is contained in:
parent
7fed896a77
commit
67135b433d
@ -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");
|
||||
@ -42,7 +43,12 @@ 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,
|
||||
};
|
||||
|
||||
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{
|
||||
@ -54,6 +60,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);
|
||||
@ -239,8 +246,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 {
|
||||
@ -249,7 +261,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,7 +270,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();
|
||||
@ -271,7 +285,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,7 +294,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();
|
||||
@ -393,6 +409,14 @@ pub fn inputCharacter(self: *TextInput, character: u21) void {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
@ -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);
|
||||
|
@ -11,6 +11,7 @@ const Label = GuiComponent.Label;
|
||||
const MutexComponent = GuiComponent.MutexComponent;
|
||||
const TextInput = GuiComponent.TextInput;
|
||||
const VerticalList = @import("../components/VerticalList.zig");
|
||||
const FixedSizeCircularBuffer = main.utils.FixedSizeCircularBuffer;
|
||||
|
||||
pub var window: GuiWindow = GuiWindow{
|
||||
.relativePosition = .{
|
||||
@ -29,6 +30,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;
|
||||
@ -37,9 +39,79 @@ var historyStart: u32 = 0;
|
||||
var fadeOutEnd: u32 = 0;
|
||||
pub var input: *TextInput = undefined;
|
||||
var hideInput: bool = true;
|
||||
var messageHistory: History = undefined;
|
||||
|
||||
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);
|
||||
}
|
||||
@ -52,6 +124,7 @@ pub fn deinit() void {
|
||||
while(messageQueue.dequeue()) |msg| {
|
||||
main.globalAllocator.free(msg);
|
||||
}
|
||||
messageHistory.deinit();
|
||||
messageQueue.deinit();
|
||||
expirationTime.deinit();
|
||||
}
|
||||
@ -87,10 +160,32 @@ 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}});
|
||||
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();
|
||||
@ -98,6 +193,7 @@ pub fn onClose() void {
|
||||
while(messageQueue.dequeue()) |msg| {
|
||||
main.globalAllocator.free(msg);
|
||||
}
|
||||
messageHistory.clear();
|
||||
expirationTime.clearRetainingCapacity();
|
||||
historyStart = 0;
|
||||
fadeOutEnd = 0;
|
||||
@ -156,6 +252,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 {
|
||||
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();
|
||||
}
|
||||
|
@ -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});
|
||||
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")));
|
||||
|
@ -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});
|
||||
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);
|
||||
|
@ -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});
|
||||
|
@ -338,12 +338,53 @@ pub fn FixedSizeCircularBuffer(T: type, capacity: comptime_int) type { // MARK:
|
||||
allocator.destroy(self.mem);
|
||||
}
|
||||
|
||||
pub fn enqueue(self: *Self, elem: T) !void {
|
||||
pub fn peekFront(self: Self) ?T {
|
||||
if(self.len == 0) return null;
|
||||
return self.mem[self.startIndex + self.len - 1 & mask];
|
||||
}
|
||||
|
||||
pub fn peekBack(self: Self) ?T {
|
||||
if(self.len == 0) return null;
|
||||
return self.mem[self.startIndex];
|
||||
}
|
||||
|
||||
pub fn enqueueFront(self: *Self, elem: T) !void {
|
||||
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;
|
||||
self.enqueueFrontAssumeCapacity(elem);
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn enqueueFrontAssumeCapacity(self: *Self, elem: T) void {
|
||||
self.mem[self.startIndex + self.len & mask] = elem;
|
||||
self.len += 1;
|
||||
}
|
||||
|
||||
pub fn enqueue(self: *Self, elem: T) !void {
|
||||
return self.enqueueFront(elem);
|
||||
}
|
||||
|
||||
pub fn enqueueBack(self: *Self, elem: T) !void {
|
||||
if(self.len >= capacity) return error.OutOfMemory;
|
||||
self.enqueueBackAssumeCapacity(elem);
|
||||
}
|
||||
|
||||
pub fn enqueueBackAssumeCapacity(self: *Self, elem: T) void {
|
||||
self.startIndex = (self.startIndex -% 1) & mask;
|
||||
self.mem[self.startIndex] = elem;
|
||||
self.len += 1;
|
||||
}
|
||||
|
||||
pub fn forceEnqueueBack(self: *Self, elem: T) ?T {
|
||||
const result = if(self.len >= capacity) 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;
|
||||
@ -377,6 +418,16 @@ pub fn FixedSizeCircularBuffer(T: type, capacity: comptime_int) type { // MARK:
|
||||
}
|
||||
|
||||
pub fn dequeue(self: *Self) ?T {
|
||||
return self.dequeueBack();
|
||||
}
|
||||
|
||||
pub fn dequeueFront(self: *Self) ?T {
|
||||
if(self.len == 0) return null;
|
||||
self.len -= 1;
|
||||
return self.mem[self.startIndex + self.len & mask];
|
||||
}
|
||||
|
||||
pub fn dequeueBack(self: *Self) ?T {
|
||||
if(self.len == 0) return null;
|
||||
const result = self.mem[self.startIndex];
|
||||
self.startIndex = (self.startIndex + 1) & mask;
|
||||
|
Loading…
x
Reference in New Issue
Block a user