mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-10 20:42:12 +00:00
Merge pull request #606 from ZIDAN44/fix/410-docking-handle-names
docking: each move handle has one name, after what it moves (#410)
This commit is contained in:
9 files changed
+394
-14
No files matched your search
@@ -0,0 +1,237 @@
|
||||
//! The accessible names of the handles that move a group or a dock column (#410).
|
||||
|
||||
use super::*;
|
||||
use crate::layout::{SplitAxis, SplitSize};
|
||||
use egui::accesskit::Role;
|
||||
use egui::{Id, vec2};
|
||||
use egui_kittest::{
|
||||
Harness,
|
||||
kittest::{NodeT, Queryable},
|
||||
};
|
||||
|
||||
const GROUP: &str = "Move the {name} tab group";
|
||||
const COLUMN: &str = "Move the {name} panel column";
|
||||
|
||||
fn tabs(panels: &[u32], active: usize) -> Node<u32> {
|
||||
Node::Tabs { panels: panels.to_vec(), active }
|
||||
}
|
||||
|
||||
/// Two tab groups side by side, showing panels 2 and 3.
|
||||
fn two_groups() -> Layout<u32> {
|
||||
Layout {
|
||||
root: Some(Node::Split {
|
||||
axis: SplitAxis::Horizontal,
|
||||
size: SplitSize::FixedSecond(300.0),
|
||||
first: Box::new(tabs(&[1, 2], 1)),
|
||||
second: Box::new(tabs(&[3], 0)),
|
||||
}),
|
||||
floating: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// A left dock of two columns beside canvas 0: [1, 2] over [3], and [4]; iconic when `iconic`.
|
||||
fn two_columns(iconic: bool) -> Layout<u32> {
|
||||
let mut first = Column::new(vec![Group::new(vec![1, 2], 1), Group::new(vec![3], 0)], 200.0);
|
||||
first.iconic = iconic;
|
||||
let second = Column::new(vec![Group::new(vec![4], 0)], 200.0);
|
||||
let dock = Node::Dock { side: Side::Start, columns: vec![first, second] };
|
||||
Layout {
|
||||
root: Some(Node::Split {
|
||||
axis: SplitAxis::Horizontal,
|
||||
size: SplitSize::FixedFirst(400.0),
|
||||
first: Box::new(dock),
|
||||
second: Box::new(tabs(&[0], 0)),
|
||||
}),
|
||||
floating: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn titled(panel: &u32) -> String {
|
||||
format!("Panel {panel}")
|
||||
}
|
||||
|
||||
/// Panels 3 and 4 have no title; panel 1 only spaces.
|
||||
fn partly_untitled(panel: &u32) -> String {
|
||||
match panel {
|
||||
3 | 4 => String::new(),
|
||||
1 => " ".into(),
|
||||
_ => titled(panel),
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders `layout` with the move labels set to `group` and `column` (the defaults when `None`).
|
||||
/// Untitled tabs have no name, so kittest's check for nameless widgets is off when a panel has
|
||||
/// none.
|
||||
fn harness(layout: Layout<u32>, group: Option<&'static str>, column: Option<&'static str>, label: fn(&u32) -> String) -> Harness<'static> {
|
||||
let check = (1..=4).all(|panel| !label(&panel).trim().is_empty());
|
||||
let mut h = Harness::builder().with_size(vec2(900.0, 500.0)).with_accessibility_check(check).build_ui(move |ui| {
|
||||
let mut style = DockStyle::from_ui(ui);
|
||||
if let Some(text) = group {
|
||||
style.move_group_label = text.into();
|
||||
}
|
||||
if let Some(text) = column {
|
||||
style.move_column_label = text.into();
|
||||
}
|
||||
DockArea::new(Id::unique("handle-names")).show(
|
||||
ui,
|
||||
&layout,
|
||||
&style,
|
||||
label,
|
||||
|_| Permissions::default(),
|
||||
|ui: &mut egui::Ui, _: &u32| {
|
||||
ui.label("Body");
|
||||
},
|
||||
);
|
||||
});
|
||||
h.run_steps(3);
|
||||
h
|
||||
}
|
||||
|
||||
fn buttons(h: &Harness<'_>, label: &str) -> usize {
|
||||
h.query_all_by_role_and_label(Role::Button, label).count()
|
||||
}
|
||||
|
||||
/// The names of every button whose name starts with "Move", sorted.
|
||||
fn move_handles(h: &Harness<'_>) -> Vec<String> {
|
||||
let mut names: Vec<String> =
|
||||
h.query_all_by_role(Role::Button).filter_map(|node| node.accesskit_node().label()).filter(|name| name.starts_with("Move")).collect();
|
||||
names.sort();
|
||||
names
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Each group has one move handle, named after the panel it shows, so two groups' handles differ.
|
||||
fn move_group_handles_are_named_after_the_panel_their_group_shows() {
|
||||
let h = harness(two_groups(), Some(GROUP), None, titled);
|
||||
assert_eq!(move_handles(&h), ["Move the Panel 2 tab group", "Move the Panel 3 tab group"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Dock groups and column bars are named after what they move: a column after the panels its
|
||||
/// groups show. No two move handles share a name.
|
||||
fn dock_group_and_column_handles_are_named_after_what_they_move() {
|
||||
let h = harness(two_columns(false), Some(GROUP), Some(COLUMN), titled);
|
||||
assert_eq!(
|
||||
move_handles(&h),
|
||||
[
|
||||
"Move the Panel 0 tab group",
|
||||
"Move the Panel 2 tab group",
|
||||
"Move the Panel 2, Panel 3 panel column",
|
||||
"Move the Panel 3 tab group",
|
||||
"Move the Panel 4 panel column",
|
||||
"Move the Panel 4 tab group",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// The grip above each icon group of an iconic column is named after the group's active panel.
|
||||
fn icon_group_grips_are_named_after_their_active_panel() {
|
||||
let h = harness(two_columns(true), Some(GROUP), Some(COLUMN), titled);
|
||||
assert_eq!(buttons(&h, "Move the Panel 2 tab group"), 1);
|
||||
assert_eq!(buttons(&h, "Move the Panel 3 tab group"), 1);
|
||||
assert_eq!(buttons(&h, "Move the Panel 2, Panel 3 panel column"), 1);
|
||||
assert_eq!(buttons(&h, GROUP), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// A label without `{name}`, the default or an app's own, is used as it is: once per group (one
|
||||
/// handle each, as in the issue's strip of a column bar over one group) and once per column.
|
||||
fn a_label_without_the_placeholder_is_kept_for_every_handle() {
|
||||
let h = harness(two_columns(false), None, None, titled);
|
||||
assert_eq!(buttons(&h, "Move tab group"), 4);
|
||||
assert_eq!(buttons(&h, "Move panel column"), 2);
|
||||
let h = harness(two_groups(), Some("Déplacer le groupe"), Some("Déplacer la colonne"), titled);
|
||||
assert_eq!(move_handles(&h), Vec::<String>::new());
|
||||
assert_eq!(buttons(&h, "Déplacer le groupe"), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// An untitled panel leaves the placeholder out rather than an empty name; a column names only
|
||||
/// its titled panels.
|
||||
fn untitled_panels_leave_the_name_out() {
|
||||
let h = harness(two_columns(false), Some(GROUP), Some(COLUMN), partly_untitled);
|
||||
assert_eq!(
|
||||
move_handles(&h),
|
||||
[
|
||||
"Move the Panel 0 tab group",
|
||||
"Move the Panel 2 panel column",
|
||||
"Move the Panel 2 tab group",
|
||||
"Move the panel column",
|
||||
"Move the tab group",
|
||||
"Move the tab group",
|
||||
]
|
||||
);
|
||||
// The hidden tab's spaces do not count either: group [1, 2] shows 1 here.
|
||||
let mut layout = two_groups();
|
||||
if let Some(Node::Split { first, .. }) = &mut layout.root {
|
||||
**first = tabs(&[1, 2], 0);
|
||||
}
|
||||
let h = harness(layout, Some("Move tab group: {name}"), None, partly_untitled);
|
||||
assert_eq!(move_handles(&h), ["Move tab group", "Move tab group"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// A label that is only the placeholder, with no name to put in, falls back to the default.
|
||||
fn a_label_that_is_only_the_placeholder_falls_back_to_the_default() {
|
||||
assert_eq!(labels::named("{name}", labels::MOVE_GROUP, String::new), "Move tab group");
|
||||
assert_eq!(labels::named("{name}", labels::MOVE_GROUP, || "Layers".into()), "Layers");
|
||||
assert_eq!(labels::named("Move the {name} column", labels::MOVE_COLUMN, || " \t".into()), "Move the column");
|
||||
// A digit is enough to keep the label's own text.
|
||||
assert_eq!(labels::named("{name} 2", labels::MOVE_GROUP, String::new), "2");
|
||||
assert_eq!(labels::named("({name})", labels::MOVE_GROUP, String::new), "Move tab group");
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// A blank name takes the brackets, quotes and separator that only made sense around it, and
|
||||
/// adds no space where the placeholder had none beside it.
|
||||
fn a_blank_name_leaves_no_stray_punctuation() {
|
||||
for (label, bare) in [
|
||||
("Move tab group ({name})", "Move tab group"),
|
||||
("Déplacer le groupe « {name} »", "Déplacer le groupe"),
|
||||
("Move “{name}” group", "Move group"),
|
||||
("Move [{name}]", "Move"),
|
||||
("Move the {name}, tab group", "Move the tab group"),
|
||||
("移动{name}标签组", "移动标签组"),
|
||||
("「{name}」タブグループを移動", "タブグループを移動"),
|
||||
("Move the {name} tab group", "Move the tab group"),
|
||||
("Move tab group: {name}", "Move tab group"),
|
||||
("Move tab group — {name}", "Move tab group"),
|
||||
("Move {name} and {name} group", "Move and group"),
|
||||
("“{name}”", "Move tab group"),
|
||||
] {
|
||||
assert_eq!(labels::named(label, labels::MOVE_GROUP, String::new), bare, "{label}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// A strip whose tabs leave no room for a drag bar still has one announced, focusable move
|
||||
/// handle per group: its top border band.
|
||||
fn without_a_drag_bar_the_border_band_is_the_one_move_handle() {
|
||||
let area = Id::unique("no-drag-bar");
|
||||
let layout = Layout { root: Some(tabs(&[1], 0)), floating: vec![] };
|
||||
let mut h = Harness::builder().with_size(vec2(300.0, 200.0)).build_ui(move |ui| {
|
||||
let mut style = DockStyle::from_ui(ui);
|
||||
style.drag_bar_width = 0.0;
|
||||
style.move_group_label = GROUP.into();
|
||||
DockArea::new(area).show(
|
||||
ui,
|
||||
&layout,
|
||||
&style,
|
||||
|panel| format!("Panel {panel} with a title far wider than its strip, so its tab takes the whole strip"),
|
||||
|_| Permissions::default(),
|
||||
|ui: &mut egui::Ui, _: &u32| {
|
||||
ui.label("Body");
|
||||
},
|
||||
);
|
||||
});
|
||||
h.run_steps(3);
|
||||
assert!(h.ctx.read_response(area.with(("drag-bar", Some(&1_u32)))).is_none(), "no drag bar is drawn");
|
||||
let border = h.ctx.read_response(area.with(("drag-border", Some(&1_u32)))).expect("the border band is drawn");
|
||||
assert!(border.sense.is_focusable());
|
||||
let handles: Vec<_> =
|
||||
h.query_all_by_role(Role::Button).filter(|node| node.accesskit_node().label().is_some_and(|l| l.starts_with("Move"))).collect();
|
||||
assert_eq!(handles.len(), 1);
|
||||
assert_eq!(handles[0].rect(), border.rect, "the band is the announced handle");
|
||||
assert!(handles[0].accesskit_node().label().is_some_and(|l| l.starts_with("Move the Panel 1 with a title")));
|
||||
}
|
||||
@@ -43,6 +43,24 @@ impl DockStyle {
|
||||
/// | `dock.restoreFrame` | `restore_frame_label` | Restore Frame Size |
|
||||
/// | `dock.autoCollapse` | `auto_collapse_label` | Auto-Collapse Iconic Panels |
|
||||
/// | `dock.floatHint` | `float_hint_label` | Hold {key} to undock |
|
||||
///
|
||||
/// Every group's drag bar and icon grip, and every column bar, has the same default name.
|
||||
/// Opt in to naming them, in the English label and each translation: a `{name}` in
|
||||
/// `move_group_label` stands for the logical label of the panel the group shows, and in
|
||||
/// `move_column_label` for those of the panels the column's groups show, joined by ", ", so a
|
||||
/// screen reader tells the handles apart. The ", " is fixed. Untitled panels are left out;
|
||||
/// with no title at all the placeholder goes with the brackets or quotes around it, a
|
||||
/// separator right after it and one of the spaces beside it ("Move the tab group"). Groups
|
||||
/// showing panels of the same title still share a name:
|
||||
///
|
||||
/// ```
|
||||
/// let style = craft_ui::docking::DockStyle::default().localized(|key| match key {
|
||||
/// "dock.moveGroup" => Some("Move the {name} tab group".to_owned()),
|
||||
/// "dock.moveColumn" => Some("Move the {name} panel column".to_owned()),
|
||||
/// _ => None,
|
||||
/// });
|
||||
/// # assert_eq!(style.move_group_label, "Move the {name} tab group");
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn localized(mut self, translate: impl Fn(&str) -> Option<String>) -> Self {
|
||||
for (key, label) in self.labels_mut() {
|
||||
@@ -82,3 +100,90 @@ impl DockStyle {
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// `label` with every `{name}` replaced by `name()`, which only runs when there is one. A blank
|
||||
/// name leaves each placeholder out with what only made sense around it (see [`unnamed`]); a
|
||||
/// label with no letter or digit left gives `fallback`, so no handle is announced with a hole in
|
||||
/// its name or without one.
|
||||
pub(super) fn named(label: &str, fallback: &str, name: impl FnOnce() -> String) -> String {
|
||||
if !label.contains(NAME) {
|
||||
return label.to_owned();
|
||||
}
|
||||
let name = name();
|
||||
if !name.trim().is_empty() {
|
||||
return label.replace(NAME, name.trim());
|
||||
}
|
||||
let bare = unnamed(label);
|
||||
if bare.chars().any(char::is_alphanumeric) { bare } else { fallback.to_owned() }
|
||||
}
|
||||
|
||||
/// `label` without its placeholders. Each goes with the brackets or quotes around it and the
|
||||
/// spaces inside them, and with a separator right after it. Where both sides were spaces, one
|
||||
/// stays; where neither was, none is added. Separators and spaces at the ends go too.
|
||||
fn unnamed(label: &str) -> String {
|
||||
let mut text = label.to_owned();
|
||||
// Every pass removes a placeholder, so the text gets shorter each time.
|
||||
while let Some((before, after)) = text.split_once(NAME) {
|
||||
let (mut head, mut tail) = (before, after);
|
||||
let (open, close) = (head.trim_end(), tail.trim_start());
|
||||
if let (Some(left), Some(right)) = (open.chars().next_back(), close.chars().next())
|
||||
&& PAIRS.contains(&(left, right))
|
||||
{
|
||||
head = open.strip_suffix(left).unwrap_or(open);
|
||||
tail = close.strip_prefix(right).unwrap_or(close);
|
||||
}
|
||||
let next = tail.trim_start();
|
||||
if let Some(separator) = next.chars().next().filter(|c| SEPARATORS.contains(c))
|
||||
&& !head.trim().is_empty()
|
||||
{
|
||||
tail = next.strip_prefix(separator).unwrap_or(next);
|
||||
}
|
||||
text = if head.ends_with(char::is_whitespace) && tail.starts_with(char::is_whitespace) {
|
||||
format!("{} {}", head.trim_end(), tail.trim_start())
|
||||
} else {
|
||||
format!("{head}{tail}")
|
||||
};
|
||||
}
|
||||
text.trim_matches(|c: char| c.is_whitespace() || SEPARATORS.contains(&c) || DASHES.contains(&c)).to_owned()
|
||||
}
|
||||
|
||||
/// Brackets and quotes, opening and closing, that can wrap the placeholder.
|
||||
const PAIRS: &[(char, char)] = &[
|
||||
('(', ')'),
|
||||
('[', ']'),
|
||||
('<', '>'),
|
||||
('"', '"'),
|
||||
('\'', '\''),
|
||||
('«', '»'),
|
||||
('»', '«'),
|
||||
('‹', '›'),
|
||||
('›', '‹'),
|
||||
('“', '”'),
|
||||
('„', '“'),
|
||||
('”', '”'),
|
||||
('‘', '’'),
|
||||
('‚', '‘'),
|
||||
('’', '’'),
|
||||
('「', '」'),
|
||||
('『', '』'),
|
||||
('(', ')'),
|
||||
('[', ']'),
|
||||
('【', '】'),
|
||||
('〈', '〉'),
|
||||
('《', '》'),
|
||||
];
|
||||
|
||||
/// Separators that go with a placeholder they follow, or that are left at an end.
|
||||
const SEPARATORS: &[char] = &[',', ';', ':', '、', ',', ';', ':'];
|
||||
|
||||
/// Dashes left at an end.
|
||||
const DASHES: &[char] = &['-', '–', '—'];
|
||||
|
||||
/// The default [`DockStyle::move_group_label`].
|
||||
pub(super) const MOVE_GROUP: &str = "Move tab group";
|
||||
/// The default [`DockStyle::move_column_label`].
|
||||
pub(super) const MOVE_COLUMN: &str = "Move panel column";
|
||||
|
||||
/// What [`DockStyle::move_group_label`] and [`DockStyle::move_column_label`] put the moved
|
||||
/// group's or column's name in place of.
|
||||
const NAME: &str = "{name}";
|
||||
@@ -53,6 +53,8 @@ mod floating_tests;
|
||||
#[cfg(test)]
|
||||
mod group_tests;
|
||||
#[cfg(test)]
|
||||
mod handle_names_tests;
|
||||
#[cfg(test)]
|
||||
mod host_hooks_tests;
|
||||
#[cfg(test)]
|
||||
mod iconic_tests;
|
||||
|
||||
@@ -628,7 +628,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
let response = ui.interact(grip, self.id.with(("column-bar", Some(first))), Sense::click_and_drag());
|
||||
let response = if movable { response.on_hover_cursor(egui::CursorIcon::Grab) } else { response };
|
||||
self.register_interaction(ui.ctx(), response.id);
|
||||
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, ui.is_enabled(), &self.style.move_column_label));
|
||||
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, ui.is_enabled(), self.move_column_name(column)));
|
||||
if movable && response.drag_started() {
|
||||
let origin = ui.input(|i| i.pointer.press_origin()).or_else(|| ui.ctx().pointer_interact_pos()).unwrap_or(grip.center());
|
||||
let drag = GroupDrag {
|
||||
|
||||
@@ -151,10 +151,10 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
}
|
||||
let bar =
|
||||
mirror(Rect::from_min_max(pos2(x.min(strip.right() - header_width), strip.top()), pos2(strip.right() - header_width, strip.bottom())));
|
||||
self.group_handle(ui, "drag-bar", bar, &owner, movable);
|
||||
let named = self.group_handle(ui, "drag-bar", bar, &owner, movable, true);
|
||||
// Registered last, so the band wins over the tabs below it.
|
||||
let border = mirror(Rect::from_min_max(strip.min, pos2(strip.right() - header_width, (strip.top() + BORDER_BAND).min(strip.bottom()))));
|
||||
self.group_handle(ui, "drag-border", border, &owner, movable);
|
||||
self.group_handle(ui, "drag-border", border, &owner, movable, !named);
|
||||
if self.style.wheel_selects_tabs {
|
||||
let tabs_area = mirror(Rect::from_min_max(strip.min, pos2(strip.right() - header_width, strip.bottom())));
|
||||
self.wheel_tabs(ui, tabs_area, extra, panels, active);
|
||||
@@ -191,22 +191,30 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
|
||||
/// The drag bar (`drag-bar`: drag, double-click, and the group menu on a right click, Enter
|
||||
/// or Space) or the top border band (`drag-border`: drag only). Both move the whole group;
|
||||
/// a left click on the bar only focuses it.
|
||||
fn group_handle(&mut self, ui: &mut Ui, name: &'static str, hit: Rect, owner: &TabGroup<'_, P>, movable: bool) {
|
||||
/// a left click on the bar only focuses it. Only an `accessible` handle takes focus and is in
|
||||
/// the accessibility tree, so each group is announced once: the band, which has no keyboard
|
||||
/// function, only where no bar is drawn. Returns whether the handle is drawn.
|
||||
fn group_handle(&mut self, ui: &mut Ui, name: &'static str, hit: Rect, owner: &TabGroup<'_, P>, movable: bool, accessible: bool) -> bool {
|
||||
if !crate::docking::targets::usable(hit) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
let bar = name == "drag-bar";
|
||||
let sense = if bar { Sense::click_and_drag() } else { Sense::drag() };
|
||||
let sense = match (bar, accessible) {
|
||||
(true, _) => Sense::click_and_drag(),
|
||||
(false, true) => Sense::drag(),
|
||||
(false, false) => Sense::DRAG,
|
||||
};
|
||||
let response = ui.interact(hit, self.id.with((name, owner.panels.first())), sense);
|
||||
let response = if movable { response.on_hover_cursor(egui::CursorIcon::Grab) } else { response };
|
||||
self.register_interaction(ui.ctx(), response.id);
|
||||
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, ui.is_enabled(), &self.style.move_group_label));
|
||||
if accessible {
|
||||
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, ui.is_enabled(), self.move_group_name(owner.selected)));
|
||||
}
|
||||
if movable && response.drag_started() {
|
||||
self.start_group_drag(ui, owner.panels, self.drag_source(owner));
|
||||
}
|
||||
if !bar {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
if self.minimizes(owner) && response.double_clicked() {
|
||||
self.output.actions.push(Action::SetMinimized { panel: owner.selected.clone(), minimized: !owner.minimized });
|
||||
@@ -243,6 +251,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
None
|
||||
};
|
||||
egui::Popup::menu(&response).open_memory(command).show(|ui| self.group_menu(ui, owner, owner.selected));
|
||||
true
|
||||
}
|
||||
|
||||
/// The wheel over a group's strip: down selects the next tab, up the previous, one tab per
|
||||
|
||||
@@ -200,7 +200,8 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
let height = group.height.filter(|h| h.is_finite()).unwrap_or(FLYOUT_HEIGHT);
|
||||
let source = Rect::from_min_size(pos2(grip.left(), top), vec2(width.max(self.style.min_pane), height.max(self.style.tab_height)));
|
||||
self.record_float(&group.panels, vec2(grip.width(), self.row_height()), source.size());
|
||||
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, ui.is_enabled(), &self.style.move_group_label));
|
||||
let shown = group.active_panel().unwrap_or(first);
|
||||
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, ui.is_enabled(), self.move_group_name(shown)));
|
||||
if movable && response.drag_started() {
|
||||
let origin = ui.input(|i| i.pointer.press_origin()).or_else(|| ui.ctx().pointer_interact_pos()).unwrap_or(grip.center());
|
||||
let drag = GroupDrag {
|
||||
|
||||
@@ -163,6 +163,20 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
self.content.logical_label(panel)
|
||||
}
|
||||
|
||||
/// The accessible name of the handle that moves the group showing `shown`.
|
||||
pub(super) fn move_group_name(&self, shown: &P) -> String {
|
||||
super::labels::named(&self.style.move_group_label, super::labels::MOVE_GROUP, || self.label_of(shown))
|
||||
}
|
||||
|
||||
/// The accessible name of `column`'s bar: its groups' shown panels that have a title name it.
|
||||
pub(super) fn move_column_name(&self, column: &super::Column<P>) -> String {
|
||||
super::labels::named(&self.style.move_column_label, super::labels::MOVE_COLUMN, || {
|
||||
let shown = column.groups.iter().filter_map(|g| g.active_panel().or(g.panels.first()));
|
||||
let titles: Vec<String> = shown.map(|panel| self.label_of(panel).trim().to_owned()).filter(|title| !title.is_empty()).collect();
|
||||
titles.join(", ")
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn permissions(&self, panel: &P) -> Permissions {
|
||||
let permissions = self.content.permissions(panel);
|
||||
if self.locked { permissions.locked() } else { permissions }
|
||||
|
||||
@@ -32,7 +32,7 @@ pub struct DockStyle {
|
||||
pub panels_label: String,
|
||||
pub resize_label: String,
|
||||
pub resize_window_label: String,
|
||||
/// Accessibility label of a group's drag bar and top border.
|
||||
/// Accessibility label of a group's drag bar and top border; `{name}` names the group.
|
||||
pub move_group_label: String,
|
||||
pub close_group_label: String,
|
||||
pub minimize_label: String,
|
||||
@@ -115,7 +115,7 @@ pub struct DockStyle {
|
||||
/// [D] Lay docks out right to left: `Side::Start` is the right edge. egui has no app-wide
|
||||
/// layout direction, so the host sets this from its locale. Splits keep their sides.
|
||||
pub right_to_left: bool,
|
||||
/// [D] Accessibility label of a column bar.
|
||||
/// [D] Accessibility label of a column bar; `{name}` names the column ([`Self::localized`]).
|
||||
pub move_column_label: String,
|
||||
/// [D] Column menu and group menu items.
|
||||
pub collapse_to_icons_label: String,
|
||||
@@ -194,7 +194,7 @@ impl DockStyle {
|
||||
panels_label: "Panels".into(),
|
||||
resize_label: "Resize panels".into(),
|
||||
resize_window_label: "Resize panel window".into(),
|
||||
move_group_label: "Move tab group".into(),
|
||||
move_group_label: super::labels::MOVE_GROUP.into(),
|
||||
close_group_label: "Close Tab Group".into(),
|
||||
minimize_label: "Minimize".into(),
|
||||
expand_label: "Expand".into(),
|
||||
@@ -221,7 +221,7 @@ impl DockStyle {
|
||||
iconic_label_width: 96.0,
|
||||
auto_collapse_label: "Auto-Collapse Iconic Panels".into(),
|
||||
right_to_left: false,
|
||||
move_column_label: "Move panel column".into(),
|
||||
move_column_label: super::labels::MOVE_COLUMN.into(),
|
||||
collapse_to_icons_label: "Collapse to Icons".into(),
|
||||
expand_panels_label: "Expand Panels".into(),
|
||||
collapse_column_label: "Collapse column to icons".into(),
|
||||
|
||||
@@ -738,6 +738,18 @@ Keys are `dock.close`, `dock.float`, `dock.collapseToIcons` and so on; the rustd
|
||||
lists them all. `DockContent::label` and `action_label` can shape panel and action text further;
|
||||
the logical label stays the accessibility name.
|
||||
|
||||
Each group and each column has one move handle in the accessibility tree: the group's drag bar
|
||||
and the column bar. A group's top border band only drags and is not announced, except in a group
|
||||
drawn without a drag bar, where the band is its move handle. By default these
|
||||
share one name per kind. Naming them is opt-in: an app puts `{name}` in `move_group_label` or
|
||||
`move_column_label`, and in each of its translations ("Move the {name} tab group"). It stands for
|
||||
what the handle moves: the logical label of the panel the group shows, or of the panels a
|
||||
column's groups show, joined by ", " (fixed, not localized). An untitled panel is left out. With
|
||||
no title at all, the placeholder goes with the brackets or quotes around it, a separator right
|
||||
after it and one of the spaces beside it ("Move the tab group", "Move tab group ({name})" gives
|
||||
"Move tab group", "移动{name}标签组" gives "移动标签组"); a label left with no letter or digit falls
|
||||
back to the English default. Two groups showing panels of the same title still get the same name.
|
||||
|
||||
## Testing
|
||||
|
||||
`DockArea::hit_rects(ctx, id)` lists every gesture source drawn last frame as a `Hit` with its
|
||||
|
||||
Reference in new issue
Block a user