From e5bc57d7b526f494bc1394cc9967c444fe09849f Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Fri, 11 Apr 2025 02:06:12 +0300 Subject: [PATCH] show statistics in marked ranges --- src/app.zig | 245 +++++++++++++++++--- src/components/systems/view_controls.zig | 55 ++++- src/components/view.zig | 72 +++--- src/components/view_ruler.zig | 280 +++++++++++++++-------- src/main.zig | 17 -- src/screens/main_screen.zig | 192 ++++++++++++---- src/ui.zig | 46 ++-- src/utils.zig | 4 +- 8 files changed, 674 insertions(+), 237 deletions(-) diff --git a/src/app.zig b/src/app.zig index 9012569..496ca8c 100644 --- a/src/app.zig +++ b/src/app.zig @@ -159,6 +159,10 @@ fn GenerationalArray(Item: type) type { pub fn isEmpty(self: *Self) bool { return self.used.eql(UsedBitSet.initFull()); } + + pub fn clear(self: *Self) void { + self.used = UsedBitSet.initFull(); + } }; } @@ -180,6 +184,7 @@ pub const Channel = struct { output_task: ?NIDaq.Task = null, graph_min_max_cache: Graph.MinMaxCache = .{}, last_sample_save_at: ?i128 = null, + processed_samples_up_to: usize = 0, pub fn deinit(self: *Channel, allocator: Allocator) void { self.clear(allocator); @@ -242,6 +247,10 @@ pub const Channel = struct { pub fn invalidateSavedSamples(self: *Channel) void { self.last_sample_save_at = null; } + + fn hasNewCollectedSamples(self: *Channel) bool { + return self.processed_samples_up_to != self.collected_samples.items.len; + } }; pub const File = struct { @@ -271,6 +280,86 @@ pub const File = struct { }; pub const View = struct { + pub const MarkedRange = struct { + // Persistent + axis: UI.Axis, + range: RangeF64, + + // Runtime + min: ?f64 = null, + max: ?f64 = null, + average: ?f64 = null, + standard_deviation: ?f64 = null, + + pub fn clear(self: *MarkedRange) void { + self.min = null; + self.max = null; + self.average = null; + self.standard_deviation = null; + } + + pub fn refresh(self: *MarkedRange, samples: []const f64) void { + self.clear(); + + if (self.axis == .X) { + const from = std.math.clamp(@as(usize, @intFromFloat(@floor(self.range.lower))), 0, samples.len); + const to = std.math.clamp(@as(usize, @intFromFloat(@ceil(self.range.upper))), 0, samples.len); + if (to - from == 0) return; + + const samples_in_range = samples[from..to]; + if (samples_in_range.len > 0) { + const sample_count: f64 = @floatFromInt(samples_in_range.len); + + var sum: f64 = 0; + var min = samples_in_range[0]; + var max = samples_in_range[0]; + for (samples_in_range) |sample| { + min = @min(min, sample); + max = @max(max, sample); + sum += sample; + } + const average = sum / sample_count; + + self.min = min; + self.max = max; + self.average = average; + + if (sample_count > 1) { + var standard_deviation: f64 = 0; + for (samples_in_range) |sample| { + standard_deviation += (sample - average) * (sample - average); + } + standard_deviation /= (sample_count - 1); + standard_deviation = @sqrt(standard_deviation); + + self.standard_deviation = standard_deviation; + } + } + } + } + }; + + const MarkedRangeIterator = struct { + view: *View, + axis: UI.Axis, + index: usize = 0, + next_index: usize = 0, + + pub fn next(self: *MarkedRangeIterator) ?RangeF64 { + const marked_ranges = self.view.marked_ranges.constSlice(); + while (self.next_index < marked_ranges.len) { + self.index = self.next_index; + const marked_range = marked_ranges[self.index]; + self.next_index += 1; + + if (marked_range.axis == self.axis) { + return marked_range.range; + } + } + return null; + } + }; + pub const Reference = union(enum) { file: Id, channel: Id @@ -283,6 +372,7 @@ pub const View = struct { follow: bool = false, graph_opts: Graph.ViewOptions = .{}, sync_controls: bool = false, + marked_ranges: std.BoundedArray(MarkedRange, 32) = .{}, // Runtime graph_cache: Graph.RenderCache = .{}, @@ -309,11 +399,31 @@ pub const View = struct { .Y => self.available_y_range }; } + + pub fn appendMarkedRange(self: *View, axis: UI.Axis, range: RangeF64) ?*MarkedRange { + if (self.marked_ranges.unusedCapacitySlice().len == 0) { + return null; + } + + const marked_range = self.marked_ranges.addOneAssumeCapacity(); + marked_range.* = MarkedRange{ + .axis = axis, + .range = range + }; + return marked_range; + } + + pub fn iterMarkedRanges(self: *View, axis: UI.Axis) MarkedRangeIterator { + return MarkedRangeIterator{ + .view = self, + .axis = axis + }; + } }; pub const Project = struct { - const file_endian = std.builtin.Endian.big; const file_format_version: u8 = 0; + const file_endian = std.builtin.Endian.big; save_location: ?[]u8 = null, sample_rate: ?f64 = null, @@ -357,16 +467,39 @@ pub const Project = struct { return result_range; } + pub fn getViewSamples(self: *Project, view_id: Id) []const f64 { + const empty = &[0]f64{}; + + var result: []const f64 = empty; + + if (self.views.get(view_id)) |view| { + switch (view.reference) { + .channel => |channel_id| if (self.channels.get(channel_id)) |channel| { + result = channel.collected_samples.items; + }, + .file => |file_id| if (self.files.get(file_id)) |file| { + result = file.samples orelse empty; + } + } + } + + return result; + } + pub fn deinit(self: *Project, allocator: Allocator) void { var file_iter = self.files.iterator(); while (file_iter.next()) |file| { file.deinit(allocator); } + self.files.clear(); var channel_iter = self.channels.iterator(); while (channel_iter.next()) |channel| { channel.deinit(allocator); } + self.channels.clear(); + + self.views.clear(); if (self.save_location) |str| { allocator.free(str); @@ -376,8 +509,9 @@ pub const Project = struct { // ------------------- Serialization ------------------ // - pub fn initFromFile(allocator: Allocator, save_location: []const u8) !Project { - var self = Project{}; + pub fn initFromFile(self: *Project, allocator: Allocator, save_location: []const u8) !void { + self.* = .{}; + errdefer self.deinit(allocator); const f = try std.fs.cwd().openFile(save_location, .{}); defer f.close(); @@ -469,13 +603,18 @@ pub const Project = struct { view.graph_opts.y_range = try readRangeF64(reader); const sync_controls = try readInt(reader, u8); view.sync_controls = sync_controls == 1; + + const marked_ranges_count = try readInt(reader, u32); + for (0..marked_ranges_count) |_| { + try view.marked_ranges.append(View.MarkedRange{ + .axis = try readEnum(reader, u8, UI.Axis), + .range = try readRangeF64(reader) + }); + } } } self.save_location = try allocator.dupe(u8, save_location); - errdefer allocator.free(self.save_location); - - return self; } pub fn save(self: *Project) !void { @@ -555,6 +694,12 @@ pub const Project = struct { 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 writeInt(writer, u32, view.marked_ranges.len); + for (view.marked_ranges.constSlice()) |marked_range| { + try writeEnum(writer, u8, UI.Axis, marked_range.axis); + try writeRangeF64(writer, marked_range.range); + } } } } @@ -567,11 +712,11 @@ pub const Project = struct { try writeFloat(writer, f64, range.upper); } - fn readRangeF64(writer: anytype) !RangeF64 { + fn readRangeF64(reader: anytype) !RangeF64 { var range: RangeF64 = undefined; - range.lower = try readFloat(writer, f64); - range.upper = try readFloat(writer, f64); + range.lower = try readFloat(reader, f64); + range.upper = try readFloat(reader, f64); return range; } @@ -617,6 +762,27 @@ pub const Project = struct { try reader.readNoEof(buff); return buff; } + + fn assertBackingIntForEnum(BackingInt: type, Enum: type) void { + assert(@typeInfo(Enum) == .Enum); + const enum_backing_type = @typeInfo(Enum).Enum.tag_type; + assert(@typeInfo(enum_backing_type) == .Int); + assert(@typeInfo(BackingInt).Int.bits >= @typeInfo(enum_backing_type).Int.bits); + } + + fn readEnum(reader: anytype, BackingInt: type, Enum: type) !Enum { + assertBackingIntForEnum(BackingInt, Enum); + + const value_int = try readInt(reader, BackingInt); + + return try std.meta.intToEnum(Enum, value_int); + } + + fn writeEnum(writer: anytype, BackingInt: type, Enum: type, value: Enum) !void { + assertBackingIntForEnum(BackingInt, Enum); + + try writer.writeInt(BackingInt, @intFromEnum(value), file_endian); + } }; pub const Command = union(enum) { @@ -755,11 +921,15 @@ fn loadProject(self: *App) !void { log.info("Load project from: {s}", .{save_location}); - const loaded = try Project.initFromFile(self.allocator, save_location); + var loaded = try self.allocator.create(Project); + defer self.allocator.destroy(loaded); + + loaded.* = .{}; errdefer loaded.deinit(self.allocator); + try loaded.initFromFile(self.allocator, save_location); self.deinitProject(); - self.project = loaded; + self.project = loaded.*; var file_iter = self.project.files.idIterator(); while (file_iter.next()) |file_id| { @@ -842,11 +1012,39 @@ pub fn tick(self: *App) !void { self.collection_samples_mutex.lock(); defer self.collection_samples_mutex.unlock(); + { + var view_iter = self.project.views.idIterator(); + while (view_iter.next()) |id| { + const view = self.getView(id) orelse continue; + if (view.reference != .channel) continue; + + const channel_id = view.reference.channel; + const channel = self.getChannel(channel_id) orelse continue; + if (!channel.hasNewCollectedSamples()) continue; + + const samples = channel.collected_samples.items; + + const new_samples_range = RangeF64.init(@floatFromInt(channel.processed_samples_up_to), @floatFromInt(samples.len)); + const marked_ranges: []View.MarkedRange = view.marked_ranges.slice(); + for (marked_ranges) |*marked_range| { + if (marked_range.axis != .X) continue; + + if (new_samples_range.intersectPositive(marked_range.range).isPositive()) { + marked_range.refresh(samples); + } + } + } + } + // Update channel min max caches { var channel_iter = self.project.channels.iterator(); while (channel_iter.next()) |channel| { - try channel.graph_min_max_cache.updateLast(self.allocator, channel.collected_samples.items); + if (channel.hasNewCollectedSamples()) { + channel.invalidateSavedSamples(); + try channel.graph_min_max_cache.updateLast(self.allocator, channel.collected_samples.items); + channel.processed_samples_up_to = channel.collected_samples.items.len; + } } } @@ -858,11 +1056,6 @@ pub fn tick(self: *App) !void { } if (self.isCollectionInProgress()) { - var channel_iter = self.project.channels.idIterator(); - while (channel_iter.next()) |channel_id| { - self.refreshViewAvailableXYRanges(channel_id); - } - var view_iter = self.project.views.iterator(); while (view_iter.next()) |view| { if (view.reference != .channel) continue; @@ -1156,7 +1349,6 @@ pub fn collectionThreadCallback(self: *App) void { log.err("Failed to append samples for channel: {}", .{e}); continue; }; - channel.invalidateSavedSamples(); } } @@ -1405,22 +1597,7 @@ pub fn addView(self: *App, reference: View.Reference) !Id { } pub fn getViewSamples(self: *App, id: Id) []const f64 { - const empty = &[0]f64{}; - - var result: []const f64 = empty; - - if (self.getView(id)) |view| { - switch (view.reference) { - .channel => |channel_id| if (self.getChannel(channel_id)) |channel| { - result = channel.collected_samples.items; - }, - .file => |file_id| if (self.getFile(file_id)) |file| { - result = file.samples orelse empty; - } - } - } - - return result; + return self.project.getViewSamples(id); } pub fn getViewMinMaxCache(self: *App, id: Id) Graph.MinMaxCache { diff --git a/src/components/systems/view_controls.zig b/src/components/systems/view_controls.zig index 559ee80..f986dcc 100644 --- a/src/components/systems/view_controls.zig +++ b/src/components/systems/view_controls.zig @@ -92,6 +92,10 @@ pub const Command = union(enum) { }; pub const CommandFrame = struct { + // When a new command is pushed after this one, it will become marked as "frozen" + // So that movement commands wouldn't be able to update it. + frozen: bool = false, + updated_at_ns: i128, commands: std.BoundedArray(Command, constants.max_views) = .{}, @@ -133,6 +137,28 @@ pub const CommandFrame = struct { pub const CommandFrameArray = std.BoundedArray(CommandFrame, 64); +const MarkedRangeIterator = struct { + system: *System, + view_id: Id, + axis: UI.Axis, + index: usize = 0, + next_index: usize = 0, + + pub fn next(self: *MarkedRangeIterator) ?RangeF64 { + const selected_ranges = self.system.marked_ranges.constSlice(); + while (self.next_index < selected_ranges.len) { + self.index = self.next_index; + const selected_range = selected_ranges[self.index]; + self.next_index += 1; + + if (selected_range.axis == self.axis and selected_range.view_id.eql(self.view_id)) { + return selected_range.range; + } + } + return null; + } +}; + project: *App.Project, // TODO: Redo @@ -144,6 +170,11 @@ view_settings: ?Id = null, // View id view_fullscreen: ?Id = null, // View id view_protocol_modal: ?Id = null, // View id show_ruler: bool = false, +selected_tool: enum { move, select } = .move, +show_marked_range: ?struct { + view_id: Id, + index: usize, +} = null, pub fn init(project: *App.Project) System { return System{ @@ -156,6 +187,10 @@ fn pushCommandFrame(self: *System) *CommandFrame { _ = self.undo_stack.orderedRemove(0); } + if (self.lastCommandFrame()) |frame| { + frame.frozen = true; + } + var frame = self.undo_stack.addOneAssumeCapacity(); frame.updated_at_ns = std.time.nanoTimestamp(); frame.commands.len = 0; @@ -190,7 +225,7 @@ pub fn pushViewMove(self: *System, view_id: Id, x_range: RangeF64, y_range: Rang frame = last_frame; const now_ns = std.time.nanoTimestamp(); - if (now_ns - last_frame.updated_at_ns < std.time.ns_per_ms * 250) { + if (!last_frame.frozen and now_ns - last_frame.updated_at_ns < std.time.ns_per_ms * 250) { last_frame.updated_at_ns = now_ns; push_new_command = false; @@ -307,10 +342,24 @@ pub fn getCursor(self: *System, view_id: Id, axis: UI.Axis) ?f64 { return ViewAxisPosition.get(&self.cursor, self.project, view_id, axis); } -pub fn setZoomStart(self: *System, view_id: Id, axis: UI.Axis, position: ?f64) void { +pub fn setCursorHoldStart(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 { +pub fn getCursorHoldStart(self: *System, view_id: Id, axis: UI.Axis) ?f64 { return ViewAxisPosition.get(&self.zoom_start, self.project,view_id, axis); +} + +pub fn toggleShownMarkedRange(self: *System, view_id: Id, index: usize) void { + if (self.show_marked_range) |show_marked_range| { + if (show_marked_range.view_id.eql(view_id) and show_marked_range.index == index) { + self.show_marked_range = null; + return; + } + } + + self.show_marked_range = .{ + .view_id = view_id, + .index = index, + }; } \ No newline at end of file diff --git a/src/components/view.zig b/src/components/view.zig index 945e7aa..8336a6d 100644 --- a/src/components/view.zig +++ b/src/components/view.zig @@ -70,45 +70,51 @@ fn showGraph(ctx: Context, view_id: Id) *UI.Box { 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); + if (ctx.view_controls.selected_tool == .move) { + 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.sub(x_offset), + view_opts.y_range.add(y_offset) + ); } - 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.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); + } + + if (signal.flags.contains(.left_released)) { + ctx.view_controls.pushBreakpoint(); + } + } else if (ctx.view_controls.selected_tool == .select) { + // TODO: } - if (signal.flags.contains(.middle_clicked)) { - ctx.view_controls.pushViewMove(view_id, view.available_x_range, view.available_y_range); - } - if (signal.flags.contains(.left_released)) { - ctx.view_controls.pushBreakpoint(); - } - - view.graph_cache.min_max_cache = app.getViewMinMaxCache(view_id); - - Graph.drawCached(&view.graph_cache, graph_box.persistent.size, view_opts.*, samples); - if (view.graph_cache.texture) |texture| { - graph_box.texture = texture.texture; + { // Render graph + view.graph_cache.min_max_cache = app.getViewMinMaxCache(view_id); + 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) { diff --git a/src/components/view_ruler.zig b/src/components/view_ruler.zig index ad3f9a5..75def9d 100644 --- a/src/components/view_ruler.zig +++ b/src/components/view_ruler.zig @@ -15,32 +15,42 @@ const assert = std.debug.assert; const ruler_size = UI.Sizing.initFixed(.{ .pixels = 32 }); +const Ruler = struct { + project: *App.Project, + box: *UI.Box, + graph_box: *UI.Box, + view_id: Id, + axis: UI.Axis, + + fn getBoxDrawContext(self: *const Ruler) DrawContext { + var draw_ctx = DrawContext.init(self.axis, self.project, self.view_id); + draw_ctx.rect = .{ + .x = 0, + .y = 0, + .width = self.box.persistent.size.x, + .height = self.box.persistent.size.y + }; + return draw_ctx; + } + + fn getGraphDrawContext(self: *const Ruler) DrawContext { + var draw_ctx = DrawContext.init(self.axis, self.project, self.view_id); + draw_ctx.rect = .{ + .x = 0, + .y = 0, + .width = self.graph_box.persistent.size.x, + .height = self.graph_box.persistent.size.y + }; + return draw_ctx; + } +}; + 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, @@ -62,7 +72,7 @@ const DrawContext = struct { }; } - fn getPoint(self: *DrawContext, along_axis_pos: f64, cross_axis_pos: f64) rl.Vector2 { + fn getPoint(self: *const 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); @@ -85,7 +95,7 @@ const DrawContext = struct { }; } - fn getLine(self: *DrawContext, along_axis_pos: f64, cross_axis_size: f64) rl.Rectangle { + fn getLine(self: *const 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)) @@ -99,7 +109,7 @@ const DrawContext = struct { ); } - fn getRect(self: *DrawContext, along_axis_pos: f64, along_axis_size: f64, cross_axis_pos: f64, cross_axis_size: f64) rl.Rectangle { + fn getRect(self: *const 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{ @@ -122,7 +132,7 @@ const DrawContext = struct { return rect; } - fn drawLine(self: *DrawContext, along_axis_pos: f64, cross_axis_size: f64, color: rl.Color) void { + fn drawLine(self: *const 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), @@ -130,7 +140,7 @@ const DrawContext = struct { ); } - fn drawTicks(self: *DrawContext, from: f64, to: f64, step: f64, line_size: f64, color: rl.Color) void { + fn drawTicks(self: *const 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); @@ -138,6 +148,26 @@ const DrawContext = struct { } }; +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; +} + fn drawRulerTicks(_ctx: ?*anyopaque, box: *UI.Box) void { const ctx: *DrawContext = @ptrCast(@alignCast(_ctx)); ctx.rect = box.rect(); @@ -192,27 +222,46 @@ fn showMouseTooltip(ctx: Context, axis: UI.Axis, view_id: Id, position: f64) voi } } +fn showMarkerLine(ui: *UI, ruler: Ruler, position: f64, color: rl.Color) void { + _ = ui.createBox(.{ + .background = color, + .float_rect = ruler.getBoxDrawContext().getLine(position, 1), + .float_relative_to = ruler.box, + .parent = ruler.box + }); + + _ = ui.createBox(.{ + .background = color, + .float_rect = ruler.getGraphDrawContext().getLine(position, 1), + .float_relative_to = ruler.graph_box, + .parent = ruler.graph_box + }); +} + +fn showMarkerRect(ui: *UI, ruler: Ruler, range: RangeF64, color: rl.Color, key: ?UI.Key) *UI.Box { + return ui.createBox(.{ + .key = key, + .background = color, + .float_relative_to = ruler.box, + .parent = ruler.box, + .float_rect = ruler.getBoxDrawContext().getRect(@min(range.lower, range.upper), range.size(), 0, 1), + }); +} + pub fn show(ctx: Context, box: *UI.Box, graph_box: *UI.Box, view_id: Id, axis: UI.Axis) !void { + const ruler = Ruler{ + .project = ctx.project, + .box = box, + .graph_box = graph_box, + .view_id = view_id, + .axis = axis + }; + 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(); @@ -229,44 +278,68 @@ pub fn show(ctx: Context, box: *UI.Box, graph_box: *UI.Box, view_id: Id, axis: U { // 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 hold_start: ?f64 = null; + if (ctx.view_controls.getCursorHoldStart(view_id, axis)) |hold_start_position| { + hold_start = hold_start_position; } var markers: std.BoundedArray(f64, 2) = .{}; - if (zoom_start != null) { - markers.appendAssumeCapacity(zoom_start.?); + if (hold_start != null) { + markers.appendAssumeCapacity(hold_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, - }); + const marker_color = srcery.green; - _ = ui.createBox(.{ - .background = srcery.green, - .float_rect = graph_ctx.getLine(marker_position, 1), - .float_relative_to = graph_box, - .parent = graph_box - }); + for (markers.constSlice()) |marker_position| { + showMarkerLine(ui, ruler, marker_position, marker_color); } - if (zoom_start != null and cursor != null) { - const zoom_end = cursor.?; + if (hold_start != null and cursor != null) { + _ = showMarkerRect(ui, ruler, RangeF64.init(hold_start.?, cursor.?), marker_color.alpha(0.5), null); + } - _ = 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), - }); + { + + var selected_range_iter = view.iterMarkedRanges(axis); + while (selected_range_iter.next()) |selected_range| { + var color = srcery.blue; + const index = selected_range_iter.index; + + if (ctx.view_controls.show_marked_range) |show_marked_range| { + if (show_marked_range.view_id.eql(view_id) and show_marked_range.index == index) { + if (@mod(rl.getTime(), 0.5) < 0.25) { + color = utils.shiftColorInHSV(color, 0.8); + } + } + } + + showMarkerLine(ui, ruler, selected_range.lower, color); + showMarkerLine(ui, ruler, selected_range.upper, color); + + var hasher = UI.Key.CombineHasher.init(); + hasher.update(std.mem.asBytes(&view_id)); + hasher.update(std.mem.asBytes(&axis)); + hasher.update(std.mem.asBytes(&index)); + const range_box_key = UI.Key.init(hasher.final()); + + var range_box = showMarkerRect(ui, ruler, selected_range, color.alpha(0.5), range_box_key); + range_box.flags.insert(.clickable); + range_box.flags.insert(.draw_hot); + range_box.flags.insert(.draw_active); + + range_box.hot_cursor = .mouse_cursor_pointing_hand; + if (ctx.view_controls.selected_tool == .select) { + const signal = ui.signal(range_box); + if (signal.clicked()) { + ctx.view_controls.toggleShownMarkedRange(view_id, index); + } + } + } } } @@ -294,42 +367,67 @@ pub fn show(ctx: Context, box: *UI.Box, graph_box: *UI.Box, view_id: Id, axis: U const cursor = ctx.view_controls.getCursor(view_id, axis); if (signal.flags.contains(.left_pressed)) { - ctx.view_controls.setZoomStart(view_id, axis, cursor); + ctx.view_controls.setCursorHoldStart(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; + if (ctx.view_controls.selected_tool == .move) { + 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); } - 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 (ctx.view_controls.getCursorHoldStart(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 (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(); + if (axis == .Y) { + new_view_range = new_view_range.flip(); + } + + ctx.view_controls.pushViewMoveAxis(view_id, axis, new_view_range); } - - ctx.view_controls.pushViewMoveAxis(view_id, axis, new_view_range); } } - ctx.view_controls.zoom_start = null; + } + } else if (ctx.view_controls.selected_tool == .select) { + // TODO: + + if (cursor != null) { + if (ctx.view_controls.getCursorHoldStart(view_id, axis)) |hold_start| { + const range = RangeF64.init( + @min(hold_start, cursor.?), + @max(hold_start, cursor.?), + ); + const hold_start_mouse = view_range.remapTo(mouse_range, range.lower); + const hold_end_mouse = view_range.remapTo(mouse_range, range.upper); + const mouse_move_distance = @abs(hold_end_mouse - hold_start_mouse); + if (signal.flags.contains(.left_released) and mouse_move_distance > 5) { + if (view.appendMarkedRange(axis, range)) |marked_range| { + marked_range.refresh(ctx.project.getViewSamples(view_id)); + } + + } + } } } + + if (signal.flags.contains(.left_released)) { + ctx.view_controls.setCursorHoldStart(view_id, axis, null); + } } \ No newline at end of file diff --git a/src/main.zig b/src/main.zig index 59e65ce..c062c30 100644 --- a/src/main.zig +++ b/src/main.zig @@ -135,23 +135,6 @@ pub fn main() !void { _ = try app.addView(.{ .file = try app.addFile("./samples-18m.bin") }); - - // _ = try app.addView(.{ - // .channel = try app.addChannel("Dev1/ai0") - // }); - // _ = try app.addView(.{ - // .channel = try app.addChannel("Dev3/ao0") - // }); - // _ = try app.addView(.{ - // .file = try app.addFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_I.bin") - // }); - - // try app.appendChannelFromDevice("Dev1/ai0"); - // try app.appendChannelFromDevice("Dev3/ao0"); - // try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_I.bin"); - // try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_IjStim.bin"); - // try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_IjStim.bin"); - // try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_IjStim.bin"); } var profiler: ?Profiler = null; diff --git a/src/screens/main_screen.zig b/src/screens/main_screen.zig index d4c925d..8df46ec 100644 --- a/src/screens/main_screen.zig +++ b/src/screens/main_screen.zig @@ -384,11 +384,149 @@ fn showViewSettings(self: *MainScreen, view_id: Id) !void { } } +fn showMarkedRange(self: *MainScreen, view_id: Id, index: usize) void { + var ui = &self.app.ui; + + const view = self.app.getView(view_id) orelse return; + + const marked_range = view.marked_ranges.get(index); + + + { + const label = ui.label("Selected range", .{}); + label.borders.bottom = .{ + .color = srcery.blue, + .size = 1 + }; + + _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) }); + } + + const sample_rate = self.app.project.getSampleRate(); + if (marked_range.axis == .X and sample_rate != null) { + _ = ui.label("From: {d:.3}s", .{ marked_range.range.lower / sample_rate.? }); + _ = ui.label("To: {d:.3}s", .{ marked_range.range.upper / sample_rate.? }); + _ = ui.label("Size: {d:.3}s", .{ marked_range.range.size() / sample_rate.? }); + } else { + _ = ui.label("From: {d:.2}", .{ marked_range.range.lower }); + _ = ui.label("To: {d:.2}", .{ marked_range.range.upper }); + _ = ui.label("Size: {d:.2}", .{ marked_range.range.size() }); + } + + if (marked_range.axis == .X) { + if (marked_range.min) |min| { + _ = ui.label("Minimum: {d:.3}", .{ min }); + } else{ + _ = ui.label("Minimum: ", .{}); + } + + if (marked_range.max) |max| { + _ = ui.label("Maximum: {d:.3}", .{ max }); + } else{ + _ = ui.label("Maximum: ", .{}); + } + + if (marked_range.average) |average| { + _ = ui.label("Average: {d:.3}", .{ average }); + } else{ + _ = ui.label("Average: ", .{}); + } + + if (marked_range.standard_deviation) |standard_deviation| { + _ = ui.label("Standard deviation: {d:.3}", .{ standard_deviation }); + } else{ + _ = ui.label("Standard deviation: ", .{}); + } + } + + _ = ui.createBox(.{ .size_y = UI.Sizing.initGrowFull() }); + + { + const btn = ui.textButton("Remove"); + btn.borders = UI.Borders.all(.{ .color = srcery.red, .size = 4 }); + const signal = ui.signal(btn); + if (signal.clicked() or ui.isKeyboardPressed(.key_backspace)) { + self.view_controls.show_marked_range = null; + _ = view.marked_ranges.swapRemove(index); + } + } +} + +fn showToolbar(self: *MainScreen) void { + var ui = &self.app.ui; + + const toolbar = ui.createBox(.{ + .background = srcery.black, + .layout_direction = .left_to_right, + .size_x = .{ .fixed = .{ .parent_percent = 1 } }, + .size_y = .{ .fixed = .{ .font_size = 2 } }, + .borders = .{ + .bottom = .{ .color = srcery.hard_black, .size = 4 } + } + }); + toolbar.beginChildren(); + defer toolbar.endChildren(); + + { + var btn = ui.textButton("Start/Stop button"); + btn.borders = UI.Borders.all(.{ .size = 4, .color = srcery.red }); + btn.background = srcery.black; + btn.size.y = UI.Sizing.initFixed(.{ .parent_percent = 1 }); + btn.padding.top = 0; + btn.padding.bottom = 0; + if (ui.signal(btn).clicked()) { + if (self.app.isCollectionInProgress()) { + self.app.pushCommand(.stop_collection); + } else { + self.app.pushCommand(.start_collection); + } + } + + if (self.app.isCollectionInProgress()) { + btn.setText("Stop"); + } else { + btn.setText("Start"); + } + } + + { + var btn = ui.textButton("Save"); + btn.borders = UI.Borders.all(.{ .size = 4, .color = srcery.green }); + if (ui.signal(btn).clicked()) { + self.app.pushCommand(.save_project); + } + } + + _ = ui.createBox(.{ .size_x = UI.Sizing.initFixedPixels(ui.rem(1)) }); + + { + var btn = ui.textButton("Move"); + if (self.view_controls.selected_tool == .move) { + btn.borders = UI.Borders.bottom(.{ .size = 4, .color = srcery.green }); + } + + if (ui.signal(btn).clicked() or ui.isKeyboardPressed(.key_one)) { + self.view_controls.selected_tool = .move; + } + } + + { + var btn = ui.textButton("Select"); + if (self.view_controls.selected_tool == .select) { + btn.borders = UI.Borders.bottom(.{ .size = 4, .color = srcery.green }); + } + + if (ui.signal(btn).clicked() or ui.isKeyboardPressed(.key_two)) { + self.view_controls.selected_tool = .select; + } + } +} + pub fn showSidePanel(self: *MainScreen) !void { var ui = &self.app.ui; const container = ui.createBox(.{ - .size_x = UI.Sizing.initGrowUpTo(.{ .parent_percent = 0.2 }), + .size_x = UI.Sizing.initFitChildren(), .size_y = UI.Sizing.initGrowFull(), .borders = .{ .right = .{ .color = srcery.hard_black, .size = 4 } @@ -400,7 +538,11 @@ pub fn showSidePanel(self: *MainScreen) !void { container.beginChildren(); defer container.endChildren(); - if (self.view_controls.view_settings) |view_id| { + _ = ui.createBox(.{ .size_x = UI.Sizing.initFixedPixels(ui.rem(12)) }); + + if (self.view_controls.show_marked_range) |show_marked_range| { + self.showMarkedRange(show_marked_range.view_id, show_marked_range.index); + } else if (self.view_controls.view_settings) |view_id| { try self.showViewSettings(view_id); } else { try self.showProjectSettings(); @@ -449,49 +591,7 @@ pub fn tick(self: *MainScreen) !void { maybe_modal_overlay = modal_overlay; } - { - const toolbar = ui.createBox(.{ - .background = srcery.black, - .layout_direction = .left_to_right, - .size_x = .{ .fixed = .{ .parent_percent = 1 } }, - .size_y = .{ .fixed = .{ .font_size = 2 } }, - .borders = .{ - .bottom = .{ .color = srcery.hard_black, .size = 4 } - } - }); - toolbar.beginChildren(); - defer toolbar.endChildren(); - - { - var btn = ui.textButton("Start/Stop button"); - btn.borders = UI.Borders.all(.{ .size = 4, .color = srcery.red }); - btn.background = srcery.black; - btn.size.y = UI.Sizing.initFixed(.{ .parent_percent = 1 }); - btn.padding.top = 0; - btn.padding.bottom = 0; - if (ui.signal(btn).clicked()) { - if (self.app.isCollectionInProgress()) { - self.app.pushCommand(.stop_collection); - } else { - self.app.pushCommand(.start_collection); - } - } - - if (self.app.isCollectionInProgress()) { - btn.setText("Stop"); - } else { - btn.setText("Start"); - } - } - - { - var btn = ui.textButton("Save"); - btn.borders = UI.Borders.all(.{ .size = 4, .color = srcery.green }); - if (ui.signal(btn).clicked()) { - self.app.pushCommand(.save_project); - } - } - } + self.showToolbar(); const ui_view_ctx = UIView.Context{ .app = self.app, @@ -561,6 +661,8 @@ pub fn tick(self: *MainScreen) !void { self.view_controls.view_fullscreen = null; } else if (self.view_controls.view_settings != null) { self.view_controls.view_settings = null; + } else if (self.view_controls.show_marked_range != null) { + self.view_controls.show_marked_range = null; } else { self.app.should_close = true; } diff --git a/src/ui.zig b/src/ui.zig index 5d825d3..e6c23cd 100644 --- a/src/ui.zig +++ b/src/ui.zig @@ -23,13 +23,22 @@ const Vec2Zero = Vec2{ .x = 0, .y = 0 }; const UI = @This(); const max_boxes = 512; -const max_events = 1024; +const max_events = 256; const draw_debug = false; //builtin.mode == .Debug; const default_font = Assets.FontId{ .variant = .regular, .size = 16 }; pub const Key = struct { + const StringHasher = std.hash.XxHash3; + pub const CombineHasher = std.hash.Fnv1a_64; + hash: u64 = 0, + pub fn init(hash: u64) Key { + return Key{ + .hash = hash + }; + } + pub fn initPtr(ptr: anytype) Key { return Key.initUsize(@intFromPtr(ptr)); } @@ -42,7 +51,17 @@ pub const Key = struct { pub fn initString(seed: u64, text: []const u8) Key { return Key{ - .hash = std.hash.XxHash3.hash(seed, text) + .hash = StringHasher.hash(seed, text) + }; + } + + pub fn combine(self: Key, other: Key) Key { + var hasher = CombineHasher.init(); + hasher.update(std.mem.asBytes(&self.hash)); + hasher.update(std.mem.asBytes(&other.hash)); + + return Key{ + .hash = hasher.final() }; } @@ -517,6 +536,8 @@ pub const Box = struct { visual_hot: bool = false, visual_active: bool = false, tooltip: ?[]const u8 = null, + float_x: ?f32 = null, + float_y: ?f32 = null, // Variables that you probably shouldn't be touching last_used_frame: u64 = 0, @@ -590,13 +611,11 @@ pub const Box = struct { } pub fn setFloatX(self: *Box, x: f32) void { - self.persistent.position.x = x; - self.flags.insert(.float_x); + self.float_x = x; } pub fn setFloatY(self: *Box, y: f32) void { - self.persistent.position.y = y; - self.flags.insert(.float_y); + self.float_y = y; } pub fn setFloatRect(self: *Box, float_rect: Rect) void { @@ -640,8 +659,8 @@ pub const Box = struct { fn isFloating(self: *const Box, axis: Axis) bool { return switch (axis) { - .X => self.flags.contains(.float_x), - .Y => self.flags.contains(.float_y), + .X => self.float_x != null, + .Y => self.float_y != null, }; } @@ -1017,11 +1036,11 @@ pub fn end(self: *UI) void { // Reset sizes and positions, because it will be recalculated in layout pass for (self.boxes.slice()) |*box| { var position = Vec2{ .x = 0, .y = 0 }; - if (box.isFloating(.X)) { - position.x = box.persistent.position.x; + if (box.float_x) |x| { + position.x = x; } - if (box.isFloating(.Y)) { - position.y = box.persistent.position.y; + if (box.float_y) |y| { + position.y = y; } box.persistent.size = Vec2Zero; box.persistent.position = position; @@ -2314,7 +2333,8 @@ pub fn textButton(self: *UI, text: []const u8) *Box { pub fn label(self: *UI, comptime fmt: []const u8, args: anytype) *Box { const box = self.createBox(.{ .size_x = Sizing.initFixed(.text), - .size_y = Sizing.initFixed(.text) + .size_y = Sizing.initFixed(.text), + .flags = &.{ .wrap_text } }); box.setFmtText(fmt, args); diff --git a/src/utils.zig b/src/utils.zig index 2006156..a5b2352 100644 --- a/src/utils.zig +++ b/src/utils.zig @@ -39,7 +39,9 @@ pub fn shiftColorInHSV(color: rl.Color, value_shift: f32) rl.Color { var hsv = rl.colorToHSV(color); hsv.z = std.math.clamp(hsv.z * (1 + value_shift), 0, 1); - return rl.colorFromHSV(hsv.x, hsv.y, hsv.z); + var new_color = rl.colorFromHSV(hsv.x, hsv.y, hsv.z); + new_color.a = color.a; + return new_color; } pub fn drawUnderline(rect: rl.Rectangle, size: f32, color: rl.Color) void {