diff --git a/src/app.zig b/src/app.zig index c7699a7..ec0a70b 100644 --- a/src/app.zig +++ b/src/app.zig @@ -250,6 +250,7 @@ pub const View = struct { height: f32 = 300, follow: bool = false, graph_opts: Graph.ViewOptions = .{}, + sync_controls: bool = false, // Runtime graph_cache: Graph.Cache = .{}, @@ -359,6 +360,9 @@ pub const Project = struct { self.sample_rate = null; } + const show_rulers_u8 = try readInt(reader, u8); + self.show_rulers = show_rulers_u8 == 1; + { // Channels const channel_count = try readInt(reader, u32); for (0..channel_count) |_| { @@ -421,6 +425,8 @@ pub const Project = struct { view.graph_opts.x_range = try readRangeF64(reader); view.graph_opts.y_range = try readRangeF64(reader); + const sync_controls = try readInt(reader, u8); + view.sync_controls = sync_controls == 1; } } @@ -433,58 +439,70 @@ pub const Project = struct { pub fn save(self: *Project) !void { const save_location = self.save_location orelse return error.NoSaveLocation; - const f = try std.fs.cwd().createFile(save_location, .{}); - defer f.close(); + var save_tmp_location: std.BoundedArray(u8, std.fs.max_path_bytes) = .{}; + save_tmp_location.appendSliceAssumeCapacity(save_location); + save_tmp_location.appendSliceAssumeCapacity("-tmp"); - const writer = f.writer(); + const dir = std.fs.cwd(); - try writeInt(writer, u8, file_format_version); - try writeFloat(writer, f64, self.sample_rate orelse 0); + { + const f = try dir.createFile(save_tmp_location.slice(), .{}); + defer f.close(); - { // Channels - try writeInt(writer, u32, @intCast(self.channels.count())); - var channel_iter = self.channels.idIterator(); - while (channel_iter.next()) |channel_id| { - const channel = self.channels.get(channel_id).?; - const channel_name = utils.getBoundedStringZ(&channel.name); + const writer = f.writer(); - try writeId(writer, channel_id); - try writeString(writer, channel_name); - } - } + try writeInt(writer, u8, file_format_version); + try writeFloat(writer, f64, self.sample_rate orelse 0); + try writeInt(writer, u8, @intFromBool(self.show_rulers)); - { // Files - try writeInt(writer, u32, @intCast(self.files.count())); - var file_iter = self.files.idIterator(); - while (file_iter.next()) |file_id| { - const file = self.files.get(file_id).?; + { // Channels + try writeInt(writer, u32, @intCast(self.channels.count())); + var channel_iter = self.channels.idIterator(); + while (channel_iter.next()) |channel_id| { + const channel = self.channels.get(channel_id).?; + const channel_name = utils.getBoundedStringZ(&channel.name); - try writeId(writer, file_id); - try writeString(writer, file.path); - } - } - - { // Views - try writeInt(writer, u32, @intCast(self.views.count())); - var view_iter = self.views.idIterator(); - while (view_iter.next()) |view_id| { - const view = self.views.get(view_id).?; - - try writeId(writer, view_id); - try writeInt(writer, u8, @intFromEnum(view.reference)); - switch (view.reference) { - .channel => |channel_id| { - try writeInt(writer, u32, channel_id.asInt()); - }, - .file => |file_id| { - try writeInt(writer, u32, file_id.asInt()); - } + try writeId(writer, channel_id); + try writeString(writer, channel_name); } + } - try writeRangeF64(writer, view.graph_opts.x_range); - try writeRangeF64(writer, view.graph_opts.y_range); + { // Files + try writeInt(writer, u32, @intCast(self.files.count())); + var file_iter = self.files.idIterator(); + while (file_iter.next()) |file_id| { + const file = self.files.get(file_id).?; + + try writeId(writer, file_id); + try writeString(writer, file.path); + } + } + + { // Views + try writeInt(writer, u32, @intCast(self.views.count())); + var view_iter = self.views.idIterator(); + while (view_iter.next()) |view_id| { + const view = self.views.get(view_id).?; + + try writeId(writer, view_id); + try writeInt(writer, u8, @intFromEnum(view.reference)); + switch (view.reference) { + .channel => |channel_id| { + try writeInt(writer, u32, channel_id.asInt()); + }, + .file => |file_id| { + try writeInt(writer, u32, file_id.asInt()); + } + } + + try writeRangeF64(writer, view.graph_opts.x_range); + try writeRangeF64(writer, view.graph_opts.y_range); + try writeInt(writer, u8, @intFromBool(view.sync_controls)); + } } } + + try std.fs.rename(dir, save_tmp_location.slice(), dir, save_location); } fn writeRangeF64(writer: anytype, range: RangeF64) !void { @@ -1083,7 +1101,6 @@ pub fn loadFile(self: *App, id: Id) !void { const samples = try readFileF64(self.allocator, samples_file); file.samples = samples; - if (samples.len > 0) { file.min_sample = samples[0]; file.max_sample = samples[0]; @@ -1255,6 +1272,11 @@ pub fn loadView(self: *App, id: Id) !void { self.refreshViewAvailableXYRanges(id); - view.graph_opts.x_range = view.available_x_range; - view.graph_opts.y_range = view.available_y_range; + if (view.graph_opts.x_range.size() == 0) { + view.graph_opts.x_range = view.available_x_range; + } + + if (view.graph_opts.y_range.size() == 0) { + view.graph_opts.y_range = view.available_y_range; + } } \ No newline at end of file diff --git a/src/assets.zig b/src/assets.zig index 304977b..b404413 100644 --- a/src/assets.zig +++ b/src/assets.zig @@ -48,6 +48,7 @@ pub var dropdown_arrow: rl.Texture2D = undefined; pub var fullscreen: rl.Texture2D = undefined; pub var output_generation: rl.Texture2D = undefined; pub var checkbox_mark: rl.Texture2D = undefined; +pub var cross: rl.Texture2D = undefined; pub fn font(font_id: FontId) FontFace { var found_font: ?LoadedFont = null; @@ -123,6 +124,7 @@ pub fn init(allocator: std.mem.Allocator) !void { fullscreen = try loadTextureFromAseprite(allocator, @embedFile("./assets/fullscreen-icon.ase")); output_generation = try loadTextureFromAseprite(allocator, @embedFile("./assets/output-generation-icon.ase")); checkbox_mark = try loadTextureFromAseprite(allocator, @embedFile("./assets/checkbox-mark.ase")); + cross = try loadTextureFromAseprite(allocator, @embedFile("./assets/cross.ase")); } fn loadTextureFromAseprite(allocator: std.mem.Allocator, memory: []const u8) !rl.Texture { diff --git a/src/components/systems/view_controls.zig b/src/components/systems/view_controls.zig index 0c49b1b..56b19a2 100644 --- a/src/components/systems/view_controls.zig +++ b/src/components/systems/view_controls.zig @@ -30,12 +30,13 @@ pub const ViewAxisPosition = struct { } } - fn get(optional_self: *?ViewAxisPosition, view_id: Id, axis: UI.Axis) ?f64 { + fn get(optional_self: *?ViewAxisPosition, project: *App.Project, view_id: Id, axis: UI.Axis) ?f64 { const self = optional_self.* orelse return null; if (self.axis != axis) return null; - if (!constants.sync_view_controls) { + const view = project.views.get(view_id) orelse return null; + if (!view.sync_controls) { if (!self.view_id.eql(view_id)) { return null; } @@ -152,6 +153,8 @@ fn lastCommandFrame(self: *System) ?*CommandFrame { } pub fn pushViewMove(self: *System, view_id: Id, x_range: RangeF64, y_range: RangeF64) void { + + var frame: *CommandFrame = undefined; { var push_new_command = true; @@ -174,11 +177,18 @@ pub fn pushViewMove(self: *System, view_id: Id, x_range: RangeF64, y_range: Rang } } + var sync_controls = false; + if (self.project.views.get(view_id)) |view| { + sync_controls = view.sync_controls; + } + var view_ids: std.BoundedArray(Id, constants.max_views) = .{}; - if (constants.sync_view_controls) { + if (sync_controls) { var iter = self.project.views.idIterator(); while (iter.next()) |id| { - view_ids.appendAssumeCapacity(id); + if (self.project.views.get(id).?.sync_controls) { + view_ids.appendAssumeCapacity(id); + } } } else { view_ids.appendAssumeCapacity(view_id); @@ -192,7 +202,7 @@ pub fn pushViewMove(self: *System, view_id: Id, x_range: RangeF64, y_range: Rang 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 = self.project.views.get(view_id) orelse continue; const view_rect = &view.graph_opts; command = frame.commands.addOneAssumeCapacity(); @@ -264,7 +274,7 @@ pub fn setCursor(self: *System, view_id: Id, axis: UI.Axis, position: ?f64) void } pub fn getCursor(self: *System, view_id: Id, axis: UI.Axis) ?f64 { - return ViewAxisPosition.get(&self.cursor, view_id, axis); + return ViewAxisPosition.get(&self.cursor, self.project, view_id, axis); } pub fn setZoomStart(self: *System, view_id: Id, axis: UI.Axis, position: ?f64) void { @@ -272,5 +282,5 @@ pub fn setZoomStart(self: *System, view_id: Id, axis: UI.Axis, position: ?f64) v } pub fn getZoomStart(self: *System, view_id: Id, axis: UI.Axis) ?f64 { - return ViewAxisPosition.get(&self.zoom_start, view_id, axis); + return ViewAxisPosition.get(&self.zoom_start, self.project,view_id, axis); } \ No newline at end of file diff --git a/src/components/view.zig b/src/components/view.zig index 0bfc1cc..1ce92b3 100644 --- a/src/components/view.zig +++ b/src/components/view.zig @@ -117,6 +117,125 @@ fn showGraph(ctx: Context, view_id: Id) *UI.Box { return graph_box; } +fn showToolbar(ctx: Context, view_id: Id) void { + var ui = ctx.ui; + + 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.sync_controls) { + const btn = ui.button(ui.keyFromString("Disable sync")); + btn.texture = Assets.cross; + btn.size.y = UI.Sizing.initGrowFull(); + btn.tooltip = "Disable sync controls"; + if (ui.signal(btn).clicked()) { + view.sync_controls = false; + } + } + + 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)); + } +} + pub fn show(ctx: Context, view_id: Id, height: UI.Sizing) !Result { var ui = ctx.ui; @@ -133,112 +252,7 @@ pub fn show(ctx: Context, view_id: Id, height: UI.Sizing) !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)); - } - } + showToolbar(ctx, view_id); if (!ctx.app.project.show_rulers) { _ = showGraph(ctx, view_id); diff --git a/src/constants.zig b/src/constants.zig index f9ccd69..5c23226 100644 --- a/src/constants.zig +++ b/src/constants.zig @@ -1,9 +1,7 @@ - pub const max_files = 32; pub const max_channels = 32; pub const max_views = 64; // UI -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 26b4960..8fe92a6 100644 --- a/src/screens/main_screen.zig +++ b/src/screens/main_screen.zig @@ -272,6 +272,11 @@ pub fn showSidePanel(self: *MainScreen) !void { _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) }); + _ = ui.checkbox(.{ + .value = &view.sync_controls, + .label = "Sync controls" + }); + var sample_count: ?usize = null; switch (view.reference) { .channel => |channel_id| { @@ -313,7 +318,6 @@ pub fn showSidePanel(self: *MainScreen) !void { _ = ui.label("Duration: {s}", .{ duration_str }); } - } else { { const label = ui.label("Project", .{}); @@ -356,12 +360,10 @@ pub fn showSidePanel(self: *MainScreen) !void { } } - { // Show ruler checkbox - _ = ui.checkbox(.{ - .value = &project.show_rulers, - .label = "Ruler" - }); - } + _ = ui.checkbox(.{ + .value = &project.show_rulers, + .label = "Ruler" + }); } } diff --git a/src/ui.zig b/src/ui.zig index b5c9df2..ecdcefe 100644 --- a/src/ui.zig +++ b/src/ui.zig @@ -515,6 +515,7 @@ pub const Box = struct { draw: ?Draw = null, visual_hot: bool = false, visual_active: bool = false, + tooltip: ?[]const u8 = null, // Variables that you probably shouldn't be touching last_used_frame: u64 = 0, @@ -699,6 +700,10 @@ pub const Box = struct { } } + fn hasChildren(self: *const Box) bool { + return self.tree.first_child_index != null; + } + pub fn bringChildToTop(self: *Box, child: *Box) void { self.removeChild(child); self.appendChild(child); @@ -942,6 +947,22 @@ pub fn begin(self: *UI) void { pub fn end(self: *UI) void { const mouse_tooltip = self.getBoxByKey(mouse_tooltip_box_key).?; + + // Add mouse tooltip to hot item + if (!mouse_tooltip.hasChildren() and self.hot_box_key != null) { + const box = self.getBoxByKey(self.hot_box_key.?).?; + if (box.tooltip) |tooltip| { + mouse_tooltip.beginChildren(); + defer mouse_tooltip.endChildren(); + + _ = self.createBox(.{ + .size_x = Sizing.initFixed(.text), + .size_y = Sizing.initFixed(.text), + .text = tooltip + }); + } + } + const root_box = self.parentBox().?; root_box.endChildren(); @@ -1632,7 +1653,7 @@ pub fn draw(self: *UI) void { self.drawBox(root_box); - if (mouse_tooltip.tree.first_child_index != null) { + if (mouse_tooltip.hasChildren()) { self.drawBox(mouse_tooltip); } }