From 3517a6f97214b90b0abac1c36cbec8254cc6f019 Mon Sep 17 00:00:00 2001 From: reugenio Date: Thu, 11 Dec 2025 18:50:37 +0100 Subject: [PATCH] docs: Documentar sistema de focus completado y widgets adaptados MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Actualizar FOCUS_TRANSITION_2025-12-11.md con patrón de integración - Actualizar CLAUDE.md: sección SISTEMA DE FOCUS - RESUELTO - Widgets adaptados a FocusSystem: - numberentry.zig: registerFocusable, requestFocus, hasFocus - textarea.zig: registerFocusable, requestFocus, hasFocus - select.zig: campo focused, integración completa - radio.zig: reemplazado focus manual por FocusSystem - slider.zig: reemplazado focus manual por FocusSystem - tabs.zig: navegación teclado solo cuando tiene focus 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 96 ++++++++++++++++------------- docs/FOCUS_TRANSITION_2025-12-11.md | 72 +++++++++++++++++++++- src/widgets/numberentry.zig | 21 +++++-- src/widgets/radio.zig | 12 +++- src/widgets/select.zig | 17 ++++- src/widgets/slider.zig | 12 +++- src/widgets/tabs.zig | 19 +++++- src/widgets/textarea.zig | 17 ++++- 8 files changed, 206 insertions(+), 60 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0c31da3..e6be68a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -600,6 +600,8 @@ const stdout = std.fs.File.stdout(); // NO std.io.getStdOut() | 2025-12-09 | v0.14.1 | FASE 9: Gio parity - 12 widgets + gesture system | | 2025-12-09 | v0.15.0 | FASE 10: Mobile/Web - WASM, Android, iOS backends | | 2025-12-09 | v0.15.0 | Documentación: REFERENCE.md completo (1370 líneas) | +| 2025-12-11 | v0.15.1 | FocusSystem rediseñado: registration_group/active_group, focus implícito | +| 2025-12-11 | v0.15.2 | Widgets adaptados a FocusSystem: numberentry, textarea, select, radio, slider, tabs | --- @@ -685,69 +687,75 @@ cd /mnt/cello2/arno/re/recode/zig/zcatgui --- -## TAREA PENDIENTE PRIORITARIA: SISTEMA DE FOCUS (2025-12-11) +## SISTEMA DE FOCUS - RESUELTO (2025-12-11) -> **IMPORTANTE**: Esta seccion describe trabajo incompleto que DEBE completarse. -> La sesion anterior hizo multiples intentos sin exito. Se requiere un analisis -> profundo antes de hacer mas cambios. +El sistema de focus fue rediseñado y ahora funciona correctamente. -### Documento de transicion (LEER PRIMERO) +### Documentación completa ``` /mnt/cello2/arno/re/recode/zig/zcatgui/docs/FOCUS_TRANSITION_2025-12-11.md ``` -### Resumen del problema +### Arquitectura final -El sistema de focus fue rediseñado (unificado de dos sistemas a uno) pero **NO FUNCIONA**: +``` + FocusSystem + | + +---------------+---------------+ + | | + FocusGroup 1 FocusGroup 2 + (Panel Lista) (Panel Detalle) + | | + Table widget TextInput widgets + | | + - registerFocusable() - registerFocusable() + - hasFocus() -> true/false - hasFocus() -> true/false +``` -1. Al iniciar app, ambos paneles muestran focus visual -2. Teclado no responde hasta hacer clic (flechas, Tab) -3. Despues de clic en panel derecho, flechas siguen moviendo tabla izquierda +### Conceptos clave -### Cambios realizados en esta sesion +- **registration_group**: Grupo donde se registran widgets durante draw +- **active_group**: Grupo con focus de teclado (solo cambia con F6/clic) +- **Focus implícito**: Primer widget del grupo activo tiene focus si `focused_index == null` -**Archivos creados**: -- `core/focus.zig` - Nuevo FocusSystem unificado +### API para widgets -**Archivos eliminados**: -- `widgets/focus.zig` - FocusManager viejo -- `core/focus_group.zig` - FocusGroupManager viejo +```zig +// 1. Generar ID único basado en dirección del state +const widget_id: u64 = @intFromPtr(state); -**Archivos modificados**: -- `core/context.zig` - Usa FocusSystem, metodos de conveniencia -- `widgets/table.zig` - Añadido `handle_tab` config, usa ctx.hasFocus() -- `widgets/text_input.zig` - Usa ctx.hasFocus(), ctx.requestFocus() -- `zcatgui.zig` - Exporta FocusSystem, FocusGroup -- `backend/backend.zig` - Nuevo evento window_exposed -- `backend/sdl2.zig` - Emite window_exposed +// 2. Registrar en el grupo de focus activo +ctx.registerFocusable(widget_id); -### Lo que funciona +// 3. Al hacer clic, solicitar focus +if (clicked) { + ctx.requestFocus(widget_id); +} -- Repintado al volver de Alt+Tab -- Navegacion por tabla post-clic -- Tab entre TextInputs post-clic -- Compilacion sin errores +// 4. Verificar si tiene focus +const has_focus = ctx.hasFocus(widget_id); +state.focused = has_focus; +``` -### Lo que NO funciona +### Widgets integrados con FocusSystem -- Focus inicial automatico -- Teclado antes de primer clic -- Focus visual exclusivo (ambos paneles lo muestran) -- Aislamiento de grupos (flechas afectan tabla aunque focus este en otro panel) +| Widget | Estado | +|--------|--------| +| Table | Adaptado - inicializa selected_row/col a 0 | +| TextInput | Correcto | +| NumberEntry | Adaptado | +| TextArea | Adaptado | +| Select | Adaptado | +| Radio | Adaptado | +| Slider | Adaptado | +| Tabs | Adaptado | -### Hipotesis del bug +### Widgets sin FocusSystem (no lo necesitan) -Ver documento de transicion para hipotesis detalladas. Resumen: -1. Cambio de `active_group` durante draw rompe logica -2. Focus visual no sincronizado con estado real -3. Table procesa teclado independientemente -4. Widgets no se registran en grupo correcto - -### Regla para continuar - -**Analizar primero, planificar despues, implementar al final.** -NO hacer cambios incrementales sin entender la causa raiz. +- **Modales**: Menu, Modal, Tooltip, Toast +- **Acción única**: Button, Checkbox, Switch +- **Solo visualización**: Label, Progress, Badge, Icon, Image --- diff --git a/docs/FOCUS_TRANSITION_2025-12-11.md b/docs/FOCUS_TRANSITION_2025-12-11.md index 3c1c908..2d5e49a 100644 --- a/docs/FOCUS_TRANSITION_2025-12-11.md +++ b/docs/FOCUS_TRANSITION_2025-12-11.md @@ -144,5 +144,75 @@ Cualquier funcion que dependa de multiples campos (como `selectedCell()`) fallar --- -*Documento cerrado: 2025-12-11 ~19:00* +## WIDGETS ADAPTADOS AL SISTEMA DE FOCUS + +Todos los widgets interactivos fueron revisados y adaptados al nuevo sistema de focus. + +### Patrón de integración + +Cada widget interactivo debe seguir este patrón: + +```zig +pub fn widgetRect(ctx: *Context, bounds: Layout.Rect, state: *WidgetState, ...) Result { + // 1. Generar ID único basado en dirección del state + const widget_id: u64 = @intFromPtr(state); + + // 2. Registrar en el grupo de focus activo + ctx.registerFocusable(widget_id); + + // 3. Al hacer clic, solicitar focus + if (clicked) { + ctx.requestFocus(widget_id); + } + + // 4. Verificar si tiene focus + const has_focus = ctx.hasFocus(widget_id); + state.focused = has_focus; + + // 5. Solo procesar teclado si tiene focus + if (has_focus) { + // ... manejo de teclas + } +} +``` + +### Widgets adaptados (integrados con FocusSystem) + +| Widget | Archivo | Estado | Notas | +|--------|---------|--------|-------| +| **Table** | `table.zig` | Adaptado | Fix principal - inicializa selected_row/col a 0 | +| **TextInput** | `text_input.zig` | Correcto | Ya usaba el patrón correcto | +| **NumberEntry** | `numberentry.zig` | Adaptado | Añadido registerFocusable, requestFocus, hasFocus | +| **TextArea** | `textarea.zig` | Adaptado | Añadido registerFocusable, requestFocus, hasFocus | +| **Select** | `select.zig` | Adaptado | Añadido campo focused, integración completa | +| **Radio** | `radio.zig` | Adaptado | Reemplazado focus manual por FocusSystem | +| **Slider** | `slider.zig` | Adaptado | Reemplazado focus manual por FocusSystem | +| **Tabs** | `tabs.zig` | Adaptado | Navegación por teclado solo cuando tiene focus | + +### Widgets que NO requieren adaptación + +| Widget | Motivo | +|--------|--------| +| **Menu** | Modal - cuando está abierto captura toda la entrada | +| **Checkbox** | Solo responde a clic, no mantiene focus persistente | +| **Switch** | Solo responde a clic, no mantiene focus persistente | +| **Button** | Solo responde a clic, no mantiene focus persistente | +| **Widgets display-only** | label, badge, progress, icon, image, etc. - no interactivos | +| **Overlays** | modal, tooltip, toast - manejan su propia modalidad | + +### Categorización de widgets + +**Widgets con focus persistente** (necesitan FocusSystem): +- Widgets de entrada de datos (TextInput, NumberEntry, TextArea) +- Widgets de selección con teclado (Table, Select, Radio, Tabs) +- Widgets de valor con teclado (Slider) + +**Widgets sin focus persistente** (NO necesitan FocusSystem): +- Widgets de acción única (Button, Checkbox, Switch) +- Widgets de solo visualización (Label, Progress, Badge, Icon) +- Widgets modales/overlay (Menu, Modal, Tooltip, Toast) + +--- + +*Documento actualizado: 2025-12-11* *Autores: Arno + Claude (Opus 4.5)* diff --git a/src/widgets/numberentry.zig b/src/widgets/numberentry.zig index 2153a64..4fbaf34 100644 --- a/src/widgets/numberentry.zig +++ b/src/widgets/numberentry.zig @@ -255,14 +255,25 @@ pub fn numberEntryRect( if (bounds.isEmpty()) return result; + // Generate unique ID for this widget based on state address + const widget_id: u64 = @intFromPtr(state); + + // Register as focusable in the active focus group + ctx.registerFocusable(widget_id); + const mouse = ctx.input.mousePos(); const mouse_pressed = ctx.input.mousePressed(.left); const hovered = bounds.contains(mouse.x, mouse.y); if (hovered and mouse_pressed) { - state.focused = true; + // Request focus through the focus system + ctx.requestFocus(widget_id); } + // Check if this widget has focus + const has_focus = ctx.hasFocus(widget_id); + state.focused = has_focus; + // Calculate areas const spinner_w: u32 = if (config.spinner) 20 else 0; const prefix_w: u32 = if (config.prefix) |p| @as(u32, @intCast(p.len * 8 + 4)) else 0; @@ -272,10 +283,10 @@ pub fn numberEntryRect( _ = bounds.w -| prefix_w -| suffix_w -| (spinner_w * 2); // input_w available for future use // Colors - const bg_color = if (state.focused) colors.background_focused else colors.background; + const bg_color = if (has_focus) colors.background_focused else colors.background; const border_color = if (!state.valid) colors.border_invalid - else if (state.focused) + else if (has_focus) colors.border_focused else colors.border; @@ -296,7 +307,7 @@ pub fn numberEntryRect( ctx.pushCommand(Command.text(input_x + 4, text_y, state.text(), text_color)); // Draw cursor if focused - if (state.focused) { + if (has_focus) { const cursor_x = input_x + 4 + @as(i32, @intCast(state.cursor * 8)); ctx.pushCommand(Command.rect(cursor_x, bounds.y + 4, 2, bounds.h - 8, colors.cursor)); } @@ -350,7 +361,7 @@ pub fn numberEntryRect( } // Handle text input - if (state.focused) { + if (has_focus) { const text_input = ctx.input.getTextInput(); for (text_input) |char| { state.insert(char); diff --git a/src/widgets/radio.zig b/src/widgets/radio.zig index 7153f05..6f36317 100644 --- a/src/widgets/radio.zig +++ b/src/widgets/radio.zig @@ -192,14 +192,24 @@ pub fn radioGroupRect( if (bounds.isEmpty() or options.len == 0) return result; + // Generate unique ID for this widget based on state address + const widget_id: u64 = @intFromPtr(state); + + // Register as focusable in the active focus group + ctx.registerFocusable(widget_id); + const mouse = ctx.input.mousePos(); const mouse_pressed = ctx.input.mousePressed(.left); // Check if group area clicked (for focus) if (mouse_pressed and bounds.contains(mouse.x, mouse.y)) { - state.focused = true; + ctx.requestFocus(widget_id); } + // Check if this widget has focus + const has_focus = ctx.hasFocus(widget_id); + state.focused = has_focus; + // Draw options var pos_x = bounds.x; var pos_y = bounds.y; diff --git a/src/widgets/select.zig b/src/widgets/select.zig index 408fda1..5642f18 100644 --- a/src/widgets/select.zig +++ b/src/widgets/select.zig @@ -18,6 +18,8 @@ pub const SelectState = struct { open: bool = false, /// Scroll offset in dropdown (for many items) scroll_offset: usize = 0, + /// Whether this widget has focus + focused: bool = false, /// Get selected index as optional usize pub fn selectedIndex(self: SelectState) ?usize { @@ -83,6 +85,12 @@ pub fn selectRect( if (bounds.isEmpty()) return result; + // Generate unique ID for this widget based on state address + const widget_id: u64 = @intFromPtr(state); + + // Register as focusable in the active focus group + ctx.registerFocusable(widget_id); + const theme = Style.Theme.dark; // Check mouse interaction on main button @@ -90,11 +98,16 @@ pub fn selectRect( const hovered = bounds.contains(mouse.x, mouse.y) and !config.disabled; const clicked = hovered and ctx.input.mousePressed(.left); - // Toggle dropdown on click + // Toggle dropdown on click and request focus if (clicked) { + ctx.requestFocus(widget_id); state.open = !state.open; } + // Check if this widget has focus + const has_focus = ctx.hasFocus(widget_id); + state.focused = has_focus; + // Determine button colors const bg_color = if (config.disabled) theme.button_bg.darken(20) @@ -105,7 +118,7 @@ pub fn selectRect( else theme.button_bg; - const border_color = if (state.open) theme.primary else theme.border; + const border_color = if (has_focus or state.open) theme.primary else theme.border; // Draw main button background ctx.pushCommand(Command.rect(bounds.x, bounds.y, bounds.w, bounds.h, bg_color)); diff --git a/src/widgets/slider.zig b/src/widgets/slider.zig index a5034cd..8061753 100644 --- a/src/widgets/slider.zig +++ b/src/widgets/slider.zig @@ -159,6 +159,12 @@ pub fn sliderRect( if (bounds.isEmpty()) return result; + // Generate unique ID for this widget based on state address + const widget_id: u64 = @intFromPtr(state); + + // Register as focusable in the active focus group + ctx.registerFocusable(widget_id); + const mouse = ctx.input.mousePos(); const mouse_down = ctx.input.mouseDown(.left); const mouse_pressed = ctx.input.mousePressed(.left); @@ -198,11 +204,15 @@ pub fn sliderRect( // Handle drag start if (mouse_pressed and bounds_hovered and !config.disabled) { + ctx.requestFocus(widget_id); state.dragging = true; - state.focused = true; result.drag_started = true; } + // Check if this widget has focus + const has_focus = ctx.hasFocus(widget_id); + state.focused = has_focus; + // Handle drag end if (mouse_released and state.dragging) { state.dragging = false; diff --git a/src/widgets/tabs.zig b/src/widgets/tabs.zig index 0191949..2ec054d 100644 --- a/src/widgets/tabs.zig +++ b/src/widgets/tabs.zig @@ -44,6 +44,8 @@ pub const TabsState = struct { hovered: i32 = -1, /// Close button hovered (-1 for none) close_hovered: i32 = -1, + /// Whether this widget has focus + focused: bool = false, const Self = @This(); @@ -179,6 +181,12 @@ pub fn tabsRect( if (bounds.isEmpty() or tab_list.len == 0) return result; + // Generate unique ID for this widget based on state address + const widget_id: u64 = @intFromPtr(state); + + // Register as focusable in the active focus group + ctx.registerFocusable(widget_id); + const mouse = ctx.input.mousePos(); const mouse_pressed = ctx.input.mousePressed(.left); @@ -308,6 +316,7 @@ pub fn tabsRect( // Handle tab click if (mouse_pressed and is_hovered and state.close_hovered != @as(i32, @intCast(i))) { + ctx.requestFocus(widget_id); if (state.selected != i) { state.selected = i; result.changed = true; @@ -318,8 +327,12 @@ pub fn tabsRect( tab_x += @as(i32, @intCast(tab_width)); } - // Handle keyboard navigation - if (ctx.input.keyPressed(.left)) { + // Check if this widget has focus + const has_focus = ctx.hasFocus(widget_id); + state.focused = has_focus; + + // Handle keyboard navigation (only when focused) + if (has_focus and ctx.input.keyPressed(.left)) { // Find previous non-disabled tab var prev = if (state.selected == 0) tab_list.len - 1 else state.selected - 1; var attempts: usize = 0; @@ -333,7 +346,7 @@ pub fn tabsRect( result.selected = prev; } } - if (ctx.input.keyPressed(.right)) { + if (has_focus and ctx.input.keyPressed(.right)) { // Find next non-disabled tab var next = (state.selected + 1) % tab_list.len; var attempts: usize = 0; diff --git a/src/widgets/textarea.zig b/src/widgets/textarea.zig index 8bb82cd..eea0f3b 100644 --- a/src/widgets/textarea.zig +++ b/src/widgets/textarea.zig @@ -470,19 +470,30 @@ pub fn textAreaRect( if (bounds.isEmpty()) return result; + // Generate unique ID for this widget based on buffer memory address + const widget_id: u64 = @intFromPtr(state.buffer.ptr); + + // Register as focusable in the active focus group + ctx.registerFocusable(widget_id); + // Check mouse interaction const mouse = ctx.input.mousePos(); const hovered = bounds.contains(mouse.x, mouse.y); const clicked = hovered and ctx.input.mousePressed(.left); if (clicked) { - state.focused = true; + // Request focus through the focus system + ctx.requestFocus(widget_id); result.clicked = true; } + // Check if this widget has focus + const has_focus = ctx.hasFocus(widget_id); + state.focused = has_focus; + // Get colors - const bg_color = if (state.focused) colors.background.lighten(5) else colors.background; - const border_color = if (state.focused) colors.border_focused else colors.border; + const bg_color = if (has_focus) colors.background.lighten(5) else colors.background; + const border_color = if (has_focus) colors.border_focused else colors.border; // Draw background ctx.pushCommand(Command.rect(bounds.x, bounds.y, bounds.w, bounds.h, bg_color));