const std = @import("std"); const App = @import("../../app.zig"); const UI = @import("../../ui.zig"); const RangeF64 = @import("../../range.zig").RangeF64; const constants = @import("../../constants.zig"); const Id = App.Id; const System = @This(); pub const ViewAxisPosition = struct { view_id: Id, axis: UI.Axis, position: f64, fn set(optional_self: *?ViewAxisPosition, view_id: Id, axis: UI.Axis, position: ?f64) void { if (optional_self.*) |self| { if (self.axis != axis or !self.view_id.eql(view_id)) { return; } } if (position != null) { optional_self.* = .{ .axis = axis, .position = position.?, .view_id = view_id }; } else { optional_self.* = null; } } fn get(optional_self: *?ViewAxisPosition, project: *App.Project, view_id: Id, axis: UI.Axis) ?f64 { const self = optional_self.* orelse return null; if (self.axis != axis) return null; const view = project.views.get(view_id) orelse return null; if (view.sync_controls) { const owner_view = project.views.get(self.view_id).?; if (!owner_view.sync_controls) { return null; } } else { if (!self.view_id.eql(view_id)) { return null; } } return self.position; } }; pub const Command = union(enum) { breakpoint, move_and_zoom: struct { view_id: Id, before_x: RangeF64, before_y: RangeF64, x: RangeF64, y: RangeF64 }, fn apply(self: *const Command, system: *System) void { const project = system.project; switch (self.*) { .breakpoint => {}, .move_and_zoom => |move_and_zoom| { const view = project.views.get(move_and_zoom.view_id) orelse return; const view_rect = &view.graph_opts; view_rect.x_range = move_and_zoom.x; view_rect.y_range = move_and_zoom.y; view.follow = false; // TODO: Undo follow } } } fn undo(self: *const Command, system: *System) void { const project = system.project; switch (self.*) { .breakpoint => {}, .move_and_zoom => |move_and_zoom| { const view = project.views.get(move_and_zoom.view_id) orelse return; const view_rect = &view.graph_opts; view_rect.x_range = move_and_zoom.before_x; view_rect.y_range = move_and_zoom.before_y; } } } }; pub const CommandFrame = struct { updated_at_ns: i128, commands: std.BoundedArray(Command, constants.max_views) = .{}, fn findCommandByView(self: *CommandFrame, view_id: Id) ?*Command { const commands: []Command = self.commands.slice(); for (commands) |*command| { if (command.* == .move_and_zoom and command.move_and_zoom.view_id.eql(view_id)) { return command; } } return null; } fn apply(self: *const CommandFrame, system: *System) void { const commands: []const Command = self.commands.constSlice(); for (commands) |*command| { command.apply(system); } } fn undo(self: *const CommandFrame, system: *System) void { const commands: []const Command = self.commands.constSlice(); for (0..commands.len) |i| { const command = commands[commands.len - 1 - i]; command.undo(system); } } fn hasBreakpoint(self: *const CommandFrame) bool { const commands: []const Command = self.commands.constSlice(); for (commands) |*command| { if (command.* == .breakpoint) { return true; } } return false; } }; pub const CommandFrameArray = std.BoundedArray(CommandFrame, 64); project: *App.Project, // TODO: Redo undo_stack: CommandFrameArray = .{}, last_applied_command: usize = 0, zoom_start: ?ViewAxisPosition = null, cursor: ?ViewAxisPosition = null, view_settings: ?Id = null, // View id view_fullscreen: ?Id = null, // View id view_protocol_modal: ?Id = null, // View id show_ruler: bool = false, pub fn init(project: *App.Project) System { return System{ .project = project }; } fn pushCommandFrame(self: *System) *CommandFrame { if (self.undo_stack.unusedCapacitySlice().len == 0) { _ = self.undo_stack.orderedRemove(0); } var frame = self.undo_stack.addOneAssumeCapacity(); frame.updated_at_ns = std.time.nanoTimestamp(); frame.commands.len = 0; return frame; } fn lastCommandFrame(self: *System) ?*CommandFrame { if (self.undo_stack.len > 0) { return &self.undo_stack.buffer[self.undo_stack.len - 1]; } return null; } pub fn pushBreakpoint(self: *System) void { if (self.lastCommandFrame()) |last_frame| { // No need to have 2 break points in a row if (last_frame.hasBreakpoint()) { return; } } const frame = self.pushCommandFrame(); frame.commands.appendAssumeCapacity(.{ .breakpoint = {} }); } pub fn pushViewMove(self: *System, view_id: Id, x_range: RangeF64, y_range: RangeF64) void { var frame: *CommandFrame = undefined; { var push_new_command = true; if (self.lastCommandFrame()) |last_frame| { frame = last_frame; const now_ns = std.time.nanoTimestamp(); if (now_ns - last_frame.updated_at_ns < std.time.ns_per_ms * 250) { last_frame.updated_at_ns = now_ns; push_new_command = false; if (self.last_applied_command == self.undo_stack.len) { self.last_applied_command -= 1; } } } if (push_new_command) { frame = self.pushCommandFrame(); } } var sync_controls = false; if (self.project.views.get(view_id)) |view| { sync_controls = view.sync_controls; } var view_ids: std.BoundedArray(Id, constants.max_views) = .{}; if (sync_controls) { var iter = self.project.views.idIterator(); while (iter.next()) |id| { if (self.project.views.get(id).?.sync_controls) { view_ids.appendAssumeCapacity(id); } } } else { view_ids.appendAssumeCapacity(view_id); } for (view_ids.constSlice()) |id| { var command: *Command = undefined; if (frame.findCommandByView(id)) |prev_command| { command = prev_command; command.move_and_zoom.x = x_range; command.move_and_zoom.y = y_range; } else { const view = self.project.views.get(view_id) orelse continue; const view_rect = &view.graph_opts; command = frame.commands.addOneAssumeCapacity(); command.* = Command{ .move_and_zoom = .{ .view_id = id, .before_x = view_rect.x_range, .before_y = view_rect.y_range, .x = x_range, .y = y_range } }; } } } pub fn pushViewMoveAxis(self: *System, view_id: Id, axis: UI.Axis, view_range: RangeF64) void { const view = self.project.views.get(view_id) orelse return; const view_rect = &view.graph_opts; if (axis == .X) { self.pushViewMove(view_id, view_range, view_rect.y_range); } else { self.pushViewMove(view_id, view_rect.x_range, view_range); } } pub fn undoLastMove(self: *System) void { while (self.undo_stack.popOrNull()) |frame| { frame.undo(self); self.last_applied_command = @min(self.last_applied_command, self.undo_stack.len); if (!frame.hasBreakpoint()) { break; } } } pub fn applyCommands(self: *System) void { while (self.last_applied_command < self.undo_stack.len) : (self.last_applied_command += 1) { const frame = self.undo_stack.buffer[self.last_applied_command]; frame.apply(self); } } pub fn toggleViewSettings(self: *System, view_id: Id) void { if (self.isViewSettingsOpen(view_id)) { self.view_settings = null; } else { self.view_settings = view_id; } } pub fn isViewSettingsOpen(self: *System, view_id: Id) bool { return self.view_settings != null and self.view_settings.?.eql(view_id); } pub fn toggleFullscreenView(self: *System, view_id: Id) void { if (self.view_fullscreen == null or !self.view_fullscreen.?.eql(view_id)) { self.view_fullscreen = view_id; } else { self.view_fullscreen = null; } } pub fn setCursor(self: *System, view_id: Id, axis: UI.Axis, position: ?f64) void { return ViewAxisPosition.set(&self.cursor, view_id, axis, position); } pub fn getCursor(self: *System, view_id: Id, axis: UI.Axis) ?f64 { return ViewAxisPosition.get(&self.cursor, self.project, view_id, axis); } pub fn setZoomStart(self: *System, view_id: Id, axis: UI.Axis, position: ?f64) void { return ViewAxisPosition.set(&self.zoom_start, view_id, axis, position); } pub fn getZoomStart(self: *System, view_id: Id, axis: UI.Axis) ?f64 { return ViewAxisPosition.get(&self.zoom_start, self.project,view_id, axis); }