Compare commits
6 commits
b67de1ce01
...
9c29faaa81
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c29faaa81 | |||
| ed2701fbd8 | |||
| 0e913cda55 | |||
| 6eae44dcfd | |||
| ce93b1fe95 | |||
| 9772d33b06 |
12 changed files with 77 additions and 38 deletions
|
|
@ -285,6 +285,14 @@ zcatgui/
|
||||||
|
|
||||||
## HITOS RECIENTES
|
## HITOS RECIENTES
|
||||||
|
|
||||||
|
### Liquid UI V2 ✅ (2025-12-30)
|
||||||
|
Sistema de transiciones de color suaves para paneles:
|
||||||
|
- **ColorTransition**: 500ms (medio segundo) para transiciones perceptibles
|
||||||
|
- **Mayor contraste**: Dark mode 4%/20% base, Light mode 1%/6% base
|
||||||
|
- **requestAnimationFrame()**: Paneles solicitan redraw durante animación
|
||||||
|
- Fondo de paneles "fluye" al cambiar focus
|
||||||
|
→ Archivos: `render/animation.zig`, `core/style.zig`, `core/context.zig`
|
||||||
|
|
||||||
### IdleCompanion Widget ✅ (2025-12-30)
|
### IdleCompanion Widget ✅ (2025-12-30)
|
||||||
Mascota animada que aparece tras inactividad del usuario:
|
Mascota animada que aparece tras inactividad del usuario:
|
||||||
- Se asoma por bordes de paneles aleatorios
|
- Se asoma por bordes de paneles aleatorios
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,10 @@ pub const Context = struct {
|
||||||
/// Used for idle detection (e.g., cursor stops blinking after inactivity)
|
/// Used for idle detection (e.g., cursor stops blinking after inactivity)
|
||||||
last_input_time_ms: u64 = 0,
|
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)
|
/// Optional text measurement function (set by application with TTF font)
|
||||||
/// Returns pixel width of text. If null, falls back to char_width * len.
|
/// Returns pixel width of text. If null, falls back to char_width * len.
|
||||||
text_measure_fn: ?*const fn ([]const u8) u32 = null,
|
text_measure_fn: ?*const fn ([]const u8) u32 = null,
|
||||||
|
|
@ -204,6 +208,9 @@ pub const Context = struct {
|
||||||
// Focus system frame start
|
// Focus system frame start
|
||||||
self.focus.beginFrame();
|
self.focus.beginFrame();
|
||||||
|
|
||||||
|
// Reset animation request (set by widgets during draw)
|
||||||
|
self.needs_animation_frame = false;
|
||||||
|
|
||||||
self.frame += 1;
|
self.frame += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -363,6 +370,19 @@ pub const Context = struct {
|
||||||
return @intCast(@max(time_until_toggle, 16)); // Minimum 16ms to avoid busy loop
|
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.
|
/// Determina si el cursor debe ser visible basado en tiempo de actividad.
|
||||||
/// Usa parpadeo mientras hay actividad reciente, sólido cuando está idle.
|
/// Usa parpadeo mientras hay actividad reciente, sólido cuando está idle.
|
||||||
/// @param last_activity_time_ms: Última vez que hubo actividad (edición, input)
|
/// @param last_activity_time_ms: Última vez que hubo actividad (edición, input)
|
||||||
|
|
|
||||||
|
|
@ -1031,9 +1031,9 @@ pub fn derivePanelPalette(base: Color, mode: ThemeMode) PanelColorScheme {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Derive palette for dark mode (dark backgrounds, light text)
|
/// Derive palette for dark mode (dark backgrounds, light text)
|
||||||
/// Z-Design V2: "Atmósfera, no fogonazo"
|
/// Z-Design V2 + Liquid UI: Mayor contraste para transiciones perceptibles
|
||||||
/// - fondo_sin_focus: 7% base color (sutil identidad del panel)
|
/// - fondo_sin_focus: 4% base color (más oscuro, punto de partida bajo)
|
||||||
/// - fondo_con_focus: 15% base color (brilla al ganar foco)
|
/// - fondo_con_focus: 20% base color (brilla al ganar foco, destino alto)
|
||||||
fn deriveDarkPalette(base: Color) PanelColorScheme {
|
fn deriveDarkPalette(base: Color) PanelColorScheme {
|
||||||
// Reference colors for dark mode
|
// Reference colors for dark mode
|
||||||
const black = Color.soft_black; // RGB(17, 17, 20) - not pure black
|
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);
|
const dark_border = Color.rgb(60, 60, 65);
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
// Backgrounds: Z-Design V2 - panel siempre tiene su "identidad"
|
// Backgrounds: Liquid UI V2 - mayor recorrido para transición perceptible
|
||||||
// Focus: 15% base, 85% black (brilla más)
|
// Focus: 20% base, 80% black (destino luminoso)
|
||||||
.fondo_con_focus = base.blendTowards(black, 85),
|
.fondo_con_focus = base.blendTowards(black, 80),
|
||||||
// Sin focus: 7% base, 93% black (sutil pero presente)
|
// Sin focus: 4% base, 96% black (punto de partida oscuro)
|
||||||
.fondo_sin_focus = base.blendTowards(black, 93),
|
.fondo_sin_focus = base.blendTowards(black, 96),
|
||||||
|
|
||||||
// Text: high contrast
|
// Text: high contrast
|
||||||
.datos = white,
|
.datos = white,
|
||||||
|
|
@ -1068,9 +1068,9 @@ fn deriveDarkPalette(base: Color) PanelColorScheme {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Derive palette for light mode (light backgrounds, dark text)
|
/// Derive palette for light mode (light backgrounds, dark text)
|
||||||
/// Z-Design V2: Sincronizado con dark mode
|
/// Z-Design V2 + Liquid UI: Mayor contraste para transiciones perceptibles
|
||||||
/// - fondo_sin_focus: 1% base (casi blanco, sutil identidad)
|
/// - fondo_sin_focus: 1% base (casi blanco, punto de partida)
|
||||||
/// - fondo_con_focus: 3% base (brilla al ganar foco)
|
/// - fondo_con_focus: 6% base (brilla al ganar foco, destino)
|
||||||
fn deriveLightPalette(base: Color) PanelColorScheme {
|
fn deriveLightPalette(base: Color) PanelColorScheme {
|
||||||
// Reference colors for light mode
|
// Reference colors for light mode
|
||||||
const white = Color.soft_white; // RGB(250, 250, 252) - slight cool tint
|
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);
|
const light_border = Color.rgb(220, 220, 225);
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
// Backgrounds: Z-Design V2 - panel siempre tiene su "identidad"
|
// Backgrounds: Liquid UI V2 - mayor recorrido para transición perceptible
|
||||||
// Focus: 3% base, 97% white
|
// Focus: 6% base, 94% white (destino más tintado)
|
||||||
.fondo_con_focus = base.blendTowards(white, 97),
|
.fondo_con_focus = base.blendTowards(white, 94),
|
||||||
// Sin focus: 1% base, 99% white (sutil pero presente)
|
// Sin focus: 1% base, 99% white (punto de partida neutro)
|
||||||
.fondo_sin_focus = base.blendTowards(white, 99),
|
.fondo_sin_focus = base.blendTowards(white, 99),
|
||||||
|
|
||||||
// Text: high contrast
|
// 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.g, palette.seleccion_fondo_con_focus.g);
|
||||||
try std.testing.expectEqual(Color.laravel_red.b, palette.seleccion_fondo_con_focus.b);
|
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)
|
// Background should be dark with visible tint (Liquid UI V2: 20% base color)
|
||||||
// 95% blend towards black = mostly black with 5% of base color
|
// 80% blend towards black = dark with noticeable tint for transitions
|
||||||
try std.testing.expect(palette.fondo_con_focus.r < 35); // Red tint visible
|
try std.testing.expect(palette.fondo_con_focus.r < 65); // Red tint visible
|
||||||
try std.testing.expect(palette.fondo_con_focus.g < 25);
|
try std.testing.expect(palette.fondo_con_focus.g < 35);
|
||||||
try std.testing.expect(palette.fondo_con_focus.b < 25);
|
try std.testing.expect(palette.fondo_con_focus.b < 35);
|
||||||
|
|
||||||
// The red component should be slightly higher than G/B (tint visible)
|
// 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);
|
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
|
// Selection should be the full base color
|
||||||
try std.testing.expectEqual(Color.laravel_blue.r, palette.seleccion_fondo_con_focus.r);
|
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)
|
// Background should be light with visible tint (Liquid UI V2: 6% base color)
|
||||||
try std.testing.expect(palette.fondo_con_focus.r > 240);
|
// 94% blend towards white = light with noticeable tint for transitions
|
||||||
try std.testing.expect(palette.fondo_con_focus.g > 245);
|
try std.testing.expect(palette.fondo_con_focus.r > 230);
|
||||||
try std.testing.expect(palette.fondo_con_focus.b > 250);
|
try std.testing.expect(palette.fondo_con_focus.g > 235);
|
||||||
|
try std.testing.expect(palette.fondo_con_focus.b > 240);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "contrastTextColor" {
|
test "contrastTextColor" {
|
||||||
|
|
|
||||||
|
|
@ -72,8 +72,8 @@ pub const ActionButtonsOpts = struct {
|
||||||
can_delete: bool = false,
|
can_delete: bool = false,
|
||||||
/// Ancho de cada boton
|
/// Ancho de cada boton
|
||||||
button_width: u32 = 80,
|
button_width: u32 = 80,
|
||||||
/// Alto de cada boton
|
/// Alto de cada boton (Z-Design V2: reducido de 28 a 22px)
|
||||||
button_height: u32 = 28,
|
button_height: u32 = 22,
|
||||||
/// Espacio entre botones
|
/// Espacio entre botones
|
||||||
spacing: i32 = 8,
|
spacing: i32 = 8,
|
||||||
/// Offset X desde el borde izquierdo del panel
|
/// Offset X desde el borde izquierdo del panel
|
||||||
|
|
@ -96,8 +96,8 @@ pub const NavButtonsOpts = struct {
|
||||||
can_last: bool = false,
|
can_last: bool = false,
|
||||||
/// Ancho de cada boton
|
/// Ancho de cada boton
|
||||||
button_width: u32 = 40,
|
button_width: u32 = 40,
|
||||||
/// Alto de cada boton
|
/// Alto de cada boton (Z-Design V2: reducido de 28 a 22px)
|
||||||
button_height: u32 = 28,
|
button_height: u32 = 22,
|
||||||
/// Offset X desde el borde izquierdo del panel
|
/// Offset X desde el borde izquierdo del panel
|
||||||
x_offset: i32 = 8,
|
x_offset: i32 = 8,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -704,8 +704,8 @@ pub const ColorTransition = struct {
|
||||||
current: Style.Color = Style.Color.rgb(0, 0, 0),
|
current: Style.Color = Style.Color.rgb(0, 0, 0),
|
||||||
/// Is initialized with a color?
|
/// Is initialized with a color?
|
||||||
initialized: bool = false,
|
initialized: bool = false,
|
||||||
/// Transition duration in milliseconds
|
/// Transition duration in milliseconds (500ms = Liquid UI feel)
|
||||||
duration_ms: f32 = 200.0,
|
duration_ms: f32 = 500.0,
|
||||||
|
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,16 @@ pub fn advancedTableRect(
|
||||||
var memory_ds = MemoryDataSource.init(table_state, table_schema.columns);
|
var memory_ds = MemoryDataSource.init(table_state, table_schema.columns);
|
||||||
const data_src = memory_ds.toDataSource();
|
const data_src = memory_ds.toDataSource();
|
||||||
|
|
||||||
|
// Z-Design: Pintar fondo del área de contenido ANTES de las filas
|
||||||
|
// Esto asegura que tablas vacías o con pocas filas no muestren negro
|
||||||
|
ctx.pushCommand(Command.rect(
|
||||||
|
bounds.x,
|
||||||
|
bounds.y + @as(i32, @intCast(header_h)),
|
||||||
|
bounds.w,
|
||||||
|
content_h,
|
||||||
|
colors.row_normal,
|
||||||
|
));
|
||||||
|
|
||||||
// Construir RowRenderColors manualmente (los dos TableColors son tipos diferentes)
|
// Construir RowRenderColors manualmente (los dos TableColors son tipos diferentes)
|
||||||
const render_colors = table_core.RowRenderColors{
|
const render_colors = table_core.RowRenderColors{
|
||||||
.row_normal = colors.row_normal,
|
.row_normal = colors.row_normal,
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,7 @@ pub fn drawScrollbar(
|
||||||
const total_rows = table_state.getRowCount();
|
const total_rows = table_state.getRowCount();
|
||||||
if (total_rows == 0) return;
|
if (total_rows == 0) return;
|
||||||
|
|
||||||
const scrollbar_w: u32 = 12;
|
const scrollbar_w: u32 = 14; // Z-Design V2: más ancho para mejor visibilidad
|
||||||
const header_h: u32 = if (config.show_headers) config.header_height else 0;
|
const header_h: u32 = if (config.show_headers) config.header_height else 0;
|
||||||
const scrollbar_h = bounds.h -| header_h;
|
const scrollbar_h = bounds.h -| header_h;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -346,7 +346,7 @@ pub fn gridRect(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn drawScrollbar(ctx: *Context, bounds: Layout.Rect, scroll_y: i32, total_height: u32, visible_height: u32, colors: Colors) void {
|
fn drawScrollbar(ctx: *Context, bounds: Layout.Rect, scroll_y: i32, total_height: u32, visible_height: u32, colors: Colors) void {
|
||||||
const scrollbar_width: u32 = 8;
|
const scrollbar_width: u32 = 14; // Z-Design V2: más ancho
|
||||||
const scrollbar_x = bounds.x + @as(i32, @intCast(bounds.w)) - @as(i32, @intCast(scrollbar_width)) - 2;
|
const scrollbar_x = bounds.x + @as(i32, @intCast(bounds.w)) - @as(i32, @intCast(scrollbar_width)) - 2;
|
||||||
const scrollbar_y = bounds.y + 2;
|
const scrollbar_y = bounds.y + 2;
|
||||||
const scrollbar_h = bounds.h -| 4;
|
const scrollbar_h = bounds.h -| 4;
|
||||||
|
|
|
||||||
|
|
@ -236,7 +236,7 @@ pub fn listRect(
|
||||||
|
|
||||||
// Draw scrollbar if needed
|
// Draw scrollbar if needed
|
||||||
if (items.len > visible_count) {
|
if (items.len > visible_count) {
|
||||||
const scrollbar_w: u32 = 8;
|
const scrollbar_w: u32 = 14; // Z-Design V2: más ancho para mejor visibilidad
|
||||||
const scrollbar_x = bounds.x + @as(i32, @intCast(bounds.w)) - @as(i32, @intCast(scrollbar_w + 1));
|
const scrollbar_x = bounds.x + @as(i32, @intCast(bounds.w)) - @as(i32, @intCast(scrollbar_w + 1));
|
||||||
|
|
||||||
// Scrollbar track
|
// Scrollbar track
|
||||||
|
|
|
||||||
|
|
@ -367,7 +367,7 @@ pub fn drawScrollbar(
|
||||||
) void {
|
) void {
|
||||||
_ = config;
|
_ = config;
|
||||||
|
|
||||||
const scrollbar_w: u32 = 12;
|
const scrollbar_w: u32 = 14; // Z-Design V2: más ancho
|
||||||
const header_h: u32 = 28; // Assume header
|
const header_h: u32 = 28; // Assume header
|
||||||
|
|
||||||
const track_x = bounds.right() - @as(i32, @intCast(scrollbar_w));
|
const track_x = bounds.right() - @as(i32, @intCast(scrollbar_w));
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ pub fn drawFilterBar(
|
||||||
const chip_h: u32 = 22;
|
const chip_h: u32 = 22;
|
||||||
const chip_padding: i32 = 10;
|
const chip_padding: i32 = 10;
|
||||||
const chip_spacing: i32 = 6;
|
const chip_spacing: i32 = 6;
|
||||||
const chip_radius: u8 = 11;
|
const chip_radius: u8 = 4; // Z-Design V2: consistente con botones
|
||||||
const clear_btn_w: u32 = 22;
|
const clear_btn_w: u32 = 22;
|
||||||
|
|
||||||
// Background
|
// Background
|
||||||
|
|
@ -446,7 +446,7 @@ pub fn drawScrollbar(
|
||||||
total_rows: usize,
|
total_rows: usize,
|
||||||
colors: *const VirtualAdvancedTableConfig.Colors,
|
colors: *const VirtualAdvancedTableConfig.Colors,
|
||||||
) void {
|
) void {
|
||||||
const scrollbar_w: u32 = 12;
|
const scrollbar_w: u32 = 14; // Z-Design V2: más ancho para mejor visibilidad
|
||||||
const content_h = bounds.h -| header_h -| footer_h;
|
const content_h = bounds.h -| header_h -| footer_h;
|
||||||
|
|
||||||
table_core.drawVerticalScrollbar(ctx, .{
|
table_core.drawVerticalScrollbar(ctx, .{
|
||||||
|
|
|
||||||
|
|
@ -129,8 +129,8 @@ pub const VirtualScrollState = struct {
|
||||||
pub const VirtualScrollConfig = struct {
|
pub const VirtualScrollConfig = struct {
|
||||||
/// Show scrollbar
|
/// Show scrollbar
|
||||||
show_scrollbar: bool = true,
|
show_scrollbar: bool = true,
|
||||||
/// Scrollbar width
|
/// Scrollbar width (Z-Design V2: 14px para mejor visibilidad)
|
||||||
scrollbar_width: u16 = 12,
|
scrollbar_width: u16 = 14,
|
||||||
/// Overscan (render extra items above/below viewport)
|
/// Overscan (render extra items above/below viewport)
|
||||||
overscan: u16 = 2,
|
overscan: u16 = 2,
|
||||||
/// Enable smooth scrolling
|
/// Enable smooth scrolling
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue