diff --git a/crates/ui-egui/src/panels.rs b/crates/ui-egui/src/panels.rs index e24db05e..557397cb 100644 --- a/crates/ui-egui/src/panels.rs +++ b/crates/ui-egui/src/panels.rs @@ -1606,8 +1606,8 @@ fn layers(app: &mut PhotocraftApp, ui: &mut egui::Ui) { actions.push(("layer.new.group".into(), json!({}))); } actions.extend(footer_drop(ui, &group, footer_drag, "layer.groupLayers")); - let adj = icons::button(ui, "contrast", 26.0, false, tl!("Create new fill or adjustment layer")); - egui::Popup::menu(&adj).show(|ui| { + let adj = footer_menu_button(ui, "contrast", 26.0, tl!("Create new fill or adjustment layer")); + egui::Popup::menu(&adj).open_memory(footer_menu_right_click(&adj)).show(|ui| { ui.set_min_width(190.0); for c in photocraft_engine::command_specs().iter().filter(|c| c.id.starts_with("layer.newAdjustmentLayer.")) { if ui.button(tl!(c.label).trim_end_matches('…')).clicked() { @@ -1638,7 +1638,7 @@ fn layers(app: &mut PhotocraftApp, ui: &mut egui::Ui) { actions.push((crate::layer_menu_ui::add_mask_command(doc.selection.is_some(), alt).into(), json!({}))); } let fx = fx_button(ui, 26.0, tl!("Add a layer style")); - egui::Popup::menu(&fx).show(|ui| { + egui::Popup::menu(&fx).open_memory(footer_menu_right_click(&fx)).show(|ui| { ui.set_min_width(180.0); if ui.button(tl!("Blending Options…")).clicked() { crate::layer_style::open(app, None); @@ -1711,6 +1711,31 @@ fn footer_label(command: &str) -> &'static str { } } +/// A secondary click opens a Layers footer popup, as in Photoshop; a primary click still toggles +/// it (the `Popup::menu` default this replaces). +fn footer_menu_right_click(response: &egui::Response) -> Option { + if response.secondary_clicked() { + Some(egui::SetOpenCommand::Bool(true)) + } else if response.clicked() { + Some(egui::SetOpenCommand::Toggle) + } else { + None + } +} + +/// The small down-arrow makes it clear that the footer icon expands into a menu. +fn footer_menu_arrow(ui: &egui::Ui, rect: Rect) { + let t = Tokens::get(ui.ctx()); + let arrow = Rect::from_min_size(rect.right_bottom() - vec2(11.0, 10.0), vec2(9.0, 9.0)); + icons::paint(ui, arrow, "chevron-down", 8.0, t.text_dim); +} + +fn footer_menu_button(ui: &mut egui::Ui, icon: &str, size: f32, tip: &str) -> egui::Response { + let response = icons::button(ui, icon, size, false, tip); + footer_menu_arrow(ui, response.rect); + response +} + /// Photoshop's italic "fx" footer button (no icon-font equivalent). fn fx_button(ui: &mut egui::Ui, size: f32, tip: &str) -> egui::Response { let t = Tokens::get(ui.ctx()); @@ -1722,6 +1747,7 @@ fn fx_button(ui: &mut egui::Ui, size: f32, tip: &str) -> egui::Response { job.append("fx", 0.0, egui::TextFormat { font_id: egui::FontId::proportional(15.0), color: t.icon, italics: true, ..Default::default() }); let g = ui.painter().layout_job(job); ui.painter().galley(r.center() - g.size() / 2.0, g, t.icon); + footer_menu_arrow(ui, r); resp.on_hover_text(tip) } @@ -3433,6 +3459,48 @@ mod layer_drag_edge_scroll_tests { } } +#[cfg(test)] +mod footer_menu_tests { + use super::*; + + // Exercise the actual egui pointer path, not just a mocked click flag. + #[test] + fn right_click_opens_footer_menu_and_primary_click_still_works() { + let ctx = egui::Context::default(); + let rect = Rect::from_min_size(Pos2::ZERO, vec2(300.0, 100.0)); + let pos = pos2(35.0, 20.0); + let frame = |events: Vec| { + let mut opened = false; + let mut output = ctx.run_ui(egui::RawInput { screen_rect: Some(rect), events, ..Default::default() }, |ui| { + let response = ui.add_sized([100.0, 28.0], egui::Button::new("Footer menu")); + egui::Popup::menu(&response).open_memory(footer_menu_right_click(&response)).show(|ui| { + opened = true; + ui.label("Menu entry"); + }); + }); + output.textures_delta.clear(); + opened + }; + frame(Vec::new()); + frame(vec![ + egui::Event::PointerMoved(pos), + egui::Event::PointerButton { pos, button: egui::PointerButton::Secondary, pressed: true, modifiers: egui::Modifiers::NONE }, + ]); + assert!( + frame(vec![egui::Event::PointerButton { pos, button: egui::PointerButton::Secondary, pressed: false, modifiers: egui::Modifiers::NONE }]), + "secondary release opens the popup" + ); + + // A primary click also remains supported by egui::Popup::menu. + frame(vec![egui::Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::NONE }]); + frame(vec![egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: true, modifiers: egui::Modifiers::NONE }]); + assert!( + frame(vec![egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: false, modifiers: egui::Modifiers::NONE }]), + "primary release still opens the popup" + ); + } +} + #[cfg(test)] mod group_drag_selection_tests { use super::*;