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

View File

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

View File

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