diff --git a/src/app.zig b/src/app.zig index 02373f6..81998a7 100644 --- a/src/app.zig +++ b/src/app.zig @@ -9,6 +9,7 @@ const UI = @import("./ui.zig"); const MainScreen = @import("./screens/main_screen.zig"); const ChannelFromDeviceScreen = @import("./screens/channel_from_device.zig"); const Platform = @import("./platform.zig"); +const constants = @import("./constants.zig"); const Allocator = std.mem.Allocator; const log = std.log.scoped(.app); diff --git a/src/components/systems/view_controls.zig b/src/components/systems/view_controls.zig new file mode 100644 index 0000000..c4d38b2 --- /dev/null +++ b/src/components/systems/view_controls.zig @@ -0,0 +1,275 @@ +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, view_id: Id, axis: UI.Axis) ?f64 { + const self = optional_self.* orelse return null; + + if (self.axis != axis) return null; + + if (!constants.sync_view_controls) { + if (!self.view_id.eql(view_id)) { + return null; + } + } + + return self.position; + } +}; + +pub const Command = struct { + view_id: Id, + action: union(enum) { + move_and_zoom: struct { + before_x: RangeF64, + before_y: RangeF64, + x: RangeF64, + y: RangeF64 + } + }, + + fn apply(self: *const Command, system: *System) void { + const project = system.project; + const view = project.views.get(self.view_id) orelse return; + + switch (self.action) { + .move_and_zoom => |move_and_zoom| { + 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; + const view = project.views.get(self.view_id) orelse return; + + switch (self.action) { + .move_and_zoom => |move_and_zoom| { + 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 { + for (self.commands.slice()) |*command| { + if (command.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); + } + } +}; + +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 + +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(); + 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 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 view_ids: std.BoundedArray(Id, constants.max_views) = .{}; + if (constants.sync_view_controls) { + var iter = self.project.views.idIterator(); + while (iter.next()) |id| { + 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.action.move_and_zoom.x = x_range; + command.action.move_and_zoom.y = y_range; + } else { + const view = self.project.views.get(id) orelse return; + const view_rect = &view.graph_opts; + + command = frame.commands.addOneAssumeCapacity(); + + command.* = Command{ + .view_id = id, + .action = .{ + .move_and_zoom = .{ + .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 { + const frame = self.undo_stack.popOrNull() orelse return; + + frame.undo(self); + + self.last_applied_command = @min(self.last_applied_command, self.undo_stack.len); +} + +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, 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, view_id, axis); +} \ No newline at end of file diff --git a/src/components/view.zig b/src/components/view.zig new file mode 100644 index 0000000..dea8c02 --- /dev/null +++ b/src/components/view.zig @@ -0,0 +1,299 @@ +const std = @import("std"); +const App = @import("../app.zig"); +const UI = @import("../ui.zig"); +const srcery = @import("../srcery.zig"); +const RangeF64 = @import("../range.zig").RangeF64; +const utils = @import("../utils.zig"); +const NIDaq = @import("../ni-daq/root.zig"); +const Assets = @import("../assets.zig"); +const rl = @import("raylib"); +const constants = @import("../constants.zig"); +const Graph = @import("../graph.zig"); +const UIViewRuler = @import("./view_ruler.zig"); + +const ViewControlsSystem = @import("./systems/view_controls.zig"); + +const Id = App.Id; +const remap = utils.remap; +const assert = std.debug.assert; + +const ruler_size = UI.Sizing.initFixed(.{ .pixels = 32 }); + +pub const ZoomStart = UIViewRuler.ZoomStart; + +pub const Context = struct { + ui: *UI, + app: *App, + + view_controls: *ViewControlsSystem, +}; + +pub const Result = struct { + box: *UI.Box, +}; + +fn showGraph(ctx: Context, view_id: Id) *UI.Box { + var ui = ctx.ui; + const app = ctx.app; + + const view = app.getView(view_id).?; + const samples = app.getViewSamples(view_id); + const view_opts = &view.graph_opts; + + const graph_box = ui.createBox(.{ + .key = ui.keyFromString("Graph"), + .size_x = UI.Sizing.initGrowFull(), + .size_y = UI.Sizing.initGrowFull(), + .background = srcery.black, + .flags = &.{ .clickable, .draggable, .scrollable, .clip_view }, + .align_x = .center, + .align_y = .center, + }); + graph_box.beginChildren(); + defer graph_box.endChildren(); + + const graph_rect = graph_box.rect(); + + const signal = ui.signal(graph_box); + + var sample_value_under_mouse: ?f64 = null; + var sample_index_under_mouse: ?f64 = null; + + const mouse_x_range = RangeF64.init(0, graph_rect.width); + const mouse_y_range = RangeF64.init(0, graph_rect.height); + + if (signal.hot) { + sample_index_under_mouse = mouse_x_range.remapTo(view_opts.x_range, signal.relative_mouse.x); + sample_value_under_mouse = mouse_y_range.remapTo(view_opts.y_range, signal.relative_mouse.y); + } + + if (signal.dragged()) { + const x_offset = mouse_x_range.remapTo(RangeF64.init(0, view_opts.x_range.size()), signal.drag.x); + const y_offset = mouse_y_range.remapTo(RangeF64.init(0, view_opts.y_range.size()), signal.drag.y); + + ctx.view_controls.pushViewMove( + view_id, + view_opts.x_range.sub(x_offset), + view_opts.y_range.add(y_offset) + ); + } + + if (signal.scrolled() and sample_index_under_mouse != null and sample_value_under_mouse != null) { + var scale_factor: f64 = 1; + if (signal.scroll.y > 0) { + scale_factor -= constants.zoom_speed; + } else { + scale_factor += constants.zoom_speed; + } + + ctx.view_controls.pushViewMove( + view_id, + view_opts.x_range.zoom(sample_index_under_mouse.?, scale_factor), + view_opts.y_range.zoom(sample_value_under_mouse.?, scale_factor) + ); + } + + if (signal.flags.contains(.middle_clicked)) { + ctx.view_controls.pushViewMove(view_id, view.available_x_range, view.available_y_range); + } + + Graph.drawCached(&view.graph_cache, graph_box.persistent.size, view_opts.*, samples); + if (view.graph_cache.texture) |texture| { + graph_box.texture = texture.texture; + } + + if (view_opts.x_range.size() == 0 or view_opts.y_range.size() == 0) { + graph_box.setText(""); + graph_box.text_color = srcery.hard_black; + graph_box.font = .{ + .variant = .bold_italic, + .size = ui.rem(3) + }; + } + + return graph_box; +} + +pub fn show(ctx: Context, view_id: Id, height: UI.Sizing) !Result { + var ui = ctx.ui; + + const view_box = ui.createBox(.{ + .key = UI.Key.initUsize(view_id.asInt()), + .layout_direction = .top_to_bottom, + .size_x = UI.Sizing.initGrowFull(), + .size_y = height, + }); + view_box.beginChildren(); + defer view_box.endChildren(); + + const result = Result{ + .box = view_box + }; + + const toolbar = ui.createBox(.{ + .layout_direction = .left_to_right, + .background = srcery.hard_black, + .size_x = UI.Sizing.initGrowFull(), + .size_y = UI.Sizing.initFixed(.{ .pixels = ui.rem(2) }) + }); + { + toolbar.beginChildren(); + defer toolbar.endChildren(); + + const view = ctx.app.getView(view_id).?; + var view_name: ?[]const u8 = null; + + { + const btn = ui.textButton("Settings"); + btn.background = srcery.hard_black; + if (ctx.view_controls.isViewSettingsOpen(view_id)) { + btn.borders.bottom = .{ + .color = srcery.green, + .size = 4 + }; + } + + if (ui.signal(btn).clicked()) { + ctx.view_controls.toggleViewSettings(view_id); + } + } + + { + const btn = ui.textButton("Reset view"); + btn.background = srcery.hard_black; + if (ui.signal(btn).clicked()) { + ctx.view_controls.pushViewMove(view_id, view.available_x_range, view.available_y_range); + } + } + + if (view.reference == .channel) { + const channel_id = view.reference.channel; + const channel = ctx.app.getChannel(channel_id).?; + const channel_name = utils.getBoundedStringZ(&channel.name); + const channel_type = NIDaq.getChannelType(channel_name).?; + + { + const follow = ui.textButton("Follow"); + follow.background = srcery.hard_black; + if (view.follow) { + follow.borders = UI.Borders.bottom(.{ + .color = srcery.green, + .size = 4 + }); + } + if (ui.signal(follow).clicked()) { + view.follow = !view.follow; + } + } + + if (channel_type == .analog_output) { + const button = ui.button(ui.keyFromString("Output generation")); + button.texture = Assets.output_generation; + button.size.y = UI.Sizing.initGrowFull(); + + const signal = ui.signal(button); + if (signal.clicked()) { + if (ctx.app.isChannelOutputing(channel_id)) { + ctx.app.pushCommand(.{ + .stop_output = channel_id + }); + } else { + ctx.view_controls.view_protocol_modal = view_id; + } + } + + var color = rl.Color.white; + if (ctx.app.isChannelOutputing(channel_id)) { + color = srcery.red; + } + + if (signal.active) { + button.texture_color = color.alpha(0.6); + } else if (signal.hot) { + button.texture_color = color.alpha(0.8); + } else { + button.texture_color = color; + } + } + + view_name = channel_name; + } else if (view.reference == .file) { + const file_id = view.reference.file; + const file = ctx.app.getFile(file_id).?; + + view_name = std.fs.path.stem(file.path); + } + + if (view_name) |text| { + _ = ui.createBox(.{ + .size_x = UI.Sizing.initGrowFull() + }); + + const label = ui.label("{s}", .{text}); + label.size.y = UI.Sizing.initGrowFull(); + label.alignment.x = .center; + label.alignment.y = .center; + label.padding = UI.Padding.horizontal(ui.rem(1)); + } + } + + if (!constants.show_ruler) { + _ = showGraph(ctx, view_id); + + } else { + const ruler_ctx = UIViewRuler.Context{ + .ui = ctx.ui, + .project = &ctx.app.project, + .view_controls = ctx.view_controls + }; + + var graph_box: *UI.Box = undefined; + var x_ruler: *UI.Box = undefined; + var y_ruler: *UI.Box = undefined; + + { + const container = ui.createBox(.{ + .layout_direction = .left_to_right, + .size_x = UI.Sizing.initGrowFull(), + .size_y = UI.Sizing.initGrowFull(), + }); + container.beginChildren(); + defer container.endChildren(); + + y_ruler = UIViewRuler.createBox(ruler_ctx, ui.keyFromString("Y ruler"), .Y); + + graph_box = showGraph(ctx, view_id); + } + + { + const container = ui.createBox(.{ + .layout_direction = .left_to_right, + .size_x = UI.Sizing.initGrowFull(), + .size_y = ruler_size, + }); + container.beginChildren(); + defer container.endChildren(); + + const fullscreen = ui.createBox(.{ + .key = ui.keyFromString("Fullscreen toggle"), + .size_x = ruler_size, + .size_y = ruler_size, + .background = srcery.hard_black, + .hot_cursor = .mouse_cursor_pointing_hand, + .flags = &.{ .draw_hot, .draw_active, .clickable }, + .texture = Assets.fullscreen, + .texture_size = .{ .x = 28, .y = 28 } + }); + if (ui.signal(fullscreen).clicked()) { + ctx.view_controls.toggleFullscreenView(view_id); + } + + x_ruler = UIViewRuler.createBox(ruler_ctx, ui.keyFromString("X ruler"), .X); + } + + try UIViewRuler.show(ruler_ctx, x_ruler, graph_box, view_id, .X); + try UIViewRuler.show(ruler_ctx, y_ruler, graph_box, view_id, .Y); + } + + return result; +} \ No newline at end of file diff --git a/src/components/view_ruler.zig b/src/components/view_ruler.zig new file mode 100644 index 0000000..ad3f9a5 --- /dev/null +++ b/src/components/view_ruler.zig @@ -0,0 +1,335 @@ +const std = @import("std"); +const rl = @import("raylib"); +const App = @import("../app.zig"); +const UI = @import("../ui.zig"); +const RangeF64 = @import("../range.zig").RangeF64; +const srcery = @import("../srcery.zig"); +const utils = @import("../utils.zig"); +const constants = @import("../constants.zig"); + +const ViewControlsSystem = @import("./systems/view_controls.zig"); + +const Id = App.Id; +const remap = utils.remap; +const assert = std.debug.assert; + +const ruler_size = UI.Sizing.initFixed(.{ .pixels = 32 }); + +pub const Context = struct { + ui: *UI, + project: *App.Project, + view_controls: *ViewControlsSystem +}; + +pub fn createBox(ctx: Context, key: UI.Key, axis: UI.Axis) *UI.Box { + var ui = ctx.ui; + + var ruler = ui.createBox(.{ + .key = key, + .background = srcery.hard_black, + .flags = &.{ .clickable, .scrollable, .clip_view }, + .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; +} + +const DrawContext = struct { + one_unit: ?f64, + render_range: RangeF64, + available_range: RangeF64, + axis: UI.Axis, + rect: rl.Rectangle = .{ .x = 0, .y = 0, .width = 0, .height = 0 }, + + fn init(axis: UI.Axis, project: *App.Project, view_id: Id) DrawContext { + const view = project.views.get(view_id).?; + + return DrawContext{ + .one_unit = switch (axis) { + .X => project.getSampleRate(), + .Y => 1 + }, + .render_range = view.getGraphView(axis).*, + .available_range = view.getAvailableView(axis), + .axis = axis, + }; + } + + fn getPoint(self: *DrawContext, along_axis_pos: f64, cross_axis_pos: f64) rl.Vector2 { + const rect_width: f64 = @floatCast(self.rect.width); + const rect_height: f64 = @floatCast(self.rect.height); + + var x: f64 = undefined; + var y: f64 = undefined; + + if (self.axis == .X) { + x = remap(f64, self.render_range.lower, self.render_range.upper, 0, rect_width, along_axis_pos); + y = cross_axis_pos * rect_height; + } else { + assert(self.axis == .Y); + + x = (1 - cross_axis_pos) * rect_width; + y = remap(f64, self.render_range.lower, self.render_range.upper, 0, rect_height, along_axis_pos); + } + + return rl.Vector2{ + .x = self.rect.x + @as(f32, @floatCast(x)), + .y = self.rect.y + @as(f32, @floatCast(y)), + }; + } + + fn getLine(self: *DrawContext, along_axis_pos: f64, cross_axis_size: f64) rl.Rectangle { + const along_axis_size = self.render_range.size() / switch(self.axis) { + .X => @as(f64, @floatCast(self.rect.width)), + .Y => @as(f64, @floatCast(self.rect.height)) + }; + + return self.getRect( + along_axis_pos, + along_axis_size, + 0, + cross_axis_size + ); + } + + fn getRect(self: *DrawContext, along_axis_pos: f64, along_axis_size: f64, cross_axis_pos: f64, cross_axis_size: f64) rl.Rectangle { + const pos = self.getPoint(along_axis_pos, cross_axis_pos); + const corner = self.getPoint(along_axis_pos + along_axis_size, cross_axis_pos + cross_axis_size); + var rect = rl.Rectangle{ + .x = pos.x, + .y = pos.y, + .width = corner.x - pos.x, + .height = corner.y - pos.y + }; + + if (rect.width < 0) { + rect.x += rect.width; + rect.width *= -1; + } + + if (rect.height < 0) { + rect.y += rect.height; + rect.height *= -1; + } + + return rect; + } + + fn drawLine(self: *DrawContext, along_axis_pos: f64, cross_axis_size: f64, color: rl.Color) void { + rl.drawLineV( + self.getPoint(along_axis_pos, 0), + self.getPoint(along_axis_pos, cross_axis_size), + color + ); + } + + fn drawTicks(self: *DrawContext, from: f64, to: f64, step: f64, line_size: f64, color: rl.Color) void { + var position = from; + while (position < to) : (position += step) { + self.drawLine(position, line_size, color); + } + } +}; + +fn drawRulerTicks(_ctx: ?*anyopaque, box: *UI.Box) void { + const ctx: *DrawContext = @ptrCast(@alignCast(_ctx)); + ctx.rect = box.rect(); + + ctx.drawLine(ctx.available_range.lower, 1, srcery.yellow); + ctx.drawLine(ctx.available_range.upper, 1, srcery.yellow); + + if (ctx.available_range.hasExclusive(0)) { + ctx.drawLine(0, 0.75, srcery.yellow); + } + + var one_unit = ctx.one_unit orelse return; + + while (ctx.render_range.size() < 5*one_unit) { + one_unit /= 2; + } + + while (ctx.render_range.size() > 50*one_unit) { + one_unit *= 2; + } + + var ticks_range = ctx.render_range.grow(one_unit).intersectPositive(ctx.available_range); + ticks_range.lower = utils.roundNearestTowardZero(f64, ticks_range.lower, one_unit); + + ctx.drawTicks(ticks_range.lower, ticks_range.upper, one_unit, 0.5, srcery.yellow); + ctx.drawTicks(ticks_range.lower + one_unit/2, ticks_range.upper, one_unit, 0.25, srcery.yellow); +} + +fn showMouseTooltip(ctx: Context, axis: UI.Axis, view_id: Id, position: f64) void { + var ui = ctx.ui; + + const view = ctx.project.views.get(view_id) orelse return; + + const mouse_tooltip = ui.mouseTooltip(); + mouse_tooltip.beginChildren(); + defer mouse_tooltip.endChildren(); + + if (view.getAvailableView(axis).hasInclusive(position)) { + const sample_rate = ctx.project.getSampleRate(); + + if (axis == .Y and view.unit != null) { + const unit_name = view.unit.?.name() orelse "Unknown"; + _ = ui.label("{s}: {d:.3}", .{unit_name, position}); + } else if (axis == .X and sample_rate != null) { + const seconds = position / sample_rate.?; + const frame_allocator = ui.frameAllocator(); + _ = ui.label("{s}", .{ utils.formatDuration(frame_allocator, seconds) catch "-" }); + + } else { + _ = ui.label("{d:.3}", .{position}); + } + } +} + +pub fn show(ctx: Context, box: *UI.Box, graph_box: *UI.Box, view_id: Id, axis: UI.Axis) !void { + var ui = ctx.ui; + const project = ctx.project; + + const view = project.views.get(view_id) orelse return; + + var ruler_ctx = DrawContext.init(axis, project, view_id); + var graph_ctx = ruler_ctx; + ruler_ctx.rect = .{ + .x = 0, + .y = 0, + .width = box.persistent.size.x, + .height = box.persistent.size.y + }; + graph_ctx.rect = .{ + .x = 0, + .y = 0, + .width = graph_box.persistent.size.x, + .height = graph_box.persistent.size.y + }; + + box.beginChildren(); + defer box.endChildren(); + + { + const ruler_draw_ctx = try ui.frameAllocator().create(DrawContext); + ruler_draw_ctx.* = DrawContext.init(axis, project, view_id); + + box.draw = .{ + .ctx = ruler_draw_ctx, + .do = drawRulerTicks + }; + } + + { // Visuals + const cursor = ctx.view_controls.getCursor(view_id, axis); + + var zoom_start: ?f64 = null; + if (ctx.view_controls.getZoomStart(view_id, axis)) |zoom_start_position| { + zoom_start = zoom_start_position; + } + + var markers: std.BoundedArray(f64, 2) = .{}; + + if (zoom_start != null) { + markers.appendAssumeCapacity(zoom_start.?); + } + + if (cursor != null) { + markers.appendAssumeCapacity(cursor.?); + } + + for (markers.constSlice()) |marker_position| { + _ = ui.createBox(.{ + .background = srcery.green, + .float_rect = ruler_ctx.getLine(marker_position, 1), + .float_relative_to = box, + }); + + _ = ui.createBox(.{ + .background = srcery.green, + .float_rect = graph_ctx.getLine(marker_position, 1), + .float_relative_to = graph_box, + .parent = graph_box + }); + } + + if (zoom_start != null and cursor != null) { + const zoom_end = cursor.?; + + _ = ui.createBox(.{ + .background = srcery.green.alpha(0.5), + .float_relative_to = box, + .float_rect = ruler_ctx.getRect(zoom_start.?, zoom_end - zoom_start.?, 0, 1), + }); + } + } + + const signal = ui.signal(box); + const mouse_position = switch (axis) { + .X => signal.relative_mouse.x, + .Y => signal.relative_mouse.y + }; + const mouse_range = switch (axis) { + .X => RangeF64.init(0, box.persistent.size.x), + .Y => RangeF64.init(0, box.persistent.size.y) + }; + const view_range = view.getGraphView(axis); + + if (signal.hot and view_range.size() > 0) { + const mouse_position_on_graph = mouse_range.remapTo(view_range.*, mouse_position); + + showMouseTooltip(ctx, axis, view_id, mouse_position_on_graph); + + ctx.view_controls.setCursor(view_id, axis, mouse_position_on_graph); + } else { + ctx.view_controls.setCursor(view_id, axis, null); + } + + const cursor = ctx.view_controls.getCursor(view_id, axis); + + if (signal.flags.contains(.left_pressed)) { + ctx.view_controls.setZoomStart(view_id, axis, cursor); + } + + if (signal.scrolled() and cursor != null) { + var scale_factor: f64 = 1; + if (signal.scroll.y > 0) { + scale_factor -= constants.zoom_speed; + } else { + scale_factor += constants.zoom_speed; + } + const new_view_range = view_range.zoom(cursor.?, scale_factor); + ctx.view_controls.pushViewMoveAxis(view_id, axis, new_view_range); + } + + if (ctx.view_controls.getZoomStart(view_id, axis)) |zoom_start| { + if (signal.flags.contains(.left_released)) { + const zoom_end = cursor; + + if (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(); + } + + ctx.view_controls.pushViewMoveAxis(view_id, axis, new_view_range); + } + } + ctx.view_controls.zoom_start = null; + } + } +} \ No newline at end of file diff --git a/src/constants.zig b/src/constants.zig index 79787cd..16d6035 100644 --- a/src/constants.zig +++ b/src/constants.zig @@ -1,7 +1,10 @@ pub const max_files = 32; - pub const max_channels = 32; +pub const max_views = 64; -pub const max_views = 64; \ No newline at end of file +// UI +pub const show_ruler = true; +pub const sync_view_controls = true; +pub const zoom_speed = 0.1; \ No newline at end of file diff --git a/src/screens/main_screen.zig b/src/screens/main_screen.zig index 3b189ef..4391c50 100644 --- a/src/screens/main_screen.zig +++ b/src/screens/main_screen.zig @@ -9,6 +9,9 @@ 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(); @@ -19,6 +22,8 @@ const Id = App.Id; const zoom_speed = 0.1; const ruler_size = UI.Sizing.initFixed(.{ .pixels = 32 }); +const show_ruler = true; +const sync_view_controls = true; const ViewCommand = struct { view_id: Id, @@ -32,27 +37,19 @@ const ViewCommand = struct { }; app: *App, -fullscreen_view: ?Id = null, +view_controls: ViewControlsSystem, -axis_zoom: ?struct { - view_id: Id, - axis: UI.Axis, - start: f64, -} = null, - -// TODO: Redo -view_undo_stack: std.BoundedArray(ViewCommand, 100) = .{}, - -protocol_modal: ?Id = null, +// Protocol modal frequency_input: UI.TextInputStorage, amplitude_input: UI.TextInputStorage, -sample_rate_input: UI.TextInputStorage, -parsed_sample_rate: ?f64 = null, protocol_error_message: ?[]const u8 = null, protocol_graph_cache: Graph.Cache = .{}, preview_samples: std.ArrayListUnmanaged(f64) = .{}, preview_samples_y_range: RangeF64 = RangeF64.init(0, 0), -view_settings: ?Id = null, + +// Project settings +sample_rate_input: UI.TextInputStorage, +parsed_sample_rate: ?f64 = null, pub fn init(app: *App) !MainScreen { const allocator = app.allocator; @@ -61,7 +58,8 @@ pub fn init(app: *App) !MainScreen { .app = app, .frequency_input = UI.TextInputStorage.init(allocator), .amplitude_input = UI.TextInputStorage.init(allocator), - .sample_rate_input = UI.TextInputStorage.init(allocator) + .sample_rate_input = UI.TextInputStorage.init(allocator), + .view_controls = ViewControlsSystem.init(&app.project) }; try self.frequency_input.setText("10"); @@ -81,657 +79,13 @@ pub fn deinit(self: *MainScreen) void { self.clearProtocolErrorMessage(); } -fn pushViewMoveCommand(self: *MainScreen, view_id: Id, x_range: RangeF64, y_range: RangeF64) void { - const view = self.app.getView(view_id) orelse return; - const view_rect = &view.graph_opts; - - const now_ns = std.time.nanoTimestamp(); - var undo_stack = &self.view_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; - } - } - - if (push_new_command) { - if (undo_stack.unusedCapacitySlice().len == 0) { - _ = undo_stack.orderedRemove(0); - } - - undo_stack.appendAssumeCapacity(ViewCommand{ - .view_id = view_id, - .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; - view.follow = false; -} - -fn pushViewMoveCommandAxis(self: *MainScreen, view_id: Id, axis: UI.Axis, view_range: RangeF64) void { - const view = self.app.getView(view_id) orelse return; - const view_rect = &view.graph_opts; - - if (axis == .X) { - self.pushViewMoveCommand(view_id, view_range, view_rect.y_range); - } else { - self.pushViewMoveCommand(view_id, view_rect.x_range, view_range); - } -} - -fn undoLastMoveCommand(self: *MainScreen) void { - const command = self.view_undo_stack.popOrNull() orelse return; - const view = self.app.getView(command.view_id) orelse return; - const view_rect = &view.graph_opts; - - switch (command.action) { - .move_and_zoom => |args| { - view_rect.x_range = args.before_x; - view_rect.y_range = args.before_y; - } - } -} - -fn showChannelViewGraph(self: *MainScreen, view_id: Id) *UI.Box { +pub fn showProtocolModal(self: *MainScreen, view_id: Id) !void { var ui = &self.app.ui; - - const view = self.app.getView(view_id).?; - const samples = self.app.getViewSamples(view_id); - const view_opts = &view.graph_opts; - - const graph_box = ui.createBox(.{ - .key = ui.keyFromString("Graph"), - .size_x = UI.Sizing.initGrowFull(), - .size_y = UI.Sizing.initGrowFull(), - .background = srcery.black, - .flags = &.{ .clickable, .draggable, .scrollable }, - .align_x = .center, - .align_y = .center, - }); - graph_box.beginChildren(); - defer graph_box.endChildren(); - - const graph_rect = graph_box.rect(); - - const signal = ui.signal(graph_box); - - var sample_value_under_mouse: ?f64 = null; - var sample_index_under_mouse: ?f64 = null; - - const mouse_x_range = RangeF64.init(0, graph_rect.width); - const mouse_y_range = RangeF64.init(0, graph_rect.height); - - if (signal.hot) { - sample_index_under_mouse = mouse_x_range.remapTo(view_opts.x_range, signal.relative_mouse.x); - sample_value_under_mouse = mouse_y_range.remapTo(view_opts.y_range, signal.relative_mouse.y); - } - - if (signal.dragged()) { - const x_offset = mouse_x_range.remapTo(RangeF64.init(0, view_opts.x_range.size()), signal.drag.x); - const y_offset = mouse_y_range.remapTo(RangeF64.init(0, view_opts.y_range.size()), signal.drag.y); - - self.pushViewMoveCommand( - view_id, - view_opts.x_range.sub(x_offset), - view_opts.y_range.add(y_offset) - ); - } - - if (signal.scrolled() and sample_index_under_mouse != null and sample_value_under_mouse != null) { - var scale_factor: f64 = 1; - if (signal.scroll.y > 0) { - scale_factor -= zoom_speed; - } else { - scale_factor += zoom_speed; - } - - self.pushViewMoveCommand( - view_id, - view_opts.x_range.zoom(sample_index_under_mouse.?, scale_factor), - view_opts.y_range.zoom(sample_value_under_mouse.?, scale_factor) - ); - } - - if (signal.flags.contains(.middle_clicked)) { - self.pushViewMoveCommand(view_id, view.available_x_range, view.available_y_range); - } - - Graph.drawCached(&view.graph_cache, graph_box.persistent.size, view_opts.*, samples); - if (view.graph_cache.texture) |texture| { - graph_box.texture = texture.texture; - } - - if (view_opts.x_range.size() == 0 or view_opts.y_range.size() == 0) { - graph_box.setText(""); - graph_box.text_color = srcery.hard_black; - graph_box.font = .{ - .variant = .bold_italic, - .size = ui.rem(3) - }; - } - - return graph_box; -} - -const RulerContext = struct { - one_unit: ?f64, - render_range: RangeF64, - available_range: RangeF64, - axis: UI.Axis, - rect: rl.Rectangle = .{ .x = 0, .y = 0, .width = 0, .height = 0 }, - - fn init(axis: UI.Axis, project: *App.Project, view_id: Id) RulerContext { - const view = project.views.get(view_id).?; - - return RulerContext{ - .one_unit = switch (axis) { - .X => project.getSampleRate(), - .Y => 1 - }, - .render_range = view.getGraphView(axis).*, - .available_range = view.getAvailableView(axis), - .axis = axis, - }; - } - - fn getPoint(self: *RulerContext, along_axis_pos: f64, cross_axis_pos: f64) rl.Vector2 { - const rect_width: f64 = @floatCast(self.rect.width); - const rect_height: f64 = @floatCast(self.rect.height); - - var x: f64 = undefined; - var y: f64 = undefined; - - if (self.axis == .X) { - x = remap(f64, self.render_range.lower, self.render_range.upper, 0, rect_width, along_axis_pos); - y = cross_axis_pos * rect_height; - } else { - assert(self.axis == .Y); - - x = (1 - cross_axis_pos) * rect_width; - y = remap(f64, self.render_range.lower, self.render_range.upper, 0, rect_height, along_axis_pos); - } - - return rl.Vector2{ - .x = self.rect.x + @as(f32, @floatCast(x)), - .y = self.rect.y + @as(f32, @floatCast(y)), - }; - } - - fn getLine(self: *RulerContext, along_axis_pos: f64, cross_axis_size: f64) rl.Rectangle { - const along_axis_size = self.render_range.size() / switch(self.axis) { - .X => @as(f64, @floatCast(self.rect.width)), - .Y => @as(f64, @floatCast(self.rect.height)) - }; - - return self.getRect( - along_axis_pos, - along_axis_size, - 0, - cross_axis_size - ); - } - - fn getRect(self: *RulerContext, along_axis_pos: f64, along_axis_size: f64, cross_axis_pos: f64, cross_axis_size: f64) rl.Rectangle { - const pos = self.getPoint(along_axis_pos, cross_axis_pos); - const corner = self.getPoint(along_axis_pos + along_axis_size, cross_axis_pos + cross_axis_size); - var rect = rl.Rectangle{ - .x = pos.x, - .y = pos.y, - .width = corner.x - pos.x, - .height = corner.y - pos.y - }; - - if (rect.width < 0) { - rect.x += rect.width; - rect.width *= -1; - } - - if (rect.height < 0) { - rect.y += rect.height; - rect.height *= -1; - } - - return rect; - } - - fn drawLine(self: *RulerContext, along_axis_pos: f64, cross_axis_size: f64, color: rl.Color) void { - rl.drawLineV( - self.getPoint(along_axis_pos, 0), - self.getPoint(along_axis_pos, cross_axis_size), - color - ); - } - - fn drawTicks(self: *RulerContext, from: f64, to: f64, step: f64, line_size: f64, color: rl.Color) void { - var position = from; - while (position < to) : (position += step) { - self.drawLine(position, line_size, color); - } - } -}; - -fn drawRulerTicks(_ctx: ?*anyopaque, box: *UI.Box) void { - const ctx: *RulerContext = @ptrCast(@alignCast(_ctx)); - ctx.rect = box.rect(); - - ctx.drawLine(ctx.available_range.lower, 1, srcery.yellow); - ctx.drawLine(ctx.available_range.upper, 1, srcery.yellow); - - if (ctx.available_range.hasExclusive(0)) { - ctx.drawLine(0, 0.75, srcery.yellow); - } - - var one_unit = ctx.one_unit orelse return; - - while (ctx.render_range.size() < 5*one_unit) { - one_unit /= 2; - } - - while (ctx.render_range.size() > 50*one_unit) { - one_unit *= 2; - } - - var ticks_range = ctx.render_range.grow(one_unit).intersectPositive(ctx.available_range); - ticks_range.lower = utils.roundNearestTowardZero(f64, ticks_range.lower, one_unit); - - ctx.drawTicks(ticks_range.lower, ticks_range.upper, one_unit, 0.5, srcery.yellow); - ctx.drawTicks(ticks_range.lower + one_unit/2, ticks_range.upper, one_unit, 0.25, srcery.yellow); -} - -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 = &.{ .clickable, .scrollable, .clip_view }, - .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 formatDuration(allocator: std.mem.Allocator, total_seconds: f64) ![]u8 { - const seconds = @mod(total_seconds, @as(f64, @floatFromInt(std.time.s_per_min))); - const minutes = total_seconds / std.time.s_per_min; - return try std.fmt.allocPrint(allocator, "{d:.0}m {d:.3}s", .{ minutes, seconds }); -} - -fn showRuler(self: *MainScreen, ruler: *UI.Box, graph_box: *UI.Box, view_id: Id, axis: UI.Axis) !void { - var ui = &self.app.ui; - - const view = self.app.getView(view_id) orelse return; - - var ruler_ctx = RulerContext.init(axis, &self.app.project, view_id); - var graph_ctx = ruler_ctx; - ruler_ctx.rect = .{ - .x = 0, - .y = 0, - .width = ruler.persistent.size.x, - .height = ruler.persistent.size.y - }; - graph_ctx.rect = .{ - .x = 0, - .y = 0, - .width = graph_box.persistent.size.x, - .height = graph_box.persistent.size.y - }; - - ruler.beginChildren(); - defer ruler.endChildren(); - - const ctx = try ui.frameAllocator().create(RulerContext); - ctx.* = RulerContext.init(axis, &self.app.project, view_id); - - ruler.draw = .{ - .ctx = ctx, - .do = drawRulerTicks - }; - - 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 = view.getGraphView(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.view_id.eql(view_id) and axis_zoom.axis == axis; - } - - if (signal.hot and view_range.size() > 0) { - const mouse_tooltip = ui.mouseTooltip(); - mouse_tooltip.beginChildren(); - defer mouse_tooltip.endChildren(); - - const project = &self.app.project; - - if (view.getAvailableView(axis).hasInclusive(mouse_position_on_graph)) { - const sample_rate = project.getSampleRate(); - - if (axis == .Y and view.unit != null) { - const unit_name = view.unit.?.name() orelse "Unknown"; - _ = ui.label("{s}: {d:.3}", .{unit_name, mouse_position_on_graph}); - } else if (axis == .X and sample_rate != null) { - const seconds = mouse_position_on_graph / sample_rate.?; - const frame_allocator = ui.frameAllocator(); - _ = ui.label("{s}", .{ formatDuration(frame_allocator, seconds) catch "-" }); - - } else { - _ = 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, - .view_id = view_id - }; - } - - 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 = ruler_ctx.getLine(zoom_start.?, 1), - .float_relative_to = ruler, - }); - - _ = ui.createBox(.{ - .background = srcery.green, - .float_rect = graph_ctx.getLine(zoom_start.?, 1), - .float_relative_to = graph_box, - .parent = graph_box - }); - } - - if (zoom_end != null) { - _ = ui.createBox(.{ - .background = srcery.green, - .float_rect = ruler_ctx.getLine(zoom_end.?, 1), - .float_relative_to = ruler, - }); - - _ = ui.createBox(.{ - .background = srcery.green, - .float_rect = graph_ctx.getLine(zoom_end.?, 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 = ruler_ctx.getRect(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.pushViewMoveCommandAxis(view_id, 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.pushViewMoveCommandAxis(view_id, axis, new_view_range); - } - } - self.axis_zoom = null; - } -} - -fn showView(self: *MainScreen, view_id: Id, height: UI.Sizing) !void { - var ui = &self.app.ui; - - const show_ruler = true; - - const view_box = ui.createBox(.{ - .key = UI.Key.initUsize(view_id.asInt()), - .layout_direction = .top_to_bottom, - .size_x = UI.Sizing.initGrowFull(), - .size_y = height, - }); - view_box.beginChildren(); - defer view_box.endChildren(); - - const toolbar = ui.createBox(.{ - .layout_direction = .left_to_right, - .background = srcery.hard_black, - .size_x = UI.Sizing.initGrowFull(), - .size_y = UI.Sizing.initFixed(.{ .pixels = ui.rem(2) }) - }); - { - toolbar.beginChildren(); - defer toolbar.endChildren(); - - const view = self.app.getView(view_id).?; - var view_name: ?[]const u8 = null; - - { - const btn = ui.textButton("Settings"); - btn.background = srcery.hard_black; - if (self.view_settings != null and self.view_settings.?.eql(view_id)) { - btn.borders.bottom = .{ - .color = srcery.green, - .size = 4 - }; - } - - if (ui.signal(btn).clicked()) { - if (self.view_settings != null and self.view_settings.?.eql(view_id)) { - self.view_settings = null; - } else { - self.view_settings = view_id; - } - } - } - - { - const btn = ui.textButton("Reset view"); - btn.background = srcery.hard_black; - if (ui.signal(btn).clicked()) { - self.pushViewMoveCommand(view_id, view.available_x_range, view.available_y_range); - } - } - - if (view.reference == .channel) { - const channel_id = view.reference.channel; - const channel = self.app.getChannel(channel_id).?; - const channel_name = utils.getBoundedStringZ(&channel.name); - const channel_type = NIDaq.getChannelType(channel_name).?; - - { - const follow = ui.textButton("Follow"); - follow.background = srcery.hard_black; - if (view.follow) { - follow.borders = UI.Borders.bottom(.{ - .color = srcery.green, - .size = 4 - }); - } - if (ui.signal(follow).clicked()) { - view.follow = !view.follow; - } - } - - if (channel_type == .analog_output) { - const button = ui.button(ui.keyFromString("Output generation")); - button.texture = Assets.output_generation; - button.size.y = UI.Sizing.initGrowFull(); - - const signal = ui.signal(button); - if (signal.clicked()) { - if (self.app.isChannelOutputing(channel_id)) { - self.app.pushCommand(.{ - .stop_output = channel_id - }); - } else { - try self.openProtocolModal(channel_id); - } - } - - var color = rl.Color.white; - if (self.app.isChannelOutputing(channel_id)) { - color = srcery.red; - } - - if (signal.active) { - button.texture_color = color.alpha(0.6); - } else if (signal.hot) { - button.texture_color = color.alpha(0.8); - } else { - button.texture_color = color; - } - } - - view_name = channel_name; - } else if (view.reference == .file) { - const file_id = view.reference.file; - const file = self.app.getFile(file_id).?; - - view_name = std.fs.path.stem(file.path); - } - - if (view_name) |text| { - _ = ui.createBox(.{ - .size_x = UI.Sizing.initGrowFull() - }); - - const label = ui.label("{s}", .{text}); - label.size.y = UI.Sizing.initGrowFull(); - label.alignment.x = .center; - label.alignment.y = .center; - label.padding = UI.Padding.horizontal(ui.rem(1)); - } - } - - if (!show_ruler) { - _ = self.showChannelViewGraph(view_id); - - } else { - var graph_box: *UI.Box = undefined; - var x_ruler: *UI.Box = undefined; - var y_ruler: *UI.Box = undefined; - - { - const container = ui.createBox(.{ - .layout_direction = .left_to_right, - .size_x = UI.Sizing.initGrowFull(), - .size_y = UI.Sizing.initGrowFull(), - }); - container.beginChildren(); - defer container.endChildren(); - - y_ruler = self.addRulerPlaceholder(ui.keyFromString("Y ruler"), .Y); - - graph_box = self.showChannelViewGraph(view_id); - } - - { - const container = ui.createBox(.{ - .layout_direction = .left_to_right, - .size_x = UI.Sizing.initGrowFull(), - .size_y = ruler_size, - }); - container.beginChildren(); - defer container.endChildren(); - - const fullscreen = ui.createBox(.{ - .key = ui.keyFromString("Fullscreen toggle"), - .size_x = ruler_size, - .size_y = ruler_size, - .background = srcery.hard_black, - .hot_cursor = .mouse_cursor_pointing_hand, - .flags = &.{ .draw_hot, .draw_active, .clickable }, - .texture = Assets.fullscreen, - .texture_size = .{ .x = 28, .y = 28 } - }); - if (ui.signal(fullscreen).clicked()) { - if (self.fullscreen_view != null and self.fullscreen_view.?.eql(view_id)) { - self.fullscreen_view = null; - } else { - self.fullscreen_view = view_id; - } - } - - x_ruler = self.addRulerPlaceholder(ui.keyFromString("X ruler"), .X); - } - - try self.showRuler(x_ruler, graph_box, view_id, .X); - try self.showRuler(y_ruler, graph_box, view_id, .Y); - - } -} - -fn openProtocolModal(self: *MainScreen, channel_id: Id) !void { - self.protocol_modal = channel_id; - self.protocol_graph_cache.clear(); -} - -fn closeModal(self: *MainScreen) void { - self.protocol_modal = null; -} - -pub fn showProtocolModal(self: *MainScreen, channel_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; @@ -877,7 +231,7 @@ pub fn showProtocolModal(self: *MainScreen, channel_id: Id) !void { try App.Channel.generateSine(&channel.write_pattern, allocator, sample_rate, frequency, amplitude); self.app.pushCommand(.{ .start_output = channel_id }); - self.protocol_modal = null; + self.view_controls.view_protocol_modal = null; } } } @@ -921,7 +275,7 @@ pub fn showSidePanel(self: *MainScreen) !void { const project = &self.app.project; const sample_rate = project.getSampleRate(); - if (self.view_settings) |view_id| { + if (self.view_controls.view_settings) |view_id| { const view = project.views.get(view_id) orelse return; { @@ -968,7 +322,7 @@ pub fn showSidePanel(self: *MainScreen) !void { var duration_str: []const u8 = "-"; if (sample_rate != null) { const duration = @as(f64, @floatFromInt(sample_count.?)) / sample_rate.?; - if (formatDuration(ui.frameAllocator(), duration)) |str| { + if (utils.formatDuration(ui.frameAllocator(), duration)) |str| { duration_str = str; } else |_| {} } @@ -1022,14 +376,16 @@ pub fn tick(self: *MainScreen) !void { var ui = &self.app.ui; if (ui.isCtrlDown() and ui.isKeyboardPressed(.key_z)) { - self.undoLastMoveCommand(); + self.view_controls.undoLastMove(); } const root = ui.parentBox().?; root.layout_direction = .top_to_bottom; + const was_protocol_modal_open = self.view_controls.view_protocol_modal != null; + var maybe_modal_overlay: ?*UI.Box = null; - if (self.protocol_modal) |channel_id| { + if (self.view_controls.view_protocol_modal) |view_id| { const padding = UI.Padding.all(ui.rem(2)); const modal_overlay = ui.createBox(.{ .key = ui.keyFromString("Overlay"), @@ -1049,10 +405,10 @@ pub fn tick(self: *MainScreen) !void { modal_overlay.beginChildren(); defer modal_overlay.endChildren(); - try self.showProtocolModal(channel_id); + try self.showProtocolModal(view_id); if (ui.signal(modal_overlay).clicked()) { - self.closeModal(); + self.view_controls.view_protocol_modal = null; } maybe_modal_overlay = modal_overlay; @@ -1102,8 +458,14 @@ pub fn tick(self: *MainScreen) !void { } } - if (self.fullscreen_view) |view_id| { - try self.showView(view_id, UI.Sizing.initGrowFull()); + 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(.{ @@ -1124,7 +486,7 @@ pub fn tick(self: *MainScreen) !void { var view_iter = self.app.project.views.idIterator(); while (view_iter.next()) |view_id| { const view = self.app.getView(view_id); - try self.showView(view_id, UI.Sizing.initFixed(.{ .pixels = view.?.height })); + _ = try UIView.show(ui_view_ctx, view_id, UI.Sizing.initFixed(.{ .pixels = view.?.height })); } { @@ -1152,19 +514,26 @@ pub fn tick(self: *MainScreen) !void { } } + self.view_controls.applyCommands(); + if (maybe_modal_overlay) |modal_overlay| { root.bringChildToTop(modal_overlay); } if (ui.isKeyboardPressed(.key_escape)) { - if (self.protocol_modal != null) { - self.closeModal(); - } else if (self.fullscreen_view != null) { - self.fullscreen_view = null; - } else if (self.view_settings != null) { - self.view_settings = null; + if (self.view_controls.view_protocol_modal != null) { + self.view_controls.view_protocol_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 { self.app.should_close = true; } } + + const is_protocol_modal_open = self.view_controls.view_protocol_modal != null; + if (!was_protocol_modal_open and is_protocol_modal_open) { + self.protocol_graph_cache.clear(); + } } \ No newline at end of file diff --git a/src/utils.zig b/src/utils.zig index 9e98718..0981e68 100644 --- a/src/utils.zig +++ b/src/utils.zig @@ -82,4 +82,10 @@ pub inline fn dumpErrorTrace() void { if (@errorReturnTrace()) |trace| { std.debug.dumpStackTrace(trace.*); } +} + +pub fn formatDuration(allocator: std.mem.Allocator, total_seconds: f64) ![]u8 { + const seconds = @mod(total_seconds, @as(f64, @floatFromInt(std.time.s_per_min))); + const minutes = @divFloor(total_seconds, std.time.s_per_min); + return try std.fmt.allocPrint(allocator, "{d:.0}m {d:.3}s", .{ minutes, seconds }); } \ No newline at end of file