From 7301c68b7e9a9e1026b53e483ce1c511b03f68b2 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Sat, 8 Feb 2025 23:10:42 +0200 Subject: [PATCH] show message if NI Daq fails to be loaded --- README.md | 24 +- build.zig | 7 +- build.zig.zon | 8 + src/app.zig | 446 ++++++++++++++++++++--------- src/assets.zig | 14 +- src/font-face.zig | 4 + src/main.zig | 3 + src/ni-daq/api.zig | 47 +++- src/ni-daq/root.zig | 11 +- src/{ => ni-daq}/task-pool.zig | 9 +- src/platform.zig | 6 + src/ui.zig | 496 +++++++++++++++++++++++---------- 12 files changed, 783 insertions(+), 292 deletions(-) rename src/{ => ni-daq}/task-pool.zig (94%) diff --git a/README.md b/README.md index 18c0d24..c930dce 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,28 @@ zig build run ``` +## Resources + +* https://www.ni.com/docs/en-US/bundle/ni-daqmx-c-api-ref/page/cdaqmx/help_file_title.html +* https://www.ni.com/en/support/documentation/supplemental/06/getting-started-with-ni-daqmx--main-page.html +* https://knowledge.ni.com/KnowledgeArticleDetails?id=kA03q000000YGfDCAW&l=en-LT +* https://ziglang.org/learn/build-system/ + ## TODO -* Use downsampling for faster rendering of samples. When viewing many samples use dowsampled versions of data for rendering. Because you either way, you won't be able to see the detail. \ No newline at end of file +* Use downsampling for faster rendering of samples. When viewing many samples use dowsampled versions of data for rendering. Because you either way, you won't be able to see the detail. + +* Export .xcf files at build time +``` +(let* ( + (image (car (gimp-file-load RUN-NONINTERACTIVE "./icon.xcf" "./icon.xcf"))) + (merged-layer (car (gimp-image-merge-visible-layers image CLIP-TO-BOTTOM-LAYER))) + ) + (file-png-save RUN-NONINTERACTIVE image merged-layer "./icon.png" "./icon.png" 0 9 0 0 0 0 0) + (gimp-image-delete image) +) +``` + +``` +gimp-console-2.10.exe -i -b -b "(gimp-quit 0)" +``` \ No newline at end of file diff --git a/build.zig b/build.zig index 2bd3736..55cf12e 100644 --- a/build.zig +++ b/build.zig @@ -82,6 +82,9 @@ pub fn build(b: *std.Build) !void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); + const known_folders = b.dependency("known-folders", .{}).module("known-folders"); + const ini = b.dependency("ini", .{}).module("ini"); + const raylib_dep = b.dependency("raylib-zig", .{ .target = target, .optimize = optimize, @@ -97,8 +100,6 @@ pub fn build(b: *std.Build) !void { }); png_to_icon_tool.root_module.addImport("stb_image", stb_image_lib); - - const exe = b.addExecutable(.{ .name = "daq-view", .root_source_file = b.path("src/main.zig"), @@ -110,6 +111,8 @@ pub fn build(b: *std.Build) !void { exe.root_module.addImport("raylib", raylib_dep.module("raylib")); exe.root_module.addImport("stb_image", stb_image_lib); exe.root_module.addImport("cute_aseprite", cute_aseprite_lib); + exe.root_module.addImport("known-folders", known_folders); + exe.root_module.addImport("ini", ini); const external_compiler_support_dir = try std.process.getEnvVarOwned(b.allocator, "NIEXTCCOMPILERSUPP"); exe.addSystemIncludePath(.{ .cwd_relative = try std.fs.path.join(b.allocator, &.{ external_compiler_support_dir, "include" }) }); diff --git a/build.zig.zon b/build.zig.zon index a12248e..17f1508 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -7,6 +7,14 @@ .url = "https://github.com/Not-Nik/raylib-zig/archive/43d15b05c2b97cab30103fa2b46cff26e91619ec.tar.gz", .hash = "12204a223b19043e17b79300413d02f60fc8004c0d9629b8d8072831e352a78bf212" }, + .@"known-folders" = .{ + .url = "git+https://github.com/ziglibs/known-folders.git#1cceeb70e77dec941a4178160ff6c8d05a74de6f", + .hash = "12205f5e7505c96573f6fc5144592ec38942fb0a326d692f9cddc0c7dd38f9028f29", + }, + .@"ini" = .{ + .url = "https://github.com/ziglibs/ini/archive/e18d36665905c1e7ba0c1ce3e8780076b33e3002.tar.gz", + .hash = "1220b0979ea9891fa4aeb85748fc42bc4b24039d9c99a4d65d893fb1c83e921efad8", + } }, .paths = .{ diff --git a/src/app.zig b/src/app.zig index 8d12e40..f83fdf2 100644 --- a/src/app.zig +++ b/src/app.zig @@ -8,7 +8,7 @@ const Graph = @import("./graph.zig"); const NIDaq = @import("ni-daq/root.zig"); const rect_utils = @import("./rect-utils.zig"); const remap = @import("./utils.zig").remap; -const TaskPool = @import("./task-pool.zig"); +const TaskPool = @import("ni-daq/task-pool.zig"); const log = std.log.scoped(.app); const assert = std.debug.assert; @@ -92,47 +92,79 @@ allocator: std.mem.Allocator, ui: UI, channel_views: std.BoundedArray(ChannelView, max_channels) = .{}, -ni_daq: NIDaq, -task_pool: TaskPool, loaded_files: [max_channels]?FileChannel = .{ null } ** max_channels, device_channels: [max_channels]?DeviceChannel = .{ null } ** max_channels, +ni_daq_api: ?NIDaq.Api = null, +ni_daq: ?NIDaq = null, +task_pool: TaskPool, + shown_window: enum { channels, add_from_device } = .channels, +shown_modal: ?union(enum) { + no_library_error, + library_version_error: std.SemanticVersion, + library_version_warning: std.SemanticVersion +} = null, + device_filter: NIDaq.BoundedDeviceName = .{}, show_voltage_analog_inputs: bool = true, show_voltage_analog_outputs: bool = true, selected_channels: std.BoundedArray([:0]u8, max_channels) = .{}, pub fn init(self: *App, allocator: std.mem.Allocator) !void { - var ni_daq = try NIDaq.init(allocator, .{ - .max_devices = 4, - .max_analog_inputs = 32, - .max_analog_outputs = 8, - .max_counter_outputs = 8, - .max_counter_inputs = 8, - .max_analog_input_voltage_ranges = 4, - .max_analog_output_voltage_ranges = 4 - }); - errdefer ni_daq.deinit(allocator); - self.* = App{ .allocator = allocator, .ui = UI.init(allocator), - .ni_daq = ni_daq, .task_pool = undefined }; + errdefer if (self.ni_daq_api != null) self.ni_daq_api.?.deinit(); + errdefer if (self.ni_daq != null) self.ni_daq.?.deinit(allocator); - try TaskPool.init(&self.task_pool, allocator, &self.ni_daq); + if (NIDaq.Api.init()) |ni_daq_api| { + self.ni_daq_api = ni_daq_api; + + const ni_daq = try NIDaq.init(allocator, &self.ni_daq_api.?, .{ + .max_devices = 4, + .max_analog_inputs = 32, + .max_analog_outputs = 8, + .max_counter_outputs = 8, + .max_counter_inputs = 8, + .max_analog_input_voltage_ranges = 4, + .max_analog_output_voltage_ranges = 4 + }); + self.ni_daq = ni_daq; + + const installed_version = try ni_daq.version(); + if (installed_version.order(NIDaq.Api.min_version) == .lt) { + self.shown_modal = .{ .library_version_warning = installed_version }; + } + + } else |e| { + log.err("Failed to load NI-Daq library: {any}", .{e}); + + switch (e) { + error.LibraryNotFound => { + self.shown_modal = .no_library_error; + }, + error.SymbolNotFound => { + if (NIDaq.Api.version()) |version| { + self.shown_modal = .{ .library_version_error = version }; + } else |_| { + self.shown_modal = .no_library_error; + } + } + } + } + + try TaskPool.init(&self.task_pool, allocator); errdefer self.task_pool.deinit(); } pub fn deinit(self: *App) void { - self.task_pool.deinit(); - for (self.channel_views.slice()) |*channel| { channel.view_cache.deinit(); } @@ -157,39 +189,9 @@ pub fn deinit(self: *App) void { self.selected_channels.len = 0; self.ui.deinit(); - self.ni_daq.deinit(self.allocator); -} -fn showButton(self: *App, text: []const u8) UI.Interaction { - var button = self.ui.newWidget(self.ui.keyFromString(text)); - button.border = srcery.bright_blue; - button.padding.vertical(8); - button.padding.horizontal(16); - button.flags.insert(.clickable); - button.size = .{ - .x = .{ .text = {} }, - .y = .{ .text = {} }, - }; - - const interaction = self.ui.getInteraction(button); - var text_color: rl.Color = undefined; - if (interaction.held_down) { - button.background = srcery.hard_black; - text_color = srcery.white; - } else if (interaction.hovering) { - button.background = srcery.bright_black; - text_color = srcery.bright_white; - } else { - button.background = srcery.blue; - text_color = srcery.bright_white; - } - - button.text = .{ - .content = text, - .color = text_color - }; - - return interaction; + self.task_pool.deinit(); + if (self.ni_daq) |*ni_daq| ni_daq.deinit(self.allocator); } fn readSamplesFromFile(allocator: std.mem.Allocator, file: std.fs.File) ![]f64 { @@ -272,6 +274,8 @@ pub fn appendChannelFromFile(self: *App, path: []const u8) !void { } pub fn appendChannelFromDevice(self: *App, channel_name: []const u8) !void { + const ni_daq = &(self.ni_daq orelse return); + const device_channel_index = findFreeSlot(DeviceChannel, &self.device_channels) orelse return error.DeviceChannelLimitReached; const name_buff = try DeviceChannel.Name.fromSlice(channel_name); @@ -283,17 +287,17 @@ pub fn appendChannelFromDevice(self: *App, channel_name: []const u8) !void { var min_value: f64 = 0; var max_value: f64 = 1; - const voltage_ranges = try self.ni_daq.listDeviceAOVoltageRanges(device_z); + const voltage_ranges = try ni_daq.listDeviceAOVoltageRanges(device_z); if (voltage_ranges.len > 0) { min_value = voltage_ranges[0].low; max_value = voltage_ranges[0].high; } - const max_sample_rate = try self.ni_daq.getMaxSampleRate(channel_name_z); + const max_sample_rate = try ni_daq.getMaxSampleRate(channel_name_z); self.device_channels[device_channel_index] = DeviceChannel{ .name = name_buff, - .min_sample_rate = self.ni_daq.getMinSampleRate(channel_name_z) catch max_sample_rate, + .min_sample_rate = ni_daq.getMinSampleRate(channel_name_z) catch max_sample_rate, .max_sample_rate = max_sample_rate, .min_value = min_value, .max_value = max_value, @@ -334,6 +338,17 @@ fn getChannelSource(self: *App, channel_view: *ChannelView) ?ChannelView.SourceO return null; } +fn findChannelIndexByName(haystack: []const [:0]const u8, needle: [:0]const u8) ?usize { + for (0.., haystack) |i, item| { + if (std.mem.eql(u8, item, needle)) { + return i; + } + } + return null; +} + +// ------------------------------- GUI -------------------------------------------- // + fn showChannelViewSlider(self: *App, view_rect: *Graph.ViewOptions, sample_count: f32) void { const min_visible_samples = 1; // sample_count*0.02; @@ -347,26 +362,23 @@ fn showChannelViewSlider(self: *App, view_rect: *Graph.ViewOptions, sample_count const minimap_rect = minimap_box.computedRect(); - const middle_box = self.ui.newBoxFromString("Middle knob"); + const middle_box = self.ui.clickableBox("Middle knob"); { - middle_box.flags.insert(.clickable); middle_box.flags.insert(.draggable_x); middle_box.background = rl.Color.black.alpha(0.5); middle_box.size.y = UI.Size.pixels(32, 1); } - const left_knob_box = self.ui.newBoxFromString("Left knob"); + const left_knob_box = self.ui.clickableBox("Left knob"); { - left_knob_box.flags.insert(.clickable); left_knob_box.flags.insert(.draggable_x); left_knob_box.background = rl.Color.black.alpha(0.5); left_knob_box.size.x = UI.Size.pixels(8, 1); left_knob_box.size.y = UI.Size.pixels(32, 1); } - const right_knob_box = self.ui.newBoxFromString("Right knob"); + const right_knob_box = self.ui.clickableBox("Right knob"); { - right_knob_box.flags.insert(.clickable); right_knob_box.flags.insert(.draggable_x); right_knob_box.background = rl.Color.black.alpha(0.5); right_knob_box.size.x = UI.Size.pixels(8, 1); @@ -460,15 +472,11 @@ fn showChannelView(self: *App, channel_view: *ChannelView) !void { if (source == .device) { const device_channel = source.device; - { - const record_button = self.ui.newBoxFromString("Record"); - record_button.flags.insert(.clickable); - record_button.size.x = UI.Size.text(1, 0); + if (self.ni_daq) |*ni_daq| { + const record_button = self.ui.button(.text, "Record"); record_button.size.y = UI.Size.percent(1, 0); - if (device_channel.active_task == null) { - record_button.setText(.text, "Record"); - } else { + if (device_channel.active_task != null) { record_button.setText(.text, "Stop"); } @@ -480,6 +488,7 @@ fn showChannelView(self: *App, channel_view: *ChannelView) !void { } else { const channel_name = device_channel.name.buffer[0..device_channel.name.len :0]; device_channel.active_task = try self.task_pool.launchAIVoltageChannel( + ni_daq, &device_channel.mutex, &device_channel.samples, .{ @@ -499,11 +508,11 @@ fn showChannelView(self: *App, channel_view: *ChannelView) !void { } { - const follow_button = self.ui.newBoxFromString("Follow"); - follow_button.flags.insert(.clickable); - follow_button.size.x = UI.Size.text(1, 0); + const follow_button = self.ui.button(.text, "Follow"); follow_button.size.y = UI.Size.percent(1, 0); - follow_button.setText(.text, if (channel_view.follow) "Unfollow" else "Follow"); + if (channel_view.follow) { + follow_button.setText(.text, "Unfollow"); + } const signal = self.ui.signalFromBox(follow_button); if (signal.clicked()) { @@ -544,13 +553,15 @@ fn showChannelsWindow(self: *App) !void { { const prompt_box = self.ui.newBoxFromString("Add prompt"); - prompt_box.layout_axis = .X; prompt_box.size.x = UI.Size.percent(1, 0); - prompt_box.size.y = UI.Size.pixels(150, 1); + prompt_box.size.y = UI.Size.percent(1, 1); self.ui.pushParent(prompt_box); defer self.ui.popParent(); - self.ui.spacer(.{ .x = UI.Size.percent(1, 0) }); + const center_box = self.ui.pushCenterBox(); + defer self.ui.popCenterBox(); + center_box.layout_axis = .X; + center_box.layout_gap = 32; const from_file_button = self.ui.button(.text, "Add from file"); from_file_button.background = srcery.green; @@ -558,28 +569,17 @@ fn showChannelsWindow(self: *App) !void { log.debug("TODO: Not implemented", .{}); } - self.ui.spacer(.{ .x = UI.Size.pixels(32, 1) }); - const from_device_button = self.ui.button(.text, "Add from device"); from_device_button.background = srcery.green; if (self.ui.signalFromBox(from_device_button).clicked()) { log.debug("TODO: Not implemented", .{}); } - - self.ui.spacer(.{ .x = UI.Size.percent(1, 0) }); } } -fn findChannelIndexByName(haystack: []const [:0]const u8, needle: [:0]const u8) ?usize { - for (0.., haystack) |i, item| { - if (std.mem.eql(u8, item, needle)) { - return i; - } - } - return null; -} - fn showAddFromDeviceWindow(self: *App) !void { + const ni_daq = &(self.ni_daq orelse return); + const window = self.ui.newBoxFromString("Device window"); window.size.x = UI.Size.percent(1, 0); window.size.y = UI.Size.percent(1, 0); @@ -595,12 +595,10 @@ fn showAddFromDeviceWindow(self: *App) !void { self.ui.pushParent(filters_box); defer self.ui.popParent(); - for (try self.ni_daq.listDeviceNames()) |device| { - const device_box = self.ui.newBoxFromString(device); - device_box.flags.insert(.clickable); + for (try ni_daq.listDeviceNames()) |device| { + const device_box = self.ui.button(.text, device); device_box.size.x = UI.Size.text(2, 1); device_box.size.y = UI.Size.text(2, 1); - device_box.setText(.text, device); const signal = self.ui.signalFromBox(device_box); if (signal.clicked()) { @@ -609,37 +607,33 @@ fn showAddFromDeviceWindow(self: *App) !void { } { - const toggle_inputs_box = self.ui.newBoxFromString("Toggle inputs"); - toggle_inputs_box.flags.insert(.clickable); + const toggle_inputs_box = self.ui.button(.text, "Toggle inputs"); toggle_inputs_box.size.x = UI.Size.text(2, 1); toggle_inputs_box.size.y = UI.Size.text(2, 1); toggle_inputs_box.setText(.text, if (self.show_voltage_analog_inputs) "Hide inputs" else "Show inputs"); - const signal = self.ui.signalFromBox(toggle_inputs_box); - if (signal.clicked()) { + + if (self.ui.signalFromBox(toggle_inputs_box).clicked()) { self.show_voltage_analog_inputs = !self.show_voltage_analog_inputs; } } { - const toggle_outputs_box = self.ui.newBoxFromString("Toggle outputs"); - toggle_outputs_box.flags.insert(.clickable); + const toggle_outputs_box = self.ui.button(.text, "Toggle outputs"); toggle_outputs_box.size.x = UI.Size.text(2, 1); toggle_outputs_box.size.y = UI.Size.text(2, 1); toggle_outputs_box.setText(.text, if (self.show_voltage_analog_outputs) "Hide outputs" else "Show outputs"); - const signal = self.ui.signalFromBox(toggle_outputs_box); - if (signal.clicked()) { + + if (self.ui.signalFromBox(toggle_outputs_box).clicked()) { self.show_voltage_analog_outputs = !self.show_voltage_analog_outputs; } } { - const add_button = self.ui.newBoxFromString("Add"); - add_button.flags.insert(.clickable); + const add_button = self.ui.button(.text, "Add selected"); add_button.size.x = UI.Size.text(2, 1); add_button.size.y = UI.Size.text(2, 1); - add_button.setText(.text, "Add selected"); - const signal = self.ui.signalFromBox(add_button); - if (signal.clicked()) { + + if (self.ui.signalFromBox(add_button).clicked()) { const selected_devices = self.selected_channels.constSlice(); for (selected_devices) |channel| { @@ -668,21 +662,21 @@ fn showAddFromDeviceWindow(self: *App) !void { self.device_filter.buffer[0..self.device_filter.len :0] }; } else { - devices = try self.ni_daq.listDeviceNames(); + devices = try ni_daq.listDeviceNames(); } for (devices) |device| { var ai_voltage_physical_channels: []const [:0]const u8 = &.{}; if (self.show_voltage_analog_inputs) { - if (try self.ni_daq.checkDeviceAIMeasurementType(device, .Voltage)) { - ai_voltage_physical_channels = try self.ni_daq.listDeviceAIPhysicalChannels(device); + if (try ni_daq.checkDeviceAIMeasurementType(device, .Voltage)) { + ai_voltage_physical_channels = try ni_daq.listDeviceAIPhysicalChannels(device); } } var ao_physical_channels: []const [:0]const u8 = &.{}; if (self.show_voltage_analog_outputs) { - if (try self.ni_daq.checkDeviceAOOutputType(device, .Voltage)) { - ao_physical_channels = try self.ni_daq.listDeviceAOPhysicalChannels(device); + if (try ni_daq.checkDeviceAOOutputType(device, .Voltage)) { + ao_physical_channels = try ni_daq.listDeviceAOPhysicalChannels(device); } } @@ -690,11 +684,7 @@ fn showAddFromDeviceWindow(self: *App) !void { for (channels) |channel| { const selected_channels_slice = self.selected_channels.constSlice(); - const channel_box = self.ui.newBoxFromString(channel); - channel_box.flags.insert(.clickable); - channel_box.size.x = UI.Size.text(1, 1); - channel_box.size.y = UI.Size.text(0.5, 1); - channel_box.setText(.text, channel); + const channel_box = self.ui.button(.text, channel); if (findChannelIndexByName(selected_channels_slice, channel) != null) { channel_box.background = srcery.xgray3; @@ -717,7 +707,6 @@ fn showAddFromDeviceWindow(self: *App) !void { fn showToolbar(self: *App) void { const toolbar = self.ui.newBoxFromString("Toolbar"); - toolbar.flags.insert(.clickable); toolbar.background = rl.Color.green; toolbar.layout_axis = .X; toolbar.size = .{ @@ -728,14 +717,9 @@ fn showToolbar(self: *App) void { defer self.ui.popParent(); { - const box = self.ui.newBoxFromString("Add from file"); - box.flags.insert(.clickable); + const box = self.ui.button(.text, "Add from file"); box.background = rl.Color.red; - box.size = .{ - .x = UI.Size.text(2, 1), - .y = UI.Size.percent(1, 1) - }; - box.setText(.text, "Add from file",); + box.size.y = UI.Size.percent(1, 1); const signal = self.ui.signalFromBox(box); if (signal.clicked()) { @@ -752,14 +736,9 @@ fn showToolbar(self: *App) void { } { - const box = self.ui.newBoxFromString("Add from device"); - box.flags.insert(.clickable); + const box = self.ui.button(.text, "Add from device"); box.background = rl.Color.lime; - box.size = .{ - .x = UI.Size.text(2, 1), - .y = UI.Size.percent(1, 1) - }; - box.setText(.text, "Add from device"); + box.size.y = UI.Size.percent(1, 1); const signal = self.ui.signalFromBox(box); if (signal.clicked()) { @@ -772,6 +751,186 @@ fn showToolbar(self: *App) void { } } +fn showModalNoLibraryError(self: *App) void { + const modal = self.ui.getParent().?; + + modal.layout_axis = .Y; + modal.size = .{ + .x = UI.Size.pixels(400, 1), + .y = UI.Size.pixels(320, 1), + }; + + self.ui.spacer(.{ .y = UI.Size.percent(1, 0) }); + + const text = self.ui.newBoxFromString("Text"); + text.flags.insert(.text_wrapping); + text.size.x = UI.Size.text(2, 0); + text.size.y = UI.Size.text(1, 1); + text.appendText("PALA, PALA! Aš neradau būtinos bibliotekos ant kompiuterio. Programa vis dar veiks, bet "); + text.appendText("dauguma funkcijų bus paslėptos. Susirask iternete \"NI MAX\" ir instaliuok. Štai nuorada "); + text.appendText("į gidą."); + + { + self.ui.pushHorizontalAlign(); + defer self.ui.popHorizontalAlign(); + + const link = self.ui.newBoxFromString("Link"); + link.flags.insert(.clickable); + link.flags.insert(.hover_mouse_hand); + link.flags.insert(.text_underline); + link.size.x = UI.Size.text(1, 1); + link.size.y = UI.Size.text(1, 1); + link.setText( + .text, + "Nuorada į gidą" + ); + link.text.?.color = srcery.blue; + + const signal = self.ui.signalFromBox(link); + if (signal.clicked()) { + rl.openURL("https://knowledge.ni.com/KnowledgeArticleDetails?id=kA03q000000YGQwCAO&l=en-LT"); + } + if (self.ui.isBoxHot(link)) { + link.text.?.color = srcery.bright_blue; + } + } + + self.ui.spacer(.{ .y = UI.Size.percent(1, 0) }); + + { + self.ui.pushHorizontalAlign(); + defer self.ui.popHorizontalAlign(); + + const btn = self.ui.button(.text, "Supratau"); + btn.background = srcery.green; + if (self.ui.signalFromBox(btn).clicked()) { + self.shown_modal = null; + } + } + + self.ui.spacer(.{ .y = UI.Size.percent(1, 0) }); +} + +fn showModalLibraryVersionError(self: *App) void { + assert(self.shown_modal.? == .library_version_error); + + const installed_version = self.shown_modal.?.library_version_error; + const modal = self.ui.getParent().?; + + modal.layout_axis = .Y; + modal.size = .{ + .x = UI.Size.pixels(400, 1), + .y = UI.Size.pixels(320, 1), + }; + + self.ui.spacer(.{ .y = UI.Size.percent(1, 0) }); + + { + const text = self.ui.newBox(UI.Key.initNil()); + text.flags.insert(.text_wrapping); + text.flags.insert(.text_left_align); + text.size.x = UI.Size.text(2, 0); + text.size.y = UI.Size.text(1, 1); + text.appendText("Ooo ne! Reikalinga biblioteka surasta, bet nesurastos reikalingos funkcijos. "); + text.appendText("Susitikrink, kad turi pakankamai naują versiją NI MAX instaliuota."); + } + + { + const text = self.ui.newBox(UI.Key.initNil()); + text.size.x = UI.Size.text(2, 0); + text.size.y = UI.Size.text(1, 1); + text.setFmtText(.text, "Instaliuota versija: {}", .{installed_version}); + } + + { + const text = self.ui.newBox(UI.Key.initNil()); + text.size.x = UI.Size.text(2, 0); + text.size.y = UI.Size.text(1, 1); + text.setFmtText(.text, "Rekomenduotina versija: {}", .{NIDaq.Api.min_version}); + } + + self.ui.spacer(.{ .y = UI.Size.percent(1, 0) }); + + { + self.ui.pushHorizontalAlign(); + defer self.ui.popHorizontalAlign(); + + const btn = self.ui.button(.text, "Supratau"); + btn.background = srcery.green; + if (self.ui.signalFromBox(btn).clicked()) { + self.shown_modal = null; + } + } + + self.ui.spacer(.{ .y = UI.Size.percent(1, 0) }); +} + +fn showModalLibraryVersionWarning(self: *App) void { + assert(self.shown_modal.? == .library_version_warning); + + const installed_version = self.shown_modal.?.library_version_warning; + const modal = self.ui.getParent().?; + + modal.layout_axis = .Y; + modal.size = .{ + .x = UI.Size.pixels(400, 1), + .y = UI.Size.pixels(320, 1), + }; + + self.ui.spacer(.{ .y = UI.Size.percent(1, 0) }); + + { + const text = self.ui.newBox(UI.Key.initNil()); + text.flags.insert(.text_wrapping); + text.flags.insert(.text_left_align); + text.size.x = UI.Size.text(2, 0); + text.size.y = UI.Size.text(1, 1); + text.appendText("Instaliuota NI MAX versija žemesnė negu rekomenduotina versija. "); + text.appendText("Daug kas turėtų veikti, bet negaliu garantuoti kad viskas veiks. "); + text.appendText("Jeigu susidursi su problemomis kur programa sustoja veikti pabandyk atsinaujinti "); + text.appendText("NI MAX."); + } + + { + const text = self.ui.newBox(UI.Key.initNil()); + text.size.x = UI.Size.text(2, 0); + text.size.y = UI.Size.text(1, 1); + text.setFmtText(.text, "Instaliuota versija: {}", .{installed_version}); + } + + { + const text = self.ui.newBox(UI.Key.initNil()); + text.size.x = UI.Size.text(2, 0); + text.size.y = UI.Size.text(1, 1); + text.setFmtText(.text, "Rekomenduotina versija: {}", .{NIDaq.Api.min_version}); + } + + self.ui.spacer(.{ .y = UI.Size.percent(1, 0) }); + + { + self.ui.pushHorizontalAlign(); + defer self.ui.popHorizontalAlign(); + + const btn = self.ui.button(.text, "Supratau"); + btn.background = srcery.green; + if (self.ui.signalFromBox(btn).clicked()) { + self.shown_modal = null; + } + } + + self.ui.spacer(.{ .y = UI.Size.percent(1, 0) }); +} + +fn showModal(self: *App) void { + assert(self.shown_modal != null); + + switch (self.shown_modal.?) { + .no_library_error => self.showModalNoLibraryError(), + .library_version_error => self.showModalLibraryVersionError(), + .library_version_warning => self.showModalLibraryVersionWarning() + } +} + fn updateUI(self: *App) !void { self.ui.begin(); defer self.ui.end(); @@ -779,6 +938,32 @@ fn updateUI(self: *App) !void { const root_box = self.ui.getParent().?; root_box.layout_axis = .Y; + var maybe_modal_overlay: ?*UI.Box = null; + + if (self.shown_modal != null) { + const modal_overlay = self.ui.newBoxNoAppend(self.ui.newKeyFromString("Modal overlay")); + maybe_modal_overlay = modal_overlay; + modal_overlay.flags.insert(.clickable); + modal_overlay.flags.insert(.scrollable); + modal_overlay.background = rl.Color.black.alpha(0.5); + modal_overlay.setFixedPosition(.{ .x = 0, .y = 0 }); + modal_overlay.size = .{ + .x = UI.Size.percent(1, 0), + .y = UI.Size.percent(1, 0), + }; + + self.ui.pushParent(modal_overlay); + defer self.ui.popParent(); + + const modal = self.ui.pushCenterBox(); + defer self.ui.popCenterBox(); + modal.background = srcery.hard_black; + + self.showModal(); + + _ = self.ui.signalFromBox(modal_overlay); + } + self.showToolbar(); if (self.shown_window == .channels) { @@ -786,6 +971,10 @@ fn updateUI(self: *App) !void { } else if (self.shown_window == .add_from_device) { try self.showAddFromDeviceWindow(); } + + if (maybe_modal_overlay) |box| { + self.ui.appendBox(box); + } } pub fn tick(self: *App) !void { @@ -822,7 +1011,6 @@ pub fn tick(self: *App) !void { } } - // On the first frame, render the UI twice. // So that on the second pass widgets that depend on sizes from other widgets have settled if (self.ui.frame_index == 0) { diff --git a/src/assets.zig b/src/assets.zig index f58f8db..290350a 100644 --- a/src/assets.zig +++ b/src/assets.zig @@ -60,12 +60,18 @@ pub fn init(allocator: std.mem.Allocator) !void { } fn loadFont(ttf_data: []const u8, font_size: u32) !rl.Font { - var codepoints: [95]i32 = undefined; - for (0..codepoints.len) |i| { - codepoints[i] = @as(i32, @intCast(i)) + 32; + var codepoints: std.BoundedArray(i32, 128) = .{}; + for (0..95) |i| { + codepoints.appendAssumeCapacity(@as(i32, @intCast(i)) + 32); } - const loaded_font = rl.loadFontFromMemory(".ttf", ttf_data, @intCast(font_size), &codepoints); + const lithuanina_characters = std.unicode.Utf8View.initComptime("ąčęėįšųūžĄČĘĖĮŠŲŪŽ"); + var char_iter = lithuanina_characters.iterator(); + while (char_iter.nextCodepoint()) |codepoint| { + codepoints.appendAssumeCapacity(codepoint); + } + + const loaded_font = rl.loadFontFromMemory(".ttf", ttf_data, @intCast(font_size), codepoints.slice()); if (!loaded_font.isReady()) { return error.LoadFontFromMemory; } diff --git a/src/font-face.zig b/src/font-face.zig index d745fd1..fd530a7 100644 --- a/src/font-face.zig +++ b/src/font-face.zig @@ -134,6 +134,10 @@ pub fn measureText(self: @This(), text: []const u8) rl.Vector2 { return text_size; } +pub fn measureWidth(self: @This(), text: []const u8) f32 { + return self.measureText(text).x; +} + pub fn drawTextCenter(self: @This(), text: []const u8, position: rl.Vector2, tint: rl.Color) void { const text_size = self.measureText(text); const adjusted_position = rl.Vector2{ diff --git a/src/main.zig b/src/main.zig index ad0d3e6..6d5af4b 100644 --- a/src/main.zig +++ b/src/main.zig @@ -4,6 +4,7 @@ const builtin = @import("builtin"); const Application = @import("./app.zig"); const Assets = @import("./assets.zig"); const Profiler = @import("./profiler.zig"); +const Platform = @import("./platform.zig"); const raylib_h = @cImport({ @cInclude("stdio.h"); @cInclude("raylib.h"); @@ -64,6 +65,8 @@ fn raylibTraceLogCallback(logType: c_int, text: [*c]const u8, args: raylib_h.va_ } pub fn main() !void { + Platform.init(); + // TODO: Setup logging to a file raylib_h.SetTraceLogCallback(raylibTraceLogCallback); rl.setTraceLogLevel(toRaylibLogLevel(std.options.log_level)); diff --git a/src/ni-daq/api.zig b/src/ni-daq/api.zig index e5ef5c5..c5b656d 100644 --- a/src/ni-daq/api.zig +++ b/src/ni-daq/api.zig @@ -9,6 +9,17 @@ const Api = @This(); const log = std.log.scoped(.ni_daq_api); +pub const min_version = std.SemanticVersion{ + .major = 24, + .minor = 8, + .patch = 0 +}; + +pub const Error = error { + LibraryNotFound, + SymbolNotFound +}; + lib: std.DynLib, // This MUST be the first field of `Api` struct DAQmxGetSysNIDAQMajorVersion: *const @TypeOf(c.DAQmxGetSysNIDAQMajorVersion), @@ -36,10 +47,10 @@ DAQmxGetDevAIPhysicalChans: *const @TypeOf(c.DAQmxGetDevAIPhysicalChans), DAQmxGetDevAOPhysicalChans: *const @TypeOf(c.DAQmxGetDevAOPhysicalChans), DAQmxReadAnalogF64: *const @TypeOf(c.DAQmxReadAnalogF64), -pub fn init() !Api { +pub fn init() Error!Api { var api: Api = undefined; - api.lib = try std.DynLib.open("nicaiu"); + api.lib = std.DynLib.open("nicaiu") catch return error.LibraryNotFound; errdefer api.lib.close(); inline for (@typeInfo(Api).Struct.fields[1..]) |field| { @@ -47,7 +58,7 @@ pub fn init() !Api { const name_z = name[0 .. (name.len - 1) :0]; @field(api, field.name) = api.lib.lookup(field.type, name_z) orelse { log.err("Symbol lookup failed for {s}", .{name}); - return error.SymbolLookup; + return error.SymbolNotFound; }; } @@ -56,4 +67,34 @@ pub fn init() !Api { pub fn deinit(self: *Api) void { self.lib.close(); +} + +pub fn version() !std.SemanticVersion { + var lib = std.DynLib.open("nicaiu") catch return error.LibraryNotFound; + defer lib.close(); + + const getMajorVersion = lib.lookup(*const @TypeOf(c.DAQmxGetSysNIDAQMajorVersion), "DAQmxGetSysNIDAQMajorVersion") orelse return error.SymbolNotFound; + const getMinorVersion = lib.lookup(*const @TypeOf(c.DAQmxGetSysNIDAQMinorVersion), "DAQmxGetSysNIDAQMinorVersion") orelse return error.SymbolNotFound; + const getUpdateVersion = lib.lookup(*const @TypeOf(c.DAQmxGetSysNIDAQUpdateVersion), "DAQmxGetSysNIDAQUpdateVersion") orelse return error.SymbolNotFound; + + var major: u32 = 0; + if (getMajorVersion(&major) < 0) { + return error.GetMajorVersion; + } + + var minor: u32 = 0; + if (getMinorVersion(&minor) < 0) { + return error.GetMinorVersion; + } + + var update: u32 = 0; + if (getUpdateVersion(&update) < 0) { + return error.GetUpdateVersion; + } + + return std.SemanticVersion{ + .major = major, + .minor = minor, + .patch = update + }; } \ No newline at end of file diff --git a/src/ni-daq/root.zig b/src/ni-daq/root.zig index 161299d..e41ebb5 100644 --- a/src/ni-daq/root.zig +++ b/src/ni-daq/root.zig @@ -1,5 +1,5 @@ const std = @import("std"); -const Api = @import("./api.zig"); +pub const Api = @import("./api.zig"); pub const c = Api.c; const assert = std.debug.assert; @@ -373,16 +373,13 @@ const DeviceBuffers = struct { }; options: Options, -api: Api, +api: *Api, device_names_buffer: []u8, device_names: StringArrayListUnmanaged, device_buffers: []DeviceBuffers, -pub fn init(allocator: std.mem.Allocator, options: Options) !NIDaq { - var api = try Api.init(); - errdefer api.deinit(); - +pub fn init(allocator: std.mem.Allocator, api: *Api, options: Options) !NIDaq { const device_names_buffer_size = options.max_devices * (max_device_name_size + 2); const device_names_buffer = try allocator.alloc(u8, device_names_buffer_size); errdefer allocator.free(device_names_buffer); @@ -408,8 +405,6 @@ pub fn init(allocator: std.mem.Allocator, options: Options) !NIDaq { } pub fn deinit(self: *NIDaq, allocator: std.mem.Allocator) void { - self.api.deinit(); - self.device_names.deinit(allocator); allocator.free(self.device_names_buffer); diff --git a/src/task-pool.zig b/src/ni-daq/task-pool.zig similarity index 94% rename from src/task-pool.zig rename to src/ni-daq/task-pool.zig index d620960..fc5c7f6 100644 --- a/src/task-pool.zig +++ b/src/ni-daq/task-pool.zig @@ -1,5 +1,5 @@ const std = @import("std"); -const NIDaq = @import("./ni-daq/root.zig"); +const NIDaq = @import("./root.zig"); const assert = std.debug.assert; const log = std.log.scoped(.task_pool); @@ -43,12 +43,10 @@ pub const Entry = struct { running: bool = false, read_thread: std.Thread, -ni_daq: *NIDaq, entries: [max_tasks]Entry = undefined, -pub fn init(self: *TaskPool, allocator: std.mem.Allocator, ni_daq: *NIDaq) !void { +pub fn init(self: *TaskPool, allocator: std.mem.Allocator) !void { self.* = TaskPool{ - .ni_daq = ni_daq, .read_thread = undefined }; @@ -132,12 +130,13 @@ fn findFreeEntry(self: *TaskPool) ?*Entry { pub fn launchAIVoltageChannel( self: *TaskPool, + ni_daq: *NIDaq, mutex: *std.Thread.Mutex, samples: *std.ArrayList(f64), sampling: Sampling, options: NIDaq.Task.AIVoltageChannelOptions ) !*Entry { - const task = try self.ni_daq.createTask(null); + const task = try ni_daq.createTask(null); errdefer task.clear(); const entry = self.findFreeEntry() orelse return error.NotEnoughSpace; diff --git a/src/platform.zig b/src/platform.zig index c3a454a..88c607a 100644 --- a/src/platform.zig +++ b/src/platform.zig @@ -132,4 +132,10 @@ pub fn openFilePicker() !std.fs.File { // TODO: Use the `openFileAbsoluteW` function. // Could not get it to work, because it always threw OBJECT_PATH_SYNTAX_BAD error return try std.fs.openFileAbsolute(filename, .{ }); +} + +pub fn init() void { + if (builtin.os.tag == .windows) { + _ = windows_h.SetConsoleOutputCP(65001); + } } \ No newline at end of file diff --git a/src/ui.zig b/src/ui.zig index 5c7e461..d640a53 100644 --- a/src/ui.zig +++ b/src/ui.zig @@ -3,6 +3,7 @@ const rl = @import("raylib"); const Assets = @import("./assets.zig"); const rect_utils = @import("./rect-utils.zig"); const srcery = @import("./srcery.zig"); +const FontFace = @import("./font-face.zig"); const log = std.log.scoped(.ui); const assert = std.debug.assert; @@ -242,6 +243,9 @@ pub const Box = struct { pub const Flag = enum { clickable, + highlight_hot, + highlight_active, + draggable_x, draggable_y, @@ -250,7 +254,15 @@ pub const Box = struct { fixed_x, fixed_y, fixed_width, - fixed_height + fixed_height, + + hover_mouse_hand, + + skip_draw, + + text_underline, + text_wrapping, + text_left_align }; pub const Flags = std.EnumSet(Flag); @@ -305,7 +317,19 @@ pub const Box = struct { }; } - pub fn setAllocText(self: *Box, font: Assets.FontId, comptime fmt: []const u8, args: anytype) void { + pub fn appendText(self: *Box, text: []const u8) void { + if (self.text == null) { + self.text = .{ + .content = self.allocator.alloc(u8, 0) catch return, + .font = .text + }; + } + + const new_content = std.mem.concat(self.allocator, u8, &.{ self.text.?.content, text }) catch return; + self.text.?.content = new_content; + } + + pub fn setFmtText(self: *Box, font: Assets.FontId, comptime fmt: []const u8, args: anytype) void { self.text = .{ .content = std.fmt.allocPrint(self.allocator, fmt, args) catch return, .font = font @@ -543,7 +567,7 @@ pub fn end(self: *UI) void { rl.setMouseCursor(@intFromEnum(rl.MouseCursor.mouse_cursor_resize_ew)); } else if (active_box_flags.contains(.draggable_y)) { rl.setMouseCursor(@intFromEnum(rl.MouseCursor.mouse_cursor_resize_ns)); - } else if (hover_box_flags.contains(.clickable)) { + } else if (hover_box_flags.contains(.hover_mouse_hand)) { rl.setMouseCursor(@intFromEnum(rl.MouseCursor.mouse_cursor_pointing_hand)); } else { rl.setMouseCursor(@intFromEnum(rl.MouseCursor.mouse_cursor_default)); @@ -645,6 +669,10 @@ pub fn draw(self: *UI) void { } fn drawBox(self: *UI, box: *Box) void { + if (box.flags.contains(.skip_draw)) { + return; + } + const box_rect = box.computedRect(); const do_scissor = box.hasClipping(); @@ -662,10 +690,14 @@ fn drawBox(self: *UI, box: *Box) void { rl.drawRectangleRec(box_rect, background); } - if (self.isBoxActive(box.key)) { - rl.drawRectangleLinesEx(box_rect, 2, rl.Color.orange); - } else if (self.isBoxHot(box.key)) { - rl.drawRectangleLinesEx(box_rect, 3, rl.Color.blue); + if (self.isKeyActive(box.key)) { + if (box.flags.contains(.highlight_active)) { + rl.drawRectangleLinesEx(box_rect, 2, rl.Color.orange); + } + } else if (self.isKeyHot(box.key)) { + if (box.flags.contains(.highlight_hot)) { + rl.drawRectangleLinesEx(box_rect, 3, rl.Color.blue); + } } if (box.texture) |texture| { @@ -686,7 +718,38 @@ fn drawBox(self: *UI, box: *Box) void { if (box.text) |text| { const font = Assets.font(text.font); - font.drawTextCenter(text.content, rect_utils.center(box_rect), text.color); + const text_size = font.measureText(text.content); + var text_rect = Rect{ + .x = box_rect.x, + .y = box_rect.y, + .width = text_size.x, + .height = text_size.y + }; + + if (box.flags.contains(.text_left_align)) { + if (box.size.x.kind == .text) { + const padding = box.size.x.kind.text; + text_rect.x += padding * font.getSize() / 2; + } + + if (box.size.y.kind == .text) { + const padding = box.size.y.kind.text; + text_rect.y += padding * font.getSize() / 2; + } + } else { + text_rect.x += (box_rect.width - text_size.x) / 2; + text_rect.y += (box_rect.height - text_size.y) / 2; + } + + font.drawText(text.content, .{ .x = text_rect.x, .y = text_rect.y }, text.color); + + if (box.flags.contains(.text_underline)) { + rl.drawLineV( + rect_utils.bottomLeft(text_rect), + rect_utils.bottomRight(text_rect), + text.color + ); + } } var child_iter = self.iterChildrenByParent(box); @@ -695,9 +758,9 @@ fn drawBox(self: *UI, box: *Box) void { } if (debug) { - if (self.isBoxActive(box.key)) { + if (self.isKeyActive(box.key)) { rl.drawRectangleLinesEx(box_rect, 3, rl.Color.red); - } else if (self.isBoxHot(box.key)) { + } else if (self.isKeyHot(box.key)) { rl.drawRectangleLinesEx(box_rect, 3, rl.Color.orange); } else { rl.drawRectangleLinesEx(box_rect, 1, rl.Color.pink); @@ -790,6 +853,8 @@ fn calcLayoutDownardsSize(self: *UI, box: *Box, axis: Axis) void { const computed_size = getVec2Axis(&box.persistent.size, axis); if (size.kind == .children_sum) { + var first_child: bool = true; + var sum: f32 = 0; var child_iter = self.iterChildrenByParent(box); while (child_iter.next()) |child| { @@ -799,83 +864,36 @@ fn calcLayoutDownardsSize(self: *UI, box: *Box, axis: Axis) void { if (box.layout_axis == axis) { sum += child_size; + if (!first_child) { + sum += box.layout_gap; + } } else { sum = @max(sum, child_size); } + + first_child = false; } computed_size.* = sum; } } -fn calcLayoutPositions(self: *UI, box: *Box, axis: Axis) void { - { - var layout_position: f32 = 0; - - var child_iter = self.iterChildrenByParent(box); - while (child_iter.next()) |child| { - const child_axis_position = getVec2Axis(&child.persistent.position, axis); - - if (child.getFixedPositionAxis(axis)) |position| { - child_axis_position.* = position; - } else { - const child_axis_size = getVec2Axis(&child.persistent.size, axis); - const parent_axis_position = getVec2Axis(&box.persistent.position, axis); - - child_axis_position.* = parent_axis_position.*; - - if (box.layout_axis == axis) { - child_axis_position.* += layout_position; - layout_position += child_axis_size.*; - layout_position += box.layout_gap; - } - } - - child_axis_position.* -= getVec2Axis(&box.view_offset, axis).*; - } - } - - if (box.layout_axis == axis) { - var child_size_sum: f32 = 0; - var child_count: f32 = 0; - - var child_iter = self.iterChildrenByParent(box); - while (child_iter.next()) |child| { - if (child.isPositionFixed(axis)) continue; - - const child_size = getVec2Axis(&child.persistent.size, axis); - child_size_sum += child_size.*; - child_count += 1; - } - - if (child_count > 1) { - child_size_sum += (child_count - 1) * box.layout_gap; - } - - getVec2Axis(&box.persistent.children_size, axis).* = child_size_sum; - } else { - var max_child_size: f32 = 0; - - var child_iter = self.iterChildrenByParent(box); - while (child_iter.next()) |child| { - if (child.isPositionFixed(axis)) continue; - - const child_size = getVec2Axis(&child.persistent.size, axis); - max_child_size = @max(max_child_size, child_size.*); - } - - getVec2Axis(&box.persistent.children_size, axis).* = max_child_size; - } - - { - var child_iter = self.iterChildrenByParent(box); - while (child_iter.next()) |child| { - self.calcLayoutPositions(child, axis); - } - } -} - fn calcLayoutEnforceConstraints(self: *UI, box: *Box, axis: Axis) void { + if (box.text != null and box.flags.contains(.text_wrapping)) { + const text = box.text.?; + const font = Assets.font(text.font); + const axis_size = box.size.getAxis(axis); + + var max_width = box.persistent.size.x; + if (axis_size.kind == .text) { + max_width -= axis_size.kind.text * font.getSize(); + } + + if (wrapText(box.allocator, font, text.content, max_width) catch null) |wrapped_content| { + box.text.?.content = wrapped_content; + } + } + // Children can't be wider than the parent on the secondary axis if (box.layout_axis != axis) { const max_child_size = getVec2Axis(&box.persistent.size, axis).*; @@ -949,12 +967,134 @@ fn calcLayoutEnforceConstraints(self: *UI, box: *Box, axis: Axis) void { } } -pub fn newKeyFromString(self: *UI, text: []const u8) Key { - var parent_hash: u64 = 0; - if (self.getParent()) |parent| { - parent_hash = parent.key.hash; +fn calcLayoutPositions(self: *UI, box: *Box, axis: Axis) void { + { + var layout_position: f32 = 0; + + var child_iter = self.iterChildrenByParent(box); + while (child_iter.next()) |child| { + const child_axis_position = getVec2Axis(&child.persistent.position, axis); + + if (child.getFixedPositionAxis(axis)) |position| { + child_axis_position.* = position; + } else { + const child_axis_size = getVec2Axis(&child.persistent.size, axis); + const parent_axis_position = getVec2Axis(&box.persistent.position, axis); + + child_axis_position.* = parent_axis_position.*; + + if (box.layout_axis == axis) { + child_axis_position.* += layout_position; + layout_position += child_axis_size.*; + layout_position += box.layout_gap; + } + } + + child_axis_position.* -= getVec2Axis(&box.view_offset, axis).*; + } } - return Key.initString(parent_hash, text); + + if (box.layout_axis == axis) { + var child_size_sum: f32 = 0; + var child_count: f32 = 0; + + var child_iter = self.iterChildrenByParent(box); + while (child_iter.next()) |child| { + if (child.isPositionFixed(axis)) continue; + + const child_size = getVec2Axis(&child.persistent.size, axis); + child_size_sum += child_size.*; + child_count += 1; + } + + if (child_count > 1) { + child_size_sum += (child_count - 1) * box.layout_gap; + } + + getVec2Axis(&box.persistent.children_size, axis).* = child_size_sum; + } else { + var max_child_size: f32 = 0; + + var child_iter = self.iterChildrenByParent(box); + while (child_iter.next()) |child| { + if (child.isPositionFixed(axis)) continue; + + const child_size = getVec2Axis(&child.persistent.size, axis); + max_child_size = @max(max_child_size, child_size.*); + } + + getVec2Axis(&box.persistent.children_size, axis).* = max_child_size; + } + + { + var child_iter = self.iterChildrenByParent(box); + while (child_iter.next()) |child| { + self.calcLayoutPositions(child, axis); + } + } +} + +pub fn wrapText(allocator: std.mem.Allocator, font: FontFace, text: []const u8, max_width: f32) !?[]u8 { + // TODO: Implement word wrapping that doesn't include re-allocating the whole text + if (font.measureWidth(text) < max_width) { + return null; + } + + var result = try std.ArrayList(u8).initCapacity(allocator, text.len); + defer result.deinit(); + + var line_buffer = std.ArrayList(u8).init(allocator); + defer line_buffer.deinit(); + + var word_iter = std.mem.splitScalar(u8, text, ' '); + while (word_iter.next()) |word| { + try line_buffer.ensureUnusedCapacity(word.len + 1); + + const current_line = line_buffer.items; + if (line_buffer.items.len > 0) { + try line_buffer.append(' '); + } + try line_buffer.appendSlice(word); + + if (font.measureWidth(line_buffer.items) > max_width) { + if (result.items.len > 0) { + try result.append('\n'); + } + try result.appendSlice(current_line); + + line_buffer.clearRetainingCapacity(); + try line_buffer.appendSlice(word); + } + } + + if (line_buffer.items.len > 0) { + if (result.items.len > 0) { + try result.append('\n'); + } + try result.appendSlice(line_buffer.items); + } + + return try result.toOwnedSlice(); +} + +fn getKeySeed(self: *UI) u64 { + var maybe_current = self.getParent(); + while (maybe_current) |current| { + if (!current.key.isNil()) { + return current.key.hash; + } + + maybe_current = null; + if (current.parent_index) |parent_index| { + maybe_current = &self.boxes.buffer[parent_index]; + } + } + + return 0; +} + +pub fn newKeyFromString(self: *UI, text: []const u8) Key { + return Key.initString(self.getKeySeed(), text); } pub fn newBoxFromString(self: *UI, text: []const u8) *Box { @@ -965,7 +1105,26 @@ pub fn newBoxFromPtr(self: *UI, ptr: anytype) *Box { return self.newBox(Key.initPtr(ptr)); } -pub fn newBox(self: *UI, key: Key) *Box { +pub fn appendBox(self: *UI, box: *Box) void { + assert(box.parent_index == null); + + if (self.getParentIndex()) |parent_index| { + const parent = &self.boxes.buffer[parent_index]; + box.parent_index = parent_index; + + if (parent.last_child_index) |last_child_index| { + const last_child = &self.boxes.buffer[last_child_index]; + + last_child.next_sibling_index = box.index; + parent.last_child_index = box.index; + } else { + parent.first_child_index = box.index; + parent.last_child_index = box.index; + } + } +} + +pub fn newBoxNoAppend(self: *UI, key: Key) *Box { var box: *Box = undefined; var box_index: ?BoxIndex = null; var persistent: Box.Persistent = .{}; @@ -995,21 +1154,12 @@ pub fn newBox(self: *UI, key: Key) *Box { .index = box_index.? }; - if (self.getParentIndex()) |parent_index| { - const parent = &self.boxes.buffer[parent_index]; - box.parent_index = parent_index; - - if (parent.last_child_index) |last_child_index| { - const last_child = &self.boxes.buffer[last_child_index]; - - last_child.next_sibling_index = box.index; - parent.last_child_index = box.index; - } else { - parent.first_child_index = box.index; - parent.last_child_index = box.index; - } - } + return box; +} +pub fn newBox(self: *UI, key: Key) *Box { + const box = self.newBoxNoAppend(key); + self.appendBox(box); return box; } @@ -1056,7 +1206,7 @@ pub fn signalFromBox(self: *UI, box: *Box) Signal { while (event_index < self.events.len) { var taken = false; const event: Event = self.events.buffer[event_index]; - const is_active = self.isBoxActive(key); + const is_active = self.isKeyActive(key); if (event == .mouse_pressed and clickable and is_mouse_inside) { const mouse_button = event.mouse_pressed; @@ -1119,7 +1269,11 @@ pub fn signalFromBox(self: *UI, box: *Box) Signal { return result; } -fn isBoxHot(self: *UI, key: Key) bool { +pub fn isBoxHot(self: *UI, box: *Box) bool { + return self.isKeyHot(box.key); +} + +pub fn isKeyHot(self: *UI, key: Key) bool { if (self.hot_box_key) |hot_box_key| { return hot_box_key.eql(key); } else { @@ -1127,7 +1281,7 @@ fn isBoxHot(self: *UI, key: Key) bool { } } -fn isBoxActive(self: *UI, key: Key) bool { +pub fn isKeyActive(self: *UI, key: Key) bool { inline for (std.meta.fields(rl.MouseButton)) |mouse_button_field| { const mouse_button: rl.MouseButton = @enumFromInt(mouse_button_field.value); @@ -1195,6 +1349,7 @@ pub fn pushScrollbar(self: *UI, key: UI.Key) *Box { self.pushParent(container); const content_area = self.newBoxFromString("Scroll area"); + content_area.flags.insert(.skip_draw); content_area.flags.insert(.scrollable); content_area.size = .{ .x = UI.Size.percent(1, 0), @@ -1215,58 +1370,62 @@ pub fn popScrollbar(self: *UI) void { const content_area = self.getParent().?; self.popParent(); // pop scroll area - const scrollbar_area = self.newBoxFromString("Scrollbar area"); - scrollbar_area.background = rl.Color.gold; - scrollbar_area.flags.insert(.scrollable); - scrollbar_area.size = .{ - .x = UI.Size.pixels(24, 1), - .y = UI.Size.percent(1, 0), - }; - self.pushParent(scrollbar_area); - defer self.popParent(); - const visible_content_size = content_area.persistent.size.y; const used_content_size = content_area.persistent.children_size.y; const visible_percent = clamp(visible_content_size / used_content_size, 0, 1); - - const draggable = self.newBoxFromString("Scrollbar button"); - draggable.background = rl.Color.dark_brown; - draggable.flags.insert(.clickable); - draggable.flags.insert(.draggable_y); - draggable.size = .{ - .x = UI.Size.percent(1, 1), - .y = UI.Size.percent(visible_percent, 1), - }; - - const sroll_offset = &content_area.persistent.sroll_offset; - const scrollbar_height = scrollbar_area.persistent.size.y; - const max_offset = scrollbar_height * (1 - visible_percent); - draggable.setFixedY(scrollbar_area.persistent.position.y + sroll_offset.* * max_offset); - - const draggable_signal = self.signalFromBox(draggable); - if (draggable_signal.dragged()) { - sroll_offset.* += draggable_signal.drag.y / max_offset; + if (used_content_size != 0) { + content_area.flags.remove(.skip_draw); } - const scroll_speed = 16; - const scrollbar_signal = self.signalFromBox(scrollbar_area); - if (scrollbar_signal.scrolled()) { - sroll_offset.* -= scrollbar_signal.scroll.y / max_offset * scroll_speed; - } + if (!content_area.flags.contains(.skip_draw)) { + const scrollbar_area = self.newBoxFromString("Scrollbar area"); + scrollbar_area.background = rl.Color.gold; + scrollbar_area.flags.insert(.scrollable); + scrollbar_area.size = .{ + .x = UI.Size.pixels(24, 1), + .y = UI.Size.percent(1, 0), + }; + self.pushParent(scrollbar_area); + defer self.popParent(); - const content_area_signal = self.signalFromBox(content_area); - if (content_area_signal.scrolled()) { - sroll_offset.* -= content_area_signal.scroll.y / max_offset * scroll_speed; - } + const draggable = self.newBoxFromString("Scrollbar button"); + draggable.background = rl.Color.dark_brown; + draggable.flags.insert(.clickable); + draggable.flags.insert(.draggable_y); + draggable.size = .{ + .x = UI.Size.percent(1, 1), + .y = UI.Size.percent(visible_percent, 1), + }; - sroll_offset.* = clamp(sroll_offset.*, 0, 1); + const sroll_offset = &content_area.persistent.sroll_offset; + const scrollbar_height = content_area.persistent.size.y; + const max_offset = scrollbar_height * (1 - visible_percent); + draggable.setFixedY(content_area.persistent.position.y + sroll_offset.* * max_offset); + + const draggable_signal = self.signalFromBox(draggable); + if (draggable_signal.dragged()) { + sroll_offset.* += draggable_signal.drag.y / max_offset; + } + + const scroll_speed = 16; + const scrollbar_signal = self.signalFromBox(scrollbar_area); + if (scrollbar_signal.scrolled()) { + sroll_offset.* -= scrollbar_signal.scroll.y / max_offset * scroll_speed; + } + + const content_area_signal = self.signalFromBox(content_area); + if (content_area_signal.scrolled()) { + sroll_offset.* -= content_area_signal.scroll.y / max_offset * scroll_speed; + } + + sroll_offset.* = clamp(sroll_offset.*, 0, 1); + } self.popParent(); // pop container } pub fn button(self: *UI, font: Assets.FontId, text: []const u8) *Box { - const box = self.newBoxFromString(text); - box.flags.insert(.clickable); + const box = self.clickableBox(text); box.size.x = UI.Size.text(1, 1); box.size.y = UI.Size.text(0.5, 1); box.setText(font, text); @@ -1274,7 +1433,64 @@ pub fn button(self: *UI, font: Assets.FontId, text: []const u8) *Box { return box; } +pub fn clickableBox(self: *UI, key: []const u8) *Box { + const box = self.newBoxFromString(key); + box.flags.insert(.clickable); + box.flags.insert(.highlight_active); + box.flags.insert(.highlight_hot); + box.flags.insert(.hover_mouse_hand); + + return box; +} + pub fn spacer(self: *UI, size: Vec2Size) void { const box = self.newBox(UI.Key.initNil()); box.size = size; +} + +pub fn pushCenterBox(self: *UI) *Box { + self.pushHorizontalAlign(); + + const vertical_align = self.newBox(UI.Key.initNil()); + vertical_align.layout_axis = .Y; + vertical_align.size = .{ + .x = UI.Size.childrenSum(1), + .y = UI.Size.percent(1, 0), + }; + self.pushParent(vertical_align); + + self.spacer(.{ .y = UI.Size.percent(1, 0) }); + + const container = self.newBox(UI.Key.initNil()); + container.size.x = UI.Size.childrenSum(1); + container.size.y = UI.Size.childrenSum(1); + self.pushParent(container); + + return container; +} + +pub fn popCenterBox(self: *UI) void { + self.popParent(); + + self.spacer(.{ .y = UI.Size.percent(1, 0) }); + self.popParent(); + + self.popHorizontalAlign(); +} + +pub fn pushHorizontalAlign(self: *UI) void { + const horizontal_align = self.newBox(UI.Key.initNil()); + horizontal_align.layout_axis = .X; + horizontal_align.size = .{ + .x = UI.Size.percent(1, 0), + .y = UI.Size.childrenSum(1), + }; + self.pushParent(horizontal_align); + + self.spacer(.{ .x = UI.Size.percent(1, 0) }); +} + +pub fn popHorizontalAlign(self: *UI) void { + self.spacer(.{ .x = UI.Size.percent(1, 0) }); + self.popParent(); } \ No newline at end of file