feat: Add LEGO Panel System
A modular panel system for building complex layouts: Panel: - Basic container with borders, title, focus state - Min/max width/height constraints - Content render callback PanelSplit: - Split container (horizontal/vertical) - Configurable ratios per child - Gap between panels TabbedPanel: - Multiple panels with tab navigation - Tab bar rendering - Per-tab content renderers DockingPanel: - Dockable positions (left/right/top/bottom/center) - Floating mode with custom position/size - Show/hide/close functionality PanelManager: - Manages collection of panels - Focus navigation - Layered rendering (docked → center → floating) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
04e18ff0c1
commit
d3e42a241d
2 changed files with 775 additions and 0 deletions
|
|
@ -172,6 +172,15 @@ pub const widgets = struct {
|
|||
pub const ScrollState = scroll_mod.ScrollState;
|
||||
pub const VirtualList = scroll_mod.VirtualList;
|
||||
pub const InfiniteScroll = scroll_mod.InfiniteScroll;
|
||||
|
||||
pub const panel_mod = @import("widgets/panel.zig");
|
||||
pub const Panel = panel_mod.Panel;
|
||||
pub const PanelSplit = panel_mod.PanelSplit;
|
||||
pub const TabbedPanel = panel_mod.TabbedPanel;
|
||||
pub const DockingPanel = panel_mod.DockingPanel;
|
||||
pub const DockPosition = panel_mod.DockPosition;
|
||||
pub const PanelManager = panel_mod.PanelManager;
|
||||
pub const SplitDirection = panel_mod.SplitDirection;
|
||||
};
|
||||
|
||||
// Backend
|
||||
|
|
|
|||
766
src/widgets/panel.zig
Normal file
766
src/widgets/panel.zig
Normal file
|
|
@ -0,0 +1,766 @@
|
|||
//! LEGO Panel System for zcatui.
|
||||
//!
|
||||
//! A modular panel system that allows building complex layouts by
|
||||
//! combining and nesting panels, similar to LEGO blocks.
|
||||
//!
|
||||
//! ## Features
|
||||
//!
|
||||
//! - **Panel**: Basic container with borders and title
|
||||
//! - **PanelGroup**: Split container (horizontal/vertical)
|
||||
//! - **TabbedPanel**: Multiple panels with tab navigation
|
||||
//! - **ResizablePanel**: Draggable dividers for resizing
|
||||
//! - **DockingPanel**: Dockable/floating panels
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```zig
|
||||
//! // Create a layout with sidebar and main area
|
||||
//! var layout = PanelGroup.horizontal(&.{
|
||||
//! Panel.init("Sidebar").setMinWidth(20),
|
||||
//! PanelGroup.vertical(&.{
|
||||
//! Panel.init("Main Content"),
|
||||
//! Panel.init("Console").setMaxHeight(10),
|
||||
//! }),
|
||||
//! });
|
||||
//!
|
||||
//! layout.render(area, buf);
|
||||
//! ```
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const buffer_mod = @import("../buffer.zig");
|
||||
const Buffer = buffer_mod.Buffer;
|
||||
const Rect = buffer_mod.Rect;
|
||||
const style_mod = @import("../style.zig");
|
||||
const Style = style_mod.Style;
|
||||
const Color = style_mod.Color;
|
||||
const block_mod = @import("block.zig");
|
||||
const Block = block_mod.Block;
|
||||
const Borders = block_mod.Borders;
|
||||
const layout_mod = @import("../layout.zig");
|
||||
const Layout = layout_mod.Layout;
|
||||
const Constraint = layout_mod.Constraint;
|
||||
|
||||
// ============================================================================
|
||||
// Panel
|
||||
// ============================================================================
|
||||
|
||||
/// A basic panel widget.
|
||||
pub const Panel = struct {
|
||||
/// Panel title.
|
||||
title: ?[]const u8 = null,
|
||||
|
||||
/// Panel ID (for identification).
|
||||
id: ?[]const u8 = null,
|
||||
|
||||
/// Base style.
|
||||
style: Style = Style.default,
|
||||
|
||||
/// Border style.
|
||||
border_style: Style = Style.default,
|
||||
|
||||
/// Whether panel is focused.
|
||||
focused: bool = false,
|
||||
|
||||
/// Focused style.
|
||||
focused_style: Style = Style.default.fg(Color.cyan),
|
||||
|
||||
/// Whether to show borders.
|
||||
show_border: bool = true,
|
||||
|
||||
/// Minimum width.
|
||||
min_width: u16 = 1,
|
||||
|
||||
/// Minimum height.
|
||||
min_height: u16 = 1,
|
||||
|
||||
/// Maximum width (0 = unlimited).
|
||||
max_width: u16 = 0,
|
||||
|
||||
/// Maximum height (0 = unlimited).
|
||||
max_height: u16 = 0,
|
||||
|
||||
/// Content render function.
|
||||
render_content: ?*const fn (*Panel, Rect, *Buffer) void = null,
|
||||
|
||||
/// User data.
|
||||
user_data: ?*anyopaque = null,
|
||||
|
||||
/// Creates a new panel.
|
||||
pub fn init(title: ?[]const u8) Panel {
|
||||
return .{
|
||||
.title = title,
|
||||
};
|
||||
}
|
||||
|
||||
/// Sets the title.
|
||||
pub fn setTitle(self: Panel, title: []const u8) Panel {
|
||||
var p = self;
|
||||
p.title = title;
|
||||
return p;
|
||||
}
|
||||
|
||||
/// Sets the ID.
|
||||
pub fn setId(self: Panel, id: []const u8) Panel {
|
||||
var p = self;
|
||||
p.id = id;
|
||||
return p;
|
||||
}
|
||||
|
||||
/// Sets the style.
|
||||
pub fn setStyle(self: Panel, style: Style) Panel {
|
||||
var p = self;
|
||||
p.style = style;
|
||||
return p;
|
||||
}
|
||||
|
||||
/// Sets whether focused.
|
||||
pub fn setFocused(self: Panel, focused: bool) Panel {
|
||||
var p = self;
|
||||
p.focused = focused;
|
||||
return p;
|
||||
}
|
||||
|
||||
/// Sets minimum width.
|
||||
pub fn setMinWidth(self: Panel, width: u16) Panel {
|
||||
var p = self;
|
||||
p.min_width = width;
|
||||
return p;
|
||||
}
|
||||
|
||||
/// Sets minimum height.
|
||||
pub fn setMinHeight(self: Panel, height: u16) Panel {
|
||||
var p = self;
|
||||
p.min_height = height;
|
||||
return p;
|
||||
}
|
||||
|
||||
/// Sets maximum width.
|
||||
pub fn setMaxWidth(self: Panel, width: u16) Panel {
|
||||
var p = self;
|
||||
p.max_width = width;
|
||||
return p;
|
||||
}
|
||||
|
||||
/// Sets maximum height.
|
||||
pub fn setMaxHeight(self: Panel, height: u16) Panel {
|
||||
var p = self;
|
||||
p.max_height = height;
|
||||
return p;
|
||||
}
|
||||
|
||||
/// Sets content renderer.
|
||||
pub fn setRenderer(self: Panel, render_fn: *const fn (*Panel, Rect, *Buffer) void) Panel {
|
||||
var p = self;
|
||||
p.render_content = render_fn;
|
||||
return p;
|
||||
}
|
||||
|
||||
/// Gets the inner area (content area after borders).
|
||||
pub fn innerArea(self: Panel, area: Rect) Rect {
|
||||
if (self.show_border) {
|
||||
return Rect.init(
|
||||
area.x + 1,
|
||||
area.y + 1,
|
||||
area.width -| 2,
|
||||
area.height -| 2,
|
||||
);
|
||||
}
|
||||
return area;
|
||||
}
|
||||
|
||||
/// Renders the panel.
|
||||
pub fn render(self: *Panel, area: Rect, buf: *Buffer) void {
|
||||
// Clear area
|
||||
buf.setStyle(area, self.style);
|
||||
|
||||
// Render border
|
||||
if (self.show_border) {
|
||||
const bs = if (self.focused) self.focused_style else self.border_style;
|
||||
var block = Block.init().setBorders(Borders.all).style(bs);
|
||||
|
||||
if (self.title) |t| {
|
||||
block = block.title(t);
|
||||
}
|
||||
|
||||
block.render(area, buf);
|
||||
}
|
||||
|
||||
// Render content
|
||||
if (self.render_content) |render_fn| {
|
||||
render_fn(self, self.innerArea(area), buf);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// PanelSplit
|
||||
// ============================================================================
|
||||
|
||||
/// Split direction.
|
||||
pub const SplitDirection = enum {
|
||||
horizontal,
|
||||
vertical,
|
||||
};
|
||||
|
||||
/// A panel split into multiple sections.
|
||||
pub const PanelSplit = struct {
|
||||
/// Split direction.
|
||||
direction: SplitDirection,
|
||||
|
||||
/// Child panels.
|
||||
children: []Panel,
|
||||
|
||||
/// Ratios for each child (1.0 = equal).
|
||||
ratios: []f32,
|
||||
|
||||
/// Allocator.
|
||||
allocator: Allocator,
|
||||
|
||||
/// Gap between panels.
|
||||
gap: u16 = 0,
|
||||
|
||||
/// Creates a horizontal split.
|
||||
pub fn horizontal(allocator: Allocator, panels: []const Panel) !PanelSplit {
|
||||
return try create(allocator, .horizontal, panels);
|
||||
}
|
||||
|
||||
/// Creates a vertical split.
|
||||
pub fn vertical(allocator: Allocator, panels: []const Panel) !PanelSplit {
|
||||
return try create(allocator, .vertical, panels);
|
||||
}
|
||||
|
||||
fn create(allocator: Allocator, direction: SplitDirection, panels: []const Panel) !PanelSplit {
|
||||
const children = try allocator.alloc(Panel, panels.len);
|
||||
@memcpy(children, panels);
|
||||
|
||||
const ratios = try allocator.alloc(f32, panels.len);
|
||||
for (ratios) |*r| {
|
||||
r.* = 1.0;
|
||||
}
|
||||
|
||||
return .{
|
||||
.direction = direction,
|
||||
.children = children,
|
||||
.ratios = ratios,
|
||||
.allocator = allocator,
|
||||
};
|
||||
}
|
||||
|
||||
/// Frees resources.
|
||||
pub fn deinit(self: *PanelSplit) void {
|
||||
self.allocator.free(self.children);
|
||||
self.allocator.free(self.ratios);
|
||||
}
|
||||
|
||||
/// Sets ratio for a child.
|
||||
pub fn setRatio(self: *PanelSplit, index: usize, ratio: f32) void {
|
||||
if (index < self.ratios.len) {
|
||||
self.ratios[index] = ratio;
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates areas for children.
|
||||
pub fn calculateAreas(self: PanelSplit, area: Rect) []Rect {
|
||||
// Static buffer for simplicity
|
||||
var areas: [16]Rect = undefined;
|
||||
|
||||
if (self.children.len == 0) return areas[0..0];
|
||||
if (self.children.len > 16) return areas[0..16];
|
||||
|
||||
// Calculate total ratio
|
||||
var total_ratio: f32 = 0;
|
||||
for (self.ratios[0..self.children.len]) |r| {
|
||||
total_ratio += r;
|
||||
}
|
||||
|
||||
// Calculate sizes
|
||||
const total_gap = self.gap * @as(u16, @intCast(self.children.len - 1));
|
||||
const available = switch (self.direction) {
|
||||
.horizontal => area.width -| total_gap,
|
||||
.vertical => area.height -| total_gap,
|
||||
};
|
||||
|
||||
var pos: u16 = switch (self.direction) {
|
||||
.horizontal => area.x,
|
||||
.vertical => area.y,
|
||||
};
|
||||
|
||||
var remaining = available;
|
||||
for (self.children, 0..) |_, i| {
|
||||
const ratio = self.ratios[i] / total_ratio;
|
||||
const size: u16 = if (i == self.children.len - 1)
|
||||
remaining
|
||||
else
|
||||
@intFromFloat(@as(f32, @floatFromInt(available)) * ratio);
|
||||
|
||||
areas[i] = switch (self.direction) {
|
||||
.horizontal => Rect.init(pos, area.y, size, area.height),
|
||||
.vertical => Rect.init(area.x, pos, area.width, size),
|
||||
};
|
||||
|
||||
pos += size + self.gap;
|
||||
remaining -|= size;
|
||||
}
|
||||
|
||||
return areas[0..self.children.len];
|
||||
}
|
||||
|
||||
/// Renders the split.
|
||||
pub fn render(self: *PanelSplit, area: Rect, buf: *Buffer) void {
|
||||
const child_areas = self.calculateAreas(area);
|
||||
|
||||
for (self.children, 0..) |*child, i| {
|
||||
if (i < child_areas.len) {
|
||||
child.render(child_areas[i], buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// TabbedPanel
|
||||
// ============================================================================
|
||||
|
||||
/// A panel with tabs.
|
||||
pub const TabbedPanel = struct {
|
||||
/// Tab titles.
|
||||
tabs: []const []const u8,
|
||||
|
||||
/// Currently selected tab.
|
||||
selected: usize = 0,
|
||||
|
||||
/// Tab bar height.
|
||||
tab_height: u16 = 1,
|
||||
|
||||
/// Style.
|
||||
style: Style = Style.default,
|
||||
|
||||
/// Selected tab style.
|
||||
selected_style: Style = Style.default.bg(Color.blue).fg(Color.white),
|
||||
|
||||
/// Content render functions (one per tab).
|
||||
renderers: []const ?*const fn (usize, Rect, *Buffer) void = &.{},
|
||||
|
||||
/// Creates a tabbed panel.
|
||||
pub fn init(tabs: []const []const u8) TabbedPanel {
|
||||
return .{
|
||||
.tabs = tabs,
|
||||
};
|
||||
}
|
||||
|
||||
/// Sets renderers for each tab.
|
||||
pub fn setRenderers(self: TabbedPanel, renderers: []const ?*const fn (usize, Rect, *Buffer) void) TabbedPanel {
|
||||
var tp = self;
|
||||
tp.renderers = renderers;
|
||||
return tp;
|
||||
}
|
||||
|
||||
/// Selects a tab.
|
||||
pub fn select(self: *TabbedPanel, index: usize) void {
|
||||
if (index < self.tabs.len) {
|
||||
self.selected = index;
|
||||
}
|
||||
}
|
||||
|
||||
/// Selects next tab.
|
||||
pub fn selectNext(self: *TabbedPanel) void {
|
||||
if (self.selected + 1 < self.tabs.len) {
|
||||
self.selected += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Selects previous tab.
|
||||
pub fn selectPrev(self: *TabbedPanel) void {
|
||||
if (self.selected > 0) {
|
||||
self.selected -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the tabbed panel.
|
||||
pub fn render(self: *TabbedPanel, area: Rect, buf: *Buffer) void {
|
||||
if (area.height < self.tab_height + 2) return;
|
||||
|
||||
// Render tab bar
|
||||
const tab_area = Rect.init(area.x, area.y, area.width, self.tab_height);
|
||||
self.renderTabs(tab_area, buf);
|
||||
|
||||
// Render content
|
||||
const content_area = Rect.init(
|
||||
area.x,
|
||||
area.y + self.tab_height,
|
||||
area.width,
|
||||
area.height - self.tab_height,
|
||||
);
|
||||
|
||||
// Border around content
|
||||
const block = Block.init().setBorders(Borders.all);
|
||||
block.render(content_area, buf);
|
||||
|
||||
// Content
|
||||
if (self.selected < self.renderers.len) {
|
||||
if (self.renderers[self.selected]) |render_fn| {
|
||||
render_fn(self.selected, block.inner(content_area), buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn renderTabs(self: *TabbedPanel, area: Rect, buf: *Buffer) void {
|
||||
var x = area.x;
|
||||
|
||||
for (self.tabs, 0..) |tab, i| {
|
||||
const tab_width: u16 = @intCast(@min(tab.len + 2, area.width -| x));
|
||||
if (tab_width == 0) break;
|
||||
|
||||
const tab_style = if (i == self.selected) self.selected_style else self.style;
|
||||
|
||||
// Fill tab background
|
||||
var tx = x;
|
||||
while (tx < x + tab_width) : (tx += 1) {
|
||||
if (buf.getCell(tx, area.y)) |cell| {
|
||||
cell.setChar(' ');
|
||||
cell.setStyle(tab_style);
|
||||
}
|
||||
}
|
||||
|
||||
// Tab text
|
||||
_ = buf.setString(x + 1, area.y, tab, tab_style);
|
||||
|
||||
x += tab_width + 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// DockPosition
|
||||
// ============================================================================
|
||||
|
||||
/// Docking position.
|
||||
pub const DockPosition = enum {
|
||||
left,
|
||||
right,
|
||||
top,
|
||||
bottom,
|
||||
center,
|
||||
floating,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// DockingPanel
|
||||
// ============================================================================
|
||||
|
||||
/// A dockable/floatable panel.
|
||||
pub const DockingPanel = struct {
|
||||
/// Inner panel.
|
||||
panel: Panel,
|
||||
|
||||
/// Current dock position.
|
||||
position: DockPosition = .center,
|
||||
|
||||
/// Floating position (when floating).
|
||||
float_x: u16 = 10,
|
||||
float_y: u16 = 5,
|
||||
|
||||
/// Floating size.
|
||||
float_width: u16 = 40,
|
||||
float_height: u16 = 15,
|
||||
|
||||
/// Whether panel is visible.
|
||||
visible: bool = true,
|
||||
|
||||
/// Whether panel can be closed.
|
||||
closable: bool = true,
|
||||
|
||||
/// Whether panel can float.
|
||||
floatable: bool = true,
|
||||
|
||||
/// Size when docked (percentage 1-100).
|
||||
dock_size: u8 = 25,
|
||||
|
||||
/// Creates a docking panel.
|
||||
pub fn init(title: []const u8) DockingPanel {
|
||||
return .{
|
||||
.panel = Panel.init(title),
|
||||
};
|
||||
}
|
||||
|
||||
/// Sets dock position.
|
||||
pub fn setPosition(self: DockingPanel, pos: DockPosition) DockingPanel {
|
||||
var dp = self;
|
||||
dp.position = pos;
|
||||
return dp;
|
||||
}
|
||||
|
||||
/// Sets dock size percentage.
|
||||
pub fn setDockSize(self: DockingPanel, size: u8) DockingPanel {
|
||||
var dp = self;
|
||||
dp.dock_size = @min(size, 100);
|
||||
return dp;
|
||||
}
|
||||
|
||||
/// Toggles floating.
|
||||
pub fn toggleFloat(self: *DockingPanel) void {
|
||||
if (self.floatable) {
|
||||
if (self.position == .floating) {
|
||||
self.position = .center;
|
||||
} else {
|
||||
self.position = .floating;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows the panel.
|
||||
pub fn show(self: *DockingPanel) void {
|
||||
self.visible = true;
|
||||
}
|
||||
|
||||
/// Hides the panel.
|
||||
pub fn hide(self: *DockingPanel) void {
|
||||
self.visible = false;
|
||||
}
|
||||
|
||||
/// Closes the panel.
|
||||
pub fn close(self: *DockingPanel) void {
|
||||
if (self.closable) {
|
||||
self.visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates panel area based on dock position.
|
||||
pub fn calculateArea(self: DockingPanel, bounds: Rect) Rect {
|
||||
if (!self.visible) return Rect.init(0, 0, 0, 0);
|
||||
|
||||
switch (self.position) {
|
||||
.floating => {
|
||||
return Rect.init(
|
||||
self.float_x,
|
||||
self.float_y,
|
||||
@min(self.float_width, bounds.width -| self.float_x),
|
||||
@min(self.float_height, bounds.height -| self.float_y),
|
||||
);
|
||||
},
|
||||
.left => {
|
||||
const width = bounds.width * self.dock_size / 100;
|
||||
return Rect.init(bounds.x, bounds.y, width, bounds.height);
|
||||
},
|
||||
.right => {
|
||||
const width = bounds.width * self.dock_size / 100;
|
||||
return Rect.init(bounds.x + bounds.width - width, bounds.y, width, bounds.height);
|
||||
},
|
||||
.top => {
|
||||
const height = bounds.height * self.dock_size / 100;
|
||||
return Rect.init(bounds.x, bounds.y, bounds.width, height);
|
||||
},
|
||||
.bottom => {
|
||||
const height = bounds.height * self.dock_size / 100;
|
||||
return Rect.init(bounds.x, bounds.y + bounds.height - height, bounds.width, height);
|
||||
},
|
||||
.center => return bounds,
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the panel.
|
||||
pub fn render(self: *DockingPanel, bounds: Rect, buf: *Buffer) void {
|
||||
if (!self.visible) return;
|
||||
|
||||
const area = self.calculateArea(bounds);
|
||||
if (area.width == 0 or area.height == 0) return;
|
||||
|
||||
self.panel.render(area, buf);
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// PanelManager
|
||||
// ============================================================================
|
||||
|
||||
/// Manages a collection of panels.
|
||||
pub const PanelManager = struct {
|
||||
/// All panels.
|
||||
panels: std.ArrayList(DockingPanel),
|
||||
|
||||
/// Currently focused panel index.
|
||||
focused: ?usize = null,
|
||||
|
||||
/// Allocator.
|
||||
allocator: Allocator,
|
||||
|
||||
/// Creates a new panel manager.
|
||||
pub fn init(allocator: Allocator) PanelManager {
|
||||
return .{
|
||||
.panels = std.ArrayList(DockingPanel).init(allocator),
|
||||
.allocator = allocator,
|
||||
};
|
||||
}
|
||||
|
||||
/// Frees resources.
|
||||
pub fn deinit(self: *PanelManager) void {
|
||||
self.panels.deinit();
|
||||
}
|
||||
|
||||
/// Adds a panel.
|
||||
pub fn add(self: *PanelManager, panel: DockingPanel) !usize {
|
||||
try self.panels.append(panel);
|
||||
return self.panels.items.len - 1;
|
||||
}
|
||||
|
||||
/// Gets a panel by index.
|
||||
pub fn get(self: *PanelManager, index: usize) ?*DockingPanel {
|
||||
if (index < self.panels.items.len) {
|
||||
return &self.panels.items[index];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Focuses a panel.
|
||||
pub fn focus(self: *PanelManager, index: usize) void {
|
||||
// Unfocus previous
|
||||
if (self.focused) |f| {
|
||||
if (f < self.panels.items.len) {
|
||||
self.panels.items[f].panel.focused = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Focus new
|
||||
if (index < self.panels.items.len) {
|
||||
self.focused = index;
|
||||
self.panels.items[index].panel.focused = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Focuses next panel.
|
||||
pub fn focusNext(self: *PanelManager) void {
|
||||
if (self.panels.items.len == 0) return;
|
||||
|
||||
const next = if (self.focused) |f|
|
||||
(f + 1) % self.panels.items.len
|
||||
else
|
||||
0;
|
||||
|
||||
self.focus(next);
|
||||
}
|
||||
|
||||
/// Renders all panels.
|
||||
pub fn render(self: *PanelManager, bounds: Rect, buf: *Buffer) void {
|
||||
// Render docked panels first (left, right, top, bottom)
|
||||
var remaining = bounds;
|
||||
|
||||
for (self.panels.items) |*panel| {
|
||||
if (!panel.visible) continue;
|
||||
|
||||
switch (panel.position) {
|
||||
.left => {
|
||||
const area = panel.calculateArea(remaining);
|
||||
panel.panel.render(area, buf);
|
||||
remaining.x += area.width;
|
||||
remaining.width -|= area.width;
|
||||
},
|
||||
.right => {
|
||||
const area = panel.calculateArea(remaining);
|
||||
panel.panel.render(area, buf);
|
||||
remaining.width -|= area.width;
|
||||
},
|
||||
.top => {
|
||||
const area = panel.calculateArea(remaining);
|
||||
panel.panel.render(area, buf);
|
||||
remaining.y += area.height;
|
||||
remaining.height -|= area.height;
|
||||
},
|
||||
.bottom => {
|
||||
const area = panel.calculateArea(remaining);
|
||||
panel.panel.render(area, buf);
|
||||
remaining.height -|= area.height;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
// Render center panels
|
||||
for (self.panels.items) |*panel| {
|
||||
if (!panel.visible) continue;
|
||||
if (panel.position == .center) {
|
||||
panel.panel.render(remaining, buf);
|
||||
}
|
||||
}
|
||||
|
||||
// Render floating panels last (on top)
|
||||
for (self.panels.items) |*panel| {
|
||||
if (!panel.visible) continue;
|
||||
if (panel.position == .floating) {
|
||||
panel.render(bounds, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
test "Panel basic" {
|
||||
var panel = Panel.init("Test");
|
||||
try std.testing.expectEqualStrings("Test", panel.title.?);
|
||||
|
||||
panel = panel.setMinWidth(10).setMaxHeight(20);
|
||||
try std.testing.expectEqual(@as(u16, 10), panel.min_width);
|
||||
try std.testing.expectEqual(@as(u16, 20), panel.max_height);
|
||||
}
|
||||
|
||||
test "Panel innerArea" {
|
||||
const panel = Panel.init("Test");
|
||||
const area = Rect.init(0, 0, 40, 20);
|
||||
const inner = panel.innerArea(area);
|
||||
|
||||
try std.testing.expectEqual(@as(u16, 1), inner.x);
|
||||
try std.testing.expectEqual(@as(u16, 1), inner.y);
|
||||
try std.testing.expectEqual(@as(u16, 38), inner.width);
|
||||
try std.testing.expectEqual(@as(u16, 18), inner.height);
|
||||
}
|
||||
|
||||
test "TabbedPanel selection" {
|
||||
var tabs = TabbedPanel.init(&.{ "Tab 1", "Tab 2", "Tab 3" });
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 0), tabs.selected);
|
||||
|
||||
tabs.selectNext();
|
||||
try std.testing.expectEqual(@as(usize, 1), tabs.selected);
|
||||
|
||||
tabs.selectNext();
|
||||
try std.testing.expectEqual(@as(usize, 2), tabs.selected);
|
||||
|
||||
tabs.selectNext(); // Should stay at 2
|
||||
try std.testing.expectEqual(@as(usize, 2), tabs.selected);
|
||||
|
||||
tabs.selectPrev();
|
||||
try std.testing.expectEqual(@as(usize, 1), tabs.selected);
|
||||
}
|
||||
|
||||
test "DockingPanel area calculation" {
|
||||
const bounds = Rect.init(0, 0, 100, 50);
|
||||
|
||||
var panel = DockingPanel.init("Test").setPosition(.left).setDockSize(20);
|
||||
const area = panel.calculateArea(bounds);
|
||||
|
||||
try std.testing.expectEqual(@as(u16, 0), area.x);
|
||||
try std.testing.expectEqual(@as(u16, 20), area.width);
|
||||
try std.testing.expectEqual(@as(u16, 50), area.height);
|
||||
}
|
||||
|
||||
test "PanelManager" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var manager = PanelManager.init(allocator);
|
||||
defer manager.deinit();
|
||||
|
||||
const idx1 = try manager.add(DockingPanel.init("Panel 1"));
|
||||
const idx2 = try manager.add(DockingPanel.init("Panel 2"));
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 0), idx1);
|
||||
try std.testing.expectEqual(@as(usize, 1), idx2);
|
||||
|
||||
manager.focus(0);
|
||||
try std.testing.expectEqual(@as(?usize, 0), manager.focused);
|
||||
|
||||
manager.focusNext();
|
||||
try std.testing.expectEqual(@as(?usize, 1), manager.focused);
|
||||
}
|
||||
Loading…
Reference in a new issue