add synchronized movement between views

This commit is contained in:
Rokas Puzonas 2025-04-08 21:36:17 +03:00
parent 5c8a4ebb54
commit 36706ff348
7 changed files with 970 additions and 682 deletions

View File

@ -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);

View File

@ -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);
}

299
src/components/view.zig Normal file
View File

@ -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("<Empty>");
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;
}

View File

@ -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;
}
}
}

View File

@ -1,7 +1,10 @@
pub const max_files = 32;
pub const max_channels = 32;
pub const max_views = 64;
// UI
pub const show_ruler = true;
pub const sync_view_controls = true;
pub const zoom_speed = 0.1;

View File

@ -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("<Empty>");
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();
}
}

View File

@ -83,3 +83,9 @@ pub inline fn dumpErrorTrace() void {
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 });
}