split up main.zig into app.zig and graph.zig

This commit is contained in:
Rokas Puzonas 2024-12-01 16:30:24 +02:00
parent 24895afce6
commit 0d9033c926
11 changed files with 1200 additions and 429 deletions

View File

@ -41,6 +41,8 @@ pub fn build(b: *std.Build) !void {
exe.addWin32ResourceFile(.{
.file = resource_file.add("daq-view.rc", "IDI_ICON ICON \"./src/assets/icon.ico\""),
});
exe.linkSystemLibrary("Comdlg32");
}
b.installArtifact(exe);

281
src/app.zig Normal file
View File

@ -0,0 +1,281 @@
const std = @import("std");
const rl = @import("raylib");
const TaskPool = @import("./task-pool.zig");
const FontFace = @import("./font-face.zig");
const Graph = @import("./graph.zig");
const Platform = @import("./platform.zig");
const NIDaq = @import("ni-daq.zig");
const Theme = @import("./theme.zig");
const RectUtils = @import("./rect-utils.zig");
const UI = @import("./ui/root.zig");
const showButton = @import("./ui/button.zig").showButton;
const log = std.log.scoped(.app);
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const Vec2 = rl.Vector2;
const App = @This();
const Channel = struct {
view_rect: Graph.ViewRectangle,
min_value: f64,
max_value: f64,
samples: union(enum) {
owned: []f64,
}
};
allocator: Allocator,
channels: std.ArrayList(Channel),
channel_samples: ?*TaskPool.ChannelSamples = null,
task_pool: TaskPool,
ni_daq: NIDaq,
start_time_ns: i128,
view_from: f32 = 0,
view_width: f32 = 5000,
ui: UI,
pub fn init(
allocator: Allocator,
task_pool_options: TaskPool.Options,
nidaq_options: NIDaq.Options
) !App {
return App{
.allocator = allocator,
.task_pool = try TaskPool.init(allocator, task_pool_options),
.channels = std.ArrayList(Channel).init(allocator),
.ni_daq = try NIDaq.init(allocator, nidaq_options),
.start_time_ns = std.time.nanoTimestamp(),
.ui = UI.init()
};
}
pub fn deinit(self: *App) void {
for (self.channels.items) |channel| {
if (channel.samples == .owned) {
self.allocator.free(channel.samples.owned);
}
}
self.channels.deinit();
self.task_pool.deinit(self.allocator);
self.ni_daq.deinit(self.allocator);
}
// fn appendChannel(self: *App) !*Channel {
// try self.channels.append(Channel.init());
// return &self.channels.items[self.channels.items.len-1];
// }
fn readSamplesFromFile(allocator: Allocator, file: std.fs.File) ![]f64 {
try file.seekTo(0);
const byte_count = try file.getEndPos();
assert(byte_count % 8 == 0);
var samples = try allocator.alloc(f64, @divExact(byte_count, 8));
errdefer allocator.free(samples);
var i: usize = 0;
var buffer: [4096]u8 = undefined;
while (true) {
const count = try file.readAll(&buffer);
if (count == 0) break;
for (0..@divExact(count, 8)) |j| {
samples[i] = std.mem.bytesToValue(f64, buffer[(j*8)..]);
i += 1;
}
}
return samples;
}
fn nanoToSeconds(ns: i128) f32 {
return @as(f32, @floatFromInt(ns)) / std.time.ns_per_s;
}
pub fn tick(self: *App) !void {
// const dt = rl.getFrameTime();
rl.beginDrawing();
defer rl.endDrawing();
rl.clearBackground(Theme.color_bg);
const window_width: f32 = @floatFromInt(rl.getScreenWidth());
const window_height: f32 = @floatFromInt(rl.getScreenHeight());
const window_rect = rl.Rectangle.init(0, 0, window_width, window_height);
const split_x = 120;
const controls_rect, const graphs_rect = RectUtils.verticalSplit(window_rect, split_x);
_ = controls_rect;
rl.drawLineV(
.{ .x = window_rect.x + split_x, .y = RectUtils.top(window_rect) },
.{ .x = window_rect.x + split_x, .y = RectUtils.bottom(window_rect) },
Theme.color_border
);
if (showButton(&self.ui, @src(), .{
.box = .{ .x = 10, .y = 10, .width = 100, .height = 100 },
.text = "Load file"
})) {
if (Platform.openFilePicker()) |file| {
defer file.close();
// TODO: Handle error
const samples = try readSamplesFromFile(self.allocator, file);
errdefer self.allocator.free(samples);
var min_value = samples[0];
var max_value = samples[0];
for (samples) |sample| {
min_value = @min(min_value, sample);
max_value = @max(max_value, sample);
}
const margin = 0.1;
try self.channels.append(Channel{
.min_value = min_value,
.max_value = max_value,
.view_rect = .{
.from = 0,
.to = @floatFromInt(samples.len),
.min_value = min_value + (min_value - max_value) * margin,
.max_value = max_value + (max_value - min_value) * margin
},
.samples = .{ .owned = samples }
});
} else |err| {
// TODO: Show error message to user;
log.err("Failed to pick file: {}", .{ err });
}
}
{
var channels_stack = UI.Stack.init(RectUtils.shrink(graphs_rect, 10), .top_to_bottom);
for (self.channels.items) |channel| {
const channel_rect = channels_stack.next(128);
Graph.draw(
channel_rect,
channel.view_rect,
channel.samples.owned
);
}
}
// rl.drawLineV(
// Vec2.init(0, window_height/2),
// Vec2.init(window_width, window_height/2),
// rl.Color.gray
// );
// if (self.channel_samples) |channel_samples| {
// channel_samples.mutex.lock();
// for (0.., channel_samples.samples) |channel_index, samples| {
// const channel = self.channels.items[channel_index];
// Graph.draw(
// rl.Rectangle{
// .x = 20,
// .y = 20,
// .width = window_width - 40,
// .height = window_height - 40
// },
// .{
// .from = self.view_from,
// .to = self.view_from + self.view_width,
// .min_value = channel.min_sample,
// .max_value = channel.max_sample
// },
// samples.items
// );
// }
// channel_samples.mutex.unlock();
// }
// drawGraph(
// rl.Rectangle{
// .x = 100,
// .y = 20,
// .width = window_width - 200,
// .height = window_height - 40
// },
// .{
// .from = view_from,
// .to = view_from + view_width,
// .min_value = -1,
// .max_value = 1
// },
// example_samples1
// );
// const move_speed = self.view_width * 0.25;
// if (rl.isKeyDown(.key_d)) {
// self.view_from += move_speed * dt;
// }
// if (rl.isKeyDown(.key_a)) {
// self.view_from -= move_speed * dt;
// }
// const zoom_speed = 0.5;
// if (rl.isKeyDown(.key_w)) {
// self.view_width *= (1 - zoom_speed * dt);
// }
// if (rl.isKeyDown(.key_s)) {
// self.view_width *= (1 + zoom_speed * dt);
// }
if (rl.isKeyPressed(rl.KeyboardKey.key_f3)) {
Platform.toggleConsoleWindow();
}
// const now_ns = std.time.nanoTimestamp();
// const now_since_start = nanoToSeconds(now_ns - self.start_time_ns);
// const now_since_samping_start = nanoToSeconds(now_ns - self.channel_samples.started_sampling_ns.?);
{
// const font_face = self.font_face;
// const allocator = self.allocator;
// var y: f32 = 10;
// try font_face.drawTextAlloc(allocator, "Time: {d:.03}", .{now_since_start}, Vec2.init(10, y), rl.Color.black);
// y += 10;
// try font_face.drawTextAlloc(allocator, "View from: {d:.03}", .{self.view_from}, Vec2.init(10, y), rl.Color.black);
// y += 10;
// try font_face.drawTextAlloc(allocator, "View width: {d:.03}", .{self.view_width}, Vec2.init(10, y), rl.Color.black);
// y += 10;
// try font_face.drawTextAlloc(allocator, "Dropped samples: {d:.03}", .{self.task_pool.droppedSamples()}, Vec2.init(10, y), rl.Color.black);
// y += 10;
// for (0..self.channels.items.len) |i| {
// const sample_count = channel_samples.samples[i].items.len;
// y += 10;
// try font_face.drawTextAlloc(allocator, "Channel {}:", .{i + 1}, Vec2.init(10, y), rl.Color.black);
// y += 10;
// try font_face.drawTextAlloc(allocator, "Sample count: {}", .{sample_count}, Vec2.init(20, y), rl.Color.black);
// y += 10;
// try font_face.drawTextAlloc(allocator, "Sample rate: {d:.03}", .{@as(f64, @floatFromInt(sample_count)) / now_since_samping_start}, Vec2.init(20, y), rl.Color.black);
// y += 10;
// }
}
rl.drawFPS(@as(i32, @intFromFloat(window_width)) - 100, 10);
}

