const std = @import("std"); const rl = @import("raylib"); const srcery = @import("./srcery.zig"); const UI = @import("./ui.zig"); const Platform = @import("./platform.zig"); const Assets = @import("./assets.zig"); const Graph = @import("./graph.zig"); const NIDaq = @import("ni-daq/root.zig"); const rect_utils = @import("./rect-utils.zig"); const remap = @import("./utils.zig").remap; const TaskPool = @import("ni-daq/task-pool.zig"); const log = std.log.scoped(.app); const assert = std.debug.assert; const clamp = std.math.clamp; const App = @This(); const max_channels = 64; const max_files = 32; const FileChannel = struct { path: []u8, min_value: f64, max_value: f64, samples: []f64, fn deinit(self: FileChannel, allocator: std.mem.Allocator) void { allocator.free(self.path); allocator.free(self.samples); } }; const DeviceChannel = struct { const Name = std.BoundedArray(u8, NIDaq.max_channel_name_size + 1); // +1 for null byte name: Name = .{}, mutex: std.Thread.Mutex = .{}, samples: std.ArrayList(f64), units: i32 = NIDaq.c.DAQmx_Val_Volts, min_sample_rate: f64, max_sample_rate: f64, min_value: f64, max_value: f64, active_task: ?*TaskPool.Entry = null, fn deinit(self: DeviceChannel) void { self.samples.deinit(); } }; const ChannelView = struct { view_cache: Graph.Cache = .{}, view_rect: Graph.ViewOptions, follow: bool = false, height: f32 = 150, source: union(enum) { file: usize, device: usize }, const SourceObject = union(enum) { file: *FileChannel, device: *DeviceChannel, fn samples(self: SourceObject) []const f64 { return switch (self) { .file => |file| file.samples, .device => |device| device.samples.items, }; } fn lockSamples(self: SourceObject) void { if (self == .device) { self.device.mutex.lock(); } } fn unlockSamples(self: SourceObject) void { if (self == .device) { self.device.mutex.unlock(); } } }; }; allocator: std.mem.Allocator, ui: UI, channel_views: std.BoundedArray(ChannelView, max_channels) = .{}, loaded_files: [max_channels]?FileChannel = .{ null } ** max_channels, device_channels: [max_channels]?DeviceChannel = .{ null } ** max_channels, ni_daq_api: ?NIDaq.Api = null, ni_daq: ?NIDaq = null, task_pool: TaskPool, shown_window: enum { channels, add_from_device } = .add_from_device, shown_modal: ?union(enum) { no_library_error, library_version_error: std.SemanticVersion, library_version_warning: std.SemanticVersion } = null, device_filter: NIDaq.BoundedDeviceName = .{}, channel_type_filter: ?NIDaq.ChannelType = null, selected_channels: std.BoundedArray([:0]u8, max_channels) = .{}, last_hot_channel: ?[:0]const u8 = null, show_device_filter_dropdown: bool = false, show_channel_type_filter_dropdown: bool = false, pub fn init(self: *App, allocator: std.mem.Allocator) !void { self.* = App{ .allocator = allocator, .ui = UI.init(allocator), .task_pool = undefined }; errdefer if (self.ni_daq_api != null) self.ni_daq_api.?.deinit(); errdefer if (self.ni_daq != null) self.ni_daq.?.deinit(allocator); 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.?, .{ .max_devices = 4, .max_analog_inputs = 32, .max_analog_outputs = 8, .max_counter_outputs = 8, .max_counter_inputs = 8, .max_analog_input_voltage_ranges = 4, .max_analog_output_voltage_ranges = 4 }); self.ni_daq = ni_daq; const installed_version = try ni_daq.version(); if (installed_version.order(NIDaq.Api.min_version) == .lt) { self.shown_modal = .{ .library_version_warning = installed_version }; } } else |e| { log.err("Failed to load NI-Daq library: {any}", .{e}); switch (e) { error.LibraryNotFound => { self.shown_modal = .no_library_error; }, error.SymbolNotFound => { if (NIDaq.Api.version()) |version| { self.shown_modal = .{ .library_version_error = version }; } else |_| { self.shown_modal = .no_library_error; } } } } try TaskPool.init(&self.task_pool, allocator); errdefer self.task_pool.deinit(); } pub fn deinit(self: *App) void { 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; } } for (self.selected_channels.constSlice()) |channel| { self.allocator.free(channel); } self.selected_channels.len = 0; self.ui.deinit(); self.task_pool.deinit(); if (self.ni_daq) |*ni_daq| ni_daq.deinit(self.allocator); } fn readSamplesFromFile(allocator: std.mem.Allocator, file: std.fs.File) ![]f64 { try file.seekTo(0); const byte_count = try file.getEndPos(); assert(byte_count % 8 == 0); var samples = try allocator.alloc(f64, @divExact(byte_count, 8)); errdefer allocator.free(samples); var i: usize = 0; var buffer: [4096]u8 = undefined; while (true) { const count = try file.readAll(&buffer); if (count == 0) break; for (0..@divExact(count, 8)) |j| { samples[i] = std.mem.bytesToValue(f64, buffer[(j*8)..]); i += 1; } } return samples; } fn findFreeSlot(T: type, slice: []const ?T) ?usize { for (0.., slice) |i, loaded_file| { if (loaded_file == null) { return i; } } return null; } pub fn appendChannelFromFile(self: *App, path: []const u8) !void { 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]; for (samples) |sample| { min_value = @min(min_value, sample); max_value = @max(max_value, sample); } } const loaded_file_index = findFreeSlot(FileChannel, &self.loaded_files) orelse return error.FileLimitReached; self.loaded_files[loaded_file_index] = FileChannel{ .min_value = min_value, .max_value = max_value, .path = path_dupe, .samples = samples }; errdefer self.loaded_files[loaded_file_index] = null; const margin = 0.1; const sample_range = max_value - min_value; self.channel_views.appendAssumeCapacity(ChannelView{ .view_rect = .{ .from = 0, .to = @floatFromInt(samples.len), .min_value = min_value - sample_range * margin, .max_value = max_value + sample_range * margin }, .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); const device_channel_index = findFreeSlot(DeviceChannel, &self.device_channels) orelse return error.DeviceChannelLimitReached; const name_buff = try DeviceChannel.Name.fromSlice(channel_name); const channel_name_z = name_buff.buffer[0..name_buff.len :0]; 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]; var min_value: f64 = 0; var max_value: f64 = 1; 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; } const max_sample_rate = try ni_daq.getMaxSampleRate(channel_name_z); self.device_channels[device_channel_index] = DeviceChannel{ .name = 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) }; errdefer self.device_channels[device_channel_index] = null; self.channel_views.appendAssumeCapacity(ChannelView{ .view_rect = .{ .from = 0, .to = 0, .min_value = min_value, .max_value = max_value }, .source = .{ .device = device_channel_index } }); errdefer _ = self.channel_views.pop(); } fn getChannelSource(self: *App, channel_view: *ChannelView) ?ChannelView.SourceObject { switch (channel_view.source) { .file => |index| { if (self.loaded_files[index]) |*loaded_file| { return ChannelView.SourceObject{ .file = loaded_file }; } }, .device => |index| { if (self.device_channels[index]) |*device_channel| { return ChannelView.SourceObject{ .device = device_channel }; } } } return null; } fn findChannelIndexByName(haystack: []const [:0]const u8, needle: [:0]const u8) ?usize { for (0.., haystack) |i, item| { if (std.mem.eql(u8, item, needle)) { return i; } } return null; } // ------------------------------- GUI -------------------------------------------- // const Row = struct { name: []const u8, value: []const u8 }; fn showLabelRows(self: *App, rows: []const Row) void { { const name_column = self.ui.newBoxFromString("Names"); name_column.layout_axis = .Y; name_column.size.y = UI.Size.childrenSum(1); name_column.size.x = UI.Size.childrenSum(1); self.ui.pushParent(name_column); defer self.ui.popParent(); for (rows) |row| { _ = self.ui.label(.text, row.name); } } { const value_column = self.ui.newBoxFromString("Values"); value_column.layout_axis = .Y; value_column.size.y = UI.Size.childrenSum(1); value_column.size.x = UI.Size.percent(1, 0); self.ui.pushParent(value_column); defer self.ui.popParent(); for (rows) |row| { const label = self.ui.label(.text, row.value); label.flags.insert(.text_wrapping); } } } fn showChannelViewSlider(self: *App, view_rect: *Graph.ViewOptions, sample_count: f32) void { const min_visible_samples = 1; // sample_count*0.02; const minimap_box = self.ui.newBoxFromString("Minimap"); minimap_box.background = rl.Color.dark_purple; minimap_box.layout_axis = .X; minimap_box.size.x = UI.Size.percent(1, 0); minimap_box.size.y = UI.Size.pixels(32, 1); self.ui.pushParent(minimap_box); defer self.ui.popParent(); const minimap_rect = minimap_box.computedRect(); const middle_box = self.ui.clickableBox("Middle knob"); { middle_box.flags.insert(.draggable_x); middle_box.background = rl.Color.black.alpha(0.5); middle_box.size.y = UI.Size.pixels(32, 1); } const left_knob_box = self.ui.clickableBox("Left knob"); { left_knob_box.flags.insert(.draggable_x); left_knob_box.background = rl.Color.black.alpha(0.5); left_knob_box.size.x = UI.Size.pixels(8, 1); left_knob_box.size.y = UI.Size.pixels(32, 1); } const right_knob_box = self.ui.clickableBox("Right knob"); { right_knob_box.flags.insert(.draggable_x); right_knob_box.background = rl.Color.black.alpha(0.5); right_knob_box.size.x = UI.Size.pixels(8, 1); right_knob_box.size.y = UI.Size.pixels(32, 1); } const left_knob_size = left_knob_box.persistent.size.x; const right_knob_size = right_knob_box.persistent.size.x; const left_signal = self.ui.signalFromBox(left_knob_box); if (left_signal.dragged()) { view_rect.from += remap( f32, 0, minimap_rect.width, 0, sample_count, left_signal.drag.x ); view_rect.from = clamp(view_rect.from, 0, view_rect.to-min_visible_samples); } const right_signal = self.ui.signalFromBox(right_knob_box); if (right_signal.dragged()) { view_rect.to += remap( f32, 0, minimap_rect.width, 0, sample_count, right_signal.drag.x ); view_rect.to = clamp(view_rect.to, view_rect.from + min_visible_samples, sample_count); } const middle_signal = self.ui.signalFromBox(middle_box); if (middle_signal.dragged()) { var samples_moved = middle_signal.drag.x / minimap_rect.width * sample_count; samples_moved = clamp(samples_moved, -view_rect.from, sample_count - view_rect.to); view_rect.from += samples_moved; view_rect.to += samples_moved; } left_knob_box.setFixedX(remap(f32, 0, sample_count, 0, minimap_rect.width - left_knob_size - right_knob_size, view_rect.from )); right_knob_box.setFixedX(remap(f32, 0, sample_count, left_knob_size, minimap_rect.width - right_knob_size, view_rect.to )); middle_box.setFixedX(remap(f32, 0, sample_count, left_knob_size, minimap_rect.width - right_knob_size, view_rect.from )); middle_box.setFixedWidth(remap(f32, 0, sample_count, 0, minimap_rect.width - right_knob_size - left_knob_size, view_rect.to - view_rect.from )); } fn showChannelView(self: *App, channel_view: *ChannelView) !void { const source = self.getChannelSource(channel_view) orelse return; const samples = source.samples(); source.lockSamples(); defer source.unlockSamples(); const channel_box = self.ui.newBoxFromPtr(channel_view); channel_box.background = rl.Color.blue; channel_box.layout_axis = .Y; channel_box.size.x = UI.Size.percent(1, 0); channel_box.size.y = UI.Size.childrenSum(1); self.ui.pushParent(channel_box); defer self.ui.popParent(); { const tools_box = self.ui.newBoxFromString("Graph tools"); tools_box.background = rl.Color.gray; tools_box.layout_axis = .X; tools_box.size.x = UI.Size.percent(1, 0); tools_box.size.y = UI.Size.pixels(32, 1); self.ui.pushParent(tools_box); defer self.ui.popParent(); if (source == .device) { const device_channel = source.device; if (self.ni_daq) |*ni_daq| { const record_button = self.ui.button(.text, "Record"); record_button.size.y = UI.Size.percent(1, 0); if (device_channel.active_task != null) { record_button.setText(.text, "Stop"); } const signal = self.ui.signalFromBox(record_button); if (signal.clicked()) { if (device_channel.active_task) |task| { try task.stop(); device_channel.active_task = null; } else { const channel_name = device_channel.name.buffer[0..device_channel.name.len :0]; device_channel.active_task = try self.task_pool.launchAIVoltageChannel( ni_daq, &device_channel.mutex, &device_channel.samples, .{ .continous = .{ .sample_rate = device_channel.max_sample_rate } }, .{ .min_value = device_channel.min_value, .max_value = device_channel.max_value, .units = device_channel.units, .channel = channel_name } ); channel_view.follow = true; } } } { const follow_button = self.ui.button(.text, "Follow"); follow_button.size.y = UI.Size.percent(1, 0); if (channel_view.follow) { follow_button.setText(.text, "Unfollow"); } const signal = self.ui.signalFromBox(follow_button); if (signal.clicked()) { channel_view.follow = !channel_view.follow; } } } } { const graph_box = self.ui.newBoxFromString("Graph"); graph_box.background = rl.Color.blue; graph_box.size.x = UI.Size.percent(1, 0); graph_box.size.y = UI.Size.pixels(channel_view.height, 1); Graph.drawCached(&channel_view.view_cache, graph_box.persistent.size, channel_view.view_rect, samples); if (channel_view.view_cache.texture) |texture| { graph_box.texture = texture.texture; } } self.showChannelViewSlider( &channel_view.view_rect, @floatFromInt(samples.len) ); } fn showChannelsWindow(self: *App) !void { const scroll_area = self.ui.pushScrollbar(self.ui.newKeyFromString("Channels")); defer self.ui.popScrollbar(); scroll_area.layout_axis = .Y; scroll_area.layout_gap = 16; for (self.channel_views.slice()) |*channel_view| { try self.showChannelView(channel_view); } { const prompt_box = self.ui.newBoxFromString("Add prompt"); prompt_box.size.x = UI.Size.percent(1, 0); prompt_box.size.y = UI.Size.pixels(200, 1); self.ui.pushParent(prompt_box); defer self.ui.popParent(); const center_box = self.ui.pushCenterBox(); defer self.ui.popCenterBox(); center_box.layout_axis = .X; center_box.layout_gap = 32; const from_file_button = self.ui.button(.text, "Add from file"); from_file_button.background = srcery.green; if (self.ui.signalFromBox(from_file_button).clicked()) { log.debug("TODO: Not implemented", .{}); } const from_device_button = self.ui.button(.text, "Add from device"); from_device_button.background = srcery.green; if (self.ui.signalFromBox(from_device_button).clicked()) { self.shown_window = .add_from_device; } } } fn showChannelInfoPanel(self: *App, hot_channel: ?[:0]const u8) !void { const ni_daq = &(self.ni_daq orelse return); var device_buff: NIDaq.BoundedDeviceName = .{}; var hot_device: ?[:0]const u8 = null; if (hot_channel) |channel| { if (NIDaq.getDeviceNameFromChannel(channel)) |device| { device_buff.appendSliceAssumeCapacity(device); device_buff.buffer[device_buff.len] = 0; hot_device = device_buff.buffer[0..device_buff.len :0]; } } const info_box = self.ui.newBoxFromString("Info box"); info_box.layout_axis = .Y; info_box.size.y = UI.Size.percent(1, 0); info_box.size.x = UI.Size.percent(1, 0); self.ui.pushParent(info_box); defer self.ui.popParent(); if (hot_channel) |channel| { _ = self.ui.label(.text, "Channel properties"); const channel_info = self.ui.newBoxFromString("Channel info"); channel_info.layout_axis = .X; channel_info.size.y = UI.Size.childrenSum(1); channel_info.size.x = UI.Size.percent(1, 0); self.ui.pushParent(channel_info); defer self.ui.popParent(); var rows: std.BoundedArray(Row, 16) = .{}; rows.appendAssumeCapacity(Row{ .name = "Name", .value = channel }); var channel_type_name: []const u8 = "unknown"; if (NIDaq.getChannelType(channel)) |channel_type| { channel_type_name = channel_type.name(); // rows.appendAssumeCapacity(Row{ // .name = "Type", // .value = channel_type_name // }); } rows.appendAssumeCapacity(Row{ .name = "Type", .value = channel_type_name }); self.showLabelRows(rows.constSlice()); } self.ui.spacer(.{ .y = UI.Size.pixels(16, 0) }); if (hot_device) |device| { _ = self.ui.label(.text, "Device properties"); const device_info = self.ui.newBoxFromString("Device info"); device_info.layout_axis = .X; device_info.size.y = UI.Size.childrenSum(1); device_info.size.x = UI.Size.percent(1, 0); self.ui.pushParent(device_info); defer self.ui.popParent(); var rows: std.BoundedArray(Row, 16) = .{}; if (ni_daq.listDeviceAIMeasurementTypes(device)) |measurement_types| { rows.appendAssumeCapacity(Row{ .name = "Measurement types", .value = try std.fmt.allocPrint(device_info.allocator, "{} types", .{measurement_types.len}) }); } else |e| { log.err("ni_daq.listDeviceAIMeasurementTypes(): {}", .{ e }); } rows.appendAssumeCapacity(Row{ .name = "Foo", .value = "bar" }); self.showLabelRows(rows.constSlice()); } } fn showAddFromDeviceWindow(self: *App) !void { const ni_daq = &(self.ni_daq orelse return); const device_names = try ni_daq.listDeviceNames(); const window = self.ui.newBoxFromString("Device window"); window.size.x = UI.Size.percent(1, 0); window.size.y = UI.Size.percent(1, 0); window.layout_axis = .X; self.ui.pushParent(window); defer self.ui.popParent(); { const filters_box = self.ui.newBoxFromString("Filters box"); filters_box.size.x = UI.Size.percent(0.5, 0); filters_box.size.y = UI.Size.percent(1, 0); filters_box.layout_axis = .Y; self.ui.pushParent(filters_box); defer self.ui.popParent(); const device_name_filter = self.ui.clickableBox("Device name filter"); const channel_type_filter = self.ui.clickableBox("Channel type filter"); if (self.show_device_filter_dropdown) { const dropdown = self.ui.clickableBox("Device name dropdown"); dropdown.size.x = UI.Size.percent(1, 1); dropdown.size.y = UI.Size.childrenSum(1); dropdown.layout_axis = .Y; dropdown.background = srcery.xgray2; self.ui.pushParent(dropdown); defer self.ui.popParent(); dropdown.setFixedPosition( device_name_filter.persistent.position.add(.{ .x = 0, .y = device_name_filter.persistent.size.y }) ); { const device_box = self.ui.button(.text, "All"); device_box.size.x = UI.Size.percent(1, 1); device_box.size.y = UI.Size.text(0.5, 1); device_box.flags.insert(.text_left_align); if (self.ui.signalFromBox(device_box).clicked()) { self.device_filter.len = 0; self.show_device_filter_dropdown = false; } } for (device_names) |device_name| { const device_box = self.ui.button(.text, device_name); device_box.size.x = UI.Size.percent(1, 1); device_box.size.y = UI.Size.text(0.5, 1); device_box.flags.insert(.text_left_align); const signal = self.ui.signalFromBox(device_box); if (signal.clicked()) { self.device_filter = try NIDaq.BoundedDeviceName.fromSlice(device_name); self.show_device_filter_dropdown = false; } } } if (self.show_channel_type_filter_dropdown) { const dropdown = self.ui.clickableBox("Channel type dropdown"); dropdown.size.x = UI.Size.percent(1, 1); dropdown.size.y = UI.Size.childrenSum(1); dropdown.layout_axis = .Y; dropdown.background = srcery.xgray2; self.ui.pushParent(dropdown); defer self.ui.popParent(); dropdown.setFixedPosition( channel_type_filter.persistent.position.add(.{ .x = 0, .y = channel_type_filter.persistent.size.y }) ); { const device_box = self.ui.button(.text, "All"); device_box.size.x = UI.Size.percent(1, 1); device_box.size.y = UI.Size.text(0.5, 1); device_box.flags.insert(.text_left_align); if (self.ui.signalFromBox(device_box).clicked()) { self.channel_type_filter = null; self.show_channel_type_filter_dropdown = false; } } for (&[_]NIDaq.ChannelType{ NIDaq.ChannelType.analog_input, NIDaq.ChannelType.analog_output }) |channel_type| { const device_box = self.ui.button(.text, channel_type.name()); device_box.size.x = UI.Size.percent(1, 1); device_box.size.y = UI.Size.text(0.5, 1); device_box.flags.insert(.text_left_align); if (self.ui.signalFromBox(device_box).clicked()) { self.channel_type_filter = channel_type; self.show_channel_type_filter_dropdown = false; } } } { device_name_filter.size.x = UI.Size.percent(1, 1); device_name_filter.size.y = UI.Size.pixels(24, 1); device_name_filter.layout_axis = .X; self.ui.pushParent(device_name_filter); defer self.ui.popParent(); { self.ui.pushVerticalAlign(); defer self.ui.popVerticalAlign(); _ = self.ui.textureBox(Assets.dropdown_arrow, 1); } if (self.device_filter.len > 0) { _ = self.ui.label(.text, self.device_filter.constSlice()); } else { _ = self.ui.label(.text, "All"); } if (self.ui.signalFromBox(device_name_filter).clicked()) { self.show_device_filter_dropdown = !self.show_device_filter_dropdown; } self.ui.spacer(.{ .x = UI.Size.percent(1, 0) }); } { channel_type_filter.size.x = UI.Size.percent(1, 1); channel_type_filter.size.y = UI.Size.pixels(24, 1); channel_type_filter.layout_axis = .X; self.ui.pushParent(channel_type_filter); defer self.ui.popParent(); { self.ui.pushVerticalAlign(); defer self.ui.popVerticalAlign(); _ = self.ui.textureBox(Assets.dropdown_arrow, 1); } if (self.channel_type_filter) |channeL_type| { _ = self.ui.label(.text, channeL_type.name()); } else { _ = self.ui.label(.text, "All"); } if (self.ui.signalFromBox(channel_type_filter).clicked()) { self.show_channel_type_filter_dropdown = !self.show_channel_type_filter_dropdown; } } } var hot_channel: ?[:0]const u8 = self.last_hot_channel; { const channels_box = self.ui.pushScrollbar(self.ui.newKeyFromString("Channels list")); defer self.ui.popScrollbar(); const channels_box_container = self.ui.getParentOf(channels_box).?; channels_box.layout_axis = .Y; //channels_box.size.x = UI.Size.childrenSum(1); channels_box_container.size.x = UI.Size.percent(1, 0); var devices: []const [:0]const u8 = &.{}; if (self.device_filter.len > 0) { devices = &.{ self.device_filter.buffer[0..self.device_filter.len :0] }; } else { devices = try ni_daq.listDeviceNames(); } for (devices) |device| { var ai_voltage_physical_channels: []const [:0]const u8 = &.{}; if (try ni_daq.checkDeviceAIMeasurementType(device, .Voltage)) { ai_voltage_physical_channels = try ni_daq.listDeviceAIPhysicalChannels(device); } var ao_physical_channels: []const [:0]const u8 = &.{}; if (try ni_daq.checkDeviceAOOutputType(device, .Voltage)) { ao_physical_channels = try ni_daq.listDeviceAOPhysicalChannels(device); } inline for (.{ ai_voltage_physical_channels, ao_physical_channels }) |channels| { for (channels) |channel| { const selected_channels_slice = self.selected_channels.constSlice(); if (self.channel_type_filter) |channel_type_filter| { if (NIDaq.getChannelType(channel) != channel_type_filter) { continue; } } const channel_box = self.ui.button(.text, channel); if (findChannelIndexByName(selected_channels_slice, channel) != null) { channel_box.background = srcery.xgray3; } const signal = self.ui.signalFromBox(channel_box); if (signal.clicked()) { if (findChannelIndexByName(selected_channels_slice, channel)) |index| { self.allocator.free(self.selected_channels.swapRemove(index)); } else { self.selected_channels.appendAssumeCapacity(try self.allocator.dupeZ(u8, channel)); } } if (signal.hot) { hot_channel = channel; } } } } } { const left_panel = self.ui.newBox(UI.Key.initNil()); left_panel.layout_axis = .Y; left_panel.size.y = UI.Size.percent(1, 0); left_panel.size.x = UI.Size.percent(1, 0); self.ui.pushParent(left_panel); defer self.ui.popParent(); try self.showChannelInfoPanel(hot_channel); const add_button = self.ui.button(.text, "Add"); if (self.ui.signalFromBox(add_button).clicked()) { for (self.selected_channels.constSlice()) |channel_name| { try self.appendChannelFromDevice(channel_name); } self.shown_window = .channels; for (self.selected_channels.constSlice()) |channel| { self.allocator.free(channel); } self.selected_channels.len = 0; } } if (hot_channel != null) { self.last_hot_channel = hot_channel; } } fn showToolbar(self: *App) void { const toolbar = self.ui.newBoxFromString("Toolbar"); toolbar.background = rl.Color.green; toolbar.layout_axis = .X; toolbar.size = .{ .x = UI.Size.percent(1, 0), .y = UI.Size.pixels(32, 1), }; self.ui.pushParent(toolbar); defer self.ui.popParent(); { const box = self.ui.button(.text, "Add from file"); box.background = rl.Color.red; box.size.y = UI.Size.percent(1, 1); const signal = self.ui.signalFromBox(box); if (signal.clicked()) { if (Platform.openFilePicker()) |file| { defer file.close(); // TODO: Handle error // self.appendChannelFromFile(file) catch @panic("Failed to append channel from file"); } else |err| { // TODO: Show error message to user; log.err("Failed to pick file: {}", .{ err }); } } } { const box = self.ui.button(.text, "Add from device"); box.background = rl.Color.lime; box.size.y = UI.Size.percent(1, 1); const signal = self.ui.signalFromBox(box); if (signal.clicked()) { if (self.shown_window == .add_from_device) { self.shown_window = .channels; } else { self.shown_window = .add_from_device; } } } } fn showModalNoLibraryError(self: *App) void { const modal = self.ui.getParent().?; modal.layout_axis = .Y; modal.size = .{ .x = UI.Size.pixels(400, 1), .y = UI.Size.pixels(320, 1), }; self.ui.spacer(.{ .y = UI.Size.percent(1, 0) }); const text = self.ui.newBoxFromString("Text"); text.flags.insert(.text_wrapping); text.size.x = UI.Size.text(2, 0); text.size.y = UI.Size.text(1, 1); text.appendText("PALA, PALA! Aš neradau būtinos bibliotekos ant kompiuterio. Programa vis dar veiks, bet "); text.appendText("dauguma funkcijų bus paslėptos. Susirask iternete \"NI MAX\" ir instaliuok. Štai nuorada "); text.appendText("į gidą."); { self.ui.pushHorizontalAlign(); defer self.ui.popHorizontalAlign(); const link = self.ui.newBoxFromString("Link"); link.flags.insert(.clickable); link.flags.insert(.hover_mouse_hand); link.flags.insert(.text_underline); link.size.x = UI.Size.text(1, 1); link.size.y = UI.Size.text(1, 1); link.setText( .text, "Nuorada į gidą" ); link.text.?.color = srcery.blue; const signal = self.ui.signalFromBox(link); if (signal.clicked()) { rl.openURL("https://knowledge.ni.com/KnowledgeArticleDetails?id=kA03q000000YGQwCAO&l=en-LT"); } if (self.ui.isBoxHot(link)) { link.text.?.color = srcery.bright_blue; } } self.ui.spacer(.{ .y = UI.Size.percent(1, 0) }); { self.ui.pushHorizontalAlign(); defer self.ui.popHorizontalAlign(); const btn = self.ui.button(.text, "Supratau"); btn.background = srcery.green; if (self.ui.signalFromBox(btn).clicked()) { self.shown_modal = null; } } self.ui.spacer(.{ .y = UI.Size.percent(1, 0) }); } fn showModalLibraryVersionError(self: *App) void { assert(self.shown_modal.? == .library_version_error); const installed_version = self.shown_modal.?.library_version_error; const modal = self.ui.getParent().?; modal.layout_axis = .Y; modal.size = .{ .x = UI.Size.pixels(400, 1), .y = UI.Size.pixels(320, 1), }; self.ui.spacer(.{ .y = UI.Size.percent(1, 0) }); { const text = self.ui.newBox(UI.Key.initNil()); text.flags.insert(.text_wrapping); text.flags.insert(.text_left_align); text.size.x = UI.Size.text(2, 0); text.size.y = UI.Size.text(1, 1); text.appendText("Ooo ne! Reikalinga biblioteka surasta, bet nesurastos reikalingos funkcijos. "); text.appendText("Susitikrink, kad turi pakankamai naują versiją NI MAX instaliuota."); } { const text = self.ui.newBox(UI.Key.initNil()); text.size.x = UI.Size.text(2, 0); text.size.y = UI.Size.text(1, 1); text.setFmtText(.text, "Instaliuota versija: {}", .{installed_version}); } { const text = self.ui.newBox(UI.Key.initNil()); text.size.x = UI.Size.text(2, 0); text.size.y = UI.Size.text(1, 1); text.setFmtText(.text, "Rekomenduotina versija: {}", .{NIDaq.Api.min_version}); } self.ui.spacer(.{ .y = UI.Size.percent(1, 0) }); { self.ui.pushHorizontalAlign(); defer self.ui.popHorizontalAlign(); const btn = self.ui.button(.text, "Supratau"); btn.background = srcery.green; if (self.ui.signalFromBox(btn).clicked()) { self.shown_modal = null; } } self.ui.spacer(.{ .y = UI.Size.percent(1, 0) }); } fn showModalLibraryVersionWarning(self: *App) void { assert(self.shown_modal.? == .library_version_warning); const installed_version = self.shown_modal.?.library_version_warning; const modal = self.ui.getParent().?; modal.layout_axis = .Y; modal.size = .{ .x = UI.Size.pixels(400, 1), .y = UI.Size.pixels(320, 1), }; self.ui.spacer(.{ .y = UI.Size.percent(1, 0) }); { const text = self.ui.newBox(UI.Key.initNil()); text.flags.insert(.text_wrapping); text.flags.insert(.text_left_align); text.size.x = UI.Size.text(2, 0); text.size.y = UI.Size.text(1, 1); text.appendText("Instaliuota NI MAX versija žemesnė negu rekomenduotina versija. "); text.appendText("Daug kas turėtų veikti, bet negaliu garantuoti kad viskas veiks. "); text.appendText("Jeigu susidursi su problemomis kur programa sustoja veikti pabandyk atsinaujinti "); text.appendText("NI MAX."); } { const text = self.ui.newBox(UI.Key.initNil()); text.size.x = UI.Size.text(2, 0); text.size.y = UI.Size.text(1, 1); text.setFmtText(.text, "Instaliuota versija: {}", .{installed_version}); } { const text = self.ui.newBox(UI.Key.initNil()); text.size.x = UI.Size.text(2, 0); text.size.y = UI.Size.text(1, 1); text.setFmtText(.text, "Rekomenduotina versija: {}", .{NIDaq.Api.min_version}); } self.ui.spacer(.{ .y = UI.Size.percent(1, 0) }); { self.ui.pushHorizontalAlign(); defer self.ui.popHorizontalAlign(); const btn = self.ui.button(.text, "Supratau"); btn.background = srcery.green; if (self.ui.signalFromBox(btn).clicked()) { self.shown_modal = null; } } self.ui.spacer(.{ .y = UI.Size.percent(1, 0) }); } fn showModal(self: *App) void { assert(self.shown_modal != null); switch (self.shown_modal.?) { .no_library_error => self.showModalNoLibraryError(), .library_version_error => self.showModalLibraryVersionError(), .library_version_warning => self.showModalLibraryVersionWarning() } } fn updateUI(self: *App) !void { self.ui.begin(); defer self.ui.end(); const root_box = self.ui.getParent().?; root_box.layout_axis = .Y; var maybe_modal_overlay: ?*UI.Box = null; if (self.shown_modal != null) { const modal_overlay = self.ui.newBoxNoAppend(self.ui.newKeyFromString("Modal overlay")); maybe_modal_overlay = modal_overlay; modal_overlay.flags.insert(.clickable); modal_overlay.flags.insert(.scrollable); modal_overlay.background = rl.Color.black.alpha(0.5); modal_overlay.setFixedPosition(.{ .x = 0, .y = 0 }); modal_overlay.size = .{ .x = UI.Size.percent(1, 0), .y = UI.Size.percent(1, 0), }; self.ui.pushParent(modal_overlay); defer self.ui.popParent(); const modal = self.ui.pushCenterBox(); defer self.ui.popCenterBox(); modal.background = srcery.hard_black; self.showModal(); _ = self.ui.signalFromBox(modal_overlay); } self.showToolbar(); if (self.shown_window == .channels) { try self.showChannelsWindow(); } else if (self.shown_window == .add_from_device) { try self.showAddFromDeviceWindow(); } if (maybe_modal_overlay) |box| { self.ui.appendBox(box); } } pub fn tick(self: *App) !void { for (self.channel_views.slice()) |*_view| { const view: *ChannelView = _view; const source = self.getChannelSource(view) orelse continue; if (source == .device) { if (view.follow) { source.lockSamples(); defer source.unlockSamples(); const sample_count: f32 = @floatFromInt(source.samples().len); const view_size = view.view_rect.to - view.view_rect.from; view.view_rect.from = sample_count - view_size; view.view_rect.to = sample_count; } } } rl.clearBackground(srcery.black); if (rl.isKeyPressed(rl.KeyboardKey.key_f3)) { Platform.toggleConsoleWindow(); } if (rl.isFileDropped()) { const file_list = rl.loadDroppedFiles(); defer rl.unloadDroppedFiles(file_list); for (file_list.paths[0..file_list.count]) |path| { const path_len = std.mem.indexOfSentinel(u8, 0, path); try self.appendChannelFromFile(path[0..path_len]); } } // On the first frame, render the UI twice. // So that on the second pass widgets that depend on sizes from other widgets have settled if (self.ui.frame_index == 0) { try self.updateUI(); } try self.updateUI(); self.ui.draw(); }