diff --git a/.gitignore b/.gitignore index c6f34c7..750b8f2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .zig-cache zig-out -.vscode \ No newline at end of file +.vscode +profile.json diff --git a/build.zig b/build.zig index 55cf12e..15b9400 100644 --- a/build.zig +++ b/build.zig @@ -90,6 +90,11 @@ pub fn build(b: *std.Build) !void { .optimize = optimize, }); + const profiler_dep = b.dependency("profiler.zig", .{ + .target = target, + .optimize = optimize, + }); + const stb_image_lib = buildStbImage(b); const cute_aseprite_lib = buildCuteAseprite(b, raylib_dep); @@ -114,6 +119,9 @@ pub fn build(b: *std.Build) !void { exe.root_module.addImport("known-folders", known_folders); exe.root_module.addImport("ini", ini); + // TODO: Add flag to disable in release + exe.root_module.addImport("profiler", profiler_dep.module("profiler")); + const external_compiler_support_dir = try std.process.getEnvVarOwned(b.allocator, "NIEXTCCOMPILERSUPP"); exe.addSystemIncludePath(.{ .cwd_relative = try std.fs.path.join(b.allocator, &.{ external_compiler_support_dir, "include" }) }); diff --git a/build.zig.zon b/build.zig.zon index 17f1508..ee8bfab 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,19 +2,16 @@ .name = "Baigiamasis projektas", .version = "0.1.0", - .dependencies = .{ - .@"raylib-zig" = .{ - .url = "https://github.com/Not-Nik/raylib-zig/archive/43d15b05c2b97cab30103fa2b46cff26e91619ec.tar.gz", - .hash = "12204a223b19043e17b79300413d02f60fc8004c0d9629b8d8072831e352a78bf212" + .dependencies = .{ .@"raylib-zig" = .{ .url = "https://github.com/Not-Nik/raylib-zig/archive/43d15b05c2b97cab30103fa2b46cff26e91619ec.tar.gz", .hash = "12204a223b19043e17b79300413d02f60fc8004c0d9629b8d8072831e352a78bf212" }, .@"known-folders" = .{ + .url = "git+https://github.com/ziglibs/known-folders.git#1cceeb70e77dec941a4178160ff6c8d05a74de6f", + .hash = "12205f5e7505c96573f6fc5144592ec38942fb0a326d692f9cddc0c7dd38f9028f29", + }, .ini = .{ + .url = "https://github.com/ziglibs/ini/archive/e18d36665905c1e7ba0c1ce3e8780076b33e3002.tar.gz", + .hash = "1220b0979ea9891fa4aeb85748fc42bc4b24039d9c99a4d65d893fb1c83e921efad8", + }, .@"profiler.zig" = .{ + .url = "git+https://github.com/lassade/profiler.zig.git#d066d066c36c4eebd494babf15c1cdbd2d512b12", + .hash = "122097461acc2064f5f89b85d76d2a02232579864b17604617a333789c892f2d262f", }, - .@"known-folders" = .{ - .url = "git+https://github.com/ziglibs/known-folders.git#1cceeb70e77dec941a4178160ff6c8d05a74de6f", - .hash = "12205f5e7505c96573f6fc5144592ec38942fb0a326d692f9cddc0c7dd38f9028f29", - }, - .@"ini" = .{ - .url = "https://github.com/ziglibs/ini/archive/e18d36665905c1e7ba0c1ce3e8780076b33e3002.tar.gz", - .hash = "1220b0979ea9891fa4aeb85748fc42bc4b24039d9c99a4d65d893fb1c83e921efad8", - } }, .paths = .{ diff --git a/src/app.zig b/src/app.zig index 5927741..8880d10 100644 --- a/src/app.zig +++ b/src/app.zig @@ -1,26 +1,24 @@ const std = @import("std"); -const rl = @import("raylib"); -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 rl = @import("raylib"); +const srcery = @import("./srcery.zig"); const NIDaq = @import("ni-daq/root.zig"); -const rect_utils = @import("./rect-utils.zig"); +const Graph = @import("./graph.zig"); const TaskPool = @import("ni-daq/task-pool.zig"); const utils = @import("./utils.zig"); +const P = @import("profiler"); -const remap = utils.remap; -const lerpColor = utils.lerpColor; const log = std.log.scoped(.app); -const assert = std.debug.assert; const clamp = std.math.clamp; - -const App = @This(); +const assert = std.debug.assert; +const remap = utils.remap; const max_channels = 64; const max_files = 32; +const App = @This(); + const FileChannel = struct { path: []u8, samples: []f64, @@ -92,41 +90,23 @@ const ChannelView = struct { }; allocator: std.mem.Allocator, - ui: UI, +should_close: bool = false, +ni_daq_api: ?NIDaq.Api = null, +ni_daq: ?NIDaq = null, +task_pool: TaskPool, channel_views: std.BoundedArray(ChannelView, max_channels) = .{}, loaded_files: [max_channels]?FileChannel = .{ null } ** max_channels, device_channels: [max_channels]?DeviceChannel = .{ null } ** max_channels, -ni_daq_api: ?NIDaq.Api = null, -ni_daq: ?NIDaq = null, -task_pool: TaskPool, +started_collecting: bool = false, -shown_window: enum { - channels, - add_from_device -} = .channels, - -shown_modal: ?union(enum) { - no_library_error, - library_version_error: std.SemanticVersion, - library_version_warning: std.SemanticVersion -} = null, - -device_filter: NIDaq.BoundedDeviceName = .{}, -channel_type_filter: ?NIDaq.ChannelType = null, -selected_channels: std.BoundedArray([:0]u8, max_channels) = .{}, - -last_hot_channel: ?[:0]const u8 = null, -show_device_filter_dropdown: bool = false, -show_channel_type_filter_dropdown: bool = false, - -should_close: bool = false, - -graph_start_sample: ?struct { - value: f64, - axis: UI.Axis - } = null, +graph_controls: struct { + graph_start_sample: ?struct { + value: f64, + axis: UI.Axis + } = null, +} = .{}, pub fn init(self: *App, allocator: std.mem.Allocator) !void { self.* = App{ @@ -134,8 +114,7 @@ pub fn init(self: *App, allocator: std.mem.Allocator) !void { .ui = UI.init(allocator), .task_pool = undefined }; - errdefer if (self.ni_daq_api != null) self.ni_daq_api.?.deinit(); - errdefer if (self.ni_daq != null) self.ni_daq.?.deinit(allocator); + errdefer self.deinit(); if (NIDaq.Api.init()) |ni_daq_api| { self.ni_daq_api = ni_daq_api; @@ -153,7 +132,8 @@ pub fn init(self: *App, allocator: std.mem.Allocator) !void { const installed_version = try ni_daq.version(); if (installed_version.order(NIDaq.Api.min_version) == .lt) { - self.shown_modal = .{ .library_version_warning = installed_version }; + // TODO: + // self.shown_modal = .{ .library_version_warning = installed_version }; } } else |e| { @@ -161,14 +141,16 @@ pub fn init(self: *App, allocator: std.mem.Allocator) !void { switch (e) { error.LibraryNotFound => { - self.shown_modal = .no_library_error; + // TODO: + // self.shown_modal = .no_library_error; }, error.SymbolNotFound => { - if (NIDaq.Api.version()) |version| { - self.shown_modal = .{ .library_version_error = version }; - } else |_| { - self.shown_modal = .no_library_error; - } + // TODO: + // if (NIDaq.Api.version()) |version| { + // self.shown_modal = .{ .library_version_error = version }; + // } else |_| { + // self.shown_modal = .no_library_error; + // } } } } @@ -196,17 +178,22 @@ pub fn deinit(self: *App) void { } } - for (self.selected_channels.constSlice()) |channel| { - self.allocator.free(channel); - } - self.selected_channels.len = 0; - self.ui.deinit(); self.task_pool.deinit(); + if (self.ni_daq_api) |*ni_daq_api| ni_daq_api.deinit(); if (self.ni_daq) |*ni_daq| ni_daq.deinit(self.allocator); } +fn findFreeSlot(T: type, slice: []const ?T) ?usize { + for (0.., slice) |i, loaded_file| { + if (loaded_file == null) { + return i; + } + } + return null; +} + fn readSamplesFromFile(allocator: std.mem.Allocator, file: std.fs.File) ![]f64 { try file.seekTo(0); const byte_count = try file.getEndPos(); @@ -230,15 +217,6 @@ fn readSamplesFromFile(allocator: std.mem.Allocator, file: std.fs.File) ![]f64 { return samples; } -fn findFreeSlot(T: type, slice: []const ?T) ?usize { - for (0.., slice) |i, loaded_file| { - if (loaded_file == null) { - return i; - } - } - return null; -} - pub fn appendChannelFromFile(self: *App, path: []const u8) !void { const path_dupe = try self.allocator.dupe(u8, path); errdefer self.allocator.free(path_dupe); @@ -285,7 +263,6 @@ pub fn appendChannelFromFile(self: *App, path: []const u8) !void { .source = .{ .file = loaded_file_index } }); errdefer _ = self.channel_views.pop(); - } pub fn appendChannelFromDevice(self: *App, channel_name: []const u8) !void { @@ -355,176 +332,139 @@ fn getChannelSource(self: *App, channel_view: *ChannelView) ?ChannelView.SourceO return null; } -fn findChannelIndexByName(haystack: []const [:0]const u8, needle: [:0]const u8) ?usize { - for (0.., haystack) |i, item| { - if (std.mem.eql(u8, item, needle)) { - return i; - } - } - return null; +pub fn button(self: *App, label: []const u8) *UI.Box { + return self.ui.createBox(.{ + .key = self.ui.keyFromString(label), + .size_x = UI.Sizing.initFixed(.text), + .size_y = UI.Sizing.initFixed(.text), + .flags = &.{ .draw_hot, .draw_active, .clickable }, + .padding = UI.Padding{ + .bottom = self.ui.rem(0.5), + .top = self.ui.rem(0.5), + .left = self.ui.rem(1), + .right = self.ui.rem(1) + }, + .hot_cursor = .mouse_cursor_pointing_hand, + .text = label + }); } -// ------------------------------- GUI -------------------------------------------- // +pub fn beginScrollbar(self: *App, key: UI.Key) *UI.Box { + const wrapper = self.ui.createBox(.{ + .key = key, + .layout_direction = .left_to_right, + .size_x = UI.Sizing.initGrowFull(), + .size_y = UI.Sizing.initGrowFull() + }); + wrapper.beginChildren(); -const Row = struct { - name: []const u8, - value: []const u8 -}; + const content_area = self.ui.createBox(.{ + .key = self.ui.keyFromString("Scrollable content area"), + .flags = &.{ .scrollable, .clip_view }, + .size_x = UI.Sizing.initGrowFull(), + .size_y = UI.Sizing.initFitChildren(), + }); + content_area.beginChildren(); -fn showLabelRows(self: *App, rows: []const Row) void { - { - const name_column = self.ui.newBoxFromString("Names"); - name_column.layout_axis = .Y; - name_column.size.y = UI.Size.childrenSum(1); - name_column.size.x = UI.Size.childrenSum(1); - self.ui.pushParent(name_column); - defer self.ui.popParent(); + const content_size = content_area.persistent.size.y; + const visible_percent = clamp(wrapper.persistent.size.y / content_size, 0, 1); + const sroll_offset = content_area.persistent.sroll_offset; + content_area.view_offset.y = sroll_offset * (1 - visible_percent) * content_size; - for (rows) |row| { - _ = self.ui.label(.text, row.name); - } - } - - { - const value_column = self.ui.newBoxFromString("Values"); - value_column.layout_axis = .Y; - value_column.size.y = UI.Size.childrenSum(1); - value_column.size.x = UI.Size.percent(1, 0); - self.ui.pushParent(value_column); - defer self.ui.popParent(); - - for (rows) |row| { - const label = self.ui.label(.text, row.value); - label.flags.insert(.text_wrapping); - } - } + return content_area; } -fn showChannelViewSlider(self: *App, view_rect: *Graph.ViewOptions, sample_count: f32) void { - const min_visible_samples = 1; // sample_count*0.02; +pub fn endScrollbar(self: *App) void { + const content_area = self.ui.parentBox().?; + content_area.endChildren(); - 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 wrapper = self.ui.parentBox().?; - const minimap_rect = minimap_box.computedRect(); + const visible_percent = clamp(wrapper.persistent.size.y / content_area.persistent.size.y, 0, 1); - const middle_box = self.ui.clickableBox("Middle knob"); { - middle_box.flags.insert(.draggable); - middle_box.background = rl.Color.black.alpha(0.5); - middle_box.size.y = UI.Size.pixels(32, 1); + const scrollbar_area = self.ui.createBox(.{ + .key = self.ui.keyFromString("Scrollbar area"), + .background = srcery.hard_black, + .flags = &.{ .scrollable }, + .size_x = .{ .fixed = .{ .pixels = 24 } }, + .size_y = UI.Sizing.initGrowFull() + }); + scrollbar_area.beginChildren(); + defer scrollbar_area.endChildren(); + + const draggable = self.ui.createBox(.{ + .key = self.ui.keyFromString("Scrollbar button"), + .background = srcery.black, + .flags = &.{ .draw_hot, .draw_active, .clickable, .draggable }, + .borders = UI.Borders.all(.{ .size = 4, .color = srcery.xgray3 }), + .size_x = UI.Sizing.initFixed(.{ .parent_percent = 1 }), + .size_y = UI.Sizing.initFixed(.{ .parent_percent = visible_percent }), + .hot_cursor = .mouse_cursor_pointing_hand + }); + + const sroll_offset = &content_area.persistent.sroll_offset; + const scrollbar_height = scrollbar_area.persistent.size.y; + const max_offset = scrollbar_height * (1 - visible_percent); + draggable.setFloatY(content_area.persistent.position.y + sroll_offset.* * max_offset); + + const draggable_signal = self.ui.signal(draggable); + if (draggable_signal.dragged()) { + sroll_offset.* += draggable_signal.drag.y / max_offset; + } + + const scroll_speed = 16; + const scrollbar_signal = self.ui.signal(scrollbar_area); + if (scrollbar_signal.scrolled()) { + sroll_offset.* -= scrollbar_signal.scroll.y / max_offset * scroll_speed; + } + + const content_area_signal = self.ui.signal(content_area); + if (content_area_signal.scrolled()) { + sroll_offset.* -= content_area_signal.scroll.y / max_offset * scroll_speed; + } + + sroll_offset.* = std.math.clamp(sroll_offset.*, 0, 1); } - const left_knob_box = self.ui.clickableBox("Left knob"); - { - left_knob_box.active_cursor = .mouse_cursor_resize_ew; - left_knob_box.flags.insert(.draggable); - left_knob_box.background = rl.Color.black.alpha(0.5); - left_knob_box.size.x = UI.Size.pixels(8, 1); - left_knob_box.size.y = UI.Size.pixels(32, 1); - } - const right_knob_box = self.ui.clickableBox("Right knob"); - { - right_knob_box.active_cursor = .mouse_cursor_resize_ew; - right_knob_box.flags.insert(.draggable); - right_knob_box.background = rl.Color.black.alpha(0.5); - right_knob_box.size.x = UI.Size.pixels(8, 1); - right_knob_box.size.y = UI.Size.pixels(32, 1); - } - - const left_knob_size = left_knob_box.persistent.size.x; - const right_knob_size = right_knob_box.persistent.size.x; - - const left_signal = self.ui.signalFromBox(left_knob_box); - if (left_signal.dragged()) { - view_rect.from += remap( - f32, - 0, minimap_rect.width, - 0, sample_count, - left_signal.drag.x - ); - - view_rect.from = clamp(view_rect.from, 0, view_rect.to-min_visible_samples); - } - - const right_signal = self.ui.signalFromBox(right_knob_box); - if (right_signal.dragged()) { - view_rect.to += remap( - f32, - 0, minimap_rect.width, - 0, sample_count, - right_signal.drag.x - ); - - view_rect.to = clamp(view_rect.to, view_rect.from + min_visible_samples, sample_count); - } - - const middle_signal = self.ui.signalFromBox(middle_box); - if (middle_signal.dragged()) { - var samples_moved = middle_signal.drag.x / minimap_rect.width * sample_count; - - samples_moved = clamp(samples_moved, -view_rect.from, sample_count - view_rect.to); - - view_rect.from += samples_moved; - view_rect.to += samples_moved; - } - - left_knob_box.setFixedX(remap(f32, - 0, sample_count, - 0, minimap_rect.width - left_knob_size - right_knob_size, - view_rect.from - )); - - right_knob_box.setFixedX(remap(f32, - 0, sample_count, - left_knob_size, minimap_rect.width - right_knob_size, - view_rect.to - )); - - middle_box.setFixedX(remap(f32, - 0, sample_count, - left_knob_size, minimap_rect.width - right_knob_size, - view_rect.from - )); - middle_box.setFixedWidth(remap(f32, - 0, sample_count, - 0, minimap_rect.width - right_knob_size - left_knob_size, - view_rect.to - view_rect.from - )); + wrapper.endChildren(); } fn showChannelViewGraph(self: *App, channel_view: *ChannelView) !void { + const zone2 = P.begin(@src(), "showChannelViewGraph"); + defer zone2.end(); + + var ui = &self.ui; + const source = self.getChannelSource(channel_view) orelse return; const samples = source.samples(); source.lockSamples(); defer source.unlockSamples(); - var channel_rect_opts: *Graph.ViewOptions = &channel_view.view_rect; + const channel_rect_opts: *Graph.ViewOptions = &channel_view.view_rect; - const graph_box = self.ui.newBoxFromString("Graph"); - graph_box.flags.insert(.clickable); - graph_box.flags.insert(.draggable); - graph_box.background = srcery.black; - graph_box.size.x = UI.Size.percent(1, 0); - graph_box.size.y = UI.Size.pixels(channel_view.height, 1); - self.ui.pushParent(graph_box); - defer self.ui.popParent(); + const graph_box = ui.createBox(.{ + .key = ui.keyFromString("Graph"), + .size_x = UI.Sizing.initGrowFull(), + .size_y = UI.Sizing.initGrowFull(), + .background = srcery.black, + .flags = &.{ .clickable, .draggable }, + }); + graph_box.beginChildren(); + defer graph_box.endChildren(); - const graph_rect = graph_box.computedRect(); + const graph_rect = graph_box.rect(); - const signal = self.ui.signalFromBox(graph_box); + const signal = self.ui.signal(graph_box); var axis = UI.Axis.X; var zooming: bool = false; var start_sample: ?f64 = null; var stop_sample: ?f64 = null; + var controls = &self.graph_controls; + if (signal.hot) { if (signal.shift_modifier) { axis = UI.Axis.Y; @@ -532,11 +472,12 @@ fn showChannelViewGraph(self: *App, channel_view: *ChannelView) !void { axis = UI.Axis.X; } - if (self.graph_start_sample) |graph_start_sample| { + if (controls.graph_start_sample) |graph_start_sample| { axis = graph_start_sample.axis; zooming = true; } + // TODO: Don't use relative mouse movement, after a lock of sliding the point where you grabbed by drifts var mouse_sample: f64 = undefined; if (axis == .X) { const mouse_sample_index = channel_rect_opts.mapSampleXToIndex(0, graph_rect.width, signal.relative_mouse.x); @@ -549,13 +490,13 @@ fn showChannelViewGraph(self: *App, channel_view: *ChannelView) !void { start_sample = mouse_sample; if (signal.flags.contains(.right_pressed)) { - self.graph_start_sample = .{ + controls.graph_start_sample = .{ .value = mouse_sample, .axis = axis }; } - if (self.graph_start_sample) |graph_start_sample| { + if (controls.graph_start_sample) |graph_start_sample| { start_sample = graph_start_sample.value; stop_sample = mouse_sample; zooming = true; @@ -570,7 +511,7 @@ fn showChannelViewGraph(self: *App, channel_view: *ChannelView) !void { } if (signal.flags.contains(.right_released)) { - self.graph_start_sample = null; + controls.graph_start_sample = null; if (start_sample != null and stop_sample != null) { const lower_sample: f64 = @min(start_sample.?, stop_sample.?); @@ -599,14 +540,15 @@ fn showChannelViewGraph(self: *App, channel_view: *ChannelView) !void { if (start_sample != null and stop_sample != null) { - const fill = self.ui.newBox(UI.Key.initNil()); - fill.background = srcery.green.alpha(0.5); + const fill = ui.createBox(.{ + .background = srcery.green.alpha(0.5), + }); if (axis == .X) { const start_x = channel_rect_opts.mapSampleIndexToX(graph_rect.x, graph_rect.width, start_sample.?); const stop_x = channel_rect_opts.mapSampleIndexToX(graph_rect.x, graph_rect.width, stop_sample.?); - fill.setFixedRect(.{ + fill.setFloatRect(.{ .x = @floatCast(@min(start_x, stop_x)), .y = graph_rect.y, .width = @floatCast(@abs(start_x - stop_x)), @@ -616,7 +558,7 @@ fn showChannelViewGraph(self: *App, channel_view: *ChannelView) !void { const start_y = channel_rect_opts.mapSampleValueToY(graph_rect.y, graph_rect.height, start_sample.?); const stop_y = channel_rect_opts.mapSampleValueToY(graph_rect.y, graph_rect.height, stop_sample.?); - fill.setFixedRect(.{ + fill.setFloatRect(.{ .x = graph_rect.x, .y = @floatCast(@min(start_y, stop_y)), .width = graph_rect.width, @@ -626,13 +568,14 @@ fn showChannelViewGraph(self: *App, channel_view: *ChannelView) !void { } if (start_sample) |sample| { - const marker = self.ui.newBox(UI.Key.initNil()); - marker.background = srcery.green; + const marker = ui.createBox(.{ + .background = srcery.green, + }); if (axis == .X) { const value = samples[@intFromFloat(sample)]; - marker.setFmtText(.text, "{d:0.2} | {d:0.6}", .{sample, value}); - marker.setFixedRect(UI.Rect{ + marker.setFmtText("{d:0.2} | {d:0.6}", .{sample, value}); + marker.setFloatRect(UI.Rect{ .x = @floatCast(channel_rect_opts.mapSampleIndexToX(graph_rect.x, graph_rect.width, sample)), .y = graph_rect.y, .width = 1, @@ -640,8 +583,8 @@ fn showChannelViewGraph(self: *App, channel_view: *ChannelView) !void { }); } else if (axis == .Y) { - marker.setFmtText(.text, "{d:0.2}", .{sample}); - marker.setFixedRect(UI.Rect{ + marker.setFmtText("{d:0.2}", .{sample}); + marker.setFloatRect(UI.Rect{ .x = graph_rect.x, .y = @floatCast(channel_rect_opts.mapSampleValueToY(graph_rect.y, graph_rect.height, sample)), .width = graph_rect.width, @@ -651,19 +594,20 @@ fn showChannelViewGraph(self: *App, channel_view: *ChannelView) !void { } if (stop_sample) |sample| { - const marker = self.ui.newBox(UI.Key.initNil()); - marker.background = srcery.green; + const marker = ui.createBox(.{ + .background = srcery.green, + }); - marker.setFmtText(.text, "{d:0.2}", .{sample}); + marker.setFmtText("{d:0.2}", .{sample}); if (axis == .X) { - marker.setFixedRect(.{ + marker.setFloatRect(.{ .x = @floatCast(channel_rect_opts.mapSampleIndexToX(graph_rect.x, graph_rect.width, sample)), .y = graph_rect.y, .width = 1, .height = graph_rect.height }); } else if (axis == .Y) { - marker.setFixedRect(.{ + marker.setFloatRect(.{ .x = graph_rect.x, .y = @floatCast(channel_rect_opts.mapSampleValueToY(graph_rect.y, graph_rect.height, sample)), .width = graph_rect.width, @@ -706,150 +650,85 @@ fn showChannelViewGraph(self: *App, channel_view: *ChannelView) !void { if (channel_view.view_cache.texture) |texture| { graph_box.texture = texture.texture; } - } fn showChannelView(self: *App, channel_view: *ChannelView) !void { - const source = self.getChannelSource(channel_view) orelse return; + const zone2 = P.begin(@src(), "showChannelView"); + defer zone2.end(); - var channel_rect_opts: *Graph.ViewOptions = &channel_view.view_rect; + // const source = self.getChannelSource(channel_view) orelse return; + var ui = &self.ui; - const channel_box = self.ui.newBoxFromPtr(channel_view); - 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.pixels(channel_view.height, 1); - self.ui.pushParent(channel_box); - defer self.ui.popParent(); + const channel_rect_opts: *Graph.ViewOptions = &channel_view.view_rect; + + const channel_view_box = ui.createBox(.{ + .key = UI.Key.initPtr(channel_view), + .layout_direction = .top_to_bottom, + .size_x = .{ .fixed = .{ .parent_percent = 1 } }, + .size_y = .{ .fixed = .{ .pixels = channel_view.height } } + }); + channel_view_box.beginChildren(); + defer channel_view_box.endChildren(); + + const ruler_size = UI.Sizing.initFixed(.{ .pixels = 32 }); + + var y_markers: *UI.Box = undefined; + var x_markers: *UI.Box = undefined; { - const tools_box = self.ui.newBoxFromString("Graph tools"); - tools_box.background = rl.Color.gray; - tools_box.layout_axis = .X; - tools_box.size.x = UI.Size.percent(1, 0); - tools_box.size.y = UI.Size.pixels(32, 1); - self.ui.pushParent(tools_box); - defer self.ui.popParent(); + const container = ui.createBox(.{ + .layout_direction = .left_to_right, + .size_x = .{ .fixed = .{ .parent_percent = 1 } }, + .size_y = UI.Sizing.initGrowFull(), + }); + container.beginChildren(); + defer container.endChildren(); - { - const reset_view_button = self.ui.button(.text, "Reset view"); - reset_view_button.size.y = UI.Size.percent(1, 0); - - if (self.ui.signalFromBox(reset_view_button).clicked()) { - channel_rect_opts.from = channel_view.default_from; - channel_rect_opts.to = channel_view.default_to; - channel_rect_opts.min_value = channel_view.default_min_value; - channel_rect_opts.max_value = channel_view.default_max_value; - } - } - - if (source == .device) { - const device_channel = source.device; - - if (self.ni_daq) |*ni_daq| { - const record_button = self.ui.button(.text, "Record"); - record_button.size.y = UI.Size.percent(1, 0); - - if (device_channel.active_task != null) { - record_button.setText(.text, "Stop"); - } - - const signal = self.ui.signalFromBox(record_button); - if (signal.clicked()) { - if (device_channel.active_task) |task| { - try task.stop(); - device_channel.active_task = null; - } else { - const channel_name = device_channel.name.buffer[0..device_channel.name.len :0]; - device_channel.active_task = try self.task_pool.launchAIVoltageChannel( - ni_daq, - &device_channel.mutex, - &device_channel.samples, - .{ - .continous = .{ .sample_rate = device_channel.max_sample_rate } - }, - .{ - .min_value = channel_view.default_min_value, - .max_value = channel_view.default_max_value, - .units = device_channel.units, - .channel = channel_name - } - ); - - channel_view.follow = true; - } - } - } - - { - const follow_button = self.ui.button(.text, "Follow"); - follow_button.size.y = UI.Size.percent(1, 0); - if (channel_view.follow) { - follow_button.setText(.text, "Unfollow"); - } - - const signal = self.ui.signalFromBox(follow_button); - if (signal.clicked()) { - channel_view.follow = !channel_view.follow; - } - - } - } - } - - // const channel_box = self.ui.newBox(UI.Key.initNil()); - // 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(); - - var y_axis_markers: *UI.Box = undefined; - var x_axis_markers: *UI.Box = undefined; - - { - const y_axis_container = self.ui.newBox(UI.Key.initNil()); - y_axis_container.layout_axis = .X; - y_axis_container.size.x = UI.Size.percent(1, 0); - y_axis_container.size.y = UI.Size.percent(1, 0); - self.ui.pushParent(y_axis_container); - defer self.ui.popParent(); - - y_axis_markers = self.ui.newBoxFromString("Y axis markers"); - y_axis_markers.background = rl.Color.blue; - y_axis_markers.size.x = UI.Size.pixels(64, 1); - y_axis_markers.size.y = UI.Size.percent(1, 0); + y_markers = ui.createBox(.{ + .key = ui.keyFromString("Y markers"), + .size_x = ruler_size, + .size_y = .{ .fixed = .{ .parent_percent = 1 } }, + .background = srcery.hard_black, + .flags = &.{ .clickable }, + .hot_cursor = .mouse_cursor_pointing_hand + }); try self.showChannelViewGraph(channel_view); } - { - const horizontal_container = self.ui.newBox(UI.Key.initNil()); - horizontal_container.layout_axis = .X; - horizontal_container.size.x = UI.Size.childrenSum(0); - horizontal_container.size.y = UI.Size.childrenSum(1); - self.ui.pushParent(horizontal_container); - defer self.ui.popParent(); + const container = ui.createBox(.{ + .layout_direction = .left_to_right, + .size_x = .{ .fixed = .{ .parent_percent = 1 } }, + .size_y = .fit_children, + }); + container.beginChildren(); + defer container.endChildren(); - const fullscreen_button = self.ui.newBoxFromString("Fullscreen"); - fullscreen_button.background = rl.Color.red; - fullscreen_button.size.x = UI.Size.pixels(y_axis_markers.computedRect().width, 1); - fullscreen_button.size.y = UI.Size.pixels(32, 1); + _ = ui.createBox(.{ + .size_y = ruler_size, + .size_x = ruler_size, + .background = srcery.hard_black + }); - x_axis_markers = self.ui.newBoxFromString("X axis markers"); - x_axis_markers.background = rl.Color.pink; - x_axis_markers.size.x = UI.Size.percent(1, 0); - x_axis_markers.size.y = UI.Size.pixels(32, 1); - self.ui.pushParent(x_axis_markers); - defer self.ui.popParent(); + x_markers = ui.createBox(.{ + .key = ui.keyFromString("X markers"), + .size_y = ruler_size, + .size_x = UI.Sizing.initShrinkFull(), + .background = srcery.hard_black, + .flags = &.{ .clickable }, + .hot_cursor = .mouse_cursor_pointing_hand + }); } { - self.ui.pushParent(y_axis_markers); - defer self.ui.popParent(); + const zone = P.begin(@src(), "Y markers"); + defer zone.end(); - const y_axis_rect = y_axis_markers.computedRect(); + y_markers.beginChildren(); + defer y_markers.endChildren(); + + const y_axis_rect = y_markers.rect(); const min_gap_between_markers = 8; @@ -860,9 +739,10 @@ fn showChannelView(self: *App, channel_view: *ChannelView) !void { var marker = utils.roundNearestUp(f64, @max(channel_rect_opts.min_value, channel_view.default_min_value), axis_marker_size); while (marker < @min(channel_rect_opts.max_value, channel_view.default_max_value)) : (marker += axis_marker_size) { - const marker_box = self.ui.newBox(UI.Key.initNil()); - marker_box.background = rl.Color.yellow; - marker_box.setFixedRect(.{ + const marker_box = ui.createBox(.{ + .background = rl.Color.yellow, + }); + marker_box.setFloatRect(.{ .width = y_axis_rect.width/5, .height = 1, .x = y_axis_rect.x + y_axis_rect.width/5*4, @@ -873,39 +753,26 @@ fn showChannelView(self: *App, channel_view: *ChannelView) !void { marker )) }); - - const label_box = self.ui.newBox(UI.Key.initNil()); - label_box.setFmtText(.text, "{d:.03}", .{marker}); - label_box.size.x = UI.Size.text(0, 1); - label_box.size.y = UI.Size.text(0, 1); - label_box.flags.insert(.text_left_align); - label_box.setFixedRect(.{ - .width = y_axis_rect.width, - .height = 1, - .x = y_axis_rect.x, - .y = @floatCast(remap( - f64, - channel_rect_opts.max_value, channel_rect_opts.min_value, - y_axis_rect.y, y_axis_rect.y + y_axis_rect.height, - marker - )) - }); } } { - self.ui.pushParent(x_axis_markers); - defer self.ui.popParent(); + const zone = P.begin(@src(), "X markers"); + defer zone.end(); - const y_axis_rect = x_axis_markers.computedRect(); + x_markers.beginChildren(); + defer x_markers.endChildren(); + + const y_axis_rect = x_markers.rect(); const axis_marker_size = 100000; var marker = utils.roundNearestUp(f64, @max(channel_rect_opts.from, channel_view.default_from), axis_marker_size); while (marker < @min(channel_rect_opts.to, channel_view.default_to)) : (marker += axis_marker_size) { - const marker_box = self.ui.newBox(UI.Key.initNil()); - marker_box.background = rl.Color.yellow; - marker_box.setFixedRect(.{ + const marker_box = ui.createBox(.{ + .background = srcery.yellow + }); + marker_box.setFloatRect(.{ .width = 1, .height = y_axis_rect.height/2, .x = @floatCast(remap( @@ -920,697 +787,153 @@ fn showChannelView(self: *App, channel_view: *ChannelView) !void { } } -fn showChannelsWindow(self: *App) !void { - if (rl.isKeyPressed(rl.KeyboardKey.key_escape)) { - self.should_close = true; - } +pub fn showWindowChannels(self: *App) !void { + const zone = P.begin(@src(), "showWindowChannels"); + defer zone.end(); - const scroll_area = self.ui.pushScrollbar(self.ui.newKeyFromString("Channels")); - defer self.ui.popScrollbar(); - scroll_area.layout_axis = .Y; - scroll_area.layout_gap = 16; - - for (self.channel_views.slice()) |*channel_view| { - try self.showChannelView(channel_view); - } + var ui = &self.ui; + const root = ui.parentBox().?; + root.layout_direction = .top_to_bottom; { - const prompt_box = self.ui.newBoxFromString("Add prompt"); - prompt_box.size.x = UI.Size.percent(1, 0); - prompt_box.size.y = UI.Size.pixels(200, 1); - self.ui.pushParent(prompt_box); - defer self.ui.popParent(); - - const center_box = self.ui.pushCenterBox(); - defer self.ui.popCenterBox(); - center_box.layout_axis = .X; - center_box.layout_gap = 32; - - const from_file_button = self.ui.button(.text, "Add from file"); - from_file_button.borders.all(.{ .size = 2, .color = srcery.green }); - if (self.ui.signalFromBox(from_file_button).clicked()) { - std.debug.print("{}\n", .{std.fs.max_path_bytes}); - if (Platform.openFilePicker(self.allocator)) |filename| { - defer self.allocator.free(filename); - - // TODO: Handle error - self.appendChannelFromFile(filename) catch @panic("Failed to append channel from file"); - } else |err| { - // TODO: Show error message to user; - log.err("Failed to pick file: {}", .{ err }); - } - } - - const from_device_button = self.ui.button(.text, "Add from device"); - from_device_button.borders.all(.{ .size = 2, .color = srcery.green }); - if (self.ui.signalFromBox(from_device_button).clicked()) { - self.shown_window = .add_from_device; - } - } -} - -fn showChannelInfoPanel(self: *App, hot_channel: ?[:0]const u8) !void { - const ni_daq = &(self.ni_daq orelse return); - - var device_buff: NIDaq.BoundedDeviceName = .{}; - var hot_device: ?[:0]const u8 = null; - if (hot_channel) |channel| { - if (NIDaq.getDeviceNameFromChannel(channel)) |device| { - device_buff.appendSliceAssumeCapacity(device); - device_buff.buffer[device_buff.len] = 0; - hot_device = device_buff.buffer[0..device_buff.len :0]; - } - } - - const info_box = self.ui.newBoxFromString("Info box"); - info_box.layout_axis = .Y; - info_box.size.y = UI.Size.percent(1, 0); - info_box.size.x = UI.Size.percent(1, 0); - self.ui.pushParent(info_box); - defer self.ui.popParent(); - - if (hot_channel) |channel| { - _ = self.ui.label(.text, "Channel properties"); - - const channel_info = self.ui.newBoxFromString("Channel info"); - channel_info.layout_axis = .X; - channel_info.size.y = UI.Size.childrenSum(1); - channel_info.size.x = UI.Size.percent(1, 0); - self.ui.pushParent(channel_info); - defer self.ui.popParent(); - - var rows: std.BoundedArray(Row, 16) = .{}; - - rows.appendAssumeCapacity(Row{ - .name = "Name", - .value = channel + const toolbar = ui.createBox(.{ + .background = srcery.black, + .layout_direction = .left_to_right, + .size_x = .{ .fixed = .{ .parent_percent = 1 } }, + .size_y = .{ .fixed = .{ .font_size = 2 } } }); + toolbar.beginChildren(); + defer toolbar.endChildren(); - var channel_type_name: []const u8 = "unknown"; - if (NIDaq.getChannelType(channel)) |channel_type| { - channel_type_name = channel_type.name(); - // rows.appendAssumeCapacity(Row{ - // .name = "Type", - // .value = channel_type_name - // }); - } + var start_all = self.button("Start/Stop button"); + start_all.borders = UI.Borders.all(.{ .size = 4, .color = srcery.red }); + start_all.background = srcery.black; + start_all.size.y = UI.Sizing.initFixed(.{ .parent_percent = 1 }); + start_all.padding.top = 0; + start_all.padding.bottom = 0; + if (ui.signal(start_all).clicked()) { + self.started_collecting = !self.started_collecting; - rows.appendAssumeCapacity(Row{ - .name = "Type", - .value = channel_type_name - }); - self.showLabelRows(rows.constSlice()); - } + if (self.ni_daq) |*ni_daq| { + for (self.channel_views.slice()) |*channel_view| { + const source = self.getChannelSource(channel_view) orelse continue; - self.ui.spacer(.{ .y = UI.Size.pixels(16, 0) }); - - if (hot_device) |device| { - _ = self.ui.label(.text, "Device properties"); - - const device_info = self.ui.newBoxFromString("Device info"); - device_info.layout_axis = .X; - device_info.size.y = UI.Size.childrenSum(1); - device_info.size.x = UI.Size.percent(1, 0); - self.ui.pushParent(device_info); - defer self.ui.popParent(); - - var rows: std.BoundedArray(Row, 16) = .{}; - - if (ni_daq.listDeviceAIMeasurementTypes(device)) |measurement_types| { - rows.appendAssumeCapacity(Row{ - .name = "Measurement types", - .value = try std.fmt.allocPrint(device_info.allocator, "{} types", .{measurement_types.len}) - }); - } else |e| { - log.err("ni_daq.listDeviceAIMeasurementTypes(): {}", .{ e }); - } - - self.showLabelRows(rows.constSlice()); - } -} - -fn showAddFromDeviceWindow(self: *App) !void { - if (rl.isKeyPressed(rl.KeyboardKey.key_escape)) { - self.shown_window = .channels; - return; - } - - const ni_daq = &(self.ni_daq orelse return); - - const device_names = try ni_daq.listDeviceNames(); - - const window = self.ui.newBoxFromString("Device window"); - window.size.x = UI.Size.percent(1, 0); - window.size.y = UI.Size.percent(1, 0); - window.layout_axis = .X; - self.ui.pushParent(window); - defer self.ui.popParent(); - - { - const filters_box = self.ui.newBoxFromString("Filters box"); - filters_box.size.x = UI.Size.percent(0.5, 0); - filters_box.size.y = UI.Size.percent(1, 0); - filters_box.layout_axis = .Y; - self.ui.pushParent(filters_box); - defer self.ui.popParent(); - - const device_name_filter = self.ui.clickableBox("Device name filter"); - const channel_type_filter = self.ui.clickableBox("Channel type filter"); - - if (self.show_device_filter_dropdown) { - const dropdown = self.ui.clickableBox("Device name dropdown"); - dropdown.size.x = UI.Size.percent(1, 1); - dropdown.size.y = UI.Size.childrenSum(1); - dropdown.layout_axis = .Y; - dropdown.background = srcery.xgray2; - self.ui.pushParent(dropdown); - defer self.ui.popParent(); - - dropdown.setFixedPosition( - device_name_filter.persistent.position.add(.{ .x = 0, .y = device_name_filter.persistent.size.y }) - ); - - { - const device_box = self.ui.button(.text, "All"); - device_box.size.x = UI.Size.percent(1, 1); - device_box.size.y = UI.Size.text(0.5, 1); - device_box.flags.insert(.text_left_align); - - if (self.ui.signalFromBox(device_box).clicked()) { - self.device_filter.len = 0; - self.show_device_filter_dropdown = false; - } - } - - for (device_names) |device_name| { - const device_box = self.ui.button(.text, device_name); - device_box.size.x = UI.Size.percent(1, 1); - device_box.size.y = UI.Size.text(0.5, 1); - device_box.flags.insert(.text_left_align); - - const signal = self.ui.signalFromBox(device_box); - if (signal.clicked()) { - self.device_filter = try NIDaq.BoundedDeviceName.fromSlice(device_name); - self.show_device_filter_dropdown = false; - } - } - } - - if (self.show_channel_type_filter_dropdown) { - const dropdown = self.ui.clickableBox("Channel type dropdown"); - dropdown.size.x = UI.Size.percent(1, 1); - dropdown.size.y = UI.Size.childrenSum(1); - dropdown.layout_axis = .Y; - dropdown.background = srcery.xgray2; - self.ui.pushParent(dropdown); - defer self.ui.popParent(); - - dropdown.setFixedPosition( - channel_type_filter.persistent.position.add(.{ .x = 0, .y = channel_type_filter.persistent.size.y }) - ); - - { - const device_box = self.ui.button(.text, "All"); - device_box.size.x = UI.Size.percent(1, 1); - device_box.size.y = UI.Size.text(0.5, 1); - device_box.flags.insert(.text_left_align); - - if (self.ui.signalFromBox(device_box).clicked()) { - self.channel_type_filter = null; - self.show_channel_type_filter_dropdown = false; - } - } - - for (&[_]NIDaq.ChannelType{ NIDaq.ChannelType.analog_input, NIDaq.ChannelType.analog_output }) |channel_type| { - const device_box = self.ui.button(.text, channel_type.name()); - device_box.size.x = UI.Size.percent(1, 1); - device_box.size.y = UI.Size.text(0.5, 1); - device_box.flags.insert(.text_left_align); - - if (self.ui.signalFromBox(device_box).clicked()) { - self.channel_type_filter = channel_type; - self.show_channel_type_filter_dropdown = false; - } - } - } - - { - device_name_filter.size.x = UI.Size.percent(1, 1); - device_name_filter.size.y = UI.Size.pixels(24, 1); - device_name_filter.layout_axis = .X; - self.ui.pushParent(device_name_filter); - defer self.ui.popParent(); - - { - self.ui.pushVerticalAlign(); - defer self.ui.popVerticalAlign(); - _ = self.ui.textureBox(Assets.dropdown_arrow, 1); - } - - if (self.device_filter.len > 0) { - _ = self.ui.label(.text, self.device_filter.constSlice()); - } else { - _ = self.ui.label(.text, "All"); - } - - if (self.ui.signalFromBox(device_name_filter).clicked()) { - self.show_device_filter_dropdown = !self.show_device_filter_dropdown; - } - - self.ui.spacer(.{ .x = UI.Size.percent(1, 0) }); - } - - { - channel_type_filter.size.x = UI.Size.percent(1, 1); - channel_type_filter.size.y = UI.Size.pixels(24, 1); - channel_type_filter.layout_axis = .X; - self.ui.pushParent(channel_type_filter); - defer self.ui.popParent(); - - { - self.ui.pushVerticalAlign(); - defer self.ui.popVerticalAlign(); - _ = self.ui.textureBox(Assets.dropdown_arrow, 1); - } - - if (self.channel_type_filter) |channeL_type| { - _ = self.ui.label(.text, channeL_type.name()); - } else { - _ = self.ui.label(.text, "All"); - } - - if (self.ui.signalFromBox(channel_type_filter).clicked()) { - self.show_channel_type_filter_dropdown = !self.show_channel_type_filter_dropdown; - } - } - } - - var hot_channel: ?[:0]const u8 = self.last_hot_channel; - { - const channels_box = self.ui.pushScrollbar(self.ui.newKeyFromString("Channels list")); - defer self.ui.popScrollbar(); - const channels_box_container = self.ui.getParentOf(channels_box).?; - channels_box.layout_axis = .Y; - //channels_box.size.x = UI.Size.childrenSum(1); - channels_box_container.size.x = UI.Size.percent(1, 0); - - var devices: []const [:0]const u8 = &.{}; - if (self.device_filter.len > 0) { - devices = &.{ - self.device_filter.buffer[0..self.device_filter.len :0] - }; - } else { - devices = try ni_daq.listDeviceNames(); - } - - for (devices) |device| { - var ai_voltage_physical_channels: []const [:0]const u8 = &.{}; - if (try ni_daq.checkDeviceAIMeasurementType(device, .Voltage)) { - ai_voltage_physical_channels = try ni_daq.listDeviceAIPhysicalChannels(device); - } - - var ao_physical_channels: []const [:0]const u8 = &.{}; - if (try ni_daq.checkDeviceAOOutputType(device, .Voltage)) { - ao_physical_channels = try ni_daq.listDeviceAOPhysicalChannels(device); - } - - inline for (.{ ai_voltage_physical_channels, ao_physical_channels }) |channels| { - for (channels) |channel| { - const selected_channels_slice = self.selected_channels.constSlice(); - - if (self.channel_type_filter) |channel_type_filter| { - if (NIDaq.getChannelType(channel) != channel_type_filter) { - continue; - } - } - - const channel_box = self.ui.button(.text, channel); - - if (findChannelIndexByName(selected_channels_slice, channel) != null) { - channel_box.background = srcery.xgray3; - } - - const signal = self.ui.signalFromBox(channel_box); - if (signal.clicked()) { - if (findChannelIndexByName(selected_channels_slice, channel)) |index| { - self.allocator.free(self.selected_channels.swapRemove(index)); + if (source == .device) { + const device_channel = source.device; + if (device_channel.active_task) |task| { + try task.stop(); + device_channel.active_task = null; } else { - self.selected_channels.appendAssumeCapacity(try self.allocator.dupeZ(u8, channel)); - } - } + const channel_name = device_channel.name.buffer[0..device_channel.name.len :0]; + device_channel.active_task = try self.task_pool.launchAIVoltageChannel( + ni_daq, + &device_channel.mutex, + &device_channel.samples, + .{ + .continous = .{ .sample_rate = device_channel.max_sample_rate } + }, + .{ + .min_value = channel_view.default_min_value, + .max_value = channel_view.default_max_value, + .units = device_channel.units, + .channel = channel_name + } + ); - if (signal.hot) { - hot_channel = channel; + channel_view.follow = true; + } } } } } + if (self.started_collecting) { + start_all.setText("Stop"); + } else { + start_all.setText("Start"); + } + } + + if (self.started_collecting) { + for (self.channel_views.slice()) |*_channel_view| { + const channel_view: *ChannelView = _channel_view; + const source = self.getChannelSource(channel_view) orelse continue; + + if (source == .device) { + source.lockSamples(); + defer source.unlockSamples(); + + const sample_rate = source.device.active_task.?.sampling.continous.sample_rate; + const sample_count: f32 = @floatFromInt(source.samples().len); + + channel_view.view_rect.from = 0; + if (sample_count > channel_view.view_rect.to) { + channel_view.view_rect.to = sample_count + @as(f32, @floatCast(sample_rate)) * 10; + } + channel_view.view_cache.invalidate(); + } + } } { - const left_panel = self.ui.newBox(UI.Key.initNil()); - left_panel.layout_axis = .Y; - left_panel.size.y = UI.Size.percent(1, 0); - left_panel.size.x = UI.Size.percent(1, 0); - self.ui.pushParent(left_panel); - defer self.ui.popParent(); + // TODO: + // const scroll_area = self.beginScrollbar(ui.keyFromString("Channels")); + // defer self.endScrollbar(); + // scroll_area.layout_direction = .top_to_bottom; - try self.showChannelInfoPanel(hot_channel); + for (self.channel_views.slice()) |*channel_view| { + try self.showChannelView(channel_view); + } - const add_button = self.ui.button(.text, "Add"); - if (self.ui.signalFromBox(add_button).clicked()) { - for (self.selected_channels.constSlice()) |channel_name| { - try self.appendChannelFromDevice(channel_name); + // TODO: + if (false) { + const add_channel_view = ui.createBox(.{ + .size_x = UI.Sizing.initFixed(.{ .parent_percent = 1 }), + .size_y = UI.Sizing.initFixed(.{ .pixels = 200 }), + .align_x = .center, + .align_y = .center, + .layout_gap = 32 + }); + add_channel_view.beginChildren(); + defer add_channel_view.endChildren(); + + const add_from_file = self.button("Add from file"); + add_from_file.borders = UI.Borders.all(.{ .size = 2, .color = srcery.green }); + if (ui.signal(add_from_file).clicked()) { + if (Platform.openFilePicker(self.allocator)) |filename| { + defer self.allocator.free(filename); + + // TODO: Handle error + self.appendChannelFromFile(filename) catch @panic("Failed to append channel from file"); + } else |err| { + // TODO: Show error message to user; + log.err("Failed to pick file: {}", .{ err }); + } } - self.shown_window = .channels; - for (self.selected_channels.constSlice()) |channel| { - self.allocator.free(channel); + const add_from_device = self.button("Add from device"); + add_from_device.borders = UI.Borders.all(.{ .size = 2, .color = srcery.green }); + if (ui.signal(add_from_device).clicked()) { + } - self.selected_channels.len = 0; - } } - - if (hot_channel != null) { - self.last_hot_channel = hot_channel; - } -} - -fn showToolbar(self: *App) void { - const toolbar = self.ui.newBoxFromString("Toolbar"); - toolbar.background = srcery.black; - toolbar.layout_axis = .X; - toolbar.size = .{ - .x = UI.Size.percent(1, 0), - .y = UI.Size.pixels(32, 1), - }; - toolbar.borders.bottom = .{ - .size = 4, - .color = srcery.hard_black - }; - self.ui.pushParent(toolbar); - defer self.ui.popParent(); - - self.ui.pushStyle(); - defer self.ui.popStyle(); - - self.ui.style.borders.all(.{ - .size = 4, - .color = srcery.hard_black - }); - self.ui.style.background = srcery.xgray2; - - { - const box = self.ui.button(.text, "Start all"); - box.size.y = UI.Size.percent(1, 1); - - const signal = self.ui.signalFromBox(box); - if (signal.clicked()) { - - } - } - - self.ui.spacer(.{ .x = UI.Size.percent(1, 0) }); - - { - const box = self.ui.button(.text, "Help"); - box.size.y = UI.Size.percent(1, 1); - - const signal = self.ui.signalFromBox(box); - if (signal.clicked()) { - - } - } -} - -fn showModalNoLibraryError(self: *App) void { - const modal = self.ui.getParent().?; - - modal.layout_axis = .Y; - modal.size = .{ - .x = UI.Size.pixels(400, 1), - .y = UI.Size.pixels(320, 1), - }; - - self.ui.spacer(.{ .y = UI.Size.percent(1, 0) }); - - const text = self.ui.newBoxFromString("Text"); - text.flags.insert(.text_wrapping); - text.size.x = UI.Size.text(2, 0); - text.size.y = UI.Size.text(1, 1); - text.appendText("PALA, PALA! Aš neradau būtinos bibliotekos ant kompiuterio. Programa vis dar veiks, bet "); - text.appendText("dauguma funkcijų bus paslėptos. Susirask iternete \"NI MAX\" ir instaliuok. Štai nuorada "); - text.appendText("į gidą."); - - { - self.ui.pushHorizontalAlign(); - defer self.ui.popHorizontalAlign(); - - const link = self.ui.newBoxFromString("Link"); - link.flags.insert(.clickable); - link.hot_cursor = .mouse_cursor_pointing_hand; - link.flags.insert(.text_underline); - link.size.x = UI.Size.text(1, 1); - link.size.y = UI.Size.text(1, 1); - link.setText( - .text, - "Nuorada į gidą" - ); - link.text.?.color = srcery.blue; - - const signal = self.ui.signalFromBox(link); - if (signal.clicked()) { - rl.openURL("https://knowledge.ni.com/KnowledgeArticleDetails?id=kA03q000000YGQwCAO&l=en-LT"); - } - if (self.ui.isBoxHot(link)) { - link.text.?.color = srcery.bright_blue; - } - } - - self.ui.spacer(.{ .y = UI.Size.percent(1, 0) }); - - { - self.ui.pushHorizontalAlign(); - defer self.ui.popHorizontalAlign(); - - const btn = self.ui.button(.text, "Supratau"); - btn.background = srcery.green; - if (self.ui.signalFromBox(btn).clicked()) { - self.shown_modal = null; - } - } - - self.ui.spacer(.{ .y = UI.Size.percent(1, 0) }); -} - -fn showModalLibraryVersionError(self: *App) void { - assert(self.shown_modal.? == .library_version_error); - - const installed_version = self.shown_modal.?.library_version_error; - const modal = self.ui.getParent().?; - - modal.layout_axis = .Y; - modal.size = .{ - .x = UI.Size.pixels(400, 1), - .y = UI.Size.pixels(320, 1), - }; - - self.ui.spacer(.{ .y = UI.Size.percent(1, 0) }); - - { - const text = self.ui.newBox(UI.Key.initNil()); - text.flags.insert(.text_wrapping); - text.flags.insert(.text_left_align); - text.size.x = UI.Size.text(2, 0); - text.size.y = UI.Size.text(1, 1); - text.appendText("Ooo ne! Reikalinga biblioteka surasta, bet nesurastos reikalingos funkcijos. "); - text.appendText("Susitikrink, kad turi pakankamai naują versiją NI MAX instaliuota."); - } - - { - const text = self.ui.newBox(UI.Key.initNil()); - text.size.x = UI.Size.text(2, 0); - text.size.y = UI.Size.text(1, 1); - text.setFmtText(.text, "Instaliuota versija: {}", .{installed_version}); - } - - { - const text = self.ui.newBox(UI.Key.initNil()); - text.size.x = UI.Size.text(2, 0); - text.size.y = UI.Size.text(1, 1); - text.setFmtText(.text, "Rekomenduotina versija: {}", .{NIDaq.Api.min_version}); - } - - self.ui.spacer(.{ .y = UI.Size.percent(1, 0) }); - - { - self.ui.pushHorizontalAlign(); - defer self.ui.popHorizontalAlign(); - - const btn = self.ui.button(.text, "Supratau"); - btn.background = srcery.green; - if (self.ui.signalFromBox(btn).clicked()) { - self.shown_modal = null; - } - } - - self.ui.spacer(.{ .y = UI.Size.percent(1, 0) }); -} - -fn showModalLibraryVersionWarning(self: *App) void { - assert(self.shown_modal.? == .library_version_warning); - - const installed_version = self.shown_modal.?.library_version_warning; - const modal = self.ui.getParent().?; - - modal.layout_axis = .Y; - modal.size = .{ - .x = UI.Size.pixels(400, 1), - .y = UI.Size.pixels(320, 1), - }; - - self.ui.spacer(.{ .y = UI.Size.percent(1, 0) }); - - { - const text = self.ui.newBox(UI.Key.initNil()); - text.flags.insert(.text_wrapping); - text.flags.insert(.text_left_align); - text.size.x = UI.Size.text(2, 0); - text.size.y = UI.Size.text(1, 1); - text.appendText("Instaliuota NI MAX versija žemesnė negu rekomenduotina versija. "); - text.appendText("Daug kas turėtų veikti, bet negaliu garantuoti kad viskas veiks. "); - text.appendText("Jeigu susidursi su problemomis kur programa sustoja veikti pabandyk atsinaujinti "); - text.appendText("NI MAX."); - } - - { - const text = self.ui.newBox(UI.Key.initNil()); - text.size.x = UI.Size.text(2, 0); - text.size.y = UI.Size.text(1, 1); - text.setFmtText(.text, "Instaliuota versija: {}", .{installed_version}); - } - - { - const text = self.ui.newBox(UI.Key.initNil()); - text.size.x = UI.Size.text(2, 0); - text.size.y = UI.Size.text(1, 1); - text.setFmtText(.text, "Rekomenduotina versija: {}", .{NIDaq.Api.min_version}); - } - - self.ui.spacer(.{ .y = UI.Size.percent(1, 0) }); - - { - self.ui.pushHorizontalAlign(); - defer self.ui.popHorizontalAlign(); - - const btn = self.ui.button(.text, "Supratau"); - btn.background = srcery.green; - if (self.ui.signalFromBox(btn).clicked()) { - self.shown_modal = null; - } - } - - self.ui.spacer(.{ .y = UI.Size.percent(1, 0) }); -} - -fn showModal(self: *App) void { - assert(self.shown_modal != null); - - switch (self.shown_modal.?) { - .no_library_error => self.showModalNoLibraryError(), - .library_version_error => self.showModalLibraryVersionError(), - .library_version_warning => self.showModalLibraryVersionWarning() - } -} - -fn updateUI(self: *App) !void { - self.ui.begin(); - defer self.ui.end(); - - const root_box = self.ui.getParent().?; - root_box.layout_axis = .Y; - - var maybe_modal_overlay: ?*UI.Box = null; - - if (self.shown_modal != null) { - const modal_overlay = self.ui.newBoxNoAppend(self.ui.newKeyFromString("Modal overlay")); - maybe_modal_overlay = modal_overlay; - modal_overlay.flags.insert(.clickable); - modal_overlay.flags.insert(.scrollable); - modal_overlay.background = rl.Color.black.alpha(0.5); - modal_overlay.setFixedPosition(.{ .x = 0, .y = 0 }); - modal_overlay.size = .{ - .x = UI.Size.percent(1, 0), - .y = UI.Size.percent(1, 0), - }; - - self.ui.pushParent(modal_overlay); - defer self.ui.popParent(); - - const modal = self.ui.pushCenterBox(); - defer self.ui.popCenterBox(); - modal.background = srcery.hard_black; - - self.showModal(); - - _ = self.ui.signalFromBox(modal_overlay); - } - - self.showToolbar(); - - if (self.shown_window == .channels) { - try self.showChannelsWindow(); - } else if (self.shown_window == .add_from_device) { - try self.showAddFromDeviceWindow(); - } - - if (maybe_modal_overlay) |box| { - self.ui.appendBox(box); - } } pub fn tick(self: *App) !void { - for (self.channel_views.slice()) |*_view| { - const view: *ChannelView = _view; - const source = self.getChannelSource(view) orelse continue; - if (source == .device) { - if (view.follow) { - source.lockSamples(); - defer source.unlockSamples(); - - const sample_count: f32 = @floatFromInt(source.samples().len); - const view_size = view.view_rect.to - view.view_rect.from; - view.view_rect.from = sample_count - view_size; - view.view_rect.to = sample_count; - } - - } + if (rl.isKeyPressed(.key_escape)) { + self.should_close = true; } rl.clearBackground(srcery.black); - if (rl.isKeyPressed(rl.KeyboardKey.key_f3)) { - Platform.toggleConsoleWindow(); + var ui = &self.ui; + { + ui.begin(); + defer ui.end(); + + try self.showWindowChannels(); } - if (rl.isFileDropped()) { - const file_list = rl.loadDroppedFiles(); - defer rl.unloadDroppedFiles(file_list); - - for (file_list.paths[0..file_list.count]) |path| { - const path_len = std.mem.indexOfSentinel(u8, 0, path); - try self.appendChannelFromFile(path[0..path_len]); - } - } - - // On the first frame or when the window resizes, render the UI twice. - // So that on the second pass widgets that depend on sizes from other widgets have "settled" - if (self.ui.frame_index == 0 or rl.isWindowResized()) { - try self.updateUI(); - } - - try self.updateUI(); - self.ui.draw(); + ui.draw(); } \ No newline at end of file diff --git a/src/assets.zig b/src/assets.zig index cddbeea..f84829b 100644 --- a/src/assets.zig +++ b/src/assets.zig @@ -5,15 +5,38 @@ const FontFace = @import("./font-face.zig"); const Aseprite = @import("cute_aseprite"); const assert = std.debug.assert; +const log = std.log.scoped(.assets); -pub const FontId = enum { - text +pub const FontVariant = enum { + regular, + regular_italic, + bold, + bold_italic, + thin, + thin_italic }; -var loaded_fonts: std.BoundedArray(rl.Font, 32) = .{}; +pub const FontId = struct { + variant: FontVariant, + size: f32, -const FontArray = std.EnumArray(FontId, FontFace); -var fonts: FontArray = FontArray.initUndefined(); + pub fn eql(self: FontId, other: FontId) bool { + return self.variant == other.variant and self.size == other.size; + } +}; + +const LoadedFont = struct { + id: FontId, + font: rl.Font, + generation: u64, +}; + +const FontTTFArray = std.EnumArray(FontVariant, []const u8); +var font_ttfs: FontTTFArray = undefined; + +const LoadedFontsArray = std.BoundedArray(LoadedFont, std.meta.fields(FontVariant).len * 8); +var current_font_generation: u64 = 0; +var loaded_fonts: LoadedFontsArray = .{ }; pub var grab_texture: struct { normal: rl.Texture2D, @@ -24,16 +47,47 @@ pub var grab_texture: struct { pub var dropdown_arrow: rl.Texture2D = undefined; pub fn font(font_id: FontId) FontFace { - return fonts.get(font_id); + var found_font: ?LoadedFont = null; + for (loaded_fonts.slice()) |*loaded_font| { + if (font_id.eql(loaded_font.id)) { + loaded_font.generation = current_font_generation; + found_font = loaded_font.*; + } + } + + if (found_font == null) { + const raylib_font = loadFont( + font_ttfs.get(font_id.variant), + @intFromFloat(@round(font_id.size)) + ) catch rl.getFontDefault(); + + found_font = LoadedFont{ + .id = font_id, + .font = raylib_font, + .generation = current_font_generation + }; + + loaded_fonts.append(found_font.?) catch { + log.warn("Failed to append font, font cache is full", .{}); + }; + } + + return FontFace{ + .line_height = 1.2, + .font = found_font.?.font, + }; } pub fn init(allocator: std.mem.Allocator) !void { - const roboto_regular = @embedFile("./assets/fonts/roboto/Roboto-Regular.ttf"); + font_ttfs = FontTTFArray.init(.{ + .regular = @embedFile("./assets/fonts/roboto/Roboto-Regular.ttf"), + .regular_italic = @embedFile("./assets/fonts/roboto/Roboto-Italic.ttf"), - const default_font = try loadFont(roboto_regular, 16); + .bold = @embedFile("./assets/fonts/roboto/Roboto-Bold.ttf"), + .bold_italic = @embedFile("./assets/fonts/roboto/Roboto-BoldItalic.ttf"), - fonts = FontArray.init(.{ - .text = FontFace{ .font = default_font, .line_height = 1.2 } + .thin = @embedFile("./assets/fonts/roboto/Roboto-Thin.ttf"), + .thin_italic = @embedFile("./assets/fonts/roboto/Roboto-ThinItalic.ttf") }); { @@ -86,22 +140,36 @@ fn loadFont(ttf_data: []const u8, font_size: u32) !rl.Font { codepoints.appendAssumeCapacity(codepoint); } - const loaded_font = rl.loadFontFromMemory(".ttf", ttf_data, @intCast(font_size), codepoints.slice()); - if (!loaded_font.isReady()) { + const raylib_font = rl.loadFontFromMemory(".ttf", ttf_data, @intCast(font_size), codepoints.slice()); + if (!raylib_font.isReady()) { return error.LoadFontFromMemory; } - loaded_fonts.appendAssumeCapacity(loaded_font); + return raylib_font; +} - return loaded_font; +pub fn deinitUnusedFonts() void { + var i: usize = 0; + while (i < loaded_fonts.len) { + const loaded_font = loaded_fonts.buffer[i]; + if (loaded_font.generation < current_font_generation) { + loaded_font.font.unload(); + _ = loaded_fonts.swapRemove(i); + } else { + i += 1; + } + } + + current_font_generation += 1; } pub fn deinit(allocator: std.mem.Allocator) void { _ = allocator; for (loaded_fonts.slice()) |loaded_font| { - loaded_font.unload(); + loaded_font.font.unload(); } + loaded_fonts.len = 0; grab_texture.active.unload(); grab_texture.hot.unload(); diff --git a/src/assets/fonts/roboto/Roboto-Bold.ttf b/src/assets/fonts/roboto/Roboto-Bold.ttf new file mode 100644 index 0000000..e64db79 Binary files /dev/null and b/src/assets/fonts/roboto/Roboto-Bold.ttf differ diff --git a/src/assets/fonts/roboto/Roboto-BoldItalic.ttf b/src/assets/fonts/roboto/Roboto-BoldItalic.ttf new file mode 100644 index 0000000..5e39ae9 Binary files /dev/null and b/src/assets/fonts/roboto/Roboto-BoldItalic.ttf differ diff --git a/src/assets/fonts/roboto/Roboto-Italic.ttf b/src/assets/fonts/roboto/Roboto-Italic.ttf new file mode 100644 index 0000000..65498ee Binary files /dev/null and b/src/assets/fonts/roboto/Roboto-Italic.ttf differ diff --git a/src/assets/fonts/roboto/Roboto-Thin.ttf b/src/assets/fonts/roboto/Roboto-Thin.ttf new file mode 100644 index 0000000..ab68508 Binary files /dev/null and b/src/assets/fonts/roboto/Roboto-Thin.ttf differ diff --git a/src/assets/fonts/roboto/Roboto-ThinItalic.ttf b/src/assets/fonts/roboto/Roboto-ThinItalic.ttf new file mode 100644 index 0000000..b2c3933 Binary files /dev/null and b/src/assets/fonts/roboto/Roboto-ThinItalic.ttf differ diff --git a/src/font-face.zig b/src/font-face.zig index fd530a7..a96d61e 100644 --- a/src/font-face.zig +++ b/src/font-face.zig @@ -60,7 +60,6 @@ pub fn drawTextEx(self: @This(), text: []const u8, position: rl.Vector2, tint: r var iter = std.unicode.Utf8Iterator{ .bytes = text, .i = 0 }; while (iter.nextCodepoint()) |codepoint| { - if (codepoint == '\n') { offset.x = 0; offset.y += font_size * self.line_height; diff --git a/src/grayscale.fs b/src/grayscale.fs deleted file mode 100644 index 9beb1dc..0000000 --- a/src/grayscale.fs +++ /dev/null @@ -1,19 +0,0 @@ -#version 330 - -// Input vertex attributes (from vertex shader) -in vec2 fragTexCoord; -in vec4 fragColor; - -// Input uniform values -uniform sampler2D texture0; -uniform vec4 colDiffuse; - -// Output fragment color -out vec4 finalColor; - -void main() -{ - vec4 texelColor = texture(texture0, fragTexCoord)*colDiffuse*fragColor; - float luminance = dot(texelColor.rgb, vec3(0.2126, 0.7152, 0.0722)); - gl_FragColor = vec4(luminance, luminance, luminance, texelColor.a); -} \ No newline at end of file diff --git a/src/main.zig b/src/main.zig index 2b1f607..d2a7418 100644 --- a/src/main.zig +++ b/src/main.zig @@ -3,12 +3,13 @@ const rl = @import("raylib"); const builtin = @import("builtin"); const Application = @import("./app.zig"); const Assets = @import("./assets.zig"); -const Profiler = @import("./profiler.zig"); +const Profiler = @import("./my-profiler.zig"); const Platform = @import("./platform.zig"); const raylib_h = @cImport({ @cInclude("stdio.h"); @cInclude("raylib.h"); }); +const P = @import("profiler"); const log = std.log; const profiler_enabled = builtin.mode == .Debug; @@ -65,6 +66,12 @@ fn raylibTraceLogCallback(logType: c_int, text: [*c]const u8, args: raylib_h.va_ } pub fn main() !void { + try P.init(.{}); + defer { + P.dump("profile.json") catch |err| std.log.err("profile dump failed: {}", .{err}); + P.deinit(); + } + Platform.init(); // TODO: Setup logging to a file @@ -156,8 +163,8 @@ pub fn main() !void { defer app.deinit(); if (builtin.mode == .Debug) { - // try app.appendChannelFromDevice("Dev1/ai0"); - try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_I.bin"); + try app.appendChannelFromDevice("Dev1/ai0"); + // try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_I.bin"); // try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_IjStim.bin"); // try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_IjStim.bin"); // try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_IjStim.bin"); @@ -168,31 +175,52 @@ pub fn main() !void { var profiler_shown = false; if (profiler_enabled) { - const font_face = Assets.font(.text); - profiler = try Profiler.init(allocator, 10 * target_fps, @divFloor(std.time.ns_per_s, target_fps), font_face); + profiler = try Profiler.init( + allocator, + 10 * target_fps, + @divFloor(std.time.ns_per_s, target_fps), + Assets.FontId{ .variant = .regular, .size = 16 } + ); } rl.setExitKey(rl.KeyboardKey.key_null); + var last_font_cleanup_at = rl.getTime(); while (!rl.windowShouldClose() and !app.should_close) { rl.beginDrawing(); - defer rl.endDrawing(); - if (profiler) |*p| { - p.start(); + { + const zone = P.begin(@src(), "tick"); + defer zone.end(); + + if (profiler) |*p| { + p.start(); + } + + try app.tick(); + + if (profiler) |*p| { + p.stop(); + if (rl.isKeyPressed(.key_p) and rl.isKeyDown(.key_left_control)) { + profiler_shown = !profiler_shown; + } + + if (profiler_shown) { + try p.showResults(); + } + } + + const now = rl.getTime(); + if (now - last_font_cleanup_at > 10) { + Assets.deinitUnusedFonts(); + last_font_cleanup_at = now; + } } - try app.tick(); - - if (profiler) |*p| { - p.stop(); - if (rl.isKeyPressed(.key_p) and rl.isKeyDown(.key_left_control)) { - profiler_shown = !profiler_shown; - } - - if (profiler_shown) { - try p.showResults(); - } + { + const zone = P.begin(@src(), "end draw"); + defer zone.end(); + rl.endDrawing(); } } } diff --git a/src/profiler.zig b/src/my-profiler.zig similarity index 91% rename from src/profiler.zig rename to src/my-profiler.zig index 9288b94..a961687 100644 --- a/src/profiler.zig +++ b/src/my-profiler.zig @@ -1,9 +1,10 @@ const std = @import("std"); const rl = @import("raylib"); +const Assets = @import("./assets.zig"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; +const FontId = Assets.FontId; -const FontFace = @import("./font-face.zig"); const srcery = @import("./srcery.zig"); const rect_utils = @import("./rect-utils.zig"); @@ -14,9 +15,9 @@ history_head: usize, started_at: i128, ns_budget: u128, -font_face: FontFace, +font: FontId, -pub fn init(allocator: Allocator, data_points: usize, ns_budget: u128, font_face: FontFace) !@This() { +pub fn init(allocator: Allocator, data_points: usize, ns_budget: u128, font: FontId) !@This() { return @This(){ .allocator = allocator, .history = try allocator.alloc(u128, data_points), @@ -24,7 +25,7 @@ pub fn init(allocator: Allocator, data_points: usize, ns_budget: u128, font_face .history_head = 0, .started_at = 0, .ns_budget = ns_budget, - .font_face = font_face, + .font = font, }; } @@ -90,7 +91,8 @@ pub fn showResults(self: *const @This()) !void { var layout_offset: f32 = 0; const allocator = self.allocator; - const font_size = self.font_face.getSize(); + const font_face = Assets.font(self.font); + const font_size = font_face.getSize(); const labels = .{ .{ "Min", @as(f32, @floatFromInt(min_time_taken)) }, @@ -104,7 +106,7 @@ pub fn showResults(self: *const @This()) !void { const min_time_str = try std.fmt.allocPrintZ(allocator, "{s}: {d:10.0}us ({d:.3}%)", .{ label_name, time_taken / std.time.ns_per_us, time_taken / ns_budget * 100 }); defer allocator.free(min_time_str); - self.font_face.drawText(min_time_str, .{ .x = content_rect.x, .y = content_rect.y + layout_offset }, color); + font_face.drawText(min_time_str, .{ .x = content_rect.x, .y = content_rect.y + layout_offset }, color); layout_offset += font_size; } } \ No newline at end of file diff --git a/src/ui.zig b/src/ui.zig index 2fdc62a..c72df6c 100644 --- a/src/ui.zig +++ b/src/ui.zig @@ -1,126 +1,25 @@ const std = @import("std"); const rl = @import("raylib"); +const srcery = @import("./srcery.zig"); const Assets = @import("./assets.zig"); const rect_utils = @import("./rect-utils.zig"); const utils = @import("./utils.zig"); -const srcery = @import("./srcery.zig"); -const FontFace = @import("./font-face.zig"); const builtin = @import("builtin"); +const P = @import("profiler"); -const log = std.log.scoped(.ui); const assert = std.debug.assert; pub const Vec2 = rl.Vector2; pub const Rect = rl.Rectangle; const clamp = std.math.clamp; +const Vec2Zero = Vec2{ .x = 0, .y = 0 }; + const UI = @This(); -const debug = false; -const max_boxes = 5120; +const max_boxes = 512; 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 }); - } - } -}; - -pub 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, - texture: 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 texture(scale: f32, strictness: f32) Size { - return Size{ - .kind = .{ .texture = scale }, - .strictness = strictness - }; - } - - pub fn childrenSum(strictness: f32) Size { - return Size{ - .kind = .children_sum, - .strictness = strictness - }; - } -}; - -pub const Vec2Size = struct { - x: Size = Size.pixels(0, 1), - y: Size = Size.pixels(0, 1), - - 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 - }; - } -}; +const draw_debug = false; //builtin.mode == .Debug; +const default_font = Assets.FontId{ .variant = .regular, .size = 16 }; pub const Key = struct { hash: u64 = 0, @@ -162,7 +61,7 @@ pub const Key = struct { _ = fmt; _ = options; - try writer.print("Key{{ 0x{x} }}", .{ self.hash }); + try writer.print("{s}{{ 0x{x} }}", .{ @typeName(Key), self.hash }); } }; @@ -194,10 +93,12 @@ pub const Signal = struct { scrolled }; - flags: std.EnumSet(Flag) = .{}, - drag: Vec2 = .{ .x = 0, .y = 0 }, - scroll: Vec2 = .{ .x = 0, .y = 0 }, - relative_mouse: Vec2 = .{ .x = 0, .y = 0 }, + pub const Flags = std.EnumSet(Flag); + + flags: Flags = .{}, + drag: Vec2 = Vec2Zero, + scroll: Vec2 = Vec2Zero, + relative_mouse: Vec2 = Vec2Zero, hot: bool = false, active: bool = false, shift_modifier: bool = false, @@ -255,221 +156,473 @@ pub const Signal = struct { } }; -const BoxIndex = std.math.IntFittingRange(0, max_boxes); +pub const Axis = enum { + X, Y, -pub const Style = struct { - borders: Box.Borders = .{}, - rounded: bool = false, - background: ?rl.Color = null, + pub fn flip(self: Axis) Axis { + return switch (self) { + .X => .Y, + .Y => .X + }; + } }; +pub const Unit = union(enum) { + pixels: f32, + parent_percent: f32, + text, + font_size: f32, + + pub fn initPixels(pixels: f32) Unit { + return Unit{ + .pixels = pixels + }; + } + + pub fn initParentPct(percent: f32) Unit { + return Unit{ + .parent_percent = percent + }; + } + + fn get(self: Unit, box: *const Box, axis: Axis) f32 { + switch (self) { + .pixels => |pixels| { + return pixels; + }, + .parent_percent => |pct| { + const parent_box = box.parent() orelse return 0; + return parent_box.availableChildrenSize(axis) * pct; + }, + .text => { + const text = box.text orelse return 0; + const font_face = Assets.font(box.font); + var text_size = font_face.measureText(text); + return vec2ByAxis(&text_size, axis).*; + }, + .font_size => |count| { + return box.font.size * count; + } + } + } +}; + +pub const Sizing = union(enum) { + fixed: Unit, + grow: struct { min: Unit, max: Unit }, + shrink: struct { min: Unit, max: Unit }, + fit_children, + + pub fn initFitChildren() Sizing { + return Sizing{ + .fit_children = {} + }; + } + + pub fn initFixed(size: Unit) Sizing { + return Sizing{ + .fixed = size + }; + } + + pub fn initGrowFull() Sizing { + return initGrow( + .{ .pixels = 0 }, + .{ .parent_percent = 1 } + ); + } + + pub fn initGrowUpTo(size: Unit) Sizing { + return initGrow( + .{ .pixels = 0 }, + size + ); + } + + pub fn initGrowFrom(size: Unit) Sizing { + return initGrow( + size, + .{ .parent_percent = 1 } + ); + } + + pub fn initGrow(min: Unit, max: Unit) Sizing { + return Sizing{ + .grow = .{ + .min = min, + .max = max + } + }; + } + + pub fn initShrinkFull() Sizing { + return initShrink( + .{ .pixels = 0 }, + .{ .parent_percent = 1 } + ); + } + + pub fn initShrink(min: Unit, max: Unit) Sizing { + return Sizing{ + .shrink = .{ + .min = min, + .max = max + } + }; + } + + pub fn dependsOnParent(self: Sizing) bool { + return switch (self) { + .fixed => |fixed| fixed == .parent_percent, + .grow => |range| range.min == .parent_percent or range.max == .parent_percent, + .shrink => |range| range.min == .parent_percent or range.max == .parent_percent, + .fit_children => false + }; + } +}; + +pub const Sizing2 = struct { + x: Sizing, + y: Sizing, + + pub fn getAxis(self: Sizing2, axis: Axis) Sizing { + return switch (axis) { + .X => self.x, + .Y => self.y, + }; + } + + pub fn getAxisPtr(self: *Sizing2, axis: Axis) *Sizing { + return switch (axis) { + .X => &self.x, + .Y => &self.y, + }; + } +}; + +pub const LayoutDirection = enum { + left_to_right, + top_to_bottom, + // TODO: right_to_left + // TODO: bottom_to_top + + pub fn isAxis(self: LayoutDirection, axis: Axis) bool { + return switch (self) { + .left_to_right => (axis == .X), + .top_to_bottom => (axis == .Y) + }; + } +}; + +// TODO: Use `Size` instead of f32 in `Padding` +// Not sure if it is worth implementing this now. This will really complicate layout because of .parent_percent +pub const Padding = struct { + top : f32 = 0, + bottom: f32 = 0, + left : f32 = 0, + right : f32 = 0, + + pub fn all(size: f32) Padding { + return Padding{ + .left = size, + .right = size, + .top = size, + .bottom = size + }; + } + + pub fn vertical(size: f32) Padding { + return Padding{ + .top = size, + .bottom = size + }; + } + + pub fn horizontal(size: f32) Padding { + return Padding{ + .left = size, + .right = size + }; + } + + pub fn byAxis(self: Padding, axis: Axis) f32 { + return switch (axis) { + .X => self.left + self.right, + .Y => self.top + self.bottom + }; + } +}; + +pub const Alignment = enum { + start, + center, + end, + + pub fn getCoefficient(self: Alignment) f32 { + return switch (self) { + .start => 0, + .center => 0.5, + .end => 1 + }; + } +}; + +pub const Alignment2 = struct { + x: Alignment, + y: Alignment, + + pub fn getAxis(self: Alignment2, axis: Axis) Alignment { + return switch (axis) { + .X => self.x, + .Y => self.y, + }; + } +}; + +pub const Border = struct { + size: f32 = 0, + color: rl.Color = rl.Color.magenta, +}; + +pub const Borders = struct { + left: Border = .{}, + right: Border = .{}, + top: Border = .{}, + bottom: Border = .{}, + + pub fn all(border: Border) Borders { + return Borders{ + .left = border, + .right = border, + .top = border, + .bottom = border, + }; + } + + pub fn vertical(border: Border) Borders { + return Borders{ + .top = border, + .bottom = border, + }; + } + + pub fn horizontal(border: Border) Borders { + return Borders{ + .left = border, + .right = border, + }; + } +}; + +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 }, - children_size: Vec2 = .{ .x = 0, .y = 0 }, - + position: Vec2 = Vec2Zero, + size: Vec2 = Vec2Zero, sroll_offset: f32 = 0, hot: f32 = 0, active: f32 = 0, }; pub const Flag = enum { + wrap_text, + float_x, + float_y, clickable, - - highlight_hot, - highlight_active, - - draggable, scrollable, - - fixed_x, - fixed_y, - fixed_width, - fixed_height, - - skip_draw, - - text_underline, - text_wrapping, - text_left_align + draggable, + draw_hot, + draw_active, + clip_view }; pub const Flags = std.EnumSet(Flag); - pub const Border = struct { - size: f32 = 0, - color: rl.Color = rl.Color.magenta, - }; - - pub const Borders = struct { - left: Border = .{}, - right: Border = .{}, - top: Border = .{}, - bottom: Border = .{}, - - pub fn all(self: *Borders, border: Border) void { - self.left = border; - self.right = border; - self.top = border; - self.bottom = border; - } - }; + const max_wrapped_lines = 64; + ui: *UI, allocator: std.mem.Allocator, - key: Key, - size: Vec2Size = Vec2Size.zero(), - flags: Flags = .{}, - fixed_rect: Rect = .{ .x = 0, .y = 0, .width = 0, .height = 0 }, - background: ?rl.Color = null, - rounded: bool = false, - layout_axis: Axis = .X, - layout_gap: f32 = 0, - last_used_frame: u64 = 0, - text: ?struct { - content: []u8, - font: Assets.FontId, - color: rl.Color = srcery.bright_white - } = null, + flags: Flags, + background: ?rl.Color, + layout_direction: LayoutDirection, + // TODO: Use `Size` instead of f32 in `layout_gap`. Not sure if this is needed at the moment. + layout_gap: f32, + persistent: Persistent, + alignment: Alignment2, + size: Sizing2, + padding: Padding, + font: Assets.FontId, + text_color: rl.Color, + borders: Borders, + text: ?[]u8, + hot_cursor: ?rl.MouseCursor, + active_cursor: ?rl.MouseCursor, + view_offset: Vec2 = Vec2Zero, texture: ?rl.Texture2D = null, - view_offset: Vec2 = .{ .x = 0, .y = 0 }, - borders: Borders = .{}, - hot_cursor: ?rl.MouseCursor = null, - active_cursor: ?rl.MouseCursor = null, - persistent: Persistent = .{}, + // Variables that you probably shouldn't be touching + last_used_frame: u64 = 0, + key: Key = Key.initNil(), + text_lines: std.BoundedArray([]u8, max_wrapped_lines) = .{ }, // Fields for maintaining tree data structure + tree: struct { + // 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, + }, - // 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 beginChildren(self: *Box) void { + self.ui.parent_stack.appendAssumeCapacity(self.tree.index); + } - pub fn computedRect(self: *Box) Rect { + pub fn endChildren(self: *Box) void { + const index = self.ui.parent_stack.pop(); + assert(self.tree.index == index); + } + + pub fn iterChildren(self: *const Box) BoxChildIterator { + return BoxChildIterator{ + .boxes = self.ui.boxes.slice(), + .current_child = self.tree.first_child_index + }; + } + + pub fn iterParents(self: *const Box) BoxParentIterator { + return BoxParentIterator{ + .boxes = self.ui.boxes.slice(), + .current_parent = self.tree.parent_index + }; + } + + pub fn rect(self: *const Box) Rect { return Rect{ .x = self.persistent.position.x, .y = self.persistent.position.y, .width = self.persistent.size.x, - .height = self.persistent.size.y + .height = self.persistent.size.y, }; } - pub fn setText(self: *Box, font: Assets.FontId, text: []const u8) void { - self.text = .{ - .content = self.allocator.dupe(u8, text) catch return, - .font = font - }; - } - - pub fn appendText(self: *Box, text: []const u8) void { - if (self.text == null) { - self.text = .{ - .content = self.allocator.alloc(u8, 0) catch return, - .font = .text - }; - } - - const new_content = std.mem.concat(self.allocator, u8, &.{ self.text.?.content, text }) catch return; - self.text.?.content = new_content; - } - - pub fn setFmtText(self: *Box, font: Assets.FontId, comptime fmt: []const u8, args: anytype) void { - self.text = .{ - .content = std.fmt.allocPrint(self.allocator, fmt, args) catch return, - .font = font - }; - } - - pub fn setFixedX(self: *Box, x: f32) void { - self.flags.insert(.fixed_x); - self.fixed_rect.x = x; - } - - pub fn setFixedY(self: *Box, y: f32) void { - self.flags.insert(.fixed_y); - self.fixed_rect.y = y; - } - - pub fn setFixedWidth(self: *Box, width: f32) void { - self.flags.insert(.fixed_width); - self.fixed_rect.width = width; - } - - pub fn setFixedHeight(self: *Box, height: f32) void { - self.flags.insert(.fixed_height); - self.fixed_rect.height = height; - } - - pub fn setFixedPosition(self: *Box, position: Vec2) void { - self.setFixedX(position.x); - self.setFixedY(position.y); - } - - pub fn setFixedSize(self: *Box, size: Vec2) void { - self.setFixedWidth(size.x); - self.setFixedHeight(size.y); - } - - pub fn setFixedRect(self: *Box, rect: Rect) void { - self.setFixedX(rect.x); - self.setFixedY(rect.y); - self.setFixedWidth(rect.width); - self.setFixedHeight(rect.height); - } - - fn getFixedPositionAxis(self: *Box, axis: Axis) ?f32 { - if (!self.isPositionFixed(axis)) { + pub fn parent(self: *const Box) ?*Box { + if (self.tree.parent_index) |parent_index| { + return self.ui.getBoxByIndex(parent_index); + } else { return null; } - - return switch (axis) { - .X => self.fixed_rect.x, - .Y => self.fixed_rect.y - }; } - fn getFixedSizeAxis(self: *Box, axis: Axis) ?f32 { - if (!self.isSizeFixed(axis)) { - return null; + pub fn setText(self: *Box, text: []const u8) void { + self.text = self.allocator.dupe(u8, text) catch return; + self.text_lines.len = 0; + } + + pub fn setFmtText(self: *Box, comptime fmt: []const u8, args: anytype) void { + self.text = std.fmt.allocPrint(self.allocator, fmt, args) catch return; + self.text_lines.len = 0; + } + + pub fn setFloatX(self: *Box, x: f32) void { + self.persistent.position.x = x; + self.flags.insert(.float_x); + } + + pub fn setFloatY(self: *Box, y: f32) void { + self.persistent.position.y = y; + self.flags.insert(.float_y); + } + + pub fn setFloatRect(self: *Box, float_rect: Rect) void { + self.setFloatX(float_rect.x); + self.setFloatY(float_rect.y); + self.size.x = .{ .fixed = .{ .pixels = float_rect.width } }; + self.size.y = .{ .fixed = .{ .pixels = float_rect.height } }; + } + + pub fn wrapText(self: *Box, width: f32) void { + self.text_lines.len = 0; + + const text = self.text orelse return; + const font_face = Assets.font(self.font); + + var line_start: usize = 0; + var prev_word_end: usize = 0; + var word_iter = std.mem.tokenizeScalar(u8, text, ' '); + while (true) { + _ = word_iter.peek(); + const word_start = word_iter.index; + if (word_iter.next() == null) break; + const word_end = word_iter.index; + + const line = text[line_start..word_end]; + const line_size = font_face.measureText(line); + if (line_size.x > width) { + const prev_line = text[line_start..prev_word_end]; + self.text_lines.append(prev_line) catch return; + + line_start = word_start; + } + + prev_word_end = word_end; } + if (line_start < text.len) { + self.text_lines.append(text[line_start..prev_word_end]) catch return; + } + } + + fn isFloating(self: *const Box, axis: Axis) bool { return switch (axis) { - .X => self.fixed_rect.width, - .Y => self.fixed_rect.height + .X => self.flags.contains(.float_x), + .Y => self.flags.contains(.float_y), }; } - fn isPositionFixed(self: *Box, axis: Axis) bool { - const flag = switch (axis) { - .X => Flag.fixed_x, - .Y => Flag.fixed_y + fn overflowEnabled(self: *const Box, axis: Axis) bool { + return switch (axis) { + .X => self.flags.contains(.overflow_x), + .Y => self.flags.contains(.overflow_y), }; - - return self.flags.contains(flag); } - fn isSizeFixed(self: *Box, axis: Axis) bool { - const flag = switch (axis) { - .X => Flag.fixed_width, - .Y => Flag.fixed_height - }; - - return self.flags.contains(flag); - } - - pub fn hasClipping(self: *Box) bool { - return self.view_offset.equals(Vec2.zero()) == 0; + fn availableChildrenSize(self: *Box, axis: Axis) f32 { + const size = vec2ByAxis(&self.persistent.size, axis).*; + return @max(size - self.padding.byAxis(axis), 0); } }; +pub const BoxOptions = struct { + key: ?Key = null, + size_x: ?Sizing = null, + size_y: ?Sizing = null, + background: ?rl.Color = null, + layout_direction: ?LayoutDirection = null, + layout_gap: ?f32 = null, + padding: ?Padding = null, + align_x: ?Alignment = null, + align_y: ?Alignment = null, + text: ?[]const u8 = null, + font: ?Assets.FontId = null, + text_color: ?rl.Color = null, + flags: ?[]const Box.Flag = null, + position_x: ?f32 = null, + position_y: ?f32 = null, + borders: ?Borders = null, + hot_cursor: ?rl.MouseCursor = null, + active_cursor: ?rl.MouseCursor = null, + view_offset: ?Vec2 = null +}; + +pub const root_box_key = Key.initString(0, "$root$"); + const BoxChildIterator = struct { current_child: ?BoxIndex, boxes: []Box, @@ -477,7 +630,7 @@ const BoxChildIterator = struct { 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; + self.current_child = child.tree.next_sibling_index; return child; } else { return null; @@ -492,7 +645,7 @@ const BoxParentIterator = struct { pub fn next(self: *BoxParentIterator) ?*Box { if (self.current_parent) |parent_index| { const parent = &self.boxes[parent_index]; - self.current_parent = parent.parent_index; + self.current_parent = parent.tree.parent_index; return parent; } else { return null; @@ -500,94 +653,80 @@ const BoxParentIterator = struct { } }; -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) = .{}, -style_stack: std.BoundedArray(Style, 16) = .{}, -style: Style = .{}, - frame_index: u64 = 0, + +// Retained structures. Used for tracking changes between frames hot_box_key: ?Key = null, active_box_keys: std.EnumMap(rl.MouseButton, Key) = .{}, +// Per frames structures +font_stack: std.BoundedArray(Assets.FontId, 16) = .{}, +boxes: std.BoundedArray(Box, max_boxes) = .{}, +parent_stack: std.BoundedArray(BoxIndex, max_boxes) = .{}, 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 }, - dt: f32 = 0, -show_grayscale: bool = false, -grayscale_shader: rl.Shader, - pub fn init(allocator: std.mem.Allocator) UI { - const shader = rl.loadShaderFromMemory(null, @embedFile("./grayscale.fs")); - assert(shader.id != 0); - return UI{ - .arenas = .{ std.heap.ArenaAllocator.init(allocator), std.heap.ArenaAllocator.init(allocator) }, - .mouse = rl.getMousePosition(), - .grayscale_shader = shader + .arenas = .{ + std.heap.ArenaAllocator.init(allocator), + std.heap.ArenaAllocator.init(allocator) + }, }; } pub fn deinit(self: *UI) void { self.arenas[0].deinit(); self.arenas[1].deinit(); - rl.unloadShader(self.grayscale_shader); +} + +pub fn frame_arena(self: *UI) *std.heap.ArenaAllocator { + return &self.arenas[@mod(self.frame_index, 2)]; +} + +pub fn frame_allocator(self: *UI) std.mem.Allocator { + return self.frame_arena().allocator(); } pub fn begin(self: *UI) void { - const window_width = rl.getScreenWidth(); - const window_height = rl.getScreenHeight(); + const zone = P.begin(@src(), "UI begin()"); + defer zone.end(); - const mouse = rl.getMousePosition(); - self.mouse_delta = mouse.subtract(self.mouse); - self.dt = rl.getFrameTime(); - - // TODO: Maybe add a flag to enable this for active box - // const active_box_flags = self.getActiveBoxFlags(); - // if (active_box_flags.contains(.draggable_x)) { - // rl.setMousePosition( - // @mod(rl.getMouseX(), window_width), - // rl.getMouseY() - // ); - // } - // if (active_box_flags.contains(.draggable_y)) { - // rl.setMousePosition( - // rl.getMouseX(), - // @mod(rl.getMouseY(), window_height) - // ); - // } + self.font_stack.len = 0; + self.parent_stack.len = 0; + self.events.len = 0; + // Remove boxes which haven't been used in the last frame { + const zone2 = P.begin(@src(), "UI remove boxes"); + defer zone2.end(); + var i: usize = 0; while (i < self.boxes.len) { const box = &self.boxes.buffer[i]; if (box.last_used_frame != self.frame_index) { - _ = self.boxes.swapRemove(i); + if (self.boxes.len > 0) { + self.boxes.buffer[i] = self.boxes.buffer[self.boxes.len - 1]; + } + self.boxes.len -= 1; } else { i += 1; } } } - self.frame_index += 1; - _ = self.frameArena().reset(.retain_capacity); - self.parent_index_stack.len = 0; - self.style_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)); + const mouse = rl.getMousePosition(); + self.mouse_delta = mouse.subtract(self.mouse); + self.dt = rl.getFrameTime(); + self.mouse = mouse; self.mouse_buttons = .{}; inline for (std.meta.fields(rl.MouseButton)) |mouse_button_field| { const mouse_button: rl.MouseButton = @enumFromInt(mouse_button_field.value); @@ -608,42 +747,27 @@ pub fn begin(self: *UI) void { self.events.appendAssumeCapacity(Event{ .mouse_scroll = mouse_wheel }); } - 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.frame_index += 1; + _ = self.frame_arena().reset(.retain_capacity); - self.pushParent(root_box); + self.pushFont(default_font); + + const root_box = self.createBox(.{ + .key = root_box_key, + .size_x = .{ .fixed = .{ .pixels = @floatFromInt(rl.getScreenWidth()) } }, + .size_y = .{ .fixed = .{ .pixels = @floatFromInt(rl.getScreenHeight()) } } + }); + root_box.beginChildren(); } pub fn end(self: *UI) void { - self.popParent(); - assert(self.parent_index_stack.len == 0); + const zone2 = P.begin(@src(), "UI end()"); + defer zone2.end(); - // Mouse cursor - { - var cursor: ?rl.MouseCursor = null; + const root_box = self.parentBox().?; + root_box.endChildren(); - var active_iter = self.active_box_keys.iterator(); - while (active_iter.next()) |active_box_key| { - if (self.findBoxByKey(active_box_key.value.*)) |active_box| { - cursor = active_box.active_cursor; - - if (cursor != null) { - break; - } - } - } - - if (cursor == null) { - if (self.hot_box_key) |hot_box_key| { - if (self.findBoxByKey(hot_box_key)) |hot_box| { - cursor = hot_box.hot_cursor; - } - } - } - - rl.setMouseCursor(@intFromEnum(cursor orelse rl.MouseCursor.mouse_cursor_default)); - } + self.popFont(); // Update Animations { @@ -661,135 +785,609 @@ pub fn end(self: *UI) void { } } - if (rl.isKeyPressed(rl.KeyboardKey.key_f5) and builtin.mode == .Debug) { - self.show_grayscale = !self.show_grayscale; + // Mouse cursor + { + var cursor: ?rl.MouseCursor = null; + + var active_iter = self.active_box_keys.iterator(); + while (active_iter.next()) |active_box_key| { + if (self.getBoxByKey(active_box_key.value.*)) |active_box| { + cursor = active_box.active_cursor; + + if (cursor != null) { + break; + } + } + } + + if (cursor == null) { + if (self.hot_box_key) |hot_box_key| { + if (self.getBoxByKey(hot_box_key)) |hot_box| { + cursor = hot_box.hot_cursor; + } + } + } + + rl.setMouseCursor(@intFromEnum(cursor orelse rl.MouseCursor.mouse_cursor_default)); } - const root_box = self.findBoxByKey(root_box_key).?; - self.calcLayout(root_box, .X); - self.calcLayout(root_box, .Y); + // Reset sizes and positions, because it will be recalculated in layout pass + for (self.boxes.slice()) |*box| { + var position = Vec2{ .x = 0, .y = 0 }; + if (box.isFloating(.X)) { + position.x = box.persistent.position.x; + } + if (box.isFloating(.Y)) { + position.y = box.persistent.position.y; + } + box.persistent.size = Vec2Zero; + box.persistent.position = position; + } + + { + const zone = P.begin(@src(), "UI layout"); + defer zone.end(); + + self.layoutSizesInitial(root_box, .X); + self.layoutSizesShrink(root_box, .X); + self.layoutSizesGrow(root_box, .X); + self.layoutSizesFitChildren(root_box, .X); + + self.layoutWrapText(root_box); + + self.layoutSizesInitial(root_box, .Y); + self.layoutSizesShrink(root_box, .Y); + self.layoutSizesGrow(root_box, .Y); + self.layoutSizesFitChildren(root_box, .Y); + + self.layoutPositions(root_box, .X); + self.layoutPositions(root_box, .Y); + } } -pub fn pushStyle(self: *UI) void { - self.style_stack.appendAssumeCapacity(self.style); +pub fn pushFont(self: *UI, font_id: Assets.FontId) void { + self.font_stack.appendAssumeCapacity(font_id); } -pub fn popStyle(self: *UI) void { - self.style = self.style_stack.pop(); +pub fn popFont(self: *UI) void { + _ = self.font_stack.pop(); } -fn getActiveBoxFlags(self: *UI) Box.Flags { - var result_flags: Box.Flags = .{}; +pub fn currentDefaultFont(self: *UI) Assets.FontId { + assert(self.font_stack.len > 0); + return self.font_stack.buffer[self.font_stack.len - 1]; +} - 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); +pub fn rem(self: *UI, count: f32) f32 { + const font_id = self.currentDefaultFont(); + return font_id.size * count; +} + +inline fn vec2ByAxis(vec2: *Vec2, axis: Axis) *f32 { + return switch (axis) { + .X => &vec2.x, + .Y => &vec2.y + }; +} + +fn layoutWrapText(self: *UI, box: *Box) void { + if (box.flags.contains(.wrap_text)) { + box.wrapText(box.availableChildrenSize(.X)); + } + + { + var child_iter = box.iterChildren(); + while (child_iter.next()) |child| { + self.layoutWrapText(child); + } + } +} + +fn layoutSizesInitial(self: *UI, box: *Box, axis: Axis) void { + const axis_persistent_size = vec2ByAxis(&box.persistent.size, axis); + const sizing = box.size.getAxis(axis); + + if (sizing == .fixed) { + axis_persistent_size.* = sizing.fixed.get(box, axis); + } else if (sizing == .shrink) { + axis_persistent_size.* = sizing.shrink.max.get(box, axis); + } else if (sizing == .grow) { + axis_persistent_size.* = sizing.grow.min.get(box, axis); + } + + axis_persistent_size.* += box.padding.byAxis(axis); + + { + var child_iter = box.iterChildren(); + while (child_iter.next()) |child| { + self.layoutSizesInitial(child, axis); + } + } +} + +fn sumChildrenSize(box: *Box, axis: Axis) f32 { + var axis_size: f32 = 0; + + if (box.layout_direction.isAxis(axis)) { + var children_count: f32 = 0; + + var child_iter = box.iterChildren(); + while (child_iter.next()) |child| { + if (child.isFloating(axis)) continue; + + const axis_child_size = vec2ByAxis(&child.persistent.size, axis); + + axis_size += axis_child_size.*; + + children_count += 1; + } + + if (children_count > 0) { + axis_size += box.layout_gap * (children_count - 1); + } + } else { + var max_child_size: f32 = 0; + + var child_iter = box.iterChildren(); + while (child_iter.next()) |child| { + if (child.isFloating(axis)) continue; + + const axis_child_size = vec2ByAxis(&child.persistent.size, axis); + + max_child_size = @max(max_child_size, axis_child_size.*); + } + + axis_size += max_child_size; + } + + return axis_size; +} + +fn layoutSizesShrink(self: *UI, box: *Box, axis: Axis) void { + { + var child_iter = box.iterChildren(); + while (child_iter.next()) |child| { + self.layoutSizesShrink(child, axis); } } - return result_flags; + if (box.layout_direction.isAxis(axis)) { + const available_children_size = box.availableChildrenSize(axis); + const used_axis_size = sumChildrenSize(box, axis); + + var overflow_size = used_axis_size - available_children_size; + if (overflow_size > 0) { + const ShrinkableChild = struct { + box: *Box, + min_size: f32 + }; + var shrinkable_children: std.BoundedArray(ShrinkableChild, max_boxes) = .{}; + + var child_iter = box.iterChildren(); + while (child_iter.next()) |child| { + if (child.isFloating(axis)) continue; + + const child_axis_sizing = child.size.getAxis(axis); + if (child_axis_sizing == .shrink) { + shrinkable_children.appendAssumeCapacity(.{ + .box = child, + .min_size = child_axis_sizing.shrink.min.get(child, axis) + }); + } + } + + while (overflow_size > 0) { + + // Remove children that have reached minimum size + { + var i: usize = 0; + while (i < shrinkable_children.len) { + const shrinkable_child = shrinkable_children.get(i); + const child = shrinkable_child.box; + const child_min_size = shrinkable_child.min_size; + const child_persistent_size = vec2ByAxis(&child.persistent.size, axis); + + if (child_persistent_size.* <= child_min_size) { + _ = shrinkable_children.swapRemove(i); + continue; + } else { + i += 1; + } + } + } + + if (shrinkable_children.len == 0) { + break; + } + + var largest_size: f32 = vec2ByAxis(&shrinkable_children.buffer[0].box.persistent.size, axis).*; + var second_largest_size: ?f32 = null; + + var largest_children: std.BoundedArray(*Box, max_boxes) = .{}; + largest_children.appendAssumeCapacity(shrinkable_children.buffer[0].box); + + for (shrinkable_children.slice()[1..]) |shrinkable_child| { + const child = shrinkable_child.box; + const child_persistent_size = vec2ByAxis(&child.persistent.size, axis); + + if (child_persistent_size.* > largest_size) { + second_largest_size = largest_size; + largest_size = child_persistent_size.*; + largest_children.len = 0; + largest_children.appendAssumeCapacity(child); + } else if (child_persistent_size.* < largest_size) { + second_largest_size = @max(second_largest_size orelse 0, child_persistent_size.*); + } else { + largest_children.appendAssumeCapacity(child); + } + } + + var shrink_largest_by = overflow_size; + if (second_largest_size != null) { + shrink_largest_by = @min(shrink_largest_by, largest_size - second_largest_size.?); + } + + const largest_count_f32: f32 = @floatFromInt(largest_children.len); + shrink_largest_by = @min(shrink_largest_by * largest_count_f32, largest_size) / largest_count_f32; + + for (largest_children.slice()) |largest_child| { + const child_persistent_size = vec2ByAxis(&largest_child.persistent.size, axis); + child_persistent_size.* -= shrink_largest_by; + overflow_size -= shrink_largest_by; + } + } + } + } +} + +fn layoutSizesFitChildren(self: *UI, box: *Box, axis: Axis) void { + { + var child_iter = box.iterChildren(); + while (child_iter.next()) |child| { + self.layoutSizesFitChildren(child, axis); + } + } + + if (box.size.getAxis(axis) == .fit_children) { + const children_size = sumChildrenSize(box, axis); + const persistent_size = vec2ByAxis(&box.persistent.size, axis); + persistent_size.* += children_size; + } +} + +fn layoutSizesGrow(self: *UI, box: *Box, axis: Axis) void { + const available_children_size = box.availableChildrenSize(axis); + + // Hack to make .{ .fixed = { .parent_percent = 1} } work when parent is .grow + // I can't be bother to find a nice solutino for this. + // I need this for a scrollbar. + const sizing = box.size.getAxis(axis); + if (sizing.dependsOnParent() and sizing == .fixed) { + const persistent_size = vec2ByAxis(&box.persistent.size, axis); + persistent_size.* = sizing.fixed.get(box, axis); + } + + if (box.layout_direction.isAxis(axis)) { + const used_axis_size = sumChildrenSize(box, axis); + var unused_size = available_children_size - used_axis_size; + + if (unused_size > 0) { + const GrowableChild = struct { + box: *Box, + max_size: f32 + }; + var growable_children: std.BoundedArray(GrowableChild, max_boxes) = .{}; + + var child_iter = box.iterChildren(); + while (child_iter.next()) |child| { + if (child.isFloating(axis)) continue; + + const child_axis_sizing = child.size.getAxis(axis); + if (child_axis_sizing == .grow) { + growable_children.appendAssumeCapacity(.{ + .box = child, + .max_size = child_axis_sizing.grow.max.get(child, axis) + }); + } + } + + while (unused_size > 0) { + + // Remove children that have reached maximum size + { + var i: usize = 0; + while (i < growable_children.len) { + const growable_child = growable_children.get(i); + const child = growable_child.box; + const child_max_size = growable_child.max_size; + const child_persistent_size = vec2ByAxis(&child.persistent.size, axis); + + if (child_persistent_size.* >= child_max_size) { + _ = growable_children.swapRemove(i); + continue; + } else { + i += 1; + } + } + } + + if (growable_children.len == 0) { + break; + } + + var smallest_size: f32 = vec2ByAxis(&growable_children.buffer[0].box.persistent.size, axis).*; + var second_smallest_size: ?f32 = null; + + var smallest_children: std.BoundedArray(*Box, max_boxes) = .{}; + smallest_children.appendAssumeCapacity(growable_children.buffer[0].box); + + for (growable_children.slice()[1..]) |growable_child| { + const child = growable_child.box; + const child_persistent_size = vec2ByAxis(&child.persistent.size, axis); + + if (child_persistent_size.* < smallest_size) { + second_smallest_size = smallest_size; + smallest_size = child_persistent_size.*; + smallest_children.len = 0; + smallest_children.appendAssumeCapacity(child); + } else if (child_persistent_size.* > smallest_size) { + second_smallest_size = @min(second_smallest_size orelse std.math.inf(f32), child_persistent_size.*); + } else { + smallest_children.appendAssumeCapacity(child); + } + } + + var grow_smallest_by = unused_size; + if (second_smallest_size != null) { + grow_smallest_by = @min(grow_smallest_by, second_smallest_size.? - smallest_size); + } + + const smallest_count_f32: f32 = @floatFromInt(smallest_children.len); + grow_smallest_by = @min(grow_smallest_by * smallest_count_f32, unused_size) / smallest_count_f32; + + for (smallest_children.slice()) |smallest_child| { + const child_persistent_size = vec2ByAxis(&smallest_child.persistent.size, axis); + child_persistent_size.* += grow_smallest_by; + unused_size -= grow_smallest_by; + } + } + } + } else { + var child_iter = box.iterChildren(); + while (child_iter.next()) |child| { + if (child.isFloating(axis)) continue; + + const child_axis_sizing = child.size.getAxis(axis); + if (child_axis_sizing == .grow) { + const child_max_size = child_axis_sizing.grow.max.get(child, axis); + const child_persistent_size = vec2ByAxis(&child.persistent.size, axis); + + child_persistent_size.* = @min(available_children_size, child_max_size); + } + } + } + + { + var child_iter = box.iterChildren(); + while (child_iter.next()) |child| { + self.layoutSizesGrow(child, axis); + } + } +} + +fn layoutPositions(self: *UI, box: *Box, axis: Axis) void { + const is_layout_axis = box.layout_direction.isAxis(axis); + + { + const available_children_size = box.availableChildrenSize(axis); + const children_size = sumChildrenSize(box, axis); + + const axis_alignment = box.alignment.getAxis(axis); + const alignment_offset_scalar = axis_alignment.getCoefficient(); + + var offset: f32 = 0; + var child_iter = box.iterChildren(); + while (child_iter.next()) |child| { + if (child.isFloating(axis)) continue; + + const axis_position = vec2ByAxis(&box.persistent.position, axis); + const axis_child_position = vec2ByAxis(&child.persistent.position, axis); + const axis_child_persistent_size = vec2ByAxis(&child.persistent.size, axis); + + axis_child_position.* = axis_position.*; + + if (axis == .X) { + axis_child_position.* += box.padding.left; + } else if (axis == .Y) { + axis_child_position.* += box.padding.top; + } + + if (is_layout_axis) { + axis_child_position.* += offset; + offset += axis_child_persistent_size.*; + offset += box.layout_gap; + } + + axis_child_position.* += @max(available_children_size - children_size, 0) * alignment_offset_scalar; + + axis_child_position.* -= vec2ByAxis(&box.view_offset, axis).*; + } + } + + var child_iter = box.iterChildren(); + while (child_iter.next()) |child| { + self.layoutPositions(child, axis); + } +} + +pub fn createBox(self: *UI, opts: BoxOptions) *Box { + var box: *Box = undefined; + var box_index: ?BoxIndex = null; + var key = opts.key orelse Key.initNil(); + var persistent = Box.Persistent{}; + + if (!key.isNil()) { + if (self.getBoxIndexByKey(key)) |last_frame_box_index| { + const last_frame_box = self.getBoxByIndex(last_frame_box_index); + + box = last_frame_box; + persistent = last_frame_box.persistent; + box_index = last_frame_box_index; + + // Safety guard for using a key multiple times in UI + assert(box.last_used_frame != self.frame_index); + } + } + + if (box_index == null) { + box = self.boxes.addOneAssumeCapacity(); + box_index = self.boxes.len - 1; + } + + var size = Sizing2{ + .x = Sizing.initFixed(.{ .pixels = 0 }), + .y = Sizing.initFixed(.{ .pixels = 0 }), + }; + if (opts.size_x) |size_x| { + size.x = size_x; + } else if (opts.text != null) { + size.x = Sizing.initFixed(.text); + } + + if (opts.size_y) |size_y| { + size.y = size_y; + } else if (opts.text != null) { + size.y = Sizing.initFixed(.text); + } + + var flags = Box.Flags.initEmpty(); + if (opts.flags) |opts_flags| { + for (opts_flags) |flag| { + flags.insert(flag); + } + } + + const default_alignment = if (opts.text != null) Alignment.center else Alignment.start; + const alignment = Alignment2{ + .x = opts.align_x orelse default_alignment, + .y = opts.align_y orelse default_alignment, + }; + + box.* = Box{ + .ui = self, + .allocator = self.frame_allocator(), + + .persistent = persistent, + .flags = flags, + .background = opts.background, + .size = size, + .layout_direction = opts.layout_direction orelse LayoutDirection.left_to_right, + .layout_gap = opts.layout_gap orelse 0, + .padding = opts.padding orelse Padding{}, + .alignment = alignment, + .font = opts.font orelse self.currentDefaultFont(), + .text = null, + .text_color = opts.text_color orelse srcery.bright_white, + .borders = opts.borders orelse Borders{}, + .hot_cursor = opts.hot_cursor, + .active_cursor = opts.active_cursor, + .view_offset = opts.view_offset orelse Vec2Zero, + + .last_used_frame = self.frame_index, + .key = key, + .tree = .{ + .index = box_index.? + }, + }; + + if (box.isFloating(.X)) { + box.persistent.position.x = 0; + } + + if (box.isFloating(.Y)) { + box.persistent.position.y = 0; + } + + if (opts.text) |text| { + box.setText(text); + } + + if (opts.position_x) |x| { + box.setFloatX(x); + } + + if (opts.position_y) |y| { + box.setFloatY(y); + } + + if (self.parentBox()) |parent| { + box.tree.parent_index = parent.tree.index; + + if (parent.tree.last_child_index) |last_child_index| { + const last_child = self.getBoxByIndex(last_child_index); + + last_child.tree.next_sibling_index = box.tree.index; + parent.tree.last_child_index = box.tree.index; + } else { + parent.tree.first_child_index = box.tree.index; + parent.tree.last_child_index = box.tree.index; + } + } + + return box; +} + +pub fn parentBoxIndex(self: *UI) ?BoxIndex { + if (self.parent_stack.len > 0) { + return self.parent_stack.buffer[self.parent_stack.len - 1]; + } else { + return null; + } +} + +pub fn parentBox(self: *UI) ?*Box { + if (self.parentBoxIndex()) |box_index| { + return self.getBoxByIndex(box_index); + } else { + return null; + } +} + +pub fn getBoxByIndex(self: *UI, box_index: BoxIndex) *Box { + return &self.boxes.buffer[box_index]; +} + +pub fn getBoxIndexByKey(self: *UI, key: Key) ?BoxIndex { + for (0.., self.boxes.slice()) |index, box| { + if (box.key.eql(key)) { + return @intCast(index); + } + } + return null; +} + +pub fn getBoxByKey(self: *UI, key: Key) ?*Box { + if (self.getBoxIndexByKey(key)) |box_index| { + return self.getBoxByIndex(box_index); + } else { + return null; + } } pub fn draw(self: *UI) void { - const root_box = self.findBoxByKey(root_box_key).?; + const root_box_index = self.getBoxIndexByKey(root_box_key).?; + const root_box = self.getBoxByIndex(root_box_index); - if (self.show_grayscale) { - self.grayscale_shader.activate(); - defer self.grayscale_shader.deactivate(); + const zone = P.begin(@src(), "UI Draw"); + defer zone.end(); - self.drawBox(root_box); - } else { - 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; - } - } - } - } + self.drawBox(root_box); } fn drawBox(self: *UI, box: *Box) void { - if (box.flags.contains(.skip_draw)) { - return; - } + const box_rect = box.rect(); - var value_shift: f32 = 0; - if (box.flags.contains(.highlight_active) and self.isKeyActive(box.key)) { - value_shift = -0.5 * box.persistent.active; - } else if (box.flags.contains(.highlight_hot) and self.isKeyHot(box.key)) { - value_shift = 0.6 * box.persistent.hot; - } - - const box_rect = box.computedRect(); - - const do_scissor = box.hasClipping(); + const do_scissor = box.flags.contains(.clip_view); if (do_scissor) { rl.beginScissorMode( @intFromFloat(box_rect.x), @@ -800,8 +1398,15 @@ fn drawBox(self: *UI, box: *Box) void { } defer if (do_scissor) rl.endScissorMode(); - if (box.background) |background| { - rl.drawRectangleRec(box_rect, utils.shiftColorInHSV(background, value_shift)); + var value_shift: f32 = 0; + if (box.flags.contains(.draw_active) and self.isKeyActive(box.key)) { + value_shift = -0.5 * box.persistent.active; + } else if (box.flags.contains(.draw_hot) and self.isKeyHot(box.key)) { + value_shift = 0.6 * box.persistent.hot; + } + + if (box.background) |bg| { + rl.drawRectangleRec(box_rect, utils.shiftColorInHSV(bg, value_shift)); } const borders_with_coords = .{ @@ -844,390 +1449,71 @@ fn drawBox(self: *UI, box: *Box) void { } if (box.text) |text| { - const font = Assets.font(text.font); - const text_size = font.measureText(text.content); - var text_rect = Rect{ - .x = box_rect.x, - .y = box_rect.y, - .width = text_size.x, - .height = text_size.y - }; + const font_face = Assets.font(box.font); + var text_position = box.persistent.position; + text_position.x += box.padding.left; + text_position.y += box.padding.top; - if (box.flags.contains(.text_left_align)) { - if (box.size.x.kind == .text) { - const padding = box.size.x.kind.text; - text_rect.x += padding * font.getSize() / 2; - } + const lines: [][]u8 = box.text_lines.slice(); - if (box.size.y.kind == .text) { - const padding = box.size.y.kind.text; - text_rect.y += padding * font.getSize() / 2; - } + const available_width = box.availableChildrenSize(.X); + const available_height = box.availableChildrenSize(.Y); + + if (lines.len == 0) { + const text_size = font_face.measureText(text); + text_position.x += @max(available_width - text_size.x, 0) * box.alignment.x.getCoefficient(); + text_position.y += @max(available_height - text_size.y, 0) * box.alignment.y.getCoefficient(); + + font_face.drawText(text, text_position, box.text_color); } else { - text_rect.x += (box_rect.width - text_size.x) / 2; - text_rect.y += (box_rect.height - text_size.y) / 2; - } + // TODO: Don't call `measureTextLines`, + // Because in the end `measureText` will be called twice for each line + const text_size = font_face.measureTextLines(lines); + text_position.x += @max(available_width - text_size.x, 0) * box.alignment.x.getCoefficient(); + text_position.y += @max(available_height - text_size.y, 0) * box.alignment.y.getCoefficient(); - const text_color = utils.shiftColorInHSV(text.color, value_shift); - font.drawText(text.content, .{ .x = text_rect.x, .y = text_rect.y }, text_color); + var offset_y: f32 = 0; - if (box.flags.contains(.text_underline)) { - rl.drawLineV( - rect_utils.bottomLeft(text_rect), - rect_utils.bottomRight(text_rect), - text_color - ); + for (lines) |line| { + const line_size = font_face.measureText(line); + const offset_x = @max(text_size.x - line_size.x, 0) * box.alignment.x.getCoefficient(); + + font_face.drawText( + line, + text_position.add(.{ .x = offset_x, .y = offset_y }), + box.text_color + ); + + offset_y += font_face.getSize() * font_face.line_height; + } } } - var child_iter = self.iterChildrenByParent(box); - while (child_iter.next()) |child| { - self.drawBox(child); - } - - if (debug) { + if (draw_debug) { if (self.isKeyActive(box.key)) { rl.drawRectangleLinesEx(box_rect, 3, rl.Color.red); } else if (self.isKeyHot(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 (box.getFixedSizeAxis(axis)) |fixed_size| { - computed_size.* = fixed_size; - } else if (size.kind == .pixels) { - computed_size.* = size.kind.pixels; - } else if (size.kind == .texture) { - if (box.texture) |texture| { - var texture_size: f32 = 0; - if (axis == .X) { - texture_size = @floatFromInt(texture.width); - } else if (axis == .Y) { - texture_size = @floatFromInt(texture.height); - } - computed_size.* = size.kind.texture * texture_size; - } else { - computed_size.* = 0; - } - } 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; + rl.drawRectangleLinesEx(box_rect, 1, rl.Color.magenta); } } - var child_iter = self.iterChildrenByParent(box); + var child_iter = box.iterChildren(); while (child_iter.next()) |child| { - self.calcLayoutStandaloneSize(child, axis); + self.drawBox(child); } } -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.isSizeFixed(axis) or - 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 first_child: bool = true; - - var sum: f32 = 0; - var child_iter = self.iterChildrenByParent(box); - while (child_iter.next()) |child| { - if (child.isPositionFixed(axis)) continue; - - const child_size = getVec2Axis(&child.persistent.size, axis).*; - - if (box.layout_axis == axis) { - sum += child_size; - if (!first_child) { - sum += box.layout_gap; - } - } else { - sum = @max(sum, child_size); - } - - first_child = false; - } - - computed_size.* = sum; - } -} - -fn calcLayoutEnforceConstraints(self: *UI, box: *Box, axis: Axis) void { - if (box.text != null and box.flags.contains(.text_wrapping)) { - const text = box.text.?; - const font = Assets.font(text.font); - const axis_size = box.size.getAxis(axis); - - var max_width = box.persistent.size.x; - if (axis_size.kind == .text) { - max_width -= axis_size.kind.text * font.getSize(); - } - - if (wrapText(box.allocator, font, text.content, max_width) catch null) |wrapped_content| { - box.text.?.content = wrapped_content; - } - } - - // 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| { - if (child.isPositionFixed(axis)) continue; - - 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 child_count: 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| { - if (child.isPositionFixed(axis)) continue; - - 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; - - child_count += 1; - } - } - - sum_children_size += @max(child_count - 1, 0 ) * box.layout_gap; - - 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) { - if (child.isPositionFixed(axis)) continue; - - const child_size_axis = getVec2Axis(&child.persistent.size, axis); - - child_size_axis.* -= children_fixups.buffer[index] * overflow_percent; - - // Recalculate all upward depedent sizes, because the parent size changed - var child_child_iter = self.iterChildrenByParent(child); - while (child_child_iter.next()) |child_child| { - self.calcLayoutUpwardsSize(child_child, axis); - } - } - } - } - - { - var child_iter = self.iterChildrenByParent(box); - while (child_iter.next()) |child| { - self.calcLayoutEnforceConstraints(child, axis); - } - } -} - -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); - - if (child.getFixedPositionAxis(axis)) |position| { - child_axis_position.* = position; - } else { - const child_axis_size = getVec2Axis(&child.persistent.size, axis); - const parent_axis_position = getVec2Axis(&box.persistent.position, axis); - - child_axis_position.* = parent_axis_position.*; - - if (box.layout_axis == axis) { - child_axis_position.* += layout_position; - layout_position += child_axis_size.*; - layout_position += box.layout_gap; - } - } - - child_axis_position.* -= getVec2Axis(&box.view_offset, axis).*; - } - } - - if (box.layout_axis == axis) { - var child_size_sum: f32 = 0; - var child_count: f32 = 0; - - var child_iter = self.iterChildrenByParent(box); - while (child_iter.next()) |child| { - if (child.isPositionFixed(axis)) continue; - - const child_size = getVec2Axis(&child.persistent.size, axis); - child_size_sum += child_size.*; - child_count += 1; - } - - if (child_count > 1) { - child_size_sum += (child_count - 1) * box.layout_gap; - } - - getVec2Axis(&box.persistent.children_size, axis).* = child_size_sum; - } else { - var max_child_size: f32 = 0; - - var child_iter = self.iterChildrenByParent(box); - while (child_iter.next()) |child| { - if (child.isPositionFixed(axis)) continue; - - const child_size = getVec2Axis(&child.persistent.size, axis); - max_child_size = @max(max_child_size, child_size.*); - } - - getVec2Axis(&box.persistent.children_size, axis).* = max_child_size; - } - - { - var child_iter = self.iterChildrenByParent(box); - while (child_iter.next()) |child| { - self.calcLayoutPositions(child, axis); - } - } -} - -pub fn wrapText(allocator: std.mem.Allocator, font: FontFace, text: []const u8, max_width: f32) !?[]u8 { - // TODO: Implement word wrapping that doesn't include re-allocating the whole text - if (font.measureWidth(text) < max_width) { - return null; - } - - var result = try std.ArrayList(u8).initCapacity(allocator, text.len); - defer result.deinit(); - - var line_buffer = std.ArrayList(u8).init(allocator); - defer line_buffer.deinit(); - - var word_iter = std.mem.splitScalar(u8, text, ' '); - while (word_iter.next()) |word| { - try line_buffer.ensureUnusedCapacity(word.len + 1); - - const current_line = line_buffer.items; - if (line_buffer.items.len > 0) { - try line_buffer.append(' '); - } - try line_buffer.appendSlice(word); - - if (font.measureWidth(line_buffer.items) > max_width) { - if (result.items.len > 0) { - try result.append('\n'); - } - try result.appendSlice(current_line); - - line_buffer.clearRetainingCapacity(); - try line_buffer.appendSlice(word); - } - } - - if (line_buffer.items.len > 0) { - if (result.items.len > 0) { - try result.append('\n'); - } - try result.appendSlice(line_buffer.items); - } - - return try result.toOwnedSlice(); -} - fn getKeySeed(self: *UI) u64 { - var maybe_current = self.getParent(); + var maybe_current = self.parentBox(); while (maybe_current) |current| { if (!current.key.isNil()) { return current.key.hash; } maybe_current = null; - if (current.parent_index) |parent_index| { + if (current.tree.parent_index) |parent_index| { maybe_current = &self.boxes.buffer[parent_index]; } } @@ -1235,111 +1521,23 @@ fn getKeySeed(self: *UI) u64 { return 0; } -pub fn newKeyFromString(self: *UI, text: []const u8) Key { +pub fn keyFromString(self: *UI, text: []const u8) Key { return Key.initString(self.getKeySeed(), text); } -pub fn newBoxFromString(self: *UI, text: []const u8) *Box { - return self.newBox(self.newKeyFromString(text)); -} - -pub fn newBoxFromPtr(self: *UI, ptr: anytype) *Box { - return self.newBox(Key.initPtr(ptr)); -} - -pub fn appendBox(self: *UI, box: *Box) void { - assert(box.parent_index == null); - - if (self.getParentIndex()) |parent_index| { - const parent = &self.boxes.buffer[parent_index]; - 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; - } - } -} - -pub fn newBoxNoAppend(self: *UI, key: Key) *Box { - var box: *Box = undefined; - var box_index: ?BoxIndex = null; - var persistent: Box.Persistent = .{}; - - if (!key.isNil()) { - if (self.findBoxIndexByKey(key)) |found_box_index| { - const found_box = &self.boxes.buffer[found_box_index]; - assert(found_box.last_used_frame < self.frame_index); - - persistent = found_box.persistent; - box = found_box; - box_index = found_box_index; - } - } - - if (box_index == null) { - 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 (!key.isNil()) { - box.background = self.style.background; - box.rounded = self.style.rounded; - box.borders = self.style.borders; - } - - return box; -} - -pub fn newBox(self: *UI, key: Key) *Box { - const box = self.newBoxNoAppend(key); - self.appendBox(box); - return box; -} - -fn findBoxIndexByKey(self: *UI, key: Key) ?BoxIndex { - for (0.., self.boxes.slice()) |index, *box| { - if (box.key.eql(key)) { - return @intCast(index); - } - } - return null; -} - -fn findBoxByKey(self: *UI, key: Key) ?*Box { - if (self.findBoxIndexByKey(key)) |box_index| { - return &self.boxes.buffer[box_index]; - } - return null; -} - -pub fn signalFromBox(self: *UI, box: *Box) Signal { - if (box.key.isNil()) { - return Signal{}; - } - +pub fn signal(self: *UI, box: *Box) Signal { var result = Signal{}; - var rect = box.computedRect(); + if (box.key.isNil()) { + return result; + } + + var rect = box.rect(); { - var parent_iter = self.iterUpwardByParent(box); + var parent_iter = box.iterParents(); while (parent_iter.next()) |parent| { - if (parent.hasClipping()) { - rect = rect_utils.intersect(rect, parent.computedRect()); + if (parent.flags.contains(.clip_view)) { + rect = rect_utils.intersect(rect, parent.rect()); } } } @@ -1422,10 +1620,6 @@ pub fn signalFromBox(self: *UI, box: *Box) Signal { return result; } -pub fn isBoxHot(self: *UI, box: *Box) bool { - return self.isKeyHot(box.key); -} - pub fn isKeyHot(self: *UI, key: Key) bool { if (self.hot_box_key) |hot_box_key| { return hot_box_key.eql(key); @@ -1446,239 +1640,4 @@ pub fn isKeyActive(self: *UI, key: Key) bool { } return false; -} - -fn getParentIndex(self: *UI) ?BoxIndex { - const parent_stack: []BoxIndex = self.parent_index_stack.slice(); - - if (parent_stack.len > 0) { - return parent_stack[parent_stack.len - 1]; - } else { - return null; - } -} - -pub fn getParentOf(self: *UI, box: *Box) ?*Box { - if (box.parent_index) |index| { - return &self.boxes.buffer[index]; - } else { - return null; - } -} - -pub fn getParent(self: *UI) ?*Box { - if (self.getParentIndex()) |parent_index| { - 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)]; -} - -pub fn pushScrollbar(self: *UI, key: UI.Key) *Box { - const container = self.newBox(key); - container.layout_axis = .X; - container.size = .{ - .x = UI.Size.percent(1, 0), - .y = UI.Size.percent(1, 0), - }; - self.pushParent(container); - - const content_area = self.newBoxFromString("Scroll area"); - content_area.flags.insert(.skip_draw); - content_area.flags.insert(.scrollable); - content_area.size = .{ - .x = UI.Size.percent(1, 0), - .y = UI.Size.percent(1, 0), - }; - self.pushParent(content_area); - - const visible_content_size = content_area.persistent.size.y; - const used_content_size = content_area.persistent.children_size.y; - const visible_percent = clamp(visible_content_size / used_content_size, 0, 1); - const sroll_offset = content_area.persistent.sroll_offset; - content_area.view_offset.y = sroll_offset * (1 - visible_percent) * used_content_size; - - return content_area; -} - -pub fn popScrollbar(self: *UI) void { - const content_area = self.getParent().?; - self.popParent(); // pop scroll area - - const visible_content_size = content_area.persistent.size.y; - const used_content_size = content_area.persistent.children_size.y; - const visible_percent = clamp(visible_content_size / used_content_size, 0, 1); - if (used_content_size != 0) { - content_area.flags.remove(.skip_draw); - } - - if (!content_area.flags.contains(.skip_draw)) { - const scrollbar_area = self.newBoxFromString("Scrollbar area"); - scrollbar_area.background = srcery.hard_black; - scrollbar_area.flags.insert(.scrollable); - scrollbar_area.size = .{ - .x = UI.Size.pixels(24, 1), - .y = UI.Size.percent(1, 0), - }; - self.pushParent(scrollbar_area); - defer self.popParent(); - - const draggable = self.clickableBox("Scrollbar button"); - draggable.background = srcery.black; - draggable.borders.all(.{ - .size = 4, - .color = srcery.xgray3 - }); - draggable.flags.insert(.draggable); - draggable.size = .{ - .x = UI.Size.percent(1, 1), - .y = UI.Size.percent(visible_percent, 1), - }; - - const sroll_offset = &content_area.persistent.sroll_offset; - const scrollbar_height = content_area.persistent.size.y; - const max_offset = scrollbar_height * (1 - visible_percent); - draggable.setFixedY(content_area.persistent.position.y + sroll_offset.* * max_offset); - - const draggable_signal = self.signalFromBox(draggable); - if (draggable_signal.dragged()) { - sroll_offset.* += draggable_signal.drag.y / max_offset; - } - - const scroll_speed = 16; - const scrollbar_signal = self.signalFromBox(scrollbar_area); - if (scrollbar_signal.scrolled()) { - sroll_offset.* -= scrollbar_signal.scroll.y / max_offset * scroll_speed; - } - - const content_area_signal = self.signalFromBox(content_area); - if (content_area_signal.scrolled()) { - sroll_offset.* -= content_area_signal.scroll.y / max_offset * scroll_speed; - } - - sroll_offset.* = clamp(sroll_offset.*, 0, 1); - } - - self.popParent(); // pop container -} - -pub fn button(self: *UI, font: Assets.FontId, text: []const u8) *Box { - const box = self.clickableBox(text); - box.size.x = UI.Size.text(1, 1); - box.size.y = UI.Size.text(0.5, 1); - box.setText(font, text); - - return box; -} - -pub fn clickableBox(self: *UI, key: []const u8) *Box { - const box = self.newBoxFromString(key); - box.flags.insert(.clickable); - box.flags.insert(.highlight_active); - box.flags.insert(.highlight_hot); - box.hot_cursor = rl.MouseCursor.mouse_cursor_pointing_hand; - - return box; -} - -pub fn spacer(self: *UI, size: Vec2Size) void { - const box = self.newBox(UI.Key.initNil()); - box.size = size; -} - -pub fn label(self: *UI, font: Assets.FontId, text: []const u8) *Box { - const box = self.newBoxFromString(text); - box.size.x = UI.Size.text(1, 1); - box.size.y = UI.Size.text(0.5, 1); - box.setText(font, text); - - return box; -} - -pub fn textureBox(self: *UI, texture: rl.Texture2D, scale: f32) *Box { - const box = self.newBox(Key.initNil()); - box.size.x = UI.Size.texture(scale, 1); - box.size.y = UI.Size.texture(scale, 1); - box.texture = texture; - - return box; -} - -pub fn pushCenterBox(self: *UI) *Box { - self.pushHorizontalAlign(); - self.pushVerticalAlign(); - - const container = self.newBox(UI.Key.initNil()); - container.size.x = UI.Size.childrenSum(1); - container.size.y = UI.Size.childrenSum(1); - self.pushParent(container); - - return container; -} - -pub fn popCenterBox(self: *UI) void { - self.popParent(); - - self.popVerticalAlign(); - self.popHorizontalAlign(); -} - -pub fn pushHorizontalAlign(self: *UI) void { - const horizontal_align = self.newBox(UI.Key.initNil()); - horizontal_align.layout_axis = .X; - horizontal_align.size = .{ - .x = UI.Size.percent(1, 0), - .y = UI.Size.childrenSum(1), - }; - self.pushParent(horizontal_align); - - self.spacer(.{ .x = UI.Size.percent(1, 0) }); -} - -pub fn popHorizontalAlign(self: *UI) void { - self.spacer(.{ .x = UI.Size.percent(1, 0) }); - self.popParent(); -} - -pub fn pushVerticalAlign(self: *UI) void { - const vertical_align = self.newBox(UI.Key.initNil()); - vertical_align.layout_axis = .Y; - vertical_align.size = .{ - .x = UI.Size.childrenSum(1), - .y = UI.Size.percent(1, 0), - }; - self.pushParent(vertical_align); - - self.spacer(.{ .y = UI.Size.percent(1, 0) }); -} - -pub fn popVerticalAlign(self: *UI) void { - self.spacer(.{ .y = UI.Size.percent(1, 0) }); - self.popParent(); } \ No newline at end of file