Kit · K02: Tab binds under FocusScope's ElyFocus context, so a deeper context's Tab wins
This commit is contained in:
1 parent
c61a5b22b0
commit
a9b30c7eab
7 files changed
+58
-11
No files matched your search
@@ -54,7 +54,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
|
||||
- `svg()` paints only with its own `text_color`. `Icon` always sets one.
|
||||
- Buttons stay out of focus on click (`prevent_default` on mouse down). Tab still reaches them.
|
||||
- The focus ring is a 1px focus-colored border (`FocusRing`). gpui paints shadows under the element, so a shadow ring floods transparent elements. Focusable elements keep a 1px border, transparent at rest.
|
||||
- `ely_gpui_component::init` binds Tab and Shift-Tab to `FocusNext` and `FocusPrev`. Wrap the root in `FocusScope` with `.root()` and focus its handle. A root scope takes focus when the focused element leaves the tree, as a toast's or an alert's button does.
|
||||
- `ely_gpui_component::init` binds Tab and Shift-Tab to `FocusNext` and `FocusPrev` under `ElyFocus`, the key context every `FocusScope` sets. gpui ranks a binding with no context at the deepest level, so a host's Tab in a deeper context, GPUI Kit's code editor among them, lost or won by which `init` ran last; under a context, the deeper one wins. Wrap the root in `FocusScope` with `.root()` and focus its handle. A root scope takes focus when the focused element leaves the tree, as a toast's or an alert's button does.
|
||||
- The gallery scripts mouse input by posting `NSEvent`s to its own queue, so a press takes AppKit's path and reaches a native view over the window (`misc::WebView`). `Window::dispatch_event` reaches only gpui's listeners. gpui's normal window takes mouse moves from a tracking area, which only a real cursor feeds, so the harness sets the window's `acceptsMouseMovedEvents` for posted moves.
|
||||
- gpui starts a drag on the first move past its threshold, and only the moves after it reach `on_drag_move`. gpui 0.2.2's mac window replayed a held drag every 16 ms; gpui_macos replays only while `pressedMouseButtons` shows a real button, which posted events never set. So `Step::DragTo` posts each move twice, as the replay did: the second post of the first step reaches `on_drag_move`, where a component that anchors on its first drag move sets its anchor (Scrollbar, Knob, Split).
|
||||
- Every shot of a gpui window, a page's or a script's, waits three of its frames through `on_next_frame`: gpui presents without waiting on the GPU, and a heavy page's draw outlasts a fixed pause, so a shot could show the frame before the last step. A page's pauses stay, for work off the main thread such as pictures decoding. A covered window draws no frames, so its capture fails after 5 s instead of shooting a stale frame.
|
||||
|
||||
@@ -4,7 +4,7 @@ Every Ely control that takes a press is a Tab stop. Focus moves with Tab and Shi
|
||||
|
||||
## What `init` binds
|
||||
|
||||
`init` binds `tab` to the `FocusNext` action and `shift-tab` to `FocusPrev`, with no key context, so they reach every window. A `FocusScope` turns the two actions into focus moves.
|
||||
`init` binds `tab` to the `FocusNext` action and `shift-tab` to `FocusPrev`, under `ElyFocus`, the key context every `FocusScope` sets. A `FocusScope` turns the two actions into focus moves. A context nested deeper keeps its own Tab, as a code editor that indents does, whichever library's `init` ran last.
|
||||
|
||||
## The root scope
|
||||
|
||||
|
||||
+3
-3
@@ -50,7 +50,7 @@ pub use assets::Assets;
|
||||
use anyhow::Context as _;
|
||||
use gpui::{App, KeyBinding};
|
||||
|
||||
use crate::primitives::{FocusNext, FocusPrev, IconName};
|
||||
use crate::primitives::{FOCUS_CONTEXT, FocusNext, FocusPrev, IconName};
|
||||
|
||||
/// Loads fonts and the theme, binds Tab and text keys. Call once, first.
|
||||
pub fn init(cx: &mut App) -> anyhow::Result<()> {
|
||||
@@ -76,8 +76,8 @@ pub fn init_for_tests(cx: &mut App) {
|
||||
fn setup(cx: &mut App) {
|
||||
theme::Theme::init(cx);
|
||||
cx.bind_keys([
|
||||
KeyBinding::new("tab", FocusNext, None),
|
||||
KeyBinding::new("shift-tab", FocusPrev, None),
|
||||
KeyBinding::new("tab", FocusNext, Some(FOCUS_CONTEXT)),
|
||||
KeyBinding::new("shift-tab", FocusPrev, Some(FOCUS_CONTEXT)),
|
||||
]);
|
||||
forms::bind_keys(cx);
|
||||
editor::bind_keys(cx);
|
||||
|
||||
@@ -8,6 +8,9 @@ use crate::theme::ActiveTheme;
|
||||
|
||||
actions!(ely, [FocusNext, FocusPrev]);
|
||||
|
||||
/// The key context of every `FocusScope`; Tab and Shift-Tab bind under it.
|
||||
pub(crate) const CONTEXT: &str = "ElyFocus";
|
||||
|
||||
/// Focus-colored border while focused. Give the element a 1px border.
|
||||
pub trait FocusRing: InteractiveElement + Sized {
|
||||
fn focus_ring(self, cx: &App) -> Self {
|
||||
@@ -30,7 +33,7 @@ pub struct FocusScope {
|
||||
impl FocusScope {
|
||||
pub fn new(handle: &FocusHandle) -> Self {
|
||||
Self {
|
||||
base: div().track_focus(handle),
|
||||
base: div().key_context(CONTEXT).track_focus(handle),
|
||||
handle: handle.clone(),
|
||||
trap: false,
|
||||
root: false,
|
||||
|
||||
@@ -20,8 +20,10 @@ pub use disclosure::Disclosure;
|
||||
pub use divider::Divider;
|
||||
pub use files::IconTheme;
|
||||
pub(crate) use files::{file_icon, icon_theme};
|
||||
pub(crate) use focus::{
|
||||
CONTEXT as FOCUS_CONTEXT, Takeover, give_back, hand_back, hold_focus, tab_stop, take_focus,
|
||||
};
|
||||
pub use focus::{FocusNext, FocusPrev, FocusRing, FocusScope};
|
||||
pub(crate) use focus::{Takeover, give_back, hand_back, hold_focus, tab_stop, take_focus};
|
||||
pub use ghost::DragGhost;
|
||||
pub use icon::{Icon, IconName};
|
||||
pub use image::Image;
|
||||
|
||||
+44
-3
@@ -1,11 +1,11 @@
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use gpui::{
|
||||
App, Context, Entity, FocusHandle, IntoElement, Modifiers, ParentElement, Render, RenderOnce,
|
||||
Styled, TestAppContext, Window, anchored, div, point, prelude::*, px,
|
||||
App, Context, Entity, FocusHandle, IntoElement, KeyBinding, Modifiers, ParentElement, Render,
|
||||
RenderOnce, Styled, TestAppContext, Window, actions, anchored, div, point, prelude::*, px,
|
||||
};
|
||||
|
||||
use super::raise;
|
||||
use super::{FocusScope, raise};
|
||||
use crate::theme::Theme;
|
||||
|
||||
/// A raised square holding a raised button, under a cover drawn after it in the same layer.
|
||||
@@ -130,3 +130,44 @@ fn a_raise_keeps_its_state_while_another_comes_before_it(cx: &mut TestAppContext
|
||||
"the box kept focus"
|
||||
);
|
||||
}
|
||||
|
||||
actions!(host, [Indent]);
|
||||
|
||||
/// A root scope around a field whose own context binds Tab.
|
||||
struct Field {
|
||||
root: FocusHandle,
|
||||
field: FocusHandle,
|
||||
indents: usize,
|
||||
}
|
||||
|
||||
impl Render for Field {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let view = cx.entity();
|
||||
FocusScope::new(&self.root).root().size_full().child(
|
||||
div()
|
||||
.key_context("HostField")
|
||||
.track_focus(&self.field)
|
||||
.size(px(100.0))
|
||||
.on_action(move |_: &Indent, _, cx| view.update(cx, |field, _| field.indents += 1)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_deeper_context_keeps_its_own_tab_whatever_init_ran_last(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
cx.bind_keys([KeyBinding::new("tab", Indent, Some("HostField"))]);
|
||||
crate::init_for_tests(cx);
|
||||
});
|
||||
let (view, cx) = cx.add_window_view(|_, cx| Field {
|
||||
root: cx.focus_handle(),
|
||||
field: cx.focus_handle(),
|
||||
indents: 0,
|
||||
});
|
||||
cx.update(|window, cx| {
|
||||
let field = view.read(cx).field.clone();
|
||||
window.focus(&field, cx);
|
||||
});
|
||||
cx.simulate_keystrokes("tab");
|
||||
assert_eq!(view.read_with(cx, |field, _| field.indents), 1);
|
||||
}
|
||||
+2
-1
@@ -14,7 +14,7 @@ Rules: the work order follows dependencies, since the PRD has no chapter for it.
|
||||
## Items
|
||||
|
||||
- [x] K01 gpui from `gpui-pre =0.3.8`, Kit's pin: crate, gallery, web build, docs, CI
|
||||
- [ ] K02 Tab and Shift-Tab bound under `FocusScope`'s own context, so a deeper context's Tab wins whatever the order of `init`
|
||||
- [x] K02 Tab and Shift-Tab bound under `FocusScope`'s own context, so a deeper context's Tab wins whatever the order of `init`
|
||||
- [ ] K03 Assets: `init` checks every embedded file through the app's source; one source serves Ely's files and another's
|
||||
- [ ] K04 `compat/kit`: a crate on the newest Kit that runs both libraries in one window, in `scripts/check.sh` and CI
|
||||
- [ ] K05 `gpui-fast`: Ely builds and tests on Kit's other engine, or the item records why not
|
||||
@@ -25,3 +25,4 @@ Rules: the work order follows dependencies, since the PRD has no chapter for it.
|
||||
|
||||
One line per item: what changed, what stayed and why, review rounds.
|
||||
- K01: gpui, gpui_platform and gpui_web come from `gpui-pre` `=0.3.8`; the lock holds no git source and one copy of each. `gpui-pre` split leak detection from `test-support`, so this crate's `test-support` turns on `gpui/leak-detection`, and a test that leaks on purpose proves it. The check, the wasm check, the web build (20.0 MB), a headless Chromium shot and the site build pass. README and the install guide name the snapshot. Review: 2 rounds.
|
||||
- K02: `FocusScope` sets the key context `ElyFocus`, and `init` binds Tab and Shift-Tab under it. A host's Tab in a deeper context wins whatever the order of `init` (a test binds the host's first, the order that lost: 0 indents before, 1 after); Ely's terminal, editor, traps and the inspector's scope behave as before. Review: 1 round.
|
||||
Reference in new issue
Block a user