add sample rate input

This commit is contained in:
Rokas Puzonas 2025-04-08 21:36:15 +03:00
parent 293d220b34
commit 23c4b99455
3 changed files with 218 additions and 46 deletions

View File

@ -292,6 +292,14 @@ pub const Project = struct {
} }
pub fn getDefaultSampleRate(self: *Project) ?f64 { 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 result_range: ?RangeF64 = null;
var channel_iter = self.channels.iterator(); var channel_iter = self.channels.iterator();
@ -308,11 +316,7 @@ pub const Project = struct {
} }
} }
if (result_range) |r| { return result_range;
return r.upper;
} else {
return null;
}
} }
pub fn deinit(self: *Project, allocator: Allocator) void { pub fn deinit(self: *Project, allocator: Allocator) void {
@ -561,6 +565,7 @@ pub const CollectionTask = struct {
allocator: Allocator, allocator: Allocator,
ui: UI, ui: UI,
double_pass_ui: bool = true,
should_close: bool = false, should_close: bool = false,
ni_daq_api: ?NIDaq.Api = null, ni_daq_api: ?NIDaq.Api = null,
ni_daq: ?NIDaq = null, ni_daq: ?NIDaq = null,
@ -586,7 +591,7 @@ pub fn init(self: *App, allocator: Allocator) !void {
.main_screen = undefined, .main_screen = undefined,
.collection_thread = undefined .collection_thread = undefined
}; };
self.main_screen = try MainScreen.init(self); try self.initUI();
if (NIDaq.Api.init()) |ni_daq_api| { if (NIDaq.Api.init()) |ni_daq_api| {
self.ni_daq_api = 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_condition.signal();
self.collection_thread.join(); self.collection_thread.join();
self.ui.deinit(); self.deinitUI();
self.main_screen.deinit();
if (self.ni_daq) |*ni_daq| { if (self.ni_daq) |*ni_daq| {
ni_daq.deinit(self.allocator); 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.stopCollection();
self.project.deinit(self.allocator); 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; const save_location = self.project.save_location orelse return error.MissingSaveLocation;
log.info("Load project from: {s}", .{save_location}); 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 }); 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; const save_location = self.project.save_location orelse return error.MissingSaveLocation;
log.info("Save project to: {s}", .{save_location}); log.info("Save project to: {s}", .{save_location});
@ -682,6 +701,20 @@ pub fn saveProject(self: *App) !void {
try self.project.save(); 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 { pub fn tick(self: *App) !void {
var ui = &self.ui; var ui = &self.ui;
self.command_queue.len = 0; self.command_queue.len = 0;
@ -728,17 +761,17 @@ pub fn tick(self: *App) !void {
} }
} }
ui.begin(); if (rl.isWindowResized() or self.double_pass_ui) {
defer ui.end(); try self.showUI();
self.double_pass_ui = false;
switch (self.screen) {
.main => try self.main_screen.tick(),
.add_channels => {
self.screen = .main;
}
} }
try self.showUI();
} }
rl.clearBackground(srcery.black);
ui.draw();
for (self.command_queue.constSlice()) |command| { for (self.command_queue.constSlice()) |command| {
switch (command) { switch (command) {
.start_collection => { .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 { pub fn pushCommand(self: *App, command: Command) void {

View File

@ -46,6 +46,8 @@ view_undo_stack: std.BoundedArray(ViewCommand, 100) = .{},
protocol_modal: ?Id = null, protocol_modal: ?Id = null,
frequency_input: UI.TextInputStorage, frequency_input: UI.TextInputStorage,
amplitude_input: UI.TextInputStorage, amplitude_input: UI.TextInputStorage,
sample_rate_input: UI.TextInputStorage,
parsed_sample_rate: ?f64 = null,
protocol_error_message: ?[]const u8 = null, protocol_error_message: ?[]const u8 = null,
protocol_graph_cache: Graph.Cache = .{}, protocol_graph_cache: Graph.Cache = .{},
preview_samples: std.ArrayListUnmanaged(f64) = .{}, preview_samples: std.ArrayListUnmanaged(f64) = .{},
@ -58,6 +60,7 @@ pub fn init(app: *App) !MainScreen {
.app = app, .app = app,
.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)
}; };
try self.frequency_input.setText("10"); try self.frequency_input.setText("10");
@ -71,6 +74,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.preview_samples.clearAndFree(allocator); self.preview_samples.clearAndFree(allocator);
self.clearProtocolErrorMessage(); self.clearProtocolErrorMessage();
@ -828,7 +832,10 @@ pub fn showProtocolModal(self: *MainScreen, channel_id: Id) !void {
label_box.size.y = UI.Sizing.initGrowFull(); label_box.size.y = UI.Sizing.initGrowFull();
label_box.alignment.y = .center; 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; 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; self.protocol_error_message = null;
} }
pub fn tick(self: *MainScreen) !void { pub fn showSidePanel(self: *MainScreen) !void {
var ui = &self.app.ui; var ui = &self.app.ui;
const frame_allocator = ui.frameAllocator();
if (ui.isKeyboardPressed(.key_escape)) { const container = ui.createBox(.{
if (self.protocol_modal != null) { .size_x = UI.Sizing.initGrowUpTo(.{ .parent_percent = 0.2 }),
self.closeModal(); .size_y = UI.Sizing.initGrowFull(),
} else if (self.fullscreen_view != null) { .borders = .{
self.fullscreen_view = null; .right = .{ .color = srcery.hard_black, .size = 4 }
} else { },
self.app.should_close = true; .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)) { if (ui.isCtrlDown() and ui.isKeyboardPressed(.key_z)) {
self.undoLastMoveCommand(); self.undoLastMoveCommand();
@ -1016,6 +1062,16 @@ pub fn tick(self: *MainScreen) !void {
try self.showView(view_id, UI.Sizing.initGrowFull()); try self.showView(view_id, UI.Sizing.initGrowFull());
} else { } 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")); const scroll_area = ui.beginScrollbar(ui.keyFromString("Channels"));
defer ui.endScrollbar(); defer ui.endScrollbar();
scroll_area.layout_direction = .top_to_bottom; scroll_area.layout_direction = .top_to_bottom;
@ -1055,4 +1111,14 @@ pub fn tick(self: *MainScreen) !void {
if (maybe_modal_overlay) |modal_overlay| { if (maybe_modal_overlay) |modal_overlay| {
root.bringChildToTop(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;
}
}
} }

View File

@ -113,6 +113,7 @@ pub const Signal = struct {
active: bool = false, active: bool = false,
shift_modifier: bool = false, shift_modifier: bool = false,
ctrl_modifier: bool = false, ctrl_modifier: bool = false,
clicked_outside: bool = false,
pub fn clicked(self: Signal) bool { pub fn clicked(self: Signal) bool {
return self.flags.contains(.left_clicked) or self.flags.contains(.right_clicked) or self.flags.contains(.middle_clicked); 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, ui: *UI,
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
created: bool = false,
flags: Flags, flags: Flags,
background: ?rl.Color, background: ?rl.Color,
@ -493,6 +495,7 @@ pub const Box = struct {
padding: Padding, padding: Padding,
font: Assets.FontId, font: Assets.FontId,
text_color: rl.Color, text_color: rl.Color,
// TODO: Add option to specify where the border is drawn: outside, inside, center.
borders: Borders, borders: Borders,
text: ?[]u8, text: ?[]u8,
hot_cursor: ?rl.MouseCursor, hot_cursor: ?rl.MouseCursor,
@ -816,12 +819,12 @@ pub fn deinit(self: *UI) void {
self.arenas[1].deinit(); 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)]; return &self.arenas[@mod(self.frame_index, 2)];
} }
pub fn frameAllocator(self: *UI) std.mem.Allocator { pub fn frameAllocator(self: *UI) std.mem.Allocator {
return self.frame_arena().allocator(); return self.frameArena().allocator();
} }
pub fn pullOsEvents(self: *UI) void { pub fn pullOsEvents(self: *UI) void {
@ -902,7 +905,7 @@ pub fn begin(self: *UI) void {
} }
self.frame_index += 1; self.frame_index += 1;
_ = self.frame_arena().reset(.retain_capacity); _ = self.frameArena().reset(.retain_capacity);
self.pushFont(default_font); self.pushFont(default_font);
@ -1452,6 +1455,7 @@ pub fn createBox(self: *UI, opts: BoxOptions) *Box {
var box_index: ?BoxIndex = null; var box_index: ?BoxIndex = null;
var key = opts.key orelse Key.initNil(); var key = opts.key orelse Key.initNil();
var persistent = Box.Persistent{}; var persistent = Box.Persistent{};
var created = false;
if (!key.isNil()) { if (!key.isNil()) {
if (self.getBoxIndexByKey(key)) |last_frame_box_index| { if (self.getBoxIndexByKey(key)) |last_frame_box_index| {
@ -1469,6 +1473,7 @@ pub fn createBox(self: *UI, opts: BoxOptions) *Box {
if (box_index == null) { if (box_index == null) {
box = self.boxes.addOneAssumeCapacity(); box = self.boxes.addOneAssumeCapacity();
box_index = self.boxes.len - 1; box_index = self.boxes.len - 1;
created = true;
} }
var size = Sizing2{ var size = Sizing2{
@ -1503,6 +1508,7 @@ pub fn createBox(self: *UI, opts: BoxOptions) *Box {
box.* = Box{ box.* = Box{
.ui = self, .ui = self,
.allocator = self.frameAllocator(), .allocator = self.frameAllocator(),
.created = created,
.persistent = persistent, .persistent = persistent,
.flags = flags, .flags = flags,
@ -1897,6 +1903,10 @@ pub fn signal(self: *UI, box: *Box) Signal {
taken = true; 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) { if (event == .mouse_scroll and is_mouse_inside and scrollable) {
result.scroll = event.mouse_scroll; result.scroll = event.mouse_scroll;
result.flags.insert(.scrolled); result.flags.insert(.scrolled);
@ -2193,8 +2203,24 @@ pub const TextInputStorage = struct {
} }
}; };
pub const TextInputResult = struct { pub const TextInputOptions = struct {
changed: bool = false 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 { pub fn mouseTooltip(self: *UI) *Box {
@ -2320,11 +2346,11 @@ pub fn endScrollbar(self: *UI) void {
wrapper.endChildren(); 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 now = std.time.nanoTimestamp();
const container = self.createBox(.{ const container = self.createBox(.{
.key = key, .key = opts.key,
.size_x = Sizing.initGrowUpTo(.{ .pixels = 200 }), .size_x = Sizing.initGrowUpTo(.{ .pixels = 200 }),
.size_y = Sizing.initFixed(Unit.initPixels(self.rem(1))), .size_y = Sizing.initFixed(Unit.initPixels(self.rem(1))),
.flags = &.{ .clickable, .clip_view, .draggable }, .flags = &.{ .clickable, .clip_view, .draggable },
@ -2336,13 +2362,26 @@ pub fn textInput(self: *UI, key: Key, storage: *TextInputStorage) !void {
defer container.endChildren(); defer container.endChildren();
const font = Assets.font(container.font); 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_start_x = storage.getCharOffsetX(font, storage.cursor_start);
const cursor_stop_x = storage.getCharOffsetX(font, storage.cursor_stop); const cursor_stop_x = storage.getCharOffsetX(font, storage.cursor_stop);
{ // Text visuals { // 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 visible_text_width = container.persistent.size.x - container.padding.byAxis(.X);
const shown_window_size = @min(visible_text_width, text_size.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(.{ _ = self.createBox(.{
.text_color = srcery.black, .text_color = text_color,
.text = text.items, .text = text,
.float_relative_to = container, .float_relative_to = container,
.float_rect = Rect{ .float_rect = Rect{
.x = container.padding.left - storage.shown_slice_start, .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( storage.cursor_stop = @intCast(std.math.clamp(
cursor_stop + move_cursor_dir, cursor_stop + move_cursor_dir,
0, 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( cursor = @intCast(std.math.clamp(
@as(isize, @intCast(cursor)) + move_cursor_dir, @as(isize, @intCast(cursor)) + move_cursor_dir,
0, 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 // 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 == storage.cursor_stop) {
if (storage.cursor_start > 0) { if (storage.cursor_start > 0) {
storage.deleteMany(storage.cursor_start-1, storage.cursor_start); 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)) { } else if (self.isKeyboardPressedOrHeld(.key_a)) {
storage.cursor_start = 0; storage.cursor_start = 0;
storage.cursor_stop = text.items.len; storage.cursor_stop = storage_text.items.len;
} else if (self.isKeyboardPressedOrHeld(.key_c)) { } else if (self.isKeyboardPressedOrHeld(.key_c)) {
if (storage.cursor_start != storage.cursor_stop) { if (storage.cursor_start != storage.cursor_stop) {
@ -2593,5 +2632,42 @@ pub fn textInput(self: *UI, key: Key, storage: *TextInputStorage) !void {
if (no_blinking) { if (no_blinking) {
storage.last_pressed_at_ns = now; 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;
} }
} }