docs: Documentar sistema de focus completado y widgets adaptados

- 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 <noreply@anthropic.com>
This commit is contained in:
reugenio 2025-12-11 18:50:37 +01:00
parent 7cde6370d8
commit 3517a6f972
8 changed files with 206 additions and 60 deletions

View file

@ -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
---

View file

@ -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)*

View file

@ -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);

View file

@ -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;

View file

@ -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));

View file

@ -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;

View file

@ -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;

View file

@ -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));