feat(context): Smart Panel V2 - base_color derivación genérica + título
- PanelFrameConfig: añadir base_color, title, title_color - drawPanelFrame: modo híbrido (explícito vs Z-Design) - derivePanelFrameColors: fórmula genérica luminosidad (L inversamente proporcional) - Documentar decisión de omitir clipping (performance + control) Consensuado: Claude + Gemini + R.Eugenio (2025-12-31)
This commit is contained in:
parent
5ba0cc9f25
commit
797cca736c
2 changed files with 190 additions and 13 deletions
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue