//! AppBar Widget - Application bar //! //! A top or bottom bar for app navigation and actions. //! Supports leading icon, title, 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"); const iconbutton = @import("iconbutton.zig"); /// AppBar position pub const Position = enum { top, bottom, }; /// AppBar action button pub const Action = struct { /// Action icon icon_type: icon_module.IconType, /// Action ID (for click detection) id: u32, /// Tooltip text tooltip: ?[]const u8 = null, /// Badge (notification count, etc.) badge: ?[]const u8 = null, /// Disabled state disabled: bool = false, }; /// AppBar configuration pub const Config = struct { /// Bar position position: Position = .top, /// Bar height height: u16 = 56, /// Title text title: []const u8 = "", /// Subtitle text subtitle: ?[]const u8 = null, /// Leading icon (e.g., menu, back) leading_icon: ?icon_module.IconType = null, /// Action buttons actions: []const Action = &.{}, /// Elevation elevated: bool = true, /// Center title center_title: bool = false, }; /// AppBar colors pub const Colors = struct { /// Background background: Style.Color = Style.Color.rgb(33, 33, 33), /// Title color title: Style.Color = Style.Color.rgb(255, 255, 255), /// Subtitle color subtitle: Style.Color = Style.Color.rgb(180, 180, 180), /// Icon color icon: Style.Color = Style.Color.rgb(255, 255, 255), /// Shadow color shadow: Style.Color = Style.Color.rgba(0, 0, 0, 40), pub fn fromTheme(theme: Style.Theme) Colors { return .{ .background = theme.primary, .title = Style.Color.white, .subtitle = Style.Color.white.darken(20), .icon = Style.Color.white, .shadow = Style.Color.rgba(0, 0, 0, 40), }; } }; /// AppBar result pub const Result = struct { /// Leading icon clicked leading_clicked: bool, /// Action that was clicked (ID) action_clicked: ?u32, /// Bar bounds bounds: Layout.Rect, /// Content area (below/above the bar) content_rect: Layout.Rect, }; /// Simple app bar with title pub fn appBar(ctx: *Context, title_text: []const u8) Result { return appBarEx(ctx, .{ .title = title_text }, .{}); } /// App bar with configuration pub fn appBarEx(ctx: *Context, config: Config, colors: Colors) Result { const screen_width = ctx.layout.area.w; const bar_y: i32 = if (config.position == .top) 0 else @as(i32, @intCast(ctx.layout.area.h - config.height)); const bounds = Layout.Rect{ .x = 0, .y = bar_y, .w = screen_width, .h = config.height, }; return appBarRect(ctx, bounds, config, colors); } /// App bar in specific rectangle pub fn appBarRect( ctx: *Context, bounds: Layout.Rect, config: Config, colors: Colors, ) Result { if (bounds.isEmpty()) { return .{ .leading_clicked = false, .action_clicked = null, .bounds = bounds, .content_rect = Layout.Rect{ .x = 0, .y = 0, .w = 0, .h = 0 }, }; } var leading_clicked = false; var action_clicked: ?u32 = null; // Draw shadow (if elevated and at top) if (config.elevated and config.position == .top) { ctx.pushCommand(Command.rect( bounds.x, bounds.y + @as(i32, @intCast(bounds.h)), bounds.w, 4, colors.shadow, )); } else if (config.elevated and config.position == .bottom) { ctx.pushCommand(Command.rect( bounds.x, bounds.y - 4, bounds.w, 4, colors.shadow, )); } // Draw background ctx.pushCommand(Command.rect(bounds.x, bounds.y, bounds.w, bounds.h, colors.background)); const padding: i32 = 8; var current_x = bounds.x + padding; const center_y = bounds.y + @as(i32, @intCast(bounds.h / 2)); // Draw leading icon if (config.leading_icon) |icon_type| { const icon_size: u32 = 24; const icon_bounds = Layout.Rect{ .x = current_x, .y = center_y - @as(i32, @intCast(icon_size / 2)), .w = 36, .h = 36, }; const result = iconbutton.iconButtonRect(ctx, icon_bounds, .{ .icon_type = icon_type, .size = .medium, .style = .ghost, }, .{ .icon = colors.icon, .icon_hover = colors.icon, .ghost_hover = colors.icon.withAlpha(30), }); if (result.clicked) { leading_clicked = true; } current_x += 44; } // Calculate title position const title_y = if (config.subtitle != null) center_y - 10 else center_y - 4; // Draw title if (config.title.len > 0) { var title_x = current_x + 8; if (config.center_title) { const title_width = config.title.len * 8; title_x = bounds.x + @as(i32, @intCast((bounds.w - @as(u32, @intCast(title_width))) / 2)); } ctx.pushCommand(Command.text(title_x, title_y, config.title, colors.title)); // Draw subtitle if (config.subtitle) |subtitle_text| { ctx.pushCommand(Command.text(title_x, title_y + 12, subtitle_text, colors.subtitle)); } } // Draw action buttons (right side) var action_x = bounds.x + @as(i32, @intCast(bounds.w)) - padding; for (config.actions) |action| { const icon_size: u32 = 36; action_x -= @as(i32, @intCast(icon_size)); const action_bounds = Layout.Rect{ .x = action_x, .y = center_y - @as(i32, @intCast(icon_size / 2)), .w = icon_size, .h = icon_size, }; const result = iconbutton.iconButtonRect(ctx, action_bounds, .{ .icon_type = action.icon_type, .size = .medium, .style = .ghost, .disabled = action.disabled, .badge = action.badge, }, .{ .icon = colors.icon, .icon_hover = colors.icon, .ghost_hover = colors.icon.withAlpha(30), }); if (result.clicked) { action_clicked = action.id; } action_x -= 4; // Spacing } // Calculate content rect const content_rect = if (config.position == .top) Layout.Rect{ .x = 0, .y = bounds.y + @as(i32, @intCast(bounds.h)), .w = bounds.w, .h = ctx.layout.area.h -| bounds.h, } else Layout.Rect{ .x = 0, .y = 0, .w = bounds.w, .h = ctx.layout.area.h -| bounds.h, }; return .{ .leading_clicked = leading_clicked, .action_clicked = action_clicked, .bounds = bounds, .content_rect = content_rect, }; } // ============================================================================= // Tests // ============================================================================= test "appBar generates commands" { var ctx = try Context.init(std.testing.allocator, 800, 600); defer ctx.deinit(); ctx.beginFrame(); const result = appBar(&ctx, "My App"); // Should generate: shadow + background + title try std.testing.expect(ctx.commands.items.len >= 2); try std.testing.expect(result.bounds.h == 56); ctx.endFrame(); } test "appBar with actions" { var ctx = try Context.init(std.testing.allocator, 800, 600); defer ctx.deinit(); ctx.beginFrame(); const actions = [_]Action{ .{ .icon_type = .search, .id = 1 }, .{ .icon_type = .settings, .id = 2 }, }; _ = appBarEx(&ctx, .{ .title = "My App", .actions = &actions, }, .{}); try std.testing.expect(ctx.commands.items.len >= 4); ctx.endFrame(); } test "appBar with leading icon" { var ctx = try Context.init(std.testing.allocator, 800, 600); defer ctx.deinit(); ctx.beginFrame(); _ = appBarEx(&ctx, .{ .title = "My App", .leading_icon = .menu, }, .{}); try std.testing.expect(ctx.commands.items.len >= 3); ctx.endFrame(); } test "appBar bottom position" { var ctx = try Context.init(std.testing.allocator, 800, 600); defer ctx.deinit(); ctx.beginFrame(); const result = appBarEx(&ctx, .{ .title = "Bottom Bar", .position = .bottom, }, .{}); try std.testing.expect(result.bounds.y > 0); ctx.endFrame(); }