diff --git a/src/app.zig b/src/app.zig index 94cc5af..67d2bed 100644 --- a/src/app.zig +++ b/src/app.zig @@ -925,6 +925,7 @@ pub const Project = struct { save_location: ?[]u8 = null, sample_rate: ?f64 = null, + notes: std.ArrayListUnmanaged(u8) = .{}, // TODO: How this to computer local settings, like appdata. Because this option shouldn't be project specific. show_rulers: bool = true, diff --git a/src/font-face.zig b/src/font-face.zig index 6abcf0f..bdf701b 100644 --- a/src/font-face.zig +++ b/src/font-face.zig @@ -5,7 +5,7 @@ const Allocator = std.mem.Allocator; const FontFace = @This(); font: rl.Font, -spacing: ?f32 = null, +spacing: ?f32 = 0, line_height: f32 = 1.4, pub const DrawTextContext = struct { diff --git a/src/rect-utils.zig b/src/rect-utils.zig index 6e610c2..8643ed0 100644 --- a/src/rect-utils.zig +++ b/src/rect-utils.zig @@ -190,6 +190,15 @@ pub fn initCentered(rect: Rect, width: f32, height: f32) Rect { return Rect.init(rect.x + unused_width / 2, rect.y + unused_height / 2, width, height); } +pub fn initVec2(pos: rl.Vector2, rect_size: rl.Vector2) Rect { + return Rect{ + .x = pos.x, + .y = pos.y, + .width = rect_size.x, + .height = rect_size.y + }; +} + pub fn aligned(rect: Rect, align_x: AlignX, align_y: AlignY) rl.Vector2 { const x = switch(align_x) { .left => rect.x, diff --git a/src/screens/main_screen.zig b/src/screens/main_screen.zig index 853f0bd..3b1ef42 100644 --- a/src/screens/main_screen.zig +++ b/src/screens/main_screen.zig @@ -22,6 +22,10 @@ const Id = App.Id; app: *App, view_controls: ViewControlsSystem, +modal: ?union(enum){ + view_protocol: Id, // View id + notes +} = null, // Protocol modal frequency_input: UI.TextInputStorage, @@ -31,6 +35,9 @@ protocol_graph_cache: Graph.RenderCache = .{}, preview_sample_list_id: App.Id, preview_samples_y_range: RangeF64 = RangeF64.init(0, 0), +// Notes modal +notes_storage: UI.TextInputStorage, + // Project settings sample_rate_input: UI.TextInputStorage, parsed_sample_rate: ?f64 = null, @@ -53,6 +60,7 @@ pub fn init(app: *App) !MainScreen { .frequency_input = UI.TextInputStorage.init(allocator), .amplitude_input = UI.TextInputStorage.init(allocator), .sample_rate_input = UI.TextInputStorage.init(allocator), + .notes_storage = UI.TextInputStorage.init(allocator), .view_controls = ViewControlsSystem.init(&app.project), .transform_inputs = transform_inputs, .preview_sample_list_id = try app.project.addSampleList(allocator) @@ -68,6 +76,7 @@ pub fn deinit(self: *MainScreen) void { self.frequency_input.deinit(); self.amplitude_input.deinit(); self.sample_rate_input.deinit(); + self.notes_storage.deinit(); for (self.transform_inputs) |input| { input.deinit(); } @@ -245,6 +254,37 @@ pub fn showProtocolModal(self: *MainScreen, view_id: Id) !void { _ = ui.signal(container); } +fn showNotesModal(self: *MainScreen) !void { + var ui = &self.app.ui; + + const container = ui.createBox(.{ + .key = ui.keyFromString("Notes modal"), + .background = srcery.black, + .size_x = UI.Sizing.initGrowUpTo(.{ .pixels = 400 }), + .size_y = UI.Sizing.initGrowUpTo(.{ .pixels = 600 }), + .layout_direction = .top_to_bottom, + .padding = UI.Padding.all(ui.rem(1.5)), + .flags = &.{ .clickable }, + .layout_gap = ui.rem(0.5) + }); + container.beginChildren(); + defer container.endChildren(); + defer _ = ui.signal(container); + + const label = ui.label("Notes", .{}); + label.font = .{ .variant = .regular_italic, .size = ui.rem(2) }; + label.alignment.x = .center; + label.size.x = UI.Sizing.initGrowFull(); + + _ = try ui.textInput(.{ + .key = ui.keyFromString("Notes"), + .storage = &self.notes_storage, + .size_x = UI.Sizing.initGrowFull(), + .size_y = UI.Sizing.initGrowFull(), + .single_line = false + }); +} + fn setProtocolErrorMessage(self: *MainScreen, comptime fmt: []const u8, args: anytype) !void { self.clearProtocolErrorMessage(); @@ -306,6 +346,10 @@ fn showProjectSettings(self: *MainScreen) !void { .value = &project.show_rulers, .label = "Ruler" }); + + if (ui.signal(ui.textButton("Open notes")).clicked()) { + self.modal = .notes; + } } fn showViewSettings(self: *MainScreen, view_id: Id) !void { @@ -689,10 +733,17 @@ pub fn tick(self: *MainScreen) !void { const root = ui.parentBox().?; root.layout_direction = .top_to_bottom; - const was_protocol_modal_open = self.view_controls.view_protocol_modal != null; + if (self.view_controls.view_protocol_modal) |view_protocol_modal| { + if (self.modal == null) { + self.modal = .{ .view_protocol = view_protocol_modal }; + } + self.view_controls.view_protocol_modal = null; + } + + const was_modal_open = self.modal != null; var maybe_modal_overlay: ?*UI.Box = null; - if (self.view_controls.view_protocol_modal) |view_id| { + if (self.modal != null) { const padding = UI.Padding.all(ui.rem(2)); const modal_overlay = ui.createBox(.{ .key = ui.keyFromString("Overlay"), @@ -712,10 +763,13 @@ pub fn tick(self: *MainScreen) !void { modal_overlay.beginChildren(); defer modal_overlay.endChildren(); - try self.showProtocolModal(view_id); + switch (self.modal.?) { + .view_protocol => |view_id| try self.showProtocolModal(view_id), + .notes => try self.showNotesModal() + } if (ui.signal(modal_overlay).clicked()) { - self.view_controls.view_protocol_modal = null; + self.modal = null; } maybe_modal_overlay = modal_overlay; @@ -785,8 +839,8 @@ pub fn tick(self: *MainScreen) !void { } if (ui.isKeyboardPressed(.key_escape)) { - if (self.view_controls.view_protocol_modal != null) { - self.view_controls.view_protocol_modal = null; + if (self.modal != null) { + self.modal = null; } else if (self.view_controls.view_fullscreen != null) { self.view_controls.view_fullscreen = null; } else if (self.view_controls.view_settings != null) { @@ -798,8 +852,8 @@ pub fn tick(self: *MainScreen) !void { } } - const is_protocol_modal_open = self.view_controls.view_protocol_modal != null; - if (!was_protocol_modal_open and is_protocol_modal_open) { + const is_modal_open = self.modal != null; + if (!was_modal_open and is_modal_open) { self.protocol_graph_cache.clear(); } } \ No newline at end of file diff --git a/src/ui.zig b/src/ui.zig index 6dda86b..ac8b546 100644 --- a/src/ui.zig +++ b/src/ui.zig @@ -403,6 +403,24 @@ pub const Padding = struct { .Y => self.top + self.bottom }; } + + pub fn apply(self: Padding, rect: Rect) Rect { + return Rect{ + .x = rect.x + self.left, + .y = rect.y + self.top, + .width = rect.width - self.left - self.right, + .height = rect.height - self.top - self.bottom + }; + } + + pub fn applyPosition(self: Padding, rect: Rect) Rect { + return Rect{ + .x = rect.x + self.left, + .y = rect.y + self.top, + .width = rect.width, + .height = rect.height + }; + } }; pub const Alignment = enum { @@ -1828,6 +1846,7 @@ fn drawBox(self: *UI, box: *Box, on_top_pass: ?bool) void { text_position.y += (available_height - text_size.y) * alignment_y_coeff; font_face.drawText(text, text_position, box.text_color); + // rl.drawRectangleLinesEx(rect_utils.initVec2(text_position, text_size), 1, rl.Color.magenta); } else { // TODO: Don't call `measureTextLines`, // Because in the end `measureText` will be called twice for each line @@ -2227,27 +2246,98 @@ pub const TextInputStorage = struct { } } - fn getCharOffsetX(self: *const TextInputStorage, font: FontFace, index: usize) f32 { - const text = self.buffer.items; - return font.measureWidth(text[0..index]); + fn countScalar(haystack: []const u8, needle: u8) usize { + var count: usize = 0; + var start_index: usize = 0; + while (std.mem.indexOfScalarPos(u8, haystack, start_index, needle)) |found| { + start_index = found + 1; + count += 1; + } + return count; } - fn getCharIndex(self: *const TextInputStorage, font: FontFace, x: f32) usize { - var measure_opts = FontFace.MeasureOptions{ - .up_to_width = x + self.shown_slice_start - }; - const before_size = font.measureTextEx(self.buffer.items, &measure_opts).x; + fn getLineAt(self: *const TextInputStorage, index: usize) [2]usize { + const text = self.buffer.items; - const index = measure_opts.last_codepoint_index; - - if (index+1 < self.buffer.items.len) { - const after_size = self.getCharOffsetX(font, index + 1); - if (@abs(before_size - x) > @abs(after_size - x)) { - return index + 1; - } + var line_start: usize = 0; + if (std.mem.lastIndexOfScalar(u8, text[0..index], '\n')) |newline| { + line_start = newline + 1; } - return index; + const line_end = std.mem.indexOfScalarPos(u8, text, line_start, '\n') orelse text.len; + + return .{ line_start, line_end }; + } + + pub fn getPositionAt(self: *const TextInputStorage, font: FontFace, index: usize) Vec2 { + const line_start, const line_end = self.getLineAt(index); + + const text = self.buffer.items; + const row: f32 = @floatFromInt(self.getLineIndexAt(line_end)); + + return Vec2.init( + font.measureWidth(text[line_start..index]), + row * font.getLineSize() + ); + } + + fn getLineByIndex(self: *const TextInputStorage, line_index: usize) ?[2]usize { + const text = self.buffer.items; + + var row: usize = 0; + var line_start: usize = 0; + while (true) { + const newline = std.mem.indexOfScalarPos(u8, text, line_start, '\n'); + const line_end = newline orelse text.len; + + if (row == line_index) { + return .{ line_start, line_end }; + } + + row += 1; + line_start = line_end+1; + if (newline == null) break; + } + + return null; + } + + fn getLineIndexAt(self: *const TextInputStorage, index: usize) usize { + const text = self.buffer.items; + return countScalar(text[0..index], '\n'); + } + + fn getIndexAt(self: *const TextInputStorage, font: FontFace, pos: Vec2) ?usize { + if (pos.y <= 0) { + return null; + } + + const text = self.buffer.items; + + if (self.getLineByIndex(@intFromFloat(@divFloor(pos.y, font.getLineSize())))) |line_range| { + const line_start, const line_end = line_range; + const line = text[line_start..line_end]; + + var measure_opts = FontFace.MeasureOptions{ + .up_to_width = pos.x + self.shown_slice_start + }; + const before_size = font.measureTextEx(line, &measure_opts).x; + + const index = measure_opts.last_codepoint_index; + + if (index+1 < line.len) { + const after_size = font.measureWidth(line[0..(index+1)]); + if (@abs(before_size - pos.x) > @abs(after_size - pos.x)) { + return line_start + index + 1; + } else { + return line_start + index; + } + } + + return line_end; + } + + return null; } fn nextJumpPoint(self: *const TextInputStorage, index: usize, step: isize) usize { @@ -2256,6 +2346,8 @@ pub const TextInputStorage = struct { const text = self.buffer.items; if (step > 0) { + if (index >= text.len) return index; + var prev_whitespace = std.ascii.isWhitespace(text[i]); while (true) { const cur_whitespace = std.ascii.isWhitespace(text[i]); @@ -2302,7 +2394,7 @@ pub const TextInputStorage = struct { return i; } - pub fn textLength(self: *TextInputStorage) []const u8 { + pub fn textSlice(self: *TextInputStorage) []const u8 { return self.buffer.items; } @@ -2331,18 +2423,43 @@ pub const TextInputStorage = struct { self.cursor_stop += text.len; } } + + fn getLowerCursor(self: *TextInputStorage) usize { + return @min(self.cursor_stop, self.cursor_start); + } + + fn getUpperCursor(self: *TextInputStorage) usize { + return @max(self.cursor_stop, self.cursor_start); + } + + fn moveCursorAlongX(self: *TextInputStorage, cursor: usize, step: isize) usize { + return @intCast(std.math.clamp( + @as(isize, @intCast(cursor)) + step, + 0, + @as(isize, @intCast(self.buffer.items.len)) + )); + } + + fn isChatAt(self: *TextInputStorage, cursor: usize, char: u8) bool { + const text = self.textSlice(); + if (cursor >= text.len) return false; + + return text[cursor] == char; + } }; pub const TextInputOptions = struct { key: Key, storage: *TextInputStorage, editable: bool = true, - width: f32 = 200, + size_x: ?Sizing = null, + size_y: ?Sizing = null, initial: ?[]const u8 = null, placeholder: ?[]const u8 = null, postfix: ?[]const u8 = null, - text_color: rl.Color = srcery.black + text_color: rl.Color = srcery.black, + single_line: bool = true }; pub const NumberInputOptions = struct { @@ -2350,7 +2467,7 @@ pub const NumberInputOptions = struct { storage: *TextInputStorage, invalid: bool = false, editable: bool = true, - width: f32 = 200, + width: ?f32 = null, display_scalar: ?f64 = null, @@ -2503,8 +2620,8 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void { const container = self.createBox(.{ .key = opts.key, - .size_x = Sizing.initGrowUpTo(.{ .pixels = opts.width }), - .size_y = Sizing.initFixed(Unit.initPixels(self.rem(1))), + .size_x = opts.size_x orelse Sizing.initGrowUpTo(.{ .pixels = 200 }), + .size_y = opts.size_y orelse Sizing.initFixed(Unit.initPixels(self.rem(1))), .flags = &.{ .clickable, .clip_view, .draggable }, .background = srcery.bright_white, .align_y = .center, @@ -2522,102 +2639,123 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void { try storage_text.appendSlice(opts.initial.?); } - const cursor_start_x = storage.getCharOffsetX(font, storage.cursor_start); - const cursor_stop_x = storage.getCharOffsetX(font, storage.cursor_stop); + const container_signal = self.signal(container); - { // Text visuals - var text_color = opts.text_color; - var text: []const u8 = storage_text.items; - if (opts.placeholder != null and text.len == 0) { - text = opts.placeholder.?; - text_color = text_color.alpha(0.6); - } + { // Visuals + { // Text visuals + var label_options = BoxOptions{ + .text_color = opts.text_color, + .text = storage_text.items, + .float_relative_to = container, + .align_x = .start + }; - const text_size = font.measureText(text); - const visible_text_width = container.persistent.size.x - container.padding.byAxis(.X); + var text: []const u8 = storage_text.items; + if (opts.placeholder != null and text.len == 0) { + text = opts.placeholder.?; + label_options.text_color = opts.text_color.alpha(0.6); + } + label_options.text = text; - const shown_window_size = @min(visible_text_width, text_size.x); - if (storage.shown_slice_end - storage.shown_slice_start != shown_window_size) { - const shown_slice_middle = (storage.shown_slice_end + storage.shown_slice_start) / 2; - storage.shown_slice_start = shown_slice_middle - shown_window_size/2; - storage.shown_slice_end = shown_slice_middle + shown_window_size/2; - } - if (storage.shown_slice_end > text_size.x) { - storage.shown_slice_end = shown_window_size; - storage.shown_slice_start = storage.shown_slice_end - shown_window_size; - } else if (storage.shown_slice_start < 0) { - storage.shown_slice_start = 0; - storage.shown_slice_end = shown_window_size; - } + const text_size = font.measureText(text); + const visible_text_width = container.persistent.size.x - container.padding.byAxis(.X); - if (cursor_stop_x > storage.shown_slice_end) { - storage.shown_slice_start = cursor_stop_x - shown_window_size; - storage.shown_slice_end = cursor_stop_x; - } - if (cursor_stop_x < storage.shown_slice_start) { - storage.shown_slice_start = cursor_stop_x; - storage.shown_slice_end = cursor_stop_x + shown_window_size; - } + const shown_window_size = @min(visible_text_width, text_size.x); + if (storage.shown_slice_end - storage.shown_slice_start != shown_window_size) { + const shown_slice_middle = (storage.shown_slice_end + storage.shown_slice_start) / 2; + storage.shown_slice_start = shown_slice_middle - shown_window_size/2; + storage.shown_slice_end = shown_slice_middle + shown_window_size/2; + } + if (storage.shown_slice_end > text_size.x) { + storage.shown_slice_end = shown_window_size; + storage.shown_slice_start = storage.shown_slice_end - shown_window_size; + } else if (storage.shown_slice_start < 0) { + storage.shown_slice_start = 0; + storage.shown_slice_end = shown_window_size; + } - const shown_text = self.createBox(.{ - .text_color = text_color, - .text = text, - .float_relative_to = container, - .float_rect = Rect{ - .x = container.padding.left - storage.shown_slice_start, + const stop_cursor_pos = storage.getPositionAt(font, storage.cursor_stop); + if (stop_cursor_pos.x > storage.shown_slice_end) { + storage.shown_slice_start = stop_cursor_pos.x - shown_window_size; + storage.shown_slice_end = stop_cursor_pos.x; + } + if (stop_cursor_pos.x < storage.shown_slice_start) { + storage.shown_slice_start = stop_cursor_pos.x; + storage.shown_slice_end = stop_cursor_pos.x + shown_window_size; + } + + label_options.float_rect = container.padding.apply(Rect{ + .x = -storage.shown_slice_start, .y = 0, .width = visible_text_width, .height = container.persistent.size.y - }, - .align_y = .center, - .align_x = .start - }); - if (opts.postfix) |postfix| { - shown_text.appendText(postfix); - } - } - - const container_signal = self.signal(container); - if (opts.editable and container_signal.hot) { - container.borders = UI.Borders.all(.{ - .color = srcery.red, - .size = 2 - }); - } - - // Text editing visuals - if (storage.editing) { - const blink_period = std.time.ns_per_s; - const blink = @mod(now - storage.last_pressed_at_ns, blink_period) < 0.5 * blink_period; - - const cursor_color = srcery.hard_black; - - if (storage.cursor_start == storage.cursor_stop and blink) { - _ = self.createBox(.{ - .background = cursor_color, - .float_relative_to = container, - .float_rect = Rect{ - .x = container.padding.left + cursor_start_x - storage.shown_slice_start, - .y = container.padding.top, - .width = 2, - .height = container.persistent.size.y - container.padding.byAxis(.Y) - }, }); + + if (opts.single_line) { + label_options.align_y = .center; + } else { + label_options.align_y = .start; + } + + const shown_text = self.createBox(label_options); + if (opts.postfix) |postfix| { + shown_text.appendText(postfix); + } } - if (storage.cursor_start != storage.cursor_stop) { - const lower_cursor_x = @min(cursor_start_x, cursor_stop_x); - const upper_cursor_x = @max(cursor_start_x, cursor_stop_x); + // Text editing visuals + if (storage.editing) { + const blink_period = std.time.ns_per_s; + const blink = @mod(now - storage.last_pressed_at_ns, blink_period) < 0.5 * blink_period; - _ = self.createBox(.{ - .background = cursor_color.alpha(0.25), - .float_relative_to = container, - .float_rect = Rect{ - .x = container.padding.left + lower_cursor_x - storage.shown_slice_start, - .y = container.padding.top, - .width = upper_cursor_x - lower_cursor_x, - .height = container.persistent.size.y - container.padding.byAxis(.Y) - }, + const cursor_color = srcery.hard_black; + + if (storage.cursor_start == storage.cursor_stop and blink) { + const cursor_width = 2; + + const cursor_pos = storage.getPositionAt(font, storage.cursor_start); + _ = self.createBox(.{ + .background = cursor_color, + .float_relative_to = container, + .float_rect = container.padding.applyPosition(Rect{ + .x = cursor_pos.x - storage.shown_slice_start, + .y = cursor_pos.y, + .width = cursor_width, + .height = font.getSize() + }), + }); + } + + if (storage.cursor_start != storage.cursor_stop) { + var cursor = storage.getLowerCursor(); + const upper_cursor = storage.getUpperCursor(); + + while (cursor < upper_cursor) { + _, const line_end = storage.getLineAt(cursor); + + const highlight_from = storage.getPositionAt(font, cursor); + const highlight_to_x = storage.getPositionAt(font, @min(line_end, upper_cursor)).x; + + _ = self.createBox(.{ + .background = cursor_color.alpha(0.25), + .float_relative_to = container, + .float_rect = container.padding.applyPosition(Rect{ + .x = highlight_from.x - storage.shown_slice_start, + .y = highlight_from.y, + .width = @max(highlight_to_x - highlight_from.x, 2), + .height = font.getLineSize() + }), + }); + + cursor = line_end+1; + } + } + } + + if (opts.editable and container_signal.hot) { + container.borders = UI.Borders.all(.{ + .color = srcery.red, + .size = 2 }); } } @@ -2638,33 +2776,29 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void { var no_blinking = false; { // Cursor movement controls - var move_cursor_dir: i32 = 0; + var move_cursor_dir_x: i32 = 0; + var move_cursor_dir_y: i32 = 0; if (self.isKeyboardPressedOrHeld(.key_left)) { - move_cursor_dir -= 1; + move_cursor_dir_x -= 1; } if (self.isKeyboardPressedOrHeld(.key_right)) { - move_cursor_dir += 1; + move_cursor_dir_x += 1; + } + if (self.isKeyboardPressedOrHeld(.key_up)) { + move_cursor_dir_y -= 1; + } + if (self.isKeyboardPressedOrHeld(.key_down)) { + move_cursor_dir_y += 1; } - if (move_cursor_dir != 0) { - if (shift) { - if (ctrl) { - storage.cursor_stop = storage.nextJumpPoint(storage.cursor_stop, move_cursor_dir); - } else { - const cursor_stop: isize = @intCast(storage.cursor_stop); - storage.cursor_stop = @intCast(std.math.clamp( - cursor_stop + move_cursor_dir, - 0, - @as(isize, @intCast(storage_text.items.len)) - )); - } + if (move_cursor_dir_x != 0 or move_cursor_dir_y != 0) { - } else { - if (storage.cursor_start != storage.cursor_stop) { + if (move_cursor_dir_x != 0) { + if (storage.cursor_start != storage.cursor_stop and !shift) { const lower = @min(storage.cursor_start, storage.cursor_stop); const upper = @max(storage.cursor_start, storage.cursor_stop); - if (move_cursor_dir < 0) { + if (move_cursor_dir_x < 0) { if (ctrl) { storage.cursor_start = storage.nextJumpPoint(lower, -1); } else { @@ -2680,23 +2814,38 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void { storage.cursor_stop = storage.cursor_start; } else { - var cursor = storage.cursor_start; - if (ctrl) { - cursor = storage.nextJumpPoint(cursor, move_cursor_dir); + storage.cursor_stop = storage.nextJumpPoint(storage.cursor_stop, move_cursor_dir_x); } else { - cursor = @intCast(std.math.clamp( - @as(isize, @intCast(cursor)) + move_cursor_dir, - 0, - @as(isize, @intCast(storage_text.items.len)) - )); + storage.cursor_stop = storage.moveCursorAlongX(storage.cursor_stop, move_cursor_dir_x); } - - storage.cursor_start = cursor; - storage.cursor_stop = cursor; } } + if (move_cursor_dir_y != 0) { + const line_bounds = storage.getLineAt(storage.cursor_stop); + const line = storage.getLineIndexAt(storage.cursor_stop); + + var next_line = line; + if (move_cursor_dir_y > 0) { + next_line += 1; + } + if (move_cursor_dir_y < 0 and next_line > 0) { + next_line -= 1; + } + + if (storage.getLineByIndex(next_line)) |next_line_bounds| { + const next_line_start, const next_line_stop = next_line_bounds; + const line_start, _ = line_bounds; + + storage.cursor_stop = @min(next_line_start + (storage.cursor_stop - line_start), next_line_stop); + } + } + + if (!shift) { + storage.cursor_start = storage.cursor_stop; + } + no_blinking = true; } } @@ -2706,16 +2855,16 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void { mouse.x -= container.padding.left; mouse.y -= container.padding.top; - const mouse_index = storage.getCharIndex(font, mouse.x); - - if (container_signal.flags.contains(.left_pressed)) { - storage.cursor_start = mouse_index; - storage.cursor_stop = mouse_index; - no_blinking = true; - } - if (container_signal.flags.contains(.left_dragging)) { - storage.cursor_stop = mouse_index; - no_blinking = true; + if (storage.getIndexAt(font, mouse)) |mouse_index| { + if (container_signal.flags.contains(.left_pressed)) { + storage.cursor_start = mouse_index; + storage.cursor_stop = mouse_index; + no_blinking = true; + } + if (container_signal.flags.contains(.left_dragging)) { + storage.cursor_stop = mouse_index; + no_blinking = true; + } } } @@ -2787,7 +2936,22 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void { storage.last_pressed_at_ns = now; } - if (self.isKeyboardPressed(.key_escape) or self.isKeyboardPressed(.key_enter)) { + if (self.isKeyboardPressed(.key_escape)) { + storage.editing = false; + } + + if (self.isKeyboardPressed(.key_enter)) { + if (opts.single_line) { + storage.editing = false; + } else { + if (storage.cursor_start != storage.cursor_stop) { + storage.deleteMany(storage.cursor_start, storage.cursor_stop); + } + try storage.insertSingle(storage.cursor_start, '\n'); + } + } + + if (opts.single_line and self.isKeyboardPressed(.key_enter)) { storage.editing = false; } @@ -2810,10 +2974,13 @@ pub fn numberInput(self: *UI, T: type, opts: NumberInputOptions) !?T { .text_color = opts.text_color, .placeholder = opts.placeholder, .editable = opts.editable, - .postfix = opts.postfix, - .width = opts.width + .postfix = opts.postfix }; + if (opts.width) |width| { + text_opts.size_x = Sizing.initGrowUpTo(.{ .pixels = width }); + } + const display_scalar = opts.display_scalar orelse 1; var is_invalid = opts.invalid;