mirror of
https://github.com/storytold/craft-libs.git
synced 2026-10-11 17:37:26 +00:00
UI.8: classify renderer-owned docking keyboard focus
This commit is contained in:
1 parent
7bb9c865e6
commit
615f0638c7
3 files changed
+133
No files matched your search
@@ -176,6 +176,55 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> DockArea<P> {
|
||||
Self { id, activation: None, blockers: Vec::new() }
|
||||
}
|
||||
|
||||
/// Whether keyboard focus belongs to a rendered docking control in this layout.
|
||||
/// Panel bodies and application-provided header/menu widgets are excluded. Hosts may
|
||||
/// route nonconflicting global shortcuts through chrome while retaining widget ownership
|
||||
/// of activation, traversal, resize and cancellation keys. Invalid layouts return false.
|
||||
pub fn chrome_has_focus(&self, ctx: &egui::Context, layout: &Layout<P>) -> bool {
|
||||
let Some(focused) = ctx.memory(|memory| memory.focused()) else { return false };
|
||||
if layout.validate().is_err() || ctx.read_response(focused).is_none() {
|
||||
return false;
|
||||
}
|
||||
let matches = |id: Id| id == focused;
|
||||
let group = |panels: &[P]| {
|
||||
panels.iter().any(|panel| matches(self.id.with(("tab", panel))) || matches(self.id.with(("overflow", panel))))
|
||||
|| matches(self.id.with(("move-group", panels.first())))
|
||||
};
|
||||
for floating in &layout.floating {
|
||||
if group(&floating.panels) || floating.panels.first().is_some_and(|first| matches(self.id.with(("floating", first)).with("resize"))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
let mut nodes = layout.root.as_ref().map(|root| vec![(root, Vec::<bool>::new())]).unwrap_or_default();
|
||||
while let Some((node, path)) = nodes.pop() {
|
||||
match node {
|
||||
Node::Tabs { panels, .. } if group(panels) => return true,
|
||||
Node::Stack { entries } => {
|
||||
if entries.iter().any(|entry| {
|
||||
matches(self.id.with(("tab", &entry.panel)))
|
||||
|| matches(self.id.with(("collapse", &entry.panel)))
|
||||
|| matches(self.id.with(("stack-resize", &entry.panel)))
|
||||
}) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Node::Split { first, second, .. } => {
|
||||
if matches(self.id.with(("split", &path))) {
|
||||
return true;
|
||||
}
|
||||
let mut second_path = path.clone();
|
||||
second_path.push(true);
|
||||
let mut first_path = path;
|
||||
first_path.push(false);
|
||||
nodes.push((second, second_path));
|
||||
nodes.push((first, first_path));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
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.
|
||||
pub fn raise_panel(mut self, panel: P) -> Self {
|
||||
|
||||
@@ -848,3 +848,79 @@ fn protected_group_member_blocks_whole_group_gesture_and_host_drop() {
|
||||
assert!(!out.actions.iter().any(|a| matches!(a, Action::MoveFloating { .. })));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chrome_focus_classifies_rendered_tab_but_excludes_body_text_field_and_invalid_layout() {
|
||||
let ctx = Context::default();
|
||||
let layout = sample();
|
||||
let tab = Id::new("focus-area").with(("tab", 3u32));
|
||||
let text = Id::new("body-text");
|
||||
let mut value = String::new();
|
||||
let mut paint = |focus: Id| {
|
||||
let mut output =
|
||||
ctx.run_ui(RawInput { screen_rect: Some(Rect::from_min_size(pos2(0.0, 0.0), vec2(800.0, 600.0))), ..Default::default() }, |ui| {
|
||||
DockArea::new(Id::new("focus-area")).show(
|
||||
ui,
|
||||
&layout,
|
||||
&DockStyle::from_ui(ui),
|
||||
|p| format!("Panel {p}"),
|
||||
|_| Permissions::default(),
|
||||
|ui, p| {
|
||||
if *p == 3 {
|
||||
ui.add(egui::TextEdit::singleline(&mut value).id(text));
|
||||
}
|
||||
},
|
||||
);
|
||||
ui.memory_mut(|memory| memory.request_focus(focus));
|
||||
});
|
||||
output.textures_delta.clear();
|
||||
};
|
||||
paint(tab);
|
||||
assert!(DockArea::new(Id::new("focus-area")).chrome_has_focus(&ctx, &layout));
|
||||
paint(text);
|
||||
assert!(!DockArea::new(Id::new("focus-area")).chrome_has_focus(&ctx, &layout));
|
||||
paint(tab);
|
||||
let mut invalid = layout.clone();
|
||||
invalid.floating.push(Floating { panels: vec![3], active: 0, rect: [10.0, 10.0, 100.0, 100.0] });
|
||||
assert!(!DockArea::new(Id::new("focus-area")).chrome_has_focus(&ctx, &invalid));
|
||||
assert!(!DockArea::<u32>::new(Id::new("focus-area")).chrome_has_focus(&ctx, &Layout::default()));
|
||||
ctx.memory_mut(|memory| memory.request_focus(Id::new("unrendered")));
|
||||
assert!(!DockArea::new(Id::new("focus-area")).chrome_has_focus(&ctx, &layout));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chrome_focus_classifies_rendered_accordion_and_split_and_floating_resizers() {
|
||||
let ctx = Context::default();
|
||||
let area = Id::new("focus-chrome-controls");
|
||||
let accordion = Layout {
|
||||
root: Some(Node::Stack {
|
||||
entries: vec![StackEntry { panel: 1u32, open: true, height: Some(150.0) }, StackEntry { panel: 2u32, open: true, height: Some(150.0) }],
|
||||
}),
|
||||
floating: Vec::new(),
|
||||
};
|
||||
for (layout, id) in [
|
||||
(accordion.clone(), area.with(("tab", 1u32))),
|
||||
(accordion.clone(), area.with(("collapse", 1u32))),
|
||||
(accordion, area.with(("stack-resize", 1u32))),
|
||||
(sample(), area.with(("split", Vec::<bool>::new()))),
|
||||
(sample(), area.with(("floating", 5u32)).with("resize")),
|
||||
] {
|
||||
let mut output =
|
||||
ctx.run_ui(RawInput { screen_rect: Some(Rect::from_min_size(pos2(0.0, 0.0), vec2(800.0, 600.0))), ..Default::default() }, |ui| {
|
||||
DockArea::new(area).show(
|
||||
ui,
|
||||
&layout,
|
||||
&DockStyle::from_ui(ui),
|
||||
|p| format!("Panel {p}"),
|
||||
|_| Permissions::default(),
|
||||
|ui, _| {
|
||||
ui.label("Body");
|
||||
},
|
||||
);
|
||||
ui.memory_mut(|memory| memory.request_focus(id));
|
||||
});
|
||||
output.textures_delta.clear();
|
||||
assert!(ctx.read_response(id).is_some(), "the fixture must render the control");
|
||||
assert!(DockArea::new(area).chrome_has_focus(&ctx, &layout));
|
||||
}
|
||||
}
|
||||
@@ -137,3 +137,11 @@ Hosts supply overlay rectangles via `blocked_rects` before showing the surface.
|
||||
block drop previews, release actions and float fallback at the pointer, including overlays
|
||||
appearing mid-drag. Hosts must provide current overlay geometry or cancel docking when
|
||||
modal state prevents interaction; later-drawn overlays cannot be discovered retroactively.
|
||||
|
||||
`DockArea::chrome_has_focus(ctx, layout)` classifies focus on rendered dock tabs,
|
||||
collapse buttons, overflow, split/stack dividers, floating resize corners and group
|
||||
grips. It excludes panel content and custom header/menu widgets and rejects invalid
|
||||
layouts. Hosts can permit global commands from chrome while reserving Enter, Space,
|
||||
Tab, Escape and arrow keys for activation, traversal, cancellation and resizing.
|
||||
Content widgets retain their usual keyboard ownership. The renderer owns the IDs;
|
||||
hosts do not need to reconstruct them.
|
||||
Reference in new issue
Block a user