add support for multiline text input
This commit is contained in:
parent
2545774575
commit
88212d001c
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
463
src/ui.zig
463
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;
|
||||
|
Loading…
Reference in New Issue
Block a user