diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 038b081dd..34c48c301 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -530,13 +530,21 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let ui_scale = if app.session.prefs.scale_cursor_with_ui { 1.0 } else { ui.ctx().zoom_factor() }; match ui.input(|i| i.pointer.hover_pos()) { // An OS cursor: the system moves it at once, where a painted one trails the pointer - // (#444). The system cursor stands in for cursors without a glyph. + // (#444). The system cursor stands in for cursors without a glyph. Over a window + // without the focus, macOS shows its own pointer whichever cursor the window sets, + // and the glyph is not painted there. Some(_) if custom && crate::cursors::os_cursors(crate::cursors::OS_CURSORS, app.services.wayland) => { let ppp = ui.ctx().pixels_per_point() / ui_scale; ui.ctx().set_cursor_image(app.canvas.cursors.get(c, ppp)); cursor_icon(c) } - Some(hp) if custom && crate::cursors::paint(&painter, c, hp, 1.0 / ui_scale) => egui::CursorIcon::None, + Some(hp) + if custom + && crate::cursors::paint_glyphs(crate::cursors::HIDES_ONLY_WHEN_KEY, ui.input(|i| i.focused)) + && crate::cursors::paint(&painter, c, hp, 1.0 / ui_scale) => + { + egui::CursorIcon::None + } _ => cursor_icon(c), } } else { diff --git a/crates/ui-egui/src/cursors.rs b/crates/ui-egui/src/cursors.rs index bf199301a..febf7083c 100644 --- a/crates/ui-egui/src/cursors.rs +++ b/crates/ui-egui/src/cursors.rs @@ -395,6 +395,18 @@ pub fn os_cursors(build: bool, wayland: bool) -> bool { build && !wayland } +/// Does the window hide the pointer only while it is the key window? macOS does: winit hides it with +/// a cursor rect holding a transparent cursor, and macOS applies the rect only in the key window. +/// Over a window behind another app or under a sheet, the system pointer shows (#2020). +pub const HIDES_ONLY_WHEN_KEY: bool = cfg!(target_os = "macos"); + +/// Does the window paint the glyphs over a hidden pointer, `focused` or not? Not where the system +/// shows its pointer over a window without the focus ([`HIDES_ONLY_WHEN_KEY`]): the system pointer +/// shows alone there. +pub fn paint_glyphs(hides_only_when_key: bool, focused: bool) -> bool { + focused || !hides_only_when_key +} + /// The cursor bitmaps handed to the OS, by (cursor, pixels per point): each glyph is rasterized /// once, and handing the same `Arc` every frame keeps the window's OS cursor instead of making a /// new one. @@ -650,6 +662,20 @@ mod tests { out } + /// One [`frame`] with `focused` set: true for a window that has the focus, false while another app + /// or a sheet has it. + fn focus_frame(app: &mut VectorcraftApp, ctx: &egui::Context, focused: bool, events: Vec) -> egui::FullOutput { + let time = Some(ctx.input(|i| i.time) + 0.1); + let screen_rect = Some(egui::Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0))); + let raw = egui::RawInput { screen_rect, time, focused, events, ..Default::default() }; + let mut out = ctx.run_ui(raw, |ui| { + app.logic(ui.ctx()); + app.ui(ui); + }); + out.textures_delta.clear(); + out + } + #[test] fn the_canvas_hands_the_tool_cursor_to_the_os_and_idles_while_the_pointer_rests() { let mut app = VectorcraftApp::new(Session::new(), Default::default()); @@ -701,6 +727,32 @@ mod tests { assert_eq!(out.platform_output.cursor_icon, egui::CursorIcon::None); } + /// On macOS a window without the focus shows the system pointer over the canvas, and the glyph + /// is not painted next to it (#2020). With the focus back, the canvas sets `CursorIcon::None` again + /// on the next frame, without a pointer move. + #[test] + fn a_window_without_the_focus_shows_the_system_cursor_alone_on_macos() { + // Wayland: every desktop build paints the glyphs. + let mut app = VectorcraftApp::new(Session::new(), crate::Services { wayland: true, ..Default::default() }); + app.run("file.new", json!({"width": 400, "height": 300})).unwrap(); + app.run("tool.select", json!({"tool": "rectangle"})).unwrap(); + let ctx = egui::Context::default(); + for _ in 0..3 { + frame(&mut app, &ctx, vec![]); + } + let over = app.canvas_rect.unwrap().center(); + let key = focus_frame(&mut app, &ctx, true, vec![egui::Event::PointerMoved(over)]); + assert_eq!(key.platform_output.cursor_icon, egui::CursorIcon::None, "with the focus: the glyph alone"); + let away = focus_frame(&mut app, &ctx, false, vec![egui::Event::WindowFocused(false), egui::Event::PointerMoved(over + vec2(5.0, 3.0))]); + if HIDES_ONLY_WHEN_KEY { + assert_eq!(away.platform_output.cursor_icon, egui::CursorIcon::Crosshair, "macOS without the focus: the system pointer alone"); + } else { + assert_eq!(away.platform_output.cursor_icon, egui::CursorIcon::None, "other platforms: the glyph without the focus too"); + } + let back = focus_frame(&mut app, &ctx, true, vec![egui::Event::WindowFocused(true)]); + assert_eq!(back.platform_output.cursor_icon, egui::CursorIcon::None, "the focus back: the glyph alone"); + } + /// User Interface › Scale Cursor Proportional to UI (#394): off, the tool cursors keep their /// size whatever the UI Scaling; on, they grow with it. #[test] @@ -745,6 +797,13 @@ mod tests { assert!(!os_cursors(false, false) && !os_cursors(false, true), "macOS, the web"); } + #[test] + fn macos_windows_paint_the_glyphs_only_with_the_focus() { + assert!(paint_glyphs(true, true), "macOS with the focus"); + assert!(!paint_glyphs(true, false), "macOS without the focus: the system pointer shows (#2020)"); + assert!(paint_glyphs(false, true) && paint_glyphs(false, false), "Wayland, the web: painted with the focus or without"); + } + #[test] fn arrow_hotspot_is_tip() { let pts = arrow_points(pos2(10.0, 20.0)); diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 76604e062..d3ecd5c5a 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -608,7 +608,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur anchor or with Alt held over a path (convert) and over another open path's end (join). The Eyedropper has an original pipette glyph with its tip at the hotspot; Use Precise Cursors restores the crosshair. The same glyph feeds OS bitmaps and painted cursors. DPI/hotspot and preference checks in `cursors.rs` and `tests_prefs.rs` cover #1269; - actual OS display and Caps Lock remain unverified. Behaviour reference: [public tool-pointer documentation](https://helpx.adobe.com/illustrator/using/tools.html#change_tool_pointers). Tool cursors are painted into the window on Wayland (#1652; not run on Linux, #1645 stays open until confirmed). A Precise Cursors command toggles the preference and can take a shortcut (#1663). + actual OS display and Caps Lock remain unverified. Behaviour reference: [public tool-pointer documentation](https://helpx.adobe.com/illustrator/using/tools.html#change_tool_pointers). Tool cursors are painted into the window on Wayland (#1652; not run on Linux, #1645 stays open until confirmed). A Precise Cursors command toggles the preference and can take a shortcut (#1663). On macOS the tool cursor is painted only while the window has the focus. Over a window behind another app, or under the File › Open… sheet, the system pointer shows alone instead of next to the painted cursor (#2020). - **Performance:** 20k shapes + 1k texts render in 27 ms per full-retina frame (7.8 ms zoomed), 7× faster than the first version. The UI thread never blocks. The web build is 7.1 MB gzipped. Print-size images (more than 512 MB decoded, up to 2 GB) draw in the canvas and in raster exports and can be trimmed with Crop Image; an image with a side past 65,535 px is shrunk to fit, a raster export says when an image could not be decoded, and Crop Image's trim leaves CMYK images alone (#1517). On Windows `vectorcraft-cli perf` reports the cores busy and, with `--deep`, its working set, and warns on a busy machine as it does on macOS and Linux (#1502). With `--deep`, each memory row of `vectorcraft-cli perf` also gives the peak so far: the peak working set on Windows and the peak resident memory (`VmHWM`) on Linux. A new last row gives the memory after the SVG export and the save → open → save at the end of the run. The renderer composes the two ink planes of a CMYK document that shows transparency into screen colors on up to as many threads as it rasterizes with, instead of on one. At 2880 × 1800 on 8 threads, a page of 300 shapes at 20% to 90% opacity takes 30 ms a frame instead of 55 ms, and 50,000 paths with one at 99% opacity take 258 ms instead of 361 ms (`vectorcraft-cli bench`), with the same pixels.