add gain change live updates

This commit is contained in:
Rokas Puzonas 2025-05-26 14:50:24 +03:00
parent eafafbab89
commit 0edeaefbf0
8 changed files with 753 additions and 701 deletions

View File

@ -194,93 +194,125 @@ const SumRingBuffer = struct {
} }
}; };
pub const Transform = union(enum) { pub const Transform = struct {
sliding_window: f64, const max_gain_changes = 32;
multiply: f64, pub const GainChanges = std.BoundedArray(GainChange, max_gain_changes);
addition: f64, pub const GainChange = struct {
gain: f64,
pub const State = struct { sample: f64
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 fn apply(self: Transform, state: *State, destination: []f64, source: []f64) void { gain_changes: GainChanges = .{},
switch (self) {
.sliding_window => {
const sum_ring_buffer = &(state.sum_ring_buffer.?);
if (sum_ring_buffer.len == 0) { pub fn applySlice(self: Transform, index: usize, source: []f64, destination: []f64) void {
@memcpy(destination[0..source.len], source); assert(source.len == destination.len);
} else { assert(source.len <= SampleList.Block.capacity);
for (0..source.len) |i| {
const sample = source[i];
sum_ring_buffer.append(sample);
destination[i] = sum_ring_buffer.sum / @as(f64, @floatFromInt(sum_ring_buffer.len)); const gain_changes = self.gain_changes.constSlice();
}
}
}, const gain_segment = Transform.findGainSegment(gain_changes, index);
.multiply => |scalar| { if (gain_segment.to > @as(f64, @floatFromInt(index+source.len))) {
for (0..source.len) |i| { for (0..source.len) |i| {
destination[i] = scalar * source[i]; destination[i] = source[i] * (100 / gain_segment.gain);
} }
},
.addition => |offset| { } else {
for (0..source.len) |i| { const gain_per_sample = Transform.createGainLookup(gain_changes, index, source.len);
destination[i] = offset + source[i];
} for (0..source.len) |i| {
destination[i] = source[i] * (100 / gain_per_sample[i]);
} }
} }
} }
pub fn eqlSlice(slice1: []const Transform, slice2: []const Transform) bool { pub fn applyBlock(self: Transform, index: usize, source: *SampleList.Block, destination: *SampleList.Block) void {
if (slice1.len != slice2.len) { destination.clear();
return false; 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(); 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) { while (self.blocks.items.len < total_block_count) {
try self.appendEmptyBlock(); 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 { pub fn apply(self: *SampleList, source: *SampleList, from_block_index: Block.Id, block_count: usize, transform: Transform) void {
try self.applyTransformations(source, from_block_index, block_count, &[_]Transform{ .{ .sliding_window = window_width } }); transform.applySampleList(from_block_index, block_count, source, self);
}
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();
}
} }
test { test {
@ -750,99 +734,6 @@ pub const File = struct {
}; };
pub const View = 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) { pub const Reference = union(enum) {
file: Id, file: Id,
channel: Id channel: Id
@ -854,9 +745,6 @@ pub const View = struct {
// TODO: Implement different styles of following: Look ahead, sliding, sliding window // TODO: Implement different styles of following: Look ahead, sliding, sliding window
follow: bool = false, follow: bool = false,
graph_opts: Graph.ViewOptions = .{}, graph_opts: Graph.ViewOptions = .{},
marked_ranges: std.BoundedArray(MarkedRange, 32) = .{},
markers: std.BoundedArray(f64, max_markers) = .{},
transforms: BoundedTransformsArray = .{},
name: std.BoundedArray(u8, 128) = .{}, name: std.BoundedArray(u8, 128) = .{},
// Runtime // Runtime
@ -865,8 +753,9 @@ pub const View = struct {
available_y_range: RangeF64 = RangeF64.init(0, 0), available_y_range: RangeF64 = RangeF64.init(0, 0),
unit: ?NIDaq.Unit = .Voltage, unit: ?NIDaq.Unit = .Voltage,
transformed_samples: ?Id = null, transformed_samples: Id,
computed_transforms: BoundedTransformsArray = .{}, computed_transform: Transform = .{},
invalidated_transform_ranges: std.BoundedArray(RangeF64, 16) = .{},
pub fn clear(self: *View) void { pub fn clear(self: *View) void {
self.graph_cache.clear(); self.graph_cache.clear();
@ -887,13 +776,6 @@ pub const View = struct {
.Y => self.available_y_range .Y => self.available_y_range
}; };
} }
pub fn iterMarkedRanges(self: *View, axis: UI.Axis) MarkedRangeIterator {
return MarkedRangeIterator{
.view = self,
.axis = axis
};
}
}; };
pub const Project = struct { pub const Project = struct {
@ -913,6 +795,7 @@ pub const Project = struct {
export_location: ?[]u8 = null, export_location: ?[]u8 = null,
solutions: std.ArrayListUnmanaged(Solution) = .{}, solutions: std.ArrayListUnmanaged(Solution) = .{},
gain_changes: Transform.GainChanges = .{},
sample_lists: GenerationalArray(SampleList) = .{}, sample_lists: GenerationalArray(SampleList) = .{},
channels: GenerationalArray(Channel) = .{}, channels: GenerationalArray(Channel) = .{},
@ -951,7 +834,7 @@ pub const Project = struct {
return result_range; 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).?; const view = self.views.get(view_id).?;
switch (view.reference) { switch (view.reference) {
@ -976,10 +859,9 @@ pub const Project = struct {
@floatFromInt(sample_list.getLength()), @floatFromInt(sample_list.getLength()),
@floatFromInt(sample_list.getLength() + samples.len) @floatFromInt(sample_list.getLength() + samples.len)
); );
_ = affected_range;
try sample_list.append(samples); try sample_list.append(samples);
self.refreshMarkedRanges(sample_list_id, affected_range);
} }
pub fn clearSamples(self: *Project, allocator: Allocator, sample_list_id: Id) !void { pub fn clearSamples(self: *Project, allocator: Allocator, sample_list_id: Id) !void {
@ -989,10 +871,10 @@ pub const Project = struct {
0, 0,
@floatFromInt(sample_list.getLength()) @floatFromInt(sample_list.getLength())
); );
_ = affected_range;
sample_list.clear(allocator); sample_list.clear(allocator);
self.refreshMarkedRanges(sample_list_id, affected_range);
} }
pub fn addSampleList(self: *Project, allocator: Allocator) !Id { pub fn addSampleList(self: *Project, allocator: Allocator) !Id {
@ -1005,32 +887,6 @@ pub const Project = struct {
self.sample_lists.remove(sample_list_id); 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 { 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 bytes_left: usize = sample_count * 8;
var buffer: [SampleList.Block.capacity * 8]u8 = undefined; 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 { pub fn getSampleTimestamp(self: *Project) ?u64 {
var channel_iter = self.channels.iterator(); var channel_iter = self.channels.iterator();
while (channel_iter.next()) |channel| { 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 { pub fn removeView(self: *Project, allocator: std.mem.Allocator, view_id: Id) void {
const view = self.views.get(view_id) orelse return; const view = self.views.get(view_id) orelse return;
if (view.transformed_samples) |sample_list_id| { self.removeSampleList(view.transformed_samples);
self.removeSampleList(sample_list_id);
}
switch (view.reference) { switch (view.reference) {
.file => |file_id| { .file => |file_id| {
@ -1125,6 +958,13 @@ pub const Project = struct {
view.* = undefined; 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 { pub fn deinit(self: *Project, allocator: Allocator) void {
var file_iter = self.files.iterator(); var file_iter = self.files.iterator();
while (file_iter.next()) |file| { while (file_iter.next()) |file| {
@ -1146,37 +986,50 @@ pub const Project = struct {
} }
self.sample_lists.clear(); self.sample_lists.clear();
for (self.solutions.items) |solution| {
if (self.solutions.capacity > 0) { allocator.free(solution.description);
for (self.solutions.items) |solution| {
allocator.free(solution.description);
}
self.solutions.deinit(allocator);
} }
self.solutions.deinit(allocator);
if (self.pipete_solution.capacity > 0) { self.pipete_solution.deinit(allocator);
self.pipete_solution.deinit(allocator); self.notes.deinit(allocator);
}
if (self.notes.capacity > 0) {
self.notes.deinit(allocator);
}
if (self.export_location) |export_location| { if (self.export_location) |export_location| {
allocator.free(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 ------------------ // // ------------------- Serialization ------------------ //
pub fn initFromFile(self: *Project, allocator: Allocator, f: std.fs.File) !void { pub fn initFromFile(self: *Project, allocator: Allocator, f: std.fs.File) !void {
self.* = .{}; try Project.init(self, allocator);
errdefer self.deinit(allocator); errdefer self.deinit(allocator);
const reader = f.reader(); const reader = f.reader();
@ -1269,9 +1122,13 @@ pub const Project = struct {
return error.InvalidReferenceTag; return error.InvalidReferenceTag;
} }
const sample_list_id = try self.addSampleList(allocator);
errdefer self.removeSampleList(sample_list_id);
const view = self.views.get(id).?; const view = self.views.get(id).?;
view.* = View{ view.* = View{
.reference = reference, .reference = reference,
.transformed_samples = sample_list_id
}; };
} }
} }
@ -1439,7 +1296,7 @@ const WorkJob = struct {
view_id: Id, view_id: Id,
stage: Stage = .init, stage: Stage = .init,
transforms: View.BoundedTransformsArray = .{}, transform: Transform = .{},
mutex: std.Thread.Mutex = .{}, mutex: std.Thread.Mutex = .{},
running_thread_jobs: std.ArrayListUnmanaged(SampleList.Block.Id) = .{}, running_thread_jobs: std.ArrayListUnmanaged(SampleList.Block.Id) = .{},
@ -1488,33 +1345,20 @@ const WorkJob = struct {
const project = &app.project; const project = &app.project;
const view = project.views.get(self.view_id) orelse return true; 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 sample_list = project.sample_lists.get(sample_list_id).?;
const transforms = self.transforms.constSlice(); if (!Transform.eql(project.getViewTransform(self.view_id), self.transform)) {
if (!Transform.eqlSlice(view.transforms.constSlice(), transforms)) {
// Transforms changed, job needs to be cancelled. // Transforms changed, job needs to be cancelled.
return true; return true;
} }
switch (self.stage) { switch (self.stage) {
.init => { .init => {
if (transforms.len == 0) { const transformed_samples = project.sample_lists.get(view.transformed_samples).?;
if (view.transformed_samples) |transformed_samples_id| { try transformed_samples.reserveEmptyBlocks(sample_list.blocks.items.len);
project.removeSampleList(transformed_samples_id);
}
view.transformed_samples = null;
return true; self.stage = .launch_threads;
} 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;
}
}, },
.launch_threads => { .launch_threads => {
const max_block_to_process = 256; const max_block_to_process = 256;
@ -1548,15 +1392,12 @@ const WorkJob = struct {
// } // }
const view = project.views.get(self.view_id) orelse return; 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(view.transformed_samples) orelse return;
const transformed_samples = project.sample_lists.get(transformed_samples_id) 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; 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| { transformed_samples.apply(sample_list, from_block_id, block_count, self.transform);
log.err("Failed to compute sliding window: {}", .{e});
};
} }
}; };
@ -1572,7 +1413,7 @@ screen: enum {
} = .main, } = .main,
main_screen: MainScreen, main_screen: MainScreen,
channel_from_device: ChannelFromDeviceScreen, channel_from_device: ChannelFromDeviceScreen,
project: Project = .{}, project: Project,
collection_mutex: std.Thread.Mutex = .{}, collection_mutex: std.Thread.Mutex = .{},
collection_samples_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, .main_screen = undefined,
.collection_thread = undefined, .collection_thread = undefined,
.channel_from_device = 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.initUI();
try self.work_thread_pool.init(.{ try self.work_thread_pool.init(.{
.allocator = allocator .allocator = allocator
@ -1689,9 +1532,8 @@ pub fn loadProject(self: *App, file: std.fs.File) !void {
var loaded = try self.allocator.create(Project); var loaded = try self.allocator.create(Project);
defer self.allocator.destroy(loaded); defer self.allocator.destroy(loaded);
loaded.* = .{};
errdefer loaded.deinit(self.allocator);
try loaded.initFromFile(self.allocator, file); try loaded.initFromFile(self.allocator, file);
errdefer loaded.deinit(self.allocator);
self.deinitUI(); self.deinitUI();
self.deinitProject(); self.deinitProject();
@ -1889,6 +1731,40 @@ pub fn tick(self: *App) !void {
} }
try self.showUI(); 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); rl.clearBackground(srcery.black);
@ -1957,18 +1833,20 @@ pub fn tick(self: *App) !void {
var view_iter = self.project.views.idIterator(); var view_iter = self.project.views.idIterator();
while (view_iter.next()) |view_id| { while (view_iter.next()) |view_id| {
const view = self.project.views.get(view_id).?; const view = self.project.views.get(view_id).?;
_ = view;
if (Transform.eqlSlice(view.computed_transforms.constSlice(), view.transforms.constSlice())) { // TODO:
continue; // if (Transform.eqlSlice(view.computed_transforms.constSlice(), view.transforms.constSlice())) {
} // continue;
// }
_ = try self.work_jobs.insert(WorkJob{ // _ = try self.work_jobs.insert(WorkJob{
.transforms = view.transforms, // .transforms = view.transforms,
.view_id = view_id // .view_id = view_id
}); // });
view.computed_transforms.len = 0; // view.computed_transforms.len = 0;
view.computed_transforms.appendSliceAssumeCapacity(view.transforms.constSlice()); // view.computed_transforms.appendSliceAssumeCapacity(view.transforms.constSlice());
} }
} }
@ -2087,7 +1965,7 @@ fn startCollection(self: *App) !void {
self.collection_condition.signal(); self.collection_condition.signal();
} }
fn stopCollection(self: *App) void { pub fn stopCollection(self: *App) void {
if (!self.isCollectionInProgress()) { if (!self.isCollectionInProgress()) {
return; return;
} }
@ -2140,7 +2018,7 @@ fn startOutput(self: *App, channel_id: Id) !void {
channel.output_task = task; 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; const channel = self.getChannel(channel_id) orelse return;
if (channel.output_task == null) { if (channel.output_task == null) {
@ -2167,6 +2045,13 @@ pub fn isOutputingInProgress(self: *App) bool {
return false; 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 { fn isNiDaqInUse(self: *App) bool {
return self.isCollectionInProgress() or self.isOutputingInProgress(); 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(); const id = try self.project.views.insertUndefined();
errdefer self.project.views.remove(id); 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).?; const view = self.project.views.get(id).?;
view.* = View{ view.* = View{
.reference = reference .reference = reference,
.transformed_samples = sample_list_id
}; };
self.loadView(id) catch |e| { 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 { fn refreshViewAvailableXYRanges(self: *App, id: Id) void {
const view = self.getView(id) orelse return; 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).?; const sample_list = self.project.sample_lists.get(sample_list_id).?;
view.available_x_range = RangeF64.init(0, @floatFromInt(sample_list.getLength())); view.available_x_range = RangeF64.init(0, @floatFromInt(sample_list.getLength()));

View File

@ -153,6 +153,7 @@ const MarkedRangeIterator = struct {
} }
}; };
app: *App,
project: *App.Project, project: *App.Project,
// TODO: Redo // TODO: Redo
@ -160,21 +161,13 @@ undo_stack: CommandFrameArray = .{},
last_applied_command: usize = 0, last_applied_command: usize = 0,
zoom_start: ?ViewAxisPosition = null, zoom_start: ?ViewAxisPosition = null,
cursor: ?ViewAxisPosition = null, cursor: ?ViewAxisPosition = null,
view_settings: ?Id = null, // View id
view_protocol_modal: ?Id = null, // View id view_protocol_modal: ?Id = null, // View id
selected_tool: enum { move, select, marker } = .move, // 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,
pub fn init(project: *App.Project) System { pub fn init(app: *App) System {
return 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 { pub fn toggleViewSettings(self: *System, view_id: Id) void {
if (self.isViewSettingsOpen(view_id)) { if (self.isViewSettingsOpen(view_id)) {
self.view_settings = null; self.app.main_screen.side_panel = .project;
} else { } 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 { 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 { 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 { 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) { if (show_marked_range.view_id.eql(view_id) and show_marked_range.index == index) {
self.show_marked_range = null; side_panel.* = .project;
return; return;
} }
} }
self.show_marked_range = .{ side_panel.* = .{
.view_id = view_id, .marked_range = .{ .index = index, .view_id = view_id }
.index = index,
}; };
} }
pub fn toggleShownMarker(self: *System, view_id: Id, index: usize) void { 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) { if (show_marker.view_id.eql(view_id) and show_marker.index == index) {
self.show_marker = null; side_panel.* = .project;
return; return;
} }
} }
self.show_marker = .{ side_panel.* = .{
.view_id = view_id, .marker = .{
.index = index, .view_id = view_id,
.index = index,
}
}; };
} }

View File

@ -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); 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()) { if (signal.dragged()) {
const x_offset = mouse_x_range.remapTo(RangeF64.init(0, view_opts.x_range.size()), signal.drag.x); 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); 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)) { if (signal.flags.contains(.left_released)) {
ctx.view_controls.pushBreakpoint(); ctx.view_controls.pushBreakpoint();
} }
} else if (ctx.view_controls.selected_tool == .select) { // } else if (ctx.view_controls.selected_tool == .select) {
// TODO: // // TODO:
} else if (ctx.view_controls.selected_tool == .marker) { // } else if (ctx.view_controls.selected_tool == .marker) {
// TODO: // // TODO:
} // }
{ // Render graph { // Render graph
var sample_list_id = app.project.getViewSampleListId(view_id); const sample_list = app.project.sample_lists.get(view.transformed_samples).?;
if (view.transformed_samples) |transformed_samples_id| {
sample_list_id = transformed_samples_id;
}
const sample_list = app.project.sample_lists.get(sample_list_id).?;
Graph.drawCached(&view.graph_cache, graph_box.persistent.size, view_opts.*, sample_list); Graph.drawCached(&view.graph_cache, graph_box.persistent.size, view_opts.*, sample_list);
if (view.graph_cache.texture) |texture| { if (view.graph_cache.texture) |texture| {
graph_box.texture = texture.texture; graph_box.texture = texture.texture;

View File

@ -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); _ = showMarkerRect(ui, ruler, RangeF64.init(hold_start.?, cursor.?), marker_color.alpha(0.5), null);
} }
{ // {
var selected_range_iter = view.iterMarkedRanges(axis); // var selected_range_iter = view.iterMarkedRanges(axis);
while (selected_range_iter.next()) |selected_range| { // while (selected_range_iter.next()) |selected_range| {
var color = srcery.blue; // var color = srcery.blue;
const index = selected_range_iter.index; // const index = selected_range_iter.index;
if (ctx.view_controls.show_marked_range) |show_marked_range| { // const side_panel = ctx.view_controls.app.main_screen.side_panel;
if (show_marked_range.view_id.eql(view_id) and show_marked_range.index == index) { // if (side_panel == .marked_range) {
if (@mod(rl.getTime(), 0.5) < 0.25) { // const show_marked_range = side_panel.marked_range;
color = utils.shiftColorInHSV(color, 0.8); // 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.lower, color);
showMarkerLine(ui, ruler, selected_range.upper, color); // showMarkerLine(ui, ruler, selected_range.upper, color);
var hasher = UI.Key.CombineHasher.init(); // var hasher = UI.Key.CombineHasher.init();
hasher.update(std.mem.asBytes("Marked ranges")); // hasher.update(std.mem.asBytes("Marked ranges"));
hasher.update(std.mem.asBytes(&view_id)); // hasher.update(std.mem.asBytes(&view_id));
hasher.update(std.mem.asBytes(&axis)); // hasher.update(std.mem.asBytes(&axis));
hasher.update(std.mem.asBytes(&index)); // hasher.update(std.mem.asBytes(&index));
const range_box_key = UI.Key.init(hasher.final()); // const range_box_key = UI.Key.init(hasher.final());
var range_box = showMarkerRect(ui, ruler, selected_range, color.alpha(0.5), range_box_key); // var range_box = showMarkerRect(ui, ruler, selected_range, color.alpha(0.5), range_box_key);
range_box.flags.insert(.clickable); // range_box.flags.insert(.clickable);
range_box.flags.insert(.draw_hot); // range_box.flags.insert(.draw_hot);
range_box.flags.insert(.draw_active); // range_box.flags.insert(.draw_active);
range_box.hot_cursor = .mouse_cursor_pointing_hand; // range_box.hot_cursor = .mouse_cursor_pointing_hand;
if (ctx.view_controls.selected_tool == .select) { // if (ctx.view_controls.selected_tool == .select) {
const signal = ui.signal(range_box); // const signal = ui.signal(range_box);
if (signal.clicked()) { // if (signal.clicked()) {
ctx.view_controls.toggleShownMarkedRange(view_id, index); // ctx.view_controls.toggleShownMarkedRange(view_id, index);
} // }
} // }
} // }
} // }
if (axis == .X) { if (axis == .X) {
for (0.., view.markers.constSlice()) |i, marker| { // for (0.., view.markers.constSlice()) |i, marker| {
const color = srcery.cyan; // 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(); 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(&view_id));
hasher.update(std.mem.asBytes(&axis)); hasher.update(std.mem.asBytes(&axis));
hasher.update(std.mem.asBytes(&i)); 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(.{ const clickable = ui.createBox(.{
.key = UI.Key.init(hasher.final()), .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, .float_relative_to = ruler.graph_box,
.parent = ruler.graph_box, .parent = ruler.graph_box,
.flags = &.{ .draw_hot, .draw_active, .clickable }, .flags = &.{ .draw_hot, .draw_active, .clickable },
.hot_cursor = .mouse_cursor_pointing_hand, .hot_cursor = .mouse_cursor_pointing_hand,
}); });
if (ui.signal(clickable).clicked()) { const signal = ui.signal(clickable);
ctx.view_controls.toggleShownMarker(view_id, i); 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); 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) { if (signal.scrolled() and cursor != null) {
var scale_factor: f64 = 1; var scale_factor: f64 = 1;
if (signal.scroll.y > 0) { 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 (cursor != null) {
if (ctx.view_controls.getCursorHoldStart(view_id, axis)) |hold_start| { // if (ctx.view_controls.getCursorHoldStart(view_id, axis)) |hold_start| {
const range = RangeF64.init( // const range = RangeF64.init(
@min(hold_start, cursor.?), // @min(hold_start, cursor.?),
@max(hold_start, cursor.?), // @max(hold_start, cursor.?),
); // );
const hold_start_mouse = view_range.remapTo(mouse_range, range.lower); // const hold_start_mouse = view_range.remapTo(mouse_range, range.lower);
const hold_end_mouse = view_range.remapTo(mouse_range, range.upper); // const hold_end_mouse = view_range.remapTo(mouse_range, range.upper);
const mouse_move_distance = @abs(hold_end_mouse - hold_start_mouse); // const mouse_move_distance = @abs(hold_end_mouse - hold_start_mouse);
if (signal.flags.contains(.left_released) and mouse_move_distance > 5) { // if (signal.flags.contains(.left_released) and mouse_move_distance > 5) {
_ = ctx.project.appendMarkedRange(view_id, axis, range); // _ = ctx.project.appendMarkedRange(view_id, axis, range);
} // }
} // }
} // }
} else if (ctx.view_controls.selected_tool == .marker) { // } else if (ctx.view_controls.selected_tool == .marker) {
if (cursor != null and signal.flags.contains(.left_released) and axis == .X) { // if (cursor != null and signal.flags.contains(.left_released) and axis == .X) {
view.markers.append(cursor.?) catch {}; // view.markers.append(cursor.?) catch {};
} // }
} // }
if (signal.flags.contains(.left_released)) { if (signal.flags.contains(.left_released)) {
ctx.view_controls.setCursorHoldStart(view_id, axis, null); ctx.view_controls.setCursorHoldStart(view_id, axis, null);

View File

@ -30,7 +30,7 @@ pub const ViewOptions = struct {
pub const RenderCache = struct { pub const RenderCache = struct {
const Key = struct { const Key = struct {
options: ViewOptions, options: ViewOptions,
drawn_x_range: RangeF64 drawn_x_range: RangeF64,
}; };
texture: ?rl.RenderTexture2D = null, texture: ?rl.RenderTexture2D = null,
@ -48,6 +48,14 @@ pub const RenderCache = struct {
self.key = null; 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 { pub fn draw(self: RenderCache, rect: rl.Rectangle) void {
if (self.texture) |texture| { if (self.texture) |texture| {
const source = rl.Rectangle{ const source = rl.Rectangle{
@ -323,7 +331,7 @@ pub fn drawCached(cache: *RenderCache, render_size: Vec2, options: ViewOptions,
const cache_key = RenderCache.Key{ const cache_key = RenderCache.Key{
.options = options, .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)) { if (cache.key != null and std.meta.eql(cache.key.?, cache_key)) {

View File

@ -119,12 +119,12 @@ pub fn main() !void {
if (app_config_dir.openFile("config.bin", .{})) |save_file| { if (app_config_dir.openFile("config.bin", .{})) |save_file| {
defer save_file.close(); defer save_file.close();
_ = try app.addView(.{ app.loadProject(save_file) catch |e| {
.file = try app.addFile("./samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_I.bin") log.err("Failed to load project: {}", .{e});
}); if (@errorReturnTrace()) |stack_trace| {
// app.loadProject(save_file) catch |e| { std.debug.dumpStackTrace(stack_trace.*);
// log.err("Failed to load project: {}", .{e}); }
// }; };
} else |e| switch (e) { } else |e| switch (e) {
error.FileNotFound => {}, error.FileNotFound => {},
else => return e 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", .{}); const save_file = try app_config_dir.createFile("config.bin", .{});
defer save_file.close(); defer save_file.close();
try app.saveProject(save_file); try app.saveProject(save_file);

View File

@ -22,8 +22,22 @@ const Id = App.Id;
app: *App, app: *App,
view_controls: ViewControlsSystem, view_controls: ViewControlsSystem,
view_solutions: bool = false, side_panel: union(enum) {
view_statistics_points: bool = false, 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){ modal: ?union(enum){
view_protocol: Id, // View id view_protocol: Id, // View id
notes notes
@ -42,6 +56,7 @@ notes_storage: UI.TextInputStorage,
// Project settings // Project settings
solution_input: UI.TextInputStorage, solution_input: UI.TextInputStorage,
gain_input: UI.TextInputStorage,
sample_rate_input: UI.TextInputStorage, sample_rate_input: UI.TextInputStorage,
experiment_name: UI.TextInputStorage, experiment_name: UI.TextInputStorage,
pipete_solution: UI.TextInputStorage, pipete_solution: UI.TextInputStorage,
@ -50,7 +65,6 @@ parsed_sample_rate: ?f64 = null,
// View settings // View settings
prev_view_settings: ?Id = null, // View ID prev_view_settings: ?Id = null, // View ID
transform_inputs: [App.View.max_transforms]UI.TextInputStorage,
view_name_input: UI.TextInputStorage, view_name_input: UI.TextInputStorage,
channel_save_file_picker: ?Platform.FilePickerId = null, channel_save_file_picker: ?Platform.FilePickerId = null,
file_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 { pub fn init(app: *App) !MainScreen {
const allocator = app.allocator; 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{ var self = MainScreen{
.app = app, .app = app,
.frequency_input = UI.TextInputStorage.init(allocator), .frequency_input = UI.TextInputStorage.init(allocator),
@ -73,8 +82,8 @@ pub fn init(app: *App) !MainScreen {
.experiment_name = UI.TextInputStorage.init(allocator), .experiment_name = UI.TextInputStorage.init(allocator),
.pipete_solution = UI.TextInputStorage.init(allocator), .pipete_solution = UI.TextInputStorage.init(allocator),
.view_name_input = UI.TextInputStorage.init(allocator), .view_name_input = UI.TextInputStorage.init(allocator),
.view_controls = ViewControlsSystem.init(&app.project), .gain_input = UI.TextInputStorage.init(allocator),
.transform_inputs = transform_inputs, .view_controls = ViewControlsSystem.init(app),
.preview_sample_list_id = try app.project.addSampleList(allocator) .preview_sample_list_id = try app.project.addSampleList(allocator)
}; };
@ -93,9 +102,7 @@ pub fn deinit(self: *MainScreen) void {
self.experiment_name.deinit(); self.experiment_name.deinit();
self.pipete_solution.deinit(); self.pipete_solution.deinit();
self.view_name_input.deinit(); self.view_name_input.deinit();
for (self.transform_inputs) |input| { self.gain_input.deinit();
input.deinit();
}
self.app.project.removeSampleList(self.preview_sample_list_id); self.app.project.removeSampleList(self.preview_sample_list_id);
self.clearProtocolErrorMessage(); self.clearProtocolErrorMessage();
@ -376,6 +383,7 @@ fn showProjectSettings(self: *MainScreen) !void {
{ {
_ = ui.label("Solution:", .{}); _ = ui.label("Solution:", .{});
try ui.textInput(.{ try ui.textInput(.{
.key = ui.keyFromString("Solution input"), .key = ui.keyFromString("Solution input"),
.storage = &self.solution_input, .storage = &self.solution_input,
@ -407,7 +415,7 @@ fn showProjectSettings(self: *MainScreen) !void {
const btn = ui.textButton("View solutions"); const btn = ui.textButton("View solutions");
btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 }); btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 });
if (ui.signal(btn).clicked()) { 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)) }); _ = 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(.{ const row = ui.createBox(.{
.size_x = UI.Sizing.initFitChildren(), .size_x = UI.Sizing.initFitChildren(),
.size_y = UI.Sizing.initFitChildren(), .size_y = UI.Sizing.initFitChildren(),
@ -439,7 +476,7 @@ fn showProjectSettings(self: *MainScreen) !void {
const btn = ui.textButton("View statistic points"); const btn = ui.textButton("View statistic points");
btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 }); btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 });
if (ui.signal(btn).clicked()) { 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)) }); _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) });
{ { // Open notes
const btn = ui.textButton("Open notes"); const btn = ui.textButton("Open notes");
btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 }); btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 });
if (ui.signal(btn).clicked()) { 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)) }); _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) });
{ { // Export folder
_ = ui.label("Export folder:", .{}); _ = ui.label("Export folder:", .{});
if (ui.fileInput(.{ if (ui.fileInput(.{
.allocator = self.app.allocator, .allocator = self.app.allocator,
@ -557,6 +594,8 @@ fn showViewSettings(self: *MainScreen, view_id: Id) !void {
} }
} }
_ = ui.label("Statistics:", .{});
switch (view.reference) { switch (view.reference) {
.channel => |channel_id| { .channel => |channel_id| {
const channel = project.channels.get(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_list = project.sample_lists.get(sample_list_id).?;
const sample_count = sample_list.getLength(); 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 "-" }); _ = 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| { // for (0.., view.transforms.slice()) |i, *_transform| {
const transform: *App.Transform = _transform; // const transform: *App.Transform = _transform;
const row = ui.createBox(.{ // const row = ui.createBox(.{
.key = UI.Key.initPtr(transform), // .key = UI.Key.initPtr(transform),
.size_x = UI.Sizing.initGrowFull(), // .size_x = UI.Sizing.initGrowFull(),
.size_y = UI.Sizing.initFixedPixels(ui.rem(1.5)), // .size_y = UI.Sizing.initFixedPixels(ui.rem(1.5)),
.layout_direction = .left_to_right // .layout_direction = .left_to_right
}); // });
row.beginChildren(); // row.beginChildren();
defer row.endChildren(); // defer row.endChildren();
if (ui.signal(ui.textButton("Remove")).clicked()) { // if (ui.signal(ui.textButton("Remove")).clicked()) {
deferred_remove.appendAssumeCapacity(i); // deferred_remove.appendAssumeCapacity(i);
} // }
{ // {
const options = .{ // const options = .{
.{ .multiply, "Multiply" }, // .{ .multiply, "Multiply" },
.{ .addition, "Addition" }, // .{ .addition, "Addition" },
.{ .sliding_window, "Sliding window" }, // .{ .sliding_window, "Sliding window" },
}; // };
const select = ui.button(ui.keyFromString("Transform select")); // const select = ui.button(ui.keyFromString("Transform select"));
select.setFmtText("{s}", .{switch (transform.*) { // select.setFmtText("{s}", .{switch (transform.*) {
.sliding_window => "Sliding window", // .sliding_window => "Sliding window",
.addition => "Addition", // .addition => "Addition",
.multiply => "Multiply" // .multiply => "Multiply"
}}); // }});
select.size.y = UI.Sizing.initGrowFull(); // select.size.y = UI.Sizing.initGrowFull();
select.alignment.y = .center; // select.alignment.y = .center;
if (ui.signal(select).clicked()) { // if (ui.signal(select).clicked()) {
select.persistent.open = !select.persistent.open; // select.persistent.open = !select.persistent.open;
} // }
if (select.persistent.open) { // if (select.persistent.open) {
const popup = ui.createBox(.{ // const popup = ui.createBox(.{
.key = ui.keyFromString("Select popup"), // .key = ui.keyFromString("Select popup"),
.size_x = UI.Sizing.initFixedPixels(ui.rem(10)), // .size_x = UI.Sizing.initFixedPixels(ui.rem(10)),
.size_y = UI.Sizing.initFitChildren(), // .size_y = UI.Sizing.initFitChildren(),
.flags = &.{ .clickable, .scrollable }, // .flags = &.{ .clickable, .scrollable },
.layout_direction = .top_to_bottom, // .layout_direction = .top_to_bottom,
.float_relative_to = select, // .float_relative_to = select,
.background = srcery.black, // .background = srcery.black,
.borders = UI.Borders.all(.{ .color = srcery.bright_black, .size = 4 }), // .borders = UI.Borders.all(.{ .color = srcery.bright_black, .size = 4 }),
.draw_on_top = true // .draw_on_top = true
}); // });
popup.setFloatPosition(0, select.persistent.size.y); // popup.setFloatPosition(0, select.persistent.size.y);
popup.beginChildren(); // popup.beginChildren();
defer popup.endChildren(); // defer popup.endChildren();
inline for (options) |option| { // inline for (options) |option| {
const select_option = ui.textButton(option[1]); // const select_option = ui.textButton(option[1]);
select_option.alignment.x = .start; // select_option.alignment.x = .start;
select_option.size.x = UI.Sizing.initGrowFull(); // select_option.size.x = UI.Sizing.initGrowFull();
select_option.borders = UI.Borders.all(.{ .color = srcery.hard_black, .size = 2 }); // select_option.borders = UI.Borders.all(.{ .color = srcery.hard_black, .size = 2 });
select_option.background = srcery.black; // select_option.background = srcery.black;
const signal = ui.signal(select_option); // const signal = ui.signal(select_option);
if (signal.clicked()) { // if (signal.clicked()) {
select.persistent.open = false; // select.persistent.open = false;
transform.* = switch (option[0]) { // transform.* = switch (option[0]) {
.sliding_window => App.Transform{ .sliding_window = sample_rate orelse 0 }, // .sliding_window => App.Transform{ .sliding_window = sample_rate orelse 0 },
.addition => App.Transform{ .addition = 0 }, // .addition => App.Transform{ .addition = 0 },
.multiply => App.Transform{ .multiply = 1 }, // .multiply => App.Transform{ .multiply = 1 },
else => unreachable // else => unreachable
}; // };
} // }
} // }
_ = ui.signal(popup); // _ = ui.signal(popup);
} // }
} // }
var input_opts = UI.NumberInputOptions{ // var input_opts = UI.NumberInputOptions{
.key = ui.keyFromString("Sliding window"), // .key = ui.keyFromString("Sliding window"),
.storage = &self.transform_inputs[i], // .storage = &self.transform_inputs[i],
.width = ui.rem(4) // .width = ui.rem(4)
// .postfix = if (sample_rate != null) " s" else null, // // .postfix = if (sample_rate != null) " s" else null,
// .display_scalar = sample_rate, // // .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) { // if (transform.* == .sliding_window and sample_rate != null) {
input_opts.postfix = " s"; // input_opts.postfix = " s";
input_opts.display_scalar = sample_rate; // input_opts.display_scalar = sample_rate;
} // }
const current_value = switch (transform.*) { // const current_value = switch (transform.*) {
.sliding_window => |*v| v, // .sliding_window => |*v| v,
.addition => |*v| v, // .addition => |*v| v,
.multiply => |*v| v // .multiply => |*v| v
}; // };
input_opts.initial = current_value.*; // input_opts.initial = current_value.*;
const new_value = try ui.numberInput(f64, input_opts); // const new_value = try ui.numberInput(f64, input_opts);
if (new_value != null) { // if (new_value != null) {
current_value.* = new_value.?; // current_value.* = new_value.?;
} // }
} // }
for (0..deferred_remove.len) |i| { // for (0..deferred_remove.len) |i| {
const transform_index = deferred_remove.get(deferred_remove.len - 1 - i); // const transform_index = deferred_remove.get(deferred_remove.len - 1 - i);
_ = view.transforms.orderedRemove(transform_index); // _ = view.transforms.orderedRemove(transform_index);
} // }
if (view.transforms.unusedCapacitySlice().len > 0) { // if (view.transforms.unusedCapacitySlice().len > 0) {
const btn = ui.textButton("Add transform"); // const btn = ui.textButton("Add transform");
if (ui.signal(btn).clicked()) { // if (ui.signal(btn).clicked()) {
view.transforms.appendAssumeCapacity(.{ .addition = 0 }); // view.transforms.appendAssumeCapacity(.{ .addition = 0 });
} // }
} // }
_ = ui.createBox(.{ .size_y = UI.Sizing.initGrowFull() }); _ = 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 { // fn showMarkedRange(self: *MainScreen, view_id: Id, index: usize) void {
var ui = &self.app.ui; // 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", .{}); // const label = ui.label("Selected range", .{});
label.borders.bottom = .{ // label.borders.bottom = .{
.color = srcery.blue, // .color = srcery.blue,
.size = 1 // .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 (marked_range.axis == .X and sample_rate != null) { // if (marked_range.axis == .X and sample_rate != null) {
_ = ui.label("From: {d:.3}s", .{ marked_range.range.lower / sample_rate.? }); // _ = 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("To: {d:.3}s", .{ marked_range.range.upper / sample_rate.? });
_ = ui.label("Size: {d:.3}s", .{ marked_range.range.size() / sample_rate.? }); // _ = ui.label("Size: {d:.3}s", .{ marked_range.range.size() / sample_rate.? });
} else { // } else {
_ = ui.label("From: {d:.2}", .{ marked_range.range.lower }); // _ = ui.label("From: {d:.2}", .{ marked_range.range.lower });
_ = ui.label("To: {d:.2}", .{ marked_range.range.upper }); // _ = ui.label("To: {d:.2}", .{ marked_range.range.upper });
_ = ui.label("Size: {d:.2}", .{ marked_range.range.size() }); // _ = 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.axis == .X) {
if (marked_range.min) |min| { // if (marked_range.min) |min| {
_ = ui.label("Minimum: {d:.3}", .{ min }); // _ = ui.label("Minimum: {d:.3}", .{ min });
} else{ // } else{
_ = ui.label("Minimum: <unknown>", .{}); // _ = ui.label("Minimum: <unknown>", .{});
} // }
if (marked_range.max) |max| { // if (marked_range.max) |max| {
_ = ui.label("Maximum: {d:.3}", .{ max }); // _ = ui.label("Maximum: {d:.3}", .{ max });
} else{ // } else{
_ = ui.label("Maximum: <unknown>", .{}); // _ = ui.label("Maximum: <unknown>", .{});
} // }
if (marked_range.average) |average| { // if (marked_range.average) |average| {
_ = ui.label("Average: {d:.3}", .{ average }); // _ = ui.label("Average: {d:.3}", .{ average });
} else{ // } else{
_ = ui.label("Average: <unknown>", .{}); // _ = ui.label("Average: <unknown>", .{});
} // }
if (marked_range.standard_deviation) |standard_deviation| { // if (marked_range.standard_deviation) |standard_deviation| {
_ = ui.label("Standard deviation: {d:.3}", .{ standard_deviation }); // _ = ui.label("Standard deviation: {d:.3}", .{ standard_deviation });
} else{ // } else{
_ = ui.label("Standard deviation: <unknown>", .{}); // _ = ui.label("Standard deviation: <unknown>", .{});
} // }
} // }
_ = ui.createBox(.{ .size_y = UI.Sizing.initGrowFull() }); // _ = ui.createBox(.{ .size_y = UI.Sizing.initGrowFull() });
{ // {
const btn = ui.textButton("Remove"); // const btn = ui.textButton("Remove");
btn.borders = UI.Borders.all(.{ .color = srcery.red, .size = 4 }); // btn.borders = UI.Borders.all(.{ .color = srcery.red, .size = 4 });
const signal = ui.signal(btn); // const signal = ui.signal(btn);
if (signal.clicked() or ui.isKeyboardPressed(.key_backspace)) { // if (signal.clicked() or ui.isKeyboardPressed(.key_backspace)) {
self.view_controls.show_marked_range = null; // self.side_panel = .project;
_ = view.marked_ranges.swapRemove(index); // _ = view.marked_ranges.swapRemove(index);
} // }
} // }
} // }
fn showMarker(self: *MainScreen, view_id: Id, index: usize) void { // fn showMarker(self: *MainScreen, view_id: Id, index: usize) void {
var ui = &self.app.ui; // 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", .{}); // const label = ui.label("Selected range", .{});
label.borders.bottom = .{ // label.borders.bottom = .{
.color = srcery.blue, // .color = srcery.blue,
.size = 1 // .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) { // if (sample_rate != null) {
const duration = utils.formatDuration(ui.frameAllocator(), marker / sample_rate.?) catch ""; // const duration = utils.formatDuration(ui.frameAllocator(), marker / sample_rate.?) catch "";
_ = ui.label("Position: {s}", .{ duration }); // _ = ui.label("Position: {s}", .{ duration });
} else { // } else {
_ = ui.label("Position: {d:.2}", .{ marker }); // _ = ui.label("Position: {d:.2}", .{ marker });
} // }
} // }
fn showToolbar(self: *MainScreen) void { fn showToolbar(self: *MainScreen) void {
var ui = &self.app.ui; var ui = &self.app.ui;
@ -945,38 +984,38 @@ fn showToolbar(self: *MainScreen) void {
_ = ui.createBox(.{ .size_x = UI.Sizing.initFixedPixels(ui.rem(1)) }); _ = ui.createBox(.{ .size_x = UI.Sizing.initFixedPixels(ui.rem(1)) });
{ // {
var btn = ui.textButton("Move"); // var btn = ui.textButton("Move");
if (self.view_controls.selected_tool == .move) { // if (self.view_controls.selected_tool == .move) {
btn.borders = UI.Borders.bottom(.{ .size = 4, .color = srcery.green }); // btn.borders = UI.Borders.bottom(.{ .size = 4, .color = srcery.green });
} // }
if (ui.signal(btn).clicked()) { // if (ui.signal(btn).clicked()) {
self.view_controls.selected_tool = .move; // self.view_controls.selected_tool = .move;
} // }
} // }
{ // {
var btn = ui.textButton("Select"); // var btn = ui.textButton("Select");
if (self.view_controls.selected_tool == .select) { // if (self.view_controls.selected_tool == .select) {
btn.borders = UI.Borders.bottom(.{ .size = 4, .color = srcery.green }); // btn.borders = UI.Borders.bottom(.{ .size = 4, .color = srcery.green });
} // }
if (ui.signal(btn).clicked()) { // if (ui.signal(btn).clicked()) {
self.view_controls.selected_tool = .select; // self.view_controls.selected_tool = .select;
} // }
} // }
{ // {
var btn = ui.textButton("Marker"); // var btn = ui.textButton("Marker");
if (self.view_controls.selected_tool == .marker) { // if (self.view_controls.selected_tool == .marker) {
btn.borders = UI.Borders.bottom(.{ .size = 4, .color = srcery.green }); // btn.borders = UI.Borders.bottom(.{ .size = 4, .color = srcery.green });
} // }
if (ui.signal(btn).clicked()) { // if (ui.signal(btn).clicked()) {
self.view_controls.selected_tool = .marker; // self.view_controls.selected_tool = .marker;
} // }
} // }
} }
fn showSolutions(self: *MainScreen) !void { fn showSolutions(self: *MainScreen) !void {
@ -1024,15 +1063,14 @@ fn showSolutions(self: *MainScreen) !void {
const btn = ui.textButton("Close"); const btn = ui.textButton("Close");
btn.borders = UI.Borders.all(.{ .color = srcery.red, .size = 4 }); btn.borders = UI.Borders.all(.{ .color = srcery.red, .size = 4 });
if (ui.signal(btn).clicked()) { if (ui.signal(btn).clicked()) {
self.view_solutions = false; self.side_panel = .project;
} }
} }
} }
fn showStatisticsPoints(self: *MainScreen) !void { fn showStatisticsPoints(self: *MainScreen) !void {
var ui = &self.app.ui; const ui = &self.app.ui;
const project = &self.app.project; const project = &self.app.project;
_ = &ui;
{ {
const label = ui.label("Statistics points", .{}); const label = ui.label("Statistics points", .{});
@ -1070,7 +1108,72 @@ fn showStatisticsPoints(self: *MainScreen) !void {
const btn = ui.textButton("Close"); const btn = ui.textButton("Close");
btn.borders = UI.Borders.all(.{ .color = srcery.red, .size = 4 }); btn.borders = UI.Borders.all(.{ .color = srcery.red, .size = 4 });
if (ui.signal(btn).clicked()) { 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)) }); _ = ui.createBox(.{ .size_x = UI.Sizing.initFixedPixels(ui.rem(18)) });
if (self.view_solutions) { switch (self.side_panel) {
try self.showSolutions(); .project => {
} else if (self.view_controls.show_marker) |marker| { try self.showProjectSettings();
self.showMarker(marker.view_id, marker.index); },
} else if (self.view_controls.show_marked_range) |show_marked_range| { .solutions => {
self.showMarkedRange(show_marked_range.view_id, show_marked_range.index); try self.showSolutions();
} else if (self.view_controls.view_settings) |view_id| { },
try self.showViewSettings(view_id); .statistics_points => {
} else if (self.view_statistics_points) { try self.showStatisticsPoints();
try self.showStatisticsPoints(); },
} else { .gain_changes => {
try self.showProjectSettings(); 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 (ui.isKeyboardPressed(.key_escape)) {
if (self.modal != null) { if (self.modal != null) {
self.modal = null; self.modal = null;
} else if (self.view_controls.view_settings != null) { } else if (self.side_panel != .project) {
self.view_controls.view_settings = null; self.side_panel = .project;
} 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 { } else {
self.app.should_close = true; self.app.should_close = true;
} }

View File

@ -2779,8 +2779,9 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void {
storage.editing = true; storage.editing = true;
} }
var stop_editing = false;
if (self.isKeyActiveAny() and !container_signal.active) { if (self.isKeyActiveAny() and !container_signal.active) {
storage.editing = false; stop_editing = true;
} }
// Text input controls // Text input controls
@ -2952,7 +2953,7 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void {
} }
if (self.isKeyboardPressed(.key_escape)) { if (self.isKeyboardPressed(.key_escape)) {
storage.editing = false; stop_editing = true;
} }
if (self.isKeyboardPressed(.key_enter)) { if (self.isKeyboardPressed(.key_enter)) {
@ -2975,9 +2976,15 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void {
} }
if (!opts.editable) { 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 { pub fn numberInput(self: *UI, T: type, opts: NumberInputOptions) !?T {