From de2941c5bf4859a18a8a117b7c452252f24d4f36 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Sun, 16 Mar 2025 04:56:06 +0200 Subject: [PATCH] add undo history to channel movements --- src/screens/main_screen.zig | 410 ++++++++++++++++++++++-------------- src/ui.zig | 29 ++- 2 files changed, 284 insertions(+), 155 deletions(-) diff --git a/src/screens/main_screen.zig b/src/screens/main_screen.zig index c43046d..8399c42 100644 --- a/src/screens/main_screen.zig +++ b/src/screens/main_screen.zig @@ -17,17 +17,78 @@ const assert = std.debug.assert; const remap = utils.remap; const zoom_speed = 0.1; +const ruler_size = UI.Sizing.initFixed(.{ .pixels = 32 }); + +const ChannelCommand = struct { + channel: *ChannelView, + updated_at_ns: i128, + action: union(enum) { + move_and_zoom: struct { + before_x: RangeF64, + before_y: RangeF64, + } + } +}; app: *App, -fullscreen_channel: ?*App.ChannelView = null, +fullscreen_channel: ?*ChannelView = null, axis_zoom: ?struct { - channel: *App.ChannelView, + channel: *ChannelView, axis: UI.Axis, - start: f64 + start: f64, } = null, -fn showChannelViewGraph(self: *MainScreen, channel_view: *ChannelView) !void { +// TODO: Redo +channel_undo_stack: std.BoundedArray(ChannelCommand, 100) = .{}, + +fn pushChannelMoveCommand(self: *MainScreen, channel_view: *ChannelView, x_range: RangeF64, y_range: RangeF64) void { + const now_ns = std.time.nanoTimestamp(); + var undo_stack = &self.channel_undo_stack; + var push_new_command = true; + + if (undo_stack.len > 0) { + const top_command = &undo_stack.buffer[undo_stack.len - 1]; + if (now_ns - top_command.updated_at_ns < std.time.ns_per_ms * 250) { + top_command.updated_at_ns = now_ns; + push_new_command = false; + } + } + + var view_rect = &channel_view.view_rect; + + if (push_new_command) { + if (undo_stack.unusedCapacitySlice().len == 0) { + _ = undo_stack.orderedRemove(0); + } + + undo_stack.appendAssumeCapacity(ChannelCommand{ + .channel = channel_view, + .updated_at_ns = now_ns, + .action = .{ + .move_and_zoom = .{ + .before_x = view_rect.x_range, + .before_y = view_rect.y_range, + } + } + }); + } + + view_rect.x_range = x_range; + view_rect.y_range = y_range; +} + +fn pushChannelMoveCommandAxis(self: *MainScreen, channel_view: *ChannelView, axis: UI.Axis, view_range: RangeF64) void { + if (axis == .X) { + const view_rect = &channel_view.view_rect; + self.pushChannelMoveCommand(channel_view, view_range, view_rect.y_range); + } else { + const view_rect = &channel_view.view_rect; + self.pushChannelMoveCommand(channel_view, view_rect.x_range, view_range); + } +} + +fn showChannelViewGraph(self: *MainScreen, channel_view: *ChannelView) *UI.Box { var ui = &self.app.ui; const samples = self.app.getChannelSamples(channel_view); @@ -62,8 +123,11 @@ fn showChannelViewGraph(self: *MainScreen, channel_view: *ChannelView) !void { const x_offset = mouse_x_range.remapTo(RangeF64.init(0, view_rect.x_range.size()), signal.drag.x); const y_offset = mouse_y_range.remapTo(RangeF64.init(0, view_rect.y_range.size()), signal.drag.y); - view_rect.x_range = view_rect.x_range.sub(x_offset); - view_rect.y_range = view_rect.y_range.add(y_offset); + self.pushChannelMoveCommand( + channel_view, + view_rect.x_range.sub(x_offset), + view_rect.y_range.add(y_offset) + ); } if (signal.scrolled() and sample_index_under_mouse != null and sample_value_under_mouse != null) { @@ -74,14 +138,23 @@ fn showChannelViewGraph(self: *MainScreen, channel_view: *ChannelView) !void { scale_factor += zoom_speed; } - view_rect.x_range = view_rect.x_range.zoom(sample_index_under_mouse.?, scale_factor); - view_rect.y_range = view_rect.y_range.zoom(sample_value_under_mouse.?, scale_factor); + self.pushChannelMoveCommand( + channel_view, + view_rect.x_range.zoom(sample_index_under_mouse.?, scale_factor), + view_rect.y_range.zoom(sample_value_under_mouse.?, scale_factor) + ); + } + + if (signal.flags.contains(.middle_clicked)) { + self.pushChannelMoveCommand(channel_view, channel_view.x_range, channel_view.y_range); } Graph.drawCached(&channel_view.view_cache, graph_box.persistent.size, view_rect.*, samples); if (channel_view.view_cache.texture) |texture| { graph_box.texture = texture.texture; } + + return graph_box; } fn getLineOnRuler( @@ -271,6 +344,158 @@ fn showRulerTicks(self: *MainScreen, channel_view: *ChannelView, axis: UI.Axis) ); } +fn addRulerPlaceholder(self: *MainScreen, key: UI.Key, axis: UI.Axis) *UI.Box { + var ui = &self.app.ui; + + var ruler = ui.createBox(.{ + .key = key, + .background = srcery.hard_black, + .flags = &.{ .clip_view, .clickable, .scrollable }, + .hot_cursor = .mouse_cursor_pointing_hand + }); + if (axis == .X) { + ruler.size.x = UI.Sizing.initGrowFull(); + ruler.size.y = ruler_size; + } else { + ruler.size.x = ruler_size; + ruler.size.y = UI.Sizing.initGrowFull(); + } + + return ruler; +} + +fn showRuler(self: *MainScreen, ruler: *UI.Box, graph_box: *UI.Box, channel_view: *ChannelView, axis: UI.Axis) void { + var ui = &self.app.ui; + + ruler.beginChildren(); + defer ruler.endChildren(); + + self.showRulerTicks(channel_view, axis); + + const signal = ui.signal(ruler); + const mouse_position = switch (axis) { + .X => signal.relative_mouse.x, + .Y => signal.relative_mouse.y + }; + const mouse_range = switch (axis) { + .X => RangeF64.init(0, ruler.persistent.size.x), + .Y => RangeF64.init(0, ruler.persistent.size.y) + }; + const view_range = channel_view.getViewRange(axis); + const mouse_position_on_graph = mouse_range.remapTo(view_range.*, mouse_position); + + var zoom_start: ?f64 = null; + var zoom_end: ?f64 = null; + + var is_zooming: bool = false; + if (self.axis_zoom) |axis_zoom| { + is_zooming = axis_zoom.channel == channel_view and axis_zoom.axis == axis; + } + + if (signal.hot) { + const mouse_tooltip = ui.mouseTooltip(); + mouse_tooltip.beginChildren(); + defer mouse_tooltip.endChildren(); + + if (channel_view.getSampleRange(axis).hasInclusive(mouse_position_on_graph)) { + _ = ui.label("{d:.3}", .{mouse_position_on_graph}); + } + + zoom_start = mouse_position_on_graph; + } + + if (signal.flags.contains(.left_pressed)) { + self.axis_zoom = .{ + .axis = axis, + .start = mouse_position_on_graph, + .channel = channel_view + }; + } + + if (is_zooming) { + zoom_start = self.axis_zoom.?.start; + zoom_end = mouse_position_on_graph; + } + + if (zoom_start != null) { + _ = ui.createBox(.{ + .background = srcery.green, + .float_rect = getLineOnRuler(channel_view, ruler, axis, zoom_start.?, 0, 1), + .float_relative_to = ruler, + }); + + _ = ui.createBox(.{ + .background = srcery.green, + .float_rect = getLineOnRuler(channel_view, graph_box, axis, zoom_start.?, 0, 1), + .float_relative_to = graph_box, + .parent = graph_box + }); + } + + if (zoom_end != null) { + _ = ui.createBox(.{ + .background = srcery.green, + .float_rect = getLineOnRuler(channel_view, ruler, axis, zoom_end.?, 0, 1), + .float_relative_to = ruler, + }); + + _ = ui.createBox(.{ + .background = srcery.green, + .float_rect = getLineOnRuler(channel_view, graph_box, axis, zoom_end.?, 0, 1), + .float_relative_to = graph_box, + .parent = graph_box + }); + } + + if (zoom_start != null and zoom_end != null) { + _ = ui.createBox(.{ + .background = srcery.green.alpha(0.5), + .float_relative_to = ruler, + .float_rect = getRectOnRuler( + channel_view, + ruler, + axis, + zoom_start.?, + zoom_end.? - zoom_start.?, + 0, + 1 + ) + }); + } + + if (signal.scrolled()) { + var scale_factor: f64 = 1; + if (signal.scroll.y > 0) { + scale_factor -= zoom_speed; + } else { + scale_factor += zoom_speed; + } + const new_view_range = view_range.zoom(mouse_position_on_graph, scale_factor); + self.pushChannelMoveCommandAxis(channel_view, axis, new_view_range); + } + + if (is_zooming and signal.flags.contains(.left_released)) { + if (zoom_start != null and zoom_end != null) { + const zoom_start_mouse = view_range.remapTo(mouse_range, zoom_start.?); + const zoom_end_mouse = view_range.remapTo(mouse_range, zoom_end.?); + const mouse_move_distance = @abs(zoom_end_mouse - zoom_start_mouse); + if (mouse_move_distance > 5) { + var new_view_range = RangeF64.init( + @min(zoom_start.?, zoom_end.?), + @max(zoom_start.?, zoom_end.?) + ); + + if (axis == .Y) { + new_view_range = new_view_range.flip(); + } + + self.pushChannelMoveCommandAxis(channel_view, axis, new_view_range); + } + } + self.axis_zoom = null; + } +} + fn showChannelView(self: *MainScreen, channel_view: *ChannelView, height: UI.Sizing) !void { var ui = &self.app.ui; @@ -286,14 +511,12 @@ fn showChannelView(self: *MainScreen, channel_view: *ChannelView, height: UI.Siz const show_ruler = true; if (!show_ruler) { - try self.showChannelViewGraph(channel_view); + _ = self.showChannelViewGraph(channel_view); } else { - const x_ruler_size = UI.Sizing.initFixed(.{ .pixels = 32 }); - const y_ruler_size = UI.Sizing.initFixed(.{ .pixels = 32 }); - - var y_ruler: *UI.Box = undefined; + var graph_box: *UI.Box = undefined; var x_ruler: *UI.Box = undefined; + var y_ruler: *UI.Box = undefined; { const container = ui.createBox(.{ @@ -304,31 +527,24 @@ fn showChannelView(self: *MainScreen, channel_view: *ChannelView, height: UI.Siz container.beginChildren(); defer container.endChildren(); - y_ruler = ui.createBox(.{ - .key = ui.keyFromString("Y ruler"), - .size_x = y_ruler_size, - .size_y = UI.Sizing.initGrowFull(), - .background = srcery.hard_black, - .flags = &.{ .clickable, .clip_view, .scrollable }, - .hot_cursor = .mouse_cursor_pointing_hand - }); + y_ruler = self.addRulerPlaceholder(ui.keyFromString("Y ruler"), .Y); - try self.showChannelViewGraph(channel_view); + graph_box = self.showChannelViewGraph(channel_view); } { const container = ui.createBox(.{ .layout_direction = .left_to_right, .size_x = UI.Sizing.initGrowFull(), - .size_y = x_ruler_size, + .size_y = ruler_size, }); container.beginChildren(); defer container.endChildren(); const fullscreen = ui.createBox(.{ .key = ui.keyFromString("Fullscreen toggle"), - .size_y = x_ruler_size, - .size_x = y_ruler_size, + .size_x = ruler_size, + .size_y = ruler_size, .background = srcery.hard_black, .hot_cursor = .mouse_cursor_pointing_hand, .flags = &.{ .draw_hot, .draw_active, .clickable }, @@ -343,141 +559,19 @@ fn showChannelView(self: *MainScreen, channel_view: *ChannelView, height: UI.Siz } } - x_ruler = ui.createBox(.{ - .key = ui.keyFromString("X ruler"), - .size_y = x_ruler_size, - .size_x = UI.Sizing.initGrowFull(), - .background = srcery.hard_black, - .flags = &.{ .clip_view, .clickable, .scrollable }, - .hot_cursor = .mouse_cursor_pointing_hand - }); + x_ruler = self.addRulerPlaceholder(ui.keyFromString("X ruler"), .X); } - const ruler_desciptions = .{ - .{ x_ruler, .X }, - .{ y_ruler, .Y } - }; + self.showRuler(x_ruler, graph_box, channel_view, .X); + self.showRuler(y_ruler, graph_box, channel_view, .Y); - inline for (ruler_desciptions) |ruler_desc| { - const ruler = ruler_desc[0]; - const axis: UI.Axis = ruler_desc[1]; - - ruler.beginChildren(); - defer ruler.endChildren(); - - self.showRulerTicks(channel_view, axis); - - const signal = ui.signal(ruler); - const mouse_position = switch (axis) { - .X => signal.relative_mouse.x, - .Y => signal.relative_mouse.y - }; - const mouse_range = switch (axis) { - .X => RangeF64.init(0, ruler.persistent.size.x), - .Y => RangeF64.init(0, ruler.persistent.size.y) - }; - const view_range = channel_view.getViewRange(axis); - const mouse_position_on_graph = mouse_range.remapTo(view_range.*, mouse_position); - - var zoom_start: ?f64 = null; - var zoom_end: ?f64 = null; - - var is_zooming: bool = false; - if (self.axis_zoom) |axis_zoom| { - is_zooming = axis_zoom.channel == channel_view and axis_zoom.axis == axis; - } - - if (signal.hot) { - const mouse_tooltip = ui.mouseTooltip(); - mouse_tooltip.beginChildren(); - defer mouse_tooltip.endChildren(); - - if (channel_view.getSampleRange(axis).hasInclusive(mouse_position_on_graph)) { - _ = ui.label("{d:.3}", .{mouse_position_on_graph}); - } - - zoom_start = mouse_position_on_graph; - } - - if (signal.flags.contains(.left_pressed)) { - self.axis_zoom = .{ - .axis = axis, - .start = mouse_position_on_graph, - .channel = channel_view - }; - } - - if (is_zooming) { - zoom_start = self.axis_zoom.?.start; - zoom_end = mouse_position_on_graph; - } - - if (zoom_start != null) { - _ = ui.createBox(.{ - .background = srcery.green, - .float_rect = getLineOnRuler(channel_view, ruler, axis, zoom_start.?, 0, 1), - .float_relative_to = ruler, - }); - } - - if (zoom_end != null) { - _ = ui.createBox(.{ - .background = srcery.green, - .float_rect = getLineOnRuler(channel_view, ruler, axis, zoom_end.?, 0, 1), - .float_relative_to = ruler, - }); - } - - if (zoom_start != null and zoom_end != null) { - _ = ui.createBox(.{ - .background = srcery.green.alpha(0.5), - .float_relative_to = ruler, - .float_rect = getRectOnRuler( - channel_view, - ruler, - axis, - zoom_start.?, - zoom_end.? - zoom_start.?, - 0, - 1 - ) - }); - } - - if (signal.scrolled()) { - var scale_factor: f64 = 1; - if (signal.scroll.y > 0) { - scale_factor -= zoom_speed; - } else { - scale_factor += zoom_speed; - } - view_range.* = view_range.zoom(mouse_position_on_graph, scale_factor); - } - - if (is_zooming and signal.flags.contains(.left_released)) { - if (zoom_start != null and zoom_end != null) { - const zoom_start_mouse = view_range.remapTo(mouse_range, zoom_start.?); - const zoom_end_mouse = view_range.remapTo(mouse_range, zoom_end.?); - const mouse_move_distance = @abs(zoom_end_mouse - zoom_start_mouse); - if (mouse_move_distance > 5) { - view_range.lower = @min(zoom_start.?, zoom_end.?); - view_range.upper = @max(zoom_start.?, zoom_end.?); - - if (axis == .Y) { - view_range.* = view_range.flip(); - } - } - } - self.axis_zoom = null; - } - } } } pub fn tick(self: *MainScreen) !void { var ui = &self.app.ui; - if (rl.isKeyPressed(.key_escape)) { + if (ui.isKeyboardPressed(.key_escape)) { if (self.fullscreen_channel != null) { self.fullscreen_channel = null; } else { @@ -485,6 +579,18 @@ pub fn tick(self: *MainScreen) !void { } } + if (ui.isCtrlDown() and ui.isKeyboardPressed(.key_z)) { + if (self.channel_undo_stack.popOrNull()) |command| { + switch (command.action) { + .move_and_zoom => |args| { + const view_rect = &command.channel.view_rect; + view_rect.x_range = args.before_x; + view_rect.y_range = args.before_y; + } + } + } + } + const root = ui.parentBox().?; root.layout_direction = .top_to_bottom; @@ -538,8 +644,6 @@ pub fn tick(self: *MainScreen) !void { } } - - if (self.fullscreen_channel) |channel| { try self.showChannelView(channel, UI.Sizing.initGrowFull()); diff --git a/src/ui.zig b/src/ui.zig index 0ba4b12..d885ec0 100644 --- a/src/ui.zig +++ b/src/ui.zig @@ -636,7 +636,8 @@ pub const BoxOptions = struct { float_rect: ?Rect = null, scientific_number: ?f64 = null, scientific_precision: ?u32 = null, - float_relative_to: ?*Box = null + float_relative_to: ?*Box = null, + parent: ?*UI.Box = null }; pub const root_box_key = Key.initString(0, "$root$"); @@ -1430,7 +1431,7 @@ pub fn createBox(self: *UI, opts: BoxOptions) *Box { box.setFloatRect(rect); } - if (self.parentBox()) |parent| { + if (opts.parent orelse self.parentBox()) |parent| { box.tree.parent_index = parent.tree.index; if (parent.tree.last_child_index) |last_child_index| { @@ -1842,6 +1843,30 @@ pub fn isKeyActive(self: *UI, key: Key) bool { return false; } +pub fn isKeyboardPressed(self: *UI, key: rl.KeyboardKey) bool { + const key_u32: u32 = @intCast(@intFromEnum(key)); + + for (0.., self.events.slice()) |i, _event| { + const event: Event = _event; + if (event == .key_pressed and event.key_pressed == key_u32) { + _ = self.events.swapRemove(i); + return true; + } + } + return false; +} + +pub fn isShiftDown(self: *UI) bool { + _ = self; + return rl.isKeyDown(rl.KeyboardKey.key_left_shift) or rl.isKeyDown(rl.KeyboardKey.key_right_shift); +} + + +pub fn isCtrlDown(self: *UI) bool { + _ = self; + return rl.isKeyDown(rl.KeyboardKey.key_left_control) or rl.isKeyDown(rl.KeyboardKey.key_right_control); +} + // --------------------------------- Widgets ----------------------------------------- // pub fn mouseTooltip(self: *UI) *Box {