diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs
--- a/crates/ui-egui/src/canvas.rs
+++ b/crates/ui-egui/src/canvas.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);
                 }
             }
         }
diff --git a/crates/ui-egui/src/canvas/canvas_tool_menu.rs b/crates/ui-egui/src/canvas/canvas_tool_menu.rs
--- a/crates/ui-egui/src/canvas/canvas_tool_menu.rs
+++ b/crates/ui-egui/src/canvas/canvas_tool_menu.rs
@@ -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;
diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs
--- a/crates/ui-egui/src/state.rs
+++ b/crates/ui-egui/src/state.rs
@@ -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(),
