369 lines
11 KiB
Zig
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();
|
|
} |