zcatgui/src/core/window.zig
R.Eugenio cead6bf26f feat(window): Unificar tipos Panel con Input.KeyEvent
- Event.KeyEvent ahora re-exporta Input.KeyEvent (unificación tipos)
- Añadir burst_sensitive a PanelInfo (para supresión ráfagas)
- Exportar Panel, PanelInfo, Zone, Event, makePanel desde raíz
- Mantener aliases WindowPanel/WindowPanelInfo para compatibilidad

Esta unificación permite que zsimifactu use WindowState de zcatgui
directamente, ya que Panel y Event son ahora el mismo tipo.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 22:31:47 +01:00

336 lines
11 KiB
Zig

//! 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;
const Input = @import("input.zig");
// =============================================================================
// 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,
/// 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
pub const Zone = enum {
left,
center,
right,
top,
bottom,
modal,
};
/// Evento simplificado para paneles
/// Usa Input.KeyEvent de core/input.zig para compatibilidad
pub const Event = union(enum) {
key: Input.KeyEvent,
mouse: MouseEvent,
resize: ResizeEvent,
text_input: TextInputEvent,
quit,
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 };
// Re-export KeyEvent para acceso uniforme
pub const KeyEvent = Input.KeyEvent;
};
/// 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 (array estático + contador - sin allocations dinámicas)
panels_arr: [MAX_PANELS]Panel,
panels_len: usize,
/// 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_arr = undefined,
.panels_len = 0,
.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 {
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];
}
/// 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(), 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()[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()[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()[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()[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()[self.focused_idx];
current.onBlur(null);
// Cambiar índice
self.focused_idx = idx;
// Notificar focus al nuevo panel
const target = self.panels()[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()[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];
return focused.handleEvent(event, app_ctx);
}
/// Número de paneles en la ventana
pub fn panelCount(self: *const Self) usize {
return self.panels_len;
}
};