Text can now be scrolled.

This commit is contained in:
IntegratedQuantum 2023-03-11 17:45:20 +01:00
parent 44eed8026d
commit b8f5b84322
8 changed files with 219 additions and 46 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 938 B

View File

@ -62,29 +62,34 @@ pub const draw = struct {
}
/// Returns the previous clip.
pub fn setClip(newClip: Vec4i) ?Vec4i {
pub fn setClip(clipRect: Vec2f) ?Vec4i {
var newClip = Vec4i {
@floatToInt(i32, translation[0]),
main.Window.height - @floatToInt(i32, translation[1] + clipRect[1]*scale),
@floatToInt(i32, clipRect[0]*scale),
@floatToInt(i32, clipRect[1]*scale),
};
if(clip) |oldClip| {
if (newClip[0] < oldClip[0]) {
newClip[2] -= oldClip[0] - newClip[0];
newClip[0] += oldClip[0] - newClip[0];
}
if (newClip[1] < oldClip[1]) {
newClip[3] -= oldClip[1] - newClip[1];
newClip[1] += oldClip[1] - newClip[1];
}
if (newClip[0] + newClip[2] > 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});

View File

@ -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,

View File

@ -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);
}

View File

@ -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});
}
}

View File

@ -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();
}

View File

@ -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));

View File

@ -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.