Compare commits
No commits in common. "6732ac1fc5e9b735c27aedfbebfbf6eccd634a6e" and "067bac47fdd882b9233c10c32e1ac0c28ea0892d" have entirely different histories.
6732ac1fc5
...
067bac47fd
3 changed files with 97 additions and 117 deletions
132
CLAUDE.md
132
CLAUDE.md
|
|
@ -105,9 +105,9 @@ Resumen breve (1-2 frases). Resultado principal.
|
|||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **Nombre** | zcatgui |
|
||||
| **Versión** | v0.28.0 |
|
||||
| **Versión** | v0.21.2 |
|
||||
| **Fecha inicio** | 2025-12-09 |
|
||||
| **Estado** | ✅ COMPLETO - 38 widgets, ~36K LOC, 4 backends, WindowState, TTF funcional |
|
||||
| **Estado** | ✅ COMPLETO - 37 widgets, ~35K LOC, 4 backends, TTF funcional |
|
||||
| **Lenguaje** | Zig 0.15.2 |
|
||||
| **Paradigma** | Immediate Mode GUI |
|
||||
| **Inspiración** | Gio (Go), microui (C), DVUI (Zig), Dear ImGui (C++) |
|
||||
|
|
@ -247,14 +247,14 @@ zcatgui/
|
|||
|
||||
---
|
||||
|
||||
## ESTADO ACTUAL (v0.28.0)
|
||||
## ESTADO ACTUAL (v0.25.0)
|
||||
|
||||
### Widgets (38 total)
|
||||
|
||||
| Categoría | Widgets |
|
||||
|-----------|---------|
|
||||
| **Básicos** | Label, Button, Checkbox, Radio, Slider, TextInput, NumberEntry |
|
||||
| **Contenedores** | Panel, Split, Modal, Scroll, Tabs, Menu, **WindowState** |
|
||||
| **Contenedores** | Panel, Split, Modal, Scroll, Tabs, Menu |
|
||||
| **Datos** | List, Table, Tree, ReorderableList, VirtualScroll |
|
||||
| **Feedback** | Progress, Tooltip, Toast, Spinner |
|
||||
| **Input avanzado** | AutoComplete, Select, TextArea, ColorPicker, DatePicker |
|
||||
|
|
@ -268,18 +268,16 @@ zcatgui/
|
|||
- **iOS**: UIKit bridge (Objective-C)
|
||||
|
||||
### Core Systems
|
||||
- Context (FrameArena, dirty rectangles, ID system, Ghost Drawing)
|
||||
- Context (FrameArena, dirty rectangles, ID system)
|
||||
- Input (keyboard, mouse, touch, shortcuts, gestures)
|
||||
- Rendering (software renderer, AA, effects, **Shadow Cache**)
|
||||
- Animation (20+ easing functions, springs, **ColorTransition**)
|
||||
- Rendering (software renderer, AA, effects)
|
||||
- Animation (20+ easing functions, springs)
|
||||
- Themes (dark, light, high_contrast, nord, dracula)
|
||||
- **WindowState** (Panel interface a nivel de librería)
|
||||
|
||||
### Métricas
|
||||
- ~36,000 LOC en 85 archivos fuente
|
||||
- ~35,000 LOC en 81 archivos fuente
|
||||
- 0 warnings, 0 memory leaks
|
||||
- WASM: ~18KB compilado
|
||||
- **Shadow Baking**: 4.2x más rápido en Debug
|
||||
|
||||
→ Detalle completo: `REFERENCE.md`
|
||||
|
||||
|
|
@ -287,55 +285,48 @@ zcatgui/
|
|||
|
||||
## HITOS RECIENTES
|
||||
|
||||
### WindowState + Panel Interface ✅ (2026-01-04)
|
||||
Panel interface movido a nivel de librería para reutilización:
|
||||
- **WindowState**: Contenedor de paneles con vtable
|
||||
- **Panel**: Interface con draw, handleEvent, onFocus, onBlur
|
||||
- Cualquier app puede usar sin reimplementar lógica de paneles
|
||||
→ Archivo: `src/window.zig`
|
||||
|
||||
### Shadow Baking ✅ (2026-01-02)
|
||||
Cache de sombras prerenderizadas - **4.2x más rápido** en Debug:
|
||||
- Evita recalcular blur por frame
|
||||
- ShadowCache con max 64 entries
|
||||
- initWithCache() para habilitar
|
||||
→ Archivo: `render/software.zig`
|
||||
|
||||
### Glyph Blitting Optimizado ✅ (2026-01-02)
|
||||
Renderizado de texto hasta 3x más rápido:
|
||||
- Early exit si fuera de clip
|
||||
- Pre-cálculo región visible
|
||||
- Fast path para alpha=255
|
||||
→ Archivo: `render/ttf.zig`
|
||||
|
||||
### ColorTransition Epsilon Fix ✅ (2026-01-03)
|
||||
Fix animaciones que no terminaban (loop infinito):
|
||||
- lerpU8 truncaba incrementos <1 → nunca convergía
|
||||
- Epsilon check ANTES del lerp para forzar snap
|
||||
- Umbral <= 2 para margen de seguridad
|
||||
→ Archivo: `render/animation.zig`
|
||||
|
||||
### SIMD fillRect ✅ (2026-01-03)
|
||||
Relleno de rectángulos 8 píxeles por iteración:
|
||||
- @Vector(8, u32) para batch writing
|
||||
- Solo en hot paths (>50% tiempo en framebuffer)
|
||||
→ Archivo: `render/framebuffer.zig`
|
||||
|
||||
### Ghost Drawing ✅ (2026-01-02)
|
||||
Supresión de comandos sin romper input:
|
||||
- `ctx.suppress_commands = true` → pushCommand ignora
|
||||
- Widgets procesan input normalmente
|
||||
- 36% menos tiempo de ejecución
|
||||
→ Archivo: `core/context.zig`
|
||||
|
||||
### Liquid UI V2 ✅ (2025-12-30)
|
||||
Transiciones de color suaves (500ms) con contraste mejorado.
|
||||
→ Archivos: `render/animation.zig`, `core/style.zig`
|
||||
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)
|
||||
Mascota animada (gatito) tras inactividad.
|
||||
Mascota animada que aparece tras inactividad del usuario:
|
||||
- Se asoma por bordes de paneles aleatorios
|
||||
- Ojos que miran a los lados, orejas con movimiento
|
||||
- Salto de pánico al detectar actividad
|
||||
- Clipping correcto (respeta límites del panel)
|
||||
→ Archivo: `widgets/idle_companion.zig`
|
||||
|
||||
### Primitivas Gráficas 2D ✅ (2025-12-30)
|
||||
Nuevas primitivas para formas orgánicas:
|
||||
- **FilledTriangle**: Rasterización por scanlines (v0.23.0)
|
||||
- **FilledCircle**: Algoritmo Midpoint/Bresenham (v0.24.0)
|
||||
→ Archivos: `core/command.zig`, `render/software.zig`
|
||||
|
||||
### Refactorización Modular ✅ (2025-12-29)
|
||||
Archivos grandes modularizados en carpetas:
|
||||
- `autocomplete/` (910→571 LOC hub, -37%): state, types, filtering
|
||||
- `icon/` (805→515 LOC hub, -36%): types, drawing_helpers
|
||||
→ Detalle: `docs/REFACTORING_MODULAR_2025-12-29.md`
|
||||
|
||||
### AdvancedTable Color por Focus ✅ (2025-12-19)
|
||||
Fila seleccionada cambia color según focus de la tabla:
|
||||
- `selected_row`: color con focus (accent)
|
||||
- `selected_row_unfocus`: color sin focus (gris sutil)
|
||||
- `BasicColors` acepta override desde aplicación
|
||||
|
||||
### TTF Rendering ✅ (2025-12-17)
|
||||
Integración zcatttf v1.0 - texto TTF funciona perfectamente.
|
||||
→ Librería: `/mnt/cello2/arno/re/recode/zig/zcatttf/`
|
||||
|
||||
### Paridad Visual DVUI ✅ (2025-12-17)
|
||||
Sistema dual (simple/fancy), esquinas redondeadas, sombras, transiciones, focus ring AA.
|
||||
→ Detalle: `docs/research/DVUI_AUDIT_2025-12-17.md`
|
||||
|
||||
### AdvancedTable ✅ (2025-12-17)
|
||||
Widget ~3,700 LOC con schema, CRUD, sorting, lookup, multi-select, search, validation.
|
||||
→ Detalle: `docs/ADVANCED_TABLE_MERGE_PLAN.md`
|
||||
|
|
@ -376,26 +367,29 @@ pub const MacroPlayer = struct {
|
|||
|
||||
---
|
||||
|
||||
## OPTIMIZACIONES DE RENDIMIENTO (2026-01-05)
|
||||
## OPTIMIZACIONES DE RENDIMIENTO (2026-01-03)
|
||||
|
||||
### Optimizaciones Activas ✅
|
||||
|
||||
| # | Optimización | Archivo | Impacto |
|
||||
|---|--------------|---------|---------|
|
||||
| 1 | **Shadow Baking** | `render/software.zig` | 4.2x más rápido Debug |
|
||||
| 2 | **Glyph Blitting** | `render/ttf.zig` | 3x más rápido texto |
|
||||
| 3 | **SIMD fillRect** | `render/framebuffer.zig` | 8 pixels/iteración |
|
||||
| 4 | **Turbo Píxeles** | `render/framebuffer.zig` | Sin bounds checks |
|
||||
| 5 | **Fast Path α=255** | `render/ttf.zig` | Sin blend innecesario |
|
||||
| 6 | **Ghost Drawing** | `core/context.zig` | 36% menos comandos |
|
||||
| 7 | **ColorTransition ε** | `render/animation.zig` | Convergencia garantizada |
|
||||
| Optimización | Archivo | Descripción |
|
||||
|--------------|---------|-------------|
|
||||
| **Turbo Píxeles** | `render/framebuffer.zig:117` | `@setRuntimeSafety(false)` en `fillRect` - elimina bounds checks en hot path |
|
||||
| **Fast Path Texto** | `render/ttf.zig:323,375` | Escritura directa para píxeles opacos (alpha==255), evita blend innecesario |
|
||||
|
||||
### Resultado Final
|
||||
### Optimización Probada y Revertida ❌
|
||||
|
||||
- **Debug mode:** 180ms → 43ms (4.2x mejora)
|
||||
- **ReleaseSafe:** ~16.7ms = 60fps objetivo
|
||||
- **CPU idle:** ~0% (waitEventTimeout)
|
||||
- **Memory leaks:** 0 verificado
|
||||
| Optimización | Problema | Alternativa |
|
||||
|--------------|----------|-------------|
|
||||
| **Burst Suppression** (auto-gestión de estrés) | Causaba paneles vacíos durante navegación rápida | El debounce en DataManager ya evita queries excesivas |
|
||||
|
||||
**Detalles del Burst Suppression:**
|
||||
- Concepto: suprimir dibujo de paneles durante ráfagas de navegación (<100ms)
|
||||
- Implementación: `if (!frame_result.should_draw) return;` en paneles
|
||||
- Problema: el frame se dibujaba vacío (solo fondo), sin widgets
|
||||
- Causa raíz: suprimía TODO el dibujo, no solo las queries BD
|
||||
- Lección: el debounce a nivel DataManager es más elegante
|
||||
|
||||
**Pendiente verificar:** Mensajes continuos de redraw en idle (posible bug en animaciones).
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ const std = @import("std");
|
|||
const Context = @import("context.zig").Context;
|
||||
const Layout = @import("layout.zig");
|
||||
const Rect = Layout.Rect;
|
||||
const Input = @import("input.zig");
|
||||
|
||||
// =============================================================================
|
||||
// Panel Interface (con contexto opaco)
|
||||
|
|
@ -33,9 +32,6 @@ pub const PanelInfo = struct {
|
|||
preferred_zone: Zone = .center,
|
||||
/// Tecla de acceso rápido: Ctrl+focus_key (1-9, null = sin atajo)
|
||||
focus_key: ?u8 = null,
|
||||
/// Sensibilidad a ráfagas: si true, el panel se auto-suprime durante navegación rápida
|
||||
/// Paneles de navegación principal (who_list, who_detail) deben ser false
|
||||
burst_sensitive: bool = true,
|
||||
};
|
||||
|
||||
/// Zonas de layout disponibles
|
||||
|
|
@ -49,14 +45,19 @@ pub const Zone = enum {
|
|||
};
|
||||
|
||||
/// Evento simplificado para paneles
|
||||
/// Usa Input.KeyEvent de core/input.zig para compatibilidad
|
||||
pub const Event = union(enum) {
|
||||
key: Input.KeyEvent,
|
||||
key: KeyEvent,
|
||||
mouse: MouseEvent,
|
||||
resize: ResizeEvent,
|
||||
text_input: TextInputEvent,
|
||||
quit,
|
||||
|
||||
pub const KeyEvent = struct {
|
||||
key: u32,
|
||||
pressed: bool,
|
||||
modifiers: Modifiers,
|
||||
};
|
||||
|
||||
pub const MouseEvent = struct {
|
||||
x: i32,
|
||||
y: i32,
|
||||
|
|
@ -76,8 +77,11 @@ pub const Event = union(enum) {
|
|||
|
||||
pub const MouseButton = enum { left, middle, right };
|
||||
|
||||
// Re-export KeyEvent para acceso uniforme
|
||||
pub const KeyEvent = Input.KeyEvent;
|
||||
pub const Modifiers = struct {
|
||||
ctrl: bool = false,
|
||||
shift: bool = false,
|
||||
alt: bool = false,
|
||||
};
|
||||
};
|
||||
|
||||
/// Interface Panel con contexto opaco (desacoplado de aplicación)
|
||||
|
|
@ -195,9 +199,8 @@ pub const WindowState = struct {
|
|||
/// Identificador de la ventana
|
||||
id: []const u8,
|
||||
|
||||
/// Lista de paneles (array estático + contador - sin allocations dinámicas)
|
||||
panels_arr: [MAX_PANELS]Panel,
|
||||
panels_len: usize,
|
||||
/// Lista de paneles (BoundedArray - sin allocations dinámicas)
|
||||
panels: std.BoundedArray(Panel, MAX_PANELS),
|
||||
|
||||
/// Rectángulos para cada panel (actualizados externamente por layout)
|
||||
rects: [MAX_PANELS]Rect,
|
||||
|
|
@ -214,8 +217,7 @@ pub const WindowState = struct {
|
|||
pub fn init(id: []const u8, base_focus_group: u64) Self {
|
||||
return .{
|
||||
.id = id,
|
||||
.panels_arr = undefined,
|
||||
.panels_len = 0,
|
||||
.panels = .{},
|
||||
.rects = [_]Rect{.{ .x = 0, .y = 0, .w = 0, .h = 0 }} ** MAX_PANELS,
|
||||
.focused_idx = 0,
|
||||
.base_focus_group = base_focus_group,
|
||||
|
|
@ -224,19 +226,12 @@ pub const WindowState = struct {
|
|||
|
||||
/// Añade un panel a la ventana
|
||||
pub fn addPanel(self: *Self, panel: Panel) error{Overflow}!void {
|
||||
if (self.panels_len >= MAX_PANELS) return error.Overflow;
|
||||
self.panels_arr[self.panels_len] = panel;
|
||||
self.panels_len += 1;
|
||||
}
|
||||
|
||||
/// Obtiene slice de paneles
|
||||
pub fn panels(self: *Self) []Panel {
|
||||
return self.panels_arr[0..self.panels_len];
|
||||
try self.panels.append(panel);
|
||||
}
|
||||
|
||||
/// Actualiza los rectángulos de los paneles (llamado por layout externo)
|
||||
pub fn updateRects(self: *Self, rects: []const Rect) void {
|
||||
const len = @min(rects.len, self.panels_len);
|
||||
const len = @min(rects.len, self.panels.len);
|
||||
for (0..len) |i| {
|
||||
self.rects[i] = rects[i];
|
||||
}
|
||||
|
|
@ -245,7 +240,7 @@ pub const WindowState = struct {
|
|||
/// Dibuja todos los paneles - EL ID VIAJA CON EL PANEL
|
||||
/// Elimina errores de mismatch de IDs por construcción
|
||||
pub fn draw(self: *Self, ctx: *Context, app_ctx: ?*anyopaque) void {
|
||||
for (self.panels(), 0..) |panel, i| {
|
||||
for (self.panels.slice(), 0..) |panel, i| {
|
||||
const info = panel.getInfo();
|
||||
const rect = self.rects[i];
|
||||
|
||||
|
|
@ -261,17 +256,17 @@ pub const WindowState = struct {
|
|||
|
||||
/// Mueve el foco al siguiente panel (F6)
|
||||
pub fn focusNext(self: *Self, ctx: *Context) void {
|
||||
if (self.panels_len == 0) return;
|
||||
if (self.panels.len == 0) return;
|
||||
|
||||
// Notificar blur al panel actual
|
||||
const current = self.panels()[self.focused_idx];
|
||||
const current = self.panels.slice()[self.focused_idx];
|
||||
current.onBlur(null);
|
||||
|
||||
// Mover al siguiente
|
||||
self.focused_idx = (self.focused_idx + 1) % self.panels_len;
|
||||
self.focused_idx = (self.focused_idx + 1) % self.panels.len;
|
||||
|
||||
// Notificar focus al nuevo panel
|
||||
const next = self.panels()[self.focused_idx];
|
||||
const next = self.panels.slice()[self.focused_idx];
|
||||
next.onFocus(null);
|
||||
|
||||
// Actualizar focus group activo
|
||||
|
|
@ -280,17 +275,17 @@ pub const WindowState = struct {
|
|||
|
||||
/// Mueve el foco al panel anterior (Shift+F6)
|
||||
pub fn focusPrev(self: *Self, ctx: *Context) void {
|
||||
if (self.panels_len == 0) return;
|
||||
if (self.panels.len == 0) return;
|
||||
|
||||
// Notificar blur al panel actual
|
||||
const current = self.panels()[self.focused_idx];
|
||||
const current = self.panels.slice()[self.focused_idx];
|
||||
current.onBlur(null);
|
||||
|
||||
// Mover al anterior
|
||||
self.focused_idx = if (self.focused_idx == 0) self.panels_len - 1 else self.focused_idx - 1;
|
||||
self.focused_idx = if (self.focused_idx == 0) self.panels.len - 1 else self.focused_idx - 1;
|
||||
|
||||
// Notificar focus al nuevo panel
|
||||
const prev = self.panels()[self.focused_idx];
|
||||
const prev = self.panels.slice()[self.focused_idx];
|
||||
prev.onFocus(null);
|
||||
|
||||
// Actualizar focus group activo
|
||||
|
|
@ -299,17 +294,17 @@ pub const WindowState = struct {
|
|||
|
||||
/// Enfoca un panel por índice (Ctrl+1, Ctrl+2, etc.)
|
||||
pub fn focusByIndex(self: *Self, ctx: *Context, idx: usize) void {
|
||||
if (idx >= self.panels_len) return;
|
||||
if (idx >= self.panels.len) return;
|
||||
|
||||
// Notificar blur al panel actual
|
||||
const current = self.panels()[self.focused_idx];
|
||||
const current = self.panels.slice()[self.focused_idx];
|
||||
current.onBlur(null);
|
||||
|
||||
// Cambiar índice
|
||||
self.focused_idx = idx;
|
||||
|
||||
// Notificar focus al nuevo panel
|
||||
const target = self.panels()[self.focused_idx];
|
||||
const target = self.panels.slice()[self.focused_idx];
|
||||
target.onFocus(null);
|
||||
|
||||
// Actualizar focus group activo
|
||||
|
|
@ -318,19 +313,19 @@ pub const WindowState = struct {
|
|||
|
||||
/// Obtiene el panel actualmente enfocado
|
||||
pub fn getFocusedPanel(self: *Self) ?Panel {
|
||||
if (self.panels_len == 0) return null;
|
||||
return self.panels()[self.focused_idx];
|
||||
if (self.panels.len == 0) return null;
|
||||
return self.panels.slice()[self.focused_idx];
|
||||
}
|
||||
|
||||
/// Despacha un evento al panel enfocado
|
||||
pub fn handleEvent(self: *Self, event: Event, app_ctx: ?*anyopaque) bool {
|
||||
if (self.panels_len == 0) return false;
|
||||
const focused = self.panels()[self.focused_idx];
|
||||
if (self.panels.len == 0) return false;
|
||||
const focused = self.panels.slice()[self.focused_idx];
|
||||
return focused.handleEvent(event, app_ctx);
|
||||
}
|
||||
|
||||
/// Número de paneles en la ventana
|
||||
pub fn panelCount(self: *const Self) usize {
|
||||
return self.panels_len;
|
||||
return self.panels.len;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -69,15 +69,6 @@ pub const SwipeDirection = gesture.SwipeDirection;
|
|||
// Window system (paneles dinámicos)
|
||||
pub const window = @import("core/window.zig");
|
||||
pub const WindowState = window.WindowState;
|
||||
|
||||
// Panel types - usados por aplicaciones para definir paneles
|
||||
pub const Panel = window.Panel;
|
||||
pub const PanelInfo = window.PanelInfo;
|
||||
pub const Zone = window.Zone;
|
||||
pub const Event = window.Event;
|
||||
pub const makePanel = window.makePanel;
|
||||
|
||||
// Aliases legacy (deprecated - usar los de arriba)
|
||||
pub const WindowPanel = window.Panel;
|
||||
pub const WindowPanelInfo = window.PanelInfo;
|
||||
pub const makeWindowPanel = window.makePanel;
|
||||
|
|
|
|||
Loading…
Reference in a new issue