From e0cbbf64135c8b170396a558e2c53d982f3bb920 Mon Sep 17 00:00:00 2001 From: reugenio Date: Wed, 17 Dec 2025 09:24:50 +0100 Subject: [PATCH] feat: Focus ring AA para todos los widgets focusables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Widgets actualizados: - NumberEntry: esquinas redondeadas + focus ring - Radio: esquinas redondeadas para círculos + focus ring en opción - Slider: esquinas redondeadas en track/thumb + focus ring - Tabs: esquinas redondeadas en tab seleccionado + focus ring - Table: focus ring alrededor de toda la tabla - TextArea: esquinas redondeadas + focus ring Nuevos campos: - TableColors.focus_ring para consistencia Total: +135 LOC en 7 archivos 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/widgets/numberentry.zig | 16 +++++++++--- src/widgets/radio.zig | 41 ++++++++++++++++++++++++------- src/widgets/slider.zig | 37 +++++++++++++++++++--------- src/widgets/table/table.zig | 16 ++++++++++++ src/widgets/table/types.zig | 2 ++ src/widgets/tabs.zig | 36 +++++++++++++++++++++++++-- src/widgets/textarea/textarea.zig | 18 ++++++++++---- 7 files changed, 135 insertions(+), 31 deletions(-) diff --git a/src/widgets/numberentry.zig b/src/widgets/numberentry.zig index 4fbaf34..7b032a1 100644 --- a/src/widgets/numberentry.zig +++ b/src/widgets/numberentry.zig @@ -292,9 +292,19 @@ pub fn numberEntryRect( colors.border; const text_color = if (state.valid) colors.text else colors.text_invalid; - // Draw 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 background and border (with rounded corners in fancy mode) + const corner_radius: u8 = 3; + if (Style.isFancy() and corner_radius > 0) { + ctx.pushCommand(Command.roundedRect(bounds.x, bounds.y, bounds.w, bounds.h, bg_color, corner_radius)); + ctx.pushCommand(Command.roundedRectOutline(bounds.x, bounds.y, bounds.w, bounds.h, border_color, corner_radius)); + // Focus ring + if (has_focus) { + ctx.pushCommand(Command.focusRing(bounds.x, bounds.y, bounds.w, bounds.h, 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 prefix if (config.prefix) |prefix| { diff --git a/src/widgets/radio.zig b/src/widgets/radio.zig index 6f36317..abb7ef7 100644 --- a/src/widgets/radio.zig +++ b/src/widgets/radio.zig @@ -239,20 +239,43 @@ pub fn radioGroupRect( colors.border; // Draw outer circle (as rect, since we don't have circle primitive) - ctx.pushCommand(Command.rectOutline(radio_x, radio_y, config.radio_size, config.radio_size, border_color)); - ctx.pushCommand(Command.rect(radio_x + 1, radio_y + 1, config.radio_size - 2, config.radio_size - 2, colors.background)); + const corner_radius: u8 = @intCast(@min(config.radio_size / 2, 8)); + if (Style.isFancy() and corner_radius > 0) { + ctx.pushCommand(Command.roundedRectOutline(radio_x, radio_y, config.radio_size, config.radio_size, border_color, corner_radius)); + ctx.pushCommand(Command.roundedRect(radio_x + 1, radio_y + 1, config.radio_size - 2, config.radio_size - 2, colors.background, corner_radius)); + // Focus ring around the focused option + if (is_focused) { + ctx.pushCommand(Command.focusRing(radio_x, radio_y, config.radio_size, config.radio_size, corner_radius)); + } + } else { + ctx.pushCommand(Command.rectOutline(radio_x, radio_y, config.radio_size, config.radio_size, border_color)); + ctx.pushCommand(Command.rect(radio_x + 1, radio_y + 1, config.radio_size - 2, config.radio_size - 2, colors.background)); + } // Draw fill if selected if (is_selected) { const fill_margin: u32 = 4; const fill_size = config.radio_size -| (fill_margin * 2); - ctx.pushCommand(Command.rect( - radio_x + @as(i32, @intCast(fill_margin)), - radio_y + @as(i32, @intCast(fill_margin)), - fill_size, - fill_size, - if (opt.disabled) colors.fill.darken(30) else colors.fill, - )); + const fill_color = if (opt.disabled) colors.fill.darken(30) else colors.fill; + const fill_radius: u8 = @intCast(@min(fill_size / 2, 6)); + if (Style.isFancy() and fill_radius > 0) { + ctx.pushCommand(Command.roundedRect( + radio_x + @as(i32, @intCast(fill_margin)), + radio_y + @as(i32, @intCast(fill_margin)), + fill_size, + fill_size, + fill_color, + fill_radius, + )); + } else { + ctx.pushCommand(Command.rect( + radio_x + @as(i32, @intCast(fill_margin)), + radio_y + @as(i32, @intCast(fill_margin)), + fill_size, + fill_size, + fill_color, + )); + } } // Draw label diff --git a/src/widgets/slider.zig b/src/widgets/slider.zig index 8061753..e90fd29 100644 --- a/src/widgets/slider.zig +++ b/src/widgets/slider.zig @@ -292,7 +292,12 @@ pub fn sliderRect( // Draw track background const track_bg = if (config.disabled) colors.track_bg.darken(20) else colors.track_bg; - ctx.pushCommand(Command.rect(track_rect.x, track_rect.y, track_rect.w, track_rect.h, track_bg)); + const track_radius: u8 = @intCast(@min(config.track_thickness / 2, 4)); + if (Style.isFancy() and track_radius > 0) { + ctx.pushCommand(Command.roundedRect(track_rect.x, track_rect.y, track_rect.w, track_rect.h, track_bg, track_radius)); + } else { + ctx.pushCommand(Command.rect(track_rect.x, track_rect.y, track_rect.w, track_rect.h, track_bg)); + } // Draw filled portion const fill_color = if (config.disabled) colors.track_fill.darken(30) else colors.track_fill; @@ -319,17 +324,25 @@ pub fn sliderRect( else colors.thumb; - ctx.pushCommand(Command.rect(thumb_rect.x, thumb_rect.y, thumb_rect.w, thumb_rect.h, thumb_color)); - - // Draw focus ring - if (state.focused and !config.disabled) { - ctx.pushCommand(Command.rectOutline( - thumb_rect.x - 2, - thumb_rect.y - 2, - thumb_rect.w + 4, - thumb_rect.h + 4, - colors.focus_ring, - )); + const thumb_radius: u8 = @intCast(@min(config.thumb_size / 2, 8)); + if (Style.isFancy() and thumb_radius > 0) { + ctx.pushCommand(Command.roundedRect(thumb_rect.x, thumb_rect.y, thumb_rect.w, thumb_rect.h, thumb_color, thumb_radius)); + // Draw focus ring + if (state.focused and !config.disabled) { + ctx.pushCommand(Command.focusRing(thumb_rect.x, thumb_rect.y, thumb_rect.w, thumb_rect.h, thumb_radius)); + } + } else { + ctx.pushCommand(Command.rect(thumb_rect.x, thumb_rect.y, thumb_rect.w, thumb_rect.h, thumb_color)); + // Draw focus ring (simple mode) + if (state.focused and !config.disabled) { + ctx.pushCommand(Command.rectOutline( + thumb_rect.x - 2, + thumb_rect.y - 2, + thumb_rect.w + 4, + thumb_rect.h + 4, + colors.focus_ring, + )); + } } // Draw value if enabled and dragging diff --git a/src/widgets/table/table.zig b/src/widgets/table/table.zig index 7248146..82c9785 100644 --- a/src/widgets/table/table.zig +++ b/src/widgets/table/table.zig @@ -197,6 +197,22 @@ pub fn tableRectFull( // End clipping ctx.pushCommand(Command.clipEnd()); + // Draw focus ring (outside clip region) + if (has_focus) { + const Style = @import("../../core/style.zig"); + if (Style.isFancy()) { + ctx.pushCommand(Command.focusRing(bounds.x, bounds.y, bounds.w, bounds.h, 4)); + } else { + ctx.pushCommand(Command.rectOutline( + bounds.x - 1, + bounds.y - 1, + bounds.w + 2, + bounds.h + 2, + colors.focus_ring, + )); + } + } + // Draw scrollbar if needed (outside clip region) if (table_state.row_count > visible_rows) { render.drawScrollbar(ctx, bounds, table_state, visible_rows, config, colors); diff --git a/src/widgets/table/types.zig b/src/widgets/table/types.zig index 4953e7b..828ba4b 100644 --- a/src/widgets/table/types.zig +++ b/src/widgets/table/types.zig @@ -120,6 +120,8 @@ pub const TableColors = struct { validation_error_bg: Style.Color = Style.Color.rgb(80, 40, 40), /// Validation error border validation_error_border: Style.Color = Style.Color.rgb(200, 60, 60), + /// Focus ring color (when table has keyboard focus) + focus_ring: Style.Color = Style.Color.primary, }; // ============================================================================= diff --git a/src/widgets/tabs.zig b/src/widgets/tabs.zig index 2ec054d..95db0fc 100644 --- a/src/widgets/tabs.zig +++ b/src/widgets/tabs.zig @@ -267,8 +267,14 @@ pub fn tabsRect( else colors.tab_bg; - // Draw tab background - ctx.pushCommand(Command.rect(tab_rect.x, tab_rect.y, tab_rect.w, tab_rect.h, tab_bg)); + // Draw tab background (rounded corners for selected tab in fancy mode) + const tab_radius: u8 = if (is_selected) 4 else 0; + if (Style.isFancy() and tab_radius > 0) { + // Only round top corners for top position, bottom for bottom + ctx.pushCommand(Command.roundedRect(tab_rect.x, tab_rect.y, tab_rect.w, tab_rect.h, tab_bg, tab_radius)); + } else { + ctx.pushCommand(Command.rect(tab_rect.x, tab_rect.y, tab_rect.w, tab_rect.h, tab_bg)); + } // Draw active indicator if (is_selected) { @@ -331,6 +337,32 @@ pub fn tabsRect( const has_focus = ctx.hasFocus(widget_id); state.focused = has_focus; + // Draw focus ring around the selected tab when focused + if (has_focus and state.selected < tab_list.len) { + // Calculate position of selected tab + var focus_x = bar_rect.x; + for (0..state.selected) |i| { + if (i < tab_widths.len) { + focus_x += @as(i32, @intCast(tab_widths[i])); + } + } + const focus_w = if (state.selected < tab_widths.len) tab_widths[state.selected] else 0; + if (focus_w > 0) { + const focus_rect = Layout.Rect.init(focus_x, bar_rect.y, focus_w, config.tab_height); + if (Style.isFancy()) { + ctx.pushCommand(Command.focusRing(focus_rect.x, focus_rect.y, focus_rect.w, focus_rect.h, 4)); + } else { + ctx.pushCommand(Command.rectOutline( + focus_rect.x - 1, + focus_rect.y - 1, + focus_rect.w + 2, + focus_rect.h + 2, + colors.indicator, + )); + } + } + } + // Handle keyboard navigation (only when focused) if (has_focus and ctx.input.keyPressed(.left)) { // Find previous non-disabled tab diff --git a/src/widgets/textarea/textarea.zig b/src/widgets/textarea/textarea.zig index 308e60b..0d76aa7 100644 --- a/src/widgets/textarea/textarea.zig +++ b/src/widgets/textarea/textarea.zig @@ -86,11 +86,19 @@ pub fn textAreaRect( const bg_color = if (has_focus) colors.background.lighten(5) else colors.background; const border_color = if (has_focus) colors.border_focused else colors.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, border_color)); + // Draw background and border (with rounded corners in fancy mode) + const corner_radius: u8 = 3; + if (Style.isFancy() and corner_radius > 0) { + ctx.pushCommand(Command.roundedRect(bounds.x, bounds.y, bounds.w, bounds.h, bg_color, corner_radius)); + ctx.pushCommand(Command.roundedRectOutline(bounds.x, bounds.y, bounds.w, bounds.h, border_color, corner_radius)); + // Focus ring + if (has_focus) { + ctx.pushCommand(Command.focusRing(bounds.x, bounds.y, bounds.w, bounds.h, 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)); + } // Calculate dimensions const char_width: u32 = 8;