add sample rate input
This commit is contained in:
parent
293d220b34
commit
23c4b99455
74
src/app.zig
74
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 {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
106
src/ui.zig
106
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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user