feat: Paridad visual DVUI - RenderMode dual (simple/fancy)

Sistema de rendering dual para zcatgui:

Core:
- RenderMode enum (simple/fancy) en style.zig
- global_render_mode con helpers: isFancy(), setRenderMode()
- fillRoundedRect con edge-fade AA en framebuffer.zig (~350 LOC)
- Nuevos comandos: rounded_rect, rounded_rect_outline

Widgets actualizados:
- Button: corner_radius=4, usa roundedRect en fancy mode
- Panel: corner_radius=6, show_shadow=true, sombra offset 4px
- TextInput: corner_radius=3
- Select: corner_radius=3
- Modal: corner_radius=8, show_shadow=true, sombra offset 6px
  - Botones y input field del modal también redondeados

Técnica edge-fade (de DVUI):
- Anti-aliasing por gradiente alfa en bordes
- Sin supersampling, mínimo impacto en rendimiento
- Bordes suaves sin multisampling

+589 líneas, 9 archivos modificados

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
reugenio 2025-12-17 01:02:46 +01:00
parent 6bf1eb1eb8
commit 364a7d963f
9 changed files with 589 additions and 32 deletions

View file

@ -12,6 +12,9 @@ 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,
@ -21,6 +24,9 @@ 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,
@ -69,6 +75,29 @@ 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,
@ -139,6 +168,46 @@ 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
// =============================================================================

View file

@ -4,6 +4,39 @@
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,

View file

@ -217,6 +217,360 @@ 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;
}
};
// =============================================================================

View file

@ -86,9 +86,11 @@ 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 => {},
@ -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 {
if (self.clip_depth >= self.clip_stack.len) return;
@ -313,6 +326,7 @@ 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
@ -334,6 +348,7 @@ 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,
};
}

View file

@ -29,6 +29,8 @@ 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
@ -78,11 +80,16 @@ pub fn buttonRect(ctx: *Context, bounds: Layout.Rect, text: []const u8, config:
else
theme.button_fg;
// 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 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 text centered
const char_width: u32 = 8;

View file

@ -111,6 +111,10 @@ 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
@ -127,6 +131,8 @@ 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
@ -187,19 +193,40 @@ pub fn modalEx(
// Draw backdrop (semi-transparent overlay)
ctx.pushCommand(Command.rect(0, 0, screen_w, screen_h, colors.backdrop));
// Draw dialog border
ctx.pushCommand(Command.rectOutline(
dialog_x - 1,
dialog_y - 1,
dialog_w + 2,
dialog_h + 2,
colors.border,
));
// Check render mode for fancy features
const fancy = Style.isFancy() and config.corner_radius > 0;
// Draw dialog background
ctx.pushCommand(Command.rect(dialog_x, dialog_y, dialog_w, dialog_h, colors.background));
// 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 title bar
// 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)
ctx.pushCommand(Command.rect(dialog_x, dialog_y, dialog_w, title_h, colors.title_bg));
// Draw title text
@ -223,10 +250,16 @@ pub fn modalEx(
24,
);
// Simple input rendering
// Input rendering
const input_bg = Style.Color.rgb(35, 35, 40);
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 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));
}
const txt = input_st.text();
if (txt.len > 0) {
@ -264,10 +297,17 @@ pub fn modalEx(
.danger => Style.Color.danger.darken(30),
};
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)));
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)));
}
}
// Button text

View file

@ -32,6 +32,10 @@ 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
@ -42,6 +46,7 @@ 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
@ -104,8 +109,28 @@ 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
ctx.pushCommand(Command.rectOutline(bounds.x, bounds.y, bounds.w, bounds.h, border_color));
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));
}
// Title bar bounds
const title_bounds = Layout.Rect.init(

View file

@ -40,6 +40,8 @@ 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
@ -120,9 +122,14 @@ pub fn selectRect(
const border_color = if (has_focus or state.open) theme.primary else theme.border;
// 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 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 selected text or placeholder
const display_text = if (state.selectedIndex()) |idx|

View file

@ -215,6 +215,8 @@ 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
@ -284,11 +286,16 @@ pub fn textInputRect(
const text_color = config.text_color orelse theme.input_fg;
const placeholder_color = theme.secondary;
// 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));
// 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));
}
// Inner area
const inner = bounds.shrink(config.padding);