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, modal: ?union(enum){ view_protocol: Id, // View id notes } = null, // Protocol modal 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 sample_rate_input: UI.TextInputStorage, parsed_sample_rate: ?f64 = null, // View settings transform_inputs: [App.View.max_transforms]UI.TextInputStorage, channel_save_file_picker: ?Platform.FilePickerId = null, file_save_file_picker: ?Platform.FilePickerId = null, pub fn init(app: *App) !MainScreen { const allocator = app.allocator; var transform_inputs: [App.View.max_transforms]UI.TextInputStorage = undefined; for (&transform_inputs) |*input| { input.* = UI.TextInputStorage.init(allocator); } var 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), .view_controls = ViewControlsSystem.init(&app.project), .transform_inputs = transform_inputs, .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(); for (self.transform_inputs) |input| { input.deinit(); } self.app.project.removeSampleList(self.preview_sample_list_id); self.clearProtocolErrorMessage(); } pub fn showProtocolModal(self: *MainScreen, view_id: Id) !void { var ui = &self.app.ui; const allocator = self.app.allocator; const view = self.app.getView(view_id) orelse return; if (view.reference != .channel) return; const channel_id = view.reference.channel; const channel = self.app.getChannel(channel_id) orelse return; const sample_rate = self.app.project.getSampleRate() orelse return; 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(); { 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; } } } _ = ui.signal(container); } 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 }); } 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)) }); { // 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() }); 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.checkbox(.{ .value = &project.show_rulers, .label = "Ruler" }); if (ui.signal(ui.textButton("Open notes")).clicked()) { self.modal = .notes; } } 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; { 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.checkbox(.{ .value = &view.sync_controls, .label = "Sync controls" }); 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", .{ }); } if (ui.fileInput(.{ .key = ui.keyFromString("Save location"), .allocator = self.app.allocator, .file_picker = &self.channel_save_file_picker, .path = channel.saved_collected_samples, .open_dialog = false })) |path| { if (channel.saved_collected_samples) |current_path| { self.app.allocator.free(current_path); } channel.saved_collected_samples = path; } }, .file => |file_id| { const file = project.files.get(file_id).?; if (ui.fileInput(.{ .key = ui.keyFromString("Filename"), .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 }); } } } const sample_list_id = project.getViewSampleListId(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)) |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 }); } } } 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.view_controls.show_marked_range = null; _ = view.marked_ranges.swapRemove(index); } } } 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"); } } { var btn = ui.textButton("Save"); btn.borders = UI.Borders.all(.{ .size = 4, .color = srcery.green }); if (ui.signal(btn).clicked()) { self.app.pushCommand(.save_project); } } _ = ui.createBox(.{ .size_x = UI.Sizing.initFixedPixels(ui.rem(1)) }); { var btn = ui.textButton("Move"); if (self.view_controls.selected_tool == .move) { btn.borders = UI.Borders.bottom(.{ .size = 4, .color = srcery.green }); } if (ui.signal(btn).clicked() or ui.isKeyboardPressed(.key_one)) { self.view_controls.selected_tool = .move; } } { 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() or ui.isKeyboardPressed(.key_two)) { self.view_controls.selected_tool = .select; } } } 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(12)) }); if (self.view_controls.show_marked_range) |show_marked_range| { self.showMarkedRange(show_marked_range.view_id, show_marked_range.index); } else if (self.view_controls.view_settings) |view_id| { try self.showViewSettings(view_id); } else { try self.showProjectSettings(); } } 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; if (self.view_controls.view_protocol_modal) |view_protocol_modal| { if (self.modal == null) { self.modal = .{ .view_protocol = view_protocol_modal }; } self.view_controls.view_protocol_modal = null; } 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.?) { .view_protocol => |view_id| try self.showProtocolModal(view_id), .notes => try self.showNotesModal() } 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 }; if (self.view_controls.view_fullscreen) |view_id| { _ = try UIView.show(ui_view_ctx, view_id, UI.Sizing.initGrowFull()); } else { 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 })); } { const add_channel_view = ui.createBox(.{ .size_x = UI.Sizing.initGrowFull(), .size_y = UI.Sizing.initFixed(.{ .pixels = 200 }), .align_x = .center, .align_y = .center, .layout_gap = 32 }); add_channel_view.beginChildren(); defer add_channel_view.endChildren(); const add_from_file = ui.textButton("Add from file"); add_from_file.borders = UI.Borders.all(.{ .size = 2, .color = srcery.green }); if (ui.signal(add_from_file).clicked()) { self.app.pushCommand(.add_file_from_picker); } const add_from_device = ui.textButton("Add from device"); add_from_device.borders = UI.Borders.all(.{ .size = 2, .color = srcery.green }); if (ui.signal(add_from_device).clicked()) { self.app.screen = .add_channels; } } } 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.view_controls.view_fullscreen != null) { self.view_controls.view_fullscreen = null; } else if (self.view_controls.view_settings != null) { self.view_controls.view_settings = null; } else if (self.view_controls.show_marked_range != null) { self.view_controls.show_marked_range = null; } 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(); } }