Compare commits
4 Commits
a2fc2befd1
...
e5bc57d7b5
Author | SHA1 | Date | |
---|---|---|---|
e5bc57d7b5 | |||
12620714ed | |||
faa0a534d5 | |||
da7a580e55 |
430
src/app.zig
430
src/app.zig
@ -8,7 +8,7 @@ const Graph = @import("./graph.zig");
|
||||
const UI = @import("./ui.zig");
|
||||
const MainScreen = @import("./screens/main_screen.zig");
|
||||
const ChannelFromDeviceScreen = @import("./screens/channel_from_device.zig");
|
||||
const Platform = @import("./platform.zig");
|
||||
const Platform = @import("./platform/root.zig");
|
||||
const constants = @import("./constants.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
@ -159,6 +159,10 @@ fn GenerationalArray(Item: type) type {
|
||||
pub fn isEmpty(self: *Self) bool {
|
||||
return self.used.eql(UsedBitSet.initFull());
|
||||
}
|
||||
|
||||
pub fn clear(self: *Self) void {
|
||||
self.used = UsedBitSet.initFull();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -168,6 +172,7 @@ pub const Channel = struct {
|
||||
|
||||
// Persistent
|
||||
name: Name = .{},
|
||||
saved_collected_samples: ?[]u8 = null,
|
||||
|
||||
// Runtime
|
||||
device: Device = .{},
|
||||
@ -178,10 +183,23 @@ pub const Channel = struct {
|
||||
write_pattern: std.ArrayListUnmanaged(f64) = .{},
|
||||
output_task: ?NIDaq.Task = null,
|
||||
graph_min_max_cache: Graph.MinMaxCache = .{},
|
||||
last_sample_save_at: ?i128 = null,
|
||||
processed_samples_up_to: usize = 0,
|
||||
|
||||
pub fn deinit(self: *Channel, allocator: Allocator) void {
|
||||
self.clear(allocator);
|
||||
|
||||
if (self.saved_collected_samples) |str| {
|
||||
allocator.free(str);
|
||||
self.saved_collected_samples = null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(self: *Channel, allocator: Allocator) void {
|
||||
self.allowed_sample_rates = null;
|
||||
self.allowed_sample_values = null;
|
||||
self.last_sample_save_at = null;
|
||||
|
||||
self.collected_samples.clearAndFree(allocator);
|
||||
self.write_pattern.clearAndFree(allocator);
|
||||
if (self.output_task) |task| {
|
||||
@ -192,12 +210,6 @@ pub const Channel = struct {
|
||||
self.graph_min_max_cache.deinit(allocator);
|
||||
}
|
||||
|
||||
pub fn clear(self: *Channel, allocator: Allocator) void {
|
||||
_ = allocator;
|
||||
self.allowed_sample_rates = null;
|
||||
self.allowed_sample_values = null;
|
||||
}
|
||||
|
||||
pub fn generateSine(
|
||||
samples: *std.ArrayListUnmanaged(f64),
|
||||
allocator: Allocator,
|
||||
@ -217,6 +229,28 @@ pub const Channel = struct {
|
||||
samples.appendAssumeCapacity(sample);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn isSavedSampleFileStale(self: *const Channel, dir: std.fs.Dir) !bool {
|
||||
const path = self.saved_collected_samples orelse return false;
|
||||
const last_sample_save_at = self.last_sample_save_at orelse return true;
|
||||
|
||||
const stat = dir.statFile(path) catch |e| {
|
||||
if (e == error.FileNotFound) {
|
||||
return true;
|
||||
}
|
||||
return e;
|
||||
};
|
||||
|
||||
return stat.mtime != last_sample_save_at;
|
||||
}
|
||||
|
||||
pub fn invalidateSavedSamples(self: *Channel) void {
|
||||
self.last_sample_save_at = null;
|
||||
}
|
||||
|
||||
fn hasNewCollectedSamples(self: *Channel) bool {
|
||||
return self.processed_samples_up_to != self.collected_samples.items.len;
|
||||
}
|
||||
};
|
||||
|
||||
pub const File = struct {
|
||||
@ -246,6 +280,86 @@ pub const File = struct {
|
||||
};
|
||||
|
||||
pub const View = struct {
|
||||
pub const MarkedRange = struct {
|
||||
// Persistent
|
||||
axis: UI.Axis,
|
||||
range: RangeF64,
|
||||
|
||||
// Runtime
|
||||
min: ?f64 = null,
|
||||
max: ?f64 = null,
|
||||
average: ?f64 = null,
|
||||
standard_deviation: ?f64 = null,
|
||||
|
||||
pub fn clear(self: *MarkedRange) void {
|
||||
self.min = null;
|
||||
self.max = null;
|
||||
self.average = null;
|
||||
self.standard_deviation = null;
|
||||
}
|
||||
|
||||
pub fn refresh(self: *MarkedRange, samples: []const f64) void {
|
||||
self.clear();
|
||||
|
||||
if (self.axis == .X) {
|
||||
const from = std.math.clamp(@as(usize, @intFromFloat(@floor(self.range.lower))), 0, samples.len);
|
||||
const to = std.math.clamp(@as(usize, @intFromFloat(@ceil(self.range.upper))), 0, samples.len);
|
||||
if (to - from == 0) return;
|
||||
|
||||
const samples_in_range = samples[from..to];
|
||||
if (samples_in_range.len > 0) {
|
||||
const sample_count: f64 = @floatFromInt(samples_in_range.len);
|
||||
|
||||
var sum: f64 = 0;
|
||||
var min = samples_in_range[0];
|
||||
var max = samples_in_range[0];
|
||||
for (samples_in_range) |sample| {
|
||||
min = @min(min, sample);
|
||||
max = @max(max, sample);
|
||||
sum += sample;
|
||||
}
|
||||
const average = sum / sample_count;
|
||||
|
||||
self.min = min;
|
||||
self.max = max;
|
||||
self.average = average;
|
||||
|
||||
if (sample_count > 1) {
|
||||
var standard_deviation: f64 = 0;
|
||||
for (samples_in_range) |sample| {
|
||||
standard_deviation += (sample - average) * (sample - average);
|
||||
}
|
||||
standard_deviation /= (sample_count - 1);
|
||||
standard_deviation = @sqrt(standard_deviation);
|
||||
|
||||
self.standard_deviation = standard_deviation;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const MarkedRangeIterator = struct {
|
||||
view: *View,
|
||||
axis: UI.Axis,
|
||||
index: usize = 0,
|
||||
next_index: usize = 0,
|
||||
|
||||
pub fn next(self: *MarkedRangeIterator) ?RangeF64 {
|
||||
const marked_ranges = self.view.marked_ranges.constSlice();
|
||||
while (self.next_index < marked_ranges.len) {
|
||||
self.index = self.next_index;
|
||||
const marked_range = marked_ranges[self.index];
|
||||
self.next_index += 1;
|
||||
|
||||
if (marked_range.axis == self.axis) {
|
||||
return marked_range.range;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Reference = union(enum) {
|
||||
file: Id,
|
||||
channel: Id
|
||||
@ -254,9 +368,11 @@ pub const View = struct {
|
||||
// Persistent
|
||||
reference: Reference,
|
||||
height: f32 = 300,
|
||||
// TODO: Implement different styles of following: Look ahead, sliding, sliding window
|
||||
follow: bool = false,
|
||||
graph_opts: Graph.ViewOptions = .{},
|
||||
sync_controls: bool = false,
|
||||
marked_ranges: std.BoundedArray(MarkedRange, 32) = .{},
|
||||
|
||||
// Runtime
|
||||
graph_cache: Graph.RenderCache = .{},
|
||||
@ -283,14 +399,36 @@ pub const View = struct {
|
||||
.Y => self.available_y_range
|
||||
};
|
||||
}
|
||||
|
||||
pub fn appendMarkedRange(self: *View, axis: UI.Axis, range: RangeF64) ?*MarkedRange {
|
||||
if (self.marked_ranges.unusedCapacitySlice().len == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const marked_range = self.marked_ranges.addOneAssumeCapacity();
|
||||
marked_range.* = MarkedRange{
|
||||
.axis = axis,
|
||||
.range = range
|
||||
};
|
||||
return marked_range;
|
||||
}
|
||||
|
||||
pub fn iterMarkedRanges(self: *View, axis: UI.Axis) MarkedRangeIterator {
|
||||
return MarkedRangeIterator{
|
||||
.view = self,
|
||||
.axis = axis
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Project = struct {
|
||||
const file_endian = std.builtin.Endian.big;
|
||||
const file_format_version: u8 = 0;
|
||||
const file_endian = std.builtin.Endian.big;
|
||||
|
||||
save_location: ?[]u8 = null,
|
||||
sample_rate: ?f64 = null,
|
||||
|
||||
// TODO: How this to computer local settings, like appdata. Because this option shouldn't be project specific.
|
||||
show_rulers: bool = true,
|
||||
|
||||
channels: GenerationalArray(Channel) = .{},
|
||||
@ -329,16 +467,39 @@ pub const Project = struct {
|
||||
return result_range;
|
||||
}
|
||||
|
||||
pub fn getViewSamples(self: *Project, view_id: Id) []const f64 {
|
||||
const empty = &[0]f64{};
|
||||
|
||||
var result: []const f64 = empty;
|
||||
|
||||
if (self.views.get(view_id)) |view| {
|
||||
switch (view.reference) {
|
||||
.channel => |channel_id| if (self.channels.get(channel_id)) |channel| {
|
||||
result = channel.collected_samples.items;
|
||||
},
|
||||
.file => |file_id| if (self.files.get(file_id)) |file| {
|
||||
result = file.samples orelse empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Project, allocator: Allocator) void {
|
||||
var file_iter = self.files.iterator();
|
||||
while (file_iter.next()) |file| {
|
||||
file.deinit(allocator);
|
||||
}
|
||||
self.files.clear();
|
||||
|
||||
var channel_iter = self.channels.iterator();
|
||||
while (channel_iter.next()) |channel| {
|
||||
channel.deinit(allocator);
|
||||
}
|
||||
self.channels.clear();
|
||||
|
||||
self.views.clear();
|
||||
|
||||
if (self.save_location) |str| {
|
||||
allocator.free(str);
|
||||
@ -348,8 +509,9 @@ pub const Project = struct {
|
||||
|
||||
// ------------------- Serialization ------------------ //
|
||||
|
||||
pub fn initFromFile(allocator: Allocator, save_location: []const u8) !Project {
|
||||
var self = Project{};
|
||||
pub fn initFromFile(self: *Project, allocator: Allocator, save_location: []const u8) !void {
|
||||
self.* = .{};
|
||||
errdefer self.deinit(allocator);
|
||||
|
||||
const f = try std.fs.cwd().openFile(save_location, .{});
|
||||
defer f.close();
|
||||
@ -379,9 +541,17 @@ pub const Project = struct {
|
||||
const channel_name = try readString(reader, allocator);
|
||||
defer allocator.free(channel_name);
|
||||
|
||||
var saved_collected_samples: ?[]u8 = try readString(reader, allocator);
|
||||
if (saved_collected_samples.?.len == 0) {
|
||||
allocator.free(saved_collected_samples.?);
|
||||
saved_collected_samples = null;
|
||||
}
|
||||
errdefer if (saved_collected_samples) |str| allocator.free(str);
|
||||
|
||||
const channel = self.channels.get(id).?;
|
||||
channel.* = Channel{
|
||||
.name = try utils.initBoundedStringZ(Channel.Name, channel_name)
|
||||
.name = try utils.initBoundedStringZ(Channel.Name, channel_name),
|
||||
.saved_collected_samples = saved_collected_samples
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -433,13 +603,18 @@ pub const Project = struct {
|
||||
view.graph_opts.y_range = try readRangeF64(reader);
|
||||
const sync_controls = try readInt(reader, u8);
|
||||
view.sync_controls = sync_controls == 1;
|
||||
|
||||
const marked_ranges_count = try readInt(reader, u32);
|
||||
for (0..marked_ranges_count) |_| {
|
||||
try view.marked_ranges.append(View.MarkedRange{
|
||||
.axis = try readEnum(reader, u8, UI.Axis),
|
||||
.range = try readRangeF64(reader)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.save_location = try allocator.dupe(u8, save_location);
|
||||
errdefer allocator.free(self.save_location);
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn save(self: *Project) !void {
|
||||
@ -470,6 +645,21 @@ pub const Project = struct {
|
||||
|
||||
try writeId(writer, channel_id);
|
||||
try writeString(writer, channel_name);
|
||||
try writeString(writer, channel.saved_collected_samples orelse &[0]u8{});
|
||||
|
||||
if (channel.saved_collected_samples != null and try channel.isSavedSampleFileStale(dir)) {
|
||||
const path = channel.saved_collected_samples.?;
|
||||
|
||||
{
|
||||
const samples_file = try dir.createFile(path, .{});
|
||||
defer samples_file.close();
|
||||
|
||||
try writeFileF64(samples_file, channel.collected_samples.items);
|
||||
}
|
||||
|
||||
const stat = try dir.statFile(path);
|
||||
channel.last_sample_save_at = stat.mtime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -504,6 +694,12 @@ pub const Project = struct {
|
||||
try writeRangeF64(writer, view.graph_opts.x_range);
|
||||
try writeRangeF64(writer, view.graph_opts.y_range);
|
||||
try writeInt(writer, u8, @intFromBool(view.sync_controls));
|
||||
|
||||
try writeInt(writer, u32, view.marked_ranges.len);
|
||||
for (view.marked_ranges.constSlice()) |marked_range| {
|
||||
try writeEnum(writer, u8, UI.Axis, marked_range.axis);
|
||||
try writeRangeF64(writer, marked_range.range);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -516,11 +712,11 @@ pub const Project = struct {
|
||||
try writeFloat(writer, f64, range.upper);
|
||||
}
|
||||
|
||||
fn readRangeF64(writer: anytype) !RangeF64 {
|
||||
fn readRangeF64(reader: anytype) !RangeF64 {
|
||||
var range: RangeF64 = undefined;
|
||||
|
||||
range.lower = try readFloat(writer, f64);
|
||||
range.upper = try readFloat(writer, f64);
|
||||
range.lower = try readFloat(reader, f64);
|
||||
range.upper = try readFloat(reader, f64);
|
||||
|
||||
return range;
|
||||
}
|
||||
@ -566,6 +762,27 @@ pub const Project = struct {
|
||||
try reader.readNoEof(buff);
|
||||
return buff;
|
||||
}
|
||||
|
||||
fn assertBackingIntForEnum(BackingInt: type, Enum: type) void {
|
||||
assert(@typeInfo(Enum) == .Enum);
|
||||
const enum_backing_type = @typeInfo(Enum).Enum.tag_type;
|
||||
assert(@typeInfo(enum_backing_type) == .Int);
|
||||
assert(@typeInfo(BackingInt).Int.bits >= @typeInfo(enum_backing_type).Int.bits);
|
||||
}
|
||||
|
||||
fn readEnum(reader: anytype, BackingInt: type, Enum: type) !Enum {
|
||||
assertBackingIntForEnum(BackingInt, Enum);
|
||||
|
||||
const value_int = try readInt(reader, BackingInt);
|
||||
|
||||
return try std.meta.intToEnum(Enum, value_int);
|
||||
}
|
||||
|
||||
fn writeEnum(writer: anytype, BackingInt: type, Enum: type, value: Enum) !void {
|
||||
assertBackingIntForEnum(BackingInt, Enum);
|
||||
|
||||
try writer.writeInt(BackingInt, @intFromEnum(value), file_endian);
|
||||
}
|
||||
};
|
||||
|
||||
pub const Command = union(enum) {
|
||||
@ -575,7 +792,8 @@ pub const Command = union(enum) {
|
||||
load_project,
|
||||
stop_output: Id, // Channel id
|
||||
start_output: Id, // Channel id
|
||||
add_file_from_picker
|
||||
add_file_from_picker,
|
||||
reload_file: Id, // File id
|
||||
};
|
||||
|
||||
pub const CollectionTask = struct {
|
||||
@ -612,6 +830,8 @@ collection_task: ?CollectionTask = null,
|
||||
|
||||
command_queue: std.BoundedArray(Command, 16) = .{},
|
||||
|
||||
file_picker_id: ?Platform.FilePickerId = null,
|
||||
|
||||
pub fn init(self: *App, allocator: Allocator) !void {
|
||||
self.* = App{
|
||||
.allocator = allocator,
|
||||
@ -689,15 +909,27 @@ fn deinitUI(self: *App) void {
|
||||
}
|
||||
|
||||
fn loadProject(self: *App) !void {
|
||||
if (self.isNiDaqInUse()) {
|
||||
log.warn("Attempt to load while collection is still in progress. Loading canceled", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
self.collection_mutex.lock();
|
||||
defer self.collection_mutex.unlock();
|
||||
|
||||
const save_location = self.project.save_location orelse return error.MissingSaveLocation;
|
||||
|
||||
log.info("Load project from: {s}", .{save_location});
|
||||
|
||||
const loaded = try Project.initFromFile(self.allocator, save_location);
|
||||
var loaded = try self.allocator.create(Project);
|
||||
defer self.allocator.destroy(loaded);
|
||||
|
||||
loaded.* = .{};
|
||||
errdefer loaded.deinit(self.allocator);
|
||||
try loaded.initFromFile(self.allocator, save_location);
|
||||
|
||||
self.deinitProject();
|
||||
self.project = loaded;
|
||||
self.project = loaded.*;
|
||||
|
||||
var file_iter = self.project.files.idIterator();
|
||||
while (file_iter.next()) |file_id| {
|
||||
@ -725,6 +957,14 @@ fn loadProject(self: *App) !void {
|
||||
}
|
||||
|
||||
fn saveProject(self: *App) !void {
|
||||
if (self.isNiDaqInUse()) {
|
||||
log.warn("Attempt to save while collection is still in progress. Saving canceled", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
self.collection_mutex.lock();
|
||||
defer self.collection_mutex.unlock();
|
||||
|
||||
const save_location = self.project.save_location orelse return error.MissingSaveLocation;
|
||||
|
||||
log.info("Save project to: {s}", .{save_location});
|
||||
@ -748,7 +988,17 @@ pub fn tick(self: *App) !void {
|
||||
var ui = &self.ui;
|
||||
self.command_queue.len = 0;
|
||||
|
||||
if (Platform.waitUntilFilePickerDone(self.allocator, &self.file_picker_id)) |path| {
|
||||
defer self.allocator.free(path);
|
||||
|
||||
const file_id = try self.addFile(path);
|
||||
_ = try self.addView(.{ .file = file_id });
|
||||
}
|
||||
|
||||
const cover_ui = Platform.isFilePickerOpen();
|
||||
if (!cover_ui) {
|
||||
ui.pullOsEvents();
|
||||
}
|
||||
|
||||
if (ui.isCtrlDown() and ui.isKeyboardPressed(.key_s)) {
|
||||
self.pushCommand(.save_project);
|
||||
@ -762,11 +1012,39 @@ pub fn tick(self: *App) !void {
|
||||
self.collection_samples_mutex.lock();
|
||||
defer self.collection_samples_mutex.unlock();
|
||||
|
||||
{
|
||||
var view_iter = self.project.views.idIterator();
|
||||
while (view_iter.next()) |id| {
|
||||
const view = self.getView(id) orelse continue;
|
||||
if (view.reference != .channel) continue;
|
||||
|
||||
const channel_id = view.reference.channel;
|
||||
const channel = self.getChannel(channel_id) orelse continue;
|
||||
if (!channel.hasNewCollectedSamples()) continue;
|
||||
|
||||
const samples = channel.collected_samples.items;
|
||||
|
||||
const new_samples_range = RangeF64.init(@floatFromInt(channel.processed_samples_up_to), @floatFromInt(samples.len));
|
||||
const marked_ranges: []View.MarkedRange = view.marked_ranges.slice();
|
||||
for (marked_ranges) |*marked_range| {
|
||||
if (marked_range.axis != .X) continue;
|
||||
|
||||
if (new_samples_range.intersectPositive(marked_range.range).isPositive()) {
|
||||
marked_range.refresh(samples);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update channel min max caches
|
||||
{
|
||||
var channel_iter = self.project.channels.iterator();
|
||||
while (channel_iter.next()) |channel| {
|
||||
if (channel.hasNewCollectedSamples()) {
|
||||
channel.invalidateSavedSamples();
|
||||
try channel.graph_min_max_cache.updateLast(self.allocator, channel.collected_samples.items);
|
||||
channel.processed_samples_up_to = channel.collected_samples.items.len;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -778,11 +1056,6 @@ pub fn tick(self: *App) !void {
|
||||
}
|
||||
|
||||
if (self.isCollectionInProgress()) {
|
||||
var channel_iter = self.project.channels.idIterator();
|
||||
while (channel_iter.next()) |channel_id| {
|
||||
self.refreshViewAvailableXYRanges(channel_id);
|
||||
}
|
||||
|
||||
var view_iter = self.project.views.iterator();
|
||||
while (view_iter.next()) |view| {
|
||||
if (view.reference != .channel) continue;
|
||||
@ -809,6 +1082,18 @@ pub fn tick(self: *App) !void {
|
||||
rl.clearBackground(srcery.black);
|
||||
ui.draw();
|
||||
|
||||
if (cover_ui) {
|
||||
rl.drawRectangleRec(
|
||||
rl.Rectangle{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
.width = @floatFromInt(rl.getScreenWidth()),
|
||||
.height = @floatFromInt(rl.getScreenHeight())
|
||||
},
|
||||
rl.Color.black.alpha(0.6)
|
||||
);
|
||||
}
|
||||
|
||||
for (self.command_queue.constSlice()) |command| {
|
||||
switch (command) {
|
||||
.start_collection => {
|
||||
@ -842,6 +1127,11 @@ pub fn tick(self: *App) !void {
|
||||
},
|
||||
.stop_output => |channel_id| {
|
||||
self.stopOutput(channel_id);
|
||||
},
|
||||
.reload_file => |file_id| {
|
||||
self.loadFile(file_id) catch |e| {
|
||||
log.err("Failed to load file: {}", .{ e });
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -855,11 +1145,15 @@ pub fn pushCommand(self: *App, command: Command) void {
|
||||
}
|
||||
|
||||
fn addFileFromPicker(self: *App) !void {
|
||||
const filename = try Platform.openFilePicker(self.allocator);
|
||||
defer self.allocator.free(filename);
|
||||
if (self.file_picker_id == null) {
|
||||
var opts: Platform.OpenFileOptions = .{};
|
||||
opts.style = .open;
|
||||
opts.file_must_exist = true;
|
||||
opts.appendFilter("All", "*") catch unreachable;
|
||||
opts.appendFilter("Binary", "*.bin") catch unreachable;
|
||||
|
||||
const file_id = try self.addFile(filename);
|
||||
_ = try self.addView(.{ .file = file_id });
|
||||
self.file_picker_id = try Platform.spawnFilePicker(&opts);
|
||||
}
|
||||
}
|
||||
|
||||
fn startCollection(self: *App) !void {
|
||||
@ -1010,6 +1304,20 @@ pub fn isCollectionInProgress(self: *App) bool {
|
||||
return self.collection_task != null;
|
||||
}
|
||||
|
||||
pub fn isOutputingInProgress(self: *App) bool {
|
||||
var channel_iter = self.project.channels.idIterator();
|
||||
while (channel_iter.next()) |channel_id| {
|
||||
if (self.isChannelOutputing(channel_id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn isNiDaqInUse(self: *App) bool {
|
||||
return self.isCollectionInProgress() or self.isOutputingInProgress();
|
||||
}
|
||||
|
||||
pub fn collectionThreadCallback(self: *App) void {
|
||||
while (!self.should_close) {
|
||||
|
||||
@ -1104,11 +1412,21 @@ fn readFileF64(allocator: Allocator, file: std.fs.File) ![]f64 {
|
||||
return samples;
|
||||
}
|
||||
|
||||
fn writeFileF64(file: std.fs.File, data: []const f64) !void {
|
||||
try file.seekTo(0);
|
||||
|
||||
for (data) |number| {
|
||||
const number_bytes = std.mem.asBytes(&number);
|
||||
try file.writeAll(number_bytes);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn loadFile(self: *App, id: Id) !void {
|
||||
const file = self.getFile(id) orelse return;
|
||||
file.clear(self.allocator);
|
||||
|
||||
const cwd = std.fs.cwd();
|
||||
|
||||
const samples_file = try cwd.openFile(file.path, .{ .mode = .read_only });
|
||||
defer samples_file.close();
|
||||
|
||||
@ -1139,9 +1457,24 @@ pub fn addChannel(self: *App, channel_name: []const u8) !Id {
|
||||
|
||||
const channel = self.project.channels.get(id).?;
|
||||
channel.* = Channel{
|
||||
.name = try utils.initBoundedStringZ(Channel.Name, channel_name)
|
||||
.name = try utils.initBoundedStringZ(Channel.Name, channel_name),
|
||||
};
|
||||
|
||||
if (self.project.save_location) |project_file_path| {
|
||||
if (std.fs.path.dirname(project_file_path)) |project_dir| {
|
||||
var clean_channel_name_buff = channel.name;
|
||||
const clean_channel_name = utils.getBoundedStringZ(&clean_channel_name_buff);
|
||||
|
||||
// Sanitize the channel name, because it will be used as a filename
|
||||
std.mem.replaceScalar(u8, clean_channel_name, '/', '_');
|
||||
|
||||
const filename = try std.mem.concat(self.allocator, u8, &.{ clean_channel_name, ".bin" });
|
||||
defer self.allocator.free(filename);
|
||||
|
||||
channel.saved_collected_samples = try std.fs.path.join(self.allocator, &.{ project_dir, filename });
|
||||
}
|
||||
}
|
||||
|
||||
self.loadChannel(id) catch |e| {
|
||||
log.err("Failed to load channel: {}", .{ e });
|
||||
};
|
||||
@ -1184,6 +1517,22 @@ fn getAllowedSampleRate(self: *App, id: Id) !RangeF64 {
|
||||
return RangeF64.init(min_sample_rate, max_sample_rate);
|
||||
}
|
||||
|
||||
fn loadSavedSamples(self: *App, channel_id: Id) !void {
|
||||
const channel = self.getChannel(channel_id) orelse return;
|
||||
const saved_samples_location = channel.saved_collected_samples orelse return;
|
||||
|
||||
const dir = std.fs.cwd();
|
||||
|
||||
const samples_file = try dir.openFile(saved_samples_location, .{ });
|
||||
defer samples_file.close();
|
||||
|
||||
const samples = try readFileF64(self.allocator, samples_file);
|
||||
channel.collected_samples = std.ArrayListUnmanaged(f64).fromOwnedSlice(samples);
|
||||
|
||||
const stat = try dir.statFile(saved_samples_location);
|
||||
channel.last_sample_save_at = stat.mtime;
|
||||
}
|
||||
|
||||
pub fn loadChannel(self: *App, id: Id) !void {
|
||||
const channel = self.getChannel(id) orelse return;
|
||||
channel.clear(self.allocator);
|
||||
@ -1201,6 +1550,10 @@ pub fn loadChannel(self: *App, id: Id) !void {
|
||||
log.err("Failed to get allowed sample rate: {}", .{e});
|
||||
break :blk null;
|
||||
};
|
||||
|
||||
self.loadSavedSamples(id) catch |e| {
|
||||
log.warn("Failed to load saved samples on channel: {}", .{e});
|
||||
};
|
||||
}
|
||||
|
||||
pub fn isChannelOutputing(self: *App, id: Id) bool {
|
||||
@ -1244,22 +1597,7 @@ pub fn addView(self: *App, reference: View.Reference) !Id {
|
||||
}
|
||||
|
||||
pub fn getViewSamples(self: *App, id: Id) []const f64 {
|
||||
const empty = &[0]f64{};
|
||||
|
||||
var result: []const f64 = empty;
|
||||
|
||||
if (self.getView(id)) |view| {
|
||||
switch (view.reference) {
|
||||
.channel => |channel_id| if (self.getChannel(channel_id)) |channel| {
|
||||
result = channel.collected_samples.items;
|
||||
},
|
||||
.file => |file_id| if (self.getFile(file_id)) |file| {
|
||||
result = file.samples orelse empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return self.project.getViewSamples(id);
|
||||
}
|
||||
|
||||
pub fn getViewMinMaxCache(self: *App, id: Id) Graph.MinMaxCache {
|
||||
|
@ -49,6 +49,7 @@ pub var fullscreen: rl.Texture2D = undefined;
|
||||
pub var output_generation: rl.Texture2D = undefined;
|
||||
pub var checkbox_mark: rl.Texture2D = undefined;
|
||||
pub var cross: rl.Texture2D = undefined;
|
||||
pub var file: rl.Texture2D = undefined;
|
||||
|
||||
pub fn font(font_id: FontId) FontFace {
|
||||
var found_font: ?LoadedFont = null;
|
||||
@ -125,6 +126,7 @@ pub fn init(allocator: std.mem.Allocator) !void {
|
||||
output_generation = try loadTextureFromAseprite(allocator, @embedFile("./assets/output-generation-icon.ase"));
|
||||
checkbox_mark = try loadTextureFromAseprite(allocator, @embedFile("./assets/checkbox-mark.ase"));
|
||||
cross = try loadTextureFromAseprite(allocator, @embedFile("./assets/cross.ase"));
|
||||
file = try loadTextureFromAseprite(allocator, @embedFile("./assets/file.ase"));
|
||||
}
|
||||
|
||||
fn loadTextureFromAseprite(allocator: std.mem.Allocator, memory: []const u8) !rl.Texture {
|
||||
|
@ -36,7 +36,12 @@ pub const ViewAxisPosition = struct {
|
||||
if (self.axis != axis) return null;
|
||||
|
||||
const view = project.views.get(view_id) orelse return null;
|
||||
if (!view.sync_controls) {
|
||||
if (view.sync_controls) {
|
||||
const owner_view = project.views.get(self.view_id).?;
|
||||
if (!owner_view.sync_controls) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
if (!self.view_id.eql(view_id)) {
|
||||
return null;
|
||||
}
|
||||
@ -46,23 +51,23 @@ pub const ViewAxisPosition = struct {
|
||||
}
|
||||
};
|
||||
|
||||
pub const Command = struct {
|
||||
view_id: Id,
|
||||
action: union(enum) {
|
||||
pub const Command = union(enum) {
|
||||
breakpoint,
|
||||
move_and_zoom: struct {
|
||||
view_id: Id,
|
||||
before_x: RangeF64,
|
||||
before_y: RangeF64,
|
||||
x: RangeF64,
|
||||
y: RangeF64
|
||||
}
|
||||
},
|
||||
|
||||
fn apply(self: *const Command, system: *System) void {
|
||||
const project = system.project;
|
||||
const view = project.views.get(self.view_id) orelse return;
|
||||
|
||||
switch (self.action) {
|
||||
switch (self.*) {
|
||||
.breakpoint => {},
|
||||
.move_and_zoom => |move_and_zoom| {
|
||||
const view = project.views.get(move_and_zoom.view_id) orelse return;
|
||||
const view_rect = &view.graph_opts;
|
||||
view_rect.x_range = move_and_zoom.x;
|
||||
view_rect.y_range = move_and_zoom.y;
|
||||
@ -73,10 +78,11 @@ pub const Command = struct {
|
||||
|
||||
fn undo(self: *const Command, system: *System) void {
|
||||
const project = system.project;
|
||||
const view = project.views.get(self.view_id) orelse return;
|
||||
|
||||
switch (self.action) {
|
||||
switch (self.*) {
|
||||
.breakpoint => {},
|
||||
.move_and_zoom => |move_and_zoom| {
|
||||
const view = project.views.get(move_and_zoom.view_id) orelse return;
|
||||
const view_rect = &view.graph_opts;
|
||||
view_rect.x_range = move_and_zoom.before_x;
|
||||
view_rect.y_range = move_and_zoom.before_y;
|
||||
@ -86,12 +92,17 @@ pub const Command = struct {
|
||||
};
|
||||
|
||||
pub const CommandFrame = struct {
|
||||
// When a new command is pushed after this one, it will become marked as "frozen"
|
||||
// So that movement commands wouldn't be able to update it.
|
||||
frozen: bool = false,
|
||||
|
||||
updated_at_ns: i128,
|
||||
commands: std.BoundedArray(Command, constants.max_views) = .{},
|
||||
|
||||
fn findCommandByView(self: *CommandFrame, view_id: Id) ?*Command {
|
||||
for (self.commands.slice()) |*command| {
|
||||
if (command.view_id.eql(view_id)) {
|
||||
const commands: []Command = self.commands.slice();
|
||||
for (commands) |*command| {
|
||||
if (command.* == .move_and_zoom and command.move_and_zoom.view_id.eql(view_id)) {
|
||||
return command;
|
||||
}
|
||||
}
|
||||
@ -112,10 +123,42 @@ pub const CommandFrame = struct {
|
||||
command.undo(system);
|
||||
}
|
||||
}
|
||||
|
||||
fn hasBreakpoint(self: *const CommandFrame) bool {
|
||||
const commands: []const Command = self.commands.constSlice();
|
||||
for (commands) |*command| {
|
||||
if (command.* == .breakpoint) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
pub const CommandFrameArray = std.BoundedArray(CommandFrame, 64);
|
||||
|
||||
const MarkedRangeIterator = struct {
|
||||
system: *System,
|
||||
view_id: Id,
|
||||
axis: UI.Axis,
|
||||
index: usize = 0,
|
||||
next_index: usize = 0,
|
||||
|
||||
pub fn next(self: *MarkedRangeIterator) ?RangeF64 {
|
||||
const selected_ranges = self.system.marked_ranges.constSlice();
|
||||
while (self.next_index < selected_ranges.len) {
|
||||
self.index = self.next_index;
|
||||
const selected_range = selected_ranges[self.index];
|
||||
self.next_index += 1;
|
||||
|
||||
if (selected_range.axis == self.axis and selected_range.view_id.eql(self.view_id)) {
|
||||
return selected_range.range;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
project: *App.Project,
|
||||
|
||||
// TODO: Redo
|
||||
@ -127,6 +170,11 @@ 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,
|
||||
show_marked_range: ?struct {
|
||||
view_id: Id,
|
||||
index: usize,
|
||||
} = null,
|
||||
|
||||
pub fn init(project: *App.Project) System {
|
||||
return System{
|
||||
@ -139,8 +187,13 @@ fn pushCommandFrame(self: *System) *CommandFrame {
|
||||
_ = self.undo_stack.orderedRemove(0);
|
||||
}
|
||||
|
||||
if (self.lastCommandFrame()) |frame| {
|
||||
frame.frozen = true;
|
||||
}
|
||||
|
||||
var frame = self.undo_stack.addOneAssumeCapacity();
|
||||
frame.updated_at_ns = std.time.nanoTimestamp();
|
||||
frame.commands.len = 0;
|
||||
return frame;
|
||||
}
|
||||
|
||||
@ -152,9 +205,19 @@ fn lastCommandFrame(self: *System) ?*CommandFrame {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn pushBreakpoint(self: *System) void {
|
||||
if (self.lastCommandFrame()) |last_frame| {
|
||||
// No need to have 2 break points in a row
|
||||
if (last_frame.hasBreakpoint()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const frame = self.pushCommandFrame();
|
||||
frame.commands.appendAssumeCapacity(.{ .breakpoint = {} });
|
||||
}
|
||||
|
||||
pub fn pushViewMove(self: *System, view_id: Id, x_range: RangeF64, y_range: RangeF64) void {
|
||||
|
||||
|
||||
var frame: *CommandFrame = undefined;
|
||||
{
|
||||
var push_new_command = true;
|
||||
@ -162,7 +225,7 @@ pub fn pushViewMove(self: *System, view_id: Id, x_range: RangeF64, y_range: Rang
|
||||
frame = last_frame;
|
||||
|
||||
const now_ns = std.time.nanoTimestamp();
|
||||
if (now_ns - last_frame.updated_at_ns < std.time.ns_per_ms * 250) {
|
||||
if (!last_frame.frozen and now_ns - last_frame.updated_at_ns < std.time.ns_per_ms * 250) {
|
||||
last_frame.updated_at_ns = now_ns;
|
||||
push_new_command = false;
|
||||
|
||||
@ -199,8 +262,8 @@ pub fn pushViewMove(self: *System, view_id: Id, x_range: RangeF64, y_range: Rang
|
||||
if (frame.findCommandByView(id)) |prev_command| {
|
||||
command = prev_command;
|
||||
|
||||
command.action.move_and_zoom.x = x_range;
|
||||
command.action.move_and_zoom.y = y_range;
|
||||
command.move_and_zoom.x = x_range;
|
||||
command.move_and_zoom.y = y_range;
|
||||
} else {
|
||||
const view = self.project.views.get(view_id) orelse continue;
|
||||
const view_rect = &view.graph_opts;
|
||||
@ -208,15 +271,13 @@ pub fn pushViewMove(self: *System, view_id: Id, x_range: RangeF64, y_range: Rang
|
||||
command = frame.commands.addOneAssumeCapacity();
|
||||
|
||||
command.* = Command{
|
||||
.view_id = id,
|
||||
.action = .{
|
||||
.move_and_zoom = .{
|
||||
.view_id = id,
|
||||
.before_x = view_rect.x_range,
|
||||
.before_y = view_rect.y_range,
|
||||
.x = x_range,
|
||||
.y = y_range
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -235,11 +296,15 @@ pub fn pushViewMoveAxis(self: *System, view_id: Id, axis: UI.Axis, view_range: R
|
||||
}
|
||||
|
||||
pub fn undoLastMove(self: *System) void {
|
||||
const frame = self.undo_stack.popOrNull() orelse return;
|
||||
|
||||
while (self.undo_stack.popOrNull()) |frame| {
|
||||
frame.undo(self);
|
||||
|
||||
self.last_applied_command = @min(self.last_applied_command, self.undo_stack.len);
|
||||
|
||||
if (!frame.hasBreakpoint()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn applyCommands(self: *System) void {
|
||||
@ -277,10 +342,24 @@ pub fn getCursor(self: *System, view_id: Id, axis: UI.Axis) ?f64 {
|
||||
return ViewAxisPosition.get(&self.cursor, self.project, view_id, axis);
|
||||
}
|
||||
|
||||
pub fn setZoomStart(self: *System, view_id: Id, axis: UI.Axis, position: ?f64) void {
|
||||
pub fn setCursorHoldStart(self: *System, view_id: Id, axis: UI.Axis, position: ?f64) void {
|
||||
return ViewAxisPosition.set(&self.zoom_start, view_id, axis, position);
|
||||
}
|
||||
|
||||
pub fn getZoomStart(self: *System, view_id: Id, axis: UI.Axis) ?f64 {
|
||||
pub fn getCursorHoldStart(self: *System, view_id: Id, axis: UI.Axis) ?f64 {
|
||||
return ViewAxisPosition.get(&self.zoom_start, self.project,view_id, axis);
|
||||
}
|
||||
|
||||
pub fn toggleShownMarkedRange(self: *System, view_id: Id, index: usize) void {
|
||||
if (self.show_marked_range) |show_marked_range| {
|
||||
if (show_marked_range.view_id.eql(view_id) and show_marked_range.index == index) {
|
||||
self.show_marked_range = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.show_marked_range = .{
|
||||
.view_id = view_id,
|
||||
.index = index,
|
||||
};
|
||||
}
|
@ -70,6 +70,7 @@ fn showGraph(ctx: Context, view_id: Id) *UI.Box {
|
||||
sample_value_under_mouse = mouse_y_range.remapTo(view_opts.y_range, signal.relative_mouse.y);
|
||||
}
|
||||
|
||||
if (ctx.view_controls.selected_tool == .move) {
|
||||
if (signal.dragged()) {
|
||||
const x_offset = mouse_x_range.remapTo(RangeF64.init(0, view_opts.x_range.size()), signal.drag.x);
|
||||
const y_offset = mouse_y_range.remapTo(RangeF64.init(0, view_opts.y_range.size()), signal.drag.y);
|
||||
@ -100,12 +101,21 @@ fn showGraph(ctx: Context, view_id: Id) *UI.Box {
|
||||
ctx.view_controls.pushViewMove(view_id, view.available_x_range, view.available_y_range);
|
||||
}
|
||||
|
||||
view.graph_cache.min_max_cache = app.getViewMinMaxCache(view_id);
|
||||
if (signal.flags.contains(.left_released)) {
|
||||
ctx.view_controls.pushBreakpoint();
|
||||
}
|
||||
} else if (ctx.view_controls.selected_tool == .select) {
|
||||
// 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);
|
||||
if (view.graph_cache.texture) |texture| {
|
||||
graph_box.texture = texture.texture;
|
||||
}
|
||||
}
|
||||
|
||||
if (view_opts.x_range.size() == 0 or view_opts.y_range.size() == 0) {
|
||||
graph_box.setText("<Empty>");
|
||||
|
@ -15,32 +15,42 @@ const assert = std.debug.assert;
|
||||
|
||||
const ruler_size = UI.Sizing.initFixed(.{ .pixels = 32 });
|
||||
|
||||
const Ruler = struct {
|
||||
project: *App.Project,
|
||||
box: *UI.Box,
|
||||
graph_box: *UI.Box,
|
||||
view_id: Id,
|
||||
axis: UI.Axis,
|
||||
|
||||
fn getBoxDrawContext(self: *const Ruler) DrawContext {
|
||||
var draw_ctx = DrawContext.init(self.axis, self.project, self.view_id);
|
||||
draw_ctx.rect = .{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
.width = self.box.persistent.size.x,
|
||||
.height = self.box.persistent.size.y
|
||||
};
|
||||
return draw_ctx;
|
||||
}
|
||||
|
||||
fn getGraphDrawContext(self: *const Ruler) DrawContext {
|
||||
var draw_ctx = DrawContext.init(self.axis, self.project, self.view_id);
|
||||
draw_ctx.rect = .{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
.width = self.graph_box.persistent.size.x,
|
||||
.height = self.graph_box.persistent.size.y
|
||||
};
|
||||
return draw_ctx;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Context = struct {
|
||||
ui: *UI,
|
||||
project: *App.Project,
|
||||
view_controls: *ViewControlsSystem
|
||||
};
|
||||
|
||||
pub fn createBox(ctx: Context, key: UI.Key, axis: UI.Axis) *UI.Box {
|
||||
var ui = ctx.ui;
|
||||
|
||||
var ruler = ui.createBox(.{
|
||||
.key = key,
|
||||
.background = srcery.hard_black,
|
||||
.flags = &.{ .clickable, .scrollable, .clip_view },
|
||||
.hot_cursor = .mouse_cursor_pointing_hand
|
||||
});
|
||||
if (axis == .X) {
|
||||
ruler.size.x = UI.Sizing.initGrowFull();
|
||||
ruler.size.y = ruler_size;
|
||||
} else {
|
||||
ruler.size.x = ruler_size;
|
||||
ruler.size.y = UI.Sizing.initGrowFull();
|
||||
}
|
||||
|
||||
return ruler;
|
||||
}
|
||||
|
||||
const DrawContext = struct {
|
||||
one_unit: ?f64,
|
||||
render_range: RangeF64,
|
||||
@ -62,7 +72,7 @@ const DrawContext = struct {
|
||||
};
|
||||
}
|
||||
|
||||
fn getPoint(self: *DrawContext, along_axis_pos: f64, cross_axis_pos: f64) rl.Vector2 {
|
||||
fn getPoint(self: *const DrawContext, along_axis_pos: f64, cross_axis_pos: f64) rl.Vector2 {
|
||||
const rect_width: f64 = @floatCast(self.rect.width);
|
||||
const rect_height: f64 = @floatCast(self.rect.height);
|
||||
|
||||
@ -85,7 +95,7 @@ const DrawContext = struct {
|
||||
};
|
||||
}
|
||||
|
||||
fn getLine(self: *DrawContext, along_axis_pos: f64, cross_axis_size: f64) rl.Rectangle {
|
||||
fn getLine(self: *const DrawContext, along_axis_pos: f64, cross_axis_size: f64) rl.Rectangle {
|
||||
const along_axis_size = self.render_range.size() / switch(self.axis) {
|
||||
.X => @as(f64, @floatCast(self.rect.width)),
|
||||
.Y => @as(f64, @floatCast(self.rect.height))
|
||||
@ -99,7 +109,7 @@ const DrawContext = struct {
|
||||
);
|
||||
}
|
||||
|
||||
fn getRect(self: *DrawContext, along_axis_pos: f64, along_axis_size: f64, cross_axis_pos: f64, cross_axis_size: f64) rl.Rectangle {
|
||||
fn getRect(self: *const DrawContext, along_axis_pos: f64, along_axis_size: f64, cross_axis_pos: f64, cross_axis_size: f64) rl.Rectangle {
|
||||
const pos = self.getPoint(along_axis_pos, cross_axis_pos);
|
||||
const corner = self.getPoint(along_axis_pos + along_axis_size, cross_axis_pos + cross_axis_size);
|
||||
var rect = rl.Rectangle{
|
||||
@ -122,7 +132,7 @@ const DrawContext = struct {
|
||||
return rect;
|
||||
}
|
||||
|
||||
fn drawLine(self: *DrawContext, along_axis_pos: f64, cross_axis_size: f64, color: rl.Color) void {
|
||||
fn drawLine(self: *const DrawContext, along_axis_pos: f64, cross_axis_size: f64, color: rl.Color) void {
|
||||
rl.drawLineV(
|
||||
self.getPoint(along_axis_pos, 0),
|
||||
self.getPoint(along_axis_pos, cross_axis_size),
|
||||
@ -130,7 +140,7 @@ const DrawContext = struct {
|
||||
);
|
||||
}
|
||||
|
||||
fn drawTicks(self: *DrawContext, from: f64, to: f64, step: f64, line_size: f64, color: rl.Color) void {
|
||||
fn drawTicks(self: *const DrawContext, from: f64, to: f64, step: f64, line_size: f64, color: rl.Color) void {
|
||||
var position = from;
|
||||
while (position < to) : (position += step) {
|
||||
self.drawLine(position, line_size, color);
|
||||
@ -138,6 +148,26 @@ const DrawContext = struct {
|
||||
}
|
||||
};
|
||||
|
||||
pub fn createBox(ctx: Context, key: UI.Key, axis: UI.Axis) *UI.Box {
|
||||
var ui = ctx.ui;
|
||||
|
||||
var ruler = ui.createBox(.{
|
||||
.key = key,
|
||||
.background = srcery.hard_black,
|
||||
.flags = &.{ .clickable, .scrollable, .clip_view },
|
||||
.hot_cursor = .mouse_cursor_pointing_hand
|
||||
});
|
||||
if (axis == .X) {
|
||||
ruler.size.x = UI.Sizing.initGrowFull();
|
||||
ruler.size.y = ruler_size;
|
||||
} else {
|
||||
ruler.size.x = ruler_size;
|
||||
ruler.size.y = UI.Sizing.initGrowFull();
|
||||
}
|
||||
|
||||
return ruler;
|
||||
}
|
||||
|
||||
fn drawRulerTicks(_ctx: ?*anyopaque, box: *UI.Box) void {
|
||||
const ctx: *DrawContext = @ptrCast(@alignCast(_ctx));
|
||||
ctx.rect = box.rect();
|
||||
@ -192,27 +222,46 @@ fn showMouseTooltip(ctx: Context, axis: UI.Axis, view_id: Id, position: f64) voi
|
||||
}
|
||||
}
|
||||
|
||||
fn showMarkerLine(ui: *UI, ruler: Ruler, position: f64, color: rl.Color) void {
|
||||
_ = ui.createBox(.{
|
||||
.background = color,
|
||||
.float_rect = ruler.getBoxDrawContext().getLine(position, 1),
|
||||
.float_relative_to = ruler.box,
|
||||
.parent = ruler.box
|
||||
});
|
||||
|
||||
_ = ui.createBox(.{
|
||||
.background = color,
|
||||
.float_rect = ruler.getGraphDrawContext().getLine(position, 1),
|
||||
.float_relative_to = ruler.graph_box,
|
||||
.parent = ruler.graph_box
|
||||
});
|
||||
}
|
||||
|
||||
fn showMarkerRect(ui: *UI, ruler: Ruler, range: RangeF64, color: rl.Color, key: ?UI.Key) *UI.Box {
|
||||
return ui.createBox(.{
|
||||
.key = key,
|
||||
.background = color,
|
||||
.float_relative_to = ruler.box,
|
||||
.parent = ruler.box,
|
||||
.float_rect = ruler.getBoxDrawContext().getRect(@min(range.lower, range.upper), range.size(), 0, 1),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn show(ctx: Context, box: *UI.Box, graph_box: *UI.Box, view_id: Id, axis: UI.Axis) !void {
|
||||
const ruler = Ruler{
|
||||
.project = ctx.project,
|
||||
.box = box,
|
||||
.graph_box = graph_box,
|
||||
.view_id = view_id,
|
||||
.axis = axis
|
||||
};
|
||||
|
||||
var ui = ctx.ui;
|
||||
const project = ctx.project;
|
||||
|
||||
const view = project.views.get(view_id) orelse return;
|
||||
|
||||
var ruler_ctx = DrawContext.init(axis, project, view_id);
|
||||
var graph_ctx = ruler_ctx;
|
||||
ruler_ctx.rect = .{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
.width = box.persistent.size.x,
|
||||
.height = box.persistent.size.y
|
||||
};
|
||||
graph_ctx.rect = .{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
.width = graph_box.persistent.size.x,
|
||||
.height = graph_box.persistent.size.y
|
||||
};
|
||||
|
||||
box.beginChildren();
|
||||
defer box.endChildren();
|
||||
|
||||
@ -229,44 +278,68 @@ pub fn show(ctx: Context, box: *UI.Box, graph_box: *UI.Box, view_id: Id, axis: U
|
||||
{ // Visuals
|
||||
const cursor = ctx.view_controls.getCursor(view_id, axis);
|
||||
|
||||
var zoom_start: ?f64 = null;
|
||||
if (ctx.view_controls.getZoomStart(view_id, axis)) |zoom_start_position| {
|
||||
zoom_start = zoom_start_position;
|
||||
var hold_start: ?f64 = null;
|
||||
if (ctx.view_controls.getCursorHoldStart(view_id, axis)) |hold_start_position| {
|
||||
hold_start = hold_start_position;
|
||||
}
|
||||
|
||||
var markers: std.BoundedArray(f64, 2) = .{};
|
||||
|
||||
if (zoom_start != null) {
|
||||
markers.appendAssumeCapacity(zoom_start.?);
|
||||
if (hold_start != null) {
|
||||
markers.appendAssumeCapacity(hold_start.?);
|
||||
}
|
||||
|
||||
if (cursor != null) {
|
||||
markers.appendAssumeCapacity(cursor.?);
|
||||
}
|
||||
|
||||
for (markers.constSlice()) |marker_position| {
|
||||
_ = ui.createBox(.{
|
||||
.background = srcery.green,
|
||||
.float_rect = ruler_ctx.getLine(marker_position, 1),
|
||||
.float_relative_to = box,
|
||||
});
|
||||
const marker_color = srcery.green;
|
||||
|
||||
_ = ui.createBox(.{
|
||||
.background = srcery.green,
|
||||
.float_rect = graph_ctx.getLine(marker_position, 1),
|
||||
.float_relative_to = graph_box,
|
||||
.parent = graph_box
|
||||
});
|
||||
for (markers.constSlice()) |marker_position| {
|
||||
showMarkerLine(ui, ruler, marker_position, marker_color);
|
||||
}
|
||||
|
||||
if (zoom_start != null and cursor != null) {
|
||||
const zoom_end = cursor.?;
|
||||
if (hold_start != null and cursor != null) {
|
||||
_ = showMarkerRect(ui, ruler, RangeF64.init(hold_start.?, cursor.?), marker_color.alpha(0.5), null);
|
||||
}
|
||||
|
||||
_ = ui.createBox(.{
|
||||
.background = srcery.green.alpha(0.5),
|
||||
.float_relative_to = box,
|
||||
.float_rect = ruler_ctx.getRect(zoom_start.?, zoom_end - zoom_start.?, 0, 1),
|
||||
});
|
||||
{
|
||||
|
||||
var selected_range_iter = view.iterMarkedRanges(axis);
|
||||
while (selected_range_iter.next()) |selected_range| {
|
||||
var color = srcery.blue;
|
||||
const index = selected_range_iter.index;
|
||||
|
||||
if (ctx.view_controls.show_marked_range) |show_marked_range| {
|
||||
if (show_marked_range.view_id.eql(view_id) and show_marked_range.index == index) {
|
||||
if (@mod(rl.getTime(), 0.5) < 0.25) {
|
||||
color = utils.shiftColorInHSV(color, 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showMarkerLine(ui, ruler, selected_range.lower, color);
|
||||
showMarkerLine(ui, ruler, selected_range.upper, color);
|
||||
|
||||
var hasher = UI.Key.CombineHasher.init();
|
||||
hasher.update(std.mem.asBytes(&view_id));
|
||||
hasher.update(std.mem.asBytes(&axis));
|
||||
hasher.update(std.mem.asBytes(&index));
|
||||
const range_box_key = UI.Key.init(hasher.final());
|
||||
|
||||
var range_box = showMarkerRect(ui, ruler, selected_range, color.alpha(0.5), range_box_key);
|
||||
range_box.flags.insert(.clickable);
|
||||
range_box.flags.insert(.draw_hot);
|
||||
range_box.flags.insert(.draw_active);
|
||||
|
||||
range_box.hot_cursor = .mouse_cursor_pointing_hand;
|
||||
if (ctx.view_controls.selected_tool == .select) {
|
||||
const signal = ui.signal(range_box);
|
||||
if (signal.clicked()) {
|
||||
ctx.view_controls.toggleShownMarkedRange(view_id, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -294,9 +367,10 @@ pub fn show(ctx: Context, box: *UI.Box, graph_box: *UI.Box, view_id: Id, axis: U
|
||||
const cursor = ctx.view_controls.getCursor(view_id, axis);
|
||||
|
||||
if (signal.flags.contains(.left_pressed)) {
|
||||
ctx.view_controls.setZoomStart(view_id, axis, cursor);
|
||||
ctx.view_controls.setCursorHoldStart(view_id, axis, cursor);
|
||||
}
|
||||
|
||||
if (ctx.view_controls.selected_tool == .move) {
|
||||
if (signal.scrolled() and cursor != null) {
|
||||
var scale_factor: f64 = 1;
|
||||
if (signal.scroll.y > 0) {
|
||||
@ -308,7 +382,7 @@ pub fn show(ctx: Context, box: *UI.Box, graph_box: *UI.Box, view_id: Id, axis: U
|
||||
ctx.view_controls.pushViewMoveAxis(view_id, axis, new_view_range);
|
||||
}
|
||||
|
||||
if (ctx.view_controls.getZoomStart(view_id, axis)) |zoom_start| {
|
||||
if (ctx.view_controls.getCursorHoldStart(view_id, axis)) |zoom_start| {
|
||||
if (signal.flags.contains(.left_released)) {
|
||||
const zoom_end = cursor;
|
||||
|
||||
@ -329,7 +403,31 @@ pub fn show(ctx: Context, box: *UI.Box, graph_box: *UI.Box, view_id: Id, axis: U
|
||||
ctx.view_controls.pushViewMoveAxis(view_id, axis, new_view_range);
|
||||
}
|
||||
}
|
||||
ctx.view_controls.zoom_start = null;
|
||||
}
|
||||
}
|
||||
} else if (ctx.view_controls.selected_tool == .select) {
|
||||
// TODO:
|
||||
|
||||
if (cursor != null) {
|
||||
if (ctx.view_controls.getCursorHoldStart(view_id, axis)) |hold_start| {
|
||||
const range = RangeF64.init(
|
||||
@min(hold_start, cursor.?),
|
||||
@max(hold_start, cursor.?),
|
||||
);
|
||||
const hold_start_mouse = view_range.remapTo(mouse_range, range.lower);
|
||||
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));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (signal.flags.contains(.left_released)) {
|
||||
ctx.view_controls.setCursorHoldStart(view_id, axis, null);
|
||||
}
|
||||
}
|
@ -50,8 +50,7 @@ pub const MinMaxCache = struct {
|
||||
max: f64
|
||||
};
|
||||
|
||||
const chunk_size = 256;
|
||||
|
||||
chunk_size: usize = 256,
|
||||
min_max_pairs: std.ArrayListUnmanaged(MinMaxPair) = .{},
|
||||
sample_count: usize = 0,
|
||||
|
||||
@ -60,8 +59,9 @@ pub const MinMaxCache = struct {
|
||||
self.sample_count = 0;
|
||||
}
|
||||
|
||||
fn getMinMaxPair(chunk: []const f64) MinMaxPair {
|
||||
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];
|
||||
@ -81,9 +81,9 @@ pub const MinMaxCache = struct {
|
||||
|
||||
if (samples.len == 0) return;
|
||||
|
||||
var iter = std.mem.window(f64, samples, chunk_size, chunk_size);
|
||||
var iter = std.mem.window(f64, samples, self.chunk_size, self.chunk_size);
|
||||
while (iter.next()) |chunk| {
|
||||
try self.min_max_pairs.append(allocator, getMinMaxPair(chunk));
|
||||
try self.min_max_pairs.append(allocator, self.getMinMaxPair(chunk));
|
||||
}
|
||||
|
||||
self.sample_count = samples.len;
|
||||
@ -97,16 +97,16 @@ pub const MinMaxCache = struct {
|
||||
return;
|
||||
}
|
||||
|
||||
const from_chunk = @divFloor(self.sample_count, chunk_size);
|
||||
const to_chunk = @divFloor(samples.len - 1, chunk_size);
|
||||
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[
|
||||
(chunk_size*i)..(@min(chunk_size*(i+1), samples.len))
|
||||
(self.chunk_size*i)..(@min(self.chunk_size*(i+1), samples.len))
|
||||
];
|
||||
const min_max_pair = getMinMaxPair(chunk);
|
||||
const min_max_pair = self.getMinMaxPair(chunk);
|
||||
|
||||
if (i <= self.min_max_pairs.items.len) {
|
||||
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;
|
||||
@ -115,6 +115,39 @@ pub const MinMaxCache = struct {
|
||||
|
||||
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 {
|
||||
@ -269,8 +302,8 @@ fn drawSamplesMinMax(draw_rect: rl.Rectangle, options: ViewOptions, min_max_cach
|
||||
var column_start: usize = @intFromFloat(i);
|
||||
var column_end: usize = @intFromFloat(i + samples_per_column);
|
||||
|
||||
column_start = @divFloor(column_start, MinMaxCache.chunk_size);
|
||||
column_end = @divFloor(column_end, MinMaxCache.chunk_size);
|
||||
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;
|
||||
@ -304,7 +337,7 @@ fn drawSamplesMinMax(draw_rect: rl.Rectangle, options: ViewOptions, min_max_cach
|
||||
}
|
||||
}
|
||||
|
||||
fn drawSamples(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64, min_max_level: ?MinMaxCache) void {
|
||||
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;
|
||||
@ -312,8 +345,8 @@ fn drawSamples(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f
|
||||
|
||||
const samples_per_column = x_range.size() / draw_rect.width;
|
||||
if (samples_per_column >= 2) {
|
||||
if (min_max_level != null and samples_per_column > 2*MinMaxCache.chunk_size) {
|
||||
drawSamplesMinMax(draw_rect, options, min_max_level.?);
|
||||
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.?);
|
||||
} else {
|
||||
drawSamplesApproximate(draw_rect, options, samples);
|
||||
}
|
||||
@ -393,3 +426,7 @@ pub fn draw(cache: ?*RenderCache, draw_rect: rl.Rectangle, options: ViewOptions,
|
||||
drawSamples(draw_rect, options, samples, null);
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
_ = MinMaxCache;
|
||||
}
|
51
src/main.zig
51
src/main.zig
@ -4,7 +4,7 @@ const builtin = @import("builtin");
|
||||
const Application = @import("./app.zig");
|
||||
const Assets = @import("./assets.zig");
|
||||
const Profiler = @import("./my-profiler.zig");
|
||||
const Platform = @import("./platform.zig");
|
||||
const Platform = @import("./platform/root.zig");
|
||||
const raylib_h = @cImport({
|
||||
@cInclude("stdio.h");
|
||||
@cInclude("raylib.h");
|
||||
@ -65,18 +65,19 @@ fn raylibTraceLogCallback(logType: c_int, text: [*c]const u8, args: raylib_h.va_
|
||||
}
|
||||
|
||||
pub fn main() !void {
|
||||
Platform.init();
|
||||
|
||||
// TODO: Setup logging to a file
|
||||
raylib_h.SetTraceLogCallback(raylibTraceLogCallback);
|
||||
rl.setTraceLogLevel(toRaylibLogLevel(std.options.log_level));
|
||||
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{
|
||||
.thread_safe = true
|
||||
}){};
|
||||
const allocator = gpa.allocator();
|
||||
defer _ = gpa.deinit();
|
||||
|
||||
Platform.init(allocator);
|
||||
defer Platform.deinit(allocator);
|
||||
|
||||
// TODO: Setup logging to a file
|
||||
raylib_h.SetTraceLogCallback(raylibTraceLogCallback);
|
||||
rl.setTraceLogLevel(toRaylibLogLevel(std.options.log_level));
|
||||
|
||||
const icon_png = @embedFile("./assets/icon.png");
|
||||
var icon_image = rl.loadImageFromMemory(".png", icon_png);
|
||||
defer icon_image.unload();
|
||||
@ -102,6 +103,15 @@ pub fn main() !void {
|
||||
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);
|
||||
|
||||
const save_location = try std.fs.path.join(allocator, &.{ cwd_realpath, "project.proj" });
|
||||
errdefer allocator.free(save_location);
|
||||
|
||||
app.project.save_location = save_location;
|
||||
app.project.sample_rate = 5000;
|
||||
|
||||
_ = try app.addView(.{
|
||||
.channel = try app.addChannel("Dev1/ai0")
|
||||
});
|
||||
@ -125,32 +135,6 @@ pub fn main() !void {
|
||||
_ = try app.addView(.{
|
||||
.file = try app.addFile("./samples-18m.bin")
|
||||
});
|
||||
|
||||
// _ = try app.addView(.{
|
||||
// .channel = try app.addChannel("Dev1/ai0")
|
||||
// });
|
||||
// _ = try app.addView(.{
|
||||
// .channel = try app.addChannel("Dev3/ao0")
|
||||
// });
|
||||
// _ = try app.addView(.{
|
||||
// .file = try app.addFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_I.bin")
|
||||
// });
|
||||
|
||||
var cwd_realpath_buff: [std.fs.MAX_PATH_BYTES]u8 = undefined;
|
||||
const cwd_realpath = try std.fs.cwd().realpath(".", &cwd_realpath_buff);
|
||||
|
||||
const save_location = try std.fs.path.join(allocator, &.{ cwd_realpath, "project.proj" });
|
||||
errdefer allocator.free(allocator);
|
||||
|
||||
app.project.save_location = save_location;
|
||||
app.project.sample_rate = 5000;
|
||||
|
||||
// try app.appendChannelFromDevice("Dev1/ai0");
|
||||
// try app.appendChannelFromDevice("Dev3/ao0");
|
||||
// try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_I.bin");
|
||||
// try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_IjStim.bin");
|
||||
// try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_IjStim.bin");
|
||||
// try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_IjStim.bin");
|
||||
}
|
||||
|
||||
var profiler: ?Profiler = null;
|
||||
@ -205,4 +189,5 @@ pub fn main() !void {
|
||||
test {
|
||||
_ = @import("./ni-daq/root.zig");
|
||||
_ = @import("./range.zig");
|
||||
_ = @import("./graph.zig");
|
||||
}
|
135
src/platform.zig
135
src/platform.zig
@ -1,135 +0,0 @@
|
||||
const std = @import("std");
|
||||
const rl = @import("raylib");
|
||||
const builtin = @import("builtin");
|
||||
const windows_h = @cImport({
|
||||
@cDefine("_WIN32_WINNT", "0x0500");
|
||||
@cInclude("windows.h");
|
||||
});
|
||||
|
||||
const assert = std.debug.assert;
|
||||
const log = std.log.scoped(.platform);
|
||||
|
||||
// Because `windows_h.HWND` has an alignment of 4,
|
||||
// we need to redefined every struct that uses `HWND` if we want to change the alignment of `HWND`.
|
||||
// Ugh... WHYYYYYY
|
||||
const HWND = [*c]align(2) windows_h.struct_HWND__;
|
||||
const OPENFILENAMEW = extern struct {
|
||||
lStructSize: windows_h.DWORD,
|
||||
hwndOwner: HWND,
|
||||
hInstance: windows_h.HINSTANCE,
|
||||
lpstrFilter: windows_h.LPCWSTR,
|
||||
lpstrCustomFilter: windows_h.LPWSTR,
|
||||
nMaxCustFilter: windows_h.DWORD,
|
||||
nFilterIndex: windows_h.DWORD,
|
||||
lpstrFile: windows_h.LPWSTR,
|
||||
nMaxFile: windows_h.DWORD,
|
||||
lpstrFileTitle: windows_h.LPWSTR,
|
||||
nMaxFileTitle: windows_h.DWORD,
|
||||
lpstrInitialDir: windows_h.LPCWSTR,
|
||||
lpstrTitle: windows_h.LPCWSTR,
|
||||
Flags: windows_h.DWORD,
|
||||
nFileOffset: windows_h.WORD,
|
||||
nFileExtension: windows_h.WORD,
|
||||
lpstrDefExt: windows_h.LPCWSTR,
|
||||
lCustData: windows_h.LPARAM,
|
||||
lpfnHook: windows_h.LPOFNHOOKPROC,
|
||||
lpTemplateName: windows_h.LPCWSTR,
|
||||
pvReserved: ?*anyopaque,
|
||||
dwReserved: windows_h.DWORD,
|
||||
FlagsEx: windows_h.DWORD,
|
||||
};
|
||||
extern fn GetOpenFileNameW([*c]OPENFILENAMEW) windows_h.WINBOOL;
|
||||
|
||||
fn printLastWindowsError(function_name: []const u8) void {
|
||||
const err = windows_h.GetLastError();
|
||||
if (err == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var message: [*c]u8 = null;
|
||||
|
||||
// TODO: Use `FormatMessageW`
|
||||
const size = windows_h.FormatMessageA(
|
||||
windows_h.FORMAT_MESSAGE_ALLOCATE_BUFFER | windows_h.FORMAT_MESSAGE_FROM_SYSTEM | windows_h.FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
null,
|
||||
err,
|
||||
windows_h.MAKELANGID(windows_h.LANG_ENGLISH, windows_h.SUBLANG_ENGLISH_US),
|
||||
@ptrCast(&message),
|
||||
0,
|
||||
null
|
||||
);
|
||||
log.err("{s}() failed ({}): {s}", .{ function_name, err, message[0..size] });
|
||||
|
||||
_ = windows_h.LocalFree(message);
|
||||
}
|
||||
|
||||
pub fn toggleConsoleWindow() void {
|
||||
if (builtin.os.tag != .windows) {
|
||||
// TODO: Maybe just toggle outputing or not outputing to terminal on linux?
|
||||
return;
|
||||
}
|
||||
|
||||
var hWnd = windows_h.GetConsoleWindow();
|
||||
if (hWnd == null) {
|
||||
if (windows_h.AllocConsole() == 0) {
|
||||
printLastWindowsError("AllocConsole");
|
||||
return;
|
||||
}
|
||||
|
||||
hWnd = windows_h.GetConsoleWindow();
|
||||
assert(hWnd != null);
|
||||
}
|
||||
|
||||
if (windows_h.IsWindowVisible(hWnd) != 0) {
|
||||
_ = windows_h.ShowWindow(hWnd, windows_h.SW_HIDE);
|
||||
} else {
|
||||
_ = windows_h.ShowWindow(hWnd, windows_h.SW_SHOWNOACTIVATE);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Maybe return the file path instead of an opened file handle?
|
||||
// So the user of this function could do something more interesting.
|
||||
pub fn openFilePicker(allocator: std.mem.Allocator) ![]u8 {
|
||||
if (builtin.os.tag != .windows) {
|
||||
return error.NotSupported;
|
||||
}
|
||||
|
||||
const hWnd: HWND = @alignCast(@ptrCast(rl.getWindowHandle()));
|
||||
assert(hWnd != null);
|
||||
|
||||
var ofn = std.mem.zeroes(OPENFILENAMEW);
|
||||
var filename_w_buffer = std.mem.zeroes([std.os.windows.PATH_MAX_WIDE]u16);
|
||||
|
||||
// Zig doesn't let you have NULL bytes in the middle of a string literal, so...
|
||||
// I guess you are forced to do this kind of string concatenation to insert those NULL bytes
|
||||
const lpstrFilter = "All" ++ .{ 0 } ++ "*" ++ .{ 0 } ++ "Binary" ++ .{ 0 } ++ "*.bin" ++ .{ 0 };
|
||||
|
||||
ofn.lStructSize = @sizeOf(@TypeOf(ofn));
|
||||
ofn.hwndOwner = hWnd;
|
||||
ofn.lpstrFile = &filename_w_buffer;
|
||||
ofn.nMaxFile = filename_w_buffer.len;
|
||||
ofn.lpstrFilter = std.unicode.utf8ToUtf16LeStringLiteral(lpstrFilter);
|
||||
ofn.nFilterIndex = 2;
|
||||
ofn.Flags = windows_h.OFN_PATHMUSTEXIST | windows_h.OFN_FILEMUSTEXIST | windows_h.OFN_EXPLORER | windows_h.OFN_LONGNAMES;
|
||||
|
||||
if (GetOpenFileNameW(&ofn) != windows_h.TRUE) {
|
||||
const err = windows_h.CommDlgExtendedError();
|
||||
if (err == err) {
|
||||
return error.Canceled;
|
||||
}
|
||||
|
||||
log.err("GetOpenFileNameW() failed, erro code: {}", .{ err });
|
||||
return error.GetOpenFileNameW;
|
||||
}
|
||||
|
||||
const filename_len = std.mem.indexOfScalar(u16, &filename_w_buffer, 0).?;
|
||||
const filename_w = filename_w_buffer[0..filename_len];
|
||||
|
||||
return try std.fmt.allocPrint(allocator, "{s}", .{std.fs.path.fmtWtf16LeAsUtf8Lossy(filename_w)});
|
||||
}
|
||||
|
||||
pub fn init() void {
|
||||
if (builtin.os.tag == .windows) {
|
||||
_ = windows_h.SetConsoleOutputCP(65001);
|
||||
}
|
||||
}
|
103
src/platform/root.zig
Normal file
103
src/platform/root.zig
Normal file
@ -0,0 +1,103 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const log = std.log.scoped(.platform);
|
||||
|
||||
const OSPlatform = switch (builtin.os.tag) {
|
||||
.windows => @import("windows.zig"),
|
||||
else => @panic("Platform layer doesn't support OS")
|
||||
};
|
||||
|
||||
var impl: OSPlatform = undefined;
|
||||
|
||||
pub const OpenFileOptions = struct {
|
||||
pub const Filter = struct {
|
||||
const Name = std.BoundedArray(u8, 32);
|
||||
const Format = std.BoundedArray(u8, 32);
|
||||
|
||||
name: Name = .{},
|
||||
format: Format = .{}
|
||||
};
|
||||
|
||||
pub const Style = enum { open, save };
|
||||
|
||||
filters: std.BoundedArray(Filter, 4) = .{},
|
||||
default_filter: ?usize = null,
|
||||
file_must_exist: bool = false,
|
||||
prompt_creation: bool = false,
|
||||
prompt_overwrite: bool = false,
|
||||
style: Style = .open,
|
||||
|
||||
pub fn appendFilter(self: *OpenFileOptions, name: []const u8, format: []const u8) !void {
|
||||
try self.filters.append(Filter{
|
||||
.name = try Filter.Name.fromSlice(name),
|
||||
.format = try Filter.Format.fromSlice(format)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
pub const FilePickerId = usize;
|
||||
pub const FilePickerStatus = enum {
|
||||
in_progress,
|
||||
success,
|
||||
canceled,
|
||||
failure
|
||||
};
|
||||
|
||||
pub fn toggleConsoleWindow() void {
|
||||
impl.toggleConsoleWindow();
|
||||
}
|
||||
|
||||
pub fn spawnFilePicker(opts: *const OpenFileOptions) !FilePickerId {
|
||||
return impl.spawnFilePicker(opts);
|
||||
}
|
||||
|
||||
pub fn isFilePickerOpen() bool {
|
||||
return impl.isFilePickerOpen();
|
||||
}
|
||||
|
||||
pub fn checkFilePicker(id: FilePickerId) ?FilePickerStatus {
|
||||
return impl.checkFilePicker(id);
|
||||
}
|
||||
|
||||
pub fn getFilePickerPath(allocator: std.mem.Allocator, id: FilePickerId) ![]u8 {
|
||||
return impl.getFilePickerPath(allocator, id);
|
||||
}
|
||||
|
||||
pub fn waitUntilFilePickerDone(allocator: std.mem.Allocator, optional_id: *?FilePickerId) ?[]u8 {
|
||||
const id = optional_id.* orelse return null;
|
||||
|
||||
const status = checkFilePicker(id) orelse {
|
||||
optional_id.* = null;
|
||||
return null;
|
||||
};
|
||||
|
||||
if (status == .in_progress) {
|
||||
return null;
|
||||
}
|
||||
|
||||
defer optional_id.* = null;
|
||||
|
||||
if (status == .success) {
|
||||
if (getFilePickerPath(allocator, id)) |path| {
|
||||
return path;
|
||||
} else |e| {
|
||||
log.err("getFilePickerPath(): {}", .{ e });
|
||||
}
|
||||
} else if (status == .canceled) {
|
||||
// Ignore this
|
||||
} else if (status == .failure) {
|
||||
log.err("Unknown error occured while file picker was open", .{ });
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) void {
|
||||
impl.init(allocator);
|
||||
}
|
||||
|
||||
pub fn deinit(allocator: std.mem.Allocator) void {
|
||||
impl.deinit(allocator);
|
||||
}
|
303
src/platform/windows.zig
Normal file
303
src/platform/windows.zig
Normal file
@ -0,0 +1,303 @@
|
||||
const std = @import("std");
|
||||
const rl = @import("raylib");
|
||||
const Platform = @import("root.zig");
|
||||
const windows_h = @cImport({
|
||||
@cDefine("_WIN32_WINNT", "0x0500");
|
||||
@cInclude("windows.h");
|
||||
@cInclude("shobjidl.h");
|
||||
});
|
||||
|
||||
const assert = std.debug.assert;
|
||||
const log = std.log.scoped(.platform);
|
||||
|
||||
const OpenFileOptions = Platform.OpenFileOptions;
|
||||
const FilePickerId = Platform.FilePickerId;
|
||||
const FilePickerStatus = Platform.FilePickerStatus;
|
||||
|
||||
const Self = @This();
|
||||
|
||||
// Because `windows_h.HWND` has an alignment of 4,
|
||||
// we need to redefined every struct that uses `HWND` if we want to change the alignment of `HWND`.
|
||||
// Ugh... WHYYYYYY
|
||||
const HWND = [*c]align(2) windows_h.struct_HWND__;
|
||||
const OPENFILENAMEW = extern struct {
|
||||
lStructSize: windows_h.DWORD,
|
||||
hwndOwner: HWND,
|
||||
hInstance: windows_h.HINSTANCE,
|
||||
lpstrFilter: windows_h.LPCWSTR,
|
||||
lpstrCustomFilter: windows_h.LPWSTR,
|
||||
nMaxCustFilter: windows_h.DWORD,
|
||||
nFilterIndex: windows_h.DWORD,
|
||||
lpstrFile: windows_h.LPWSTR,
|
||||
nMaxFile: windows_h.DWORD,
|
||||
lpstrFileTitle: windows_h.LPWSTR,
|
||||
nMaxFileTitle: windows_h.DWORD,
|
||||
lpstrInitialDir: windows_h.LPCWSTR,
|
||||
lpstrTitle: windows_h.LPCWSTR,
|
||||
Flags: windows_h.DWORD,
|
||||
nFileOffset: windows_h.WORD,
|
||||
nFileExtension: windows_h.WORD,
|
||||
lpstrDefExt: windows_h.LPCWSTR,
|
||||
lCustData: windows_h.LPARAM,
|
||||
lpfnHook: windows_h.LPOFNHOOKPROC,
|
||||
lpTemplateName: windows_h.LPCWSTR,
|
||||
pvReserved: ?*anyopaque,
|
||||
dwReserved: windows_h.DWORD,
|
||||
FlagsEx: windows_h.DWORD,
|
||||
};
|
||||
extern fn GetOpenFileNameW([*c]OPENFILENAMEW) windows_h.WINBOOL;
|
||||
extern fn GetSaveFileNameW([*c]OPENFILENAMEW) windows_h.WINBOOL;
|
||||
|
||||
const OpenedFilePicker = struct {
|
||||
style: OpenFileOptions.Style,
|
||||
filename_w_buffer: [std.os.windows.PATH_MAX_WIDE]u16,
|
||||
lpstrFilter_utf16: [:0]u16,
|
||||
ofn: OPENFILENAMEW,
|
||||
thread: std.Thread,
|
||||
status: Platform.FilePickerStatus,
|
||||
|
||||
fn spawn(self: *OpenedFilePicker, windows_platform: *Self, opts: *const OpenFileOptions) !void {
|
||||
const allocator = windows_platform.allocator;
|
||||
|
||||
self.* = OpenedFilePicker{
|
||||
.ofn = std.mem.zeroes(OPENFILENAMEW),
|
||||
.style = opts.style,
|
||||
.filename_w_buffer = undefined,
|
||||
.lpstrFilter_utf16 = undefined,
|
||||
.thread = undefined,
|
||||
.status = Platform.FilePickerStatus.in_progress
|
||||
};
|
||||
|
||||
const hWnd: HWND = @alignCast(@ptrCast(rl.getWindowHandle()));
|
||||
assert(hWnd != null);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
self.thread = try std.Thread.spawn(.{}, threadCallback, .{ self });
|
||||
}
|
||||
|
||||
fn deinit(self: *OpenedFilePicker, allocator: std.mem.Allocator) void {
|
||||
self.thread.join();
|
||||
allocator.free(self.lpstrFilter_utf16);
|
||||
}
|
||||
|
||||
fn showDialog(self: *OpenedFilePicker) !void {
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn threadCallback(self: *OpenedFilePicker) void {
|
||||
self.showDialog() catch |e| {
|
||||
switch (e) {
|
||||
error.Canceled => {
|
||||
self.status = .canceled;
|
||||
return;
|
||||
},
|
||||
else => {
|
||||
log.err("Windows file picker error: {}", .{ e });
|
||||
self.status = .failure;
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.status = .success;
|
||||
}
|
||||
};
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
opened_file_pickers: [8]?OpenedFilePicker = [1]?OpenedFilePicker{ null } ** 8,
|
||||
|
||||
fn printLastWindowsError(function_name: []const u8) void {
|
||||
const err = windows_h.GetLastError();
|
||||
if (err == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var message: [*c]u8 = null;
|
||||
|
||||
// TODO: Use `FormatMessageW`
|
||||
const size = windows_h.FormatMessageA(
|
||||
windows_h.FORMAT_MESSAGE_ALLOCATE_BUFFER | windows_h.FORMAT_MESSAGE_FROM_SYSTEM | windows_h.FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
null,
|
||||
err,
|
||||
windows_h.MAKELANGID(windows_h.LANG_ENGLISH, windows_h.SUBLANG_ENGLISH_US),
|
||||
@ptrCast(&message),
|
||||
0,
|
||||
null
|
||||
);
|
||||
log.err("{s}() failed ({}): {s}", .{ function_name, err, message[0..size] });
|
||||
|
||||
_ = windows_h.LocalFree(message);
|
||||
}
|
||||
|
||||
pub fn toggleConsoleWindow(self: *Self) void {
|
||||
_ = self;
|
||||
|
||||
var hWnd = windows_h.GetConsoleWindow();
|
||||
if (hWnd == null) {
|
||||
if (windows_h.AllocConsole() == 0) {
|
||||
printLastWindowsError("AllocConsole");
|
||||
return;
|
||||
}
|
||||
|
||||
hWnd = windows_h.GetConsoleWindow();
|
||||
assert(hWnd != null);
|
||||
}
|
||||
|
||||
if (windows_h.IsWindowVisible(hWnd) != 0) {
|
||||
_ = windows_h.ShowWindow(hWnd, windows_h.SW_HIDE);
|
||||
} else {
|
||||
_ = windows_h.ShowWindow(hWnd, windows_h.SW_SHOWNOACTIVATE);
|
||||
}
|
||||
}
|
||||
|
||||
fn getFreeOpenedFilePicker(self: *Self) ?FilePickerId {
|
||||
for (0.., &self.opened_file_pickers) |i, *opened_file_picker| {
|
||||
if (opened_file_picker.* == null) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
for (0.., &self.opened_file_pickers) |i, *opened_file_picker| {
|
||||
if (opened_file_picker.*) |*file_picker| {
|
||||
if (file_picker.status != .in_progress) {
|
||||
file_picker.deinit(self.allocator);
|
||||
opened_file_picker.* = null;
|
||||
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn spawnFilePicker(self: *Self, opts: *const OpenFileOptions) !FilePickerId {
|
||||
const file_picker_id = self.getFreeOpenedFilePicker() orelse return error.MaxFilePickersReached;
|
||||
|
||||
const opened_file_picker = &self.opened_file_pickers[file_picker_id];
|
||||
opened_file_picker.* = undefined;
|
||||
|
||||
try OpenedFilePicker.spawn(&(opened_file_picker.*.?), self, opts);
|
||||
errdefer opened_file_picker.* = null;
|
||||
|
||||
return file_picker_id;
|
||||
}
|
||||
|
||||
pub fn checkFilePicker(self: *Self, id: FilePickerId) ?FilePickerStatus {
|
||||
const file_picker = &(self.opened_file_pickers[id] orelse return null);
|
||||
return file_picker.status;
|
||||
}
|
||||
|
||||
pub fn getFilePickerPath(self: *Self, allocator: std.mem.Allocator, id: FilePickerId) ![]u8 {
|
||||
const file_picker = &(self.opened_file_pickers[id] orelse return error.FilePickerNotFound);
|
||||
|
||||
const filename_w_buffer = file_picker.filename_w_buffer;
|
||||
|
||||
const filename_len = std.mem.indexOfScalar(u16, &filename_w_buffer, 0).?;
|
||||
const filename_w = filename_w_buffer[0..filename_len];
|
||||
|
||||
return try std.fmt.allocPrint(allocator, "{s}", .{std.fs.path.fmtWtf16LeAsUtf8Lossy(filename_w)});
|
||||
}
|
||||
|
||||
pub fn isFilePickerOpen(self: *Self) bool {
|
||||
for (&self.opened_file_pickers) |*maybe_file_picker| {
|
||||
const file_picker = &(maybe_file_picker.* orelse continue);
|
||||
|
||||
if (file_picker.status == .in_progress) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn init(self: *Self, allocator: std.mem.Allocator) void {
|
||||
_ = windows_h.SetConsoleOutputCP(65001);
|
||||
|
||||
self.* = Self{
|
||||
.allocator = allocator
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self, allocator: std.mem.Allocator) void {
|
||||
for (&self.opened_file_pickers) |*maybe_opened_file_picker| {
|
||||
if (maybe_opened_file_picker.*) |*opened_file_picker| {
|
||||
opened_file_picker.deinit(allocator);
|
||||
maybe_opened_file_picker.* = null;
|
||||
}
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ const rl = @import("raylib");
|
||||
const UI = @import("../ui.zig");
|
||||
const App = @import("../app.zig");
|
||||
const srcery = @import("../srcery.zig");
|
||||
const Platform = @import("../platform.zig");
|
||||
const Platform = @import("../platform/root.zig");
|
||||
const RangeF64 = @import("../range.zig").RangeF64;
|
||||
const Graph = @import("../graph.zig");
|
||||
const Assets = @import("../assets.zig");
|
||||
@ -35,6 +35,10 @@ preview_samples_y_range: RangeF64 = RangeF64.init(0, 0),
|
||||
sample_rate_input: UI.TextInputStorage,
|
||||
parsed_sample_rate: ?f64 = null,
|
||||
|
||||
// View settings
|
||||
channel_save_file_picker: ?Platform.FilePickerId = null,
|
||||
file_save_file_picker: ?Platform.FilePickerId = null,
|
||||
|
||||
pub fn init(app: *App) !MainScreen {
|
||||
const allocator = app.allocator;
|
||||
|
||||
@ -239,86 +243,11 @@ fn clearProtocolErrorMessage(self: *MainScreen) void {
|
||||
self.protocol_error_message = null;
|
||||
}
|
||||
|
||||
pub fn showSidePanel(self: *MainScreen) !void {
|
||||
fn showProjectSettings(self: *MainScreen) !void {
|
||||
var ui = &self.app.ui;
|
||||
const frame_allocator = ui.frameAllocator();
|
||||
|
||||
const container = ui.createBox(.{
|
||||
.size_x = UI.Sizing.initGrowUpTo(.{ .parent_percent = 0.2 }),
|
||||
.size_y = UI.Sizing.initGrowFull(),
|
||||
.borders = .{
|
||||
.right = .{ .color = srcery.hard_black, .size = 4 }
|
||||
},
|
||||
.layout_direction = .top_to_bottom,
|
||||
.padding = UI.Padding.all(ui.rem(1)),
|
||||
.layout_gap = ui.rem(0.2)
|
||||
});
|
||||
container.beginChildren();
|
||||
defer container.endChildren();
|
||||
|
||||
const project = &self.app.project;
|
||||
const sample_rate = project.getSampleRate();
|
||||
|
||||
if (self.view_controls.view_settings) |view_id| {
|
||||
const view = project.views.get(view_id) orelse return;
|
||||
|
||||
{
|
||||
const label = ui.label("Settings", .{});
|
||||
label.borders.bottom = .{
|
||||
.color = srcery.bright_white,
|
||||
.size = 1
|
||||
};
|
||||
}
|
||||
|
||||
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) });
|
||||
|
||||
_ = ui.checkbox(.{
|
||||
.value = &view.sync_controls,
|
||||
.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 });
|
||||
|
||||
if (channel_type != null) {
|
||||
_ = ui.label("Type: {s}", .{ channel_type.?.name() });
|
||||
} else {
|
||||
_ = ui.label("Type: unknown", .{ });
|
||||
}
|
||||
|
||||
sample_count = samples.len;
|
||||
},
|
||||
.file => |file_id| {
|
||||
const file = project.files.get(file_id).?;
|
||||
|
||||
if (file.samples) |samples| {
|
||||
sample_count = samples.len;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (sample_count != null) {
|
||||
_ = ui.label("Samples: {d}", .{ sample_count.? });
|
||||
|
||||
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("Duration: {s}", .{ duration_str });
|
||||
}
|
||||
|
||||
} else {
|
||||
{
|
||||
const label = ui.label("Project", .{});
|
||||
label.borders.bottom = .{
|
||||
@ -364,6 +293,259 @@ pub fn showSidePanel(self: *MainScreen) !void {
|
||||
.value = &project.show_rulers,
|
||||
.label = "Ruler"
|
||||
});
|
||||
}
|
||||
|
||||
fn showViewSettings(self: *MainScreen, view_id: Id) !void {
|
||||
var ui = &self.app.ui;
|
||||
|
||||
const project = &self.app.project;
|
||||
const sample_rate = project.getSampleRate();
|
||||
const view = project.views.get(view_id) orelse return;
|
||||
|
||||
{
|
||||
const label = ui.label("Settings", .{});
|
||||
label.borders.bottom = .{
|
||||
.color = srcery.bright_white,
|
||||
.size = 1
|
||||
};
|
||||
|
||||
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) });
|
||||
}
|
||||
|
||||
_ = ui.checkbox(.{
|
||||
.value = &view.sync_controls,
|
||||
.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 });
|
||||
|
||||
if (channel_type != null) {
|
||||
_ = ui.label("Type: {s}", .{ channel_type.?.name() });
|
||||
} 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,
|
||||
.file_picker = &self.file_save_file_picker,
|
||||
.path = file.path
|
||||
})) |path| {
|
||||
self.app.allocator.free(file.path);
|
||||
file.path = path;
|
||||
self.app.pushCommand(.{ .reload_file = file_id });
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (sample_count != null) {
|
||||
_ = ui.label("Samples: {d}", .{ sample_count.? });
|
||||
|
||||
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("Duration: {s}", .{ duration_str });
|
||||
}
|
||||
}
|
||||
|
||||
fn showMarkedRange(self: *MainScreen, view_id: Id, index: usize) void {
|
||||
var ui = &self.app.ui;
|
||||
|
||||
const view = self.app.getView(view_id) orelse return;
|
||||
|
||||
const marked_range = view.marked_ranges.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 (marked_range.axis == .X and sample_rate != null) {
|
||||
_ = ui.label("From: {d:.3}s", .{ marked_range.range.lower / sample_rate.? });
|
||||
_ = ui.label("To: {d:.3}s", .{ marked_range.range.upper / sample_rate.? });
|
||||
_ = ui.label("Size: {d:.3}s", .{ marked_range.range.size() / sample_rate.? });
|
||||
} else {
|
||||
_ = ui.label("From: {d:.2}", .{ marked_range.range.lower });
|
||||
_ = ui.label("To: {d:.2}", .{ marked_range.range.upper });
|
||||
_ = ui.label("Size: {d:.2}", .{ marked_range.range.size() });
|
||||
}
|
||||
|
||||
if (marked_range.axis == .X) {
|
||||
if (marked_range.min) |min| {
|
||||
_ = ui.label("Minimum: {d:.3}", .{ min });
|
||||
} else{
|
||||
_ = ui.label("Minimum: <unknown>", .{});
|
||||
}
|
||||
|
||||
if (marked_range.max) |max| {
|
||||
_ = ui.label("Maximum: {d:.3}", .{ max });
|
||||
} else{
|
||||
_ = ui.label("Maximum: <unknown>", .{});
|
||||
}
|
||||
|
||||
if (marked_range.average) |average| {
|
||||
_ = ui.label("Average: {d:.3}", .{ average });
|
||||
} else{
|
||||
_ = ui.label("Average: <unknown>", .{});
|
||||
}
|
||||
|
||||
if (marked_range.standard_deviation) |standard_deviation| {
|
||||
_ = ui.label("Standard deviation: {d:.3}", .{ standard_deviation });
|
||||
} else{
|
||||
_ = ui.label("Standard deviation: <unknown>", .{});
|
||||
}
|
||||
}
|
||||
|
||||
_ = ui.createBox(.{ .size_y = UI.Sizing.initGrowFull() });
|
||||
|
||||
{
|
||||
const btn = ui.textButton("Remove");
|
||||
btn.borders = UI.Borders.all(.{ .color = srcery.red, .size = 4 });
|
||||
const signal = ui.signal(btn);
|
||||
if (signal.clicked() or ui.isKeyboardPressed(.key_backspace)) {
|
||||
self.view_controls.show_marked_range = null;
|
||||
_ = view.marked_ranges.swapRemove(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn showToolbar(self: *MainScreen) void {
|
||||
var ui = &self.app.ui;
|
||||
|
||||
const toolbar = ui.createBox(.{
|
||||
.background = srcery.black,
|
||||
.layout_direction = .left_to_right,
|
||||
.size_x = .{ .fixed = .{ .parent_percent = 1 } },
|
||||
.size_y = .{ .fixed = .{ .font_size = 2 } },
|
||||
.borders = .{
|
||||
.bottom = .{ .color = srcery.hard_black, .size = 4 }
|
||||
}
|
||||
});
|
||||
toolbar.beginChildren();
|
||||
defer toolbar.endChildren();
|
||||
|
||||
{
|
||||
var btn = ui.textButton("Start/Stop button");
|
||||
btn.borders = UI.Borders.all(.{ .size = 4, .color = srcery.red });
|
||||
btn.background = srcery.black;
|
||||
btn.size.y = UI.Sizing.initFixed(.{ .parent_percent = 1 });
|
||||
btn.padding.top = 0;
|
||||
btn.padding.bottom = 0;
|
||||
if (ui.signal(btn).clicked()) {
|
||||
if (self.app.isCollectionInProgress()) {
|
||||
self.app.pushCommand(.stop_collection);
|
||||
} else {
|
||||
self.app.pushCommand(.start_collection);
|
||||
}
|
||||
}
|
||||
|
||||
if (self.app.isCollectionInProgress()) {
|
||||
btn.setText("Stop");
|
||||
} else {
|
||||
btn.setText("Start");
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
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)) });
|
||||
|
||||
{
|
||||
var btn = ui.textButton("Move");
|
||||
if (self.view_controls.selected_tool == .move) {
|
||||
btn.borders = UI.Borders.bottom(.{ .size = 4, .color = srcery.green });
|
||||
}
|
||||
|
||||
if (ui.signal(btn).clicked() or ui.isKeyboardPressed(.key_one)) {
|
||||
self.view_controls.selected_tool = .move;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var btn = ui.textButton("Select");
|
||||
if (self.view_controls.selected_tool == .select) {
|
||||
btn.borders = UI.Borders.bottom(.{ .size = 4, .color = srcery.green });
|
||||
}
|
||||
|
||||
if (ui.signal(btn).clicked() or ui.isKeyboardPressed(.key_two)) {
|
||||
self.view_controls.selected_tool = .select;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn showSidePanel(self: *MainScreen) !void {
|
||||
var ui = &self.app.ui;
|
||||
|
||||
const container = ui.createBox(.{
|
||||
.size_x = UI.Sizing.initFitChildren(),
|
||||
.size_y = UI.Sizing.initGrowFull(),
|
||||
.borders = .{
|
||||
.right = .{ .color = srcery.hard_black, .size = 4 }
|
||||
},
|
||||
.layout_direction = .top_to_bottom,
|
||||
.padding = UI.Padding.all(ui.rem(1)),
|
||||
.layout_gap = ui.rem(0.2)
|
||||
});
|
||||
container.beginChildren();
|
||||
defer container.endChildren();
|
||||
|
||||
_ = ui.createBox(.{ .size_x = UI.Sizing.initFixedPixels(ui.rem(12)) });
|
||||
|
||||
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 {
|
||||
try self.showProjectSettings();
|
||||
}
|
||||
}
|
||||
|
||||
@ -409,49 +591,7 @@ pub fn tick(self: *MainScreen) !void {
|
||||
maybe_modal_overlay = modal_overlay;
|
||||
}
|
||||
|
||||
{
|
||||
const toolbar = ui.createBox(.{
|
||||
.background = srcery.black,
|
||||
.layout_direction = .left_to_right,
|
||||
.size_x = .{ .fixed = .{ .parent_percent = 1 } },
|
||||
.size_y = .{ .fixed = .{ .font_size = 2 } },
|
||||
.borders = .{
|
||||
.bottom = .{ .color = srcery.hard_black, .size = 4 }
|
||||
}
|
||||
});
|
||||
toolbar.beginChildren();
|
||||
defer toolbar.endChildren();
|
||||
|
||||
{
|
||||
var btn = ui.textButton("Start/Stop button");
|
||||
btn.borders = UI.Borders.all(.{ .size = 4, .color = srcery.red });
|
||||
btn.background = srcery.black;
|
||||
btn.size.y = UI.Sizing.initFixed(.{ .parent_percent = 1 });
|
||||
btn.padding.top = 0;
|
||||
btn.padding.bottom = 0;
|
||||
if (ui.signal(btn).clicked()) {
|
||||
if (self.app.isCollectionInProgress()) {
|
||||
self.app.pushCommand(.stop_collection);
|
||||
} else {
|
||||
self.app.pushCommand(.start_collection);
|
||||
}
|
||||
}
|
||||
|
||||
if (self.app.isCollectionInProgress()) {
|
||||
btn.setText("Stop");
|
||||
} else {
|
||||
btn.setText("Start");
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.showToolbar();
|
||||
|
||||
const ui_view_ctx = UIView.Context{
|
||||
.app = self.app,
|
||||
@ -521,6 +661,8 @@ pub fn tick(self: *MainScreen) !void {
|
||||
self.view_controls.view_fullscreen = 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 {
|
||||
self.app.should_close = true;
|
||||
}
|
||||
|
120
src/ui.zig
120
src/ui.zig
@ -6,6 +6,7 @@ const rect_utils = @import("./rect-utils.zig");
|
||||
const utils = @import("./utils.zig");
|
||||
const builtin = @import("builtin");
|
||||
const FontFace = @import("./font-face.zig");
|
||||
const Platform = @import("./platform/root.zig");
|
||||
const raylib_h = @cImport({
|
||||
@cInclude("stdio.h");
|
||||
@cInclude("raylib.h");
|
||||
@ -22,13 +23,22 @@ const Vec2Zero = Vec2{ .x = 0, .y = 0 };
|
||||
const UI = @This();
|
||||
|
||||
const max_boxes = 512;
|
||||
const max_events = 1024;
|
||||
const max_events = 256;
|
||||
const draw_debug = false; //builtin.mode == .Debug;
|
||||
const default_font = Assets.FontId{ .variant = .regular, .size = 16 };
|
||||
|
||||
pub const Key = struct {
|
||||
const StringHasher = std.hash.XxHash3;
|
||||
pub const CombineHasher = std.hash.Fnv1a_64;
|
||||
|
||||
hash: u64 = 0,
|
||||
|
||||
pub fn init(hash: u64) Key {
|
||||
return Key{
|
||||
.hash = hash
|
||||
};
|
||||
}
|
||||
|
||||
pub fn initPtr(ptr: anytype) Key {
|
||||
return Key.initUsize(@intFromPtr(ptr));
|
||||
}
|
||||
@ -41,7 +51,17 @@ pub const Key = struct {
|
||||
|
||||
pub fn initString(seed: u64, text: []const u8) Key {
|
||||
return Key{
|
||||
.hash = std.hash.XxHash3.hash(seed, text)
|
||||
.hash = StringHasher.hash(seed, text)
|
||||
};
|
||||
}
|
||||
|
||||
pub fn combine(self: Key, other: Key) Key {
|
||||
var hasher = CombineHasher.init();
|
||||
hasher.update(std.mem.asBytes(&self.hash));
|
||||
hasher.update(std.mem.asBytes(&other.hash));
|
||||
|
||||
return Key{
|
||||
.hash = hasher.final()
|
||||
};
|
||||
}
|
||||
|
||||
@ -516,6 +536,8 @@ pub const Box = struct {
|
||||
visual_hot: bool = false,
|
||||
visual_active: bool = false,
|
||||
tooltip: ?[]const u8 = null,
|
||||
float_x: ?f32 = null,
|
||||
float_y: ?f32 = null,
|
||||
|
||||
// Variables that you probably shouldn't be touching
|
||||
last_used_frame: u64 = 0,
|
||||
@ -589,13 +611,11 @@ pub const Box = struct {
|
||||
}
|
||||
|
||||
pub fn setFloatX(self: *Box, x: f32) void {
|
||||
self.persistent.position.x = x;
|
||||
self.flags.insert(.float_x);
|
||||
self.float_x = x;
|
||||
}
|
||||
|
||||
pub fn setFloatY(self: *Box, y: f32) void {
|
||||
self.persistent.position.y = y;
|
||||
self.flags.insert(.float_y);
|
||||
self.float_y = y;
|
||||
}
|
||||
|
||||
pub fn setFloatRect(self: *Box, float_rect: Rect) void {
|
||||
@ -639,8 +659,8 @@ pub const Box = struct {
|
||||
|
||||
fn isFloating(self: *const Box, axis: Axis) bool {
|
||||
return switch (axis) {
|
||||
.X => self.flags.contains(.float_x),
|
||||
.Y => self.flags.contains(.float_y),
|
||||
.X => self.float_x != null,
|
||||
.Y => self.float_y != null,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1016,11 +1036,11 @@ pub fn end(self: *UI) void {
|
||||
// Reset sizes and positions, because it will be recalculated in layout pass
|
||||
for (self.boxes.slice()) |*box| {
|
||||
var position = Vec2{ .x = 0, .y = 0 };
|
||||
if (box.isFloating(.X)) {
|
||||
position.x = box.persistent.position.x;
|
||||
if (box.float_x) |x| {
|
||||
position.x = x;
|
||||
}
|
||||
if (box.isFloating(.Y)) {
|
||||
position.y = box.persistent.position.y;
|
||||
if (box.float_y) |y| {
|
||||
position.y = y;
|
||||
}
|
||||
box.persistent.size = Vec2Zero;
|
||||
box.persistent.position = position;
|
||||
@ -2272,6 +2292,14 @@ pub const CheckboxOptions = struct {
|
||||
label: ?[]const u8 = null
|
||||
};
|
||||
|
||||
pub const FileInputOptions = struct {
|
||||
key: Key,
|
||||
allocator: std.mem.Allocator,
|
||||
file_picker: *?Platform.FilePickerId,
|
||||
open_dialog: bool = true,
|
||||
path: ?[]const u8 = null
|
||||
};
|
||||
|
||||
pub fn mouseTooltip(self: *UI) *Box {
|
||||
const tooltip = self.getBoxByKey(mouse_tooltip_box_key).?;
|
||||
|
||||
@ -2305,7 +2333,8 @@ pub fn textButton(self: *UI, text: []const u8) *Box {
|
||||
pub fn label(self: *UI, comptime fmt: []const u8, args: anytype) *Box {
|
||||
const box = self.createBox(.{
|
||||
.size_x = Sizing.initFixed(.text),
|
||||
.size_y = Sizing.initFixed(.text)
|
||||
.size_y = Sizing.initFixed(.text),
|
||||
.flags = &.{ .wrap_text }
|
||||
});
|
||||
|
||||
box.setFmtText(fmt, args);
|
||||
@ -2766,3 +2795,68 @@ pub fn checkbox(self: *UI, opts: CheckboxOptions) void {
|
||||
opts.value.* = !opts.value.*;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fileInput(self: *UI, opts: FileInputOptions) ?[]u8 {
|
||||
var result: ?[]u8 = null;
|
||||
|
||||
const container = self.createBox(.{
|
||||
.key = opts.key,
|
||||
.size_x = Sizing.initGrowUpTo(.{ .pixels = 200 }),
|
||||
.size_y = Sizing.initFixed(Unit.initPixels(self.rem(1))),
|
||||
.flags = &.{ .clickable, .clip_view, .draw_hot, .draw_active },
|
||||
.background = srcery.bright_white,
|
||||
.align_y = .center,
|
||||
.padding = UI.Padding.all(self.rem(0.25)),
|
||||
.hot_cursor = .mouse_cursor_pointing_hand,
|
||||
.layout_gap = self.rem(0.5)
|
||||
});
|
||||
container.beginChildren();
|
||||
defer container.endChildren();
|
||||
|
||||
_ = self.createBox(.{
|
||||
.texture = Assets.file,
|
||||
.size_x = UI.Sizing.initFixed(.{ .pixels = 16 }),
|
||||
.size_y = UI.Sizing.initFixed(.{ .pixels = 16 })
|
||||
});
|
||||
|
||||
const path_box = self.createBox(.{
|
||||
.size_x = UI.Sizing.initGrowFull(),
|
||||
.size_y = UI.Sizing.initGrowFull(),
|
||||
.text_color = srcery.black
|
||||
});
|
||||
|
||||
if (opts.path) |path| {
|
||||
path_box.setText(std.fs.path.basename(path));
|
||||
container.tooltip = path;
|
||||
} else {
|
||||
path_box.setText("<none>");
|
||||
}
|
||||
|
||||
if (opts.file_picker.* != null) {
|
||||
if (Platform.waitUntilFilePickerDone(opts.allocator, opts.file_picker)) |path| {
|
||||
result = path;
|
||||
}
|
||||
} else {
|
||||
const container_signal = self.signal(container);
|
||||
if (container_signal.clicked()) {
|
||||
var file_open_options: Platform.OpenFileOptions = .{};
|
||||
if (opts.open_dialog) {
|
||||
file_open_options.style = .open;
|
||||
file_open_options.file_must_exist = true;
|
||||
} else {
|
||||
file_open_options.style = .save;
|
||||
file_open_options.prompt_overwrite = true;
|
||||
}
|
||||
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;
|
||||
} else |e| {
|
||||
log.err("Failed to open file picker: {}", .{e});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
@ -39,7 +39,9 @@ pub fn shiftColorInHSV(color: rl.Color, value_shift: f32) rl.Color {
|
||||
|
||||
var hsv = rl.colorToHSV(color);
|
||||
hsv.z = std.math.clamp(hsv.z * (1 + value_shift), 0, 1);
|
||||
return rl.colorFromHSV(hsv.x, hsv.y, hsv.z);
|
||||
var new_color = rl.colorFromHSV(hsv.x, hsv.y, hsv.z);
|
||||
new_color.a = color.a;
|
||||
return new_color;
|
||||
}
|
||||
|
||||
pub fn drawUnderline(rect: rl.Rectangle, size: f32, color: rl.Color) void {
|
||||
@ -74,7 +76,7 @@ pub fn initBoundedStringZ(comptime BoundedString: type, text: []const u8) !Bound
|
||||
return bounded_string;
|
||||
}
|
||||
|
||||
pub fn getBoundedStringZ(bounded_array: anytype) [:0]const u8 {
|
||||
pub fn getBoundedStringZ(bounded_array: anytype) [:0]u8 {
|
||||
return bounded_array.buffer[0..(bounded_array.len-1) :0];
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user