//! Style - Colors and visual styling //! //! Based on zcatui's style system, adapted for GUI with RGBA colors. const std = @import("std"); // ============================================================================= // Render Mode - Simple vs Fancy // ============================================================================= /// Render mode controls visual quality vs performance tradeoff pub const RenderMode = enum { /// Fast rendering: rectangles, no AA on shapes, no shadows /// Best for: low-end hardware, SSH, WASM with limited resources simple, /// Pretty rendering: rounded corners, edge-fade AA, shadows /// Best for: desktop with good CPU, visual polish needed fancy, }; /// Global render mode - widgets check this to decide how to render var global_render_mode: RenderMode = .fancy; /// Get current render mode pub fn getRenderMode() RenderMode { return global_render_mode; } /// Set render mode pub fn setRenderMode(mode: RenderMode) void { global_render_mode = mode; } /// Check if fancy rendering is enabled pub fn isFancy() bool { return global_render_mode == .fancy; } /// RGBA Color pub const Color = struct { r: u8, g: u8, b: u8, a: u8 = 255, const Self = @This(); /// Create a color from RGB values pub fn rgb(r: u8, g: u8, b: u8) Self { return .{ .r = r, .g = g, .b = b, .a = 255 }; } /// Create a color from RGBA values pub fn rgba(r: u8, g: u8, b: u8, a: u8) Self { return .{ .r = r, .g = g, .b = b, .a = a }; } /// Convert to u32 (RGBA format) pub fn toU32(self: Self) u32 { return (@as(u32, self.r) << 24) | (@as(u32, self.g) << 16) | (@as(u32, self.b) << 8) | @as(u32, self.a); } /// Convert to u32 (ABGR format for SDL) pub fn toABGR(self: Self) u32 { return (@as(u32, self.a) << 24) | (@as(u32, self.b) << 16) | (@as(u32, self.g) << 8) | @as(u32, self.r); } /// Blend this color over another pub fn blend(self: Self, bg_color: Self) Self { if (self.a == 255) return self; if (self.a == 0) return bg_color; const alpha = @as(u16, self.a); const inv_alpha = 255 - alpha; return .{ .r = @intCast((@as(u16, self.r) * alpha + @as(u16, bg_color.r) * inv_alpha) / 255), .g = @intCast((@as(u16, self.g) * alpha + @as(u16, bg_color.g) * inv_alpha) / 255), .b = @intCast((@as(u16, self.b) * alpha + @as(u16, bg_color.b) * inv_alpha) / 255), .a = 255, }; } /// Darken color by percentage (0-100) pub fn darken(self: Self, percent: u8) Self { const factor = @as(u16, 100 - @min(percent, 100)); return .{ .r = @intCast((@as(u16, self.r) * factor) / 100), .g = @intCast((@as(u16, self.g) * factor) / 100), .b = @intCast((@as(u16, self.b) * factor) / 100), .a = self.a, }; } /// Lighten color by percentage (0-100) pub fn lighten(self: Self, percent: u8) Self { const factor = @as(u16, @min(percent, 100)); return .{ .r = @intCast(@as(u16, self.r) + ((@as(u16, 255) - self.r) * factor) / 100), .g = @intCast(@as(u16, self.g) + ((@as(u16, 255) - self.g) * factor) / 100), .b = @intCast(@as(u16, self.b) + ((@as(u16, 255) - self.b) * factor) / 100), .a = self.a, }; } /// Return same color with different alpha pub fn withAlpha(self: Self, alpha: u8) Self { return .{ .r = self.r, .g = self.g, .b = self.b, .a = alpha, }; } // ========================================================================= // Predefined colors // ========================================================================= pub const transparent = Color.rgba(0, 0, 0, 0); pub const black = Color.rgb(0, 0, 0); pub const white = Color.rgb(255, 255, 255); pub const red = Color.rgb(255, 0, 0); pub const green = Color.rgb(0, 255, 0); pub const blue = Color.rgb(0, 0, 255); pub const yellow = Color.rgb(255, 255, 0); pub const cyan = Color.rgb(0, 255, 255); pub const magenta = Color.rgb(255, 0, 255); pub const gray = Color.rgb(128, 128, 128); pub const dark_gray = Color.rgb(64, 64, 64); pub const light_gray = Color.rgb(192, 192, 192); // UI colors pub const background = Color.rgb(30, 30, 30); pub const foreground = Color.rgb(220, 220, 220); pub const primary = Color.rgb(66, 135, 245); pub const secondary = Color.rgb(100, 100, 100); pub const success = Color.rgb(76, 175, 80); pub const warning = Color.rgb(255, 152, 0); pub const danger = Color.rgb(244, 67, 54); pub const border = Color.rgb(80, 80, 80); }; /// Visual style for widgets pub const Style = struct { foreground: Color = Color.foreground, background: Color = Color.background, border: ?Color = null, border_radius: u8 = 0, const Self = @This(); /// Set foreground color pub fn fg(self: Self, color: Color) Self { var s = self; s.foreground = color; return s; } /// Set background color pub fn bg(self: Self, color: Color) Self { var s = self; s.background = color; return s; } /// Set border color pub fn withBorder(self: Self, color: Color) Self { var s = self; s.border = color; return s; } }; // ============================================================================= // Theme // ============================================================================= /// A theme defines colors for all UI elements pub const Theme = struct { /// Theme name name: []const u8 = "custom", // Base colors background: Color, foreground: Color, primary: Color, secondary: Color, success: Color, warning: Color, danger: Color, border: Color, // Surface colors (panels, cards) surface: Color, surface_variant: Color, // Text variants text_primary: Color, text_secondary: Color, text_disabled: Color, // Button colors button_bg: Color, button_fg: Color, button_hover: Color, button_active: Color, button_disabled_bg: Color, button_disabled_fg: Color, // Input colors input_bg: Color, input_fg: Color, input_border: Color, input_focus_border: Color, input_placeholder: Color, // Selection colors selection_bg: Color, selection_fg: Color, // Header/menu bar header_bg: Color, header_fg: Color, // Table colors table_header_bg: Color, table_row_even: Color, table_row_odd: Color, table_row_hover: Color, table_row_selected: Color, // Scrollbar scrollbar_track: Color, scrollbar_thumb: Color, scrollbar_thumb_hover: Color, // Modal/dialog modal_overlay: Color, modal_bg: Color, const Self = @This(); /// Dark theme (default) pub const dark = Self{ .name = "dark", .background = Color.rgb(30, 30, 30), .foreground = Color.rgb(220, 220, 220), .primary = Color.rgb(66, 135, 245), .secondary = Color.rgb(100, 100, 100), .success = Color.rgb(76, 175, 80), .warning = Color.rgb(255, 152, 0), .danger = Color.rgb(244, 67, 54), .border = Color.rgb(80, 80, 80), .surface = Color.rgb(40, 40, 40), .surface_variant = Color.rgb(50, 50, 50), .text_primary = Color.rgb(220, 220, 220), .text_secondary = Color.rgb(160, 160, 160), .text_disabled = Color.rgb(100, 100, 100), .button_bg = Color.rgb(60, 60, 60), .button_fg = Color.rgb(220, 220, 220), .button_hover = Color.rgb(80, 80, 80), .button_active = Color.rgb(50, 50, 50), .button_disabled_bg = Color.rgb(45, 45, 45), .button_disabled_fg = Color.rgb(100, 100, 100), .input_bg = Color.rgb(45, 45, 45), .input_fg = Color.rgb(220, 220, 220), .input_border = Color.rgb(80, 80, 80), .input_focus_border = Color.rgb(66, 135, 245), .input_placeholder = Color.rgb(120, 120, 120), .selection_bg = Color.rgb(66, 135, 245), .selection_fg = Color.rgb(255, 255, 255), .header_bg = Color.rgb(35, 35, 40), .header_fg = Color.rgb(200, 200, 200), .table_header_bg = Color.rgb(50, 50, 50), .table_row_even = Color.rgb(35, 35, 35), .table_row_odd = Color.rgb(40, 40, 40), .table_row_hover = Color.rgb(50, 50, 60), .table_row_selected = Color.rgb(66, 135, 245), .scrollbar_track = Color.rgb(40, 40, 40), .scrollbar_thumb = Color.rgb(80, 80, 80), .scrollbar_thumb_hover = Color.rgb(100, 100, 100), .modal_overlay = Color.rgba(0, 0, 0, 180), .modal_bg = Color.rgb(45, 45, 50), }; /// Light theme pub const light = Self{ .name = "light", .background = Color.rgb(245, 245, 245), .foreground = Color.rgb(30, 30, 30), .primary = Color.rgb(33, 150, 243), .secondary = Color.rgb(158, 158, 158), .success = Color.rgb(76, 175, 80), .warning = Color.rgb(255, 152, 0), .danger = Color.rgb(244, 67, 54), .border = Color.rgb(200, 200, 200), .surface = Color.rgb(255, 255, 255), .surface_variant = Color.rgb(240, 240, 240), .text_primary = Color.rgb(30, 30, 30), .text_secondary = Color.rgb(100, 100, 100), .text_disabled = Color.rgb(180, 180, 180), .button_bg = Color.rgb(230, 230, 230), .button_fg = Color.rgb(30, 30, 30), .button_hover = Color.rgb(210, 210, 210), .button_active = Color.rgb(190, 190, 190), .button_disabled_bg = Color.rgb(240, 240, 240), .button_disabled_fg = Color.rgb(180, 180, 180), .input_bg = Color.rgb(255, 255, 255), .input_fg = Color.rgb(30, 30, 30), .input_border = Color.rgb(180, 180, 180), .input_focus_border = Color.rgb(33, 150, 243), .input_placeholder = Color.rgb(160, 160, 160), .selection_bg = Color.rgb(33, 150, 243), .selection_fg = Color.rgb(255, 255, 255), .header_bg = Color.rgb(255, 255, 255), .header_fg = Color.rgb(50, 50, 50), .table_header_bg = Color.rgb(240, 240, 240), .table_row_even = Color.rgb(255, 255, 255), .table_row_odd = Color.rgb(248, 248, 248), .table_row_hover = Color.rgb(235, 245, 255), .table_row_selected = Color.rgb(33, 150, 243), .scrollbar_track = Color.rgb(240, 240, 240), .scrollbar_thumb = Color.rgb(200, 200, 200), .scrollbar_thumb_hover = Color.rgb(180, 180, 180), .modal_overlay = Color.rgba(0, 0, 0, 120), .modal_bg = Color.rgb(255, 255, 255), }; /// High contrast dark theme pub const high_contrast_dark = Self{ .name = "high_contrast_dark", .background = Color.rgb(0, 0, 0), .foreground = Color.rgb(255, 255, 255), .primary = Color.rgb(0, 200, 255), .secondary = Color.rgb(180, 180, 180), .success = Color.rgb(0, 255, 0), .warning = Color.rgb(255, 255, 0), .danger = Color.rgb(255, 0, 0), .border = Color.rgb(255, 255, 255), .surface = Color.rgb(20, 20, 20), .surface_variant = Color.rgb(40, 40, 40), .text_primary = Color.rgb(255, 255, 255), .text_secondary = Color.rgb(200, 200, 200), .text_disabled = Color.rgb(128, 128, 128), .button_bg = Color.rgb(40, 40, 40), .button_fg = Color.rgb(255, 255, 255), .button_hover = Color.rgb(60, 60, 60), .button_active = Color.rgb(20, 20, 20), .button_disabled_bg = Color.rgb(30, 30, 30), .button_disabled_fg = Color.rgb(100, 100, 100), .input_bg = Color.rgb(0, 0, 0), .input_fg = Color.rgb(255, 255, 255), .input_border = Color.rgb(255, 255, 255), .input_focus_border = Color.rgb(0, 200, 255), .input_placeholder = Color.rgb(150, 150, 150), .selection_bg = Color.rgb(0, 200, 255), .selection_fg = Color.rgb(0, 0, 0), .header_bg = Color.rgb(0, 0, 0), .header_fg = Color.rgb(255, 255, 255), .table_header_bg = Color.rgb(30, 30, 30), .table_row_even = Color.rgb(0, 0, 0), .table_row_odd = Color.rgb(20, 20, 20), .table_row_hover = Color.rgb(40, 40, 60), .table_row_selected = Color.rgb(0, 200, 255), .scrollbar_track = Color.rgb(20, 20, 20), .scrollbar_thumb = Color.rgb(150, 150, 150), .scrollbar_thumb_hover = Color.rgb(200, 200, 200), .modal_overlay = Color.rgba(0, 0, 0, 200), .modal_bg = Color.rgb(20, 20, 20), }; /// Solarized Dark theme pub const solarized_dark = Self{ .name = "solarized_dark", .background = Color.rgb(0, 43, 54), // base03 .foreground = Color.rgb(131, 148, 150), // base0 .primary = Color.rgb(38, 139, 210), // blue .secondary = Color.rgb(88, 110, 117), // base01 .success = Color.rgb(133, 153, 0), // green .warning = Color.rgb(181, 137, 0), // yellow .danger = Color.rgb(220, 50, 47), // red .border = Color.rgb(88, 110, 117), // base01 .surface = Color.rgb(7, 54, 66), // base02 .surface_variant = Color.rgb(0, 43, 54), // base03 .text_primary = Color.rgb(147, 161, 161), // base1 .text_secondary = Color.rgb(131, 148, 150), // base0 .text_disabled = Color.rgb(88, 110, 117), // base01 .button_bg = Color.rgb(7, 54, 66), .button_fg = Color.rgb(147, 161, 161), .button_hover = Color.rgb(88, 110, 117), .button_active = Color.rgb(0, 43, 54), .button_disabled_bg = Color.rgb(0, 43, 54), .button_disabled_fg = Color.rgb(88, 110, 117), .input_bg = Color.rgb(0, 43, 54), .input_fg = Color.rgb(147, 161, 161), .input_border = Color.rgb(88, 110, 117), .input_focus_border = Color.rgb(38, 139, 210), .input_placeholder = Color.rgb(88, 110, 117), .selection_bg = Color.rgb(38, 139, 210), .selection_fg = Color.rgb(253, 246, 227), .header_bg = Color.rgb(7, 54, 66), .header_fg = Color.rgb(147, 161, 161), .table_header_bg = Color.rgb(7, 54, 66), .table_row_even = Color.rgb(0, 43, 54), .table_row_odd = Color.rgb(7, 54, 66), .table_row_hover = Color.rgb(88, 110, 117), .table_row_selected = Color.rgb(38, 139, 210), .scrollbar_track = Color.rgb(0, 43, 54), .scrollbar_thumb = Color.rgb(88, 110, 117), .scrollbar_thumb_hover = Color.rgb(101, 123, 131), .modal_overlay = Color.rgba(0, 0, 0, 180), .modal_bg = Color.rgb(7, 54, 66), }; /// Solarized Light theme pub const solarized_light = Self{ .name = "solarized_light", .background = Color.rgb(253, 246, 227), // base3 .foreground = Color.rgb(101, 123, 131), // base00 .primary = Color.rgb(38, 139, 210), // blue .secondary = Color.rgb(147, 161, 161), // base1 .success = Color.rgb(133, 153, 0), // green .warning = Color.rgb(181, 137, 0), // yellow .danger = Color.rgb(220, 50, 47), // red .border = Color.rgb(147, 161, 161), // base1 .surface = Color.rgb(238, 232, 213), // base2 .surface_variant = Color.rgb(253, 246, 227), // base3 .text_primary = Color.rgb(88, 110, 117), // base01 .text_secondary = Color.rgb(101, 123, 131), // base00 .text_disabled = Color.rgb(147, 161, 161), // base1 .button_bg = Color.rgb(238, 232, 213), .button_fg = Color.rgb(88, 110, 117), .button_hover = Color.rgb(147, 161, 161), .button_active = Color.rgb(253, 246, 227), .button_disabled_bg = Color.rgb(253, 246, 227), .button_disabled_fg = Color.rgb(147, 161, 161), .input_bg = Color.rgb(253, 246, 227), .input_fg = Color.rgb(88, 110, 117), .input_border = Color.rgb(147, 161, 161), .input_focus_border = Color.rgb(38, 139, 210), .input_placeholder = Color.rgb(147, 161, 161), .selection_bg = Color.rgb(38, 139, 210), .selection_fg = Color.rgb(253, 246, 227), .header_bg = Color.rgb(238, 232, 213), .header_fg = Color.rgb(88, 110, 117), .table_header_bg = Color.rgb(238, 232, 213), .table_row_even = Color.rgb(253, 246, 227), .table_row_odd = Color.rgb(238, 232, 213), .table_row_hover = Color.rgb(147, 161, 161), .table_row_selected = Color.rgb(38, 139, 210), .scrollbar_track = Color.rgb(253, 246, 227), .scrollbar_thumb = Color.rgb(147, 161, 161), .scrollbar_thumb_hover = Color.rgb(131, 148, 150), .modal_overlay = Color.rgba(0, 0, 0, 120), .modal_bg = Color.rgb(238, 232, 213), }; }; // ============================================================================= // Theme Manager // ============================================================================= /// Global theme manager pub const ThemeManager = struct { /// Current theme current: *const Theme, const Self = @This(); /// Initialize with default dark theme pub fn init() Self { return Self{ .current = &Theme.dark, }; } /// Set current theme pub fn setTheme(self: *Self, theme: *const Theme) void { self.current = theme; } /// Get current theme pub fn getTheme(self: Self) *const Theme { return self.current; } /// Switch to dark theme pub fn setDark(self: *Self) void { self.current = &Theme.dark; } /// Switch to light theme pub fn setLight(self: *Self) void { self.current = &Theme.light; } /// Toggle between dark and light pub fn toggle(self: *Self) void { if (std.mem.eql(u8, self.current.name, "dark")) { self.current = &Theme.light; } else { self.current = &Theme.dark; } } }; /// Global theme manager instance var global_theme_manager: ?ThemeManager = null; /// Get global theme manager pub fn getThemeManager() *ThemeManager { if (global_theme_manager == null) { global_theme_manager = ThemeManager.init(); } return &global_theme_manager.?; } /// Get current theme (convenience function) pub fn currentTheme() *const Theme { return getThemeManager().current; } // ============================================================================= // Tests // ============================================================================= test "Color creation" { const c = Color.rgb(100, 150, 200); try std.testing.expectEqual(@as(u8, 100), c.r); try std.testing.expectEqual(@as(u8, 150), c.g); try std.testing.expectEqual(@as(u8, 200), c.b); try std.testing.expectEqual(@as(u8, 255), c.a); } test "Color darken" { const white = Color.white; const darkened = white.darken(50); try std.testing.expectEqual(@as(u8, 127), darkened.r); } test "Color blend" { const fg = Color.rgba(255, 0, 0, 128); const bg = Color.rgb(0, 0, 255); const blended = fg.blend(bg); // Should be purple-ish try std.testing.expect(blended.r > 100); try std.testing.expect(blended.b > 100); } test "Theme dark" { const theme = Theme.dark; try std.testing.expect(std.mem.eql(u8, theme.name, "dark")); try std.testing.expectEqual(@as(u8, 30), theme.background.r); } test "Theme light" { const theme = Theme.light; try std.testing.expect(std.mem.eql(u8, theme.name, "light")); try std.testing.expectEqual(@as(u8, 245), theme.background.r); } test "ThemeManager toggle" { var tm = ThemeManager.init(); try std.testing.expect(std.mem.eql(u8, tm.current.name, "dark")); tm.toggle(); try std.testing.expect(std.mem.eql(u8, tm.current.name, "light")); tm.toggle(); try std.testing.expect(std.mem.eql(u8, tm.current.name, "dark")); } test "ThemeManager setTheme" { var tm = ThemeManager.init(); tm.setTheme(&Theme.solarized_dark); try std.testing.expect(std.mem.eql(u8, tm.current.name, "solarized_dark")); tm.setTheme(&Theme.high_contrast_dark); try std.testing.expect(std.mem.eql(u8, tm.current.name, "high_contrast_dark")); }