show message if NI Daq fails to be loaded
This commit is contained in:
parent
f563ff931b
commit
7301c68b7e
22
README.md
22
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.
|
||||
|
||||
* 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)"
|
||||
```
|
@ -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" }) });
|
||||
|
@ -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 = .{
|
||||
|
446
src/app.zig
446
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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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{
|
||||
|
@ -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));
|
||||
|
@ -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
|
||||
};
|
||||
}
|
@ -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);
|
||||
|
||||
|
@ -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;
|
@ -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);
|
||||
}
|
||||
}
|
496
src/ui.zig
496
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();
|
||||
}
|
Loading…
Reference in New Issue
Block a user