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, 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 } }); } pub fn tick(self: *App) !void { rl.beginDrawing(); defer rl.endDrawing(); rl.clearBackground(srcery.black); if (rl.isKeyPressed(rl.KeyboardKey.key_f3)) { Platform.toggleConsoleWindow(); } { self.ui.begin(); defer self.ui.end(); self.ui.getParent().?.layout_axis = .Y; { 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()) { std.debug.print("click two!\n", .{}); } } } { 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 )); } } } } self.ui.draw(); }