daq-view/src/app.zig
2025-02-03 01:02:41 +02:00

369 lines
11 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.zig");
const rect_utils = @import("./rect-utils.zig");
const remap = @import("./utils.zig").remap;
const log = std.log.scoped(.app);
const assert = std.debug.assert;
const clamp = std.math.clamp;
const App = @This();
const Channel = struct {
view_cache: Graph.Cache = .{},
view_rect: Graph.ViewOptions,
height: f32 = 150,
min_value: f64,
max_value: f64,
samples: union(enum) {
owned: []f64,
},
};
allocator: std.mem.Allocator,
ui: UI,
channels: std.BoundedArray(Channel, 64) = .{},
ni_daq: NIDaq,
shown_window: enum {
channels,
add_from_device
} = .channels,
pub fn init(allocator: std.mem.Allocator) !App {
return App{
.allocator = allocator,
.ui = UI.init(allocator),
.ni_daq = try NIDaq.init(allocator, .{
.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
}),
};
}
pub fn deinit(self: *App) void {
self.ni_daq.deinit(self.allocator);
for (self.channels.slice()) |*channel| {
switch (channel.samples) {
.owned => |owned| self.allocator.free(owned)
}
channel.view_cache.deinit();
}
self.ui.deinit();
}
fn showButton(self: *App, text: []const u8) UI.Interaction {
var button = self.ui.newWidget(self.ui.keyFromString(text));
button.border = srcery.bright_blue;
button.padding.vertical(8);
button.padding.horizontal(16);
button.flags.insert(.clickable);
button.size = .{
.x = .{ .text = {} },
.y = .{ .text = {} },
};
const interaction = self.ui.getInteraction(button);
var text_color: rl.Color = undefined;
if (interaction.held_down) {
button.background = srcery.hard_black;
text_color = srcery.white;
} else if (interaction.hovering) {
button.background = srcery.bright_black;
text_color = srcery.bright_white;
} else {
button.background = srcery.blue;
text_color = srcery.bright_white;
}
button.text = .{
.content = text,
.color = text_color
};
return interaction;
}
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;
}
pub fn appendChannelFromFile(self: *App, file: std.fs.File) !void {
const samples = try readSamplesFromFile(self.allocator, file);
errdefer self.allocator.free(samples);
var min_value = samples[0];
var max_value = samples[0];
for (samples) |sample| {
min_value = @min(min_value, sample);
max_value = @max(max_value, sample);
}
const margin = 0.1;
try self.channels.append(Channel{
.min_value = min_value,
.max_value = max_value,
.view_rect = .{
.from = 0,
.to = @floatFromInt(samples.len),
.min_value = min_value + (min_value - max_value) * margin,
.max_value = max_value + (max_value - min_value) * margin
},
.samples = .{ .owned = samples }
});
}
fn showChannelsWindow(self: *App) void {
const scroll_area = self.ui.pushScrollbar(self.ui.newKeyFromString("Channels"));
scroll_area.layout_axis = .Y;
defer self.ui.popScrollbar();
for (self.channels.slice()) |*_channel| {
const channel: *Channel = _channel;
const channel_box = self.ui.newBoxFromPtr(channel);
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 graph_box = self.ui.newBoxFromString("Graph");
graph_box.background = rl.Color.blue;
graph_box.layout_axis = .Y;
graph_box.size.x = UI.Size.percent(1, 0);
graph_box.size.y = UI.Size.pixels(256, 1);
Graph.drawCached(&channel.view_cache, graph_box.persistent.size, channel.view_rect, channel.samples.owned);
if (channel.view_cache.texture) |texture| {
graph_box.texture = texture.texture;
}
{
const sample_count: f32 = @floatFromInt(channel.samples.owned.len);
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.newBoxFromString("Middle knob");
{
middle_box.flags.insert(.clickable);
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.newBoxFromString("Left knob");
{
left_knob_box.flags.insert(.clickable);
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.newBoxFromString("Right knob");
{
right_knob_box.flags.insert(.clickable);
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()) {
channel.view_rect.from += remap(
f32,
0, minimap_rect.width,
0, sample_count,
left_signal.drag.x
);
channel.view_rect.from = clamp(channel.view_rect.from, 0, channel.view_rect.to-min_visible_samples);
}
const right_signal = self.ui.signalFromBox(right_knob_box);
if (right_signal.dragged()) {
channel.view_rect.to += remap(
f32,
0, minimap_rect.width,
0, sample_count,
right_signal.drag.x
);
channel.view_rect.to = clamp(channel.view_rect.to, channel.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, -channel.view_rect.from, sample_count - channel.view_rect.to);
channel.view_rect.from += samples_moved;
channel.view_rect.to += samples_moved;
}
left_knob_box.setFixedX(remap(f32,
0, sample_count,
0, minimap_rect.width - left_knob_size - right_knob_size,
channel.view_rect.from
));
right_knob_box.setFixedX(remap(f32,
0, sample_count,
left_knob_size, minimap_rect.width - right_knob_size,
channel.view_rect.to
));
middle_box.setFixedX(remap(f32,
0, sample_count,
left_knob_size, minimap_rect.width - right_knob_size,
channel.view_rect.from
));
middle_box.setFixedWidth(remap(f32,
0, sample_count,
0, minimap_rect.width - right_knob_size - left_knob_size,
channel.view_rect.to - channel.view_rect.from
));
}
}
}
fn showAddFromDeviceWindow(self: *App) !void {
const names = try self.ni_daq.listDeviceNames();
_ = names;
}
fn showToolbar(self: *App) void {
const toolbar = self.ui.newBoxFromString("Toolbar");
toolbar.flags.insert(.clickable);
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.newBoxFromString("Add from file");
box.flags.insert(.clickable);
box.background = rl.Color.red;
box.size = .{
.x = UI.Size.text(2, 1),
.y = UI.Size.percent(1, 1)
};
box.setText("Add from file", .text);
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.newBoxFromString("Add from device");
box.flags.insert(.clickable);
box.background = rl.Color.lime;
box.size = .{
.x = UI.Size.text(2, 1),
.y = UI.Size.percent(1, 1)
};
box.setText("Add from device", .text);
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;
}
}
}
}
pub fn tick(self: *App) !void {
rl.clearBackground(srcery.black);
if (rl.isKeyPressed(rl.KeyboardKey.key_f3)) {
Platform.toggleConsoleWindow();
}
{
self.ui.begin();
defer self.ui.end();
const root_box = self.ui.getParent().?;
root_box.layout_axis = .Y;
self.showToolbar();
if (self.shown_window == .channels) {
self.showChannelsWindow();
} else if (self.shown_window == .add_from_device) {
try self.showAddFromDeviceWindow();
}
}
self.ui.draw();
}