diff --git a/build.zig b/build.zig index 0aaff40..57ef24e 100644 --- a/build.zig +++ b/build.zig @@ -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); diff --git a/src/app.zig b/src/app.zig new file mode 100644 index 0000000..66d29a8 --- /dev/null +++ b/src/app.zig @@ -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); +} \ No newline at end of file diff --git a/src/graph.zig b/src/graph.zig new file mode 100644 index 0000000..27eea14 --- /dev/null +++ b/src/graph.zig @@ -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); + } + } + } +} \ No newline at end of file diff --git a/src/main.zig b/src/main.zig index f53b482..9d84e7e 100644 --- a/src/main.zig +++ b/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,300 +51,62 @@ 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}); + // } - // for (try ni_daq.listDeviceCOPhysicalChannels(device)) |channel_name| { - // std.debug.print(" * '{s}' (Counter output)\n", .{channel_name}); + // for (try ni_daq.listDeviceCIPhysicalChannels(device)) |channel_name| { + // std.debug.print(" * '{s}' (Counter input)\n", .{channel_name}); + // } + // } + + // for (devices) |device| { + // 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; + + // 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.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); - - 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; - } - } - // if (try ni_daq.checkDeviceAOOutputType(device, .Voltage)) { // const voltage_ranges = try ni_daq.listDeviceAOVoltageRanges(device); // assert(voltage_ranges.len > 0); @@ -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); + var app = try Application.init( + allocator, + .{ + .max_tasks = 32, // devices.len * 2, + .max_channels = 64 + }, + .{ + .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 app.deinit(); 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 - }, - .{ - .from = view_from, - .to = view_from + 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 = 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); + try app.tick(); } } test { - _ = NIDaq; + _ = @import("./ni-daq.zig"); } \ No newline at end of file diff --git a/src/platform.zig b/src/platform.zig index 863899f..c3a454a 100644 --- a/src/platform.zig +++ b/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; } @@ -30,4 +85,51 @@ pub fn toggleConsoleWindow() void { } else { _ = 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, .{ }); } \ No newline at end of file diff --git a/src/rect-utils.zig b/src/rect-utils.zig new file mode 100644 index 0000000..0a8c442 --- /dev/null +++ b/src/rect-utils.zig @@ -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); +} diff --git a/src/srcery.zig b/src/srcery.zig new file mode 100644 index 0000000..77ee3d9 --- /dev/null +++ b/src/srcery.zig @@ -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); \ No newline at end of file diff --git a/src/theme.zig b/src/theme.zig new file mode 100644 index 0000000..0b517a5 --- /dev/null +++ b/src/theme.zig @@ -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 +} \ No newline at end of file diff --git a/src/ui/button.zig b/src/ui/button.zig new file mode 100644 index 0000000..c3270d4 --- /dev/null +++ b/src/ui/button.zig @@ -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); +} \ No newline at end of file diff --git a/src/ui/root.zig b/src/ui/root.zig new file mode 100644 index 0000000..6ccaa28 --- /dev/null +++ b/src/ui/root.zig @@ -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; + } +}; diff --git a/src/utils.zig b/src/utils.zig new file mode 100644 index 0000000..beb3ad5 --- /dev/null +++ b/src/utils.zig @@ -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); +} \ No newline at end of file