152
src/graph.zig Normal file
View File

@ -0,0 +1,152 @@
const std = @import("std");
const rl = @import("raylib");
const Theme = @import("./theme.zig");
const assert = std.debug.assert;
const Vec2 = rl.Vector2;
const clamp = std.math.clamp;
pub const ViewRectangle = struct {
from: f32, // inclusive
to: f32, // exclusive
min_value: f64,
max_value: f64,
left_aligned: bool = true,
color: rl.Color = Theme.color_graph,
dot_size: f32 = 2
};
fn remap(comptime T: type, from_min: T, from_max: T, to_min: T, to_max: T, value: T) T {
const t = (value - from_min) / (from_max - from_min);
return std.math.lerp(to_min, to_max, t);
}
fn mapSampleX(draw_rect: rl.Rectangle, view_rect: ViewRectangle, index: f64) f64 {
return remap(
f64,
view_rect.from, view_rect.to,
draw_rect.x, draw_rect.x + draw_rect.width,
index
);
}
fn mapSampleY(draw_rect: rl.Rectangle, view_rect: ViewRectangle, sample: f64) f64 {
return remap(
f64,
view_rect.min_value, view_rect.max_value,
@floatCast(draw_rect.y + draw_rect.height), @floatCast(draw_rect.y),
sample
);
}
fn mapSamplePointToGraph(draw_rect: rl.Rectangle, view_rect: ViewRectangle, index: f64, sample: f64) Vec2 {
return .{
.x = @floatCast(mapSampleX(draw_rect, view_rect, index)),
.y = @floatCast(mapSampleY(draw_rect, view_rect, sample))
};
}
fn clampIndex(value: f32, size: usize) f32 {
const size_f32: f32 = @floatFromInt(size);
return clamp(value, 0, size_f32);
}
fn clampIndexUsize(value: f32, size: usize) usize {
const size_f32: f32 = @floatFromInt(size);
return @intFromFloat(clamp(value, 0, size_f32));
}
pub fn draw(draw_rect: rl.Rectangle, view_rect: ViewRectangle, samples: []const f64) void {
assert(view_rect.left_aligned); // TODO:
assert(view_rect.to > view_rect.from);
rl.drawRectangleLinesEx(draw_rect, 1, rl.Color.black);
if (view_rect.from > @as(f32, @floatFromInt(samples.len))) return;
if (view_rect.to < 0) return;
const sample_count = view_rect.to - view_rect.from;
const samples_per_column = sample_count / draw_rect.width;
const samples_threshold = 2;
if (samples_per_column >= samples_threshold) {
var i = clampIndex(view_rect.from, samples.len);
while (i < clampIndex(view_rect.to, samples.len)) : (i += samples_per_column) {
const from_index = clampIndexUsize(i, samples.len);
const to_index = clampIndexUsize(i+samples_per_column, samples.len);
const column_samples = samples[from_index..to_index];
if (column_samples.len == 0) continue;
var column_min = column_samples[0];
var column_max = column_samples[0];
for (column_samples) |sample| {
column_min = @min(column_min, sample);
column_max = @max(column_max, sample);
}
const x = mapSampleX(draw_rect, view_rect, @floatFromInt(from_index));
const y_min = mapSampleY(draw_rect, view_rect, column_min);
const y_max = mapSampleY(draw_rect, view_rect, column_max);
if (column_samples.len == 1) {
rl.drawLineV(
mapSamplePointToGraph(draw_rect, view_rect, i, samples[from_index]),
mapSamplePointToGraph(draw_rect, view_rect, i-1, samples[clampIndexUsize(i-1, samples.len-1)]),
view_rect.color
);
rl.drawLineV(
mapSamplePointToGraph(draw_rect, view_rect, i, samples[from_index]),
mapSamplePointToGraph(draw_rect, view_rect, i+1, samples[clampIndexUsize(i+1, samples.len-1)]),
view_rect.color
);
} else if (@abs(y_max - y_min) < 1) {
rl.drawPixelV(
.{ .x = @floatCast(x), .y = @floatCast(y_min) },
view_rect.color
);
} else {
rl.drawLineV(
.{ .x = @floatCast(x), .y = @floatCast(y_min) },
.{ .x = @floatCast(x), .y = @floatCast(y_max) },
view_rect.color
);
}
}
} else {
rl.beginScissorMode(
@intFromFloat(@round(draw_rect.x)),
@intFromFloat(@round(draw_rect.y)),
@intFromFloat(@round(draw_rect.width)),
@intFromFloat(@round(draw_rect.height))
);
defer rl.endScissorMode();
{
const from_index = clampIndexUsize(@floor(view_rect.from), samples.len);
const to_index = clampIndexUsize(@ceil(view_rect.to) + 1, samples.len);
for (from_index..(to_index-1)) |i| {
const from_point = mapSamplePointToGraph(draw_rect, view_rect, @floatFromInt(i), samples[i]);
const to_point = mapSamplePointToGraph(draw_rect, view_rect, @floatFromInt(i + 1), samples[i + 1]);
rl.drawLineV(from_point, to_point, view_rect.color);
}
}
{
const from_index = clampIndexUsize(@ceil(view_rect.from), samples.len);
const to_index = clampIndexUsize(@ceil(view_rect.to), samples.len);
const min_circle_size = 0.5;
const max_circle_size = view_rect.dot_size;
var circle_size = remap(f32, samples_threshold, 0.2, min_circle_size, max_circle_size, samples_per_column);
circle_size = @min(circle_size, max_circle_size);
for (from_index..to_index) |i| {
const center = mapSamplePointToGraph(draw_rect, view_rect, @floatFromInt(i), samples[i]);
rl.drawCircleV(center, circle_size, view_rect.color);
}
}
}
}

