const std = @import("std"); const rl = @import("raylib"); const UI = @import("../ui.zig"); const App = @import("../app.zig"); const srcery = @import("../srcery.zig"); const Platform = @import("../platform/root.zig"); const RangeF64 = @import("../range.zig").RangeF64; const Graph = @import("../graph.zig"); const Assets = @import("../assets.zig"); const utils = @import("../utils.zig"); const NIDaq = @import("../ni-daq/root.zig"); const UIView = @import("../components/view.zig"); const ViewControlsSystem = @import("../components/systems/view_controls.zig"); const MainScreen = @This(); const log = std.log.scoped(.main_screen); const assert = std.debug.assert; const remap = utils.remap; const Id = App.Id; app: *App, view_controls: ViewControlsSystem, side_panel: union(enum) { project, solutions, gain_changes, statistics_points, view_settings: Id, // marker: struct { // view_id: Id, // index: usize // }, // marked_range: struct { // view_id: Id, // index: usize, // } } = .project, modal: ?union(enum){ notes, add_proprotional, protocol } = null, // Protocol modal protocol_filename: ?[]u8 = null, protocol_file_picker: ?Platform.FilePickerId = null, protocol_channel: std.BoundedArray(u8, 128) = .{}, // frequency_input: UI.TextInputStorage, // amplitude_input: UI.TextInputStorage, // protocol_error_message: ?[]const u8 = null, // protocol_graph_cache: Graph.RenderCache = .{}, // preview_sample_list_id: App.Id, // preview_samples_y_range: RangeF64 = RangeF64.init(0, 0), // Notes modal notes_storage: UI.TextInputStorage, // Project settings solution_input: UI.TextInputStorage, gain_input: UI.TextInputStorage, sample_rate_input: UI.TextInputStorage, experiment_name: UI.TextInputStorage, pipete_solution: UI.TextInputStorage, export_location_picker: ?Platform.FilePickerId = null, parsed_sample_rate: ?f64 = null, // View settings prev_view_settings: ?Id = null, // View ID view_name_input: UI.TextInputStorage, channel_save_file_picker: ?Platform.FilePickerId = null, file_save_file_picker: ?Platform.FilePickerId = null, // Proportional modal proportion_view1: ?Id = null, // View ID proportion_view2: ?Id = null, // View ID pub fn init(app: *App) !MainScreen { const allocator = app.allocator; const self = MainScreen{ .app = app, // .frequency_input = UI.TextInputStorage.init(allocator), // .amplitude_input = UI.TextInputStorage.init(allocator), .sample_rate_input = UI.TextInputStorage.init(allocator), .notes_storage = UI.TextInputStorage.init(allocator), .solution_input = UI.TextInputStorage.init(allocator), .experiment_name = UI.TextInputStorage.init(allocator), .pipete_solution = UI.TextInputStorage.init(allocator), .view_name_input = UI.TextInputStorage.init(allocator), .gain_input = UI.TextInputStorage.init(allocator), .view_controls = ViewControlsSystem.init(app), // .preview_sample_list_id = try app.project.addSampleList(allocator) }; // try self.frequency_input.setText("10"); // try self.amplitude_input.setText("10"); return self; } pub fn deinit(self: *MainScreen) void { // self.frequency_input.deinit(); // self.amplitude_input.deinit(); self.sample_rate_input.deinit(); self.notes_storage.deinit(); self.solution_input.deinit(); self.experiment_name.deinit(); self.pipete_solution.deinit(); self.view_name_input.deinit(); self.gain_input.deinit(); // self.app.project.removeSampleList(self.preview_sample_list_id); // self.clearProtocolErrorMessage(); if (self.protocol_filename) |str| { self.app.allocator.free(str); } } pub fn showProtocolModal(self: *MainScreen) !void { var ui = &self.app.ui; const ni_daq = &(self.app.ni_daq orelse return); // const allocator = self.app.allocator; // const sample_rate = self.app.project.getSampleRate() orelse return; // _ = sample_rate; const container = ui.createBox(.{ .key = ui.keyFromString("Protocol modal"), .background = srcery.black, .size_x = UI.Sizing.initGrowUpTo(.{ .pixels = 300 }), .size_y = UI.Sizing.initGrowUpTo(.{ .pixels = 300 }), .layout_direction = .top_to_bottom, .padding = UI.Padding.all(ui.rem(1.5)), .flags = &.{ .clickable }, .layout_gap = ui.rem(0.5) }); container.beginChildren(); defer container.endChildren(); defer _ = ui.signal(container); if (ui.fileInput(.{ .key = ui.keyFromString("Protocol filename"), .allocator = self.app.allocator, .path = self.protocol_filename, .file_picker = &self.protocol_file_picker })) |filename| { if (self.protocol_filename) |str| { self.app.allocator.free(str); } self.protocol_filename = filename; } { // Select channel var available_channels = std.ArrayList([]const u8).init(self.app.allocator); defer available_channels.deinit(); const devices = try ni_daq.listDeviceNames(); for (devices) |device| { if (!try ni_daq.checkDeviceAOOutputType(device, .Voltage)) { continue; } for (try ni_daq.listDeviceAOPhysicalChannels(device)) |channel| { try available_channels.append(channel); } } const select = ui.button(ui.keyFromString("Protocol channel")); select.borders = UI.Borders.all(.{ .color = srcery.green, .size = 4 }); if (self.protocol_channel.len == 0) { select.setText("Channel: -"); } else { select.setFmtText("Channel: {s}", .{self.protocol_channel.constSlice()}); } const select_signal = ui.signal(select); if (select_signal.clicked()) { select.persistent.open = !select.persistent.open; } if (select.persistent.open) { const popup = ui.createBox(.{ .key = ui.keyFromString("Channel popup"), .size_x = UI.Sizing.initFixedPixels(ui.rem(10)), .size_y = UI.Sizing.initFitChildren(), .flags = &.{ .clickable, .scrollable }, .layout_direction = .top_to_bottom, .float_relative_to = select, .background = srcery.black, .borders = UI.Borders.all(.{ .color = srcery.bright_black, .size = 4 }), .draw_on_top = true }); popup.setFloatPosition(0, select.persistent.size.y); popup.beginChildren(); defer popup.endChildren(); defer _ = ui.signal(popup); for (available_channels.items) |channel| { const select_option = ui.textButton(channel); select_option.alignment.x = .start; select_option.size.x = UI.Sizing.initGrowFull(); select_option.borders = UI.Borders.all(.{ .color = srcery.hard_black, .size = 2 }); select_option.background = srcery.black; const signal = ui.signal(select_option); if (signal.clicked()) { select.persistent.open = false; self.protocol_channel.len = 0; try self.protocol_channel.appendSlice(channel); } } } if (select_signal.clicked_outside) { select.persistent.open = false; } } { // Start var btn = ui.textButton("Start"); btn.borders = UI.Borders.all(.{ .color = srcery.hard_black, .size = 4 }); if (self.protocol_filename != null and self.protocol_channel.len > 0) { btn.borders = UI.Borders.all(.{ .color = srcery.green, .size = 4 }); if (ui.signal(btn).clicked()) { try self.app.startOutput(self.protocol_channel.constSlice(), self.protocol_filename.?); self.modal = null; } } } // { // const protocol_view = ui.createBox(.{ // .key = ui.keyFromString("Protocol view"), // .size_x = UI.Sizing.initGrowFull(), // .size_y = UI.Sizing.initFixedPixels(ui.rem(4)), // .borders = UI.Borders.all(.{ .color = srcery.hard_black, .size = 2 }) // }); // if (self.app.project.sample_lists.get(self.preview_sample_list_id)) |sample_list| { // const view_rect = Graph.ViewOptions{ // .x_range = RangeF64.init(0, @floatFromInt(sample_list.getLength())), // .y_range = self.preview_samples_y_range // }; // Graph.drawCached(&self.protocol_graph_cache, protocol_view.persistent.size, view_rect, sample_list); // if (self.protocol_graph_cache.texture) |texture| { // protocol_view.texture = texture.texture; // } // } // } // const FormInput = struct { // name: []const u8, // storage: *UI.TextInputStorage, // value: *f32 // }; // var frequency: f32 = 0; // var amplitude: f32 = 0; // const form_inputs = &[_]FormInput{ // .{ // .name = "Frequency", // .storage = &self.frequency_input, // .value = &frequency // }, // .{ // .name = "Amplitude", // .storage = &self.amplitude_input, // .value = &litude // }, // }; // var any_input_modified = false; // for (form_inputs) |form_input| { // const label = form_input.name; // const text_input_storage = form_input.storage; // const row = ui.createBox(.{ // .key = ui.keyFromString(label), // .layout_direction = .left_to_right, // .size_x = UI.Sizing.initGrowFull(), // .size_y = UI.Sizing.initFixedPixels(ui.rem(1.5)) // }); // row.beginChildren(); // defer row.endChildren(); // const label_box = ui.label("{s}", .{ label }); // label_box.size.x = UI.Sizing.initFixed(.{ .font_size = 5 }); // label_box.size.y = UI.Sizing.initGrowFull(); // label_box.alignment.y = .center; // try ui.textInput(.{ // .key = ui.keyFromString("Text input"), // .storage = text_input_storage // }); // any_input_modified = any_input_modified or text_input_storage.modified; // } // if (any_input_modified) { // self.clearProtocolErrorMessage(); // for (form_inputs) |form_input| { // const label = form_input.name; // const text_input_storage: *UI.TextInputStorage = form_input.storage; // const number = std.fmt.parseFloat(f32, text_input_storage.buffer.items) catch { // try self.setProtocolErrorMessage("ERROR: {s} must be a number", .{ label }); // continue; // }; // if (number <= 0) { // try self.setProtocolErrorMessage("ERROR: {s} must be positive", .{ label }); // continue; // } // form_input.value.* = number; // } // } // if (self.protocol_error_message == null and any_input_modified) { // if (self.app.project.sample_lists.get(self.preview_sample_list_id)) |sample_list| { // var preview_samples: std.ArrayListUnmanaged(f64) = .{}; // defer preview_samples.deinit(allocator); // try App.Channel.generateSine(&preview_samples, allocator, sample_rate, frequency, amplitude); // sample_list.clear(allocator); // try sample_list.append(preview_samples.items); // self.preview_samples_y_range = RangeF64.init(-amplitude*1.1, amplitude*1.1); // self.protocol_graph_cache.invalidate(); // } // } // if (self.protocol_error_message) |message| { // _ = ui.createBox(.{ // .size_x = UI.Sizing.initGrowFull(), // .size_y = UI.Sizing.initFixedText(), // .text_color = srcery.red, // .align_x = .start, // .align_y = .start, // .flags = &.{ .wrap_text }, // .text = message // }); // } // _ = ui.createBox(.{ // .size_x = UI.Sizing.initGrowFull(), // .size_y = UI.Sizing.initGrowFull(), // }); // { // const row = ui.createBox(.{ // .size_x = UI.Sizing.initGrowFull(), // .size_y = UI.Sizing.initFitChildren(), // .align_x = .end // }); // row.beginChildren(); // defer row.endChildren(); // const btn = ui.textButton("Confirm"); // btn.borders = UI.Borders.all(.{ .color = srcery.green, .size = 4 }); // if (ui.signal(btn).clicked() or ui.isKeyboardPressed(.key_enter)) { // if (self.protocol_error_message == null) { // try App.Channel.generateSine(&channel.write_pattern, allocator, sample_rate, frequency, amplitude); // self.app.pushCommand(.{ .start_output = channel_id }); // self.view_controls.view_protocol_modal = null; // } // } // } } fn showNotesModal(self: *MainScreen) !void { var ui = &self.app.ui; const container = ui.createBox(.{ .key = ui.keyFromString("Notes modal"), .background = srcery.black, .size_x = UI.Sizing.initGrowUpTo(.{ .pixels = 400 }), .size_y = UI.Sizing.initGrowUpTo(.{ .pixels = 600 }), .layout_direction = .top_to_bottom, .padding = UI.Padding.all(ui.rem(1.5)), .flags = &.{ .clickable }, .layout_gap = ui.rem(0.5) }); container.beginChildren(); defer container.endChildren(); defer _ = ui.signal(container); const label = ui.label("Notes", .{}); label.font = .{ .variant = .regular_italic, .size = ui.rem(2) }; label.alignment.x = .center; label.size.x = UI.Sizing.initGrowFull(); try ui.textInput(.{ .key = ui.keyFromString("Notes"), .storage = &self.notes_storage, .size_x = UI.Sizing.initGrowFull(), .size_y = UI.Sizing.initGrowFull(), .single_line = false }); if (self.notes_storage.modified) { const project = &self.app.project; project.notes.clearRetainingCapacity(); try project.notes.insertSlice(self.app.allocator, 0, self.notes_storage.buffer.items); } } fn showProportionalAdd(self: *MainScreen) !void { var ui = &self.app.ui; const container = ui.createBox(.{ .key = ui.keyFromString("Proportional add modal"), .background = srcery.black, .size_x = UI.Sizing.initGrowUpTo(.{ .pixels = 300 }), .size_y = UI.Sizing.initGrowUpTo(.{ .pixels = 200 }), .layout_direction = .top_to_bottom, .padding = UI.Padding.all(ui.rem(1.5)), .flags = &.{ .clickable }, .layout_gap = ui.rem(0.5) }); container.beginChildren(); defer container.endChildren(); defer _ = ui.signal(container); if (self.proportion_view1 != null and self.app.project.views.get(self.proportion_view1.?) == null) { self.proportion_view1 = null; } if (self.proportion_view2 != null and self.app.project.views.get(self.proportion_view2.?) == null) { self.proportion_view2 = null; } var arena = std.heap.ArenaAllocator.init(self.app.allocator); defer arena.deinit(); var available_views = std.ArrayList(struct { view_id: App.Id, name: []const u8 }).init(arena.allocator()); { var view_iter = self.app.project.views.idIterator(); while (view_iter.next()) |view_id| { const view = self.app.project.views.get(view_id).?; if (view.reference != .channel) { continue; } const channel_id = view.reference.channel; const channel = self.app.project.channels.get(channel_id).?; const view_name = utils.getBoundedStringZ(&channel.name); try available_views.append(.{ .view_id = view_id, .name = view_name }); } } { var row = ui.createBox(.{ .size_x = UI.Sizing.initGrowFull(), .size_y = UI.Sizing.initFitChildren(), .layout_direction = .left_to_right, .layout_gap = ui.rem(0.5), }); row.beginChildren(); defer row.endChildren(); { const select = ui.textButton("Select dividend"); if (self.proportion_view1) |view_id| { const view = self.app.project.views.get(view_id).?; assert(view.reference == .channel); const channel = self.app.project.channels.get(view.reference.channel).?; select.setFmtText("Dividend: {s}", .{utils.getBoundedStringZ(&channel.name)}); } select.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 }); const signal = ui.signal(select); if (signal.clicked()) { select.persistent.open = !select.persistent.open; } if (select.persistent.open) { const popup = ui.createBox(.{ .key = ui.keyFromString("Transform 1 popup"), .size_x = UI.Sizing.initFixedPixels(ui.rem(10)), .size_y = UI.Sizing.initFitChildren(), .flags = &.{ .clickable, .scrollable }, .layout_direction = .top_to_bottom, .float_relative_to = select, .background = srcery.black, .borders = UI.Borders.all(.{ .color = srcery.bright_black, .size = 4 }), .draw_on_top = true }); popup.setFloatPosition(0, select.persistent.size.y); popup.beginChildren(); defer popup.endChildren(); defer _ = ui.signal(popup); for (available_views.items) |option| { const select_option = ui.textButton(option.name); select_option.alignment.x = .start; select_option.size.x = UI.Sizing.initGrowFull(); select_option.borders = UI.Borders.all(.{ .color = srcery.hard_black, .size = 2 }); select_option.background = srcery.black; if (ui.signal(select_option).clicked()) { select.persistent.open = false; self.proportion_view1 = option.view_id; } } } if (signal.clicked_outside) { select.persistent.open = false; } } { const select = ui.textButton("Select divisor"); if (self.proportion_view2) |view_id| { const view = self.app.project.views.get(view_id).?; assert(view.reference == .channel); const channel = self.app.project.channels.get(view.reference.channel).?; select.setFmtText("Divisor: {s}", .{utils.getBoundedStringZ(&channel.name)}); } select.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 }); const signal = ui.signal(select); if (signal.clicked()) { select.persistent.open = !select.persistent.open; } if (select.persistent.open) { const popup = ui.createBox(.{ .key = ui.keyFromString("Transform 1 popup"), .size_x = UI.Sizing.initFixedPixels(ui.rem(10)), .size_y = UI.Sizing.initFitChildren(), .flags = &.{ .clickable, .scrollable }, .layout_direction = .top_to_bottom, .float_relative_to = select, .background = srcery.black, .borders = UI.Borders.all(.{ .color = srcery.bright_black, .size = 4 }), .draw_on_top = true }); popup.setFloatPosition(0, select.persistent.size.y); popup.beginChildren(); defer popup.endChildren(); defer _ = ui.signal(popup); for (available_views.items) |option| { const select_option = ui.textButton(option.name); select_option.alignment.x = .start; select_option.size.x = UI.Sizing.initGrowFull(); select_option.borders = UI.Borders.all(.{ .color = srcery.hard_black, .size = 2 }); select_option.background = srcery.black; if (ui.signal(select_option).clicked()) { select.persistent.open = false; self.proportion_view2 = option.view_id; } } } if (signal.clicked_outside) { select.persistent.open = false; } } } { const btn = ui.textButton("Confirm"); btn.borders = UI.Borders.all(.{ .color = srcery.hard_black, .size = 4 }); if (self.proportion_view1 != null and self.proportion_view2 != null) { btn.borders = UI.Borders.all(.{ .color = srcery.green, .size = 4 }); if (ui.signal(btn).clicked()) { self.modal = null; const view1 = self.app.project.views.get(self.proportion_view1.?).?; const channel1 = self.app.project.channels.get(view1.reference.channel).?; const view2 = self.app.project.views.get(self.proportion_view2.?).?; const channel2 = self.app.project.channels.get(view2.reference.channel).?; const new_view_id = try self.app.addView(.{ .mirror = channel1.collected_samples_id }); const new_view = self.app.project.views.get(new_view_id).?; new_view.transformation.divider = channel2.collected_samples_id; } } } } // fn setProtocolErrorMessage(self: *MainScreen, comptime fmt: []const u8, args: anytype) !void { // self.clearProtocolErrorMessage(); // const allocator = self.app.allocator; // self.protocol_error_message = try std.fmt.allocPrint(allocator, fmt, args); // } // fn clearProtocolErrorMessage(self: *MainScreen) void { // const allocator = self.app.allocator; // if (self.protocol_error_message) |msg| { // allocator.free(msg); // } // self.protocol_error_message = null; // } fn showProjectSettings(self: *MainScreen) !void { var ui = &self.app.ui; const frame_allocator = ui.frameAllocator(); const project = &self.app.project; { const label = ui.label("Project", .{}); label.borders.bottom = .{ .color = srcery.bright_white, .size = 1 }; } _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) }); { _ = ui.label("Experiment:", .{}); try ui.textInput(.{ .key = ui.keyFromString("Experiment name"), .storage = &self.experiment_name, .initial = project.experiment_name.items, .size_x = UI.Sizing.initGrowFull() }); if (self.experiment_name.modified) { project.experiment_name.clearRetainingCapacity(); try project.experiment_name.insertSlice(self.app.allocator, 0, self.experiment_name.buffer.items); } } _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) }); { _ = ui.label("Pipete solution:", .{}); try ui.textInput(.{ .key = ui.keyFromString("Pipete solution"), .storage = &self.pipete_solution, .initial = project.pipete_solution.items, .size_x = UI.Sizing.initGrowFull() }); if (self.pipete_solution.modified) { project.pipete_solution.clearRetainingCapacity(); try project.pipete_solution.insertSlice(self.app.allocator, 0, self.pipete_solution.buffer.items); } } _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) }); { _ = ui.label("Solution:", .{}); try ui.textInput(.{ .key = ui.keyFromString("Solution input"), .storage = &self.solution_input, .size_x = UI.Sizing.initGrowFull() }); { const row = ui.createBox(.{ .size_x = UI.Sizing.initFitChildren(), .size_y = UI.Sizing.initFitChildren(), .layout_direction = .left_to_right, .layout_gap = ui.rem(1) }); row.beginChildren(); defer row.endChildren(); { const btn = ui.textButton("Append solution"); btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 }); if (ui.signal(btn).clicked()) { if (self.solution_input.buffer.items.len > 0) { try project.appendSolution(self.app.allocator, self.solution_input.buffer.items); } self.solution_input.clear(); } } if (project.solutions.items.len > 0) { const btn = ui.textButton("View solutions"); btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 }); if (ui.signal(btn).clicked()) { self.side_panel = .solutions; } } } } _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) }); { // Gain _ = ui.label("Gain:", .{}); const last_gain_change = project.gain_changes.buffer[project.gain_changes.len - 1]; const current_gain = try ui.numberInput(f64, .{ .key = ui.keyFromString("Gain input"), .storage = &self.gain_input, .initial = last_gain_change.gain, .width = 400 }); if (!self.gain_input.editing and current_gain != null and current_gain.? != last_gain_change.gain) { try project.gain_changes.append(.{ .gain = current_gain.?, .sample = @floatFromInt(project.getSampleTimestamp() orelse 0) }); } { const btn = ui.textButton("Show gain changes"); btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 }); if (ui.signal(btn).clicked()) { self.side_panel = .gain_changes; } } } _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) }); { // Statistic points const row = ui.createBox(.{ .size_x = UI.Sizing.initFitChildren(), .size_y = UI.Sizing.initFitChildren(), .layout_direction = .left_to_right, .layout_gap = ui.rem(0.5) }); row.beginChildren(); defer row.endChildren(); { const btn = ui.textButton("Add statistic point"); btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 }); if (ui.signal(btn).clicked()) { const current_sample = project.getSampleTimestamp() orelse 0; try project.statistic_points.append(self.app.allocator, current_sample); } } if (project.statistic_points.items.len > 0) { const btn = ui.textButton("View statistic points"); btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 }); if (ui.signal(btn).clicked()) { self.side_panel = .statistics_points; } } } _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) }); { // Sample rate var placeholder: ?[]const u8 = null; if (project.getDefaultSampleRate()) |default_sample_rate| { placeholder = try std.fmt.allocPrint(frame_allocator, "{d}", .{ default_sample_rate }); } _ = ui.label("Sample rate:", .{}); self.parsed_sample_rate = try ui.numberInput(f64, .{ .key = ui.keyFromString("Sample rate input"), .storage = &self.sample_rate_input, .placeholder = placeholder, .initial = project.sample_rate, .invalid = self.parsed_sample_rate != project.sample_rate, .editable = !self.app.isCollectionInProgress(), .width = 400 }); project.sample_rate = self.parsed_sample_rate; if (project.getAllowedSampleRates()) |allowed_sample_rates| { if (project.sample_rate) |selected_sample_rate| { if (!allowed_sample_rates.hasInclusive(selected_sample_rate)) { project.sample_rate = null; } } } } _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) }); { // Open notes const btn = ui.textButton("Open notes"); btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 }); if (ui.signal(btn).clicked()) { self.modal = .notes; } } _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) }); { // Export folder _ = ui.label("Export folder:", .{}); if (ui.fileInput(.{ .allocator = self.app.allocator, .key = ui.keyFromString("Export location"), .path = project.export_location, .file_picker = &self.export_location_picker, .open_dialog = true, .folder = true, .size_x = UI.Sizing.initGrowFull() })) |path| { if (project.export_location) |str| { self.app.allocator.free(str); } project.export_location = path; } const btn = ui.textButton("Export"); if (self.app.isCollectionInProgress() or project.export_location == null) { btn.borders = UI.Borders.all(.{ .color = srcery.hard_black, .size = 4 }); } else { btn.borders = UI.Borders.all(.{ .color = srcery.green, .size = 4 }); if (ui.signal(btn).clicked()) { self.app.pushCommand(.export_project); } } } } fn showViewSettings(self: *MainScreen, view_id: Id) !void { var ui = &self.app.ui; const project = &self.app.project; const sample_rate = project.getSampleRate(); const view = project.views.get(view_id) orelse return; // TODO: Hack if (self.prev_view_settings == null or !self.prev_view_settings.?.eql(view_id)) { self.view_name_input.clear(); try self.view_name_input.setText(view.name.constSlice()); self.prev_view_settings = view_id; } { const label = ui.label("Settings", .{}); label.borders.bottom = .{ .color = srcery.bright_white, .size = 1 }; _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) }); } { _ = ui.label("Name", .{}); try ui.textInput(.{ .key = ui.keyFromString("Name input"), .storage = &self.view_name_input, .initial = view.name.constSlice() }); if (self.view_name_input.modified) { const name = self.view_name_input.textSlice(); view.name.len = 0; if (name.len > view.name.buffer.len) { view.name.appendSliceAssumeCapacity(name[0..view.name.buffer.len]); } else { view.name.appendSliceAssumeCapacity(name); } } } _ = ui.label("Statistics:", .{}); switch (view.reference) { .channel => |channel_id| { const channel = project.channels.get(channel_id).?; const channel_name = utils.getBoundedStringZ(&channel.name); const channel_type = NIDaq.getChannelType(channel_name); _ = ui.label("Channel: {s}", .{ channel_name }); if (channel_type != null) { _ = ui.label("Type: {s}", .{ channel_type.?.name() }); } else { _ = ui.label("Type: unknown", .{ }); } }, .file => |file_id| { const file = project.files.get(file_id).?; _ = ui.label("File", .{}); if (ui.fileInput(.{ .key = ui.keyFromString("Filename input"), .allocator = self.app.allocator, .file_picker = &self.file_save_file_picker, .path = file.path })) |path| { self.app.allocator.free(file.path); file.path = path; self.app.pushCommand(.{ .reload_file = file_id }); } }, .mirror => |sample_list_id| { _ = sample_list_id; } } const sample_list_id = project.getViewReferenceSampleListId(view_id); const sample_list = project.sample_lists.get(sample_list_id).?; const sample_count = sample_list.getLength(); _ = ui.label("Samples: {d}", .{ sample_count }); var duration_str: ?[]const u8 = null; if (sample_rate != null) { const duration = @as(f64, @floatFromInt(sample_count)) / sample_rate.?; if (utils.formatDuration(ui.frameAllocator(), duration, false)) |str| { duration_str = str; } else |_| {} } if (duration_str == null) { duration_str = std.fmt.allocPrint(ui.frameAllocator(), "{d}", .{ sample_count }) catch null; } _ = ui.label("Duration: {s}", .{ duration_str orelse "-" }); // var deferred_remove: std.BoundedArray(usize, App.View.max_transforms) = .{}; // for (0.., view.transforms.slice()) |i, *_transform| { // const transform: *App.Transform = _transform; // const row = ui.createBox(.{ // .key = UI.Key.initPtr(transform), // .size_x = UI.Sizing.initGrowFull(), // .size_y = UI.Sizing.initFixedPixels(ui.rem(1.5)), // .layout_direction = .left_to_right // }); // row.beginChildren(); // defer row.endChildren(); // if (ui.signal(ui.textButton("Remove")).clicked()) { // deferred_remove.appendAssumeCapacity(i); // } // { // const options = .{ // .{ .multiply, "Multiply" }, // .{ .addition, "Addition" }, // .{ .sliding_window, "Sliding window" }, // }; // const select = ui.button(ui.keyFromString("Transform select")); // select.setFmtText("{s}", .{switch (transform.*) { // .sliding_window => "Sliding window", // .addition => "Addition", // .multiply => "Multiply" // }}); // select.size.y = UI.Sizing.initGrowFull(); // select.alignment.y = .center; // if (ui.signal(select).clicked()) { // select.persistent.open = !select.persistent.open; // } // if (select.persistent.open) { // const popup = ui.createBox(.{ // .key = ui.keyFromString("Select popup"), // .size_x = UI.Sizing.initFixedPixels(ui.rem(10)), // .size_y = UI.Sizing.initFitChildren(), // .flags = &.{ .clickable, .scrollable }, // .layout_direction = .top_to_bottom, // .float_relative_to = select, // .background = srcery.black, // .borders = UI.Borders.all(.{ .color = srcery.bright_black, .size = 4 }), // .draw_on_top = true // }); // popup.setFloatPosition(0, select.persistent.size.y); // popup.beginChildren(); // defer popup.endChildren(); // inline for (options) |option| { // const select_option = ui.textButton(option[1]); // select_option.alignment.x = .start; // select_option.size.x = UI.Sizing.initGrowFull(); // select_option.borders = UI.Borders.all(.{ .color = srcery.hard_black, .size = 2 }); // select_option.background = srcery.black; // const signal = ui.signal(select_option); // if (signal.clicked()) { // select.persistent.open = false; // transform.* = switch (option[0]) { // .sliding_window => App.Transform{ .sliding_window = sample_rate orelse 0 }, // .addition => App.Transform{ .addition = 0 }, // .multiply => App.Transform{ .multiply = 1 }, // else => unreachable // }; // } // } // _ = ui.signal(popup); // } // } // var input_opts = UI.NumberInputOptions{ // .key = ui.keyFromString("Sliding window"), // .storage = &self.transform_inputs[i], // .width = ui.rem(4) // // .postfix = if (sample_rate != null) " s" else null, // // .display_scalar = sample_rate, // }; // _ = ui.createBox(.{ .size_x = UI.Sizing.initGrowFull() }); // if (transform.* == .sliding_window and sample_rate != null) { // input_opts.postfix = " s"; // input_opts.display_scalar = sample_rate; // } // const current_value = switch (transform.*) { // .sliding_window => |*v| v, // .addition => |*v| v, // .multiply => |*v| v // }; // input_opts.initial = current_value.*; // const new_value = try ui.numberInput(f64, input_opts); // if (new_value != null) { // current_value.* = new_value.?; // } // } // for (0..deferred_remove.len) |i| { // const transform_index = deferred_remove.get(deferred_remove.len - 1 - i); // _ = view.transforms.orderedRemove(transform_index); // } // if (view.transforms.unusedCapacitySlice().len > 0) { // const btn = ui.textButton("Add transform"); // if (ui.signal(btn).clicked()) { // view.transforms.appendAssumeCapacity(.{ .addition = 0 }); // } // } _ = ui.createBox(.{ .size_y = UI.Sizing.initGrowFull() }); { const btn = ui.textButton("Delete"); btn.borders = UI.Borders.all(.{ .color = srcery.red, .size = 4 }); if (ui.signal(btn).clicked()) { project.removeView(self.app.allocator, view_id); } } } // fn showMarkedRange(self: *MainScreen, view_id: Id, index: usize) void { // var ui = &self.app.ui; // const view = self.app.getView(view_id) orelse return; // const marked_range = view.marked_ranges.get(index); // { // const label = ui.label("Selected range", .{}); // label.borders.bottom = .{ // .color = srcery.blue, // .size = 1 // }; // _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) }); // } // const sample_rate = self.app.project.getSampleRate(); // if (marked_range.axis == .X and sample_rate != null) { // _ = ui.label("From: {d:.3}s", .{ marked_range.range.lower / sample_rate.? }); // _ = ui.label("To: {d:.3}s", .{ marked_range.range.upper / sample_rate.? }); // _ = ui.label("Size: {d:.3}s", .{ marked_range.range.size() / sample_rate.? }); // } else { // _ = ui.label("From: {d:.2}", .{ marked_range.range.lower }); // _ = ui.label("To: {d:.2}", .{ marked_range.range.upper }); // _ = ui.label("Size: {d:.2}", .{ marked_range.range.size() }); // } // _ = ui.label("Samples: {d:.2}", .{ marked_range.range.size() }); // if (marked_range.axis == .X) { // if (marked_range.min) |min| { // _ = ui.label("Minimum: {d:.3}", .{ min }); // } else{ // _ = ui.label("Minimum: ", .{}); // } // if (marked_range.max) |max| { // _ = ui.label("Maximum: {d:.3}", .{ max }); // } else{ // _ = ui.label("Maximum: ", .{}); // } // if (marked_range.average) |average| { // _ = ui.label("Average: {d:.3}", .{ average }); // } else{ // _ = ui.label("Average: ", .{}); // } // if (marked_range.standard_deviation) |standard_deviation| { // _ = ui.label("Standard deviation: {d:.3}", .{ standard_deviation }); // } else{ // _ = ui.label("Standard deviation: ", .{}); // } // } // _ = ui.createBox(.{ .size_y = UI.Sizing.initGrowFull() }); // { // const btn = ui.textButton("Remove"); // btn.borders = UI.Borders.all(.{ .color = srcery.red, .size = 4 }); // const signal = ui.signal(btn); // if (signal.clicked() or ui.isKeyboardPressed(.key_backspace)) { // self.side_panel = .project; // _ = view.marked_ranges.swapRemove(index); // } // } // } // fn showMarker(self: *MainScreen, view_id: Id, index: usize) void { // var ui = &self.app.ui; // const view = self.app.getView(view_id) orelse return; // const marker = view.markers.get(index); // { // const label = ui.label("Selected range", .{}); // label.borders.bottom = .{ // .color = srcery.blue, // .size = 1 // }; // _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) }); // } // const sample_rate = self.app.project.getSampleRate(); // if (sample_rate != null) { // const duration = utils.formatDuration(ui.frameAllocator(), marker / sample_rate.?) catch ""; // _ = ui.label("Position: {s}", .{ duration }); // } else { // _ = ui.label("Position: {d:.2}", .{ marker }); // } // } fn showToolbar(self: *MainScreen) void { var ui = &self.app.ui; const toolbar = ui.createBox(.{ .background = srcery.black, .layout_direction = .left_to_right, .size_x = .{ .fixed = .{ .parent_percent = 1 } }, .size_y = .{ .fixed = .{ .font_size = 2 } }, .borders = .{ .bottom = .{ .color = srcery.hard_black, .size = 4 } } }); toolbar.beginChildren(); defer toolbar.endChildren(); { var btn = ui.textButton("Start/Stop button"); btn.borders = UI.Borders.all(.{ .size = 4, .color = srcery.red }); btn.background = srcery.black; btn.size.y = UI.Sizing.initFixed(.{ .parent_percent = 1 }); btn.padding.top = 0; btn.padding.bottom = 0; if (ui.signal(btn).clicked()) { if (self.app.isCollectionInProgress()) { self.app.pushCommand(.stop_collection); } else { self.app.pushCommand(.start_collection); } } if (self.app.isCollectionInProgress()) { btn.setText("Stop"); } else { btn.setText("Start"); } } { const btn = ui.textButton("Add"); btn.borders = UI.Borders.all(.{ .size = 4, .color = srcery.green }); const signal = ui.signal(btn); if (signal.clicked()) { btn.persistent.open = !btn.persistent.open; } if (btn.persistent.open) { const popup = ui.createBox(.{ .key = ui.keyFromString("Add popup"), .size_x = UI.Sizing.initFixedPixels(ui.rem(10)), .size_y = UI.Sizing.initFitChildren(), .flags = &.{ .clickable, .scrollable }, .layout_direction = .top_to_bottom, .float_relative_to = btn, .background = srcery.black, .draw_on_top = true }); popup.setFloatPosition(0, btn.persistent.size.y); popup.beginChildren(); defer popup.endChildren(); { const select_btn = ui.textButton("... from file"); select_btn.alignment.x = .start; select_btn.size.x = UI.Sizing.initGrowFull(); select_btn.background = srcery.hard_black; select_btn.borders = UI.Borders.all(.{ .color = srcery.hard_black, .size = 2 }); if (ui.signal(select_btn).clicked()) { self.app.pushCommand(.add_file_from_picker); } } { const select_btn = ui.textButton("... from device"); select_btn.alignment.x = .start; select_btn.size.x = UI.Sizing.initGrowFull(); select_btn.background = srcery.hard_black; select_btn.borders = UI.Borders.all(.{ .color = srcery.hard_black, .size = 2 }); if (ui.signal(select_btn).clicked()) { self.app.screen = .add_channels; } } { const select_btn = ui.textButton("... proportional"); select_btn.alignment.x = .start; select_btn.size.x = UI.Sizing.initGrowFull(); select_btn.background = srcery.hard_black; select_btn.borders = UI.Borders.all(.{ .color = srcery.hard_black, .size = 2 }); if (ui.signal(select_btn).clicked()) { self.app.main_screen.modal = .add_proprotional; } } // inline for (options) |option| { // const select_option = ui.textButton(option[1]); // select_option.alignment.x = .start; // select_option.size.x = UI.Sizing.initGrowFull(); // select_option.borders = UI.Borders.all(.{ .color = srcery.hard_black, .size = 2 }); // select_option.background = srcery.black; // const signal = ui.signal(select_option); // if (signal.clicked()) { // select.persistent.open = false; // transform.* = switch (option[0]) { // .sliding_window => App.Transform{ .sliding_window = sample_rate orelse 0 }, // .addition => App.Transform{ .addition = 0 }, // .multiply => App.Transform{ .multiply = 1 }, // else => unreachable // }; // } // } _ = ui.signal(popup); } if (signal.clicked_outside) { btn.persistent.open = false; } } { var btn = ui.textButton("Protocol"); btn.borders = UI.Borders.all(.{ .size = 4, .color = srcery.hard_black }); if (self.app.isCollectionInProgress()) { const is_outputing = self.app.isOutputingInProgress(); if (is_outputing) { btn.borders = UI.Borders.all(.{ .size = 4, .color = srcery.red }); } else { btn.borders = UI.Borders.all(.{ .size = 4, .color = srcery.green }); } if (ui.signal(btn).clicked()) { if (is_outputing) { self.app.stopAllOutput(); } else { self.app.main_screen.modal = .protocol; } } } } // { // var btn = ui.textButton("Select"); // if (self.view_controls.selected_tool == .select) { // btn.borders = UI.Borders.bottom(.{ .size = 4, .color = srcery.green }); // } // if (ui.signal(btn).clicked()) { // self.view_controls.selected_tool = .select; // } // } // { // var btn = ui.textButton("Marker"); // if (self.view_controls.selected_tool == .marker) { // btn.borders = UI.Borders.bottom(.{ .size = 4, .color = srcery.green }); // } // if (ui.signal(btn).clicked()) { // self.view_controls.selected_tool = .marker; // } // } } fn showSolutions(self: *MainScreen) !void { var ui = &self.app.ui; const project = &self.app.project; _ = &ui; { const label = ui.label("Solutions", .{}); label.borders.bottom = .{ .color = srcery.white, .size = 1 }; _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) }); } const sample_rate = project.getSampleRate(); for (project.solutions.items) |solution| { const row = ui.createBox(.{ .size_x = UI.Sizing.initGrowFull(), .size_y = UI.Sizing.initFitChildren(), .layout_direction = .left_to_right, .layout_gap = ui.rem(1) }); row.beginChildren(); defer row.endChildren(); if (sample_rate != null) { const seconds = @as(f64, @floatFromInt(solution.sample)) / sample_rate.?; _ = ui.label("{s}", .{ try utils.formatDuration(ui.frameAllocator(), seconds, false) }); } else { _ = ui.label("{d}", .{ solution.sample }); } var description = ui.label("{s}", .{ solution.description }); description.size.x = UI.Sizing.initGrowFull(); description.flags.insert(.wrap_text); } _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) }); { const btn = ui.textButton("Close"); btn.borders = UI.Borders.all(.{ .color = srcery.red, .size = 4 }); if (ui.signal(btn).clicked()) { self.side_panel = .project; } } } fn showStatisticsPoints(self: *MainScreen) !void { const ui = &self.app.ui; const project = &self.app.project; { const label = ui.label("Statistics points", .{}); label.borders.bottom = .{ .color = srcery.white, .size = 1 }; _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) }); } const sample_rate = project.getSampleRate(); for (project.statistic_points.items) |sample_timestamp| { const row = ui.createBox(.{ .size_x = UI.Sizing.initGrowFull(), .size_y = UI.Sizing.initFitChildren(), .layout_direction = .left_to_right, .layout_gap = ui.rem(1) }); row.beginChildren(); defer row.endChildren(); if (sample_rate != null) { const seconds = @as(f64, @floatFromInt(sample_timestamp)) / sample_rate.?; _ = ui.label("{s}", .{ try utils.formatDuration(ui.frameAllocator(), seconds, false) }); } else { _ = ui.label("{d}", .{ sample_timestamp }); } } _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) }); { const btn = ui.textButton("Close"); btn.borders = UI.Borders.all(.{ .color = srcery.red, .size = 4 }); if (ui.signal(btn).clicked()) { self.side_panel = .project; } } } fn showGainChanges(self: *MainScreen) !void { const project = &self.app.project; var ui = &self.app.ui; { const label = ui.label("Gain changes", .{}); label.borders.bottom = .{ .color = srcery.white, .size = 1 }; _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) }); } const sample_rate = project.getSampleRate(); var removed_index: ?usize = null; for (0.., project.gain_changes.slice()) |i, *gain_change| { const row = ui.createBox(.{ .key = UI.Key.initPtr(gain_change), .size_x = UI.Sizing.initGrowFull(), .size_y = UI.Sizing.initFitChildren(), .layout_direction = .left_to_right, .align_y = .center, .layout_gap = ui.rem(1) }); row.beginChildren(); defer row.endChildren(); if (sample_rate != null) { const seconds = gain_change.sample / sample_rate.?; _ = ui.label("{s}", .{ try utils.formatDuration(ui.frameAllocator(), seconds, false) }); } else { _ = ui.label("{d}", .{ gain_change.sample }); } var gain_label = ui.label("{d:.3}", .{ gain_change.gain }); gain_label.size.x = UI.Sizing.initGrowFull(); if (i > 0) { const btn = ui.textButton("Remove"); btn.background = srcery.bright_red; btn.padding = UI.Padding.all(ui.rem(0.2)); if (ui.signal(btn).clicked()) { removed_index = i; } } } if (removed_index) |index| { project.removeGainChange(index); } _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) }); { const btn = ui.textButton("Close"); btn.borders = UI.Borders.all(.{ .color = srcery.red, .size = 4 }); if (ui.signal(btn).clicked()) { self.side_panel = .project; } } } pub fn showSidePanel(self: *MainScreen) !void { var ui = &self.app.ui; const container = ui.createBox(.{ .size_x = UI.Sizing.initFitChildren(), .size_y = UI.Sizing.initGrowFull(), .borders = .{ .right = .{ .color = srcery.hard_black, .size = 4 } }, .layout_direction = .top_to_bottom, .padding = UI.Padding.all(ui.rem(1)), .layout_gap = ui.rem(0.2) }); container.beginChildren(); defer container.endChildren(); _ = ui.createBox(.{ .size_x = UI.Sizing.initFixedPixels(ui.rem(18)) }); switch (self.side_panel) { .project => { try self.showProjectSettings(); }, .solutions => { try self.showSolutions(); }, .statistics_points => { try self.showStatisticsPoints(); }, .gain_changes => { try self.showGainChanges(); }, .view_settings => |view_id| { try self.showViewSettings(view_id); }, // .marker => |args| { // self.showMarker(args.view_id, args.index); // }, // .marked_range => |args| { // self.showMarkedRange(args.view_id, args.index); // } } } pub fn tick(self: *MainScreen) !void { var ui = &self.app.ui; if (ui.isCtrlDown() and ui.isKeyboardPressed(.key_z)) { self.view_controls.undoLastMove(); } const root = ui.parentBox().?; root.layout_direction = .top_to_bottom; // const was_modal_open = self.modal != null; var maybe_modal_overlay: ?*UI.Box = null; if (self.modal != null) { const padding = UI.Padding.all(ui.rem(2)); const modal_overlay = ui.createBox(.{ .key = ui.keyFromString("Overlay"), .float_rect = .{ .x = 0, .y = 0, // TODO: This is a hack, UI core should handle this .width = root.persistent.size.x - padding.byAxis(.X), .height = root.persistent.size.y - padding.byAxis(.Y) }, .background = rl.Color.black.alpha(0.6), .flags = &.{ .clickable, .scrollable }, .padding = padding, .align_x = .center, .align_y = .center, }); modal_overlay.beginChildren(); defer modal_overlay.endChildren(); switch (self.modal.?) { .notes => try self.showNotesModal(), .add_proprotional => try self.showProportionalAdd(), .protocol => try self.showProtocolModal() } if (ui.signal(modal_overlay).clicked()) { self.modal = null; } maybe_modal_overlay = modal_overlay; } self.showToolbar(); const ui_view_ctx = UIView.Context{ .app = self.app, .ui = &self.app.ui, .view_controls = &self.view_controls }; { const container = ui.createBox(.{ .size_x = UI.Sizing.initGrowFull(), .size_y = UI.Sizing.initGrowFull(), .layout_direction = .left_to_right }); container.beginChildren(); defer container.endChildren(); try self.showSidePanel(); const scroll_area = ui.beginScrollbar(ui.keyFromString("Channels")); defer ui.endScrollbar(); scroll_area.layout_direction = .top_to_bottom; var view_iter = self.app.project.views.idIterator(); while (view_iter.next()) |view_id| { const view = self.app.getView(view_id); _ = try UIView.show(ui_view_ctx, view_id, UI.Sizing.initFixed(.{ .pixels = view.?.height })); } } self.view_controls.applyCommands(); if (maybe_modal_overlay) |modal_overlay| { root.bringChildToTop(modal_overlay); } if (ui.isKeyboardPressed(.key_escape)) { if (self.modal != null) { self.modal = null; } else if (self.side_panel != .project) { self.side_panel = .project; } else { self.app.should_close = true; } } // const is_modal_open = self.modal != null; // if (!was_modal_open and is_modal_open) { // self.protocol_graph_cache.clear(); // } }