feat(window): Añadir WindowState y Panel interface con app_ctx

Sistema de ventanas dinámicas donde:
- Panel interface usa app_ctx: ?*anyopaque (desacoplado)
- WindowState gestiona lista de paneles con BoundedArray(8)
- El ID del panel viaja con el panel (elimina errores de mismatch)
- Focus groups encapsulados con base_focus_group

Exports:
- WindowState, WindowPanel, WindowPanelInfo, makeWindowPanel
- window module completo

Parte de refactorización consensuada (R.Eugenio, Claude, Gemini)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
R.Eugenio 2026-01-04 21:50:51 +01:00
parent 1f2d4abb0b
commit 067bac47fd
2 changed files with 338 additions and 0 deletions

331
src/core/window.zig Normal file
View file

@ -0,0 +1,331 @@
//! Window State - Sistema de ventanas con paneles dinámicos
//!
//! Arquitectura LEGO donde:
//! - WindowState contiene una lista de paneles
//! - El ID del panel viaja con el panel (elimina errores de mismatch)
//! - Los paneles usan app_ctx opaco (desacoplado de aplicación)
//!
//! Diseño consensuado: R.Eugenio, Claude, Gemini (2026-01-04)
const std = @import("std");
const Context = @import("context.zig").Context;
const Layout = @import("layout.zig");
const Rect = Layout.Rect;
// =============================================================================
// Panel Interface (con contexto opaco)
// =============================================================================
/// Información del panel para layout y identificación
pub const PanelInfo = struct {
/// Nombre visible del panel
name: []const u8,
/// Identificador único - VIAJA CON EL PANEL
id: []const u8,
/// Ancho mínimo requerido
min_width: u32 = 200,
/// Alto mínimo requerido
min_height: u32 = 100,
/// Si el panel puede recibir foco
focusable: bool = true,
/// Zona preferida para layout
preferred_zone: Zone = .center,
/// Tecla de acceso rápido: Ctrl+focus_key (1-9, null = sin atajo)
focus_key: ?u8 = null,
};
/// Zonas de layout disponibles
pub const Zone = enum {
left,
center,
right,
top,
bottom,
modal,
};
/// Evento simplificado para paneles
pub const Event = union(enum) {
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,
button: ?MouseButton,
pressed: bool,
};
pub const ResizeEvent = struct {
width: u32,
height: u32,
};
pub const TextInputEvent = struct {
text: [32]u8,
len: usize,
};
pub const MouseButton = enum { left, middle, right };
pub const Modifiers = struct {
ctrl: bool = false,
shift: bool = false,
alt: bool = false,
};
};
/// Interface Panel con contexto opaco (desacoplado de aplicación)
pub const Panel = struct {
ptr: *anyopaque,
vtable: *const VTable,
pub const VTable = struct {
/// Dibuja el panel en el rectángulo asignado
/// app_ctx es opaco - la aplicación lo castea a su tipo (ej: *DataManager)
draw: *const fn (ptr: *anyopaque, ctx: *Context, rect: Rect, app_ctx: ?*anyopaque) void,
/// Maneja eventos de entrada
/// Retorna true si el evento fue consumido
handleEvent: *const fn (ptr: *anyopaque, event: Event, app_ctx: ?*anyopaque) bool,
/// Obtiene información del panel (incluye ID único)
getInfo: *const fn (ptr: *anyopaque) PanelInfo,
/// Llamado cuando el panel gana foco (opcional)
onFocus: ?*const fn (ptr: *anyopaque, app_ctx: ?*anyopaque) void = null,
/// Llamado cuando el panel pierde foco (opcional)
onBlur: ?*const fn (ptr: *anyopaque, app_ctx: ?*anyopaque) void = null,
};
/// Dibuja el panel
pub fn draw(self: Panel, ctx: *Context, rect: Rect, app_ctx: ?*anyopaque) void {
self.vtable.draw(self.ptr, ctx, rect, app_ctx);
}
/// Maneja un evento
pub fn handleEvent(self: Panel, event: Event, app_ctx: ?*anyopaque) bool {
return self.vtable.handleEvent(self.ptr, event, app_ctx);
}
/// Obtiene información del panel
pub fn getInfo(self: Panel) PanelInfo {
return self.vtable.getInfo(self.ptr);
}
/// Notifica que el panel ganó foco
pub fn onFocus(self: Panel, app_ctx: ?*anyopaque) void {
if (self.vtable.onFocus) |f| {
f(self.ptr, app_ctx);
}
}
/// Notifica que el panel perdió foco
pub fn onBlur(self: Panel, app_ctx: ?*anyopaque) void {
if (self.vtable.onBlur) |f| {
f(self.ptr, app_ctx);
}
}
};
/// Crea un Panel desde una implementación concreta
/// T debe tener: info (const PanelInfo), draw(), handleEvent()
/// Opcionales: onFocus(), onBlur()
pub fn makePanel(comptime T: type, impl: *T) Panel {
const gen = struct {
fn draw(ptr: *anyopaque, ctx: *Context, rect: Rect, app_ctx: ?*anyopaque) void {
const self: *T = @ptrCast(@alignCast(ptr));
self.draw(ctx, rect, app_ctx);
}
fn handleEvent(ptr: *anyopaque, event: Event, app_ctx: ?*anyopaque) bool {
const self: *T = @ptrCast(@alignCast(ptr));
return self.handleEvent(event, app_ctx);
}
fn getInfo(ptr: *anyopaque) PanelInfo {
_ = ptr;
return T.info;
}
fn onFocus(ptr: *anyopaque, app_ctx: ?*anyopaque) void {
if (@hasDecl(T, "onFocus")) {
const self: *T = @ptrCast(@alignCast(ptr));
self.onFocus(app_ctx);
}
}
fn onBlur(ptr: *anyopaque, app_ctx: ?*anyopaque) void {
if (@hasDecl(T, "onBlur")) {
const self: *T = @ptrCast(@alignCast(ptr));
self.onBlur(app_ctx);
}
}
const vtable = Panel.VTable{
.draw = draw,
.handleEvent = handleEvent,
.getInfo = getInfo,
.onFocus = if (@hasDecl(T, "onFocus")) onFocus else null,
.onBlur = if (@hasDecl(T, "onBlur")) onBlur else null,
};
};
return Panel{
.ptr = impl,
.vtable = &gen.vtable,
};
}
// =============================================================================
// WindowState
// =============================================================================
/// Máximo de paneles por ventana (decisión pragmática: rápido, suficiente)
pub const MAX_PANELS = 8;
/// Estado de una ventana con paneles dinámicos
pub const WindowState = struct {
/// Identificador de la ventana
id: []const u8,
/// 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,
/// Índice del panel con foco (dentro de esta ventana)
focused_idx: usize,
/// Base de focus groups para esta ventana (ej: clientes=1, config=5)
base_focus_group: u64,
const Self = @This();
/// Inicializa una ventana vacía
pub fn init(id: []const u8, base_focus_group: u64) Self {
return .{
.id = id,
.panels = .{},
.rects = [_]Rect{.{ .x = 0, .y = 0, .w = 0, .h = 0 }} ** MAX_PANELS,
.focused_idx = 0,
.base_focus_group = base_focus_group,
};
}
/// Añade un panel a la ventana
pub fn addPanel(self: *Self, panel: Panel) error{Overflow}!void {
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);
for (0..len) |i| {
self.rects[i] = rects[i];
}
}
/// 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.slice(), 0..) |panel, i| {
const info = panel.getInfo();
const rect = self.rects[i];
// Registrar área con el ID REAL del panel
ctx.setRegistrationGroup(self.base_focus_group + @as(u64, @intCast(i)));
// Solo dibujar si está dirty (optimización LEGO)
if (ctx.isPanelDirty(info.id)) {
panel.draw(ctx, rect, app_ctx);
}
}
}
/// Mueve el foco al siguiente panel (F6)
pub fn focusNext(self: *Self, ctx: *Context) void {
if (self.panels.len == 0) return;
// Notificar blur al panel actual
const current = self.panels.slice()[self.focused_idx];
current.onBlur(null);
// Mover al siguiente
self.focused_idx = (self.focused_idx + 1) % self.panels.len;
// Notificar focus al nuevo panel
const next = self.panels.slice()[self.focused_idx];
next.onFocus(null);
// Actualizar focus group activo
ctx.setActiveFocusGroup(self.base_focus_group + @as(u64, @intCast(self.focused_idx)));
}
/// Mueve el foco al panel anterior (Shift+F6)
pub fn focusPrev(self: *Self, ctx: *Context) void {
if (self.panels.len == 0) return;
// Notificar blur al panel actual
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;
// Notificar focus al nuevo panel
const prev = self.panels.slice()[self.focused_idx];
prev.onFocus(null);
// Actualizar focus group activo
ctx.setActiveFocusGroup(self.base_focus_group + @as(u64, @intCast(self.focused_idx)));
}
/// 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;
// Notificar blur al panel actual
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.slice()[self.focused_idx];
target.onFocus(null);
// Actualizar focus group activo
ctx.setActiveFocusGroup(self.base_focus_group + @as(u64, @intCast(self.focused_idx)));
}
/// Obtiene el panel actualmente enfocado
pub fn getFocusedPanel(self: *Self) ?Panel {
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.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;
}
};

View file

@ -66,6 +66,13 @@ pub const GestureResult = gesture.Result;
pub const GestureConfig = gesture.Config; pub const GestureConfig = gesture.Config;
pub const SwipeDirection = gesture.SwipeDirection; pub const SwipeDirection = gesture.SwipeDirection;
// Window system (paneles dinámicos)
pub const window = @import("core/window.zig");
pub const WindowState = window.WindowState;
pub const WindowPanel = window.Panel;
pub const WindowPanelInfo = window.PanelInfo;
pub const makeWindowPanel = window.makePanel;
// ============================================================================= // =============================================================================
// Macro system // Macro system
// ============================================================================= // =============================================================================