daq-view/src/app.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();
}