From ed2701fbd844431ac16f59c748e3a2477c6a19d7 Mon Sep 17 00:00:00 2001 From: "R.Eugenio" Date: Tue, 30 Dec 2025 19:24:36 +0100 Subject: [PATCH] feat(animation): Liquid UI V2 - Mayor fluidez y contraste MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ColorTransition: 200ms → 500ms (transiciones más perceptibles) - deriveDarkPalette: 4%/20% base color (mayor contraste focus/unfocus) - deriveLightPalette: 1%/6% base color (proporcional) - Context: añadir requestAnimationFrame/needsAnimationFrame - Tests actualizados para nuevos umbrales 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/core/context.zig | 20 ++++++++++++++++ src/core/style.zig | 49 ++++++++++++++++++++-------------------- src/render/animation.zig | 4 ++-- 3 files changed, 47 insertions(+), 26 deletions(-) diff --git a/src/core/context.zig b/src/core/context.zig index e5eb0e4..1443cc7 100644 --- a/src/core/context.zig +++ b/src/core/context.zig @@ -102,6 +102,10 @@ pub const Context = struct { /// Used for idle detection (e.g., cursor stops blinking after inactivity) last_input_time_ms: u64 = 0, + /// Flag set by widgets that have ongoing animations (e.g., color transitions). + /// Main loop should check this and request another frame if true. + needs_animation_frame: bool = false, + /// Optional text measurement function (set by application with TTF font) /// Returns pixel width of text. If null, falls back to char_width * len. text_measure_fn: ?*const fn ([]const u8) u32 = null, @@ -204,6 +208,9 @@ pub const Context = struct { // Focus system frame start self.focus.beginFrame(); + // Reset animation request (set by widgets during draw) + self.needs_animation_frame = false; + self.frame += 1; } @@ -363,6 +370,19 @@ pub const Context = struct { return @intCast(@max(time_until_toggle, 16)); // Minimum 16ms to avoid busy loop } + /// Request another animation frame (for color transitions, etc.). + /// Widgets call this during draw when they have ongoing animations. + /// Main loop should check needsAnimationFrame() after draw and schedule redraw. + pub fn requestAnimationFrame(self: *Self) void { + self.needs_animation_frame = true; + } + + /// Check if any widget requested an animation frame. + /// Call after draw to determine if immediate redraw is needed. + pub fn needsAnimationFrame(self: Self) bool { + return self.needs_animation_frame; + } + /// Determina si el cursor debe ser visible basado en tiempo de actividad. /// Usa parpadeo mientras hay actividad reciente, sólido cuando está idle. /// @param last_activity_time_ms: Última vez que hubo actividad (edición, input) diff --git a/src/core/style.zig b/src/core/style.zig index a52c8b3..d049f99 100644 --- a/src/core/style.zig +++ b/src/core/style.zig @@ -1031,9 +1031,9 @@ pub fn derivePanelPalette(base: Color, mode: ThemeMode) PanelColorScheme { } /// Derive palette for dark mode (dark backgrounds, light text) -/// Z-Design V2: "Atmósfera, no fogonazo" -/// - fondo_sin_focus: 7% base color (sutil identidad del panel) -/// - fondo_con_focus: 15% base color (brilla al ganar foco) +/// Z-Design V2 + Liquid UI: Mayor contraste para transiciones perceptibles +/// - fondo_sin_focus: 4% base color (más oscuro, punto de partida bajo) +/// - fondo_con_focus: 20% base color (brilla al ganar foco, destino alto) fn deriveDarkPalette(base: Color) PanelColorScheme { // Reference colors for dark mode const black = Color.soft_black; // RGB(17, 17, 20) - not pure black @@ -1042,11 +1042,11 @@ fn deriveDarkPalette(base: Color) PanelColorScheme { const dark_border = Color.rgb(60, 60, 65); return .{ - // Backgrounds: Z-Design V2 - panel siempre tiene su "identidad" - // Focus: 15% base, 85% black (brilla más) - .fondo_con_focus = base.blendTowards(black, 85), - // Sin focus: 7% base, 93% black (sutil pero presente) - .fondo_sin_focus = base.blendTowards(black, 93), + // Backgrounds: Liquid UI V2 - mayor recorrido para transición perceptible + // Focus: 20% base, 80% black (destino luminoso) + .fondo_con_focus = base.blendTowards(black, 80), + // Sin focus: 4% base, 96% black (punto de partida oscuro) + .fondo_sin_focus = base.blendTowards(black, 96), // Text: high contrast .datos = white, @@ -1068,9 +1068,9 @@ fn deriveDarkPalette(base: Color) PanelColorScheme { } /// Derive palette for light mode (light backgrounds, dark text) -/// Z-Design V2: Sincronizado con dark mode -/// - fondo_sin_focus: 1% base (casi blanco, sutil identidad) -/// - fondo_con_focus: 3% base (brilla al ganar foco) +/// Z-Design V2 + Liquid UI: Mayor contraste para transiciones perceptibles +/// - fondo_sin_focus: 1% base (casi blanco, punto de partida) +/// - fondo_con_focus: 6% base (brilla al ganar foco, destino) fn deriveLightPalette(base: Color) PanelColorScheme { // Reference colors for light mode const white = Color.soft_white; // RGB(250, 250, 252) - slight cool tint @@ -1079,10 +1079,10 @@ fn deriveLightPalette(base: Color) PanelColorScheme { const light_border = Color.rgb(220, 220, 225); return .{ - // Backgrounds: Z-Design V2 - panel siempre tiene su "identidad" - // Focus: 3% base, 97% white - .fondo_con_focus = base.blendTowards(white, 97), - // Sin focus: 1% base, 99% white (sutil pero presente) + // Backgrounds: Liquid UI V2 - mayor recorrido para transición perceptible + // Focus: 6% base, 94% white (destino más tintado) + .fondo_con_focus = base.blendTowards(white, 94), + // Sin focus: 1% base, 99% white (punto de partida neutro) .fondo_sin_focus = base.blendTowards(white, 99), // Text: high contrast @@ -1151,11 +1151,11 @@ test "derivePanelPalette dark mode" { try std.testing.expectEqual(Color.laravel_red.g, palette.seleccion_fondo_con_focus.g); try std.testing.expectEqual(Color.laravel_red.b, palette.seleccion_fondo_con_focus.b); - // Background should be very dark (close to black, slight red tint) - // 95% blend towards black = mostly black with 5% of base color - try std.testing.expect(palette.fondo_con_focus.r < 35); // Red tint visible - try std.testing.expect(palette.fondo_con_focus.g < 25); - try std.testing.expect(palette.fondo_con_focus.b < 25); + // Background should be dark with visible tint (Liquid UI V2: 20% base color) + // 80% blend towards black = dark with noticeable tint for transitions + try std.testing.expect(palette.fondo_con_focus.r < 65); // Red tint visible + try std.testing.expect(palette.fondo_con_focus.g < 35); + try std.testing.expect(palette.fondo_con_focus.b < 35); // The red component should be slightly higher than G/B (tint visible) try std.testing.expect(palette.fondo_con_focus.r >= palette.fondo_con_focus.g); @@ -1167,10 +1167,11 @@ test "derivePanelPalette light mode" { // Selection should be the full base color try std.testing.expectEqual(Color.laravel_blue.r, palette.seleccion_fondo_con_focus.r); - // Background should be very light (close to white, slight blue tint) - try std.testing.expect(palette.fondo_con_focus.r > 240); - try std.testing.expect(palette.fondo_con_focus.g > 245); - try std.testing.expect(palette.fondo_con_focus.b > 250); + // Background should be light with visible tint (Liquid UI V2: 6% base color) + // 94% blend towards white = light with noticeable tint for transitions + try std.testing.expect(palette.fondo_con_focus.r > 230); + try std.testing.expect(palette.fondo_con_focus.g > 235); + try std.testing.expect(palette.fondo_con_focus.b > 240); } test "contrastTextColor" { diff --git a/src/render/animation.zig b/src/render/animation.zig index c852d6f..ad248e5 100644 --- a/src/render/animation.zig +++ b/src/render/animation.zig @@ -704,8 +704,8 @@ pub const ColorTransition = struct { current: Style.Color = Style.Color.rgb(0, 0, 0), /// Is initialized with a color? initialized: bool = false, - /// Transition duration in milliseconds - duration_ms: f32 = 200.0, + /// Transition duration in milliseconds (500ms = Liquid UI feel) + duration_ms: f32 = 500.0, const Self = @This();