feat(panels): Add DetailPanelBase for edit/detail panels
New module panels/detail/ with: - DetailPanelState: 8-state machine for edit lifecycle (empty, viewing, editing, new, saving, saved, error_save, deleting) - DetailPanelBase: Common functionality for detail panels - Semaphore visual indicator (color + text) - Action buttons (New, Save, Delete) - Navigation buttons (|< < > >|) - Progressive escape handling - Saved state timer (2s transition) Enables code reuse across detail panels (Customer, Document, Product, etc.) 🤖 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
7aab1ef7c9
commit
7f5550dd1f
4 changed files with 665 additions and 0 deletions
461
src/panels/detail/base.zig
Normal file
461
src/panels/detail/base.zig
Normal file
|
|
@ -0,0 +1,461 @@
|
||||||
|
//! DetailPanelBase - Funcionalidad comun para paneles de detalle/edicion
|
||||||
|
//!
|
||||||
|
//! Proporciona:
|
||||||
|
//! - Maquina de estados (8 estados)
|
||||||
|
//! - Semaforo visual (indicador de estado)
|
||||||
|
//! - Botones de accion (Nuevo, Guardar, Eliminar)
|
||||||
|
//! - Botones de navegacion (|< < > >|)
|
||||||
|
//! - Escape progresivo
|
||||||
|
//! - Timer de estado saved
|
||||||
|
//!
|
||||||
|
//! Uso: Incluir como campo en paneles de detalle concretos.
|
||||||
|
//!
|
||||||
|
//! ```zig
|
||||||
|
//! const WhoDetailPanel = struct {
|
||||||
|
//! base: DetailPanelBase,
|
||||||
|
//! // campos especificos...
|
||||||
|
//!
|
||||||
|
//! pub fn draw(self: *Self, ctx: *Context, rect: Rect) void {
|
||||||
|
//! self.base.updateSavedTimer();
|
||||||
|
//! self.base.drawSemaphore(ctx, rect);
|
||||||
|
//! // dibujar campos especificos...
|
||||||
|
//! const actions = self.base.drawActionButtons(ctx, rect, y, .{...});
|
||||||
|
//! if (actions.save_clicked) self.save();
|
||||||
|
//! }
|
||||||
|
//! };
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const zcatgui = @import("../../zcatgui.zig");
|
||||||
|
const Context = zcatgui.Context;
|
||||||
|
const Rect = zcatgui.Rect;
|
||||||
|
const Color = zcatgui.Color;
|
||||||
|
const widgets = zcatgui.widgets;
|
||||||
|
|
||||||
|
const state_module = @import("state.zig");
|
||||||
|
pub const DetailPanelState = state_module.DetailPanelState;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Result Types
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Resultado del escape progresivo
|
||||||
|
pub const EscapeResult = enum {
|
||||||
|
/// Se revirtieron los cambios pendientes
|
||||||
|
consumed_revert,
|
||||||
|
/// Se limpio el formulario
|
||||||
|
consumed_clear,
|
||||||
|
/// El panel no consumio ESC (pasar al nivel superior)
|
||||||
|
not_consumed,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Resultado de los botones de accion
|
||||||
|
pub const ActionButtonsResult = struct {
|
||||||
|
new_clicked: bool = false,
|
||||||
|
save_clicked: bool = false,
|
||||||
|
delete_clicked: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Resultado de los botones de navegacion
|
||||||
|
pub const NavButtonsResult = struct {
|
||||||
|
first_clicked: bool = false,
|
||||||
|
prev_clicked: bool = false,
|
||||||
|
next_clicked: bool = false,
|
||||||
|
last_clicked: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Opciones para botones de accion
|
||||||
|
pub const ActionButtonsOpts = struct {
|
||||||
|
/// Puede guardar (hay cambios y datos validos)
|
||||||
|
can_save: bool = false,
|
||||||
|
/// Puede eliminar (registro existente)
|
||||||
|
can_delete: bool = false,
|
||||||
|
/// Ancho de cada boton
|
||||||
|
button_width: u32 = 80,
|
||||||
|
/// Alto de cada boton
|
||||||
|
button_height: u32 = 28,
|
||||||
|
/// Espacio entre botones
|
||||||
|
spacing: i32 = 8,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Opciones para botones de navegacion
|
||||||
|
pub const NavButtonsOpts = struct {
|
||||||
|
/// Indice actual (0-based), null si no hay seleccion
|
||||||
|
current_index: ?usize = null,
|
||||||
|
/// Total de registros
|
||||||
|
total_count: usize = 0,
|
||||||
|
/// Puede ir al primero
|
||||||
|
can_first: bool = false,
|
||||||
|
/// Puede ir al anterior
|
||||||
|
can_prev: bool = false,
|
||||||
|
/// Puede ir al siguiente
|
||||||
|
can_next: bool = false,
|
||||||
|
/// Puede ir al ultimo
|
||||||
|
can_last: bool = false,
|
||||||
|
/// Ancho de cada boton
|
||||||
|
button_width: u32 = 40,
|
||||||
|
/// Alto de cada boton
|
||||||
|
button_height: u32 = 28,
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// DetailPanelBase
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Base para paneles de detalle/edicion de entidades
|
||||||
|
pub const DetailPanelBase = struct {
|
||||||
|
/// Estado actual del panel
|
||||||
|
state: DetailPanelState = .empty,
|
||||||
|
/// Timestamp del ultimo guardado (para timer de estado saved)
|
||||||
|
saved_timer: i64 = 0,
|
||||||
|
/// Mensaje de error actual (si hay)
|
||||||
|
error_message: ?[]const u8 = null,
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Timer de estado saved
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// Actualiza el timer del estado saved.
|
||||||
|
/// Llamar en cada frame/draw.
|
||||||
|
/// Transiciona automaticamente de saved -> viewing despues de 2 segundos.
|
||||||
|
pub fn updateSavedTimer(self: *Self) void {
|
||||||
|
if (self.state == .saved) {
|
||||||
|
const elapsed = std.time.milliTimestamp() - self.saved_timer;
|
||||||
|
if (elapsed >= 2000) {
|
||||||
|
self.state = .viewing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Transiciones de estado
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// Marca el panel como guardado exitosamente.
|
||||||
|
/// Inicia el timer para volver a viewing.
|
||||||
|
pub fn markSaved(self: *Self) void {
|
||||||
|
self.state = .saved;
|
||||||
|
self.saved_timer = std.time.milliTimestamp();
|
||||||
|
self.error_message = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marca el panel con error de guardado.
|
||||||
|
pub fn markError(self: *Self, message: []const u8) void {
|
||||||
|
self.state = .error_save;
|
||||||
|
self.error_message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Limpia el error y vuelve al estado anterior.
|
||||||
|
pub fn clearError(self: *Self, has_entity: bool) void {
|
||||||
|
self.error_message = null;
|
||||||
|
self.state = if (has_entity) .editing else .new;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transiciona a estado de edicion (si estaba en viewing).
|
||||||
|
pub fn beginEditing(self: *Self) void {
|
||||||
|
if (self.state == .viewing) {
|
||||||
|
self.state = .editing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transiciona a estado nuevo.
|
||||||
|
pub fn beginNew(self: *Self) void {
|
||||||
|
self.state = .new;
|
||||||
|
self.error_message = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transiciona a estado vacio.
|
||||||
|
pub fn clear(self: *Self) void {
|
||||||
|
self.state = .empty;
|
||||||
|
self.error_message = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transiciona a estado viewing (despues de cargar datos).
|
||||||
|
pub fn setViewing(self: *Self) void {
|
||||||
|
self.state = .viewing;
|
||||||
|
self.error_message = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Escape progresivo
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// Maneja el escape progresivo.
|
||||||
|
///
|
||||||
|
/// Logica:
|
||||||
|
/// 1. Si hay error -> limpiar error, quedarse en edicion
|
||||||
|
/// 2. Si hay cambios (y no es nuevo) -> revertir (caller debe hacerlo)
|
||||||
|
/// 3. Si tiene entidad o es nuevo -> limpiar formulario (caller debe hacerlo)
|
||||||
|
/// 4. Si no consumido -> devolver control (ir a panel anterior)
|
||||||
|
///
|
||||||
|
/// Params:
|
||||||
|
/// - has_changes: true si hay cambios pendientes en los campos
|
||||||
|
/// - has_entity: true si hay una entidad cargada (uuid != null)
|
||||||
|
///
|
||||||
|
/// Returns: EscapeResult indicando que accion tomar
|
||||||
|
pub fn handleEscape(self: *Self, has_changes: bool, has_entity: bool) EscapeResult {
|
||||||
|
// 1. Si hay error, limpiar y quedarse
|
||||||
|
if (self.state == .error_save) {
|
||||||
|
self.clearError(has_entity);
|
||||||
|
return .consumed_revert;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Si hay cambios y no es nuevo, revertir
|
||||||
|
if (has_changes and self.state != .new) {
|
||||||
|
self.state = .viewing;
|
||||||
|
return .consumed_revert; // Caller debe revertir campos
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Si tiene entidad cargada o es nuevo, limpiar
|
||||||
|
if (has_entity or self.state == .new) {
|
||||||
|
return .consumed_clear; // Caller debe limpiar formulario
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. No consumido, pasar al nivel superior
|
||||||
|
return .not_consumed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Dibujo: Semaforo
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// Dibuja el semaforo de estado en la esquina superior izquierda del rect.
|
||||||
|
///
|
||||||
|
/// Muestra:
|
||||||
|
/// - Cuadrado de color (12x12) indicando el estado
|
||||||
|
/// - Texto del estado en la esquina superior derecha
|
||||||
|
pub fn drawSemaphore(self: *Self, ctx: *Context, rect: Rect) void {
|
||||||
|
const state_color = self.state.getColor();
|
||||||
|
const state_text = self.state.getText();
|
||||||
|
|
||||||
|
// Cuadrado de color (indicador visual)
|
||||||
|
ctx.pushCommand(.{ .rect = .{
|
||||||
|
.x = rect.x + 8,
|
||||||
|
.y = rect.y + 4,
|
||||||
|
.w = 12,
|
||||||
|
.h = 12,
|
||||||
|
.color = state_color,
|
||||||
|
} });
|
||||||
|
|
||||||
|
// Texto del estado (esquina derecha)
|
||||||
|
const text_x = rect.x + @as(i32, @intCast(rect.w)) - 80;
|
||||||
|
ctx.pushCommand(.{ .text = .{
|
||||||
|
.x = text_x,
|
||||||
|
.y = rect.y + 4,
|
||||||
|
.text = state_text,
|
||||||
|
.color = state_color,
|
||||||
|
} });
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Dibujo: Botones de accion
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// Dibuja los botones de accion (Nuevo, Guardar, Eliminar).
|
||||||
|
///
|
||||||
|
/// Params:
|
||||||
|
/// - ctx: Contexto de dibujo
|
||||||
|
/// - rect: Rectangulo del panel
|
||||||
|
/// - y: Posicion Y donde dibujar los botones
|
||||||
|
/// - opts: Opciones (can_save, can_delete, dimensiones)
|
||||||
|
///
|
||||||
|
/// Returns: ActionButtonsResult con los botones clickeados
|
||||||
|
pub fn drawActionButtons(self: *Self, ctx: *Context, rect: Rect, y: i32, opts: ActionButtonsOpts) ActionButtonsResult {
|
||||||
|
_ = self;
|
||||||
|
var result = ActionButtonsResult{};
|
||||||
|
|
||||||
|
const x = rect.x + 8;
|
||||||
|
const bw = opts.button_width;
|
||||||
|
const bh = opts.button_height;
|
||||||
|
const sp = opts.spacing;
|
||||||
|
|
||||||
|
// Boton Nuevo (siempre habilitado)
|
||||||
|
if (widgets.button.buttonRect(ctx, .{
|
||||||
|
.x = x,
|
||||||
|
.y = y,
|
||||||
|
.w = bw,
|
||||||
|
.h = bh,
|
||||||
|
}, "Nuevo", .{})) {
|
||||||
|
result.new_clicked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Boton Guardar
|
||||||
|
if (widgets.button.buttonRect(ctx, .{
|
||||||
|
.x = x + @as(i32, @intCast(bw)) + sp,
|
||||||
|
.y = y,
|
||||||
|
.w = bw,
|
||||||
|
.h = bh,
|
||||||
|
}, "Guardar", .{ .disabled = !opts.can_save })) {
|
||||||
|
result.save_clicked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Boton Eliminar
|
||||||
|
if (widgets.button.buttonRect(ctx, .{
|
||||||
|
.x = x + @as(i32, @intCast(bw)) * 2 + sp * 2,
|
||||||
|
.y = y,
|
||||||
|
.w = bw,
|
||||||
|
.h = bh,
|
||||||
|
}, "Eliminar", .{ .disabled = !opts.can_delete })) {
|
||||||
|
result.delete_clicked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Dibujo: Botones de navegacion
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// Dibuja los botones de navegacion (|< < > >|) y el indicador de posicion.
|
||||||
|
///
|
||||||
|
/// Params:
|
||||||
|
/// - ctx: Contexto de dibujo
|
||||||
|
/// - rect: Rectangulo del panel
|
||||||
|
/// - y: Posicion Y donde dibujar los botones
|
||||||
|
/// - opts: Opciones de navegacion
|
||||||
|
///
|
||||||
|
/// Returns: NavButtonsResult con los botones clickeados
|
||||||
|
pub fn drawNavButtons(self: *Self, ctx: *Context, rect: Rect, y: i32, opts: NavButtonsOpts) NavButtonsResult {
|
||||||
|
_ = self;
|
||||||
|
var result = NavButtonsResult{};
|
||||||
|
|
||||||
|
const x = rect.x + 8;
|
||||||
|
const bw = opts.button_width;
|
||||||
|
const bh = opts.button_height;
|
||||||
|
|
||||||
|
// |< Primero
|
||||||
|
if (widgets.button.buttonRect(ctx, .{
|
||||||
|
.x = x,
|
||||||
|
.y = y,
|
||||||
|
.w = bw,
|
||||||
|
.h = bh,
|
||||||
|
}, "|<", .{ .disabled = !opts.can_first })) {
|
||||||
|
result.first_clicked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// < Anterior
|
||||||
|
if (widgets.button.buttonRect(ctx, .{
|
||||||
|
.x = x + @as(i32, @intCast(bw)) + 4,
|
||||||
|
.y = y,
|
||||||
|
.w = bw,
|
||||||
|
.h = bh,
|
||||||
|
}, "<", .{ .disabled = !opts.can_prev })) {
|
||||||
|
result.prev_clicked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// > Siguiente
|
||||||
|
if (widgets.button.buttonRect(ctx, .{
|
||||||
|
.x = x + @as(i32, @intCast(bw)) * 2 + 8,
|
||||||
|
.y = y,
|
||||||
|
.w = bw,
|
||||||
|
.h = bh,
|
||||||
|
}, ">", .{ .disabled = !opts.can_next })) {
|
||||||
|
result.next_clicked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// >| Ultimo
|
||||||
|
if (widgets.button.buttonRect(ctx, .{
|
||||||
|
.x = x + @as(i32, @intCast(bw)) * 3 + 12,
|
||||||
|
.y = y,
|
||||||
|
.w = bw,
|
||||||
|
.h = bh,
|
||||||
|
}, ">|", .{ .disabled = !opts.can_last })) {
|
||||||
|
result.last_clicked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Indicador de posicion (ej: "15/520")
|
||||||
|
if (opts.current_index != null and opts.total_count > 0) {
|
||||||
|
const theme = zcatgui.Style.currentTheme();
|
||||||
|
var pos_buf: [32]u8 = undefined;
|
||||||
|
const pos_text = std.fmt.bufPrint(&pos_buf, "{d}/{d}", .{
|
||||||
|
opts.current_index.? + 1,
|
||||||
|
opts.total_count,
|
||||||
|
}) catch "?/?";
|
||||||
|
|
||||||
|
const pos_x = x + @as(i32, @intCast(bw)) * 4 + 24;
|
||||||
|
ctx.pushCommand(.{ .text = .{
|
||||||
|
.x = pos_x,
|
||||||
|
.y = y + 6,
|
||||||
|
.text = pos_text,
|
||||||
|
.color = theme.text_secondary,
|
||||||
|
} });
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Dibujo: Mensaje de error
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// Dibuja el mensaje de error si hay uno.
|
||||||
|
pub fn drawErrorMessage(self: *Self, ctx: *Context, rect: Rect, y: i32) void {
|
||||||
|
if (self.state == .error_save) {
|
||||||
|
if (self.error_message) |msg| {
|
||||||
|
ctx.pushCommand(.{ .text = .{
|
||||||
|
.x = rect.x + 8,
|
||||||
|
.y = y,
|
||||||
|
.text = msg,
|
||||||
|
.color = Color{ .r = 231, .g = 76, .b = 60, .a = 255 },
|
||||||
|
} });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Tests
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
test "DetailPanelBase state transitions" {
|
||||||
|
var base = DetailPanelBase{};
|
||||||
|
|
||||||
|
try std.testing.expectEqual(DetailPanelState.empty, base.state);
|
||||||
|
|
||||||
|
base.setViewing();
|
||||||
|
try std.testing.expectEqual(DetailPanelState.viewing, base.state);
|
||||||
|
|
||||||
|
base.beginEditing();
|
||||||
|
try std.testing.expectEqual(DetailPanelState.editing, base.state);
|
||||||
|
|
||||||
|
base.markSaved();
|
||||||
|
try std.testing.expectEqual(DetailPanelState.saved, base.state);
|
||||||
|
|
||||||
|
base.clear();
|
||||||
|
try std.testing.expectEqual(DetailPanelState.empty, base.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "DetailPanelBase error handling" {
|
||||||
|
var base = DetailPanelBase{};
|
||||||
|
|
||||||
|
base.markError("Test error");
|
||||||
|
try std.testing.expectEqual(DetailPanelState.error_save, base.state);
|
||||||
|
try std.testing.expect(base.error_message != null);
|
||||||
|
|
||||||
|
base.clearError(true);
|
||||||
|
try std.testing.expectEqual(DetailPanelState.editing, base.state);
|
||||||
|
try std.testing.expect(base.error_message == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "DetailPanelBase escape logic" {
|
||||||
|
var base = DetailPanelBase{};
|
||||||
|
|
||||||
|
// Empty panel -> not consumed
|
||||||
|
try std.testing.expectEqual(EscapeResult.not_consumed, base.handleEscape(false, false));
|
||||||
|
|
||||||
|
// Viewing with entity -> clear
|
||||||
|
base.state = .viewing;
|
||||||
|
try std.testing.expectEqual(EscapeResult.consumed_clear, base.handleEscape(false, true));
|
||||||
|
|
||||||
|
// Editing with changes -> revert
|
||||||
|
base.state = .editing;
|
||||||
|
try std.testing.expectEqual(EscapeResult.consumed_revert, base.handleEscape(true, true));
|
||||||
|
|
||||||
|
// Error -> revert (clear error)
|
||||||
|
base.state = .error_save;
|
||||||
|
base.error_message = "test";
|
||||||
|
try std.testing.expectEqual(EscapeResult.consumed_revert, base.handleEscape(false, true));
|
||||||
|
try std.testing.expect(base.error_message == null);
|
||||||
|
}
|
||||||
53
src/panels/detail/detail.zig
Normal file
53
src/panels/detail/detail.zig
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
//! Detail Panel Module - Componentes para paneles de detalle/edicion
|
||||||
|
//!
|
||||||
|
//! Este modulo proporciona la infraestructura comun para paneles que
|
||||||
|
//! muestran y editan fichas/registros individuales (ej: Cliente, Documento, Producto).
|
||||||
|
//!
|
||||||
|
//! Componentes:
|
||||||
|
//! - DetailPanelState: Maquina de 8 estados para ciclo de vida de edicion
|
||||||
|
//! - DetailPanelBase: Funcionalidad comun (semaforo, botones, escape progresivo)
|
||||||
|
//!
|
||||||
|
//! Ejemplo de uso:
|
||||||
|
//! ```zig
|
||||||
|
//! const detail = @import("zcatgui").panels.detail;
|
||||||
|
//!
|
||||||
|
//! pub const CustomerDetailPanel = struct {
|
||||||
|
//! base: detail.DetailPanelBase,
|
||||||
|
//! name_input: widgets.TextInputState,
|
||||||
|
//! // ...
|
||||||
|
//!
|
||||||
|
//! pub fn draw(self: *Self, ctx: *Context, rect: Rect) void {
|
||||||
|
//! self.base.updateSavedTimer();
|
||||||
|
//! self.base.drawSemaphore(ctx, rect);
|
||||||
|
//! // dibujar campos...
|
||||||
|
//! const actions = self.base.drawActionButtons(ctx, rect, y, .{
|
||||||
|
//! .can_save = self.base.state.canSave(),
|
||||||
|
//! .can_delete = self.base.state.canDelete(),
|
||||||
|
//! });
|
||||||
|
//! if (actions.save_clicked) self.save();
|
||||||
|
//! }
|
||||||
|
//! };
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
// Re-exports
|
||||||
|
pub const state = @import("state.zig");
|
||||||
|
pub const base = @import("base.zig");
|
||||||
|
|
||||||
|
// Tipos principales
|
||||||
|
pub const DetailPanelState = state.DetailPanelState;
|
||||||
|
pub const DetailPanelBase = base.DetailPanelBase;
|
||||||
|
pub const EscapeResult = base.EscapeResult;
|
||||||
|
pub const ActionButtonsResult = base.ActionButtonsResult;
|
||||||
|
pub const NavButtonsResult = base.NavButtonsResult;
|
||||||
|
pub const ActionButtonsOpts = base.ActionButtonsOpts;
|
||||||
|
pub const NavButtonsOpts = base.NavButtonsOpts;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Tests
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
test {
|
||||||
|
std.testing.refAllDecls(@This());
|
||||||
|
}
|
||||||
137
src/panels/detail/state.zig
Normal file
137
src/panels/detail/state.zig
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
//! DetailPanelState - Maquina de estados para paneles de detalle/edicion
|
||||||
|
//!
|
||||||
|
//! Define los 8 estados posibles de un panel de edicion de fichas
|
||||||
|
//! y sus propiedades visuales (color, simbolo, texto).
|
||||||
|
//!
|
||||||
|
//! Uso tipico: paneles de detalle de entidades (Cliente, Documento, Producto)
|
||||||
|
|
||||||
|
const zcatgui = @import("../../zcatgui.zig");
|
||||||
|
const Color = zcatgui.Color;
|
||||||
|
|
||||||
|
/// Estados posibles de un panel de detalle/edicion
|
||||||
|
pub const DetailPanelState = enum {
|
||||||
|
/// Panel vacio, sin datos cargados
|
||||||
|
empty,
|
||||||
|
/// Mostrando registro existente (sin cambios pendientes)
|
||||||
|
viewing,
|
||||||
|
/// Registro existente con cambios pendientes
|
||||||
|
editing,
|
||||||
|
/// Creando registro nuevo (antes de guardar)
|
||||||
|
new,
|
||||||
|
/// Guardando en BD (estado transitorio)
|
||||||
|
saving,
|
||||||
|
/// Guardado exitoso (temporal ~2 segundos)
|
||||||
|
saved,
|
||||||
|
/// Error al guardar
|
||||||
|
error_save,
|
||||||
|
/// Confirmando eliminacion
|
||||||
|
deleting,
|
||||||
|
|
||||||
|
/// Retorna el color del semaforo para este estado
|
||||||
|
pub fn getColor(self: DetailPanelState) Color {
|
||||||
|
return switch (self) {
|
||||||
|
.empty => Color{ .r = 128, .g = 128, .b = 128, .a = 255 }, // Gris
|
||||||
|
.viewing => Color{ .r = 39, .g = 174, .b = 96, .a = 255 }, // Verde
|
||||||
|
.editing => Color{ .r = 241, .g = 196, .b = 15, .a = 255 }, // Amarillo
|
||||||
|
.new => Color{ .r = 52, .g = 152, .b = 219, .a = 255 }, // Azul
|
||||||
|
.saving => Color{ .r = 241, .g = 196, .b = 15, .a = 255 }, // Amarillo
|
||||||
|
.saved => Color{ .r = 46, .g = 204, .b = 113, .a = 255 }, // Verde brillante
|
||||||
|
.error_save => Color{ .r = 231, .g = 76, .b = 60, .a = 255 }, // Rojo
|
||||||
|
.deleting => Color{ .r = 231, .g = 76, .b = 60, .a = 255 }, // Rojo
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retorna el simbolo del semaforo para este estado (ASCII safe)
|
||||||
|
pub fn getSymbol(self: DetailPanelState) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.empty => "o", // Circulo vacio
|
||||||
|
.viewing => "*", // Circulo lleno
|
||||||
|
.editing => "~", // Modificado
|
||||||
|
.new => "+", // Nuevo
|
||||||
|
.saving => "...", // Procesando
|
||||||
|
.saved => "OK", // Exito
|
||||||
|
.error_save => "X", // Error
|
||||||
|
.deleting => "!", // Advertencia
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retorna el texto descriptivo del estado
|
||||||
|
pub fn getText(self: DetailPanelState) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.empty => "Vacio",
|
||||||
|
.viewing => "Viendo",
|
||||||
|
.editing => "Editando",
|
||||||
|
.new => "Nuevo",
|
||||||
|
.saving => "Guardando...",
|
||||||
|
.saved => "Guardado",
|
||||||
|
.error_save => "Error",
|
||||||
|
.deleting => "Eliminar?",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verifica si el estado permite guardar
|
||||||
|
pub fn canSave(self: DetailPanelState) bool {
|
||||||
|
return self == .editing or self == .new;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verifica si el estado permite eliminar
|
||||||
|
pub fn canDelete(self: DetailPanelState) bool {
|
||||||
|
return self == .viewing or self == .editing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verifica si el estado tiene cambios pendientes
|
||||||
|
pub fn isDirty(self: DetailPanelState) bool {
|
||||||
|
return self == .editing or self == .new;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verifica si el estado es de error
|
||||||
|
pub fn isError(self: DetailPanelState) bool {
|
||||||
|
return self == .error_save;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verifica si el panel tiene datos cargados
|
||||||
|
pub fn hasData(self: DetailPanelState) bool {
|
||||||
|
return self != .empty;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Tests
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
test "DetailPanelState colors" {
|
||||||
|
// Verificar que cada estado tiene un color definido
|
||||||
|
const states = [_]DetailPanelState{
|
||||||
|
.empty, .viewing, .editing, .new, .saving, .saved, .error_save, .deleting,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (states) |state| {
|
||||||
|
const color = state.getColor();
|
||||||
|
// Alpha siempre debe ser 255
|
||||||
|
try std.testing.expectEqual(@as(u8, 255), color.a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "DetailPanelState canSave" {
|
||||||
|
try std.testing.expect(!DetailPanelState.empty.canSave());
|
||||||
|
try std.testing.expect(!DetailPanelState.viewing.canSave());
|
||||||
|
try std.testing.expect(DetailPanelState.editing.canSave());
|
||||||
|
try std.testing.expect(DetailPanelState.new.canSave());
|
||||||
|
try std.testing.expect(!DetailPanelState.saved.canSave());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "DetailPanelState canDelete" {
|
||||||
|
try std.testing.expect(!DetailPanelState.empty.canDelete());
|
||||||
|
try std.testing.expect(DetailPanelState.viewing.canDelete());
|
||||||
|
try std.testing.expect(DetailPanelState.editing.canDelete());
|
||||||
|
try std.testing.expect(!DetailPanelState.new.canDelete());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "DetailPanelState isDirty" {
|
||||||
|
try std.testing.expect(!DetailPanelState.empty.isDirty());
|
||||||
|
try std.testing.expect(!DetailPanelState.viewing.isDirty());
|
||||||
|
try std.testing.expect(DetailPanelState.editing.isDirty());
|
||||||
|
try std.testing.expect(DetailPanelState.new.isDirty());
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
//! - AutonomousPanel: Self-contained UI component
|
//! - AutonomousPanel: Self-contained UI component
|
||||||
//! - Composite patterns: Vertical, Horizontal, Split, Tab, Grid
|
//! - Composite patterns: Vertical, Horizontal, Split, Tab, Grid
|
||||||
//! - DataManager: Observer pattern for panel communication
|
//! - DataManager: Observer pattern for panel communication
|
||||||
|
//! - Detail: Components for detail/edit panels (state machine, semaphore, buttons)
|
||||||
//!
|
//!
|
||||||
//! Architecture based on Simifactu's Lego Panels system:
|
//! Architecture based on Simifactu's Lego Panels system:
|
||||||
//! - Panels are autonomous (own state, UI, logic)
|
//! - Panels are autonomous (own state, UI, logic)
|
||||||
|
|
@ -20,6 +21,7 @@ const std = @import("std");
|
||||||
pub const panel = @import("panel.zig");
|
pub const panel = @import("panel.zig");
|
||||||
pub const composite = @import("composite.zig");
|
pub const composite = @import("composite.zig");
|
||||||
pub const data_manager = @import("data_manager.zig");
|
pub const data_manager = @import("data_manager.zig");
|
||||||
|
pub const detail = @import("detail/detail.zig");
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Panel types
|
// Panel types
|
||||||
|
|
@ -58,6 +60,18 @@ pub const ObserverCallback = data_manager.ObserverCallback;
|
||||||
pub const getDataManager = data_manager.getDataManager;
|
pub const getDataManager = data_manager.getDataManager;
|
||||||
pub const setDataManager = data_manager.setDataManager;
|
pub const setDataManager = data_manager.setDataManager;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Detail Panel types (for edit/detail panels)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
pub const DetailPanelState = detail.DetailPanelState;
|
||||||
|
pub const DetailPanelBase = detail.DetailPanelBase;
|
||||||
|
pub const EscapeResult = detail.EscapeResult;
|
||||||
|
pub const ActionButtonsResult = detail.ActionButtonsResult;
|
||||||
|
pub const NavButtonsResult = detail.NavButtonsResult;
|
||||||
|
pub const ActionButtonsOpts = detail.ActionButtonsOpts;
|
||||||
|
pub const NavButtonsOpts = detail.NavButtonsOpts;
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Tests
|
// Tests
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue