//! 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; } };