Compare commits
No commits in common. "ebad736c756afee44e51255728ae9632ec65f3da" and "6bf1eb1eb882b56c16ddc08ba7167181b8b777fb" have entirely different histories.
ebad736c75
...
6bf1eb1eb8
10 changed files with 69 additions and 640 deletions
88
CLAUDE.md
88
CLAUDE.md
|
|
@ -78,67 +78,54 @@ Resumen breve (1-2 frases). Resultado principal.
|
|||
|
||||
---
|
||||
|
||||
## ⭐ Paridad Visual DVUI - Fase 1 COMPLETADA ✅
|
||||
## ⭐ TAREA PENDIENTE: Paridad Visual con DVUI
|
||||
|
||||
> **Estado**: Fase 1 completada (2025-12-17)
|
||||
> **Auditoría original**: `docs/research/DVUI_AUDIT_2025-12-17.md` (570 líneas)
|
||||
> **Prioridad**: ALTA
|
||||
> **Auditoría completa**: `docs/research/DVUI_AUDIT_2025-12-17.md` (570 líneas)
|
||||
> **Consensuado**: 2025-12-17
|
||||
|
||||
### El Problema (resuelto)
|
||||
### El Problema
|
||||
|
||||
zcatgui tenía MÁS widgets que DVUI pero DVUI **se veía mejor** por falta de:
|
||||
1. ✅ **Esquinas redondeadas** → `fillRoundedRect` con edge-fade AA
|
||||
2. ✅ **Anti-aliasing en bordes** → edge-fade technique implementada
|
||||
3. ⏳ **Transiciones suaves** (Fase 2)
|
||||
4. ✅ **Sombras en paneles/modales** → Panel/Modal con shadow
|
||||
zcatgui tiene MÁS widgets que DVUI (~45 vs ~42), pero DVUI **se ve mejor** porque:
|
||||
1. **Esquinas redondeadas** en todos los widgets
|
||||
2. **Anti-aliasing en bordes** via edge-fade (no solo en TTF)
|
||||
3. **Transiciones suaves** entre estados (hover, press)
|
||||
4. **Sombras aplicadas** a paneles y modales
|
||||
|
||||
### Sistema Dual Implementado
|
||||
### Solución Acordada
|
||||
|
||||
```zig
|
||||
// style.zig
|
||||
pub const RenderMode = enum { simple, fancy };
|
||||
var global_render_mode: RenderMode = .fancy; // Default: bonito
|
||||
Implementar sistema dual: `RenderMode.simple` (rápido) vs `RenderMode.fancy` (bonito)
|
||||
|
||||
// Uso en widgets
|
||||
if (Style.isFancy() and config.corner_radius > 0) {
|
||||
ctx.pushCommand(Command.roundedRect(...));
|
||||
} else {
|
||||
ctx.pushCommand(Command.rect(...));
|
||||
}
|
||||
```
|
||||
### Fases de Implementación
|
||||
|
||||
### Widgets Actualizados
|
||||
|
||||
| Widget | corner_radius | shadow | Notas |
|
||||
|--------|---------------|--------|-------|
|
||||
| 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 1: Rendering Visual (CRÍTICO)**
|
||||
1. Crear `src/render/path.zig` - Paths con arcos para esquinas redondeadas
|
||||
2. Modificar `software.zig` - AA en bordes via edge-fade technique
|
||||
3. Actualizar widgets - corner_radius en Button, Panel, TextInput, Select, Modal
|
||||
|
||||
**Fase 2: Estados Visuales**
|
||||
- Integrar AnimationManager en widgets para transiciones hover/press
|
||||
- Focus ring con anti-aliasing
|
||||
4. Integrar AnimationManager en widgets para transiciones hover/press
|
||||
5. Focus ring con anti-aliasing
|
||||
|
||||
**Fase 3: Efectos Avanzados**
|
||||
- Mejorar uso de gradientes
|
||||
- Blur effect para backdrops
|
||||
**Fase 3: Efectos**
|
||||
6. Activar Shadow API existente (`effects.zig`) en Panel/Modal
|
||||
7. Mejorar uso de gradientes
|
||||
|
||||
**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
|
||||
|
||||
- `docs/research/DVUI_AUDIT_2025-12-17.md` - Auditoría completa
|
||||
- `docs/research/WIDGET_COMPARISON.md` - Comparativa (actualizar después)
|
||||
- `docs/research/WIDGET_COMPARISON.md` - Comparativa anterior (actualizar después)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -188,7 +175,7 @@ ttf.drawText(fb, x, y, "Hola UTF-8: áéíóú ñ €", color, clip);
|
|||
|
||||
## Fuentes TTF: Estado Técnico
|
||||
|
||||
### Estado actual (v0.18.0) - FUNCIONAL
|
||||
### Estado actual (v0.17.0) - FUNCIONAL
|
||||
- ✅ Parsing TTF via zcatttf (cmap format 4 y 12)
|
||||
- ✅ Rasterización con áreas trapezoidales (antialiasing)
|
||||
- ✅ Fuente embebida (DroidSans)
|
||||
|
|
@ -217,7 +204,7 @@ font.drawText(fb, x, y, "Texto con UTF-8: ñ €", color, clip);
|
|||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **Nombre** | zcatgui |
|
||||
| **Versión** | v0.18.0 |
|
||||
| **Versión** | v0.17.0 |
|
||||
| **Fecha inicio** | 2025-12-09 |
|
||||
| **Estado** | ✅ COMPLETO - 37 widgets, ~35K LOC, 4 backends, TTF funcional |
|
||||
| **Lenguaje** | Zig 0.15.2 |
|
||||
|
|
@ -779,13 +766,12 @@ 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.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.18.0 | Paridad Visual DVUI Fase 1: RenderMode dual, esquinas redondeadas, sombras |
|
||||
|
||||
---
|
||||
|
||||
## ESTADO ACTUAL
|
||||
|
||||
**✅ PROYECTO COMPLETADO - v0.18.0**
|
||||
**✅ PROYECTO COMPLETADO - v0.17.0**
|
||||
|
||||
> **Para detalles técnicos completos, ver `REFERENCE.md`** (1370 líneas de documentación)
|
||||
|
||||
|
|
|
|||
|
|
@ -12,9 +12,6 @@ pub const DrawCommand = union(enum) {
|
|||
/// Draw a filled rectangle
|
||||
rect: RectCommand,
|
||||
|
||||
/// Draw a filled rounded rectangle (fancy mode)
|
||||
rounded_rect: RoundedRectCommand,
|
||||
|
||||
/// Draw text
|
||||
text: TextCommand,
|
||||
|
||||
|
|
@ -24,9 +21,6 @@ pub const DrawCommand = union(enum) {
|
|||
/// Draw a rectangle outline (border)
|
||||
rect_outline: RectOutlineCommand,
|
||||
|
||||
/// Draw a rounded rectangle outline (fancy mode)
|
||||
rounded_rect_outline: RoundedRectOutlineCommand,
|
||||
|
||||
/// Begin clipping to a rectangle
|
||||
clip: ClipCommand,
|
||||
|
||||
|
|
@ -75,29 +69,6 @@ pub const RectOutlineCommand = struct {
|
|||
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
|
||||
pub const ClipCommand = struct {
|
||||
x: i32,
|
||||
|
|
@ -168,46 +139,6 @@ pub fn clipEnd() DrawCommand {
|
|||
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
|
||||
// =============================================================================
|
||||
|
|
|
|||
|
|
@ -4,39 +4,6 @@
|
|||
|
||||
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
|
||||
pub const Color = struct {
|
||||
r: u8,
|
||||
|
|
|
|||
|
|
@ -217,360 +217,6 @@ pub const Framebuffer = struct {
|
|||
pub fn getPitch(self: Self) u32 {
|
||||
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,11 +86,9 @@ pub const SoftwareRenderer = struct {
|
|||
pub fn execute(self: *Self, cmd: DrawCommand) void {
|
||||
switch (cmd) {
|
||||
.rect => |r| self.drawRect(r),
|
||||
.rounded_rect => |r| self.drawRoundedRect(r),
|
||||
.text => |t| self.drawText(t),
|
||||
.line => |l| self.drawLine(l),
|
||||
.rect_outline => |r| self.drawRectOutline(r),
|
||||
.rounded_rect_outline => |r| self.drawRoundedRectOutline(r),
|
||||
.clip => |c| self.pushClip(c),
|
||||
.clip_end => self.popClip(),
|
||||
.nop => {},
|
||||
|
|
@ -289,17 +287,6 @@ 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 {
|
||||
if (self.clip_depth >= self.clip_stack.len) return;
|
||||
|
||||
|
|
@ -326,7 +313,6 @@ pub const SoftwareRenderer = struct {
|
|||
fn commandBounds(cmd: DrawCommand) ?Rect {
|
||||
return switch (cmd) {
|
||||
.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: {
|
||||
// Estimate text bounds (width based on text length, height based on font)
|
||||
// This is approximate; actual font metrics would be better
|
||||
|
|
@ -348,7 +334,6 @@ fn commandBounds(cmd: DrawCommand) ?Rect {
|
|||
);
|
||||
},
|
||||
.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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,8 +29,6 @@ pub const ButtonConfig = struct {
|
|||
disabled: bool = false,
|
||||
/// Padding around text
|
||||
padding: u32 = 8,
|
||||
/// Corner radius (0 = square, default 4 for fancy mode)
|
||||
corner_radius: u8 = 4,
|
||||
};
|
||||
|
||||
/// Draw a button and return true if clicked
|
||||
|
|
@ -80,16 +78,11 @@ pub fn buttonRect(ctx: *Context, bounds: Layout.Rect, text: []const u8, config:
|
|||
else
|
||||
theme.button_fg;
|
||||
|
||||
// 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.rectOutline(bounds.x, bounds.y, bounds.w, bounds.h, theme.border));
|
||||
}
|
||||
// Draw background
|
||||
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));
|
||||
|
||||
// Draw text centered
|
||||
const char_width: u32 = 8;
|
||||
|
|
|
|||
|
|
@ -111,10 +111,6 @@ pub const ModalConfig = struct {
|
|||
show_input: bool = false,
|
||||
/// Input placeholder
|
||||
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
|
||||
|
|
@ -131,8 +127,6 @@ pub const ModalColors = struct {
|
|||
title_fg: Style.Color = Style.Color.rgb(220, 220, 220),
|
||||
/// Message text color
|
||||
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
|
||||
|
|
@ -193,40 +187,19 @@ pub fn modalEx(
|
|||
// Draw backdrop (semi-transparent overlay)
|
||||
ctx.pushCommand(Command.rect(0, 0, screen_w, screen_h, colors.backdrop));
|
||||
|
||||
// Check render mode for fancy features
|
||||
const fancy = Style.isFancy() and config.corner_radius > 0;
|
||||
// Draw dialog border
|
||||
ctx.pushCommand(Command.rectOutline(
|
||||
dialog_x - 1,
|
||||
dialog_y - 1,
|
||||
dialog_w + 2,
|
||||
dialog_h + 2,
|
||||
colors.border,
|
||||
));
|
||||
|
||||
// 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 background
|
||||
ctx.pushCommand(Command.rect(dialog_x, dialog_y, dialog_w, dialog_h, colors.background));
|
||||
|
||||
// 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(
|
||||
dialog_x - 1,
|
||||
dialog_y - 1,
|
||||
dialog_w + 2,
|
||||
dialog_h + 2,
|
||||
colors.border,
|
||||
));
|
||||
ctx.pushCommand(Command.rect(dialog_x, dialog_y, dialog_w, dialog_h, colors.background));
|
||||
}
|
||||
|
||||
// Draw title bar (inside dialog, so no rounded corners needed)
|
||||
// Draw title bar
|
||||
ctx.pushCommand(Command.rect(dialog_x, dialog_y, dialog_w, title_h, colors.title_bg));
|
||||
|
||||
// Draw title text
|
||||
|
|
@ -250,16 +223,10 @@ pub fn modalEx(
|
|||
24,
|
||||
);
|
||||
|
||||
// Input rendering
|
||||
// Simple input rendering
|
||||
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.rectOutline(input_rect.x, input_rect.y, input_rect.w, input_rect.h, colors.border));
|
||||
}
|
||||
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));
|
||||
|
||||
const txt = input_st.text();
|
||||
if (txt.len > 0) {
|
||||
|
|
@ -297,17 +264,10 @@ pub fn modalEx(
|
|||
.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));
|
||||
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.rect(btn_x, btn_y, btn_width, button_h - 4, btn_bg));
|
||||
|
||||
if (is_focused) {
|
||||
ctx.pushCommand(Command.rectOutline(btn_x, btn_y, btn_width, button_h - 4, Style.Color.rgb(200, 200, 200)));
|
||||
}
|
||||
|
||||
// Button text
|
||||
|
|
|
|||
|
|
@ -32,10 +32,6 @@ pub const PanelConfig = struct {
|
|||
collapsible: bool = false,
|
||||
/// Show close button (X)
|
||||
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
|
||||
|
|
@ -46,7 +42,6 @@ pub const PanelColors = struct {
|
|||
content_bg: Style.Color = Style.Color.rgb(35, 35, 40),
|
||||
border: Style.Color = Style.Color.rgb(70, 70, 75),
|
||||
border_focused: Style.Color = Style.Color.primary,
|
||||
shadow: Style.Color = Style.Color.rgba(0, 0, 0, 60),
|
||||
};
|
||||
|
||||
/// Panel result
|
||||
|
|
@ -109,28 +104,8 @@ pub fn panelRect(
|
|||
// Border color
|
||||
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
|
||||
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
|
||||
const title_bounds = Layout.Rect.init(
|
||||
|
|
|
|||
|
|
@ -40,8 +40,6 @@ pub const SelectConfig = struct {
|
|||
item_height: u32 = 24,
|
||||
/// Padding
|
||||
padding: u32 = 4,
|
||||
/// Corner radius (default 3 for fancy mode)
|
||||
corner_radius: u8 = 3,
|
||||
};
|
||||
|
||||
/// Select result
|
||||
|
|
@ -122,14 +120,9 @@ pub fn selectRect(
|
|||
|
||||
const border_color = if (has_focus or state.open) theme.primary else theme.border;
|
||||
|
||||
// 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.rectOutline(bounds.x, bounds.y, bounds.w, bounds.h, border_color));
|
||||
}
|
||||
// Draw main button background
|
||||
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));
|
||||
|
||||
// Draw selected text or placeholder
|
||||
const display_text = if (state.selectedIndex()) |idx|
|
||||
|
|
|
|||
|
|
@ -215,8 +215,6 @@ pub const TextInputConfig = struct {
|
|||
text_color: ?Style.Color = null,
|
||||
/// Override border color (for validation feedback). If null, uses theme default.
|
||||
border_color: ?Style.Color = null,
|
||||
/// Corner radius (default 3 for fancy mode)
|
||||
corner_radius: u8 = 3,
|
||||
};
|
||||
|
||||
/// Result of text input widget
|
||||
|
|
@ -286,16 +284,11 @@ pub fn textInputRect(
|
|||
const text_color = config.text_color orelse theme.input_fg;
|
||||
const placeholder_color = theme.secondary;
|
||||
|
||||
// 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.rectOutline(bounds.x, bounds.y, bounds.w, bounds.h, border_color));
|
||||
}
|
||||
// Draw background
|
||||
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));
|
||||
|
||||
// Inner area
|
||||
const inner = bounds.shrink(config.padding);
|
||||
|
|
|
|||
Loading…
Reference in a new issue