From d2b4942fa03e33162ac3dc075212f444814c47ff Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Tue, 8 Apr 2025 21:36:12 +0300 Subject: [PATCH] refactor in preparation for saving state to a file --- src/app.zig | 1296 +++++++++++++++++++++-------------- src/constants.zig | 7 + src/graph.zig | 24 +- src/main.zig | 22 +- src/ni-daq/root.zig | 2 +- src/range.zig | 2 +- src/screens/main_screen.zig | 378 +++++----- 7 files changed, 1025 insertions(+), 706 deletions(-) create mode 100644 src/constants.zig diff --git a/src/app.zig b/src/app.zig index 16726d3..8bb2c6e 100644 --- a/src/app.zig +++ b/src/app.zig @@ -1,88 +1,195 @@ const std = @import("std"); -const UI = @import("./ui.zig"); -const Platform = @import("./platform.zig"); const rl = @import("raylib"); -const srcery = @import("./srcery.zig"); +const srcery = @import("srcery.zig"); const NIDaq = @import("ni-daq/root.zig"); -const Graph = @import("./graph.zig"); const utils = @import("./utils.zig"); -const Assets = @import("./assets.zig"); const RangeF64 = @import("./range.zig").RangeF64; - +const Graph = @import("./graph.zig"); +const UI = @import("./ui.zig"); const MainScreen = @import("./screens/main_screen.zig"); -const ChannelFromDeviceScreen = @import("./screens/channel_from_device.zig"); +const Platform = @import("./platform.zig"); +const Allocator = std.mem.Allocator; const log = std.log.scoped(.app); -const clamp = std.math.clamp; const assert = std.debug.assert; -const remap = utils.remap; - -const max_channels = 64; -const max_files = 32; const App = @This(); -const ChannelTask = struct { - task: NIDaq.Task, - sample_rate: f64, - read: bool, +pub const Id = packed struct { + pub const Index = u8; + pub const Generation = u24; - fn deinit(self: ChannelTask) void { - self.task.clear(); + pub const max_items = std.math.maxInt(Index) + 1; + + index: Index, + generation: Generation, + + pub fn asInt(self: Id) u32 { + return @bitCast(self); + } + + pub fn eql(self: Id, other: Id) bool { + return @as(u32, @bitCast(self)) == @as(u32, @bitCast(other)); } }; -const FileChannel = struct { - path: []u8, - samples: []f64, +fn GenerationalArray(Item: type) type { + const GenerationalItem = struct { + generation: Id.Generation = 0, + item: Item + }; - fn deinit(self: FileChannel, allocator: std.mem.Allocator) void { - allocator.free(self.path); - allocator.free(self.samples); - } -}; + const UsedBitSet = std.StaticBitSet(Id.max_items); -pub const DeviceChannel = struct { - const ChannelName = std.BoundedArray(u8, NIDaq.max_channel_name_size + 1); // +1 for null byte - const Direction = enum { input, output }; + return struct { + const Self = @This(); - device_name: NIDaq.BoundedDeviceName = .{}, - channel_name: ChannelName = .{}, + const IdIterator = struct { + array: *Self, + index: usize = 0, - samples: std.ArrayList(f64), + pub fn next(self: *IdIterator) ?Id { + while (self.index < Id.max_items) { + defer self.index += 1; - write_pattern: std.ArrayList(f64), + if (!self.array.used.isSet(self.index)) { + return Id{ + .index = @intCast(self.index), + .generation = self.array.items[self.index].generation + }; + } + } + return null; + } + }; - units: NIDaq.Unit = .Voltage, - min_sample_rate: f64, - max_sample_rate: f64, - min_value: f64, - max_value: f64, + const ItemIterator = struct { + id_iter: IdIterator, - task: ?ChannelTask = null, + pub fn next(self: *ItemIterator) ?*Item { + if (self.id_iter.next()) |id| { + return &self.id_iter.array.items[id.index].item; + } else { + return null; + } + } + }; - fn deinit(self: DeviceChannel) void { - self.samples.deinit(); - self.write_pattern.deinit(); - if (self.task) |task| { - task.deinit(); + used: UsedBitSet = UsedBitSet.initFull(), + items: [Id.max_items]GenerationalItem = undefined, + + pub fn insertUndefined(self: *Self) !Id { + const index: Id.Index = @intCast(self.used.findFirstSet() orelse return error.OutOfMemory); + + self.used.unset(index); + + return Id{ + .index = index, + .generation = self.items[index].generation + }; + } + + pub fn insert(self: *Self, item: Item) !Id { + const id = try self.insertUndefined(); + self.items[id.index].item = item; + return id; + } + + pub fn remove(self: *Self, id: Id) void { + if (self.used.isSet(id.generation)) { + return; + } + + self.used.set(id.generation); + self.items[id.index].generation += 1; + } + + pub fn get(self: *Self, id: Id) ?*Item { + if (!self.has(id)) { + return null; + } + + return &self.items[id.index].item; + } + + pub fn has(self: *Self, id: Id) bool { + if (self.used.isSet(id.generation)) { + return false; + } + + if (self.items[id.index].generation != id.generation) { + return false; + } + + return true; + } + + pub fn idIterator(self: *Self) IdIterator { + return IdIterator{ + .array = self + }; + } + + pub fn iterator(self: *Self) ItemIterator { + return ItemIterator{ + .id_iter = self.idIterator() + }; + } + + pub fn count(self: *Self) usize { + return Id.max_items - self.used.count(); + } + + pub fn isEmpty(self: *Self) bool { + return self.used.eql(UsedBitSet.initFull()); + } + }; +} + +pub const Channel = struct { + const Name = std.BoundedArray(u8, NIDaq.max_channel_name_size + 1); + const Device = std.BoundedArray(u8, NIDaq.max_device_name_size + 1); + + // Persistent + name: Name = .{}, + device: Device = .{}, + + // Runtime + allowed_sample_values: ?RangeF64 = null, + allowed_sample_rates: ?RangeF64 = null, + collected_samples: std.ArrayListUnmanaged(f64) = .{}, + write_pattern: std.ArrayListUnmanaged(f64) = .{}, + output_task: ?NIDaq.Task = null, + + pub fn deinit(self: *Channel, allocator: Allocator) void { + self.clear(allocator); + + self.collected_samples.clearAndFree(allocator); + self.write_pattern.clearAndFree(allocator); + if (self.output_task) |task| { + task.clear(); + self.output_task = null; } } - pub fn getDeviceName(self: *DeviceChannel) [:0]const u8 { - return utils.getBoundedStringZ(&self.device_name); + pub fn clear(self: *Channel, allocator: Allocator) void { + _ = allocator; + self.allowed_sample_rates = null; + self.allowed_sample_values = null; } - pub fn getChannelName(self: *DeviceChannel) [:0]const u8 { - return utils.getBoundedStringZ(&self.channel_name); - } - - pub fn generateSine(samples: *std.ArrayList(f64), sample_rate: f64, frequency: f64, amplitude: f64) !void { + pub fn generateSine( + samples: *std.ArrayListUnmanaged(f64), + allocator: Allocator, + sample_rate: f64, + frequency: f64, + amplitude: f64 + ) !void { samples.clearRetainingCapacity(); const sample_count: usize = @intFromFloat(@ceil(sample_rate)); assert(sample_count >= 1); - try samples.ensureTotalCapacity(sample_count); + try samples.ensureTotalCapacity(allocator, sample_count); for (0..sample_count) |i| { const i_f64: f64 = @floatFromInt(i); @@ -92,95 +199,171 @@ pub const DeviceChannel = struct { } }; -pub const ChannelView = struct { - view_cache: Graph.Cache = .{}, - view_rect: Graph.ViewOptions, +pub const File = struct { + // Persistent + path: []u8, + // Runtime + samples: ?[]f64 = null, + min_sample: f64 = 0, + max_sample: f64 = 0, + + pub fn deinit(self: *File, allocator: Allocator) void { + self.clear(allocator); + allocator.free(self.path); + } + + pub fn clear(self: *File, allocator: Allocator) void { + if (self.samples) |samples| { + allocator.free(samples); + self.samples = null; + } + self.min_sample = 0; + self.max_sample = 0; + } +}; + +pub const View = struct { + pub const Reference = union(enum) { + file: Id, + channel: Id + }; + + // Persistent + reference: Reference, height: f32 = 300, - - x_range: RangeF64, - y_range: RangeF64, - sample_rate: ?f64 = null, - unit: ?NIDaq.Unit = null, follow: bool = false, + graph_opts: Graph.ViewOptions = .{}, - source: union(enum) { - file: usize, - device: usize - }, + // Runtime + graph_cache: Graph.Cache = .{}, + available_x_range: RangeF64 = RangeF64.init(0, 0), + available_y_range: RangeF64 = RangeF64.init(0, 0), + unit: ?NIDaq.Unit = .Voltage, - pub fn isFromFile(self: *ChannelView) bool { - return self.source == .file; + pub fn clear(self: *View) void { + self.graph_cache.clear(); + self.available_x_range = RangeF64.init(0, 0); + self.available_y_range = RangeF64.init(0, 0); } - pub fn isFromDevice(self: *ChannelView) bool { - return self.source == .device; - } - - pub fn getViewRange(self: *ChannelView, axis: UI.Axis) *RangeF64 { + pub fn getGraphView(self: *View, axis: UI.Axis) *RangeF64 { return switch (axis) { - .X => &self.view_rect.x_range, - .Y => &self.view_rect.y_range, + .X => &self.graph_opts.x_range, + .Y => &self.graph_opts.y_range }; } - pub fn getSampleRange(self: *ChannelView, axis: UI.Axis) *RangeF64 { + pub fn getAvailableView(self: *View, axis: UI.Axis) RangeF64 { return switch (axis) { - .X => &self.x_range, - .Y => &self.y_range, + .X => self.available_x_range, + .Y => self.available_y_range }; } }; -pub const DeferredChannelAction = union(enum) { - activate: *ChannelView, - deactivate: *ChannelView, - toggle_input_channels +pub const Project = struct { + sample_rate: ?f64 = null, + + channels: GenerationalArray(Channel) = .{}, + files: GenerationalArray(File) = .{}, + views: GenerationalArray(View) = .{}, + + pub fn getSampleRate(self: *Project) ?f64 { + return self.sample_rate orelse self.getDefaultSampleRate(); + } + + pub fn getDefaultSampleRate(self: *Project) ?f64 { + var result_range: ?RangeF64 = null; + + var channel_iter = self.channels.iterator(); + while (channel_iter.next()) |channel| { + const allowed_sample_rates = channel.allowed_sample_rates orelse continue; + + if (result_range == null) { + result_range = allowed_sample_rates; + } else { + result_range.? = result_range.?.intersectPositive(allowed_sample_rates); + if (result_range.?.isNegative()) { + return null; + } + } + } + + if (result_range) |r| { + return r.upper; + } else { + return null; + } + } + + pub fn deinit(self: *Project, allocator: Allocator) void { + var file_iter = self.files.iterator(); + while (file_iter.next()) |file| { + file.deinit(allocator); + } + + var channel_iter = self.channels.iterator(); + while (channel_iter.next()) |channel| { + channel.deinit(allocator); + } + } }; -allocator: std.mem.Allocator, +pub const Command = union(enum) { + start_collection, + stop_collection, + save_project, + stop_output: Id, // Channel id + start_output: Id, // Channel id + add_file_from_picker +}; + +pub const CollectionTask = struct { + ni_task: NIDaq.Task, + sample_rate: f64, + channels: std.BoundedArray(Id, Id.max_items) = .{}, + read_array: []f64, + + pub fn deinit(self: *CollectionTask, allocator: Allocator) void { + self.ni_task.clear(); + allocator.free(self.read_array); + } +}; + +allocator: Allocator, +ui: UI, should_close: bool = false, ni_daq_api: ?NIDaq.Api = null, ni_daq: ?NIDaq = null, -channel_views: std.BoundedArray(ChannelView, max_channels) = .{}, -loaded_files: [max_channels]?FileChannel = .{ null } ** max_channels, -device_channels: [max_channels]?DeviceChannel = .{ null } ** max_channels, - -// Reading & writing tasks -samples_mutex: std.Thread.Mutex = .{}, -device_channels_mutex: std.Thread.Mutex = .{}, -thread_wake_condition: std.Thread.Condition = .{}, -task_read_thread: ?std.Thread = null, -task_read_active: bool = false, - -// UI Fields -ui: UI, -deferred_actions: std.BoundedArray(DeferredChannelAction, max_channels) = .{}, -current_screen: enum { - main_menu, - channel_from_device -} = .main_menu, +screen: enum { + main, + add_channels +} = .main, main_screen: MainScreen, -channel_from_device: ChannelFromDeviceScreen, +project: Project = .{}, -pub fn init(self: *App, allocator: std.mem.Allocator) !void { +collection_mutex: std.Thread.Mutex = .{}, +collection_samples_mutex: std.Thread.Mutex = .{}, +collection_condition: std.Thread.Condition = .{}, +collection_thread: std.Thread, +collection_task: ?CollectionTask = null, + +command_queue: std.BoundedArray(Command, 16) = .{}, + +pub fn init(self: *App, allocator: Allocator) !void { self.* = App{ .allocator = allocator, .ui = UI.init(allocator), .main_screen = undefined, - .channel_from_device = ChannelFromDeviceScreen{ - .app = self, - .channel_names = std.heap.ArenaAllocator.init(allocator) - }, + .collection_thread = undefined }; - errdefer self.deinit(); - self.main_screen = try MainScreen.init(self); if (NIDaq.Api.init()) |ni_daq_api| { self.ni_daq_api = ni_daq_api; - const ni_daq = try NIDaq.init(allocator, &self.ni_daq_api.?, .{ + self.ni_daq = try NIDaq.init(allocator, &self.ni_daq_api.?, .{ .max_devices = 4, .max_analog_inputs = 32, .max_analog_outputs = 8, @@ -189,92 +372,371 @@ pub fn init(self: *App, allocator: std.mem.Allocator) !void { .max_analog_input_voltage_ranges = 4, .max_analog_output_voltage_ranges = 4 }); - self.ni_daq = ni_daq; - const installed_version = try ni_daq.version(); + const installed_version = try self.ni_daq.?.version(); if (installed_version.order(NIDaq.Api.min_version) == .lt) { - // TODO: - // self.shown_modal = .{ .library_version_warning = installed_version }; + log.warn("NI-Daq library version is too old, installed version: '{}', min version: '{}'", .{ installed_version, NIDaq.Api.min_version }); } } else |e| { - log.err("Failed to load NI-Daq library: {any}", .{e}); - - switch (e) { - error.LibraryNotFound => { - // TODO: - // self.shown_modal = .no_library_error; - }, - error.SymbolNotFound => { - // TODO: - // if (NIDaq.Api.version()) |version| { - // self.shown_modal = .{ .library_version_error = version }; - // } else |_| { - // self.shown_modal = .no_library_error; - // } - } - } + log.err("Failed to load NI-Daq library: {}", .{ e }); } - self.task_read_thread = try std.Thread.spawn( - .{ .allocator = allocator }, - readThreadCallback, + self.collection_thread = try std.Thread.spawn( + .{ .allocator = self.allocator }, + collectionThreadCallback, .{ self } ); } pub fn deinit(self: *App) void { - self.main_screen.deinit(); - - if (self.task_read_thread) |thread| { - self.should_close = true; - self.thread_wake_condition.signal(); - thread.join(); - self.task_read_thread = null; - } - - for (self.channel_views.slice()) |*channel| { - channel.view_cache.deinit(); - } - - for (&self.loaded_files) |*loaded_file| { - if (loaded_file.*) |*f| { - f.deinit(self.allocator); - loaded_file.* = null; - } - } - - for (&self.device_channels) |*device_channel| { - if (device_channel.*) |*c| { - c.deinit(); - device_channel.* = null; - } - } + self.stopCollection(); self.ui.deinit(); - self.channel_from_device.deinit(); + self.main_screen.deinit(); + self.project.deinit(self.allocator); - if (self.ni_daq_api) |*ni_daq_api| ni_daq_api.deinit(); - if (self.ni_daq) |*ni_daq| ni_daq.deinit(self.allocator); + if (self.ni_daq) |*ni_daq| { + ni_daq.deinit(self.allocator); + } + + if (self.ni_daq_api) |*ni_daq_api| { + ni_daq_api.deinit(); + } } -fn findFreeSlot(T: type, slice: []const ?T) ?usize { - for (0.., slice) |i, loaded_file| { - if (loaded_file == null) { - return i; +pub fn loadProject(self: *App, project_file: []const u8) !void { + _ = self; + _ = project_file; +} + +pub fn tick(self: *App) !void { + var ui = &self.ui; + self.command_queue.len = 0; + + ui.pullOsEvents(); + { + self.collection_samples_mutex.lock(); + defer self.collection_samples_mutex.unlock(); + + { + var view_iter = self.project.views.idIterator(); + while (view_iter.next()) |id| { + self.refreshViewAvailableXYRanges(id); + } + } + + if (self.isCollectionInProgress()) { + var channel_iter = self.project.channels.idIterator(); + while (channel_iter.next()) |channel_id| { + self.refreshViewAvailableXYRanges(channel_id); + } + + var view_iter = self.project.views.iterator(); + while (view_iter.next()) |view| { + if (view.reference != .channel) continue; + if (!view.follow) continue; + + const sample_rate = self.project.sample_rate orelse 1; + + view.graph_opts.y_range = view.available_y_range; + view.graph_opts.x_range.lower = 0; + if (view.available_x_range.upper > view.graph_opts.x_range.upper) { + view.graph_opts.x_range.upper = view.available_x_range.upper + sample_rate * 10; + } + } + } + + if (ui.isCtrlDown() and ui.isKeyboardPressed(.key_s)) { + self.pushCommand(.save_project); + } + + ui.begin(); + defer ui.end(); + + switch (self.screen) { + .main => try self.main_screen.tick(), + .add_channels => { + self.screen = .main; + } } } - return null; + + for (self.command_queue.constSlice()) |command| { + switch (command) { + .start_collection => { + self.startCollection() catch |e| { + log.err("Failed to start collection: {}", .{e}); + }; + }, + .stop_collection => { + self.stopCollection(); + }, + .save_project => { + // TODO: + }, + .add_file_from_picker => { + self.addFileFromPicker() catch |e| { + log.err("Failed to add file from picker: {}", .{e}); + }; + }, + .start_output => |channel_id| { + self.startOutput(channel_id) catch |e| { + log.err("Failed to start output on channel: {}", .{e}); + }; + }, + .stop_output => |channel_id| { + self.stopOutput(channel_id); + } + } + } + + rl.clearBackground(srcery.black); + ui.draw(); } -fn readSamplesFromFile(allocator: std.mem.Allocator, file: std.fs.File) ![]f64 { - try file.seekTo(0); +pub fn pushCommand(self: *App, command: Command) void { + self.command_queue.append(command) catch { + log.warn("Failed to push a command, ignoring it", .{}); + return; + }; +} + +fn addFileFromPicker(self: *App) !void { + const filename = try Platform.openFilePicker(self.allocator); + defer self.allocator.free(filename); + + const file_id = try self.addFile(filename); + _ = try self.addView(.{ .file = file_id }); +} + +fn startCollection(self: *App) !void { + const ni_daq = &(self.ni_daq orelse return); + + if (self.isCollectionInProgress()) { + return; + } + + self.collection_mutex.lock(); + defer self.collection_mutex.unlock(); + + const task = try ni_daq.createTask(null); + errdefer task.clear(); + + const sample_rate = self.project.getSampleRate() orelse return error.MissingSampleRate; + + var channels_in_task: std.BoundedArray(Id, Id.max_items) = .{}; + + var channel_iter = self.project.channels.idIterator(); + while (channel_iter.next()) |id| { + const channel = self.getChannel(id).?; + const channel_name = utils.getBoundedStringZ(&channel.name); + const channel_type = NIDaq.getChannelType(channel_name) orelse continue; + + if (channel_type == .analog_input) { + const allowed_sample_values = channel.allowed_sample_values orelse continue; + + try task.createAIVoltageChannel(.{ + .channel = channel_name, + .min_value = allowed_sample_values.lower, + .max_value = allowed_sample_values.upper + }); + + channels_in_task.appendAssumeCapacity(id); + } + } + + try task.setContinousSampleRate(.{ .sample_rate = sample_rate }); + + try task.start(); + log.info("Started collection at {d}Hz", .{ sample_rate }); + + for (channels_in_task.constSlice()) |id| { + const channel = self.getChannel(id).?; + channel.collected_samples.clearAndFree(self.allocator); + } + + const read_array = try self.allocator.alloc(f64, + channels_in_task.len * @as(usize, @intFromFloat(@ceil(sample_rate))) + ); + errdefer self.allocator.free(read_array); + + var view_iter = self.project.views.iterator(); + while (view_iter.next()) |view| { + if (view.reference != .channel) continue; + const channel_id = view.reference.channel; + + var has_channel = false; + for (channels_in_task.constSlice()) |id| { + if (id.eql(channel_id)) { + has_channel = true; + break; + } + } + + if (has_channel) { + view.follow = true; + } + } + + self.collection_task = CollectionTask{ + .ni_task = task, + .channels = channels_in_task, + .sample_rate = sample_rate, + .read_array = read_array, + }; + self.collection_condition.signal(); +} + +fn stopCollection(self: *App) void { + if (!self.isCollectionInProgress()) { + return; + } + + self.collection_mutex.lock(); + defer self.collection_mutex.unlock(); + + var collection_task = self.collection_task.?; + collection_task.deinit(self.allocator); + + self.collection_task = null; +} + +fn startOutput(self: *App, channel_id: Id) !void { + const ni_daq = &(self.ni_daq orelse return error.MissingNiDaq); + const channel = self.getChannel(channel_id) orelse return error.ChannelNotFound; + const channel_name = utils.getBoundedStringZ(&channel.name); + const allowed_sample_values = channel.allowed_sample_values orelse return error.NoAllowedSampleValues; + const sample_rate = self.project.getSampleRate() orelse return error.MissingSampleRate; + + if (channel.output_task != null) { + return; + } + + const task = try ni_daq.createTask(null); + errdefer task.clear(); + + try task.createAOVoltageChannel(.{ + .channel = channel_name, + .min_value = allowed_sample_values.lower, + .max_value = allowed_sample_values.upper + }); + + try task.setContinousSampleRate(.{ + .sample_rate = sample_rate + }); + + const samples_per_channel: u32 = @intCast(channel.write_pattern.items.len); + const written = try task.writeAnalog(.{ + .write_array = channel.write_pattern.items, + .samples_per_channel = samples_per_channel, + .timeout = -1 + }); + if (written != samples_per_channel) { + return error.WriteAnalog; + } + + try task.start(); + + channel.output_task = task; +} + +fn stopOutput(self: *App, channel_id: Id) void { + const channel = self.getChannel(channel_id) orelse return; + + if (channel.output_task == null) { + return; + } + + const output_task = channel.output_task.?; + output_task.clear(); + + channel.output_task = null; +} + +pub fn isCollectionInProgress(self: *App) bool { + return self.collection_task != null; +} + +pub fn collectionThreadCallback(self: *App) void { + while (!self.should_close) { + + { + self.collection_mutex.lock(); + defer self.collection_mutex.unlock(); + + if (self.collection_task) |*collection_task| { + self.collection_samples_mutex.lock(); + defer self.collection_samples_mutex.unlock(); + + const samples_per_channel = collection_task.ni_task.readAnalog(.{ + .timeout = 0, + .read_array = collection_task.read_array, + }) catch |e| { + log.err("Failed to read analog samples: {}", .{e}); + continue; + }; + const read_samples = collection_task.read_array[0..(samples_per_channel * collection_task.channels.len)]; + + if (read_samples.len > 0) { + var channel_index: usize = 0; + var window_iter = std.mem.window(f64, read_samples, samples_per_channel, samples_per_channel); + while (window_iter.next()) |channel_samples| : (channel_index += 1) { + const channel_id = collection_task.channels.get(channel_index); + const channel = self.getChannel(channel_id) orelse continue; + + channel.collected_samples.appendSlice(self.allocator, channel_samples) catch |e| { + log.err("Failed to append samples for channel: {}", .{e}); + continue; + }; + } + } + + } else { + self.collection_condition.wait(&self.collection_mutex); + } + } + + std.time.sleep(5 * std.time.ns_per_ms); + } +} + +// ---------------- Files --------------------------------- // + +pub inline fn getFile(self: *App, id: Id) ?*File { + return self.project.files.get(id); +} + +pub fn addFile(self: *App, path: []const u8) !Id { + const id = try self.project.files.insertUndefined(); + errdefer self.project.files.remove(id); + + const path_dupe = try self.allocator.dupe(u8, path); + errdefer self.allocator.free(path_dupe); + + const file = self.project.files.get(id).?; + file.* = File{ + .path = path_dupe + }; + + self.loadFile(id) catch |e| { + log.err("Failed to load file: {}", .{ e }); + }; + + return id; +} + +fn readFileF64(allocator: Allocator, file: std.fs.File) ![]f64 { const byte_count = try file.getEndPos(); - assert(byte_count % 8 == 0); + if (byte_count % 8 != 0) { + return error.NotMultipleOf8; + } var samples = try allocator.alloc(f64, @divExact(byte_count, 8)); errdefer allocator.free(samples); + try file.seekTo(0); + var i: usize = 0; var buffer: [4096]u8 = undefined; while (true) { @@ -290,377 +752,177 @@ fn readSamplesFromFile(allocator: std.mem.Allocator, file: std.fs.File) ![]f64 { return samples; } -pub fn listChannelViews(self: *App) []ChannelView { - return self.channel_views.slice(); -} +pub fn loadFile(self: *App, id: Id) !void { + const file = self.getFile(id) orelse return; + file.clear(self.allocator); -pub fn tick(self: *App) !void { - var ui = &self.ui; - rl.clearBackground(srcery.black); + const cwd = std.fs.cwd(); + const samples_file = try cwd.openFile(file.path, .{ .mode = .read_only }); + defer samples_file.close(); - self.deferred_actions.len = 0; - ui.pullOsEvents(); + const samples = try readFileF64(self.allocator, samples_file); + file.samples = samples; - { - self.samples_mutex.lock(); - defer self.samples_mutex.unlock(); - for (self.listChannelViews()) |*channel_view| { - const device_channel = self.getChannelSourceDevice(channel_view) orelse continue; - const sample_count: f32 = @floatFromInt(device_channel.samples.items.len); - - channel_view.x_range = RangeF64.init(0, sample_count); - } - - { - ui.begin(); - defer ui.end(); - - switch (self.current_screen) { - .main_menu => try self.main_screen.tick(), - .channel_from_device => try self.channel_from_device.tick() - } - } - } - - for (self.deferred_actions.constSlice()) |action| { - switch (action) { - .activate => |channel_view| { - try self.activateDeviceChannel(channel_view); - }, - .deactivate => |channel_view| { - try self.deactivateDeviceChannel(channel_view); - }, - .toggle_input_channels => { - try self.toggleInputDeviceChannels(); - } - } - } - - ui.draw(); -} - -// ---------------------- Reading & Writing tasks ----------------------------- // - -fn readThreadCallback(self: *App) void { - while (!self.should_close) { - var has_tasks_configured = false; - - { - self.device_channels_mutex.lock(); - defer self.device_channels_mutex.unlock(); - - for (&self.device_channels) |*maybe_device_channel| { - const device_channel = &(maybe_device_channel.* orelse continue); - - const channel_task = &(device_channel.task orelse continue); - if (!channel_task.read) { - continue; - } - - has_tasks_configured = true; - - self.readThreadDevice(device_channel) catch |e| { - log.err("Failed to read samples in thread: {}", .{e}); - if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); - } - - if (device_channel.task) |task| { - task.deinit(); - device_channel.task = null; - } - continue; - }; - } - - if (!has_tasks_configured) { - self.thread_wake_condition.wait(&self.device_channels_mutex); - } - } - - std.time.sleep(5 * std.time.ns_per_ms); - } -} - -fn readThreadDevice(self: *App, device_channel: *DeviceChannel) !void { - self.samples_mutex.lock(); - defer self.samples_mutex.unlock(); - - const channel_task = &device_channel.task.?; - assert(channel_task.read); - - try device_channel.samples.ensureUnusedCapacity(@intFromFloat(@ceil(channel_task.sample_rate))); - - const read_amount = try channel_task.task.readAnalog(.{ - .timeout = 0, - .read_array = device_channel.samples.unusedCapacitySlice() - }); - - device_channel.samples.items.len += read_amount; -} - -// ------------------------ Channel management -------------------------------- // - -pub fn getChannelDeviceByName(self: *App, name: []const u8) ?*DeviceChannel { - for (&self.device_channels) |*maybe_channel| { - var channel: *DeviceChannel = &(maybe_channel.* orelse continue); - if (std.mem.eql(u8, channel.channel_name.slice(), name)) { - return channel; - } - } - return null; -} - -pub fn getChannelSamples(self: *App, channel_view: *ChannelView) []const f64 { - return switch (channel_view.source) { - .file => |index| { - const loaded_file = self.loaded_files[index].?; - return loaded_file.samples; - }, - .device => |index| { - const device_channel = self.device_channels[index].?; - return device_channel.samples.items; - } - }; -} - -pub fn getChannelSourceDevice(self: *App, channel_view: *ChannelView) ?*DeviceChannel { - if (channel_view.source == .device) { - const device_channel_index = channel_view.source.device; - return &self.device_channels[device_channel_index].?; - } - - return null; -} - -pub fn isDeviceChannelActive(self: *App, channel_view: *ChannelView) bool { - const device_channel = self.getChannelSourceDevice(channel_view) orelse return false; - return device_channel.task != null; -} - -pub fn activateDeviceChannel(self: *App, channel_view: *ChannelView) !void { - const ni_daq = &(self.ni_daq orelse return); - - self.device_channels_mutex.lock(); - defer self.device_channels_mutex.unlock(); - - const device_channel = self.getChannelSourceDevice(channel_view) orelse return; - if (device_channel.task != null) { - return; - } - - defer self.thread_wake_condition.signal(); - - assert(device_channel.units == .Voltage); - const channel_type = NIDaq.getChannelType(device_channel.getChannelName()) orelse return; - - if (channel_type == .analog_input) { - const task = try ni_daq.createTask(null); - errdefer task.clear(); - - const sample_rate = channel_view.sample_rate.?; - try task.createAIVoltageChannel(.{ - .channel = device_channel.getChannelName(), - .min_value = device_channel.min_value, - .max_value = device_channel.max_value, - }); - try task.setContinousSampleRate(.{ - .sample_rate = sample_rate - }); - - try task.start(); - - device_channel.samples.clearRetainingCapacity(); - device_channel.task = ChannelTask{ - .task = task, - .sample_rate = sample_rate, - .read = true - }; - - } else if (channel_type == .analog_output) { - const task = try ni_daq.createTask(null); - errdefer task.clear(); - - const write_pattern = device_channel.write_pattern.items; - const samples_per_channel: u32 = @intCast(write_pattern.len); - - const sample_rate = channel_view.sample_rate.?; - try task.createAOVoltageChannel(.{ - .channel = device_channel.getChannelName(), - .min_value = device_channel.min_value, - .max_value = device_channel.max_value, - }); - try task.setContinousSampleRate(.{ - .sample_rate = sample_rate, - }); - - const write_amount = try task.writeAnalog(.{ - .write_array = write_pattern, - .samples_per_channel = samples_per_channel, - .timeout = -1 - }); - if (write_amount != samples_per_channel) { - return error.WriteAnalog; - } - - try task.start(); - - device_channel.task = ChannelTask{ - .task = task, - .sample_rate = sample_rate, - .read = false - }; - } else { - return; - } - - channel_view.follow = true; -} - -pub fn deactivateDeviceChannel(self: *App, channel_view: *ChannelView) !void { - self.device_channels_mutex.lock(); - defer self.device_channels_mutex.unlock(); - - const device_channel = self.getChannelSourceDevice(channel_view) orelse return; - if (device_channel.task == null) { - return; - } - - device_channel.task.?.deinit(); - device_channel.task = null; -} - -pub fn toggleInputDeviceChannels(self: *App) !void { - self.task_read_active = !self.task_read_active; - - for (self.listChannelViews()) |*channel_view| { - const device_channel = self.getChannelSourceDevice(channel_view) orelse continue; - const channel_name = device_channel.getChannelName(); - const channel_type = NIDaq.getChannelType(channel_name) orelse continue; - - if (channel_type == .analog_input) { - if (self.task_read_active) { - try self.activateDeviceChannel(channel_view); - } else { - try self.deactivateDeviceChannel(channel_view); - } - } - } -} - -pub fn appendChannelFromFile(self: *App, path: []const u8) !void { - self.device_channels_mutex.lock(); - defer self.device_channels_mutex.unlock(); - - const path_dupe = try self.allocator.dupe(u8, path); - errdefer self.allocator.free(path_dupe); - - const file = try std.fs.cwd().openFile(path, .{}); - defer file.close(); - - const samples = try readSamplesFromFile(self.allocator, file); - errdefer self.allocator.free(samples); - - var min_value: f64 = 0; - var max_value: f64 = 0; if (samples.len > 0) { - min_value = samples[0]; - max_value = samples[0]; - + file.min_sample = samples[0]; + file.max_sample = samples[0]; for (samples) |sample| { - min_value = @min(min_value, sample); - max_value = @max(max_value, sample); + file.min_sample = @min(file.min_sample, sample); + file.max_sample = @max(file.max_sample, sample); } } - - const loaded_file_index = findFreeSlot(FileChannel, &self.loaded_files) orelse return error.FileLimitReached; - - self.loaded_files[loaded_file_index] = FileChannel{ - .path = path_dupe, - .samples = samples - }; - errdefer self.loaded_files[loaded_file_index] = null; - - const from: f64 = 0; - const to = @max(@as(f64, @floatFromInt(samples.len)) - 1, 0); - self.channel_views.appendAssumeCapacity(ChannelView{ - .view_rect = .{ - .x_range = RangeF64.init(from, to), - .y_range = RangeF64.init(max_value, min_value) - }, - .x_range = RangeF64.init(from, to), - .y_range = RangeF64.init(max_value, min_value), - .source = .{ .file = loaded_file_index } - }); - errdefer _ = self.channel_views.pop(); } -pub fn appendChannelFromDevice(self: *App, channel_name: []const u8) !void { - const ni_daq = &(self.ni_daq orelse return); +// ---------------- Channels --------------------------------- // - self.device_channels_mutex.lock(); - defer self.device_channels_mutex.unlock(); +pub inline fn getChannel(self: *App, id: Id) ?*Channel { + return self.project.channels.get(id); +} - const device_channel_index = findFreeSlot(DeviceChannel, &self.device_channels) orelse return error.DeviceChannelLimitReached; +pub fn addChannel(self: *App, channel_name: []const u8) !Id { + const id = try self.project.channels.insertUndefined(); + errdefer self.project.channels.remove(id); - const device_name = NIDaq.getDeviceNameFromChannel(channel_name) orelse return; - const device_name_buff = try utils.initBoundedStringZ(NIDaq.BoundedDeviceName, device_name); + const channel = self.project.channels.get(id).?; + channel.* = Channel{ + .name = try utils.initBoundedStringZ(Channel.Name, channel_name) + }; - const channel_name_buff = try utils.initBoundedStringZ(DeviceChannel.ChannelName, channel_name); - const channel_name_z = utils.getBoundedStringZ(&channel_name_buff); + self.loadChannel(id) catch |e| { + log.err("Failed to load channel: {}", .{ e }); + }; - const device = NIDaq.getDeviceNameFromChannel(channel_name) orelse return error.InvalidChannelName; - const device_buff = try NIDaq.BoundedDeviceName.fromSlice(device); - const device_z = device_buff.buffer[0..device_buff.len :0]; + return id; +} - // TODO: Add support for other measurement types - const unit = NIDaq.Unit.Voltage; +fn getAllowedSampleValue(self: *App, id: Id) !RangeF64 { + var ni_daq = &(self.ni_daq orelse return error.MissingNiDaq); + const channel = self.getChannel(id) orelse return error.NoChannel; - var min_value: f64 = 0; - var max_value: f64 = 1; + const channel_name = utils.getBoundedStringZ(&channel.name); + const device_name = utils.getBoundedStringZ(&channel.device); - const channel_type = NIDaq.getChannelType(channel_name_z) orelse return error.UnknownChannelType; - if (channel_type == .analog_output) { - const voltage_ranges = try ni_daq.listDeviceAOVoltageRanges(device_z); - if (voltage_ranges.len > 0) { - min_value = voltage_ranges[0].low; - max_value = voltage_ranges[0].high; - } - } else if (channel_type == .analog_input) { - const voltage_ranges = try ni_daq.listDeviceAIVoltageRanges(device_z); - if (voltage_ranges.len > 0) { - min_value = voltage_ranges[0].low; - max_value = voltage_ranges[0].high; - } - } else { - return error.UnknownChannelType; + const channel_type = NIDaq.getChannelType(channel_name) orelse return error.UnknownChannelType; + const ranges = switch (channel_type) { + .analog_input => try ni_daq.listDeviceAIVoltageRanges(device_name), + .analog_output => try ni_daq.listDeviceAOVoltageRanges(device_name), + else => return error.ChannelTypeNotSupported + }; + + if (ranges.len == 0) { + return error.NoRangesFound; + } else if (ranges.len > 1) { + log.warn("Multiple sample value ranges found, picking first option", .{}); } - const max_sample_rate = try ni_daq.getMaxSampleRate(channel_name_z); + return RangeF64.init(ranges[0].low, ranges[0].high); +} - self.device_channels[device_channel_index] = DeviceChannel{ - .channel_name = channel_name_buff, - .device_name = device_name_buff, - .min_sample_rate = ni_daq.getMinSampleRate(channel_name_z) catch max_sample_rate, - .max_sample_rate = max_sample_rate, - .min_value = min_value, - .max_value = max_value, - .samples = std.ArrayList(f64).init(self.allocator), - .write_pattern = std.ArrayList(f64).init(self.allocator) +fn getAllowedSampleRate(self: *App, id: Id) !RangeF64 { + const ni_daq = self.ni_daq orelse return error.MissingNiDaq; + const channel = self.getChannel(id) orelse return error.NoChannel; + + const channel_name = utils.getBoundedStringZ(&channel.name); + + const max_sample_rate = try ni_daq.getMaxSampleRate(channel_name); + const min_sample_rate = ni_daq.getMinSampleRate(channel_name) catch max_sample_rate; + + return RangeF64.init(min_sample_rate, max_sample_rate); +} + +pub fn loadChannel(self: *App, id: Id) !void { + const channel = self.getChannel(id) orelse return; + channel.clear(self.allocator); + + const channel_name = utils.getBoundedStringZ(&channel.name); + const device_name = NIDaq.getDeviceNameFromChannel(channel_name) orelse unreachable; + channel.device = try utils.initBoundedStringZ(Channel.Device, device_name); + + channel.allowed_sample_values = self.getAllowedSampleValue(id) catch |e| blk: { + log.err("Failed to get allowed sample values: {}", .{ e }); + break :blk null; }; - errdefer self.device_channels[device_channel_index] = null; - self.channel_views.appendAssumeCapacity(ChannelView{ - .view_rect = .{ - .x_range = RangeF64.init(0, 0), - .y_range = RangeF64.init(max_value, min_value) + channel.allowed_sample_rates = self.getAllowedSampleRate(id) catch |e| blk: { + log.err("Failed to get allowed sample rate: {}", .{e}); + break :blk null; + }; +} + +pub fn isChannelOutputing(self: *App, id: Id) bool { + const channel = self.getChannel(id) orelse return false; + + return channel.output_task != null; +} + +// ---------------- Views --------------------------------- // + +pub inline fn getView(self: *App, id: Id) ?*View { + return self.project.views.get(id); +} + +pub fn addView(self: *App, reference: View.Reference) !Id { + const id = try self.project.views.insertUndefined(); + errdefer self.project.views.remove(id); + + const view = self.project.views.get(id).?; + view.* = View{ + .reference = reference + }; + + self.loadView(id) catch |e| { + log.err("Failed to load view: {}", .{ e }); + }; + + return id; +} + +pub fn getViewSamples(self: *App, id: Id) []const f64 { + const empty = &[0]f64{}; + + var result: []const f64 = empty; + + if (self.getView(id)) |view| { + switch (view.reference) { + .channel => |channel_id| if (self.getChannel(channel_id)) |channel| { + result = channel.collected_samples.items; + }, + .file => |file_id| if (self.getFile(file_id)) |file| { + result = file.samples orelse empty; + } + } + } + + return result; +} + +fn refreshViewAvailableXYRanges(self: *App, id: Id) void { + const view = self.getView(id) orelse return; + + switch (view.reference) { + .channel => |channel_id| if (self.getChannel(channel_id)) |channel| { + const allowed_sample_values = channel.allowed_sample_values orelse RangeF64.init(0, 0); + const samples = channel.collected_samples.items; + + view.available_x_range = RangeF64.init(0, @floatFromInt(samples.len)); + view.available_y_range = RangeF64.init(allowed_sample_values.upper, allowed_sample_values.lower); }, - .x_range = RangeF64.init(0, 0), - .y_range = RangeF64.init(max_value, min_value), - .source = .{ .device = device_channel_index }, - .sample_rate = @min(max_sample_rate, 5000), - .unit = unit - }); - errdefer _ = self.channel_views.pop(); + .file => |file_id| if (self.getFile(file_id)) |file| { + const samples = file.samples orelse &[_]f64{ }; + + view.available_x_range = RangeF64.init(0, @floatFromInt(samples.len)); + view.available_y_range = RangeF64.init(file.max_sample, file.min_sample); + } + } +} + +pub fn loadView(self: *App, id: Id) !void { + const view = self.getView(id) orelse return; + view.clear(); + + self.refreshViewAvailableXYRanges(id); + + view.graph_opts.x_range = view.available_x_range; + view.graph_opts.y_range = view.available_y_range; } \ No newline at end of file diff --git a/src/constants.zig b/src/constants.zig new file mode 100644 index 0000000..79787cd --- /dev/null +++ b/src/constants.zig @@ -0,0 +1,7 @@ + + +pub const max_files = 32; + +pub const max_channels = 32; + +pub const max_views = 64; \ No newline at end of file diff --git a/src/graph.zig b/src/graph.zig index 255884b..83df29c 100644 --- a/src/graph.zig +++ b/src/graph.zig @@ -20,10 +20,28 @@ comptime { } pub const ViewOptions = struct { - x_range: RangeF64, - y_range: RangeF64, + x_range: RangeF64 = RangeF64.init(0, 0), + y_range: RangeF64 = RangeF64.init(0, 0), color: rl.Color = srcery.red, + + fn writeStruct(self: ViewOptions, writer: anytype) !void { + _ = self; + _ = writer; + // try writer.writeStructEndian(self.id, file_endian); + // try writer.writeStructEndian(self.channel_name.constSlice(), file_endian); + } + + fn readStruct(reader: anytype) !ViewOptions { + _ = reader; + // const id = try reader.readStructEndian(Id, file_endian); + // const channel_name = try reader.readStructEndian([]const u8, file_endian); + + return ViewOptions{ + .x_range = RangeF64.init(0, 0), + .y_range = RangeF64.init(0, 0), + }; + } }; pub const Cache = struct { @@ -35,7 +53,7 @@ pub const Cache = struct { texture: ?rl.RenderTexture2D = null, key: ?Key = null, - pub fn deinit(self: *Cache) void { + pub fn clear(self: *Cache) void { if (self.texture) |texture| { texture.unload(); self.texture = null; diff --git a/src/main.zig b/src/main.zig index 8d3d64d..a74ac9e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -71,7 +71,9 @@ pub fn main() !void { raylib_h.SetTraceLogCallback(raylibTraceLogCallback); rl.setTraceLogLevel(toRaylibLogLevel(std.options.log_level)); - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa = std.heap.GeneralPurposeAllocator(.{ + .thread_safe = true + }){}; const allocator = gpa.allocator(); defer _ = gpa.deinit(); @@ -100,8 +102,22 @@ pub fn main() !void { defer app.deinit(); if (builtin.mode == .Debug) { - try app.appendChannelFromDevice("Dev1/ai0"); - try app.appendChannelFromDevice("Dev3/ao0"); + // const cwd_path = try std.fs.cwd().realpathAlloc(allocator, "."); + // defer allocator.free(cwd_path); + // try app.loadProject(cwd_path); + + _ = try app.addView(.{ + .channel = try app.addChannel("Dev1/ai0") + }); + _ = try app.addView(.{ + .channel = try app.addChannel("Dev3/ao0") + }); + _ = try app.addView(.{ + .file = try app.addFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_I.bin") + }); + + // try app.appendChannelFromDevice("Dev1/ai0"); + // try app.appendChannelFromDevice("Dev3/ao0"); // try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_I.bin"); // try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_IjStim.bin"); // try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_IjStim.bin"); diff --git a/src/ni-daq/root.zig b/src/ni-daq/root.zig index 67d0df2..b952ad1 100644 --- a/src/ni-daq/root.zig +++ b/src/ni-daq/root.zig @@ -5,7 +5,7 @@ pub const c = Api.c; const assert = std.debug.assert; const log = std.log.scoped(.ni_daq); -const max_device_name_size = 255; +pub const max_device_name_size = 255; const max_task_name_size = 255; pub const max_channel_name_size = count: { diff --git a/src/range.zig b/src/range.zig index b5afb3f..8e4542b 100644 --- a/src/range.zig +++ b/src/range.zig @@ -4,7 +4,7 @@ const remap_number = @import("./utils.zig").remap; const assert = std.debug.assert; pub fn Range(Number: type) type { - return struct { + return packed struct { const Self = @This(); lower: Number, diff --git a/src/screens/main_screen.zig b/src/screens/main_screen.zig index fe9bac8..3a53ced 100644 --- a/src/screens/main_screen.zig +++ b/src/screens/main_screen.zig @@ -13,15 +13,15 @@ const NIDaq = @import("../ni-daq/root.zig"); const MainScreen = @This(); const log = std.log.scoped(.main_screen); -const ChannelView = App.ChannelView; const assert = std.debug.assert; const remap = utils.remap; +const Id = App.Id; const zoom_speed = 0.1; const ruler_size = UI.Sizing.initFixed(.{ .pixels = 32 }); -const ChannelCommand = struct { - channel: *ChannelView, +const ViewCommand = struct { + view_id: Id, updated_at_ns: i128, action: union(enum) { move_and_zoom: struct { @@ -32,23 +32,23 @@ const ChannelCommand = struct { }; app: *App, -fullscreen_channel: ?*ChannelView = null, +fullscreen_view: ?Id = null, axis_zoom: ?struct { - channel: *ChannelView, + view_id: Id, axis: UI.Axis, start: f64, } = null, // TODO: Redo -channel_undo_stack: std.BoundedArray(ChannelCommand, 100) = .{}, +view_undo_stack: std.BoundedArray(ViewCommand, 100) = .{}, -protocol_modal: ?*ChannelView = null, +protocol_modal: ?Id = null, frequency_input: UI.TextInputStorage, amplitude_input: UI.TextInputStorage, protocol_error_message: ?[]const u8 = null, protocol_graph_cache: Graph.Cache = .{}, -preview_samples: std.ArrayList(f64), +preview_samples: std.ArrayListUnmanaged(f64) = .{}, preview_samples_y_range: RangeF64 = RangeF64.init(0, 0), pub fn init(app: *App) !MainScreen { @@ -58,7 +58,6 @@ pub fn init(app: *App) !MainScreen { .app = app, .frequency_input = UI.TextInputStorage.init(allocator), .amplitude_input = UI.TextInputStorage.init(allocator), - .preview_samples = std.ArrayList(f64).init(allocator) }; try self.frequency_input.setText("10"); @@ -68,16 +67,21 @@ pub fn init(app: *App) !MainScreen { } pub fn deinit(self: *MainScreen) void { + const allocator = self.app.allocator; + self.frequency_input.deinit(); self.amplitude_input.deinit(); - self.preview_samples.deinit(); + self.preview_samples.clearAndFree(allocator); self.clearProtocolErrorMessage(); } -fn pushChannelMoveCommand(self: *MainScreen, channel_view: *ChannelView, x_range: RangeF64, y_range: RangeF64) void { +fn pushViewMoveCommand(self: *MainScreen, view_id: Id, x_range: RangeF64, y_range: RangeF64) void { + const view = self.app.getView(view_id) orelse return; + const view_rect = &view.graph_opts; + const now_ns = std.time.nanoTimestamp(); - var undo_stack = &self.channel_undo_stack; + var undo_stack = &self.view_undo_stack; var push_new_command = true; if (undo_stack.len > 0) { @@ -88,15 +92,13 @@ fn pushChannelMoveCommand(self: *MainScreen, channel_view: *ChannelView, x_range } } - var view_rect = &channel_view.view_rect; - if (push_new_command) { if (undo_stack.unusedCapacitySlice().len == 0) { _ = undo_stack.orderedRemove(0); } - undo_stack.appendAssumeCapacity(ChannelCommand{ - .channel = channel_view, + undo_stack.appendAssumeCapacity(ViewCommand{ + .view_id = view_id, .updated_at_ns = now_ns, .action = .{ .move_and_zoom = .{ @@ -109,24 +111,39 @@ fn pushChannelMoveCommand(self: *MainScreen, channel_view: *ChannelView, x_range view_rect.x_range = x_range; view_rect.y_range = y_range; - channel_view.follow = false; + view.follow = false; } -fn pushChannelMoveCommandAxis(self: *MainScreen, channel_view: *ChannelView, axis: UI.Axis, view_range: RangeF64) void { +fn pushViewMoveCommandAxis(self: *MainScreen, view_id: Id, axis: UI.Axis, view_range: RangeF64) void { + const view = self.app.getView(view_id) orelse return; + const view_rect = &view.graph_opts; + if (axis == .X) { - const view_rect = &channel_view.view_rect; - self.pushChannelMoveCommand(channel_view, view_range, view_rect.y_range); + self.pushViewMoveCommand(view_id, view_range, view_rect.y_range); } else { - const view_rect = &channel_view.view_rect; - self.pushChannelMoveCommand(channel_view, view_rect.x_range, view_range); + self.pushViewMoveCommand(view_id, view_rect.x_range, view_range); } } -fn showChannelViewGraph(self: *MainScreen, channel_view: *ChannelView) *UI.Box { +fn undoLastMoveCommand(self: *MainScreen) void { + const command = self.view_undo_stack.popOrNull() orelse return; + const view = self.app.getView(command.view_id) orelse return; + const view_rect = &view.graph_opts; + + switch (command.action) { + .move_and_zoom => |args| { + view_rect.x_range = args.before_x; + view_rect.y_range = args.before_y; + } + } +} + +fn showChannelViewGraph(self: *MainScreen, view_id: Id) *UI.Box { var ui = &self.app.ui; - const samples = self.app.getChannelSamples(channel_view); - const view_rect: *Graph.ViewOptions = &channel_view.view_rect; + const view = self.app.getView(view_id).?; + const samples = self.app.getViewSamples(view_id); + const view_opts = &view.graph_opts; const graph_box = ui.createBox(.{ .key = ui.keyFromString("Graph"), @@ -151,18 +168,18 @@ fn showChannelViewGraph(self: *MainScreen, channel_view: *ChannelView) *UI.Box { const mouse_y_range = RangeF64.init(0, graph_rect.height); if (signal.hot) { - sample_index_under_mouse = mouse_x_range.remapTo(view_rect.x_range, signal.relative_mouse.x); - sample_value_under_mouse = mouse_y_range.remapTo(view_rect.y_range, signal.relative_mouse.y); + sample_index_under_mouse = mouse_x_range.remapTo(view_opts.x_range, signal.relative_mouse.x); + sample_value_under_mouse = mouse_y_range.remapTo(view_opts.y_range, signal.relative_mouse.y); } if (signal.dragged()) { - const x_offset = mouse_x_range.remapTo(RangeF64.init(0, view_rect.x_range.size()), signal.drag.x); - const y_offset = mouse_y_range.remapTo(RangeF64.init(0, view_rect.y_range.size()), signal.drag.y); + const x_offset = mouse_x_range.remapTo(RangeF64.init(0, view_opts.x_range.size()), signal.drag.x); + const y_offset = mouse_y_range.remapTo(RangeF64.init(0, view_opts.y_range.size()), signal.drag.y); - self.pushChannelMoveCommand( - channel_view, - view_rect.x_range.sub(x_offset), - view_rect.y_range.add(y_offset) + self.pushViewMoveCommand( + view_id, + view_opts.x_range.sub(x_offset), + view_opts.y_range.add(y_offset) ); } @@ -174,23 +191,23 @@ fn showChannelViewGraph(self: *MainScreen, channel_view: *ChannelView) *UI.Box { scale_factor += zoom_speed; } - self.pushChannelMoveCommand( - channel_view, - view_rect.x_range.zoom(sample_index_under_mouse.?, scale_factor), - view_rect.y_range.zoom(sample_value_under_mouse.?, scale_factor) + self.pushViewMoveCommand( + view_id, + view_opts.x_range.zoom(sample_index_under_mouse.?, scale_factor), + view_opts.y_range.zoom(sample_value_under_mouse.?, scale_factor) ); } if (signal.flags.contains(.middle_clicked)) { - self.pushChannelMoveCommand(channel_view, channel_view.x_range, channel_view.y_range); + self.pushViewMoveCommand(view_id, view.available_x_range, view.available_y_range); } - Graph.drawCached(&channel_view.view_cache, graph_box.persistent.size, view_rect.*, samples); - if (channel_view.view_cache.texture) |texture| { + Graph.drawCached(&view.graph_cache, graph_box.persistent.size, view_opts.*, samples); + if (view.graph_cache.texture) |texture| { graph_box.texture = texture.texture; } - if (view_rect.x_range.size() == 0 or view_rect.y_range.size() == 0) { + if (view_opts.x_range.size() == 0 or view_opts.y_range.size() == 0) { graph_box.setText(""); graph_box.text_color = srcery.hard_black; graph_box.font = .{ @@ -203,7 +220,8 @@ fn showChannelViewGraph(self: *MainScreen, channel_view: *ChannelView) *UI.Box { } fn getLineOnRuler( - channel_view: *ChannelView, + self: *MainScreen, + view_id: Id, ruler: *UI.Box, axis: UI.Axis, @@ -211,16 +229,16 @@ fn getLineOnRuler( cross_axis_pos: f64, cross_axis_size: f64 ) rl.Rectangle { + const view = self.app.getView(view_id).?; + const shown_size = view.getGraphView(axis).size(); - const view_rect = channel_view.view_rect; - - const along_axis_size = switch (axis) { - .X => view_rect.x_range.size()/ruler.persistent.size.x, - .Y => view_rect.y_range.size()/ruler.persistent.size.y, + const along_axis_size = shown_size / switch (axis) { + .X => ruler.persistent.size.x, + .Y => ruler.persistent.size.y, }; - return getRectOnRuler( - channel_view, + return self.getRectOnRuler( + view_id, ruler, axis, @@ -232,7 +250,8 @@ fn getLineOnRuler( } fn getRectOnRuler( - channel_view: *ChannelView, + self: *MainScreen, + view_id: Id, ruler: *UI.Box, axis: UI.Axis, @@ -246,7 +265,9 @@ fn getRectOnRuler( const rect = ruler.rect(); const rect_height: f64 = @floatCast(rect.height); const rect_width: f64 = @floatCast(rect.width); - const view_range = channel_view.getViewRange(axis); + + const view = self.app.getView(view_id).?; + const view_range = view.getGraphView(axis).*; if (axis == .X) { const width_range = RangeF64.init(0, rect.width); @@ -283,7 +304,7 @@ fn getRectOnRuler( fn showRulerTicksRange( self: *MainScreen, - channel_view: *ChannelView, + view_id: Id, ruler: *UI.Box, axis: UI.Axis, @@ -297,15 +318,17 @@ fn showRulerTicksRange( while (marker < to) : (marker += step) { _ = self.app.ui.createBox(.{ .background = srcery.yellow, - .float_rect = getLineOnRuler(channel_view, ruler, axis, marker, 0, marker_size), + .float_rect = self.getLineOnRuler(view_id, ruler, axis, marker, 0, marker_size), .float_relative_to = ruler }); } } -fn showRulerTicks(self: *MainScreen, channel_view: *ChannelView, axis: UI.Axis) void { - const view_range = channel_view.getViewRange(axis); - const full_range = channel_view.getSampleRange(axis); +fn showRulerTicks(self: *MainScreen, view_id: Id, axis: UI.Axis) void { + const view = self.app.getView(view_id).?; + + const view_range = view.getGraphView(axis); + const full_range = view.getAvailableView(axis); var ui = &self.app.ui; const ruler = ui.parentBox().?; @@ -351,7 +374,7 @@ fn showRulerTicks(self: *MainScreen, channel_view: *ChannelView, axis: UI.Axis) { _ = self.app.ui.createBox(.{ .background = srcery.yellow, - .float_rect = getLineOnRuler(channel_view, ruler, axis, full_range.lower, 0, 0.75), + .float_rect = self.getLineOnRuler(view_id, ruler, axis, full_range.lower, 0, 0.75), .float_relative_to = ruler }); } @@ -359,7 +382,7 @@ fn showRulerTicks(self: *MainScreen, channel_view: *ChannelView, axis: UI.Axis) { _ = self.app.ui.createBox(.{ .background = srcery.yellow, - .float_rect = getLineOnRuler(channel_view, ruler, axis, full_range.upper, 0, 0.75), + .float_rect = self.getLineOnRuler(view_id, ruler, axis, full_range.upper, 0, 0.75), .float_relative_to = ruler }); } @@ -367,15 +390,15 @@ fn showRulerTicks(self: *MainScreen, channel_view: *ChannelView, axis: UI.Axis) if (full_range.hasExclusive(0)) { _ = ui.createBox(.{ .background = srcery.yellow, - .float_rect = getLineOnRuler(channel_view, ruler, axis, 0, 0, 0.75), + .float_rect = self.getLineOnRuler(view_id, ruler, axis, 0, 0, 0.75), .float_relative_to = ruler }); } - const ticks_range = view_range.grow(step).intersectPositive(full_range.*); + const ticks_range = view_range.grow(step).intersectPositive(full_range); self.showRulerTicksRange( - channel_view, + view_id, ruler, axis, utils.roundNearestTowardZero(f64, ticks_range.lower, step) + step/2, @@ -385,7 +408,7 @@ fn showRulerTicks(self: *MainScreen, channel_view: *ChannelView, axis: UI.Axis) ); self.showRulerTicksRange( - channel_view, + view_id, ruler, axis, utils.roundNearestTowardZero(f64, ticks_range.lower, step), @@ -415,13 +438,15 @@ fn addRulerPlaceholder(self: *MainScreen, key: UI.Key, axis: UI.Axis) *UI.Box { return ruler; } -fn showRuler(self: *MainScreen, ruler: *UI.Box, graph_box: *UI.Box, channel_view: *ChannelView, axis: UI.Axis) void { +fn showRuler(self: *MainScreen, ruler: *UI.Box, graph_box: *UI.Box, view_id: Id, axis: UI.Axis) void { var ui = &self.app.ui; + const view = self.app.getView(view_id) orelse return; + ruler.beginChildren(); defer ruler.endChildren(); - self.showRulerTicks(channel_view, axis); + self.showRulerTicks(view_id, axis); const signal = ui.signal(ruler); const mouse_position = switch (axis) { @@ -432,7 +457,7 @@ fn showRuler(self: *MainScreen, ruler: *UI.Box, graph_box: *UI.Box, channel_view .X => RangeF64.init(0, ruler.persistent.size.x), .Y => RangeF64.init(0, ruler.persistent.size.y) }; - const view_range = channel_view.getViewRange(axis); + const view_range = view.getGraphView(axis); const mouse_position_on_graph = mouse_range.remapTo(view_range.*, mouse_position); var zoom_start: ?f64 = null; @@ -440,7 +465,7 @@ fn showRuler(self: *MainScreen, ruler: *UI.Box, graph_box: *UI.Box, channel_view var is_zooming: bool = false; if (self.axis_zoom) |axis_zoom| { - is_zooming = axis_zoom.channel == channel_view and axis_zoom.axis == axis; + is_zooming = axis_zoom.view_id.eql(view_id) and axis_zoom.axis == axis; } if (signal.hot and view_range.size() > 0) { @@ -448,12 +473,14 @@ fn showRuler(self: *MainScreen, ruler: *UI.Box, graph_box: *UI.Box, channel_view mouse_tooltip.beginChildren(); defer mouse_tooltip.endChildren(); - if (channel_view.getSampleRange(axis).hasInclusive(mouse_position_on_graph)) { - if (axis == .Y and channel_view.unit != null) { - const unit_name = channel_view.unit.?.name() orelse "Unknown"; + const project = &self.app.project; + + if (view.getAvailableView(axis).hasInclusive(mouse_position_on_graph)) { + if (axis == .Y and view.unit != null) { + const unit_name = view.unit.?.name() orelse "Unknown"; _ = ui.label("{s}: {d:.3}", .{unit_name, mouse_position_on_graph}); - } else if (axis == .X and channel_view.sample_rate != null) { - const sample_rate = channel_view.sample_rate.?; + } else if (axis == .X and project.sample_rate != null) { + const sample_rate = project.sample_rate.?; _ = ui.label("{d:.3}s", .{mouse_position_on_graph / sample_rate}); } else { _ = ui.label("{d:.3}", .{mouse_position_on_graph}); @@ -467,7 +494,7 @@ fn showRuler(self: *MainScreen, ruler: *UI.Box, graph_box: *UI.Box, channel_view self.axis_zoom = .{ .axis = axis, .start = mouse_position_on_graph, - .channel = channel_view + .view_id = view_id }; } @@ -479,13 +506,13 @@ fn showRuler(self: *MainScreen, ruler: *UI.Box, graph_box: *UI.Box, channel_view if (zoom_start != null) { _ = ui.createBox(.{ .background = srcery.green, - .float_rect = getLineOnRuler(channel_view, ruler, axis, zoom_start.?, 0, 1), + .float_rect = self.getLineOnRuler(view_id, ruler, axis, zoom_start.?, 0, 1), .float_relative_to = ruler, }); _ = ui.createBox(.{ .background = srcery.green, - .float_rect = getLineOnRuler(channel_view, graph_box, axis, zoom_start.?, 0, 1), + .float_rect = self.getLineOnRuler(view_id, graph_box, axis, zoom_start.?, 0, 1), .float_relative_to = graph_box, .parent = graph_box }); @@ -494,13 +521,13 @@ fn showRuler(self: *MainScreen, ruler: *UI.Box, graph_box: *UI.Box, channel_view if (zoom_end != null) { _ = ui.createBox(.{ .background = srcery.green, - .float_rect = getLineOnRuler(channel_view, ruler, axis, zoom_end.?, 0, 1), + .float_rect = self.getLineOnRuler(view_id, ruler, axis, zoom_end.?, 0, 1), .float_relative_to = ruler, }); _ = ui.createBox(.{ .background = srcery.green, - .float_rect = getLineOnRuler(channel_view, graph_box, axis, zoom_end.?, 0, 1), + .float_rect = self.getLineOnRuler(view_id, graph_box, axis, zoom_end.?, 0, 1), .float_relative_to = graph_box, .parent = graph_box }); @@ -510,8 +537,8 @@ fn showRuler(self: *MainScreen, ruler: *UI.Box, graph_box: *UI.Box, channel_view _ = ui.createBox(.{ .background = srcery.green.alpha(0.5), .float_relative_to = ruler, - .float_rect = getRectOnRuler( - channel_view, + .float_rect = self.getRectOnRuler( + view_id, ruler, axis, zoom_start.?, @@ -530,7 +557,7 @@ fn showRuler(self: *MainScreen, ruler: *UI.Box, graph_box: *UI.Box, channel_view scale_factor += zoom_speed; } const new_view_range = view_range.zoom(mouse_position_on_graph, scale_factor); - self.pushChannelMoveCommandAxis(channel_view, axis, new_view_range); + self.pushViewMoveCommandAxis(view_id, axis, new_view_range); } if (is_zooming and signal.flags.contains(.left_released)) { @@ -548,26 +575,26 @@ fn showRuler(self: *MainScreen, ruler: *UI.Box, graph_box: *UI.Box, channel_view new_view_range = new_view_range.flip(); } - self.pushChannelMoveCommandAxis(channel_view, axis, new_view_range); + self.pushViewMoveCommandAxis(view_id, axis, new_view_range); } } self.axis_zoom = null; } } -fn showChannelView(self: *MainScreen, channel_view: *ChannelView, height: UI.Sizing) !void { +fn showView(self: *MainScreen, view_id: Id, height: UI.Sizing) !void { var ui = &self.app.ui; const show_ruler = true; - const channel_view_box = ui.createBox(.{ - .key = UI.Key.initPtr(channel_view), + const view_box = ui.createBox(.{ + .key = UI.Key.initUsize(view_id.asInt()), .layout_direction = .top_to_bottom, .size_x = UI.Sizing.initGrowFull(), .size_y = height }); - channel_view_box.beginChildren(); - defer channel_view_box.endChildren(); + view_box.beginChildren(); + defer view_box.endChildren(); const toolbar = ui.createBox(.{ .layout_direction = .left_to_right, @@ -580,21 +607,26 @@ fn showChannelView(self: *MainScreen, channel_view: *ChannelView, height: UI.Siz toolbar.beginChildren(); defer toolbar.endChildren(); - if (self.app.getChannelSourceDevice(channel_view)) |device_channel| { - const channel_name = device_channel.getChannelName(); + var view_name: ?[]const u8 = null; + + const view = self.app.getView(view_id).?; + if (view.reference == .channel) { + const channel_id = view.reference.channel; + const channel = self.app.getChannel(channel_id).?; + const channel_name = utils.getBoundedStringZ(&channel.name); const channel_type = NIDaq.getChannelType(channel_name).?; { const follow = ui.textButton("Follow"); follow.background = srcery.hard_black; - if (channel_view.follow) { + if (view.follow) { follow.borders = UI.Borders.bottom(.{ .color = srcery.green, .size = 4 }); } if (ui.signal(follow).clicked()) { - channel_view.follow = !channel_view.follow; + view.follow = !view.follow; } } @@ -605,17 +637,17 @@ fn showChannelView(self: *MainScreen, channel_view: *ChannelView, height: UI.Siz const signal = ui.signal(button); if (signal.clicked()) { - if (self.app.isDeviceChannelActive(channel_view)) { - self.app.deferred_actions.appendAssumeCapacity(.{ - .deactivate = channel_view + if (self.app.isChannelOutputing(channel_id)) { + self.app.pushCommand(.{ + .stop_output = channel_id }); } else { - try self.openProtocolModal(channel_view); + try self.openProtocolModal(channel_id); } } var color = rl.Color.white; - if (self.app.isDeviceChannelActive(channel_view)) { + if (self.app.isChannelOutputing(channel_id)) { color = srcery.red; } @@ -628,22 +660,29 @@ fn showChannelView(self: *MainScreen, channel_view: *ChannelView, height: UI.Siz } } + view_name = channel_name; + } else if (view.reference == .file) { + const file_id = view.reference.file; + const file = self.app.getFile(file_id).?; + + view_name = std.fs.path.stem(file.path); + } + + if (view_name) |text| { _ = ui.createBox(.{ .size_x = UI.Sizing.initGrowFull() }); - { - const label = ui.label("{s}", .{channel_name}); - label.size.y = UI.Sizing.initGrowFull(); - label.alignment.x = .center; - label.alignment.y = .center; - label.padding = UI.Padding.horizontal(ui.rem(1)); - } + const label = ui.label("{s}", .{text}); + label.size.y = UI.Sizing.initGrowFull(); + label.alignment.x = .center; + label.alignment.y = .center; + label.padding = UI.Padding.horizontal(ui.rem(1)); } } if (!show_ruler) { - _ = self.showChannelViewGraph(channel_view); + _ = self.showChannelViewGraph(view_id); } else { var graph_box: *UI.Box = undefined; @@ -661,7 +700,7 @@ fn showChannelView(self: *MainScreen, channel_view: *ChannelView, height: UI.Siz y_ruler = self.addRulerPlaceholder(ui.keyFromString("Y ruler"), .Y); - graph_box = self.showChannelViewGraph(channel_view); + graph_box = self.showChannelViewGraph(view_id); } { @@ -684,35 +723,37 @@ fn showChannelView(self: *MainScreen, channel_view: *ChannelView, height: UI.Siz .texture_size = .{ .x = 28, .y = 28 } }); if (ui.signal(fullscreen).clicked()) { - if (self.fullscreen_channel != null and self.fullscreen_channel.? == channel_view) { - self.fullscreen_channel = null; + if (self.fullscreen_view != null and self.fullscreen_view.?.eql(view_id)) { + self.fullscreen_view = null; } else { - self.fullscreen_channel = channel_view; + self.fullscreen_view = view_id; } } x_ruler = self.addRulerPlaceholder(ui.keyFromString("X ruler"), .X); } - self.showRuler(x_ruler, graph_box, channel_view, .X); - self.showRuler(y_ruler, graph_box, channel_view, .Y); + self.showRuler(x_ruler, graph_box, view_id, .X); + self.showRuler(y_ruler, graph_box, view_id, .Y); } } -fn openProtocolModal(self: *MainScreen, channel_view: *ChannelView) !void { - self.protocol_modal = channel_view; - self.protocol_graph_cache.deinit(); +fn openProtocolModal(self: *MainScreen, channel_id: Id) !void { + self.protocol_modal = channel_id; + self.protocol_graph_cache.clear(); } fn closeModal(self: *MainScreen) void { self.protocol_modal = null; } -pub fn showProtocolModal(self: *MainScreen, channel_view: *ChannelView) !void { +pub fn showProtocolModal(self: *MainScreen, channel_id: Id) !void { var ui = &self.app.ui; - const device_channel = self.app.getChannelSourceDevice(channel_view).?; + const allocator = self.app.allocator; + const channel = self.app.getChannel(channel_id) orelse return; + const sample_rate = self.app.project.getSampleRate() orelse return; const container = ui.createBox(.{ .key = ui.keyFromString("Protocol modal"), @@ -814,7 +855,7 @@ pub fn showProtocolModal(self: *MainScreen, channel_view: *ChannelView) !void { } if (self.protocol_error_message == null and any_input_modified) { - try App.DeviceChannel.generateSine(&self.preview_samples, channel_view.sample_rate.?, frequency, amplitude); + try App.Channel.generateSine(&self.preview_samples, allocator, sample_rate, frequency, amplitude); self.preview_samples_y_range = RangeF64.init(-amplitude*1.1, amplitude*1.1); self.protocol_graph_cache.invalidate(); } @@ -850,11 +891,9 @@ pub fn showProtocolModal(self: *MainScreen, channel_view: *ChannelView) !void { if (ui.signal(btn).clicked() or ui.isKeyboardPressed(.key_enter)) { if (self.protocol_error_message == null) { - try App.DeviceChannel.generateSine(&device_channel.write_pattern, channel_view.sample_rate.?, frequency, amplitude); + try App.Channel.generateSine(&channel.write_pattern, allocator, sample_rate, frequency, amplitude); - self.app.deferred_actions.appendAssumeCapacity(.{ - .activate = channel_view - }); + self.app.pushCommand(.{ .start_output = channel_id }); self.protocol_modal = null; } } @@ -885,30 +924,22 @@ pub fn tick(self: *MainScreen) !void { if (ui.isKeyboardPressed(.key_escape)) { if (self.protocol_modal != null) { self.closeModal(); - } else if (self.fullscreen_channel != null) { - self.fullscreen_channel = null; + } else if (self.fullscreen_view != null) { + self.fullscreen_view = null; } else { self.app.should_close = true; } } if (ui.isCtrlDown() and ui.isKeyboardPressed(.key_z)) { - if (self.channel_undo_stack.popOrNull()) |command| { - switch (command.action) { - .move_and_zoom => |args| { - const view_rect = &command.channel.view_rect; - view_rect.x_range = args.before_x; - view_rect.y_range = args.before_y; - } - } - } + self.undoLastMoveCommand(); } const root = ui.parentBox().?; root.layout_direction = .top_to_bottom; var maybe_modal_overlay: ?*UI.Box = null; - if (self.protocol_modal) |channel_view| { + if (self.protocol_modal) |channel_id| { const padding = UI.Padding.all(ui.rem(2)); const modal_overlay = ui.createBox(.{ .key = ui.keyFromString("Overlay"), @@ -928,7 +959,7 @@ pub fn tick(self: *MainScreen) !void { modal_overlay.beginChildren(); defer modal_overlay.endChildren(); - try self.showProtocolModal(channel_view); + try self.showProtocolModal(channel_id); if (ui.signal(modal_overlay).clicked()) { self.closeModal(); @@ -950,26 +981,39 @@ pub fn tick(self: *MainScreen) !void { toolbar.beginChildren(); defer toolbar.endChildren(); - var start_all = ui.textButton("Start/Stop button"); - start_all.borders = UI.Borders.all(.{ .size = 4, .color = srcery.red }); - start_all.background = srcery.black; - start_all.size.y = UI.Sizing.initFixed(.{ .parent_percent = 1 }); - start_all.padding.top = 0; - start_all.padding.bottom = 0; - if (ui.signal(start_all).clicked()) { - self.app.deferred_actions.appendAssumeCapacity(.{ - .toggle_input_channels = {} - }); + { + var btn = ui.textButton("Start/Stop button"); + btn.borders = UI.Borders.all(.{ .size = 4, .color = srcery.red }); + btn.background = srcery.black; + btn.size.y = UI.Sizing.initFixed(.{ .parent_percent = 1 }); + btn.padding.top = 0; + btn.padding.bottom = 0; + if (ui.signal(btn).clicked()) { + if (self.app.isCollectionInProgress()) { + self.app.pushCommand(.stop_collection); + } else { + self.app.pushCommand(.start_collection); + } + } + + if (self.app.isCollectionInProgress()) { + btn.setText("Stop"); + } else { + btn.setText("Start"); + } } - if (self.app.task_read_active) { - start_all.setText("Stop"); - } else { - start_all.setText("Start"); + + { + var btn = ui.textButton("Save"); + btn.borders = UI.Borders.all(.{ .size = 4, .color = srcery.green }); + if (ui.signal(btn).clicked()) { + self.app.pushCommand(.save_project); + } } } - if (self.fullscreen_channel) |channel| { - try self.showChannelView(channel, UI.Sizing.initGrowFull()); + if (self.fullscreen_view) |view_id| { + try self.showView(view_id, UI.Sizing.initGrowFull()); } else { const scroll_area = ui.beginScrollbar(ui.keyFromString("Channels")); @@ -977,8 +1021,10 @@ pub fn tick(self: *MainScreen) !void { scroll_area.layout_direction = .top_to_bottom; scroll_area.layout_gap = 4; - for (self.app.listChannelViews()) |*channel_view| { - try self.showChannelView(channel_view, UI.Sizing.initFixed(.{ .pixels = channel_view.height })); + var view_iter = self.app.project.views.idIterator(); + while (view_iter.next()) |view_id| { + const view = self.app.getView(view_id); + try self.showView(view_id, UI.Sizing.initFixed(.{ .pixels = view.?.height })); } { @@ -995,24 +1041,13 @@ pub fn tick(self: *MainScreen) !void { const add_from_file = ui.textButton("Add from file"); add_from_file.borders = UI.Borders.all(.{ .size = 2, .color = srcery.green }); if (ui.signal(add_from_file).clicked()) { - self.app.samples_mutex.unlock(); - defer self.app.samples_mutex.lock(); - - if (Platform.openFilePicker(self.app.allocator)) |filename| { - defer self.app.allocator.free(filename); - - // TODO: Handle error - self.app.appendChannelFromFile(filename) catch @panic("Failed to append channel from file"); - } else |err| { - // TODO: Show error message to user; - log.err("Failed to pick file: {}", .{ err }); - } + self.app.pushCommand(.add_file_from_picker); } const add_from_device = ui.textButton("Add from device"); add_from_device.borders = UI.Borders.all(.{ .size = 2, .color = srcery.green }); if (ui.signal(add_from_device).clicked()) { - self.app.current_screen = .channel_from_device; + self.app.screen = .add_channels; } } } @@ -1020,23 +1055,4 @@ pub fn tick(self: *MainScreen) !void { if (maybe_modal_overlay) |modal_overlay| { root.bringChildToTop(modal_overlay); } - - if (self.app.task_read_active) { - for (self.app.listChannelViews()) |*channel_view| { - const device_channel = self.app.getChannelSourceDevice(channel_view) orelse continue; - if (!channel_view.follow) continue; - - const channel_task = device_channel.task orelse continue; - - const sample_rate = channel_task.sample_rate; - const sample_count: f32 = @floatFromInt(device_channel.samples.items.len); - - channel_view.view_rect.y_range = channel_view.y_range; - - channel_view.view_rect.x_range.lower = 0; - if (sample_count > channel_view.view_rect.x_range.upper) { - channel_view.view_rect.x_range.upper = sample_count + @as(f32, @floatCast(sample_rate)) * 10; - } - } - } } \ No newline at end of file