diff --git a/src/app.zig b/src/app.zig index 5d11ddb..68f1929 100644 --- a/src/app.zig +++ b/src/app.zig @@ -212,14 +212,9 @@ pub fn tick(self: *App) !void { } { - const rows_container = self.ui.newBoxFromString("Channels"); - rows_container.layout_axis = .Y; - rows_container.size = .{ - .x = UI.Size.percent(1, 1), - .y = UI.Size.percent(1, 0), - }; - self.ui.pushParent(rows_container); - defer self.ui.popParent(); + const scroll_area = self.ui.pushScrollbar(self.ui.newKeyFromString("Channels")); + scroll_area.layout_axis = .Y; + defer self.ui.popScrollbar(); for (self.channels.slice()) |*_channel| { const channel: *Channel = _channel; @@ -260,7 +255,7 @@ pub fn tick(self: *App) !void { { const middle_box = self.ui.newBoxFromString("Middle knob"); middle_box.flags.insert(.clickable); - middle_box.flags.insert(.draggable); + middle_box.flags.insert(.draggable_x); middle_box.background = rl.Color.ray_white; middle_box.size.y = UI.Size.pixels(32, 1); @@ -275,14 +270,14 @@ pub fn tick(self: *App) !void { channel.view_rect.to += samples_moved; } - middle_box.override_x = minimap_rect.width * channel.view_rect.from / sample_count + 4; - middle_box.size.x = UI.Size.pixels(minimap_rect.width * (channel.view_rect.to - channel.view_rect.from) / sample_count - 8, 1); + middle_box.setFixedX(minimap_rect.width * channel.view_rect.from / sample_count + 4); + middle_box.setFixedWidth(minimap_rect.width * (channel.view_rect.to - channel.view_rect.from) / sample_count - 8); } { const left_knob_box = self.ui.newBoxFromString("Left knob"); left_knob_box.flags.insert(.clickable); - left_knob_box.flags.insert(.draggable); + left_knob_box.flags.insert(.draggable_x); left_knob_box.background = rl.Color.ray_white; left_knob_box.size.x = UI.Size.pixels(8, 1); left_knob_box.size.y = UI.Size.pixels(32, 1); @@ -299,13 +294,13 @@ pub fn tick(self: *App) !void { channel.view_rect.from = clamp(channel.view_rect.from, 0, channel.view_rect.to-min_visible_samples); } - left_knob_box.override_x = minimap_rect.width * channel.view_rect.from / sample_count - left_knob_box.persistent.size.x/2; + left_knob_box.setFixedX(minimap_rect.width * channel.view_rect.from / sample_count - left_knob_box.persistent.size.x/2); } { const right_knob_box = self.ui.newBoxFromString("Right knobaaa"); right_knob_box.flags.insert(.clickable); - right_knob_box.flags.insert(.draggable); + right_knob_box.flags.insert(.draggable_x); right_knob_box.background = rl.Color.ray_white; right_knob_box.size.x = UI.Size.pixels(8, 1); right_knob_box.size.y = UI.Size.pixels(32, 1); @@ -322,22 +317,9 @@ pub fn tick(self: *App) !void { channel.view_rect.to = clamp(channel.view_rect.to, channel.view_rect.from+min_visible_samples, sample_count); } - right_knob_box.override_x = minimap_rect.width * channel.view_rect.to / sample_count - right_knob_box.persistent.size.x/2; + right_knob_box.setFixedX(minimap_rect.width * channel.view_rect.to / sample_count - right_knob_box.persistent.size.x/2); } } - - // const graph_widget = self.ui.newWidget(self.ui.keyFromString("samples-plot")); - // graph_widget.size.y = .{ .pixels = channel.height }; - // graph_widget.size.x = .{ .percent = 1 }; - // graph_widget.graph = .{ - // .cache = &channel.view_cache, - // .options = channel.view_rect, - // .samples = channel.samples.owned - // }; - - // const minimap_widget = self.showChannelMinimap(channel); - // minimap_widget.size.y = .fit_children; - // minimap_widget.size.x = .{ .percent = 1 }; } } } diff --git a/src/ui.zig b/src/ui.zig index 74391f8..e87bee9 100644 --- a/src/ui.zig +++ b/src/ui.zig @@ -4,10 +4,11 @@ const Assets = @import("./assets.zig"); const rect_utils = @import("./rect-utils.zig"); const srcery = @import("./srcery.zig"); -const log = std.lgo.scoped(.ui); +const log = std.log.scoped(.ui); const assert = std.debug.assert; const Vec2 = rl.Vector2; const Rect = rl.Rectangle; +const clamp = std.math.clamp; const UI = @This(); @@ -225,11 +226,21 @@ 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 }, + + sroll_offset: f32 = 0 }; pub const Flag = enum { clickable, - draggable + + draggable_x, + draggable_y, + + fixed_x, + fixed_y, + fixed_width, + fixed_height }; pub const Flags = std.EnumSet(Flag); @@ -239,8 +250,7 @@ pub const Box = struct { key: Key, size: Vec2Size = Vec2Size.zero(), flags: Flags = .{}, - override_x: ?f32 = null, - override_y: ?f32 = null, + fixed_rect: Rect = .{ .x = 0, .y = 0, .width = 0, .height = 0 }, background: ?rl.Color = null, rounded: bool = false, layout_axis: Axis = .X, @@ -251,6 +261,7 @@ pub const Box = struct { color: rl.Color = srcery.bright_white } = null, texture: ?rl.Texture2D = null, + view_offset: Vec2 = .{ .x = 0, .y = 0 }, persistent: Persistent = .{}, @@ -282,6 +293,83 @@ pub const Box = struct { .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)) { + 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; + } + + return switch (axis) { + .X => self.fixed_rect.width, + .Y => self.fixed_rect.height + }; + } + + fn isPositionFixed(self: *Box, axis: Axis) bool { + const flag = switch (axis) { + .X => Flag.fixed_x, + .Y => Flag.fixed_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); + } }; const BoxChildIterator = struct { @@ -351,13 +439,16 @@ pub fn begin(self: *UI) void { self.mouse_delta = mouse.subtract(self.mouse); const active_box_flags = self.getActiveBoxFlags(); - if (active_box_flags.contains(.draggable)) { - const mouse_x = rl.getMouseX(); - const mouse_y = rl.getMouseY(); - + if (active_box_flags.contains(.draggable_x)) { rl.setMousePosition( - @mod(mouse_x, @as(i32, @intFromFloat(self.window_size.x))), - @mod(mouse_y, @as(i32, @intFromFloat(self.window_size.y))) + @mod(rl.getMouseX(), window_width), + rl.getMouseY() + ); + } + if (active_box_flags.contains(.draggable_y)) { + rl.setMousePosition( + rl.getMouseX(), + @mod(rl.getMouseY(), window_height) ); } @@ -420,8 +511,10 @@ pub fn end(self: *UI) void { } } - if (active_box_flags.contains(.draggable)) { + if (active_box_flags.contains(.draggable_x)) { rl.setMouseCursor(@intFromEnum(rl.MouseCursor.mouse_cursor_resize_ew)); + } else if (active_box_flags.contains(.draggable_y)) { + rl.setMouseCursor(@intFromEnum(rl.MouseCursor.mouse_cursor_resize_ns)); } else if (hover_box_flags.contains(.clickable)) { rl.setMouseCursor(@intFromEnum(rl.MouseCursor.mouse_cursor_pointing_hand)); } else { @@ -526,6 +619,17 @@ pub fn draw(self: *UI) void { fn drawBox(self: *UI, box: *Box) void { const box_rect = box.computedRect(); + const do_scissor = box.view_offset.equals(Vec2.zero()) == 0; + if (do_scissor) { + rl.beginScissorMode( + @intFromFloat(box_rect.x), + @intFromFloat(box_rect.y), + @intFromFloat(box_rect.width), + @intFromFloat(box_rect.height) + ); + } + defer if (do_scissor) rl.endScissorMode(); + if (box.background) |background| { rl.drawRectangleRec(box_rect, background); } @@ -592,7 +696,9 @@ fn calcLayoutStandaloneSize(self: *UI, box: *Box, axis: Axis) void { const size = box.size.getAxis(axis); const computed_size = getVec2Axis(&box.persistent.size, axis); - if (size.kind == .pixels) { + if (box.getFixedSizeAxis(axis)) |fixed_size| { + computed_size.* = fixed_size; + } else if (size.kind == .pixels) { computed_size.* = size.kind.pixels; } else if (size.kind == .text) { if (box.text) |text| { @@ -621,7 +727,11 @@ fn calcLayoutUpwardsSize(self: *UI, box: *Box, axis: Axis) void { var parent_iter = self.iterUpwardByParent(box); while (parent_iter.next()) |parent| { const parent_size_kind = parent.size.getAxis(axis).kind; - if (parent_size_kind == .pixels or parent_size_kind == .percent or parent_size_kind == .text) { + if (parent.isSizeFixed(axis) or + parent_size_kind == .pixels or + parent_size_kind == .percent or + parent_size_kind == .text) + { maybe_fixed_parent = parent; break; } @@ -655,6 +765,8 @@ fn calcLayoutDownardsSize(self: *UI, box: *Box, axis: Axis) void { 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) { @@ -675,24 +787,51 @@ fn calcLayoutPositions(self: *UI, box: *Box, axis: Axis) void { var child_iter = self.iterChildrenByParent(box); while (child_iter.next()) |child| { const child_axis_position = getVec2Axis(&child.persistent.position, axis); - const child_axis_size = getVec2Axis(&child.persistent.size, axis); - const parent_axis_position = getVec2Axis(&box.persistent.position, axis); - const child_override_position = switch (axis) { - .X => child.override_x, - .Y => child.override_y, - }; - child_axis_position.* = parent_axis_position.*; + if (child.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); - if (child_override_position) |position| { - child_axis_position.* += position; - } else if (box.layout_axis == axis) { - child_axis_position.* += layout_position; - layout_position += child_axis_size.*; + child_axis_position.* = parent_axis_position.*; + + if (box.layout_axis == axis) { + child_axis_position.* += layout_position; + layout_position += child_axis_size.*; + } } + + child_axis_position.* -= getVec2Axis(&box.view_offset, axis).*; } } + if (box.layout_axis == axis) { + var child_size_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); + child_size_sum += child_size.*; + } + + 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| { @@ -752,7 +891,6 @@ fn calcLayoutEnforceConstraints(self: *UI, box: *Box, axis: Axis) void { } } - { var child_iter = self.iterChildrenByParent(box); while (child_iter.next()) |child| { @@ -761,12 +899,16 @@ fn calcLayoutEnforceConstraints(self: *UI, box: *Box, axis: Axis) void { } } -pub fn newBoxFromString(self: *UI, text: []const u8) *Box { +pub fn newKeyFromString(self: *UI, text: []const u8) Key { var parent_hash: u64 = 0; if (self.getParent()) |parent| { parent_hash = parent.key.hash; } - return self.newBox(Key.initString(parent_hash, text)); + return Key.initString(parent_hash, text); +} + +pub fn newBoxFromString(self: *UI, text: []const u8) *Box { + return self.newBox(self.newKeyFromString(text)); } pub fn newBoxFromPtr(self: *UI, ptr: anytype) *Box { @@ -832,7 +974,7 @@ pub fn signalFromBox(self: *UI, box: *Box) Signal { const rect = box.computedRect(); const is_mouse_inside = rect_utils.isInsideVec2(rect, self.mouse); const clickable = box.flags.contains(.clickable); - const draggable = box.flags.contains(.draggable); + const draggable = box.flags.contains(.draggable_x) or box.flags.contains(.draggable_y); var event_index: usize = 0; while (event_index < self.events.len) { @@ -952,3 +1094,64 @@ pub fn iterUpwardByParent(self: *UI, box: *const Box) BoxParentIterator { 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 scroll_area = self.newBoxFromString("Scroll area"); + scroll_area.size = .{ + .x = UI.Size.percent(1, 0), + .y = UI.Size.percent(1, 0), + }; + self.pushParent(scroll_area); + + return scroll_area; +} + +pub fn popScrollbar(self: *UI) void { + const scroll_area = self.getParent().?; + self.popParent(); // pop scroll area + + const scrollbar_area = self.newBoxFromString("Scrollbar area"); + scrollbar_area.background = rl.Color.gold; + scrollbar_area.size = .{ + .x = UI.Size.pixels(24, 1), + .y = UI.Size.percent(1, 0), + }; + self.pushParent(scrollbar_area); + defer self.popParent(); + + var scrollbar_size_percent = scroll_area.persistent.size.y / scroll_area.persistent.children_size.y; + scrollbar_size_percent = std.math.clamp(scrollbar_size_percent, 0, 1); + + const draggable = self.newBoxFromString("Scrollbar button"); + draggable.background = rl.Color.dark_brown; + draggable.flags.insert(.clickable); + draggable.flags.insert(.draggable_y); + draggable.size = .{ + .x = UI.Size.percent(1, 1), + .y = UI.Size.percent(scrollbar_size_percent, 1), + }; + + const scrollbar_area_height = scrollbar_area.persistent.size.y; + const scrollbar_max_move = scrollbar_area_height * (1 - scrollbar_size_percent); + + const sroll_offset = &draggable.persistent.sroll_offset; + const signal = self.signalFromBox(draggable); + if (signal.dragged()) { + sroll_offset.* += signal.drag.y / scrollbar_max_move; + sroll_offset.* = clamp(sroll_offset.*, 0, 1); + } + + draggable.setFixedY(scrollbar_area.persistent.position.y + sroll_offset.* * scrollbar_max_move); + scroll_area.view_offset.y = sroll_offset.* * (1 - scrollbar_size_percent) * scroll_area.persistent.children_size.y; + + self.popParent(); // pop container +} \ No newline at end of file