//! IconButton Widget - Circular button with icon //! //! A button that displays only an icon, typically circular. //! Commonly used in toolbars, app bars, and action buttons. const std = @import("std"); const Context = @import("../core/context.zig").Context; const Command = @import("../core/command.zig"); const Layout = @import("../core/layout.zig"); const Style = @import("../core/style.zig"); const Input = @import("../core/input.zig"); const icon_module = @import("icon.zig"); /// IconButton style variants pub const ButtonStyle = enum { /// Filled background (primary action) filled, /// Outlined with border outlined, /// Ghost (transparent, only visible on hover) ghost, /// Tonal (subtle background) tonal, }; /// IconButton size presets pub const Size = enum { /// 24x24 button (16x16 icon) small, /// 36x36 button (20x20 icon) medium, /// 48x48 button (24x24 icon) large, /// 56x56 button (32x32 icon) xlarge, pub fn buttonSize(self: Size) u32 { return switch (self) { .small => 24, .medium => 36, .large => 48, .xlarge => 56, }; } pub fn iconSize(self: Size) u32 { return switch (self) { .small => 16, .medium => 20, .large => 24, .xlarge => 32, }; } }; /// IconButton configuration pub const Config = struct { /// Icon to display icon_type: icon_module.IconType, /// Button size size: Size = .medium, /// Button style style: ButtonStyle = .ghost, /// Tooltip text (shown on hover) tooltip: ?[]const u8 = null, /// Disabled state disabled: bool = false, /// Selected/active state (for toggle buttons) selected: bool = false, /// Badge text (small indicator) badge: ?[]const u8 = null, }; /// IconButton colors pub const Colors = struct { /// Icon color (normal) icon: Style.Color = Style.Color.rgba(220, 220, 220, 255), /// Icon color (hovered) icon_hover: Style.Color = Style.Color.white, /// Icon color (disabled) icon_disabled: Style.Color = Style.Color.rgba(100, 100, 100, 255), /// Background (filled style) background: Style.Color = Style.Color.rgba(66, 133, 244, 255), /// Background (hovered) background_hover: Style.Color = Style.Color.rgba(86, 153, 255, 255), /// Background (pressed) background_pressed: Style.Color = Style.Color.rgba(46, 113, 224, 255), /// Border color (outlined style) border: Style.Color = Style.Color.rgba(100, 100, 100, 255), /// Ghost hover background ghost_hover: Style.Color = Style.Color.rgba(255, 255, 255, 20), /// Selected background selected_bg: Style.Color = Style.Color.rgba(66, 133, 244, 50), /// Badge background badge_bg: Style.Color = Style.Color.rgba(244, 67, 54, 255), /// Badge text badge_text: Style.Color = Style.Color.white, pub fn fromTheme(theme: Style.Theme) Colors { return .{ .icon = theme.foreground, .icon_hover = theme.foreground.lighten(20), .icon_disabled = theme.foreground.darken(40), .background = theme.primary, .background_hover = theme.primary.lighten(10), .background_pressed = theme.primary.darken(10), .border = theme.border, .ghost_hover = theme.foreground.withAlpha(20), .selected_bg = theme.primary.withAlpha(50), .badge_bg = theme.danger, .badge_text = Style.Color.white, }; } }; /// IconButton result pub const Result = struct { /// True if button was clicked this frame clicked: bool, /// True if button is currently hovered hovered: bool, /// True if button is currently pressed pressed: bool, /// Bounding rectangle of the button bounds: Layout.Rect, }; /// Simple icon button pub fn iconButton(ctx: *Context, icon_type: icon_module.IconType) Result { return iconButtonEx(ctx, .{ .icon_type = icon_type }, .{}); } /// Icon button with tooltip pub fn iconButtonTooltip(ctx: *Context, icon_type: icon_module.IconType, tooltip_text: []const u8) Result { return iconButtonEx(ctx, .{ .icon_type = icon_type, .tooltip = tooltip_text }, .{}); } /// Icon button with full configuration pub fn iconButtonEx(ctx: *Context, config: Config, colors: Colors) Result { const btn_size = config.size.buttonSize(); // Get bounds from layout var bounds = ctx.layout.nextRect(); // Override size if layout gives us something different if (bounds.w != btn_size or bounds.h != btn_size) { bounds.w = btn_size; bounds.h = btn_size; } return iconButtonRect(ctx, bounds, config, colors); } /// Icon button in a specific rectangle pub fn iconButtonRect( ctx: *Context, bounds: Layout.Rect, config: Config, colors: Colors, ) Result { if (bounds.isEmpty()) { return .{ .clicked = false, .hovered = false, .pressed = false, .bounds = bounds, }; } // Mouse interaction const mouse = ctx.input.mousePos(); const in_bounds = bounds.contains(mouse.x, mouse.y); const hovered = in_bounds and !config.disabled; const pressed = hovered and ctx.input.mousePressed(.left); const clicked = hovered and ctx.input.mouseReleased(.left); // Determine background color const bg_color: ?Style.Color = switch (config.style) { .filled => if (config.disabled) colors.background.darken(30) else if (pressed) colors.background_pressed else if (hovered) colors.background_hover else colors.background, .outlined => if (hovered or config.selected) colors.ghost_hover else null, .ghost => if (pressed) colors.ghost_hover.withAlpha(40) else if (hovered or config.selected) colors.ghost_hover else null, .tonal => if (config.disabled) colors.ghost_hover.darken(20) else if (pressed) colors.ghost_hover.withAlpha(60) else if (hovered) colors.ghost_hover.withAlpha(40) else colors.ghost_hover.withAlpha(25), }; // Draw background (circular approximation with rounded rect) if (bg_color) |bg| { ctx.pushCommand(Command.rect(bounds.x, bounds.y, bounds.w, bounds.h, bg)); } // Draw border for outlined style if (config.style == .outlined) { ctx.pushCommand(Command.rectOutline(bounds.x, bounds.y, bounds.w, bounds.h, colors.border)); } // Draw selected indicator if (config.selected and config.style != .filled) { ctx.pushCommand(Command.rect(bounds.x, bounds.y, bounds.w, bounds.h, colors.selected_bg)); } // Draw icon const icon_size = config.size.iconSize(); const icon_x = bounds.x + @as(i32, @intCast((bounds.w - icon_size) / 2)); const icon_y = bounds.y + @as(i32, @intCast((bounds.h - icon_size) / 2)); const icon_color = if (config.disabled) colors.icon_disabled else if (hovered and config.style != .filled) colors.icon_hover else if (config.style == .filled) Style.Color.white else colors.icon; const icon_rect = Layout.Rect{ .x = icon_x, .y = icon_y, .w = icon_size, .h = icon_size, }; icon_module.iconRect(ctx, icon_rect, config.icon_type, .{ .custom_size = icon_size, }, .{ .foreground = icon_color, }); // Draw badge if (config.badge) |badge_text| { if (badge_text.len > 0) { const badge_size: u32 = if (badge_text.len == 1) 16 else @as(u32, @intCast(badge_text.len * 6 + 8)); const badge_x = bounds.x + @as(i32, @intCast(bounds.w)) - @as(i32, @intCast(badge_size / 2)) - 2; const badge_y = bounds.y - @as(i32, @intCast(badge_size / 2)) + 4; // Badge background ctx.pushCommand(Command.rect(badge_x, badge_y, badge_size, 16, colors.badge_bg)); // Badge text ctx.pushCommand(Command.text(badge_x + 4, badge_y + 4, badge_text, colors.badge_text)); } } // Tooltip is handled externally by the tooltip widget // The caller should check if hovered and show tooltip return .{ .clicked = clicked, .hovered = hovered, .pressed = pressed, .bounds = bounds, }; } /// Create a row of icon buttons (toolbar style) pub fn iconButtonRow( ctx: *Context, buttons: []const Config, colors: Colors, spacing: u16, ) []Result { // This is a convenience function - in practice you'd want to allocate // For now, we just draw them and return the last result var last_x = ctx.layout.current_x; for (buttons) |config| { const btn_size = config.size.buttonSize(); const bounds = Layout.Rect{ .x = last_x, .y = ctx.layout.current_y, .w = btn_size, .h = btn_size, }; _ = iconButtonRect(ctx, bounds, config, colors); last_x += @as(i32, @intCast(btn_size + spacing)); } // Return empty slice - caller should call individually if they need results return &.{}; } // ============================================================================= // Tests // ============================================================================= test "iconButton click" { var ctx = try Context.init(std.testing.allocator, 800, 600); defer ctx.deinit(); // Frame 1: Press inside button ctx.beginFrame(); ctx.layout.row_height = 36; ctx.input.setMousePos(18, 18); // Center of 36x36 button ctx.input.setMouseButton(.left, true); _ = iconButton(&ctx, .check); ctx.endFrame(); // Frame 2: Release ctx.beginFrame(); ctx.layout.row_height = 36; ctx.input.setMousePos(18, 18); ctx.input.setMouseButton(.left, false); const result = iconButton(&ctx, .check); ctx.endFrame(); try std.testing.expect(result.clicked); } test "iconButton disabled no click" { var ctx = try Context.init(std.testing.allocator, 800, 600); defer ctx.deinit(); // Frame 1: Press ctx.beginFrame(); ctx.layout.row_height = 36; ctx.input.setMousePos(18, 18); ctx.input.setMouseButton(.left, true); _ = iconButtonEx(&ctx, .{ .icon_type = .check, .disabled = true }, .{}); ctx.endFrame(); // Frame 2: Release ctx.beginFrame(); ctx.layout.row_height = 36; ctx.input.setMousePos(18, 18); ctx.input.setMouseButton(.left, false); const result = iconButtonEx(&ctx, .{ .icon_type = .check, .disabled = true }, .{}); ctx.endFrame(); try std.testing.expect(!result.clicked); } test "iconButton generates commands" { var ctx = try Context.init(std.testing.allocator, 800, 600); defer ctx.deinit(); ctx.beginFrame(); ctx.layout.row_height = 36; _ = iconButtonEx(&ctx, .{ .icon_type = .settings, .style = .filled, }, .{}); // Should generate: background rect + icon lines try std.testing.expect(ctx.commands.items.len >= 2); ctx.endFrame(); } test "iconButton with badge" { var ctx = try Context.init(std.testing.allocator, 800, 600); defer ctx.deinit(); ctx.beginFrame(); ctx.layout.row_height = 36; _ = iconButtonEx(&ctx, .{ .icon_type = .bell, .badge = "3", }, .{}); // Should generate: icon + badge background + badge text try std.testing.expect(ctx.commands.items.len >= 3); ctx.endFrame(); } test "iconButton sizes" { try std.testing.expectEqual(@as(u32, 24), Size.small.buttonSize()); try std.testing.expectEqual(@as(u32, 36), Size.medium.buttonSize()); try std.testing.expectEqual(@as(u32, 48), Size.large.buttonSize()); try std.testing.expectEqual(@as(u32, 56), Size.xlarge.buttonSize()); try std.testing.expectEqual(@as(u32, 16), Size.small.iconSize()); try std.testing.expectEqual(@as(u32, 20), Size.medium.iconSize()); try std.testing.expectEqual(@as(u32, 24), Size.large.iconSize()); try std.testing.expectEqual(@as(u32, 32), Size.xlarge.iconSize()); }