From 067bac47fdd882b9233c10c32e1ac0c28ea0892d Mon Sep 17 00:00:00 2001 From: "R.Eugenio" Date: Sun, 4 Jan 2026 21:50:51 +0100 Subject: [PATCH] =?UTF-8?q?feat(window):=20A=C3=B1adir=20WindowState=20y?= =?UTF-8?q?=20Panel=20interface=20con=20app=5Fctx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/core/window.zig | 331 ++++++++++++++++++++++++++++++++++++++++++++ src/zcatgui.zig | 7 + 2 files changed, 338 insertions(+) create mode 100644 src/core/window.zig diff --git a/src/core/window.zig b/src/core/window.zig new file mode 100644 index 0000000..c20342e --- /dev/null +++ b/src/core/window.zig @@ -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; + } +}; diff --git a/src/zcatgui.zig b/src/zcatgui.zig index b019261..3adcfb7 100644 --- a/src/zcatgui.zig +++ b/src/zcatgui.zig @@ -66,6 +66,13 @@ pub const GestureResult = gesture.Result; pub const GestureConfig = gesture.Config; 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 // =============================================================================