add basic UI for adding and removing transforms

This commit is contained in:
Rokas Puzonas 2025-05-07 22:30:46 +03:00
parent b9e0a7a2d6
commit 2545774575
3 changed files with 628 additions and 300 deletions

View File

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

View File

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

View File

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