skillby longbridge

gpui-focus-handle

Focus management and keyboard navigation in GPUI. Use when handling focus, focus handles, or keyboard navigation.

Installs: 0
Used in: 1 repos
Updated: 9h ago
$npx ai-builder add skill longbridge/gpui-focus-handle

Installs to .claude/skills/gpui-focus-handle/

## Overview

FocusHandle in GPUI manages keyboard focus within the UI hierarchy. It provides a way to programmatically control which element receives keyboard input and coordinates with GPUI's action system to dispatch keyboard-driven commands. Focus handles create a focus tree that mirrors the UI element hierarchy, enabling predictable keyboard navigation and action routing.

## Core Concepts

### FocusHandle

A `FocusHandle` represents a focusable element in the UI tree:

```rust
// Create focus handle
let focus_handle = cx.focus_handle();

// Check focus state
let is_focused = focus_handle.is_focused(window);

// Set focus
focus_handle.focus(window);

// Get containing focus handle
let parent_handle = focus_handle.parent(window);
```

### Focus Tree

GPUI maintains a focus tree that mirrors the element hierarchy:

- **Root**: The window itself
- **Branches**: Container elements that can contain focusable children
- **Leaves**: Individual focusable elements (buttons, inputs, etc.)

```rust
// Focus tree structure
Window (root)
├── Container A
│   ├── Focusable Element 1
│   └── Focusable Element 2
└── Container B
    └── Focusable Element 3
```

## Focus Management

### Creating Focusable Elements

Elements become focusable by requesting a focus handle:

```rust
struct FocusableButton {
    focus_handle: FocusHandle,
    label: String,
}

impl FocusableButton {
    fn new(cx: &mut Context<Self>) -> Self {
        Self {
            focus_handle: cx.focus_handle(),
            label: "Click me".to_string(),
        }
    }
}

impl Render for FocusableButton {
    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        div()
            .track_focus(&self.focus_handle)
            .bg(if self.focus_handle.is_focused(cx.window) {
                rgb(0x007acc)
            } else {
                rgb(0xcccccc)
            })
            .child(&self.label)
    }
}
```

### Focus Navigation

#### Programmatic Focus Control

```rust
impl MyComponent {
    fn focus_next_element(&mut self, cx: &mut Context<Self>) {
        // Focus next sibling
        if let Some(next_focus) = self.focus_handle.next_focusable(cx.window) {
            next_focus.focus(cx.window);
        }
    }

    fn focus_previous_element(&mut self, cx: &mut Context<Self>) {
        // Focus previous sibling
        if let Some(prev_focus) = self.focus_handle.previous_focusable(cx.window) {
            prev_focus.focus(cx.window);
        }
    }
}
```

#### Keyboard Navigation

GPUI provides default keyboard navigation (Tab/Shift+Tab):

```rust
// Elements automatically participate in tab order
// unless explicitly configured otherwise

div()
    .track_focus(&focus_handle)
    .tab_index(0) // Default tab index
    .child("Focusable content")
```

### Focus Events

Respond to focus changes:

```rust
impl FocusableButton {
    fn new(cx: &mut Context<Self>) -> Self {
        let focus_handle = cx.focus_handle();

        // Observe focus changes
        cx.on_focus(&focus_handle, window, |this, cx| {
            // Gained focus
            this.on_focus_gained(cx);
        });

        cx.on_blur(&focus_handle, window, |this, cx| {
            // Lost focus
            this.on_focus_lost(cx);
        });

        Self {
            focus_handle,
            label: "Click me".to_string(),
        }
    }

    fn on_focus_gained(&mut self, cx: &mut Context<Self>) {
        self.is_focused = true;
        cx.notify();
    }

    fn on_focus_lost(&mut self, cx: &mut Context<Self>) {
        self.is_focused = false;
        cx.notify();
    }
}
```

## Relationship with Actions

### Action Dispatching

Focus handles route actions to focused elements:

```rust
// Dispatch action to focused element
focus_handle.dispatch_action(&MyAction::default(), window, cx);

// Global action dispatch (routes to focused element)
window.dispatch_action(MyAction.boxed_clone(), cx);
```

### Action Context

Actions receive focus context:

```rust
#[derive(Clone, PartialEq)]
struct MoveCursor {
    direction: Direction,
}

impl_actions!(editor, [MoveCursor]);

// Action handler receives focus information
fn move_cursor(action: &MoveCursor, window: &mut Window, cx: &mut App) {
    // Action dispatched to focused element
    if let Some(focused) = window.focused_element() {
        // Handle action based on focused element
    }
}
```

