diff --git a/apps/vectorcraft/src/main.rs b/apps/vectorcraft/src/main.rs index b80e79ff..9bd591d6 100644 --- a/apps/vectorcraft/src/main.rs +++ b/apps/vectorcraft/src/main.rs @@ -489,6 +489,8 @@ fn start_pick(request: PickRequest, parent: &Parent, ui: &egui::Context) -> Opti /// The host's services; `ui` is woken when a file dialog answers. fn services(parent: Parent, ui: egui::Context) -> Services { + use winit::raw_window_handle::{HasWindowHandle as _, RawWindowHandle}; + let wayland = parent.as_ref().and_then(|w| w.window_handle().ok()).is_some_and(|h| matches!(h.as_raw(), RawWindowHandle::Wayland(_))); let (p1, p2, p3, p4, p5) = (parent.clone(), parent.clone(), parent.clone(), parent.clone(), parent); Services { pick_open: Some(Box::new(move |pick: &FilePick| pick_now(PickRequest::Open(pick.clone()), &p1).into_iter().next())), @@ -498,6 +500,7 @@ fn services(parent: Parent, ui: egui::Context) -> Services { // dialogs run the window's events while they are open. start_pick: cfg!(unix).then(|| Box::new(move |request: PickRequest| start_pick(request, &p5, &ui)) as vectorcraft_ui_egui::picks::StartPick), dialogs_are_sheets: cfg!(target_os = "macos"), + wayland, read: Some(Box::new(|p: &str| std::fs::read(p).map_err(|e| e.to_string()))), write: Some(Box::new(write_file)), // Background Save and Export write from a worker thread. diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 8d3ac45f..fbabb16a 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -492,7 +492,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { 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. - Some(_) if custom && crate::cursors::OS_CURSORS => { + 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) diff --git a/crates/ui-egui/src/cursors.rs b/crates/ui-egui/src/cursors.rs index 8fb77cfa..acc800d0 100644 --- a/crates/ui-egui/src/cursors.rs +++ b/crates/ui-egui/src/cursors.rs @@ -386,6 +386,14 @@ pub fn paint(painter: &Painter, c: Cursor, p: Pos2, scale: f32) -> bool { /// bitmap cursors there): those paint them into the window ([`paint`]). pub const OS_CURSORS: bool = !cfg!(any(target_os = "macos", target_arch = "wasm32")); +/// Does this window show the glyphs as OS cursors, on a build that can ([`OS_CURSORS`])? Not on +/// Wayland, where the compositor decides how big a cursor bitmap shows: Hyprland shows each pixel +/// as a point, twice too big on a 2× display (#1645). The window paints them there, as PhotoCraft +/// and PdfCraft do. +pub fn os_cursors(build: bool, wayland: bool) -> bool { + build && !wayland +} + /// 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. @@ -655,6 +663,21 @@ mod tests { assert!(off.platform_output.cursor_image.is_none()); } + /// On Wayland the glyph is painted and the pointer hidden over the canvas, on every build (#1645). + #[test] + fn a_wayland_window_paints_the_tool_cursor() { + let mut app = VectorcraftApp::new(Session::new(), crate::Services { wayland: true, ..Default::default() }); + app.run("file.new", json!({"width": 400, "height": 300})).unwrap(); + let ctx = egui::Context::default(); + for _ in 0..3 { + frame(&mut app, &ctx, vec![]); + } + let over = app.canvas_rect.unwrap().center(); + let out = frame(&mut app, &ctx, vec![egui::Event::PointerMoved(over)]); + assert!(out.platform_output.cursor_image.is_none()); + assert_eq!(out.platform_output.cursor_icon, egui::CursorIcon::None); + } + /// 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] @@ -692,6 +715,13 @@ mod tests { assert!(full.contains(pos2(100.0, 100.0)) && half.contains(pos2(100.0, 100.0)), "{full:?} {half:?}"); } + #[test] + fn wayland_windows_paint_the_glyphs() { + assert!(os_cursors(true, false), "Windows, X11"); + assert!(!os_cursors(true, true), "Wayland: the compositor would size the bitmap (#1645)"); + assert!(!os_cursors(false, false) && !os_cursors(false, true), "macOS, the web"); + } + #[test] fn arrow_hotspot_is_tip() { let pts = arrow_points(pos2(10.0, 20.0)); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 441e48e9..5462a291 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -261,6 +261,9 @@ pub struct Services { /// [`Self::start_pick`]'s dialogs are sheets on the window (macOS): while one is open the app /// neither quits nor closes documents, as under the modal dialog before. pub dialogs_are_sheets: bool, + /// The window is on Wayland: the tool cursors are painted into it instead of handed to the + /// compositor as bitmaps ([`cursors::os_cursors`]). + pub wayland: bool, } /// Cached canvas raster.