add checkbox to toggle view sync controls

This commit is contained in:
Rokas Puzonas 2025-04-08 21:36:19 +03:00
parent 772f35fee7
commit 8d1cad16b3
7 changed files with 237 additions and 168 deletions

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

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