add basic UI for adding and removing transforms
This commit is contained in:
parent
b9e0a7a2d6
commit
2545774575
384
src/app.zig
384
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", .{});
|
||||
|
@ -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 });
|
||||
|
391
src/ui.zig
391
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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user