feat: Add ContextMenu and Tooltip widgets
ContextMenu: - Right-click context menu support - Auto-adjust position to fit in bounds - containsPoint for hit testing - Wraps existing Menu widget Tooltip: - Single and multiline text support - Configurable positioning (above/below/left/right/auto) - Customizable style, border, padding - TooltipManager with hover delay timing 🤖 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
b393008497
commit
8c218a3f0d
3 changed files with 700 additions and 0 deletions
|
|
@ -148,6 +148,12 @@ pub const widgets = struct {
|
|||
pub const MenuItemType = menu_mod.MenuItemType;
|
||||
pub const MenuBar = menu_mod.MenuBar;
|
||||
pub const MenuBarItem = menu_mod.MenuBarItem;
|
||||
pub const ContextMenu = menu_mod.ContextMenu;
|
||||
|
||||
pub const tooltip_mod = @import("widgets/tooltip.zig");
|
||||
pub const Tooltip = tooltip_mod.Tooltip;
|
||||
pub const TooltipPosition = tooltip_mod.TooltipPosition;
|
||||
pub const TooltipManager = tooltip_mod.TooltipManager;
|
||||
};
|
||||
|
||||
// Backend
|
||||
|
|
|
|||
|
|
@ -576,6 +576,184 @@ pub const MenuBarItem = struct {
|
|||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// ContextMenu
|
||||
// ============================================================================
|
||||
|
||||
/// A context menu (right-click menu) that appears at a specific position.
|
||||
///
|
||||
/// Wraps a Menu and handles positioning relative to mouse click location.
|
||||
pub const ContextMenu = struct {
|
||||
/// The underlying menu.
|
||||
menu: Menu,
|
||||
|
||||
/// Position where the menu should appear.
|
||||
x: u16 = 0,
|
||||
y: u16 = 0,
|
||||
|
||||
/// Whether the context menu is visible.
|
||||
visible: bool = false,
|
||||
|
||||
/// Whether to adjust position to fit within bounds.
|
||||
auto_adjust: bool = true,
|
||||
|
||||
/// Creates a new context menu.
|
||||
pub fn init() ContextMenu {
|
||||
return .{
|
||||
.menu = Menu.init(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Creates a context menu with items.
|
||||
pub fn withItems(items: []const MenuItem) ContextMenu {
|
||||
return .{
|
||||
.menu = Menu.init().setItems(items),
|
||||
};
|
||||
}
|
||||
|
||||
/// Sets the menu items.
|
||||
pub fn setItems(self: ContextMenu, items: []const MenuItem) ContextMenu {
|
||||
var cm = self;
|
||||
cm.menu = self.menu.setItems(items);
|
||||
return cm;
|
||||
}
|
||||
|
||||
/// Sets the menu style.
|
||||
pub fn setStyle(self: ContextMenu, style: Style) ContextMenu {
|
||||
var cm = self;
|
||||
cm.menu = self.menu.setStyle(style);
|
||||
return cm;
|
||||
}
|
||||
|
||||
/// Sets the selected item style.
|
||||
pub fn setSelectedStyle(self: ContextMenu, style: Style) ContextMenu {
|
||||
var cm = self;
|
||||
cm.menu = self.menu.setSelectedStyle(style);
|
||||
return cm;
|
||||
}
|
||||
|
||||
/// Shows the context menu at the specified position.
|
||||
pub fn show(self: *ContextMenu, x: u16, y: u16) void {
|
||||
self.x = x;
|
||||
self.y = y;
|
||||
self.visible = true;
|
||||
self.menu.selected = 0;
|
||||
}
|
||||
|
||||
/// Shows the context menu from a mouse event position.
|
||||
pub fn showAt(self: *ContextMenu, mouse_x: u16, mouse_y: u16) void {
|
||||
self.show(mouse_x, mouse_y);
|
||||
}
|
||||
|
||||
/// Hides the context menu.
|
||||
pub fn hide(self: *ContextMenu) void {
|
||||
self.visible = false;
|
||||
}
|
||||
|
||||
/// Toggles visibility.
|
||||
pub fn toggle(self: *ContextMenu, x: u16, y: u16) void {
|
||||
if (self.visible) {
|
||||
self.hide();
|
||||
} else {
|
||||
self.show(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the menu is visible.
|
||||
pub fn isVisible(self: ContextMenu) bool {
|
||||
return self.visible;
|
||||
}
|
||||
|
||||
/// Moves selection to next item.
|
||||
pub fn selectNext(self: *ContextMenu) void {
|
||||
self.menu.selectNext();
|
||||
}
|
||||
|
||||
/// Moves selection to previous item.
|
||||
pub fn selectPrev(self: *ContextMenu) void {
|
||||
self.menu.selectPrev();
|
||||
}
|
||||
|
||||
/// Returns the currently selected item.
|
||||
pub fn getSelectedItem(self: ContextMenu) ?MenuItem {
|
||||
return self.menu.getSelectedItem();
|
||||
}
|
||||
|
||||
/// Returns the selected item index.
|
||||
pub fn getSelectedIndex(self: ContextMenu) usize {
|
||||
return self.menu.selected;
|
||||
}
|
||||
|
||||
/// Checks if a point is within the menu bounds.
|
||||
pub fn containsPoint(self: ContextMenu, px: u16, py: u16, bounds: Rect) bool {
|
||||
const menu_area = self.getMenuArea(bounds);
|
||||
return px >= menu_area.x and px < menu_area.x + menu_area.width and
|
||||
py >= menu_area.y and py < menu_area.y + menu_area.height;
|
||||
}
|
||||
|
||||
/// Calculates the menu area, adjusting if needed to fit within bounds.
|
||||
pub fn getMenuArea(self: ContextMenu, bounds: Rect) Rect {
|
||||
const width = self.menu.calculateWidth();
|
||||
const height = self.menu.calculateHeight();
|
||||
|
||||
var menu_x = self.x;
|
||||
var menu_y = self.y;
|
||||
|
||||
if (self.auto_adjust) {
|
||||
// Adjust X to fit within bounds
|
||||
if (menu_x + width > bounds.x + bounds.width) {
|
||||
if (width <= self.x) {
|
||||
menu_x = self.x -| width;
|
||||
} else {
|
||||
menu_x = bounds.x + bounds.width -| width;
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust Y to fit within bounds
|
||||
if (menu_y + height > bounds.y + bounds.height) {
|
||||
if (height <= self.y) {
|
||||
menu_y = self.y -| height;
|
||||
} else {
|
||||
menu_y = bounds.y + bounds.height -| height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Rect.init(menu_x, menu_y, width, height);
|
||||
}
|
||||
|
||||
/// Renders the context menu if visible.
|
||||
pub fn render(self: ContextMenu, bounds: Rect, buf: *Buffer) void {
|
||||
if (!self.visible) return;
|
||||
|
||||
const menu_area = self.getMenuArea(bounds);
|
||||
|
||||
// Dim background slightly around menu (optional visual effect)
|
||||
// For now, just render the menu directly
|
||||
self.menu.render(menu_area, buf);
|
||||
}
|
||||
|
||||
/// Renders with background dimming.
|
||||
pub fn renderWithDim(self: ContextMenu, bounds: Rect, buf: *Buffer, dim_style: Style) void {
|
||||
if (!self.visible) return;
|
||||
|
||||
// Dim entire background
|
||||
var y = bounds.top();
|
||||
while (y < bounds.bottom()) : (y += 1) {
|
||||
var x = bounds.left();
|
||||
while (x < bounds.right()) : (x += 1) {
|
||||
if (buf.getCell(x, y)) |cell| {
|
||||
cell.setStyle(dim_style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render menu on top
|
||||
const menu_area = self.getMenuArea(bounds);
|
||||
self.menu.render(menu_area, buf);
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
|
@ -649,3 +827,58 @@ test "MenuBar navigation" {
|
|||
bar.closeMenus();
|
||||
try std.testing.expect(bar.open_menu == null);
|
||||
}
|
||||
|
||||
test "ContextMenu show/hide" {
|
||||
var ctx = ContextMenu.withItems(&.{
|
||||
MenuItem.action("Cut", 'x'),
|
||||
MenuItem.action("Copy", 'c'),
|
||||
MenuItem.action("Paste", 'v'),
|
||||
});
|
||||
|
||||
try std.testing.expect(!ctx.isVisible());
|
||||
|
||||
ctx.show(10, 20);
|
||||
try std.testing.expect(ctx.isVisible());
|
||||
try std.testing.expectEqual(@as(u16, 10), ctx.x);
|
||||
try std.testing.expectEqual(@as(u16, 20), ctx.y);
|
||||
|
||||
ctx.hide();
|
||||
try std.testing.expect(!ctx.isVisible());
|
||||
}
|
||||
|
||||
test "ContextMenu auto-adjust position" {
|
||||
const ctx = ContextMenu.withItems(&.{
|
||||
MenuItem.action("Item 1", null),
|
||||
MenuItem.action("Item 2", null),
|
||||
});
|
||||
|
||||
const bounds = Rect.init(0, 0, 80, 24);
|
||||
|
||||
// Menu at corner should adjust
|
||||
var ctx2 = ctx;
|
||||
ctx2.x = 75;
|
||||
ctx2.y = 22;
|
||||
const area = ctx2.getMenuArea(bounds);
|
||||
|
||||
// Should be adjusted to fit within bounds
|
||||
try std.testing.expect(area.x + area.width <= bounds.width);
|
||||
try std.testing.expect(area.y + area.height <= bounds.height);
|
||||
}
|
||||
|
||||
test "ContextMenu containsPoint" {
|
||||
var ctx = ContextMenu.withItems(&.{
|
||||
MenuItem.action("A", null),
|
||||
MenuItem.action("B", null),
|
||||
});
|
||||
ctx.x = 10;
|
||||
ctx.y = 10;
|
||||
ctx.visible = true;
|
||||
|
||||
const bounds = Rect.init(0, 0, 80, 24);
|
||||
|
||||
// Point inside menu
|
||||
try std.testing.expect(ctx.containsPoint(12, 11, bounds));
|
||||
|
||||
// Point outside menu
|
||||
try std.testing.expect(!ctx.containsPoint(0, 0, bounds));
|
||||
}
|
||||
|
|
|
|||
461
src/widgets/tooltip.zig
Normal file
461
src/widgets/tooltip.zig
Normal file
|
|
@ -0,0 +1,461 @@
|
|||
//! Tooltip widget for zcatui.
|
||||
//!
|
||||
//! Provides hover-style information popups:
|
||||
//! - Tooltip: Simple text tooltip
|
||||
//! - TooltipManager: Manages tooltip timing and positioning
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```zig
|
||||
//! var tooltip = Tooltip.init("Click to save your document")
|
||||
//! .setStyle(Style.default.bg(Color.yellow).fg(Color.black));
|
||||
//!
|
||||
//! // Show at position
|
||||
//! tooltip.showAt(mouse_x, mouse_y);
|
||||
//! tooltip.render(area, buf);
|
||||
//! ```
|
||||
|
||||
const std = @import("std");
|
||||
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;
|
||||
|
||||
// ============================================================================
|
||||
// Tooltip
|
||||
// ============================================================================
|
||||
|
||||
/// Position of tooltip relative to target.
|
||||
pub const TooltipPosition = enum {
|
||||
above,
|
||||
below,
|
||||
left,
|
||||
right,
|
||||
auto, // Automatically choose best position
|
||||
};
|
||||
|
||||
/// A simple tooltip widget.
|
||||
pub const Tooltip = struct {
|
||||
/// Tooltip text (single line).
|
||||
text: []const u8 = "",
|
||||
|
||||
/// Multi-line text (if set, overrides single text).
|
||||
lines: []const []const u8 = &.{},
|
||||
|
||||
/// Position relative to anchor point.
|
||||
x: u16 = 0,
|
||||
y: u16 = 0,
|
||||
|
||||
/// Whether tooltip is visible.
|
||||
visible: bool = false,
|
||||
|
||||
/// Preferred position relative to anchor.
|
||||
position: TooltipPosition = .auto,
|
||||
|
||||
/// Style for tooltip background and text.
|
||||
style: Style = Style.default.bg(Color.yellow).fg(Color.black),
|
||||
|
||||
/// Border style (null = no border).
|
||||
border: bool = true,
|
||||
border_style: Style = Style.default.fg(Color.black),
|
||||
|
||||
/// Padding inside tooltip.
|
||||
padding_x: u16 = 1,
|
||||
padding_y: u16 = 0,
|
||||
|
||||
/// Maximum width (0 = no limit).
|
||||
max_width: u16 = 60,
|
||||
|
||||
/// Arrow indicator character.
|
||||
show_arrow: bool = false,
|
||||
|
||||
/// Creates a new tooltip with text.
|
||||
pub fn init(text: []const u8) Tooltip {
|
||||
return .{
|
||||
.text = text,
|
||||
};
|
||||
}
|
||||
|
||||
/// Creates a tooltip with multiple lines.
|
||||
pub fn initMultiline(lines: []const []const u8) Tooltip {
|
||||
return .{
|
||||
.lines = lines,
|
||||
};
|
||||
}
|
||||
|
||||
/// Sets the text.
|
||||
pub fn setText(self: Tooltip, text: []const u8) Tooltip {
|
||||
var t = self;
|
||||
t.text = text;
|
||||
t.lines = &.{};
|
||||
return t;
|
||||
}
|
||||
|
||||
/// Sets multiple lines.
|
||||
pub fn setLines(self: Tooltip, lines: []const []const u8) Tooltip {
|
||||
var t = self;
|
||||
t.lines = lines;
|
||||
return t;
|
||||
}
|
||||
|
||||
/// Sets the style.
|
||||
pub fn setStyle(self: Tooltip, style: Style) Tooltip {
|
||||
var t = self;
|
||||
t.style = style;
|
||||
return t;
|
||||
}
|
||||
|
||||
/// Sets the border.
|
||||
pub fn setBorder(self: Tooltip, border: bool) Tooltip {
|
||||
var t = self;
|
||||
t.border = border;
|
||||
return t;
|
||||
}
|
||||
|
||||
/// Sets the border style.
|
||||
pub fn setBorderStyle(self: Tooltip, style: Style) Tooltip {
|
||||
var t = self;
|
||||
t.border_style = style;
|
||||
return t;
|
||||
}
|
||||
|
||||
/// Sets the preferred position.
|
||||
pub fn setPosition(self: Tooltip, pos: TooltipPosition) Tooltip {
|
||||
var t = self;
|
||||
t.position = pos;
|
||||
return t;
|
||||
}
|
||||
|
||||
/// Sets maximum width.
|
||||
pub fn setMaxWidth(self: Tooltip, width: u16) Tooltip {
|
||||
var t = self;
|
||||
t.max_width = width;
|
||||
return t;
|
||||
}
|
||||
|
||||
/// Shows the tooltip at a specific position.
|
||||
pub fn showAt(self: *Tooltip, x: u16, y: u16) void {
|
||||
self.x = x;
|
||||
self.y = y;
|
||||
self.visible = true;
|
||||
}
|
||||
|
||||
/// Shows the tooltip near an anchor point with offset.
|
||||
pub fn showNear(self: *Tooltip, anchor_x: u16, anchor_y: u16, bounds: Rect) void {
|
||||
const size = self.calculateSize();
|
||||
|
||||
// Calculate position based on preference
|
||||
var tx: u16 = anchor_x;
|
||||
var ty: u16 = anchor_y;
|
||||
|
||||
switch (self.position) {
|
||||
.above => {
|
||||
ty = anchor_y -| size.height -| 1;
|
||||
},
|
||||
.below => {
|
||||
ty = anchor_y + 1;
|
||||
},
|
||||
.left => {
|
||||
tx = anchor_x -| size.width -| 1;
|
||||
},
|
||||
.right => {
|
||||
tx = anchor_x + 1;
|
||||
},
|
||||
.auto => {
|
||||
// Try below first, then above
|
||||
if (anchor_y + 1 + size.height <= bounds.height) {
|
||||
ty = anchor_y + 1;
|
||||
} else if (anchor_y >= size.height + 1) {
|
||||
ty = anchor_y -| size.height -| 1;
|
||||
} else {
|
||||
ty = anchor_y + 1;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Adjust to fit within bounds
|
||||
if (tx + size.width > bounds.x + bounds.width) {
|
||||
tx = bounds.x + bounds.width -| size.width;
|
||||
}
|
||||
if (ty + size.height > bounds.y + bounds.height) {
|
||||
ty = bounds.y + bounds.height -| size.height;
|
||||
}
|
||||
|
||||
self.x = tx;
|
||||
self.y = ty;
|
||||
self.visible = true;
|
||||
}
|
||||
|
||||
/// Hides the tooltip.
|
||||
pub fn hide(self: *Tooltip) void {
|
||||
self.visible = false;
|
||||
}
|
||||
|
||||
/// Returns whether visible.
|
||||
pub fn isVisible(self: Tooltip) bool {
|
||||
return self.visible;
|
||||
}
|
||||
|
||||
/// Calculates the size of the tooltip.
|
||||
pub fn calculateSize(self: Tooltip) struct { width: u16, height: u16 } {
|
||||
var content_width: u16 = 0;
|
||||
var content_height: u16 = 0;
|
||||
|
||||
if (self.lines.len > 0) {
|
||||
for (self.lines) |line| {
|
||||
const line_len: u16 = @intCast(@min(line.len, 65535));
|
||||
content_width = @max(content_width, line_len);
|
||||
}
|
||||
content_height = @intCast(self.lines.len);
|
||||
} else {
|
||||
content_width = @intCast(@min(self.text.len, 65535));
|
||||
content_height = 1;
|
||||
}
|
||||
|
||||
// Apply max width
|
||||
if (self.max_width > 0 and content_width > self.max_width) {
|
||||
content_width = self.max_width;
|
||||
}
|
||||
|
||||
// Add padding
|
||||
var width = content_width + self.padding_x * 2;
|
||||
var height = content_height + self.padding_y * 2;
|
||||
|
||||
// Add border
|
||||
if (self.border) {
|
||||
width += 2;
|
||||
height += 2;
|
||||
}
|
||||
|
||||
return .{ .width = width, .height = height };
|
||||
}
|
||||
|
||||
/// Gets the tooltip area.
|
||||
pub fn getArea(self: Tooltip) Rect {
|
||||
const size = self.calculateSize();
|
||||
return Rect.init(self.x, self.y, size.width, size.height);
|
||||
}
|
||||
|
||||
/// Renders the tooltip.
|
||||
pub fn render(self: Tooltip, bounds: Rect, buf: *Buffer) void {
|
||||
if (!self.visible) return;
|
||||
|
||||
const size = self.calculateSize();
|
||||
const tooltip_area = Rect.init(
|
||||
@min(self.x, bounds.x + bounds.width -| size.width),
|
||||
@min(self.y, bounds.y + bounds.height -| size.height),
|
||||
size.width,
|
||||
size.height,
|
||||
);
|
||||
|
||||
// Render background
|
||||
self.fillBackground(tooltip_area, buf);
|
||||
|
||||
// Render border if enabled
|
||||
var content_area = tooltip_area;
|
||||
if (self.border) {
|
||||
const block = Block.init()
|
||||
.setBorders(Borders.all)
|
||||
.style(self.border_style);
|
||||
block.render(tooltip_area, buf);
|
||||
content_area = block.inner(tooltip_area);
|
||||
}
|
||||
|
||||
// Apply padding
|
||||
const text_area = Rect.init(
|
||||
content_area.x + self.padding_x,
|
||||
content_area.y + self.padding_y,
|
||||
content_area.width -| self.padding_x * 2,
|
||||
content_area.height -| self.padding_y * 2,
|
||||
);
|
||||
|
||||
// Render text
|
||||
if (self.lines.len > 0) {
|
||||
var y = text_area.y;
|
||||
for (self.lines) |line| {
|
||||
if (y >= text_area.y + text_area.height) break;
|
||||
_ = buf.setString(text_area.x, y, line, self.style);
|
||||
y += 1;
|
||||
}
|
||||
} else {
|
||||
_ = buf.setString(text_area.x, text_area.y, self.text, self.style);
|
||||
}
|
||||
}
|
||||
|
||||
fn fillBackground(self: Tooltip, area: Rect, buf: *Buffer) void {
|
||||
var y = area.top();
|
||||
while (y < area.bottom()) : (y += 1) {
|
||||
var x = area.left();
|
||||
while (x < area.right()) : (x += 1) {
|
||||
if (buf.getCell(x, y)) |cell| {
|
||||
cell.setChar(' ');
|
||||
cell.setStyle(self.style);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// TooltipManager
|
||||
// ============================================================================
|
||||
|
||||
/// Manages tooltip display with timing/delay.
|
||||
pub const TooltipManager = struct {
|
||||
/// Current tooltip.
|
||||
tooltip: Tooltip,
|
||||
|
||||
/// Hover start time (milliseconds).
|
||||
hover_start_ms: i64 = 0,
|
||||
|
||||
/// Delay before showing tooltip (ms).
|
||||
show_delay_ms: u32 = 500,
|
||||
|
||||
/// How long to show tooltip (0 = until mouse moves).
|
||||
display_duration_ms: u32 = 0,
|
||||
|
||||
/// Current hover position.
|
||||
hover_x: u16 = 0,
|
||||
hover_y: u16 = 0,
|
||||
|
||||
/// Whether we're waiting to show.
|
||||
pending: bool = false,
|
||||
|
||||
/// Creates a new tooltip manager.
|
||||
pub fn init() TooltipManager {
|
||||
return .{
|
||||
.tooltip = Tooltip.init(""),
|
||||
};
|
||||
}
|
||||
|
||||
/// Sets the show delay.
|
||||
pub fn setDelay(self: *TooltipManager, delay_ms: u32) void {
|
||||
self.show_delay_ms = delay_ms;
|
||||
}
|
||||
|
||||
/// Called when mouse moves to a position with a tooltip.
|
||||
pub fn onHover(self: *TooltipManager, x: u16, y: u16, text: []const u8, current_time_ms: i64) void {
|
||||
if (self.hover_x != x or self.hover_y != y) {
|
||||
// Position changed, reset timer
|
||||
self.hover_x = x;
|
||||
self.hover_y = y;
|
||||
self.hover_start_ms = current_time_ms;
|
||||
self.pending = true;
|
||||
self.tooltip.hide();
|
||||
self.tooltip.text = text;
|
||||
}
|
||||
}
|
||||
|
||||
/// Called when mouse leaves tooltip area.
|
||||
pub fn onLeave(self: *TooltipManager) void {
|
||||
self.pending = false;
|
||||
self.tooltip.hide();
|
||||
}
|
||||
|
||||
/// Updates the tooltip state, should be called each frame.
|
||||
pub fn update(self: *TooltipManager, current_time_ms: i64, bounds: Rect) void {
|
||||
if (self.pending) {
|
||||
const elapsed = current_time_ms - self.hover_start_ms;
|
||||
if (elapsed >= self.show_delay_ms) {
|
||||
self.tooltip.showNear(self.hover_x, self.hover_y, bounds);
|
||||
self.pending = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the tooltip if visible.
|
||||
pub fn render(self: TooltipManager, bounds: Rect, buf: *Buffer) void {
|
||||
self.tooltip.render(bounds, buf);
|
||||
}
|
||||
|
||||
/// Returns whether tooltip is visible.
|
||||
pub fn isVisible(self: TooltipManager) bool {
|
||||
return self.tooltip.isVisible();
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
test "Tooltip basic" {
|
||||
var tooltip = Tooltip.init("Hello, World!");
|
||||
|
||||
try std.testing.expect(!tooltip.isVisible());
|
||||
try std.testing.expectEqualStrings("Hello, World!", tooltip.text);
|
||||
|
||||
tooltip.showAt(10, 5);
|
||||
try std.testing.expect(tooltip.isVisible());
|
||||
try std.testing.expectEqual(@as(u16, 10), tooltip.x);
|
||||
try std.testing.expectEqual(@as(u16, 5), tooltip.y);
|
||||
|
||||
tooltip.hide();
|
||||
try std.testing.expect(!tooltip.isVisible());
|
||||
}
|
||||
|
||||
test "Tooltip size calculation" {
|
||||
const tooltip = Tooltip.init("Test").setBorder(true);
|
||||
const size = tooltip.calculateSize();
|
||||
|
||||
// "Test" = 4 chars + 2 padding + 2 border = 8
|
||||
try std.testing.expectEqual(@as(u16, 8), size.width);
|
||||
// 1 line + 0 padding + 2 border = 3
|
||||
try std.testing.expectEqual(@as(u16, 3), size.height);
|
||||
}
|
||||
|
||||
test "Tooltip multiline" {
|
||||
const tooltip = Tooltip.initMultiline(&.{
|
||||
"Line 1",
|
||||
"Longer line 2",
|
||||
"L3",
|
||||
}).setBorder(false);
|
||||
|
||||
const size = tooltip.calculateSize();
|
||||
|
||||
// "Longer line 2" = 13 chars + 2 padding = 15
|
||||
try std.testing.expectEqual(@as(u16, 15), size.width);
|
||||
// 3 lines
|
||||
try std.testing.expectEqual(@as(u16, 3), size.height);
|
||||
}
|
||||
|
||||
test "Tooltip showNear auto position" {
|
||||
var tooltip = Tooltip.init("Tip").setPosition(.auto);
|
||||
const bounds = Rect.init(0, 0, 80, 24);
|
||||
|
||||
// Near top - should go below
|
||||
tooltip.showNear(10, 2, bounds);
|
||||
try std.testing.expect(tooltip.y > 2);
|
||||
|
||||
// Near bottom - should go above
|
||||
tooltip.showNear(10, 22, bounds);
|
||||
try std.testing.expect(tooltip.y < 22);
|
||||
}
|
||||
|
||||
test "TooltipManager delay" {
|
||||
var manager = TooltipManager.init();
|
||||
manager.setDelay(100);
|
||||
|
||||
const bounds = Rect.init(0, 0, 80, 24);
|
||||
|
||||
// Start hover
|
||||
manager.onHover(10, 10, "Test", 0);
|
||||
try std.testing.expect(!manager.isVisible());
|
||||
try std.testing.expect(manager.pending);
|
||||
|
||||
// Not enough time
|
||||
manager.update(50, bounds);
|
||||
try std.testing.expect(!manager.isVisible());
|
||||
|
||||
// Enough time
|
||||
manager.update(100, bounds);
|
||||
try std.testing.expect(manager.isVisible());
|
||||
|
||||
// Leave
|
||||
manager.onLeave();
|
||||
try std.testing.expect(!manager.isVisible());
|
||||
}
|
||||
Loading…
Reference in a new issue