show message if NI Daq fails to be loaded

This commit is contained in:
Rokas Puzonas 2025-02-08 23:10:42 +02:00
parent f563ff931b
commit 7301c68b7e
12 changed files with 783 additions and 292 deletions

View File

@ -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.
* 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 <batch> -b "(gimp-quit 0)"
```

View File

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

View File

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

View File

@ -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,23 +92,42 @@ 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, .{
self.* = App{
.allocator = allocator,
.ui = UI.init(allocator),
.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);
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,
@ -117,22 +136,35 @@ pub fn init(self: *App, allocator: std.mem.Allocator) !void {
.max_analog_input_voltage_ranges = 4,
.max_analog_output_voltage_ranges = 4
});
errdefer ni_daq.deinit(allocator);
self.ni_daq = ni_daq;
self.* = App{
.allocator = allocator,
.ui = UI.init(allocator),
.ni_daq = ni_daq,
.task_pool = undefined
};
const installed_version = try ni_daq.version();
if (installed_version.order(NIDaq.Api.min_version) == .lt) {
self.shown_modal = .{ .library_version_warning = installed_version };
}
try TaskPool.init(&self.task_pool, allocator, &self.ni_daq);
} 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) {

View File

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

View File

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

View File

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

View File

@ -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;
};
}
@ -57,3 +68,33 @@ 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
};
}

View File

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

View File

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

View File

@ -133,3 +133,9 @@ pub fn openFilePicker() !std.fs.File {
// 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);
}
}

View File

@ -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,11 +690,15 @@ fn drawBox(self: *UI, box: *Box) void {
rl.drawRectangleRec(box_rect, background);
}
if (self.isBoxActive(box.key)) {
if (self.isKeyActive(box.key)) {
if (box.flags.contains(.highlight_active)) {
rl.drawRectangleLinesEx(box_rect, 2, rl.Color.orange);
} else if (self.isBoxHot(box.key)) {
}
} else if (self.isKeyHot(box.key)) {
if (box.flags.contains(.highlight_hot)) {
rl.drawRectangleLinesEx(box_rect, 3, rl.Color.blue);
}
}
if (box.texture) |texture| {
const source = rl.Rectangle{
@ -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;
}
return Key.initString(parent_hash, text);
}
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);
}
}
}
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,6 +1370,14 @@ pub fn popScrollbar(self: *UI) void {
const content_area = self.getParent().?;
self.popParent(); // pop scroll area
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);
if (used_content_size != 0) {
content_area.flags.remove(.skip_draw);
}
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);
@ -1225,10 +1388,6 @@ pub fn popScrollbar(self: *UI) void {
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);
@ -1239,9 +1398,9 @@ pub fn popScrollbar(self: *UI) void {
};
const sroll_offset = &content_area.persistent.sroll_offset;
const scrollbar_height = scrollbar_area.persistent.size.y;
const scrollbar_height = content_area.persistent.size.y;
const max_offset = scrollbar_height * (1 - visible_percent);
draggable.setFixedY(scrollbar_area.persistent.position.y + sroll_offset.* * max_offset);
draggable.setFixedY(content_area.persistent.position.y + sroll_offset.* * max_offset);
const draggable_signal = self.signalFromBox(draggable);
if (draggable_signal.dragged()) {
@ -1260,13 +1419,13 @@ pub fn popScrollbar(self: *UI) void {
}
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();
}