View File

@ -1,8 +1,8 @@
const std = @import("std");
const rl = @import("raylib");
const NIDaq = @import("ni-daq.zig");
const TaskPool = @import("./task-pool.zig");
const Platform = @import("./platform.zig");
const builtin = @import("builtin");
const Application = @import("./app.zig");
const Theme = @import("./theme.zig");
const raylib_h = @cImport({
@cInclude("stdio.h");
@cInclude("raylib.h");
@ -10,16 +10,7 @@ const raylib_h = @cImport({
const log = std.log;
const Allocator = std.mem.Allocator;
const FontFace = @import("font-face.zig");
const assert = std.debug.assert;
const Vec2 = rl.Vector2;
const Rect = rl.Rectangle;
const clamp = std.math.clamp;
const icon_png = @embedFile("./assets/icon.png");
fn toRaylibTraceLogLevel(log_level: std.log.Level) rl.TraceLogLevel {
fn toRaylibLogLevel(log_level: log.Level) rl.TraceLogLevel {
return switch (log_level) {
.err => rl.TraceLogLevel.log_error,
.warn => rl.TraceLogLevel.log_warning,
@ -28,14 +19,14 @@ fn toRaylibTraceLogLevel(log_level: std.log.Level) rl.TraceLogLevel {
};
}
fn toZigLogLevel(log_type: c_int) ?std.log.Level {
fn toZigLogLevel(log_type: c_int) ?log.Level {
return switch (log_type) {
@intFromEnum(rl.TraceLogLevel.log_trace) => std.log.Level.debug,
@intFromEnum(rl.TraceLogLevel.log_debug) => std.log.Level.debug,
@intFromEnum(rl.TraceLogLevel.log_info) => std.log.Level.info,
@intFromEnum(rl.TraceLogLevel.log_warning) => std.log.Level.warn,
@intFromEnum(rl.TraceLogLevel.log_error) => std.log.Level.err,
@intFromEnum(rl.TraceLogLevel.log_fatal) => std.log.Level.err,
@intFromEnum(rl.TraceLogLevel.log_trace) => log.Level.debug,
@intFromEnum(rl.TraceLogLevel.log_debug) => log.Level.debug,
@intFromEnum(rl.TraceLogLevel.log_info) => log.Level.info,
@intFromEnum(rl.TraceLogLevel.log_warning) => log.Level.warn,
@intFromEnum(rl.TraceLogLevel.log_error) => log.Level.err,
@intFromEnum(rl.TraceLogLevel.log_fatal) => log.Level.err,
else => null
};
}
@ -51,7 +42,7 @@ fn raylibTraceLogCallback(logType: c_int, text: [*c]const u8, args: raylib_h.va_
const formatted_text = buffer[0..@intCast(text_length)];
const raylib_log = std.log.scoped(.raylib);
const raylib_log = log.scoped(.raylib);
switch (log_level) {
.debug => raylib_log.debug("{s}", .{ formatted_text }),
.info => raylib_log.info("{s}", .{ formatted_text }),
@ -60,255 +51,31 @@ fn raylibTraceLogCallback(logType: c_int, text: [*c]const u8, args: raylib_h.va_
}
}
fn remap(comptime T: type, from_min: T, from_max: T, to_min: T, to_max: T, value: T) T {
const t = (value - from_min) / (from_max - from_min);
return std.math.lerp(to_min, to_max, t);
}
const Channel = struct {
color: rl.Color,
min_sample: f64,
max_sample: f64,
fn init() Channel {
return Channel{
.color = rl.Color.red,
.min_sample = 0,
.max_sample = 0
};
}
};
const Application = struct {
allocator: Allocator,
channels: std.ArrayList(Channel),
channel_samples: ?*TaskPool.ChannelSamples = null,
task_pool: TaskPool,
fn init(allocator: Allocator, task_pool_options: TaskPool.Options) !Application {
return Application{
.allocator = allocator,
.task_pool = try TaskPool.init(allocator, task_pool_options),
.channels = std.ArrayList(Channel).init(allocator)
};
}
fn deinit(self: *Application) void {
self.channels.deinit();
self.task_pool.deinit(self.allocator);
}
fn appendChannel(self: *Application) !*Channel {
try self.channels.append(Channel.init());
return &self.channels.items[self.channels.items.len-1];
}
};
pub fn nanoToSeconds(ns: i128) f32 {
return @as(f32, @floatFromInt(ns)) / std.time.ns_per_s;
}
const ViewRectangle = struct {
from: f32, // inclusive
to: f32, // exclusive
min_value: f64,
max_value: f64,
left_aligned: bool = true,
color: rl.Color = rl.Color.red,
dot_size: f32 = 2
};
fn mapSampleX(draw_rect: rl.Rectangle, view_rect: ViewRectangle, index: f64) f64 {
return remap(
f64,
view_rect.from, view_rect.to,
draw_rect.x, draw_rect.x + draw_rect.width,
index
);
}
fn mapSampleY(draw_rect: rl.Rectangle, view_rect: ViewRectangle, sample: f64) f64 {
return remap(
f64,
view_rect.min_value, view_rect.max_value,
@floatCast(draw_rect.y + draw_rect.height), @floatCast(draw_rect.y),
sample
);
}
fn mapSamplePointToGraph(draw_rect: rl.Rectangle, view_rect: ViewRectangle, index: f64, sample: f64) Vec2 {
return .{
.x = @floatCast(mapSampleX(draw_rect, view_rect, index)),
.y = @floatCast(mapSampleY(draw_rect, view_rect, sample))
};
}
fn clampIndex(value: f32, size: usize) f32 {
const size_f32: f32 = @floatFromInt(size);
return clamp(value, 0, size_f32);
}
fn clampIndexUsize(value: f32, size: usize) usize {
const size_f32: f32 = @floatFromInt(size);
return @intFromFloat(clamp(value, 0, size_f32));
}
fn drawGraph(draw_rect: rl.Rectangle, view_rect: ViewRectangle, samples: []const f64) void {
assert(view_rect.left_aligned); // TODO:
assert(view_rect.to > view_rect.from);
rl.drawRectangleLinesEx(draw_rect, 1, rl.Color.black);
if (view_rect.from > @as(f32, @floatFromInt(samples.len))) return;
if (view_rect.to < 0) return;
const sample_count = view_rect.to - view_rect.from;
const samples_per_column = sample_count / draw_rect.width;
const samples_threshold = 2;
if (samples_per_column >= samples_threshold) {
var i = clampIndex(view_rect.from, samples.len);
while (i < clampIndex(view_rect.to, samples.len)) : (i += samples_per_column) {
const color = view_rect.color;
const from_index = clampIndexUsize(i, samples.len);
const to_index = clampIndexUsize(i+samples_per_column, samples.len);
const column_samples = samples[from_index..to_index];
if (column_samples.len == 0) continue;
var column_min = column_samples[0];
var column_max = column_samples[0];
for (column_samples) |sample| {
column_min = @min(column_min, sample);
column_max = @max(column_max, sample);
}
const x = mapSampleX(draw_rect, view_rect, @floatFromInt(from_index));
const y_min = mapSampleY(draw_rect, view_rect, column_min);
const y_max = mapSampleY(draw_rect, view_rect, column_max);
if (column_samples.len == 1 or @abs(y_max - y_min) < 1) {
rl.drawLineV(
mapSamplePointToGraph(draw_rect, view_rect, i, samples[from_index]),
mapSamplePointToGraph(draw_rect, view_rect, i-1, samples[clampIndexUsize(i-1, samples.len-1)]),
rl.Color.blue
);
rl.drawLineV(
mapSamplePointToGraph(draw_rect, view_rect, i, samples[from_index]),
mapSamplePointToGraph(draw_rect, view_rect, i+1, samples[clampIndexUsize(i+1, samples.len-1)]),
rl.Color.blue
);
} else {
rl.drawLineV(
.{ .x = @floatCast(x), .y = @floatCast(y_min) },
.{ .x = @floatCast(x), .y = @floatCast(y_max) },
color
);
}
}
} else {
rl.beginScissorMode(
@intFromFloat(@round(draw_rect.x)),
@intFromFloat(@round(draw_rect.y)),
@intFromFloat(@round(draw_rect.width)),
@intFromFloat(@round(draw_rect.height))
);
defer rl.endScissorMode();
{
const from_index = clampIndexUsize(@floor(view_rect.from), samples.len);
const to_index = clampIndexUsize(@ceil(view_rect.to) + 1, samples.len);
for (from_index..(to_index-1)) |i| {
const from_point = mapSamplePointToGraph(draw_rect, view_rect, @floatFromInt(i), samples[i]);
const to_point = mapSamplePointToGraph(draw_rect, view_rect, @floatFromInt(i + 1), samples[i + 1]);
rl.drawLineV(from_point, to_point, view_rect.color);
}
}
{
const from_index = clampIndexUsize(@ceil(view_rect.from), samples.len);
const to_index = clampIndexUsize(@ceil(view_rect.to), samples.len);
const min_circle_size = 0.5;
const max_circle_size = view_rect.dot_size;
var circle_size = remap(f32, samples_threshold, 0.2, min_circle_size, max_circle_size, samples_per_column);
circle_size = @min(circle_size, max_circle_size);
for (from_index..to_index) |i| {
const center = mapSamplePointToGraph(draw_rect, view_rect, @floatFromInt(i), samples[i]);
rl.drawCircleV(center, circle_size, view_rect.color);
}
}
}
}
fn readSamplesFromFile(allocator: Allocator, file: std.fs.File) ![]f64 {
try file.seekTo(0);
const byte_count = try file.getEndPos();
assert(byte_count % 8 == 0);
var samples = try allocator.alloc(f64, @divExact(byte_count, 8));
errdefer allocator.free(samples);
var i: usize = 0;
var buffer: [4096]u8 = undefined;
while (true) {
const count = try file.readAll(&buffer);
if (count == 0) break;
for (0..@divExact(count, 8)) |j| {
samples[i] = std.mem.bytesToValue(f64, &buffer[(j*8)..][0..8]);
i += 1;
}
}
return samples;
}
pub fn main() !void {
const start_time = std.time.nanoTimestamp();
// TODO: Setup logging to a file
raylib_h.SetTraceLogCallback(raylibTraceLogCallback);
rl.setTraceLogLevel(toRaylibTraceLogLevel(std.log.default_level));
rl.setTraceLogLevel(toRaylibLogLevel(std.options.log_level));
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
defer _ = gpa.deinit();
var ni_daq = try NIDaq.init(allocator, .{
.max_devices = 4,
.max_analog_inputs = 32,
.max_analog_outputs = 8,
.max_counter_outputs = 8,
.max_counter_inputs = 8,
.max_analog_input_voltage_ranges = 4,
.max_analog_output_voltage_ranges = 4
});
defer ni_daq.deinit(allocator);
// const devices = try ni_daq.listDeviceNames();
const devices = try ni_daq.listDeviceNames();
// log.info("NI-DAQ version: {}", .{try NIDaq.version()});
log.info("NI-DAQ version: {}", .{try NIDaq.version()});
// std.debug.print("Devices ({}):\n", .{devices.len});
// for (devices) |device| {
// std.debug.print(" * '{s}' ({})\n", .{device, device.len});
std.debug.print("Devices ({}):\n", .{devices.len});
for (devices) |device| {
std.debug.print(" * '{s}' ({})\n", .{device, device.len});
// const analog_inputs = try ni_daq.listDeviceAIPhysicalChannels(device);
// for (analog_inputs) |channel_name| {
// std.debug.print(" * '{s}' (Analog input)\n", .{channel_name});
// }
const analog_inputs = try ni_daq.listDeviceAIPhysicalChannels(device);
for (analog_inputs) |channel_name| {
std.debug.print(" * '{s}' (Analog input)\n", .{channel_name});
}
for (try ni_daq.listDeviceAOPhysicalChannels(device)) |channel_name| {
std.debug.print(" * '{s}' (Analog output)\n", .{channel_name});
}
// for (try ni_daq.listDeviceAOPhysicalChannels(device)) |channel_name| {
// std.debug.print(" * '{s}' (Analog output)\n", .{channel_name});
// }
// for (try ni_daq.listDeviceCOPhysicalChannels(device)) |channel_name| {
// std.debug.print(" * '{s}' (Counter output)\n", .{channel_name});
@ -317,42 +84,28 @@ pub fn main() !void {
// for (try ni_daq.listDeviceCIPhysicalChannels(device)) |channel_name| {
// std.debug.print(" * '{s}' (Counter input)\n", .{channel_name});
// }
}
var app = try Application.init(allocator, .{
.max_tasks = devices.len * 2,
.max_channels = 64
});
defer app.deinit();
const example_samples1_file = try std.fs.cwd().openFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_I.bin", .{});
defer example_samples1_file.close();
const example_samples1 = try readSamplesFromFile(allocator, example_samples1_file);
defer allocator.free(example_samples1);
// }
// for (devices) |device| {
{
const device = "Dev1";
// if (try ni_daq.checkDeviceAIMeasurementType(device, .Voltage)) {
// const voltage_ranges = try ni_daq.listDeviceAIVoltageRanges(device);
// assert(voltage_ranges.len > 0);
if (try ni_daq.checkDeviceAIMeasurementType(device, .Voltage)) {
const voltage_ranges = try ni_daq.listDeviceAIVoltageRanges(device);
assert(voltage_ranges.len > 0);
// const min_sample = voltage_ranges[0].low;
// const max_sample = voltage_ranges[0].high;
const min_sample = voltage_ranges[0].low;
const max_sample = voltage_ranges[0].high;
for (try ni_daq.listDeviceAIPhysicalChannels(device)) |channel_name| {
var channel = try app.appendChannel();
channel.min_sample = min_sample;
channel.max_sample = max_sample;
try app.task_pool.createAIVoltageChannel(ni_daq, .{
.channel = channel_name,
.min_value = min_sample,
.max_value = max_sample,
});
break;
}
}
// for (try ni_daq.listDeviceAIPhysicalChannels(device)) |channel_name| {
// var channel = try app.appendChannel();
// channel.min_sample = min_sample;
// channel.max_sample = max_sample;
// try app.task_pool.createAIVoltageChannel(ni_daq, .{
// .channel = channel_name,
// .min_value = min_sample,
// .max_value = max_sample,
// });
// break;
// }
// }
// if (try ni_daq.checkDeviceAOOutputType(device, .Voltage)) {
// const voltage_ranges = try ni_daq.listDeviceAOVoltageRanges(device);
@ -372,151 +125,60 @@ pub fn main() !void {
// });
// }
// }
}
// }
for (0.., app.channels.items) |i, *channel| {
channel.color = rl.Color.fromHSV(@as(f32, @floatFromInt(i)) / @as(f32, @floatFromInt(app.channels.items.len)) * 360, 0.75, 0.8);
}
// for (0.., app.channels.items) |i, *channel| {
// channel.color = rl.Color.fromHSV(@as(f32, @floatFromInt(i)) / @as(f32, @floatFromInt(app.channels.items.len)) * 360, 0.75, 0.8);
// }
const sample_rate: f64 = 5000;
try app.task_pool.setContinousSampleRate(sample_rate);
// const sample_rate: f64 = 5000;
// try app.task_pool.setContinousSampleRate(sample_rate);
var channel_samples = try app.task_pool.start(0.01, allocator);
defer channel_samples.deinit();
defer app.task_pool.stop() catch @panic("stop task failed");
// var channel_samples = try app.task_pool.start(0.01, allocator);
// defer channel_samples.deinit();
// defer app.task_pool.stop() catch @panic("stop task failed");
app.channel_samples = channel_samples;
// app.channel_samples = channel_samples;
const icon_png = @embedFile("./assets/icon.png");
var icon_image = rl.loadImageFromMemory(".png", icon_png);
defer icon_image.unload();
rl.initWindow(800, 450, "DAQ view");
defer rl.closeWindow();
rl.setWindowState(.{ .window_resizable = true });
rl.setWindowState(.{ .window_resizable = true, .vsync_hint = true });
rl.setWindowIcon(icon_image);
rl.setTargetFPS(60);
if (builtin.mode != .Debug) {
rl.setExitKey(.key_null);
}
var font_face = FontFace{
.font = rl.getFontDefault()
};
try Theme.init();
defer Theme.deinit();
var view_from: f32 = 0;
//var view_width: f32 = @floatCast(sample_rate * 20);
var view_width: f32 = 1400;//@floatFromInt(example_samples1.len);
while (!rl.windowShouldClose()) {
const dt = rl.getFrameTime();
rl.beginDrawing();
defer rl.endDrawing();
rl.clearBackground(rl.Color.white);
const window_width: f32 = @floatFromInt(rl.getScreenWidth());
const window_height: f32 = @floatFromInt(rl.getScreenHeight());
rl.drawLineV(
Vec2.init(0, window_height/2),
Vec2.init(window_width, window_height/2),
rl.Color.gray
);
channel_samples.mutex.lock();
for (0.., channel_samples.samples) |channel_index, samples| {
const channel = app.channels.items[channel_index];
drawGraph(
rl.Rectangle{
.x = 20,
.y = 20,
.width = window_width - 40,
.height = window_height - 40
var app = try Application.init(
allocator,
.{
.max_tasks = 32, // devices.len * 2,
.max_channels = 64
},
.{
.from = view_from,
.to = view_from + view_width,
.min_value = channel.min_sample,
.max_value = channel.max_sample
},
samples.items
.max_devices = 4,
.max_analog_inputs = 32,
.max_analog_outputs = 8,
.max_counter_outputs = 8,
.max_counter_inputs = 8,
.max_analog_input_voltage_ranges = 4,
.max_analog_output_voltage_ranges = 4
}
);
}
channel_samples.mutex.unlock();
defer app.deinit();
// drawGraph(
// rl.Rectangle{
// .x = 100,
// .y = 20,
// .width = window_width - 200,
// .height = window_height - 40
// },
// .{
// .from = view_from,
// .to = view_from + view_width,
// .min_value = -1,
// .max_value = 1
// },
// example_samples1
// );
const move_speed = view_width * 0.25;
if (rl.isKeyDown(.key_d)) {
view_from += move_speed * dt;
}
if (rl.isKeyDown(.key_a)) {
view_from -= move_speed * dt;
}
const zoom_speed = 0.5;
if (rl.isKeyDown(.key_w)) {
view_width *= (1 - zoom_speed * dt);
}
if (rl.isKeyDown(.key_s)) {
view_width *= (1 + zoom_speed * dt);
}
if (rl.isKeyPressed(rl.KeyboardKey.key_f3)) {
Platform.toggleConsoleWindow();
}
const now_ns = std.time.nanoTimestamp();
const now_since_start = nanoToSeconds(now_ns - start_time);
const now_since_samping_start = nanoToSeconds(now_ns - channel_samples.started_sampling_ns.?);
{
var y: f32 = 10;
try font_face.drawTextAlloc(allocator, "Time: {d:.03}", .{now_since_start}, Vec2.init(10, y), rl.Color.black);
y += 10;
try font_face.drawTextAlloc(allocator, "View from: {d:.03}", .{view_from}, Vec2.init(10, y), rl.Color.black);
y += 10;
try font_face.drawTextAlloc(allocator, "View width: {d:.03}", .{view_width}, Vec2.init(10, y), rl.Color.black);
y += 10;
try font_face.drawTextAlloc(allocator, "Dropped samples: {d:.03}", .{app.task_pool.droppedSamples()}, Vec2.init(10, y), rl.Color.black);
y += 10;
for (0..app.channels.items.len) |i| {
const sample_count = channel_samples.samples[i].items.len;
y += 10;
try font_face.drawTextAlloc(allocator, "Channel {}:", .{i + 1}, Vec2.init(10, y), rl.Color.black);
y += 10;
try font_face.drawTextAlloc(allocator, "Sample count: {}", .{sample_count}, Vec2.init(20, y), rl.Color.black);
y += 10;
try font_face.drawTextAlloc(allocator, "Sample rate: {d:.03}", .{@as(f64, @floatFromInt(sample_count)) / now_since_samping_start}, Vec2.init(20, y), rl.Color.black);
y += 10;
}
}
rl.drawFPS(@as(i32, @intFromFloat(window_width)) - 100, 10);
while (!rl.windowShouldClose()) {
try app.tick();
}
}
test {
_ = NIDaq;
_ = @import("./ni-daq.zig");
}

View File

@ -1,4 +1,5 @@
const std = @import("std");
const rl = @import("raylib");
const builtin = @import("builtin");
const windows_h = @cImport({
@cDefine("_WIN32_WINNT", "0x0500");
@ -8,16 +9,70 @@ const windows_h = @cImport({
const assert = std.debug.assert;
const log = std.log.scoped(.platform);
// Because `windows_h.HWND` has an alignment of 4,
// we need to redefined every struct that uses `HWND` if we want to change the alignment of `HWND`.
// Ugh... WHYYYYYY
const HWND = [*c]align(2) windows_h.struct_HWND__;
const OPENFILENAMEW = extern struct {
lStructSize: windows_h.DWORD,
hwndOwner: HWND,
hInstance: windows_h.HINSTANCE,
lpstrFilter: windows_h.LPCWSTR,
lpstrCustomFilter: windows_h.LPWSTR,
nMaxCustFilter: windows_h.DWORD,
nFilterIndex: windows_h.DWORD,
lpstrFile: windows_h.LPWSTR,
nMaxFile: windows_h.DWORD,
lpstrFileTitle: windows_h.LPWSTR,
nMaxFileTitle: windows_h.DWORD,
lpstrInitialDir: windows_h.LPCWSTR,
lpstrTitle: windows_h.LPCWSTR,
Flags: windows_h.DWORD,
nFileOffset: windows_h.WORD,
nFileExtension: windows_h.WORD,
lpstrDefExt: windows_h.LPCWSTR,
lCustData: windows_h.LPARAM,
lpfnHook: windows_h.LPOFNHOOKPROC,
lpTemplateName: windows_h.LPCWSTR,
pvReserved: ?*anyopaque,
dwReserved: windows_h.DWORD,
FlagsEx: windows_h.DWORD,
};
extern fn GetOpenFileNameW([*c]OPENFILENAMEW) windows_h.WINBOOL;
fn printLastWindowsError(function_name: []const u8) void {
const err = windows_h.GetLastError();
if (err == 0) {
return;
}
var message: [*c]u8 = null;
// TODO: Use `FormatMessageW`
const size = windows_h.FormatMessageA(
windows_h.FORMAT_MESSAGE_ALLOCATE_BUFFER | windows_h.FORMAT_MESSAGE_FROM_SYSTEM | windows_h.FORMAT_MESSAGE_IGNORE_INSERTS,
null,
err,
windows_h.MAKELANGID(windows_h.LANG_ENGLISH, windows_h.SUBLANG_ENGLISH_US),
@ptrCast(&message),
0,
null
);
log.err("{s}() failed ({}): {s}", .{ function_name, err, message[0..size] });
_ = windows_h.LocalFree(message);
}
pub fn toggleConsoleWindow() void {
if (builtin.os.tag != .windows) {
// TODO: Maybe just toggle outputing or not outputing to terminal on linux?
return;
}
var hWnd = windows_h.GetConsoleWindow();
if (hWnd == null) {
if (windows_h.AllocConsole() == 0) {
// TODO: Use windows.FormatMessages
log.err("AllocConsole() failed: {}", .{ windows_h.GetLastError() });
printLastWindowsError("AllocConsole");
return;
}
@ -31,3 +86,50 @@ pub fn toggleConsoleWindow() void {
_ = windows_h.ShowWindow(hWnd, windows_h.SW_SHOWNOACTIVATE);
}
}
// TODO: Maybe return the file path instead of an opened file handle?
// So the user of this function could do something more interesting.
pub fn openFilePicker() !std.fs.File {
if (builtin.os.tag != .windows) {
return error.NotSupported;
}
const hWnd: HWND = @alignCast(@ptrCast(rl.getWindowHandle()));
assert(hWnd != null);
var ofn = std.mem.zeroes(OPENFILENAMEW);
var filename_w_buffer = std.mem.zeroes([std.os.windows.PATH_MAX_WIDE]u16);
// Zig doesn't let you have NULL bytes in the middle of a string literal, so...
// I guess you are forced to do this kind of string concatenation to insert those NULL bytes
const lpstrFilter = "All" ++ .{ 0 } ++ "*" ++ .{ 0 } ++ "Binary" ++ .{ 0 } ++ "*.bin" ++ .{ 0 };
ofn.lStructSize = @sizeOf(@TypeOf(ofn));
ofn.hwndOwner = hWnd;
ofn.lpstrFile = &filename_w_buffer;
ofn.nMaxFile = filename_w_buffer.len;
ofn.lpstrFilter = std.unicode.utf8ToUtf16LeStringLiteral(lpstrFilter);
ofn.nFilterIndex = 2;
ofn.Flags = windows_h.OFN_PATHMUSTEXIST | windows_h.OFN_FILEMUSTEXIST | windows_h.OFN_EXPLORER | windows_h.OFN_LONGNAMES;
if (GetOpenFileNameW(&ofn) != windows_h.TRUE) {
const err = windows_h.CommDlgExtendedError();
if (err == err) {
return error.Canceled;
}
log.err("GetOpenFileNameW() failed, erro code: {}", .{ err });
return error.GetOpenFileNameW;
}
const filename_len = std.mem.indexOfScalar(u16, &filename_w_buffer, 0).?;
const filename_w = filename_w_buffer[0..filename_len];
var filename_buffer: [std.fs.max_path_bytes]u8 = undefined;
// It should be safe to do "catch unreachable" here because `filename_buffer` will always be big enough.
const filename = std.fmt.bufPrint(&filename_buffer, "{s}", .{std.fs.path.fmtWtf16LeAsUtf8Lossy(filename_w)}) catch unreachable;
// TODO: Use the `openFileAbsoluteW` function.
// Could not get it to work, because it always threw OBJECT_PATH_SYNTAX_BAD error
return try std.fs.openFileAbsolute(filename, .{ });
}

200
src/rect-utils.zig Normal file
View File

@ -0,0 +1,200 @@
const rl = @import("raylib");
const Rect = rl.Rectangle;
pub const AlignX = enum { left, center, right };
pub const AlignY = enum { top, center, bottom };
// ----------------- Positioning functions ----------------- //
pub fn position(rect: rl.Rectangle) rl.Vector2 {
return rl.Vector2.init(rect.x, rect.y);
}
pub fn size(rect: rl.Rectangle) rl.Vector2 {
return rl.Vector2.init(rect.width, rect.height);
}
pub fn isInside(rect: rl.Rectangle, x: f32, y: f32) bool {
return (rect.x <= x and x < rect.x + rect.width) and (rect.y < y and y < rect.y + rect.height);
}
pub fn isInsideVec2(rect: rl.Rectangle, vec2: rl.Vector2) bool {
return isInside(rect, vec2.x, vec2.y);
}
pub fn top(rect: rl.Rectangle) f32 {
return rect.y;
}
pub fn bottom(rect: rl.Rectangle) f32 {
return rect.y + rect.height;
}
pub fn left(rect: rl.Rectangle) f32 {
return rect.x;
}
pub fn right(rect: rl.Rectangle) f32 {
return rect.x + rect.width;
}
pub fn verticalSplit(rect: rl.Rectangle, left_side_width: f32) [2]rl.Rectangle {
var left_side = rect;
left_side.width = left_side_width;
var right_side = rect;
right_side.x += left_side_width;
right_side.width -= left_side_width;
return .{
left_side,
right_side
};
}
pub fn horizontalSplit(rect: rl.Rectangle, top_side_height: f32) [2]rl.Rectangle {
var top_side = rect;
top_side.height = top_side_height;
var bottom_side = rect;
bottom_side.y += top_side_height;
bottom_side.height -= top_side_height;
return .{
top_side,
bottom_side
};
}
pub fn center(rect: Rect) rl.Vector2 {
return rl.Vector2{
.x = rect.x + rect.width / 2,
.y = rect.y + rect.height / 2,
};
}
pub fn bottomLeft(rect: Rect) rl.Vector2 {
return rl.Vector2.init(left(rect), bottom(rect));
}
pub fn bottomRight(rect: Rect) rl.Vector2 {
return rl.Vector2.init(right(rect), bottom(rect));
}
pub fn topLeft(rect: Rect) rl.Vector2 {
return rl.Vector2.init(left(rect), top(rect));
}
pub fn topRight(rect: Rect) rl.Vector2 {
return rl.Vector2.init(right(rect), top(rect));
}
// ----------------- Shrinking/Growing functions ----------------- //
pub fn shrink(rect: Rect, amount: f32) rl.Rectangle {
return Rect.init(
rect.x + amount,
rect.y + amount,
rect.width - 2 * amount,
rect.height - 2 * amount
);
}
pub fn grow(rect: Rect, amount: f32) rl.Rectangle {
return shrink(rect, -amount);
}
pub fn shrinkY(rect: Rect, amount: f32) rl.Rectangle {
return Rect.init(
rect.x,
rect.y + amount,
rect.width,
rect.height - 2 * amount
);
}
pub fn growY(rect: Rect, amount: f32) rl.Rectangle {
return shrinkY(rect, -amount);
}
pub fn shrinkX(rect: Rect, amount: f32) rl.Rectangle {
return Rect.init(
rect.x + amount,
rect.y,
rect.width - 2 * amount,
rect.height
);
}
pub fn growX(rect: Rect, amount: f32) rl.Rectangle {
return shrinkX(rect, -amount);
}
pub fn shrinkTop(rect: Rect, amount: f32) rl.Rectangle {
return Rect.init(
rect.x,
rect.y + amount,
rect.width,
rect.height - amount
);
}
pub fn growTop(rect: Rect, amount: f32) rl.Rectangle {
return shrinkTop(rect, -amount);
}
pub fn shrinkBottom(rect: Rect, amount: f32) rl.Rectangle {
return Rect.init(
rect.x,
rect.y,
rect.width,
rect.height - amount
);
}
pub fn growBottom(rect: Rect, amount: f32) rl.Rectangle {
return shrinkBottom(rect, -amount);
}
pub fn shrinkLeft(rect: Rect, amount: f32) rl.Rectangle {
return Rect.init(
rect.x + amount,
rect.y,
rect.width - amount,
rect.height
);
}
pub fn growLeft(rect: Rect, amount: f32) rl.Rectangle {
return shrinkLeft(rect, -amount);
}
pub fn shrinkRight(rect: Rect, amount: f32) rl.Rectangle {
return Rect.init(
rect.x,
rect.y,
rect.width - amount,
rect.height
);
}
pub fn growRight(rect: Rect, amount: f32) rl.Rectangle {
return shrinkRight(rect, -amount);
}
// ----------------- Other functions (idk) ----------------- //
pub fn initCentered(rect: Rect, width: f32, height: f32) Rect {
const unused_width = rect.width - width;
const unused_height = rect.height - height;
return Rect.init(rect.x + unused_width / 2, rect.y + unused_height / 2, width, height);
}
pub fn aligned(rect: Rect, align_x: AlignX, align_y: AlignY) rl.Vector2 {
const x = switch(align_x) {
.left => rect.x,
.center => rect.x + rect.width/2,
.right => rect.x + rect.width,
};
const y = switch(align_y) {
.top => rect.y,
.center => rect.y + rect.height/2,
.bottom => rect.y + rect.height,
};
return rl.Vector2.init(x, y);
}

40
src/srcery.zig Normal file
View File

@ -0,0 +1,40 @@
const rl = @import("raylib");
const rgb = @import("./utils.zig").rgb;
// Primary
pub const black = rgb(28 , 27 , 25 );
pub const red = rgb(239, 47 , 39 );
pub const green = rgb(81 , 159, 80 );
pub const yellow = rgb(251, 184, 41 );
pub const blue = rgb(44 , 120, 191);
pub const magenta = rgb(224, 44 , 109);
pub const cyan = rgb(10 , 174, 179);
pub const white = rgb(186, 166, 127);
pub const bright_black = rgb(145, 129, 117);
pub const bright_red = rgb(247, 83 , 65 );
pub const bright_green = rgb(152, 188, 55 );
pub const bright_yellow = rgb(254, 208, 110);
pub const bright_blue = rgb(104, 168, 228);
pub const bright_magenta = rgb(255, 92 , 143);
pub const bright_cyan = rgb(43 , 228, 208);
pub const bright_white = rgb(252, 232, 195);
// Secondary
pub const orange = rgb(255, 95, 0);
pub const bright_orange = rgb(255, 135, 0);
pub const hard_black = rgb(18, 18, 18);
pub const teal = rgb(0, 128, 128);
// Grays
pub const xgray1 = rgb(38 , 38 , 38 );
pub const xgray2 = rgb(48 , 48 , 48 );
pub const xgray3 = rgb(58 , 58 , 58 );
pub const xgray4 = rgb(68 , 68 , 68 );
pub const xgray5 = rgb(78 , 78 , 78 );
pub const xgray6 = rgb(88 , 88 , 88 );
pub const xgray7 = rgb(98 , 98 , 98 );
pub const xgray8 = rgb(108, 108, 108);
pub const xgray9 = rgb(118, 118, 118);
pub const xgray10 = rgb(128, 128, 128);
pub const xgray11 = rgb(138, 138, 138);
pub const xgray12 = rgb(148, 148, 148);

40
src/theme.zig Normal file
View File

@ -0,0 +1,40 @@
const std = @import("std");
const rl = @import("raylib");
const srcery = @import("./srcery.zig");
const FontFace = @import("./font-face.zig");
const assert = std.debug.assert;
// TODO: Maybe don't have this as a global? Pass the theme around where it is needed.
//
// But for now, having it as a global is very convenient.
pub const FontId = enum {
text
};
const FontArray = std.EnumArray(FontId, FontFace);
var fonts: FontArray = FontArray.initUndefined();
pub const color_bg = srcery.black;
pub const color_border = srcery.bright_black;
pub const color_button = srcery.xgray7;
pub const color_text = srcery.bright_white;
pub const color_graph = srcery.red;
pub fn font(font_id: FontId) FontFace {
return fonts.get(font_id);
}
pub fn init() !void {
const default_font = rl.getFontDefault();
assert(default_font.isReady());
fonts = FontArray.init(.{
.text = FontFace{ .font = default_font }
});
}
pub fn deinit() void {
// TODO: Deinit fonts
}

65
src/ui/button.zig Normal file
View File

@ -0,0 +1,65 @@
const std = @import("std");
const rl = @import("raylib");
const Theme = @import("../theme.zig");
const UI = @import("./root.zig");
const rectUtils = @import("../rect-utils.zig");
const utils = @import("../utils.zig");
const SourceLocation = std.builtin.SourceLocation;
const Id = UI.Id;
pub const ButtonOptions = struct {
box: rl.Rectangle,
text: []const u8,
font: Theme.FontId = .text,
};
pub fn showButtonId(ui: *UI, id: Id, opts: ButtonOptions) bool {
const bg_color = Theme.color_button;
const text_color = Theme.color_text;
const font = Theme.font(opts.font);
var clicked = false;
const is_mouse_inside = ui.isMouseInside(opts.box);
if (is_mouse_inside) {
ui.hot_widget = id;
}
if (ui.isHot(id) and rl.isMouseButtonPressed(.mouse_button_left)) {
ui.active_widget = id;
clicked = true;
}
if (ui.isActive(id) and rl.isMouseButtonReleased(.mouse_button_left)) {
ui.active_widget = null;
ui.hot_widget = null;
}
if (ui.isHot(id) and !is_mouse_inside) {
ui.hot_widget = null;
}
const text_size = font.measureText(opts.text);
var text_position = rectUtils.aligned(opts.box, .center, .center);
text_position.x -= text_size.x/2;
text_position.y -= text_size.y/2;
var color = bg_color.fade(0.5);
if (ui.isHot(id)) {
color = bg_color;
}
rl.drawRectangleRec(opts.box, color);
rl.drawLineV(
rectUtils.bottomLeft(opts.box),
rectUtils.bottomRight(opts.box),
color
);
font.drawText(opts.text, text_position, text_color);
return clicked;
}
pub fn showButton(ui: *UI, comptime src: SourceLocation, opts: ButtonOptions) bool {
return showButtonId(ui, Id.init(src), opts);
}

201
src/ui/root.zig Normal file
View File

@ -0,0 +1,201 @@
const std = @import("std");
const rl = @import("raylib");
const rect_utils = @import("../rect-utils.zig");
const assert = std.debug.assert;
const SourceLocation = std.builtin.SourceLocation;
// TODO: Implement Id context (I.e. ID parenting, aggregate ids)
const UI = @This();
const max_stack_depth = 16;
const TransformFrame = struct {
offset: rl.Vector2,
scale: rl.Vector2,
};
const TransformStack = std.BoundedArray(TransformFrame, max_stack_depth);
hot_widget: ?Id = null,
active_widget: ?Id = null,
transform_stack: TransformStack,
pub fn init() UI {
var stack = TransformStack.init(0) catch unreachable;
stack.appendAssumeCapacity(TransformFrame{
.offset = rl.Vector2{ .x = 0, .y = 0 },
.scale = rl.Vector2{ .x = 1, .y = 1 },
});
return UI{
.transform_stack = stack
};
}
pub fn isHot(self: *const UI, id: Id) bool {
if (self.hot_widget) |hot_id| {
return hot_id.eql(id);
}
return false;
}
pub fn isActive(self: *const UI, id: Id) bool {
if (self.active_widget) |active_id| {
return active_id.eql(id);
}
return false;
}
pub fn hashSrc(src: SourceLocation) u64 {
var hash = std.hash.Fnv1a_64.init();
hash.update(src.file);
hash.update(std.mem.asBytes(&src.line));
hash.update(std.mem.asBytes(&src.column));
return hash.value;
}
fn getTopFrame(self: *UI) *TransformFrame {
assert(self.transform_stack.len >= 1);
return &self.transform_stack.buffer[self.transform_stack.len-1];
}
pub fn getMousePosition(self: *UI) rl.Vector2 {
const frame = self.getTopFrame();
return rl.getMousePosition().subtract(frame.offset).divide(frame.scale);
}
pub fn getMouseDelta(self: *UI) rl.Vector2 {
const frame = self.getTopFrame();
return rl.Vector2.multiply(rl.getMouseDelta(), frame.scale);
}
pub fn getMouseWheelMove(self: *UI) f32 {
const frame = self.getTopFrame();
return rl.getMouseWheelMove() * frame.scale.y;
}
pub fn isMouseInside(self: *UI, rect: rl.Rectangle) bool {
return rect_utils.isInsideVec2(rect, self.getMousePosition());
}
pub fn transformScale(self: *UI, x: f32, y: f32) void {
const frame = self.getTopFrame();
frame.scale.x *= x;
frame.scale.y *= y;
rl.gl.rlScalef(x, y, 1);
}
pub fn transformTranslate(self: *UI, x: f32, y: f32) void {
const frame = self.getTopFrame();
frame.offset.x += x * frame.scale.x;
frame.offset.y += y * frame.scale.y;
rl.gl.rlTranslatef(x, y, 0);
}
pub fn pushTransform(self: *UI) void {
rl.gl.rlPushMatrix();
self.transform_stack.appendAssumeCapacity(self.getTopFrame().*);
}
pub fn popTransform(self: *UI) void {
assert(self.transform_stack.len >= 2);
rl.gl.rlPopMatrix();
_ = self.transform_stack.pop();
}
pub fn beginScissorMode(self: *UI, x: f32, y: f32, width: f32, height: f32) void {
const frame = self.getTopFrame();
rl.beginScissorMode(
@intFromFloat(x * frame.scale.x + frame.offset.x),
@intFromFloat(y * frame.scale.y + frame.offset.y),
@intFromFloat(width * frame.scale.x),
@intFromFloat(height * frame.scale.y),
);
}
pub fn beginScissorModeRect(self: *UI, rect: rl.Rectangle) void {
self.beginScissorMode(rect.x, rect.y, rect.width, rect.height);
}
pub fn endScissorMode(self: *UI) void {
_ = self;
rl.endScissorMode();
}
pub const Id = struct {
location: u64,
extra: u32 = 0,
pub fn init(comptime src: SourceLocation) Id {
return Id{ .location = comptime hashSrc(src) };
}
pub fn eql(a: Id, b: Id) bool {
return a.location == b.location and a.extra == b.extra;
}
};
pub const Stack = struct {
pub const Direction = enum {
top_to_bottom,
bottom_to_top,
left_to_right
};
unused_box: rl.Rectangle,
dir: Direction,
gap: f32 = 0,
pub fn init(box: rl.Rectangle, dir: Direction) Stack {
return Stack{
.unused_box = box,
.dir = dir
};
}
pub fn next(self: *Stack, size: f32) rl.Rectangle {
return switch (self.dir) {
.top_to_bottom => {
const next_box = rl.Rectangle.init(self.unused_box.x, self.unused_box.y, self.unused_box.width, size);
self.unused_box.y += size;
self.unused_box.y += self.gap;
return next_box;
},
.bottom_to_top => {
const next_box = rl.Rectangle.init(self.unused_box.x, self.unused_box.y + self.unused_box.height - size, self.unused_box.width, size);
self.unused_box.height -= size;
self.unused_box.height -= self.gap;
return next_box;
},
.left_to_right => {
const next_box = rl.Rectangle.init(self.unused_box.x, self.unused_box.y, size, self.unused_box.height);
self.unused_box.x += size;
self.unused_box.x += self.gap;
return next_box;
},
};
}
};
pub const IdIterator = struct {
id: Id,
counter: u32,
pub fn init(comptime src: SourceLocation) IdIterator {
return IdIterator{
.id = Id.init(src),
.counter = 0
};
}
pub fn next(self: *IdIterator) Id {
var id = self.id;
id.extra = self.counter;
self.counter += 1;
return id;
}
};

26
src/utils.zig Normal file
View File

@ -0,0 +1,26 @@
const std = @import("std");
const rl = @import("raylib");
pub fn vec2Round(vec2: rl.Vector2) rl.Vector2 {
return rl.Vector2{
.x = @round(vec2.x),
.y = @round(vec2.y),
};
}
pub fn rgb(r: u8, g: u8, b: u8) rl.Color {
return rl.Color.init(r, g, b, 255);
}
pub fn rgba(r: u8, g: u8, b: u8, a: f32) rl.Color {
return rl.Color.init(r, g, b, a * 255);
}
pub fn drawUnderline(rect: rl.Rectangle, size: f32, color: rl.Color) void {
rl.drawRectangleRec(rl.Rectangle{
.x = rect.x,
.y = rect.y + rect.height - size,
.width = rect.width,
.height = size
}, color);
}