feat(bevel): API configurable de biseles 3D

Nueva API BevelStyle para control fino de efectos 3D:
- effect: raised (elevado) o sunken (hundido)
- inset: 0 (exterior) o 1 (interior)
- light_top, light_left: intensidad de luz por borde
- dark_bottom, dark_right: intensidad de sombra por borde
- use_hsl: HSL (true) o RGB (false)

Presets incluidos:
- BevelStyle.raised_inset (default, botones)
- BevelStyle.sunken_inset (botones presionados)
- BevelStyle.raised_outer (estilo Windows 95)
- BevelStyle.sunken_outer (estilo Windows 95)

API:
- ctx.drawBevel(x, y, w, h, color, style) - nueva función principal
- ctx.drawBeveledRect() - wrapper compatibilidad (usa raised_inset)
- ctx.drawBeveledRectPressed() - wrapper compatibilidad (usa sunken_inset)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
R.Eugenio 2026-01-05 22:00:16 +01:00
parent 6732ac1fc5
commit f17104c08b

View file

@ -495,17 +495,104 @@ pub const Context = struct {
}
// =========================================================================
// High-level Drawing Helpers
// High-level Drawing Helpers - Bevel System
// =========================================================================
/// Draw a rectangle with 3D bevel effect
/// Creates illusion of depth with light from top-left
/// - Top/Left edges: lighter (raised)
/// - Bottom/Right edges: darker (shadow)
/// Note: Bevel is drawn INSIDE the rect (inset by 1px) to not overlap border
pub fn drawBeveledRect(self: *Self, x: i32, y: i32, w: u32, h: u32, base_color: Style.Color) void {
const light = base_color.lightenHsl(10);
const dark = base_color.darkenHsl(15);
/// Configuración de estilo de bisel 3D
/// Permite control fino sobre el efecto visual
pub const BevelStyle = struct {
/// Tipo de efecto 3D
effect: Effect = .raised,
/// Offset del bisel en pixels (0 = borde exterior, 1 = interior)
inset: u8 = 1,
/// Intensidad de la luz (0-100, típico 10-20)
light_top: u8 = 10,
light_left: u8 = 10,
/// Intensidad de la sombra (0-100, típico 10-20)
dark_bottom: u8 = 15,
dark_right: u8 = 15,
/// Usar HSL (true) o RGB (false) para calcular colores
use_hsl: bool = true,
pub const Effect = enum {
raised, // Elevado: luz arriba/izq, sombra abajo/der
sunken, // Hundido: sombra arriba/izq, luz abajo/der
};
// === PRESETS COMUNES ===
/// Bisel elevado interior (default, para botones)
pub const raised_inset = BevelStyle{};
/// Bisel hundido interior (para botones presionados)
pub const sunken_inset = BevelStyle{ .effect = .sunken };
/// Bisel elevado exterior (estilo Windows 95/StatusLine)
pub const raised_outer = BevelStyle{
.inset = 0,
.use_hsl = false,
.light_top = 15,
.light_left = 12,
.dark_bottom = 15,
.dark_right = 12,
};
/// Bisel hundido exterior (estilo Windows 95/StatusLine)
pub const sunken_outer = BevelStyle{
.effect = .sunken,
.inset = 0,
.use_hsl = false,
.light_top = 10,
.light_left = 8,
.dark_bottom = 20,
.dark_right = 15,
};
};
/// Dibuja un rectángulo con efecto bisel 3D configurable
///
/// Ejemplo de uso:
/// ```zig
/// // Bisel elevado (default)
/// ctx.drawBevel(x, y, w, h, color, .{});
///
/// // Bisel hundido exterior (estilo StatusLine)
/// ctx.drawBevel(x, y, w, h, color, BevelStyle.sunken_outer);
///
/// // Personalizado
/// ctx.drawBevel(x, y, w, h, color, .{ .effect = .raised, .inset = 0, .light_top = 20 });
/// ```
pub fn drawBevel(self: *Self, x: i32, y: i32, w: u32, h: u32, base_color: Style.Color, style: BevelStyle) void {
// Calcular colores de luz y sombra
const light_top = if (style.use_hsl)
base_color.lightenHsl(@floatFromInt(style.light_top))
else
base_color.lighten(style.light_top);
const light_left = if (style.use_hsl)
base_color.lightenHsl(@floatFromInt(style.light_left))
else
base_color.lighten(style.light_left);
const dark_bottom = if (style.use_hsl)
base_color.darkenHsl(@floatFromInt(style.dark_bottom))
else
base_color.darken(style.dark_bottom);
const dark_right = if (style.use_hsl)
base_color.darkenHsl(@floatFromInt(style.dark_right))
else
base_color.darken(style.dark_right);
// Determinar colores según efecto (raised vs sunken)
const top_color = if (style.effect == .raised) light_top else dark_bottom;
const left_color = if (style.effect == .raised) light_left else dark_right;
const bottom_color = if (style.effect == .raised) dark_bottom else light_top;
const right_color = if (style.effect == .raised) dark_right else light_left;
// Main fill
self.pushCommand(.{ .rect = .{
@ -516,102 +603,57 @@ pub const Context = struct {
.color = base_color,
} });
// Bevel inset by 1px to stay inside border
const inner_w = if (w > 2) w - 2 else 1;
const inner_h = if (h > 2) h - 2 else 1;
// Calcular dimensiones según inset
const offset: i32 = @intCast(style.inset);
const size_reduction: u32 = @as(u32, style.inset) * 2;
const inner_w = if (w > size_reduction) w - size_reduction else 1;
const inner_h = if (h > size_reduction) h - size_reduction else 1;
// Top edge (light) - inset
// Top edge
self.pushCommand(.{ .rect = .{
.x = x + 1,
.y = y + 1,
.x = x + offset,
.y = y + offset,
.w = inner_w,
.h = 1,
.color = light,
.color = top_color,
} });
// Left edge (light) - inset
// Left edge
self.pushCommand(.{ .rect = .{
.x = x + 1,
.y = y + 1,
.x = x + offset,
.y = y + offset + 1,
.w = 1,
.h = inner_h,
.color = light,
.h = if (inner_h > 2) inner_h - 2 else 1,
.color = left_color,
} });
// Bottom edge (dark) - inset
// Bottom edge
self.pushCommand(.{ .rect = .{
.x = x + 1,
.y = y + @as(i32, @intCast(h)) - 2,
.x = x + offset,
.y = y + @as(i32, @intCast(h)) - 1 - offset,
.w = inner_w,
.h = 1,
.color = dark,
.color = bottom_color,
} });
// Right edge (dark) - inset
// Right edge
self.pushCommand(.{ .rect = .{
.x = x + @as(i32, @intCast(w)) - 2,
.y = y + 1,
.x = x + @as(i32, @intCast(w)) - 1 - offset,
.y = y + offset + 1,
.w = 1,
.h = inner_h,
.color = dark,
.h = if (inner_h > 2) inner_h - 2 else 1,
.color = right_color,
} });
}
/// Draw a rectangle with inverted 3D bevel effect (pressed state)
/// Dark edges on top/left, light on bottom/right
/// Note: Bevel is drawn INSIDE the rect (inset by 1px) to not overlap border
/// Wrapper simple: bisel elevado interior (compatibilidad)
pub fn drawBeveledRect(self: *Self, x: i32, y: i32, w: u32, h: u32, base_color: Style.Color) void {
self.drawBevel(x, y, w, h, base_color, BevelStyle.raised_inset);
}
/// Wrapper simple: bisel hundido interior (compatibilidad)
pub fn drawBeveledRectPressed(self: *Self, x: i32, y: i32, w: u32, h: u32, base_color: Style.Color) void {
const light = base_color.lightenHsl(10);
const dark = base_color.darkenHsl(15);
// Main fill
self.pushCommand(.{ .rect = .{
.x = x,
.y = y,
.w = w,
.h = h,
.color = base_color,
} });
// Bevel inset by 1px to stay inside border
const inner_w = if (w > 2) w - 2 else 1;
const inner_h = if (h > 2) h - 2 else 1;
// Top edge (dark - inverted) - inset
self.pushCommand(.{ .rect = .{
.x = x + 1,
.y = y + 1,
.w = inner_w,
.h = 1,
.color = dark,
} });
// Left edge (dark - inverted) - inset
self.pushCommand(.{ .rect = .{
.x = x + 1,
.y = y + 1,
.w = 1,
.h = inner_h,
.color = dark,
} });
// Bottom edge (light - inverted) - inset
self.pushCommand(.{ .rect = .{
.x = x + 1,
.y = y + @as(i32, @intCast(h)) - 2,
.w = inner_w,
.h = 1,
.color = light,
} });
// Right edge (light - inverted) - inset
self.pushCommand(.{ .rect = .{
.x = x + @as(i32, @intCast(w)) - 2,
.y = y + 1,
.w = 1,
.h = inner_h,
.color = light,
} });
self.drawBevel(x, y, w, h, base_color, BevelStyle.sunken_inset);
}
/// Draw a complete panel frame with focus-dependent styling.