UI.8: shape dock action labels with logical accessibility

This commit is contained in:
skylar taylor-barrick committed 2026-10-10 14:08:31 -06:00
1 parent b1406e3e6a
commit 5c874d51ee
3 files changed
+85 -9

No files matched your search

+25 -8
View File
@@ -158,6 +158,11 @@ pub trait DockContent<P> {
fn label(&mut self, _ui: &Ui, _panel: &P, logical: &str, _font: &FontId, _color: Color32) -> egui::WidgetText {
logical.into()
}
/// Shape stock Float/Move/Close action text independently of panel identity.
/// The renderer preserves `logical` for accessibility, even when the visual text differs.
fn action_label(&mut self, _ui: &Ui, logical: &str, _font: &FontId, _color: Color32) -> egui::WidgetText {
logical.into()
}
fn panel_menu(&mut self, _ui: &mut Ui, _panel: &P) {}
fn group_header_width(&self, _panels: &[P], _active: &P, _floating: bool) -> f32 {
0.0
@@ -690,13 +695,19 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
}
response.context_menu(|ui| {
self.content.panel_menu(ui, panel);
if policy.movable && policy.floatable && ui.button(&self.style.float_label).clicked() {
let rect = clamp_float(group.translate(vec2(24.0, 24.0)), ui.ctx().content_rect());
self.output.actions.push(Action::Float { panel: panel.clone(), rect: [rect.left(), rect.top(), rect.width(), rect.height()] });
ui.close();
if policy.movable && policy.floatable {
let text = self.content.action_label(ui, &self.style.float_label, &self.style.font, self.style.text);
let response = ui.button(text);
response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), &self.style.float_label));
if response.clicked() {
let rect = clamp_float(group.translate(vec2(24.0, 24.0)), ui.ctx().content_rect());
self.output.actions.push(Action::Float { panel: panel.clone(), rect: [rect.left(), rect.top(), rect.width(), rect.height()] });
ui.close();
}
}
if policy.movable {
ui.menu_button(&self.style.move_label, |ui| {
let text = self.content.action_label(ui, &self.style.move_label, &self.style.font, self.style.text);
let response = ui.menu_button(text, |ui| {
egui::ScrollArea::vertical().max_height(360.0).show(ui, |ui| {
for target in self.layout.panels() {
if target != panel && (self.permissions)(target).accepts_tabs && self.menu_label(ui, target).clicked() {
@@ -710,10 +721,16 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
}
});
});
response.response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), &self.style.move_label));
}
if policy.closable && ui.button(&self.style.close_label).clicked() {
self.output.actions.push(Action::Close { panel: panel.clone() });
ui.close();
if policy.closable {
let text = self.content.action_label(ui, &self.style.close_label, &self.style.font, self.style.text);
let response = ui.button(text);
response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), &self.style.close_label));
if response.clicked() {
self.output.actions.push(Action::Close { panel: panel.clone() });
ui.close();
}
}
});
}
+55
View File
@@ -683,6 +683,9 @@ impl DockContent<u32> for CustomContent {
self.labels.push(logical.to_owned());
ui.painter().layout_no_wrap("Custom visual label".into(), font.clone(), color).into()
}
fn action_label(&mut self, ui: &egui::Ui, logical: &str, font: &egui::FontId, color: egui::Color32) -> egui::WidgetText {
ui.painter().layout_no_wrap(format!("Action visual: {logical}"), font.clone(), color).into()
}
fn panel_menu(&mut self, ui: &mut egui::Ui, _: &u32) {
self.menus += 1;
ui.button("App menu").clicked();
@@ -1035,3 +1038,55 @@ fn workspace_cancel_preserves_other_area_and_body_drag_ownership() {
}
}
}
#[test]
fn popup_action_labels_use_custom_visual_text_and_logical_accessibility() {
use egui_kittest::{Harness, kittest::Queryable};
fn paints(shape: &egui::Shape, text: &str) -> bool {
match shape {
egui::Shape::Text(shape) => shape.galley.text() == text,
egui::Shape::Vec(shapes) => shapes.iter().any(|shape| paints(shape, text)),
_ => false,
}
}
let layout = sample();
let mut h = Harness::builder().with_size(vec2(800.0, 600.0)).build_ui_state(
move |ui, content: &mut CustomContent| {
DockArea::new(Id::new("action-label-test")).show_customized(
ui,
&layout,
&DockStyle::from_ui(ui),
|p| format!("Logical {p}"),
|_| Permissions::default(),
|_| PanelLimits::default(),
content,
);
},
CustomContent { labels: vec![], headers: 0, menus: 0, bodies: 0 },
);
h.run_steps(2);
let tab = Id::new("action-label-test").with(("tab", &5_u32));
let at = h.ctx.read_response(tab).unwrap().rect.center();
h.input_mut().events.extend([
Event::PointerMoved(at),
Event::PointerButton { pos: at, button: PointerButton::Secondary, pressed: true, modifiers: Modifiers::NONE },
]);
h.run_steps(1);
h.input_mut().events.push(Event::PointerButton { pos: at, button: PointerButton::Secondary, pressed: false, modifiers: Modifiers::NONE });
h.run_steps(2);
assert!(egui::Popup::is_any_open(&h.ctx));
for logical in ["Float panel", "Move to group", "Close panel"] {
assert!(
h.output().shapes.iter().any(|shape| paints(&shape.shape, &format!("Action visual: {logical}"))),
"custom action must paint: {logical}"
);
// This sample also has a floating-group grip labeled Move to group.
let expected = if logical == "Move to group" { 2 } else { 1 };
assert_eq!(h.query_all_by_label(logical).count(), expected, "popup action must retain its own logical accessibility: {logical}");
assert_eq!(
h.query_all_by_label(&format!("Action visual: {logical}")).count(),
0,
"visual substitution must not replace logical accessibility"
);
}
}
+5 -1
View File
@@ -120,7 +120,11 @@ Collapsed accordion entries still have panel menus; collapsing tab groups is hos
`label` returns `WidgetText`, including an application-shaped galley, used for tab width,
painting and menu labels. The separate logical label callback supplies tab accessibility
text. Original `show` and `show_with_limits` call sites remain supported.
text. `action_label` independently shapes the localized stock Float/Move/Close menu
text without receiving a panel identity. Its default returns unchanged logical `WidgetText`;
custom visual text never replaces the action's logical accessibility label. Panel targets
and overflow entries continue to use `label`. Original `show` and `show_with_limits` call
sites remain supported.
Apply `Action::Activate` in the host, then use `DockArea::raise_panel(panel)` on the next
frame to raise the owning in-viewport floating layer and focus the panel tab. This request