Compare commits

..

3 Commits

22 changed files with 9116 additions and 385 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.
* 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)"
```

114
build.zig
View File

@ -1,15 +1,105 @@
const std = @import("std");
const Module = std.Build.Module;
const AddExecutableIcon = struct {
obj: *std.Build.Step.Compile,
step: std.Build.Step,
icon_file: std.Build.LazyPath,
resource_file: std.Build.LazyPath,
fn init(obj: *std.Build.Step.Compile, icon_file: std.Build.LazyPath) *AddExecutableIcon {
const b = obj.step.owner;
const self = b.allocator.create(AddExecutableIcon) catch @panic("OOM");
self.obj = obj;
self.step = std.Build.Step.init(.{
.id = .custom,
.name = "add executable icon",
.owner = b,
.makeFn = make
});
self.icon_file = icon_file;
icon_file.addStepDependencies(&self.step);
const write_files = b.addWriteFiles();
self.resource_file = write_files.add("resource-file.rc", "");
self.step.dependOn(&write_files.step);
self.obj.addWin32ResourceFile(.{
.file = self.resource_file,
});
return self;
}
fn make(step: *std.Build.Step, _: std.Progress.Node) !void {
const b = step.owner;
const self: *AddExecutableIcon = @fieldParentPtr("step", step);
const resource_file = try std.fs.createFileAbsolute(self.resource_file.getPath(b), .{ });
defer resource_file.close();
const relative_icon_path = try std.fs.path.relative(
b.allocator,
self.resource_file.dirname().getPath(b),
self.icon_file.getPath(b)
);
std.mem.replaceScalar(u8, relative_icon_path, '\\', '/');
try resource_file.writeAll("IDI_ICON ICON \"");
try resource_file.writeAll(relative_icon_path);
try resource_file.writeAll("\"");
}
};
fn buildStbImage(b: *std.Build) *std.Build.Module {
const module = b.createModule(.{
.root_source_file = b.path("libs/stb_image/root.zig"),
.link_libc = true
});
module.addCSourceFile(.{ .file = b.path("libs/stb_image/stb_image.c") });
module.addIncludePath(b.path("libs/stb_image/"));
return module;
}
fn buildCuteAseprite(b: *std.Build, raylib_dep: *std.Build.Dependency) *std.Build.Module {
const module = b.createModule(.{
.root_source_file = b.path("libs/cute_aseprite/root.zig"),
});
module.addCSourceFile(.{ .file = b.path("libs/cute_aseprite/cute_aseprite.c") });
module.addIncludePath(b.path("libs/cute_aseprite/"));
module.linkLibrary(raylib_dep.artifact("raylib"));
module.addImport("raylib", raylib_dep.module("raylib"));
return module;
}
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,
});
const stb_image_lib = buildStbImage(b);
const cute_aseprite_lib = buildCuteAseprite(b, raylib_dep);
const png_to_icon_tool = b.addExecutable(.{
.name = "png-to-icon",
.root_source_file = b.path("tools/png-to-icon.zig"),
.target = target,
});
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"),
@ -17,34 +107,28 @@ pub fn build(b: *std.Build) !void {
.optimize = optimize,
});
exe.addIncludePath(b.path("src"));
exe.addCSourceFile(.{
.file = b.path("src/cute_aseprite.c")
});
exe.linkLibrary(raylib_dep.artifact("raylib"));
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" }) });
const lib_folder_name = if (target.result.ptrBitWidth() == 64) "lib64" else "lib32";
exe.addLibraryPath(.{ .cwd_relative = try std.fs.path.join(b.allocator, &.{ external_compiler_support_dir, lib_folder_name, "msvc" }) });
exe.linkSystemLibrary("nidaqmx");
// TODO: Emebed all assets using build.zig
// https://github.com/ziglang/zig/issues/14637#issuecomment-1428689051
if (target.result.os.tag == .windows) {
exe.subsystem = if (optimize == .Debug) .Console else .Windows;
const resource_file = b.addWriteFiles();
const png_to_icon_step = b.addRunArtifact(png_to_icon_tool);
png_to_icon_step.addFileArg(b.path("src/assets/icon.png"));
const icon_file = png_to_icon_step.addOutputFileArg("icon.ico");
// https://www.ryanliptak.com/blog/zig-is-a-windows-resource-compiler/
// TODO: Generate icon file at build time
exe.addWin32ResourceFile(.{
.file = resource_file.add("daq-view.rc", "IDI_ICON ICON \"./src/assets/icon.ico\""),
});
const add_icon_step = AddExecutableIcon.init(exe, icon_file);
exe.step.dependOn(&add_icon_step.step);
exe.linkSystemLibrary("Comdlg32");
}

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

28
libs/stb_image/root.zig Normal file
View File

@ -0,0 +1,28 @@
const std = @import("std");
const assert = std.debug.assert;
extern fn zig_stbi_load_from_memory(buffer: [*]const u8, len: i32, x: ?*i32, y: ?*i32, comp: ?*i32, req_comp: i32) callconv(.C) ?[*]u8;
extern fn zig_stbi_image_free(buffer: ?*anyopaque) callconv(.C) void;
pub const Image = struct {
width: u32,
height: u32,
rgba: []u8,
pub fn deinit(self: Image) void {
zig_stbi_image_free(self.rgba.ptr);
}
};
pub fn load(buffer: []const u8) !Image {
var width: i32 = 0;
var height: i32 = 0;
const image_rgba = zig_stbi_load_from_memory(buffer.ptr, @intCast(buffer.len), &width, &height, null, 4);
if (image_rgba == null) {
return error.PNGDecode;
}
errdefer zig_stbi_image_free(image_rgba);
const byte_count: u32 = @intCast(width * height * 4);
return Image{ .width = @intCast(width), .height = @intCast(height), .rgba = image_rgba.?[0..byte_count] };
}

View File

@ -0,0 +1,23 @@
#include <stdint.h>
// TODO: Use zig allocators
extern void *stb_image_zig_malloc(uint32_t amount);
extern void *stb_image_zig_realloc(void *mem, uint32_t amount);
extern void stb_image_zig_free(void *mem);
#define STB_IMAGE_IMPLEMENTATION
#define STBI_NO_STDIO
#define STBI_ONLY_PNG
#define STB_IMAGE_STATIC
#include "stb_image.h"
void zig_stbi_image_free(void *retval_from_stbi_load)
{
return stbi_image_free(retval_from_stbi_load);
}
stbi_uc *zig_stbi_load_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp, int req_comp)
{
return stbi_load_from_memory(buffer, len, x, y, comp, req_comp);
}

7988
libs/stb_image/stb_image.h Normal file

File diff suppressed because it is too large Load Diff

View File

@ -5,10 +5,10 @@ const UI = @import("./ui.zig");
const Platform = @import("./platform.zig");
const Assets = @import("./assets.zig");
const Graph = @import("./graph.zig");
const NIDaq = @import("ni-daq.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) {

View File

@ -2,7 +2,7 @@ const std = @import("std");
const rl = @import("raylib");
const srcery = @import("./srcery.zig");
const FontFace = @import("./font-face.zig");
const Aseprite = @import("./aseprite.zig");
const Aseprite = @import("cute_aseprite");
const assert = std.debug.assert;
@ -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;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

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));
@ -153,7 +156,7 @@ pub fn main() !void {
defer app.deinit();
if (builtin.mode == .Debug) {
try app.appendChannelFromDevice("Dev1/ai0");
// try app.appendChannelFromDevice("Dev1/ai0");
// try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_I.bin");
// try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_IjStim.bin");
}
@ -191,5 +194,5 @@ pub fn main() !void {
}
test {
_ = @import("./ni-daq.zig");
_ = @import("./ni-daq/root.zig");
}

100
src/ni-daq/api.zig Normal file
View File

@ -0,0 +1,100 @@
const std = @import("std");
pub const c = @cImport({
@cInclude("stdint.h");
@cDefine("__int64", "long long");
@cInclude("NIDAQmx.h");
});
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),
DAQmxGetSysNIDAQMinorVersion: *const @TypeOf(c.DAQmxGetSysNIDAQMinorVersion),
DAQmxGetSysNIDAQUpdateVersion: *const @TypeOf(c.DAQmxGetSysNIDAQUpdateVersion),
DAQmxGetErrorString: *const @TypeOf(c.DAQmxGetErrorString),
DAQmxStopTask: *const @TypeOf(c.DAQmxStopTask),
DAQmxStartTask: *const @TypeOf(c.DAQmxStartTask),
DAQmxClearTask: *const @TypeOf(c.DAQmxClearTask),
DAQmxGetTaskName: *const @TypeOf(c.DAQmxGetTaskName),
DAQmxCfgSampClkTiming: *const @TypeOf(c.DAQmxCfgSampClkTiming),
DAQmxCreateTask: *const @TypeOf(c.DAQmxCreateTask),
DAQmxCreateAIVoltageChan: *const @TypeOf(c.DAQmxCreateAIVoltageChan),
DAQmxGetSysDevNames: *const @TypeOf(c.DAQmxGetSysDevNames),
DAQmxGetDevAIVoltageRngs: *const @TypeOf(c.DAQmxGetDevAIVoltageRngs),
DAQmxGetDevAOVoltageRngs: *const @TypeOf(c.DAQmxGetDevAOVoltageRngs),
DAQmxGetDevAIMaxSingleChanRate: *const @TypeOf(c.DAQmxGetDevAIMaxSingleChanRate),
DAQmxGetDevAOMaxRate: *const @TypeOf(c.DAQmxGetDevAOMaxRate),
DAQmxGetDevAIMinRate: *const @TypeOf(c.DAQmxGetDevAIMinRate),
DAQmxGetDevAOMinRate: *const @TypeOf(c.DAQmxGetDevAOMinRate),
DAQmxGetDevAISupportedMeasTypes: *const @TypeOf(c.DAQmxGetDevAISupportedMeasTypes),
DAQmxGetDevAOSupportedOutputTypes: *const @TypeOf(c.DAQmxGetDevAOSupportedOutputTypes),
DAQmxGetDevProductCategory: *const @TypeOf(c.DAQmxGetDevProductCategory),
DAQmxGetDevAIPhysicalChans: *const @TypeOf(c.DAQmxGetDevAIPhysicalChans),
DAQmxGetDevAOPhysicalChans: *const @TypeOf(c.DAQmxGetDevAOPhysicalChans),
DAQmxReadAnalogF64: *const @TypeOf(c.DAQmxReadAnalogF64),
pub fn init() Error!Api {
var api: Api = undefined;
api.lib = std.DynLib.open("nicaiu") catch return error.LibraryNotFound;
errdefer api.lib.close();
inline for (@typeInfo(Api).Struct.fields[1..]) |field| {
const name = std.fmt.comptimePrint("{s}\x00", .{field.name});
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.SymbolNotFound;
};
}
return 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,9 +1,6 @@
const std = @import("std");
pub const c = @cImport({
@cInclude("stdint.h");
@cDefine("__int64", "long long");
@cInclude("NIDAQmx.h");
});
pub const Api = @import("./api.zig");
pub const c = Api.c;
const assert = std.debug.assert;
const log = std.log.scoped(.ni_daq);
@ -38,6 +35,7 @@ pub const Options = struct {
};
pub const Task = struct {
ni_daq: *NIDaq,
handle: TaskHandle,
name_buffer: [max_task_name_size]u8 = undefined,
@ -45,19 +43,23 @@ pub const Task = struct {
dropped_samples: u32 = 0,
pub fn clear(self: Task) void {
logDAQmxError(c.DAQmxClearTask(self.handle));
// Ignore error
const api = self.ni_daq.api;
self.ni_daq.logDAQmxError(api.DAQmxClearTask(self.handle));
}
pub fn name(self: *Task) ![]const u8 {
const required_size = c.DAQmxGetTaskName(self.handle, null, 0);
const api = self.ni_daq.api;
const required_size = api.DAQmxGetTaskName(self.handle, null, 0);
assert(required_size >= 0);
if (required_size > self.name_buffer.len) {
return error.BufferTooSmall;
}
try checkDAQmxError(
c.DAQmxGetTaskName(
try self.ni_daq.checkDAQmxError(
api.DAQmxGetTaskName(
self.handle,
&self.name_buffer,
self.name_buffer.len
@ -69,29 +71,33 @@ pub const Task = struct {
}
pub fn start(self: Task) !void {
try checkDAQmxError(
c.DAQmxStartTask(self.handle),
const api = self.ni_daq.api;
try self.ni_daq.checkDAQmxError(
api.DAQmxStartTask(self.handle),
error.DAQmxStartTask
);
}
pub fn stop(self: Task) !void {
try checkDAQmxError(
c.DAQmxStopTask(self.handle),
const api = self.ni_daq.api;
try self.ni_daq.checkDAQmxError(
api.DAQmxStopTask(self.handle),
error.DAQmxStopTask
);
}
pub fn setContinousSampleRate(self: Task, sample_rate: f64) !void {
try checkDAQmxError(
c.DAQmxCfgSampClkTiming(self.handle, null, sample_rate, c.DAQmx_Val_Rising, c.DAQmx_Val_ContSamps, 0),
const api = self.ni_daq.api;
try self.ni_daq.checkDAQmxError(
api.DAQmxCfgSampClkTiming(self.handle, null, sample_rate, c.DAQmx_Val_Rising, c.DAQmx_Val_ContSamps, 0),
error.DAQmxCfgSampClkTiming
);
}
pub fn setFiniteSampleRate(self: Task, sample_rate: f64, samples_per_channel: u64) !void {
try checkDAQmxError(
c.DAQmxCfgSampClkTiming(self.handle, null, sample_rate, c.DAQmx_Val_Rising, c.DAQmx_Val_FiniteSamps, samples_per_channel),
const api = self.ni_daq.api;
try self.ni_daq.checkDAQmxError(
api.DAQmxCfgSampClkTiming(self.handle, null, sample_rate, c.DAQmx_Val_Rising, c.DAQmx_Val_FiniteSamps, samples_per_channel),
error.DAQmxCfgSampClkTiming
);
}
@ -107,8 +113,9 @@ pub const Task = struct {
};
pub fn createAIVoltageChannel(self: Task, options: AIVoltageChannelOptions) !void {
try checkDAQmxError(
c.DAQmxCreateAIVoltageChan(
const api = self.ni_daq.api;
try self.ni_daq.checkDAQmxError(
api.DAQmxCreateAIVoltageChan(
self.handle,
options.channel,
options.assigned_name,
@ -132,7 +139,7 @@ pub const Task = struct {
};
pub fn createAOVoltageChannel(self: Task, options: AOVoltageChannelOptions) !void {
try checkDAQmxError(
try self.ni_daq.checkDAQmxError(
c.DAQmxCreateAOVoltageChan(
self.handle,
options.channel,
@ -155,8 +162,9 @@ pub const Task = struct {
};
pub fn readAnalog(self: *Task, options: ReadAnalogOptions) !u32 {
const api = self.ni_daq.api;
var samples_per_channel: i32 = 0;
const err = c.DAQmxReadAnalogF64(
const err = api.DAQmxReadAnalogF64(
self.handle,
options.samples_per_channel,
options.timeout,
@ -171,7 +179,7 @@ pub const Task = struct {
self.dropped_samples += 1;
log.err("Dropped samples, not reading samples fast enough.", .{});
} else if (err < 0) {
try checkDAQmxError(err, error.DAQmxReadAnalogF64);
try self.ni_daq.checkDAQmxError(err, error.DAQmxReadAnalogF64);
}
return @intCast(samples_per_channel);
@ -179,7 +187,7 @@ pub const Task = struct {
pub fn isDone(self: Task) !bool {
var result: c.bool32 = 0;
try checkDAQmxError(
try self.ni_daq.checkDAQmxError(
c.DAQmxIsTaskDone(self.handle, &result),
error.DAQmxIsTaskDone
);
@ -365,12 +373,13 @@ const DeviceBuffers = struct {
};
options: Options,
api: *Api,
device_names_buffer: []u8,
device_names: StringArrayListUnmanaged,
device_buffers: []DeviceBuffers,
pub fn init(allocator: std.mem.Allocator, options: Options) !NIDaq {
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);
@ -388,6 +397,7 @@ pub fn init(allocator: std.mem.Allocator, options: Options) !NIDaq {
return NIDaq{
.options = options,
.api = api,
.device_names_buffer = device_names_buffer,
.device_names = device_names,
.device_buffers = device_buffers
@ -423,33 +433,33 @@ test {
try std.testing.expectEqual(3, maxIndexStringLength(101));
}
pub fn logDAQmxError(error_code: i32) void {
pub fn logDAQmxError(self: NIDaq, error_code: i32) void {
if (error_code == 0) {
return;
}
var msg: [512:0]u8 = .{ 0 } ** 512;
if (c.DAQmxGetErrorString(error_code, &msg, msg.len) == 0) {
if (self.api.DAQmxGetErrorString(error_code, &msg, msg.len) == 0) {
log.err("DAQmx ({}): {s}", .{error_code, msg});
} else {
log.err("DAQmx ({}): Unknown (Buffer too small for message)", .{error_code});
}
}
pub fn checkDAQmxError(error_code: i32, err: anyerror) !void {
logDAQmxError(error_code);
pub fn checkDAQmxError(self: NIDaq, error_code: i32, err: anyerror) !void {
self.logDAQmxError(error_code);
if (error_code < 0) {
return err;
}
}
pub fn checkDAQmxErrorIgnoreWarnings(error_code: i32, err: anyerror) !void {
pub fn checkDAQmxErrorIgnoreWarnings(self: NIDaq, error_code: i32, err: anyerror) !void {
if (error_code > 0) {
return;
}
try checkDAQmxError(error_code, err);
try self.checkDAQmxError(error_code, err);
}
fn splitCommaDelimitedList(array_list: *std.ArrayListUnmanaged([:0]const u8), buffer: []u8) !void {
@ -471,22 +481,22 @@ fn splitCommaDelimitedList(array_list: *std.ArrayListUnmanaged([:0]const u8), bu
}
}
pub fn version() !std.SemanticVersion {
pub fn version(self: NIDaq) !std.SemanticVersion {
var major: u32 = 0;
try checkDAQmxError(
c.DAQmxGetSysNIDAQMajorVersion(&major),
try self.checkDAQmxError(
self.api.DAQmxGetSysNIDAQMajorVersion(&major),
error.GetMajorVersion
);
var minor: u32 = 0;
try checkDAQmxError(
c.DAQmxGetSysNIDAQMinorVersion(&minor),
try self.checkDAQmxError(
self.api.DAQmxGetSysNIDAQMinorVersion(&minor),
error.GetMinorVersion
);
var update: u32 = 0;
try checkDAQmxError(
c.DAQmxGetSysNIDAQUpdateVersion(&update),
try self.checkDAQmxError(
self.api.DAQmxGetSysNIDAQUpdateVersion(&update),
error.GetUpdateVersion
);
@ -501,8 +511,8 @@ pub fn listDeviceNames(self: *NIDaq) ![]const [:0]const u8 {
self.device_names.clearRetainingCapacity();
self.clearAllDeviceBuffers();
const required_size = c.DAQmxGetSysDevNames(null, 0);
try checkDAQmxErrorIgnoreWarnings(required_size, error.DAQmxGetSysDevNames);
const required_size = self.api.DAQmxGetSysDevNames(null, 0);
try self.checkDAQmxErrorIgnoreWarnings(required_size, error.DAQmxGetSysDevNames);
if (required_size == 0) {
return self.device_names.items;
}
@ -511,8 +521,8 @@ pub fn listDeviceNames(self: *NIDaq) ![]const [:0]const u8 {
return error.BufferTooSmall;
}
try checkDAQmxError(
c.DAQmxGetSysDevNames(self.device_names_buffer.ptr, @intCast(self.device_names_buffer.len)),
try self.checkDAQmxError(
self.api.DAQmxGetSysDevNames(self.device_names_buffer.ptr, @intCast(self.device_names_buffer.len)),
error.GetDeviceNames
);
@ -549,6 +559,7 @@ fn clearAllDeviceBuffers(self: *NIDaq) void {
}
fn listDevicePhysicalChannels(
self: NIDaq,
getPhysicalChannels: anytype,
device: [:0]const u8,
channel_names: *DeviceBuffers.ChannelNames,
@ -559,7 +570,7 @@ fn listDevicePhysicalChannels(
array_list.clearRetainingCapacity();
const required_size = getPhysicalChannels(device, null, 0);
try checkDAQmxErrorIgnoreWarnings(required_size, error.GetPhysicalChannels);
try self.checkDAQmxErrorIgnoreWarnings(required_size, error.GetPhysicalChannels);
if (required_size == 0) {
return array_list.items;
}
@ -568,7 +579,7 @@ fn listDevicePhysicalChannels(
return error.BufferTooSmall;
}
try checkDAQmxError(
try self.checkDAQmxError(
getPhysicalChannels(device, buffer.ptr, @intCast(buffer.len)),
error.GetPhysicalChannels
);
@ -584,8 +595,8 @@ fn listDevicePhysicalChannels(
pub fn listDeviceAIPhysicalChannels(self: *NIDaq, device: [:0]const u8) ![]const [:0]const u8 {
var device_buffers = try self.getDeviceBuffers(device);
return listDevicePhysicalChannels(
c.DAQmxGetDevAIPhysicalChans,
return self.listDevicePhysicalChannels(
self.api.DAQmxGetDevAIPhysicalChans,
device,
&device_buffers.analog_input_names
);
@ -594,8 +605,8 @@ pub fn listDeviceAIPhysicalChannels(self: *NIDaq, device: [:0]const u8) ![]const
pub fn listDeviceAOPhysicalChannels(self: *NIDaq, device: [:0]const u8) ![]const [:0]const u8 {
var device_buffers = try self.getDeviceBuffers(device);
return listDevicePhysicalChannels(
c.DAQmxGetDevAOPhysicalChans,
return self.listDevicePhysicalChannels(
self.api.DAQmxGetDevAOPhysicalChans,
device,
&device_buffers.analog_output_names
);
@ -604,7 +615,7 @@ pub fn listDeviceAOPhysicalChannels(self: *NIDaq, device: [:0]const u8) ![]const
pub fn listDeviceCOPhysicalChannels(self: *NIDaq, device: [:0]const u8) ![]const [:0]const u8 {
var device_buffers = try self.getDeviceBuffers(device);
return listDevicePhysicalChannels(
return self.listDevicePhysicalChannels(
c.DAQmxGetDevCOPhysicalChans,
device,
&device_buffers.counter_output_names
@ -614,7 +625,7 @@ pub fn listDeviceCOPhysicalChannels(self: *NIDaq, device: [:0]const u8) ![]const
pub fn listDeviceCIPhysicalChannels(self: *NIDaq, device: [:0]const u8) ![]const [:0]const u8 {
var device_buffers = try self.getDeviceBuffers(device);
return listDevicePhysicalChannels(
return self.listDevicePhysicalChannels(
c.DAQmxGetDevCIPhysicalChans,
device,
&device_buffers.counter_input_names
@ -646,21 +657,19 @@ fn getChannelType(device: [:0]const u8) ?ChannelType {
}
pub fn getMinSampleRate(self: NIDaq, channel: [:0]const u8) !f64 {
_ = self;
var result: f64 = 0;
const channel_type = getChannelType(channel) orelse return error.UnknownChannelType;
switch (channel_type) {
.analog_input => {
try checkDAQmxError(
c.DAQmxGetDevAIMinRate(channel, &result),
try self.checkDAQmxError(
self.api.DAQmxGetDevAIMinRate(channel, &result),
error.DAQmxGetDevAIMinRate
);
},
.analog_output => {
try checkDAQmxError(
c.DAQmxGetDevAOMinRate(channel, &result),
try self.checkDAQmxError(
self.api.DAQmxGetDevAOMinRate(channel, &result),
error.DAQmxGetDevAOMinRate
);
},
@ -674,21 +683,19 @@ pub fn getMinSampleRate(self: NIDaq, channel: [:0]const u8) !f64 {
}
pub fn getMaxSampleRate(self: NIDaq, channel: [:0]const u8) !f64 {
_ = self;
var result: f64 = 0;
const channel_type = getChannelType(channel) orelse return error.UnknownChannelType;
switch (channel_type) {
.analog_input => {
try checkDAQmxError(
c.DAQmxGetDevAIMaxSingleChanRate(channel, &result),
try self.checkDAQmxError(
self.api.DAQmxGetDevAIMaxSingleChanRate(channel, &result),
error.DAQmxGetDevAIMaxSingleChanRate
);
},
.analog_output => {
try checkDAQmxError(
c.DAQmxGetDevAOMaxRate(channel, &result),
try self.checkDAQmxError(
self.api.DAQmxGetDevAOMaxRate(channel, &result),
error.DAQmxGetDevAOMaxRate
);
},
@ -702,6 +709,7 @@ pub fn getMaxSampleRate(self: NIDaq, channel: [:0]const u8) !f64 {
}
fn listDeviceVoltageRanges(
self: NIDaq,
getVoltageRanges: anytype,
device: [:0]const u8,
voltage_ranges: *std.ArrayListUnmanaged(Range)
@ -709,7 +717,7 @@ fn listDeviceVoltageRanges(
voltage_ranges.clearRetainingCapacity();
const count = getVoltageRanges(device, null, 0);
try checkDAQmxErrorIgnoreWarnings(count, error.GetVoltageRanges);
try self.checkDAQmxErrorIgnoreWarnings(count, error.GetVoltageRanges);
if (count == 0) {
return voltage_ranges.items;
}
@ -722,7 +730,7 @@ fn listDeviceVoltageRanges(
return error.BufferTooSmall;
}
try checkDAQmxError(
try self.checkDAQmxError(
getVoltageRanges(device, buffer, @intCast(buffer_len)),
error.GetVoltageRanges
);
@ -735,8 +743,8 @@ fn listDeviceVoltageRanges(
pub fn listDeviceAIVoltageRanges(self: *NIDaq, device: [:0]const u8) ![]Range {
var device_buffers = try self.getDeviceBuffers(device);
return listDeviceVoltageRanges(
c.DAQmxGetDevAIVoltageRngs,
return self.listDeviceVoltageRanges(
self.api.DAQmxGetDevAIVoltageRngs,
device,
&device_buffers.analog_input_voltage_ranges
);
@ -745,36 +753,33 @@ pub fn listDeviceAIVoltageRanges(self: *NIDaq, device: [:0]const u8) ![]Range {
pub fn listDeviceAOVoltageRanges(self: *NIDaq, device: [:0]const u8) ![]Range {
var device_buffers = try self.getDeviceBuffers(device);
return listDeviceVoltageRanges(
c.DAQmxGetDevAOVoltageRngs,
return self.listDeviceVoltageRanges(
self.api.DAQmxGetDevAOVoltageRngs,
device,
&device_buffers.analog_output_voltage_ranges
);
}
pub fn createTask(self: NIDaq, name: ?[:0]const u8) !Task {
_ = self;
pub fn createTask(self: *NIDaq, name: ?[:0]const u8) !Task {
var handle: TaskHandle = undefined;
try checkDAQmxError(
c.DAQmxCreateTask(name orelse "", &handle),
try self.checkDAQmxError(
self.api.DAQmxCreateTask(name orelse "", &handle),
error.DAQmxCreateTask
);
return Task{ .handle = handle };
return Task{ .ni_daq = self, .handle = handle };
}
pub fn listDeviceAIMeasurementTypes(self: NIDaq, device: [:0]const u8) !AIMeasurementTypeList {
var result = AIMeasurementTypeList.init(0) catch unreachable;
_ = self;
const count = c.DAQmxGetDevAISupportedMeasTypes(device, null, 0);
try checkDAQmxErrorIgnoreWarnings(count, error.DAQmxGetDevAISupportedMeasTypes);
const count = self.api.DAQmxGetDevAISupportedMeasTypes(device, null, 0);
try self.checkDAQmxErrorIgnoreWarnings(count, error.DAQmxGetDevAISupportedMeasTypes);
assert(count <= result.buffer.len);
try checkDAQmxError(
c.DAQmxGetDevAISupportedMeasTypes(device, @as([*c]c_int, @ptrCast(&result.buffer)), result.buffer.len),
try self.checkDAQmxError(
self.api.DAQmxGetDevAISupportedMeasTypes(device, @as([*c]c_int, @ptrCast(&result.buffer)), result.buffer.len),
error.DAQmxGetDevAISupportedMeasTypes
);
@ -790,14 +795,13 @@ pub fn checkDeviceAIMeasurementType(self: NIDaq, device: [:0]const u8, measureme
pub fn listDeviceAOOutputTypes(self: NIDaq, device: [:0]const u8) !AOOutputTypeList {
var result = AOOutputTypeList.init(0) catch unreachable;
_ = self;
const count = c.DAQmxGetDevAOSupportedOutputTypes(device, null, 0);
try checkDAQmxErrorIgnoreWarnings(count, error.DAQmxGetDevAOSupportedOutputTypes);
const count = self.api.DAQmxGetDevAOSupportedOutputTypes(device, null, 0);
try self.checkDAQmxErrorIgnoreWarnings(count, error.DAQmxGetDevAOSupportedOutputTypes);
assert(count <= result.buffer.len);
try checkDAQmxError(
c.DAQmxGetDevAOSupportedOutputTypes(device, @as([*c]c_int, @ptrCast(&result.buffer)), result.buffer.len),
try self.checkDAQmxError(
self.api.DAQmxGetDevAOSupportedOutputTypes(device, @as([*c]c_int, @ptrCast(&result.buffer)), result.buffer.len),
error.DAQmxGetDevAOSupportedOutputTypes
);
@ -812,11 +816,9 @@ pub fn checkDeviceAOOutputType(self: NIDaq, device: [:0]const u8, output_type: A
}
pub fn getDeviceProductCategory(self: NIDaq, device: [:0]const u8) !ProductCategory {
_ = self;
var product_category = ProductCategory.Unknown;
try checkDAQmxError(
c.DAQmxGetDevProductCategory(device, @ptrCast(&product_category)),
try self.checkDAQmxError(
self.api.DAQmxGetDevProductCategory(device, @ptrCast(&product_category)),
error.DAQmxGetDevProductCategory
);

View File

@ -1,5 +1,5 @@
const std = @import("std");
const NIDaq = @import("./ni-daq.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

@ -132,4 +132,10 @@ pub fn openFilePicker() !std.fs.File {
// TODO: Use the `openFileAbsoluteW` function.
// Could not get it to work, because it always threw OBJECT_PATH_SYNTAX_BAD error
return try std.fs.openFileAbsolute(filename, .{ });
}
pub fn init() void {
if (builtin.os.tag == .windows) {
_ = windows_h.SetConsoleOutputCP(65001);
}
}

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,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();
}

54
tools/png-to-icon.zig Normal file
View File

@ -0,0 +1,54 @@
const std = @import("std");
const stb_image = @import("stb_image");
// https://en.wikipedia.org/wiki/ICO_(file_format)#Icon_file_structure
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
defer _ = gpa.deinit();
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
if (args.len != 3) {
std.debug.print("Usage: ./png-to-icon <png-file> <output-ico>", .{});
std.process.exit(1);
}
const cwd = std.fs.cwd();
const input_png_path = args[1];
const output_ico_path = args[2];
const input_png_data = try cwd.readFileAlloc(allocator, input_png_path, 1024 * 1024 * 5);
defer allocator.free(input_png_data);
const png_image = try stb_image.load(input_png_data);
defer png_image.deinit();
std.debug.assert(png_image.width > 0 and png_image.width <= 256);
std.debug.assert(png_image.height > 0 and png_image.height <= 256);
const output_ico_file = try std.fs.cwd().createFile(output_ico_path, .{ });
defer output_ico_file.close();
const writer = output_ico_file.writer();
// ICONDIR structure
try writer.writeInt(u16, 0, .little); // Must always be zero
try writer.writeInt(u16, 1, .little); // Image type. 1 for .ICO
try writer.writeInt(u16, 1, .little); // Number of images
// ICONDIRENTRY structure
try writer.writeInt(u8, @truncate(png_image.width), .little); // Image width
try writer.writeInt(u8, @truncate(png_image.height), .little); // Image height
try writer.writeInt(u8, 0, .little); // Number of colors in color pallete. 0 means that color pallete is not used
try writer.writeInt(u8, 0, .little); // Must always be zero
try writer.writeInt(u16, 0, .little); // Color plane
try writer.writeInt(u16, 32, .little); // Bits per pixel
try writer.writeInt(u32, @intCast(input_png_data.len), .little); // Image size in bytes
try writer.writeInt(u32, 22, .little); // Offset to image data from the start
// PNG image data
try writer.writeAll(input_png_data);
}