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