From 0edeaefbf09a4541ba68ce8b3b646d12c90a66fb Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Mon, 26 May 2025 14:50:24 +0300 Subject: [PATCH] add gain change live updates --- src/app.zig | 581 +++++++++------------- src/components/systems/view_controls.zig | 51 +- src/components/view.zig | 19 +- src/components/view_ruler.zig | 162 +++--- src/graph.zig | 12 +- src/main.zig | 19 +- src/screens/main_screen.zig | 597 +++++++++++++---------- src/ui.zig | 13 +- 8 files changed, 753 insertions(+), 701 deletions(-) diff --git a/src/app.zig b/src/app.zig index 8cb1183..d1a869d 100644 --- a/src/app.zig +++ b/src/app.zig @@ -194,93 +194,125 @@ const SumRingBuffer = struct { } }; -pub const Transform = union(enum) { - sliding_window: f64, - multiply: f64, - addition: f64, - - pub const State = struct { - sum_ring_buffer: ?SumRingBuffer = null, - - pub fn init(arena: Allocator, transform: Transform) !State { - var sum_ring_buffer: ?SumRingBuffer = null; - if (transform == .sliding_window) { - const window_width: usize = @intFromFloat(@ceil(transform.sliding_window)); - sum_ring_buffer = SumRingBuffer.init(try arena.alloc(f64, window_width)); - } - - return State{ - .sum_ring_buffer = sum_ring_buffer - }; - } - - pub fn prepare(self: *State, transform: Transform, source: *SampleList, from_block_index: SampleList.Block.Id) void { - switch (transform) { - .sliding_window => |window_width_f64| { - const sum_ring_buffer = &(self.sum_ring_buffer.?); - const window_width: usize = @intFromFloat(@ceil(window_width_f64)); - - - const window_width_blocks: usize = @intFromFloat(@ceil(@as(f64, @floatFromInt(window_width)) / SampleList.Block.capacity)); - - for (0..window_width_blocks) |block_offset| { - const block_id = @as(isize, @intCast(from_block_index)) - @as(isize, @intCast(window_width_blocks - block_offset)); - if (block_id < 0) { - continue; - } - - const source_block = source.getBlock(@intCast(block_id)).?; - for (source_block.samplesSlice()) |sample| { - sum_ring_buffer.append(sample); - } - } - }, - else => {} - } - } +pub const Transform = struct { + const max_gain_changes = 32; + pub const GainChanges = std.BoundedArray(GainChange, max_gain_changes); + pub const GainChange = struct { + gain: f64, + sample: f64 }; - pub fn apply(self: Transform, state: *State, destination: []f64, source: []f64) void { - switch (self) { - .sliding_window => { - const sum_ring_buffer = &(state.sum_ring_buffer.?); + gain_changes: GainChanges = .{}, - if (sum_ring_buffer.len == 0) { - @memcpy(destination[0..source.len], source); - } else { - for (0..source.len) |i| { - const sample = source[i]; - sum_ring_buffer.append(sample); + pub fn applySlice(self: Transform, index: usize, source: []f64, destination: []f64) void { + assert(source.len == destination.len); + assert(source.len <= SampleList.Block.capacity); - destination[i] = sum_ring_buffer.sum / @as(f64, @floatFromInt(sum_ring_buffer.len)); - } - } + const gain_changes = self.gain_changes.constSlice(); - }, - .multiply => |scalar| { - for (0..source.len) |i| { - destination[i] = scalar * source[i]; - } - }, - .addition => |offset| { - for (0..source.len) |i| { - destination[i] = offset + source[i]; - } + const gain_segment = Transform.findGainSegment(gain_changes, index); + if (gain_segment.to > @as(f64, @floatFromInt(index+source.len))) { + for (0..source.len) |i| { + destination[i] = source[i] * (100 / gain_segment.gain); + } + + } else { + const gain_per_sample = Transform.createGainLookup(gain_changes, index, source.len); + + for (0..source.len) |i| { + destination[i] = source[i] * (100 / gain_per_sample[i]); } } } - pub fn eqlSlice(slice1: []const Transform, slice2: []const Transform) bool { - if (slice1.len != slice2.len) { - return false; + pub fn applyBlock(self: Transform, index: usize, source: *SampleList.Block, destination: *SampleList.Block) void { + destination.clear(); + destination.len = source.len; + + self.applySlice(index, source.samplesSlice(), destination.samplesSlice()); + destination.recomputeMinMax(); + } + + pub fn applySampleList(self: Transform, from_block_index: SampleList.Block.Id, block_count: usize, source: *SampleList, destination: *SampleList) void { + for (0..block_count) |block_offset| { + const block_id = from_block_index + block_offset; + + const source_block = source.getBlock(block_id).?; + const destination_block = destination.getBlock(block_id).?; + + self.applyBlock(block_id * SampleList.Block.capacity, source_block, destination_block); } - for (0.., slice1) |i, transform1| { - if (!std.meta.eql(transform1, slice2[i])) { - return false; + } + + const GainSegment = struct { + index: usize, + from: f64, + to: f64, + gain: f64 + }; + + pub fn findGainSegment(changes: []const GainChange, index: usize) GainSegment { + // TODO: + // Assumes that `changes` is sorted + assert(changes.len > 0); + assert(changes[0].sample == 0); + + const index_f64: f64 = @floatFromInt(index); + + for (0..(changes.len-1)) |i| { + if (changes[i].sample <= index_f64 and index_f64 <= changes[i+1].sample) { + return GainSegment{ + .index = i, + .from = changes[i].sample, + .to = changes[i+1].sample, + .gain = changes[i].gain + }; } } - return true; + return GainSegment{ + .index = changes.len-1, + .from = changes[changes.len-1].sample, + .to = std.math.inf(f64), + .gain = changes[changes.len-1].gain + }; + } + + pub fn createGainLookup(changes: []const GainChange, index: usize, size: usize) [SampleList.Block.capacity]f64 { + assert(size <= SampleList.Block.capacity); + + var result: [SampleList.Block.capacity]f64 = undefined; + var last_result_index: usize = 0; + + const from_index = findGainSegment(changes, index).index; + + for (from_index..changes.len-1) |i| { + const next_segment_index = @min(@as(usize, @intFromFloat(changes[i+1].sample)) - index, result.len); + @memset( + result[last_result_index..next_segment_index], + changes[i].gain + ); + + last_result_index = next_segment_index; + if (next_segment_index == result.len) { + break; + } + } + + if (last_result_index != result.len) { + @memset( + result[last_result_index..], + changes[changes.len-1].gain + ); + } + + return result; + } + + pub fn eql(self: Transform, other: Transform) bool { + _ = self; + _ = other; + return false; } }; @@ -461,6 +493,10 @@ pub const SampleList = struct { block.clear(); } + try self.ensureTotalBlocks(total_block_count); + } + + pub fn ensureTotalBlocks(self: *SampleList, total_block_count: usize) !void { while (self.blocks.items.len < total_block_count) { try self.appendEmptyBlock(); } @@ -541,60 +577,8 @@ pub const SampleList = struct { } } - pub fn applySlidingWindow(self: *SampleList, source: *SampleList, from_block_index: Block.Id, block_count: Block.Len, window_width: usize) !void { - try self.applyTransformations(source, from_block_index, block_count, &[_]Transform{ .{ .sliding_window = window_width } }); - } - - pub fn applyTransformations(self: *SampleList, source: *SampleList, from_block_index: Block.Id, block_count: Block.Len, transforms: []const Transform) !void { - var arena = std.heap.ArenaAllocator.init(self.arena.child_allocator); - defer arena.deinit(); - const allocator = arena.allocator(); - - var states = try allocator.alloc(Transform.State, transforms.len); - for (0..transforms.len) |i| { - states[i] = try Transform.State.init(allocator, transforms[i]); - } - - for (0..transforms.len) |i| { - states[i].prepare(transforms[i], source, from_block_index); - } - - for (0..block_count) |block_offset| { - const block_id = from_block_index + block_offset; - - const source_block = source.getBlock(block_id).?; - const self_block = self.getBlock(block_id).?; - self_block.clear(); - - var temp_buffer1: SampleList.Block.Buffer = undefined; - var temp_buffer2: SampleList.Block.Buffer = undefined; - - var front_buffer: []f64 = temp_buffer1[0..source_block.len]; - var back_buffer: []f64 = temp_buffer2[0..source_block.len]; - - for (0..transforms.len) |i| { - var source_samples: []f64 = undefined; - if (i == 0) { - source_samples = source_block.samplesSlice(); - } else { - source_samples = back_buffer; - } - - var destination_samples: []f64 = undefined; - if (i == transforms.len - 1) { - destination_samples = self_block.buffer[0..]; - } else { - destination_samples = front_buffer; - } - - transforms[i].apply(&states[i], destination_samples, source_samples); - - std.mem.swap([]f64, &front_buffer, &back_buffer); - } - - self_block.len = source_block.len; - self_block.recomputeMinMax(); - } + pub fn apply(self: *SampleList, source: *SampleList, from_block_index: Block.Id, block_count: usize, transform: Transform) void { + transform.applySampleList(from_block_index, block_count, source, self); } test { @@ -750,99 +734,6 @@ pub const File = struct { }; pub const View = struct { - pub const max_markers = 32; - pub const max_transforms = 16; - pub const BoundedTransformsArray = std.BoundedArray(Transform, max_transforms); - - 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, sample_list: *SampleList) void { - self.clear(); - - if (self.axis == .X) { - var sample_count: f64 = 0; - var sum: f64 = 0; - var min = std.math.inf(f64); - var max = -std.math.inf(f64); - - const from_sample: usize = @intFromFloat(@floor(std.math.clamp(self.range.lower, 0, @as(f64, @floatFromInt(sample_list.getLength()))))); - const to_sample: usize = @intFromFloat(@ceil(std.math.clamp(self.range.upper, 0, @as(f64, @floatFromInt(sample_list.getLength()))))); - - { - var iter = sample_list.iterator(from_sample, to_sample); - while (iter.next()) |segment| { - sample_count += @floatFromInt(segment.len); - - for (segment) |sample| { - min = @min(min, sample); - max = @max(max, sample); - sum += sample; - } - } - } - - if (sample_count > 0) { - const average = sum / sample_count; - - self.min = min; - self.max = max; - self.average = average; - - if (sample_count > 1) { - var standard_deviation: f64 = 0; - var iter = sample_list.iterator(from_sample, to_sample); - while (iter.next()) |segment| { - for (segment) |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 @@ -854,9 +745,6 @@ pub const View = struct { // TODO: Implement different styles of following: Look ahead, sliding, sliding window follow: bool = false, graph_opts: Graph.ViewOptions = .{}, - marked_ranges: std.BoundedArray(MarkedRange, 32) = .{}, - markers: std.BoundedArray(f64, max_markers) = .{}, - transforms: BoundedTransformsArray = .{}, name: std.BoundedArray(u8, 128) = .{}, // Runtime @@ -865,8 +753,9 @@ pub const View = struct { available_y_range: RangeF64 = RangeF64.init(0, 0), unit: ?NIDaq.Unit = .Voltage, - transformed_samples: ?Id = null, - computed_transforms: BoundedTransformsArray = .{}, + transformed_samples: Id, + computed_transform: Transform = .{}, + invalidated_transform_ranges: std.BoundedArray(RangeF64, 16) = .{}, pub fn clear(self: *View) void { self.graph_cache.clear(); @@ -887,13 +776,6 @@ pub const View = struct { .Y => self.available_y_range }; } - - pub fn iterMarkedRanges(self: *View, axis: UI.Axis) MarkedRangeIterator { - return MarkedRangeIterator{ - .view = self, - .axis = axis - }; - } }; pub const Project = struct { @@ -913,6 +795,7 @@ pub const Project = struct { export_location: ?[]u8 = null, solutions: std.ArrayListUnmanaged(Solution) = .{}, + gain_changes: Transform.GainChanges = .{}, sample_lists: GenerationalArray(SampleList) = .{}, channels: GenerationalArray(Channel) = .{}, @@ -951,7 +834,7 @@ pub const Project = struct { return result_range; } - pub fn getViewSampleListId(self: *Project, view_id: Id) Id { + pub fn getViewReferenceSampleListId(self: *Project, view_id: Id) Id { const view = self.views.get(view_id).?; switch (view.reference) { @@ -976,10 +859,9 @@ pub const Project = struct { @floatFromInt(sample_list.getLength()), @floatFromInt(sample_list.getLength() + samples.len) ); + _ = affected_range; try sample_list.append(samples); - - self.refreshMarkedRanges(sample_list_id, affected_range); } pub fn clearSamples(self: *Project, allocator: Allocator, sample_list_id: Id) !void { @@ -989,10 +871,10 @@ pub const Project = struct { 0, @floatFromInt(sample_list.getLength()) ); + _ = affected_range; sample_list.clear(allocator); - self.refreshMarkedRanges(sample_list_id, affected_range); } pub fn addSampleList(self: *Project, allocator: Allocator) !Id { @@ -1005,32 +887,6 @@ pub const Project = struct { self.sample_lists.remove(sample_list_id); } - fn refreshMarkedRanges(self: *Project, sample_list_id: Id, affected_range: RangeF64) void { - const sample_list = self.sample_lists.get(sample_list_id).?; - _ = sample_list; - _ = affected_range; - - // const total_samples = sample_list.samples.items; - - // var view_iter = self.views.idIterator(); - // while (view_iter.next()) |view_id| { - // const view_sample_list_id = self.getViewSampleList(view_id); - // if (!view_sample_list_id.eql(sample_list_id)) { - // continue; - // } - - // const view = self.views.get(view_id).?; - // const marked_ranges: []View.MarkedRange = view.marked_ranges.slice(); - // for (marked_ranges) |*marked_range| { - // if (marked_range.axis != .X) continue; - - // if (affected_range.intersectPositive(marked_range.range).isPositive()) { - // marked_range.refresh(total_samples); - // } - // } - // } - } - pub fn readSamplesFromFile(self: *Project, allocator: Allocator, sample_list_id: Id, file: std.fs.File, sample_count: usize) !void { var bytes_left: usize = sample_count * 8; var buffer: [SampleList.Block.capacity * 8]u8 = undefined; @@ -1045,27 +901,6 @@ pub const Project = struct { } } - pub fn appendMarkedRange(self: *Project, view_id: Id, axis: UI.Axis, range: RangeF64) ?*View.MarkedRange { - const view = self.views.get(view_id) orelse return null; - - if (view.marked_ranges.unusedCapacitySlice().len == 0) { - return null; - } - - const marked_range = view.marked_ranges.addOneAssumeCapacity(); - marked_range.* = View.MarkedRange{ - .axis = axis, - .range = range - }; - - const sample_list_id = self.getViewSampleListId(view_id); - if (self.sample_lists.get(sample_list_id)) |sample_list| { - marked_range.refresh(sample_list); - } - - return marked_range; - } - pub fn getSampleTimestamp(self: *Project) ?u64 { var channel_iter = self.channels.iterator(); while (channel_iter.next()) |channel| { @@ -1108,9 +943,7 @@ pub const Project = struct { pub fn removeView(self: *Project, allocator: std.mem.Allocator, view_id: Id) void { const view = self.views.get(view_id) orelse return; - if (view.transformed_samples) |sample_list_id| { - self.removeSampleList(sample_list_id); - } + self.removeSampleList(view.transformed_samples); switch (view.reference) { .file => |file_id| { @@ -1125,6 +958,13 @@ pub const Project = struct { view.* = undefined; } + pub fn init(self: *Project, allocator: std.mem.Allocator) !void { + self.* = Project{}; + _ = allocator; + + try self.gain_changes.append(.{ .gain = 100, .sample = 0 }); + } + pub fn deinit(self: *Project, allocator: Allocator) void { var file_iter = self.files.iterator(); while (file_iter.next()) |file| { @@ -1146,37 +986,50 @@ pub const Project = struct { } self.sample_lists.clear(); - - if (self.solutions.capacity > 0) { - for (self.solutions.items) |solution| { - allocator.free(solution.description); - } - self.solutions.deinit(allocator); + for (self.solutions.items) |solution| { + allocator.free(solution.description); } + self.solutions.deinit(allocator); - if (self.pipete_solution.capacity > 0) { - self.pipete_solution.deinit(allocator); - } - - if (self.notes.capacity > 0) { - self.notes.deinit(allocator); - } + self.pipete_solution.deinit(allocator); + self.notes.deinit(allocator); if (self.export_location) |export_location| { allocator.free(export_location); } - if (self.statistic_points.items.len > 0) { - self.statistic_points.deinit(allocator); + self.statistic_points.deinit(allocator); + self.experiment_name.deinit(allocator); + } + + pub fn getViewTransform(self: *Project, view_id: Id) Transform { + _ = view_id; + return Transform{ + .gain_changes = self.gain_changes + }; + } + + pub fn removeGainChange(self: *Project, index: usize) void { + const gain_change = self.gain_changes.orderedRemove(index); + + const invalidate_lower_bound = gain_change.sample; + var invalidate_upper_bound = std.math.inf(f64); + if (index < self.gain_changes.len) { + invalidate_upper_bound = self.gain_changes.get(index).sample; } - self.experiment_name.deinit(allocator); + const invalidate_range = RangeF64.init(invalidate_lower_bound, invalidate_upper_bound); + + var view_iter = self.views.iterator(); + while (view_iter.next()) |view| { + view.invalidated_transform_ranges.append(invalidate_range) catch continue; + } } // ------------------- Serialization ------------------ // pub fn initFromFile(self: *Project, allocator: Allocator, f: std.fs.File) !void { - self.* = .{}; + try Project.init(self, allocator); errdefer self.deinit(allocator); const reader = f.reader(); @@ -1269,9 +1122,13 @@ pub const Project = struct { return error.InvalidReferenceTag; } + const sample_list_id = try self.addSampleList(allocator); + errdefer self.removeSampleList(sample_list_id); + const view = self.views.get(id).?; view.* = View{ .reference = reference, + .transformed_samples = sample_list_id }; } } @@ -1439,7 +1296,7 @@ const WorkJob = struct { view_id: Id, stage: Stage = .init, - transforms: View.BoundedTransformsArray = .{}, + transform: Transform = .{}, mutex: std.Thread.Mutex = .{}, running_thread_jobs: std.ArrayListUnmanaged(SampleList.Block.Id) = .{}, @@ -1488,33 +1345,20 @@ const WorkJob = struct { const project = &app.project; const view = project.views.get(self.view_id) orelse return true; - const sample_list_id = project.getViewSampleListId(self.view_id); + const sample_list_id = project.getViewReferenceSampleListId(self.view_id); const sample_list = project.sample_lists.get(sample_list_id).?; - const transforms = self.transforms.constSlice(); - if (!Transform.eqlSlice(view.transforms.constSlice(), transforms)) { + if (!Transform.eql(project.getViewTransform(self.view_id), self.transform)) { // Transforms changed, job needs to be cancelled. return true; } switch (self.stage) { .init => { - if (transforms.len == 0) { - if (view.transformed_samples) |transformed_samples_id| { - project.removeSampleList(transformed_samples_id); - } - view.transformed_samples = null; + const transformed_samples = project.sample_lists.get(view.transformed_samples).?; + try transformed_samples.reserveEmptyBlocks(sample_list.blocks.items.len); - return true; - } else { - if (view.transformed_samples == null) { - view.transformed_samples = try project.addSampleList(app.allocator); - } - const transformed_samples = project.sample_lists.get(view.transformed_samples.?).?; - try transformed_samples.reserveEmptyBlocks(sample_list.blocks.items.len); - - self.stage = .launch_threads; - } + self.stage = .launch_threads; }, .launch_threads => { const max_block_to_process = 256; @@ -1548,15 +1392,12 @@ const WorkJob = struct { // } const view = project.views.get(self.view_id) orelse return; - const transformed_samples_id = view.transformed_samples orelse return; - const transformed_samples = project.sample_lists.get(transformed_samples_id) orelse return; + const transformed_samples = project.sample_lists.get(view.transformed_samples) orelse return; - const sample_list_id = project.getViewSampleListId(self.view_id); + const sample_list_id = project.getViewReferenceSampleListId(self.view_id); const sample_list = project.sample_lists.get(sample_list_id) orelse return; - transformed_samples.applyTransformations(sample_list, from_block_id, block_count, self.transforms.constSlice()) catch |e| { - log.err("Failed to compute sliding window: {}", .{e}); - }; + transformed_samples.apply(sample_list, from_block_id, block_count, self.transform); } }; @@ -1572,7 +1413,7 @@ screen: enum { } = .main, main_screen: MainScreen, channel_from_device: ChannelFromDeviceScreen, -project: Project = .{}, +project: Project, collection_mutex: std.Thread.Mutex = .{}, collection_samples_mutex: std.Thread.Mutex = .{}, @@ -1594,8 +1435,10 @@ pub fn init(self: *App, allocator: Allocator) !void { .main_screen = undefined, .collection_thread = undefined, .channel_from_device = undefined, - .work_thread_pool = undefined + .work_thread_pool = undefined, + .project = undefined }; + try Project.init(&self.project, allocator); try self.initUI(); try self.work_thread_pool.init(.{ .allocator = allocator @@ -1689,9 +1532,8 @@ pub fn loadProject(self: *App, file: std.fs.File) !void { var loaded = try self.allocator.create(Project); defer self.allocator.destroy(loaded); - loaded.* = .{}; - errdefer loaded.deinit(self.allocator); try loaded.initFromFile(self.allocator, file); + errdefer loaded.deinit(self.allocator); self.deinitUI(); self.deinitProject(); @@ -1889,6 +1731,40 @@ pub fn tick(self: *App) !void { } try self.showUI(); + + { + const block_size = SampleList.Block.capacity; + + var view_iter = self.project.views.idIterator(); + while (view_iter.next()) |view_id| { + const view = self.getView(view_id).?; + + const referenced_samples_id = self.project.getViewReferenceSampleListId(view_id); + const referenced_samples = self.project.sample_lists.get(referenced_samples_id).?; + const computed_samples_id = view.transformed_samples; + const computed_samples = self.project.sample_lists.get(computed_samples_id).?; + + const transform = self.project.getViewTransform(view_id); + const computed_len = computed_samples.getLength(); + const reference_len = referenced_samples.getLength(); + + if (reference_len > computed_len) { + view.invalidated_transform_ranges.append(RangeF64.init(@floatFromInt(computed_len), @floatFromInt(reference_len))) catch {}; + } + + for (view.invalidated_transform_ranges.constSlice()) |range| { + const from_block: usize = @intFromFloat(@divFloor(range.lower, block_size)); + const to_block: usize = @intFromFloat(@divFloor(@min(range.upper, @as(f64, @floatFromInt(reference_len)) - 1), block_size)); + const block_count = to_block - from_block + 1; + + try computed_samples.ensureTotalBlocks(from_block + block_count); + + transform.applySampleList(@intCast(from_block), block_count, referenced_samples, computed_samples); + view.graph_cache.invalidateRange(range); + } + view.invalidated_transform_ranges.len = 0; + } + } } rl.clearBackground(srcery.black); @@ -1957,18 +1833,20 @@ pub fn tick(self: *App) !void { var view_iter = self.project.views.idIterator(); while (view_iter.next()) |view_id| { const view = self.project.views.get(view_id).?; + _ = view; - if (Transform.eqlSlice(view.computed_transforms.constSlice(), view.transforms.constSlice())) { - continue; - } + // TODO: + // if (Transform.eqlSlice(view.computed_transforms.constSlice(), view.transforms.constSlice())) { + // continue; + // } - _ = try self.work_jobs.insert(WorkJob{ - .transforms = view.transforms, - .view_id = view_id - }); + // _ = try self.work_jobs.insert(WorkJob{ + // .transforms = view.transforms, + // .view_id = view_id + // }); - view.computed_transforms.len = 0; - view.computed_transforms.appendSliceAssumeCapacity(view.transforms.constSlice()); + // view.computed_transforms.len = 0; + // view.computed_transforms.appendSliceAssumeCapacity(view.transforms.constSlice()); } } @@ -2087,7 +1965,7 @@ fn startCollection(self: *App) !void { self.collection_condition.signal(); } -fn stopCollection(self: *App) void { +pub fn stopCollection(self: *App) void { if (!self.isCollectionInProgress()) { return; } @@ -2140,7 +2018,7 @@ fn startOutput(self: *App, channel_id: Id) !void { channel.output_task = task; } -fn stopOutput(self: *App, channel_id: Id) void { +pub fn stopOutput(self: *App, channel_id: Id) void { const channel = self.getChannel(channel_id) orelse return; if (channel.output_task == null) { @@ -2167,6 +2045,13 @@ pub fn isOutputingInProgress(self: *App) bool { return false; } +pub fn stopAllOutput(self: *App) void { + var channel_iter = self.project.channels.idIterator(); + while (channel_iter.next()) |channel_id| { + self.stopOutput(channel_id); + } +} + fn isNiDaqInUse(self: *App) bool { return self.isCollectionInProgress() or self.isOutputingInProgress(); } @@ -2371,9 +2256,13 @@ pub fn addView(self: *App, reference: View.Reference) !Id { const id = try self.project.views.insertUndefined(); errdefer self.project.views.remove(id); + const sample_list_id = try self.project.addSampleList(self.allocator); + errdefer self.project.removeSampleList(sample_list_id); + const view = self.project.views.get(id).?; view.* = View{ - .reference = reference + .reference = reference, + .transformed_samples = sample_list_id }; self.loadView(id) catch |e| { @@ -2385,7 +2274,7 @@ pub fn addView(self: *App, reference: View.Reference) !Id { fn refreshViewAvailableXYRanges(self: *App, id: Id) void { const view = self.getView(id) orelse return; - const sample_list_id = self.project.getViewSampleListId(id); + const sample_list_id = self.project.getViewReferenceSampleListId(id); const sample_list = self.project.sample_lists.get(sample_list_id).?; view.available_x_range = RangeF64.init(0, @floatFromInt(sample_list.getLength())); diff --git a/src/components/systems/view_controls.zig b/src/components/systems/view_controls.zig index 788a698..063ccf9 100644 --- a/src/components/systems/view_controls.zig +++ b/src/components/systems/view_controls.zig @@ -153,6 +153,7 @@ const MarkedRangeIterator = struct { } }; +app: *App, project: *App.Project, // TODO: Redo @@ -160,21 +161,13 @@ undo_stack: CommandFrameArray = .{}, last_applied_command: usize = 0, zoom_start: ?ViewAxisPosition = null, cursor: ?ViewAxisPosition = null, -view_settings: ?Id = null, // View id view_protocol_modal: ?Id = null, // View id -selected_tool: enum { move, select, marker } = .move, -show_marked_range: ?struct { - view_id: Id, - index: usize, -} = null, -show_marker: ?struct { - view_id: Id, - index: usize -} = null, +// selected_tool: enum { move, select, marker } = .move, -pub fn init(project: *App.Project) System { +pub fn init(app: *App) System { return System{ - .project = project + .app = app, + .project = &app.project }; } @@ -293,14 +286,15 @@ pub fn applyCommands(self: *System) void { pub fn toggleViewSettings(self: *System, view_id: Id) void { if (self.isViewSettingsOpen(view_id)) { - self.view_settings = null; + self.app.main_screen.side_panel = .project; } else { - self.view_settings = view_id; + self.app.main_screen.side_panel = .{ .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); + const side_panel = self.app.main_screen.side_panel; + return side_panel == .view_settings and side_panel.view_settings.eql(view_id); } pub fn setCursor(self: *System, view_id: Id, axis: UI.Axis, position: ?f64) void { @@ -320,29 +314,36 @@ pub fn getCursorHoldStart(self: *System, view_id: Id, axis: UI.Axis) ?f64 { } pub fn toggleShownMarkedRange(self: *System, view_id: Id, index: usize) void { - if (self.show_marked_range) |show_marked_range| { + const side_panel = &self.app.main_screen.side_panel; + + if (side_panel.* == .marked_range) { + const show_marked_range = side_panel.marked_range; if (show_marked_range.view_id.eql(view_id) and show_marked_range.index == index) { - self.show_marked_range = null; + side_panel.* = .project; return; } } - self.show_marked_range = .{ - .view_id = view_id, - .index = index, + side_panel.* = .{ + .marked_range = .{ .index = index, .view_id = view_id } }; } pub fn toggleShownMarker(self: *System, view_id: Id, index: usize) void { - if (self.show_marker) |show_marker| { + const side_panel = &self.app.main_screen.side_panel; + + if (side_panel.* == .marker) { + const show_marker = side_panel.marker; if (show_marker.view_id.eql(view_id) and show_marker.index == index) { - self.show_marker = null; + side_panel.* = .project; return; } } - self.show_marker = .{ - .view_id = view_id, - .index = index, + side_panel.* = .{ + .marker = .{ + .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 d856c89..99b700d 100644 --- a/src/components/view.zig +++ b/src/components/view.zig @@ -73,7 +73,7 @@ fn showGraph(ctx: Context, graph_box: *UI.Box, view_id: Id) void { sample_value_under_mouse = mouse_y_range.remapTo(view_opts.y_range, signal.relative_mouse.y); } - if (ctx.view_controls.selected_tool == .move) { + // 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); @@ -107,21 +107,16 @@ fn showGraph(ctx: Context, graph_box: *UI.Box, view_id: Id) void { if (signal.flags.contains(.left_released)) { ctx.view_controls.pushBreakpoint(); } - } else if (ctx.view_controls.selected_tool == .select) { - // TODO: + // } else if (ctx.view_controls.selected_tool == .select) { + // // TODO: - } else if (ctx.view_controls.selected_tool == .marker) { - // TODO: - } + // } else if (ctx.view_controls.selected_tool == .marker) { + // // TODO: + // } { // Render graph - var sample_list_id = app.project.getViewSampleListId(view_id); - if (view.transformed_samples) |transformed_samples_id| { - sample_list_id = transformed_samples_id; - } - - const sample_list = app.project.sample_lists.get(sample_list_id).?; + const sample_list = app.project.sample_lists.get(view.transformed_samples).?; Graph.drawCached(&view.graph_cache, graph_box.persistent.size, view_opts.*, sample_list); if (view.graph_cache.texture) |texture| { graph_box.texture = texture.texture; diff --git a/src/components/view_ruler.zig b/src/components/view_ruler.zig index b91521c..f7053c7 100644 --- a/src/components/view_ruler.zig +++ b/src/components/view_ruler.zig @@ -329,54 +329,87 @@ pub fn show(ctx: Context, box: *UI.Box, graph_box: *UI.Box, view_id: Id, axis: U _ = showMarkerRect(ui, ruler, RangeF64.init(hold_start.?, cursor.?), marker_color.alpha(0.5), null); } - { - var selected_range_iter = view.iterMarkedRanges(axis); - while (selected_range_iter.next()) |selected_range| { - var color = srcery.blue; - const index = selected_range_iter.index; + // { + // 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); - } - } - } + // const side_panel = ctx.view_controls.app.main_screen.side_panel; + // if (side_panel == .marked_range) { + // const show_marked_range = side_panel.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); + // 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("Marked ranges")); - 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 hasher = UI.Key.CombineHasher.init(); + // hasher.update(std.mem.asBytes("Marked ranges")); + // 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); + // 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); - } - } - } - } + // 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); + // } + // } + // } + // } if (axis == .X) { - for (0.., view.markers.constSlice()) |i, marker| { - const color = srcery.cyan; + // for (0.., view.markers.constSlice()) |i, marker| { + // const color = srcery.cyan; - showMarkerLine(ui, ruler, marker, color); - showMarkerLine(ui, ruler, marker, color); + // showMarkerLine(ui, ruler, marker, color); + // showMarkerLine(ui, ruler, marker, color); + + // var hasher = UI.Key.CombineHasher.init(); + // hasher.update(std.mem.asBytes("Markers")); + // hasher.update(std.mem.asBytes(&view_id)); + // hasher.update(std.mem.asBytes(&axis)); + // hasher.update(std.mem.asBytes(&i)); + + // const view_size = view.graph_opts.x_range.size(); + // const clickable_width = view_size * 0.01; + + // const clickable = ui.createBox(.{ + // .key = UI.Key.init(hasher.final()), + // .float_rect = ruler.getGraphDrawContext().getRect(marker - clickable_width/2, clickable_width, 0, 1), + // .float_relative_to = ruler.graph_box, + // .parent = ruler.graph_box, + // .flags = &.{ .draw_hot, .draw_active, .clickable }, + // .hot_cursor = .mouse_cursor_pointing_hand, + // }); + + // if (ui.signal(clickable).clicked()) { + // ctx.view_controls.toggleShownMarker(view_id, i); + // } + // } + + for (0.., project.gain_changes.slice()) |i, gain_change| { + const color = srcery.bright_orange; + + const sample = gain_change.sample; + + showMarkerLine(ui, ruler, sample, color); + showMarkerLine(ui, ruler, sample, color); var hasher = UI.Key.CombineHasher.init(); - hasher.update(std.mem.asBytes("Markers")); + hasher.update(std.mem.asBytes("Gain change")); hasher.update(std.mem.asBytes(&view_id)); hasher.update(std.mem.asBytes(&axis)); hasher.update(std.mem.asBytes(&i)); @@ -386,15 +419,20 @@ pub fn show(ctx: Context, box: *UI.Box, graph_box: *UI.Box, view_id: Id, axis: U const clickable = ui.createBox(.{ .key = UI.Key.init(hasher.final()), - .float_rect = ruler.getGraphDrawContext().getRect(marker - clickable_width/2, clickable_width, 0, 1), + .float_rect = ruler.getGraphDrawContext().getRect(sample - clickable_width/2, clickable_width, 0, 1), .float_relative_to = ruler.graph_box, .parent = ruler.graph_box, .flags = &.{ .draw_hot, .draw_active, .clickable }, .hot_cursor = .mouse_cursor_pointing_hand, }); - if (ui.signal(clickable).clicked()) { - ctx.view_controls.toggleShownMarker(view_id, i); + const signal = ui.signal(clickable); + if (signal.hot) { + const mouse_tooltip = ui.mouseTooltip(); + mouse_tooltip.beginChildren(); + defer mouse_tooltip.endChildren(); + + _ = ui.label("Gain: {d:.3}", .{ gain_change.gain }); } } } @@ -427,7 +465,7 @@ pub fn show(ctx: Context, box: *UI.Box, graph_box: *UI.Box, view_id: Id, axis: U ctx.view_controls.setCursorHoldStart(view_id, axis, cursor); } - if (ctx.view_controls.selected_tool == .move) { + // if (ctx.view_controls.selected_tool == .move) { if (signal.scrolled() and cursor != null) { var scale_factor: f64 = 1; if (signal.scroll.y > 0) { @@ -462,27 +500,27 @@ pub fn show(ctx: Context, box: *UI.Box, graph_box: *UI.Box, view_id: Id, axis: U } } } - } else if (ctx.view_controls.selected_tool == .select) { + // } else if (ctx.view_controls.selected_tool == .select) { - 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) { - _ = ctx.project.appendMarkedRange(view_id, axis, range); - } - } - } - } else if (ctx.view_controls.selected_tool == .marker) { - if (cursor != null and signal.flags.contains(.left_released) and axis == .X) { - view.markers.append(cursor.?) catch {}; - } - } + // 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) { + // _ = ctx.project.appendMarkedRange(view_id, axis, range); + // } + // } + // } + // } else if (ctx.view_controls.selected_tool == .marker) { + // if (cursor != null and signal.flags.contains(.left_released) and axis == .X) { + // view.markers.append(cursor.?) catch {}; + // } + // } if (signal.flags.contains(.left_released)) { ctx.view_controls.setCursorHoldStart(view_id, axis, null); diff --git a/src/graph.zig b/src/graph.zig index 86d1e2f..796d06e 100644 --- a/src/graph.zig +++ b/src/graph.zig @@ -30,7 +30,7 @@ pub const ViewOptions = struct { pub const RenderCache = struct { const Key = struct { options: ViewOptions, - drawn_x_range: RangeF64 + drawn_x_range: RangeF64, }; texture: ?rl.RenderTexture2D = null, @@ -48,6 +48,14 @@ pub const RenderCache = struct { self.key = null; } + pub fn invalidateRange(self: *RenderCache, x_range: RangeF64) void { + if (self.key) |key| { + if (key.drawn_x_range.intersectPositive(x_range).isPositive()) { + self.invalidate(); + } + } + } + pub fn draw(self: RenderCache, rect: rl.Rectangle) void { if (self.texture) |texture| { const source = rl.Rectangle{ @@ -323,7 +331,7 @@ pub fn drawCached(cache: *RenderCache, render_size: Vec2, options: ViewOptions, const cache_key = RenderCache.Key{ .options = options, - .drawn_x_range = RangeF64.init(0, @max(@as(f64, @floatFromInt(sample_list.getLength())) - 1, 0)).intersectPositive(options.x_range) + .drawn_x_range = RangeF64.init(0, @max(@as(f64, @floatFromInt(sample_list.getLength())) - 1, 0)).intersectPositive(options.x_range), }; if (cache.key != null and std.meta.eql(cache.key.?, cache_key)) { diff --git a/src/main.zig b/src/main.zig index 08427ac..a1cffad 100644 --- a/src/main.zig +++ b/src/main.zig @@ -119,12 +119,12 @@ pub fn main() !void { if (app_config_dir.openFile("config.bin", .{})) |save_file| { defer save_file.close(); - _ = try app.addView(.{ - .file = try app.addFile("./samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_I.bin") - }); - // app.loadProject(save_file) catch |e| { - // log.err("Failed to load project: {}", .{e}); - // }; + app.loadProject(save_file) catch |e| { + log.err("Failed to load project: {}", .{e}); + if (@errorReturnTrace()) |stack_trace| { + std.debug.dumpStackTrace(stack_trace.*); + } + }; } else |e| switch (e) { error.FileNotFound => {}, else => return e @@ -178,6 +178,13 @@ pub fn main() !void { } { + if (app.isCollectionInProgress()) { + app.stopCollection(); + } + if (app.isOutputingInProgress()) { + app.stopAllOutput(); + } + const save_file = try app_config_dir.createFile("config.bin", .{}); defer save_file.close(); try app.saveProject(save_file); diff --git a/src/screens/main_screen.zig b/src/screens/main_screen.zig index fd37e5d..c59c17c 100644 --- a/src/screens/main_screen.zig +++ b/src/screens/main_screen.zig @@ -22,8 +22,22 @@ const Id = App.Id; app: *App, view_controls: ViewControlsSystem, -view_solutions: bool = false, -view_statistics_points: bool = false, +side_panel: union(enum) { + project, + solutions, + gain_changes, + statistics_points, + view_settings: Id, + // marker: struct { + // view_id: Id, + // index: usize + // }, + // marked_range: struct { + // view_id: Id, + // index: usize, + // } +} = .project, + modal: ?union(enum){ view_protocol: Id, // View id notes @@ -42,6 +56,7 @@ notes_storage: UI.TextInputStorage, // Project settings solution_input: UI.TextInputStorage, +gain_input: UI.TextInputStorage, sample_rate_input: UI.TextInputStorage, experiment_name: UI.TextInputStorage, pipete_solution: UI.TextInputStorage, @@ -50,7 +65,6 @@ parsed_sample_rate: ?f64 = null, // View settings prev_view_settings: ?Id = null, // View ID -transform_inputs: [App.View.max_transforms]UI.TextInputStorage, view_name_input: UI.TextInputStorage, channel_save_file_picker: ?Platform.FilePickerId = null, file_save_file_picker: ?Platform.FilePickerId = null, @@ -58,11 +72,6 @@ file_save_file_picker: ?Platform.FilePickerId = null, pub fn init(app: *App) !MainScreen { const allocator = app.allocator; - var transform_inputs: [App.View.max_transforms]UI.TextInputStorage = undefined; - for (&transform_inputs) |*input| { - input.* = UI.TextInputStorage.init(allocator); - } - var self = MainScreen{ .app = app, .frequency_input = UI.TextInputStorage.init(allocator), @@ -73,8 +82,8 @@ pub fn init(app: *App) !MainScreen { .experiment_name = UI.TextInputStorage.init(allocator), .pipete_solution = UI.TextInputStorage.init(allocator), .view_name_input = UI.TextInputStorage.init(allocator), - .view_controls = ViewControlsSystem.init(&app.project), - .transform_inputs = transform_inputs, + .gain_input = UI.TextInputStorage.init(allocator), + .view_controls = ViewControlsSystem.init(app), .preview_sample_list_id = try app.project.addSampleList(allocator) }; @@ -93,9 +102,7 @@ pub fn deinit(self: *MainScreen) void { self.experiment_name.deinit(); self.pipete_solution.deinit(); self.view_name_input.deinit(); - for (self.transform_inputs) |input| { - input.deinit(); - } + self.gain_input.deinit(); self.app.project.removeSampleList(self.preview_sample_list_id); self.clearProtocolErrorMessage(); @@ -376,6 +383,7 @@ fn showProjectSettings(self: *MainScreen) !void { { _ = ui.label("Solution:", .{}); + try ui.textInput(.{ .key = ui.keyFromString("Solution input"), .storage = &self.solution_input, @@ -407,7 +415,7 @@ fn showProjectSettings(self: *MainScreen) !void { const btn = ui.textButton("View solutions"); btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 }); if (ui.signal(btn).clicked()) { - self.view_solutions = true; + self.side_panel = .solutions; } } } @@ -416,7 +424,36 @@ fn showProjectSettings(self: *MainScreen) !void { _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) }); - { + { // Gain + _ = ui.label("Gain:", .{}); + + const last_gain_change = project.gain_changes.buffer[project.gain_changes.len - 1]; + const current_gain = try ui.numberInput(f64, .{ + .key = ui.keyFromString("Gain input"), + .storage = &self.gain_input, + .initial = last_gain_change.gain, + .width = 400 + }); + + if (!self.gain_input.editing and current_gain != null and current_gain.? != last_gain_change.gain) { + try project.gain_changes.append(.{ + .gain = current_gain.?, + .sample = @floatFromInt(project.getSampleTimestamp() orelse 0) + }); + } + + { + const btn = ui.textButton("Show gain changes"); + btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 }); + if (ui.signal(btn).clicked()) { + self.side_panel = .gain_changes; + } + } + } + + _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) }); + + { // Statistic points const row = ui.createBox(.{ .size_x = UI.Sizing.initFitChildren(), .size_y = UI.Sizing.initFitChildren(), @@ -439,7 +476,7 @@ fn showProjectSettings(self: *MainScreen) !void { const btn = ui.textButton("View statistic points"); btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 }); if (ui.signal(btn).clicked()) { - self.view_statistics_points = true; + self.side_panel = .statistics_points; } } } @@ -475,7 +512,7 @@ fn showProjectSettings(self: *MainScreen) !void { _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) }); - { + { // Open notes const btn = ui.textButton("Open notes"); btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 }); if (ui.signal(btn).clicked()) { @@ -485,7 +522,7 @@ fn showProjectSettings(self: *MainScreen) !void { _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) }); - { + { // Export folder _ = ui.label("Export folder:", .{}); if (ui.fileInput(.{ .allocator = self.app.allocator, @@ -557,6 +594,8 @@ fn showViewSettings(self: *MainScreen, view_id: Id) !void { } } + _ = ui.label("Statistics:", .{}); + switch (view.reference) { .channel => |channel_id| { const channel = project.channels.get(channel_id).?; @@ -588,7 +627,7 @@ fn showViewSettings(self: *MainScreen, view_id: Id) !void { } } - const sample_list_id = project.getViewSampleListId(view_id); + const sample_list_id = project.getViewReferenceSampleListId(view_id); const sample_list = project.sample_lists.get(sample_list_id).?; const sample_count = sample_list.getLength(); @@ -607,122 +646,122 @@ fn showViewSettings(self: *MainScreen, view_id: Id) !void { _ = ui.label("Duration: {s}", .{ duration_str orelse "-" }); - var deferred_remove: std.BoundedArray(usize, App.View.max_transforms) = .{}; + // var deferred_remove: std.BoundedArray(usize, App.View.max_transforms) = .{}; - for (0.., view.transforms.slice()) |i, *_transform| { - const transform: *App.Transform = _transform; + // for (0.., view.transforms.slice()) |i, *_transform| { + // const transform: *App.Transform = _transform; - const row = ui.createBox(.{ - .key = UI.Key.initPtr(transform), - .size_x = UI.Sizing.initGrowFull(), - .size_y = UI.Sizing.initFixedPixels(ui.rem(1.5)), - .layout_direction = .left_to_right - }); - row.beginChildren(); - defer row.endChildren(); + // const row = ui.createBox(.{ + // .key = UI.Key.initPtr(transform), + // .size_x = UI.Sizing.initGrowFull(), + // .size_y = UI.Sizing.initFixedPixels(ui.rem(1.5)), + // .layout_direction = .left_to_right + // }); + // row.beginChildren(); + // defer row.endChildren(); - if (ui.signal(ui.textButton("Remove")).clicked()) { - deferred_remove.appendAssumeCapacity(i); - } + // if (ui.signal(ui.textButton("Remove")).clicked()) { + // deferred_remove.appendAssumeCapacity(i); + // } - { - const options = .{ - .{ .multiply, "Multiply" }, - .{ .addition, "Addition" }, - .{ .sliding_window, "Sliding window" }, - }; + // { + // const options = .{ + // .{ .multiply, "Multiply" }, + // .{ .addition, "Addition" }, + // .{ .sliding_window, "Sliding window" }, + // }; - const select = ui.button(ui.keyFromString("Transform select")); - select.setFmtText("{s}", .{switch (transform.*) { - .sliding_window => "Sliding window", - .addition => "Addition", - .multiply => "Multiply" - }}); + // const select = ui.button(ui.keyFromString("Transform select")); + // select.setFmtText("{s}", .{switch (transform.*) { + // .sliding_window => "Sliding window", + // .addition => "Addition", + // .multiply => "Multiply" + // }}); - select.size.y = UI.Sizing.initGrowFull(); - select.alignment.y = .center; - if (ui.signal(select).clicked()) { - select.persistent.open = !select.persistent.open; - } + // select.size.y = UI.Sizing.initGrowFull(); + // select.alignment.y = .center; + // if (ui.signal(select).clicked()) { + // select.persistent.open = !select.persistent.open; + // } - if (select.persistent.open) { - const popup = ui.createBox(.{ - .key = ui.keyFromString("Select popup"), - .size_x = UI.Sizing.initFixedPixels(ui.rem(10)), - .size_y = UI.Sizing.initFitChildren(), - .flags = &.{ .clickable, .scrollable }, - .layout_direction = .top_to_bottom, - .float_relative_to = select, - .background = srcery.black, - .borders = UI.Borders.all(.{ .color = srcery.bright_black, .size = 4 }), - .draw_on_top = true - }); - popup.setFloatPosition(0, select.persistent.size.y); - popup.beginChildren(); - defer popup.endChildren(); + // if (select.persistent.open) { + // const popup = ui.createBox(.{ + // .key = ui.keyFromString("Select popup"), + // .size_x = UI.Sizing.initFixedPixels(ui.rem(10)), + // .size_y = UI.Sizing.initFitChildren(), + // .flags = &.{ .clickable, .scrollable }, + // .layout_direction = .top_to_bottom, + // .float_relative_to = select, + // .background = srcery.black, + // .borders = UI.Borders.all(.{ .color = srcery.bright_black, .size = 4 }), + // .draw_on_top = true + // }); + // popup.setFloatPosition(0, select.persistent.size.y); + // popup.beginChildren(); + // defer popup.endChildren(); - inline for (options) |option| { - const select_option = ui.textButton(option[1]); - select_option.alignment.x = .start; - select_option.size.x = UI.Sizing.initGrowFull(); - select_option.borders = UI.Borders.all(.{ .color = srcery.hard_black, .size = 2 }); - select_option.background = srcery.black; + // inline for (options) |option| { + // const select_option = ui.textButton(option[1]); + // select_option.alignment.x = .start; + // select_option.size.x = UI.Sizing.initGrowFull(); + // select_option.borders = UI.Borders.all(.{ .color = srcery.hard_black, .size = 2 }); + // select_option.background = srcery.black; - const signal = ui.signal(select_option); - if (signal.clicked()) { - select.persistent.open = false; - transform.* = switch (option[0]) { - .sliding_window => App.Transform{ .sliding_window = sample_rate orelse 0 }, - .addition => App.Transform{ .addition = 0 }, - .multiply => App.Transform{ .multiply = 1 }, - else => unreachable - }; - } - } + // const signal = ui.signal(select_option); + // if (signal.clicked()) { + // select.persistent.open = false; + // transform.* = switch (option[0]) { + // .sliding_window => App.Transform{ .sliding_window = sample_rate orelse 0 }, + // .addition => App.Transform{ .addition = 0 }, + // .multiply => App.Transform{ .multiply = 1 }, + // else => unreachable + // }; + // } + // } - _ = ui.signal(popup); - } - } + // _ = ui.signal(popup); + // } + // } - var input_opts = UI.NumberInputOptions{ - .key = ui.keyFromString("Sliding window"), - .storage = &self.transform_inputs[i], - .width = ui.rem(4) - // .postfix = if (sample_rate != null) " s" else null, - // .display_scalar = sample_rate, - }; + // var input_opts = UI.NumberInputOptions{ + // .key = ui.keyFromString("Sliding window"), + // .storage = &self.transform_inputs[i], + // .width = ui.rem(4) + // // .postfix = if (sample_rate != null) " s" else null, + // // .display_scalar = sample_rate, + // }; - _ = ui.createBox(.{ .size_x = UI.Sizing.initGrowFull() }); + // _ = ui.createBox(.{ .size_x = UI.Sizing.initGrowFull() }); - if (transform.* == .sliding_window and sample_rate != null) { - input_opts.postfix = " s"; - input_opts.display_scalar = sample_rate; - } + // if (transform.* == .sliding_window and sample_rate != null) { + // input_opts.postfix = " s"; + // input_opts.display_scalar = sample_rate; + // } - const current_value = switch (transform.*) { - .sliding_window => |*v| v, - .addition => |*v| v, - .multiply => |*v| v - }; - input_opts.initial = current_value.*; + // const current_value = switch (transform.*) { + // .sliding_window => |*v| v, + // .addition => |*v| v, + // .multiply => |*v| v + // }; + // input_opts.initial = current_value.*; - const new_value = try ui.numberInput(f64, input_opts); - if (new_value != null) { - current_value.* = new_value.?; - } - } + // const new_value = try ui.numberInput(f64, input_opts); + // if (new_value != null) { + // current_value.* = new_value.?; + // } + // } - for (0..deferred_remove.len) |i| { - const transform_index = deferred_remove.get(deferred_remove.len - 1 - i); - _ = view.transforms.orderedRemove(transform_index); - } + // for (0..deferred_remove.len) |i| { + // const transform_index = deferred_remove.get(deferred_remove.len - 1 - i); + // _ = view.transforms.orderedRemove(transform_index); + // } - if (view.transforms.unusedCapacitySlice().len > 0) { - const btn = ui.textButton("Add transform"); - if (ui.signal(btn).clicked()) { - view.transforms.appendAssumeCapacity(.{ .addition = 0 }); - } - } + // if (view.transforms.unusedCapacitySlice().len > 0) { + // const btn = ui.textButton("Add transform"); + // if (ui.signal(btn).clicked()) { + // view.transforms.appendAssumeCapacity(.{ .addition = 0 }); + // } + // } _ = ui.createBox(.{ .size_y = UI.Sizing.initGrowFull() }); @@ -735,103 +774,103 @@ fn showViewSettings(self: *MainScreen, view_id: Id) !void { } } -fn showMarkedRange(self: *MainScreen, view_id: Id, index: usize) void { - var ui = &self.app.ui; +// 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 view = self.app.getView(view_id) orelse return; - const marked_range = view.marked_ranges.get(index); +// const marked_range = view.marked_ranges.get(index); - { - const label = ui.label("Selected range", .{}); - label.borders.bottom = .{ - .color = srcery.blue, - .size = 1 - }; +// { +// const label = ui.label("Selected range", .{}); +// label.borders.bottom = .{ +// .color = srcery.blue, +// .size = 1 +// }; - _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(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() }); - } +// 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() }); +// } - _ = ui.label("Samples: {d:.2}", .{ marked_range.range.size() }); +// _ = ui.label("Samples: {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.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.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.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: ", .{}); - } - } +// 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() }); +// _ = 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); - } - } -} +// { +// 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.side_panel = .project; +// _ = view.marked_ranges.swapRemove(index); +// } +// } +// } -fn showMarker(self: *MainScreen, view_id: Id, index: usize) void { - var ui = &self.app.ui; +// fn showMarker(self: *MainScreen, view_id: Id, index: usize) void { +// var ui = &self.app.ui; - const view = self.app.getView(view_id) orelse return; +// const view = self.app.getView(view_id) orelse return; - const marker = view.markers.get(index); +// const marker = view.markers.get(index); - { - const label = ui.label("Selected range", .{}); - label.borders.bottom = .{ - .color = srcery.blue, - .size = 1 - }; +// { +// const label = ui.label("Selected range", .{}); +// label.borders.bottom = .{ +// .color = srcery.blue, +// .size = 1 +// }; - _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) }); - } +// _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) }); +// } - const sample_rate = self.app.project.getSampleRate(); +// const sample_rate = self.app.project.getSampleRate(); - if (sample_rate != null) { - const duration = utils.formatDuration(ui.frameAllocator(), marker / sample_rate.?) catch ""; - _ = ui.label("Position: {s}", .{ duration }); - } else { - _ = ui.label("Position: {d:.2}", .{ marker }); - } +// if (sample_rate != null) { +// const duration = utils.formatDuration(ui.frameAllocator(), marker / sample_rate.?) catch ""; +// _ = ui.label("Position: {s}", .{ duration }); +// } else { +// _ = ui.label("Position: {d:.2}", .{ marker }); +// } -} +// } fn showToolbar(self: *MainScreen) void { var ui = &self.app.ui; @@ -945,38 +984,38 @@ fn showToolbar(self: *MainScreen) void { _ = 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 }); - } + // { + // 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()) { - self.view_controls.selected_tool = .move; - } - } + // if (ui.signal(btn).clicked()) { + // 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 }); - } + // { + // 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()) { - self.view_controls.selected_tool = .select; - } - } + // if (ui.signal(btn).clicked()) { + // self.view_controls.selected_tool = .select; + // } + // } - { - var btn = ui.textButton("Marker"); - if (self.view_controls.selected_tool == .marker) { - btn.borders = UI.Borders.bottom(.{ .size = 4, .color = srcery.green }); - } + // { + // var btn = ui.textButton("Marker"); + // if (self.view_controls.selected_tool == .marker) { + // btn.borders = UI.Borders.bottom(.{ .size = 4, .color = srcery.green }); + // } - if (ui.signal(btn).clicked()) { - self.view_controls.selected_tool = .marker; - } - } + // if (ui.signal(btn).clicked()) { + // self.view_controls.selected_tool = .marker; + // } + // } } fn showSolutions(self: *MainScreen) !void { @@ -1024,15 +1063,14 @@ fn showSolutions(self: *MainScreen) !void { const btn = ui.textButton("Close"); btn.borders = UI.Borders.all(.{ .color = srcery.red, .size = 4 }); if (ui.signal(btn).clicked()) { - self.view_solutions = false; + self.side_panel = .project; } } } fn showStatisticsPoints(self: *MainScreen) !void { - var ui = &self.app.ui; + const ui = &self.app.ui; const project = &self.app.project; - _ = &ui; { const label = ui.label("Statistics points", .{}); @@ -1070,7 +1108,72 @@ fn showStatisticsPoints(self: *MainScreen) !void { const btn = ui.textButton("Close"); btn.borders = UI.Borders.all(.{ .color = srcery.red, .size = 4 }); if (ui.signal(btn).clicked()) { - self.view_statistics_points = false; + self.side_panel = .project; + } + } +} + +fn showGainChanges(self: *MainScreen) !void { + const project = &self.app.project; + var ui = &self.app.ui; + + { + const label = ui.label("Gain changes", .{}); + label.borders.bottom = .{ + .color = srcery.white, + .size = 1 + }; + + _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) }); + } + + const sample_rate = project.getSampleRate(); + + var removed_index: ?usize = null; + + for (0.., project.gain_changes.slice()) |i, *gain_change| { + const row = ui.createBox(.{ + .key = UI.Key.initPtr(gain_change), + .size_x = UI.Sizing.initGrowFull(), + .size_y = UI.Sizing.initFitChildren(), + .layout_direction = .left_to_right, + .align_y = .center, + .layout_gap = ui.rem(1) + }); + row.beginChildren(); + defer row.endChildren(); + + if (sample_rate != null) { + const seconds = gain_change.sample / sample_rate.?; + _ = ui.label("{s}", .{ try utils.formatDuration(ui.frameAllocator(), seconds) }); + } else { + _ = ui.label("{d}", .{ gain_change.sample }); + } + + var gain_label = ui.label("{d:.3}", .{ gain_change.gain }); + gain_label.size.x = UI.Sizing.initGrowFull(); + + if (i > 0) { + const btn = ui.textButton("Remove"); + btn.background = srcery.bright_red; + btn.padding = UI.Padding.all(ui.rem(0.2)); + if (ui.signal(btn).clicked()) { + removed_index = i; + } + } + } + + if (removed_index) |index| { + project.removeGainChange(index); + } + + _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) }); + + { + const btn = ui.textButton("Close"); + btn.borders = UI.Borders.all(.{ .color = srcery.red, .size = 4 }); + if (ui.signal(btn).clicked()) { + self.side_panel = .project; } } } @@ -1093,18 +1196,28 @@ pub fn showSidePanel(self: *MainScreen) !void { _ = ui.createBox(.{ .size_x = UI.Sizing.initFixedPixels(ui.rem(18)) }); - if (self.view_solutions) { - try self.showSolutions(); - } else if (self.view_controls.show_marker) |marker| { - self.showMarker(marker.view_id, marker.index); - } else 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 if (self.view_statistics_points) { - try self.showStatisticsPoints(); - } else { - try self.showProjectSettings(); + switch (self.side_panel) { + .project => { + try self.showProjectSettings(); + }, + .solutions => { + try self.showSolutions(); + }, + .statistics_points => { + try self.showStatisticsPoints(); + }, + .gain_changes => { + try self.showGainChanges(); + }, + .view_settings => |view_id| { + try self.showViewSettings(view_id); + }, + // .marker => |args| { + // self.showMarker(args.view_id, args.index); + // }, + // .marked_range => |args| { + // self.showMarkedRange(args.view_id, args.index); + // } } } @@ -1199,14 +1312,8 @@ pub fn tick(self: *MainScreen) !void { if (ui.isKeyboardPressed(.key_escape)) { if (self.modal != null) { self.modal = 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 if (self.view_solutions) { - self.view_solutions = false; - } else if (self.view_statistics_points) { - self.view_statistics_points = false; + } else if (self.side_panel != .project) { + self.side_panel = .project; } else { self.app.should_close = true; } diff --git a/src/ui.zig b/src/ui.zig index 5c68e40..b313f2c 100644 --- a/src/ui.zig +++ b/src/ui.zig @@ -2779,8 +2779,9 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void { storage.editing = true; } + var stop_editing = false; if (self.isKeyActiveAny() and !container_signal.active) { - storage.editing = false; + stop_editing = true; } // Text input controls @@ -2952,7 +2953,7 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void { } if (self.isKeyboardPressed(.key_escape)) { - storage.editing = false; + stop_editing = true; } if (self.isKeyboardPressed(.key_enter)) { @@ -2975,9 +2976,15 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void { } if (!opts.editable) { - storage.editing = false; + stop_editing = true; } } + + if (stop_editing) { + storage.editing = false; + storage.cursor_start = 0; + storage.cursor_stop = 0; + } } pub fn numberInput(self: *UI, T: type, opts: NumberInputOptions) !?T {