add saving samples to a file
This commit is contained in:
parent
a2fc2befd1
commit
da7a580e55
149
src/app.zig
149
src/app.zig
@ -8,7 +8,7 @@ const Graph = @import("./graph.zig");
|
|||||||
const UI = @import("./ui.zig");
|
const UI = @import("./ui.zig");
|
||||||
const MainScreen = @import("./screens/main_screen.zig");
|
const MainScreen = @import("./screens/main_screen.zig");
|
||||||
const ChannelFromDeviceScreen = @import("./screens/channel_from_device.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 constants = @import("./constants.zig");
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
@ -168,6 +168,7 @@ pub const Channel = struct {
|
|||||||
|
|
||||||
// Persistent
|
// Persistent
|
||||||
name: Name = .{},
|
name: Name = .{},
|
||||||
|
saved_collected_samples: ?[]u8 = null,
|
||||||
|
|
||||||
// Runtime
|
// Runtime
|
||||||
device: Device = .{},
|
device: Device = .{},
|
||||||
@ -178,10 +179,22 @@ pub const Channel = struct {
|
|||||||
write_pattern: std.ArrayListUnmanaged(f64) = .{},
|
write_pattern: std.ArrayListUnmanaged(f64) = .{},
|
||||||
output_task: ?NIDaq.Task = null,
|
output_task: ?NIDaq.Task = null,
|
||||||
graph_min_max_cache: Graph.MinMaxCache = .{},
|
graph_min_max_cache: Graph.MinMaxCache = .{},
|
||||||
|
last_sample_save_at: ?i128 = null,
|
||||||
|
|
||||||
pub fn deinit(self: *Channel, allocator: Allocator) void {
|
pub fn deinit(self: *Channel, allocator: Allocator) void {
|
||||||
self.clear(allocator);
|
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.collected_samples.clearAndFree(allocator);
|
||||||
self.write_pattern.clearAndFree(allocator);
|
self.write_pattern.clearAndFree(allocator);
|
||||||
if (self.output_task) |task| {
|
if (self.output_task) |task| {
|
||||||
@ -192,12 +205,6 @@ pub const Channel = struct {
|
|||||||
self.graph_min_max_cache.deinit(allocator);
|
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(
|
pub fn generateSine(
|
||||||
samples: *std.ArrayListUnmanaged(f64),
|
samples: *std.ArrayListUnmanaged(f64),
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
@ -217,6 +224,24 @@ pub const Channel = struct {
|
|||||||
samples.appendAssumeCapacity(sample);
|
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;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const File = struct {
|
pub const File = struct {
|
||||||
@ -254,6 +279,7 @@ pub const View = struct {
|
|||||||
// Persistent
|
// Persistent
|
||||||
reference: Reference,
|
reference: Reference,
|
||||||
height: f32 = 300,
|
height: f32 = 300,
|
||||||
|
// TODO: Implement different styles of following: Look ahead, sliding, sliding window
|
||||||
follow: bool = false,
|
follow: bool = false,
|
||||||
graph_opts: Graph.ViewOptions = .{},
|
graph_opts: Graph.ViewOptions = .{},
|
||||||
sync_controls: bool = false,
|
sync_controls: bool = false,
|
||||||
@ -291,6 +317,8 @@ pub const Project = struct {
|
|||||||
|
|
||||||
save_location: ?[]u8 = null,
|
save_location: ?[]u8 = null,
|
||||||
sample_rate: ?f64 = 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,
|
show_rulers: bool = true,
|
||||||
|
|
||||||
channels: GenerationalArray(Channel) = .{},
|
channels: GenerationalArray(Channel) = .{},
|
||||||
@ -379,9 +407,17 @@ pub const Project = struct {
|
|||||||
const channel_name = try readString(reader, allocator);
|
const channel_name = try readString(reader, allocator);
|
||||||
defer allocator.free(channel_name);
|
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).?;
|
const channel = self.channels.get(id).?;
|
||||||
channel.* = Channel{
|
channel.* = Channel{
|
||||||
.name = try utils.initBoundedStringZ(Channel.Name, channel_name)
|
.name = try utils.initBoundedStringZ(Channel.Name, channel_name),
|
||||||
|
.saved_collected_samples = saved_collected_samples
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -470,6 +506,21 @@ pub const Project = struct {
|
|||||||
|
|
||||||
try writeId(writer, channel_id);
|
try writeId(writer, channel_id);
|
||||||
try writeString(writer, channel_name);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -612,6 +663,8 @@ collection_task: ?CollectionTask = null,
|
|||||||
|
|
||||||
command_queue: std.BoundedArray(Command, 16) = .{},
|
command_queue: std.BoundedArray(Command, 16) = .{},
|
||||||
|
|
||||||
|
file_picker_id: ?Platform.FilePickerId = null,
|
||||||
|
|
||||||
pub fn init(self: *App, allocator: Allocator) !void {
|
pub fn init(self: *App, allocator: Allocator) !void {
|
||||||
self.* = App{
|
self.* = App{
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
@ -689,6 +742,14 @@ fn deinitUI(self: *App) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn loadProject(self: *App) !void {
|
fn loadProject(self: *App) !void {
|
||||||
|
if (self.isCollectionInProgress()) {
|
||||||
|
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;
|
const save_location = self.project.save_location orelse return error.MissingSaveLocation;
|
||||||
|
|
||||||
log.info("Load project from: {s}", .{save_location});
|
log.info("Load project from: {s}", .{save_location});
|
||||||
@ -725,6 +786,14 @@ fn loadProject(self: *App) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn saveProject(self: *App) !void {
|
fn saveProject(self: *App) !void {
|
||||||
|
if (self.isCollectionInProgress()) {
|
||||||
|
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;
|
const save_location = self.project.save_location orelse return error.MissingSaveLocation;
|
||||||
|
|
||||||
log.info("Save project to: {s}", .{save_location});
|
log.info("Save project to: {s}", .{save_location});
|
||||||
@ -748,7 +817,17 @@ pub fn tick(self: *App) !void {
|
|||||||
var ui = &self.ui;
|
var ui = &self.ui;
|
||||||
self.command_queue.len = 0;
|
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();
|
ui.pullOsEvents();
|
||||||
|
}
|
||||||
|
|
||||||
if (ui.isCtrlDown() and ui.isKeyboardPressed(.key_s)) {
|
if (ui.isCtrlDown() and ui.isKeyboardPressed(.key_s)) {
|
||||||
self.pushCommand(.save_project);
|
self.pushCommand(.save_project);
|
||||||
@ -809,6 +888,18 @@ pub fn tick(self: *App) !void {
|
|||||||
rl.clearBackground(srcery.black);
|
rl.clearBackground(srcery.black);
|
||||||
ui.draw();
|
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| {
|
for (self.command_queue.constSlice()) |command| {
|
||||||
switch (command) {
|
switch (command) {
|
||||||
.start_collection => {
|
.start_collection => {
|
||||||
@ -855,11 +946,15 @@ pub fn pushCommand(self: *App, command: Command) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn addFileFromPicker(self: *App) !void {
|
fn addFileFromPicker(self: *App) !void {
|
||||||
const filename = try Platform.openFilePicker(self.allocator);
|
if (self.file_picker_id == null) {
|
||||||
defer self.allocator.free(filename);
|
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);
|
self.file_picker_id = try Platform.spawnFilePicker(&opts);
|
||||||
_ = try self.addView(.{ .file = file_id });
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn startCollection(self: *App) !void {
|
fn startCollection(self: *App) !void {
|
||||||
@ -1041,6 +1136,7 @@ pub fn collectionThreadCallback(self: *App) void {
|
|||||||
log.err("Failed to append samples for channel: {}", .{e});
|
log.err("Failed to append samples for channel: {}", .{e});
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
channel.invalidateSavedSamples();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1104,6 +1200,15 @@ fn readFileF64(allocator: Allocator, file: std.fs.File) ![]f64 {
|
|||||||
return samples;
|
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 {
|
pub fn loadFile(self: *App, id: Id) !void {
|
||||||
const file = self.getFile(id) orelse return;
|
const file = self.getFile(id) orelse return;
|
||||||
file.clear(self.allocator);
|
file.clear(self.allocator);
|
||||||
@ -1184,6 +1289,22 @@ fn getAllowedSampleRate(self: *App, id: Id) !RangeF64 {
|
|||||||
return RangeF64.init(min_sample_rate, max_sample_rate);
|
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 {
|
pub fn loadChannel(self: *App, id: Id) !void {
|
||||||
const channel = self.getChannel(id) orelse return;
|
const channel = self.getChannel(id) orelse return;
|
||||||
channel.clear(self.allocator);
|
channel.clear(self.allocator);
|
||||||
@ -1201,6 +1322,10 @@ pub fn loadChannel(self: *App, id: Id) !void {
|
|||||||
log.err("Failed to get allowed sample rate: {}", .{e});
|
log.err("Failed to get allowed sample rate: {}", .{e});
|
||||||
break :blk null;
|
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 {
|
pub fn isChannelOutputing(self: *App, id: Id) bool {
|
||||||
|
@ -49,6 +49,7 @@ pub var fullscreen: rl.Texture2D = undefined;
|
|||||||
pub var output_generation: rl.Texture2D = undefined;
|
pub var output_generation: rl.Texture2D = undefined;
|
||||||
pub var checkbox_mark: rl.Texture2D = undefined;
|
pub var checkbox_mark: rl.Texture2D = undefined;
|
||||||
pub var cross: rl.Texture2D = undefined;
|
pub var cross: rl.Texture2D = undefined;
|
||||||
|
pub var file: rl.Texture2D = undefined;
|
||||||
|
|
||||||
pub fn font(font_id: FontId) FontFace {
|
pub fn font(font_id: FontId) FontFace {
|
||||||
var found_font: ?LoadedFont = null;
|
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"));
|
output_generation = try loadTextureFromAseprite(allocator, @embedFile("./assets/output-generation-icon.ase"));
|
||||||
checkbox_mark = try loadTextureFromAseprite(allocator, @embedFile("./assets/checkbox-mark.ase"));
|
checkbox_mark = try loadTextureFromAseprite(allocator, @embedFile("./assets/checkbox-mark.ase"));
|
||||||
cross = try loadTextureFromAseprite(allocator, @embedFile("./assets/cross.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 {
|
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;
|
if (self.axis != axis) return null;
|
||||||
|
|
||||||
const view = project.views.get(view_id) orelse 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)) {
|
if (!self.view_id.eql(view_id)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -50,8 +50,7 @@ pub const MinMaxCache = struct {
|
|||||||
max: f64
|
max: f64
|
||||||
};
|
};
|
||||||
|
|
||||||
const chunk_size = 256;
|
chunk_size: usize = 256,
|
||||||
|
|
||||||
min_max_pairs: std.ArrayListUnmanaged(MinMaxPair) = .{},
|
min_max_pairs: std.ArrayListUnmanaged(MinMaxPair) = .{},
|
||||||
sample_count: usize = 0,
|
sample_count: usize = 0,
|
||||||
|
|
||||||
@ -60,8 +59,9 @@ pub const MinMaxCache = struct {
|
|||||||
self.sample_count = 0;
|
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 > 0);
|
||||||
|
assert(chunk.len <= self.chunk_size);
|
||||||
|
|
||||||
var min_sample = chunk[0];
|
var min_sample = chunk[0];
|
||||||
var max_sample = chunk[0];
|
var max_sample = chunk[0];
|
||||||
@ -81,9 +81,9 @@ pub const MinMaxCache = struct {
|
|||||||
|
|
||||||
if (samples.len == 0) return;
|
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| {
|
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;
|
self.sample_count = samples.len;
|
||||||
@ -97,16 +97,16 @@ pub const MinMaxCache = struct {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const from_chunk = @divFloor(self.sample_count, chunk_size);
|
const from_chunk = @divFloor(self.sample_count, self.chunk_size);
|
||||||
const to_chunk = @divFloor(samples.len - 1, chunk_size);
|
const to_chunk = @divFloor(samples.len - 1, self.chunk_size);
|
||||||
|
|
||||||
for (from_chunk..(to_chunk+1)) |i| {
|
for (from_chunk..(to_chunk+1)) |i| {
|
||||||
const chunk = samples[
|
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);
|
try self.min_max_pairs.append(allocator, min_max_pair);
|
||||||
} else {
|
} else {
|
||||||
self.min_max_pairs.items[i] = min_max_pair;
|
self.min_max_pairs.items[i] = min_max_pair;
|
||||||
@ -115,6 +115,39 @@ pub const MinMaxCache = struct {
|
|||||||
|
|
||||||
self.sample_count = samples.len;
|
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 {
|
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_start: usize = @intFromFloat(i);
|
||||||
var column_end: usize = @intFromFloat(i + samples_per_column);
|
var column_end: usize = @intFromFloat(i + samples_per_column);
|
||||||
|
|
||||||
column_start = @divFloor(column_start, MinMaxCache.chunk_size);
|
column_start = @divFloor(column_start, min_max_cache.chunk_size);
|
||||||
column_end = @divFloor(column_end, MinMaxCache.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];
|
const min_max_pairs = min_max_cache.min_max_pairs.items[column_start..column_end];
|
||||||
if (min_max_pairs.len == 0) continue;
|
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;
|
const x_range = options.x_range;
|
||||||
|
|
||||||
if (x_range.lower >= @as(f64, @floatFromInt(samples.len))) return;
|
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;
|
const samples_per_column = x_range.size() / draw_rect.width;
|
||||||
if (samples_per_column >= 2) {
|
if (samples_per_column >= 2) {
|
||||||
if (min_max_level != null and samples_per_column > 2*MinMaxCache.chunk_size) {
|
if (min_max_cache != null and samples_per_column > @as(f64, @floatFromInt(2*min_max_cache.?.chunk_size))) {
|
||||||
drawSamplesMinMax(draw_rect, options, min_max_level.?);
|
drawSamplesMinMax(draw_rect, options, min_max_cache.?);
|
||||||
} else {
|
} else {
|
||||||
drawSamplesApproximate(draw_rect, options, samples);
|
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);
|
drawSamples(draw_rect, options, samples, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
_ = MinMaxCache;
|
||||||
|
}
|
16
src/main.zig
16
src/main.zig
@ -4,7 +4,7 @@ const builtin = @import("builtin");
|
|||||||
const Application = @import("./app.zig");
|
const Application = @import("./app.zig");
|
||||||
const Assets = @import("./assets.zig");
|
const Assets = @import("./assets.zig");
|
||||||
const Profiler = @import("./my-profiler.zig");
|
const Profiler = @import("./my-profiler.zig");
|
||||||
const Platform = @import("./platform.zig");
|
const Platform = @import("./platform/root.zig");
|
||||||
const raylib_h = @cImport({
|
const raylib_h = @cImport({
|
||||||
@cInclude("stdio.h");
|
@cInclude("stdio.h");
|
||||||
@cInclude("raylib.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 {
|
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(.{
|
var gpa = std.heap.GeneralPurposeAllocator(.{
|
||||||
.thread_safe = true
|
.thread_safe = true
|
||||||
}){};
|
}){};
|
||||||
const allocator = gpa.allocator();
|
const allocator = gpa.allocator();
|
||||||
defer _ = gpa.deinit();
|
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");
|
const icon_png = @embedFile("./assets/icon.png");
|
||||||
var icon_image = rl.loadImageFromMemory(".png", icon_png);
|
var icon_image = rl.loadImageFromMemory(".png", icon_png);
|
||||||
defer icon_image.unload();
|
defer icon_image.unload();
|
||||||
@ -205,4 +206,5 @@ pub fn main() !void {
|
|||||||
test {
|
test {
|
||||||
_ = @import("./ni-daq/root.zig");
|
_ = @import("./ni-daq/root.zig");
|
||||||
_ = @import("./range.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 UI = @import("../ui.zig");
|
||||||
const App = @import("../app.zig");
|
const App = @import("../app.zig");
|
||||||
const srcery = @import("../srcery.zig");
|
const srcery = @import("../srcery.zig");
|
||||||
const Platform = @import("../platform.zig");
|
const Platform = @import("../platform/root.zig");
|
||||||
const RangeF64 = @import("../range.zig").RangeF64;
|
const RangeF64 = @import("../range.zig").RangeF64;
|
||||||
const Graph = @import("../graph.zig");
|
const Graph = @import("../graph.zig");
|
||||||
const Assets = @import("../assets.zig");
|
const Assets = @import("../assets.zig");
|
||||||
@ -35,6 +35,9 @@ preview_samples_y_range: RangeF64 = RangeF64.init(0, 0),
|
|||||||
sample_rate_input: UI.TextInputStorage,
|
sample_rate_input: UI.TextInputStorage,
|
||||||
parsed_sample_rate: ?f64 = null,
|
parsed_sample_rate: ?f64 = null,
|
||||||
|
|
||||||
|
// View settings
|
||||||
|
channel_save_file_picker: ?Platform.FilePickerId = null,
|
||||||
|
|
||||||
pub fn init(app: *App) !MainScreen {
|
pub fn init(app: *App) !MainScreen {
|
||||||
const allocator = app.allocator;
|
const allocator = app.allocator;
|
||||||
|
|
||||||
@ -239,86 +242,11 @@ fn clearProtocolErrorMessage(self: *MainScreen) void {
|
|||||||
self.protocol_error_message = null;
|
self.protocol_error_message = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn showSidePanel(self: *MainScreen) !void {
|
fn showProjectSettings(self: *MainScreen) !void {
|
||||||
var ui = &self.app.ui;
|
var ui = &self.app.ui;
|
||||||
const frame_allocator = ui.frameAllocator();
|
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 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", .{});
|
const label = ui.label("Project", .{});
|
||||||
label.borders.bottom = .{
|
label.borders.bottom = .{
|
||||||
@ -364,6 +292,105 @@ pub fn showSidePanel(self: *MainScreen) !void {
|
|||||||
.value = &project.show_rulers,
|
.value = &project.show_rulers,
|
||||||
.label = "Ruler"
|
.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
|
||||||
|
})) |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 (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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn showSidePanel(self: *MainScreen) !void {
|
||||||
|
var ui = &self.app.ui;
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
if (self.view_controls.view_settings) |view_id| {
|
||||||
|
try self.showViewSettings(view_id);
|
||||||
|
} else {
|
||||||
|
try self.showProjectSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
68
src/ui.zig
68
src/ui.zig
@ -6,6 +6,7 @@ const rect_utils = @import("./rect-utils.zig");
|
|||||||
const utils = @import("./utils.zig");
|
const utils = @import("./utils.zig");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
const FontFace = @import("./font-face.zig");
|
const FontFace = @import("./font-face.zig");
|
||||||
|
const Platform = @import("./platform/root.zig");
|
||||||
const raylib_h = @cImport({
|
const raylib_h = @cImport({
|
||||||
@cInclude("stdio.h");
|
@cInclude("stdio.h");
|
||||||
@cInclude("raylib.h");
|
@cInclude("raylib.h");
|
||||||
@ -2272,6 +2273,13 @@ pub const CheckboxOptions = struct {
|
|||||||
label: ?[]const u8 = null
|
label: ?[]const u8 = null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const FileInputOptions = struct {
|
||||||
|
key: Key,
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
file_picker: *?Platform.FilePickerId,
|
||||||
|
path: ?[]const u8 = null
|
||||||
|
};
|
||||||
|
|
||||||
pub fn mouseTooltip(self: *UI) *Box {
|
pub fn mouseTooltip(self: *UI) *Box {
|
||||||
const tooltip = self.getBoxByKey(mouse_tooltip_box_key).?;
|
const tooltip = self.getBoxByKey(mouse_tooltip_box_key).?;
|
||||||
|
|
||||||
@ -2766,3 +2774,63 @@ pub fn checkbox(self: *UI, opts: CheckboxOptions) void {
|
|||||||
opts.value.* = !opts.value.*;
|
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 = .{};
|
||||||
|
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;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user