From 25457745752ec2b7bf41655793ac271e84d6eb50 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Wed, 7 May 2025 22:30:46 +0300 Subject: [PATCH] add basic UI for adding and removing transforms --- src/app.zig | 384 +++++++++++++++++++++++------------ src/screens/main_screen.zig | 153 ++++++++++++-- src/ui.zig | 391 ++++++++++++++++++++++-------------- 3 files changed, 628 insertions(+), 300 deletions(-) diff --git a/src/app.zig b/src/app.zig index 6fd5409..94cc5af 100644 --- a/src/app.zig +++ b/src/app.zig @@ -166,6 +166,123 @@ fn GenerationalArray(Item: type) type { }; } +const SumRingBuffer = struct { + buffer: []f64, + len: usize = 0, + last_index: usize = 0, + + sum: f64 = 0, + + pub fn init(buffer: []f64) SumRingBuffer { + return SumRingBuffer{ + .buffer = buffer + }; + } + + pub fn append(self: *SumRingBuffer, sample: f64) void { + if (self.len < self.buffer.len) { + self.buffer[self.len] = sample; + self.len += 1; + } else { + self.sum -= self.buffer[self.last_index]; + self.buffer[self.last_index] = sample; + self.last_index = @mod(self.last_index + 1, self.buffer.len); + } + + self.sum += sample; + } +}; + +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 fn apply(self: Transform, state: *State, destination: []f64, source: []f64) void { + switch (self) { + .sliding_window => { + const sum_ring_buffer = &(state.sum_ring_buffer.?); + + 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); + + destination[i] = sum_ring_buffer.sum / @as(f64, @floatFromInt(sum_ring_buffer.len)); + } + } + + }, + .multiply => |scalar| { + for (0..source.len) |i| { + destination[i] = scalar * source[i]; + } + }, + .addition => |offset| { + for (0..source.len) |i| { + destination[i] = offset + source[i]; + } + } + } + } + + pub fn eqlSlice(slice1: []const Transform, slice2: []const Transform) bool { + if (slice1.len != slice2.len) { + return false; + } + for (0.., slice1) |i, transform1| { + if (!std.meta.eql(transform1, slice2[i])) { + return false; + } + } + + return true; + } +}; + pub const SampleList = struct { pub const Block = struct { pub const Id = usize; @@ -348,6 +465,20 @@ pub const SampleList = struct { } } + fn appendToBlock(self: *SampleList, block: *Block, samples: []const f64) usize { + const appended = block.append(samples); + + if (block.min) |block_min| { + self.min = @min(self.min orelse block_min, block_min); + } + + if (block.max) |block_max| { + self.max = @max(self.max orelse block_max, block_max); + } + + return appended; + } + pub fn append(self: *SampleList, samples: []const f64) !void { if (samples.len == 0) return; @@ -358,15 +489,7 @@ pub const SampleList = struct { } const last_block = &self.blocks.items[self.blocks.items.len - 1]; - appended_count += last_block.append(samples[appended_count..]); - - if (last_block.min) |block_min| { - self.min = @min(self.min orelse block_min, block_min); - } - - if (last_block.max) |block_max| { - self.max = @max(self.max orelse block_max, block_max); - } + appended_count += self.appendToBlock(last_block, samples[appended_count..]); } } @@ -417,6 +540,62 @@ 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(); + } + } + test { var sample_list = SampleList.init(std.testing.allocator); defer sample_list.deinit(); @@ -596,6 +775,9 @@ pub const File = struct { }; pub const View = struct { + pub const max_transforms = 16; + pub const BoundedTransformsArray = std.BoundedArray(Transform, max_transforms); + pub const MarkedRange = struct { // Persistent axis: UI.Axis, @@ -698,7 +880,7 @@ pub const View = struct { graph_opts: Graph.ViewOptions = .{}, sync_controls: bool = false, marked_ranges: std.BoundedArray(MarkedRange, 32) = .{}, - sliding_window: ?f64 = null, + transforms: BoundedTransformsArray = .{}, // Runtime graph_cache: Graph.RenderCache = .{}, @@ -707,6 +889,7 @@ pub const View = struct { unit: ?NIDaq.Unit = .Voltage, transformed_samples: ?Id = null, + computed_transforms: BoundedTransformsArray = .{}, pub fn clear(self: *View) void { self.graph_cache.clear(); @@ -1222,10 +1405,6 @@ pub const Command = union(enum) { start_output: Id, // Channel id add_file_from_picker, reload_file: Id, // File id - update_sliding_window: struct { - view_id: Id, - sliding_window: ?f64 - } }; pub const CollectionTask = struct { @@ -1243,12 +1422,13 @@ pub const CollectionTask = struct { const WorkJob = struct { const Stage = enum { init, - calculate_blocks + launch_threads, + finished }; view_id: Id, stage: Stage = .init, - sliding_window: ?f64 = null, + transforms: View.BoundedTransformsArray = .{}, mutex: std.Thread.Mutex = .{}, running_thread_jobs: std.ArrayListUnmanaged(SampleList.Block.Id) = .{}, @@ -1289,18 +1469,26 @@ const WorkJob = struct { self.running_thread_jobs.deinit(allocator); } - pub fn update(self: *WorkJob, id: Id, app: *App) !bool { + pub fn update(self: *WorkJob, app: *App) !bool { + if (self.stage == .finished) { + return true; + } + 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 = project.sample_lists.get(sample_list_id).?; - if (view.sliding_window != self.sliding_window) return true; + const transforms = self.transforms.constSlice(); + if (!Transform.eqlSlice(view.transforms.constSlice(), transforms)) { + // Transforms changed, job needs to be cancelled. + return true; + } switch (self.stage) { .init => { - if (self.sliding_window == null) { + if (transforms.len == 0) { if (view.transformed_samples) |transformed_samples_id| { project.removeSampleList(transformed_samples_id); } @@ -1314,29 +1502,51 @@ const WorkJob = struct { const transformed_samples = project.sample_lists.get(view.transformed_samples.?).?; try transformed_samples.reserveEmptyBlocks(sample_list.blocks.items.len); - self.stage = .calculate_blocks; + self.stage = .launch_threads; } }, - .calculate_blocks => { - const max_block_to_process = 32; + .launch_threads => { + const max_block_to_process = 256; while (self.getRunningThreadCount() < app.work_thread_pool.threads.len and self.processed_up_to < sample_list.blocks.items.len) { const block_id = self.processed_up_to; const block_count = @min(sample_list.blocks.items.len - self.processed_up_to, max_block_to_process); self.processed_up_to += block_count; - try app.work_thread_pool.spawn(transformedSamplesWorker, .{ app, id, block_id, block_count }); + try app.work_thread_pool.spawn(workThread, .{ self, project, block_id, block_count }); try self.appendRunningThread(app.allocator, block_id); } - if (self.processed_up_to == sample_list.blocks.items.len and self.getRunningThreadCount() == 0) { + if (self.processed_up_to == sample_list.blocks.items.len) { return true; } - } + }, + .finished => unreachable } return false; } + + fn workThread(self: *WorkJob, project: *Project, from_block_id: SampleList.Block.Id, block_count: SampleList.Block.Len) void { + defer self.removeRunningThread(from_block_id); + + // var timer = std.time.Timer.start() catch unreachable; + // defer { + // const duration = timer.read(); + // std.debug.print("finished {d:.5}ms\n", .{ @as(f64, @floatFromInt(duration)) / std.time.ns_per_ms }); + // } + + 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 sample_list_id = project.getViewSampleListId(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}); + }; + } }; allocator: Allocator, @@ -1643,31 +1853,39 @@ pub fn tick(self: *App) !void { self.loadFile(file_id) catch |e| { log.err("Failed to load file: {}", .{ e }); }; - }, - .update_sliding_window => |args| { - const view = self.project.views.get(args.view_id) orelse continue; - view.sliding_window = args.sliding_window; - - _ = self.work_jobs.insert(WorkJob{ - .view_id = args.view_id, - .sliding_window = args.sliding_window - }) catch |e| { - log.err("Failed to create a work job: {}", .{ e }); - continue; - }; } } } + { + var view_iter = self.project.views.idIterator(); + while (view_iter.next()) |view_id| { + const view = self.project.views.get(view_id).?; + + if (Transform.eqlSlice(view.computed_transforms.constSlice(), view.transforms.constSlice())) { + continue; + } + + _ = 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()); + } + } + { var work_job_iter = self.work_jobs.idIterator(); while (work_job_iter.next()) |work_job_id| { const work_job = self.work_jobs.get(work_job_id).?; - const job_done = try work_job.update(work_job_id, self); + const job_done = try work_job.update(self); if (job_done) { + work_job.stage = .finished; if (work_job.getRunningThreadCount() == 0) { - // std.debug.print("job done {}\n", .{work_job_id}); + std.debug.print("job done {}\n", .{work_job_id}); work_job.deinit(self.allocator); self.work_jobs.remove(work_job_id); } @@ -1676,94 +1894,6 @@ pub fn tick(self: *App) !void { } } -fn transformedSamplesWorker(self: *App, work_job_id: Id, starting_block_id: SampleList.Block.Id, block_count: usize) void { - const work_job = self.work_jobs.get(work_job_id) orelse return; - defer work_job.removeRunningThread(starting_block_id); - - const allocator = self.allocator; - var timer = std.time.Timer.start() catch unreachable; - - const view = self.project.views.get(work_job.view_id) orelse return; - const transformed_samples_id = view.transformed_samples orelse return; - const transformed_samples = self.project.sample_lists.get(transformed_samples_id) orelse return; - const sliding_window_f64: f64 = @ceil(view.sliding_window.?); - const sliding_window: usize = @intFromFloat(sliding_window_f64); - - const sample_list_id = self.project.getViewSampleListId(work_job.view_id); - const sample_list = self.project.sample_lists.get(sample_list_id) orelse return; - - var running_sum: f64 = 0; - var last_samples = std.ArrayList(f64).init(allocator); - defer last_samples.deinit(); - last_samples.ensureTotalCapacityPrecise(sliding_window) catch return; - - for (0..@intFromFloat(@ceil(sliding_window_f64/SampleList.Block.capacity))) |block_offset| { - if (block_offset >= starting_block_id) { - break; - } - - const source_block = sample_list.getBlock(starting_block_id - (block_offset + 1)).?; - for (0..source_block.len) |i| { - const sample = source_block.buffer[source_block.len - (i + 1)]; - - if (last_samples.items.len == last_samples.capacity) { - _ = last_samples.orderedRemove(0); - } - last_samples.appendAssumeCapacity(sample); - } - } - - for (0..block_count) |i| { - const block_id = starting_block_id + i; - - const source_block = sample_list.getBlock(block_id).?; - const transformed_block = transformed_samples.getBlock(block_id).?; - - for (0..source_block.len) |j| { - const sample = source_block.buffer[j]; - transformed_block.buffer[j] = sample * 0.1; - - if (last_samples.items.len == last_samples.capacity) { - running_sum -= last_samples.orderedRemove(0); - } - last_samples.appendAssumeCapacity(sample); - running_sum += sample; - - transformed_block.buffer[j] = running_sum / @as(f64, @floatFromInt(last_samples.items.len)); - } - transformed_block.len = source_block.len; - transformed_block.recomputeMinMax(); - } - - // for (0..(SampleList.Block.capacity * block_count)) |offset| { - // const i = starting_block_id * SampleList.Block.capacity + offset; - - // const transformed_sample = &transformed_samples.getBlock(@divFloor(i, SampleList.Block.capacity)).?.buffer[@mod(i, SampleList.Block.capacity)]; - - // if (i >= 3) { - // const zero: f64 = 0; - // const sample1 = (sample_list.getSample(i) orelse &zero).*; - // const sample2 = (sample_list.getSample(i-1) orelse &zero).*; - // const sample3 = (sample_list.getSample(i-2) orelse &zero).*; - // const sample4 = (sample_list.getSample(i-3) orelse &zero).*; - - // // if (sample_list.getSample(i)) |sample| { - // // } - // transformed_sample.* = @tan(sample1 + sample2 + sample3 + sample4); - // } else { - // transformed_sample.* = 0; - // } - // } - - // for (0..block_count) |i| { - // transformed_samples.getBlock(starting_block_id + i).?.len = sample_list.getBlock(starting_block_id + i).?.len; - // } - - const duration = timer.read(); - _ = duration; - // std.debug.print("finished {d:.5}ms\n", .{ @as(f64, @floatFromInt(duration)) / std.time.ns_per_ms }); -} - pub fn pushCommand(self: *App, command: Command) void { self.command_queue.append(command) catch { log.warn("Failed to push a command, ignoring it", .{}); diff --git a/src/screens/main_screen.zig b/src/screens/main_screen.zig index 1956ba4..853f0bd 100644 --- a/src/screens/main_screen.zig +++ b/src/screens/main_screen.zig @@ -36,20 +36,25 @@ sample_rate_input: UI.TextInputStorage, parsed_sample_rate: ?f64 = null, // View settings -sliding_window_input: UI.TextInputStorage, +transform_inputs: [App.View.max_transforms]UI.TextInputStorage, channel_save_file_picker: ?Platform.FilePickerId = null, 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), .amplitude_input = UI.TextInputStorage.init(allocator), .sample_rate_input = UI.TextInputStorage.init(allocator), .view_controls = ViewControlsSystem.init(&app.project), - .sliding_window_input = UI.TextInputStorage.init(allocator), + .transform_inputs = transform_inputs, .preview_sample_list_id = try app.project.addSampleList(allocator) }; @@ -63,7 +68,9 @@ pub fn deinit(self: *MainScreen) void { self.frequency_input.deinit(); self.amplitude_input.deinit(); self.sample_rate_input.deinit(); - self.sliding_window_input.deinit(); + for (self.transform_inputs) |input| { + input.deinit(); + } self.app.project.removeSampleList(self.preview_sample_list_id); self.clearProtocolErrorMessage(); @@ -275,17 +282,12 @@ fn showProjectSettings(self: *MainScreen) !void { placeholder = try std.fmt.allocPrint(frame_allocator, "{d}", .{ default_sample_rate }); } - var initial: ?[]const u8 = null; - if (project.sample_rate) |selected_sample_rate| { - initial = try std.fmt.allocPrint(frame_allocator, "{d}", .{ selected_sample_rate }); - } - _ = ui.label("Sample rate", .{}); self.parsed_sample_rate = try ui.numberInput(f64, .{ .key = ui.keyFromString("Sample rate input"), .storage = &self.sample_rate_input, .placeholder = placeholder, - .initial = initial, + .initial = project.sample_rate, .invalid = self.parsed_sample_rate != project.sample_rate, .editable = !self.app.isCollectionInProgress() }); @@ -379,28 +381,133 @@ fn showViewSettings(self: *MainScreen, view_id: Id) !void { _ = ui.label("Samples: {d}", .{ sample_count }); - var duration_str: []const u8 = "-"; + var duration_str: ?[]const u8 = null; if (sample_rate != null) { const duration = @as(f64, @floatFromInt(sample_count)) / sample_rate.?; if (utils.formatDuration(ui.frameAllocator(), duration)) |str| { duration_str = str; } else |_| {} } - _ = ui.label("Duration: {s}", .{ duration_str }); + if (duration_str == null) { + duration_str = std.fmt.allocPrint(ui.frameAllocator(), "{d}", .{ sample_count }) catch null; + } - const new_sliding_window = try ui.numberInput(f64, .{ - .key = ui.keyFromString("Sliding window"), - .storage = &self.sliding_window_input - }); + _ = ui.label("Duration: {s}", .{ duration_str orelse "-" }); - if (new_sliding_window != view.sliding_window) { - if (new_sliding_window == null or new_sliding_window.? > 0) { - self.app.pushCommand(.{ - .update_sliding_window = .{ - .view_id = view_id, - .sliding_window = new_sliding_window + var deferred_remove: std.BoundedArray(usize, App.View.max_transforms) = .{}; + + 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(); + + if (ui.signal(ui.textButton("Remove")).clicked()) { + deferred_remove.appendAssumeCapacity(i); + } + + { + 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" + }}); + + 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(); + + 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 + }; + } } - }); + + _ = 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, + }; + + _ = 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; + } + + 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.?; + } + } + + 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 }); } } } @@ -434,6 +541,8 @@ fn showMarkedRange(self: *MainScreen, view_id: Id, index: usize) void { _ = ui.label("Size: {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 }); diff --git a/src/ui.zig b/src/ui.zig index e6c23cd..6dda86b 100644 --- a/src/ui.zig +++ b/src/ui.zig @@ -480,6 +480,7 @@ pub const Box = struct { sroll_offset: f32 = 0, hot: f32 = 0, active: f32 = 0, + open: bool = false }; pub const Flag = enum { @@ -538,6 +539,7 @@ pub const Box = struct { tooltip: ?[]const u8 = null, float_x: ?f32 = null, float_y: ?f32 = null, + draw_on_top: bool = false, // Variables that you probably shouldn't be touching last_used_frame: u64 = 0, @@ -576,6 +578,14 @@ pub const Box = struct { }; } + pub fn iterChildrenDeep(self: *const Box) BoxChildDeepIterator { + return BoxChildDeepIterator{ + .root = self.tree.index, + .boxes = self.ui.boxes.slice(), + .current_child = self.tree.first_child_index + }; + } + pub fn iterParents(self: *const Box) BoxParentIterator { return BoxParentIterator{ .boxes = self.ui.boxes.slice(), @@ -610,6 +620,15 @@ pub const Box = struct { self.text_lines.len = 0; } + pub fn appendText(self: *Box, text: []const u8) void { + if (self.text) |self_text| { + self.text = std.mem.concat(self.allocator, u8, &.{ self_text, text }) catch return; + self.text_lines.len = 0; + } else { + self.setText(text); + } + } + pub fn setFloatX(self: *Box, x: f32) void { self.float_x = x; } @@ -618,6 +637,11 @@ pub const Box = struct { self.float_y = y; } + pub fn setFloatPosition(self: *Box, x: f32, y: f32) void { + self.setFloatX(x); + self.setFloatY(y); + } + pub fn setFloatRect(self: *Box, float_rect: Rect) void { self.setFloatX(float_rect.x); self.setFloatY(float_rect.y); @@ -760,7 +784,8 @@ pub const BoxOptions = struct { texture_color: ?rl.Color = null, draw: ?Box.Draw = null, visual_hot: ?bool = null, - visual_active: ?bool = null + visual_active: ?bool = null, + draw_on_top: ?bool = null }; pub const root_box_key = Key.initString(0, "$root$"); @@ -781,6 +806,33 @@ const BoxChildIterator = struct { } }; +const BoxChildDeepIterator = struct { + root: BoxIndex, + current_child: ?BoxIndex, + boxes: []Box, + + pub fn next(self: *BoxChildDeepIterator) ?*Box { + const current_child = self.current_child orelse return null; + + const box = &self.boxes[current_child]; + + var next_box: ?BoxIndex = null; + if (box.tree.first_child_index) |first_child_index| { + next_box = first_child_index; + } else if (box.tree.next_sibling_index) |next_sibling_index| { + next_box = next_sibling_index; + } else if (box.tree.parent_index) |parent_index| { + if (parent_index != self.root) { + const parent = &self.boxes[parent_index]; + next_box = parent.tree.next_sibling_index; + } + } + + self.current_child = next_box; + return box; + } +}; + const BoxParentIterator = struct { current_parent: ?BoxIndex, boxes: []Box, @@ -1497,6 +1549,12 @@ fn layoutFloatingPositions(self: *UI, box: *Box, axis: Axis) void { const axis_position_target = vec2ByAxis(&target.persistent.position, axis); axis_position.* += axis_position_target.*; + + var child_iter = box.iterChildrenDeep(); + while (child_iter.next()) |child| { + const child_axis_position = vec2ByAxis(&child.persistent.position, axis); + child_axis_position.* += axis_position_target.*; + } } var child_iter = box.iterChildren(); @@ -1589,6 +1647,7 @@ pub fn createBox(self: *UI, opts: BoxOptions) *Box { .draw = opts.draw, .visual_hot = opts.visual_hot orelse false, .visual_active = opts.visual_active orelse false, + .draw_on_top = opts.draw_on_top orelse false, .last_used_frame = self.frame_index, .key = key, @@ -1671,186 +1730,195 @@ pub fn draw(self: *UI) void { const root_box = self.getBoxByKey(root_box_key).?; const mouse_tooltip = self.getBoxByKey(mouse_tooltip_box_key).?; - self.drawBox(root_box); + self.drawBox(root_box, false); + self.drawBox(root_box, true); if (mouse_tooltip.hasChildren()) { - self.drawBox(mouse_tooltip); + self.drawBox(mouse_tooltip, null); } } -fn drawBox(self: *UI, box: *Box) void { - const box_rect = box.rect(); +fn drawBox(self: *UI, box: *Box, on_top_pass: ?bool) void { + var child_on_top_pass = on_top_pass; - const do_scissor = box.flags.contains(.clip_view); - if (do_scissor) self.beginScissor(box_rect); - defer if (do_scissor) self.endScissor(); + if (on_top_pass == null or box.draw_on_top == on_top_pass) { + const box_rect = box.rect(); - var value_shift: f32 = 0; - if (box.flags.contains(.draw_active) and box.persistent.active > 0.01) { - value_shift = -0.5 * box.persistent.active; - } else if (box.flags.contains(.draw_hot) and box.persistent.hot > 0.01) { - value_shift = 0.6 * box.persistent.hot; - } + const do_scissor = box.flags.contains(.clip_view); + if (do_scissor) self.beginScissor(box_rect); + defer if (do_scissor) self.endScissor(); - if (box.background) |bg| { - rl.drawRectangleRec(box_rect, utils.shiftColorInHSV(bg, value_shift)); - } - - if (box.texture) |texture| { - const source = rl.Rectangle{ - .x = 0, - .y = 0, - .width = @floatFromInt(texture.width), - .height = @floatFromInt(texture.height) - }; - var destination = box_rect; - if (box.texture_size) |texture_size| { - destination = rect_utils.initCentered(destination, texture_size.x, texture_size.y); + var value_shift: f32 = 0; + if (box.flags.contains(.draw_active) and box.persistent.active > 0.01) { + value_shift = -0.5 * box.persistent.active; + } else if (box.flags.contains(.draw_hot) and box.persistent.hot > 0.01) { + value_shift = 0.6 * box.persistent.hot; } - rl.drawTexturePro( - texture, - source, - destination, - rl.Vector2.zero(), - 0, - box.texture_color orelse rl.Color.white - ); - } + if (box.background) |bg| { + rl.drawRectangleRec(box_rect, utils.shiftColorInHSV(bg, value_shift)); + } - const borders_with_coords = .{ - .{ box.borders.left , rl.Vector2.init(0, 0), rl.Vector2.init(0, 1), rl.Vector2.init( 1, 0) }, - .{ box.borders.right , rl.Vector2.init(1, 0), rl.Vector2.init(1, 1), rl.Vector2.init(-1, 0) }, - .{ box.borders.top , rl.Vector2.init(0, 0), rl.Vector2.init(1, 0), rl.Vector2.init( 0, 1) }, - .{ box.borders.bottom, rl.Vector2.init(0, 1), rl.Vector2.init(1, 1), rl.Vector2.init( 0, -1) } - }; - inline for (borders_with_coords) |border_with_coords| { - const border = border_with_coords[0]; - const line_from = border_with_coords[1]; - const line_to = border_with_coords[2]; - const inset_direction: rl.Vector2 = border_with_coords[3]; + if (box.texture) |texture| { + const source = rl.Rectangle{ + .x = 0, + .y = 0, + .width = @floatFromInt(texture.width), + .height = @floatFromInt(texture.height) + }; + var destination = box_rect; + if (box.texture_size) |texture_size| { + destination = rect_utils.initCentered(destination, texture_size.x, texture_size.y); + } - if (border.size > 0) { - const inset = inset_direction.multiply(Vec2.init(border.size/2, border.size/2)); - rl.drawLineEx( - rect_utils.positionAt(box_rect, line_from).add(inset), - rect_utils.positionAt(box_rect, line_to).add(inset), - border.size, - utils.shiftColorInHSV(border.color, value_shift) + rl.drawTexturePro( + texture, + source, + destination, + rl.Vector2.zero(), + 0, + box.texture_color orelse rl.Color.white ); } - } - if (box.draw) |box_draw| { - box_draw.do(box_draw.ctx, box); - } + const borders_with_coords = .{ + .{ box.borders.left , rl.Vector2.init(0, 0), rl.Vector2.init(0, 1), rl.Vector2.init( 1, 0) }, + .{ box.borders.right , rl.Vector2.init(1, 0), rl.Vector2.init(1, 1), rl.Vector2.init(-1, 0) }, + .{ box.borders.top , rl.Vector2.init(0, 0), rl.Vector2.init(1, 0), rl.Vector2.init( 0, 1) }, + .{ box.borders.bottom, rl.Vector2.init(0, 1), rl.Vector2.init(1, 1), rl.Vector2.init( 0, -1) } + }; + inline for (borders_with_coords) |border_with_coords| { + const border = border_with_coords[0]; + const line_from = border_with_coords[1]; + const line_to = border_with_coords[2]; + const inset_direction: rl.Vector2 = border_with_coords[3]; - const alignment_x_coeff = box.alignment.x.getCoefficient(); - const alignment_y_coeff = box.alignment.y.getCoefficient(); - - if (box.text) |text| { - const font_face = Assets.font(box.font); - var text_position = box.persistent.position; - text_position.x += box.padding.left; - text_position.y += box.padding.top; - - const lines: [][]u8 = box.text_lines.slice(); - - const available_width = box.availableChildrenSize(.X); - const available_height = box.availableChildrenSize(.Y); - - if (lines.len == 0) { - const text_size = font_face.measureText(text); - text_position.x += (available_width - text_size.x) * alignment_x_coeff; - text_position.y += (available_height - text_size.y) * alignment_y_coeff; - - font_face.drawText(text, text_position, box.text_color); - } else { - // TODO: Don't call `measureTextLines`, - // Because in the end `measureText` will be called twice for each line - const text_size = font_face.measureTextLines(lines); - text_position.x += (available_width - text_size.x) * alignment_x_coeff; - text_position.y += (available_height - text_size.y) * alignment_y_coeff; - - var offset_y: f32 = 0; - - for (lines) |line| { - const line_size = font_face.measureText(line); - const offset_x = (text_size.x - line_size.x) * alignment_x_coeff; - - font_face.drawText( - line, - text_position.add(.{ .x = offset_x, .y = offset_y }), - box.text_color + if (border.size > 0) { + const inset = inset_direction.multiply(Vec2.init(border.size/2, border.size/2)); + rl.drawLineEx( + rect_utils.positionAt(box_rect, line_from).add(inset), + rect_utils.positionAt(box_rect, line_to).add(inset), + border.size, + utils.shiftColorInHSV(border.color, value_shift) ); - - offset_y += font_face.getSize() * font_face.line_height; } } - } - if (box.scientific_number) |scientific_number| { - const regular = Assets.font(box.font); - const superscript = Assets.font(.{ - .size = box.font.size * 0.8, - .variant = box.font.variant - }); + if (box.draw) |box_draw| { + box_draw.do(box_draw.ctx, box); + } - var text_position = box.persistent.position; - text_position.x += box.padding.left; - text_position.y += box.padding.top; + const alignment_x_coeff = box.alignment.x.getCoefficient(); + const alignment_y_coeff = box.alignment.y.getCoefficient(); - const available_width = box.availableChildrenSize(.X); - const available_height = box.availableChildrenSize(.Y); + if (box.text) |text| { + const font_face = Assets.font(box.font); + var text_position = box.persistent.position; + text_position.x += box.padding.left; + text_position.y += box.padding.top; - const exponent = @floor(std.math.log10(scientific_number)); - const multiplier = std.math.pow(f64, 10, exponent); - const coefficient = scientific_number / multiplier; + const lines: [][]u8 = box.text_lines.slice(); - // const text = std.fmt.allocPrint(box.allocator, "{d:.2}x10{d}", .{coefficient, exponent}) catch ""; + const available_width = box.availableChildrenSize(.X); + const available_height = box.availableChildrenSize(.Y); - var coefficient_buff: [256]u8 = undefined; - const coefficient_str = std.fmt.formatFloat(&coefficient_buff, coefficient, .{ .mode = .decimal, .precision = box.scientific_precision }) catch ""; - const exponent_str = std.fmt.allocPrint(box.allocator, "{d:.0}", .{exponent}) catch ""; + if (lines.len == 0) { + const text_size = font_face.measureText(text); + text_position.x += (available_width - text_size.x) * alignment_x_coeff; + text_position.y += (available_height - text_size.y) * alignment_y_coeff; - var text_size = regular.measureText(coefficient_str); - text_size.x += regular.measureWidth("x10"); - text_size.x += superscript.measureWidth(exponent_str); + font_face.drawText(text, text_position, box.text_color); + } else { + // TODO: Don't call `measureTextLines`, + // Because in the end `measureText` will be called twice for each line + const text_size = font_face.measureTextLines(lines); + text_position.x += (available_width - text_size.x) * alignment_x_coeff; + text_position.y += (available_height - text_size.y) * alignment_y_coeff; - text_position.x += (available_width - text_size.x) * alignment_x_coeff; - text_position.y += (available_height - text_size.y) * alignment_y_coeff; + var offset_y: f32 = 0; - var ctx = FontFace.DrawTextContext{ - .font_face = regular, - .origin = text_position, - .tint = box.text_color - }; + for (lines) |line| { + const line_size = font_face.measureText(line); + const offset_x = (text_size.x - line_size.x) * alignment_x_coeff; - ctx.drawText(coefficient_str); - ctx.advanceY(-0.04); - ctx.advanceX(0.1); - ctx.drawText("x"); - ctx.advanceY(0.04); - ctx.drawText("10"); + font_face.drawText( + line, + text_position.add(.{ .x = offset_x, .y = offset_y }), + box.text_color + ); - ctx.font_face = superscript; - ctx.advanceY(-0.2); - ctx.drawText(exponent_str); - } + offset_y += font_face.getSize() * font_face.line_height; + } + } + } - if (draw_debug) { - if (self.isKeyActive(box.key)) { - rl.drawRectangleLinesEx(box_rect, 3, rl.Color.red); - } else if (self.isKeyHot(box.key)) { - rl.drawRectangleLinesEx(box_rect, 3, rl.Color.orange); - } else { - rl.drawRectangleLinesEx(box_rect, 1, rl.Color.magenta); + if (box.scientific_number) |scientific_number| { + const regular = Assets.font(box.font); + const superscript = Assets.font(.{ + .size = box.font.size * 0.8, + .variant = box.font.variant + }); + + var text_position = box.persistent.position; + text_position.x += box.padding.left; + text_position.y += box.padding.top; + + const available_width = box.availableChildrenSize(.X); + const available_height = box.availableChildrenSize(.Y); + + const exponent = @floor(std.math.log10(scientific_number)); + const multiplier = std.math.pow(f64, 10, exponent); + const coefficient = scientific_number / multiplier; + + // const text = std.fmt.allocPrint(box.allocator, "{d:.2}x10{d}", .{coefficient, exponent}) catch ""; + + var coefficient_buff: [256]u8 = undefined; + const coefficient_str = std.fmt.formatFloat(&coefficient_buff, coefficient, .{ .mode = .decimal, .precision = box.scientific_precision }) catch ""; + const exponent_str = std.fmt.allocPrint(box.allocator, "{d:.0}", .{exponent}) catch ""; + + var text_size = regular.measureText(coefficient_str); + text_size.x += regular.measureWidth("x10"); + text_size.x += superscript.measureWidth(exponent_str); + + text_position.x += (available_width - text_size.x) * alignment_x_coeff; + text_position.y += (available_height - text_size.y) * alignment_y_coeff; + + var ctx = FontFace.DrawTextContext{ + .font_face = regular, + .origin = text_position, + .tint = box.text_color + }; + + ctx.drawText(coefficient_str); + ctx.advanceY(-0.04); + ctx.advanceX(0.1); + ctx.drawText("x"); + ctx.advanceY(0.04); + ctx.drawText("10"); + + ctx.font_face = superscript; + ctx.advanceY(-0.2); + ctx.drawText(exponent_str); + } + + if (draw_debug) { + if (self.isKeyActive(box.key)) { + rl.drawRectangleLinesEx(box_rect, 3, rl.Color.red); + } else if (self.isKeyHot(box.key)) { + rl.drawRectangleLinesEx(box_rect, 3, rl.Color.orange); + } else { + rl.drawRectangleLinesEx(box_rect, 1, rl.Color.magenta); + } + } + + if (box.draw_on_top) { + child_on_top_pass = null; } } var child_iter = box.iterChildren(); while (child_iter.next()) |child| { - self.drawBox(child); + self.drawBox(child, child_on_top_pass); } } @@ -2269,9 +2337,11 @@ pub const TextInputOptions = struct { key: Key, storage: *TextInputStorage, editable: bool = true, + width: f32 = 200, initial: ?[]const u8 = null, placeholder: ?[]const u8 = null, + postfix: ?[]const u8 = null, text_color: rl.Color = srcery.black }; @@ -2280,9 +2350,13 @@ pub const NumberInputOptions = struct { storage: *TextInputStorage, invalid: bool = false, editable: bool = true, + width: f32 = 200, - initial: ?[]const u8 = null, + display_scalar: ?f64 = null, + + initial: ?f64 = null, placeholder: ?[]const u8 = null, + postfix: ?[]const u8 = null, text_color: rl.Color = srcery.black, invalid_color: rl.Color = srcery.red }; @@ -2429,7 +2503,7 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void { const container = self.createBox(.{ .key = opts.key, - .size_x = Sizing.initGrowUpTo(.{ .pixels = 200 }), + .size_x = Sizing.initGrowUpTo(.{ .pixels = opts.width }), .size_y = Sizing.initFixed(Unit.initPixels(self.rem(1))), .flags = &.{ .clickable, .clip_view, .draggable }, .background = srcery.bright_white, @@ -2485,7 +2559,7 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void { storage.shown_slice_end = cursor_stop_x + shown_window_size; } - _ = self.createBox(.{ + const shown_text = self.createBox(.{ .text_color = text_color, .text = text, .float_relative_to = container, @@ -2498,6 +2572,9 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void { .align_y = .center, .align_x = .start }); + if (opts.postfix) |postfix| { + shown_text.appendText(postfix); + } } const container_signal = self.signal(container); @@ -2730,12 +2807,15 @@ pub fn numberInput(self: *UI, T: type, opts: NumberInputOptions) !?T { var text_opts = TextInputOptions{ .key = opts.key, .storage = opts.storage, - .initial = opts.initial, .text_color = opts.text_color, .placeholder = opts.placeholder, - .editable = opts.editable + .editable = opts.editable, + .postfix = opts.postfix, + .width = opts.width }; + const display_scalar = opts.display_scalar orelse 1; + var is_invalid = opts.invalid; if (storage.buffer.items.len > 0 and std.meta.isError(std.fmt.parseFloat(T, storage.buffer.items))) { is_invalid = true; @@ -2747,8 +2827,17 @@ pub fn numberInput(self: *UI, T: type, opts: NumberInputOptions) !?T { try self.textInput(text_opts); + const box = self.getBoxByKey(opts.key).?; + if (opts.initial != null and box.created) { + opts.storage.buffer.clearAndFree(); + + const initial = opts.initial.? / display_scalar; + const initial_text = try std.fmt.allocPrint(box.allocator, "{d}", .{ initial }); + try opts.storage.buffer.appendSlice(initial_text); + } + if (std.fmt.parseFloat(T, storage.buffer.items)) |new_value| { - return new_value; + return new_value * display_scalar; } else |_| { return null; }