feat: Focus ring AA para todos los widgets focusables

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 <noreply@anthropic.com>
This commit is contained in:
reugenio 2025-12-17 09:24:50 +01:00
parent ed0e3e8e5b
commit e0cbbf6413
7 changed files with 135 additions and 31 deletions

View file

@ -292,9 +292,19 @@ pub fn numberEntryRect(
colors.border; colors.border;
const text_color = if (state.valid) colors.text else colors.text_invalid; const text_color = if (state.valid) colors.text else colors.text_invalid;
// Draw background // Draw background and border (with rounded corners in fancy mode)
ctx.pushCommand(Command.rect(bounds.x, bounds.y, bounds.w, bounds.h, bg_color)); const corner_radius: u8 = 3;
ctx.pushCommand(Command.rectOutline(bounds.x, bounds.y, bounds.w, bounds.h, border_color)); 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 // Draw prefix
if (config.prefix) |prefix| { if (config.prefix) |prefix| {

View file

@ -239,20 +239,43 @@ pub fn radioGroupRect(
colors.border; colors.border;
// Draw outer circle (as rect, since we don't have circle primitive) // 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)); const corner_radius: u8 = @intCast(@min(config.radio_size / 2, 8));
ctx.pushCommand(Command.rect(radio_x + 1, radio_y + 1, config.radio_size - 2, config.radio_size - 2, colors.background)); 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 // Draw fill if selected
if (is_selected) { if (is_selected) {
const fill_margin: u32 = 4; const fill_margin: u32 = 4;
const fill_size = config.radio_size -| (fill_margin * 2); const fill_size = config.radio_size -| (fill_margin * 2);
ctx.pushCommand(Command.rect( const fill_color = if (opt.disabled) colors.fill.darken(30) else colors.fill;
radio_x + @as(i32, @intCast(fill_margin)), const fill_radius: u8 = @intCast(@min(fill_size / 2, 6));
radio_y + @as(i32, @intCast(fill_margin)), if (Style.isFancy() and fill_radius > 0) {
fill_size, ctx.pushCommand(Command.roundedRect(
fill_size, radio_x + @as(i32, @intCast(fill_margin)),
if (opt.disabled) colors.fill.darken(30) else colors.fill, 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 // Draw label

View file

@ -292,7 +292,12 @@ pub fn sliderRect(
// Draw track background // Draw track background
const track_bg = if (config.disabled) colors.track_bg.darken(20) else colors.track_bg; 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 // Draw filled portion
const fill_color = if (config.disabled) colors.track_fill.darken(30) else colors.track_fill; const fill_color = if (config.disabled) colors.track_fill.darken(30) else colors.track_fill;
@ -319,17 +324,25 @@ pub fn sliderRect(
else else
colors.thumb; colors.thumb;
ctx.pushCommand(Command.rect(thumb_rect.x, thumb_rect.y, thumb_rect.w, thumb_rect.h, thumb_color)); const thumb_radius: u8 = @intCast(@min(config.thumb_size / 2, 8));
if (Style.isFancy() and thumb_radius > 0) {
// Draw focus ring ctx.pushCommand(Command.roundedRect(thumb_rect.x, thumb_rect.y, thumb_rect.w, thumb_rect.h, thumb_color, thumb_radius));
if (state.focused and !config.disabled) { // Draw focus ring
ctx.pushCommand(Command.rectOutline( if (state.focused and !config.disabled) {
thumb_rect.x - 2, ctx.pushCommand(Command.focusRing(thumb_rect.x, thumb_rect.y, thumb_rect.w, thumb_rect.h, thumb_radius));
thumb_rect.y - 2, }
thumb_rect.w + 4, } else {
thumb_rect.h + 4, ctx.pushCommand(Command.rect(thumb_rect.x, thumb_rect.y, thumb_rect.w, thumb_rect.h, thumb_color));
colors.focus_ring, // 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 // Draw value if enabled and dragging

View file

@ -197,6 +197,22 @@ pub fn tableRectFull(
// End clipping // End clipping
ctx.pushCommand(Command.clipEnd()); 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) // Draw scrollbar if needed (outside clip region)
if (table_state.row_count > visible_rows) { if (table_state.row_count > visible_rows) {
render.drawScrollbar(ctx, bounds, table_state, visible_rows, config, colors); render.drawScrollbar(ctx, bounds, table_state, visible_rows, config, colors);

View file

@ -120,6 +120,8 @@ pub const TableColors = struct {
validation_error_bg: Style.Color = Style.Color.rgb(80, 40, 40), validation_error_bg: Style.Color = Style.Color.rgb(80, 40, 40),
/// Validation error border /// Validation error border
validation_error_border: Style.Color = Style.Color.rgb(200, 60, 60), 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,
}; };
// ============================================================================= // =============================================================================

View file

@ -267,8 +267,14 @@ pub fn tabsRect(
else else
colors.tab_bg; colors.tab_bg;
// Draw tab background // Draw tab background (rounded corners for selected tab in fancy mode)
ctx.pushCommand(Command.rect(tab_rect.x, tab_rect.y, tab_rect.w, tab_rect.h, tab_bg)); 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 // Draw active indicator
if (is_selected) { if (is_selected) {
@ -331,6 +337,32 @@ pub fn tabsRect(
const has_focus = ctx.hasFocus(widget_id); const has_focus = ctx.hasFocus(widget_id);
state.focused = has_focus; 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) // Handle keyboard navigation (only when focused)
if (has_focus and ctx.input.keyPressed(.left)) { if (has_focus and ctx.input.keyPressed(.left)) {
// Find previous non-disabled tab // Find previous non-disabled tab

View file

@ -86,11 +86,19 @@ pub fn textAreaRect(
const bg_color = if (has_focus) colors.background.lighten(5) else colors.background; 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; const border_color = if (has_focus) colors.border_focused else colors.border;
// Draw background // Draw background and border (with rounded corners in fancy mode)
ctx.pushCommand(Command.rect(bounds.x, bounds.y, bounds.w, bounds.h, bg_color)); const corner_radius: u8 = 3;
if (Style.isFancy() and corner_radius > 0) {
// Draw border ctx.pushCommand(Command.roundedRect(bounds.x, bounds.y, bounds.w, bounds.h, bg_color, corner_radius));
ctx.pushCommand(Command.rectOutline(bounds.x, bounds.y, bounds.w, bounds.h, border_color)); 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 // Calculate dimensions
const char_width: u32 = 8; const char_width: u32 = 8;