add support for multiline text input

This commit is contained in:
Rokas Puzonas 2025-05-09 01:22:14 +03:00
parent 2545774575
commit 88212d001c
5 changed files with 388 additions and 157 deletions

View File

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

View File

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

View File

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

View File

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

View File

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