split up main.zig into app.zig and graph.zig
This commit is contained in:
parent
24895afce6
commit
0d9033c926
@ -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
281
src/app.zig
Normal 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
152
src/graph.zig
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
494
src/main.zig
494
src/main.zig
@ -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");
|
||||
}
|
106
src/platform.zig
106
src/platform.zig
@ -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
200
src/rect-utils.zig
Normal 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
40
src/srcery.zig
Normal 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
40
src/theme.zig
Normal 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
65
src/ui/button.zig
Normal 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
201
src/ui/root.zig
Normal 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
26
src/utils.zig
Normal 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);
|
||||
}
|
Loading…
Reference in New Issue
Block a user