Browse / github.com/storytold/photocraft / patch
fix: dismiss context menu without triggering tool action on outside click
Add a right-click zoom context menu to the Zoom Tool In PhotoCraft 0.16 on Ubuntu 22.04, right-clicking with the Zoom Tool selected zooms further into the image instead of opening a context menu. The Hand Tool correctly displays a context menu, and Ctrl+0 successfully fits the image to the screen. Suggested improvement: Allow right-clicking with the Zoom Tool to open a context menu offering: * Fit on Screen * 100% Zoom * Zoom In * Zoom Out This would provide a more intuitive workflow for users accustomed to Adobe Photoshop, where these options are directly accessible from the Zoom Tool.
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- Kaedo17 (imported from pull request #9841, open)
- Base
7ac7b8b106acccf6826d45ed5c9e481dfe341035- Size
- +115 −40 in 3 files
- Review
- pending
- Shared
- 2026-10-28
- Id
57d5dcf5bbb571d1cb037393
How it went
No other operator has reported using it yet.
Diff
raw · crates/ui-egui/src/canvas.rs crates/ui-egui/src/canvas/canvas_tool_menu.rs crates/ui-egui/src/state.rs
@@ -1156,49 +1156,107 @@ pub fn canvas_view(app: &mut PhotocraftApp, ui: &mut egui::Ui, idx: usize, rect: if buttons.clicked && let Some(p) = response.interact_pointer_pos() {- let click_button = if app.secondary_erase { PointerButton::Secondary } else { PointerButton::Primary };- let click_mods = ui.input(|i| pointer_button_modifiers(&i.events, click_button)).unwrap_or(mods);- let click_mods = if tool == Tool::ObjectSelection && click_button == PointerButton::Primary {- ctx.data(|d| d.get_temp(tool_press)).unwrap_or(click_mods)- } else {- click_mods- };- let d = xf.to_doc(p);- match tool {- Tool::Zoom if !transform_owns_pointer => {- let nz = crate::zoom_levels::step(view.zoom, if zoom_out(click_mods.alt) { -1 } else { 1 }, view.doc_size);- let center = app.session.prefs().tools.zoom_clicked_point_to_center;- let doc_anchor = xf.to_doc(p);- let screen_anchor = p;- zoom_about(&mut view, &xf, p, nz, center, ppp);- view.request_zoom_animation(app.session.prefs().general.animated_zoom && nz != display_view.zoom);- view.zoom_anchor = (view.zoom_animating && !center).then_some((doc_anchor, [screen_anchor.x, screen_anchor.y]));- }- // A click with the (temporary) Hand does nothing, never the tool underneath.- Tool::Hand | Tool::RotateView => {}- // Double-clicking closes the polygonal lasso: the first click placed the last- // vertex (or already closed it), so the second never starts a new polygon. egui- // reports it as a triple click when a vertex went down shortly before.- Tool::PolygonLasso if response.double_clicked() || response.triple_clicked() => commit_polygon(app),- // The same for the Magnetic Lasso: along the edges, or straight with ⌥.- Tool::MagneticLasso if response.double_clicked() || response.triple_clicked() => crate::magnetic_lasso_ui::close(app, mods.alt),- // The first click placed (or grabbed) the point; the second toggles it.- Tool::CurvaturePen if response.double_clicked() => crate::vector_ui::curvature_toggle(app, d[0], d[1]),- // Double-clicking a guide opens Preferences › Guides, Grid & Slices, as in Photoshop.- Tool::Move if response.double_clicked() && crate::rulers::guide_at(app, d[0], d[1]).is_some() => {- if let Err(e) = crate::menus::invoke(app, &ctx, "edit.preferences.guidesGridAndSlices", json!({})) {- app.ui.status = e;+ // If an outside press dismissed the canvas tool menu, consume this release click+ // so it doesn't also trigger a tool action (Photoshop behavior).+ if app.ui.canvas_tool_menu_dismissed {+ app.ui.canvas_tool_menu_dismissed = false;+ } else if let Some(menu_rect) = crate::canvas_tool_menu::menu_rect_if_open(app, &ctx) {+ // If a canvas tool menu is open and this click is outside it, the click will+ // dismiss the menu and should not also trigger a tool action (Photoshop behavior).+ if !menu_rect.contains(p) {+ // Click is outside the open menu - it will dismiss the menu, don't trigger tool action.+ } else {+ let click_button = if app.secondary_erase { PointerButton::Secondary } else { PointerButton::Primary };+ let click_mods = ui.input(|i| pointer_button_modifiers(&i.events, click_button)).unwrap_or(mods);+ let click_mods = if tool == Tool::ObjectSelection && click_button == PointerButton::Primary {+ ctx.data(|d| d.get_temp(tool_press)).unwrap_or(click_mods)+ } else {+ click_mods+ };+ let d = xf.to_doc(p);+ match tool {+ Tool::Zoom if !transform_owns_pointer => {+ let nz = crate::zoom_levels::step(view.zoom, if zoom_out(click_mods.alt) { -1 } else { 1 }, view.doc_size);+ let center = app.session.prefs().tools.zoom_clicked_point_to_center;+ let doc_anchor = xf.to_doc(p);+ let screen_anchor = p;+ zoom_about(&mut view, &xf, p, nz, center, ppp);+ view.request_zoom_animation(app.session.prefs().general.animated_zoom && nz != display_view.zoom);+ view.zoom_anchor = (view.zoom_animating && !center).then_some((doc_anchor, [screen_anchor.x, screen_anchor.y]));+ }+ // A click with the (temporary) Hand does nothing, never the tool underneath.+ Tool::Hand | Tool::RotateView => {}+ // Double-clicking closes the polygonal lasso: the first click placed the last+ // vertex (or already closed it), so the second never starts a new polygon. egui+ // reports it as a triple click when a vertex went down shortly before.+ Tool::PolygonLasso if response.double_clicked() || response.triple_clicked() => commit_polygon(app),+ // The same for the Magnetic Lasso: along the edges, or straight with ⌥.+ Tool::MagneticLasso if response.double_clicked() || response.triple_clicked() => crate::magnetic_lasso_ui::close(app, mods.alt),+ // The first click placed (or grabbed) the point; the second toggles it.+ Tool::CurvaturePen if response.double_clicked() => crate::vector_ui::curvature_toggle(app, d[0], d[1]),+ // Double-clicking a guide opens Preferences › Guides, Grid & Slices, as in Photoshop.+ Tool::Move if response.double_clicked() && crate::rulers::guide_at(app, d[0], d[1]).is_some() => {+ if let Err(e) = crate::menus::invoke(app, &ctx, "edit.preferences.guidesGridAndSlices", json!({})) {+ app.ui.status = e;+ }+ }+ _ => {+ if tool == Tool::Move && app.ui.transform.is_none() {+ begin_transform_controls_at(app, &ctx, &xf, p);+ }+ // If this tool was not handled on pointer-down, fall back to a+ // click-sized gesture while preserving pen pressure (mouse stays at 1).+ tool_event(app, ToolEvent::Down { x: d[0], y: d[1], pressure: app.stylus.pressure() }, click_mods);+ tap_at_peak_pressure(app);+ tool_event(app, ToolEvent::Up { x: d[0], y: d[1] }, click_mods);+ } } }- _ => {- if tool == Tool::Move && app.ui.transform.is_none() {- begin_transform_controls_at(app, &ctx, &xf, p);+ } else {+ let click_button = if app.secondary_erase { PointerButton::Secondary } else { PointerButton::Primary };+ let click_mods = ui.input(|i| pointer_button_modifiers(&i.events, click_button)).unwrap_or(mods);+ let click_mods = if tool == Tool::ObjectSelection && click_button == PointerButton::Primary {+ ctx.data(|d| d.get_temp(tool_press)).unwrap_or(click_mods)+ } else {+ click_mods+ };+ let d = xf.to_doc(p);+ match tool {+ Tool::Zoom if !transform_owns_pointer => {+ let nz = crate::zoom_levels::step(view.zoom, if zoom_out(click_mods.alt) { -1 } else { 1 }, view.doc_size);+ let center = app.session.prefs().tools.zoom_clicked_point_to_center;+ let doc_anchor = xf.to_doc(p);+ let screen_anchor = p;+ zoom_about(&mut view, &xf, p, nz, center, ppp);+ view.request_zoom_animation(app.session.prefs().general.animated_zoom && nz != display_view.zoom);+ view.zoom_anchor = (view.zoom_animating && !center).then_some((doc_anchor, [screen_anchor.x, screen_anchor.y]));+ }+ // A click with the (temporary) Hand does nothing, never the tool underneath.+ Tool::Hand | Tool::RotateView => {}+ // Double-clicking closes the polygonal lasso: the first click placed the last+ // vertex (or already closed it), so the second never starts a new polygon. egui+ // reports it as a triple click when a vertex went down shortly before.+ Tool::PolygonLasso if response.double_clicked() || response.triple_clicked() => commit_polygon(app),+ // The same for the Magnetic Lasso: along the edges, or straight with ⌥.+ Tool::MagneticLasso if response.double_clicked() || response.triple_clicked() => crate::magnetic_lasso_ui::close(app, mods.alt),+ // The first click placed (or grabbed) the point; the second toggles it.+ Tool::CurvaturePen if response.double_clicked() => crate::vector_ui::curvature_toggle(app, d[0], d[1]),+ // Double-clicking a guide opens Preferences › Guides, Grid & Slices, as in Photoshop.+ Tool::Move if response.double_clicked() && crate::rulers::guide_at(app, d[0], d[1]).is_some() => {+ if let Err(e) = crate::menus::invoke(app, &ctx, "edit.preferences.guidesGridAndSlices", json!({})) {+ app.ui.status = e;+ }+ }+ _ => {+ if tool == Tool::Move && app.ui.transform.is_none() {+ begin_transform_controls_at(app, &ctx, &xf, p);+ }+ // If this tool was not handled on pointer-down, fall back to a+ // click-sized gesture while preserving pen pressure (mouse stays at 1).+ tool_event(app, ToolEvent::Down { x: d[0], y: d[1], pressure: app.stylus.pressure() }, click_mods);+ tap_at_peak_pressure(app);+ tool_event(app, ToolEvent::Up { x: d[0], y: d[1] }, click_mods); }- // If this tool was not handled on pointer-down, fall back to a- // click-sized gesture while preserving pen pressure (mouse stays at 1).- tool_event(app, ToolEvent::Down { x: d[0], y: d[1], pressure: app.stylus.pressure() }, click_mods);- tap_at_peak_pressure(app);- tool_event(app, ToolEvent::Up { x: d[0], y: d[1] }, click_mods); } } }@@ -1173,8 +1173,20 @@ pub fn show(app: &mut PhotocraftApp, ctx: &Context) { choose(app, ctx, &command); } else if crate::canvas_popup::outside_pressed(ctx, id, area.response.rect) { app.ui.canvas_tool_menu = None;+ app.ui.canvas_tool_menu_dismissed = true; } } +/// Returns the menu rect if the canvas tool menu is currently open, for hit-testing+/// outside-click dismissal in the click handler.+pub fn menu_rect_if_open(app: &PhotocraftApp, ctx: &Context) -> Option<egui::Rect> {+ let menu = app.ui.canvas_tool_menu.as_ref()?;+ let id = menu_id();+ let screen = ctx.content_rect();+ let size = ctx.memory(|m| m.area_rect(id)).map_or(egui::vec2(210.0, 140.0), |r| r.size());+ let pos = egui::pos2(menu.pos[0].min(screen.right() - size.x).max(screen.left()), menu.pos[1].min(screen.bottom() - size.y).max(screen.top()));+ Some(egui::Rect::from_min_size(pos, size))+}+ #[cfg(test)] mod tests;@@ -287,6 +287,10 @@ pub struct UiState { /// Selection-tool context menu opened by a plain canvas right-click. #[serde(default)] pub canvas_tool_menu: Option<crate::canvas_tool_menu::CanvasToolMenu>,+ /// An outside press dismissed the canvas tool menu; suppress the corresponding+ /// release click from triggering a tool action.+ #[serde(default)]+ pub canvas_tool_menu_dismissed: bool, /// The guide a Move-tool click selected (`guide_select`): drawn blue, Delete removes it. #[serde(default)] pub selected_guide: Option<crate::guide_select::SelectedGuide>,@@ -507,6 +511,7 @@ impl Default for UiState { brush_picker_size: None, layer_menu: None, canvas_tool_menu: None,+ canvas_tool_menu_dismissed: false, selected_guide: None, brush_tool: Tool::Brush, tool_brushes: Vec::new(),