diff --git a/src/core/context.zig b/src/core/context.zig index 0b764a1..be8a152 100644 --- a/src/core/context.zig +++ b/src/core/context.zig @@ -563,28 +563,57 @@ pub const Context = struct { /// Draw a complete panel frame with focus-dependent styling. /// Encapsulates the common pattern: transition -> shadow -> bevel -> border. /// - /// Usage: - /// ```zig - /// // In panel state: - /// bg_transition: zcatgui.Context.ColorTransition = .{}, + /// ## Clipping (Design Decision 2025-12-31) + /// Automatic clipping is OMITTED for performance and full team control + /// over widget coordinates. The team ensures widgets stay within bounds. /// - /// // In draw: + /// MUST be implemented if the library becomes Open Source to guarantee + /// visual safety for third-party users. + /// + /// ## Usage Modes + /// + /// ### Mode 1: Explicit (full control) + /// ```zig /// ctx.drawPanelFrame(rect, &self.bg_transition, .{ /// .has_focus = panel_has_focus, /// .focus_bg = colors.fondo_con_focus, /// .unfocus_bg = colors.fondo_sin_focus, - /// .border_color = if (panel_has_focus) colors.borde_con_focus else colors.borde_sin_focus, + /// .border_color = border_color, + /// }); + /// ``` + /// + /// ### Mode 2: Z-Design (automatic derivation) + /// ```zig + /// ctx.drawPanelFrame(rect, &self.bg_transition, .{ + /// .has_focus = panel_has_focus, + /// .base_color = Color.laravel_blue, // Derives all colors + /// .title = "[1] Clientes", // Optional title /// }); /// ``` pub const PanelFrameConfig = struct { /// Whether the panel currently has focus has_focus: bool = false, - /// Background color when focused - focus_bg: Style.Color = Style.Color.rgb(40, 40, 50), - /// Background color when not focused - unfocus_bg: Style.Color = Style.Color.rgb(30, 30, 40), - /// Border color (typically borde_con_focus or borde_sin_focus) + + // === Mode 1: Explicit colors (backwards compatible) === + /// Background color when focused (used if base_color is null) + focus_bg: ?Style.Color = null, + /// Background color when not focused (used if base_color is null) + unfocus_bg: ?Style.Color = null, + /// Border color (used if base_color is null) border_color: ?Style.Color = null, + + // === Mode 2: Z-Design automatic derivation === + /// Base color for Z-Design derivation. If set, derives all colors automatically. + /// Uses generic luminance formula: blend inversely proportional to perceived brightness. + base_color: ?Style.Color = null, + + // === Title (optional, works in both modes) === + /// Panel title (drawn at top-left if provided) + title: ?[]const u8 = null, + /// Title color (if null, uses border color or derived title_color) + title_color: ?Style.Color = null, + + // === Behavior === /// Draw shadow when focused (default true) draw_shadow: bool = true, /// Draw bevel effect (default true) @@ -593,14 +622,52 @@ pub const Context = struct { /// Draw a complete panel frame with focus transition and 3D effects. /// Returns true if the transition is still animating (need more frames). + /// + /// Supports two modes: + /// - **Explicit**: Provide focus_bg, unfocus_bg, border_color directly + /// - **Z-Design**: Provide base_color, all colors derived automatically pub fn drawPanelFrame( self: *Self, rect: Layout.Rect, transition: *ColorTransition, config: PanelFrameConfig, ) bool { + // Determine colors: Z-Design derivation or explicit + const focus_bg: Style.Color = blk: { + if (config.base_color) |base| { + const derived = Style.derivePanelFrameColors(base); + break :blk derived.focus_bg; + } + break :blk config.focus_bg orelse Style.Color.rgb(40, 40, 50); + }; + + const unfocus_bg: Style.Color = blk: { + if (config.base_color) |base| { + const derived = Style.derivePanelFrameColors(base); + break :blk derived.unfocus_bg; + } + break :blk config.unfocus_bg orelse Style.Color.rgb(30, 30, 40); + }; + + const border_color: ?Style.Color = blk: { + if (config.base_color) |base| { + const derived = Style.derivePanelFrameColors(base); + break :blk if (config.has_focus) derived.border_focus else derived.border_unfocus; + } + break :blk config.border_color; + }; + + const title_color: ?Style.Color = blk: { + if (config.title_color) |tc| break :blk tc; + if (config.base_color) |base| { + const derived = Style.derivePanelFrameColors(base); + break :blk if (config.has_focus) derived.title_color else derived.border_unfocus; + } + break :blk border_color; + }; + // 1. Calculate target color and update transition - const target_bg = if (config.has_focus) config.focus_bg else config.unfocus_bg; + const target_bg = if (config.has_focus) focus_bg else unfocus_bg; const animating = transition.update(target_bg, self.frame_delta_ms); // Request animation frame if still transitioning @@ -627,10 +694,22 @@ pub const Context = struct { } // 4. Draw border if specified - if (config.border_color) |border| { + if (border_color) |border| { self.pushCommand(Command.rectOutline(rect.x, rect.y, rect.w, rect.h, border)); } + // 5. Draw title if specified + if (config.title) |title| { + if (title_color) |tc| { + self.pushCommand(.{ .text = .{ + .x = rect.x + 8, + .y = rect.y + 4, + .text = title, + .color = tc, + } }); + } + } + return animating; } diff --git a/src/core/style.zig b/src/core/style.zig index 2dc806d..f44c7fc 100644 --- a/src/core/style.zig +++ b/src/core/style.zig @@ -1342,3 +1342,101 @@ test "contrastTextColor" { const text_on_mid = contrastTextColor(mid_gray); try std.testing.expect(text_on_mid.r < 50); // Dark text (L = 0.502 > 0.5) } + +// ============================================================================= +// SMART PANEL V2: Derivación Genérica para drawPanelFrame +// ============================================================================= +// +// Fórmula matemática genérica (consensuada 2025-12-31): +// L = 0.2126*R + 0.7152*G + 0.0722*B (luminosidad percibida) +// blend_factor = base_blend + (max_blend - base_blend) * (1.0 - L) +// +// El blend es INVERSAMENTE proporcional a la luminosidad: +// - Colores oscuros (L baja, ej: azul 0.07) → más color visible (blend alto ~20%) +// - Colores brillantes (L alta, ej: amarillo 0.93) → menos color (blend bajo ~10%) +// +// Esto es puramente matemático, funciona para CUALQUIER color del círculo cromático. +// ============================================================================= + +/// Colors derived from a single base color for panel frame rendering. +/// Used by Context.drawPanelFrame() when base_color is provided. +pub const DerivedPanelColors = struct { + /// Background when focused + focus_bg: Color, + /// Background when unfocused + unfocus_bg: Color, + /// Border when focused (full base color) + border_focus: Color, + /// Border when unfocused (darkened) + border_unfocus: Color, + /// Title color (lightened base) + title_color: Color, +}; + +/// Derive panel frame colors from a single base color using generic luminance formula. +/// +/// The derivation is purely mathematical and works for ANY color: +/// - Luminance L = 0.2126*R + 0.7152*G + 0.0722*B +/// - Blend factor inversely proportional to L +/// +/// Example values: +/// - Blue (L=0.07): blend ~19% → more blue visible in dark background +/// - Red (L=0.21): blend ~18% → moderate visibility +/// - Green (L=0.72): blend ~13% → less green (already bright) +/// - Yellow (L=0.93): blend ~11% → minimal (very bright) +pub fn derivePanelFrameColors(base: Color) DerivedPanelColors { + const L = base.perceptualLuminance(); + + // Generic formula: blend inversely proportional to luminance + // Base blend: 10% (bright colors), Max blend: 20% (dark colors) + const base_blend: f32 = 0.10; + const max_blend: f32 = 0.20; + const blend_factor = base_blend + (max_blend - base_blend) * (1.0 - L); + + // Convert to percentage for blendTowards (inverted: 100% = all black) + const focus_pct: u8 = @intFromFloat((1.0 - blend_factor) * 100.0); + const unfocus_pct: u8 = @intFromFloat((1.0 - blend_factor * 0.5) * 100.0); + + const black = Color.soft_black; + + return .{ + .focus_bg = base.blendTowards(black, focus_pct), + .unfocus_bg = base.blendTowards(black, unfocus_pct), + .border_focus = base, + .border_unfocus = base.darken(30), + .title_color = base.lightenHsl(20), + }; +} + +test "derivePanelFrameColors blue" { + const blue = Color.rgb(59, 130, 246); // laravel_blue + const derived = derivePanelFrameColors(blue); + + // Blue has low luminance, should get more visible background + // L ~= 0.07 * 0.23 + 0.51 * 0.72 + 0.96 * 0.07 = 0.016 + 0.37 + 0.067 = 0.45 + // Actually for laravel_blue: L = 59/255*0.2126 + 130/255*0.7152 + 246/255*0.0722 + // = 0.049 + 0.365 + 0.070 = 0.484 + + // Border focus should be the original color + try std.testing.expectEqual(blue.r, derived.border_focus.r); + try std.testing.expectEqual(blue.g, derived.border_focus.g); + try std.testing.expectEqual(blue.b, derived.border_focus.b); + + // Focus bg should be darker than unfocus bg (more blend towards black) + // Actually focus has MORE color (less blend to black) + try std.testing.expect(derived.focus_bg.r >= derived.unfocus_bg.r); +} + +test "derivePanelFrameColors generic formula" { + // Test that brighter colors get less visibility (lower blend) + const dark_blue = Color.rgb(0, 0, 200); // L ~= 0.057 + const bright_yellow = Color.rgb(255, 255, 0); // L ~= 0.93 + + const blue_derived = derivePanelFrameColors(dark_blue); + const yellow_derived = derivePanelFrameColors(bright_yellow); + + // Blue background should have more color visible (less black blend) + // This means blue's focus_bg should be brighter/more colorful than yellow's relative to base + // We check that the formula produces different results + try std.testing.expect(blue_derived.focus_bg.b != yellow_derived.focus_bg.b); +}