Compare commits

...

8 Commits

17 changed files with 2766 additions and 1221 deletions

View File

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

File diff suppressed because it is too large Load Diff

Binary file not shown.

BIN
src/assets/cross.ase Normal file

Binary file not shown.

BIN
src/assets/file.ase Normal file

Binary file not shown.

View File

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

View File

@ -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,63 +286,51 @@ 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);
const ruler_ctx = UIViewRuler.Context{
.ui = ctx.ui,
.project = &ctx.app.project,
.view_controls = ctx.view_controls
};
} else {
const ruler_ctx = UIViewRuler.Context{
.ui = ctx.ui,
.project = &ctx.app.project,
.view_controls = ctx.view_controls
};
var graph_box: *UI.Box = undefined;
var x_ruler: *UI.Box = undefined;
var y_ruler: *UI.Box = undefined;
var graph_box: *UI.Box = undefined;
var x_ruler: *UI.Box = undefined;
var y_ruler: *UI.Box = undefined;
{
const container = ui.createBox(.{
.layout_direction = .left_to_right,
.size_x = UI.Sizing.initGrowFull(),
.size_y = UI.Sizing.initGrowFull(),
});
container.beginChildren();
defer container.endChildren();
{
const container = ui.createBox(.{
.layout_direction = .left_to_right,
.size_x = UI.Sizing.initGrowFull(),
.size_y = UI.Sizing.initGrowFull(),
});
container.beginChildren();
defer container.endChildren();
y_ruler = UIViewRuler.createBox(ruler_ctx, ui.keyFromString("Y ruler"), .Y);
y_ruler = UIViewRuler.createBox(ruler_ctx, ui.keyFromString("Y ruler"), .Y);
graph_box = showGraph(ctx, view_id);
}
{
const container = ui.createBox(.{
.layout_direction = .left_to_right,
.size_x = UI.Sizing.initGrowFull(),
.size_y = ruler_size,
});
container.beginChildren();
defer container.endChildren();
const fullscreen = ui.createBox(.{
.key = ui.keyFromString("Fullscreen toggle"),
.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);
graph_box = createGraphBox(ctx);
}
{
const container = ui.createBox(.{
.layout_direction = .left_to_right,
.size_x = UI.Sizing.initGrowFull(),
.size_y = ruler_size,
});
container.beginChildren();
defer container.endChildren();
_ = ui.createBox(.{
.size_x = ruler_size,
.size_y = ruler_size,
.background = srcery.hard_black,
});
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;
}

View File

@ -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)) {

View File

@ -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 {

View File

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

View File

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

View File

@ -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 {

View File

@ -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,60 +71,64 @@ 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
};
const hWnd: HWND = @alignCast(@ptrCast(rl.getWindowHandle()));
assert(hWnd != null);
if (opts.kind == .file) {
const hWnd: HWND = @alignCast(@ptrCast(rl.getWindowHandle()));
assert(hWnd != null);
var lpstrFilter_utf8: std.ArrayListUnmanaged(u8) = .{};
defer lpstrFilter_utf8.deinit(allocator);
var lpstrFilter_utf8: std.ArrayListUnmanaged(u8) = .{};
defer lpstrFilter_utf8.deinit(allocator);
for (opts.filters.constSlice()) |filter| {
try lpstrFilter_utf8.appendSlice(allocator, filter.name.slice());
try lpstrFilter_utf8.append(allocator, 0);
try lpstrFilter_utf8.appendSlice(allocator, filter.format.slice());
try lpstrFilter_utf8.append(allocator, 0);
for (opts.filters.constSlice()) |filter| {
try lpstrFilter_utf8.appendSlice(allocator, filter.name.slice());
try lpstrFilter_utf8.append(allocator, 0);
try lpstrFilter_utf8.appendSlice(allocator, filter.format.slice());
try lpstrFilter_utf8.append(allocator, 0);
}
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;
{
flags |= windows_h.OFN_NOCHANGEDIR;
flags |= windows_h.OFN_PATHMUSTEXIST;
flags |= windows_h.OFN_EXPLORER;
flags |= windows_h.OFN_LONGNAMES;
flags |= windows_h.OFN_ENABLESIZING;
if (opts.file_must_exist) {
flags |= windows_h.OFN_FILEMUSTEXIST;
}
if (opts.prompt_creation) {
flags |= windows_h.OFN_CREATEPROMPT;
}
if (opts.prompt_overwrite) {
flags |= windows_h.OFN_OVERWRITEPROMPT;
}
}
var nFilterIndex: c_ulong = 0;
if (opts.default_filter) |default_filter| {
assert(default_filter < opts.filters.len);
nFilterIndex = @intCast(default_filter + 1);
}
@memset(&self.filename_w_buffer, 0);
self.ofn.lStructSize = @sizeOf(@TypeOf(self.ofn));
self.ofn.hwndOwner = hWnd;
self.ofn.lpstrFile = &self.filename_w_buffer;
self.ofn.nMaxFile = self.filename_w_buffer.len;
self.ofn.lpstrFilter = lpstrFilter_utf16;
self.ofn.nFilterIndex = nFilterIndex;
self.ofn.Flags = flags;
}
self.lpstrFilter_utf16 = try std.unicode.utf8ToUtf16LeWithNull(allocator, lpstrFilter_utf8.items);
errdefer allocator.free(self.lpstrFilter_utf16);
var flags: c_ulong = 0;
{
flags |= windows_h.OFN_NOCHANGEDIR;
flags |= windows_h.OFN_PATHMUSTEXIST;
flags |= windows_h.OFN_EXPLORER;
flags |= windows_h.OFN_LONGNAMES;
flags |= windows_h.OFN_ENABLESIZING;
if (opts.file_must_exist) {
flags |= windows_h.OFN_FILEMUSTEXIST;
}
if (opts.prompt_creation) {
flags |= windows_h.OFN_CREATEPROMPT;
}
if (opts.prompt_overwrite) {
flags |= windows_h.OFN_OVERWRITEPROMPT;
}
}
var nFilterIndex: c_ulong = 0;
if (opts.default_filter) |default_filter| {
assert(default_filter < opts.filters.len);
nFilterIndex = @intCast(default_filter + 1);
}
@memset(&self.filename_w_buffer, 0);
self.ofn.lStructSize = @sizeOf(@TypeOf(self.ofn));
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.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,35 +137,95 @@ 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 {
const result = switch (self.style) {
.open => GetOpenFileNameW(&self.ofn),
.save => GetSaveFileNameW(&self.ofn)
};
if (self.kind == .file) {
const result = switch (self.style) {
.open => GetOpenFileNameW(&self.ofn),
.save => GetSaveFileNameW(&self.ofn)
};
if (result != windows_h.TRUE) {
const err = windows_h.CommDlgExtendedError();
if (err == 0) {
if (result != windows_h.TRUE) {
const err = windows_h.CommDlgExtendedError();
if (err == 0) {
return error.Canceled;
}
// From the MSDN docs, these 3 error should only be able to occur
// Source: https://learn.microsoft.com/en-us/windows/win32/api/commdlg/nf-commdlg-commdlgextendederror
if (err == windows_h.FNERR_BUFFERTOOSMALL) {
return error.BufferTooSmall;
} else if (err == windows_h.FNERR_INVALIDFILENAME) {
return error.InvalidFilename;
} else if (err == windows_h.FNERR_SUBCLASSFAILURE) {
return error.OutOfMemory;
} else {
log.err("Unknown file picker error occured: {}", .{ err });
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;
}
// From the MSDN docs, these 3 error should only be able to occur
// Source: https://learn.microsoft.com/en-us/windows/win32/api/commdlg/nf-commdlg-commdlgextendederror
if (err == windows_h.FNERR_BUFFERTOOSMALL) {
return error.BufferTooSmall;
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.?);
} else if (err == windows_h.FNERR_INVALIDFILENAME) {
return error.InvalidFilename;
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);
} else if (err == windows_h.FNERR_SUBCLASSFAILURE) {
return error.OutOfMemory;
} else {
log.err("Unknown file picker error occured: {}", .{ err });
return error.UnknownFileDialogError;
const path_slice = std.mem.sliceTo(path, 0);
@memcpy(self.filename_w_buffer[0..path_slice.len], path_slice);
}
}
}
}
@ -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();
}

View File

@ -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,

View File

@ -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 {

View File

@ -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,14 +128,15 @@ 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;
const view_rect = Graph.ViewOptions{
.x_range = RangeF64.init(0, @floatFromInt(samples.len)),
.y_range = self.preview_samples_y_range
};
Graph.drawCached(&self.protocol_graph_cache, protocol_view.persistent.size, view_rect, samples);
if (self.protocol_graph_cache.texture) |texture| {
protocol_view.texture = texture.texture;
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(sample_list.getLength())),
.y_range = self.preview_samples_y_range
};
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;
}
}
}
@ -180,9 +211,17 @@ 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);
self.preview_samples_y_range = RangeF64.init(-amplitude*1.1, amplitude*1.1);
self.protocol_graph_cache.invalidate();
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| {
@ -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 = "-";
if (sample_rate != null) {
const duration = @as(f64, @floatFromInt(sample_count.?)) / sample_rate.?;
if (utils.formatDuration(ui.frameAllocator(), duration)) |str| {
duration_str = str;
} else |_| {}
_ = ui.label("Samples: {d}", .{ sample_count });
var duration_str: ?[]const u8 = null;
if (sample_rate != null) {
const duration = @as(f64, @floatFromInt(sample_count)) / sample_rate.?;
if (utils.formatDuration(ui.frameAllocator(), duration)) |str| {
duration_str = str;
} else |_| {}
}
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 });
}
_ = ui.label("Duration: {s}", .{ duration_str });
}
}
@ -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();
}
}

File diff suppressed because it is too large Load Diff