From fdf600106810e213edec33ec75455191ef307c32 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Tue, 13 May 2025 04:32:49 +0300 Subject: [PATCH] add exporting --- build.zig | 8 + src/app.zig | 424 ++++++++++++----------- src/components/systems/view_controls.zig | 9 - src/components/view.zig | 10 +- src/main.zig | 59 ++-- src/platform/root.zig | 6 +- src/platform/windows.zig | 221 ++++++++---- src/screens/channel_from_device.zig | 3 + src/screens/main_screen.zig | 304 ++++++++++++++-- src/ui.zig | 15 +- 10 files changed, 698 insertions(+), 361 deletions(-) diff --git a/build.zig b/build.zig index 76576e7..a3faca7 100644 --- a/build.zig +++ b/build.zig @@ -90,6 +90,11 @@ pub fn build(b: *std.Build) !void { .optimize = optimize, }); + const datetime_dep = b.dependency("zig-datetime", .{ + .target = target, + .optimize = optimize, + }); + const profiler_dep = b.dependency("profiler.zig", .{ .target = target, .optimize = optimize, @@ -124,6 +129,7 @@ pub fn build(b: *std.Build) !void { exe.root_module.addImport("cute_aseprite", cute_aseprite_lib); exe.root_module.addImport("known-folders", known_folders); exe.root_module.addImport("ini", ini); + exe.root_module.addImport("datetime", datetime_dep.module("zig-datetime")); // TODO: Add flag to disable in release exe.root_module.addImport("profiler", profiler_dep.module("profiler")); @@ -145,6 +151,8 @@ pub fn build(b: *std.Build) !void { exe.step.dependOn(&add_icon_step.step); exe.linkSystemLibrary("Comdlg32"); + exe.linkSystemLibrary("ole32"); + exe.linkSystemLibrary("uuid"); } b.installArtifact(exe); diff --git a/src/app.zig b/src/app.zig index 7a378ed..7553031 100644 --- a/src/app.zig +++ b/src/app.zig @@ -10,6 +10,7 @@ const MainScreen = @import("./screens/main_screen.zig"); const ChannelFromDeviceScreen = @import("./screens/channel_from_device.zig"); const Platform = @import("./platform/root.zig"); const constants = @import("./constants.zig"); +const datetime = @import("datetime").datetime; const Allocator = std.mem.Allocator; const log = std.log.scoped(.app); @@ -685,7 +686,6 @@ pub const Channel = struct { // Persistent name: Name = .{}, - saved_collected_samples: ?[]u8 = null, // Runtime device: Device = .{}, @@ -694,21 +694,14 @@ pub const Channel = struct { allowed_sample_rates: ?RangeF64 = null, write_pattern: std.ArrayListUnmanaged(f64) = .{}, output_task: ?NIDaq.Task = null, - last_sample_save_at: ?i128 = null, 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.write_pattern.clearAndFree(allocator); if (self.output_task) |task| { @@ -736,24 +729,6 @@ 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; - } }; pub const File = struct { @@ -922,12 +897,22 @@ pub const View = struct { }; pub const Project = struct { + pub const Solution = struct { + description: []u8, + sample: u64 + }; + const file_format_version: u8 = 0; const file_endian = std.builtin.Endian.big; - save_location: ?[]u8 = null, - sample_rate: ?f64 = null, + sample_rate: ?f64 = 5000, notes: std.ArrayListUnmanaged(u8) = .{}, + experiment_name: std.ArrayListUnmanaged(u8) = .{}, + pipete_solution: std.ArrayListUnmanaged(u8) = .{}, + statistic_points: std.ArrayListUnmanaged(u64) = .{}, + export_location: ?[]u8 = null, + + solutions: std.ArrayListUnmanaged(Solution) = .{}, sample_lists: GenerationalArray(SampleList) = .{}, channels: GenerationalArray(Channel) = .{}, @@ -1081,6 +1066,25 @@ pub const Project = struct { return marked_range; } + pub fn getSampleTimestamp(self: *Project) ?u64 { + var channel_iter = self.channels.iterator(); + while (channel_iter.next()) |channel| { + const collected_samples = self.sample_lists.get(channel.collected_samples_id) orelse continue; + return collected_samples.getLength(); + } + + return null; + } + + pub fn appendSolution(self: *Project, allocator: std.mem.Allocator, solution: []u8) !void { + const current_sample = self.getSampleTimestamp() orelse 0; + + try self.solutions.append(allocator, Solution{ + .description = try allocator.dupe(u8, solution), + .sample = current_sample + }); + } + pub fn deinit(self: *Project, allocator: Allocator) void { var file_iter = self.files.iterator(); while (file_iter.next()) |file| { @@ -1102,21 +1106,39 @@ pub const Project = struct { } self.sample_lists.clear(); - if (self.save_location) |str| { - allocator.free(str); - self.save_location = null; + + if (self.solutions.capacity > 0) { + for (self.solutions.items) |solution| { + allocator.free(solution.description); + } + self.solutions.deinit(allocator); } + + if (self.pipete_solution.capacity > 0) { + self.pipete_solution.deinit(allocator); + } + + if (self.notes.capacity > 0) { + self.notes.deinit(allocator); + } + + if (self.export_location) |export_location| { + allocator.free(export_location); + } + + if (self.statistic_points.items.len > 0) { + self.statistic_points.deinit(allocator); + } + + self.experiment_name.deinit(allocator); } // ------------------- Serialization ------------------ // - pub fn initFromFile(self: *Project, allocator: Allocator, save_location: []const u8) !void { + pub fn initFromFile(self: *Project, allocator: Allocator, f: std.fs.File) !void { self.* = .{}; errdefer self.deinit(allocator); - const f = try std.fs.cwd().openFile(save_location, .{}); - defer f.close(); - const reader = f.reader(); const version = try readInt(reader, u8); @@ -1129,6 +1151,21 @@ pub const Project = struct { self.sample_rate = null; } + const export_location = try readString(reader, allocator); + if (export_location.len > 0) { + self.export_location = export_location; + } else { + allocator.free(export_location); + } + + const experiment_name = try readString(reader, allocator); + defer allocator.free(experiment_name); + try self.experiment_name.insertSlice(allocator, 0, experiment_name); + + const pipete_solution = try readString(reader, allocator); + defer allocator.free(pipete_solution); + try self.pipete_solution.insertSlice(allocator, 0, pipete_solution); + { // Channels const channel_count = try readInt(reader, u32); for (0..channel_count) |_| { @@ -1139,20 +1176,12 @@ 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 sample_list_id = try self.addSampleList(allocator); errdefer self.removeSampleList(sample_list_id); const channel = self.channels.get(id).?; channel.* = Channel{ .name = try utils.initBoundedStringZ(Channel.Name, channel_name), - .saved_collected_samples = saved_collected_samples, .collected_samples_id = sample_list_id }; } @@ -1204,113 +1233,60 @@ pub const Project = struct { view.* = View{ .reference = reference, }; - - view.graph_opts.x_range = try readRangeF64(reader); - 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); } - pub fn save(self: *Project) !void { - const save_location = self.save_location orelse return error.NoSaveLocation; + pub fn save(self: *Project, f: std.fs.File) !void { + const writer = f.writer(); - var save_tmp_location: std.BoundedArray(u8, std.fs.max_path_bytes) = .{}; - save_tmp_location.appendSliceAssumeCapacity(save_location); - save_tmp_location.appendSliceAssumeCapacity("-tmp"); + try writeInt(writer, u8, file_format_version); + try writeFloat(writer, f64, self.sample_rate orelse 0); + try writeString(writer, self.export_location orelse &[_]u8{}); + try writeString(writer, self.experiment_name.items); + try writeString(writer, self.pipete_solution.items); - const dir = std.fs.cwd(); + { // Channels + try writeInt(writer, u32, @intCast(self.channels.count())); + var channel_iter = self.channels.idIterator(); + while (channel_iter.next()) |channel_id| { + const channel = self.channels.get(channel_id).?; + const channel_name = utils.getBoundedStringZ(&channel.name); - { - const f = try dir.createFile(save_tmp_location.slice(), .{}); - defer f.close(); - - const writer = f.writer(); - - try writeInt(writer, u8, file_format_version); - try writeFloat(writer, f64, self.sample_rate orelse 0); - - { // Channels - try writeInt(writer, u32, @intCast(self.channels.count())); - var channel_iter = self.channels.idIterator(); - while (channel_iter.next()) |channel_id| { - const channel = self.channels.get(channel_id).?; - const channel_name = utils.getBoundedStringZ(&channel.name); - - 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(); - - const sample_list = self.sample_lists.get(channel.collected_samples_id).?; - try sample_list.writeToFile(samples_file); - } - - const stat = try dir.statFile(path); - channel.last_sample_save_at = stat.mtime; - } - } + try writeId(writer, channel_id); + try writeString(writer, channel_name); } + } - { // Files - try writeInt(writer, u32, @intCast(self.files.count())); - var file_iter = self.files.idIterator(); - while (file_iter.next()) |file_id| { - const file = self.files.get(file_id).?; + { // Files + try writeInt(writer, u32, @intCast(self.files.count())); + var file_iter = self.files.idIterator(); + while (file_iter.next()) |file_id| { + const file = self.files.get(file_id).?; - try writeId(writer, file_id); - try writeString(writer, file.path); - } + try writeId(writer, file_id); + try writeString(writer, file.path); } + } - { // Views - try writeInt(writer, u32, @intCast(self.views.count())); - var view_iter = self.views.idIterator(); - while (view_iter.next()) |view_id| { - const view = self.views.get(view_id).?; + { // Views + try writeInt(writer, u32, @intCast(self.views.count())); + var view_iter = self.views.idIterator(); + while (view_iter.next()) |view_id| { + const view = self.views.get(view_id).?; - try writeId(writer, view_id); - try writeInt(writer, u8, @intFromEnum(view.reference)); - switch (view.reference) { - .channel => |channel_id| { - try writeInt(writer, u32, channel_id.asInt()); - }, - .file => |file_id| { - try writeInt(writer, u32, file_id.asInt()); - } - } - - 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); + try writeId(writer, view_id); + try writeInt(writer, u8, @intFromEnum(view.reference)); + switch (view.reference) { + .channel => |channel_id| { + try writeInt(writer, u32, channel_id.asInt()); + }, + .file => |file_id| { + try writeInt(writer, u32, file_id.asInt()); } } } } - - try std.fs.rename(dir, save_tmp_location.slice(), dir, save_location); } fn writeRangeF64(writer: anytype, range: RangeF64) !void { @@ -1395,8 +1371,7 @@ pub const Project = struct { pub const Command = union(enum) { start_collection, stop_collection, - save_project, - load_project, + export_project, stop_output: Id, // Channel id start_output: Id, // Channel id add_file_from_picker, @@ -1660,7 +1635,7 @@ fn deinitUI(self: *App) void { self.channel_from_device.deinit(); } -fn loadProject(self: *App) !void { +pub fn loadProject(self: *App, file: std.fs.File) !void { if (self.isNiDaqInUse()) { log.warn("Attempt to load while collection is still in progress. Loading canceled", .{}); return; @@ -1669,16 +1644,14 @@ fn loadProject(self: *App) !void { 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}); + log.info("Load project", .{}); 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); + try loaded.initFromFile(self.allocator, file); self.deinitUI(); self.deinitProject(); @@ -1709,7 +1682,7 @@ fn loadProject(self: *App) !void { self.initUI() catch @panic("Failed to initialize UI, can't recover"); } -fn saveProject(self: *App) !void { +pub fn saveProject(self: *App, file: std.fs.File) !void { if (self.isNiDaqInUse()) { log.warn("Attempt to save while collection is still in progress. Saving canceled", .{}); return; @@ -1718,11 +1691,9 @@ fn saveProject(self: *App) !void { self.collection_mutex.lock(); defer self.collection_mutex.unlock(); - const save_location = self.project.save_location orelse return error.MissingSaveLocation; + log.info("Save project", .{}); - log.info("Save project to: {s}", .{save_location}); - - try self.project.save(); + try self.project.save(file); } pub fn showUI(self: *App) !void { @@ -1737,6 +1708,96 @@ pub fn showUI(self: *App) !void { } } +fn exportProject(self: *App) !void { + log.debug("Export", .{}); + + const project = self.project; + const export_location = project.export_location orelse return error.NoExportLocation; + const experiment_name = project.experiment_name.items; + if (experiment_name.len == 0) { + return error.NoExperimentName; + } + + var export_dir = try std.fs.openDirAbsolute(export_location, .{}); + defer export_dir.close(); + + const now = datetime.Datetime.now(); + + var maybe_export_filename: ?[]u8 = null; + defer if (maybe_export_filename) |str| self.allocator.free(str); + + for (1..10000) |i| { + const export_filename = try std.fmt.allocPrint(self.allocator, "{s}_{}-{}-{}_{d:0>4}", .{ experiment_name, now.date.day, now.date.month, now.date.year, i }); + + const file_exists: bool = if (export_dir.access(export_filename, .{})) true else |_| false; + if (file_exists) { + self.allocator.free(export_filename); + continue; + } + + maybe_export_filename = export_filename; + break; + } + + const export_filename = maybe_export_filename orelse return error.NoFilename; + + const sample_rate = self.project.sample_rate orelse 1; + + var file = try export_dir.createFile(export_filename, .{}); + defer file.close(); + const writer = file.writer(); + + { + try writer.writeAll(export_filename); + try writer.writeAll("\n\n"); + } + + { + try writer.writeAll("Time Solution\n"); + for (self.project.solutions.items) |solution| { + const total_seconds = @as(f64, @floatFromInt(solution.sample)) / sample_rate; + + const text = try std.fmt.allocPrint(self.allocator, "{d:<8.2} {s}\n", .{total_seconds, solution.description}); + defer self.allocator.free(text); + + _ = try writer.writeAll(text); + } + try writer.writeAll("\n"); + } + + { + try writer.writeAll("Time 100/Gain\n"); + // TODO: + try writer.writeAll("\n"); + } + + { + try writer.writeAll("Time of Statistic points\n"); + for (project.statistic_points.items) |sample_timestamp| { + const total_seconds = @as(f64, @floatFromInt(sample_timestamp)) / sample_rate; + + const text = try std.fmt.allocPrint(self.allocator, "{d:.2}\n", .{total_seconds}); + defer self.allocator.free(text); + + _ = try writer.writeAll(text); + + } + try writer.writeAll("\n"); + } + + { + try writer.writeAll("Pipete Solution:\n"); + try writer.writeAll(self.project.pipete_solution.notes.items); + try writer.writeAll("\n"); + } + + { + try writer.writeAll("Note:\n"); + try writer.writeAll(self.project.notes.items); + try writer.writeAll("\n"); + } +} + pub fn tick(self: *App) !void { var ui = &self.ui; self.command_queue.len = 0; @@ -1753,14 +1814,6 @@ pub fn tick(self: *App) !void { ui.pullOsEvents(); } - if (ui.isCtrlDown() and ui.isKeyboardPressed(.key_s)) { - self.pushCommand(.save_project); - } - - if (ui.isCtrlDown() and ui.isKeyboardPressed(.key_l)) { - self.pushCommand(.load_project); - } - { self.collection_samples_mutex.lock(); defer self.collection_samples_mutex.unlock(); @@ -1821,17 +1874,22 @@ pub fn tick(self: *App) !void { .stop_collection => { self.stopCollection(); }, - .save_project => { - self.saveProject() catch |e| { - log.err("Failed to save project: {}", .{e}); - }; - }, - .load_project => { - self.loadProject() catch |e| { - log.err("Failed to load project: {}", .{e}); - utils.dumpErrorTrace(); + .export_project => { + self.exportProject() catch |e| { + log.err("Failed to export project: {}", .{e}); }; }, + // .save_project => { + // self.saveProject() catch |e| { + // log.err("Failed to save project: {}", .{e}); + // }; + // }, + // .load_project => { + // self.loadProject() catch |e| { + // log.err("Failed to load project: {}", .{e}); + // utils.dumpErrorTrace(); + // }; + // }, .add_file_from_picker => { self.addFileFromPicker() catch |e| { log.err("Failed to add file from picker: {}", .{e}); @@ -2182,21 +2240,6 @@ pub fn addChannel(self: *App, channel_name: []const u8) !Id { .collected_samples_id = sample_list_id }; - 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 }); }; @@ -2239,29 +2282,6 @@ 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 byte_count = try samples_file.getEndPos(); - if (byte_count % 8 != 0) { - return error.NotMultipleOf8; - } - - const sample_list = self.project.sample_lists.get(channel.collected_samples_id).?; - sample_list.clear(self.allocator); - - try self.project.readSamplesFromFile(self.allocator, channel.collected_samples_id, samples_file, @divExact(byte_count, 8)); - - 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); @@ -2279,10 +2299,6 @@ 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 { diff --git a/src/components/systems/view_controls.zig b/src/components/systems/view_controls.zig index 0d42eef..e65d877 100644 --- a/src/components/systems/view_controls.zig +++ b/src/components/systems/view_controls.zig @@ -167,7 +167,6 @@ last_applied_command: usize = 0, zoom_start: ?ViewAxisPosition = null, cursor: ?ViewAxisPosition = null, view_settings: ?Id = null, // View id -view_fullscreen: ?Id = null, // View id view_protocol_modal: ?Id = null, // View id selected_tool: enum { move, select, marker } = .move, show_marked_range: ?struct { @@ -329,14 +328,6 @@ pub fn isViewSettingsOpen(self: *System, view_id: Id) bool { return self.view_settings != null and self.view_settings.?.eql(view_id); } -pub fn toggleFullscreenView(self: *System, view_id: Id) void { - if (self.view_fullscreen == null or !self.view_fullscreen.?.eql(view_id)) { - self.view_fullscreen = view_id; - } else { - self.view_fullscreen = null; - } -} - pub fn setCursor(self: *System, view_id: Id, axis: UI.Axis, position: ?f64) void { return ViewAxisPosition.set(&self.cursor, view_id, axis, position); } diff --git a/src/components/view.zig b/src/components/view.zig index 7a8e323..4aaa48c 100644 --- a/src/components/view.zig +++ b/src/components/view.zig @@ -319,19 +319,11 @@ pub fn show(ctx: Context, view_id: Id, height: UI.Sizing) !Result { container.beginChildren(); defer container.endChildren(); - const fullscreen = ui.createBox(.{ - .key = ui.keyFromString("Fullscreen toggle"), + _ = ui.createBox(.{ .size_x = ruler_size, .size_y = ruler_size, .background = srcery.hard_black, - .hot_cursor = .mouse_cursor_pointing_hand, - .flags = &.{ .draw_hot, .draw_active, .clickable }, - .texture = Assets.fullscreen, - .texture_size = .{ .x = 28, .y = 28 } }); - if (ui.signal(fullscreen).clicked()) { - ctx.view_controls.toggleFullscreenView(view_id); - } x_ruler = UIViewRuler.createBox(ruler_ctx, ui.keyFromString("X ruler"), .X); } diff --git a/src/main.zig b/src/main.zig index 8d989db..1f86d70 100644 --- a/src/main.zig +++ b/src/main.zig @@ -5,6 +5,7 @@ const Application = @import("./app.zig"); const Assets = @import("./assets.zig"); const Profiler = @import("./my-profiler.zig"); const Platform = @import("./platform/root.zig"); +const known_folders = @import("known-folders"); const raylib_h = @cImport({ @cInclude("stdio.h"); @cInclude("raylib.h"); @@ -71,7 +72,7 @@ pub fn main() !void { const allocator = gpa.allocator(); defer _ = gpa.deinit(); - Platform.init(allocator); + try Platform.init(allocator); defer Platform.deinit(allocator); // TODO: Setup logging to a file @@ -82,6 +83,7 @@ pub fn main() !void { var icon_image = rl.loadImageFromMemory(".png", icon_png); defer icon_image.unload(); + rl.initWindow(800, 600, "DAQ view"); defer rl.closeWindow(); rl.setWindowState(.{ .window_resizable = true, .vsync_hint = true }); @@ -102,39 +104,26 @@ pub fn main() !void { try Application.init(&app, allocator); defer app.deinit(); - if (builtin.mode == .Debug and false) { - var cwd_realpath_buff: [std.fs.MAX_PATH_BYTES]u8 = undefined; - const cwd_realpath = try std.fs.cwd().realpath(".", &cwd_realpath_buff); + var config_dir = (try known_folders.open(allocator, .roaming_configuration, .{})) orelse return error.ConfigDirNotFound; + defer config_dir.close(); - const save_location = try std.fs.path.join(allocator, &.{ cwd_realpath, "project.proj" }); - errdefer allocator.free(save_location); + var app_config_dir = config_dir.openDir("daq-view", .{}) catch |e| switch (e) { + error.FileNotFound => blk: { + try config_dir.makeDir("daq-view"); + break :blk try config_dir.openDir("daq-view", .{}); + }, + else => return e + }; + defer app_config_dir.close(); - app.project.save_location = save_location; - app.project.sample_rate = 5000; - - // _ = try app.addView(.{ - // .channel = try app.addChannel("Dev1/ai0") - // }); - - // _ = try app.addView(.{ - // .file = try app.addFile("./samples-5k.bin") - // }); - - // _ = try app.addView(.{ - // .file = try app.addFile("./samples-50k.bin") - // }); - - // _ = try app.addView(.{ - // .file = try app.addFile("./samples-300k.bin") - // }); - - // _ = try app.addView(.{ - // .file = try app.addFile("./samples-9m.bin") - // }); - - // _ = try app.addView(.{ - // .file = try app.addFile("./samples-18m.bin") - // }); + if (app_config_dir.openFile("config.bin", .{})) |save_file| { + defer save_file.close(); + app.loadProject(save_file) catch |e| { + log.err("Failed to load project: {}", .{e}); + }; + } else |e| switch (e) { + error.FileNotFound => {}, + else => return e } var profiler: ?Profiler = null; @@ -183,6 +172,12 @@ pub fn main() !void { rl.endDrawing(); } + + { + const save_file = try app_config_dir.createFile("config.bin", .{}); + defer save_file.close(); + try app.saveProject(save_file); + } } test { diff --git a/src/platform/root.zig b/src/platform/root.zig index 2a95cc4..2131379 100644 --- a/src/platform/root.zig +++ b/src/platform/root.zig @@ -21,12 +21,14 @@ pub const OpenFileOptions = struct { }; pub const Style = enum { open, save }; + pub const Kind = enum { file, folder }; filters: std.BoundedArray(Filter, 4) = .{}, default_filter: ?usize = null, file_must_exist: bool = false, prompt_creation: bool = false, prompt_overwrite: bool = false, + kind: Kind = .file, style: Style = .open, pub fn appendFilter(self: *OpenFileOptions, name: []const u8, format: []const u8) !void { @@ -94,8 +96,8 @@ pub fn waitUntilFilePickerDone(allocator: std.mem.Allocator, optional_id: *?File return null; } -pub fn init(allocator: std.mem.Allocator) void { - impl.init(allocator); +pub fn init(allocator: std.mem.Allocator) !void { + try impl.init(allocator); } pub fn deinit(allocator: std.mem.Allocator) void { diff --git a/src/platform/windows.zig b/src/platform/windows.zig index 27f18fd..773a99e 100644 --- a/src/platform/windows.zig +++ b/src/platform/windows.zig @@ -2,7 +2,7 @@ const std = @import("std"); const rl = @import("raylib"); const Platform = @import("root.zig"); const windows_h = @cImport({ - @cDefine("_WIN32_WINNT", "0x0500"); + @cDefine("_WIN32_WINNT", "0x0600"); @cInclude("windows.h"); @cInclude("shobjidl.h"); }); @@ -48,10 +48,18 @@ const OPENFILENAMEW = extern struct { extern fn GetOpenFileNameW([*c]OPENFILENAMEW) windows_h.WINBOOL; extern fn GetSaveFileNameW([*c]OPENFILENAMEW) windows_h.WINBOOL; +const FileDialogShow = *const fn ([*c]windows_h.IFileDialog, HWND) callconv(.C) windows_h.HRESULT; + +const CLSCTX_INPROC_SERVER = 0x1; +const FOS_PICKFOLDERS = 0x00000020; +const FOS_FORCEFILESYSTEM = 0x00000040; +const SIGDN_FILESYSPATH: c_uint = 0x80058000; + const OpenedFilePicker = struct { + kind: OpenFileOptions.Kind, style: OpenFileOptions.Style, filename_w_buffer: [std.os.windows.PATH_MAX_WIDE]u16, - lpstrFilter_utf16: [:0]u16, + lpstrFilter_utf16: ?[:0]u16 = null, ofn: OPENFILENAMEW, thread: std.Thread, status: Platform.FilePickerStatus, @@ -63,60 +71,64 @@ const OpenedFilePicker = struct { .ofn = std.mem.zeroes(OPENFILENAMEW), .style = opts.style, .filename_w_buffer = undefined, - .lpstrFilter_utf16 = undefined, .thread = undefined, - .status = Platform.FilePickerStatus.in_progress + .status = Platform.FilePickerStatus.in_progress, + .kind = opts.kind }; - const hWnd: HWND = @alignCast(@ptrCast(rl.getWindowHandle())); - assert(hWnd != null); + if (opts.kind == .file) { + const hWnd: HWND = @alignCast(@ptrCast(rl.getWindowHandle())); + assert(hWnd != null); - var lpstrFilter_utf8: std.ArrayListUnmanaged(u8) = .{}; - defer lpstrFilter_utf8.deinit(allocator); + var lpstrFilter_utf8: std.ArrayListUnmanaged(u8) = .{}; + defer lpstrFilter_utf8.deinit(allocator); - for (opts.filters.constSlice()) |filter| { - try lpstrFilter_utf8.appendSlice(allocator, filter.name.slice()); - try lpstrFilter_utf8.append(allocator, 0); - try lpstrFilter_utf8.appendSlice(allocator, filter.format.slice()); - try lpstrFilter_utf8.append(allocator, 0); + for (opts.filters.constSlice()) |filter| { + try lpstrFilter_utf8.appendSlice(allocator, filter.name.slice()); + try lpstrFilter_utf8.append(allocator, 0); + try lpstrFilter_utf8.appendSlice(allocator, filter.format.slice()); + try lpstrFilter_utf8.append(allocator, 0); + } + + const lpstrFilter_utf16 = try std.unicode.utf8ToUtf16LeWithNull(allocator, lpstrFilter_utf8.items); + errdefer allocator.free(lpstrFilter_utf16); + self.lpstrFilter_utf16 = lpstrFilter_utf16; + + var flags: c_ulong = 0; + { + flags |= windows_h.OFN_NOCHANGEDIR; + flags |= windows_h.OFN_PATHMUSTEXIST; + flags |= windows_h.OFN_EXPLORER; + flags |= windows_h.OFN_LONGNAMES; + flags |= windows_h.OFN_ENABLESIZING; + + if (opts.file_must_exist) { + flags |= windows_h.OFN_FILEMUSTEXIST; + } + if (opts.prompt_creation) { + flags |= windows_h.OFN_CREATEPROMPT; + } + if (opts.prompt_overwrite) { + flags |= windows_h.OFN_OVERWRITEPROMPT; + } + } + + var nFilterIndex: c_ulong = 0; + if (opts.default_filter) |default_filter| { + assert(default_filter < opts.filters.len); + nFilterIndex = @intCast(default_filter + 1); + } + + @memset(&self.filename_w_buffer, 0); + self.ofn.lStructSize = @sizeOf(@TypeOf(self.ofn)); + self.ofn.hwndOwner = hWnd; + self.ofn.lpstrFile = &self.filename_w_buffer; + self.ofn.nMaxFile = self.filename_w_buffer.len; + self.ofn.lpstrFilter = lpstrFilter_utf16; + self.ofn.nFilterIndex = nFilterIndex; + self.ofn.Flags = flags; } - self.lpstrFilter_utf16 = try std.unicode.utf8ToUtf16LeWithNull(allocator, lpstrFilter_utf8.items); - errdefer allocator.free(self.lpstrFilter_utf16); - - var flags: c_ulong = 0; - { - flags |= windows_h.OFN_NOCHANGEDIR; - flags |= windows_h.OFN_PATHMUSTEXIST; - flags |= windows_h.OFN_EXPLORER; - flags |= windows_h.OFN_LONGNAMES; - flags |= windows_h.OFN_ENABLESIZING; - - if (opts.file_must_exist) { - flags |= windows_h.OFN_FILEMUSTEXIST; - } - if (opts.prompt_creation) { - flags |= windows_h.OFN_CREATEPROMPT; - } - if (opts.prompt_overwrite) { - flags |= windows_h.OFN_OVERWRITEPROMPT; - } - } - - var nFilterIndex: c_ulong = 0; - if (opts.default_filter) |default_filter| { - assert(default_filter < opts.filters.len); - nFilterIndex = @intCast(default_filter + 1); - } - - @memset(&self.filename_w_buffer, 0); - self.ofn.lStructSize = @sizeOf(@TypeOf(self.ofn)); - self.ofn.hwndOwner = hWnd; - self.ofn.lpstrFile = &self.filename_w_buffer; - self.ofn.nMaxFile = self.filename_w_buffer.len; - self.ofn.lpstrFilter = self.lpstrFilter_utf16; - self.ofn.nFilterIndex = nFilterIndex; - self.ofn.Flags = flags; // Creating this thread must be the last thing that this function does. // Because if an error occurs after a thread is spawn, it will not be joined/stopped @@ -125,35 +137,95 @@ const OpenedFilePicker = struct { fn deinit(self: *OpenedFilePicker, allocator: std.mem.Allocator) void { self.thread.join(); - allocator.free(self.lpstrFilter_utf16); + if (self.lpstrFilter_utf16) |lpstrFilter_utf16| { + allocator.free(lpstrFilter_utf16); + } } fn showDialog(self: *OpenedFilePicker) !void { - const result = switch (self.style) { - .open => GetOpenFileNameW(&self.ofn), - .save => GetSaveFileNameW(&self.ofn) - }; + if (self.kind == .file) { + const result = switch (self.style) { + .open => GetOpenFileNameW(&self.ofn), + .save => GetSaveFileNameW(&self.ofn) + }; - if (result != windows_h.TRUE) { - const err = windows_h.CommDlgExtendedError(); - if (err == 0) { + if (result != windows_h.TRUE) { + const err = windows_h.CommDlgExtendedError(); + if (err == 0) { + return error.Canceled; + } + + // From the MSDN docs, these 3 error should only be able to occur + // Source: https://learn.microsoft.com/en-us/windows/win32/api/commdlg/nf-commdlg-commdlgextendederror + if (err == windows_h.FNERR_BUFFERTOOSMALL) { + return error.BufferTooSmall; + + } else if (err == windows_h.FNERR_INVALIDFILENAME) { + return error.InvalidFilename; + + } else if (err == windows_h.FNERR_SUBCLASSFAILURE) { + return error.OutOfMemory; + + } else { + log.err("Unknown file picker error occured: {}", .{ err }); + return error.UnknownFileDialogError; + } + } + } else { + + var maybe_file_dialog: ?*windows_h.IFileDialog = null; + const hr_create = windows_h.CoCreateInstance( + &windows_h.CLSID_FileOpenDialog, + null, + CLSCTX_INPROC_SERVER, + &windows_h.IID_IFileDialog, + @ptrCast(&maybe_file_dialog), + ); + + if (windows_h.FAILED(hr_create) or maybe_file_dialog == null) { + log.err("Failed to create FileOpenDialog (HRESULT: {x})", .{hr_create}); + return error.CoCreateInstance; + } + + const file_dialog = maybe_file_dialog.?; + const lpVtbl = maybe_file_dialog.?.lpVtbl[0]; + + defer _ = lpVtbl.Release.?(file_dialog); + + var options: windows_h.DWORD = 0; + if (windows_h.FAILED(lpVtbl.GetOptions.?(file_dialog, &options))) { + log.err("Failed to get options", .{}); + return; + } + + _ = lpVtbl.SetOptions.?( + file_dialog, + options | FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM, + ); + + const hWnd: HWND = @alignCast(@ptrCast(rl.getWindowHandle())); + assert(hWnd != null); + + const lpVtblShow: FileDialogShow = @ptrCast(lpVtbl.Show.?); + const hr_show = lpVtblShow(file_dialog, hWnd); + if (!windows_h.SUCCEEDED(hr_show)) { + log.warn("Dialog cancelled or failed (HRESULT: {x})", .{hr_show}); return error.Canceled; } - // From the MSDN docs, these 3 error should only be able to occur - // Source: https://learn.microsoft.com/en-us/windows/win32/api/commdlg/nf-commdlg-commdlgextendederror - if (err == windows_h.FNERR_BUFFERTOOSMALL) { - return error.BufferTooSmall; + var item: ?*windows_h.IShellItem = null; + const hr_result = lpVtbl.GetResult.?(file_dialog, &item); + if (windows_h.SUCCEEDED(hr_result) and item != null) { + defer _ = item.?.lpVtbl[0].Release.?(item.?); - } else if (err == windows_h.FNERR_INVALIDFILENAME) { - return error.InvalidFilename; + var path: windows_h.LPWSTR = null; + const hr_display = item.?.lpVtbl[0].GetDisplayName.?(item.?, @bitCast(SIGDN_FILESYSPATH), &path); + if (windows_h.SUCCEEDED(hr_display) and path != null) { + defer windows_h.CoTaskMemFree(path); - } else if (err == windows_h.FNERR_SUBCLASSFAILURE) { - return error.OutOfMemory; - - } else { - log.err("Unknown file picker error occured: {}", .{ err }); - return error.UnknownFileDialogError; + const path_slice = std.mem.sliceTo(path, 0); + @memcpy(self.filename_w_buffer[0..path_slice.len], path_slice); + } } } } @@ -285,9 +357,16 @@ pub fn isFilePickerOpen(self: *Self) bool { return false; } -pub fn init(self: *Self, allocator: std.mem.Allocator) void { +pub fn init(self: *Self, allocator: std.mem.Allocator) !void { _ = windows_h.SetConsoleOutputCP(65001); + const COINIT_APARTMENTTHREADED = 0x2; + + if (windows_h.CoInitializeEx(null, COINIT_APARTMENTTHREADED) != windows_h.S_OK) { + log.err("Failed to initialize COM", .{}); + return error.CoInitialize; + } + self.* = Self{ .allocator = allocator }; @@ -300,4 +379,6 @@ pub fn deinit(self: *Self, allocator: std.mem.Allocator) void { maybe_opened_file_picker.* = null; } } + + windows_h.CoUninitialize(); } \ No newline at end of file diff --git a/src/screens/channel_from_device.zig b/src/screens/channel_from_device.zig index d84f078..99c4e82 100644 --- a/src/screens/channel_from_device.zig +++ b/src/screens/channel_from_device.zig @@ -222,6 +222,9 @@ pub fn tick(self: *Screen) !void { .channel = try self.app.addChannel(channel) }); } + + _ = self.channel_names.reset(.free_all); + self.selected_channels.len = 0; } } else { diff --git a/src/screens/main_screen.zig b/src/screens/main_screen.zig index 1242e48..57a379d 100644 --- a/src/screens/main_screen.zig +++ b/src/screens/main_screen.zig @@ -22,6 +22,8 @@ const Id = App.Id; app: *App, view_controls: ViewControlsSystem, +view_solutions: bool = false, +view_statistics_points: bool = false, modal: ?union(enum){ view_protocol: Id, // View id notes @@ -39,7 +41,11 @@ preview_samples_y_range: RangeF64 = RangeF64.init(0, 0), notes_storage: UI.TextInputStorage, // Project settings +solution_input: UI.TextInputStorage, sample_rate_input: UI.TextInputStorage, +experiment_name: UI.TextInputStorage, +pipete_solution: UI.TextInputStorage, +export_location_picker: ?Platform.FilePickerId = null, parsed_sample_rate: ?f64 = null, // View settings @@ -61,6 +67,9 @@ pub fn init(app: *App) !MainScreen { .amplitude_input = UI.TextInputStorage.init(allocator), .sample_rate_input = UI.TextInputStorage.init(allocator), .notes_storage = UI.TextInputStorage.init(allocator), + .solution_input = UI.TextInputStorage.init(allocator), + .experiment_name = UI.TextInputStorage.init(allocator), + .pipete_solution = UI.TextInputStorage.init(allocator), .view_controls = ViewControlsSystem.init(&app.project), .transform_inputs = transform_inputs, .preview_sample_list_id = try app.project.addSampleList(allocator) @@ -77,6 +86,9 @@ pub fn deinit(self: *MainScreen) void { self.amplitude_input.deinit(); self.sample_rate_input.deinit(); self.notes_storage.deinit(); + self.solution_input.deinit(); + self.experiment_name.deinit(); + self.pipete_solution.deinit(); for (self.transform_inputs) |input| { input.deinit(); } @@ -276,13 +288,20 @@ fn showNotesModal(self: *MainScreen) !void { label.alignment.x = .center; label.size.x = UI.Sizing.initGrowFull(); - _ = try ui.textInput(.{ + try ui.textInput(.{ .key = ui.keyFromString("Notes"), .storage = &self.notes_storage, .size_x = UI.Sizing.initGrowFull(), .size_y = UI.Sizing.initGrowFull(), .single_line = false }); + + if (self.notes_storage.modified) { + const project = &self.app.project; + + project.notes.clearRetainingCapacity(); + try project.notes.insertSlice(self.app.allocator, 0, self.notes_storage.buffer.items); + } } fn setProtocolErrorMessage(self: *MainScreen, comptime fmt: []const u8, args: anytype) !void { @@ -316,13 +335,120 @@ fn showProjectSettings(self: *MainScreen) !void { _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) }); + { + _ = ui.label("Experiment:", .{}); + try ui.textInput(.{ + .key = ui.keyFromString("Experiment name"), + .storage = &self.experiment_name, + .initial = project.experiment_name.items, + .size_x = UI.Sizing.initGrowFull() + }); + + if (self.experiment_name.modified) { + project.experiment_name.clearRetainingCapacity(); + try project.experiment_name.insertSlice(self.app.allocator, 0, self.experiment_name.buffer.items); + } + } + + _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) }); + + { + _ = ui.label("Pipete solution:", .{}); + try ui.textInput(.{ + .key = ui.keyFromString("Pipete solution"), + .storage = &self.pipete_solution, + .initial = project.pipete_solution.items, + .size_x = UI.Sizing.initGrowFull() + }); + + if (self.pipete_solution.modified) { + project.pipete_solution.clearRetainingCapacity(); + try project.pipete_solution.insertSlice(self.app.allocator, 0, self.pipete_solution.buffer.items); + } + } + + _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) }); + + { + _ = ui.label("Solution:", .{}); + + try ui.textInput(.{ + .key = ui.keyFromString("Solution input"), + .storage = &self.solution_input, + .size_x = UI.Sizing.initGrowFull() + }); + + { + const row = ui.createBox(.{ + .size_x = UI.Sizing.initFitChildren(), + .size_y = UI.Sizing.initFitChildren(), + .layout_direction = .left_to_right, + .layout_gap = ui.rem(1) + }); + row.beginChildren(); + defer row.endChildren(); + + { + const btn = ui.textButton("Append solution"); + btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 }); + if (ui.signal(btn).clicked()) { + if (self.solution_input.buffer.items.len > 0) { + try project.appendSolution(self.app.allocator, self.solution_input.buffer.items); + } + self.solution_input.clear(); + } + } + + if (project.solutions.items.len > 0) { + const btn = ui.textButton("View solutions"); + btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 }); + if (ui.signal(btn).clicked()) { + self.view_solutions = true; + } + } + } + + } + + _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) }); + + { + const row = ui.createBox(.{ + .size_x = UI.Sizing.initFitChildren(), + .size_y = UI.Sizing.initFitChildren(), + .layout_direction = .left_to_right, + .layout_gap = ui.rem(0.5) + }); + row.beginChildren(); + defer row.endChildren(); + + { + const btn = ui.textButton("Add statistic point"); + btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 }); + if (ui.signal(btn).clicked()) { + const current_sample = project.getSampleTimestamp() orelse 0; + try project.statistic_points.append(self.app.allocator, current_sample); + } + } + + if (project.statistic_points.items.len > 0) { + const btn = ui.textButton("View statistic points"); + btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 }); + if (ui.signal(btn).clicked()) { + self.view_statistics_points = true; + } + } + } + + _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) }); + { // Sample rate var placeholder: ?[]const u8 = null; if (project.getDefaultSampleRate()) |default_sample_rate| { placeholder = try std.fmt.allocPrint(frame_allocator, "{d}", .{ default_sample_rate }); } - _ = ui.label("Sample rate", .{}); + _ = ui.label("Sample rate:", .{}); self.parsed_sample_rate = try ui.numberInput(f64, .{ .key = ui.keyFromString("Sample rate input"), .storage = &self.sample_rate_input, @@ -342,8 +468,43 @@ fn showProjectSettings(self: *MainScreen) !void { } } - if (ui.signal(ui.textButton("Open notes")).clicked()) { - self.modal = .notes; + _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) }); + + { + const btn = ui.textButton("Open notes"); + btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 }); + if (ui.signal(btn).clicked()) { + self.modal = .notes; + } + } + + _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) }); + + { + _ = ui.label("Export folder:", .{}); + if (ui.fileInput(.{ + .allocator = self.app.allocator, + .key = ui.keyFromString("Export location"), + .path = project.export_location, + .file_picker = &self.export_location_picker, + .open_dialog = true, + .folder = true + })) |path| { + if (project.export_location) |str| { + self.app.allocator.free(str); + } + project.export_location = path; + } + + const btn = ui.textButton("Export"); + if (self.app.isCollectionInProgress() or project.export_location == null) { + btn.borders = UI.Borders.all(.{ .color = srcery.hard_black, .size = 4 }); + } else { + btn.borders = UI.Borders.all(.{ .color = srcery.green, .size = 4 }); + if (ui.signal(btn).clicked()) { + self.app.pushCommand(.export_project); + } + } } } @@ -383,20 +544,6 @@ fn showViewSettings(self: *MainScreen, view_id: Id) !void { } else { _ = ui.label("Type: unknown", .{ }); } - - if (ui.fileInput(.{ - .key = ui.keyFromString("Save location"), - .allocator = self.app.allocator, - .file_picker = &self.channel_save_file_picker, - .path = channel.saved_collected_samples, - .open_dialog = false - })) |path| { - if (channel.saved_collected_samples) |current_path| { - self.app.allocator.free(current_path); - } - - channel.saved_collected_samples = path; - } }, .file => |file_id| { const file = project.files.get(file_id).?; @@ -686,14 +833,6 @@ fn showToolbar(self: *MainScreen) void { } } - { - var btn = ui.textButton("Save"); - btn.borders = UI.Borders.all(.{ .size = 4, .color = srcery.green }); - if (ui.signal(btn).clicked()) { - self.app.pushCommand(.save_project); - } - } - _ = ui.createBox(.{ .size_x = UI.Sizing.initFixedPixels(ui.rem(1)) }); { @@ -730,6 +869,102 @@ fn showToolbar(self: *MainScreen) void { } } +fn showSolutions(self: *MainScreen) !void { + var ui = &self.app.ui; + const project = &self.app.project; + _ = &ui; + + { + const label = ui.label("Solutions", .{}); + label.borders.bottom = .{ + .color = srcery.white, + .size = 1 + }; + + _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) }); + } + + const sample_rate = project.getSampleRate(); + + for (project.solutions.items) |solution| { + const row = ui.createBox(.{ + .size_x = UI.Sizing.initGrowFull(), + .size_y = UI.Sizing.initFitChildren(), + .layout_direction = .left_to_right, + .layout_gap = ui.rem(1) + }); + row.beginChildren(); + defer row.endChildren(); + + if (sample_rate != null) { + const seconds = @as(f64, @floatFromInt(solution.sample)) / sample_rate.?; + _ = ui.label("{s}", .{ try utils.formatDuration(ui.frameAllocator(), seconds) }); + } else { + _ = ui.label("{d}", .{ solution.sample }); + } + + var description = ui.label("{s}", .{ solution.description }); + description.size.x = UI.Sizing.initGrowFull(); + description.flags.insert(.wrap_text); + } + + _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) }); + + { + const btn = ui.textButton("Close"); + btn.borders = UI.Borders.all(.{ .color = srcery.red, .size = 4 }); + if (ui.signal(btn).clicked()) { + self.view_solutions = false; + } + } +} + +fn showStatisticsPoints(self: *MainScreen) !void { + var ui = &self.app.ui; + const project = &self.app.project; + _ = &ui; + + { + const label = ui.label("Statistics points", .{}); + label.borders.bottom = .{ + .color = srcery.white, + .size = 1 + }; + + _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) }); + } + + const sample_rate = project.getSampleRate(); + + for (project.statistic_points.items) |sample_timestamp| { + const row = ui.createBox(.{ + .size_x = UI.Sizing.initGrowFull(), + .size_y = UI.Sizing.initFitChildren(), + .layout_direction = .left_to_right, + .layout_gap = ui.rem(1) + }); + row.beginChildren(); + defer row.endChildren(); + + if (sample_rate != null) { + const seconds = @as(f64, @floatFromInt(sample_timestamp)) / sample_rate.?; + _ = ui.label("{s}", .{ try utils.formatDuration(ui.frameAllocator(), seconds) }); + } else { + _ = ui.label("{d}", .{ sample_timestamp }); + } + } + + _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) }); + + { + const btn = ui.textButton("Close"); + btn.borders = UI.Borders.all(.{ .color = srcery.red, .size = 4 }); + if (ui.signal(btn).clicked()) { + self.view_statistics_points = false; + } + } +} + pub fn showSidePanel(self: *MainScreen) !void { var ui = &self.app.ui; @@ -748,12 +983,16 @@ pub fn showSidePanel(self: *MainScreen) !void { _ = ui.createBox(.{ .size_x = UI.Sizing.initFixedPixels(ui.rem(18)) }); - if (self.view_controls.show_marker) |marker| { + if (self.view_solutions) { + try self.showSolutions(); + } else if (self.view_controls.show_marker) |marker| { self.showMarker(marker.view_id, marker.index); } else if (self.view_controls.show_marked_range) |show_marked_range| { self.showMarkedRange(show_marked_range.view_id, show_marked_range.index); } else if (self.view_controls.view_settings) |view_id| { try self.showViewSettings(view_id); + } else if (self.view_statistics_points) { + try self.showStatisticsPoints(); } else { try self.showProjectSettings(); } @@ -819,10 +1058,7 @@ pub fn tick(self: *MainScreen) !void { .view_controls = &self.view_controls }; - if (self.view_controls.view_fullscreen) |view_id| { - _ = try UIView.show(ui_view_ctx, view_id, UI.Sizing.initGrowFull()); - - } else { + { const container = ui.createBox(.{ .size_x = UI.Sizing.initGrowFull(), .size_y = UI.Sizing.initGrowFull(), @@ -877,12 +1113,14 @@ pub fn tick(self: *MainScreen) !void { if (ui.isKeyboardPressed(.key_escape)) { if (self.modal != null) { self.modal = null; - } else if (self.view_controls.view_fullscreen != null) { - 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 if (self.view_solutions) { + self.view_solutions = false; + } else if (self.view_statistics_points) { + self.view_statistics_points = false; } else { self.app.should_close = true; } diff --git a/src/ui.zig b/src/ui.zig index 7d59c50..c31e4c6 100644 --- a/src/ui.zig +++ b/src/ui.zig @@ -2200,6 +2200,10 @@ pub const TextInputStorage = struct { self.buffer.deinit(); } + pub fn clear(self: *TextInputStorage) void { + self.deleteMany(0, self.buffer.items.len); + } + pub fn setText(self: *TextInputStorage, text: []const u8) !void { self.modified = true; @@ -2497,6 +2501,7 @@ pub const FileInputOptions = struct { allocator: std.mem.Allocator, file_picker: *?Platform.FilePickerId, open_dialog: bool = true, + folder: bool = false, path: ?[]const u8 = null }; @@ -3112,8 +3117,14 @@ pub fn fileInput(self: *UI, opts: FileInputOptions) ?[]u8 { 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 (opts.folder) { + file_open_options.kind = .folder; + } else { + file_open_options.kind = .file; + file_open_options.appendFilter("All", "*") catch unreachable; + file_open_options.appendFilter("Binary", "*.bin") catch unreachable; + } if (Platform.spawnFilePicker(&file_open_options)) |file_picker_id| { opts.file_picker.* = file_picker_id;