Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions skills/gpui-kit/references/gpui/focus-handle.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,28 @@ impl Searchable {
}
```

### 4. Restore Lost Focus

If the focused element is no longer rendered (a table swapped for an empty
state), key dispatch starts at the dispatch tree's root node, so the view's
`key_context` bindings and `on_action` handlers stop firing. Register one
`cx.on_focus_lost` listener per window, usually on the root view, and move
Focus to `window.focus_lost_restore_target(cx)`: the closest focusable
ancestor that is still rendered. Track the view's own handle on the element
that carries its `key_context`, so that element is the ancestor Focus returns
to.

```rust
let focus_lost = cx.on_focus_lost(window, |this, window, cx| {
let target = window
.focus_lost_restore_target(cx)
.unwrap_or_else(|| this.focus_handle.clone());
target.focus(window, cx);
});
// `this.focus_handle` is the view's tracked handle. Keep `focus_lost:
// Subscription` on the view; dropping it unregisters.
```

## Best Practices

### ✅ Track Focus on Interactive Elements
Expand Down
32 changes: 32 additions & 0 deletions website/docs/focus.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,37 @@ div()

GPUI Kit's `Dialog` and `Sheet` components provide their own modal focus behavior; use them for ordinary modal UI. The manual trap is useful for a custom surface whose entry, dismissal, and restoration lifecycle you explicitly own. An `on_focus_out` listener on a container can help observe Focus leaving; it is not a substitute for the modal lifecycle.

## When the focused element stops rendering

A view can stop rendering the element that holds Focus: a table is replaced by an empty state, a panel collapses, or a row is deleted. The window keeps the old `FocusHandle` as its Focus, but that handle has no node in the rendered dispatch tree. GPUI then dispatches key bindings and actions from the tree's root node. The view's `key_context` and `on_action` handlers are no longer on the path, so the view's shortcuts stop working. Only handlers on the root node and global `cx.on_action` handlers still run.

GPUI reports this case through `cx.on_focus_lost(window, ...)`. The listener runs when a newly drawn frame has no rendered path to Focus, although the previous frame had one. Inside the listener, `window.focus_lost_restore_target(cx)` returns the closest ancestor of the lost element that is still rendered and can take Focus. Register one listener per window, usually on the root view, and retain its `Subscription`:

```rust
struct Library {
focus_handle: FocusHandle,
_focus_lost: Subscription,
}

impl Library {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let focus_lost = cx.on_focus_lost(window, |this, window, cx| {
let target = window
.focus_lost_restore_target(cx)
.unwrap_or_else(|| this.focus_handle.clone());
target.focus(window, cx);
});

Self {
focus_handle: cx.focus_handle(),
_focus_lost: focus_lost,
}
}
}
```

Attach `focus_handle` with `.track_focus(&self.focus_handle)` to the element that carries the view's `key_context`. That element is then a focusable ancestor of the view's children, so when a child disappears it becomes the restore target and the view's bindings apply again. When the view removes the focused element on purpose, it can also move Focus in the same update instead of waiting for the listener.

## Verify and debug

Start with the `focus_trap` example to see pointer entry and both Tab directions in a running window. In an application test, render the real view, click its intended control, send Tab or the command key, and assert the resulting owner state. The [Testing](./test) guide covers Kit's UI integration test helpers; focus scopes need an explicit tracked handle for reliable inspection.
Expand All @@ -159,5 +190,6 @@ Start with the `focus_trap` example to see pointer entry and both Tab directions
| Panel appears inactive when a child is focused | Use `contains_focused`, and verify the child's tracked element is nested under the panel's tracked element. |
| Tab escapes a custom trap | Focus entered the trap first; the container is rendered through Kit's Base `Root`; children are actual Tab stops. |
| Focus disappears after closing an overlay | Save the previous target and restore an existing rendered target after dismissal. |
| Shortcuts stop working after a view hides its focused element | The focused element is no longer rendered, so dispatch starts at the root. Move Focus to a rendered element, or restore it from `cx.on_focus_lost`. |

Related guides: [Window](./window), [Action](./action), [KeyBinding](./keybinding), [Accessibility](./accessibility), and [Testing](./test).
32 changes: 32 additions & 0 deletions website/zh-CN/docs/focus.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,37 @@ div()

GPUI Kit 的 `Dialog` 和 `Sheet` 自带其模态焦点行为;普通模态界面优先使用它们。手动 trap 适用于你明确负责进入、关闭和恢复生命周期的自定义容器。容器上的 `on_focus_out` listener 可用于观察 Focus 离开,但不能代替完整模态流程。

## 聚焦元素不再渲染时

View 可能不再渲染持有 Focus 的元素:表格被空状态替换、面板折叠,或某一行被删除。窗口仍把旧的 `FocusHandle` 当作 Focus,但该句柄在已渲染的 dispatch tree 中已没有节点。此时 GPUI 从 dispatch tree 的根节点分发按键绑定和 action。View 的 `key_context` 与 `on_action` handler 不在这条路径上,View 的快捷键因此失效;只有根节点上的 handler 和全局 `cx.on_action` handler 仍会执行。

GPUI 用 `cx.on_focus_lost(window, ...)` 处理这种情况:当上一帧中 Focus 有已渲染路径、而新绘制的一帧中没有时,listener 会被调用。在 listener 内,`window.focus_lost_restore_target(cx)` 返回丢失元素最近的、仍在渲染且可获得 Focus 的祖先。每个窗口注册一个 listener,通常放在根 View 上,并保存返回的 `Subscription`:

```rust
struct Library {
focus_handle: FocusHandle,
_focus_lost: Subscription,
}

impl Library {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let focus_lost = cx.on_focus_lost(window, |this, window, cx| {
let target = window
.focus_lost_restore_target(cx)
.unwrap_or_else(|| this.focus_handle.clone());
target.focus(window, cx);
});

Self {
focus_handle: cx.focus_handle(),
_focus_lost: focus_lost,
}
}
}
```

在带有 View `key_context` 的元素上调用 `.track_focus(&self.focus_handle)`。该元素因此成为 View 子元素的可聚焦祖先:子元素消失时,它就是恢复目标,View 的绑定重新生效。若 View 有意移除聚焦元素,也可以在同一次更新中直接移动 Focus,不必等待 listener。

## 验证与排错

先运行 `focus_trap` 示例,观察鼠标进入以及两个方向的 Tab 行为。应用测试应渲染真实 View,点击目标控件,发送 Tab 或命令按键,并断言 owner 最终状态。[Testing](./test) 介绍 Kit 的 UI 集成测试辅助方法;焦点范围需要明确的已登记句柄,检查结果才可靠。
Expand All @@ -159,5 +190,6 @@ GPUI Kit 的 `Dialog` 和 `Sheet` 自带其模态焦点行为;普通模态界
| 子控件聚焦时面板显得不活动 | 使用 `contains_focused`,并确认子元素在面板的已登记元素内部。 |
| Tab 离开自定义 trap | Focus 已先进入 trap;容器通过 Kit 的 Base `Root` 渲染;子控件是真正的 Tab stop。 |
| 关闭弹层后 Focus 消失 | 保存先前目标,并在关闭后恢复当前仍存在的已渲染目标。 |
| View 隐藏聚焦元素后快捷键失效 | 聚焦元素已不再渲染,分发从根节点开始。把 Focus 移到已渲染元素,或在 `cx.on_focus_lost` 中恢复。 |

相关章节:[Window](./window)、[Action](./action)、[KeyBinding](./keybinding)、[Accessibility](./accessibility) 和 [Testing](./test)。
Loading