From b8f5b8432256f09c7f84281936db6fc28524e80c Mon Sep 17 00:00:00 2001 From: IntegratedQuantum Date: Sat, 11 Mar 2023 17:45:20 +0100 Subject: [PATCH] Text can now be scrolled. --- assets/cubyz/ui/scrollbar.png | Bin 0 -> 938 bytes src/graphics.zig | 69 ++++++++++---------- src/gui/GuiComponent.zig | 2 + src/gui/components/ScrollBar.zig | 104 +++++++++++++++++++++++++++++++ src/gui/components/TextInput.zig | 81 ++++++++++++++++++++---- src/gui/gui.zig | 3 + src/gui/windows/change_name.zig | 2 +- src/main.zig | 4 +- 8 files changed, 219 insertions(+), 46 deletions(-) create mode 100644 assets/cubyz/ui/scrollbar.png create mode 100644 src/gui/components/ScrollBar.zig diff --git a/assets/cubyz/ui/scrollbar.png b/assets/cubyz/ui/scrollbar.png new file mode 100644 index 0000000000000000000000000000000000000000..131e5510787f6d201e32dbdddec2117f800fb8bf GIT binary patch literal 938 zcmV;b16BNqP)EX>4Tx04R}tkv&MmKpe$i(@OoQI9No)AwzZ1f~bh2RIvyaN?V~-2a`*`ph-iL z;^HW{799LotU9+0Yt2!bCVZf;JBE>hzEl0u7E503ls?%w0>9U#<7Of`KIfT~$W zA{r6XnN`vMiXIH03uEv}%+zDa#T43(uX}j-dKczd?a%!=dX=on0FOvK$8^IY-XNaY zv~M{K$3L zc3I)P#aS&?SmU1jg@LTLlH@wgVMMWn7~&8iqkA{&2mgcL-I}?nNjE7F0Xkl6`(p&~?*jFzZGRuzcKrnKJ_A=;(_gLuv!A3_ zn_B1y=-UP^uA7>?2VCv|gHO6-NRH&CDdcj%`x$*x2I#*9y4T#^8v8hX08-S|@(pls z2#n?_d%exOJ6n7E_e`U|A8Uegq`x0Wu>b%724YJ`L;yzsLjXZwmwmYa000SaNLh0L z04^f{04^f|c%?sf00007bV*G`2jvSE4H7x0)^bY#000?uMObu0Z*6U5Zgc=ca%Ew3 zWn>_CX>@2HM@dakSAh-}0004jNkl1vTb1>3Y{@y@DKOkN}5EQg% z5rnz$n%a5`E=+>1!4_^pgi6FkL=o!DkklC2Shk37JfqXu44m`5-_P^CV;$`8&4@Tt z-v^*nF4FDv0I(ewfMI{Y&-XU~>h$a+B)o1k8TJP#t%JMGtqr=J9_dsP+i`>B0Lm1~ z$>g%UJUx>ywMlgoyZyuFT4p67+;uT|?yV+h1y zf52)i27nZ`DwXqD6u6z;Z5|&UBiAP;0$35D=|pLNZ9OrEr8P?HKld3nSK@JV=Be)o zrZTI`k2UrE0IG_zt69Mtk3)>iINNbiriDddexuf?wC3*io oldClip[0] + oldClip[2]) { + newClip[2] -= (newClip[0] + newClip[2]) - (oldClip[0] + oldClip[2]); + } + if (newClip[1] + newClip[3] > oldClip[1] + oldClip[3]) { + newClip[3] -= (newClip[1] + newClip[3]) - (oldClip[1] + oldClip[3]); + } + } else { + c.glEnable(c.GL_SCISSOR_TEST); + } + c.glScissor(newClip[0], newClip[1], newClip[2], newClip[3]); const oldClip = clip; clip = newClip; - var clipRef: *Vec4i = &clip.?; - if(oldClip == null) { - c.glEnable(c.GL_SCISSOR_TEST); - } else { - if (clipRef.x < oldClip.x) { - clipRef.z -= oldClip.x - clipRef.x; - clipRef.x += oldClip.x - clipRef.x; - } - if (clipRef.y < oldClip.y) { - clipRef.w -= oldClip.y - clipRef.y; - clipRef.y += oldClip.y - clipRef.y; - } - if (clipRef.x + clipRef.z > oldClip.x + oldClip.z) { - clipRef.z -= (clipRef.x + clipRef.z) - (oldClip.x + oldClip.z); - } - if (clipRef.y + clipRef.w > oldClip.y + oldClip.w) { - clipRef.w -= (clipRef.y + clipRef.w) - (oldClip.y + oldClip.w); - } - } - c.glScissor(clipRef.x, clipRef.y, clipRef.z, clipRef.w); return oldClip; } @@ -92,7 +97,7 @@ pub const draw = struct { pub fn restoreClip(previousClip: ?Vec4i) void { clip = previousClip; if (clip) |clipRef| { - c.glScissor(clipRef.x, clipRef.y, clipRef.z, clipRef.w); + c.glScissor(clipRef[0], clipRef[1], clipRef[2], clipRef[3]); } else { c.glDisable(c.GL_SCISSOR_TEST); } @@ -668,6 +673,16 @@ pub const TextBuffer = struct { var lastSpaceIndex: u32 = 0; for(self.glyphs, 0..) |glyph, i| { lineWidth += glyph.x_advance; + if(glyph.character == ' ') { + lastSpaceWidth = lineWidth; + lastSpaceIndex = @intCast(u32, i+1); + } + if(glyph.character == '\n') { + try self.lineBreaks.append(.{.index = @intCast(u32, i+1), .width = lineWidth - spaceCharacterWidth}); + lineWidth = 0; + lastSpaceIndex = 0; + lastSpaceWidth = 0; + } if(lineWidth > scaledMaxWidth) { if(lastSpaceIndex != 0) { lineWidth -= lastSpaceWidth; @@ -681,16 +696,6 @@ pub const TextBuffer = struct { lastSpaceWidth = 0; } } - if(glyph.character == ' ') { - lastSpaceWidth = lineWidth; - lastSpaceIndex = @intCast(u32, i+1); - } - if(glyph.character == '\n') { - try self.lineBreaks.append(.{.index = @intCast(u32, i+1), .width = lineWidth - spaceCharacterWidth}); - lineWidth = 0; - lastSpaceIndex = 0; - lastSpaceWidth = 0; - } } self.width = maxLineWidth; try self.lineBreaks.append(.{.index = @intCast(u32, self.glyphs.len), .width = lineWidth}); diff --git a/src/gui/GuiComponent.zig b/src/gui/GuiComponent.zig index 55f215fe..f59032d8 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 ScrollBar = @import("components/ScrollBar.zig"); pub const TextInput = @import("components/TextInput.zig"); pub const VerticalList = @import("components/VerticalList.zig"); @@ -21,6 +22,7 @@ const Impl = union(enum) { button: Button, checkBox: CheckBox, label: Label, + scrollBar: ScrollBar, slider: Slider, textInput: TextInput, verticalList: VerticalList, diff --git a/src/gui/components/ScrollBar.zig b/src/gui/components/ScrollBar.zig new file mode 100644 index 00000000..3c3456f4 --- /dev/null +++ b/src/gui/components/ScrollBar.zig @@ -0,0 +1,104 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const main = @import("root"); +const graphics = main.graphics; +const draw = graphics.draw; +const Image = graphics.Image; +const Shader = graphics.Shader; +const TextBuffer = graphics.TextBuffer; +const Texture = graphics.Texture; +const random = main.random; +const vec = main.vec; +const Vec2f = vec.Vec2f; + +const gui = @import("../gui.zig"); +const GuiComponent = gui.GuiComponent; +const Button = GuiComponent.Button; +const Label = GuiComponent.Label; + +const ScrollBar = @This(); + +const fontSize: f32 = 16; + +var texture: Texture = undefined; + +// TODO: Scroll wheel support. + +currentState: f32, +button: Button, +buttonSize: Vec2f, +buttonPos: Vec2f = .{0, 0}, +mouseAnchor: f32 = undefined, + +pub fn __init() !void { + texture = Texture.init(); + const image = try Image.readFromFile(main.threadAllocator, "assets/cubyz/ui/scrollbar.png"); + defer image.deinit(main.threadAllocator); + try texture.generate(image); +} + +pub fn __deinit() void { + texture.deinit(); +} + +pub fn init(pos: Vec2f, width: f32, height: f32, initialState: f32) Allocator.Error!GuiComponent { + const buttonComponent = try Button.init(undefined, undefined, "", null); + var self = ScrollBar { + .currentState = initialState, + .button = buttonComponent.impl.button, + .buttonSize = .{width, 16}, + }; + const size = Vec2f{width, height}; + self.setButtonPosFromValue(size); + return GuiComponent { + .pos = pos, + .size = size, + .impl = .{.scrollBar = self} + }; +} + +pub fn deinit(self: ScrollBar) void { + self.button.deinit(); +} + +fn setButtonPosFromValue(self: *ScrollBar, size: Vec2f) void { + const range: f32 = size[1] - self.buttonSize[1]; + self.buttonPos[1] = range*self.currentState; +} + +fn updateValueFromButtonPos(self: *ScrollBar, size: Vec2f) void { + const range: f32 = size[1] - self.buttonSize[1]; + const value = self.buttonPos[1]/range; + if(value != self.currentState) { + self.currentState = value; + } +} + +pub fn mainButtonPressed(self: *ScrollBar, pos: Vec2f, _: Vec2f, mousePosition: Vec2f) void { + if(GuiComponent.contains(self.buttonPos, self.buttonSize, mousePosition - pos)) { + self.button.mainButtonPressed(self.buttonPos, self.buttonSize, mousePosition - pos); + self.mouseAnchor = mousePosition[1] - self.buttonPos[1]; + } +} + +pub fn mainButtonReleased(self: *ScrollBar, _: Vec2f, _: Vec2f, _: Vec2f) void { + self.button.mainButtonReleased(undefined, undefined, undefined); +} + +pub fn render(self: *ScrollBar, pos: Vec2f, size: Vec2f, mousePosition: Vec2f) !void { + graphics.c.glActiveTexture(graphics.c.GL_TEXTURE0); + texture.bind(); + Button.shader.bind(); + draw.setColor(0xff000000); + draw.customShadedRect(Button.buttonUniforms, pos, size); + + const range: f32 = size[1] - self.buttonSize[1]; + self.setButtonPosFromValue(size); + if(self.button.pressed) { + self.buttonPos[1] = mousePosition[1] - self.mouseAnchor; + self.buttonPos[1] = @min(@max(self.buttonPos[1], 0), range - 0.001); + self.updateValueFromButtonPos(size); + } + try self.button.render(pos + self.buttonPos, self.buttonSize, mousePosition); +} \ No newline at end of file diff --git a/src/gui/components/TextInput.zig b/src/gui/components/TextInput.zig index 85a479a7..f06402f6 100644 --- a/src/gui/components/TextInput.zig +++ b/src/gui/components/TextInput.zig @@ -13,9 +13,12 @@ const Vec2f = vec.Vec2f; const gui = @import("../gui.zig"); const GuiComponent = gui.GuiComponent; const Button = GuiComponent.Button; +const ScrollBar = GuiComponent.ScrollBar; const TextInput = @This(); +const scrollBarWidth = 5; +const border: f32 = 3; const fontSize: f32 = 16; var texture: Texture = undefined; @@ -26,7 +29,10 @@ selectionStart: ?u32 = null, currentString: std.ArrayList(u8), textBuffer: TextBuffer, maxWidth: f32, +maxHeight: f32, textSize: Vec2f = undefined, +scrollBar: ScrollBar, +scrollBarSize: Vec2f, pub fn __init() !void { texture = Texture.init(); @@ -39,19 +45,21 @@ pub fn __deinit() void { texture.deinit(); } -// TODO: Make this scrollable. - -pub fn init(pos: Vec2f, maxWidth: f32, text: []const u8) Allocator.Error!GuiComponent { +pub fn init(pos: Vec2f, maxWidth: f32, maxHeight: f32, text: []const u8) Allocator.Error!GuiComponent { + const scrollBarComponent = try ScrollBar.init(undefined, scrollBarWidth, maxHeight - 2*border, 0); var self = TextInput { .currentString = std.ArrayList(u8).init(gui.allocator), .textBuffer = try TextBuffer.init(gui.allocator, text, .{}, true, .left), .maxWidth = maxWidth, + .maxHeight = maxHeight, + .scrollBar = scrollBarComponent.impl.scrollBar, + .scrollBarSize = scrollBarComponent.size, }; try self.currentString.appendSlice(text); - self.textSize = try self.textBuffer.calculateLineBreaks(fontSize, maxWidth); + self.textSize = try self.textBuffer.calculateLineBreaks(fontSize, maxWidth - 2*border - scrollBarWidth); return GuiComponent { .pos = pos, - .size = self.textSize, + .size = .{maxWidth, maxHeight}, .impl = .{.textInput = self} }; } @@ -59,15 +67,23 @@ pub fn init(pos: Vec2f, maxWidth: f32, text: []const u8) Allocator.Error!GuiComp pub fn deinit(self: TextInput) void { self.textBuffer.deinit(); self.currentString.deinit(); + self.scrollBar.deinit(); } -pub fn mainButtonPressed(self: *TextInput, pos: Vec2f, _: Vec2f, mousePosition: Vec2f) void { +pub fn mainButtonPressed(self: *TextInput, pos: Vec2f, size: Vec2f, mousePosition: Vec2f) void { + if(self.textSize[1] > self.maxHeight - 2*border) { + const scrollBarPos = Vec2f{size[0] - border - scrollBarWidth, border}; + if(GuiComponent.contains(scrollBarPos, self.scrollBarSize, mousePosition - pos)) { + self.scrollBar.mainButtonPressed(scrollBarPos, self.scrollBarSize, mousePosition - pos); + return; + } + } 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 { +pub fn mainButtonReleased(self: *TextInput, pos: Vec2f, size: Vec2f, mousePosition: Vec2f) void { if(self.pressed) { self.cursor = self.textBuffer.mousePosToIndex(mousePosition - pos, self.currentString.items.len); if(self.cursor == self.selectionStart) { @@ -75,6 +91,9 @@ pub fn mainButtonReleased(self: *TextInput, pos: Vec2f, _: Vec2f, mousePosition: } self.pressed = false; gui.setSelectedTextInput(self); + } else if(self.textSize[1] > self.maxHeight - 2*border) { + self.scrollBar.mainButtonReleased(.{size[0] - border - scrollBarWidth, border}, self.scrollBarSize, mousePosition - pos); + gui.setSelectedTextInput(self); } } @@ -86,7 +105,7 @@ pub fn deselect(self: *TextInput) void { fn reloadText(self: *TextInput) !void { self.textBuffer.deinit(); self.textBuffer = try TextBuffer.init(gui.allocator, self.currentString.items, .{}, true, .left); - self.textSize = try self.textBuffer.calculateLineBreaks(fontSize, self.maxWidth); + self.textSize = try self.textBuffer.calculateLineBreaks(fontSize, self.maxWidth - 2*border - scrollBarWidth); } fn moveCursorLeft(self: *TextInput, mods: main.Key.Modifiers) void { @@ -131,6 +150,7 @@ pub fn left(self: *TextInput, mods: main.Key.Modifiers) void { self.moveCursorLeft(mods); } } + self.ensureCursorVisibility(); } } @@ -172,6 +192,7 @@ pub fn right(self: *TextInput, mods: main.Key.Modifiers) void { self.moveCursorRight(mods); } } + self.ensureCursorVisibility(); } } @@ -197,6 +218,7 @@ pub fn down(self: *TextInput, mods: main.Key.Modifiers) void { self.moveCursorVertically(1); } } + self.ensureCursorVisibility(); } } @@ -218,6 +240,7 @@ pub fn up(self: *TextInput, mods: main.Key.Modifiers) void { self.moveCursorVertically(-1); } } + self.ensureCursorVisibility(); } } @@ -247,6 +270,7 @@ pub fn gotoStart(self: *TextInput, mods: main.Key.Modifiers) void { self.moveCursorToStart(mods); } } + self.ensureCursorVisibility(); } } @@ -276,6 +300,7 @@ pub fn gotoEnd(self: *TextInput, mods: main.Key.Modifiers) void { self.moveCursorToEnd(mods); } } + self.ensureCursorVisibility(); } } @@ -287,6 +312,7 @@ fn deleteSelection(self: *TextInput) void { self.currentString.replaceRange(start, end - start, &[0]u8{}) catch unreachable; self.cursor.? = start; self.selectionStart = null; + self.ensureCursorVisibility(); } } @@ -300,6 +326,7 @@ pub fn deleteLeft(self: *TextInput, _: main.Key.Modifiers) void { self.reloadText() catch |err| { std.log.err("Error while deleting text: {s}", .{@errorName(err)}); }; + self.ensureCursorVisibility(); } pub fn deleteRight(self: *TextInput, _: main.Key.Modifiers) void { @@ -312,6 +339,7 @@ pub fn deleteRight(self: *TextInput, _: main.Key.Modifiers) void { self.reloadText() catch |err| { std.log.err("Error while deleting text: {s}", .{@errorName(err)}); }; + self.ensureCursorVisibility(); } pub fn inputCharacter(self: *TextInput, character: u21) !void { @@ -322,6 +350,7 @@ pub fn inputCharacter(self: *TextInput, character: u21) !void { try self.currentString.insertSlice(cursor.*, utf8); try self.reloadText(); cursor.* += @intCast(u32, utf8.len); + self.ensureCursorVisibility(); } } @@ -334,6 +363,7 @@ pub fn copy(self: *TextInput, mods: main.Key.Modifiers) void { main.Window.setClipboardString(self.currentString.items[start..end]); } } + self.ensureCursorVisibility(); } } @@ -348,6 +378,7 @@ pub fn paste(self: *TextInput, mods: main.Key.Modifiers) void { self.reloadText() catch |err| { std.log.err("Error while pasting text: {s}", .{@errorName(err)}); }; + self.ensureCursorVisibility(); } } @@ -358,6 +389,7 @@ pub fn cut(self: *TextInput, mods: main.Key.Modifiers) void { self.reloadText() catch |err| { std.log.err("Error while cutting text: {s}", .{@errorName(err)}); }; + self.ensureCursorVisibility(); } } @@ -365,6 +397,23 @@ pub fn newline(self: *TextInput, _: main.Key.Modifiers) void { self.inputCharacter('\n') catch |err| { std.log.err("Error while entering text: {s}", .{@errorName(err)}); }; + self.ensureCursorVisibility(); +} + +fn ensureCursorVisibility(self: *TextInput) void { + if(self.textSize[1] > self.maxHeight - 2*border) { + var y: f32 = 0; + const diff = self.textSize[1] - (self.maxHeight - 2*border); + y -= diff*self.scrollBar.currentState; + if(self.cursor) |cursor| { + var cursorPos = y + self.textBuffer.indexToCursorPos(cursor)[1]; + if(cursorPos < 0) { + self.scrollBar.currentState += cursorPos/diff; + } else if(cursorPos + 16 >= self.maxHeight - 2*border) { + self.scrollBar.currentState += (cursorPos + 16 - (self.maxHeight - 2*border))/diff; + } + } + } } pub fn render(self: *TextInput, pos: Vec2f, size: Vec2f, mousePosition: Vec2f) !void { @@ -373,18 +422,28 @@ pub fn render(self: *TextInput, pos: Vec2f, size: Vec2f, mousePosition: Vec2f) ! Button.shader.bind(); draw.setColor(0xff000000); draw.customShadedRect(Button.buttonUniforms, pos, size); + const oldTranslation = draw.setTranslation(pos); + defer draw.restoreTranslation(oldTranslation); + const oldClip = draw.setClip(size); + defer draw.restoreClip(oldClip); - try self.textBuffer.render(pos[0], pos[1], fontSize); + var textPos = Vec2f{border, border}; + if(self.textSize[1] > self.maxHeight - 2*border) { + const diff = self.textSize[1] - (self.maxHeight - 2*border); + textPos[1] -= diff*self.scrollBar.currentState; + try self.scrollBar.render(.{size[0] - self.scrollBarSize[0] - border, border}, self.scrollBarSize, mousePosition - pos); + } + try self.textBuffer.render(textPos[0], textPos[1], fontSize); if(self.pressed) { self.cursor = self.textBuffer.mousePosToIndex(mousePosition - pos, self.currentString.items.len); } if(self.cursor) |cursor| { + var cursorPos = textPos + self.textBuffer.indexToCursorPos(cursor); if(self.selectionStart) |selectionStart| { draw.setColor(0x440000ff); - try self.textBuffer.drawSelection(pos, @min(selectionStart, cursor), @max(selectionStart, cursor)); + try self.textBuffer.drawSelection(textPos, @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 ee06a24e..6ed6d4f6 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -10,6 +10,7 @@ const Vec2f = vec.Vec2f; const Button = @import("components/Button.zig"); const CheckBox = @import("components/CheckBox.zig"); +const ScrollBar = @import("components/ScrollBar.zig"); const Slider = @import("components/Slider.zig"); const TextInput = @import("components/TextInput.zig"); pub const GuiComponent = @import("GuiComponent.zig"); @@ -36,6 +37,7 @@ pub fn init(_allocator: Allocator) !void { try GuiWindow.__init(); try Button.__init(); try CheckBox.__init(); + try ScrollBar.__init(); try Slider.__init(); try TextInput.__init(); } @@ -50,6 +52,7 @@ pub fn deinit() void { GuiWindow.__deinit(); Button.__deinit(); CheckBox.__deinit(); + ScrollBar.__deinit(); Slider.__deinit(); TextInput.__deinit(); } diff --git a/src/gui/windows/change_name.zig b/src/gui/windows/change_name.zig index 43b1a7c0..4cc91565 100644 --- a/src/gui/windows/change_name.zig +++ b/src/gui/windows/change_name.zig @@ -30,7 +30,7 @@ const padding: f32 = 8; pub fn onOpen() Allocator.Error!void { var list = try VerticalList.init(); // TODO Please change your name bla bla - try list.add(try TextInput.init(.{0, 16}, 128, "gr da jkwa hfeka fuei \n ofuiewo\natg78o4ea74e8t\nz57 t4738qa0 47a80 t47803a t478aqv t487 5t478a0 tg478a09 t748ao t7489a rt4e5 okv5895 678v54vgvo6r z8or z578v rox74et8ys9otv 4z3789so z4oa9t z489saoyt z")); + try list.add(try TextInput.init(.{0, 16}, 128, 256, "gr da jkwa hfeka fuei \n ofuiewo\natg78o4ea74e8t\nz57 t4738qa0 47a80 t47803a t478aqv t487 5t478a0 tg478a09 t748ao t7489a rt4e5 okv5895 678v54vgvo6r z8or z578v rox74et8ys9otv 4z3789so z4oa9t z489saoyt z")); // TODO: Done button. components[0] = list.toComponent(.{padding, padding}); window.contentSize = components[0].size + @splat(2, @as(f32, 2*padding)); diff --git a/src/main.zig b/src/main.zig index 58fdecdc..02b6fdfb 100644 --- a/src/main.zig +++ b/src/main.zig @@ -421,12 +421,12 @@ pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{.thread_safe=false}){}; threadAllocator = gpa.allocator(); defer if(gpa.deinit()) { - @panic("Memory leak"); + std.log.err("Memory leak", .{}); }; var global_gpa = std.heap.GeneralPurposeAllocator(.{.thread_safe=true}){}; globalAllocator = global_gpa.allocator(); defer if(global_gpa.deinit()) { - @panic("Memory leak"); + std.log.err("Memory leak", .{}); }; // init logging.