From c2e888031f21a9262e9caa5730a791b796bc0e9b Mon Sep 17 00:00:00 2001 From: IntegratedQuantum Date: Tue, 7 Mar 2023 20:53:57 +0100 Subject: [PATCH] Add a TextInput component that supports multiline text editing. --- assets/cubyz/shaders/chunks/chunk_fragment.fs | 2 +- assets/cubyz/shaders/item_drop.fs | 2 +- src/graphics.zig | 129 +++++-- src/gui/GuiComponent.zig | 2 + src/gui/components/TextInput.zig | 362 ++++++++++++++++++ src/gui/gui.zig | 70 ++++ src/gui/windows/change_name.zig | 3 + src/main.zig | 58 ++- 8 files changed, 601 insertions(+), 27 deletions(-) create mode 100644 src/gui/components/TextInput.zig diff --git a/assets/cubyz/shaders/chunks/chunk_fragment.fs b/assets/cubyz/shaders/chunks/chunk_fragment.fs index 1a6dba611..e98a6ffbc 100644 --- a/assets/cubyz/shaders/chunks/chunk_fragment.fs +++ b/assets/cubyz/shaders/chunks/chunk_fragment.fs @@ -97,7 +97,7 @@ RayMarchResult rayMarching(vec3 startPosition, vec3 direction) { // TODO: Mipmap vec3 stepInIndex = step*vec3(1 << 10, 1 << 5, 1); int overflowMask = 1<<14 | 1<<9 | 1<<4; vec3 t1 = (floor(startPosition) - startPosition)/direction; - vec3 tDelta = 1/(direction); + vec3 tDelta = 1/direction; vec3 t2 = t1 + tDelta; tDelta = abs(tDelta); vec3 tMax = max(t1, t2) - tDelta; diff --git a/assets/cubyz/shaders/item_drop.fs b/assets/cubyz/shaders/item_drop.fs index ddb48e876..9f5c38f2e 100644 --- a/assets/cubyz/shaders/item_drop.fs +++ b/assets/cubyz/shaders/item_drop.fs @@ -246,7 +246,7 @@ void mainItemDrop() { // Implementation of "A Fast Voxel Traversal Algorithm for Ray Tracing" http://www.cse.yorku.ca/~amana/research/grid.pdf ivec3 step = ivec3(sign(direction)); vec3 t1 = (floor(startPosition) - startPosition)/direction; - vec3 tDelta = 1/(direction); + vec3 tDelta = 1/direction; vec3 t2 = t1 + tDelta; tDelta = abs(tDelta); vec3 tMax = max(t1, t2); diff --git a/src/graphics.zig b/src/graphics.zig index f117b6521..ab274f448 100644 --- a/src/graphics.zig +++ b/src/graphics.zig @@ -402,6 +402,7 @@ pub const TextBuffer = struct { codepoint: u32, cluster: u32, fontEffect: FontEffect, + characterIndex: u32, }; buffer: harfbuzz.Buffer, @@ -445,63 +446,71 @@ pub const TextBuffer = struct { currentFontEffect: FontEffect, parsedText: std.ArrayList(u32), fontEffects: std.ArrayList(FontEffect), + characterIndex: std.ArrayList(u32), showControlCharacters: bool, + curChar: u21 = undefined, + curIndex: u32 = 0, - fn appendControlGetNext(self: *Parser, char: u32) !?u21 { + fn appendControlGetNext(self: *Parser) !?void { if(self.showControlCharacters) { try self.fontEffects.append(.{.color = 0x808080}); - try self.parsedText.append(char); + try self.parsedText.append(self.curChar); + try self.characterIndex.append(self.curIndex); } - return self.unicodeIterator.nextCodepoint(); + self.curIndex = @intCast(u32, self.unicodeIterator.i); + self.curChar = self.unicodeIterator.nextCodepoint() orelse return null; } - fn appendGetNext(self: *Parser, char: u32) !?u21 { + fn appendGetNext(self: *Parser) !?void { try self.fontEffects.append(self.currentFontEffect); - try self.parsedText.append(char); - return self.unicodeIterator.nextCodepoint(); + try self.parsedText.append(self.curChar); + try self.characterIndex.append(self.curIndex); + self.curIndex = @intCast(u32, self.unicodeIterator.i); + self.curChar = self.unicodeIterator.nextCodepoint() orelse return null; } fn parse(self: *Parser) !void { - var char = self.unicodeIterator.nextCodepoint() orelse return; - while(true) switch(char) { + self.curIndex = @intCast(u32, self.unicodeIterator.i); + self.curChar = self.unicodeIterator.nextCodepoint() orelse return; + while(true) switch(self.curChar) { '*' => { - char = try self.appendControlGetNext(char) orelse return; - if(char == '*') { - char = try self.appendControlGetNext(char) orelse return; + try self.appendControlGetNext() orelse return; + if(self.curChar == '*') { + try self.appendControlGetNext() orelse return; self.currentFontEffect.bold = !self.currentFontEffect.bold; } else { self.currentFontEffect.italic = !self.currentFontEffect.italic; } }, '_' => { - char = try self.appendControlGetNext(char) orelse return; - if(char == '_') { - char = try self.appendControlGetNext(char) orelse return; + try self.appendControlGetNext() orelse return; + if(self.curChar == '_') { + try self.appendControlGetNext() orelse return; self.currentFontEffect.strikethrough = !self.currentFontEffect.strikethrough; } else { self.currentFontEffect.underline = !self.currentFontEffect.underline; } }, '\\' => { - char = try self.appendControlGetNext(char) orelse return; - char = try self.appendGetNext(char) orelse return; + try self.appendControlGetNext() orelse return; + try self.appendGetNext() orelse return; }, '#' => { - char = try self.appendControlGetNext(char) orelse return; + try self.appendControlGetNext() orelse return; var shift: u5 = 20; while(true) : (shift -= 4) { - self.currentFontEffect.color = (self.currentFontEffect.color & ~(@as(u24, 0xf) << shift)) | @as(u24, switch(char) { - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => char - '0', - 'a', 'b', 'c', 'd', 'e', 'f' => char - 'a' + 10, - 'A', 'B', 'C', 'D', 'E', 'F' => char - 'A' + 10, + self.currentFontEffect.color = (self.currentFontEffect.color & ~(@as(u24, 0xf) << shift)) | @as(u24, switch(self.curChar) { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => self.curChar - '0', + 'a', 'b', 'c', 'd', 'e', 'f' => self.curChar - 'a' + 10, + 'A', 'B', 'C', 'D', 'E', 'F' => self.curChar - 'A' + 10, else => 0, }) << shift; - char = try self.appendControlGetNext(char) orelse return; + try self.appendControlGetNext() orelse return; if(shift == 0) break; } }, else => { - char = try self.appendGetNext(char) orelse return; + try self.appendGetNext() orelse return; } }; } @@ -515,10 +524,12 @@ pub const TextBuffer = struct { .currentFontEffect = initialFontEffect, .parsedText = std.ArrayList(u32).init(main.threadAllocator), .fontEffects = std.ArrayList(FontEffect).init(allocator), + .characterIndex = std.ArrayList(u32).init(allocator), .showControlCharacters = showControlCharacters }; defer parser.fontEffects.deinit(); defer parser.parsedText.deinit(); + defer parser.characterIndex.deinit(); self.lines = std.ArrayList(Line).init(allocator); self.lineBreakIndices = std.ArrayList(u32).init(allocator); try parser.parse(); @@ -565,6 +576,7 @@ pub const TextBuffer = struct { glyph.codepoint = glyphInfos[i].codepoint; glyph.cluster = glyphInfos[i].cluster; glyph.fontEffect = parser.fontEffects.items[textIndexGuess[i]]; + glyph.characterIndex = parser.characterIndex.items[textIndexGuess[i]]; } // Find the lines: @@ -581,8 +593,46 @@ pub const TextBuffer = struct { self.lineBreakIndices.deinit(); } + pub fn mousePosToIndex(self: TextBuffer, mousePos: Vec2f, bufferLen: usize) u32 { + var line: usize = @floatToInt(usize, @max(0, mousePos[1]/16.0)); + line = @min(line, self.lineBreakIndices.items.len - 2); + var x: f32 = 0; + const start = self.lineBreakIndices.items[line]; + const end = self.lineBreakIndices.items[line + 1]; + for(self.glyphs[start..end]) |glyph| { + if(mousePos[0] < x + glyph.x_advance/2) { + return @intCast(u32, glyph.characterIndex); + } + + x += glyph.x_advance; + } + return @intCast(u32, if(end < self.glyphs.len) self.glyphs[end-1].characterIndex else bufferLen); + } + + pub fn indexToCursorPos(self: TextBuffer, index: u32) Vec2f { + var x: f32 = 0; + var y: f32 = 0; + var i: usize = 0; + while(true) { + for(self.glyphs[self.lineBreakIndices.items[i]..self.lineBreakIndices.items[i+1]]) |glyph| { + if(glyph.characterIndex == index) { + return .{x, y}; + } + + x += glyph.x_advance; + y -= glyph.y_advance; + } + i += 1; + if(i >= self.lineBreakIndices.items.len - 1) { + return .{x, y}; + } + x = 0; + y += 16; + } + } + /// Returns the calculated dimensions of the text block. - pub fn calculateLineBreaks(self: *TextBuffer, fontSize: f32, maxLineWidth: f32) !Vec2f { + pub fn calculateLineBreaks(self: *TextBuffer, fontSize: f32, maxLineWidth: f32) !Vec2f { // TODO: Support newlines. self.lineBreakIndices.clearRetainingCapacity(); try self.lineBreakIndices.append(0); var scaledMaxWidth = maxLineWidth/fontSize*16.0; @@ -609,6 +659,37 @@ pub const TextBuffer = struct { return Vec2f{totalWidth*fontSize/16.0, @intToFloat(f32, self.lineBreakIndices.items.len - 1)*fontSize}; } + pub fn drawSelection(self: TextBuffer, pos: Vec2f, selectionStart: u32, selectionEnd: u32) !void { + std.debug.assert(selectionStart <= selectionEnd); + var x: f32 = 0; + var y: f32 = 0; + var i: usize = 0; + var j: usize = 0; + // Find the start row: + outer: while(i < self.lineBreakIndices.items.len - 1) : (i += 1) { + while(j < self.lineBreakIndices.items[i+1]) : (j += 1) { + const glyph = self.glyphs[j]; + if(glyph.characterIndex >= selectionStart) break :outer; + x += glyph.x_advance; + y -= glyph.y_advance; + } + x = 0; + y += 16; + } + while(i < self.lineBreakIndices.items.len - 1) : (i += 1) { + const startX = x; + while(j < self.lineBreakIndices.items[i+1] and j < selectionEnd) : (j += 1) { + const glyph = self.glyphs[j]; + if(glyph.characterIndex >= selectionEnd) break; + x += glyph.x_advance; + y -= glyph.y_advance; + } + draw.rect(pos + Vec2f{startX, y}, .{x - startX, 16}); + x = 0; + y += 16; + } + } + pub fn render(self: TextBuffer, _x: f32, _y: f32, _fontSize: f32) !void { var x = _x; var y = _y; diff --git a/src/gui/GuiComponent.zig b/src/gui/GuiComponent.zig index 91c314300..55f215fe8 100644 --- a/src/gui/GuiComponent.zig +++ b/src/gui/GuiComponent.zig @@ -8,6 +8,7 @@ pub const Button = @import("components/Button.zig"); pub const CheckBox = @import("components/CheckBox.zig"); pub const Label = @import("components/Label.zig"); pub const Slider = @import("components/Slider.zig"); +pub const TextInput = @import("components/TextInput.zig"); pub const VerticalList = @import("components/VerticalList.zig"); const GuiComponent = @This(); @@ -21,6 +22,7 @@ const Impl = union(enum) { checkBox: CheckBox, label: Label, slider: Slider, + textInput: TextInput, verticalList: VerticalList, }; diff --git a/src/gui/components/TextInput.zig b/src/gui/components/TextInput.zig new file mode 100644 index 000000000..d833f4178 --- /dev/null +++ b/src/gui/components/TextInput.zig @@ -0,0 +1,362 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const main = @import("root"); +const graphics = main.graphics; +const draw = graphics.draw; +const TextBuffer = graphics.TextBuffer; +const vec = main.vec; +const Vec2f = vec.Vec2f; + +const gui = @import("../gui.zig"); +const GuiComponent = gui.GuiComponent; + +const TextInput = @This(); + +const fontSize: f32 = 16; + +pressed: bool = false, +cursor: ?u32 = null, +selectionStart: ?u32 = null, +currentString: std.ArrayList(u8), +textBuffer: TextBuffer, +maxWidth: f32, +textSize: Vec2f = undefined, + +// TODO: Make this scrollable. + +pub fn init(allocator: Allocator, pos: Vec2f, maxWidth: f32, text: []const u8) Allocator.Error!GuiComponent { + var self = TextInput { + .currentString = std.ArrayList(u8).init(allocator), + .textBuffer = try TextBuffer.init(allocator, text, .{}, true), + .maxWidth = maxWidth, + }; + try self.currentString.appendSlice(text); + self.textSize = try self.textBuffer.calculateLineBreaks(fontSize, maxWidth); + return GuiComponent { + .pos = pos, + .size = self.textSize, + .impl = .{.textInput = self} + }; +} + +pub fn deinit(self: TextInput) void { + self.textBuffer.deinit(); + self.currentString.deinit(); +} + +pub fn mainButtonPressed(self: *TextInput, pos: Vec2f, _: Vec2f, mousePosition: Vec2f) void { + self.cursor = null; + self.selectionStart = self.textBuffer.mousePosToIndex(mousePosition - pos, self.currentString.items.len); + self.pressed = true; +} + +pub fn mainButtonReleased(self: *TextInput, pos: Vec2f, _: Vec2f, mousePosition: Vec2f) void { + if(self.pressed) { + self.cursor = self.textBuffer.mousePosToIndex(mousePosition - pos, self.currentString.items.len); + if(self.cursor == self.selectionStart) { + self.selectionStart = null; + } + self.pressed = false; + gui.setSelectedTextInput(self); + } +} + +pub fn deselect(self: *TextInput) void { + self.cursor = null; + self.selectionStart = null; +} + +fn reloadText(self: *TextInput) !void { + self.textBuffer.deinit(); + self.textBuffer = try TextBuffer.init(self.currentString.allocator, self.currentString.items, .{}, true); + self.textSize = try self.textBuffer.calculateLineBreaks(fontSize, self.maxWidth); +} + +fn moveCursorLeft(self: *TextInput, mods: main.Key.Modifiers) void { + if(mods.control) { + const text = self.currentString.items; + if(self.cursor.? == 0) return; + self.cursor.? -= 1; + // Find end of previous "word": + while(!std.ascii.isAlphabetic(text[self.cursor.?]) and std.ascii.isASCII(text[self.cursor.?])) { + if(self.cursor.? == 0) return; + self.cursor.? -= 1; + } + // Find the start of the previous "word": + while(std.ascii.isAlphabetic(text[self.cursor.?]) or !std.ascii.isASCII(text[self.cursor.?])) { + if(self.cursor.? == 0) return; + self.cursor.? -= 1; + } + self.cursor.? += 1; + } else { + while(self.cursor.? > 0) { + self.cursor.? -= 1; + if((std.unicode.utf8ByteSequenceLength(self.currentString.items[self.cursor.?]) catch 0) != 0) break; // Ugly hack to check if we found a valid start byte. + } + } +} + +pub fn left(self: *TextInput, mods: main.Key.Modifiers) void { + if(self.cursor) |*cursor| { + if(mods.shift) { + if(self.selectionStart == null) { + self.selectionStart = cursor.*; + } + self.moveCursorLeft(mods); + if(self.selectionStart == self.cursor) { + self.selectionStart = null; + } + } else { + if(self.selectionStart) |selectionStart| { + cursor.* = @min(cursor.*, selectionStart); + self.selectionStart = null; + } else { + self.moveCursorLeft(mods); + } + } + } +} + +fn moveCursorRight(self: *TextInput, mods: main.Key.Modifiers) void { + if(self.cursor.? < self.currentString.items.len) { + if(mods.control) { + const text = self.currentString.items; + // Find start of next "word": + while(!std.ascii.isAlphabetic(text[self.cursor.?]) and std.ascii.isASCII(text[self.cursor.?])) { + self.cursor.? += 1; + if(self.cursor.? >= self.currentString.items.len) return; + } + // Find the end of the next "word": + while(std.ascii.isAlphabetic(text[self.cursor.?]) or !std.ascii.isASCII(text[self.cursor.?])) { + self.cursor.? += 1; + if(self.cursor.? >= self.currentString.items.len) return; + } + } else { + self.cursor.? += std.unicode.utf8ByteSequenceLength(self.currentString.items[self.cursor.?]) catch unreachable; + } + } +} + +pub fn right(self: *TextInput, mods: main.Key.Modifiers) void { + if(self.cursor) |*cursor| { + if(mods.shift) { + if(self.selectionStart == null) { + self.selectionStart = cursor.*; + } + self.moveCursorRight(mods); + if(self.selectionStart == self.cursor) { + self.selectionStart = null; + } + } else { + if(self.selectionStart) |selectionStart| { + cursor.* = @max(cursor.*, selectionStart); + self.selectionStart = null; + } else { + self.moveCursorRight(mods); + } + } + } +} + +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); +} + +pub fn down(self: *TextInput, mods: main.Key.Modifiers) void { + if(self.cursor) |*cursor| { + if(mods.shift) { + if(self.selectionStart == null) { + self.selectionStart = cursor.*; + } + self.moveCursorVertically(1); + if(self.selectionStart == self.cursor) { + self.selectionStart = null; + } + } else { + if(self.selectionStart) |selectionStart| { + cursor.* = @max(cursor.*, selectionStart); + self.selectionStart = null; + } else { + self.moveCursorVertically(1); + } + } + } +} + +pub fn up(self: *TextInput, mods: main.Key.Modifiers) void { + if(self.cursor) |*cursor| { + if(mods.shift) { + if(self.selectionStart == null) { + self.selectionStart = cursor.*; + } + self.moveCursorVertically(-1); + if(self.selectionStart == self.cursor) { + self.selectionStart = null; + } + } else { + if(self.selectionStart) |selectionStart| { + cursor.* = @min(cursor.*, selectionStart); + self.selectionStart = null; + } else { + self.moveCursorVertically(-1); + } + } + } +} + +fn moveCursorToStart(self: *TextInput, mods: main.Key.Modifiers) void { + if(mods.control) { + self.cursor.? = 0; + } else { + self.cursor.? = @intCast(u32, if(std.mem.lastIndexOf(u8, self.currentString.items[0..self.cursor.?], "\n")) |nextPos| nextPos + 1 else 0); + } +} + +pub fn gotoStart(self: *TextInput, mods: main.Key.Modifiers) void { + if(self.cursor) |*cursor| { + if(mods.shift) { + if(self.selectionStart == null) { + self.selectionStart = cursor.*; + } + self.moveCursorToStart(mods); + if(self.selectionStart == self.cursor) { + self.selectionStart = null; + } + } else { + if(self.selectionStart) |selectionStart| { + cursor.* = @min(cursor.*, selectionStart); + self.selectionStart = null; + } else { + self.moveCursorToStart(mods); + } + } + } +} + +fn moveCursorToEnd(self: *TextInput, mods: main.Key.Modifiers) void { + if(mods.control) { + self.cursor.? = @intCast(u32, self.currentString.items.len); + } else { + self.cursor.? = @intCast(u32, std.mem.indexOf(u8, self.currentString.items[self.cursor.?..], "\n") orelse self.currentString.items.len); + } +} + +pub fn gotoEnd(self: *TextInput, mods: main.Key.Modifiers) void { + if(self.cursor) |*cursor| { + if(mods.shift) { + if(self.selectionStart == null) { + self.selectionStart = cursor.*; + } + self.moveCursorToEnd(mods); + if(self.selectionStart == self.cursor) { + self.selectionStart = null; + } + } else { + if(self.selectionStart) |selectionStart| { + cursor.* = @min(cursor.*, selectionStart); + self.selectionStart = null; + } else { + self.moveCursorToEnd(mods); + } + } + } +} + +fn deleteSelection(self: *TextInput) void { + if(self.selectionStart) |selectionStart| { + const start = @min(selectionStart, self.cursor.?); + const end = @max(selectionStart, self.cursor.?); + + self.currentString.replaceRange(start, end - start, &[0]u8{}) catch unreachable; + self.cursor.? = start; + self.selectionStart = null; + } +} + +pub fn deleteLeft(self: *TextInput, _: main.Key.Modifiers) void { + if(self.cursor == null) return; + if(self.selectionStart == null) { + self.selectionStart = self.cursor; + self.moveCursorLeft(.{}); + } + self.deleteSelection(); + self.reloadText() catch |err| { + std.log.err("Error while deleting text: {s}", .{@errorName(err)}); + }; +} + +pub fn deleteRight(self: *TextInput, _: main.Key.Modifiers) void { + if(self.cursor == null) return; + if(self.selectionStart == null) { + self.selectionStart = self.cursor; + self.moveCursorRight(.{}); + } + self.deleteSelection(); + self.reloadText() catch |err| { + std.log.err("Error while deleting text: {s}", .{@errorName(err)}); + }; +} + +pub fn inputCharacter(self: *TextInput, character: u21) !void { + if(self.cursor) |*cursor| { + self.deleteSelection(); + var buf: [4]u8 = undefined; + var utf8 = buf[0..try std.unicode.utf8Encode(character, &buf)]; + try self.currentString.insertSlice(cursor.*, utf8); + try self.reloadText(); + cursor.* += @intCast(u32, utf8.len); + } +} + +pub fn copy(self: *TextInput, mods: main.Key.Modifiers) void { + if(mods.control) { + if(self.cursor) |cursor| { + if(self.selectionStart) |selectionStart| { + const start = @min(cursor, selectionStart); + const end = @max(cursor, selectionStart); + main.Window.setClipboardString(self.currentString.items[start..end]); + } + } + } +} + +pub fn paste(self: *TextInput, mods: main.Key.Modifiers) void { + if(mods.control) { + const string = main.Window.getClipboardString(); + self.deleteSelection(); + self.currentString.insertSlice(self.cursor.?, string) catch |err| { + std.log.err("Error while pasting text: {s}", .{@errorName(err)}); + }; + self.cursor.? += @intCast(u32, string.len); + self.reloadText() catch |err| { + std.log.err("Error while pasting text: {s}", .{@errorName(err)}); + }; + } +} + +pub fn cut(self: *TextInput, mods: main.Key.Modifiers) void { + if(mods.control) { + self.copy(mods); + self.deleteSelection(); + self.reloadText() catch |err| { + std.log.err("Error while cutting text: {s}", .{@errorName(err)}); + }; + } +} + +pub fn render(self: *TextInput, pos: Vec2f, _: Vec2f, mousePosition: Vec2f) !void { + try self.textBuffer.render(pos[0], pos[1], fontSize); + if(self.pressed) { + self.cursor = self.textBuffer.mousePosToIndex(mousePosition - pos, self.currentString.items.len); + } + if(self.cursor) |cursor| { + if(self.selectionStart) |selectionStart| { + draw.setColor(0x440000ff); + try self.textBuffer.drawSelection(pos, @min(selectionStart, cursor), @max(selectionStart, cursor)); + } + draw.setColor(0xff000000); + const cursorPos = pos + self.textBuffer.indexToCursorPos(cursor); + draw.line(cursorPos, cursorPos + Vec2f{0, 16}); + } +} \ No newline at end of file diff --git a/src/gui/gui.zig b/src/gui/gui.zig index 7cc214c2a..cdc80b68c 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -11,6 +11,7 @@ const Vec2f = vec.Vec2f; const Button = @import("components/Button.zig"); const CheckBox = @import("components/CheckBox.zig"); const Slider = @import("components/Slider.zig"); +const TextInput = @import("components/TextInput.zig"); pub const GuiComponent = @import("GuiComponent.zig"); pub const GuiWindow = @import("GuiWindow.zig"); @@ -20,6 +21,7 @@ var windowList: std.ArrayList(*GuiWindow) = undefined; var hudWindows: std.ArrayList(*GuiWindow) = undefined; pub var openWindows: std.ArrayList(*GuiWindow) = undefined; pub var selectedWindow: ?*GuiWindow = null; // TODO: Make private. +pub var selectedTextInput: ?*TextInput = null; pub fn init() !void { windowList = std.ArrayList(*GuiWindow).init(main.globalAllocator); @@ -111,8 +113,76 @@ pub fn closeWindow(window: *GuiWindow) void { window.onCloseFn(); } +pub fn setSelectedTextInput(newSelectedTextInput: ?*TextInput) void { + if(selectedTextInput) |current| { + if(current != newSelectedTextInput) { + current.deselect(); + } + } + selectedTextInput = newSelectedTextInput; +} + +pub const textCallbacks = struct { + pub fn left(mods: main.Key.Modifiers) void { + if(selectedTextInput) |current| { + current.left(mods); + } + } + pub fn right(mods: main.Key.Modifiers) void { + if(selectedTextInput) |current| { + current.right(mods); + } + } + pub fn down(mods: main.Key.Modifiers) void { + if(selectedTextInput) |current| { + current.down(mods); + } + } + pub fn up(mods: main.Key.Modifiers) void { + if(selectedTextInput) |current| { + current.up(mods); + } + } + pub fn gotoStart(mods: main.Key.Modifiers) void { + if(selectedTextInput) |current| { + current.gotoStart(mods); + } + } + pub fn gotoEnd(mods: main.Key.Modifiers) void { + if(selectedTextInput) |current| { + current.gotoEnd(mods); + } + } + pub fn deleteLeft(mods: main.Key.Modifiers) void { + if(selectedTextInput) |current| { + current.deleteLeft(mods); + } + } + pub fn deleteRight(mods: main.Key.Modifiers) void { + if(selectedTextInput) |current| { + current.deleteRight(mods); + } + } + pub fn copy(mods: main.Key.Modifiers) void { + if(selectedTextInput) |current| { + current.copy(mods); + } + } + pub fn paste(mods: main.Key.Modifiers) void { + if(selectedTextInput) |current| { + current.paste(mods); + } + } + pub fn cut(mods: main.Key.Modifiers) void { + if(selectedTextInput) |current| { + current.cut(mods); + } + } +}; + pub fn mainButtonPressed() void { selectedWindow = null; + selectedTextInput = null; var selectedI: usize = 0; for(openWindows.items, 0..) |window, i| { var mousePosition = main.Window.getMousePosition(); diff --git a/src/gui/windows/change_name.zig b/src/gui/windows/change_name.zig index e57faa0c4..84dd841ca 100644 --- a/src/gui/windows/change_name.zig +++ b/src/gui/windows/change_name.zig @@ -8,6 +8,7 @@ const gui = @import("../gui.zig"); const GuiComponent = gui.GuiComponent; const GuiWindow = gui.GuiWindow; const Button = @import("../components/Button.zig"); +const TextInput = @import("../components/TextInput.zig"); const VerticalList = @import("../components/VerticalList.zig"); var window: GuiWindow = undefined; @@ -28,6 +29,8 @@ const padding: f32 = 8; pub fn onOpen() Allocator.Error!void { var list = try VerticalList.init(main.globalAllocator); + // TODO Please change your name bla bla + try list.add(try TextInput.init(main.globalAllocator, .{0, 16}, 128, "gr da jkwa hfeka fuei ofuiewo atg78o4ea74e8t z57 t4738qa0 47a80 t47803a t478aqv t487 5t478a0 tg478a09 t748ao t7489a rt4e5 okv5895 678v54vgvo6r z8or z578v rox74et8ys9otv 4z3789so z4oa9t z489saoyt z")); // TODO //try list.add(try Button.init(.{0, 16}, 128, main.globalAllocator, "Singleplayer TODO", &buttonCallbackTest)); //try list.add(try Button.init(.{0, 16}, 128, main.globalAllocator, "Multiplayer TODO", &buttonCallbackTest)); diff --git a/src/main.zig b/src/main.zig index d1a6c59d3..501f62047 100644 --- a/src/main.zig +++ b/src/main.zig @@ -68,6 +68,16 @@ pub const Key = struct { scancode: c_int = 0, releaseAction: ?*const fn() void = null, pressAction: ?*const fn() void = null, + repeatAction: ?*const fn(Modifiers) void = null, + + pub const Modifiers = packed struct(u6) { + shift: bool = false, + control: bool = false, + alt: bool = false, + super: bool = false, + capsLock: bool = false, + numLock: bool = false, + }; pub fn getName(self: Key) []const u8 { if(self.mouseButton == -1) { @@ -149,6 +159,7 @@ pub fn setNextKeypressListener(listener: ?*const fn(c_int, c_int, c_int) void) ! nextKeypressListener = listener; } pub var keyboard: struct { + // Gameplay: forward: Key = Key{.key = c.GLFW_KEY_W}, left: Key = Key{.key = c.GLFW_KEY_A}, backward: Key = Key{.key = c.GLFW_KEY_S}, @@ -157,9 +168,23 @@ pub var keyboard: struct { jump: Key = Key{.key = c.GLFW_KEY_SPACE}, fall: Key = Key{.key = c.GLFW_KEY_LEFT_SHIFT}, fullscreen: Key = Key{.key = c.GLFW_KEY_F11, .releaseAction = &Window.toggleFullscreen}, + + // Gui: mainGuiButton: Key = Key{.mouseButton = c.GLFW_MOUSE_BUTTON_LEFT, .pressAction = &gui.mainButtonPressed, .releaseAction = &gui.mainButtonReleased}, rightMouseButton: Key = Key{.mouseButton = c.GLFW_MOUSE_BUTTON_RIGHT}, middleMouseButton: Key = Key{.mouseButton = c.GLFW_MOUSE_BUTTON_MIDDLE}, + // text: + textCursorLeft: Key = Key{.key = c.GLFW_KEY_LEFT, .repeatAction = &gui.textCallbacks.left}, + textCursorRight: Key = Key{.key = c.GLFW_KEY_RIGHT, .repeatAction = &gui.textCallbacks.right}, + textCursorDown: Key = Key{.key = c.GLFW_KEY_DOWN, .repeatAction = &gui.textCallbacks.down}, + textCursorUp: Key = Key{.key = c.GLFW_KEY_UP, .repeatAction = &gui.textCallbacks.up}, + textGotoStart: Key = Key{.key = c.GLFW_KEY_HOME, .repeatAction = &gui.textCallbacks.gotoStart}, + textGotoEnd: Key = Key{.key = c.GLFW_KEY_END, .repeatAction = &gui.textCallbacks.gotoEnd}, + textDeleteLeft: Key = Key{.key = c.GLFW_KEY_BACKSPACE, .repeatAction = &gui.textCallbacks.deleteLeft}, + textDeleteRight: Key = Key{.key = c.GLFW_KEY_DELETE, .repeatAction = &gui.textCallbacks.deleteRight}, + textCopy: Key = Key{.key = c.GLFW_KEY_C, .repeatAction = &gui.textCallbacks.copy}, + textPaste: Key = Key{.key = c.GLFW_KEY_V, .repeatAction = &gui.textCallbacks.paste}, + textCut: Key = Key{.key = c.GLFW_KEY_X, .repeatAction = &gui.textCallbacks.cut}, } = .{}; pub const Window = struct { @@ -173,7 +198,6 @@ pub const Window = struct { std.log.err("GLFW Error({}): {s}", .{errorCode, description}); } fn keyCallback(_: ?*c.GLFWwindow, key: c_int, scancode: c_int, action: c_int, mods: c_int) callconv(.C) void { - _ = mods; if(action == c.GLFW_PRESS) { inline for(@typeInfo(@TypeOf(keyboard)).Struct.fields) |field| { if(key == @field(keyboard, field.name).key) { @@ -182,6 +206,9 @@ pub const Window = struct { if(@field(keyboard, field.name).pressAction) |pressAction| { pressAction(); } + if(@field(keyboard, field.name).repeatAction) |repeatAction| { + repeatAction(@bitCast(Key.Modifiers, @intCast(u6, mods))); + } } } } @@ -200,8 +227,26 @@ pub const Window = struct { } } } + } else if(action == c.GLFW_REPEAT) { + inline for(@typeInfo(@TypeOf(keyboard)).Struct.fields) |field| { + if(key == @field(keyboard, field.name).key) { + if(key != c.GLFW_KEY_UNKNOWN or scancode == @field(keyboard, field.name).scancode) { + if(@field(keyboard, field.name).repeatAction) |repeatAction| { + repeatAction(@bitCast(Key.Modifiers, @intCast(u6, mods))); + } + } + } + } } } + fn charCallback(_: ?*c.GLFWwindow, codepoint: c_uint) callconv(.C) void { + if(gui.selectedTextInput) |textInput| { + textInput.inputCharacter(@intCast(u21, codepoint)) catch |err| { + std.log.err("Error while adding character to textInput: {s}", .{@errorName(err)}); + }; + } + } + fn framebufferSize(_: ?*c.GLFWwindow, newWidth: c_int, newHeight: c_int) callconv(.C) void { std.log.info("Framebuffer: {}, {}", .{newWidth, newHeight}); width = @intCast(u31, newWidth); @@ -297,6 +342,16 @@ pub const Window = struct { c.glfwSwapInterval(@boolToInt(settings.vsync)); } + pub fn getClipboardString() []const u8 { + return std.mem.span(c.glfwGetClipboardString(window)); + } + + pub fn setClipboardString(string: []const u8) void { + const nullTerminatedString = threadAllocator.dupeZ(u8, string) catch return; + defer threadAllocator.free(nullTerminatedString); + c.glfwSetClipboardString(window, nullTerminatedString.ptr); + } + fn init() !void { _ = c.glfwSetErrorCallback(GLFWCallbacks.errorCallback); @@ -313,6 +368,7 @@ pub const Window = struct { window = c.glfwCreateWindow(width, height, "Cubyz", null, null) orelse return error.GLFWFailed; _ = c.glfwSetKeyCallback(window, GLFWCallbacks.keyCallback); + _ = c.glfwSetCharCallback(window, GLFWCallbacks.charCallback); _ = c.glfwSetFramebufferSizeCallback(window, GLFWCallbacks.framebufferSize); _ = c.glfwSetCursorPosCallback(window, GLFWCallbacks.cursorPosition); _ = c.glfwSetMouseButtonCallback(window, GLFWCallbacks.mouseButton);