diff --git a/crates/ui/src/docking/surface.rs b/crates/ui/src/docking/surface.rs
index efb933d..b0e5bad 100644
--- a/crates/ui/src/docking/surface.rs
+++ b/crates/ui/src/docking/surface.rs
@@ -158,6 +158,11 @@ pub trait DockContent
{
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 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 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();
+ }
}
});
}
diff --git a/crates/ui/src/docking/tests.rs b/crates/ui/src/docking/tests.rs
index 9a06946..54d37df 100644
--- a/crates/ui/src/docking/tests.rs
+++ b/crates/ui/src/docking/tests.rs
@@ -683,6 +683,9 @@ impl DockContent 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"
+ );
+ }
+}
diff --git a/docs/ui-docking.md b/docs/ui-docking.md
index 76586f0..8232450 100644
--- a/docs/ui-docking.md
+++ b/docs/ui-docking.md
@@ -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