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
|
||||
//! - Composite patterns: Vertical, Horizontal, Split, Tab, Grid
|
||||
//! - DataManager: Observer pattern for panel communication
|
||||
//! - Detail: Components for detail/edit panels (state machine, semaphore, buttons)
|
||||
//!
|
||||
//! Architecture based on Simifactu's Lego Panels system:
|
||||
//! - Panels are autonomous (own state, UI, logic)
|
||||
|
|
@ -20,6 +21,7 @@ const std = @import("std");
|
|||
pub const panel = @import("panel.zig");
|
||||
pub const composite = @import("composite.zig");
|
||||
pub const data_manager = @import("data_manager.zig");
|
||||
pub const detail = @import("detail/detail.zig");
|
||||
|
||||
// =============================================================================
|
||||
// Panel types
|
||||
|
|
@ -58,6 +60,18 @@ pub const ObserverCallback = data_manager.ObserverCallback;
|
|||
pub const getDataManager = data_manager.getDataManager;
|
||||
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
|
||||
// =============================================================================
|
||||
|
|
|
|||
Loading…
Reference in a new issue