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,
|
save_location: ?[]u8 = null,
|
||||||
sample_rate: ?f64 = 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.
|
// TODO: How this to computer local settings, like appdata. Because this option shouldn't be project specific.
|
||||||
show_rulers: bool = true,
|
show_rulers: bool = true,
|
||||||
|
@ -5,7 +5,7 @@ const Allocator = std.mem.Allocator;
|
|||||||
const FontFace = @This();
|
const FontFace = @This();
|
||||||
|
|
||||||
font: rl.Font,
|
font: rl.Font,
|
||||||
spacing: ?f32 = null,
|
spacing: ?f32 = 0,
|
||||||
line_height: f32 = 1.4,
|
line_height: f32 = 1.4,
|
||||||
|
|
||||||
pub const DrawTextContext = struct {
|
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);
|
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 {
|
pub fn aligned(rect: Rect, align_x: AlignX, align_y: AlignY) rl.Vector2 {
|
||||||
const x = switch(align_x) {
|
const x = switch(align_x) {
|
||||||
.left => rect.x,
|
.left => rect.x,
|
||||||
|
@ -22,6 +22,10 @@ const Id = App.Id;
|
|||||||
|
|
||||||
app: *App,
|
app: *App,
|
||||||
view_controls: ViewControlsSystem,
|
view_controls: ViewControlsSystem,
|
||||||
|
modal: ?union(enum){
|
||||||
|
view_protocol: Id, // View id
|
||||||
|
notes
|
||||||
|
} = null,
|
||||||
|
|
||||||
// Protocol modal
|
// Protocol modal
|
||||||
frequency_input: UI.TextInputStorage,
|
frequency_input: UI.TextInputStorage,
|
||||||
@ -31,6 +35,9 @@ protocol_graph_cache: Graph.RenderCache = .{},
|
|||||||
preview_sample_list_id: App.Id,
|
preview_sample_list_id: App.Id,
|
||||||
preview_samples_y_range: RangeF64 = RangeF64.init(0, 0),
|
preview_samples_y_range: RangeF64 = RangeF64.init(0, 0),
|
||||||
|
|
||||||
|
// Notes modal
|
||||||
|
notes_storage: UI.TextInputStorage,
|
||||||
|
|
||||||
// Project settings
|
// Project settings
|
||||||
sample_rate_input: UI.TextInputStorage,
|
sample_rate_input: UI.TextInputStorage,
|
||||||
parsed_sample_rate: ?f64 = null,
|
parsed_sample_rate: ?f64 = null,
|
||||||
@ -53,6 +60,7 @@ pub fn init(app: *App) !MainScreen {
|
|||||||
.frequency_input = UI.TextInputStorage.init(allocator),
|
.frequency_input = UI.TextInputStorage.init(allocator),
|
||||||
.amplitude_input = UI.TextInputStorage.init(allocator),
|
.amplitude_input = UI.TextInputStorage.init(allocator),
|
||||||
.sample_rate_input = UI.TextInputStorage.init(allocator),
|
.sample_rate_input = UI.TextInputStorage.init(allocator),
|
||||||
|
.notes_storage = UI.TextInputStorage.init(allocator),
|
||||||
.view_controls = ViewControlsSystem.init(&app.project),
|
.view_controls = ViewControlsSystem.init(&app.project),
|
||||||
.transform_inputs = transform_inputs,
|
.transform_inputs = transform_inputs,
|
||||||
.preview_sample_list_id = try app.project.addSampleList(allocator)
|
.preview_sample_list_id = try app.project.addSampleList(allocator)
|
||||||
@ -68,6 +76,7 @@ pub fn deinit(self: *MainScreen) void {
|
|||||||
self.frequency_input.deinit();
|
self.frequency_input.deinit();
|
||||||
self.amplitude_input.deinit();
|
self.amplitude_input.deinit();
|
||||||
self.sample_rate_input.deinit();
|
self.sample_rate_input.deinit();
|
||||||
|
self.notes_storage.deinit();
|
||||||
for (self.transform_inputs) |input| {
|
for (self.transform_inputs) |input| {
|
||||||
input.deinit();
|
input.deinit();
|
||||||
}
|
}
|
||||||
@ -245,6 +254,37 @@ pub fn showProtocolModal(self: *MainScreen, view_id: Id) !void {
|
|||||||
_ = ui.signal(container);
|
_ = 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 {
|
fn setProtocolErrorMessage(self: *MainScreen, comptime fmt: []const u8, args: anytype) !void {
|
||||||
self.clearProtocolErrorMessage();
|
self.clearProtocolErrorMessage();
|
||||||
|
|
||||||
@ -306,6 +346,10 @@ fn showProjectSettings(self: *MainScreen) !void {
|
|||||||
.value = &project.show_rulers,
|
.value = &project.show_rulers,
|
||||||
.label = "Ruler"
|
.label = "Ruler"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (ui.signal(ui.textButton("Open notes")).clicked()) {
|
||||||
|
self.modal = .notes;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn showViewSettings(self: *MainScreen, view_id: Id) !void {
|
fn showViewSettings(self: *MainScreen, view_id: Id) !void {
|
||||||
@ -689,10 +733,17 @@ pub fn tick(self: *MainScreen) !void {
|
|||||||
const root = ui.parentBox().?;
|
const root = ui.parentBox().?;
|
||||||
root.layout_direction = .top_to_bottom;
|
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;
|
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 padding = UI.Padding.all(ui.rem(2));
|
||||||
const modal_overlay = ui.createBox(.{
|
const modal_overlay = ui.createBox(.{
|
||||||
.key = ui.keyFromString("Overlay"),
|
.key = ui.keyFromString("Overlay"),
|
||||||
@ -712,10 +763,13 @@ pub fn tick(self: *MainScreen) !void {
|
|||||||
modal_overlay.beginChildren();
|
modal_overlay.beginChildren();
|
||||||
defer modal_overlay.endChildren();
|
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()) {
|
if (ui.signal(modal_overlay).clicked()) {
|
||||||
self.view_controls.view_protocol_modal = null;
|
self.modal = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
maybe_modal_overlay = modal_overlay;
|
maybe_modal_overlay = modal_overlay;
|
||||||
@ -785,8 +839,8 @@ pub fn tick(self: *MainScreen) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ui.isKeyboardPressed(.key_escape)) {
|
if (ui.isKeyboardPressed(.key_escape)) {
|
||||||
if (self.view_controls.view_protocol_modal != null) {
|
if (self.modal != null) {
|
||||||
self.view_controls.view_protocol_modal = null;
|
self.modal = null;
|
||||||
} else if (self.view_controls.view_fullscreen != null) {
|
} else if (self.view_controls.view_fullscreen != null) {
|
||||||
self.view_controls.view_fullscreen = null;
|
self.view_controls.view_fullscreen = null;
|
||||||
} else if (self.view_controls.view_settings != 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;
|
const is_modal_open = self.modal != null;
|
||||||
if (!was_protocol_modal_open and is_protocol_modal_open) {
|
if (!was_modal_open and is_modal_open) {
|
||||||
self.protocol_graph_cache.clear();
|
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
|
.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 {
|
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;
|
text_position.y += (available_height - text_size.y) * alignment_y_coeff;
|
||||||
|
|
||||||
font_face.drawText(text, text_position, box.text_color);
|
font_face.drawText(text, text_position, box.text_color);
|
||||||
|
// rl.drawRectangleLinesEx(rect_utils.initVec2(text_position, text_size), 1, rl.Color.magenta);
|
||||||
} else {
|
} else {
|
||||||
// TODO: Don't call `measureTextLines`,
|
// TODO: Don't call `measureTextLines`,
|
||||||
// Because in the end `measureText` will be called twice for each line
|
// 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 {
|
fn countScalar(haystack: []const u8, needle: u8) usize {
|
||||||
const text = self.buffer.items;
|
var count: usize = 0;
|
||||||
return font.measureWidth(text[0..index]);
|
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 {
|
fn getLineAt(self: *const TextInputStorage, index: usize) [2]usize {
|
||||||
var measure_opts = FontFace.MeasureOptions{
|
const text = self.buffer.items;
|
||||||
.up_to_width = x + self.shown_slice_start
|
|
||||||
};
|
|
||||||
const before_size = font.measureTextEx(self.buffer.items, &measure_opts).x;
|
|
||||||
|
|
||||||
const index = measure_opts.last_codepoint_index;
|
var line_start: usize = 0;
|
||||||
|
if (std.mem.lastIndexOfScalar(u8, text[0..index], '\n')) |newline| {
|
||||||
if (index+1 < self.buffer.items.len) {
|
line_start = newline + 1;
|
||||||
const after_size = self.getCharOffsetX(font, index + 1);
|
|
||||||
if (@abs(before_size - x) > @abs(after_size - x)) {
|
|
||||||
return index + 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 {
|
fn nextJumpPoint(self: *const TextInputStorage, index: usize, step: isize) usize {
|
||||||
@ -2256,6 +2346,8 @@ pub const TextInputStorage = struct {
|
|||||||
const text = self.buffer.items;
|
const text = self.buffer.items;
|
||||||
|
|
||||||
if (step > 0) {
|
if (step > 0) {
|
||||||
|
if (index >= text.len) return index;
|
||||||
|
|
||||||
var prev_whitespace = std.ascii.isWhitespace(text[i]);
|
var prev_whitespace = std.ascii.isWhitespace(text[i]);
|
||||||
while (true) {
|
while (true) {
|
||||||
const cur_whitespace = std.ascii.isWhitespace(text[i]);
|
const cur_whitespace = std.ascii.isWhitespace(text[i]);
|
||||||
@ -2302,7 +2394,7 @@ pub const TextInputStorage = struct {
|
|||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn textLength(self: *TextInputStorage) []const u8 {
|
pub fn textSlice(self: *TextInputStorage) []const u8 {
|
||||||
return self.buffer.items;
|
return self.buffer.items;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2331,18 +2423,43 @@ pub const TextInputStorage = struct {
|
|||||||
self.cursor_stop += text.len;
|
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 {
|
pub const TextInputOptions = struct {
|
||||||
key: Key,
|
key: Key,
|
||||||
storage: *TextInputStorage,
|
storage: *TextInputStorage,
|
||||||
editable: bool = true,
|
editable: bool = true,
|
||||||
width: f32 = 200,
|
size_x: ?Sizing = null,
|
||||||
|
size_y: ?Sizing = null,
|
||||||
|
|
||||||
initial: ?[]const u8 = null,
|
initial: ?[]const u8 = null,
|
||||||
placeholder: ?[]const u8 = null,
|
placeholder: ?[]const u8 = null,
|
||||||
postfix: ?[]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 {
|
pub const NumberInputOptions = struct {
|
||||||
@ -2350,7 +2467,7 @@ pub const NumberInputOptions = struct {
|
|||||||
storage: *TextInputStorage,
|
storage: *TextInputStorage,
|
||||||
invalid: bool = false,
|
invalid: bool = false,
|
||||||
editable: bool = true,
|
editable: bool = true,
|
||||||
width: f32 = 200,
|
width: ?f32 = null,
|
||||||
|
|
||||||
display_scalar: ?f64 = null,
|
display_scalar: ?f64 = null,
|
||||||
|
|
||||||
@ -2503,8 +2620,8 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void {
|
|||||||
|
|
||||||
const container = self.createBox(.{
|
const container = self.createBox(.{
|
||||||
.key = opts.key,
|
.key = opts.key,
|
||||||
.size_x = Sizing.initGrowUpTo(.{ .pixels = opts.width }),
|
.size_x = opts.size_x orelse Sizing.initGrowUpTo(.{ .pixels = 200 }),
|
||||||
.size_y = Sizing.initFixed(Unit.initPixels(self.rem(1))),
|
.size_y = opts.size_y orelse Sizing.initFixed(Unit.initPixels(self.rem(1))),
|
||||||
.flags = &.{ .clickable, .clip_view, .draggable },
|
.flags = &.{ .clickable, .clip_view, .draggable },
|
||||||
.background = srcery.bright_white,
|
.background = srcery.bright_white,
|
||||||
.align_y = .center,
|
.align_y = .center,
|
||||||
@ -2522,102 +2639,123 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void {
|
|||||||
try storage_text.appendSlice(opts.initial.?);
|
try storage_text.appendSlice(opts.initial.?);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cursor_start_x = storage.getCharOffsetX(font, storage.cursor_start);
|
const container_signal = self.signal(container);
|
||||||
const cursor_stop_x = storage.getCharOffsetX(font, storage.cursor_stop);
|
|
||||||
|
|
||||||
{ // Text visuals
|
{ // Visuals
|
||||||
var text_color = opts.text_color;
|
{ // Text visuals
|
||||||
var text: []const u8 = storage_text.items;
|
var label_options = BoxOptions{
|
||||||
if (opts.placeholder != null and text.len == 0) {
|
.text_color = opts.text_color,
|
||||||
text = opts.placeholder.?;
|
.text = storage_text.items,
|
||||||
text_color = text_color.alpha(0.6);
|
.float_relative_to = container,
|
||||||
}
|
.align_x = .start
|
||||||
|
};
|
||||||
|
|
||||||
const text_size = font.measureText(text);
|
var text: []const u8 = storage_text.items;
|
||||||
const visible_text_width = container.persistent.size.x - container.padding.byAxis(.X);
|
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);
|
const text_size = font.measureText(text);
|
||||||
if (storage.shown_slice_end - storage.shown_slice_start != shown_window_size) {
|
const visible_text_width = container.persistent.size.x - container.padding.byAxis(.X);
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cursor_stop_x > storage.shown_slice_end) {
|
const shown_window_size = @min(visible_text_width, text_size.x);
|
||||||
storage.shown_slice_start = cursor_stop_x - shown_window_size;
|
if (storage.shown_slice_end - storage.shown_slice_start != shown_window_size) {
|
||||||
storage.shown_slice_end = cursor_stop_x;
|
const shown_slice_middle = (storage.shown_slice_end + storage.shown_slice_start) / 2;
|
||||||
}
|
storage.shown_slice_start = shown_slice_middle - shown_window_size/2;
|
||||||
if (cursor_stop_x < storage.shown_slice_start) {
|
storage.shown_slice_end = shown_slice_middle + shown_window_size/2;
|
||||||
storage.shown_slice_start = cursor_stop_x;
|
}
|
||||||
storage.shown_slice_end = cursor_stop_x + shown_window_size;
|
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(.{
|
const stop_cursor_pos = storage.getPositionAt(font, storage.cursor_stop);
|
||||||
.text_color = text_color,
|
if (stop_cursor_pos.x > storage.shown_slice_end) {
|
||||||
.text = text,
|
storage.shown_slice_start = stop_cursor_pos.x - shown_window_size;
|
||||||
.float_relative_to = container,
|
storage.shown_slice_end = stop_cursor_pos.x;
|
||||||
.float_rect = Rect{
|
}
|
||||||
.x = container.padding.left - storage.shown_slice_start,
|
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,
|
.y = 0,
|
||||||
.width = visible_text_width,
|
.width = visible_text_width,
|
||||||
.height = container.persistent.size.y
|
.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) {
|
// Text editing visuals
|
||||||
const lower_cursor_x = @min(cursor_start_x, cursor_stop_x);
|
if (storage.editing) {
|
||||||
const upper_cursor_x = @max(cursor_start_x, cursor_stop_x);
|
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(.{
|
const cursor_color = srcery.hard_black;
|
||||||
.background = cursor_color.alpha(0.25),
|
|
||||||
.float_relative_to = container,
|
if (storage.cursor_start == storage.cursor_stop and blink) {
|
||||||
.float_rect = Rect{
|
const cursor_width = 2;
|
||||||
.x = container.padding.left + lower_cursor_x - storage.shown_slice_start,
|
|
||||||
.y = container.padding.top,
|
const cursor_pos = storage.getPositionAt(font, storage.cursor_start);
|
||||||
.width = upper_cursor_x - lower_cursor_x,
|
_ = self.createBox(.{
|
||||||
.height = container.persistent.size.y - container.padding.byAxis(.Y)
|
.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;
|
var no_blinking = false;
|
||||||
|
|
||||||
{ // Cursor movement controls
|
{ // 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)) {
|
if (self.isKeyboardPressedOrHeld(.key_left)) {
|
||||||
move_cursor_dir -= 1;
|
move_cursor_dir_x -= 1;
|
||||||
}
|
}
|
||||||
if (self.isKeyboardPressedOrHeld(.key_right)) {
|
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 (move_cursor_dir_x != 0 or move_cursor_dir_y != 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))
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
if (move_cursor_dir_x != 0) {
|
||||||
if (storage.cursor_start != storage.cursor_stop) {
|
if (storage.cursor_start != storage.cursor_stop and !shift) {
|
||||||
const lower = @min(storage.cursor_start, storage.cursor_stop);
|
const lower = @min(storage.cursor_start, storage.cursor_stop);
|
||||||
const upper = @max(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) {
|
if (ctrl) {
|
||||||
storage.cursor_start = storage.nextJumpPoint(lower, -1);
|
storage.cursor_start = storage.nextJumpPoint(lower, -1);
|
||||||
} else {
|
} else {
|
||||||
@ -2680,23 +2814,38 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void {
|
|||||||
storage.cursor_stop = storage.cursor_start;
|
storage.cursor_stop = storage.cursor_start;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
var cursor = storage.cursor_start;
|
|
||||||
|
|
||||||
if (ctrl) {
|
if (ctrl) {
|
||||||
cursor = storage.nextJumpPoint(cursor, move_cursor_dir);
|
storage.cursor_stop = storage.nextJumpPoint(storage.cursor_stop, move_cursor_dir_x);
|
||||||
} else {
|
} else {
|
||||||
cursor = @intCast(std.math.clamp(
|
storage.cursor_stop = storage.moveCursorAlongX(storage.cursor_stop, move_cursor_dir_x);
|
||||||
@as(isize, @intCast(cursor)) + move_cursor_dir,
|
|
||||||
0,
|
|
||||||
@as(isize, @intCast(storage_text.items.len))
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
no_blinking = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2706,16 +2855,16 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void {
|
|||||||
mouse.x -= container.padding.left;
|
mouse.x -= container.padding.left;
|
||||||
mouse.y -= container.padding.top;
|
mouse.y -= container.padding.top;
|
||||||
|
|
||||||
const mouse_index = storage.getCharIndex(font, mouse.x);
|
if (storage.getIndexAt(font, mouse)) |mouse_index| {
|
||||||
|
if (container_signal.flags.contains(.left_pressed)) {
|
||||||
if (container_signal.flags.contains(.left_pressed)) {
|
storage.cursor_start = mouse_index;
|
||||||
storage.cursor_start = mouse_index;
|
storage.cursor_stop = mouse_index;
|
||||||
storage.cursor_stop = mouse_index;
|
no_blinking = true;
|
||||||
no_blinking = true;
|
}
|
||||||
}
|
if (container_signal.flags.contains(.left_dragging)) {
|
||||||
if (container_signal.flags.contains(.left_dragging)) {
|
storage.cursor_stop = mouse_index;
|
||||||
storage.cursor_stop = mouse_index;
|
no_blinking = true;
|
||||||
no_blinking = true;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2787,7 +2936,22 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void {
|
|||||||
storage.last_pressed_at_ns = now;
|
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;
|
storage.editing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2810,10 +2974,13 @@ pub fn numberInput(self: *UI, T: type, opts: NumberInputOptions) !?T {
|
|||||||
.text_color = opts.text_color,
|
.text_color = opts.text_color,
|
||||||
.placeholder = opts.placeholder,
|
.placeholder = opts.placeholder,
|
||||||
.editable = opts.editable,
|
.editable = opts.editable,
|
||||||
.postfix = opts.postfix,
|
.postfix = opts.postfix
|
||||||
.width = opts.width
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (opts.width) |width| {
|
||||||
|
text_opts.size_x = Sizing.initGrowUpTo(.{ .pixels = width });
|
||||||
|
}
|
||||||
|
|
||||||
const display_scalar = opts.display_scalar orelse 1;
|
const display_scalar = opts.display_scalar orelse 1;
|
||||||
|
|
||||||
var is_invalid = opts.invalid;
|
var is_invalid = opts.invalid;
|
||||||
|
Loading…
Reference in New Issue
Block a user