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:
parent
1f2d4abb0b
commit
067bac47fd
2 changed files with 338 additions and 0 deletions
331
src/core/window.zig
Normal file
331
src/core/window.zig
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue