1260 lines
41 KiB
Zig
1260 lines
41 KiB
Zig
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();
|
|
} |