Compare commits
8 Commits
e5bc57d7b5
...
fdf6001068
Author | SHA1 | Date | |
---|---|---|---|
fdf6001068 | |||
a6e0fd9e67 | |||
a2da649d96 | |||
88212d001c | |||
2545774575 | |||
b9e0a7a2d6 | |||
e48887cdee | |||
af8cf21f53 |
@ -90,6 +90,11 @@ pub fn build(b: *std.Build) !void {
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
const datetime_dep = b.dependency("zig-datetime", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
const profiler_dep = b.dependency("profiler.zig", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
@ -124,6 +129,7 @@ pub fn build(b: *std.Build) !void {
|
||||
exe.root_module.addImport("cute_aseprite", cute_aseprite_lib);
|
||||
exe.root_module.addImport("known-folders", known_folders);
|
||||
exe.root_module.addImport("ini", ini);
|
||||
exe.root_module.addImport("datetime", datetime_dep.module("zig-datetime"));
|
||||
|
||||
// TODO: Add flag to disable in release
|
||||
exe.root_module.addImport("profiler", profiler_dep.module("profiler"));
|
||||
@ -145,6 +151,8 @@ pub fn build(b: *std.Build) !void {
|
||||
exe.step.dependOn(&add_icon_step.step);
|
||||
|
||||
exe.linkSystemLibrary("Comdlg32");
|
||||
exe.linkSystemLibrary("ole32");
|
||||
exe.linkSystemLibrary("uuid");
|
||||
}
|
||||
|
||||
b.installArtifact(exe);
|
||||
|
1411
src/app.zig
1411
src/app.zig
File diff suppressed because it is too large
Load Diff
BIN
src/assets/checkbox-mark.ase
Normal file
BIN
src/assets/checkbox-mark.ase
Normal file
Binary file not shown.
BIN
src/assets/cross.ase
Normal file
BIN
src/assets/cross.ase
Normal file
Binary file not shown.
BIN
src/assets/file.ase
Normal file
BIN
src/assets/file.ase
Normal file
Binary file not shown.
@ -167,14 +167,16 @@ last_applied_command: usize = 0,
|
||||
zoom_start: ?ViewAxisPosition = null,
|
||||
cursor: ?ViewAxisPosition = null,
|
||||
view_settings: ?Id = null, // View id
|
||||
view_fullscreen: ?Id = null, // View id
|
||||
view_protocol_modal: ?Id = null, // View id
|
||||
show_ruler: bool = false,
|
||||
selected_tool: enum { move, select } = .move,
|
||||
selected_tool: enum { move, select, marker } = .move,
|
||||
show_marked_range: ?struct {
|
||||
view_id: Id,
|
||||
index: usize,
|
||||
} = null,
|
||||
show_marker: ?struct {
|
||||
view_id: Id,
|
||||
index: usize
|
||||
} = null,
|
||||
|
||||
pub fn init(project: *App.Project) System {
|
||||
return System{
|
||||
@ -326,14 +328,6 @@ pub fn isViewSettingsOpen(self: *System, view_id: Id) bool {
|
||||
return self.view_settings != null and self.view_settings.?.eql(view_id);
|
||||
}
|
||||
|
||||
pub fn toggleFullscreenView(self: *System, view_id: Id) void {
|
||||
if (self.view_fullscreen == null or !self.view_fullscreen.?.eql(view_id)) {
|
||||
self.view_fullscreen = view_id;
|
||||
} else {
|
||||
self.view_fullscreen = null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setCursor(self: *System, view_id: Id, axis: UI.Axis, position: ?f64) void {
|
||||
return ViewAxisPosition.set(&self.cursor, view_id, axis, position);
|
||||
}
|
||||
@ -363,3 +357,17 @@ pub fn toggleShownMarkedRange(self: *System, view_id: Id, index: usize) void {
|
||||
.index = index,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toggleShownMarker(self: *System, view_id: Id, index: usize) void {
|
||||
if (self.show_marker) |show_marker| {
|
||||
if (show_marker.view_id.eql(view_id) and show_marker.index == index) {
|
||||
self.show_marker = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.show_marker = .{
|
||||
.view_id = view_id,
|
||||
.index = index,
|
||||
};
|
||||
}
|
@ -32,15 +32,10 @@ pub const Result = struct {
|
||||
box: *UI.Box,
|
||||
};
|
||||
|
||||
fn showGraph(ctx: Context, view_id: Id) *UI.Box {
|
||||
fn createGraphBox(ctx: Context) *UI.Box {
|
||||
var ui = ctx.ui;
|
||||
const app = ctx.app;
|
||||
|
||||
const view = app.getView(view_id).?;
|
||||
const samples = app.getViewSamples(view_id);
|
||||
const view_opts = &view.graph_opts;
|
||||
|
||||
const graph_box = ui.createBox(.{
|
||||
return ui.createBox(.{
|
||||
.key = ui.keyFromString("Graph"),
|
||||
.size_x = UI.Sizing.initGrowFull(),
|
||||
.size_y = UI.Sizing.initGrowFull(),
|
||||
@ -52,6 +47,16 @@ fn showGraph(ctx: Context, view_id: Id) *UI.Box {
|
||||
.bottom = .{ .color = srcery.hard_black, .size = 4 }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn showGraph(ctx: Context, graph_box: *UI.Box, view_id: Id) void {
|
||||
var ui = ctx.ui;
|
||||
const app = ctx.app;
|
||||
|
||||
const view = app.getView(view_id).?;
|
||||
const view_opts = &view.graph_opts;
|
||||
|
||||
|
||||
graph_box.beginChildren();
|
||||
defer graph_box.endChildren();
|
||||
|
||||
@ -106,12 +111,20 @@ fn showGraph(ctx: Context, view_id: Id) *UI.Box {
|
||||
}
|
||||
} else if (ctx.view_controls.selected_tool == .select) {
|
||||
// TODO:
|
||||
|
||||
} else if (ctx.view_controls.selected_tool == .marker) {
|
||||
// TODO:
|
||||
}
|
||||
|
||||
|
||||
{ // Render graph
|
||||
view.graph_cache.min_max_cache = app.getViewMinMaxCache(view_id);
|
||||
Graph.drawCached(&view.graph_cache, graph_box.persistent.size, view_opts.*, samples);
|
||||
var sample_list_id = app.project.getViewSampleListId(view_id);
|
||||
if (view.transformed_samples) |transformed_samples_id| {
|
||||
sample_list_id = transformed_samples_id;
|
||||
}
|
||||
|
||||
const sample_list = app.project.sample_lists.get(sample_list_id).?;
|
||||
Graph.drawCached(&view.graph_cache, graph_box.persistent.size, view_opts.*, sample_list);
|
||||
if (view.graph_cache.texture) |texture| {
|
||||
graph_box.texture = texture.texture;
|
||||
}
|
||||
@ -125,8 +138,6 @@ fn showGraph(ctx: Context, view_id: Id) *UI.Box {
|
||||
.size = ui.rem(3)
|
||||
};
|
||||
}
|
||||
|
||||
return graph_box;
|
||||
}
|
||||
|
||||
fn showToolbar(ctx: Context, view_id: Id) void {
|
||||
@ -240,11 +251,20 @@ fn showToolbar(ctx: Context, view_id: Id) void {
|
||||
.size_x = UI.Sizing.initGrowFull()
|
||||
});
|
||||
|
||||
_ = ui.createBox(.{
|
||||
.size_x = UI.Sizing.initFixedPixels(ui.rem(1))
|
||||
});
|
||||
|
||||
const label = ui.label("{s}", .{text});
|
||||
label.size.y = UI.Sizing.initGrowFull();
|
||||
label.alignment.x = .center;
|
||||
label.alignment.y = .center;
|
||||
label.padding = UI.Padding.horizontal(ui.rem(1));
|
||||
// TODO:
|
||||
// label.padding = UI.Padding.horizontal(ui.rem(1));
|
||||
|
||||
_ = ui.createBox(.{
|
||||
.size_x = UI.Sizing.initFixedPixels(ui.rem(1))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -266,10 +286,6 @@ pub fn show(ctx: Context, view_id: Id, height: UI.Sizing) !Result {
|
||||
|
||||
showToolbar(ctx, view_id);
|
||||
|
||||
if (!ctx.app.project.show_rulers) {
|
||||
_ = showGraph(ctx, view_id);
|
||||
|
||||
} else {
|
||||
const ruler_ctx = UIViewRuler.Context{
|
||||
.ui = ctx.ui,
|
||||
.project = &ctx.app.project,
|
||||
@ -291,7 +307,7 @@ pub fn show(ctx: Context, view_id: Id, height: UI.Sizing) !Result {
|
||||
|
||||
y_ruler = UIViewRuler.createBox(ruler_ctx, ui.keyFromString("Y ruler"), .Y);
|
||||
|
||||
graph_box = showGraph(ctx, view_id);
|
||||
graph_box = createGraphBox(ctx);
|
||||
}
|
||||
|
||||
{
|
||||
@ -303,26 +319,18 @@ pub fn show(ctx: Context, view_id: Id, height: UI.Sizing) !Result {
|
||||
container.beginChildren();
|
||||
defer container.endChildren();
|
||||
|
||||
const fullscreen = ui.createBox(.{
|
||||
.key = ui.keyFromString("Fullscreen toggle"),
|
||||
_ = ui.createBox(.{
|
||||
.size_x = ruler_size,
|
||||
.size_y = ruler_size,
|
||||
.background = srcery.hard_black,
|
||||
.hot_cursor = .mouse_cursor_pointing_hand,
|
||||
.flags = &.{ .draw_hot, .draw_active, .clickable },
|
||||
.texture = Assets.fullscreen,
|
||||
.texture_size = .{ .x = 28, .y = 28 }
|
||||
});
|
||||
if (ui.signal(fullscreen).clicked()) {
|
||||
ctx.view_controls.toggleFullscreenView(view_id);
|
||||
}
|
||||
|
||||
x_ruler = UIViewRuler.createBox(ruler_ctx, ui.keyFromString("X ruler"), .X);
|
||||
}
|
||||
|
||||
try UIViewRuler.show(ruler_ctx, x_ruler, graph_box, view_id, .X);
|
||||
try UIViewRuler.show(ruler_ctx, y_ruler, graph_box, view_id, .Y);
|
||||
}
|
||||
showGraph(ctx, graph_box, view_id);
|
||||
|
||||
return result;
|
||||
}
|
@ -304,7 +304,6 @@ pub fn show(ctx: Context, box: *UI.Box, graph_box: *UI.Box, view_id: Id, axis: U
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
var selected_range_iter = view.iterMarkedRanges(axis);
|
||||
while (selected_range_iter.next()) |selected_range| {
|
||||
var color = srcery.blue;
|
||||
@ -322,6 +321,7 @@ pub fn show(ctx: Context, box: *UI.Box, graph_box: *UI.Box, view_id: Id, axis: U
|
||||
showMarkerLine(ui, ruler, selected_range.upper, color);
|
||||
|
||||
var hasher = UI.Key.CombineHasher.init();
|
||||
hasher.update(std.mem.asBytes("Marked ranges"));
|
||||
hasher.update(std.mem.asBytes(&view_id));
|
||||
hasher.update(std.mem.asBytes(&axis));
|
||||
hasher.update(std.mem.asBytes(&index));
|
||||
@ -341,6 +341,37 @@ pub fn show(ctx: Context, box: *UI.Box, graph_box: *UI.Box, view_id: Id, axis: U
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (axis == .X) {
|
||||
for (0.., view.markers.constSlice()) |i, marker| {
|
||||
const color = srcery.cyan;
|
||||
|
||||
showMarkerLine(ui, ruler, marker, color);
|
||||
showMarkerLine(ui, ruler, marker, color);
|
||||
|
||||
var hasher = UI.Key.CombineHasher.init();
|
||||
hasher.update(std.mem.asBytes("Markers"));
|
||||
hasher.update(std.mem.asBytes(&view_id));
|
||||
hasher.update(std.mem.asBytes(&axis));
|
||||
hasher.update(std.mem.asBytes(&i));
|
||||
|
||||
const view_size = view.graph_opts.x_range.size();
|
||||
const clickable_width = view_size * 0.01;
|
||||
|
||||
const clickable = ui.createBox(.{
|
||||
.key = UI.Key.init(hasher.final()),
|
||||
.float_rect = ruler.getGraphDrawContext().getRect(marker - clickable_width/2, clickable_width, 0, 1),
|
||||
.float_relative_to = ruler.graph_box,
|
||||
.parent = ruler.graph_box,
|
||||
.flags = &.{ .draw_hot, .draw_active, .clickable },
|
||||
.hot_cursor = .mouse_cursor_pointing_hand,
|
||||
});
|
||||
|
||||
if (ui.signal(clickable).clicked()) {
|
||||
ctx.view_controls.toggleShownMarker(view_id, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const signal = ui.signal(box);
|
||||
@ -406,7 +437,6 @@ pub fn show(ctx: Context, box: *UI.Box, graph_box: *UI.Box, view_id: Id, axis: U
|
||||
}
|
||||
}
|
||||
} else if (ctx.view_controls.selected_tool == .select) {
|
||||
// TODO:
|
||||
|
||||
if (cursor != null) {
|
||||
if (ctx.view_controls.getCursorHoldStart(view_id, axis)) |hold_start| {
|
||||
@ -418,13 +448,14 @@ pub fn show(ctx: Context, box: *UI.Box, graph_box: *UI.Box, view_id: Id, axis: U
|
||||
const hold_end_mouse = view_range.remapTo(mouse_range, range.upper);
|
||||
const mouse_move_distance = @abs(hold_end_mouse - hold_start_mouse);
|
||||
if (signal.flags.contains(.left_released) and mouse_move_distance > 5) {
|
||||
if (view.appendMarkedRange(axis, range)) |marked_range| {
|
||||
marked_range.refresh(ctx.project.getViewSamples(view_id));
|
||||
}
|
||||
|
||||
_ = ctx.project.appendMarkedRange(view_id, axis, range);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (ctx.view_controls.selected_tool == .marker) {
|
||||
if (cursor != null and signal.flags.contains(.left_released) and axis == .X) {
|
||||
view.markers.append(cursor.?) catch {};
|
||||
}
|
||||
}
|
||||
|
||||
if (signal.flags.contains(.left_released)) {
|
||||
|
@ -5,7 +5,7 @@ const Allocator = std.mem.Allocator;
|
||||
const FontFace = @This();
|
||||
|
||||
font: rl.Font,
|
||||
spacing: ?f32 = null,
|
||||
spacing: ?f32 = 0,
|
||||
line_height: f32 = 1.4,
|
||||
|
||||
pub const DrawTextContext = struct {
|
||||
|
493
src/graph.zig
493
src/graph.zig
@ -3,6 +3,7 @@ const std = @import("std");
|
||||
const rl = @import("raylib");
|
||||
const srcery = @import("./srcery.zig");
|
||||
const RangeF64 = @import("./range.zig").RangeF64;
|
||||
const SampleList = @import("./app.zig").SampleList;
|
||||
|
||||
const remap = @import("./utils.zig").remap;
|
||||
const assert = std.debug.assert;
|
||||
@ -24,130 +25,6 @@ pub const ViewOptions = struct {
|
||||
y_range: RangeF64 = RangeF64.init(0, 0),
|
||||
|
||||
color: rl.Color = srcery.red,
|
||||
|
||||
fn writeStruct(self: ViewOptions, writer: anytype) !void {
|
||||
_ = self;
|
||||
_ = writer;
|
||||
// try writer.writeStructEndian(self.id, file_endian);
|
||||
// try writer.writeStructEndian(self.channel_name.constSlice(), file_endian);
|
||||
}
|
||||
|
||||
fn readStruct(reader: anytype) !ViewOptions {
|
||||
_ = reader;
|
||||
// const id = try reader.readStructEndian(Id, file_endian);
|
||||
// const channel_name = try reader.readStructEndian([]const u8, file_endian);
|
||||
|
||||
return ViewOptions{
|
||||
.x_range = RangeF64.init(0, 0),
|
||||
.y_range = RangeF64.init(0, 0),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const MinMaxCache = struct {
|
||||
const MinMaxPair = struct {
|
||||
min: f64,
|
||||
max: f64
|
||||
};
|
||||
|
||||
chunk_size: usize = 256,
|
||||
min_max_pairs: std.ArrayListUnmanaged(MinMaxPair) = .{},
|
||||
sample_count: usize = 0,
|
||||
|
||||
pub fn deinit(self: *MinMaxCache, allocator: std.mem.Allocator) void {
|
||||
self.min_max_pairs.clearAndFree(allocator);
|
||||
self.sample_count = 0;
|
||||
}
|
||||
|
||||
fn getMinMaxPair(self: *MinMaxCache, chunk: []const f64) MinMaxPair {
|
||||
assert(chunk.len > 0);
|
||||
assert(chunk.len <= self.chunk_size);
|
||||
|
||||
var min_sample = chunk[0];
|
||||
var max_sample = chunk[0];
|
||||
for (chunk) |sample| {
|
||||
min_sample = @min(min_sample, sample);
|
||||
max_sample = @max(max_sample, sample);
|
||||
}
|
||||
return MinMaxPair{
|
||||
.min = min_sample,
|
||||
.max = max_sample
|
||||
};
|
||||
}
|
||||
|
||||
pub fn updateAll(self: *MinMaxCache, allocator: std.mem.Allocator, samples: []const f64) !void {
|
||||
self.min_max_pairs.clearRetainingCapacity();
|
||||
self.sample_count = 0;
|
||||
|
||||
if (samples.len == 0) return;
|
||||
|
||||
var iter = std.mem.window(f64, samples, self.chunk_size, self.chunk_size);
|
||||
while (iter.next()) |chunk| {
|
||||
try self.min_max_pairs.append(allocator, self.getMinMaxPair(chunk));
|
||||
}
|
||||
|
||||
self.sample_count = samples.len;
|
||||
}
|
||||
|
||||
pub fn updateLast(self: *MinMaxCache, allocator: std.mem.Allocator, samples: []const f64) !void {
|
||||
if (self.sample_count > samples.len) {
|
||||
try self.updateAll(allocator, samples);
|
||||
}
|
||||
if (self.sample_count == samples.len) {
|
||||
return;
|
||||
}
|
||||
|
||||
const from_chunk = @divFloor(self.sample_count, self.chunk_size);
|
||||
const to_chunk = @divFloor(samples.len - 1, self.chunk_size);
|
||||
|
||||
for (from_chunk..(to_chunk+1)) |i| {
|
||||
const chunk = samples[
|
||||
(self.chunk_size*i)..(@min(self.chunk_size*(i+1), samples.len))
|
||||
];
|
||||
const min_max_pair = self.getMinMaxPair(chunk);
|
||||
|
||||
if (i >= self.min_max_pairs.items.len) {
|
||||
try self.min_max_pairs.append(allocator, min_max_pair);
|
||||
} else {
|
||||
self.min_max_pairs.items[i] = min_max_pair;
|
||||
}
|
||||
}
|
||||
|
||||
self.sample_count = samples.len;
|
||||
}
|
||||
|
||||
test {
|
||||
const allocator = std.testing.allocator;
|
||||
var min_max_cache: MinMaxCache = .{ .chunk_size = 4 };
|
||||
defer min_max_cache.deinit(allocator);
|
||||
|
||||
try min_max_cache.updateLast(allocator, &.{ 1, 2 });
|
||||
try std.testing.expectEqualSlices(
|
||||
MinMaxPair,
|
||||
&.{
|
||||
MinMaxPair{ .min = 1, .max = 2 }
|
||||
},
|
||||
min_max_cache.min_max_pairs.items
|
||||
);
|
||||
|
||||
try min_max_cache.updateLast(allocator, &.{ 1, 2, 3 });
|
||||
try std.testing.expectEqualSlices(
|
||||
MinMaxPair,
|
||||
&.{
|
||||
MinMaxPair{ .min = 1, .max = 3 }
|
||||
},
|
||||
min_max_cache.min_max_pairs.items
|
||||
);
|
||||
|
||||
try min_max_cache.updateLast(allocator, &.{ 1, 2, 3, -1 });
|
||||
try std.testing.expectEqualSlices(
|
||||
MinMaxPair,
|
||||
&.{
|
||||
MinMaxPair{ .min = -1, .max = 3 }
|
||||
},
|
||||
min_max_cache.min_max_pairs.items
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
pub const RenderCache = struct {
|
||||
@ -156,7 +33,6 @@ pub const RenderCache = struct {
|
||||
drawn_x_range: RangeF64
|
||||
};
|
||||
|
||||
min_max_cache: ?MinMaxCache = null,
|
||||
texture: ?rl.RenderTexture2D = null,
|
||||
key: ?Key = null,
|
||||
|
||||
@ -191,7 +67,208 @@ pub const RenderCache = struct {
|
||||
}
|
||||
};
|
||||
|
||||
fn drawSamplesExact(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void {
|
||||
pub const SampleIterator = struct {
|
||||
samples: []const f64,
|
||||
to: f64,
|
||||
step: f64,
|
||||
i: f64,
|
||||
next_i: f64,
|
||||
|
||||
pub fn init(samples: []const f64, from: f64, to: f64, step: f64) SampleIterator {
|
||||
return SampleIterator{
|
||||
.samples = samples,
|
||||
.i = from,
|
||||
.next_i = from + step,
|
||||
.to = to,
|
||||
.step = step
|
||||
};
|
||||
}
|
||||
|
||||
pub fn initRange(samples: []const f64, range: RangeF64, step: f64) SampleIterator {
|
||||
return SampleIterator.init(samples, range.lower, range.upper, step);
|
||||
}
|
||||
|
||||
pub fn next(self: *SampleIterator) ?[]const f64 {
|
||||
if (self.next_i < self.to) {
|
||||
self.i = self.next_i;
|
||||
self.next_i = self.i + self.step;
|
||||
|
||||
const i_usize: usize = @intFromFloat(self.i);
|
||||
const next_i_usize: usize = @intFromFloat(self.next_i);
|
||||
if (next_i_usize >= self.samples.len) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const samples = self.samples[i_usize..next_i_usize];
|
||||
if (samples.len == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return samples;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fn drawSamplesExact(draw_rect: rl.Rectangle, options: ViewOptions, sample_list: *SampleList) void {
|
||||
const draw_x_range, const draw_y_range = RangeF64.initRect(draw_rect);
|
||||
|
||||
// TODO: Wtf is this, I don't remember why I fiddled with the x range.
|
||||
var view_x_range = options.x_range;
|
||||
view_x_range.upper += 2;
|
||||
view_x_range.lower -= 1;
|
||||
|
||||
const i_range = view_x_range.intersectPositive(
|
||||
RangeF64.init(0, @max(@as(f64, @floatFromInt(sample_list.getLength())) - 1, 0))
|
||||
);
|
||||
if (i_range.lower > i_range.upper) {
|
||||
return;
|
||||
}
|
||||
|
||||
var block_id_iter = sample_list.blockIdIterator(
|
||||
@intFromFloat(i_range.lower),
|
||||
@intFromFloat(i_range.upper)
|
||||
);
|
||||
while (block_id_iter.next()) |block_id| {
|
||||
const block = sample_list.getBlock(block_id) orelse continue;
|
||||
const block_samples = block.samplesSlice();
|
||||
if (block_samples.len == 0) continue;
|
||||
|
||||
if (block_samples.len > 1) {
|
||||
for (0..(block_samples.len-1)) |i| {
|
||||
const i_f64: f64 = @floatFromInt(block_id * SampleList.Block.capacity + i);
|
||||
rl.drawLineV(
|
||||
.{
|
||||
.x = @floatCast(options.x_range.remapTo(draw_x_range, i_f64)),
|
||||
.y = @floatCast(options.y_range.remapTo(draw_y_range, block_samples[i])),
|
||||
},
|
||||
.{
|
||||
.x = @floatCast(options.x_range.remapTo(draw_x_range, i_f64 + 1)),
|
||||
.y = @floatCast(options.y_range.remapTo(draw_y_range, block_samples[i + 1])),
|
||||
},
|
||||
options.color
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (block_samples.len == SampleList.Block.capacity) {
|
||||
if (sample_list.getBlock(block_id + 1)) |next_block| {
|
||||
const next_block_samples = next_block.samplesSlice();
|
||||
if (next_block_samples.len > 0) {
|
||||
const i_f64: f64 = @floatFromInt((block_id + 1) * SampleList.Block.capacity - 1);
|
||||
rl.drawLineV(
|
||||
.{
|
||||
.x = @floatCast(options.x_range.remapTo(draw_x_range, i_f64)),
|
||||
.y = @floatCast(options.y_range.remapTo(draw_y_range, block_samples[SampleList.Block.capacity - 1])),
|
||||
},
|
||||
.{
|
||||
.x = @floatCast(options.x_range.remapTo(draw_x_range, i_f64 + 1)),
|
||||
.y = @floatCast(options.y_range.remapTo(draw_y_range, next_block_samples[0])),
|
||||
},
|
||||
options.color
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn drawColumn(draw_rect: rl.Rectangle, options: ViewOptions, sample_index: f64, column_min: f64, column_max: f64) void {
|
||||
const draw_x_range, const draw_y_range = RangeF64.initRect(draw_rect);
|
||||
|
||||
const x = options.x_range.remapTo(draw_x_range, sample_index);
|
||||
const y_min = options.y_range.remapTo(draw_y_range, column_min);
|
||||
const y_max = options.y_range.remapTo(draw_y_range, column_max);
|
||||
|
||||
if (@abs(y_max - y_min) < 1) {
|
||||
const avg = (y_min + y_max) / 2;
|
||||
rl.drawLineV(
|
||||
.{ .x = @floatCast(x), .y = @floatCast(avg) },
|
||||
.{ .x = @floatCast(x), .y = @floatCast(avg+1) },
|
||||
options.color
|
||||
);
|
||||
} else {
|
||||
rl.drawLineV(
|
||||
.{ .x = @floatCast(x), .y = @floatCast(y_min) },
|
||||
.{ .x = @floatCast(x), .y = @floatCast(y_max) },
|
||||
options.color
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn drawSamplesApproximate(draw_rect: rl.Rectangle, options: ViewOptions, sample_list: *SampleList) void {
|
||||
const i_range = options.x_range.intersectPositive(
|
||||
RangeF64.init(0, @max(@as(f64, @floatFromInt(sample_list.getLength())) - 1, 0))
|
||||
);
|
||||
if (i_range.isNegative()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const samples_per_column = options.x_range.size() / @as(f64, @floatCast(draw_rect.width));
|
||||
assert(samples_per_column >= 1);
|
||||
|
||||
var sample_index = i_range.lower;
|
||||
while (sample_index < i_range.upper) : (sample_index += samples_per_column) {
|
||||
var column_min: f64 = std.math.inf(f64);
|
||||
var column_max: f64 = -std.math.inf(f64);
|
||||
|
||||
var iter = sample_list.iterator(
|
||||
@intFromFloat(sample_index),
|
||||
@intFromFloat(sample_index + samples_per_column + 1)
|
||||
);
|
||||
while (iter.next()) |segment| {
|
||||
for (segment) |sample| {
|
||||
column_min = @min(column_min, sample);
|
||||
column_max = @max(column_max, sample);
|
||||
}
|
||||
}
|
||||
|
||||
if (!std.math.isInf(column_min)) {
|
||||
drawColumn(draw_rect, options, sample_index, column_min, column_max);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn drawSamplesMinMax(draw_rect: rl.Rectangle, options: ViewOptions, sample_list: *SampleList) void {
|
||||
const i_range = options.x_range.intersectPositive(
|
||||
RangeF64.init(0, @max(@as(f64, @floatFromInt(sample_list.getLength())) - 1, 0))
|
||||
);
|
||||
if (i_range.isNegative()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const samples_per_column = options.x_range.size() / @as(f64, @floatCast(draw_rect.width));
|
||||
assert(samples_per_column >= @as(f64, @floatFromInt(SampleList.Block.capacity)));
|
||||
|
||||
var i = i_range.lower;
|
||||
while (i < i_range.upper) : (i += samples_per_column) {
|
||||
var column_min: f64 = std.math.inf(f64);
|
||||
var column_max: f64 = -std.math.inf(f64);
|
||||
|
||||
var block_id_iter = sample_list.blockIdIterator(
|
||||
@intFromFloat(i),
|
||||
@intFromFloat(i + samples_per_column)
|
||||
);
|
||||
while (block_id_iter.next()) |block_id| {
|
||||
const block = sample_list.getBlock(block_id) orelse continue;
|
||||
|
||||
if (block.min) |block_min| {
|
||||
column_min = @min(column_min, block_min);
|
||||
}
|
||||
|
||||
if (block.max) |block_max| {
|
||||
column_max = @max(column_max, block_max);
|
||||
}
|
||||
}
|
||||
|
||||
if (!std.math.isInf(column_min)) {
|
||||
drawColumn(draw_rect, options, i, column_min, column_max);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn drawSamples(draw_rect: rl.Rectangle, options: ViewOptions, sample_list: *SampleList) void {
|
||||
rl.beginScissorMode(
|
||||
@intFromFloat(draw_rect.x),
|
||||
@intFromFloat(draw_rect.y),
|
||||
@ -200,162 +277,24 @@ fn drawSamplesExact(draw_rect: rl.Rectangle, options: ViewOptions, samples: []co
|
||||
);
|
||||
defer rl.endScissorMode();
|
||||
|
||||
const draw_x_range, const draw_y_range = RangeF64.initRect(draw_rect);
|
||||
|
||||
var view_x_range = options.x_range;
|
||||
view_x_range.upper += 2;
|
||||
view_x_range.lower -= 1;
|
||||
|
||||
const i_range = view_x_range.intersectPositive(
|
||||
RangeF64.init(0, @max(@as(f64, @floatFromInt(samples.len)) - 1, 0))
|
||||
);
|
||||
if (i_range.lower > i_range.upper) {
|
||||
return;
|
||||
}
|
||||
|
||||
const from_i: usize = @intFromFloat(i_range.lower);
|
||||
const to_i: usize = @intFromFloat(i_range.upper);
|
||||
if (to_i == 0 or from_i == to_i) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (from_i..(to_i-1)) |i| {
|
||||
const i_f64: f64 = @floatFromInt(i);
|
||||
rl.drawLineV(
|
||||
.{
|
||||
.x = @floatCast(options.x_range.remapTo(draw_x_range, i_f64)),
|
||||
.y = @floatCast(options.y_range.remapTo(draw_y_range, samples[i])),
|
||||
},
|
||||
.{
|
||||
.x = @floatCast(options.x_range.remapTo(draw_x_range, i_f64 + 1)),
|
||||
.y = @floatCast(options.y_range.remapTo(draw_y_range, samples[i + 1])),
|
||||
},
|
||||
options.color
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn drawSamplesApproximate(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void {
|
||||
const draw_x_range, const draw_y_range = RangeF64.initRect(draw_rect);
|
||||
|
||||
const i_range = options.x_range.intersectPositive(
|
||||
RangeF64.init(0, @max(@as(f64, @floatFromInt(samples.len)) - 1, 0))
|
||||
);
|
||||
if (i_range.lower > i_range.upper) {
|
||||
return;
|
||||
}
|
||||
|
||||
const samples_per_column = options.x_range.size() / draw_x_range.size();
|
||||
assert(samples_per_column >= 1);
|
||||
|
||||
var i = i_range.lower;
|
||||
while (i < i_range.upper - samples_per_column) : (i += samples_per_column) {
|
||||
const column_start: usize = @intFromFloat(i);
|
||||
const column_end: usize = @intFromFloat(i + samples_per_column);
|
||||
const column_samples = samples[column_start..column_end];
|
||||
if (column_samples.len == 0) continue;
|
||||
|
||||
var column_min = column_samples[0];
|
||||
var column_max = column_samples[0];
|
||||
|
||||
for (column_samples) |sample| {
|
||||
column_min = @min(column_min, sample);
|
||||
column_max = @max(column_max, sample);
|
||||
}
|
||||
|
||||
const x = options.x_range.remapTo(draw_x_range, i);
|
||||
const y_min = options.y_range.remapTo(draw_y_range, column_min);
|
||||
const y_max = options.y_range.remapTo(draw_y_range, column_max);
|
||||
|
||||
if (@abs(y_max - y_min) < 1) {
|
||||
const avg = (y_min + y_max) / 2;
|
||||
rl.drawLineV(
|
||||
.{ .x = @floatCast(x), .y = @floatCast(avg) },
|
||||
.{ .x = @floatCast(x), .y = @floatCast(avg+1) },
|
||||
options.color
|
||||
);
|
||||
} else {
|
||||
rl.drawLineV(
|
||||
.{ .x = @floatCast(x), .y = @floatCast(y_min) },
|
||||
.{ .x = @floatCast(x), .y = @floatCast(y_max) },
|
||||
options.color
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn drawSamplesMinMax(draw_rect: rl.Rectangle, options: ViewOptions, min_max_cache: MinMaxCache) void {
|
||||
const draw_x_range, const draw_y_range = RangeF64.initRect(draw_rect);
|
||||
|
||||
const i_range = options.x_range.intersectPositive(
|
||||
RangeF64.init(0, @max(@as(f64, @floatFromInt(min_max_cache.sample_count)) - 1, 0))
|
||||
);
|
||||
if (i_range.lower > i_range.upper) {
|
||||
return;
|
||||
}
|
||||
|
||||
const samples_per_column = options.x_range.size() / draw_x_range.size();
|
||||
assert(samples_per_column >= 1);
|
||||
|
||||
var i = i_range.lower;
|
||||
while (i < i_range.upper - samples_per_column) : (i += samples_per_column) {
|
||||
var column_start: usize = @intFromFloat(i);
|
||||
var column_end: usize = @intFromFloat(i + samples_per_column);
|
||||
|
||||
column_start = @divFloor(column_start, min_max_cache.chunk_size);
|
||||
column_end = @divFloor(column_end, min_max_cache.chunk_size);
|
||||
|
||||
const min_max_pairs = min_max_cache.min_max_pairs.items[column_start..column_end];
|
||||
if (min_max_pairs.len == 0) continue;
|
||||
|
||||
var column_min = min_max_pairs[0].min;
|
||||
var column_max = min_max_pairs[0].max;
|
||||
|
||||
for (min_max_pairs) |min_max_pair| {
|
||||
column_min = @min(column_min, min_max_pair.min);
|
||||
column_max = @max(column_max, min_max_pair.max);
|
||||
}
|
||||
|
||||
const x = options.x_range.remapTo(draw_x_range, i);
|
||||
const y_min = options.y_range.remapTo(draw_y_range, column_min);
|
||||
const y_max = options.y_range.remapTo(draw_y_range, column_max);
|
||||
|
||||
if (@abs(y_max - y_min) < 1) {
|
||||
const avg = (y_min + y_max) / 2;
|
||||
rl.drawLineV(
|
||||
.{ .x = @floatCast(x), .y = @floatCast(avg) },
|
||||
.{ .x = @floatCast(x), .y = @floatCast(avg+1) },
|
||||
options.color
|
||||
);
|
||||
} else {
|
||||
rl.drawLineV(
|
||||
.{ .x = @floatCast(x), .y = @floatCast(y_min) },
|
||||
.{ .x = @floatCast(x), .y = @floatCast(y_max) },
|
||||
options.color
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn drawSamples(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64, min_max_cache: ?MinMaxCache) void {
|
||||
const x_range = options.x_range;
|
||||
|
||||
if (x_range.lower >= @as(f64, @floatFromInt(samples.len))) return;
|
||||
if (x_range.lower >= @as(f64, @floatFromInt(sample_list.getLength()))) return;
|
||||
if (x_range.upper < 0) return;
|
||||
|
||||
const samples_per_column = x_range.size() / draw_rect.width;
|
||||
if (samples_per_column >= 2) {
|
||||
if (min_max_cache != null and samples_per_column > @as(f64, @floatFromInt(2*min_max_cache.?.chunk_size))) {
|
||||
drawSamplesMinMax(draw_rect, options, min_max_cache.?);
|
||||
if (samples_per_column >= @as(f64, @floatFromInt(2*SampleList.Block.capacity))) {
|
||||
drawSamplesMinMax(draw_rect, options, sample_list);
|
||||
} else {
|
||||
drawSamplesApproximate(draw_rect, options, samples);
|
||||
drawSamplesApproximate(draw_rect, options, sample_list);
|
||||
}
|
||||
} else {
|
||||
drawSamplesExact(draw_rect, options, samples);
|
||||
drawSamplesExact(draw_rect, options, sample_list);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn drawCached(cache: *RenderCache, render_size: Vec2, options: ViewOptions, samples: []const f64) void {
|
||||
pub fn drawCached(cache: *RenderCache, render_size: Vec2, options: ViewOptions, sample_list: *SampleList) void {
|
||||
const render_width: i32 = @intFromFloat(@ceil(render_size.x));
|
||||
const render_height: i32 = @intFromFloat(@ceil(render_size.y));
|
||||
|
||||
@ -384,7 +323,7 @@ pub fn drawCached(cache: *RenderCache, render_size: Vec2, options: ViewOptions,
|
||||
|
||||
const cache_key = RenderCache.Key{
|
||||
.options = options,
|
||||
.drawn_x_range = RangeF64.init(0, @max(@as(f64, @floatFromInt(samples.len)) - 1, 0)).intersectPositive(options.x_range)
|
||||
.drawn_x_range = RangeF64.init(0, @max(@as(f64, @floatFromInt(sample_list.getLength())) - 1, 0)).intersectPositive(options.x_range)
|
||||
};
|
||||
|
||||
if (cache.key != null and std.meta.eql(cache.key.?, cache_key)) {
|
||||
@ -410,23 +349,19 @@ pub fn drawCached(cache: *RenderCache, render_size: Vec2, options: ViewOptions,
|
||||
.width = render_size.x,
|
||||
.height = render_size.y
|
||||
};
|
||||
drawSamples(draw_rect, options, samples, cache.min_max_cache);
|
||||
drawSamples(draw_rect, options, sample_list);
|
||||
}
|
||||
|
||||
pub fn draw(cache: ?*RenderCache, draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void {
|
||||
pub fn draw(cache: ?*RenderCache, draw_rect: rl.Rectangle, options: ViewOptions, sample_list: *SampleList) void {
|
||||
if (draw_rect.width < 0 or draw_rect.height < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cache != null and !disable_caching) {
|
||||
const c = cache.?;
|
||||
drawCached(c, .{ .x = draw_rect.width, .y = draw_rect.height }, options, samples);
|
||||
drawCached(c, .{ .x = draw_rect.width, .y = draw_rect.height }, options, sample_list);
|
||||
c.draw(draw_rect);
|
||||
} else {
|
||||
drawSamples(draw_rect, options, samples, null);
|
||||
drawSamples(draw_rect, options, sample_list, null);
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
_ = MinMaxCache;
|
||||
}
|
61
src/main.zig
61
src/main.zig
@ -5,6 +5,7 @@ const Application = @import("./app.zig");
|
||||
const Assets = @import("./assets.zig");
|
||||
const Profiler = @import("./my-profiler.zig");
|
||||
const Platform = @import("./platform/root.zig");
|
||||
const known_folders = @import("known-folders");
|
||||
const raylib_h = @cImport({
|
||||
@cInclude("stdio.h");
|
||||
@cInclude("raylib.h");
|
||||
@ -66,12 +67,12 @@ fn raylibTraceLogCallback(logType: c_int, text: [*c]const u8, args: raylib_h.va_
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{
|
||||
.thread_safe = true
|
||||
.thread_safe = true,
|
||||
}){};
|
||||
const allocator = gpa.allocator();
|
||||
defer _ = gpa.deinit();
|
||||
|
||||
Platform.init(allocator);
|
||||
try Platform.init(allocator);
|
||||
defer Platform.deinit(allocator);
|
||||
|
||||
// TODO: Setup logging to a file
|
||||
@ -82,6 +83,7 @@ pub fn main() !void {
|
||||
var icon_image = rl.loadImageFromMemory(".png", icon_png);
|
||||
defer icon_image.unload();
|
||||
|
||||
|
||||
rl.initWindow(800, 600, "DAQ view");
|
||||
defer rl.closeWindow();
|
||||
rl.setWindowState(.{ .window_resizable = true, .vsync_hint = true });
|
||||
@ -102,39 +104,26 @@ pub fn main() !void {
|
||||
try Application.init(&app, allocator);
|
||||
defer app.deinit();
|
||||
|
||||
if (builtin.mode == .Debug) {
|
||||
var cwd_realpath_buff: [std.fs.MAX_PATH_BYTES]u8 = undefined;
|
||||
const cwd_realpath = try std.fs.cwd().realpath(".", &cwd_realpath_buff);
|
||||
var config_dir = (try known_folders.open(allocator, .roaming_configuration, .{})) orelse return error.ConfigDirNotFound;
|
||||
defer config_dir.close();
|
||||
|
||||
const save_location = try std.fs.path.join(allocator, &.{ cwd_realpath, "project.proj" });
|
||||
errdefer allocator.free(save_location);
|
||||
var app_config_dir = config_dir.openDir("daq-view", .{}) catch |e| switch (e) {
|
||||
error.FileNotFound => blk: {
|
||||
try config_dir.makeDir("daq-view");
|
||||
break :blk try config_dir.openDir("daq-view", .{});
|
||||
},
|
||||
else => return e
|
||||
};
|
||||
defer app_config_dir.close();
|
||||
|
||||
app.project.save_location = save_location;
|
||||
app.project.sample_rate = 5000;
|
||||
|
||||
_ = try app.addView(.{
|
||||
.channel = try app.addChannel("Dev1/ai0")
|
||||
});
|
||||
|
||||
_ = try app.addView(.{
|
||||
.file = try app.addFile("./samples-5k.bin")
|
||||
});
|
||||
|
||||
_ = try app.addView(.{
|
||||
.file = try app.addFile("./samples-50k.bin")
|
||||
});
|
||||
|
||||
_ = try app.addView(.{
|
||||
.file = try app.addFile("./samples-300k.bin")
|
||||
});
|
||||
|
||||
_ = try app.addView(.{
|
||||
.file = try app.addFile("./samples-9m.bin")
|
||||
});
|
||||
|
||||
_ = try app.addView(.{
|
||||
.file = try app.addFile("./samples-18m.bin")
|
||||
});
|
||||
if (app_config_dir.openFile("config.bin", .{})) |save_file| {
|
||||
defer save_file.close();
|
||||
app.loadProject(save_file) catch |e| {
|
||||
log.err("Failed to load project: {}", .{e});
|
||||
};
|
||||
} else |e| switch (e) {
|
||||
error.FileNotFound => {},
|
||||
else => return e
|
||||
}
|
||||
|
||||
var profiler: ?Profiler = null;
|
||||
@ -184,10 +173,16 @@ pub fn main() !void {
|
||||
rl.endDrawing();
|
||||
}
|
||||
|
||||
{
|
||||
const save_file = try app_config_dir.createFile("config.bin", .{});
|
||||
defer save_file.close();
|
||||
try app.saveProject(save_file);
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
_ = @import("./ni-daq/root.zig");
|
||||
_ = @import("./range.zig");
|
||||
_ = @import("./graph.zig");
|
||||
_ = Application;
|
||||
}
|
@ -21,12 +21,14 @@ pub const OpenFileOptions = struct {
|
||||
};
|
||||
|
||||
pub const Style = enum { open, save };
|
||||
pub const Kind = enum { file, folder };
|
||||
|
||||
filters: std.BoundedArray(Filter, 4) = .{},
|
||||
default_filter: ?usize = null,
|
||||
file_must_exist: bool = false,
|
||||
prompt_creation: bool = false,
|
||||
prompt_overwrite: bool = false,
|
||||
kind: Kind = .file,
|
||||
style: Style = .open,
|
||||
|
||||
pub fn appendFilter(self: *OpenFileOptions, name: []const u8, format: []const u8) !void {
|
||||
@ -94,8 +96,8 @@ pub fn waitUntilFilePickerDone(allocator: std.mem.Allocator, optional_id: *?File
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) void {
|
||||
impl.init(allocator);
|
||||
pub fn init(allocator: std.mem.Allocator) !void {
|
||||
try impl.init(allocator);
|
||||
}
|
||||
|
||||
pub fn deinit(allocator: std.mem.Allocator) void {
|
||||
|
@ -2,7 +2,7 @@ const std = @import("std");
|
||||
const rl = @import("raylib");
|
||||
const Platform = @import("root.zig");
|
||||
const windows_h = @cImport({
|
||||
@cDefine("_WIN32_WINNT", "0x0500");
|
||||
@cDefine("_WIN32_WINNT", "0x0600");
|
||||
@cInclude("windows.h");
|
||||
@cInclude("shobjidl.h");
|
||||
});
|
||||
@ -48,10 +48,18 @@ const OPENFILENAMEW = extern struct {
|
||||
extern fn GetOpenFileNameW([*c]OPENFILENAMEW) windows_h.WINBOOL;
|
||||
extern fn GetSaveFileNameW([*c]OPENFILENAMEW) windows_h.WINBOOL;
|
||||
|
||||
const FileDialogShow = *const fn ([*c]windows_h.IFileDialog, HWND) callconv(.C) windows_h.HRESULT;
|
||||
|
||||
const CLSCTX_INPROC_SERVER = 0x1;
|
||||
const FOS_PICKFOLDERS = 0x00000020;
|
||||
const FOS_FORCEFILESYSTEM = 0x00000040;
|
||||
const SIGDN_FILESYSPATH: c_uint = 0x80058000;
|
||||
|
||||
const OpenedFilePicker = struct {
|
||||
kind: OpenFileOptions.Kind,
|
||||
style: OpenFileOptions.Style,
|
||||
filename_w_buffer: [std.os.windows.PATH_MAX_WIDE]u16,
|
||||
lpstrFilter_utf16: [:0]u16,
|
||||
lpstrFilter_utf16: ?[:0]u16 = null,
|
||||
ofn: OPENFILENAMEW,
|
||||
thread: std.Thread,
|
||||
status: Platform.FilePickerStatus,
|
||||
@ -63,11 +71,12 @@ const OpenedFilePicker = struct {
|
||||
.ofn = std.mem.zeroes(OPENFILENAMEW),
|
||||
.style = opts.style,
|
||||
.filename_w_buffer = undefined,
|
||||
.lpstrFilter_utf16 = undefined,
|
||||
.thread = undefined,
|
||||
.status = Platform.FilePickerStatus.in_progress
|
||||
.status = Platform.FilePickerStatus.in_progress,
|
||||
.kind = opts.kind
|
||||
};
|
||||
|
||||
if (opts.kind == .file) {
|
||||
const hWnd: HWND = @alignCast(@ptrCast(rl.getWindowHandle()));
|
||||
assert(hWnd != null);
|
||||
|
||||
@ -81,8 +90,9 @@ const OpenedFilePicker = struct {
|
||||
try lpstrFilter_utf8.append(allocator, 0);
|
||||
}
|
||||
|
||||
self.lpstrFilter_utf16 = try std.unicode.utf8ToUtf16LeWithNull(allocator, lpstrFilter_utf8.items);
|
||||
errdefer allocator.free(self.lpstrFilter_utf16);
|
||||
const lpstrFilter_utf16 = try std.unicode.utf8ToUtf16LeWithNull(allocator, lpstrFilter_utf8.items);
|
||||
errdefer allocator.free(lpstrFilter_utf16);
|
||||
self.lpstrFilter_utf16 = lpstrFilter_utf16;
|
||||
|
||||
var flags: c_ulong = 0;
|
||||
{
|
||||
@ -114,9 +124,11 @@ const OpenedFilePicker = struct {
|
||||
self.ofn.hwndOwner = hWnd;
|
||||
self.ofn.lpstrFile = &self.filename_w_buffer;
|
||||
self.ofn.nMaxFile = self.filename_w_buffer.len;
|
||||
self.ofn.lpstrFilter = self.lpstrFilter_utf16;
|
||||
self.ofn.lpstrFilter = lpstrFilter_utf16;
|
||||
self.ofn.nFilterIndex = nFilterIndex;
|
||||
self.ofn.Flags = flags;
|
||||
}
|
||||
|
||||
|
||||
// Creating this thread must be the last thing that this function does.
|
||||
// Because if an error occurs after a thread is spawn, it will not be joined/stopped
|
||||
@ -125,10 +137,13 @@ const OpenedFilePicker = struct {
|
||||
|
||||
fn deinit(self: *OpenedFilePicker, allocator: std.mem.Allocator) void {
|
||||
self.thread.join();
|
||||
allocator.free(self.lpstrFilter_utf16);
|
||||
if (self.lpstrFilter_utf16) |lpstrFilter_utf16| {
|
||||
allocator.free(lpstrFilter_utf16);
|
||||
}
|
||||
}
|
||||
|
||||
fn showDialog(self: *OpenedFilePicker) !void {
|
||||
if (self.kind == .file) {
|
||||
const result = switch (self.style) {
|
||||
.open => GetOpenFileNameW(&self.ofn),
|
||||
.save => GetSaveFileNameW(&self.ofn)
|
||||
@ -156,6 +171,63 @@ const OpenedFilePicker = struct {
|
||||
return error.UnknownFileDialogError;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
var maybe_file_dialog: ?*windows_h.IFileDialog = null;
|
||||
const hr_create = windows_h.CoCreateInstance(
|
||||
&windows_h.CLSID_FileOpenDialog,
|
||||
null,
|
||||
CLSCTX_INPROC_SERVER,
|
||||
&windows_h.IID_IFileDialog,
|
||||
@ptrCast(&maybe_file_dialog),
|
||||
);
|
||||
|
||||
if (windows_h.FAILED(hr_create) or maybe_file_dialog == null) {
|
||||
log.err("Failed to create FileOpenDialog (HRESULT: {x})", .{hr_create});
|
||||
return error.CoCreateInstance;
|
||||
}
|
||||
|
||||
const file_dialog = maybe_file_dialog.?;
|
||||
const lpVtbl = maybe_file_dialog.?.lpVtbl[0];
|
||||
|
||||
defer _ = lpVtbl.Release.?(file_dialog);
|
||||
|
||||
var options: windows_h.DWORD = 0;
|
||||
if (windows_h.FAILED(lpVtbl.GetOptions.?(file_dialog, &options))) {
|
||||
log.err("Failed to get options", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
_ = lpVtbl.SetOptions.?(
|
||||
file_dialog,
|
||||
options | FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM,
|
||||
);
|
||||
|
||||
const hWnd: HWND = @alignCast(@ptrCast(rl.getWindowHandle()));
|
||||
assert(hWnd != null);
|
||||
|
||||
const lpVtblShow: FileDialogShow = @ptrCast(lpVtbl.Show.?);
|
||||
const hr_show = lpVtblShow(file_dialog, hWnd);
|
||||
if (!windows_h.SUCCEEDED(hr_show)) {
|
||||
log.warn("Dialog cancelled or failed (HRESULT: {x})", .{hr_show});
|
||||
return error.Canceled;
|
||||
}
|
||||
|
||||
var item: ?*windows_h.IShellItem = null;
|
||||
const hr_result = lpVtbl.GetResult.?(file_dialog, &item);
|
||||
if (windows_h.SUCCEEDED(hr_result) and item != null) {
|
||||
defer _ = item.?.lpVtbl[0].Release.?(item.?);
|
||||
|
||||
var path: windows_h.LPWSTR = null;
|
||||
const hr_display = item.?.lpVtbl[0].GetDisplayName.?(item.?, @bitCast(SIGDN_FILESYSPATH), &path);
|
||||
if (windows_h.SUCCEEDED(hr_display) and path != null) {
|
||||
defer windows_h.CoTaskMemFree(path);
|
||||
|
||||
const path_slice = std.mem.sliceTo(path, 0);
|
||||
@memcpy(self.filename_w_buffer[0..path_slice.len], path_slice);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn threadCallback(self: *OpenedFilePicker) void {
|
||||
@ -285,9 +357,16 @@ pub fn isFilePickerOpen(self: *Self) bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn init(self: *Self, allocator: std.mem.Allocator) void {
|
||||
pub fn init(self: *Self, allocator: std.mem.Allocator) !void {
|
||||
_ = windows_h.SetConsoleOutputCP(65001);
|
||||
|
||||
const COINIT_APARTMENTTHREADED = 0x2;
|
||||
|
||||
if (windows_h.CoInitializeEx(null, COINIT_APARTMENTTHREADED) != windows_h.S_OK) {
|
||||
log.err("Failed to initialize COM", .{});
|
||||
return error.CoInitialize;
|
||||
}
|
||||
|
||||
self.* = Self{
|
||||
.allocator = allocator
|
||||
};
|
||||
@ -300,4 +379,6 @@ pub fn deinit(self: *Self, allocator: std.mem.Allocator) void {
|
||||
maybe_opened_file_picker.* = null;
|
||||
}
|
||||
}
|
||||
|
||||
windows_h.CoUninitialize();
|
||||
}
|
@ -190,6 +190,15 @@ pub fn initCentered(rect: Rect, width: f32, height: f32) Rect {
|
||||
return Rect.init(rect.x + unused_width / 2, rect.y + unused_height / 2, width, height);
|
||||
}
|
||||
|
||||
pub fn initVec2(pos: rl.Vector2, rect_size: rl.Vector2) Rect {
|
||||
return Rect{
|
||||
.x = pos.x,
|
||||
.y = pos.y,
|
||||
.width = rect_size.x,
|
||||
.height = rect_size.y
|
||||
};
|
||||
}
|
||||
|
||||
pub fn aligned(rect: Rect, align_x: AlignX, align_y: AlignY) rl.Vector2 {
|
||||
const x = switch(align_x) {
|
||||
.left => rect.x,
|
||||
|
@ -222,6 +222,9 @@ pub fn tick(self: *Screen) !void {
|
||||
.channel = try self.app.addChannel(channel)
|
||||
});
|
||||
}
|
||||
|
||||
_ = self.channel_names.reset(.free_all);
|
||||
self.selected_channels.len = 0;
|
||||
}
|
||||
|
||||
} else {
|
||||
|
@ -22,32 +22,57 @@ const Id = App.Id;
|
||||
|
||||
app: *App,
|
||||
view_controls: ViewControlsSystem,
|
||||
view_solutions: bool = false,
|
||||
view_statistics_points: bool = false,
|
||||
modal: ?union(enum){
|
||||
view_protocol: Id, // View id
|
||||
notes
|
||||
} = null,
|
||||
|
||||
// Protocol modal
|
||||
frequency_input: UI.TextInputStorage,
|
||||
amplitude_input: UI.TextInputStorage,
|
||||
protocol_error_message: ?[]const u8 = null,
|
||||
protocol_graph_cache: Graph.RenderCache = .{},
|
||||
preview_samples: std.ArrayListUnmanaged(f64) = .{},
|
||||
preview_sample_list_id: App.Id,
|
||||
preview_samples_y_range: RangeF64 = RangeF64.init(0, 0),
|
||||
|
||||
// Notes modal
|
||||
notes_storage: UI.TextInputStorage,
|
||||
|
||||
// Project settings
|
||||
solution_input: UI.TextInputStorage,
|
||||
sample_rate_input: UI.TextInputStorage,
|
||||
experiment_name: UI.TextInputStorage,
|
||||
pipete_solution: UI.TextInputStorage,
|
||||
export_location_picker: ?Platform.FilePickerId = null,
|
||||
parsed_sample_rate: ?f64 = null,
|
||||
|
||||
// View settings
|
||||
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)
|
||||
.notes_storage = UI.TextInputStorage.init(allocator),
|
||||
.solution_input = UI.TextInputStorage.init(allocator),
|
||||
.experiment_name = UI.TextInputStorage.init(allocator),
|
||||
.pipete_solution = UI.TextInputStorage.init(allocator),
|
||||
.view_controls = ViewControlsSystem.init(&app.project),
|
||||
.transform_inputs = transform_inputs,
|
||||
.preview_sample_list_id = try app.project.addSampleList(allocator)
|
||||
};
|
||||
|
||||
try self.frequency_input.setText("10");
|
||||
@ -57,12 +82,17 @@ pub fn init(app: *App) !MainScreen {
|
||||
}
|
||||
|
||||
pub fn deinit(self: *MainScreen) void {
|
||||
const allocator = self.app.allocator;
|
||||
|
||||
self.frequency_input.deinit();
|
||||
self.amplitude_input.deinit();
|
||||
self.sample_rate_input.deinit();
|
||||
self.preview_samples.clearAndFree(allocator);
|
||||
self.notes_storage.deinit();
|
||||
self.solution_input.deinit();
|
||||
self.experiment_name.deinit();
|
||||
self.pipete_solution.deinit();
|
||||
for (self.transform_inputs) |input| {
|
||||
input.deinit();
|
||||
}
|
||||
self.app.project.removeSampleList(self.preview_sample_list_id);
|
||||
|
||||
self.clearProtocolErrorMessage();
|
||||
}
|
||||
@ -98,16 +128,17 @@ pub fn showProtocolModal(self: *MainScreen, view_id: Id) !void {
|
||||
.borders = UI.Borders.all(.{ .color = srcery.hard_black, .size = 2 })
|
||||
});
|
||||
|
||||
const samples = self.preview_samples.items;
|
||||
if (self.app.project.sample_lists.get(self.preview_sample_list_id)) |sample_list| {
|
||||
const view_rect = Graph.ViewOptions{
|
||||
.x_range = RangeF64.init(0, @floatFromInt(samples.len)),
|
||||
.x_range = RangeF64.init(0, @floatFromInt(sample_list.getLength())),
|
||||
.y_range = self.preview_samples_y_range
|
||||
};
|
||||
Graph.drawCached(&self.protocol_graph_cache, protocol_view.persistent.size, view_rect, samples);
|
||||
Graph.drawCached(&self.protocol_graph_cache, protocol_view.persistent.size, view_rect, sample_list);
|
||||
if (self.protocol_graph_cache.texture) |texture| {
|
||||
protocol_view.texture = texture.texture;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const FormInput = struct {
|
||||
name: []const u8,
|
||||
@ -180,10 +211,18 @@ pub fn showProtocolModal(self: *MainScreen, view_id: Id) !void {
|
||||
}
|
||||
|
||||
if (self.protocol_error_message == null and any_input_modified) {
|
||||
try App.Channel.generateSine(&self.preview_samples, allocator, sample_rate, frequency, amplitude);
|
||||
if (self.app.project.sample_lists.get(self.preview_sample_list_id)) |sample_list| {
|
||||
var preview_samples: std.ArrayListUnmanaged(f64) = .{};
|
||||
defer preview_samples.deinit(allocator);
|
||||
|
||||
try App.Channel.generateSine(&preview_samples, allocator, sample_rate, frequency, amplitude);
|
||||
sample_list.clear(allocator);
|
||||
try sample_list.append(preview_samples.items);
|
||||
|
||||
self.preview_samples_y_range = RangeF64.init(-amplitude*1.1, amplitude*1.1);
|
||||
self.protocol_graph_cache.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
if (self.protocol_error_message) |message| {
|
||||
_ = ui.createBox(.{
|
||||
@ -227,6 +266,44 @@ pub fn showProtocolModal(self: *MainScreen, view_id: Id) !void {
|
||||
_ = ui.signal(container);
|
||||
}
|
||||
|
||||
fn showNotesModal(self: *MainScreen) !void {
|
||||
var ui = &self.app.ui;
|
||||
|
||||
const container = ui.createBox(.{
|
||||
.key = ui.keyFromString("Notes modal"),
|
||||
.background = srcery.black,
|
||||
.size_x = UI.Sizing.initGrowUpTo(.{ .pixels = 400 }),
|
||||
.size_y = UI.Sizing.initGrowUpTo(.{ .pixels = 600 }),
|
||||
.layout_direction = .top_to_bottom,
|
||||
.padding = UI.Padding.all(ui.rem(1.5)),
|
||||
.flags = &.{ .clickable },
|
||||
.layout_gap = ui.rem(0.5)
|
||||
});
|
||||
container.beginChildren();
|
||||
defer container.endChildren();
|
||||
defer _ = ui.signal(container);
|
||||
|
||||
const label = ui.label("Notes", .{});
|
||||
label.font = .{ .variant = .regular_italic, .size = ui.rem(2) };
|
||||
label.alignment.x = .center;
|
||||
label.size.x = UI.Sizing.initGrowFull();
|
||||
|
||||
try ui.textInput(.{
|
||||
.key = ui.keyFromString("Notes"),
|
||||
.storage = &self.notes_storage,
|
||||
.size_x = UI.Sizing.initGrowFull(),
|
||||
.size_y = UI.Sizing.initGrowFull(),
|
||||
.single_line = false
|
||||
});
|
||||
|
||||
if (self.notes_storage.modified) {
|
||||
const project = &self.app.project;
|
||||
|
||||
project.notes.clearRetainingCapacity();
|
||||
try project.notes.insertSlice(self.app.allocator, 0, self.notes_storage.buffer.items);
|
||||
}
|
||||
}
|
||||
|
||||
fn setProtocolErrorMessage(self: *MainScreen, comptime fmt: []const u8, args: anytype) !void {
|
||||
self.clearProtocolErrorMessage();
|
||||
|
||||
@ -258,23 +335,125 @@ fn showProjectSettings(self: *MainScreen) !void {
|
||||
|
||||
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) });
|
||||
|
||||
{
|
||||
_ = ui.label("Experiment:", .{});
|
||||
try ui.textInput(.{
|
||||
.key = ui.keyFromString("Experiment name"),
|
||||
.storage = &self.experiment_name,
|
||||
.initial = project.experiment_name.items,
|
||||
.size_x = UI.Sizing.initGrowFull()
|
||||
});
|
||||
|
||||
if (self.experiment_name.modified) {
|
||||
project.experiment_name.clearRetainingCapacity();
|
||||
try project.experiment_name.insertSlice(self.app.allocator, 0, self.experiment_name.buffer.items);
|
||||
}
|
||||
}
|
||||
|
||||
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) });
|
||||
|
||||
{
|
||||
_ = ui.label("Pipete solution:", .{});
|
||||
try ui.textInput(.{
|
||||
.key = ui.keyFromString("Pipete solution"),
|
||||
.storage = &self.pipete_solution,
|
||||
.initial = project.pipete_solution.items,
|
||||
.size_x = UI.Sizing.initGrowFull()
|
||||
});
|
||||
|
||||
if (self.pipete_solution.modified) {
|
||||
project.pipete_solution.clearRetainingCapacity();
|
||||
try project.pipete_solution.insertSlice(self.app.allocator, 0, self.pipete_solution.buffer.items);
|
||||
}
|
||||
}
|
||||
|
||||
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) });
|
||||
|
||||
{
|
||||
_ = ui.label("Solution:", .{});
|
||||
|
||||
try ui.textInput(.{
|
||||
.key = ui.keyFromString("Solution input"),
|
||||
.storage = &self.solution_input,
|
||||
.size_x = UI.Sizing.initGrowFull()
|
||||
});
|
||||
|
||||
{
|
||||
const row = ui.createBox(.{
|
||||
.size_x = UI.Sizing.initFitChildren(),
|
||||
.size_y = UI.Sizing.initFitChildren(),
|
||||
.layout_direction = .left_to_right,
|
||||
.layout_gap = ui.rem(1)
|
||||
});
|
||||
row.beginChildren();
|
||||
defer row.endChildren();
|
||||
|
||||
{
|
||||
const btn = ui.textButton("Append solution");
|
||||
btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 });
|
||||
if (ui.signal(btn).clicked()) {
|
||||
if (self.solution_input.buffer.items.len > 0) {
|
||||
try project.appendSolution(self.app.allocator, self.solution_input.buffer.items);
|
||||
}
|
||||
self.solution_input.clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (project.solutions.items.len > 0) {
|
||||
const btn = ui.textButton("View solutions");
|
||||
btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 });
|
||||
if (ui.signal(btn).clicked()) {
|
||||
self.view_solutions = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) });
|
||||
|
||||
{
|
||||
const row = ui.createBox(.{
|
||||
.size_x = UI.Sizing.initFitChildren(),
|
||||
.size_y = UI.Sizing.initFitChildren(),
|
||||
.layout_direction = .left_to_right,
|
||||
.layout_gap = ui.rem(0.5)
|
||||
});
|
||||
row.beginChildren();
|
||||
defer row.endChildren();
|
||||
|
||||
{
|
||||
const btn = ui.textButton("Add statistic point");
|
||||
btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 });
|
||||
if (ui.signal(btn).clicked()) {
|
||||
const current_sample = project.getSampleTimestamp() orelse 0;
|
||||
try project.statistic_points.append(self.app.allocator, current_sample);
|
||||
}
|
||||
}
|
||||
|
||||
if (project.statistic_points.items.len > 0) {
|
||||
const btn = ui.textButton("View statistic points");
|
||||
btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 });
|
||||
if (ui.signal(btn).clicked()) {
|
||||
self.view_statistics_points = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) });
|
||||
|
||||
{ // Sample rate
|
||||
var placeholder: ?[]const u8 = null;
|
||||
if (project.getDefaultSampleRate()) |default_sample_rate| {
|
||||
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", .{});
|
||||
_ = 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()
|
||||
});
|
||||
@ -289,10 +468,44 @@ fn showProjectSettings(self: *MainScreen) !void {
|
||||
}
|
||||
}
|
||||
|
||||
_ = ui.checkbox(.{
|
||||
.value = &project.show_rulers,
|
||||
.label = "Ruler"
|
||||
});
|
||||
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) });
|
||||
|
||||
{
|
||||
const btn = ui.textButton("Open notes");
|
||||
btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 });
|
||||
if (ui.signal(btn).clicked()) {
|
||||
self.modal = .notes;
|
||||
}
|
||||
}
|
||||
|
||||
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) });
|
||||
|
||||
{
|
||||
_ = ui.label("Export folder:", .{});
|
||||
if (ui.fileInput(.{
|
||||
.allocator = self.app.allocator,
|
||||
.key = ui.keyFromString("Export location"),
|
||||
.path = project.export_location,
|
||||
.file_picker = &self.export_location_picker,
|
||||
.open_dialog = true,
|
||||
.folder = true
|
||||
})) |path| {
|
||||
if (project.export_location) |str| {
|
||||
self.app.allocator.free(str);
|
||||
}
|
||||
project.export_location = path;
|
||||
}
|
||||
|
||||
const btn = ui.textButton("Export");
|
||||
if (self.app.isCollectionInProgress() or project.export_location == null) {
|
||||
btn.borders = UI.Borders.all(.{ .color = srcery.hard_black, .size = 4 });
|
||||
} else {
|
||||
btn.borders = UI.Borders.all(.{ .color = srcery.green, .size = 4 });
|
||||
if (ui.signal(btn).clicked()) {
|
||||
self.app.pushCommand(.export_project);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn showViewSettings(self: *MainScreen, view_id: Id) !void {
|
||||
@ -317,13 +530,12 @@ fn showViewSettings(self: *MainScreen, view_id: Id) !void {
|
||||
.label = "Sync controls"
|
||||
});
|
||||
|
||||
var sample_count: ?usize = null;
|
||||
|
||||
switch (view.reference) {
|
||||
.channel => |channel_id| {
|
||||
const channel = project.channels.get(channel_id).?;
|
||||
const channel_name = utils.getBoundedStringZ(&channel.name);
|
||||
const channel_type = NIDaq.getChannelType(channel_name);
|
||||
const samples = channel.collected_samples.items;
|
||||
|
||||
_ = ui.label("Channel: {s}", .{ channel_name });
|
||||
|
||||
@ -332,30 +544,10 @@ fn showViewSettings(self: *MainScreen, view_id: Id) !void {
|
||||
} else {
|
||||
_ = ui.label("Type: unknown", .{ });
|
||||
}
|
||||
|
||||
if (ui.fileInput(.{
|
||||
.key = ui.keyFromString("Save location"),
|
||||
.allocator = self.app.allocator,
|
||||
.file_picker = &self.channel_save_file_picker,
|
||||
.path = channel.saved_collected_samples,
|
||||
.open_dialog = false
|
||||
})) |path| {
|
||||
if (channel.saved_collected_samples) |current_path| {
|
||||
self.app.allocator.free(current_path);
|
||||
}
|
||||
|
||||
channel.saved_collected_samples = path;
|
||||
}
|
||||
|
||||
sample_count = samples.len;
|
||||
},
|
||||
.file => |file_id| {
|
||||
const file = project.files.get(file_id).?;
|
||||
|
||||
if (file.samples) |samples| {
|
||||
sample_count = samples.len;
|
||||
}
|
||||
|
||||
if (ui.fileInput(.{
|
||||
.key = ui.keyFromString("Filename"),
|
||||
.allocator = self.app.allocator,
|
||||
@ -367,20 +559,142 @@ fn showViewSettings(self: *MainScreen, view_id: Id) !void {
|
||||
self.app.pushCommand(.{ .reload_file = file_id });
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (sample_count != null) {
|
||||
_ = ui.label("Samples: {d}", .{ sample_count.? });
|
||||
const sample_list_id = project.getViewSampleListId(view_id);
|
||||
const sample_list = project.sample_lists.get(sample_list_id).?;
|
||||
const sample_count = sample_list.getLength();
|
||||
|
||||
var duration_str: []const u8 = "-";
|
||||
_ = ui.label("Samples: {d}", .{ sample_count });
|
||||
|
||||
var duration_str: ?[]const u8 = null;
|
||||
if (sample_rate != null) {
|
||||
const duration = @as(f64, @floatFromInt(sample_count.?)) / sample_rate.?;
|
||||
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;
|
||||
}
|
||||
|
||||
_ = ui.label("Duration: {s}", .{ duration_str orelse "-" });
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -413,6 +727,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 });
|
||||
@ -452,6 +768,34 @@ fn showMarkedRange(self: *MainScreen, view_id: Id, index: usize) void {
|
||||
}
|
||||
}
|
||||
|
||||
fn showMarker(self: *MainScreen, view_id: Id, index: usize) void {
|
||||
var ui = &self.app.ui;
|
||||
|
||||
const view = self.app.getView(view_id) orelse return;
|
||||
|
||||
const marker = view.markers.get(index);
|
||||
|
||||
{
|
||||
const label = ui.label("Selected range", .{});
|
||||
label.borders.bottom = .{
|
||||
.color = srcery.blue,
|
||||
.size = 1
|
||||
};
|
||||
|
||||
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) });
|
||||
}
|
||||
|
||||
const sample_rate = self.app.project.getSampleRate();
|
||||
|
||||
if (sample_rate != null) {
|
||||
const duration = utils.formatDuration(ui.frameAllocator(), marker / sample_rate.?) catch "";
|
||||
_ = ui.label("Position: {s}", .{ duration });
|
||||
} else {
|
||||
_ = ui.label("Position: {d:.2}", .{ marker });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fn showToolbar(self: *MainScreen) void {
|
||||
var ui = &self.app.ui;
|
||||
|
||||
@ -489,14 +833,6 @@ fn showToolbar(self: *MainScreen) void {
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var btn = ui.textButton("Save");
|
||||
btn.borders = UI.Borders.all(.{ .size = 4, .color = srcery.green });
|
||||
if (ui.signal(btn).clicked()) {
|
||||
self.app.pushCommand(.save_project);
|
||||
}
|
||||
}
|
||||
|
||||
_ = ui.createBox(.{ .size_x = UI.Sizing.initFixedPixels(ui.rem(1)) });
|
||||
|
||||
{
|
||||
@ -505,7 +841,7 @@ fn showToolbar(self: *MainScreen) void {
|
||||
btn.borders = UI.Borders.bottom(.{ .size = 4, .color = srcery.green });
|
||||
}
|
||||
|
||||
if (ui.signal(btn).clicked() or ui.isKeyboardPressed(.key_one)) {
|
||||
if (ui.signal(btn).clicked()) {
|
||||
self.view_controls.selected_tool = .move;
|
||||
}
|
||||
}
|
||||
@ -516,10 +852,117 @@ fn showToolbar(self: *MainScreen) void {
|
||||
btn.borders = UI.Borders.bottom(.{ .size = 4, .color = srcery.green });
|
||||
}
|
||||
|
||||
if (ui.signal(btn).clicked() or ui.isKeyboardPressed(.key_two)) {
|
||||
if (ui.signal(btn).clicked()) {
|
||||
self.view_controls.selected_tool = .select;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var btn = ui.textButton("Marker");
|
||||
if (self.view_controls.selected_tool == .marker) {
|
||||
btn.borders = UI.Borders.bottom(.{ .size = 4, .color = srcery.green });
|
||||
}
|
||||
|
||||
if (ui.signal(btn).clicked()) {
|
||||
self.view_controls.selected_tool = .marker;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn showSolutions(self: *MainScreen) !void {
|
||||
var ui = &self.app.ui;
|
||||
const project = &self.app.project;
|
||||
_ = &ui;
|
||||
|
||||
{
|
||||
const label = ui.label("Solutions", .{});
|
||||
label.borders.bottom = .{
|
||||
.color = srcery.white,
|
||||
.size = 1
|
||||
};
|
||||
|
||||
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) });
|
||||
}
|
||||
|
||||
const sample_rate = project.getSampleRate();
|
||||
|
||||
for (project.solutions.items) |solution| {
|
||||
const row = ui.createBox(.{
|
||||
.size_x = UI.Sizing.initGrowFull(),
|
||||
.size_y = UI.Sizing.initFitChildren(),
|
||||
.layout_direction = .left_to_right,
|
||||
.layout_gap = ui.rem(1)
|
||||
});
|
||||
row.beginChildren();
|
||||
defer row.endChildren();
|
||||
|
||||
if (sample_rate != null) {
|
||||
const seconds = @as(f64, @floatFromInt(solution.sample)) / sample_rate.?;
|
||||
_ = ui.label("{s}", .{ try utils.formatDuration(ui.frameAllocator(), seconds) });
|
||||
} else {
|
||||
_ = ui.label("{d}", .{ solution.sample });
|
||||
}
|
||||
|
||||
var description = ui.label("{s}", .{ solution.description });
|
||||
description.size.x = UI.Sizing.initGrowFull();
|
||||
description.flags.insert(.wrap_text);
|
||||
}
|
||||
|
||||
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) });
|
||||
|
||||
{
|
||||
const btn = ui.textButton("Close");
|
||||
btn.borders = UI.Borders.all(.{ .color = srcery.red, .size = 4 });
|
||||
if (ui.signal(btn).clicked()) {
|
||||
self.view_solutions = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn showStatisticsPoints(self: *MainScreen) !void {
|
||||
var ui = &self.app.ui;
|
||||
const project = &self.app.project;
|
||||
_ = &ui;
|
||||
|
||||
{
|
||||
const label = ui.label("Statistics points", .{});
|
||||
label.borders.bottom = .{
|
||||
.color = srcery.white,
|
||||
.size = 1
|
||||
};
|
||||
|
||||
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) });
|
||||
}
|
||||
|
||||
const sample_rate = project.getSampleRate();
|
||||
|
||||
for (project.statistic_points.items) |sample_timestamp| {
|
||||
const row = ui.createBox(.{
|
||||
.size_x = UI.Sizing.initGrowFull(),
|
||||
.size_y = UI.Sizing.initFitChildren(),
|
||||
.layout_direction = .left_to_right,
|
||||
.layout_gap = ui.rem(1)
|
||||
});
|
||||
row.beginChildren();
|
||||
defer row.endChildren();
|
||||
|
||||
if (sample_rate != null) {
|
||||
const seconds = @as(f64, @floatFromInt(sample_timestamp)) / sample_rate.?;
|
||||
_ = ui.label("{s}", .{ try utils.formatDuration(ui.frameAllocator(), seconds) });
|
||||
} else {
|
||||
_ = ui.label("{d}", .{ sample_timestamp });
|
||||
}
|
||||
}
|
||||
|
||||
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) });
|
||||
|
||||
{
|
||||
const btn = ui.textButton("Close");
|
||||
btn.borders = UI.Borders.all(.{ .color = srcery.red, .size = 4 });
|
||||
if (ui.signal(btn).clicked()) {
|
||||
self.view_statistics_points = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn showSidePanel(self: *MainScreen) !void {
|
||||
@ -538,12 +981,18 @@ pub fn showSidePanel(self: *MainScreen) !void {
|
||||
container.beginChildren();
|
||||
defer container.endChildren();
|
||||
|
||||
_ = ui.createBox(.{ .size_x = UI.Sizing.initFixedPixels(ui.rem(12)) });
|
||||
_ = ui.createBox(.{ .size_x = UI.Sizing.initFixedPixels(ui.rem(18)) });
|
||||
|
||||
if (self.view_controls.show_marked_range) |show_marked_range| {
|
||||
if (self.view_solutions) {
|
||||
try self.showSolutions();
|
||||
} else if (self.view_controls.show_marker) |marker| {
|
||||
self.showMarker(marker.view_id, marker.index);
|
||||
} else if (self.view_controls.show_marked_range) |show_marked_range| {
|
||||
self.showMarkedRange(show_marked_range.view_id, show_marked_range.index);
|
||||
} else if (self.view_controls.view_settings) |view_id| {
|
||||
try self.showViewSettings(view_id);
|
||||
} else if (self.view_statistics_points) {
|
||||
try self.showStatisticsPoints();
|
||||
} else {
|
||||
try self.showProjectSettings();
|
||||
}
|
||||
@ -559,10 +1008,17 @@ pub fn tick(self: *MainScreen) !void {
|
||||
const root = ui.parentBox().?;
|
||||
root.layout_direction = .top_to_bottom;
|
||||
|
||||
const was_protocol_modal_open = self.view_controls.view_protocol_modal != null;
|
||||
if (self.view_controls.view_protocol_modal) |view_protocol_modal| {
|
||||
if (self.modal == null) {
|
||||
self.modal = .{ .view_protocol = view_protocol_modal };
|
||||
}
|
||||
self.view_controls.view_protocol_modal = null;
|
||||
}
|
||||
|
||||
const was_modal_open = self.modal != null;
|
||||
|
||||
var maybe_modal_overlay: ?*UI.Box = null;
|
||||
if (self.view_controls.view_protocol_modal) |view_id| {
|
||||
if (self.modal != null) {
|
||||
const padding = UI.Padding.all(ui.rem(2));
|
||||
const modal_overlay = ui.createBox(.{
|
||||
.key = ui.keyFromString("Overlay"),
|
||||
@ -582,10 +1038,13 @@ pub fn tick(self: *MainScreen) !void {
|
||||
modal_overlay.beginChildren();
|
||||
defer modal_overlay.endChildren();
|
||||
|
||||
try self.showProtocolModal(view_id);
|
||||
switch (self.modal.?) {
|
||||
.view_protocol => |view_id| try self.showProtocolModal(view_id),
|
||||
.notes => try self.showNotesModal()
|
||||
}
|
||||
|
||||
if (ui.signal(modal_overlay).clicked()) {
|
||||
self.view_controls.view_protocol_modal = null;
|
||||
self.modal = null;
|
||||
}
|
||||
|
||||
maybe_modal_overlay = modal_overlay;
|
||||
@ -599,10 +1058,7 @@ pub fn tick(self: *MainScreen) !void {
|
||||
.view_controls = &self.view_controls
|
||||
};
|
||||
|
||||
if (self.view_controls.view_fullscreen) |view_id| {
|
||||
_ = try UIView.show(ui_view_ctx, view_id, UI.Sizing.initGrowFull());
|
||||
|
||||
} else {
|
||||
{
|
||||
const container = ui.createBox(.{
|
||||
.size_x = UI.Sizing.initGrowFull(),
|
||||
.size_y = UI.Sizing.initGrowFull(),
|
||||
@ -655,21 +1111,23 @@ pub fn tick(self: *MainScreen) !void {
|
||||
}
|
||||
|
||||
if (ui.isKeyboardPressed(.key_escape)) {
|
||||
if (self.view_controls.view_protocol_modal != null) {
|
||||
self.view_controls.view_protocol_modal = null;
|
||||
} else if (self.view_controls.view_fullscreen != null) {
|
||||
self.view_controls.view_fullscreen = null;
|
||||
if (self.modal != null) {
|
||||
self.modal = null;
|
||||
} else if (self.view_controls.view_settings != null) {
|
||||
self.view_controls.view_settings = null;
|
||||
} else if (self.view_controls.show_marked_range != null) {
|
||||
self.view_controls.show_marked_range = null;
|
||||
} else if (self.view_solutions) {
|
||||
self.view_solutions = false;
|
||||
} else if (self.view_statistics_points) {
|
||||
self.view_statistics_points = false;
|
||||
} else {
|
||||
self.app.should_close = true;
|
||||
}
|
||||
}
|
||||
|
||||
const is_protocol_modal_open = self.view_controls.view_protocol_modal != null;
|
||||
if (!was_protocol_modal_open and is_protocol_modal_open) {
|
||||
const is_modal_open = self.modal != null;
|
||||
if (!was_modal_open and is_modal_open) {
|
||||
self.protocol_graph_cache.clear();
|
||||
}
|
||||
}
|
468
src/ui.zig
468
src/ui.zig
@ -403,6 +403,24 @@ pub const Padding = struct {
|
||||
.Y => self.top + self.bottom
|
||||
};
|
||||
}
|
||||
|
||||
pub fn apply(self: Padding, rect: Rect) Rect {
|
||||
return Rect{
|
||||
.x = rect.x + self.left,
|
||||
.y = rect.y + self.top,
|
||||
.width = rect.width - self.left - self.right,
|
||||
.height = rect.height - self.top - self.bottom
|
||||
};
|
||||
}
|
||||
|
||||
pub fn applyPosition(self: Padding, rect: Rect) Rect {
|
||||
return Rect{
|
||||
.x = rect.x + self.left,
|
||||
.y = rect.y + self.top,
|
||||
.width = rect.width,
|
||||
.height = rect.height
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Alignment = enum {
|
||||
@ -480,6 +498,7 @@ pub const Box = struct {
|
||||
sroll_offset: f32 = 0,
|
||||
hot: f32 = 0,
|
||||
active: f32 = 0,
|
||||
open: bool = false
|
||||
};
|
||||
|
||||
pub const Flag = enum {
|
||||
@ -491,6 +510,8 @@ pub const Box = struct {
|
||||
draggable,
|
||||
draw_hot,
|
||||
draw_active,
|
||||
// TODO: Add a way to specify relative to which box should clipping occur.
|
||||
// Useful for floating boxes
|
||||
clip_view
|
||||
};
|
||||
|
||||
@ -538,6 +559,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 +598,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 +640,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 +657,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 +804,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 +826,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 +1569,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 +1667,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,19 +1750,26 @@ 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 {
|
||||
fn drawBox(self: *UI, box: *Box, on_top_pass: ?bool) void {
|
||||
var child_on_top_pass = on_top_pass;
|
||||
|
||||
var do_scissor = false;
|
||||
defer if (do_scissor) self.endScissor();
|
||||
|
||||
if (on_top_pass == null or box.draw_on_top == on_top_pass) {
|
||||
const box_rect = box.rect();
|
||||
|
||||
const do_scissor = box.flags.contains(.clip_view);
|
||||
do_scissor = box.flags.contains(.clip_view);
|
||||
if (do_scissor) self.beginScissor(box_rect);
|
||||
defer if (do_scissor) self.endScissor();
|
||||
// defer if (do_scissor) rl.drawRectangleLinesEx(box_rect, 1, rl.Color.magenta);
|
||||
|
||||
var value_shift: f32 = 0;
|
||||
if (box.flags.contains(.draw_active) and box.persistent.active > 0.01) {
|
||||
@ -1765,6 +1851,7 @@ fn drawBox(self: *UI, box: *Box) void {
|
||||
text_position.y += (available_height - text_size.y) * alignment_y_coeff;
|
||||
|
||||
font_face.drawText(text, text_position, box.text_color);
|
||||
// rl.drawRectangleLinesEx(rect_utils.initVec2(text_position, text_size), 1, rl.Color.magenta);
|
||||
} else {
|
||||
// TODO: Don't call `measureTextLines`,
|
||||
// Because in the end `measureText` will be called twice for each line
|
||||
@ -1848,9 +1935,14 @@ fn drawBox(self: *UI, box: *Box) void {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1862,6 +1954,7 @@ fn beginScissor(self: *UI, rect: Rect) void {
|
||||
}
|
||||
|
||||
self.scissor_stack.appendAssumeCapacity(intersected_rect);
|
||||
// rl.drawRectangleLinesEx(intersected_rect, 1, rl.Color.magenta);
|
||||
|
||||
rl.beginScissorMode(
|
||||
@intFromFloat(intersected_rect.x),
|
||||
@ -1873,11 +1966,13 @@ fn beginScissor(self: *UI, rect: Rect) void {
|
||||
|
||||
fn endScissor(self: *UI) void {
|
||||
rl.endScissorMode();
|
||||
|
||||
_ = self.scissor_stack.pop();
|
||||
|
||||
if (self.scissor_stack.len > 0) {
|
||||
const top_scissor_rect = self.scissor_stack.buffer[self.scissor_stack.len - 1];
|
||||
|
||||
// rl.drawRectangleLinesEx(top_scissor_rect, 1, rl.Color.magenta);
|
||||
rl.endScissorMode();
|
||||
rl.beginScissorMode(
|
||||
@intFromFloat(top_scissor_rect.x),
|
||||
@ -1885,6 +1980,7 @@ fn endScissor(self: *UI) void {
|
||||
@intFromFloat(top_scissor_rect.width),
|
||||
@intFromFloat(top_scissor_rect.height)
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -2104,6 +2200,10 @@ pub const TextInputStorage = struct {
|
||||
self.buffer.deinit();
|
||||
}
|
||||
|
||||
pub fn clear(self: *TextInputStorage) void {
|
||||
self.deleteMany(0, self.buffer.items.len);
|
||||
}
|
||||
|
||||
pub fn setText(self: *TextInputStorage, text: []const u8) !void {
|
||||
self.modified = true;
|
||||
|
||||
@ -2159,27 +2259,98 @@ pub const TextInputStorage = struct {
|
||||
}
|
||||
}
|
||||
|
||||
fn getCharOffsetX(self: *const TextInputStorage, font: FontFace, index: usize) f32 {
|
||||
const text = self.buffer.items;
|
||||
return font.measureWidth(text[0..index]);
|
||||
fn countScalar(haystack: []const u8, needle: u8) usize {
|
||||
var count: usize = 0;
|
||||
var start_index: usize = 0;
|
||||
while (std.mem.indexOfScalarPos(u8, haystack, start_index, needle)) |found| {
|
||||
start_index = found + 1;
|
||||
count += 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
fn getCharIndex(self: *const TextInputStorage, font: FontFace, x: f32) usize {
|
||||
fn getLineAt(self: *const TextInputStorage, index: usize) [2]usize {
|
||||
const text = self.buffer.items;
|
||||
|
||||
var line_start: usize = 0;
|
||||
if (std.mem.lastIndexOfScalar(u8, text[0..index], '\n')) |newline| {
|
||||
line_start = newline + 1;
|
||||
}
|
||||
|
||||
const line_end = std.mem.indexOfScalarPos(u8, text, line_start, '\n') orelse text.len;
|
||||
|
||||
return .{ line_start, line_end };
|
||||
}
|
||||
|
||||
pub fn getPositionAt(self: *const TextInputStorage, font: FontFace, index: usize) Vec2 {
|
||||
const line_start, const line_end = self.getLineAt(index);
|
||||
|
||||
const text = self.buffer.items;
|
||||
const row: f32 = @floatFromInt(self.getLineIndexAt(line_end));
|
||||
|
||||
return Vec2.init(
|
||||
font.measureWidth(text[line_start..index]),
|
||||
row * font.getLineSize()
|
||||
);
|
||||
}
|
||||
|
||||
fn getLineByIndex(self: *const TextInputStorage, line_index: usize) ?[2]usize {
|
||||
const text = self.buffer.items;
|
||||
|
||||
var row: usize = 0;
|
||||
var line_start: usize = 0;
|
||||
while (true) {
|
||||
const newline = std.mem.indexOfScalarPos(u8, text, line_start, '\n');
|
||||
const line_end = newline orelse text.len;
|
||||
|
||||
if (row == line_index) {
|
||||
return .{ line_start, line_end };
|
||||
}
|
||||
|
||||
row += 1;
|
||||
line_start = line_end+1;
|
||||
if (newline == null) break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn getLineIndexAt(self: *const TextInputStorage, index: usize) usize {
|
||||
const text = self.buffer.items;
|
||||
return countScalar(text[0..index], '\n');
|
||||
}
|
||||
|
||||
fn getIndexAt(self: *const TextInputStorage, font: FontFace, pos: Vec2) ?usize {
|
||||
if (pos.y <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const text = self.buffer.items;
|
||||
|
||||
if (self.getLineByIndex(@intFromFloat(@divFloor(pos.y, font.getLineSize())))) |line_range| {
|
||||
const line_start, const line_end = line_range;
|
||||
const line = text[line_start..line_end];
|
||||
|
||||
var measure_opts = FontFace.MeasureOptions{
|
||||
.up_to_width = x + self.shown_slice_start
|
||||
.up_to_width = pos.x + self.shown_slice_start
|
||||
};
|
||||
const before_size = font.measureTextEx(self.buffer.items, &measure_opts).x;
|
||||
const before_size = font.measureTextEx(line, &measure_opts).x;
|
||||
|
||||
const index = measure_opts.last_codepoint_index;
|
||||
|
||||
if (index+1 < self.buffer.items.len) {
|
||||
const after_size = self.getCharOffsetX(font, index + 1);
|
||||
if (@abs(before_size - x) > @abs(after_size - x)) {
|
||||
return index + 1;
|
||||
if (index+1 < line.len) {
|
||||
const after_size = font.measureWidth(line[0..(index+1)]);
|
||||
if (@abs(before_size - pos.x) > @abs(after_size - pos.x)) {
|
||||
return line_start + index + 1;
|
||||
} else {
|
||||
return line_start + index;
|
||||
}
|
||||
}
|
||||
|
||||
return index;
|
||||
return line_end;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn nextJumpPoint(self: *const TextInputStorage, index: usize, step: isize) usize {
|
||||
@ -2188,6 +2359,8 @@ pub const TextInputStorage = struct {
|
||||
const text = self.buffer.items;
|
||||
|
||||
if (step > 0) {
|
||||
if (index >= text.len) return index;
|
||||
|
||||
var prev_whitespace = std.ascii.isWhitespace(text[i]);
|
||||
while (true) {
|
||||
const cur_whitespace = std.ascii.isWhitespace(text[i]);
|
||||
@ -2234,7 +2407,7 @@ pub const TextInputStorage = struct {
|
||||
return i;
|
||||
}
|
||||
|
||||
pub fn textLength(self: *TextInputStorage) []const u8 {
|
||||
pub fn textSlice(self: *TextInputStorage) []const u8 {
|
||||
return self.buffer.items;
|
||||
}
|
||||
|
||||
@ -2263,16 +2436,43 @@ pub const TextInputStorage = struct {
|
||||
self.cursor_stop += text.len;
|
||||
}
|
||||
}
|
||||
|
||||
fn getLowerCursor(self: *TextInputStorage) usize {
|
||||
return @min(self.cursor_stop, self.cursor_start);
|
||||
}
|
||||
|
||||
fn getUpperCursor(self: *TextInputStorage) usize {
|
||||
return @max(self.cursor_stop, self.cursor_start);
|
||||
}
|
||||
|
||||
fn moveCursorAlongX(self: *TextInputStorage, cursor: usize, step: isize) usize {
|
||||
return @intCast(std.math.clamp(
|
||||
@as(isize, @intCast(cursor)) + step,
|
||||
0,
|
||||
@as(isize, @intCast(self.buffer.items.len))
|
||||
));
|
||||
}
|
||||
|
||||
fn isChatAt(self: *TextInputStorage, cursor: usize, char: u8) bool {
|
||||
const text = self.textSlice();
|
||||
if (cursor >= text.len) return false;
|
||||
|
||||
return text[cursor] == char;
|
||||
}
|
||||
};
|
||||
|
||||
pub const TextInputOptions = struct {
|
||||
key: Key,
|
||||
storage: *TextInputStorage,
|
||||
editable: bool = true,
|
||||
size_x: ?Sizing = null,
|
||||
size_y: ?Sizing = null,
|
||||
|
||||
initial: ?[]const u8 = null,
|
||||
placeholder: ?[]const u8 = null,
|
||||
text_color: rl.Color = srcery.black
|
||||
postfix: ?[]const u8 = null,
|
||||
text_color: rl.Color = srcery.black,
|
||||
single_line: bool = true
|
||||
};
|
||||
|
||||
pub const NumberInputOptions = struct {
|
||||
@ -2280,9 +2480,13 @@ pub const NumberInputOptions = struct {
|
||||
storage: *TextInputStorage,
|
||||
invalid: bool = false,
|
||||
editable: bool = true,
|
||||
width: ?f32 = null,
|
||||
|
||||
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
|
||||
};
|
||||
@ -2297,6 +2501,7 @@ pub const FileInputOptions = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
file_picker: *?Platform.FilePickerId,
|
||||
open_dialog: bool = true,
|
||||
folder: bool = false,
|
||||
path: ?[]const u8 = null
|
||||
};
|
||||
|
||||
@ -2429,8 +2634,8 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void {
|
||||
|
||||
const container = self.createBox(.{
|
||||
.key = opts.key,
|
||||
.size_x = Sizing.initGrowUpTo(.{ .pixels = 200 }),
|
||||
.size_y = Sizing.initFixed(Unit.initPixels(self.rem(1))),
|
||||
.size_x = opts.size_x orelse Sizing.initGrowUpTo(.{ .pixels = 200 }),
|
||||
.size_y = opts.size_y orelse Sizing.initFixed(Unit.initPixels(self.rem(1))),
|
||||
.flags = &.{ .clickable, .clip_view, .draggable },
|
||||
.background = srcery.bright_white,
|
||||
.align_y = .center,
|
||||
@ -2448,16 +2653,23 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void {
|
||||
try storage_text.appendSlice(opts.initial.?);
|
||||
}
|
||||
|
||||
const cursor_start_x = storage.getCharOffsetX(font, storage.cursor_start);
|
||||
const cursor_stop_x = storage.getCharOffsetX(font, storage.cursor_stop);
|
||||
const container_signal = self.signal(container);
|
||||
|
||||
{ // Visuals
|
||||
{ // Text visuals
|
||||
var text_color = opts.text_color;
|
||||
var label_options = BoxOptions{
|
||||
.text_color = opts.text_color,
|
||||
.text = storage_text.items,
|
||||
.float_relative_to = container,
|
||||
.align_x = .start
|
||||
};
|
||||
|
||||
var text: []const u8 = storage_text.items;
|
||||
if (opts.placeholder != null and text.len == 0) {
|
||||
text = opts.placeholder.?;
|
||||
text_color = text_color.alpha(0.6);
|
||||
label_options.text_color = opts.text_color.alpha(0.6);
|
||||
}
|
||||
label_options.text = text;
|
||||
|
||||
const text_size = font.measureText(text);
|
||||
const visible_text_width = container.persistent.size.x - container.padding.byAxis(.X);
|
||||
@ -2476,36 +2688,33 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void {
|
||||
storage.shown_slice_end = shown_window_size;
|
||||
}
|
||||
|
||||
if (cursor_stop_x > storage.shown_slice_end) {
|
||||
storage.shown_slice_start = cursor_stop_x - shown_window_size;
|
||||
storage.shown_slice_end = cursor_stop_x;
|
||||
const stop_cursor_pos = storage.getPositionAt(font, storage.cursor_stop);
|
||||
if (stop_cursor_pos.x > storage.shown_slice_end) {
|
||||
storage.shown_slice_start = stop_cursor_pos.x - shown_window_size;
|
||||
storage.shown_slice_end = stop_cursor_pos.x;
|
||||
}
|
||||
if (cursor_stop_x < storage.shown_slice_start) {
|
||||
storage.shown_slice_start = cursor_stop_x;
|
||||
storage.shown_slice_end = cursor_stop_x + shown_window_size;
|
||||
if (stop_cursor_pos.x < storage.shown_slice_start) {
|
||||
storage.shown_slice_start = stop_cursor_pos.x;
|
||||
storage.shown_slice_end = stop_cursor_pos.x + shown_window_size;
|
||||
}
|
||||
|
||||
_ = self.createBox(.{
|
||||
.text_color = text_color,
|
||||
.text = text,
|
||||
.float_relative_to = container,
|
||||
.float_rect = Rect{
|
||||
.x = container.padding.left - storage.shown_slice_start,
|
||||
label_options.float_rect = container.padding.apply(Rect{
|
||||
.x = -storage.shown_slice_start,
|
||||
.y = 0,
|
||||
.width = visible_text_width,
|
||||
.height = container.persistent.size.y
|
||||
},
|
||||
.align_y = .center,
|
||||
.align_x = .start
|
||||
});
|
||||
|
||||
if (opts.single_line) {
|
||||
label_options.align_y = .center;
|
||||
} else {
|
||||
label_options.align_y = .start;
|
||||
}
|
||||
|
||||
const container_signal = self.signal(container);
|
||||
if (opts.editable and container_signal.hot) {
|
||||
container.borders = UI.Borders.all(.{
|
||||
.color = srcery.red,
|
||||
.size = 2
|
||||
});
|
||||
const shown_text = self.createBox(label_options);
|
||||
if (opts.postfix) |postfix| {
|
||||
shown_text.appendText(postfix);
|
||||
}
|
||||
}
|
||||
|
||||
// Text editing visuals
|
||||
@ -2516,31 +2725,51 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void {
|
||||
const cursor_color = srcery.hard_black;
|
||||
|
||||
if (storage.cursor_start == storage.cursor_stop and blink) {
|
||||
const cursor_width = 2;
|
||||
|
||||
const cursor_pos = storage.getPositionAt(font, storage.cursor_start);
|
||||
_ = self.createBox(.{
|
||||
.background = cursor_color,
|
||||
.float_relative_to = container,
|
||||
.float_rect = Rect{
|
||||
.x = container.padding.left + cursor_start_x - storage.shown_slice_start,
|
||||
.y = container.padding.top,
|
||||
.width = 2,
|
||||
.height = container.persistent.size.y - container.padding.byAxis(.Y)
|
||||
},
|
||||
.float_rect = container.padding.applyPosition(Rect{
|
||||
.x = cursor_pos.x - storage.shown_slice_start,
|
||||
.y = cursor_pos.y,
|
||||
.width = cursor_width,
|
||||
.height = font.getSize()
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
if (storage.cursor_start != storage.cursor_stop) {
|
||||
const lower_cursor_x = @min(cursor_start_x, cursor_stop_x);
|
||||
const upper_cursor_x = @max(cursor_start_x, cursor_stop_x);
|
||||
var cursor = storage.getLowerCursor();
|
||||
const upper_cursor = storage.getUpperCursor();
|
||||
|
||||
while (cursor < upper_cursor) {
|
||||
_, const line_end = storage.getLineAt(cursor);
|
||||
|
||||
const highlight_from = storage.getPositionAt(font, cursor);
|
||||
const highlight_to_x = storage.getPositionAt(font, @min(line_end, upper_cursor)).x;
|
||||
|
||||
_ = self.createBox(.{
|
||||
.background = cursor_color.alpha(0.25),
|
||||
.float_relative_to = container,
|
||||
.float_rect = Rect{
|
||||
.x = container.padding.left + lower_cursor_x - storage.shown_slice_start,
|
||||
.y = container.padding.top,
|
||||
.width = upper_cursor_x - lower_cursor_x,
|
||||
.height = container.persistent.size.y - container.padding.byAxis(.Y)
|
||||
},
|
||||
.float_rect = container.padding.applyPosition(Rect{
|
||||
.x = highlight_from.x - storage.shown_slice_start,
|
||||
.y = highlight_from.y,
|
||||
.width = @max(highlight_to_x - highlight_from.x, 2),
|
||||
.height = font.getLineSize()
|
||||
}),
|
||||
});
|
||||
|
||||
cursor = line_end+1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.editable and container_signal.hot) {
|
||||
container.borders = UI.Borders.all(.{
|
||||
.color = srcery.red,
|
||||
.size = 2
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -2561,33 +2790,29 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void {
|
||||
var no_blinking = false;
|
||||
|
||||
{ // Cursor movement controls
|
||||
var move_cursor_dir: i32 = 0;
|
||||
var move_cursor_dir_x: i32 = 0;
|
||||
var move_cursor_dir_y: i32 = 0;
|
||||
if (self.isKeyboardPressedOrHeld(.key_left)) {
|
||||
move_cursor_dir -= 1;
|
||||
move_cursor_dir_x -= 1;
|
||||
}
|
||||
if (self.isKeyboardPressedOrHeld(.key_right)) {
|
||||
move_cursor_dir += 1;
|
||||
move_cursor_dir_x += 1;
|
||||
}
|
||||
if (self.isKeyboardPressedOrHeld(.key_up)) {
|
||||
move_cursor_dir_y -= 1;
|
||||
}
|
||||
if (self.isKeyboardPressedOrHeld(.key_down)) {
|
||||
move_cursor_dir_y += 1;
|
||||
}
|
||||
|
||||
if (move_cursor_dir != 0) {
|
||||
if (shift) {
|
||||
if (ctrl) {
|
||||
storage.cursor_stop = storage.nextJumpPoint(storage.cursor_stop, move_cursor_dir);
|
||||
} else {
|
||||
const cursor_stop: isize = @intCast(storage.cursor_stop);
|
||||
storage.cursor_stop = @intCast(std.math.clamp(
|
||||
cursor_stop + move_cursor_dir,
|
||||
0,
|
||||
@as(isize, @intCast(storage_text.items.len))
|
||||
));
|
||||
}
|
||||
if (move_cursor_dir_x != 0 or move_cursor_dir_y != 0) {
|
||||
|
||||
} else {
|
||||
if (storage.cursor_start != storage.cursor_stop) {
|
||||
if (move_cursor_dir_x != 0) {
|
||||
if (storage.cursor_start != storage.cursor_stop and !shift) {
|
||||
const lower = @min(storage.cursor_start, storage.cursor_stop);
|
||||
const upper = @max(storage.cursor_start, storage.cursor_stop);
|
||||
|
||||
if (move_cursor_dir < 0) {
|
||||
if (move_cursor_dir_x < 0) {
|
||||
if (ctrl) {
|
||||
storage.cursor_start = storage.nextJumpPoint(lower, -1);
|
||||
} else {
|
||||
@ -2603,21 +2828,36 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void {
|
||||
storage.cursor_stop = storage.cursor_start;
|
||||
|
||||
} else {
|
||||
var cursor = storage.cursor_start;
|
||||
|
||||
if (ctrl) {
|
||||
cursor = storage.nextJumpPoint(cursor, move_cursor_dir);
|
||||
storage.cursor_stop = storage.nextJumpPoint(storage.cursor_stop, move_cursor_dir_x);
|
||||
} else {
|
||||
cursor = @intCast(std.math.clamp(
|
||||
@as(isize, @intCast(cursor)) + move_cursor_dir,
|
||||
0,
|
||||
@as(isize, @intCast(storage_text.items.len))
|
||||
));
|
||||
storage.cursor_stop = storage.moveCursorAlongX(storage.cursor_stop, move_cursor_dir_x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
storage.cursor_start = cursor;
|
||||
storage.cursor_stop = cursor;
|
||||
if (move_cursor_dir_y != 0) {
|
||||
const line_bounds = storage.getLineAt(storage.cursor_stop);
|
||||
const line = storage.getLineIndexAt(storage.cursor_stop);
|
||||
|
||||
var next_line = line;
|
||||
if (move_cursor_dir_y > 0) {
|
||||
next_line += 1;
|
||||
}
|
||||
if (move_cursor_dir_y < 0 and next_line > 0) {
|
||||
next_line -= 1;
|
||||
}
|
||||
|
||||
if (storage.getLineByIndex(next_line)) |next_line_bounds| {
|
||||
const next_line_start, const next_line_stop = next_line_bounds;
|
||||
const line_start, _ = line_bounds;
|
||||
|
||||
storage.cursor_stop = @min(next_line_start + (storage.cursor_stop - line_start), next_line_stop);
|
||||
}
|
||||
}
|
||||
|
||||
if (!shift) {
|
||||
storage.cursor_start = storage.cursor_stop;
|
||||
}
|
||||
|
||||
no_blinking = true;
|
||||
@ -2629,8 +2869,7 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void {
|
||||
mouse.x -= container.padding.left;
|
||||
mouse.y -= container.padding.top;
|
||||
|
||||
const mouse_index = storage.getCharIndex(font, mouse.x);
|
||||
|
||||
if (storage.getIndexAt(font, mouse)) |mouse_index| {
|
||||
if (container_signal.flags.contains(.left_pressed)) {
|
||||
storage.cursor_start = mouse_index;
|
||||
storage.cursor_stop = mouse_index;
|
||||
@ -2641,6 +2880,7 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void {
|
||||
no_blinking = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deletion
|
||||
if (self.isKeyboardPressedOrHeld(.key_backspace) and storage_text.items.len > 0) {
|
||||
@ -2710,7 +2950,22 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void {
|
||||
storage.last_pressed_at_ns = now;
|
||||
}
|
||||
|
||||
if (self.isKeyboardPressed(.key_escape) or self.isKeyboardPressed(.key_enter)) {
|
||||
if (self.isKeyboardPressed(.key_escape)) {
|
||||
storage.editing = false;
|
||||
}
|
||||
|
||||
if (self.isKeyboardPressed(.key_enter)) {
|
||||
if (opts.single_line) {
|
||||
storage.editing = false;
|
||||
} else {
|
||||
if (storage.cursor_start != storage.cursor_stop) {
|
||||
storage.deleteMany(storage.cursor_start, storage.cursor_stop);
|
||||
}
|
||||
try storage.insertSingle(storage.cursor_start, '\n');
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.single_line and self.isKeyboardPressed(.key_enter)) {
|
||||
storage.editing = false;
|
||||
}
|
||||
|
||||
@ -2730,12 +2985,18 @@ 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
|
||||
};
|
||||
|
||||
if (opts.width) |width| {
|
||||
text_opts.size_x = Sizing.initGrowUpTo(.{ .pixels = 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 +3008,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;
|
||||
}
|
||||
@ -2847,8 +3117,14 @@ pub fn fileInput(self: *UI, opts: FileInputOptions) ?[]u8 {
|
||||
file_open_options.style = .save;
|
||||
file_open_options.prompt_overwrite = true;
|
||||
}
|
||||
|
||||
if (opts.folder) {
|
||||
file_open_options.kind = .folder;
|
||||
} else {
|
||||
file_open_options.kind = .file;
|
||||
file_open_options.appendFilter("All", "*") catch unreachable;
|
||||
file_open_options.appendFilter("Binary", "*.bin") catch unreachable;
|
||||
}
|
||||
|
||||
if (Platform.spawnFilePicker(&file_open_options)) |file_picker_id| {
|
||||
opts.file_picker.* = file_picker_id;
|
||||
|
Loading…
Reference in New Issue
Block a user