mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-05 18:20:09 +00:00
docking: focus only follows the user's input
This commit is contained in:
1 parent
a014337eb2
commit
13be52fbaa
6 files changed
+85
-12
No files matched your search
@@ -588,3 +588,40 @@ fn a_short_docked_panel_floats_at_its_content_height_up_to_its_floating_size() {
|
||||
let Some(Target::Float { rect: Some([_, _, _, height]) }) = moved_group(&out.actions) else { panic!("{:?}", out.actions) };
|
||||
assert_eq!(*height, title + strip + 150.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_focus_follows_only_the_users_input() {
|
||||
let mut d = Driver::new(docked());
|
||||
let tab = Id::new(AREA).with(("tab", &4u32));
|
||||
// Keyboard focus as Tab gives it.
|
||||
d.ctx.memory_mut(|m| m.request_focus(tab));
|
||||
d.step(vec![]);
|
||||
assert!(d.ctx.memory(|m| m.has_focus(tab)));
|
||||
|
||||
// A reset that moves the panel takes its focus away and reports no new focus.
|
||||
let mut reset = docked();
|
||||
let Some(Node::Split { first, .. }) = &mut reset.root else { panic!() };
|
||||
let Node::Dock { columns, .. } = first.as_mut() else { panic!() };
|
||||
columns[0].groups.reverse();
|
||||
d.layout = reset;
|
||||
let out = d.step(vec![]);
|
||||
assert_eq!(out.focused, None);
|
||||
d.step(vec![]);
|
||||
assert_eq!(d.ctx.memory(|m| m.focused()), None, "the moved tab gives up keyboard focus");
|
||||
|
||||
// A user drag keeps the dragged tab's focus where the user put it.
|
||||
let mut d = Driver::new(docked());
|
||||
d.ctx.memory_mut(|m| m.request_focus(tab));
|
||||
d.drag(d.tab(4).center(), d.tab(2).center());
|
||||
d.step(vec![]);
|
||||
assert!(d.layout.place(&4) != docked().place(&4));
|
||||
assert!(d.ctx.memory(|m| m.has_focus(tab)), "a tab moved by the user keeps focus");
|
||||
|
||||
// A closed panel's tab gives focus up too.
|
||||
let out = d.step(vec![]);
|
||||
assert!(out.actions.is_empty());
|
||||
d.layout.apply(Action::Close { panel: 4 }).unwrap();
|
||||
d.step(vec![]);
|
||||
d.step(vec![]);
|
||||
assert_eq!(d.ctx.memory(|m| m.focused()), None);
|
||||
}
|
||||
@@ -66,8 +66,6 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
let id = self.id.with(("floating", first));
|
||||
if self.activation.as_ref().is_some_and(|panel| window.contains(panel)) {
|
||||
ctx.move_to_top(FloatingFrame::layer(id));
|
||||
ctx.memory_mut(|memory| memory.request_focus(self.id.with(("tab", self.activation.as_ref().unwrap_or(first)))));
|
||||
self.output.focused = self.activation.clone();
|
||||
}
|
||||
let movable = window.panels().all(|panel| self.permissions(panel).movable);
|
||||
let style = ChromeStyle::from_dock(self.style);
|
||||
|
||||
@@ -79,7 +79,6 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
.with_min_inner_size(MIN_SIZE);
|
||||
if self.activation.as_ref().is_some_and(|panel| window.contains(panel)) {
|
||||
ctx.send_viewport_cmd_to(viewport, ViewportCommand::Focus);
|
||||
self.output.focused = self.activation.clone();
|
||||
}
|
||||
let (targets, stacks) = (self.targets.len(), self.frame.stacks.len());
|
||||
let (closed, placed) = ctx.show_viewport_immediate(viewport, builder, |ui, class| {
|
||||
|
||||
@@ -201,8 +201,9 @@ impl DockStyle {
|
||||
pub struct DockOutput<P> {
|
||||
pub actions: Vec<Action<P>>,
|
||||
pub error: Option<DockError>,
|
||||
/// The panel the user focused this frame: a clicked tab or menu entry, a drop, a raised
|
||||
/// window, or a press of any pointer button in a panel body, stripless panels included.
|
||||
/// The panel the user focused this frame: a clicked tab or menu entry, a drop, or a press
|
||||
/// of any pointer button in a panel body, stripless panels included. Only user input sets
|
||||
/// it; [`DockArea::raise_panel`] and host commands do not.
|
||||
pub focused: Option<P>,
|
||||
/// Whole-floating-group gesture identity; host may display its own group drop preview.
|
||||
pub group_drag: Option<P>,
|
||||
@@ -309,6 +310,37 @@ fn float_space_key(area: Id) -> Id {
|
||||
area.with("float-space")
|
||||
}
|
||||
|
||||
/// The panel whose tab held keyboard focus at the end of the last frame, where it was, and
|
||||
/// whether that frame's own actions may have moved it.
|
||||
type TabFocus<P> = (P, super::dock_model::Place, bool);
|
||||
|
||||
fn tab_focus_key(area: Id) -> Id {
|
||||
area.with("tab-focus")
|
||||
}
|
||||
|
||||
/// Keyboard focus stays on a tab only while its panel stays where the user put it: a panel
|
||||
/// hidden, closed or moved by the host (a command or a workspace reset) gives focus up.
|
||||
fn release_moved_tab_focus<P: Clone + Eq + Hash + Debug + Send + Sync + 'static>(ctx: &Context, area: Id, layout: &Layout<P>) {
|
||||
let Some((panel, place, moved_by_area)) = ctx.data(|d| d.get_temp::<TabFocus<P>>(tab_focus_key(area))) else { return };
|
||||
let tab = area.with(("tab", &panel));
|
||||
let place_now = layout.place(&panel);
|
||||
if ctx.memory(|m| m.has_focus(tab)) && (place_now.is_none() || (place_now != Some(place) && !moved_by_area)) {
|
||||
ctx.memory_mut(|m| m.surrender_focus(tab));
|
||||
}
|
||||
}
|
||||
|
||||
fn record_tab_focus<P: Clone + Eq + Hash + Debug + Send + Sync + 'static>(ctx: &Context, area: Id, layout: &Layout<P>, acted: bool) {
|
||||
let focused = ctx.memory(|m| m.focused());
|
||||
let tab = focused.and_then(|id| layout.panels().into_iter().find(|panel| area.with(("tab", *panel)) == id));
|
||||
let record = tab.and_then(|panel| Some((panel.clone(), layout.place(panel)?, acted)));
|
||||
ctx.data_mut(|d| match record {
|
||||
Some(record) => {
|
||||
d.insert_temp(tab_focus_key(area), record);
|
||||
}
|
||||
None => d.remove::<TabFocus<P>>(tab_focus_key(area)),
|
||||
});
|
||||
}
|
||||
|
||||
/// A request for host state outside the layout.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
@@ -692,8 +724,9 @@ impl<'a, P: Clone + Eq + Hash + Debug + Send + Sync + 'static> DockArea<'a, P> {
|
||||
false
|
||||
}
|
||||
|
||||
/// Raise and focus a panel's owning floating area on this frame. Apply Activate through
|
||||
/// the application's command path as well to select its tab.
|
||||
/// Raise a panel's floating window (or open its flyout) on this frame. Apply Activate
|
||||
/// through the application's command path as well to select its tab. A raise is not user
|
||||
/// input: it moves no keyboard focus and is not reported as [`DockOutput::focused`].
|
||||
pub fn raise_panel(mut self, panel: P) -> Self {
|
||||
self.activation = Some(panel);
|
||||
self
|
||||
@@ -764,6 +797,7 @@ impl<'a, P: Clone + Eq + Hash + Debug + Send + Sync + 'static> DockArea<'a, P> {
|
||||
cancel_drag::<P>(ui.ctx(), self.id);
|
||||
return DockOutput { error: Some(DockError::InvalidGeometry), ..Default::default() };
|
||||
}
|
||||
release_moved_tab_focus(ui.ctx(), self.id, layout);
|
||||
let (view, frame) = super::render::view(layout, self.maximized.as_ref(), self.hide, &*content);
|
||||
let mut renderer = Renderer {
|
||||
id: self.id,
|
||||
@@ -797,6 +831,7 @@ impl<'a, P: Clone + Eq + Hash + Debug + Send + Sync + 'static> DockArea<'a, P> {
|
||||
renderer.finish_drag(ui.ctx());
|
||||
renderer.report_flyout(ui.ctx());
|
||||
super::hits::record(ui.ctx(), self.id, &view, &renderer.owned_interactions);
|
||||
record_tab_focus(ui.ctx(), self.id, layout, !renderer.output.actions.is_empty());
|
||||
ui.ctx().data_mut(|d| d.insert_temp(float_space_key(self.id), renderer.float_space));
|
||||
ui.ctx().data_mut(|d| d.insert_temp(self.id.with("owned-interactions"), renderer.owned_interactions));
|
||||
renderer.output
|
||||
|
||||
@@ -912,7 +912,8 @@ fn custom_chrome_shaped_labels_and_programmatic_raise_are_available() {
|
||||
custom_frame(&ctx, &layout, &mut content, vec![], &[], false);
|
||||
}
|
||||
let out = custom_frame(&ctx, &layout, &mut content, vec![], &[], true);
|
||||
assert_eq!(out.focused, Some(5));
|
||||
assert_eq!(out.focused, None, "a programmatic raise is not user focus");
|
||||
assert_eq!(ctx.memory(|m| m.focused()), None, "a raise draws no keyboard-focus ring");
|
||||
assert!(out.actions.is_empty());
|
||||
assert!(content.headers >= 3 && content.bodies >= 3);
|
||||
assert!(content.labels.iter().any(|s| s == "Logical 5"));
|
||||
|
||||
+7
-4
@@ -110,8 +110,10 @@ survives.
|
||||
|
||||
`DockOutput` also has:
|
||||
|
||||
- `focused`: the panel the user focused this frame: a clicked tab, a drop, a raised window, or a
|
||||
press of any pointer button in a panel body, bare and protected panels included.
|
||||
- `focused`: the panel the user focused this frame: a clicked tab, a drop, or a press of any
|
||||
pointer button in a panel body, bare and protected panels included. Commands, `raise_panel` and
|
||||
workspace resets never set it; a host that tracks the last focused panel clears it when a reset
|
||||
or command hides, closes or moves that panel.
|
||||
- `events`: requests outside the layout, such as `DockEvent::ToggleMaximize` (Frame) and
|
||||
`DockEvent::ToggleAutoCollapse` (Dock), and `DockEvent::FlyoutChanged { panel }` (Dock): the
|
||||
flyout opened, switched or closed (`None`). It comes once per change, whatever made it: an
|
||||
@@ -125,7 +127,7 @@ Builder options on `DockArea`:
|
||||
| `maximized(panel)` | Maximize Frame: that frame fills the area. |
|
||||
| `hide_panels(HidePanels)` | Hide panels for Tab and Shift+Tab. Protected panels stay. |
|
||||
| `auto_collapse_iconic(on)` | Close a flyout on Escape or a click outside (default on). |
|
||||
| `raise_panel(panel)` | Raise and focus a panel's floating window this frame. |
|
||||
| `raise_panel(panel)` | Raise a panel's floating window, or open its flyout, this frame. It moves no keyboard focus and does not set `DockOutput::focused`. |
|
||||
| `blocked_rects(rects)` | App overlays that docking gestures must ignore. |
|
||||
|
||||
`DockStyle::close_menu_items` and `DockStyle::close_button` hide the built-in Close menu items
|
||||
@@ -133,7 +135,8 @@ and the floating window ×. They hide controls only: a panel that stays `closabl
|
||||
by the app's own Close items and commands.
|
||||
|
||||
`DockArea::chrome_has_focus` tells whether keyboard focus is on docking chrome rather than on a
|
||||
panel body. `begin_drag` hands a drag that started in the app's own chrome to the area.
|
||||
panel body. Focus follows only the user's input: a tab keeps keyboard focus until a command or a
|
||||
workspace reset hides, closes or moves its panel, and then gives it up. `begin_drag` hands a drag that started in the app's own chrome to the area.
|
||||
`docking::chrome` has the floating frame, the title bar and `drop_preview` (the drop preview
|
||||
docking and `craft_ui::panes` both draw) for apps that draw their own windows.
|
||||
|
||||
|
||||
Reference in new issue
Block a user