add modal for protocol parameters
This commit is contained in:
parent
8ba3d0c914
commit
31d0af0a5c
337
src/app.zig
337
src/app.zig
@ -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;
|
||||
|
||||
|
@ -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 {
|
||||
|
BIN
src/assets/output-generation-icon.ase
Normal file
BIN
src/assets/output-generation-icon.ase
Normal file
Binary file not shown.
@ -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 {
|
||||
|
@ -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) {
|
||||
|
24
src/main.zig
24
src/main.zig
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
@ -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 = &litude
|
||||
},
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
705
src/ui.zig
705
src/ui.zig
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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];
|
||||
}
|
Loading…
Reference in New Issue
Block a user