- 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>
336 lines
11 KiB
Zig
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;
|
|
}
|
|
};
|