zcatgui/src/core/style.zig
reugenio 364a7d963f feat: Paridad visual DVUI - RenderMode dual (simple/fancy)
Sistema de rendering dual para zcatgui:

Core:
- RenderMode enum (simple/fancy) en style.zig
- global_render_mode con helpers: isFancy(), setRenderMode()
- fillRoundedRect con edge-fade AA en framebuffer.zig (~350 LOC)
- Nuevos comandos: rounded_rect, rounded_rect_outline

Widgets actualizados:
- Button: corner_radius=4, usa roundedRect en fancy mode
- Panel: corner_radius=6, show_shadow=true, sombra offset 4px
- TextInput: corner_radius=3
- Select: corner_radius=3
- Modal: corner_radius=8, show_shadow=true, sombra offset 6px
  - Botones y input field del modal también redondeados

Técnica edge-fade (de DVUI):
- Anti-aliasing por gradiente alfa en bordes
- Sin supersampling, mínimo impacto en rendimiento
- Bordes suaves sin multisampling

+589 líneas, 9 archivos modificados

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 01:02:46 +01:00

634 lines
20 KiB
Zig

//! 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"));
}