diff --git a/README.md b/README.md index b272705e40..d1e839323c 100644 --- a/README.md +++ b/README.md @@ -277,7 +277,7 @@ flatpak run ai.storyteller.photocraft The AppImage needs no install: the first run registers its launcher icon and menu entry in `~/.local/share` so the dock shows PhotoCraft's icon on Wayland. Set `PHOTOCRAFT_NO_DESKTOP_INTEGRATION=1` to skip that, and see [`docs/releasing.md`](docs/releasing.md) › Linux to undo it. -On a Wayland session, files dropped on the window don't open yet: winit 0.30, the windowing library under egui, has no Wayland drag and drop (#386). Use File › Open, or copy the image in your file manager and paste it with Ctrl+V. To drag and drop, start PhotoCraft under XWayland: `WAYLAND_DISPLAY= photocraft`, `WAYLAND_DISPLAY= ./photocraft--linux-x86_64.AppImage`, or `flatpak run --nosocket=wayland --socket=x11 ai.storyteller.photocraft`. On Wayland, PhotoCraft shows the command for your install in a notice until you dismiss it. +On a Wayland session, files dropped on the window don't open yet: winit 0.30, the windowing library under egui, has no Wayland drag and drop (#386). Use File › Open, or copy the image in your file manager and paste it with Ctrl+V. To drag and drop, start PhotoCraft under XWayland: `WAYLAND_DISPLAY= photocraft`, `WAYLAND_DISPLAY= ./photocraft--linux-x86_64.AppImage`, or `flatpak run --nosocket=wayland --socket=x11 ai.storyteller.photocraft`. On Wayland, PhotoCraft shows the command for your install in a notice until you dismiss it. Preferences › Performance › Linux display server = X11 does the same on every launch. With a fractional display scale (125%, 150%, 175%) an XWayland window is drawn at a lower resolution and enlarged by the compositor, so the interface is blurred there; native Wayland stays sharp (#1072). On macOS, the command-line tool comes as `photocraft-cli--macos-universal.zip`. The binary is signed with the same Developer ID as the app and notarized by Apple. A bare binary can't carry a stapled notarization ticket the way the DMG does, so the first time you run it macOS checks the notarization online. You can confirm it yourself: diff --git a/apps/photocraft/src/tablet.rs b/apps/photocraft/src/tablet.rs index 75c1388f94..fc063d2fbc 100644 --- a/apps/photocraft/src/tablet.rs +++ b/apps/photocraft/src/tablet.rs @@ -64,17 +64,35 @@ pub fn pen_needs_xwayland(session: DisplaySession, var: impl Fn(&str) -> Option< #[cfg(any(target_os = "linux", test))] const BTN_TOOL_PEN: u32 = 0x140; -/// Is a pen among the input devices `/proc/bus/input/devices` lists? Each device's `B: KEY=` line -/// is its key bitmap as hex words of the kernel's `long`, most significant first. +/// `ABS_X` and `ABS_Y` (linux/input-event-codes.h): the absolute axes every pen reports. +#[cfg(any(target_os = "linux", test))] +const ABS_X: u32 = 0x00; +#[cfg(any(target_os = "linux", test))] +const ABS_Y: u32 = 0x01; + +/// Is a pen among the input devices `/proc/bus/input/devices` lists? A pen is a device whose +/// `B: KEY=` bitmap has `BTN_TOOL_PEN` *and* whose `B: ABS=` bitmap has `ABS_X` and `ABS_Y` +/// (libinput asks the same of a tablet tool). The key alone is not enough: a virtual keyboard +/// made with uinput (RustDesk's, for one) claims every key code, `BTN_TOOL_PEN` included, but +/// has no absolute axes. Taking it for a pen sent the window through Xwayland, where a +/// fractionally scaled Wayland desktop shows it upscaled and blurred (#1072, #639). #[cfg(any(target_os = "linux", test))] pub fn has_pen(devices: &str) -> bool { - let bits = usize::BITS; - let word = usize::try_from(BTN_TOOL_PEN / bits).unwrap_or(usize::MAX); - devices.lines().filter_map(|l| l.strip_prefix("B: KEY=")).any(|keys| { - keys.split_whitespace().rev().nth(word).and_then(|w| u64::from_str_radix(w, 16).ok()).is_some_and(|w| (w >> (BTN_TOOL_PEN % bits)) & 1 == 1) + devices.split("\n\n").any(|device| { + let bitmap = |name: &str| device.lines().find_map(|l| l.strip_prefix(name)); + bitmap("B: KEY=").is_some_and(|keys| bit_set(keys, BTN_TOOL_PEN)) && bitmap("B: ABS=").is_some_and(|axes| bit_set(axes, ABS_X) && bit_set(axes, ABS_Y)) }) } +/// Bit `bit` of a `/proc/bus/input/devices` bitmap: hex words of the kernel's `long`, most +/// significant first. Short or malformed bitmaps have no bits set. +#[cfg(any(target_os = "linux", test))] +fn bit_set(bitmap: &str, bit: u32) -> bool { + let bits = usize::BITS; + let word = usize::try_from(bit / bits).unwrap_or(usize::MAX); + bitmap.split_whitespace().rev().nth(word).and_then(|w| u64::from_str_radix(w, 16).ok()).is_some_and(|w| (w >> (bit % bits)) & 1 == 1) +} + /// Start the XInput2 reader when eframe runs on X11 (Xwayland included). On native Wayland, /// `$DISPLAY` is Xwayland, which sees none of this window's input (and a pen sample from another /// X app would outlive a mouse stroke here), so nothing starts. @@ -238,6 +256,49 @@ mod tests { for keys in ["", "B: KEY=", "B: KEY=1", "B: KEY=zz 0 0 0 0 0", "B: KEY=ffffffffffffffffff 0 0 0 0 0", "B: KEY=0 0 0 0 0 0 0 0"] { assert!(!has_pen(keys), "{keys:?}"); } + for abs in ["B: ABS=", "B: ABS=zz", "B: ABS=ffffffffffffffffff", "B: ABS=0 0"] { + assert!(!has_pen(&format!("B: KEY=1c03 0 0 0 0 0\n{abs}")), "{abs:?}"); + } + } + + /// #1072: RustDesk's uinput keyboard claims every key code, `BTN_TOOL_PEN` included, and has + /// no absolute axes (`EV=20013`: no `EV_ABS`, no `B: ABS=` line). It is no pen, so the window + /// stays on native Wayland, where the fractional display scale renders sharp. The bitmaps are + /// the device's own, from `/proc/bus/input/devices`. + const VIRTUAL_KEYBOARD: &str = "I: Bus=0003 Vendor=1234 Product=5678 Version=0111\n\ + N: Name=\"RustDesk UInput Keyboard\"\n\ + P: Phys=\n\ + S: Sysfs=/devices/virtual/input/input54\n\ + U: Uniq=\n\ + H: Handlers=sysrq rfkill kbd event256 leds \n\ + B: PROP=0\n\ + B: EV=20013\n\ + B: KEY=ffffffffff 0 7ffffff000301ff 1000f7fffffff 7fe001fffff000f 7ffffffffffffff ffffffff0003fdff 7fff8fff00ff03ff 1ffffffffffff07 ffffffffffffffff ffffffffffefffff fffffffffffffffe\n\ + B: MSC=10\n\ + B: LED=7\n"; + + #[test] + fn a_virtual_keyboard_claiming_btn_tool_pen_is_no_pen() { + assert!( + bit_set( + "ffffffffff 0 7ffffff000301ff 1000f7fffffff 7fe001fffff000f 7ffffffffffffff ffffffff0003fdff 7fff8fff00ff03ff 1ffffffffffff07 ffffffffffffffff ffffffffffefffff fffffffffffffffe", + BTN_TOOL_PEN + ), + "the fixture claims the key" + ); + assert!(!has_pen(VIRTUAL_KEYBOARD)); + // Beside a real pen, the pen is still found; the keyboard's key bits never leak into the + // pen's device and the pen's axes never into the keyboard's. + let both = format!("{VIRTUAL_KEYBOARD}\n{WITH_PEN}"); + assert!(has_pen(&both)); + let pen_without_axes = WITH_PEN.lines().filter(|l| !l.starts_with("B: ABS=1000f")).collect::>().join("\n"); + assert!(!has_pen(&pen_without_axes), "a pen reports absolute positions"); + // A pen display or pen-enabled touchscreen reports the pen with its own axes. + let pen_display = "N: Name=\"Wacom One Pen\"\nB: KEY=1c03 0 0 0 0 0\nB: ABS=3\n"; + assert!(has_pen(pen_display)); + // Axes without `ABS_X` or `ABS_Y` don't count. + assert!(!has_pen("B: KEY=1c03 0 0 0 0 0\nB: ABS=1000f000002\n")); + assert!(!has_pen("B: KEY=1c03 0 0 0 0 0\nB: ABS=1000f000001\n")); } #[test] diff --git a/docs/development.md b/docs/development.md index 3c539f70bc..f685f653cc 100644 --- a/docs/development.md +++ b/docs/development.md @@ -111,7 +111,7 @@ By default PhotoCraft's own crates log at `info` and everything else at `warn`. | `PHOTOCRAFT_AUTOMATION_READ_ROOT` | Desktop app only: automation read root. Headless CLI modes require the `--automation-read-root` flag | | `PHOTOCRAFT_AUTOMATION_WRITE_ROOT` | Desktop app only: automation write root. Headless CLI modes require the `--automation-write-root` flag | | `PHOTOCRAFT_CPU_CANVAS=1` | Force the CPU canvas path instead of the wgpu shader canvas | -| `PHOTOCRAFT_NATIVE_WAYLAND=1` | Linux: stay on native Wayland when a pen is attached (by default the window then opens through Xwayland, because Wayland gives the app no pen input; #639) | +| `PHOTOCRAFT_NATIVE_WAYLAND=1` | Linux: stay on native Wayland when a pen is attached (by default the window then opens through Xwayland, because Wayland gives the app no pen input; #639). A pen is an input device with `BTN_TOOL_PEN` and the `ABS_X`/`ABS_Y` axes; virtual keyboards that claim every key are not pens (#1072) | | `WGPU_BACKEND=dx12` | Pick the wgpu backend(s) (`vulkan`, `dx12`, `metal`, `gl`); overrides `performance.gpuBackend` and the startup fallback | | `WGPU_DX12_COMPILER=fxc` | DX12 shader compiler (`fxc`, `dxc`, `auto`); `dxc` and `auto` look `dxcompiler.dll` up through the DLL search path | | `PHOTOCRAFT_GPU_TILE=2048` | Force GPU canvas tiling (tests tile seams) | diff --git a/docs/ui-design.md b/docs/ui-design.md index 13fd8b99f1..96a389d7f7 100644 --- a/docs/ui-design.md +++ b/docs/ui-design.md @@ -173,6 +173,15 @@ monitors. A 4K display the system runs at 100% stays at 100% (#1278). The fixed 300%) set an absolute UI scale, for example 200% on an unscaled 4K display. Canvas zoom shortcuts continue to control the document independently of UI scaling. +On a Wayland session with a fractional display scale, the interface is sharp only in a native +Wayland window, where the window is drawn at the display's physical resolution +(`wp_fractional_scale_v1`). Through Xwayland the compositor enlarges a lower-resolution window, so +everything is blurred and looks larger than the chosen scale (#1072). The window opens through +Xwayland when a pen is attached (#639), with Preferences › Performance › Linux display server = +X11, or with `WAYLAND_DISPLAY=` cleared; those are also the ways to get file drag and drop, which +native Wayland lacks (#386). Sharp text or drag and drop: on such a desktop it is one or the other +until winit supports Wayland drops. + Interface → UI Font Size changes interface text independently of UI Scale and canvas zoom. Tiny, Small (the default), Medium and Large use 10/12, 1, 14/12 and 16/12 of the theme's original font sizes, preserving the relative sizes of headings, captions and numeric fields. Apply or OK