Compare commits
2 commits
6bf1eb1eb8
...
ebad736c75
| Author | SHA1 | Date | |
|---|---|---|---|
| ebad736c75 | |||
| 364a7d963f |
10 changed files with 640 additions and 69 deletions
88
CLAUDE.md
88
CLAUDE.md
|
|
@ -78,54 +78,67 @@ Resumen breve (1-2 frases). Resultado principal.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ⭐ TAREA PENDIENTE: Paridad Visual con DVUI
|
## ⭐ Paridad Visual DVUI - Fase 1 COMPLETADA ✅
|
||||||
|
|
||||||
> **Prioridad**: ALTA
|
> **Estado**: Fase 1 completada (2025-12-17)
|
||||||
> **Auditoría completa**: `docs/research/DVUI_AUDIT_2025-12-17.md` (570 líneas)
|
> **Auditoría original**: `docs/research/DVUI_AUDIT_2025-12-17.md` (570 líneas)
|
||||||
> **Consensuado**: 2025-12-17
|
|
||||||
|
|
||||||
### El Problema
|
### El Problema (resuelto)
|
||||||
|
|
||||||
zcatgui tiene MÁS widgets que DVUI (~45 vs ~42), pero DVUI **se ve mejor** porque:
|
zcatgui tenía MÁS widgets que DVUI pero DVUI **se veía mejor** por falta de:
|
||||||
1. **Esquinas redondeadas** en todos los widgets
|
1. ✅ **Esquinas redondeadas** → `fillRoundedRect` con edge-fade AA
|
||||||
2. **Anti-aliasing en bordes** via edge-fade (no solo en TTF)
|
2. ✅ **Anti-aliasing en bordes** → edge-fade technique implementada
|
||||||
3. **Transiciones suaves** entre estados (hover, press)
|
3. ⏳ **Transiciones suaves** (Fase 2)
|
||||||
4. **Sombras aplicadas** a paneles y modales
|
4. ✅ **Sombras en paneles/modales** → Panel/Modal con shadow
|
||||||
|
|
||||||
### Solución Acordada
|
### Sistema Dual Implementado
|
||||||
|
|
||||||
Implementar sistema dual: `RenderMode.simple` (rápido) vs `RenderMode.fancy` (bonito)
|
```zig
|
||||||
|
// style.zig
|
||||||
|
pub const RenderMode = enum { simple, fancy };
|
||||||
|
var global_render_mode: RenderMode = .fancy; // Default: bonito
|
||||||
|
|
||||||
### Fases de Implementación
|
// Uso en widgets
|
||||||
|
if (Style.isFancy() and config.corner_radius > 0) {
|
||||||
|
ctx.pushCommand(Command.roundedRect(...));
|
||||||
|
} else {
|
||||||
|
ctx.pushCommand(Command.rect(...));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
**Fase 1: Rendering Visual (CRÍTICO)**
|
### Widgets Actualizados
|
||||||
1. Crear `src/render/path.zig` - Paths con arcos para esquinas redondeadas
|
|
||||||
2. Modificar `software.zig` - AA en bordes via edge-fade technique
|
| Widget | corner_radius | shadow | Notas |
|
||||||
3. Actualizar widgets - corner_radius en Button, Panel, TextInput, Select, Modal
|
|--------|---------------|--------|-------|
|
||||||
|
| Button | 4 | - | Esquinas redondeadas en fancy mode |
|
||||||
|
| Panel | 6 | ✅ offset 4px | Borde y shadow |
|
||||||
|
| TextInput | 3 | - | Esquinas sutiles |
|
||||||
|
| Select | 3 | - | Esquinas sutiles |
|
||||||
|
| Modal | 8 | ✅ offset 6px | Diálogo + botones + input |
|
||||||
|
|
||||||
|
### Código Añadido
|
||||||
|
|
||||||
|
- `framebuffer.zig`: +350 LOC (`fillRoundedRect`, `drawRoundedRect`, edge-fade AA)
|
||||||
|
- `command.zig`: +69 LOC (nuevos comandos `rounded_rect`, `rounded_rect_outline`)
|
||||||
|
- `style.zig`: +33 LOC (RenderMode system)
|
||||||
|
- Widgets: ~120 LOC entre todos
|
||||||
|
|
||||||
|
**Total: ~590 líneas nuevas/modificadas**
|
||||||
|
|
||||||
|
### Fases Pendientes
|
||||||
|
|
||||||
**Fase 2: Estados Visuales**
|
**Fase 2: Estados Visuales**
|
||||||
4. Integrar AnimationManager en widgets para transiciones hover/press
|
- Integrar AnimationManager en widgets para transiciones hover/press
|
||||||
5. Focus ring con anti-aliasing
|
- Focus ring con anti-aliasing
|
||||||
|
|
||||||
**Fase 3: Efectos**
|
**Fase 3: Efectos Avanzados**
|
||||||
6. Activar Shadow API existente (`effects.zig`) en Panel/Modal
|
- Mejorar uso de gradientes
|
||||||
7. Mejorar uso de gradientes
|
- Blur effect para backdrops
|
||||||
|
|
||||||
**Estimación**: ~1,400 LOC nuevas/modificadas
|
|
||||||
|
|
||||||
### Técnica Edge-Fade (de DVUI)
|
|
||||||
|
|
||||||
```
|
|
||||||
Cada borde tiene 2 vértices paralelos:
|
|
||||||
- Interno: color sólido (alpha = 1.0)
|
|
||||||
- Externo: transparente (alpha = 0.0)
|
|
||||||
GPU/CPU interpola → bordes suaves sin multisampling
|
|
||||||
```
|
|
||||||
|
|
||||||
### Documentación de Referencia
|
### Documentación de Referencia
|
||||||
|
|
||||||
- `docs/research/DVUI_AUDIT_2025-12-17.md` - Auditoría completa
|
- `docs/research/DVUI_AUDIT_2025-12-17.md` - Auditoría completa
|
||||||
- `docs/research/WIDGET_COMPARISON.md` - Comparativa anterior (actualizar después)
|
- `docs/research/WIDGET_COMPARISON.md` - Comparativa (actualizar después)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -175,7 +188,7 @@ ttf.drawText(fb, x, y, "Hola UTF-8: áéíóú ñ €", color, clip);
|
||||||
|
|
||||||
## Fuentes TTF: Estado Técnico
|
## Fuentes TTF: Estado Técnico
|
||||||
|
|
||||||
### Estado actual (v0.17.0) - FUNCIONAL
|
### Estado actual (v0.18.0) - FUNCIONAL
|
||||||
- ✅ Parsing TTF via zcatttf (cmap format 4 y 12)
|
- ✅ Parsing TTF via zcatttf (cmap format 4 y 12)
|
||||||
- ✅ Rasterización con áreas trapezoidales (antialiasing)
|
- ✅ Rasterización con áreas trapezoidales (antialiasing)
|
||||||
- ✅ Fuente embebida (DroidSans)
|
- ✅ Fuente embebida (DroidSans)
|
||||||
|
|
@ -204,7 +217,7 @@ font.drawText(fb, x, y, "Texto con UTF-8: ñ €", color, clip);
|
||||||
| Campo | Valor |
|
| Campo | Valor |
|
||||||
|-------|-------|
|
|-------|-------|
|
||||||
| **Nombre** | zcatgui |
|
| **Nombre** | zcatgui |
|
||||||
| **Versión** | v0.17.0 |
|
| **Versión** | v0.18.0 |
|
||||||
| **Fecha inicio** | 2025-12-09 |
|
| **Fecha inicio** | 2025-12-09 |
|
||||||
| **Estado** | ✅ COMPLETO - 37 widgets, ~35K LOC, 4 backends, TTF funcional |
|
| **Estado** | ✅ COMPLETO - 37 widgets, ~35K LOC, 4 backends, TTF funcional |
|
||||||
| **Lenguaje** | Zig 0.15.2 |
|
| **Lenguaje** | Zig 0.15.2 |
|
||||||
|
|
@ -766,12 +779,13 @@ const stdout = std.fs.File.stdout(); // NO std.io.getStdOut()
|
||||||
| 2025-12-16 | v0.16.1 | Fuente embebida: TtfFont.initEmbedded() |
|
| 2025-12-16 | v0.16.1 | Fuente embebida: TtfFont.initEmbedded() |
|
||||||
| 2025-12-16 | v0.16.2 | Fix TTF: DroidSans (187KB) reemplaza AdwaitaSans (variable). Y-flip rasterización. |
|
| 2025-12-16 | v0.16.2 | Fix TTF: DroidSans (187KB) reemplaza AdwaitaSans (variable). Y-flip rasterización. |
|
||||||
| 2025-12-17 | v0.17.0 | ⭐⭐⭐ Integración zcatttf v1.0 - TTF FUNCIONA PERFECTAMENTE |
|
| 2025-12-17 | v0.17.0 | ⭐⭐⭐ Integración zcatttf v1.0 - TTF FUNCIONA PERFECTAMENTE |
|
||||||
|
| 2025-12-17 | v0.18.0 | Paridad Visual DVUI Fase 1: RenderMode dual, esquinas redondeadas, sombras |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ESTADO ACTUAL
|
## ESTADO ACTUAL
|
||||||
|
|
||||||
**✅ PROYECTO COMPLETADO - v0.17.0**
|
**✅ PROYECTO COMPLETADO - v0.18.0**
|
||||||
|
|
||||||
> **Para detalles técnicos completos, ver `REFERENCE.md`** (1370 líneas de documentación)
|
> **Para detalles técnicos completos, ver `REFERENCE.md`** (1370 líneas de documentación)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,9 @@ pub const DrawCommand = union(enum) {
|
||||||
/// Draw a filled rectangle
|
/// Draw a filled rectangle
|
||||||
rect: RectCommand,
|
rect: RectCommand,
|
||||||
|
|
||||||
|
/// Draw a filled rounded rectangle (fancy mode)
|
||||||
|
rounded_rect: RoundedRectCommand,
|
||||||
|
|
||||||
/// Draw text
|
/// Draw text
|
||||||
text: TextCommand,
|
text: TextCommand,
|
||||||
|
|
||||||
|
|
@ -21,6 +24,9 @@ pub const DrawCommand = union(enum) {
|
||||||
/// Draw a rectangle outline (border)
|
/// Draw a rectangle outline (border)
|
||||||
rect_outline: RectOutlineCommand,
|
rect_outline: RectOutlineCommand,
|
||||||
|
|
||||||
|
/// Draw a rounded rectangle outline (fancy mode)
|
||||||
|
rounded_rect_outline: RoundedRectOutlineCommand,
|
||||||
|
|
||||||
/// Begin clipping to a rectangle
|
/// Begin clipping to a rectangle
|
||||||
clip: ClipCommand,
|
clip: ClipCommand,
|
||||||
|
|
||||||
|
|
@ -69,6 +75,29 @@ pub const RectOutlineCommand = struct {
|
||||||
thickness: u32 = 1,
|
thickness: u32 = 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Draw a filled rounded rectangle (fancy mode)
|
||||||
|
pub const RoundedRectCommand = struct {
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
w: u32,
|
||||||
|
h: u32,
|
||||||
|
color: Style.Color,
|
||||||
|
radius: u8,
|
||||||
|
aa: bool = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Draw a rounded rectangle outline (fancy mode)
|
||||||
|
pub const RoundedRectOutlineCommand = struct {
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
w: u32,
|
||||||
|
h: u32,
|
||||||
|
color: Style.Color,
|
||||||
|
radius: u8,
|
||||||
|
thickness: u8 = 1,
|
||||||
|
aa: bool = true,
|
||||||
|
};
|
||||||
|
|
||||||
/// Begin clipping to a rectangle
|
/// Begin clipping to a rectangle
|
||||||
pub const ClipCommand = struct {
|
pub const ClipCommand = struct {
|
||||||
x: i32,
|
x: i32,
|
||||||
|
|
@ -139,6 +168,46 @@ pub fn clipEnd() DrawCommand {
|
||||||
return .clip_end;
|
return .clip_end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a rounded rect command (fancy mode)
|
||||||
|
pub fn roundedRect(x: i32, y: i32, w: u32, h: u32, color: Style.Color, radius: u8) DrawCommand {
|
||||||
|
return .{ .rounded_rect = .{
|
||||||
|
.x = x,
|
||||||
|
.y = y,
|
||||||
|
.w = w,
|
||||||
|
.h = h,
|
||||||
|
.color = color,
|
||||||
|
.radius = radius,
|
||||||
|
.aa = true,
|
||||||
|
} };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a rounded rect command with configurable AA
|
||||||
|
pub fn roundedRectAA(x: i32, y: i32, w: u32, h: u32, color: Style.Color, radius: u8, aa: bool) DrawCommand {
|
||||||
|
return .{ .rounded_rect = .{
|
||||||
|
.x = x,
|
||||||
|
.y = y,
|
||||||
|
.w = w,
|
||||||
|
.h = h,
|
||||||
|
.color = color,
|
||||||
|
.radius = radius,
|
||||||
|
.aa = aa,
|
||||||
|
} };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a rounded rect outline command (fancy mode)
|
||||||
|
pub fn roundedRectOutline(x: i32, y: i32, w: u32, h: u32, color: Style.Color, radius: u8) DrawCommand {
|
||||||
|
return .{ .rounded_rect_outline = .{
|
||||||
|
.x = x,
|
||||||
|
.y = y,
|
||||||
|
.w = w,
|
||||||
|
.h = h,
|
||||||
|
.color = color,
|
||||||
|
.radius = radius,
|
||||||
|
.thickness = 1,
|
||||||
|
.aa = true,
|
||||||
|
} };
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Tests
|
// Tests
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,39 @@
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Render Mode - Simple vs Fancy
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Render mode controls visual quality vs performance tradeoff
|
||||||
|
pub const RenderMode = enum {
|
||||||
|
/// Fast rendering: rectangles, no AA on shapes, no shadows
|
||||||
|
/// Best for: low-end hardware, SSH, WASM with limited resources
|
||||||
|
simple,
|
||||||
|
|
||||||
|
/// Pretty rendering: rounded corners, edge-fade AA, shadows
|
||||||
|
/// Best for: desktop with good CPU, visual polish needed
|
||||||
|
fancy,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Global render mode - widgets check this to decide how to render
|
||||||
|
var global_render_mode: RenderMode = .fancy;
|
||||||
|
|
||||||
|
/// Get current render mode
|
||||||
|
pub fn getRenderMode() RenderMode {
|
||||||
|
return global_render_mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set render mode
|
||||||
|
pub fn setRenderMode(mode: RenderMode) void {
|
||||||
|
global_render_mode = mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if fancy rendering is enabled
|
||||||
|
pub fn isFancy() bool {
|
||||||
|
return global_render_mode == .fancy;
|
||||||
|
}
|
||||||
|
|
||||||
/// RGBA Color
|
/// RGBA Color
|
||||||
pub const Color = struct {
|
pub const Color = struct {
|
||||||
r: u8,
|
r: u8,
|
||||||
|
|
|
||||||
|
|
@ -217,6 +217,360 @@ pub const Framebuffer = struct {
|
||||||
pub fn getPitch(self: Self) u32 {
|
pub fn getPitch(self: Self) u32 {
|
||||||
return self.width * 4;
|
return self.width * 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Rounded Rectangle Drawing (Fancy Mode)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// Draw a filled rounded rectangle with optional edge-fade anti-aliasing
|
||||||
|
/// radius: corner radius in pixels
|
||||||
|
/// aa: if true, applies 1-pixel edge fade for smooth borders
|
||||||
|
pub fn fillRoundedRect(self: *Self, x: i32, y: i32, w: u32, h: u32, color: Color, radius: u8, aa: bool) void {
|
||||||
|
if (w == 0 or h == 0) return;
|
||||||
|
|
||||||
|
// Clamp radius to half the smallest dimension
|
||||||
|
const max_radius = @min(w, h) / 2;
|
||||||
|
const r: u32 = @min(@as(u32, radius), max_radius);
|
||||||
|
|
||||||
|
if (r == 0) {
|
||||||
|
// No radius, use fast path
|
||||||
|
self.fillRect(x, y, w, h, color);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate bounds
|
||||||
|
const x_start = @max(0, x);
|
||||||
|
const y_start = @max(0, y);
|
||||||
|
const x_end = @min(@as(i32, @intCast(self.width)), x + @as(i32, @intCast(w)));
|
||||||
|
const y_end = @min(@as(i32, @intCast(self.height)), y + @as(i32, @intCast(h)));
|
||||||
|
|
||||||
|
if (x_start >= x_end or y_start >= y_end) return;
|
||||||
|
|
||||||
|
// Corner circle centers (relative to rect origin)
|
||||||
|
const r_i32: i32 = @intCast(r);
|
||||||
|
const w_i32: i32 = @intCast(w);
|
||||||
|
const h_i32: i32 = @intCast(h);
|
||||||
|
|
||||||
|
// Corner centers in screen coordinates
|
||||||
|
const tl_cx = x + r_i32; // top-left
|
||||||
|
const tl_cy = y + r_i32;
|
||||||
|
const tr_cx = x + w_i32 - r_i32; // top-right
|
||||||
|
const tr_cy = y + r_i32;
|
||||||
|
const bl_cx = x + r_i32; // bottom-left
|
||||||
|
const bl_cy = y + h_i32 - r_i32;
|
||||||
|
const br_cx = x + w_i32 - r_i32; // bottom-right
|
||||||
|
const br_cy = y + h_i32 - r_i32;
|
||||||
|
|
||||||
|
const r_f: f32 = @floatFromInt(r);
|
||||||
|
|
||||||
|
var py = y_start;
|
||||||
|
while (py < y_end) : (py += 1) {
|
||||||
|
const row_start = @as(u32, @intCast(py)) * self.width;
|
||||||
|
var px = x_start;
|
||||||
|
while (px < x_end) : (px += 1) {
|
||||||
|
// Check which region the pixel is in
|
||||||
|
const in_corner = self.getCornerDistance(px, py, x, y, w_i32, h_i32, tl_cx, tl_cy, tr_cx, tr_cy, bl_cx, bl_cy, br_cx, br_cy, r_f);
|
||||||
|
|
||||||
|
if (in_corner) |dist| {
|
||||||
|
// In corner region - check distance to arc
|
||||||
|
if (dist <= r_f) {
|
||||||
|
// Inside the arc
|
||||||
|
if (aa and dist > r_f - 1.0) {
|
||||||
|
// Edge fade zone (last pixel)
|
||||||
|
const alpha_f = r_f - dist;
|
||||||
|
const alpha: u8 = @intFromFloat(@min(255.0, @max(0.0, alpha_f * 255.0)));
|
||||||
|
const aa_color = color.withAlpha(@as(u8, @intCast((@as(u16, color.a) * alpha) / 255)));
|
||||||
|
self.blendPixelAt(row_start + @as(u32, @intCast(px)), aa_color);
|
||||||
|
} else {
|
||||||
|
// Fully inside
|
||||||
|
self.blendPixelAt(row_start + @as(u32, @intCast(px)), color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Outside arc - don't draw
|
||||||
|
} else {
|
||||||
|
// Not in corner region - check edge fade for straight edges
|
||||||
|
if (aa) {
|
||||||
|
const edge_dist = self.getEdgeDistance(px, py, x, y, w_i32, h_i32);
|
||||||
|
if (edge_dist < 1.0) {
|
||||||
|
const alpha: u8 = @intFromFloat(@min(255.0, edge_dist * 255.0));
|
||||||
|
const aa_color = color.withAlpha(@as(u8, @intCast((@as(u16, color.a) * alpha) / 255)));
|
||||||
|
self.blendPixelAt(row_start + @as(u32, @intCast(px)), aa_color);
|
||||||
|
} else {
|
||||||
|
self.blendPixelAt(row_start + @as(u32, @intCast(px)), color);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.blendPixelAt(row_start + @as(u32, @intCast(px)), color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw a rounded rectangle outline with optional AA
|
||||||
|
pub fn drawRoundedRect(self: *Self, x: i32, y: i32, w: u32, h: u32, color: Color, radius: u8, thickness: u8, aa: bool) void {
|
||||||
|
if (w == 0 or h == 0 or thickness == 0) return;
|
||||||
|
|
||||||
|
const t: u32 = thickness;
|
||||||
|
|
||||||
|
// For thin outlines, we can use the difference of two rounded rects
|
||||||
|
// Outer rect
|
||||||
|
self.fillRoundedRect(x, y, w, h, color, radius, aa);
|
||||||
|
|
||||||
|
// Inner rect (punch out with background)
|
||||||
|
// This is a simplification - proper impl would track background color
|
||||||
|
// For now, we'll draw the outline pixel by pixel
|
||||||
|
|
||||||
|
// Actually, let's do this properly with a stroke approach
|
||||||
|
const max_radius = @min(w, h) / 2;
|
||||||
|
const r: u32 = @min(@as(u32, radius), max_radius);
|
||||||
|
const inner_r: u32 = if (r > t) r - t else 0;
|
||||||
|
|
||||||
|
// Draw using edge detection
|
||||||
|
const x_start = @max(0, x);
|
||||||
|
const y_start = @max(0, y);
|
||||||
|
const x_end = @min(@as(i32, @intCast(self.width)), x + @as(i32, @intCast(w)));
|
||||||
|
const y_end = @min(@as(i32, @intCast(self.height)), y + @as(i32, @intCast(h)));
|
||||||
|
|
||||||
|
if (x_start >= x_end or y_start >= y_end) return;
|
||||||
|
|
||||||
|
const r_i32: i32 = @intCast(r);
|
||||||
|
const w_i32: i32 = @intCast(w);
|
||||||
|
const h_i32: i32 = @intCast(h);
|
||||||
|
const t_i32: i32 = @intCast(t);
|
||||||
|
|
||||||
|
const r_f: f32 = @floatFromInt(r);
|
||||||
|
const inner_r_f: f32 = @floatFromInt(inner_r);
|
||||||
|
const t_f: f32 = @floatFromInt(t);
|
||||||
|
|
||||||
|
// Corner centers
|
||||||
|
const tl_cx = x + r_i32;
|
||||||
|
const tl_cy = y + r_i32;
|
||||||
|
const tr_cx = x + w_i32 - r_i32;
|
||||||
|
const tr_cy = y + r_i32;
|
||||||
|
const bl_cx = x + r_i32;
|
||||||
|
const bl_cy = y + h_i32 - r_i32;
|
||||||
|
const br_cx = x + w_i32 - r_i32;
|
||||||
|
const br_cy = y + h_i32 - r_i32;
|
||||||
|
|
||||||
|
var py = y_start;
|
||||||
|
while (py < y_end) : (py += 1) {
|
||||||
|
const row_start = @as(u32, @intCast(py)) * self.width;
|
||||||
|
var px = x_start;
|
||||||
|
while (px < x_end) : (px += 1) {
|
||||||
|
// Check if pixel is in the stroke region (between outer and inner bounds)
|
||||||
|
const in_stroke = self.isInStroke(px, py, x, y, w_i32, h_i32, t_i32, tl_cx, tl_cy, tr_cx, tr_cy, bl_cx, bl_cy, br_cx, br_cy, r_f, inner_r_f, t_f, aa);
|
||||||
|
|
||||||
|
if (in_stroke) |alpha_mult| {
|
||||||
|
if (alpha_mult >= 1.0) {
|
||||||
|
self.blendPixelAt(row_start + @as(u32, @intCast(px)), color);
|
||||||
|
} else if (alpha_mult > 0.0) {
|
||||||
|
const alpha: u8 = @intFromFloat(@min(255.0, alpha_mult * 255.0));
|
||||||
|
const aa_color = color.withAlpha(@as(u8, @intCast((@as(u16, color.a) * alpha) / 255)));
|
||||||
|
self.blendPixelAt(row_start + @as(u32, @intCast(px)), aa_color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: blend pixel at index with alpha
|
||||||
|
fn blendPixelAt(self: *Self, idx: u32, color: Color) void {
|
||||||
|
if (idx >= self.pixels.len) return;
|
||||||
|
|
||||||
|
if (color.a == 255) {
|
||||||
|
self.pixels[idx] = color.toABGR();
|
||||||
|
} else if (color.a > 0) {
|
||||||
|
const existing = self.pixels[idx];
|
||||||
|
const bg = Color{
|
||||||
|
.r = @truncate(existing),
|
||||||
|
.g = @truncate(existing >> 8),
|
||||||
|
.b = @truncate(existing >> 16),
|
||||||
|
.a = @truncate(existing >> 24),
|
||||||
|
};
|
||||||
|
self.pixels[idx] = color.blend(bg).toABGR();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: get distance from pixel to corner arc (null if not in corner region)
|
||||||
|
fn getCornerDistance(
|
||||||
|
self: *Self,
|
||||||
|
px: i32,
|
||||||
|
py: i32,
|
||||||
|
rect_x: i32,
|
||||||
|
rect_y: i32,
|
||||||
|
rect_w: i32,
|
||||||
|
rect_h: i32,
|
||||||
|
tl_cx: i32,
|
||||||
|
tl_cy: i32,
|
||||||
|
tr_cx: i32,
|
||||||
|
tr_cy: i32,
|
||||||
|
bl_cx: i32,
|
||||||
|
bl_cy: i32,
|
||||||
|
br_cx: i32,
|
||||||
|
br_cy: i32,
|
||||||
|
radius: f32,
|
||||||
|
) ?f32 {
|
||||||
|
_ = self;
|
||||||
|
_ = rect_w;
|
||||||
|
_ = rect_h;
|
||||||
|
|
||||||
|
// Check if pixel is in a corner region
|
||||||
|
// Top-left corner
|
||||||
|
if (px < tl_cx and py < tl_cy) {
|
||||||
|
const dx: f32 = @floatFromInt(tl_cx - px);
|
||||||
|
const dy: f32 = @floatFromInt(tl_cy - py);
|
||||||
|
return @sqrt(dx * dx + dy * dy);
|
||||||
|
}
|
||||||
|
// Top-right corner
|
||||||
|
if (px > tr_cx and py < tr_cy) {
|
||||||
|
const dx: f32 = @floatFromInt(px - tr_cx);
|
||||||
|
const dy: f32 = @floatFromInt(tr_cy - py);
|
||||||
|
return @sqrt(dx * dx + dy * dy);
|
||||||
|
}
|
||||||
|
// Bottom-left corner
|
||||||
|
if (px < bl_cx and py > bl_cy) {
|
||||||
|
const dx: f32 = @floatFromInt(bl_cx - px);
|
||||||
|
const dy: f32 = @floatFromInt(py - bl_cy);
|
||||||
|
return @sqrt(dx * dx + dy * dy);
|
||||||
|
}
|
||||||
|
// Bottom-right corner
|
||||||
|
if (px > br_cx and py > br_cy) {
|
||||||
|
const dx: f32 = @floatFromInt(px - br_cx);
|
||||||
|
const dy: f32 = @floatFromInt(py - br_cy);
|
||||||
|
return @sqrt(dx * dx + dy * dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check if outside rect bounds entirely
|
||||||
|
if (px < rect_x or py < rect_y) return radius + 10.0; // Outside
|
||||||
|
|
||||||
|
return null; // Not in corner region
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: get minimum distance to edge (for straight edges AA)
|
||||||
|
fn getEdgeDistance(self: *Self, px: i32, py: i32, rect_x: i32, rect_y: i32, rect_w: i32, rect_h: i32) f32 {
|
||||||
|
_ = self;
|
||||||
|
const left: f32 = @floatFromInt(px - rect_x);
|
||||||
|
const right: f32 = @floatFromInt((rect_x + rect_w - 1) - px);
|
||||||
|
const top: f32 = @floatFromInt(py - rect_y);
|
||||||
|
const bottom: f32 = @floatFromInt((rect_y + rect_h - 1) - py);
|
||||||
|
|
||||||
|
// Return minimum distance to any edge (clamped to positive)
|
||||||
|
return @max(0.0, @min(@min(left, right), @min(top, bottom))) + 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: check if pixel is in stroke region for outline
|
||||||
|
fn isInStroke(
|
||||||
|
self: *Self,
|
||||||
|
px: i32,
|
||||||
|
py: i32,
|
||||||
|
rect_x: i32,
|
||||||
|
rect_y: i32,
|
||||||
|
rect_w: i32,
|
||||||
|
rect_h: i32,
|
||||||
|
thickness: i32,
|
||||||
|
tl_cx: i32,
|
||||||
|
tl_cy: i32,
|
||||||
|
tr_cx: i32,
|
||||||
|
tr_cy: i32,
|
||||||
|
bl_cx: i32,
|
||||||
|
bl_cy: i32,
|
||||||
|
br_cx: i32,
|
||||||
|
br_cy: i32,
|
||||||
|
outer_r: f32,
|
||||||
|
inner_r: f32,
|
||||||
|
t_f: f32,
|
||||||
|
aa: bool,
|
||||||
|
) ?f32 {
|
||||||
|
_ = self;
|
||||||
|
_ = thickness;
|
||||||
|
|
||||||
|
// Check corners first
|
||||||
|
// Top-left
|
||||||
|
if (px < tl_cx and py < tl_cy) {
|
||||||
|
const dx: f32 = @floatFromInt(tl_cx - px);
|
||||||
|
const dy: f32 = @floatFromInt(tl_cy - py);
|
||||||
|
const dist = @sqrt(dx * dx + dy * dy);
|
||||||
|
if (dist > outer_r) {
|
||||||
|
if (aa and dist < outer_r + 1.0) return outer_r + 1.0 - dist;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (dist < inner_r) {
|
||||||
|
if (aa and dist > inner_r - 1.0) return dist - (inner_r - 1.0);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
// Top-right
|
||||||
|
if (px > tr_cx and py < tr_cy) {
|
||||||
|
const dx: f32 = @floatFromInt(px - tr_cx);
|
||||||
|
const dy: f32 = @floatFromInt(tr_cy - py);
|
||||||
|
const dist = @sqrt(dx * dx + dy * dy);
|
||||||
|
if (dist > outer_r) {
|
||||||
|
if (aa and dist < outer_r + 1.0) return outer_r + 1.0 - dist;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (dist < inner_r) {
|
||||||
|
if (aa and dist > inner_r - 1.0) return dist - (inner_r - 1.0);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
// Bottom-left
|
||||||
|
if (px < bl_cx and py > bl_cy) {
|
||||||
|
const dx: f32 = @floatFromInt(bl_cx - px);
|
||||||
|
const dy: f32 = @floatFromInt(py - bl_cy);
|
||||||
|
const dist = @sqrt(dx * dx + dy * dy);
|
||||||
|
if (dist > outer_r) {
|
||||||
|
if (aa and dist < outer_r + 1.0) return outer_r + 1.0 - dist;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (dist < inner_r) {
|
||||||
|
if (aa and dist > inner_r - 1.0) return dist - (inner_r - 1.0);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
// Bottom-right
|
||||||
|
if (px > br_cx and py > br_cy) {
|
||||||
|
const dx: f32 = @floatFromInt(px - br_cx);
|
||||||
|
const dy: f32 = @floatFromInt(py - br_cy);
|
||||||
|
const dist = @sqrt(dx * dx + dy * dy);
|
||||||
|
if (dist > outer_r) {
|
||||||
|
if (aa and dist < outer_r + 1.0) return outer_r + 1.0 - dist;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (dist < inner_r) {
|
||||||
|
if (aa and dist > inner_r - 1.0) return dist - (inner_r - 1.0);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Straight edges
|
||||||
|
const left_dist: f32 = @floatFromInt(px - rect_x);
|
||||||
|
const right_dist: f32 = @floatFromInt((rect_x + rect_w - 1) - px);
|
||||||
|
const top_dist: f32 = @floatFromInt(py - rect_y);
|
||||||
|
const bottom_dist: f32 = @floatFromInt((rect_y + rect_h - 1) - py);
|
||||||
|
|
||||||
|
// Check if in stroke region for straight edges
|
||||||
|
const in_left_stroke = left_dist >= 0 and left_dist < t_f;
|
||||||
|
const in_right_stroke = right_dist >= 0 and right_dist < t_f;
|
||||||
|
const in_top_stroke = top_dist >= 0 and top_dist < t_f;
|
||||||
|
const in_bottom_stroke = bottom_dist >= 0 and bottom_dist < t_f;
|
||||||
|
|
||||||
|
if (in_left_stroke or in_right_stroke or in_top_stroke or in_bottom_stroke) {
|
||||||
|
// AA for outer edge
|
||||||
|
if (aa) {
|
||||||
|
const min_outer = @min(@min(left_dist, right_dist), @min(top_dist, bottom_dist));
|
||||||
|
if (min_outer < 1.0 and min_outer >= 0) {
|
||||||
|
return min_outer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
|
||||||
|
|
@ -86,9 +86,11 @@ pub const SoftwareRenderer = struct {
|
||||||
pub fn execute(self: *Self, cmd: DrawCommand) void {
|
pub fn execute(self: *Self, cmd: DrawCommand) void {
|
||||||
switch (cmd) {
|
switch (cmd) {
|
||||||
.rect => |r| self.drawRect(r),
|
.rect => |r| self.drawRect(r),
|
||||||
|
.rounded_rect => |r| self.drawRoundedRect(r),
|
||||||
.text => |t| self.drawText(t),
|
.text => |t| self.drawText(t),
|
||||||
.line => |l| self.drawLine(l),
|
.line => |l| self.drawLine(l),
|
||||||
.rect_outline => |r| self.drawRectOutline(r),
|
.rect_outline => |r| self.drawRectOutline(r),
|
||||||
|
.rounded_rect_outline => |r| self.drawRoundedRectOutline(r),
|
||||||
.clip => |c| self.pushClip(c),
|
.clip => |c| self.pushClip(c),
|
||||||
.clip_end => self.popClip(),
|
.clip_end => self.popClip(),
|
||||||
.nop => {},
|
.nop => {},
|
||||||
|
|
@ -287,6 +289,17 @@ pub const SoftwareRenderer = struct {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn drawRoundedRect(self: *Self, r: Command.RoundedRectCommand) void {
|
||||||
|
// TODO: Apply clipping (for now, draw directly)
|
||||||
|
// The fillRoundedRect function handles bounds checking internally
|
||||||
|
self.framebuffer.fillRoundedRect(r.x, r.y, r.w, r.h, r.color, r.radius, r.aa);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drawRoundedRectOutline(self: *Self, r: Command.RoundedRectOutlineCommand) void {
|
||||||
|
// TODO: Apply clipping
|
||||||
|
self.framebuffer.drawRoundedRect(r.x, r.y, r.w, r.h, r.color, r.radius, r.thickness, r.aa);
|
||||||
|
}
|
||||||
|
|
||||||
fn pushClip(self: *Self, c: Command.ClipCommand) void {
|
fn pushClip(self: *Self, c: Command.ClipCommand) void {
|
||||||
if (self.clip_depth >= self.clip_stack.len) return;
|
if (self.clip_depth >= self.clip_stack.len) return;
|
||||||
|
|
||||||
|
|
@ -313,6 +326,7 @@ pub const SoftwareRenderer = struct {
|
||||||
fn commandBounds(cmd: DrawCommand) ?Rect {
|
fn commandBounds(cmd: DrawCommand) ?Rect {
|
||||||
return switch (cmd) {
|
return switch (cmd) {
|
||||||
.rect => |r| Rect.init(r.x, r.y, r.w, r.h),
|
.rect => |r| Rect.init(r.x, r.y, r.w, r.h),
|
||||||
|
.rounded_rect => |r| Rect.init(r.x, r.y, r.w, r.h),
|
||||||
.text => |t| blk: {
|
.text => |t| blk: {
|
||||||
// Estimate text bounds (width based on text length, height based on font)
|
// Estimate text bounds (width based on text length, height based on font)
|
||||||
// This is approximate; actual font metrics would be better
|
// This is approximate; actual font metrics would be better
|
||||||
|
|
@ -334,6 +348,7 @@ fn commandBounds(cmd: DrawCommand) ?Rect {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
.rect_outline => |r| Rect.init(r.x, r.y, r.w, r.h),
|
.rect_outline => |r| Rect.init(r.x, r.y, r.w, r.h),
|
||||||
|
.rounded_rect_outline => |r| Rect.init(r.x, r.y, r.w, r.h),
|
||||||
.clip, .clip_end, .nop => null,
|
.clip, .clip_end, .nop => null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ pub const ButtonConfig = struct {
|
||||||
disabled: bool = false,
|
disabled: bool = false,
|
||||||
/// Padding around text
|
/// Padding around text
|
||||||
padding: u32 = 8,
|
padding: u32 = 8,
|
||||||
|
/// Corner radius (0 = square, default 4 for fancy mode)
|
||||||
|
corner_radius: u8 = 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Draw a button and return true if clicked
|
/// Draw a button and return true if clicked
|
||||||
|
|
@ -78,11 +80,16 @@ pub fn buttonRect(ctx: *Context, bounds: Layout.Rect, text: []const u8, config:
|
||||||
else
|
else
|
||||||
theme.button_fg;
|
theme.button_fg;
|
||||||
|
|
||||||
// Draw background
|
// Draw background and border based on render mode
|
||||||
|
if (Style.isFancy() and config.corner_radius > 0) {
|
||||||
|
// Fancy mode: rounded corners with AA
|
||||||
|
ctx.pushCommand(Command.roundedRect(bounds.x, bounds.y, bounds.w, bounds.h, bg_color, config.corner_radius));
|
||||||
|
ctx.pushCommand(Command.roundedRectOutline(bounds.x, bounds.y, bounds.w, bounds.h, theme.border, config.corner_radius));
|
||||||
|
} else {
|
||||||
|
// Simple mode: square corners
|
||||||
ctx.pushCommand(Command.rect(bounds.x, bounds.y, bounds.w, bounds.h, bg_color));
|
ctx.pushCommand(Command.rect(bounds.x, bounds.y, bounds.w, bounds.h, bg_color));
|
||||||
|
|
||||||
// Draw border
|
|
||||||
ctx.pushCommand(Command.rectOutline(bounds.x, bounds.y, bounds.w, bounds.h, theme.border));
|
ctx.pushCommand(Command.rectOutline(bounds.x, bounds.y, bounds.w, bounds.h, theme.border));
|
||||||
|
}
|
||||||
|
|
||||||
// Draw text centered
|
// Draw text centered
|
||||||
const char_width: u32 = 8;
|
const char_width: u32 = 8;
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,10 @@ pub const ModalConfig = struct {
|
||||||
show_input: bool = false,
|
show_input: bool = false,
|
||||||
/// Input placeholder
|
/// Input placeholder
|
||||||
input_placeholder: []const u8 = "",
|
input_placeholder: []const u8 = "",
|
||||||
|
/// Corner radius (default 8 for fancy mode)
|
||||||
|
corner_radius: u8 = 8,
|
||||||
|
/// Show shadow (fancy mode only)
|
||||||
|
show_shadow: bool = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Modal colors
|
/// Modal colors
|
||||||
|
|
@ -127,6 +131,8 @@ pub const ModalColors = struct {
|
||||||
title_fg: Style.Color = Style.Color.rgb(220, 220, 220),
|
title_fg: Style.Color = Style.Color.rgb(220, 220, 220),
|
||||||
/// Message text color
|
/// Message text color
|
||||||
message_fg: Style.Color = Style.Color.rgb(200, 200, 200),
|
message_fg: Style.Color = Style.Color.rgb(200, 200, 200),
|
||||||
|
/// Shadow color (fancy mode only)
|
||||||
|
shadow: Style.Color = Style.Color.rgba(0, 0, 0, 80),
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Modal result
|
/// Modal result
|
||||||
|
|
@ -187,7 +193,29 @@ pub fn modalEx(
|
||||||
// Draw backdrop (semi-transparent overlay)
|
// Draw backdrop (semi-transparent overlay)
|
||||||
ctx.pushCommand(Command.rect(0, 0, screen_w, screen_h, colors.backdrop));
|
ctx.pushCommand(Command.rect(0, 0, screen_w, screen_h, colors.backdrop));
|
||||||
|
|
||||||
// Draw dialog border
|
// Check render mode for fancy features
|
||||||
|
const fancy = Style.isFancy() and config.corner_radius > 0;
|
||||||
|
|
||||||
|
// Draw shadow first (behind dialog) in fancy mode
|
||||||
|
if (fancy and config.show_shadow) {
|
||||||
|
const shadow_offset: i32 = 6;
|
||||||
|
ctx.pushCommand(Command.roundedRect(
|
||||||
|
dialog_x + shadow_offset,
|
||||||
|
dialog_y + shadow_offset,
|
||||||
|
dialog_w,
|
||||||
|
dialog_h,
|
||||||
|
colors.shadow,
|
||||||
|
config.corner_radius,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw dialog border and background based on render mode
|
||||||
|
if (fancy) {
|
||||||
|
// Fancy mode: rounded corners
|
||||||
|
ctx.pushCommand(Command.roundedRect(dialog_x, dialog_y, dialog_w, dialog_h, colors.background, config.corner_radius));
|
||||||
|
ctx.pushCommand(Command.roundedRectOutline(dialog_x, dialog_y, dialog_w, dialog_h, colors.border, config.corner_radius));
|
||||||
|
} else {
|
||||||
|
// Simple mode: square corners
|
||||||
ctx.pushCommand(Command.rectOutline(
|
ctx.pushCommand(Command.rectOutline(
|
||||||
dialog_x - 1,
|
dialog_x - 1,
|
||||||
dialog_y - 1,
|
dialog_y - 1,
|
||||||
|
|
@ -195,11 +223,10 @@ pub fn modalEx(
|
||||||
dialog_h + 2,
|
dialog_h + 2,
|
||||||
colors.border,
|
colors.border,
|
||||||
));
|
));
|
||||||
|
|
||||||
// Draw dialog background
|
|
||||||
ctx.pushCommand(Command.rect(dialog_x, dialog_y, dialog_w, dialog_h, colors.background));
|
ctx.pushCommand(Command.rect(dialog_x, dialog_y, dialog_w, dialog_h, colors.background));
|
||||||
|
}
|
||||||
|
|
||||||
// Draw title bar
|
// Draw title bar (inside dialog, so no rounded corners needed)
|
||||||
ctx.pushCommand(Command.rect(dialog_x, dialog_y, dialog_w, title_h, colors.title_bg));
|
ctx.pushCommand(Command.rect(dialog_x, dialog_y, dialog_w, title_h, colors.title_bg));
|
||||||
|
|
||||||
// Draw title text
|
// Draw title text
|
||||||
|
|
@ -223,10 +250,16 @@ pub fn modalEx(
|
||||||
24,
|
24,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Simple input rendering
|
// Input rendering
|
||||||
const input_bg = Style.Color.rgb(35, 35, 40);
|
const input_bg = Style.Color.rgb(35, 35, 40);
|
||||||
|
const input_radius: u8 = 3;
|
||||||
|
if (fancy) {
|
||||||
|
ctx.pushCommand(Command.roundedRect(input_rect.x, input_rect.y, input_rect.w, input_rect.h, input_bg, input_radius));
|
||||||
|
ctx.pushCommand(Command.roundedRectOutline(input_rect.x, input_rect.y, input_rect.w, input_rect.h, colors.border, input_radius));
|
||||||
|
} else {
|
||||||
ctx.pushCommand(Command.rect(input_rect.x, input_rect.y, input_rect.w, input_rect.h, input_bg));
|
ctx.pushCommand(Command.rect(input_rect.x, input_rect.y, input_rect.w, input_rect.h, input_bg));
|
||||||
ctx.pushCommand(Command.rectOutline(input_rect.x, input_rect.y, input_rect.w, input_rect.h, colors.border));
|
ctx.pushCommand(Command.rectOutline(input_rect.x, input_rect.y, input_rect.w, input_rect.h, colors.border));
|
||||||
|
}
|
||||||
|
|
||||||
const txt = input_st.text();
|
const txt = input_st.text();
|
||||||
if (txt.len > 0) {
|
if (txt.len > 0) {
|
||||||
|
|
@ -264,11 +297,18 @@ pub fn modalEx(
|
||||||
.danger => Style.Color.danger.darken(30),
|
.danger => Style.Color.danger.darken(30),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const btn_radius: u8 = 4;
|
||||||
|
if (fancy) {
|
||||||
|
ctx.pushCommand(Command.roundedRect(btn_x, btn_y, btn_width, button_h - 4, btn_bg, btn_radius));
|
||||||
|
if (is_focused) {
|
||||||
|
ctx.pushCommand(Command.roundedRectOutline(btn_x, btn_y, btn_width, button_h - 4, Style.Color.rgb(200, 200, 200), btn_radius));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
ctx.pushCommand(Command.rect(btn_x, btn_y, btn_width, button_h - 4, btn_bg));
|
ctx.pushCommand(Command.rect(btn_x, btn_y, btn_width, button_h - 4, btn_bg));
|
||||||
|
|
||||||
if (is_focused) {
|
if (is_focused) {
|
||||||
ctx.pushCommand(Command.rectOutline(btn_x, btn_y, btn_width, button_h - 4, Style.Color.rgb(200, 200, 200)));
|
ctx.pushCommand(Command.rectOutline(btn_x, btn_y, btn_width, button_h - 4, Style.Color.rgb(200, 200, 200)));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Button text
|
// Button text
|
||||||
const text_w = btn.label.len * 8;
|
const text_w = btn.label.len * 8;
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,10 @@ pub const PanelConfig = struct {
|
||||||
collapsible: bool = false,
|
collapsible: bool = false,
|
||||||
/// Show close button (X)
|
/// Show close button (X)
|
||||||
closable: bool = false,
|
closable: bool = false,
|
||||||
|
/// Corner radius (default 6 for fancy mode)
|
||||||
|
corner_radius: u8 = 6,
|
||||||
|
/// Show shadow (fancy mode only)
|
||||||
|
show_shadow: bool = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Panel colors
|
/// Panel colors
|
||||||
|
|
@ -42,6 +46,7 @@ pub const PanelColors = struct {
|
||||||
content_bg: Style.Color = Style.Color.rgb(35, 35, 40),
|
content_bg: Style.Color = Style.Color.rgb(35, 35, 40),
|
||||||
border: Style.Color = Style.Color.rgb(70, 70, 75),
|
border: Style.Color = Style.Color.rgb(70, 70, 75),
|
||||||
border_focused: Style.Color = Style.Color.primary,
|
border_focused: Style.Color = Style.Color.primary,
|
||||||
|
shadow: Style.Color = Style.Color.rgba(0, 0, 0, 60),
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Panel result
|
/// Panel result
|
||||||
|
|
@ -104,8 +109,28 @@ pub fn panelRect(
|
||||||
// Border color
|
// Border color
|
||||||
const border_color = if (state.focused) colors.border_focused else colors.border;
|
const border_color = if (state.focused) colors.border_focused else colors.border;
|
||||||
|
|
||||||
|
// Check render mode for fancy features
|
||||||
|
const fancy = Style.isFancy() and config.corner_radius > 0;
|
||||||
|
|
||||||
|
// Draw shadow first (behind panel) in fancy mode
|
||||||
|
if (fancy and config.show_shadow) {
|
||||||
|
const shadow_offset: i32 = 4;
|
||||||
|
ctx.pushCommand(Command.roundedRect(
|
||||||
|
bounds.x + shadow_offset,
|
||||||
|
bounds.y + shadow_offset,
|
||||||
|
bounds.w,
|
||||||
|
bounds.h,
|
||||||
|
colors.shadow,
|
||||||
|
config.corner_radius,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
// Draw outer border
|
// Draw outer border
|
||||||
|
if (fancy) {
|
||||||
|
ctx.pushCommand(Command.roundedRectOutline(bounds.x, bounds.y, bounds.w, bounds.h, border_color, config.corner_radius));
|
||||||
|
} else {
|
||||||
ctx.pushCommand(Command.rectOutline(bounds.x, bounds.y, bounds.w, bounds.h, border_color));
|
ctx.pushCommand(Command.rectOutline(bounds.x, bounds.y, bounds.w, bounds.h, border_color));
|
||||||
|
}
|
||||||
|
|
||||||
// Title bar bounds
|
// Title bar bounds
|
||||||
const title_bounds = Layout.Rect.init(
|
const title_bounds = Layout.Rect.init(
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,8 @@ pub const SelectConfig = struct {
|
||||||
item_height: u32 = 24,
|
item_height: u32 = 24,
|
||||||
/// Padding
|
/// Padding
|
||||||
padding: u32 = 4,
|
padding: u32 = 4,
|
||||||
|
/// Corner radius (default 3 for fancy mode)
|
||||||
|
corner_radius: u8 = 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Select result
|
/// Select result
|
||||||
|
|
@ -120,9 +122,14 @@ pub fn selectRect(
|
||||||
|
|
||||||
const border_color = if (has_focus or state.open) theme.primary else theme.border;
|
const border_color = if (has_focus or state.open) theme.primary else theme.border;
|
||||||
|
|
||||||
// Draw main button background
|
// Draw main button background based on render mode
|
||||||
|
if (Style.isFancy() and config.corner_radius > 0) {
|
||||||
|
ctx.pushCommand(Command.roundedRect(bounds.x, bounds.y, bounds.w, bounds.h, bg_color, config.corner_radius));
|
||||||
|
ctx.pushCommand(Command.roundedRectOutline(bounds.x, bounds.y, bounds.w, bounds.h, border_color, config.corner_radius));
|
||||||
|
} else {
|
||||||
ctx.pushCommand(Command.rect(bounds.x, bounds.y, bounds.w, bounds.h, bg_color));
|
ctx.pushCommand(Command.rect(bounds.x, bounds.y, bounds.w, bounds.h, bg_color));
|
||||||
ctx.pushCommand(Command.rectOutline(bounds.x, bounds.y, bounds.w, bounds.h, border_color));
|
ctx.pushCommand(Command.rectOutline(bounds.x, bounds.y, bounds.w, bounds.h, border_color));
|
||||||
|
}
|
||||||
|
|
||||||
// Draw selected text or placeholder
|
// Draw selected text or placeholder
|
||||||
const display_text = if (state.selectedIndex()) |idx|
|
const display_text = if (state.selectedIndex()) |idx|
|
||||||
|
|
|
||||||
|
|
@ -215,6 +215,8 @@ pub const TextInputConfig = struct {
|
||||||
text_color: ?Style.Color = null,
|
text_color: ?Style.Color = null,
|
||||||
/// Override border color (for validation feedback). If null, uses theme default.
|
/// Override border color (for validation feedback). If null, uses theme default.
|
||||||
border_color: ?Style.Color = null,
|
border_color: ?Style.Color = null,
|
||||||
|
/// Corner radius (default 3 for fancy mode)
|
||||||
|
corner_radius: u8 = 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Result of text input widget
|
/// Result of text input widget
|
||||||
|
|
@ -284,11 +286,16 @@ pub fn textInputRect(
|
||||||
const text_color = config.text_color orelse theme.input_fg;
|
const text_color = config.text_color orelse theme.input_fg;
|
||||||
const placeholder_color = theme.secondary;
|
const placeholder_color = theme.secondary;
|
||||||
|
|
||||||
// Draw background
|
// Draw background and border based on render mode
|
||||||
|
if (Style.isFancy() and config.corner_radius > 0) {
|
||||||
|
// Fancy mode: rounded corners
|
||||||
|
ctx.pushCommand(Command.roundedRect(bounds.x, bounds.y, bounds.w, bounds.h, bg_color, config.corner_radius));
|
||||||
|
ctx.pushCommand(Command.roundedRectOutline(bounds.x, bounds.y, bounds.w, bounds.h, border_color, config.corner_radius));
|
||||||
|
} else {
|
||||||
|
// Simple mode: square corners
|
||||||
ctx.pushCommand(Command.rect(bounds.x, bounds.y, bounds.w, bounds.h, bg_color));
|
ctx.pushCommand(Command.rect(bounds.x, bounds.y, bounds.w, bounds.h, bg_color));
|
||||||
|
|
||||||
// Draw border
|
|
||||||
ctx.pushCommand(Command.rectOutline(bounds.x, bounds.y, bounds.w, bounds.h, border_color));
|
ctx.pushCommand(Command.rectOutline(bounds.x, bounds.y, bounds.w, bounds.h, border_color));
|
||||||
|
}
|
||||||
|
|
||||||
// Inner area
|
// Inner area
|
||||||
const inner = bounds.shrink(config.padding);
|
const inner = bounds.shrink(config.padding);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue