diff --git a/src/app.zig b/src/app.zig index 2e87e2a..5d11ddb 100644 --- a/src/app.zig +++ b/src/app.zig @@ -1,22 +1,17 @@ const std = @import("std"); const rl = @import("raylib"); -const TaskPool = @import("./task-pool.zig"); -const FontFace = @import("./font-face.zig"); -const Graph = @import("./ui/graph.zig"); +const srcery = @import("./srcery.zig"); +const UI = @import("./ui.zig"); const Platform = @import("./platform.zig"); +const Assets = @import("./assets.zig"); +const Graph = @import("./graph.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 rect_utils = @import("./rect-utils.zig"); const remap = @import("./utils.zig").remap; -const Aseprite = @import("./aseprite.zig"); const log = std.log.scoped(.app); - -const Allocator = std.mem.Allocator; const assert = std.debug.assert; -const Vec2 = rl.Vector2; +const clamp = std.math.clamp; const App = @This(); @@ -24,7 +19,7 @@ const Channel = struct { view_cache: Graph.Cache = .{}, view_rect: Graph.ViewOptions, - dragged_marker: ?enum { from, to, both } = null, + height: f32 = 150, min_value: f64, max_value: f64, @@ -33,83 +28,73 @@ const Channel = struct { }, }; -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, +allocator: std.mem.Allocator, ui: UI, +channels: std.BoundedArray(Channel, 64) = .{}, +ni_daq: NIDaq, -grab_texture: struct { - normal: rl.Texture2D, - hot: rl.Texture2D, - active: rl.Texture2D, -}, - -pub fn init( - allocator: Allocator, - task_pool_options: TaskPool.Options, - nidaq_options: NIDaq.Options -) !App { - - // TODO: Maybe store a compressed version of aseprite files when embedding? - // Setup a build step to compress the files - const grab_ase = try Aseprite.init(allocator, @embedFile("./assets/grab-marker.ase")); - defer grab_ase.deinit(); - - const grab_normal_image = grab_ase.getTagImage(grab_ase.getTag("normal") orelse return error.TagNotFound); - defer rl.unloadImage(grab_normal_image); - const grab_normal_texture = rl.loadTextureFromImage(grab_normal_image); - - const grab_hot_image = grab_ase.getTagImage(grab_ase.getTag("hot") orelse return error.TagNotFound); - defer rl.unloadImage(grab_hot_image); - const grab_hot_texture = rl.loadTextureFromImage(grab_hot_image); - - const grab_active_image = grab_ase.getTagImage(grab_ase.getTag("active") orelse return error.TagNotFound); - defer rl.unloadImage(grab_active_image); - const grab_active_texture = rl.loadTextureFromImage(grab_active_image); - +pub fn init(allocator: std.mem.Allocator) !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(), - - .grab_texture = .{ - .normal = grab_normal_texture, - .hot = grab_hot_texture, - .active = grab_active_texture - } + .ui = UI.init(allocator), + .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 + }), }; } pub fn deinit(self: *App) void { - for (self.channels.items) |*channel| { - if (channel.samples == .owned) { - self.allocator.free(channel.samples.owned); + self.ni_daq.deinit(self.allocator); + for (self.channels.slice()) |*channel| { + switch (channel.samples) { + .owned => |owned| self.allocator.free(owned) } channel.view_cache.deinit(); } - self.channels.deinit(); - self.task_pool.deinit(self.allocator); - self.ni_daq.deinit(self.allocator); - rl.unloadTexture(self.grab_texture.normal); - rl.unloadTexture(self.grab_texture.hot); - rl.unloadTexture(self.grab_texture.active); + self.ui.deinit(); } -fn readSamplesFromFile(allocator: Allocator, file: std.fs.File) ![]f64 { +fn showButton(self: *App, text: []const u8) UI.Interaction { + var button = self.ui.newWidget(self.ui.keyFromString(text)); + button.border = srcery.bright_blue; + button.padding.vertical(8); + button.padding.horizontal(16); + button.flags.insert(.clickable); + button.size = .{ + .x = .{ .text = {} }, + .y = .{ .text = {} }, + }; + + const interaction = self.ui.getInteraction(button); + var text_color: rl.Color = undefined; + if (interaction.held_down) { + button.background = srcery.hard_black; + text_color = srcery.white; + } else if (interaction.hovering) { + button.background = srcery.bright_black; + text_color = srcery.bright_white; + } else { + button.background = srcery.blue; + text_color = srcery.bright_white; + } + + button.text = .{ + .content = text, + .color = text_color + }; + + return interaction; +} + +fn readSamplesFromFile(allocator: std.mem.Allocator, file: std.fs.File) ![]f64 { try file.seekTo(0); const byte_count = try file.getEndPos(); assert(byte_count % 8 == 0); @@ -132,10 +117,6 @@ fn readSamplesFromFile(allocator: Allocator, file: std.fs.File) ![]f64 { return samples; } -fn nanoToSeconds(ns: i128) f32 { - return @as(f32, @floatFromInt(ns)) / std.time.ns_per_s; -} - pub fn appendChannelFromFile(self: *App, file: std.fs.File) !void { const samples = try readSamplesFromFile(self.allocator, file); errdefer self.allocator.free(samples); @@ -162,322 +143,204 @@ pub fn appendChannelFromFile(self: *App, file: std.fs.File) !void { }); } -fn showChannelMinimap(self: *App, channel: *Channel, minimap_rect: rl.Rectangle) void { - const MarkerState = enum { - normal, - hot, - active - }; - - var from_state = MarkerState.normal; - var to_state = MarkerState.normal; - var area_state = MarkerState.normal; - - const sample_count = channel.samples.owned.len; - const sample_count_f32: f32 = @floatFromInt(sample_count); - - const grab_marker_width: f32 = @floatFromInt(self.grab_texture.normal.width); - const view_column_min = RectUtils.left(minimap_rect) + grab_marker_width/2; - const view_column_max = RectUtils.right(minimap_rect) - grab_marker_width/2; - - const from_column_position = remap(f32, - 0, - sample_count_f32, - view_column_min, - view_column_max, - channel.view_rect.from - ); - - const to_column_position = remap(f32, - 0, - sample_count_f32, - view_column_min, - view_column_max, - channel.view_rect.to - ); - - const visible_area_rect = rl.Rectangle{ - .x = from_column_position, - .y = minimap_rect.y, - .width = to_column_position - from_column_position, - .height = minimap_rect.height - }; - - const mouse = self.ui.getMousePosition(); - if (RectUtils.isInsideVec2(minimap_rect, mouse)) { - rl.drawRectangleLinesEx(minimap_rect, 1, rl.Color.gray); - - const grab_distance = 20; - - if (@abs(mouse.x - from_column_position) < grab_distance) { - from_state = .hot; - } else if (@abs(mouse.x - to_column_position) < grab_distance) { - to_state = .hot; - } else if (RectUtils.isInsideVec2(visible_area_rect, mouse)) { - area_state = .hot; - } - - if (rl.isMouseButtonPressed(.mouse_button_left)) { - if (area_state == .hot) { - channel.dragged_marker = .both; - } else if (from_state == .hot) { - channel.dragged_marker = .from; - } else if (to_state == .hot) { - channel.dragged_marker = .to; - } - } - - } else { - rl.drawRectangleLinesEx(minimap_rect, 1, rl.Color.black); - } - - if (rl.isMouseButtonReleased(.mouse_button_left)) { - channel.dragged_marker = null; - } - - const sample_under_mouse = remap(f32, - view_column_min, - view_column_max, - 0, - sample_count_f32, - mouse.x - ); - - if (channel.dragged_marker) |marker| { - const min_shown_samples = 1000; - switch (marker) { - .from => { - from_state = .active; - channel.view_rect.from = std.math.clamp(sample_under_mouse, 0, channel.view_rect.to-min_shown_samples); - }, - .to => { - to_state = .active; - channel.view_rect.to = std.math.clamp(sample_under_mouse, channel.view_rect.from+min_shown_samples, @as(f32, sample_count_f32)); - }, - .both => { - area_state = .active; - from_state = .active; - to_state = .active; - - var delta = remap(f32, - 0, - minimap_rect.width, - 0, - sample_count_f32, - self.ui.getMouseDelta().x - ); - delta = std.math.clamp(delta, -channel.view_rect.from, sample_count_f32 - channel.view_rect.to); - - channel.view_rect.from += delta; - channel.view_rect.to += delta; - } - } - } - - rl.drawRectangleRec(visible_area_rect, rl.Color.gray); - - { - const texture = switch (from_state) { - .normal => self.grab_texture.normal, - .hot => self.grab_texture.hot, - .active => self.grab_texture.active - }; - - rl.drawTextureV( - texture, - .{ - .x = from_column_position - @as(f32, @floatFromInt(texture.width)) / 2, - .y = RectUtils.top(minimap_rect) - }, - rl.Color.white - ); - } - - { - const texture = switch (to_state) { - .normal => self.grab_texture.normal, - .hot => self.grab_texture.hot, - .active => self.grab_texture.active - }; - - rl.drawTextureV( - texture, - .{ - .x = to_column_position - @as(f32, @floatFromInt(texture.width)) / 2, - .y = RectUtils.top(minimap_rect) - }, - rl.Color.white - ); - } -} - -const graph_height = 128; -const minimap_height = 16; -const channel_height = graph_height + minimap_height; -fn showChannelRow(self: *App, channel: *Channel, channel_rect: rl.Rectangle) void { - const graph_rect, const minimap_rect = RectUtils.horizontalSplit(channel_rect, graph_height); - - { // Graph - Graph.draw( - &self.ui, - &channel.view_cache, - graph_rect, - channel.view_rect, - channel.samples.owned - ); - - rl.drawRectangleLinesEx(graph_rect, 1, rl.Color.black); - } - - self.showChannelMinimap(channel, minimap_rect); -} - 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 - try self.appendChannelFromFile(file); - } 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| { - self.showChannelRow(channel, channels_stack.next(channel_height)); - } - } - - // 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); - // } + rl.clearBackground(srcery.black); 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; + self.ui.begin(); + defer self.ui.end(); + self.ui.getParent().?.layout_axis = .Y; - // var y: f32 = 10; - // try font_face.drawTextAlloc(allocator, "Time: {d:.03}", .{now_since_start}, Vec2.init(10, y), rl.Color.black); - // y += 10; + { + const toolbar = self.ui.newBoxFromString("Toolbar"); + toolbar.flags.insert(.clickable); + toolbar.background = rl.Color.green; + toolbar.layout_axis = .X; + toolbar.size = .{ + .x = UI.Size.percent(1, 0), + .y = UI.Size.pixels(32, 1), + }; + self.ui.pushParent(toolbar); + defer self.ui.popParent(); - // try font_face.drawTextAlloc(allocator, "View from: {d:.03}", .{self.view_from}, Vec2.init(10, y), rl.Color.black); - // y += 10; + { + const box = self.ui.newBoxFromString("Add from file"); + box.flags.insert(.clickable); + box.background = rl.Color.red; + box.size = .{ + .x = UI.Size.text(2, 1), + .y = UI.Size.percent(1, 1) + }; + box.setText("Add from file", .text); - // try font_face.drawTextAlloc(allocator, "View width: {d:.03}", .{self.view_width}, Vec2.init(10, y), rl.Color.black); - // y += 10; + const signal = self.ui.signalFromBox(box); + if (signal.clicked()) { + if (Platform.openFilePicker()) |file| { + defer file.close(); - // try font_face.drawTextAlloc(allocator, "Dropped samples: {d:.03}", .{self.task_pool.droppedSamples()}, Vec2.init(10, y), rl.Color.black); - // y += 10; + // TODO: Handle error + self.appendChannelFromFile(file) catch @panic("Failed to append channel from file"); + } else |err| { + // TODO: Show error message to user; + log.err("Failed to pick file: {}", .{ err }); + } + } + } - // for (0..self.channels.items.len) |i| { - // const sample_count = channel_samples.samples[i].items.len; - // y += 10; + { + const box = self.ui.newBoxFromString("Add from device"); + box.flags.insert(.clickable); + box.background = rl.Color.lime; + box.size = .{ + .x = UI.Size.text(2, 1), + .y = UI.Size.percent(1, 1) + }; + box.setText("Add from device", .text); - // try font_face.drawTextAlloc(allocator, "Channel {}:", .{i + 1}, Vec2.init(10, y), rl.Color.black); - // y += 10; + const signal = self.ui.signalFromBox(box); + if (signal.clicked()) { + std.debug.print("click two!\n", .{}); + } + } + } - // try font_face.drawTextAlloc(allocator, "Sample count: {}", .{sample_count}, Vec2.init(20, y), rl.Color.black); - // y += 10; + { + const rows_container = self.ui.newBoxFromString("Channels"); + rows_container.layout_axis = .Y; + rows_container.size = .{ + .x = UI.Size.percent(1, 1), + .y = UI.Size.percent(1, 0), + }; + self.ui.pushParent(rows_container); + defer self.ui.popParent(); - // 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; - // } + for (self.channels.slice()) |*_channel| { + const channel: *Channel = _channel; + + const channel_box = self.ui.newBoxFromPtr(channel); + channel_box.background = rl.Color.blue; + channel_box.layout_axis = .Y; + channel_box.size.x = UI.Size.percent(1, 0); + channel_box.size.y = UI.Size.childrenSum(1); + self.ui.pushParent(channel_box); + defer self.ui.popParent(); + + const graph_box = self.ui.newBoxFromString("Graph"); + graph_box.background = rl.Color.blue; + graph_box.layout_axis = .Y; + graph_box.size.x = UI.Size.percent(1, 0); + graph_box.size.y = UI.Size.pixels(256, 1); + + Graph.drawCached(&channel.view_cache, graph_box.persistent.size, channel.view_rect, channel.samples.owned); + if (channel.view_cache.texture) |texture| { + graph_box.texture = texture.texture; + } + + { + const sample_count: f32 = @floatFromInt(channel.samples.owned.len); + const min_visible_samples = sample_count*0.02; + + const minimap_box = self.ui.newBoxFromString("Minimap"); + minimap_box.background = rl.Color.dark_purple; + minimap_box.layout_axis = .X; + minimap_box.size.x = UI.Size.percent(1, 0); + minimap_box.size.y = UI.Size.pixels(32, 1); + self.ui.pushParent(minimap_box); + defer self.ui.popParent(); + + const minimap_rect = minimap_box.computedRect(); + + { + const middle_box = self.ui.newBoxFromString("Middle knob"); + middle_box.flags.insert(.clickable); + middle_box.flags.insert(.draggable); + middle_box.background = rl.Color.ray_white; + middle_box.size.y = UI.Size.pixels(32, 1); + + const signal = self.ui.signalFromBox(middle_box); + if (signal.dragged()) { + + var samples_moved = signal.drag.x / minimap_rect.width * sample_count; + + samples_moved = clamp(samples_moved, -channel.view_rect.from, sample_count - channel.view_rect.to); + + channel.view_rect.from += samples_moved; + channel.view_rect.to += samples_moved; + } + + middle_box.override_x = minimap_rect.width * channel.view_rect.from / sample_count + 4; + middle_box.size.x = UI.Size.pixels(minimap_rect.width * (channel.view_rect.to - channel.view_rect.from) / sample_count - 8, 1); + } + + { + const left_knob_box = self.ui.newBoxFromString("Left knob"); + left_knob_box.flags.insert(.clickable); + left_knob_box.flags.insert(.draggable); + left_knob_box.background = rl.Color.ray_white; + left_knob_box.size.x = UI.Size.pixels(8, 1); + left_knob_box.size.y = UI.Size.pixels(32, 1); + + const left_signal = self.ui.signalFromBox(left_knob_box); + if (left_signal.dragged()) { + channel.view_rect.from += remap( + f32, + 0, minimap_rect.width, + 0, sample_count, + left_signal.drag.x + ); + + channel.view_rect.from = clamp(channel.view_rect.from, 0, channel.view_rect.to-min_visible_samples); + } + + left_knob_box.override_x = minimap_rect.width * channel.view_rect.from / sample_count - left_knob_box.persistent.size.x/2; + } + + { + const right_knob_box = self.ui.newBoxFromString("Right knobaaa"); + right_knob_box.flags.insert(.clickable); + right_knob_box.flags.insert(.draggable); + right_knob_box.background = rl.Color.ray_white; + right_knob_box.size.x = UI.Size.pixels(8, 1); + right_knob_box.size.y = UI.Size.pixels(32, 1); + + const right_signal = self.ui.signalFromBox(right_knob_box); + if (right_signal.dragged()) { + channel.view_rect.to += remap( + f32, + 0, minimap_rect.width, + 0, sample_count, + right_signal.drag.x + ); + + channel.view_rect.to = clamp(channel.view_rect.to, channel.view_rect.from+min_visible_samples, sample_count); + } + + right_knob_box.override_x = minimap_rect.width * channel.view_rect.to / sample_count - right_knob_box.persistent.size.x/2; + } + } + + // const graph_widget = self.ui.newWidget(self.ui.keyFromString("samples-plot")); + // graph_widget.size.y = .{ .pixels = channel.height }; + // graph_widget.size.x = .{ .percent = 1 }; + // graph_widget.graph = .{ + // .cache = &channel.view_cache, + // .options = channel.view_rect, + // .samples = channel.samples.owned + // }; + + // const minimap_widget = self.showChannelMinimap(channel); + // minimap_widget.size.y = .fit_children; + // minimap_widget.size.x = .{ .percent = 1 }; + } + } } - rl.drawFPS(@as(i32, @intFromFloat(window_width)) - 100, 10); + self.ui.draw(); } \ No newline at end of file diff --git a/src/assets.zig b/src/assets.zig new file mode 100644 index 0000000..880a616 --- /dev/null +++ b/src/assets.zig @@ -0,0 +1,88 @@ +const std = @import("std"); +const rl = @import("raylib"); +const srcery = @import("./srcery.zig"); +const FontFace = @import("./font-face.zig"); +const Aseprite = @import("./aseprite.zig"); + +const assert = std.debug.assert; + +pub const FontId = enum { + text +}; + +var loaded_fonts: std.BoundedArray(rl.Font, 32) = .{}; + +const FontArray = std.EnumArray(FontId, FontFace); +var fonts: FontArray = FontArray.initUndefined(); + +pub var grab_texture: struct { + normal: rl.Texture2D, + hot: rl.Texture2D, + active: rl.Texture2D, +} = undefined; + +pub fn font(font_id: FontId) FontFace { + return fonts.get(font_id); +} + +pub fn init(allocator: std.mem.Allocator) !void { + const roboto_regular = @embedFile("./assets/fonts/roboto/Roboto-Regular.ttf"); + + const default_font = try loadFont(roboto_regular, 16); + + fonts = FontArray.init(.{ + .text = FontFace{ .font = default_font, .line_height = 1.2 } + }); + + const grab_ase = try Aseprite.init(allocator, @embedFile("./assets/grab-marker.ase")); + defer grab_ase.deinit(); + + const grab_normal_image = grab_ase.getTagImage(grab_ase.getTag("normal") orelse return error.TagNotFound); + defer grab_normal_image.unload(); + const grab_normal_texture = rl.loadTextureFromImage(grab_normal_image); + errdefer grab_normal_texture.unload(); + + const grab_hot_image = grab_ase.getTagImage(grab_ase.getTag("hot") orelse return error.TagNotFound); + defer grab_hot_image.unload(); + const grab_hot_texture = rl.loadTextureFromImage(grab_hot_image); + errdefer grab_hot_texture.unload(); + + const grab_active_image = grab_ase.getTagImage(grab_ase.getTag("active") orelse return error.TagNotFound); + defer grab_active_image.unload(); + const grab_active_texture = rl.loadTextureFromImage(grab_active_image); + errdefer grab_active_texture.unload(); + + grab_texture = .{ + .normal = grab_normal_texture, + .hot = grab_hot_texture, + .active = grab_active_texture + }; +} + +fn loadFont(ttf_data: []const u8, font_size: u32) !rl.Font { + var codepoints: [95]i32 = undefined; + for (0..codepoints.len) |i| { + codepoints[i] = @as(i32, @intCast(i)) + 32; + } + + const loaded_font = rl.loadFontFromMemory(".ttf", ttf_data, @intCast(font_size), &codepoints); + if (!loaded_font.isReady()) { + return error.LoadFontFromMemory; + } + + loaded_fonts.appendAssumeCapacity(loaded_font); + + return loaded_font; +} + +pub fn deinit(allocator: std.mem.Allocator) void { + _ = allocator; + + for (loaded_fonts.slice()) |loaded_font| { + loaded_font.unload(); + } + + grab_texture.active.unload(); + grab_texture.hot.unload(); + grab_texture.normal.unload(); +} \ No newline at end of file diff --git a/src/assets/fonts/roboto/LICENSE.txt b/src/assets/fonts/roboto/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/src/assets/fonts/roboto/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/assets/fonts/roboto/Roboto-Regular.ttf b/src/assets/fonts/roboto/Roboto-Regular.ttf new file mode 100644 index 0000000..2d116d9 Binary files /dev/null and b/src/assets/fonts/roboto/Roboto-Regular.ttf differ diff --git a/src/font-face.zig b/src/font-face.zig index 0fb5d95..d745fd1 100644 --- a/src/font-face.zig +++ b/src/font-face.zig @@ -18,6 +18,10 @@ pub fn getSize(self: @This()) f32 { return @floatFromInt(self.font.baseSize); } +pub fn getLineSize(self: @This()) f32 { + return self.getSize() * self.line_height; +} + pub fn drawTextLines(self: @This(), lines: []const []const u8, position: rl.Vector2, tint: rl.Color) void { var offset_y: f32 = 0; diff --git a/src/ui/graph.zig b/src/graph.zig similarity index 70% rename from src/ui/graph.zig rename to src/graph.zig index 49ff12b..6d4c40a 100644 --- a/src/ui/graph.zig +++ b/src/graph.zig @@ -1,10 +1,9 @@ const builtin = @import("builtin"); const std = @import("std"); const rl = @import("raylib"); -const Theme = @import("../theme.zig"); -const UI = @import("./root.zig"); +const srcery = @import("./srcery.zig"); -const remap = @import("../utils.zig").remap; +const remap = @import("./utils.zig").remap; const assert = std.debug.assert; const Vec2 = rl.Vector2; const clamp = std.math.clamp; @@ -23,7 +22,7 @@ pub const ViewOptions = struct { min_value: f64, max_value: f64, left_aligned: bool = true, - color: rl.Color = Theme.color_graph, + color: rl.Color = srcery.red, dot_size: f32 = 2 }; @@ -49,7 +48,7 @@ pub const Cache = struct { .x = 0, .y = 0, .width = @floatFromInt(texture.texture.width), - .height = @floatFromInt(-texture.texture.height) + .height = @floatFromInt(texture.texture.height) }; rl.drawTexturePro( texture.texture, @@ -97,7 +96,7 @@ fn clampIndexUsize(value: f32, size: usize) usize { return @intFromFloat(clamp(value, 0, size_f32)); } -fn drawSamples(ui: *UI, draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void { +fn drawSamples(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void { assert(options.left_aligned); // TODO: assert(options.to >= options.from); @@ -154,8 +153,13 @@ fn drawSamples(ui: *UI, draw_rect: rl.Rectangle, options: ViewOptions, samples: } } } else { - ui.beginScissorModeRect(draw_rect); - defer ui.endScissorMode(); + rl.beginScissorMode( + @intFromFloat(draw_rect.x), + @intFromFloat(draw_rect.y), + @intFromFloat(draw_rect.width), + @intFromFloat(draw_rect.height), + ); + defer rl.endScissorMode(); { const from_index = clampIndexUsize(@floor(options.from), samples.len); @@ -185,56 +189,69 @@ fn drawSamples(ui: *UI, draw_rect: rl.Rectangle, options: ViewOptions, samples: } } -pub fn draw(ui: *UI, cache: ?*Cache, draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void { - if (disable_caching) { - drawSamples(ui, draw_rect, options, samples); +pub fn drawCached(cache: *Cache, render_size: Vec2, options: ViewOptions, samples: []const f64) void { + const render_width: i32 = @intFromFloat(@ceil(render_size.x)); + const render_height: i32 = @intFromFloat(@ceil(render_size.y)); + + if (render_width <= 0 or render_height <= 0) { return; } - if (cache) |c| { - const render_width: i32 = @intFromFloat(@ceil(draw_rect.width)); - const render_height: i32 = @intFromFloat(@ceil(draw_rect.height)); - - // Unload render texture if rendering width or height changed - if (c.texture) |render_texture| { - const texure = render_texture.texture; - if (texure.width != render_width or texure.height != render_height) { - render_texture.unload(); - c.texture = null; - c.options = null; - } + // Unload render texture if rendering width or height changed + if (cache.texture) |render_texture| { + const texure = render_texture.texture; + if (texure.width != render_width or texure.height != render_height) { + render_texture.unload(); + cache.texture = null; + cache.options = null; } - - if (c.texture == null) { - const texture = rl.loadRenderTexture(render_width, render_height); - // TODO: Maybe fallback to just drawing without caching, if GPU doesn't have enough memory? - assert(rl.isRenderTextureReady(texture)); - c.texture = texture; - } - - const render_texture = c.texture.?; - - if (c.options != null and std.meta.eql(c.options.?, options)) { - c.draw(draw_rect); - return; - } - - c.options = options; - render_texture.begin(); - - ui.pushTransform(); - ui.transformTranslate(-draw_rect.x, -draw_rect.y); - rl.clearBackground(rl.Color.black.alpha(0)); } - drawSamples(ui, draw_rect, options, samples); + if (cache.texture == null) { + const texture = rl.loadRenderTexture(render_width, render_height); + // TODO: Maybe fallback to just drawing without caching, if GPU doesn't have enough memory? + assert(rl.isRenderTextureReady(texture)); + cache.texture = texture; + } - if (cache) |c| { - ui.popTransform(); + const render_texture = cache.texture.?; - const render_texture = c.texture.?; - render_texture.end(); + if (cache.options != null and std.meta.eql(cache.options.?, options)) { + // Cached graph hasn't changed, no need to redraw. + return; + } + cache.options = options; + + render_texture.begin(); + defer render_texture.end(); + + rl.gl.rlPushMatrix(); + defer rl.gl.rlPopMatrix(); + + rl.clearBackground(rl.Color.black.alpha(0)); + rl.gl.rlTranslatef(0, render_size.y, 0); + rl.gl.rlScalef(1, -1, 1); + + const draw_rect = rl.Rectangle{ + .x = 0, + .y = 0, + .width = render_size.x, + .height = render_size.y + }; + drawSamples(draw_rect, options, samples); +} + +pub fn draw(cache: ?*Cache, draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void { + if (draw_rect.width < 0 or draw_rect.height < 0) { + return; + } + + if (cache != null and !disable_caching) { + const c = cache.?; + drawCached(c, .{ .x = draw_rect.width, .y = draw_rect.height }, options, samples); c.draw(draw_rect); + } else { + drawSamples(draw_rect, options, samples); } } \ No newline at end of file diff --git a/src/main.zig b/src/main.zig index 192c320..a9412e6 100644 --- a/src/main.zig +++ b/src/main.zig @@ -2,7 +2,7 @@ const std = @import("std"); const rl = @import("raylib"); const builtin = @import("builtin"); const Application = @import("./app.zig"); -const Theme = @import("./theme.zig"); +const Assets = @import("./assets.zig"); const raylib_h = @cImport({ @cInclude("stdio.h"); @cInclude("raylib.h"); @@ -10,6 +10,14 @@ const raylib_h = @cImport({ const log = std.log; +// TODO: Maybe move this to a config.zig or options.zig file. +// Have all of the contstants in a single file. +pub const version = std.SemanticVersion{ + .major = 0, + .minor = 1, + .patch = 0 +}; + fn toRaylibLogLevel(log_level: log.Level) rl.TraceLogLevel { return switch (log_level) { .err => rl.TraceLogLevel.log_error, @@ -62,30 +70,6 @@ pub fn main() !void { // const devices = try ni_daq.listDeviceNames(); - // 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}); - - // 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.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); @@ -147,31 +131,17 @@ pub fn main() !void { rl.initWindow(800, 450, "DAQ view"); defer rl.closeWindow(); rl.setWindowState(.{ .window_resizable = true, .vsync_hint = true }); + rl.setWindowMinSize(256, 256); rl.setWindowIcon(icon_image); if (builtin.mode != .Debug) { rl.setExitKey(.key_null); } - try Theme.init(); - defer Theme.deinit(); + try Assets.init(allocator); + defer Assets.deinit(allocator); - 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 - } - ); + var app = try Application.init(allocator); defer app.deinit(); if (builtin.mode == .Debug) { @@ -188,7 +158,6 @@ pub fn main() !void { } } - while (!rl.windowShouldClose()) { try app.tick(); } diff --git a/src/ni-daq.zig b/src/ni-daq.zig index 2e19d34..5947513 100644 --- a/src/ni-daq.zig +++ b/src/ni-daq.zig @@ -368,6 +368,7 @@ const DeviceBuffers = struct { } }; +options: Options, device_names_buffer: []u8, device_names: StringArrayListUnmanaged, @@ -390,6 +391,7 @@ pub fn init(allocator: std.mem.Allocator, options: Options) !NIDaq { } return NIDaq{ + .options = options, .device_names_buffer = device_names_buffer, .device_names = device_names, .device_buffers = device_buffers diff --git a/src/theme.zig b/src/theme.zig deleted file mode 100644 index 0b517a5..0000000 --- a/src/theme.zig +++ /dev/null @@ -1,40 +0,0 @@ -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.zig b/src/ui.zig new file mode 100644 index 0000000..74391f8 --- /dev/null +++ b/src/ui.zig @@ -0,0 +1,954 @@ +const std = @import("std"); +const rl = @import("raylib"); +const Assets = @import("./assets.zig"); +const rect_utils = @import("./rect-utils.zig"); +const srcery = @import("./srcery.zig"); + +const log = std.lgo.scoped(.ui); +const assert = std.debug.assert; +const Vec2 = rl.Vector2; +const Rect = rl.Rectangle; + +const UI = @This(); + +const debug = false; +const max_boxes = 128; +const max_events = 256; + +const RectFormatted = struct { + rect: ?Rect, + + pub fn init(rect: ?Rect) RectFormatted { + return RectFormatted{ + .rect = rect + }; + } + + pub fn format( + self: RectFormatted, + comptime fmt: []const u8, + options: std.fmt.FormatOptions, + writer: anytype, + ) !void { + _ = fmt; + _ = options; + + if (self.rect) |rect| { + try writer.print("Rect{{ {d:.02}, {d:.02}, {d:.02}, {d:.02} }}", .{ rect.x, rect.y, rect.width, rect.height }); + } else { + try writer.print("{}", .{ null }); + } + } +}; + +const Axis = enum { + X, + Y, + + fn flip(self: Axis) Axis { + return switch (self) { + .X => .Y, + .Y => .X + }; + } +}; + +pub const Size = struct { + kind: union(enum) { + pixels: f32, + percent: f32, + text: f32, + children_sum, + }, + strictness: f32 = 1, + + pub fn pixels(amount: f32, strictness: f32) Size { + return Size{ + .kind = .{ .pixels = amount }, + .strictness = strictness + }; + } + + pub fn text(padding: f32, strictness: f32) Size { + return Size{ + .kind = .{ .text = padding }, + .strictness = strictness + }; + } + + pub fn percent(amount: f32, strictness: f32) Size { + return Size{ + .kind = .{ .percent = amount }, + .strictness = strictness + }; + } + + pub fn childrenSum(strictness: f32) Size { + return Size{ + .kind = .children_sum, + .strictness = strictness + }; + } +}; + +pub const Vec2Size = struct { + x: Size, + y: Size, + + pub fn zero() Vec2Size { + return Vec2Size{ + .x = Size.pixels(0, 1), + .y = Size.pixels(0, 1) + }; + } + + inline fn getAxis(self: *Vec2Size, axis: Axis) *Size { + return switch (axis) { + .X => &self.x, + .Y => &self.y + }; + } +}; + +pub const Key = struct { + hash: u64 = 0, + + pub fn initPtr(ptr: anytype) Key { + return Key.initUsize(@intFromPtr(ptr)); + } + + pub fn initUsize(num: usize) Key { + return Key{ + .hash = @truncate(num) + }; + } + + pub fn initString(seed: u64, text: []const u8) Key { + return Key{ + .hash = std.hash.XxHash3.hash(seed, text) + }; + } + + pub fn initNil() Key { + return Key{ .hash = 0 }; + } + + pub fn eql(self: Key, other: Key) bool { + return self.hash == other.hash; + } + + pub fn isNil(self: Key) bool { + return self.hash == 0; + } + + pub fn format( + self: Key, + comptime fmt: []const u8, + options: std.fmt.FormatOptions, + writer: anytype, + ) !void { + _ = fmt; + _ = options; + + try writer.print("Key{{ 0x{x} }}", .{ self.hash }); + } +}; + +pub const Event = union(enum) { + mouse_pressed: rl.MouseButton, + mouse_released: rl.MouseButton, + mouse_move: Vec2 +}; + +pub const Signal = struct { + pub const Flag = enum { + left_pressed, + right_pressed, + + left_released, + right_released, + + left_clicked, + right_clicked, + + left_dragging, + right_dragging + }; + + flags: std.EnumSet(Flag) = .{}, + drag: Vec2 = .{ .x = 0, .y = 0 }, + + pub fn clicked(self: Signal) bool { + return self.flags.contains(.left_clicked) or self.flags.contains(.right_clicked); + } + + pub fn dragged(self: Signal) bool { + return self.flags.contains(.left_dragging) or self.flags.contains(.right_dragging); + } + + fn insertMousePressed(self: *Signal, mouse_button: rl.MouseButton) void { + if (mouse_button == .mouse_button_left) { + self.flags.insert(.left_pressed); + } else if (mouse_button == .mouse_button_right) { + self.flags.insert(.right_pressed); + } + } + + fn insertMouseReleased(self: *Signal, mouse_button: rl.MouseButton) void { + if (mouse_button == .mouse_button_left) { + self.flags.insert(.left_released); + } else if (mouse_button == .mouse_button_right) { + self.flags.insert(.right_released); + } + } + + fn insertMouseClicked(self: *Signal, mouse_button: rl.MouseButton) void { + if (mouse_button == .mouse_button_left) { + self.flags.insert(.left_clicked); + } else if (mouse_button == .mouse_button_right) { + self.flags.insert(.right_clicked); + } + } + + fn insertMouseDragged(self: *Signal, mouse_button: rl.MouseButton) void { + if (mouse_button == .mouse_button_left) { + self.flags.insert(.left_dragging); + } else if (mouse_button == .mouse_button_right) { + self.flags.insert(.right_dragging); + } + } +}; + +const BoxIndex = std.math.IntFittingRange(0, max_boxes); + +pub const Box = struct { + pub const Persistent = struct { + size: Vec2 = .{ .x = 0, .y = 0 }, + position: Vec2 = .{ .x = 0, .y = 0 }, + }; + + pub const Flag = enum { + clickable, + draggable + }; + + pub const Flags = std.EnumSet(Flag); + + allocator: std.mem.Allocator, + + key: Key, + size: Vec2Size = Vec2Size.zero(), + flags: Flags = .{}, + override_x: ?f32 = null, + override_y: ?f32 = null, + background: ?rl.Color = null, + rounded: bool = false, + layout_axis: Axis = .X, + last_used_frame: u64 = 0, + text: ?struct { + content: []u8, + font: Assets.FontId, + color: rl.Color = srcery.bright_white + } = null, + texture: ?rl.Texture2D = null, + + persistent: Persistent = .{}, + + // Fields for maintaining tree data structure + + // Index of this box + index: BoxIndex, + // Go down the tree to the first child + first_child_index: ?BoxIndex = null, + // Go down the tree to the last child + last_child_index: ?BoxIndex = null, + // Go up the tree + parent_index: ?BoxIndex = null, + // Go the next node on the same level + next_sibling_index: ?BoxIndex = null, + + pub fn computedRect(self: *Box) Rect { + return Rect{ + .x = self.persistent.position.x, + .y = self.persistent.position.y, + .width = self.persistent.size.x, + .height = self.persistent.size.y + }; + } + + pub fn setText(self: *Box, text: []const u8, font: Assets.FontId) void { + self.text = .{ + .content = self.allocator.dupe(u8, text) catch return, + .font = font + }; + } +}; + +const BoxChildIterator = struct { + current_child: ?BoxIndex, + boxes: []Box, + + pub fn next(self: *BoxChildIterator) ?*Box { + if (self.current_child) |child_index| { + const child = &self.boxes[child_index]; + self.current_child = child.next_sibling_index; + return child; + } else { + return null; + } + } +}; + +const BoxParentIterator = struct { + current_parent: ?BoxIndex, + boxes: []Box, + + pub fn next(self: *BoxParentIterator) ?*Box { + if (self.current_parent) |parent_index| { + const parent = &self.boxes[parent_index]; + self.current_parent = parent.parent_index; + return parent; + } else { + return null; + } + } +}; + +pub const root_box_key = Key.initString(0, "$root$"); + +arenas: [2]std.heap.ArenaAllocator, + +boxes: std.BoundedArray(Box, max_boxes) = .{}, +parent_index_stack: std.BoundedArray(BoxIndex, max_boxes) = .{}, + +frame_index: u64 = 0, +hot_box_key: ?Key = null, +active_box_keys: std.EnumMap(rl.MouseButton, Key) = .{}, + +events: std.BoundedArray(Event, max_events) = .{}, +mouse: Vec2 = .{ .x = 0, .y = 0 }, +mouse_delta: Vec2 = .{ .x = 0, .y = 0 }, +mouse_buttons: std.EnumSet(rl.MouseButton) = .{}, +window_size: Vec2 = .{ .x = 0, .y = 0 }, + +pub fn init(allocator: std.mem.Allocator) UI { + return UI{ + .arenas = .{ std.heap.ArenaAllocator.init(allocator), std.heap.ArenaAllocator.init(allocator) }, + .mouse = rl.getMousePosition() + }; +} + +pub fn deinit(self: *UI) void { + self.arenas[0].deinit(); + self.arenas[1].deinit(); +} + +pub fn begin(self: *UI) void { + const window_width = rl.getScreenWidth(); + const window_height = rl.getScreenHeight(); + + const mouse = rl.getMousePosition(); + self.mouse_delta = mouse.subtract(self.mouse); + + const active_box_flags = self.getActiveBoxFlags(); + if (active_box_flags.contains(.draggable)) { + const mouse_x = rl.getMouseX(); + const mouse_y = rl.getMouseY(); + + rl.setMousePosition( + @mod(mouse_x, @as(i32, @intFromFloat(self.window_size.x))), + @mod(mouse_y, @as(i32, @intFromFloat(self.window_size.y))) + ); + } + + self.frame_index += 1; + _ = self.frameArena().reset(.retain_capacity); + self.parent_index_stack.len = 0; + + if (self.active_box_keys.count() == 0) { + self.hot_box_key = null; + } + + self.events.len = 0; + self.mouse = rl.getMousePosition(); + self.window_size = Vec2.init(@floatFromInt(window_width), @floatFromInt(window_width)); + self.mouse_buttons = .{}; + inline for (std.meta.fields(rl.MouseButton)) |mouse_button_field| { + const mouse_button: rl.MouseButton = @enumFromInt(mouse_button_field.value); + + if (rl.isMouseButtonPressed(mouse_button)) { + self.events.appendAssumeCapacity(Event{ .mouse_pressed = mouse_button }); + } + if (rl.isMouseButtonReleased(mouse_button)) { + self.events.appendAssumeCapacity(Event{ .mouse_released = mouse_button }); + } + if (rl.isMouseButtonDown(mouse_button)) { + self.mouse_buttons.insert(mouse_button); + } + } + + const root_box = self.newBox(root_box_key); + root_box.size.x = Size.pixels(@floatFromInt(window_width), 1); + root_box.size.y = Size.pixels(@floatFromInt(window_height), 1); + + self.pushParent(root_box); +} + +pub fn end(self: *UI) void { + self.popParent(); + assert(self.parent_index_stack.len == 0); + + { + var i: usize = 0; + while (i < self.boxes.len) { + const box = &self.boxes.buffer[i]; + if (box.last_used_frame != self.frame_index) { + _ = self.boxes.swapRemove(i); + continue; + } + + i += 1; + } + } + + { + var active_box_flags = self.getActiveBoxFlags(); + var hover_box_flags: Box.Flags = .{}; + if (self.hot_box_key) |hot_box_key| { + if (self.findBoxByKey(hot_box_key)) |hot_box| { + hover_box_flags = hot_box.flags; + } + } + + if (active_box_flags.contains(.draggable)) { + rl.setMouseCursor(@intFromEnum(rl.MouseCursor.mouse_cursor_resize_ew)); + } else if (hover_box_flags.contains(.clickable)) { + rl.setMouseCursor(@intFromEnum(rl.MouseCursor.mouse_cursor_pointing_hand)); + } else { + rl.setMouseCursor(@intFromEnum(rl.MouseCursor.mouse_cursor_default)); + } + } + + const root_box = self.findBoxByKey(root_box_key).?; + self.calcLayout(root_box, .X); + self.calcLayout(root_box, .Y); +} + +fn getActiveBoxFlags(self: *UI) Box.Flags { + var result_flags: Box.Flags = .{}; + + var active_iter = self.active_box_keys.iterator(); + while (active_iter.next()) |active_box_key| { + if (self.findBoxByKey(active_box_key.value.*)) |active_box| { + result_flags = result_flags.unionWith(active_box.flags); + } + } + + return result_flags; +} + +pub fn draw(self: *UI) void { + const root_box = self.findBoxByKey(root_box_key).?; + self.drawBox(root_box); + + if (debug) { + const font = Assets.font(.text); + const debug_box = Rect{ + .x = self.mouse.x, + .y = self.mouse.y + 32, + .width = 400, + .height = 100 + }; + rl.drawRectangleRec(debug_box, srcery.hard_black); + + var layout_y: f32 = 0; + + { + var buff: [256]u8 = undefined; + const text = std.fmt.bufPrint(&buff, "Hot: {?}", .{self.hot_box_key}) catch ""; + font.drawText(text, .{ .x = debug_box.x, .y = debug_box.y + layout_y }, srcery.white); + layout_y += 16; + } + + { + var rect: ?Rect = null; + if (self.hot_box_key) |hot_box_key| { + rect = self.findBoxByKey(hot_box_key).?.computedRect(); + } + + var buff: [256]u8 = undefined; + const text = std.fmt.bufPrint(&buff, "Hot rect: {?}", .{RectFormatted.init(rect)}) catch ""; + font.drawText(text, .{ .x = debug_box.x, .y = debug_box.y + layout_y }, srcery.white); + layout_y += 16; + } + + { + var hot_parent_key: ?Key = null; + if (self.hot_box_key) |hot_box_key| { + if (self.findBoxByKey(hot_box_key)) |hot_box| { + if (hot_box.parent_index) |parent_index| { + hot_parent_key = self.boxes.buffer[parent_index].key; + } + } + } + + var buff: [256]u8 = undefined; + const text = std.fmt.bufPrint(&buff, "Parent of hot: {?}", .{hot_parent_key}) catch ""; + font.drawText(text, .{ .x = debug_box.x, .y = debug_box.y + layout_y }, srcery.white); + layout_y += 16; + } + + // { + // var buff: [256]u8 = undefined; + // const text = std.fmt.bufPrint(&buff, "Active: {?}", .{self.active_box_key}) catch ""; + // font.drawText(text, .{ .x = debug_box.x, .y = debug_box.y + layout_y }, srcery.white); + // layout_y += 16; + // } + + { + font.drawText("Children of hot:", .{ .x = debug_box.x, .y = debug_box.y + layout_y }, srcery.white); + layout_y += 16; + + if (self.hot_box_key) |hot_box_key| { + const hot_box = self.findBoxByKey(hot_box_key).?; + var child_iter = self.iterChildrenByParent(hot_box); + while (child_iter.next()) |child| { + var buff: [256]u8 = undefined; + const text = std.fmt.bufPrint(&buff, "{}", .{child.key}) catch ""; + font.drawText(text, .{ .x = debug_box.x, .y = debug_box.y + layout_y }, srcery.white); + layout_y += 16; + } + } + } + } +} + +fn drawBox(self: *UI, box: *Box) void { + const box_rect = box.computedRect(); + + if (box.background) |background| { + rl.drawRectangleRec(box_rect, background); + } + + if (self.isBoxActive(box.key)) { + rl.drawRectangleLinesEx(box_rect, 2, rl.Color.orange); + } else if (self.isBoxHot(box.key)) { + rl.drawRectangleLinesEx(box_rect, 3, rl.Color.blue); + } + + if (box.texture) |texture| { + const source = rl.Rectangle{ + .x = 0, + .y = 0, + .width = @floatFromInt(texture.width), + .height = @floatFromInt(texture.height) + }; + rl.drawTexturePro( + texture, + source, + box_rect, + rl.Vector2.zero(), + 0, rl.Color.white + ); + } + + if (box.text) |text| { + const font = Assets.font(text.font); + font.drawTextCenter(text.content, rect_utils.center(box_rect), text.color); + } + + var child_iter = self.iterChildrenByParent(box); + while (child_iter.next()) |child| { + self.drawBox(child); + } + + if (debug) { + if (self.isBoxActive(box.key)) { + rl.drawRectangleLinesEx(box_rect, 3, rl.Color.red); + } else if (self.isBoxHot(box.key)) { + rl.drawRectangleLinesEx(box_rect, 3, rl.Color.orange); + } else { + rl.drawRectangleLinesEx(box_rect, 1, rl.Color.pink); + } + } +} + +inline fn getVec2Axis(vec2: *Vec2, axis: Axis) *f32 { + return switch (axis) { + .X => &vec2.x, + .Y => &vec2.y + }; +} + +fn calcLayout(self: *UI, box: *Box, axis: Axis) void { + self.calcLayoutStandaloneSize(box, axis); + self.calcLayoutUpwardsSize(box, axis); + self.calcLayoutDownardsSize(box, axis); + self.calcLayoutEnforceConstraints(box, axis); + self.calcLayoutPositions(box, axis); +} + +fn calcLayoutStandaloneSize(self: *UI, box: *Box, axis: Axis) void { + const size = box.size.getAxis(axis); + const computed_size = getVec2Axis(&box.persistent.size, axis); + + if (size.kind == .pixels) { + computed_size.* = size.kind.pixels; + } else if (size.kind == .text) { + if (box.text) |text| { + const font = Assets.font(text.font); + var text_size = font.measureText(text.content); + computed_size.* = getVec2Axis(&text_size, axis).*; + computed_size.* += size.kind.text * font.getSize(); + } else { + computed_size.* = 0; + } + } + + var child_iter = self.iterChildrenByParent(box); + while (child_iter.next()) |child| { + self.calcLayoutStandaloneSize(child, axis); + } +} + +fn calcLayoutUpwardsSize(self: *UI, box: *Box, axis: Axis) void { + const size = box.size.getAxis(axis); + const computed_size = getVec2Axis(&box.persistent.size, axis); + + if (size.kind == .percent) { + var maybe_fixed_parent: ?*Box = null; + + var parent_iter = self.iterUpwardByParent(box); + while (parent_iter.next()) |parent| { + const parent_size_kind = parent.size.getAxis(axis).kind; + if (parent_size_kind == .pixels or parent_size_kind == .percent or parent_size_kind == .text) { + maybe_fixed_parent = parent; + break; + } + } + + if (maybe_fixed_parent) |fixed_parent| { + computed_size.* = getVec2Axis(&fixed_parent.persistent.size, axis).* * size.kind.percent; + } + } + + { + var child_iter = self.iterChildrenByParent(box); + while (child_iter.next()) |child| { + self.calcLayoutUpwardsSize(child, axis); + } + } +} + +fn calcLayoutDownardsSize(self: *UI, box: *Box, axis: Axis) void { + { + var child_iter = self.iterChildrenByParent(box); + while (child_iter.next()) |child| { + self.calcLayoutDownardsSize(child, axis); + } + } + + const size = box.size.getAxis(axis); + const computed_size = getVec2Axis(&box.persistent.size, axis); + + if (size.kind == .children_sum) { + var sum: f32 = 0; + var child_iter = self.iterChildrenByParent(box); + while (child_iter.next()) |child| { + const child_size = getVec2Axis(&child.persistent.size, axis).*; + + if (box.layout_axis == axis) { + sum += child_size; + } else { + sum = @max(sum, child_size); + } + } + + computed_size.* = sum; + } +} + +fn calcLayoutPositions(self: *UI, box: *Box, axis: Axis) void { + { + var layout_position: f32 = 0; + + var child_iter = self.iterChildrenByParent(box); + while (child_iter.next()) |child| { + const child_axis_position = getVec2Axis(&child.persistent.position, axis); + const child_axis_size = getVec2Axis(&child.persistent.size, axis); + const parent_axis_position = getVec2Axis(&box.persistent.position, axis); + const child_override_position = switch (axis) { + .X => child.override_x, + .Y => child.override_y, + }; + + child_axis_position.* = parent_axis_position.*; + + if (child_override_position) |position| { + child_axis_position.* += position; + } else if (box.layout_axis == axis) { + child_axis_position.* += layout_position; + layout_position += child_axis_size.*; + } + } + } + + { + var child_iter = self.iterChildrenByParent(box); + while (child_iter.next()) |child| { + self.calcLayoutPositions(child, axis); + } + } +} + +fn calcLayoutEnforceConstraints(self: *UI, box: *Box, axis: Axis) void { + // Children can't be wider than the parent on the secondary axis + if (box.layout_axis != axis) { + const max_child_size = getVec2Axis(&box.persistent.size, axis).*; + + var child_iter = self.iterChildrenByParent(box); + while (child_iter.next()) |child| { + const child_size = getVec2Axis(&child.persistent.size, axis); + + if (child_size.* > max_child_size) { + child_size.* = max_child_size; + } + } + } + + // Children need to be shrunk relative to "strictness" on the primary axis + if (box.layout_axis == axis) { + const max_sum_children_size = getVec2Axis(&box.persistent.size, axis).*; + var sum_children_size: f32 = 0; + + var children_fixups: std.BoundedArray(f32, max_boxes) = .{}; + var children_fixup_sum: f32 = 0; + + { + var child_iter = self.iterChildrenByParent(box); + while (child_iter.next()) |child| { + const child_semantic_size_axis = child.size.getAxis(axis).*; + const child_size_axis = getVec2Axis(&child.persistent.size, axis).*; + + sum_children_size += child_size_axis; + + const child_fixup = child_size_axis * (1 - child_semantic_size_axis.strictness); + children_fixups.appendAssumeCapacity(child_fixup); + children_fixup_sum += child_fixup; + } + } + + const overflow = sum_children_size - max_sum_children_size; + if (overflow > 0) { + const overflow_percent = std.math.clamp(overflow / children_fixup_sum, 0, 1); + + var index: usize = 0; + var child_iter = self.iterChildrenByParent(box); + while (child_iter.next()) |child| : (index += 1) { + const child_size_axis = getVec2Axis(&child.persistent.size, axis); + + child_size_axis.* -= children_fixups.buffer[index] * overflow_percent; + } + } + } + + + { + var child_iter = self.iterChildrenByParent(box); + while (child_iter.next()) |child| { + self.calcLayoutEnforceConstraints(child, axis); + } + } +} + +pub fn newBoxFromString(self: *UI, text: []const u8) *Box { + var parent_hash: u64 = 0; + if (self.getParent()) |parent| { + parent_hash = parent.key.hash; + } + return self.newBox(Key.initString(parent_hash, text)); +} + +pub fn newBoxFromPtr(self: *UI, ptr: anytype) *Box { + return self.newBox(Key.initPtr(ptr)); +} + +pub fn newBox(self: *UI, key: Key) *Box { + assert(key.hash != 0); + + var box: *Box = undefined; + var box_index: BoxIndex = undefined; + var persistent: Box.Persistent = .{}; + if (self.findBoxByKey(key)) |found_box| { + assert(found_box.last_used_frame < self.frame_index); + + persistent = found_box.persistent; + box = found_box; + box_index = found_box.index; + } else { + box = self.boxes.addOneAssumeCapacity(); + box_index = self.boxes.len - 1; + } + + box.* = Box{ + .key = key, + .allocator = self.frameArena().allocator(), + .last_used_frame = self.frame_index, + .persistent = persistent, + + .index = box_index + }; + + if (self.getParent()) |parent| { + box.parent_index = parent.index; + + if (parent.last_child_index) |last_child_index| { + const last_child = &self.boxes.buffer[last_child_index]; + + last_child.next_sibling_index = box.index; + parent.last_child_index = box.index; + } else { + parent.first_child_index = box.index; + parent.last_child_index = box.index; + } + } + + return box; +} + +fn findBoxByKey(self: *UI, key: Key) ?*Box { + for (self.boxes.slice()) |*box| { + if (box.key.eql(key)) { + return box; + } + } + return null; +} + +pub fn signalFromBox(self: *UI, box: *Box) Signal { + var result = Signal{}; + + const key = box.key; + const rect = box.computedRect(); + const is_mouse_inside = rect_utils.isInsideVec2(rect, self.mouse); + const clickable = box.flags.contains(.clickable); + const draggable = box.flags.contains(.draggable); + + var event_index: usize = 0; + while (event_index < self.events.len) { + var taken = false; + const event: Event = self.events.buffer[event_index]; + const is_active = self.isBoxActive(key); + + if (event == .mouse_pressed and clickable and is_mouse_inside) { + const mouse_button = event.mouse_pressed; + result.insertMousePressed(mouse_button); + + self.active_box_keys.put(mouse_button, key); + taken = true; + } + + if (event == .mouse_released and clickable and is_active and is_mouse_inside) { + const mouse_button = event.mouse_released; + result.insertMousePressed(mouse_button); + result.insertMouseClicked(mouse_button); + + self.active_box_keys.remove(mouse_button); + taken = true; + } + + if (event == .mouse_released and clickable and is_active and !is_mouse_inside) { + const mouse_button = event.mouse_released; + result.insertMousePressed(mouse_button); + + self.hot_box_key = null; + self.active_box_keys.remove(mouse_button); + taken = true; + } + + if (taken) { + _ = self.events.swapRemove(event_index); + } else { + event_index += 1; + } + } + + if (draggable and self.mouse_delta.equals(Vec2.zero()) == 0) { + inline for (.{ rl.MouseButton.mouse_button_left, rl.MouseButton.mouse_button_right }) |mouse_button| { + const active_box = self.active_box_keys.get(mouse_button); + + if (active_box != null and active_box.?.eql(key)) { + result.insertMouseDragged(mouse_button); + result.drag = self.mouse_delta; + } + + } + } + + if (is_mouse_inside and clickable) { + if (self.hot_box_key == null) { + self.hot_box_key = key; + } + } + + return result; +} + +fn isBoxHot(self: *UI, key: Key) bool { + if (self.hot_box_key) |hot_box_key| { + return hot_box_key.eql(key); + } else { + return false; + } +} + +fn isBoxActive(self: *UI, key: Key) bool { + inline for (std.meta.fields(rl.MouseButton)) |mouse_button_field| { + const mouse_button: rl.MouseButton = @enumFromInt(mouse_button_field.value); + + if (self.active_box_keys.get(mouse_button)) |active_box| { + if (active_box.eql(key)) { + return true; + } + } + } + + return false; +} + +pub fn getParent(self: *UI) ?*Box { + const parent_stack: []BoxIndex = self.parent_index_stack.slice(); + + if (parent_stack.len > 0) { + const parent_index = parent_stack[parent_stack.len - 1]; + return &self.boxes.buffer[parent_index]; + } else { + return null; + } +} + +pub fn pushParent(self: *UI, box: *const Box) void { + self.parent_index_stack.appendAssumeCapacity(box.index); +} + +pub fn popParent(self: *UI) void { + _ = self.parent_index_stack.pop(); +} + +pub fn iterChildrenByParent(self: *UI, box: *const Box) BoxChildIterator { + return BoxChildIterator{ + .boxes = self.boxes.slice(), + .current_child = box.first_child_index + }; +} + +pub fn iterUpwardByParent(self: *UI, box: *const Box) BoxParentIterator { + return BoxParentIterator{ + .boxes = self.boxes.slice(), + .current_parent = box.parent_index + }; +} + +pub fn frameArena(self: *UI) *std.heap.ArenaAllocator { + return &self.arenas[@mod(self.frame_index, 2)]; +} diff --git a/src/ui/button.zig b/src/ui/button.zig deleted file mode 100644 index c3270d4..0000000 --- a/src/ui/button.zig +++ /dev/null @@ -1,65 +0,0 @@ -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 deleted file mode 100644 index 6ccaa28..0000000 --- a/src/ui/root.zig +++ /dev/null @@ -1,201 +0,0 @@ -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; - } -};