### Focus-Aware Actions

Actions can be conditional based on focus:

```rust
impl MyComponent {
    fn handle_action(&mut self, action: &MyAction, window: &mut Window, cx: &mut Context<Self>) {
        // Only handle if this element is focused
        if self.focus_handle.is_focused(window) {
            // Handle action
            match action {
                MyAction::Activate => self.activate(cx),
                MyAction::Deactivate => self.deactivate(cx),
            }
        }
    }
}
```

### Action Registration

Register action handlers on elements:

```rust
impl Render for MyComponent {
    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        div()
            .track_focus(&self.focus_handle)
            .on_action(cx.listener(Self::handle_action))
            .child("Focusable element")
    }
}
```

## Focus Scopes

### Modal Focus

Create focus scopes for modal dialogs:

```rust
struct Modal {
    focus_handle: FocusHandle,
    content: Entity<ModalContent>,
}

impl Modal {
    fn new(cx: &mut Context<Self>) -> Self {
        let focus_handle = cx.focus_handle();

        // Create focus scope
        focus_handle.set_focus_scope(window, cx);

        Self {
            focus_handle,
            content: cx.new(|_| ModalContent::new()),
        }
    }
}

impl Render for Modal {
    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        div()
            .track_focus(&self.focus_handle)
            .on_action(cx.listener(Self::handle_escape))
            .child(self.content.clone())
    }
}
```

### Focus Trapping

Prevent focus from escaping a component:

```rust
impl Modal {
    fn handle_tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
        // Keep focus within modal
        let focusables = self.focus_handle.focusable_children(window);
        if focusables.is_empty() {
            return;
        }

        let current_index = focusables
            .iter()
            .position(|fh| fh.is_focused(window))
            .unwrap_or(0);

        let next_index = if window.modifiers().shift {
            // Shift+Tab: previous
            if current_index == 0 {
                focusables.len() - 1
            } else {
                current_index - 1
            }
        } else {
            // Tab: next
            (current_index + 1) % focusables.len()
        };

        focusables[next_index].focus(window);
    }
}
```

## Best Practices

### Focus Indicator Visibility

Always provide visible focus indicators:

```rust
div()
    .track_focus(&focus_handle)
    .when(focus_handle.is_focused(cx.window), |el| {
        el.border_2()
            .border_color(rgb(0x007acc))
            .outline_2()
            .outline_color(rgb(0x007acc))
    })
    .child("Focusable content")
```

### Logical Tab Order

Ensure tab order matches visual layout:

```rust
// Use tab_index for custom ordering
div()
    .track_focus(&focus_handle)
    .tab_index(1)
    .child("First in tab order")

div()
    .track_focus(&other_handle)
    .tab_index(2)
    .child("Second in tab order")
```

### Focus Management in Lists

```rust
struct ListItem {
    focus_handle: FocusHandle,
    index: usize,
}

impl ListItem {
    fn handle_arrow_keys(&mut self, action: &ArrowKey, window: &mut Window, cx: &mut Context<Self>) {
        match action.direction {
            Direction::Up => {
                if self.index > 0 {
                    // Focus previous item
                    self.parent_list.focus_item(self.index - 1, window);
                }
            }
            Direction::Down => {
                // Focus next item
                self.parent_list.focus_item(self.index + 1, window);
            }
            _ => {}
        }
    }
}
```

### Testing Focus Behavior

```rust
#[cfg(test)]
impl MyComponent {
    fn test_focus_navigation(&mut self, cx: &mut TestAppContext) {
        // Set initial focus
        self.focus_handle.focus(cx.window);

        // Simulate tab
        cx.dispatch_action(Tab, cx.window);

        // Assert focus moved to expected element
        assert!(self.next_element.focus_handle.is_focused(cx.window));
    }
}
```

### Accessibility Considerations

- Provide clear focus indicators
- Ensure keyboard navigation works without mouse
- Test with screen readers
- Avoid focus traps unless intentional (modals)
- Maintain logical tab order

### Performance Considerations

- Focus handles are lightweight
- Focus queries are fast
- Avoid excessive focus observers
- Cache focus state when possible

FocusHandle coordinates with GPUI's action system to create a cohesive keyboard-driven interface. Proper focus management ensures accessibility and provides users with predictable keyboard navigation patterns.</content>
<parameter name="filePath">.claude/skills/focus-handle/SKILL.md

Quick Install

$npx ai-builder add skill longbridge/gpui-focus-handle

Details

Type
skill
Slug
longbridge/gpui-focus-handle
Created
9h ago