add modal for protocol parameters

This commit is contained in:
Rokas Puzonas 2025-04-08 21:38:53 +03:00
parent 8ba3d0c914
commit 31d0af0a5c
12 changed files with 1462 additions and 373 deletions

View File

@ -5,11 +5,9 @@ const rl = @import("raylib");
const srcery = @import("./srcery.zig");
const NIDaq = @import("ni-daq/root.zig");
const Graph = @import("./graph.zig");
const TaskPool = @import("ni-daq/task-pool.zig");
const utils = @import("./utils.zig");
const Assets = @import("./assets.zig");
const RangeF64 = @import("./range.zig").RangeF64;
const P = @import("profiler");
const MainScreen = @import("./screens/main_screen.zig");
const ChannelFromDeviceScreen = @import("./screens/channel_from_device.zig");
@ -24,6 +22,16 @@ const max_files = 32;
const App = @This();
const ChannelTask = struct {
task: NIDaq.Task,
sample_rate: f64,
read: bool,
fn deinit(self: ChannelTask) void {
self.task.clear();
}
};
const FileChannel = struct {
path: []u8,
samples: []f64,
@ -35,21 +43,38 @@ const FileChannel = struct {
};
const DeviceChannel = struct {
const Name = std.BoundedArray(u8, NIDaq.max_channel_name_size + 1); // +1 for null byte
name: Name = .{},
const ChannelName = std.BoundedArray(u8, NIDaq.max_channel_name_size + 1); // +1 for null byte
const Direction = enum { input, output };
device_name: NIDaq.BoundedDeviceName = .{},
channel_name: ChannelName = .{},
samples: std.ArrayList(f64),
units: i32 = NIDaq.c.DAQmx_Val_Volts,
write_pattern: std.ArrayList(f64),
units: NIDaq.Unit = .Voltage,
min_sample_rate: f64,
max_sample_rate: f64,
min_value: f64,
max_value: f64,
active_task: ?*TaskPool.Entry = null,
task: ?ChannelTask = null,
fn deinit(self: DeviceChannel) void {
self.samples.deinit();
self.write_pattern.deinit();
if (self.task) |task| {
task.deinit();
}
}
pub fn getDeviceName(self: *DeviceChannel) [:0]const u8 {
return utils.getBoundedStringZ(&self.device_name);
}
pub fn getChannelName(self: *DeviceChannel) [:0]const u8 {
return utils.getBoundedStringZ(&self.channel_name);
}
};
@ -93,21 +118,30 @@ pub const ChannelView = struct {
}
};
pub const DeferredChannelAction = union(enum) {
activate: *ChannelView,
deactivate: *ChannelView,
toggle_input_channels
};
allocator: std.mem.Allocator,
should_close: bool = false,
ni_daq_api: ?NIDaq.Api = null,
ni_daq: ?NIDaq = null,
task_pool: TaskPool,
channel_views: std.BoundedArray(ChannelView, max_channels) = .{},
loaded_files: [max_channels]?FileChannel = .{ null } ** max_channels,
device_channels: [max_channels]?DeviceChannel = .{ null } ** max_channels,
started_collecting: bool = false,
channel_mutex: std.Thread.Mutex = .{},
// Reading & writing tasks
samples_mutex: std.Thread.Mutex = .{},
device_channels_mutex: std.Thread.Mutex = .{},
thread_wake_condition: std.Thread.Condition = .{},
task_read_thread: ?std.Thread = null,
task_read_active: bool = false,
// UI Fields
ui: UI,
deferred_actions: std.BoundedArray(DeferredChannelAction, max_channels) = .{},
current_screen: enum {
main_menu,
channel_from_device
@ -119,17 +153,16 @@ pub fn init(self: *App, allocator: std.mem.Allocator) !void {
self.* = App{
.allocator = allocator,
.ui = UI.init(allocator),
.main_screen = MainScreen{
.app = self
},
.main_screen = undefined,
.channel_from_device = ChannelFromDeviceScreen{
.app = self,
.channel_names = std.heap.ArenaAllocator.init(allocator)
},
.task_pool = undefined,
};
errdefer self.deinit();
self.main_screen = try MainScreen.init(self);
if (NIDaq.Api.init()) |ni_daq_api| {
self.ni_daq_api = ni_daq_api;
@ -169,12 +202,22 @@ pub fn init(self: *App, allocator: std.mem.Allocator) !void {
}
}
try TaskPool.init(&self.task_pool, &self.channel_mutex, allocator);
errdefer self.task_pool.deinit();
self.task_read_thread = try std.Thread.spawn(
.{ .allocator = allocator },
readThreadCallback,
.{ self }
);
}
pub fn deinit(self: *App) void {
self.task_pool.deinit();
self.main_screen.deinit();
if (self.task_read_thread) |thread| {
self.should_close = true;
self.thread_wake_condition.signal();
thread.join();
self.task_read_thread = null;
}
for (self.channel_views.slice()) |*channel| {
channel.view_cache.deinit();
@ -241,10 +284,12 @@ pub fn tick(self: *App) !void {
var ui = &self.ui;
rl.clearBackground(srcery.black);
self.deferred_actions.len = 0;
ui.pullOsEvents();
{
self.channel_mutex.lock();
defer self.channel_mutex.unlock();
self.samples_mutex.lock();
defer self.samples_mutex.unlock();
for (self.listChannelViews()) |*channel_view| {
const device_channel = self.getChannelSourceDevice(channel_view) orelse continue;
@ -264,15 +309,89 @@ pub fn tick(self: *App) !void {
}
}
for (self.deferred_actions.constSlice()) |action| {
switch (action) {
.activate => |channel_view| {
try self.activateDeviceChannel(channel_view);
},
.deactivate => |channel_view| {
try self.deactivateDeviceChannel(channel_view);
},
.toggle_input_channels => {
try self.toggleInputDeviceChannels();
}
}
}
ui.draw();
}
// ---------------------- Reading & Writing tasks ----------------------------- //
fn readThreadCallback(self: *App) void {
while (!self.should_close) {
var has_tasks_configured = false;
{
self.device_channels_mutex.lock();
defer self.device_channels_mutex.unlock();
for (&self.device_channels) |*maybe_device_channel| {
const device_channel = &(maybe_device_channel.* orelse continue);
const channel_task = &(device_channel.task orelse continue);
if (!channel_task.read) {
continue;
}
has_tasks_configured = true;
self.readThreadDevice(device_channel) catch |e| {
log.err("Failed to read samples in thread: {}", .{e});
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
if (device_channel.task) |task| {
task.deinit();
device_channel.task = null;
}
continue;
};
}
if (!has_tasks_configured) {
self.thread_wake_condition.wait(&self.device_channels_mutex);
}
}
std.time.sleep(5 * std.time.ns_per_ms);
}
}
fn readThreadDevice(self: *App, device_channel: *DeviceChannel) !void {
self.samples_mutex.lock();
defer self.samples_mutex.unlock();
const channel_task = &device_channel.task.?;
assert(channel_task.read);
try device_channel.samples.ensureUnusedCapacity(@intFromFloat(@ceil(channel_task.sample_rate)));
const read_amount = try channel_task.task.readAnalog(.{
.timeout = 0,
.read_array = device_channel.samples.unusedCapacitySlice()
});
device_channel.samples.items.len += read_amount;
}
// ------------------------ Channel management -------------------------------- //
pub fn getChannelDeviceByName(self: *App, name: []const u8) ?*DeviceChannel {
for (&self.device_channels) |*maybe_channel| {
var channel: *DeviceChannel = &(maybe_channel.* orelse continue);
if (std.mem.eql(u8, channel.name.slice(), name)) {
if (std.mem.eql(u8, channel.channel_name.slice(), name)) {
return channel;
}
}
@ -301,64 +420,128 @@ pub fn getChannelSourceDevice(self: *App, channel_view: *ChannelView) ?*DeviceCh
return null;
}
pub fn isDeviceChannelReading(self: *App, channel_view: *ChannelView) bool {
const device_channel = self.getChannelSourceDevice(channel_view) orelse return;
return device_channel.active_task != null;
pub fn isDeviceChannelActive(self: *App, channel_view: *ChannelView) bool {
const device_channel = self.getChannelSourceDevice(channel_view) orelse return false;
return device_channel.task != null;
}
pub fn stopDeviceChannelReading(self: *App, channel_view: *ChannelView) void {
const device_channel = self.getChannelSourceDevice(channel_view) orelse return;
const task = device_channel.active_task orelse return;
task.stop() catch |e| {
log.err("Failed to stop collection task {}", .{e});
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
return;
};
device_channel.active_task = null;
}
pub fn startDeviceChannelReading(self: *App, channel_view: *ChannelView) void {
pub fn activateDeviceChannel(self: *App, channel_view: *ChannelView) !void {
const ni_daq = &(self.ni_daq orelse return);
self.device_channels_mutex.lock();
defer self.device_channels_mutex.unlock();
const device_channel = self.getChannelSourceDevice(channel_view) orelse return;
if (device_channel.active_task != null) {
// Device channel is already reading
if (device_channel.task != null) {
return;
}
const channel_name = device_channel.name.buffer[0..device_channel.name.len :0];
const sample_rate = device_channel.max_sample_rate;
defer self.thread_wake_condition.signal();
const task = self.task_pool.launchAIVoltageChannel(
ni_daq,
&device_channel.samples,
.{
.sample_rate = sample_rate
},
.{
assert(device_channel.units == .Voltage);
const channel_type = NIDaq.getChannelType(device_channel.getChannelName()) orelse return;
if (channel_type == .analog_input) {
const task = try ni_daq.createTask(null);
errdefer task.clear();
const sample_rate = device_channel.max_sample_rate;
try task.createAIVoltageChannel(.{
.channel = device_channel.getChannelName(),
.min_value = device_channel.min_value,
.max_value = device_channel.max_value,
.units = device_channel.units,
.channel = channel_name
}
) catch |e| {
log.err("Failed to start collection task {}", .{e});
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
});
try task.setContinousSampleRate(.{
.sample_rate = sample_rate
});
try task.start();
device_channel.samples.clearRetainingCapacity();
device_channel.task = ChannelTask{
.task = task,
.sample_rate = sample_rate,
.read = true
};
} else if (channel_type == .analog_output) {
const task = try ni_daq.createTask(null);
errdefer task.clear();
device_channel.write_pattern.clearAndFree();
try device_channel.write_pattern.appendNTimes(2, 1000);
const write_pattern = device_channel.write_pattern.items;
const samples_per_channel: u32 = @intCast(write_pattern.len);
const sample_rate = 5000; // device_channel.max_sample_rate;
try task.createAOVoltageChannel(.{
.channel = device_channel.getChannelName(),
.min_value = device_channel.min_value,
.max_value = device_channel.max_value,
});
try task.setContinousSampleRate(.{
.sample_rate = sample_rate,
});
const write_amount = try task.writeAnalog(.{
.write_array = write_pattern,
.samples_per_channel = samples_per_channel,
.timeout = -1
});
if (write_amount != samples_per_channel) {
return error.WriteAnalog;
}
try task.start();
device_channel.task = ChannelTask{
.task = task,
.sample_rate = sample_rate,
.read = false
};
} else {
return;
};
}
device_channel.active_task = task;
channel_view.follow = true;
}
pub fn deactivateDeviceChannel(self: *App, channel_view: *ChannelView) !void {
self.device_channels_mutex.lock();
defer self.device_channels_mutex.unlock();
const device_channel = self.getChannelSourceDevice(channel_view) orelse return;
if (device_channel.task == null) {
return;
}
device_channel.task.?.deinit();
device_channel.task = null;
}
pub fn toggleInputDeviceChannels(self: *App) !void {
self.task_read_active = !self.task_read_active;
for (self.listChannelViews()) |*channel_view| {
const device_channel = self.getChannelSourceDevice(channel_view) orelse continue;
const channel_name = device_channel.getChannelName();
const channel_type = NIDaq.getChannelType(channel_name) orelse continue;
if (channel_type == .analog_input) {
if (self.task_read_active) {
try self.activateDeviceChannel(channel_view);
} else {
try self.deactivateDeviceChannel(channel_view);
}
}
}
}
pub fn appendChannelFromFile(self: *App, path: []const u8) !void {
self.device_channels_mutex.lock();
defer self.device_channels_mutex.unlock();
const path_dupe = try self.allocator.dupe(u8, path);
errdefer self.allocator.free(path_dupe);
@ -405,10 +588,16 @@ 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);
self.device_channels_mutex.lock();
defer self.device_channels_mutex.unlock();
const device_channel_index = findFreeSlot(DeviceChannel, &self.device_channels) orelse return error.DeviceChannelLimitReached;
const name_buff = try DeviceChannel.Name.fromSlice(channel_name);
const channel_name_z = name_buff.buffer[0..name_buff.len :0];
const device_name = NIDaq.getDeviceNameFromChannel(channel_name) orelse return;
const device_name_buff = try utils.initBoundedStringZ(NIDaq.BoundedDeviceName, device_name);
const channel_name_buff = try utils.initBoundedStringZ(DeviceChannel.ChannelName, channel_name);
const channel_name_z = utils.getBoundedStringZ(&channel_name_buff);
const device = NIDaq.getDeviceNameFromChannel(channel_name) orelse return error.InvalidChannelName;
const device_buff = try NIDaq.BoundedDeviceName.fromSlice(device);
@ -419,21 +608,35 @@ pub fn appendChannelFromDevice(self: *App, channel_name: []const u8) !void {
var min_value: f64 = 0;
var max_value: f64 = 1;
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 channel_type = NIDaq.getChannelType(channel_name_z) orelse return error.UnknownChannelType;
if (channel_type == .analog_output) {
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;
}
} else if (channel_type == .analog_input) {
const voltage_ranges = try ni_daq.listDeviceAIVoltageRanges(device_z);
if (voltage_ranges.len > 0) {
min_value = voltage_ranges[0].low;
max_value = voltage_ranges[0].high;
}
} else {
return error.UnknownChannelType;
}
const max_sample_rate = try ni_daq.getMaxSampleRate(channel_name_z);
self.device_channels[device_channel_index] = DeviceChannel{
.name = name_buff,
.channel_name = channel_name_buff,
.device_name = device_name_buff,
.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,
.samples = std.ArrayList(f64).init(self.allocator)
.samples = std.ArrayList(f64).init(self.allocator),
.write_pattern = std.ArrayList(f64).init(self.allocator)
};
errdefer self.device_channels[device_channel_index] = null;

View File

@ -48,6 +48,8 @@ pub var dropdown_arrow: rl.Texture2D = undefined;
pub var fullscreen: rl.Texture2D = undefined;
pub var output_generation: rl.Texture2D = undefined;
pub fn font(font_id: FontId) FontFace {
var found_font: ?LoadedFont = null;
for (loaded_fonts.slice()) |*loaded_font| {
@ -139,6 +141,17 @@ pub fn init(allocator: std.mem.Allocator) !void {
fullscreen = rl.loadTextureFromImage(fullscreen_image);
assert(rl.isTextureReady(fullscreen));
}
{
const output_generation_ase = try Aseprite.init(allocator, @embedFile("./assets/output-generation-icon.ase"));
defer output_generation_ase.deinit();
const output_generation_image = output_generation_ase.getFrameImage(0);
defer output_generation_image.unload();
output_generation = rl.loadTextureFromImage(output_generation_image);
assert(rl.isTextureReady(output_generation));
}
}
fn loadFont(ttf_data: []const u8, font_size: u32) !rl.Font {

Binary file not shown.

View File

@ -134,20 +134,26 @@ pub fn drawTextAlloc(self: @This(), allocator: Allocator, comptime fmt: []const
self.drawText(text, position, tint);
}
pub fn measureText(self: @This(), text: []const u8) rl.Vector2 {
var text_size = rl.Vector2.zero();
pub const MeasureOptions = struct {
up_to_width: ?f32 = null,
last_codepoint_index: usize = 0
};
if (self.font.texture.id == 0) return text_size; // Security check
if (text.len == 0) return text_size;
pub fn measureTextEx(self: @This(), text: []const u8, opts: *MeasureOptions) rl.Vector2 {
var result = rl.Vector2.zero();
if (self.font.texture.id == 0) return result; // Security check
if (text.len == 0) return result;
const font_size = self.getSize();
const spacing = self.getSpacing();
var line_width: f32 = 0;
text_size.y = font_size;
result.y = font_size;
var iter = std.unicode.Utf8Iterator{ .bytes = text, .i = 0 };
while (iter.nextCodepoint()) |codepoint| {
var text_size = result;
if (codepoint == '\n') {
text_size.y += font_size * self.line_height;
@ -158,18 +164,34 @@ pub fn measureText(self: @This(), text: []const u8) rl.Vector2 {
}
const index: u32 = @intCast(rl.getGlyphIndex(self.font, codepoint));
if (self.font.glyphs[index].advanceX != 0) {
line_width += @floatFromInt(self.font.glyphs[index].advanceX);
const glyph = self.font.glyphs[index];
if (glyph.advanceX != 0) {
line_width += @floatFromInt(glyph.advanceX);
} else {
line_width += self.font.recs[index].width;
line_width += @floatFromInt(self.font.glyphs[index].offsetX);
line_width += @floatFromInt(glyph.offsetX);
}
text_size.x = @max(text_size.x, line_width);
}
if (opts.up_to_width) |up_to_width| {
if (text_size.x > up_to_width) {
break;
}
}
opts.last_codepoint_index = iter.i;
result = text_size;
}
return text_size;
return result;
}
pub fn measureText(self: @This(), text: []const u8) rl.Vector2 {
var opts = MeasureOptions{};
return self.measureTextEx(text, &opts);
}
pub fn measureWidth(self: @This(), text: []const u8) f32 {

View File

@ -77,7 +77,11 @@ fn drawSamplesExact(draw_rect: rl.Rectangle, options: ViewOptions, samples: []co
const draw_x_range, const draw_y_range = RangeF64.initRect(draw_rect);
const i_range = options.x_range.intersectPositive(
var view_x_range = options.x_range;
view_x_range.upper += 2;
view_x_range.lower -= 1;
const i_range = view_x_range.intersectPositive(
RangeF64.init(0, @max(@as(f64, @floatFromInt(samples.len)) - 1, 0))
);
if (i_range.lower > i_range.upper) {

View File

@ -9,7 +9,6 @@ const raylib_h = @cImport({
@cInclude("stdio.h");
@cInclude("raylib.h");
});
const P = @import("profiler");
const log = std.log;
const profiler_enabled = builtin.mode == .Debug;
@ -66,12 +65,6 @@ fn raylibTraceLogCallback(logType: c_int, text: [*c]const u8, args: raylib_h.va_
}
pub fn main() !void {
try P.init(.{});
defer {
P.dump("profile.json") catch |err| std.log.err("profile dump failed: {}", .{err});
P.deinit();
}
Platform.init();
// TODO: Setup logging to a file
@ -107,11 +100,12 @@ pub fn main() !void {
defer app.deinit();
if (builtin.mode == .Debug) {
// try app.appendChannelFromDevice("Dev1/ai0");
try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_I.bin");
try app.appendChannelFromDevice("Dev1/ai0");
try app.appendChannelFromDevice("Dev3/ao0");
// 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");
// try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_IjStim.bin");
// try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_IjStim.bin");
try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_IjStim.bin");
}
var profiler: ?Profiler = null;
@ -134,9 +128,6 @@ pub fn main() !void {
rl.beginDrawing();
{
const zone = P.begin(@src(), "tick");
defer zone.end();
if (profiler) |*p| {
p.start();
}
@ -161,12 +152,9 @@ pub fn main() !void {
}
}
{
const zone = P.begin(@src(), "end draw");
defer zone.end();
rl.endDrawing();
}
rl.endDrawing();
}
}
test {

View File

@ -46,6 +46,13 @@ DAQmxGetDevProductCategory: *const @TypeOf(c.DAQmxGetDevProductCategory),
DAQmxGetDevAIPhysicalChans: *const @TypeOf(c.DAQmxGetDevAIPhysicalChans),
DAQmxGetDevAOPhysicalChans: *const @TypeOf(c.DAQmxGetDevAOPhysicalChans),
DAQmxReadAnalogF64: *const @TypeOf(c.DAQmxReadAnalogF64),
DAQmxWriteAnalogF64: *const @TypeOf(c.DAQmxWriteAnalogF64),
DAQmxGetWriteCurrWritePos: *const @TypeOf(c.DAQmxGetWriteCurrWritePos),
DAQmxGetWriteSpaceAvail: *const @TypeOf(c.DAQmxGetWriteSpaceAvail),
DAQmxCreateAOVoltageChan: *const @TypeOf(c.DAQmxCreateAOVoltageChan),
DAQmxGetWriteTotalSampPerChanGenerated: *const @TypeOf(c.DAQmxGetWriteTotalSampPerChanGenerated),
DAQmxCfgOutputBuffer: *const @TypeOf(c.DAQmxCfgOutputBuffer),
DAQmxCreateAOFuncGenChan: *const @TypeOf(c.DAQmxCreateAOFuncGenChan),
pub fn init() Error!Api {
var api: Api = undefined;

View File

@ -86,10 +86,28 @@ pub const Task = struct {
);
}
pub fn setContinousSampleRate(self: Task, sample_rate: f64) !void {
pub const ContinousSamplingOptions = struct {
pub const Edge = enum(i32) {
rising = c.DAQmx_Val_Rising,
falling = c.DAQmx_Val_Falling
};
sample_rate: f64,
active_edge: Edge = .rising,
buffer_size: u64 = 0
};
pub fn setContinousSampleRate(self: Task, opts: ContinousSamplingOptions) !void {
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),
api.DAQmxCfgSampClkTiming(
self.handle,
null,
opts.sample_rate,
@intFromEnum(opts.active_edge),
c.DAQmx_Val_ContSamps,
opts.buffer_size
),
error.DAQmxCfgSampClkTiming
);
}
@ -97,33 +115,63 @@ pub const Task = struct {
pub fn setFiniteSampleRate(self: Task, sample_rate: f64, samples_per_channel: u64) !void {
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),
api.DAQmxCfgSampClkTiming(
self.handle,
null,
sample_rate,
c.DAQmx_Val_Rising,
c.DAQmx_Val_FiniteSamps,
samples_per_channel
),
error.DAQmxCfgSampClkTiming
);
}
pub const AIVoltageChannelOptions = struct {
pub const TerminalConfig = enum(i32) {
default = c.DAQmx_Val_Cfg_Default,
rse = c.DAQmx_Val_RSE,
nrse = c.DAQmx_Val_NRSE,
diff = c.DAQmx_Val_Diff,
pseudo_diff = c.DAQmx_Val_PseudoDiff,
};
channel: [:0]const u8,
assigned_name: [*c]const u8 = null,
terminal_config: i32 = c.DAQmx_Val_Cfg_Default,
terminal_config: TerminalConfig = .default,
min_value: f64,
max_value: f64,
units: i32 = c.DAQmx_Val_Volts,
custom_scale_name: [*c]const u8 = null,
units: union(enum) {
volts,
custom_scale: [*]const u8
} = .volts
};
pub fn createAIVoltageChannel(self: Task, options: AIVoltageChannelOptions) !void {
const api = self.ni_daq.api;
var custom_scale_name: [*c]const u8 = null;
var units: i32 = undefined;
switch (options.units) {
.volts => {
units = c.DAQmx_Val_Volts;
},
.custom_scale => |_custom_scale_name| {
units = c.DAQmx_Val_FromCustomScale;
custom_scale_name = _custom_scale_name;
}
}
try self.ni_daq.checkDAQmxError(
api.DAQmxCreateAIVoltageChan(
self.handle,
options.channel,
options.assigned_name,
options.terminal_config,
@intFromEnum(options.terminal_config),
options.min_value,
options.max_value,
options.units,
options.custom_scale_name
units,
custom_scale_name
),
error.DAQmxCreateAIVoltageChan
);
@ -134,25 +182,74 @@ pub const Task = struct {
assigned_name: [*c]const u8 = null,
min_value: f64,
max_value: f64,
units: i32 = c.DAQmx_Val_Volts,
custom_scale_name: [*c]const u8 = null,
units: union(enum) {
volts,
custom_scale: [*]const u8
} = .volts
};
pub fn createAOVoltageChannel(self: Task, options: AOVoltageChannelOptions) !void {
var custom_scale_name: [*c]const u8 = null;
var units: i32 = undefined;
switch (options.units) {
.volts => {
units = c.DAQmx_Val_Volts;
},
.custom_scale => |_custom_scale_name| {
units = c.DAQmx_Val_FromCustomScale;
custom_scale_name = _custom_scale_name;
}
}
const api = self.ni_daq.api;
try self.ni_daq.checkDAQmxError(
c.DAQmxCreateAOVoltageChan(
api.DAQmxCreateAOVoltageChan(
self.handle,
options.channel,
options.assigned_name,
options.min_value,
options.max_value,
options.units,
options.custom_scale_name
units,
custom_scale_name
),
error.DAQmxCreateAOVoltageChan
);
}
pub const AOGenerationChannelOptions = struct {
pub const Function = enum(i32) {
sine = c.DAQmx_Val_Sine,
triangle = c.DAQmx_Val_Triangle,
square = c.DAQmx_Val_Square,
Sawtooth = c.DAQmx_Val_Sawtooth,
_
};
channel: [:0]const u8,
assigned_name: [*c]const u8 = null,
function: Function,
frequency: f64,
amplitude: f64,
offset: f64 = 0
};
pub fn createAOGenerationChannel(self: Task, options: AOGenerationChannelOptions) !void {
const api = self.ni_daq.api;
try self.ni_daq.checkDAQmxError(
api.DAQmxCreateAOFuncGenChan(
self.handle,
options.channel,
options.assigned_name,
@intFromEnum(options.function),
options.frequency,
options.amplitude,
options.offset
),
error.DAQmxCreateAOFuncGenChan
);
}
pub const ReadAnalogOptions = struct {
read_array: []f64,
timeout: f64,
@ -177,7 +274,7 @@ pub const Task = struct {
if (err == c.DAQmxErrorSamplesNoLongerAvailable) {
self.dropped_samples += 1;
log.err("Dropped samples, not reading samples fast enough.", .{});
log.warn("Dropped samples, not reading samples fast enough.", .{});
} else if (err < 0) {
try self.ni_daq.checkDAQmxError(err, error.DAQmxReadAnalogF64);
}
@ -185,10 +282,77 @@ pub const Task = struct {
return @intCast(samples_per_channel);
}
pub const WriteAnalogOptions = struct {
write_array: []f64,
samples_per_channel: u32,
timeout: f64,
data_layout: u32 = c.DAQmx_Val_GroupByChannel,
auto_start: bool = false
};
pub fn writeAnalog(self: Task, options: WriteAnalogOptions) !u32 {
const api = self.ni_daq.api;
var samples_per_channel_written: i32 = 0;
const err = api.DAQmxWriteAnalogF64(
self.handle,
@intCast(options.samples_per_channel),
@intFromBool(options.auto_start),
options.timeout,
options.data_layout,
options.write_array.ptr,
&samples_per_channel_written,
null
);
try self.ni_daq.checkDAQmxError(err, error.DAQmxWriteAnalogF64);
return @intCast(samples_per_channel_written);
}
pub fn writeSpaceAvailable(self: Task) !u32 {
const api = self.ni_daq.api;
var amount: u32 = 0;
try self.ni_daq.checkDAQmxError(
api.DAQmxGetWriteSpaceAvail(self.handle, &amount),
error.DAQmxGetWriteSpaceAvail
);
return amount;
}
pub fn writeCurrentPosition(self: Task) !u64 {
const api = self.ni_daq.api;
var amount: u64 = 0;
try self.ni_daq.checkDAQmxError(
api.DAQmxGetWriteCurrWritePos(self.handle, &amount),
error.DAQmxGetWriteCurrWritePos
);
return amount;
}
pub fn writeTotalSamplesPerChannelGenerated(self: Task) !u64 {
const api = self.ni_daq.api;
var amount: u64 = 0;
try self.ni_daq.checkDAQmxError(
api.DAQmxGetWriteTotalSampPerChanGenerated(self.handle, &amount),
error.DAQmxGetWriteTotalSampPerChanGenerated
);
return amount;
}
pub fn configureOutputBuffer(self: Task, samples_per_channel: u32) !void {
const api = self.ni_daq.api;
try self.ni_daq.checkDAQmxError(
api.DAQmxCfgOutputBuffer(self.handle, samples_per_channel),
error.DAQmxCfgOutputBuffer
);
}
pub fn isDone(self: Task) !bool {
const api = self.ni_daq.api;
var result: c.bool32 = 0;
try self.ni_daq.checkDAQmxError(
c.DAQmxIsTaskDone(self.handle, &result),
api.DAQmxIsTaskDone(self.handle, &result),
error.DAQmxIsTaskDone
);
return result != 0;

View File

@ -1,159 +0,0 @@
const std = @import("std");
const NIDaq = @import("./root.zig");
const assert = std.debug.assert;
const log = std.log.scoped(.task_pool);
const TaskPool = @This();
const max_tasks = 32;
pub const Sampling = struct {
sample_rate: f64,
sample_count: ?u64 = null
};
pub const Entry = struct {
task: NIDaq.Task,
in_use: bool = false,
running: bool = false,
started_sampling_ns: i128,
stopped_sampling_ns: ?i128 = null,
dropped_samples: u32 = 0,
sampling: Sampling,
samples: *std.ArrayList(f64),
pub fn stop(self: *Entry) !void {
self.running = false;
if (self.in_use) {
try self.task.stop();
self.task.clear();
}
self.in_use = false;
}
};
running: bool = false,
read_thread: std.Thread,
entries: [max_tasks]Entry = undefined,
mutex: *std.Thread.Mutex,
pub fn init(self: *TaskPool, mutex: *std.Thread.Mutex, allocator: std.mem.Allocator) !void {
self.* = TaskPool{
.read_thread = undefined,
.mutex = mutex
};
self.running = true;
self.read_thread = try std.Thread.spawn(
.{ .allocator = allocator },
readThreadCallback,
.{ self }
);
for (&self.entries) |*entry| {
entry.in_use = false;
}
}
pub fn deinit(self: *TaskPool) void {
for (&self.entries) |*entry| {
entry.stop() catch log.err("Failed to stop entry", .{});
}
self.running = false;
self.read_thread.join();
}
fn readAnalog(task_pool: *TaskPool, entry: *Entry, timeout: f64) !void {
if (!entry.in_use) return;
if (!entry.running) return;
task_pool.mutex.lock();
defer task_pool.mutex.unlock();
if (entry.sampling.sample_count) |sample_count| {
try entry.samples.ensureTotalCapacity(sample_count);
} else {
try entry.samples.ensureUnusedCapacity(@intFromFloat(@ceil(entry.sampling.sample_rate)));
}
const unused_capacity = entry.samples.unusedCapacitySlice();
if (unused_capacity.len == 0) return;
const read_amount = try entry.task.readAnalog(.{
.timeout = timeout,
.read_array = unused_capacity,
});
if (read_amount == 0) return;
entry.samples.items.len += read_amount;
}
fn readThreadCallback(task_pool: *TaskPool) void {
const timeout = 0.05;
while (task_pool.running) {
for (&task_pool.entries) |*entry| {
readAnalog(task_pool, entry, timeout) catch |e| {
log.err("readAnalog() failed in thread: {}", .{e});
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
entry.stop() catch log.err("failed to stop collecting", .{});
};
}
std.time.sleep(timeout * std.time.ns_per_s);
}
}
fn findFreeEntry(self: *TaskPool) ?*Entry {
for (&self.entries) |*entry| {
if (!entry.in_use) {
return entry;
}
}
return null;
}
pub fn launchAIVoltageChannel(
self: *TaskPool,
ni_daq: *NIDaq,
samples: *std.ArrayList(f64),
sampling: Sampling,
options: NIDaq.Task.AIVoltageChannelOptions
) !*Entry {
const task = try ni_daq.createTask(null);
errdefer task.clear();
const entry = self.findFreeEntry() orelse return error.NotEnoughSpace;
errdefer entry.in_use = false;
try task.createAIVoltageChannel(options);
if (sampling.sample_count) |sample_count| {
try task.setFiniteSampleRate(sampling.sample_rate, sample_count);
} else {
try task.setContinousSampleRate(sampling.sample_rate);
}
samples.clearRetainingCapacity();
try task.start();
const started_at = std.time.nanoTimestamp();
entry.* = Entry{
.task = task,
.started_sampling_ns = started_at,
.in_use = true,
.running = true,
.samples = samples,
.sampling = sampling,
};
return entry;
}

View File

@ -8,6 +8,7 @@ const RangeF64 = @import("../range.zig").RangeF64;
const Graph = @import("../graph.zig");
const Assets = @import("../assets.zig");
const utils = @import("../utils.zig");
const NIDaq = @import("../ni-daq/root.zig");
const MainScreen = @This();
@ -42,6 +43,33 @@ axis_zoom: ?struct {
// TODO: Redo
channel_undo_stack: std.BoundedArray(ChannelCommand, 100) = .{},
protocol_modal: ?*ChannelView = null,
frequency_input: UI.TextInputStorage,
amplitude_input: UI.TextInputStorage,
protocol_error_message: ?[]const u8 = null,
pub fn init(app: *App) !MainScreen {
const allocator = app.allocator;
var self = MainScreen{
.app = app,
.frequency_input = UI.TextInputStorage.init(allocator),
.amplitude_input = UI.TextInputStorage.init(allocator)
};
try self.frequency_input.setText("1000");
try self.amplitude_input.setText("10");
return self;
}
pub fn deinit(self: *MainScreen) void {
self.frequency_input.deinit();
self.amplitude_input.deinit();
self.clearProtocolErrorMessage();
}
fn pushChannelMoveCommand(self: *MainScreen, channel_view: *ChannelView, x_range: RangeF64, y_range: RangeF64) void {
const now_ns = std.time.nanoTimestamp();
var undo_stack = &self.channel_undo_stack;
@ -548,23 +576,67 @@ fn showChannelView(self: *MainScreen, channel_view: *ChannelView, height: UI.Siz
defer toolbar.endChildren();
if (self.app.getChannelSourceDevice(channel_view)) |device_channel| {
_ = device_channel;
const channel_name = device_channel.getChannelName();
const channel_type = NIDaq.getChannelType(channel_name).?;
const follow = ui.textButton("Follow");
follow.background = srcery.hard_black;
if (channel_view.follow) {
follow.borders = UI.Borders.bottom(.{
.color = srcery.green,
.size = 4
});
{
const follow = ui.textButton("Follow");
follow.background = srcery.hard_black;
if (channel_view.follow) {
follow.borders = UI.Borders.bottom(.{
.color = srcery.green,
.size = 4
});
}
if (ui.signal(follow).clicked()) {
channel_view.follow = !channel_view.follow;
}
}
if (ui.signal(follow).clicked()) {
channel_view.follow = !channel_view.follow;
if (channel_type == .analog_output) {
const button = ui.button(ui.keyFromString("Output generation"));
button.texture = Assets.output_generation;
button.size.y = UI.Sizing.initGrowFull();
const signal = ui.signal(button);
if (signal.clicked()) {
if (self.app.isDeviceChannelActive(channel_view)) {
self.app.deferred_actions.appendAssumeCapacity(.{
.deactivate = channel_view
});
} else {
try self.openProtocolModal(channel_view);
}
}
var color = rl.Color.white;
if (self.app.isDeviceChannelActive(channel_view)) {
color = srcery.red;
}
if (signal.active) {
button.texture_color = color.alpha(0.6);
} else if (signal.hot) {
button.texture_color = color.alpha(0.8);
} else {
button.texture_color = color;
}
}
_ = ui.createBox(.{
.size_x = UI.Sizing.initGrowFull()
});
{
const label = ui.label("{s}", .{channel_name});
label.size.y = UI.Sizing.initGrowFull();
label.alignment.x = .center;
label.alignment.y = .center;
label.padding = UI.Padding.horizontal(ui.rem(1));
}
}
}
if (!show_ruler) {
_ = self.showChannelViewGraph(channel_view);
@ -623,11 +695,157 @@ fn showChannelView(self: *MainScreen, channel_view: *ChannelView, height: UI.Siz
}
}
fn openProtocolModal(self: *MainScreen, channel_view: *ChannelView) !void {
self.protocol_modal = channel_view;
}
fn closeModal(self: *MainScreen) void {
self.protocol_modal = null;
}
pub fn showProtocolModal(self: *MainScreen, channel_view: *ChannelView) !void {
var ui = &self.app.ui;
const container = ui.createBox(.{
.key = ui.keyFromString("Protocol modal"),
.background = srcery.black,
.size_x = UI.Sizing.initGrowUpTo(.{ .pixels = 300 }),
.size_y = UI.Sizing.initGrowUpTo(.{ .pixels = 300 }),
.layout_direction = .top_to_bottom,
.padding = UI.Padding.all(ui.rem(1.5)),
.flags = &.{ .clickable },
.layout_gap = ui.rem(0.5)
});
container.beginChildren();
defer container.endChildren();
const FormInput = struct {
name: []const u8,
storage: *UI.TextInputStorage,
value: *f32
};
var frequency: f32 = 0;
var amplitude: f32 = 0;
const form_inputs = &[_]FormInput{
.{
.name = "Frequency",
.storage = &self.frequency_input,
.value = &frequency
},
.{
.name = "Amplitude",
.storage = &self.amplitude_input,
.value = &amplitude
},
};
for (form_inputs) |form_input| {
const label = form_input.name;
const text_input_storage = form_input.storage;
const row = ui.createBox(.{
.key = ui.keyFromString(label),
.layout_direction = .left_to_right,
.size_x = UI.Sizing.initGrowFull(),
.size_y = UI.Sizing.initFixedPixels(ui.rem(1.5))
});
row.beginChildren();
defer row.endChildren();
const label_box = ui.label("{s}", .{ label });
label_box.size.x = UI.Sizing.initFixed(.{ .font_size = 5 });
label_box.size.y = UI.Sizing.initGrowFull();
label_box.alignment.y = .center;
try ui.textInput(ui.keyFromString("Text input"), text_input_storage);
}
if (self.protocol_error_message) |message| {
_ = ui.createBox(.{
.size_x = UI.Sizing.initGrowFull(),
.size_y = UI.Sizing.initFixedText(),
.text_color = srcery.red,
.align_x = .start,
.align_y = .start,
.flags = &.{ .wrap_text },
.text = message
});
}
_ = ui.createBox(.{
.size_x = UI.Sizing.initGrowFull(),
.size_y = UI.Sizing.initGrowFull(),
});
{
const row = ui.createBox(.{
.size_x = UI.Sizing.initGrowFull(),
.size_y = UI.Sizing.initFitChildren(),
.align_x = .end
});
row.beginChildren();
defer row.endChildren();
const btn = ui.textButton("Confirm");
btn.borders = UI.Borders.all(.{ .color = srcery.green, .size = 4 });
if (ui.signal(btn).clicked() or ui.isKeyboardPressed(.key_enter)) {
self.clearProtocolErrorMessage();
for (form_inputs) |form_input| {
const label = form_input.name;
const text_input_storage: *UI.TextInputStorage = form_input.storage;
const number = std.fmt.parseFloat(f32, text_input_storage.buffer.items) catch {
try self.setProtocolErrorMessage("ERROR: {s} must be a number", .{ label });
continue;
};
if (number <= 0) {
try self.setProtocolErrorMessage("ERROR: {s} must be positive", .{ label });
continue;
}
form_input.value.* = number;
}
if (self.protocol_error_message == null) {
self.app.deferred_actions.appendAssumeCapacity(.{
.activate = channel_view
});
self.protocol_modal = null;
}
}
}
_ = ui.signal(container);
}
fn setProtocolErrorMessage(self: *MainScreen, comptime fmt: []const u8, args: anytype) !void {
self.clearProtocolErrorMessage();
const allocator = self.app.allocator;
self.protocol_error_message = try std.fmt.allocPrint(allocator, fmt, args);
}
fn clearProtocolErrorMessage(self: *MainScreen) void {
const allocator = self.app.allocator;
if (self.protocol_error_message) |msg| {
allocator.free(msg);
}
self.protocol_error_message = null;
}
pub fn tick(self: *MainScreen) !void {
var ui = &self.app.ui;
if (ui.isKeyboardPressed(.key_escape)) {
if (self.fullscreen_channel != null) {
if (self.protocol_modal != null) {
self.closeModal();
} else if (self.fullscreen_channel != null) {
self.fullscreen_channel = null;
} else {
self.app.should_close = true;
@ -649,12 +867,45 @@ pub fn tick(self: *MainScreen) !void {
const root = ui.parentBox().?;
root.layout_direction = .top_to_bottom;
var maybe_modal_overlay: ?*UI.Box = null;
if (self.protocol_modal) |channel_view| {
const padding = UI.Padding.all(ui.rem(2));
const modal_overlay = ui.createBox(.{
.key = ui.keyFromString("Overlay"),
.float_rect = .{
.x = 0,
.y = 0,
// TODO: This is a hack, UI core should handle this
.width = root.persistent.size.x - padding.byAxis(.X),
.height = root.persistent.size.y - padding.byAxis(.Y)
},
.background = rl.Color.black.alpha(0.6),
.flags = &.{ .clickable, .scrollable },
.padding = padding,
.align_x = .center,
.align_y = .center,
});
modal_overlay.beginChildren();
defer modal_overlay.endChildren();
try self.showProtocolModal(channel_view);
if (ui.signal(modal_overlay).clicked()) {
self.closeModal();
}
maybe_modal_overlay = modal_overlay;
}
{
const toolbar = ui.createBox(.{
.background = srcery.black,
.layout_direction = .left_to_right,
.size_x = .{ .fixed = .{ .parent_percent = 1 } },
.size_y = .{ .fixed = .{ .font_size = 2 } }
.size_y = .{ .fixed = .{ .font_size = 2 } },
.borders = .{
.bottom = .{ .color = srcery.hard_black, .size = 4 }
}
});
toolbar.beginChildren();
defer toolbar.endChildren();
@ -666,41 +917,17 @@ pub fn tick(self: *MainScreen) !void {
start_all.padding.top = 0;
start_all.padding.bottom = 0;
if (ui.signal(start_all).clicked()) {
self.app.started_collecting = !self.app.started_collecting;
for (self.app.listChannelViews()) |*channel_view| {
if (self.app.started_collecting) {
self.app.startDeviceChannelReading(channel_view);
} else {
self.app.stopDeviceChannelReading(channel_view);
}
}
self.app.deferred_actions.appendAssumeCapacity(.{
.toggle_input_channels = {}
});
}
if (self.app.started_collecting) {
if (self.app.task_read_active) {
start_all.setText("Stop");
} else {
start_all.setText("Start");
}
}
if (self.app.started_collecting) {
for (self.app.listChannelViews()) |*channel_view| {
const device_channel = self.app.getChannelSourceDevice(channel_view) orelse continue;
if (!channel_view.follow) continue;
const sample_rate = device_channel.active_task.?.sampling.sample_rate;
const sample_count: f32 = @floatFromInt(device_channel.samples.items.len);
channel_view.view_rect.y_range = channel_view.y_range;
channel_view.view_rect.x_range.lower = 0;
if (sample_count > channel_view.view_rect.x_range.upper) {
channel_view.view_rect.x_range.upper = sample_count + @as(f32, @floatCast(sample_rate)) * 10;
}
// channel_view.view_cache.invalidate();
}
}
if (self.fullscreen_channel) |channel| {
try self.showChannelView(channel, UI.Sizing.initGrowFull());
@ -728,8 +955,8 @@ pub fn tick(self: *MainScreen) !void {
const add_from_file = ui.textButton("Add from file");
add_from_file.borders = UI.Borders.all(.{ .size = 2, .color = srcery.green });
if (ui.signal(add_from_file).clicked()) {
self.app.channel_mutex.unlock();
defer self.app.channel_mutex.lock();
self.app.samples_mutex.unlock();
defer self.app.samples_mutex.lock();
if (Platform.openFilePicker(self.app.allocator)) |filename| {
defer self.app.allocator.free(filename);
@ -749,4 +976,27 @@ pub fn tick(self: *MainScreen) !void {
}
}
}
if (maybe_modal_overlay) |modal_overlay| {
root.bringChildToTop(modal_overlay);
}
if (self.app.task_read_active) {
for (self.app.listChannelViews()) |*channel_view| {
const device_channel = self.app.getChannelSourceDevice(channel_view) orelse continue;
if (!channel_view.follow) continue;
const channel_task = device_channel.task orelse continue;
const sample_rate = channel_task.sample_rate;
const sample_count: f32 = @floatFromInt(device_channel.samples.items.len);
channel_view.view_rect.y_range = channel_view.y_range;
channel_view.view_rect.x_range.lower = 0;
if (sample_count > channel_view.view_rect.x_range.upper) {
channel_view.view_rect.x_range.upper = sample_count + @as(f32, @floatCast(sample_rate)) * 10;
}
}
}
}

View File

@ -5,7 +5,6 @@ const Assets = @import("./assets.zig");
const rect_utils = @import("./rect-utils.zig");
const utils = @import("./utils.zig");
const builtin = @import("builtin");
const P = @import("profiler");
const FontFace = @import("./font-face.zig");
const raylib_h = @cImport({
@cInclude("stdio.h");
@ -16,6 +15,7 @@ const assert = std.debug.assert;
pub const Vec2 = rl.Vector2;
pub const Rect = rl.Rectangle;
const clamp = std.math.clamp;
const log = std.log.scoped(.ui);
const Vec2Zero = Vec2{ .x = 0, .y = 0 };
@ -76,7 +76,8 @@ pub const Event = union(enum) {
mouse_move: Vec2,
mouse_scroll: Vec2,
key_pressed: u32,
key_released: u32
key_released: u32,
char_pressed: u32
};
pub const Signal = struct {
@ -107,9 +108,11 @@ pub const Signal = struct {
scroll: Vec2 = Vec2Zero,
relative_mouse: Vec2 = Vec2Zero,
mouse: Vec2 = Vec2Zero,
is_mouse_inside: bool = false,
hot: bool = false,
active: bool = false,
shift_modifier: bool = false,
ctrl_modifier: bool = false,
pub fn clicked(self: Signal) bool {
return self.flags.contains(.left_clicked) or self.flags.contains(.right_clicked) or self.flags.contains(.middle_clicked);
@ -205,7 +208,15 @@ pub const Unit = union(enum) {
.text => {
const text = box.text orelse return 0;
const font_face = Assets.font(box.font);
var text_size = font_face.measureText(text);
const lines: []const []u8 = box.text_lines.constSlice();
var text_size: Vec2 = undefined;
if (lines.len == 0) {
text_size = font_face.measureText(text);
} else {
text_size = font_face.measureTextLines(lines);
}
return vec2ByAxis(&text_size, axis).*;
},
.font_size => |count| {
@ -233,6 +244,18 @@ pub const Sizing = union(enum) {
};
}
pub fn initFixedPixels(size: f32) Sizing {
return Sizing{
.fixed = Unit.initPixels(size)
};
}
pub fn initFixedText() Sizing {
return Sizing{
.fixed = .text
};
}
pub fn initGrowFull() Sizing {
return initGrow(
.{ .pixels = 0 },
@ -465,6 +488,8 @@ pub const Box = struct {
persistent: Persistent,
alignment: Alignment2,
size: Sizing2,
// TODO: Add option for changing how padding interacts with size. Does it remove from the available size, or is it added on top
// Currently it is added on top
padding: Padding,
font: Assets.FontId,
text_color: rl.Color,
@ -475,6 +500,7 @@ pub const Box = struct {
view_offset: Vec2 = Vec2Zero,
texture: ?rl.Texture2D = null,
texture_size: ?Vec2 = null,
texture_color: ?rl.Color = null,
scientific_number: ?f64 = null,
scientific_precision: u32 = 1,
float_relative_to: ?*Box = null,
@ -496,6 +522,8 @@ pub const Box = struct {
parent_index: ?BoxIndex = null,
// Go the next node on the same level
next_sibling_index: ?BoxIndex = null,
// Go to previous node on the same level
prev_sibling_index: ?BoxIndex = null,
},
pub fn beginChildren(self: *Box) void {
@ -615,6 +643,55 @@ pub const Box = struct {
const size = vec2ByAxis(&self.persistent.size, axis).*;
return @max(size - self.padding.byAxis(axis), 0);
}
pub fn appendChild(self: *Box, child: *Box) void {
child.tree.parent_index = self.tree.index;
if (self.tree.last_child_index) |last_child_index| {
const last_child = self.ui.getBoxByIndex(last_child_index);
last_child.tree.next_sibling_index = child.tree.index;
self.tree.last_child_index = child.tree.index;
self.tree.prev_sibling_index = last_child.tree.index;
} else {
self.tree.first_child_index = child.tree.index;
self.tree.last_child_index = child.tree.index;
}
}
pub fn removeChild(self: *Box, child: *Box) void {
assert(child.tree.parent_index == self.tree.index);
child.tree.parent_index = null;
defer child.tree.next_sibling_index = null;
defer child.tree.prev_sibling_index = null;
var next_sibling: ?*Box = null;
if (child.tree.next_sibling_index) |next_sibling_index| {
next_sibling = self.ui.getBoxByIndex(next_sibling_index);
}
var prev_sibling: ?*Box = null;
if (child.tree.prev_sibling_index) |prev_sibling_index| {
prev_sibling = self.ui.getBoxByIndex(prev_sibling_index);
}
if (next_sibling != null and prev_sibling != null) {
next_sibling.?.tree.prev_sibling_index = prev_sibling.?.tree.index;
prev_sibling.?.tree.next_sibling_index = next_sibling.?.tree.index;
} else if (next_sibling != null) {
next_sibling.?.tree.prev_sibling_index = null;
self.tree.first_child_index = next_sibling.?.tree.index;
} else if (prev_sibling != null) {
prev_sibling.?.tree.next_sibling_index = null;
self.tree.last_child_index = prev_sibling.?.tree.index;
}
}
pub fn bringChildToTop(self: *Box, child: *Box) void {
self.removeChild(child);
self.appendChild(child);
}
};
pub const BoxOptions = struct {
@ -643,7 +720,8 @@ pub const BoxOptions = struct {
scientific_number: ?f64 = null,
scientific_precision: ?u32 = null,
float_relative_to: ?*Box = null,
parent: ?*UI.Box = null
parent: ?*UI.Box = null,
texture_color: ?rl.Color = null,
};
pub const root_box_key = Key.initString(0, "$root$");
@ -679,6 +757,31 @@ const BoxParentIterator = struct {
}
};
const CharPressIterator = struct {
events: *Events,
index: usize = 0,
pub fn next(self: *CharPressIterator) ?u32 {
while (self.index < self.events.len) {
const event = self.events.get(self.index);
self.index += 1;
if (event == .char_pressed) {
return event.char_pressed;
}
}
return null;
}
pub fn consume(self: *CharPressIterator) void {
self.index -= 1;
_ = self.events.swapRemove(self.index);
}
};
const Events = std.BoundedArray(Event, max_events);
arenas: [2]std.heap.ArenaAllocator,
// Retained structures. Used for tracking changes between frames
@ -691,7 +794,7 @@ scissor_stack: std.BoundedArray(Rect, 16) = .{},
mouse: Vec2 = .{ .x = 0, .y = 0 },
mouse_delta: Vec2 = .{ .x = 0, .y = 0 },
mouse_buttons: std.EnumSet(rl.MouseButton) = .{},
events: std.BoundedArray(Event, max_events) = .{},
events: Events = .{},
dt: f32 = 0,
// Per layout pass fields
@ -717,41 +820,13 @@ pub fn frame_arena(self: *UI) *std.heap.ArenaAllocator {
return &self.arenas[@mod(self.frame_index, 2)];
}
pub fn frame_allocator(self: *UI) std.mem.Allocator {
pub fn frameAllocator(self: *UI) std.mem.Allocator {
return self.frame_arena().allocator();
}
pub fn begin(self: *UI) void {
const zone = P.begin(@src(), "UI begin()");
defer zone.end();
self.font_stack.len = 0;
self.parent_stack.len = 0;
pub fn pullOsEvents(self: *UI) void {
self.events.len = 0;
// Remove boxes which haven't been used in the last frame
{
const zone2 = P.begin(@src(), "UI remove boxes");
defer zone2.end();
var i: usize = 0;
while (i < self.boxes.len) {
const box = &self.boxes.buffer[i];
if (box.last_used_frame != self.frame_index) {
if (self.boxes.len > 0) {
self.boxes.buffer[i] = self.boxes.buffer[self.boxes.len - 1];
}
self.boxes.len -= 1;
} else {
i += 1;
}
}
}
if (self.active_box_keys.count() == 0) {
self.hot_box_key = null;
}
const mouse = rl.getMousePosition();
self.mouse_delta = mouse.subtract(self.mouse);
self.dt = rl.getFrameTime();
@ -785,10 +860,46 @@ pub fn begin(self: *UI) void {
}
}
while (true) {
const char = rl.getCharPressed();
if (char == 0) {
break;
}
self.events.appendAssumeCapacity(Event{
.char_pressed = @intCast(char)
});
}
const mouse_wheel = rl.getMouseWheelMoveV();
if (mouse_wheel.x != 0 or mouse_wheel.y != 0) {
self.events.appendAssumeCapacity(Event{ .mouse_scroll = mouse_wheel });
}
}
pub fn begin(self: *UI) void {
self.font_stack.len = 0;
self.parent_stack.len = 0;
// Remove boxes which haven't been used in the last frame
{
var i: usize = 0;
while (i < self.boxes.len) {
const box = &self.boxes.buffer[i];
if (box.last_used_frame != self.frame_index) {
if (self.boxes.len > 0) {
self.boxes.buffer[i] = self.boxes.buffer[self.boxes.len - 1];
}
self.boxes.len -= 1;
} else {
i += 1;
}
}
}
if (!self.isKeyActiveAny()) {
self.hot_box_key = null;
}
self.frame_index += 1;
_ = self.frame_arena().reset(.retain_capacity);
@ -912,8 +1023,11 @@ pub fn end(self: *UI) void {
}
fn layoutSizesPass(self: *UI, root_box: *Box) void {
// TODO: Figure out a way for fit children to work without calling it twice.
self.layoutSizesInitial(root_box, .X);
self.layoutSizesShrink(root_box, .X);
self.layoutSizesFitChildren(root_box, .X);
self.layoutSizesGrow(root_box, .X);
self.layoutSizesFitChildren(root_box, .X);
@ -921,6 +1035,7 @@ fn layoutSizesPass(self: *UI, root_box: *Box) void {
self.layoutSizesInitial(root_box, .Y);
self.layoutSizesShrink(root_box, .Y);
self.layoutSizesFitChildren(root_box, .Y);
self.layoutSizesGrow(root_box, .Y);
self.layoutSizesFitChildren(root_box, .Y);
}
@ -1092,11 +1207,12 @@ fn layoutSizesShrink(self: *UI, box: *Box, axis: Axis) void {
break;
}
var largest_size: f32 = vec2ByAxis(&shrinkable_children.buffer[0].box.persistent.size, axis).*;
var largest_child = shrinkable_children.buffer[0];
var largest_size: f32 = vec2ByAxis(&largest_child.box.persistent.size, axis).*;
var second_largest_size: ?f32 = null;
var largest_children: std.BoundedArray(*Box, max_boxes) = .{};
largest_children.appendAssumeCapacity(shrinkable_children.buffer[0].box);
largest_children.appendAssumeCapacity(largest_child.box);
for (shrinkable_children.slice()[1..]) |shrinkable_child| {
const child = shrinkable_child.box;
@ -1105,6 +1221,7 @@ fn layoutSizesShrink(self: *UI, box: *Box, axis: Axis) void {
if (child_persistent_size.* > largest_size) {
second_largest_size = largest_size;
largest_size = child_persistent_size.*;
largest_child = shrinkable_child;
largest_children.len = 0;
largest_children.appendAssumeCapacity(child);
} else if (child_persistent_size.* < largest_size) {
@ -1114,7 +1231,7 @@ fn layoutSizesShrink(self: *UI, box: *Box, axis: Axis) void {
}
}
var shrink_largest_by = overflow_size;
var shrink_largest_by = @max(overflow_size, largest_child.min_size);
if (second_largest_size != null) {
shrink_largest_by = @min(shrink_largest_by, largest_size - second_largest_size.?);
}
@ -1122,8 +1239,8 @@ fn layoutSizesShrink(self: *UI, box: *Box, axis: Axis) void {
const largest_count_f32: f32 = @floatFromInt(largest_children.len);
shrink_largest_by = @min(shrink_largest_by * largest_count_f32, largest_size) / largest_count_f32;
for (largest_children.slice()) |largest_child| {
const child_persistent_size = vec2ByAxis(&largest_child.persistent.size, axis);
for (largest_children.slice()) |child| {
const child_persistent_size = vec2ByAxis(&child.persistent.size, axis);
child_persistent_size.* -= shrink_largest_by;
overflow_size -= shrink_largest_by;
}
@ -1143,7 +1260,7 @@ fn layoutSizesFitChildren(self: *UI, box: *Box, axis: Axis) void {
if (box.size.getAxis(axis) == .fit_children) {
const children_size = sumChildrenSize(box, axis);
const persistent_size = vec2ByAxis(&box.persistent.size, axis);
persistent_size.* += children_size;
persistent_size.* = children_size + box.padding.byAxis(axis);
}
}
@ -1207,11 +1324,12 @@ fn layoutSizesGrow(self: *UI, box: *Box, axis: Axis) void {
break;
}
var smallest_size: f32 = vec2ByAxis(&growable_children.buffer[0].box.persistent.size, axis).*;
var smallest_child = growable_children.buffer[0];
var smallest_size: f32 = vec2ByAxis(&smallest_child.box.persistent.size, axis).*;
var second_smallest_size: ?f32 = null;
var smallest_children: std.BoundedArray(*Box, max_boxes) = .{};
smallest_children.appendAssumeCapacity(growable_children.buffer[0].box);
smallest_children.appendAssumeCapacity(smallest_child.box);
for (growable_children.slice()[1..]) |growable_child| {
const child = growable_child.box;
@ -1220,6 +1338,7 @@ fn layoutSizesGrow(self: *UI, box: *Box, axis: Axis) void {
if (child_persistent_size.* < smallest_size) {
second_smallest_size = smallest_size;
smallest_size = child_persistent_size.*;
smallest_child = growable_child;
smallest_children.len = 0;
smallest_children.appendAssumeCapacity(child);
} else if (child_persistent_size.* > smallest_size) {
@ -1229,7 +1348,7 @@ fn layoutSizesGrow(self: *UI, box: *Box, axis: Axis) void {
}
}
var grow_smallest_by = unused_size;
var grow_smallest_by = @min(unused_size, smallest_child.max_size);
if (second_smallest_size != null) {
grow_smallest_by = @min(grow_smallest_by, second_smallest_size.? - smallest_size);
}
@ -1237,8 +1356,8 @@ fn layoutSizesGrow(self: *UI, box: *Box, axis: Axis) void {
const smallest_count_f32: f32 = @floatFromInt(smallest_children.len);
grow_smallest_by = @min(grow_smallest_by * smallest_count_f32, unused_size) / smallest_count_f32;
for (smallest_children.slice()) |smallest_child| {
const child_persistent_size = vec2ByAxis(&smallest_child.persistent.size, axis);
for (smallest_children.slice()) |child| {
const child_persistent_size = vec2ByAxis(&child.persistent.size, axis);
child_persistent_size.* += grow_smallest_by;
unused_size -= grow_smallest_by;
}
@ -1383,7 +1502,7 @@ pub fn createBox(self: *UI, opts: BoxOptions) *Box {
box.* = Box{
.ui = self,
.allocator = self.frame_allocator(),
.allocator = self.frameAllocator(),
.persistent = persistent,
.flags = flags,
@ -1405,6 +1524,7 @@ pub fn createBox(self: *UI, opts: BoxOptions) *Box {
.scientific_number = opts.scientific_number,
.scientific_precision = opts.scientific_precision orelse 1,
.float_relative_to = opts.float_relative_to,
.texture_color = opts.texture_color,
.last_used_frame = self.frame_index,
.key = key,
@ -1438,17 +1558,7 @@ pub fn createBox(self: *UI, opts: BoxOptions) *Box {
}
if (opts.parent orelse self.parentBox()) |parent| {
box.tree.parent_index = parent.tree.index;
if (parent.tree.last_child_index) |last_child_index| {
const last_child = self.getBoxByIndex(last_child_index);
last_child.tree.next_sibling_index = box.tree.index;
parent.tree.last_child_index = box.tree.index;
} else {
parent.tree.first_child_index = box.tree.index;
parent.tree.last_child_index = box.tree.index;
}
parent.appendChild(box);
}
return box;
@ -1563,7 +1673,7 @@ fn drawBox(self: *UI, box: *Box) void {
destination,
rl.Vector2.zero(),
0,
rl.Color.white
box.texture_color orelse rl.Color.white
);
}
@ -1734,6 +1844,7 @@ pub fn signal(self: *UI, box: *Box) Signal {
var result = Signal{};
if (box.key.isNil()) {
log.warn("Called signal() with nil key", .{});
return result;
}
@ -1822,7 +1933,9 @@ pub fn signal(self: *UI, box: *Box) Signal {
result.active = self.isKeyActive(box.key);
result.relative_mouse = self.mouse.subtract(rect_utils.position(rect));
result.mouse = self.mouse;
result.is_mouse_inside = is_mouse_inside;
result.shift_modifier = rl.isKeyDown(rl.KeyboardKey.key_left_shift) or rl.isKeyDown(rl.KeyboardKey.key_right_shift);
result.ctrl_modifier = rl.isKeyDown(rl.KeyboardKey.key_left_control) or rl.isKeyDown(rl.KeyboardKey.key_right_control);
return result;
}
@ -1849,6 +1962,10 @@ pub fn isKeyActive(self: *UI, key: Key) bool {
return false;
}
pub fn isKeyActiveAny(self: *UI) bool {
return self.active_box_keys.count() != 0;
}
pub fn isKeyboardPressed(self: *UI, key: rl.KeyboardKey) bool {
const key_u32: u32 = @intCast(@intFromEnum(key));
@ -1862,12 +1979,31 @@ pub fn isKeyboardPressed(self: *UI, key: rl.KeyboardKey) bool {
return false;
}
pub fn isKeyboardPressedOrHeld(self: *UI, key: rl.KeyboardKey) bool {
return self.isKeyboardPressed(key) or rl.isKeyPressedRepeat(key);
}
pub fn hasKeyboardPressess(self: *UI) bool {
for (self.events.slice()) |_event| {
const event: Event = _event;
if (event == .key_pressed) {
return true;
}
}
return false;
}
pub fn iterCharacterPressess(self: *UI) CharPressIterator {
return CharPressIterator{
.events = &self.events,
};
}
pub fn isShiftDown(self: *UI) bool {
_ = self;
return rl.isKeyDown(rl.KeyboardKey.key_left_shift) or rl.isKeyDown(rl.KeyboardKey.key_right_shift);
}
pub fn isCtrlDown(self: *UI) bool {
_ = self;
return rl.isKeyDown(rl.KeyboardKey.key_left_control) or rl.isKeyDown(rl.KeyboardKey.key_right_control);
@ -1875,6 +2011,179 @@ pub fn isCtrlDown(self: *UI) bool {
// --------------------------------- Widgets ----------------------------------------- //
pub const TextInputStorage = struct {
buffer: std.ArrayList(u8),
editing: bool = false,
last_pressed_at_ns: i128 = 0,
cursor_start: usize = 0,
cursor_stop: usize = 0,
shown_slice_start: f32 = 0,
shown_slice_end: f32 = 0,
pub fn init(allocator: std.mem.Allocator) TextInputStorage {
return TextInputStorage{
.buffer = std.ArrayList(u8).init(allocator)
};
}
pub fn deinit(self: TextInputStorage) void {
self.buffer.deinit();
}
pub fn setText(self: *TextInputStorage, text: []const u8) !void {
self.buffer.clearAndFree();
try self.buffer.appendSlice(text);
}
pub fn insertSingle(self: *TextInputStorage, index: usize, symbol: u8) !void {
try self.buffer.insert(index, symbol);
if (self.cursor_start >= index) {
self.cursor_start += 1;
}
if (self.cursor_stop >= index) {
self.cursor_stop += 1;
}
}
fn deleteSingle(self: *TextInputStorage, index: usize) void {
self.deleteMany(index, index+1);
}
/// If `from` and `to` are the same number, nothing will be deleted
fn deleteMany(self: *TextInputStorage, from: usize, to: usize) void {
const from_clamped = std.math.clamp(from, 0, self.buffer.items.len);
const to_clamped = std.math.clamp(to , 0, self.buffer.items.len);
if (from_clamped == to_clamped) return;
const lower = @min(from_clamped, to_clamped);
const upper = @max(from_clamped, to_clamped);
assert(lower < upper);
const deleted_count = (upper - lower);
std.mem.copyForwards(u8, self.buffer.items[lower..], self.buffer.items[upper..]);
self.buffer.items.len -= deleted_count;
if (self.cursor_start > upper) {
self.cursor_start -= deleted_count;
} else if (self.cursor_start > lower) {
self.cursor_start = lower;
}
if (self.cursor_stop > upper) {
self.cursor_stop -= deleted_count;
} else if (self.cursor_stop > lower) {
self.cursor_stop = lower;
}
}
fn getCharOffsetX(self: *const TextInputStorage, font: FontFace, index: usize) f32 {
const text = self.buffer.items;
return font.measureWidth(text[0..index]);
}
fn getCharIndex(self: *const TextInputStorage, font: FontFace, x: f32) usize {
var measure_opts = FontFace.MeasureOptions{
.up_to_width = x + self.shown_slice_start
};
const before_size = font.measureTextEx(self.buffer.items, &measure_opts).x;
const index = measure_opts.last_codepoint_index;
if (index+1 < self.buffer.items.len) {
const after_size = self.getCharOffsetX(font, index + 1);
if (@abs(before_size - x) > @abs(after_size - x)) {
return index + 1;
}
}
return index;
}
fn nextJumpPoint(self: *const TextInputStorage, index: usize, step: isize) usize {
assert(step == 1 or step == -1); // Only tested for these cases
var i = index;
const text = self.buffer.items;
if (step > 0) {
var prev_whitespace = std.ascii.isWhitespace(text[i]);
while (true) {
const cur_whitespace = std.ascii.isWhitespace(text[i]);
if (!prev_whitespace and cur_whitespace) {
break;
}
prev_whitespace = cur_whitespace;
const next_i = @as(isize, @intCast(i)) + step;
if (next_i > text.len) {
break;
}
i = @intCast(next_i);
if (i == text.len) {
break;
}
}
} else {
if (index == 0) return index;
i = @intCast(@as(isize, @intCast(i)) + step);
var cur_whitespace = std.ascii.isWhitespace(text[i]);
while (true) {
const next_i = @as(isize, @intCast(i)) + step;
if (next_i < 0) {
break;
}
const next_whitespace = std.ascii.isWhitespace(text[@intCast(next_i)]);
if (!cur_whitespace and next_whitespace) {
break;
}
cur_whitespace = next_whitespace;
i = @intCast(next_i);
}
}
return i;
}
pub fn textLength(self: *TextInputStorage) []const u8 {
return self.buffer.items;
}
fn cursorSlice(self: *const TextInputStorage) []const u8 {
if (self.cursor_start == self.cursor_stop) {
return self.buffer.items[self.cursor_start..(self.cursor_start+1)];
} else {
const lower = @min(self.cursor_start, self.cursor_stop);
const upper = @max(self.cursor_start, self.cursor_stop);
return self.buffer.items[lower..upper];
}
}
fn insertMany(self: *TextInputStorage, index: usize, text: []const u8) !void {
if (index > self.buffer.items.len) return;
try self.buffer.insertSlice(index, text);
if (self.cursor_start >= index) {
self.cursor_start += text.len;
}
if (self.cursor_stop >= index) {
self.cursor_stop += text.len;
}
}
};
pub fn mouseTooltip(self: *UI) *Box {
const tooltip = self.getBoxByKey(mouse_tooltip_box_key).?;
@ -1996,4 +2305,280 @@ pub fn endScrollbar(self: *UI) void {
}
wrapper.endChildren();
}
pub fn textInput(self: *UI, key: Key, storage: *TextInputStorage) !void {
const now = std.time.nanoTimestamp();
const container = self.createBox(.{
.key = key,
.size_x = Sizing.initGrowUpTo(.{ .pixels = 200 }),
.size_y = Sizing.initFixed(Unit.initPixels(self.rem(1))),
.flags = &.{ .clickable, .clip_view, .draggable },
.background = srcery.bright_white,
.align_y = .center,
.padding = UI.Padding.all(self.rem(0.25)),
});
container.beginChildren();
defer container.endChildren();
const font = Assets.font(container.font);
const text = &storage.buffer;
const cursor_start_x = storage.getCharOffsetX(font, storage.cursor_start);
const cursor_stop_x = storage.getCharOffsetX(font, storage.cursor_stop);
{ // Text visuals
const text_size = font.measureText(text.items);
const visible_text_width = container.persistent.size.x - container.padding.byAxis(.X);
const shown_window_size = @min(visible_text_width, text_size.x);
if (storage.shown_slice_end - storage.shown_slice_start != shown_window_size) {
const shown_slice_middle = (storage.shown_slice_end + storage.shown_slice_start) / 2;
storage.shown_slice_start = shown_slice_middle - shown_window_size/2;
storage.shown_slice_end = shown_slice_middle + shown_window_size/2;
}
if (storage.shown_slice_end > text_size.x) {
storage.shown_slice_end = shown_window_size;
storage.shown_slice_start = storage.shown_slice_end - shown_window_size;
} else if (storage.shown_slice_start < 0) {
storage.shown_slice_start = 0;
storage.shown_slice_end = shown_window_size;
}
if (cursor_stop_x > storage.shown_slice_end) {
storage.shown_slice_start = cursor_stop_x - shown_window_size;
storage.shown_slice_end = cursor_stop_x;
}
if (cursor_stop_x < storage.shown_slice_start) {
storage.shown_slice_start = cursor_stop_x;
storage.shown_slice_end = cursor_stop_x + shown_window_size;
}
_ = self.createBox(.{
.text_color = srcery.black,
.text = text.items,
.float_relative_to = container,
.float_rect = Rect{
.x = container.padding.left - storage.shown_slice_start,
.y = 0,
.width = visible_text_width,
.height = container.persistent.size.y
},
.align_y = .center,
.align_x = .start
});
}
const container_signal = self.signal(container);
if (container_signal.hot) {
container.borders = UI.Borders.all(.{
.color = srcery.red,
.size = 2
});
}
// Text editing visuals
if (storage.editing) {
const blink_period = std.time.ns_per_s;
const blink = @mod(now - storage.last_pressed_at_ns, blink_period) < 0.5 * blink_period;
const cursor_color = srcery.hard_black;
if (storage.cursor_start == storage.cursor_stop and blink) {
_ = self.createBox(.{
.background = cursor_color,
.float_relative_to = container,
.float_rect = Rect{
.x = container.padding.left + cursor_start_x - storage.shown_slice_start,
.y = container.padding.top,
.width = 2,
.height = container.persistent.size.y - container.padding.byAxis(.Y)
},
});
}
if (storage.cursor_start != storage.cursor_stop) {
const lower_cursor_x = @min(cursor_start_x, cursor_stop_x);
const upper_cursor_x = @max(cursor_start_x, cursor_stop_x);
_ = self.createBox(.{
.background = cursor_color.alpha(0.25),
.float_relative_to = container,
.float_rect = Rect{
.x = container.padding.left + lower_cursor_x - storage.shown_slice_start,
.y = container.padding.top,
.width = upper_cursor_x - lower_cursor_x,
.height = container.persistent.size.y - container.padding.byAxis(.Y)
},
});
}
}
if (container_signal.active) {
storage.editing = true;
}
if (self.isKeyActiveAny() and !container_signal.active) {
storage.editing = false;
}
// Text input controls
if (storage.editing) {
const shift = container_signal.shift_modifier;
const ctrl = container_signal.ctrl_modifier;
var no_blinking = false;
{ // Cursor movement controls
var move_cursor_dir: i32 = 0;
if (self.isKeyboardPressedOrHeld(.key_left)) {
move_cursor_dir -= 1;
}
if (self.isKeyboardPressedOrHeld(.key_right)) {
move_cursor_dir += 1;
}
if (move_cursor_dir != 0) {
if (shift) {
if (ctrl) {
storage.cursor_stop = storage.nextJumpPoint(storage.cursor_stop, move_cursor_dir);
} else {
const cursor_stop: isize = @intCast(storage.cursor_stop);
storage.cursor_stop = @intCast(std.math.clamp(
cursor_stop + move_cursor_dir,
0,
@as(isize, @intCast(text.items.len))
));
}
} else {
if (storage.cursor_start != storage.cursor_stop) {
const lower = @min(storage.cursor_start, storage.cursor_stop);
const upper = @max(storage.cursor_start, storage.cursor_stop);
if (move_cursor_dir < 0) {
if (ctrl) {
storage.cursor_start = storage.nextJumpPoint(lower, -1);
} else {
storage.cursor_start = lower;
}
} else {
if (ctrl) {
storage.cursor_start = storage.nextJumpPoint(upper, 1);
} else {
storage.cursor_start = upper;
}
}
storage.cursor_stop = storage.cursor_start;
} else {
var cursor = storage.cursor_start;
if (ctrl) {
cursor = storage.nextJumpPoint(cursor, move_cursor_dir);
} else {
cursor = @intCast(std.math.clamp(
@as(isize, @intCast(cursor)) + move_cursor_dir,
0,
@as(isize, @intCast(text.items.len))
));
}
storage.cursor_start = cursor;
storage.cursor_stop = cursor;
}
}
no_blinking = true;
}
}
{ // Cursor movement with mouse
var mouse = container_signal.relative_mouse;
mouse.x -= container.padding.left;
mouse.y -= container.padding.top;
const mouse_index = storage.getCharIndex(font, mouse.x);
if (container_signal.flags.contains(.left_pressed)) {
storage.cursor_start = mouse_index;
storage.cursor_stop = mouse_index;
no_blinking = true;
}
if (container_signal.flags.contains(.left_dragging)) {
storage.cursor_stop = mouse_index;
no_blinking = true;
}
}
// Deletion
if (self.isKeyboardPressedOrHeld(.key_backspace) and text.items.len > 0) {
if (storage.cursor_start == storage.cursor_stop) {
if (storage.cursor_start > 0) {
storage.deleteMany(storage.cursor_start-1, storage.cursor_start);
}
} else {
storage.deleteMany(storage.cursor_start, storage.cursor_stop);
}
no_blinking = true;
}
// Common commands (E.g. Ctrl+A, Ctrl+C, etc.)
if (ctrl) {
if (self.isKeyboardPressedOrHeld(.key_x)) {
if (storage.cursor_start != storage.cursor_stop) {
const allocator = storage.buffer.allocator;
const text_z = try allocator.dupeZ(u8, storage.cursorSlice());
defer allocator.free(text_z);
rl.setClipboardText(text_z);
storage.deleteMany(storage.cursor_start, storage.cursor_stop);
}
} else if (self.isKeyboardPressedOrHeld(.key_a)) {
storage.cursor_start = 0;
storage.cursor_stop = text.items.len;
} else if (self.isKeyboardPressedOrHeld(.key_c)) {
if (storage.cursor_start != storage.cursor_stop) {
const allocator = storage.buffer.allocator;
const text_z = try allocator.dupeZ(u8, storage.cursorSlice());
defer allocator.free(text_z);
rl.setClipboardText(text_z);
}
} else if (self.isKeyboardPressedOrHeld(.key_v)) {
if (storage.cursor_start != storage.cursor_stop) {
storage.deleteMany(storage.cursor_start, storage.cursor_stop);
}
const clipboard = rl.getClipboardText();
try storage.insertMany(storage.cursor_start, clipboard);
}
}
{ // Insertion
// TODO: Handle UTF8 characters
var char_iter = self.iterCharacterPressess();
while (char_iter.next()) |char| {
if (char <= 255 and std.ascii.isPrint(@intCast(char))) {
char_iter.consume();
if (storage.cursor_start != storage.cursor_stop) {
storage.deleteMany(storage.cursor_start, storage.cursor_stop);
}
try storage.insertSingle(storage.cursor_start, @intCast(char));
no_blinking = true;
}
}
}
if (no_blinking) {
storage.last_pressed_at_ns = now;
}
}
}

View File

@ -1,6 +1,8 @@
const std = @import("std");
const rl = @import("raylib");
const assert = std.debug.assert;
pub fn vec2Round(vec2: rl.Vector2) rl.Vector2 {
return rl.Vector2{
.x = @round(vec2.x),
@ -64,4 +66,14 @@ pub fn roundNearestDown(comptime T: type, value: T, multiple: T) T {
pub fn roundNearestTowardZero(comptime T: type, value: T, multiple: T) T {
return @trunc(value / multiple) * multiple;
}
pub fn initBoundedStringZ(comptime BoundedString: type, text: []const u8) !BoundedString {
var bounded_string = try BoundedString.fromSlice(text);
try bounded_string.append(0);
return bounded_string;
}
pub fn getBoundedStringZ(bounded_array: anytype) [:0]const u8 {
return bounded_array.buffer[0..(bounded_array.len-1) :0];
}