From 23c4b994556fc7272787f2af487781c5df32afef Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Tue, 8 Apr 2025 21:36:15 +0300 Subject: [PATCH] add sample rate input --- src/app.zig | 74 +++++++++++++++++-------- src/screens/main_screen.zig | 84 +++++++++++++++++++++++++--- src/ui.zig | 106 +++++++++++++++++++++++++++++++----- 3 files changed, 218 insertions(+), 46 deletions(-) diff --git a/src/app.zig b/src/app.zig index 4886561..3083f8d 100644 --- a/src/app.zig +++ b/src/app.zig @@ -292,6 +292,14 @@ pub const Project = struct { } pub fn getDefaultSampleRate(self: *Project) ?f64 { + if (self.getAllowedSampleRates()) |range| { + return range.upper; + } else { + return null; + } + } + + pub fn getAllowedSampleRates(self: *Project) ?RangeF64 { var result_range: ?RangeF64 = null; var channel_iter = self.channels.iterator(); @@ -308,11 +316,7 @@ pub const Project = struct { } } - if (result_range) |r| { - return r.upper; - } else { - return null; - } + return result_range; } pub fn deinit(self: *Project, allocator: Allocator) void { @@ -561,6 +565,7 @@ pub const CollectionTask = struct { allocator: Allocator, ui: UI, +double_pass_ui: bool = true, should_close: bool = false, ni_daq_api: ?NIDaq.Api = null, ni_daq: ?NIDaq = null, @@ -586,7 +591,7 @@ pub fn init(self: *App, allocator: Allocator) !void { .main_screen = undefined, .collection_thread = undefined }; - self.main_screen = try MainScreen.init(self); + try self.initUI(); if (NIDaq.Api.init()) |ni_daq_api| { self.ni_daq_api = ni_daq_api; @@ -624,8 +629,7 @@ pub fn deinit(self: *App) void { self.collection_condition.signal(); self.collection_thread.join(); - self.ui.deinit(); - self.main_screen.deinit(); + self.deinitUI(); if (self.ni_daq) |*ni_daq| { ni_daq.deinit(self.allocator); @@ -636,12 +640,24 @@ pub fn deinit(self: *App) void { } } -pub fn deinitProject(self: *App) void { +fn deinitProject(self: *App) void { self.stopCollection(); self.project.deinit(self.allocator); } -pub fn loadProject(self: *App) !void { +fn initUI(self: *App) !void { + self.ui = UI.init(self.allocator); + self.main_screen = try MainScreen.init(self); + self.screen = .main; + self.double_pass_ui = true; +} + +fn deinitUI(self: *App) void { + self.ui.deinit(); + self.main_screen.deinit(); +} + +fn loadProject(self: *App) !void { const save_location = self.project.save_location orelse return error.MissingSaveLocation; log.info("Load project from: {s}", .{save_location}); @@ -672,9 +688,12 @@ pub fn loadProject(self: *App) !void { log.err("Failed to load view: {}", .{ e }); }; } + + self.deinitUI(); + self.initUI() catch @panic("Failed to initialize UI, can't recover"); } -pub fn saveProject(self: *App) !void { +fn saveProject(self: *App) !void { const save_location = self.project.save_location orelse return error.MissingSaveLocation; log.info("Save project to: {s}", .{save_location}); @@ -682,6 +701,20 @@ pub fn saveProject(self: *App) !void { try self.project.save(); } +pub fn showUI(self: *App) !void { + var ui = &self.ui; + + ui.begin(); + defer ui.end(); + + switch (self.screen) { + .main => try self.main_screen.tick(), + .add_channels => { + self.screen = .main; + } + } +} + pub fn tick(self: *App) !void { var ui = &self.ui; self.command_queue.len = 0; @@ -728,17 +761,17 @@ pub fn tick(self: *App) !void { } } - ui.begin(); - defer ui.end(); - - switch (self.screen) { - .main => try self.main_screen.tick(), - .add_channels => { - self.screen = .main; - } + if (rl.isWindowResized() or self.double_pass_ui) { + try self.showUI(); + self.double_pass_ui = false; } + + try self.showUI(); } + rl.clearBackground(srcery.black); + ui.draw(); + for (self.command_queue.constSlice()) |command| { switch (command) { .start_collection => { @@ -775,9 +808,6 @@ pub fn tick(self: *App) !void { } } } - - rl.clearBackground(srcery.black); - ui.draw(); } pub fn pushCommand(self: *App, command: Command) void { diff --git a/src/screens/main_screen.zig b/src/screens/main_screen.zig index 3a53ced..8f651f9 100644 --- a/src/screens/main_screen.zig +++ b/src/screens/main_screen.zig @@ -46,6 +46,8 @@ view_undo_stack: std.BoundedArray(ViewCommand, 100) = .{}, protocol_modal: ?Id = null, frequency_input: UI.TextInputStorage, amplitude_input: UI.TextInputStorage, +sample_rate_input: UI.TextInputStorage, +parsed_sample_rate: ?f64 = null, protocol_error_message: ?[]const u8 = null, protocol_graph_cache: Graph.Cache = .{}, preview_samples: std.ArrayListUnmanaged(f64) = .{}, @@ -58,6 +60,7 @@ pub fn init(app: *App) !MainScreen { .app = app, .frequency_input = UI.TextInputStorage.init(allocator), .amplitude_input = UI.TextInputStorage.init(allocator), + .sample_rate_input = UI.TextInputStorage.init(allocator) }; try self.frequency_input.setText("10"); @@ -71,6 +74,7 @@ pub fn deinit(self: *MainScreen) void { self.frequency_input.deinit(); self.amplitude_input.deinit(); + self.sample_rate_input.deinit(); self.preview_samples.clearAndFree(allocator); self.clearProtocolErrorMessage(); @@ -828,7 +832,10 @@ pub fn showProtocolModal(self: *MainScreen, channel_id: Id) !void { label_box.size.y = UI.Sizing.initGrowFull(); label_box.alignment.y = .center; - try ui.textInput(ui.keyFromString("Text input"), text_input_storage); + try ui.textInput(.{ + .key = ui.keyFromString("Text input"), + .storage = text_input_storage + }); any_input_modified = any_input_modified or text_input_storage.modified; } @@ -918,18 +925,57 @@ fn clearProtocolErrorMessage(self: *MainScreen) void { self.protocol_error_message = null; } -pub fn tick(self: *MainScreen) !void { +pub fn showSidePanel(self: *MainScreen) !void { var ui = &self.app.ui; + const frame_allocator = ui.frameAllocator(); - if (ui.isKeyboardPressed(.key_escape)) { - if (self.protocol_modal != null) { - self.closeModal(); - } else if (self.fullscreen_view != null) { - self.fullscreen_view = null; - } else { - self.app.should_close = true; + const container = ui.createBox(.{ + .size_x = UI.Sizing.initGrowUpTo(.{ .parent_percent = 0.2 }), + .size_y = UI.Sizing.initGrowFull(), + .borders = .{ + .right = .{ .color = srcery.hard_black, .size = 4 } + }, + .layout_direction = .top_to_bottom, + .padding = UI.Padding.all(ui.rem(1)) + }); + container.beginChildren(); + defer container.endChildren(); + + const project = &self.app.project; + + { + var placeholder: ?[]const u8 = null; + if (project.getDefaultSampleRate()) |sample_rate| { + placeholder = try std.fmt.allocPrint(frame_allocator, "{d}", .{ sample_rate }); + } + + var initial: ?[]const u8 = null; + if (project.sample_rate) |sample_rate| { + initial = try std.fmt.allocPrint(frame_allocator, "{d}", .{ sample_rate }); + } + + _ = ui.label("Sample rate", .{}); + self.parsed_sample_rate = try ui.numberInput(f64, .{ + .key = ui.keyFromString("Sample rate input"), + .storage = &self.sample_rate_input, + .placeholder = placeholder, + .initial = initial, + .invalid = self.parsed_sample_rate != project.sample_rate + }); + project.sample_rate = self.parsed_sample_rate; + + if (project.getAllowedSampleRates()) |allowed_sample_rates| { + if (project.sample_rate) |sample_rate| { + if (!allowed_sample_rates.hasInclusive(sample_rate)) { + project.sample_rate = null; + } + } } } +} + +pub fn tick(self: *MainScreen) !void { + var ui = &self.app.ui; if (ui.isCtrlDown() and ui.isKeyboardPressed(.key_z)) { self.undoLastMoveCommand(); @@ -1016,6 +1062,16 @@ pub fn tick(self: *MainScreen) !void { try self.showView(view_id, UI.Sizing.initGrowFull()); } else { + const container = ui.createBox(.{ + .size_x = UI.Sizing.initGrowFull(), + .size_y = UI.Sizing.initGrowFull(), + .layout_direction = .left_to_right + }); + container.beginChildren(); + defer container.endChildren(); + + try self.showSidePanel(); + const scroll_area = ui.beginScrollbar(ui.keyFromString("Channels")); defer ui.endScrollbar(); scroll_area.layout_direction = .top_to_bottom; @@ -1055,4 +1111,14 @@ pub fn tick(self: *MainScreen) !void { if (maybe_modal_overlay) |modal_overlay| { root.bringChildToTop(modal_overlay); } + + if (ui.isKeyboardPressed(.key_escape)) { + if (self.protocol_modal != null) { + self.closeModal(); + } else if (self.fullscreen_view != null) { + self.fullscreen_view = null; + } else { + self.app.should_close = true; + } + } } \ No newline at end of file diff --git a/src/ui.zig b/src/ui.zig index 0ac175e..07ca0a5 100644 --- a/src/ui.zig +++ b/src/ui.zig @@ -113,6 +113,7 @@ pub const Signal = struct { active: bool = false, shift_modifier: bool = false, ctrl_modifier: bool = false, + clicked_outside: bool = false, pub fn clicked(self: Signal) bool { return self.flags.contains(.left_clicked) or self.flags.contains(.right_clicked) or self.flags.contains(.middle_clicked); @@ -479,6 +480,7 @@ pub const Box = struct { ui: *UI, allocator: std.mem.Allocator, + created: bool = false, flags: Flags, background: ?rl.Color, @@ -493,6 +495,7 @@ pub const Box = struct { padding: Padding, font: Assets.FontId, text_color: rl.Color, + // TODO: Add option to specify where the border is drawn: outside, inside, center. borders: Borders, text: ?[]u8, hot_cursor: ?rl.MouseCursor, @@ -816,12 +819,12 @@ pub fn deinit(self: *UI) void { self.arenas[1].deinit(); } -pub fn frame_arena(self: *UI) *std.heap.ArenaAllocator { +pub fn frameArena(self: *UI) *std.heap.ArenaAllocator { return &self.arenas[@mod(self.frame_index, 2)]; } pub fn frameAllocator(self: *UI) std.mem.Allocator { - return self.frame_arena().allocator(); + return self.frameArena().allocator(); } pub fn pullOsEvents(self: *UI) void { @@ -902,7 +905,7 @@ pub fn begin(self: *UI) void { } self.frame_index += 1; - _ = self.frame_arena().reset(.retain_capacity); + _ = self.frameArena().reset(.retain_capacity); self.pushFont(default_font); @@ -1452,6 +1455,7 @@ pub fn createBox(self: *UI, opts: BoxOptions) *Box { var box_index: ?BoxIndex = null; var key = opts.key orelse Key.initNil(); var persistent = Box.Persistent{}; + var created = false; if (!key.isNil()) { if (self.getBoxIndexByKey(key)) |last_frame_box_index| { @@ -1469,6 +1473,7 @@ pub fn createBox(self: *UI, opts: BoxOptions) *Box { if (box_index == null) { box = self.boxes.addOneAssumeCapacity(); box_index = self.boxes.len - 1; + created = true; } var size = Sizing2{ @@ -1503,6 +1508,7 @@ pub fn createBox(self: *UI, opts: BoxOptions) *Box { box.* = Box{ .ui = self, .allocator = self.frameAllocator(), + .created = created, .persistent = persistent, .flags = flags, @@ -1897,6 +1903,10 @@ pub fn signal(self: *UI, box: *Box) Signal { taken = true; } + if (event == .mouse_released and clickable and !is_mouse_inside) { + result.clicked_outside = true; + } + if (event == .mouse_scroll and is_mouse_inside and scrollable) { result.scroll = event.mouse_scroll; result.flags.insert(.scrolled); @@ -2193,8 +2203,24 @@ pub const TextInputStorage = struct { } }; -pub const TextInputResult = struct { - changed: bool = false +pub const TextInputOptions = struct { + key: Key, + storage: *TextInputStorage, + + initial: ?[]const u8 = null, + placeholder: ?[]const u8 = null, + text_color: rl.Color = srcery.black +}; + +pub const NumberInputOptions = struct { + key: Key, + storage: *TextInputStorage, + invalid: bool = false, + + initial: ?[]const u8 = null, + placeholder: ?[]const u8 = null, + text_color: rl.Color = srcery.black, + invalid_color: rl.Color = srcery.red }; pub fn mouseTooltip(self: *UI) *Box { @@ -2320,11 +2346,11 @@ pub fn endScrollbar(self: *UI) void { wrapper.endChildren(); } -pub fn textInput(self: *UI, key: Key, storage: *TextInputStorage) !void { +pub fn textInput(self: *UI, opts: TextInputOptions) !void { const now = std.time.nanoTimestamp(); const container = self.createBox(.{ - .key = key, + .key = opts.key, .size_x = Sizing.initGrowUpTo(.{ .pixels = 200 }), .size_y = Sizing.initFixed(Unit.initPixels(self.rem(1))), .flags = &.{ .clickable, .clip_view, .draggable }, @@ -2336,13 +2362,26 @@ pub fn textInput(self: *UI, key: Key, storage: *TextInputStorage) !void { defer container.endChildren(); const font = Assets.font(container.font); - const text = &storage.buffer; + const storage = opts.storage; + const storage_text = &storage.buffer; + + if (opts.initial != null and container.created) { + storage_text.clearAndFree(); + 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); { // Text visuals - const text_size = font.measureText(text.items); + 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); + } + + const text_size = font.measureText(text); const visible_text_width = container.persistent.size.x - container.padding.byAxis(.X); const shown_window_size = @min(visible_text_width, text_size.x); @@ -2369,8 +2408,8 @@ pub fn textInput(self: *UI, key: Key, storage: *TextInputStorage) !void { } _ = self.createBox(.{ - .text_color = srcery.black, - .text = text.items, + .text_color = text_color, + .text = text, .float_relative_to = container, .float_rect = Rect{ .x = container.padding.left - storage.shown_slice_start, @@ -2461,7 +2500,7 @@ pub fn textInput(self: *UI, key: Key, storage: *TextInputStorage) !void { storage.cursor_stop = @intCast(std.math.clamp( cursor_stop + move_cursor_dir, 0, - @as(isize, @intCast(text.items.len)) + @as(isize, @intCast(storage_text.items.len)) )); } @@ -2494,7 +2533,7 @@ pub fn textInput(self: *UI, key: Key, storage: *TextInputStorage) !void { cursor = @intCast(std.math.clamp( @as(isize, @intCast(cursor)) + move_cursor_dir, 0, - @as(isize, @intCast(text.items.len)) + @as(isize, @intCast(storage_text.items.len)) )); } @@ -2526,7 +2565,7 @@ pub fn textInput(self: *UI, key: Key, storage: *TextInputStorage) !void { } // Deletion - if (self.isKeyboardPressedOrHeld(.key_backspace) and text.items.len > 0) { + if (self.isKeyboardPressedOrHeld(.key_backspace) and storage_text.items.len > 0) { if (storage.cursor_start == storage.cursor_stop) { if (storage.cursor_start > 0) { storage.deleteMany(storage.cursor_start-1, storage.cursor_start); @@ -2552,7 +2591,7 @@ pub fn textInput(self: *UI, key: Key, storage: *TextInputStorage) !void { } } else if (self.isKeyboardPressedOrHeld(.key_a)) { storage.cursor_start = 0; - storage.cursor_stop = text.items.len; + storage.cursor_stop = storage_text.items.len; } else if (self.isKeyboardPressedOrHeld(.key_c)) { if (storage.cursor_start != storage.cursor_stop) { @@ -2593,5 +2632,42 @@ pub fn textInput(self: *UI, key: Key, storage: *TextInputStorage) !void { if (no_blinking) { storage.last_pressed_at_ns = now; } + + if (self.isKeyboardPressed(.key_escape) or self.isKeyboardPressed(.key_enter)) { + storage.editing = false; + } + + if (container_signal.clicked_outside and !container_signal.is_mouse_inside) { + storage.editing = false; + } + } +} + +pub fn numberInput(self: *UI, T: type, opts: NumberInputOptions) !?T { + const storage = opts.storage; + + var text_opts = TextInputOptions{ + .key = opts.key, + .storage = opts.storage, + .initial = opts.initial, + .text_color = opts.text_color, + .placeholder = opts.placeholder + }; + + var is_invalid = opts.invalid; + if (storage.buffer.items.len > 0 and std.meta.isError(std.fmt.parseFloat(T, storage.buffer.items))) { + is_invalid = true; + } + + if (is_invalid) { + text_opts.text_color = opts.invalid_color; + } + + try self.textInput(text_opts); + + if (std.fmt.parseFloat(T, storage.buffer.items)) |new_value| { + return new_value; + } else |_| { + return null; } } \ No newline at end of file