diff --git a/.github/workflows/build-packages.yml b/.github/workflows/build-packages.yml index 945ddd03f..ceaafebea 100644 --- a/.github/workflows/build-packages.yml +++ b/.github/workflows/build-packages.yml @@ -43,7 +43,7 @@ jobs: rpm - name: Set up Rust - uses: dtolnay/rust-toolchain@1.95.0 + uses: dtolnay/rust-toolchain@1.98.1 - name: Install nfpm run: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f5b582920..ba9bd7e8f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,7 +59,7 @@ jobs: echo "LD_LIBRARY_PATH=$RUNNER_TEMP/gtk4-layer-shell/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" >> "$GITHUB_ENV" - name: Set up Rust - uses: dtolnay/rust-toolchain@1.95.0 + uses: dtolnay/rust-toolchain@1.98.1 with: components: clippy, rustfmt diff --git a/AGENTS.md b/AGENTS.md index f5c765f93..cc07a872a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ - This file applies to the whole wayscriber workspace. - wayscriber is a Rust desktop annotation app for Wayland compositors. - The workspace root crate is `wayscriber`; `configurator/` is a separate GTK4/libadwaita (Relm4) desktop configurator crate. -- Both crates use Rust 1.95 and edition 2024. +- Both crates use Rust 1.98.1 and edition 2024; `rust-toolchain.toml` pins the development toolchain. - The workspace default member is only the root crate; use `-p wayscriber-configurator` or `--workspace` when configurator coverage is needed. ## Architecture diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2d3195c73..f6b17596f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,9 +15,12 @@ module boundary. ## Workspace and toolchain -The repository is one Cargo workspace using Rust 1.95 and edition 2024. The -Nix flake compiles with `nixpkgs` rustc from `flake.lock`; after raising -`rust-version`, run `nix flake update` so that lock is new enough. +The repository is one Cargo workspace using Rust 1.98.1 and edition 2024. +`rust-toolchain.toml` pins the development toolchain, including Clippy and rustfmt; CI uses +the same version. The Nix flake selects that version through the `rust-overlay` input pinned +in `flake.lock`, using the root package's `rust-version` for both crates and the development +shell. When raising the version, update both manifests, `rust-toolchain.toml`, and CI pins; +run `nix flake update rust-overlay` if the locked overlay does not yet provide it. - the root `wayscriber` package owns the overlay, daemon, CLI, shared domain/config/session code, rendering, and integration tests; diff --git a/Cargo.toml b/Cargo.toml index b1fcd4c32..33c982500 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "wayscriber" version = "0.9.24" edition = "2024" -rust-version = "1.95" +rust-version = "1.98.1" description = "Screen annotation tool for Wayland compositors" homepage = "https://wayscriber.com" repository = "https://github.com/devmobasa/wayscriber" diff --git a/README.md b/README.md index d9bd6f239..4cd4a395a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # wayscriber [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -[![Rust](https://img.shields.io/badge/rust-1.95%2B-orange.svg)](https://www.rust-lang.org/) +[![Rust](https://img.shields.io/badge/rust-1.98.1%2B-orange.svg)](https://www.rust-lang.org/) A ZoomIt-like real-time screen annotation tool for Linux/Wayland, written in Rust. @@ -480,7 +480,7 @@ sudo dnf install ./wayscriber-configurator-x86_64.rpm ### From source -Rust 1.95 or newer is required. If `rustup` is not already installed, install it (this also installs `cargo`), then load Cargo into your current shell: +Rust 1.98.1 or newer is required. If `rustup` is not already installed, install it (this also installs `cargo`), then load Cargo into your current shell: ```bash curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh @@ -492,12 +492,13 @@ Then clone the repository — the dependency and build steps below run from insi ```bash git clone https://github.com/devmobasa/wayscriber.git cd wayscriber -rustup toolchain install 1.95.0 -rustup override set 1.95.0 +rustup toolchain install 1.98.1 --component clippy --component rustfmt cargo --version ``` -The override selects Rust 1.95 only inside this checkout and leaves your global default unchanged. You can instead keep any existing default that is already Rust 1.95 or newer. +The repository's `rust-toolchain.toml` selects Rust 1.98.1 inside this checkout. If an older +directory override is already set, remove it with `rustup override unset` from the checkout +so the file can select the toolchain. **Dependencies:** diff --git a/configurator/Cargo.toml b/configurator/Cargo.toml index cc116dd90..9ea818895 100644 --- a/configurator/Cargo.toml +++ b/configurator/Cargo.toml @@ -2,7 +2,7 @@ name = "wayscriber-configurator" version = "0.9.24" edition = "2024" -rust-version = "1.95" +rust-version = "1.98.1" [lints] workspace = true diff --git a/configurator/README.md b/configurator/README.md index ad67e0d9d..32233a5aa 100644 --- a/configurator/README.md +++ b/configurator/README.md @@ -8,7 +8,7 @@ This file covers building and running the configurator from source. For screensh ## Prerequisites -- Rust toolchain 1.95 or newer. +- Rust toolchain 1.98.1 or newer (`../rust-toolchain.toml` pins development builds). - System development packages for GTK 4 and libadwaita. ## Run It diff --git a/docs/codebase-overview.md b/docs/codebase-overview.md index 0e7aebaa2..c9f2af461 100644 --- a/docs/codebase-overview.md +++ b/docs/codebase-overview.md @@ -79,6 +79,8 @@ Daemon mode therefore provides a persistent background service that reacts to us `WaylandState` coordinates the runtime owners handlers need. `FocusState` owns activation, focus, and startup acquisition; `ProtocolGlobals` owns bound globals and toolkit handler state; `PointerRuntime` owns pointer position, board-pan and chrome gestures, cursor, pointer-lock, and single-contact touch protocol lifecycles; `ToolbarChrome` owns toolbar placement, inline interaction, and fade state; `ToolbarDrag` owns built-in and GTK drag lifecycles; `RegionCaptureRuntime` owns region selection generations, active/review/window-snap state, and the window-query and cut-preview workers; `AcquisitionRuntime` owns the capacity-one screen-acquisition and zoom-waiter registries plus eyedropper source correlation; `FrozenState` owns its availability and one-shot startup gate; `SurfaceState` owns output/fullscreen/layer placement and frozen-fullscreen transitions; `OverlaySuppressionState` owns suppression reason, keyboard policy, capture barrier, and clickthrough state; `RenderRuntime` owns the canvas layer cache, render-profile baseline, and per-effect damage history; `InputHudRuntime` owns system-reader lifecycle and reconciliation; `SpotlightRuntime` owns render memory, warning latches, and wheel timing; `ClipboardRuntime` owns single-flight clipboard workers and queue policy; `PreferenceStores` groups durable preference stores and workers; `UiAnimationClock` owns animation scheduling; and `FontCatalogPrewarm` owns the one-shot font scan. The root retains cross-owner coordination. `handlers::route::SurfaceRouter` is the single classifier for pointer, touch, and stylus surfaces and supplies overlay screen coordinates before modality-specific dispatch. +Within `PointerRuntime`, pending chrome targets and device-owned release suppression have separate lifecycles. Clearing a toast, HUD, or zoom-chip press preserves mouse and touch suppression; cancelling a touch clears only its own release latch. + Freeze capture waits for the overlay-suppression frame, then selects `wlr-screencopy`, `ext-image-copy-capture`, or the screenshot portal in that order. The two direct protocols capture the active output into shared memory; the portal captures the desktop and the client crops the selected output when needed. Direct capture and portal crop both require compositor-reported output pixels; a missing current mode fails instead of guessing from the overlay buffer. --- diff --git a/flake.lock b/flake.lock index de06ac71c..695af92f6 100644 --- a/flake.lock +++ b/flake.lock @@ -37,7 +37,28 @@ "root": { "inputs": { "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1788505699, + "narHash": "sha256-PSPDCdbEBEbSDCWcqv5GkbyD2ACQ9Diz6vmEbWdy4dM=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "9eccf73c5b810052f08aa77ae0548c383259f17f", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" } }, "systems": { diff --git a/flake.nix b/flake.nix index 2317c89cd..61a9e08a1 100644 --- a/flake.nix +++ b/flake.nix @@ -4,27 +4,41 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; - outputs = { self, nixpkgs, flake-utils }: + outputs = { self, nixpkgs, flake-utils, rust-overlay }: # Linux only. eachDefaultSystem also evaluates x86_64-darwin, which # nixpkgs 26.11 dropped. flake-utils.lib.eachSystem [ "x86_64-linux" "aarch64-linux" ] (system: let - pkgs = import nixpkgs { inherit system; }; + pkgs = import nixpkgs { + inherit system; + overlays = [ rust-overlay.overlays.default ]; + }; cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml); version = cargoToml.package.version; rustVersion = cargoToml.package.rust-version; + rustToolchain = pkgs.rust-bin.stable.${rustVersion}.default.override { + extensions = [ "rust-analyzer" ]; + }; + rustPlatform = pkgs.makeRustPlatform { + cargo = rustToolchain; + rustc = rustToolchain; + }; servicePath = pkgs.lib.makeBinPath [ pkgs.grim pkgs.slurp pkgs.wl-clipboard ]; in # Instantiation (and CI `nix eval`) must fail before cargoBuildHook - # when flake.lock's nixpkgs rustc lags Cargo.toml's rust-version. + # when the selected compiler lags Cargo.toml's rust-version. assert pkgs.lib.assertMsg - (pkgs.lib.versionAtLeast pkgs.rustc.version rustVersion) - "nixpkgs rustc ${pkgs.rustc.version} is older than Cargo.toml rust-version ${rustVersion}; run `nix flake update`."; + (pkgs.lib.versionAtLeast rustToolchain.version rustVersion) + "selected rustc ${rustToolchain.version} is older than Cargo.toml rust-version ${rustVersion}; run `nix flake update rust-overlay`."; { packages = { - wayscriber = pkgs.rustPlatform.buildRustPackage { + wayscriber = rustPlatform.buildRustPackage { pname = "wayscriber"; inherit version; src = ./.; @@ -78,7 +92,7 @@ }; }; - wayscriber-configurator = pkgs.rustPlatform.buildRustPackage { + wayscriber-configurator = rustPlatform.buildRustPackage { pname = "wayscriber-configurator"; inherit version; src = ./.; @@ -127,11 +141,7 @@ devShells.default = pkgs.mkShell { buildInputs = with pkgs; [ - cargo - rustc - rust-analyzer - clippy - rustfmt + rustToolchain pkg-config cairo pango diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 000000000..e63004962 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.98.1" +profile = "minimal" +components = ["clippy", "rustfmt"] diff --git a/src/about_window/icon.rs b/src/about_window/icon.rs index 87344b205..647b503d6 100644 --- a/src/about_window/icon.rs +++ b/src/about_window/icon.rs @@ -86,7 +86,7 @@ mod tests { fn transparent_pixels_are_fully_premultiplied_away() { let (_, _, argb) = decode_premultiplied_argb(ICON_PNG).expect("icon decodes"); - for pixel in argb.chunks_exact(4) { + for pixel in argb.as_chunks::<4>().0 { let alpha = pixel[3]; assert!( pixel[0] <= alpha && pixel[1] <= alpha && pixel[2] <= alpha, diff --git a/src/backend/wayland/frozen/image.rs b/src/backend/wayland/frozen/image.rs index 9b62d5306..be1f9335a 100644 --- a/src/backend/wayland/frozen/image.rs +++ b/src/backend/wayland/frozen/image.rs @@ -148,7 +148,7 @@ pub(in crate::backend::wayland) fn copy_shm_argb( } if format == wl_shm::Format::Xrgb8888 { - for pixel in data.chunks_exact_mut(4) { + for pixel in data.as_chunks_mut::<4>().0 { pixel[3] = 0xff; } } @@ -252,7 +252,13 @@ mod tests { } fn values(image: &FrozenImage) -> Vec { - image.data.chunks_exact(4).map(|chunk| chunk[0]).collect() + image + .data + .as_chunks::<4>() + .0 + .iter() + .map(|chunk| chunk[0]) + .collect() } #[test] diff --git a/src/backend/wayland/handlers/AGENTS.md b/src/backend/wayland/handlers/AGENTS.md index 0faa553fc..a2bc72219 100644 --- a/src/backend/wayland/handlers/AGENTS.md +++ b/src/backend/wayland/handlers/AGENTS.md @@ -12,6 +12,7 @@ ## Invariants - Keep handlers thin; do not bury durable business logic in protocol callbacks. - Preserve coordinate transforms, modifier synchronization, seat/device lifetimes, frame/callback ordering, and tablet feature gating. +- Keep pending chrome targets separate from device-owned release suppression; target cleanup preserves both latches, and touch cancellation clears only Touch suppression. - Route protocol surfaces through `SurfaceRouter`; do not add modality-specific canvas/toolbar classifiers. - Avoid blocking protocol callback paths. diff --git a/src/backend/wayland/handlers/compositor.rs b/src/backend/wayland/handlers/compositor.rs index 15e96c48c..1fa0ce8ca 100644 --- a/src/backend/wayland/handlers/compositor.rs +++ b/src/backend/wayland/handlers/compositor.rs @@ -119,7 +119,6 @@ impl CompositorHandler for WaylandState { let previous_output = self.surface.current_output(); let output_changed = previous_output.as_ref() != Some(output); self.surface.set_current_output(output.clone()); - self.focus.note_surface_enter(); if output_changed { // Keep layer-shell toolbars pinned to the monitor that owns the drawing surface. self.toolbar_chrome.set_needs_recreate(true); @@ -171,9 +170,6 @@ impl CompositorHandler for WaylandState { debug!("Surface left output"); self.surface.clear_output(output); - if self.surface.current_output().is_none() { - self.focus.clear_surface_enter(); - } self.refresh_active_output_label(); self.frozen.set_active_output(None, None); self.zoom.set_active_output(None, None); diff --git a/src/backend/wayland/handlers/output.rs b/src/backend/wayland/handlers/output.rs index 3af5871dc..724f7e953 100644 --- a/src/backend/wayland/handlers/output.rs +++ b/src/backend/wayland/handlers/output.rs @@ -47,9 +47,6 @@ impl OutputHandler for WaylandState { ) { debug!("Output destroyed"); self.surface.clear_output(&output); - if self.surface.current_output().is_none() { - self.focus.clear_surface_enter(); - } self.refresh_active_output_label(); // SCTK 0.20 calls this before removing the output from OutputState, so // a walk of current outputs would still include it. Exclude it here; diff --git a/src/backend/wayland/handlers/touch.rs b/src/backend/wayland/handlers/touch.rs index 18c12e93b..8024c8e75 100644 --- a/src/backend/wayland/handlers/touch.rs +++ b/src/backend/wayland/handlers/touch.rs @@ -122,6 +122,8 @@ impl WaylandState { // is dragging is untouched. self.cancel_region_selection_from(RegionInputSource::Touch); self.pointer.clear_chrome_press(); + self.pointer + .clear_suppressed_release(RegionInputSource::Touch); self.input_state .clear_help_overlay_press_for(HelpOverlayPressSource::Touch); diff --git a/src/backend/wayland/handlers/xdg.rs b/src/backend/wayland/handlers/xdg.rs index 21d4ac03e..6a11769c4 100644 --- a/src/backend/wayland/handlers/xdg.rs +++ b/src/backend/wayland/handlers/xdg.rs @@ -90,7 +90,6 @@ impl WindowHandler for WaylandState { && let Some(output) = self.protocol.output().outputs().next() { self.surface.set_current_output(output); - self.focus.clear_surface_enter(); } self.refresh_active_output_label(); diff --git a/src/backend/wayland/state/buffer_damage.rs b/src/backend/wayland/state/buffer_damage.rs index 7cbf158ba..043c63226 100644 --- a/src/backend/wayland/state/buffer_damage.rs +++ b/src/backend/wayland/state/buffer_damage.rs @@ -275,7 +275,7 @@ impl BufferDamageTracker { (true, full_reason, Vec::new()) } else { // Take and deduplicate regions - let mut regions: Vec = damage.regions.drain(..).collect(); + let mut regions = std::mem::take(&mut damage.regions); regions.retain(Rect::is_valid); (false, None, regions) } diff --git a/src/backend/wayland/state/core/output/focus.rs b/src/backend/wayland/state/core/output/focus.rs index 209974de2..1c27e4d65 100644 --- a/src/backend/wayland/state/core/output/focus.rs +++ b/src/backend/wayland/state/core/output/focus.rs @@ -83,7 +83,6 @@ impl WaylandState { window.set_fullscreen(Some(&target_output)); window.commit(); self.surface.set_current_output(target_output); - self.focus.clear_surface_enter(); self.refresh_active_output_label(); self.begin_session_output_transition(target_identity, "output switch"); self.request_xdg_activation(qh); @@ -101,7 +100,6 @@ impl WaylandState { self.teardown_keyboard_focus(); self.recreate_layer_surface_for_output(qh, &target_output); self.surface.set_current_output(target_output); - self.focus.clear_surface_enter(); self.refresh_active_output_label(); self.begin_session_output_transition(target_identity, "output switch"); self.input_state.needs_redraw = true; diff --git a/src/backend/wayland/state/focus.rs b/src/backend/wayland/state/focus.rs index 80a7f07ba..947ac61c0 100644 --- a/src/backend/wayland/state/focus.rs +++ b/src/backend/wayland/state/focus.rs @@ -19,7 +19,6 @@ pub(in crate::backend::wayland) struct FocusState { has_pointer_focus: bool, current_seat: Option, last_activation_serial: Option, - has_seen_surface_enter: bool, overlay_ready: bool, suppress_focus_exit_until: Option, xdg_close_guard_until: Option, @@ -37,7 +36,6 @@ impl FocusState { has_pointer_focus: false, current_seat: None, last_activation_serial: None, - has_seen_surface_enter: false, overlay_ready: false, suppress_focus_exit_until: None, xdg_close_guard_until: None, @@ -203,14 +201,6 @@ impl FocusState { std::mem::take(&mut self.xdg_explicit_close_requested) } - pub(in crate::backend::wayland) fn note_surface_enter(&mut self) { - self.has_seen_surface_enter = true; - } - - pub(in crate::backend::wayland) fn clear_surface_enter(&mut self) { - self.has_seen_surface_enter = false; - } - pub(in crate::backend::wayland) fn activation_token_to_apply(&self) -> Option { self.pending_activation_token.clone() } diff --git a/src/backend/wayland/state/pointer_runtime.rs b/src/backend/wayland/state/pointer_runtime.rs index 37ebec75e..5c8d29235 100644 --- a/src/backend/wayland/state/pointer_runtime.rs +++ b/src/backend/wayland/state/pointer_runtime.rs @@ -136,11 +136,6 @@ impl ReleaseSuppression { fn take(&mut self, source: RegionInputSource) -> bool { self.slot_mut(source).is_some_and(std::mem::take) } - - fn clear_all(&mut self) { - self.clear(RegionInputSource::Pointer); - self.clear(RegionInputSource::Touch); - } } #[derive(Debug, Clone, Copy, Default)] @@ -148,7 +143,6 @@ struct PendingChromePress { toast: Option, status_hud: bool, zoom_chip: ZoomChipPress, - release_suppression: ReleaseSuppression, } impl PendingChromePress { @@ -160,7 +154,6 @@ impl PendingChromePress { self.toast = None; self.status_hud = false; self.zoom_chip = ZoomChipPress::None; - self.release_suppression.clear_all(); } fn arm_toast(&mut self, press: ToastPress) -> bool { @@ -214,6 +207,7 @@ pub(in crate::backend::wayland) struct PointerRuntime { position: (i32, i32), board_pan: BoardPanGesture, chrome_press: PendingChromePress, + release_suppression: ReleaseSuppression, } impl PointerRuntime { @@ -230,6 +224,7 @@ impl PointerRuntime { position: (0, 0), board_pan: BoardPanGesture::default(), chrome_press: PendingChromePress::default(), + release_suppression: ReleaseSuppression::default(), } } @@ -437,6 +432,8 @@ impl PointerRuntime { } pub(in crate::backend::wayland) fn clear_chrome_press(&mut self) { + // Another device can still owe a swallowed release when chrome targets + // reset for a new press or release cleanup. self.chrome_press.clear(); } @@ -468,14 +465,21 @@ impl PointerRuntime { } pub(in crate::backend::wayland) fn suppress_release(&mut self, source: RegionInputSource) { - self.chrome_press.release_suppression.arm(source); + self.release_suppression.arm(source); + } + + pub(in crate::backend::wayland) fn clear_suppressed_release( + &mut self, + source: RegionInputSource, + ) { + self.release_suppression.clear(source); } pub(in crate::backend::wayland) fn take_suppressed_release( &mut self, source: RegionInputSource, ) -> bool { - self.chrome_press.release_suppression.take(source) + self.release_suppression.take(source) } fn reset_cursor_cache(&mut self) { @@ -557,18 +561,24 @@ mod tests { } #[test] - fn clearing_chrome_press_empties_targets_and_release_latches() { + fn clearing_chrome_press_preserves_both_release_latches() { let mut runtime = PointerRuntime::new(); assert!(runtime.arm_status_hud_press()); runtime.suppress_release(RegionInputSource::Pointer); runtime.suppress_release(RegionInputSource::Touch); runtime.clear_chrome_press(); - assert!(!runtime.take_status_hud_press()); + + assert!(runtime.arm_toast_press(ToastPress::body(7))); + runtime.clear_chrome_press(); + assert_eq!(runtime.take_toast_press(), None); + + assert!(runtime.arm_zoom_chip_press(ZoomChipPress::Passive)); + runtime.clear_chrome_press(); assert_eq!(runtime.take_zoom_chip_press(), ZoomChipPress::None); - assert!(!runtime.take_suppressed_release(RegionInputSource::Pointer)); - assert!(!runtime.take_suppressed_release(RegionInputSource::Touch)); + assert!(runtime.take_suppressed_release(RegionInputSource::Pointer)); + assert!(runtime.take_suppressed_release(RegionInputSource::Touch)); } #[test] @@ -576,10 +586,7 @@ mod tests { let mut runtime = PointerRuntime::new(); runtime.suppress_release(RegionInputSource::Pointer); runtime.suppress_release(RegionInputSource::Touch); - runtime - .chrome_press - .release_suppression - .clear(RegionInputSource::Touch); + runtime.clear_suppressed_release(RegionInputSource::Touch); assert!(!runtime.take_suppressed_release(RegionInputSource::Touch)); assert!(runtime.take_suppressed_release(RegionInputSource::Pointer)); @@ -587,6 +594,82 @@ mod tests { assert!(!runtime.take_suppressed_release(RegionInputSource::Stylus)); } + #[test] + fn pointer_cleanup_preserves_a_pending_touch_release() { + let mut runtime = PointerRuntime::new(); + runtime.suppress_release(RegionInputSource::Pointer); + runtime.suppress_release(RegionInputSource::Touch); + + assert!(runtime.take_suppressed_release(RegionInputSource::Pointer)); + runtime.clear_chrome_press(); + + assert!(!runtime.take_suppressed_release(RegionInputSource::Pointer)); + assert!(runtime.take_suppressed_release(RegionInputSource::Touch)); + assert!(!runtime.take_suppressed_release(RegionInputSource::Touch)); + } + + #[test] + fn touch_cancellation_cleanup_preserves_a_pending_pointer_release() { + let mut runtime = PointerRuntime::new(); + runtime.suppress_release(RegionInputSource::Pointer); + runtime.suppress_release(RegionInputSource::Touch); + + runtime.clear_chrome_press(); + runtime.clear_suppressed_release(RegionInputSource::Touch); + + assert!(!runtime.take_suppressed_release(RegionInputSource::Touch)); + assert!(runtime.take_suppressed_release(RegionInputSource::Pointer)); + } + + #[test] + fn held_eyedropper_pointer_release_does_not_finish_a_new_touch_stroke() { + assert_interleaved_release_keeps_stroke( + RegionInputSource::Pointer, + RegionInputSource::Touch, + ); + } + + #[test] + fn held_region_touch_release_does_not_finish_a_new_pointer_stroke() { + assert_interleaved_release_keeps_stroke( + RegionInputSource::Touch, + RegionInputSource::Pointer, + ); + } + + fn assert_interleaved_release_keeps_stroke( + consumed_source: RegionInputSource, + drawing_source: RegionInputSource, + ) { + use crate::input::{MouseButton, state::DrawingState}; + + let mut runtime = PointerRuntime::new(); + let mut input = crate::input::state::test_support::make_test_input_state(); + + // The modal press was consumed, but that device remains held while the + // other device starts drawing. Both canvas press handlers reset chrome. + runtime.suppress_release(consumed_source); + runtime.clear_chrome_press(); + input.on_mouse_press_with_canvas(MouseButton::Left, 10, 20, 10, 20); + input.on_mouse_motion_with_canvas(30, 40, 30, 40); + assert!(matches!(input.state, DrawingState::Drawing { .. })); + + // Exercise the release gate shared by the pointer and touch handlers. + // Falling through here would commit the other device's unfinished stroke. + if runtime.take_suppressed_release(consumed_source) { + runtime.clear_chrome_press(); + } else { + input.on_mouse_release_with_canvas(MouseButton::Left, 30, 40, 30, 40); + } + assert!(matches!(input.state, DrawingState::Drawing { .. })); + assert!(input.boards.active_frame().shapes.is_empty()); + + assert!(!runtime.take_suppressed_release(drawing_source)); + input.on_mouse_release_with_canvas(MouseButton::Left, 50, 60, 50, 60); + assert!(matches!(input.state, DrawingState::Idle)); + assert_eq!(input.boards.active_frame().shapes.len(), 1); + } + #[test] fn chrome_press_targets_are_taken_once() { let mut runtime = PointerRuntime::new(); diff --git a/src/backend/wayland/state/region_capture/runtime.rs b/src/backend/wayland/state/region_capture/runtime.rs index 999048b3a..567ac115e 100644 --- a/src/backend/wayland/state/region_capture/runtime.rs +++ b/src/backend/wayland/state/region_capture/runtime.rs @@ -654,8 +654,13 @@ impl WaylandState { #[cfg(test)] mod owner_tests { + use super::super::window_snap::WindowSnapCorrelation; use super::*; use crate::backend::wayland::RuntimeWakeSource; + use crate::backend::wayland::state::screen_image::ScreenImageKind; + use crate::capture::window_geometry::WindowQueryContext; + use crate::util::Rect; + use wayland_client::protocol::wl_output::Transform; fn runtime() -> RegionCaptureRuntime { let wake = RuntimeWakeSource::new().expect("runtime wake source"); @@ -673,8 +678,42 @@ mod owner_tests { #[test] fn clearing_retires_the_active_region() { let mut runtime = runtime(); - let generation = runtime.begin_measure((1920, 1080)); - assert_eq!(generation, 1); + let generation = runtime.next_generation(); + let source = ScreenSourceToken { + output_id: 1, + output_layout_generation: 1, + kind: ScreenImageKind::Frozen, + image_generation: 1, + image_size: (100, 80), + stride: 400, + surface: (100, 80), + output_scale: 1, + output_transform: Transform::Normal, + zoom_transformed: false, + zoom_scale: 1.0, + zoom_view_offset: (0.0, 0.0), + }; + runtime.set_ready( + RegionPurposeTag::CaptureInteractive, + generation, + source, + FreezeOwnership::PreExisting, + false, + false, + ); + runtime.set_review_edits_for( + ImagePixelRect::new(10, 10, 20, 20, source.image_size).expect("review rectangle"), + ); + runtime.set_window_snap(WindowSnapSession::queued( + WindowSnapCorrelation::new(generation, source), + WindowQueryContext { + output_name: "DP-1".to_string(), + output_logical_rect: Rect::new(0, 0, 100, 80).expect("output rectangle"), + }, + )); + assert!(runtime.active().is_some()); + assert!(runtime.review_edits().is_some()); + assert!(runtime.window_snap().is_some()); runtime.clear(); @@ -683,6 +722,18 @@ mod owner_tests { assert!(runtime.window_snap().is_none()); } + #[test] + fn review_edits_without_an_active_region_remain_absent() { + let mut runtime = runtime(); + assert!(runtime.active().is_none()); + + runtime.set_review_edits_for( + ImagePixelRect::new(0, 0, 20, 20, (100, 80)).expect("review rectangle"), + ); + + assert!(runtime.review_edits().is_none()); + } + #[test] #[should_panic(expected = "screen region generation space exhausted")] fn generation_exhaustion_is_explicit() { diff --git a/src/backend/wayland/state/render/runtime.rs b/src/backend/wayland/state/render/runtime.rs index fd9f006df..56369028c 100644 --- a/src/backend/wayland/state/render/runtime.rs +++ b/src/backend/wayland/state/render/runtime.rs @@ -157,6 +157,7 @@ impl RenderRuntime { #[cfg(test)] mod tests { + use super::super::tool_preview::mouse_tool_preview_damage_rect; use super::*; fn rect(x: i32) -> Rect { @@ -191,15 +192,35 @@ mod tests { } #[test] - fn disappearing_effect_damages_its_old_footprint() { + fn tool_preview_appearance_motion_and_disappearance_roll_rendered_bounds() { let mut history = UiDamageHistory::default(); let mut regions = Vec::new(); - history.roll(UiEffect::ZoomChip, Some(rect(5)), &mut regions); + let first = mouse_tool_preview_damage_rect(8.0, (100.0, 100.0), 800, 600) + .expect("first preview footprint"); + let moved = mouse_tool_preview_damage_rect(8.0, (400.0, 400.0), 800, 600) + .expect("moved preview footprint"); + assert_ne!(first, moved); + + history.roll(UiEffect::ToolPreview, Some(first), &mut regions); + assert_eq!(regions, vec![first]); + assert_eq!(history.previous(UiEffect::ToolPreview), Some(first)); regions.clear(); - history.roll(UiEffect::ZoomChip, None, &mut regions); + history.roll(UiEffect::ToolPreview, Some(moved), &mut regions); + assert_eq!(regions, vec![first, moved]); + assert_eq!(history.previous(UiEffect::ToolPreview), Some(moved)); + regions.clear(); + + history.roll(UiEffect::ToolPreview, None, &mut regions); + assert_eq!(regions, vec![moved]); + assert_eq!(history.previous(UiEffect::ToolPreview), None); + regions.clear(); - assert_eq!(regions, vec![rect(5)]); + history.roll(UiEffect::ToolPreview, None, &mut regions); + assert!( + regions.is_empty(), + "the hidden preview is cleared only once" + ); } #[test] diff --git a/src/backend/wayland/state/render/tool_preview.rs b/src/backend/wayland/state/render/tool_preview.rs index 2dfdd8727..4c4484900 100644 --- a/src/backend/wayland/state/render/tool_preview.rs +++ b/src/backend/wayland/state/render/tool_preview.rs @@ -144,38 +144,6 @@ pub(super) struct MouseToolPreviewRedraw { pub rects: Vec, } -#[cfg(test)] -pub(super) struct MouseToolPreviewDamageUpdate { - pub current: Option, - pub rects: Vec, -} - -/// Per-frame damage update for the preview bubble. Unlike the pointer-motion -/// helper, this accepts the last rendered footprint explicitly, so a visible -/// preview becoming hidden still damages and clears its old pixels. -#[cfg(test)] -pub(super) fn mouse_tool_preview_damage_update( - previous: Option, - active: bool, - thickness: f64, - position: (f64, f64), - width: u32, - height: u32, -) -> MouseToolPreviewDamageUpdate { - let current = active - .then(|| mouse_tool_preview_damage_rect(thickness, position, width, height)) - .flatten(); - let mut rects = Vec::with_capacity(2); - match (previous, current) { - (Some(previous), Some(current)) if previous == current => rects.push(current), - (previous, current) => { - rects.extend(previous); - rects.extend(current); - } - } - MouseToolPreviewDamageUpdate { current, rects } -} - /// Damage the mouse tool-preview bubble needs to follow the pointer from /// `prev` to `next`. /// @@ -351,7 +319,7 @@ mod tests { /// Little-endian ARgb32 stores premultiplied BGRA in memory. fn has_strong_green_pixel(pixels: &[u8]) -> bool { - pixels.chunks_exact(4).any(|px| { + pixels.as_chunks::<4>().0.iter().any(|px| { let (b, g, r, a) = (px[0], px[1], px[2], px[3]); g > 200 && r < 60 && b < 60 && a > 200 }) @@ -388,22 +356,6 @@ mod tests { assert!(out.rects.is_empty()); } - #[test] - fn mouse_preview_visibility_loss_damages_the_last_visible_bounds() { - let position = (100.0, 100.0); - let previous = effect_rect( - tool_preview_bubble(8.0, position.0, position.1, 800.0, 600.0), - 800, - 600, - ) - .expect("previous footprint"); - let update = - mouse_tool_preview_damage_update(Some(previous), false, 8.0, position, 800, 600); - - assert!(update.current.is_none()); - assert_eq!(update.rects, vec![previous]); - } - #[test] fn mouse_preview_motion_damages_old_and_new_bounds() { // The pointer-to-render path: an eligible preview that moves must diff --git a/src/backend/wayland/state/screen_image.rs b/src/backend/wayland/state/screen_image.rs index 3fee52fa9..ff5ece1a9 100644 --- a/src/backend/wayland/state/screen_image.rs +++ b/src/backend/wayland/state/screen_image.rs @@ -829,7 +829,12 @@ mod tests { copy_image_rect(&rotated, ImagePixelRect::new(0, 0, 2, 1, (2, 3)).unwrap()).unwrap(); assert_eq!( - crop.data.chunks_exact(4).map(|p| p[0]).collect::>(), + crop.data + .as_chunks::<4>() + .0 + .iter() + .map(|p| p[0]) + .collect::>(), vec![4, 1] ); } diff --git a/src/backend/wayland/state/toolbar/drag/move_drag.rs b/src/backend/wayland/state/toolbar/drag/move_drag.rs index 619d45eaf..76ce9efeb 100644 --- a/src/backend/wayland/state/toolbar/drag/move_drag.rs +++ b/src/backend/wayland/state/toolbar/drag/move_drag.rs @@ -1,3 +1,4 @@ +use super::state::MoveSample; use super::*; impl WaylandState { @@ -65,272 +66,62 @@ impl WaylandState { true } - /// Handle toolbar move with toolbar-surface-local coordinates. - /// On layer-shell, toolbar-local coords stay consistent as the toolbar moves, - /// so we use them directly for delta calculation. + /// Handle motion reported by the toolbar surface, preserving local samples + /// while its inline preview is active. pub(in crate::backend::wayland) fn handle_toolbar_move( &mut self, kind: MoveDragKind, local_coord: (f64, f64), ) { - if !self.toolbar_position_drag_update_allowed(kind) { - // Consume the coordinate baseline without moving the toolbar. If - // the exact same authority resumes this untouched preview, the - // next accepted event applies only post-barrier movement. - self.toolbar_drag.note_move(kind, local_coord, false); - return; - } - if self.pointer_lock_active() { - drag_log(|| { - format!( - "skip handle_toolbar_move_local: pointer locked, kind={:?}, coord=({:.3}, {:.3})", - kind, local_coord.0, local_coord.1 - ) - }); - return; - } - drag_log(|| { - format!( - "handle_toolbar_move_local: kind={:?}, local_coord=({:.3}, {:.3}), offsets=({}, {})", - kind, - local_coord.0, - local_coord.1, - self.toolbar_chrome.top_offset().0, - self.toolbar_chrome.top_offset().1 - ) - }); - // For layer-shell surfaces, use local coordinates directly since they're - // consistent within the toolbar surface. Only convert to screen coords - // when transitioning to/from main surface. - self.handle_toolbar_move_local(kind, local_coord); - } - - /// Handle toolbar move with toolbar-surface-local coordinates. - fn handle_toolbar_move_local(&mut self, kind: MoveDragKind, local_coord: (f64, f64)) { - let snapshot = self - .toolbar - .last_snapshot() - .cloned() - .unwrap_or_else(|| self.toolbar_snapshot()); - - // When inline drag preview is active we keep the layer-shell toolbars - // suppressed and only move the inline-rendered preview. - if self.toolbar_drag.preview_active() { - let delta = self - .toolbar_drag - .move_to(kind, local_coord, false) - .unwrap_or((0.0, 0.0)); - if delta.0 == 0.0 && delta.1 == 0.0 { - return; - } - - match kind { - MoveDragKind::Top => { - self.toolbar_chrome.add_top_offset(delta); - } - } - - // Clamp offsets; pointer-locked preview drags also move the suppressed - // layer surface so release does not visibly replay the drag. - self.apply_toolbar_offsets_throttled(&snapshot); - - let inline_render_active = self.inline_toolbars_render_active(); - if inline_render_active { - self.toolbar.mark_dirty(); - self.input_state.dirty_tracker.mark_full(); - self.input_state.needs_redraw = true; - } - if self.protocol.layer_shell().is_none() || inline_render_active { - self.toolbar_chrome.clear_inline_hits(); - } - return; - } - - // Check if we need to transition coordinate systems - let (last_coord, coord_is_screen) = self - .toolbar_drag - .move_sample() - .map_or((local_coord, false), |sample| { - (sample.coord, sample.is_screen) - }); - - // If last coord was screen-based, convert current local to screen for comparison - let last_screen = if coord_is_screen { - last_coord - } else { - self.local_to_screen_coords(kind, last_coord) - }; - let effective_coord = self.local_to_screen_coords(kind, local_coord); - - if !coord_is_screen { - self.toolbar_drag.note_move(kind, last_screen, true); - } - let delta = self - .toolbar_drag - .move_to(kind, effective_coord, true) - .unwrap_or((0.0, 0.0)); - drag_log(|| { - format!( - "move_local delta: kind={:?}, local=({:.3}, {:.3}), effective=({:.3}, {:.3}), last_screen=({:.3}, {:.3}), delta=({:.3}, {:.3}), offsets_before=({}, {})", - kind, - local_coord.0, - local_coord.1, - effective_coord.0, - effective_coord.1, - last_screen.0, - last_screen.1, - delta.0, - delta.1, - self.toolbar_chrome.top_offset().0, - self.toolbar_chrome.top_offset().1 - ) - }); - log::debug!( - "handle_toolbar_move_local: kind={:?}, local_coord=({:.3}, {:.3}), effective_coord=({:.3}, {:.3}), last_coord=({:.3}, {:.3}), delta=({:.3}, {:.3}), offsets=({}, {})", - kind, - local_coord.0, - local_coord.1, - effective_coord.0, - effective_coord.1, - last_screen.0, - last_screen.1, - delta.0, - delta.1, - self.toolbar_chrome.top_offset().0, - self.toolbar_chrome.top_offset().1 - ); - if delta.0 == 0.0 && delta.1 == 0.0 { - return; - } - - match kind { - MoveDragKind::Top => { - self.toolbar_chrome.add_top_offset(delta); - } - } - drag_log(|| { - format!( - "move_local applied: kind={:?}, offsets_after=({}, {})", - kind, - self.toolbar_chrome.top_offset().0, - self.toolbar_chrome.top_offset().1 - ) - }); - log::debug!( - "After update offsets: top=({}, {})", - self.toolbar_chrome.top_offset().0, - self.toolbar_chrome.top_offset().1 - ); - - self.apply_toolbar_offsets_throttled(&snapshot); - let inline_render_active = self.inline_toolbars_render_active(); - if inline_render_active { - self.toolbar.mark_dirty(); - self.input_state.dirty_tracker.mark_full(); - self.input_state.needs_redraw = true; - } - if self.protocol.layer_shell().is_none() || inline_render_active { - self.toolbar_chrome.clear_inline_hits(); - } + self.handle_toolbar_move_sample(kind, MoveSample::Local(local_coord)); } - /// Handle toolbar move with screen-relative coordinates (no conversion). - /// Use this when coords are already in screen space (e.g., from main overlay surface). + /// Handle motion already in screen space, such as the main overlay surface. pub(in crate::backend::wayland) fn handle_toolbar_move_screen( &mut self, kind: MoveDragKind, screen_coord: (f64, f64), ) { + self.handle_toolbar_move_sample(kind, MoveSample::Screen(screen_coord)); + } + + fn handle_toolbar_move_sample(&mut self, kind: MoveDragKind, sample: MoveSample) { if !self.toolbar_position_drag_update_allowed(kind) { - self.toolbar_drag.note_move(kind, screen_coord, true); + self.toolbar_drag.note_move(kind, sample); return; } if self.pointer_lock_active() { drag_log(|| { - format!( - "skip handle_toolbar_move_screen: pointer locked, kind={:?}, coord=({:.3}, {:.3})", - kind, screen_coord.0, screen_coord.1 - ) + format!("skip toolbar move: pointer locked, kind={kind:?}, sample={sample:?}") }); return; } - drag_log(|| { - format!( - "handle_toolbar_move_screen: kind={:?}, screen_coord=({:.3}, {:.3}), offsets=({}, {})", - kind, - screen_coord.0, - screen_coord.1, - self.toolbar_chrome.top_offset().0, - self.toolbar_chrome.top_offset().1 - ) - }); - let snapshot = self - .toolbar - .last_snapshot() - .cloned() - .unwrap_or_else(|| self.toolbar_snapshot()); - // Get last coord, converting from local to screen if needed - let last_screen_coord = match self.toolbar_drag.move_sample() { - Some(sample) if sample.is_screen => sample.coord, - Some(sample) => self.local_to_screen_coords(kind, sample.coord), - None => screen_coord, + let local_origin = self.local_to_screen_coords(kind, (0.0, 0.0)); + let Some(delta) = self.toolbar_drag.move_to(kind, sample, local_origin) else { + return; }; - - if self - .toolbar_drag - .move_sample() - .is_some_and(|sample| !sample.is_screen) - { - self.toolbar_drag.note_move(kind, last_screen_coord, true); - } - let delta = self - .toolbar_drag - .move_to(kind, screen_coord, true) - .unwrap_or((0.0, 0.0)); drag_log(|| { format!( - "move_screen delta: kind={:?}, screen=({:.3}, {:.3}), last_screen=({:.3}, {:.3}), delta=({:.3}, {:.3}), offsets_before=({}, {})", - kind, - screen_coord.0, - screen_coord.1, - last_screen_coord.0, - last_screen_coord.1, - delta.0, - delta.1, - self.toolbar_chrome.top_offset().0, - self.toolbar_chrome.top_offset().1 + "toolbar move: kind={kind:?}, sample={sample:?}, delta={delta:?}, offsets_before={:?}", + self.toolbar_chrome.top_offset(), ) }); - log::debug!( - "handle_toolbar_move_screen: kind={:?}, screen_coord=({:.3}, {:.3}), last_screen_coord=({:.3}, {:.3}), delta=({:.3}, {:.3}), offsets=({}, {})", - kind, - screen_coord.0, - screen_coord.1, - last_screen_coord.0, - last_screen_coord.1, - delta.0, - delta.1, - self.toolbar_chrome.top_offset().0, - self.toolbar_chrome.top_offset().1 - ); if delta.0 == 0.0 && delta.1 == 0.0 { return; } + + let snapshot = self + .toolbar + .last_snapshot() + .cloned() + .unwrap_or_else(|| self.toolbar_snapshot()); match kind { - MoveDragKind::Top => { - self.toolbar_chrome.add_top_offset(delta); - } + MoveDragKind::Top => self.toolbar_chrome.add_top_offset(delta), } - drag_log(|| { - format!( - "move_screen applied: kind={:?}, offsets_after=({}, {})", - kind, - self.toolbar_chrome.top_offset().0, - self.toolbar_chrome.top_offset().1 - ) - }); + // Clamp and throttle both coordinate routes identically. A preview also + // moves the suppressed layer surface so release does not replay the drag. self.apply_toolbar_offsets_throttled(&snapshot); let inline_render_active = self.inline_toolbars_render_active(); if inline_render_active { @@ -339,7 +130,6 @@ impl WaylandState { self.input_state.needs_redraw = true; } if self.protocol.layer_shell().is_none() || inline_render_active { - // Inline mode uses cached rects, so force a relayout. self.toolbar_chrome.clear_inline_hits(); } } diff --git a/src/backend/wayland/state/toolbar/drag/state.rs b/src/backend/wayland/state/toolbar/drag/state.rs index 5cc188a47..16b0d991c 100644 --- a/src/backend/wayland/state/toolbar/drag/state.rs +++ b/src/backend/wayland/state/toolbar/drag/state.rs @@ -27,8 +27,7 @@ enum MoveDragPhase { Idle, Moving { kind: MoveDragKind, - last_coord: (f64, f64), - coord_is_screen: bool, + sample: MoveSample, frozen_base: (f64, f64), throttle: ApplyThrottle, }, @@ -45,9 +44,18 @@ enum MoveDragPhase { } #[derive(Debug, Clone, Copy, PartialEq)] -pub(in crate::backend::wayland) struct MoveSample { - pub coord: (f64, f64), - pub is_screen: bool, +pub(super) enum MoveSample { + Local((f64, f64)), + Screen((f64, f64)), +} + +impl MoveSample { + fn screen_coord(self, local_origin: (f64, f64)) -> (f64, f64) { + match self { + Self::Local(coord) => (local_origin.0 + coord.0, local_origin.1 + coord.1), + Self::Screen(coord) => coord, + } + } } #[derive(Debug, Clone, Copy, PartialEq)] @@ -115,8 +123,11 @@ impl ToolbarDrag { ) { self.phase = MoveDragPhase::Moving { kind, - last_coord: coord, - coord_is_screen, + sample: if coord_is_screen { + MoveSample::Screen(coord) + } else { + MoveSample::Local(coord) + }, frozen_base, throttle: ApplyThrottle::new(), }; @@ -154,30 +165,16 @@ impl ToolbarDrag { } } - pub(in crate::backend::wayland) fn move_sample(&self) -> Option { - match self.phase { - MoveDragPhase::Moving { - last_coord, - coord_is_screen, - .. - } => Some(MoveSample { - coord: last_coord, - is_screen: coord_is_screen, - }), - _ => None, - } - } - - pub(in crate::backend::wayland) fn note_move( + /// Consume a rejected event too, so resuming the same drag authority does + /// not replay movement that happened behind its update barrier. + pub(super) fn note_move( &mut self, kind: MoveDragKind, - coord: (f64, f64), - coord_is_screen: bool, + sample: MoveSample, ) -> Option { let MoveDragPhase::Moving { kind: active_kind, - last_coord, - coord_is_screen: active_is_screen, + sample: previous, .. } = &mut self.phase else { @@ -186,26 +183,31 @@ impl ToolbarDrag { if *active_kind != kind { return None; } - let previous = MoveSample { - coord: *last_coord, - is_screen: *active_is_screen, - }; - *last_coord = coord; - *active_is_screen = coord_is_screen; - Some(previous) + Some(std::mem::replace(previous, sample)) } - pub(in crate::backend::wayland) fn move_to( + /// Compare screen samples across surfaces, except that a suppressed toolbar's + /// local preview keeps its own local baseline. Entering that local preview + /// from screen space rebases without applying a jump. + pub(super) fn move_to( &mut self, kind: MoveDragKind, - coord: (f64, f64), - coord_is_screen: bool, + sample: MoveSample, + local_origin: (f64, f64), ) -> Option<(f64, f64)> { - let previous = self.note_move(kind, coord, coord_is_screen)?; - if previous.is_screen != coord_is_screen { - return None; - } - Some((coord.0 - previous.coord.0, coord.1 - previous.coord.1)) + let (coord, previous_coord) = if self.preview + && let MoveSample::Local(coord) = sample + { + let MoveSample::Local(previous) = self.note_move(kind, sample)? else { + return None; + }; + (coord, previous) + } else { + let coord = sample.screen_coord(local_origin); + let previous = self.note_move(kind, MoveSample::Screen(coord))?; + (coord, previous.screen_coord(local_origin)) + }; + Some((coord.0 - previous_coord.0, coord.1 - previous_coord.1)) } pub(in crate::backend::wayland) fn should_apply( @@ -408,132 +410,5 @@ impl ToolbarDrag { } #[cfg(test)] -mod tests { - use super::*; - use crate::toolbar_gtk::{GtkToolbarDragPhase, GtkToolbarSurfaceSize}; - - const TEST_SURFACE_SIZE: GtkToolbarSurfaceSize = GtkToolbarSurfaceSize { - width: 260, - height: 789, - }; - - fn gtk_offset(phase: GtkToolbarDragPhase, seq: u64) -> GtkToolbarFeedback { - GtkToolbarFeedback::SetTopOffset { - x: 10.0, - y: 20.0, - surface_size: TEST_SURFACE_SIZE, - seq, - phase, - } - } - - fn moving(preview: bool) -> ToolbarDrag { - let mut drag = ToolbarDrag::new(); - drag.set_preview_active(preview); - drag.begin_move(MoveDragKind::Top, (1.0, 2.0), false, (24.0, 12.0)); - drag - } - - #[test] - fn move_and_handoff_transition_table_is_explicit() { - let now = Instant::now(); - let mut drag = moving(true); - let ended = drag.end_move().unwrap(); - assert_eq!(ended.commit_base, Some(24.0)); - assert!(ended.had_preview); - - drag.begin_handoff(now + Duration::from_millis(10)); - assert_eq!(drag.finish_handoff_if_due(now), None); - assert_eq!( - drag.finish_handoff_if_due(now + Duration::from_millis(10)), - Some(HandoffEnd::BuiltIn) - ); - assert!(!drag.preview_active()); - } - - #[test] - fn move_cancel_can_return_directly_to_idle() { - let mut drag = moving(false); - assert!(drag.end_move().is_some()); - assert!(!drag.is_moving()); - assert_eq!(drag.finish_handoff(), None); - } - - #[test] - fn gtk_preview_handoff_and_cancel_follow_their_own_phase() { - let now = Instant::now(); - let mut drag = ToolbarDrag::new(); - drag.begin_handoff(now); - drag.begin_gtk_preview(GtkToolbarKind::Top, 24.0); - assert_eq!(drag.handoff_timeout(now), None); - drag.begin_handoff(now + Duration::from_millis(10)); - assert_eq!( - drag.finish_handoff_if_due(now + Duration::from_millis(10)), - Some(HandoffEnd::Gtk) - ); - - drag.begin_gtk_preview(GtkToolbarKind::Top, 24.0); - assert!(drag.cancel_gtk()); - assert!(!drag.cancel_gtk()); - } - - #[test] - fn throttle_reports_a_pending_terminal_apply() { - let start = Instant::now(); - let mut drag = moving(false); - let interval = Duration::from_millis(20); - assert!(drag.should_apply(start, interval)); - assert!(!drag.should_apply(start + Duration::from_millis(5), interval)); - assert!(drag.end_move().unwrap().pending_apply); - } - - #[test] - fn blocked_gtk_drag_advances_sequence_and_stays_blocked_until_end() { - let mut drag = ToolbarDrag::new(); - drag.note_gtk_offset_seq(4); - - assert!(drag.gtk_note_feedback(true, >k_offset(GtkToolbarDragPhase::Start, 9))); - assert!(drag.gtk_note_feedback(false, >k_offset(GtkToolbarDragPhase::Move, 8))); - assert_eq!(drag.gtk_offset_seq(), 9); - assert!(drag.gtk_note_feedback(false, >k_offset(GtkToolbarDragPhase::End, 10))); - assert_eq!(drag.gtk_offset_seq(), 10); - assert!(!drag.gtk_note_feedback(false, >k_offset(GtkToolbarDragPhase::Start, 11))); - } - - #[test] - fn passive_and_capture_feedback_follow_modal_policy() { - let mut drag = ToolbarDrag::new(); - drag.block_gtk_drag(); - assert!(!drag.gtk_note_feedback( - true, - &GtkToolbarFeedback::CaptureSuppressionReady { generation: 7 } - )); - let shortcut = GtkToolbarFeedback::PointerShortcut { - button: 8, - ctrl: false, - shift: false, - alt: false, - logo: false, - }; - assert!(drag.gtk_note_feedback(true, &shortcut)); - assert!(!drag.gtk_note_feedback(false, &shortcut)); - assert!(drag.gtk_note_feedback(false, >k_offset(GtkToolbarDragPhase::Move, 1))); - } - - #[test] - fn move_samples_are_updated_with_their_coordinate_space() { - let mut drag = moving(false); - assert_eq!( - drag.move_to(MoveDragKind::Top, (3.0, 4.0), false), - Some((2.0, 2.0)) - ); - assert_eq!(drag.move_to(MoveDragKind::Top, (5.0, 7.0), true), None); - assert_eq!( - drag.move_sample(), - Some(MoveSample { - coord: (5.0, 7.0), - is_screen: true, - }) - ); - } -} +#[path = "tests.rs"] +mod tests; diff --git a/src/backend/wayland/state/toolbar/drag/tests.rs b/src/backend/wayland/state/toolbar/drag/tests.rs new file mode 100644 index 000000000..752af2e76 --- /dev/null +++ b/src/backend/wayland/state/toolbar/drag/tests.rs @@ -0,0 +1,334 @@ +use super::*; +use crate::toolbar_gtk::{GtkToolbarDragPhase, GtkToolbarSurfaceSize}; + +const TEST_SURFACE_SIZE: GtkToolbarSurfaceSize = GtkToolbarSurfaceSize { + width: 260, + height: 789, +}; + +fn gtk_offset(phase: GtkToolbarDragPhase, seq: u64) -> GtkToolbarFeedback { + GtkToolbarFeedback::SetTopOffset { + x: 10.0, + y: 20.0, + surface_size: TEST_SURFACE_SIZE, + seq, + phase, + } +} + +fn moving(preview: bool) -> ToolbarDrag { + let mut drag = ToolbarDrag::new(); + drag.set_preview_active(preview); + drag.begin_move(MoveDragKind::Top, (1.0, 2.0), false, (24.0, 12.0)); + drag +} + +#[test] +fn move_and_handoff_transition_table_is_explicit() { + let now = Instant::now(); + let mut drag = moving(true); + let ended = drag.end_move().unwrap(); + assert_eq!(ended.commit_base, Some(24.0)); + assert!(ended.had_preview); + + drag.begin_handoff(now + Duration::from_millis(10)); + assert_eq!(drag.finish_handoff_if_due(now), None); + assert_eq!( + drag.finish_handoff_if_due(now + Duration::from_millis(10)), + Some(HandoffEnd::BuiltIn) + ); + assert!(!drag.preview_active()); +} + +#[test] +fn move_cancel_can_return_directly_to_idle() { + let mut drag = moving(false); + assert!(drag.end_move().is_some()); + assert!(!drag.is_moving()); + assert_eq!(drag.finish_handoff(), None); +} + +#[test] +fn gtk_preview_handoff_and_cancel_follow_their_own_phase() { + let now = Instant::now(); + let mut drag = ToolbarDrag::new(); + drag.begin_handoff(now); + drag.begin_gtk_preview(GtkToolbarKind::Top, 24.0); + assert_eq!(drag.handoff_timeout(now), None); + drag.begin_handoff(now + Duration::from_millis(10)); + assert_eq!( + drag.finish_handoff_if_due(now + Duration::from_millis(10)), + Some(HandoffEnd::Gtk) + ); + + drag.begin_gtk_preview(GtkToolbarKind::Top, 24.0); + assert!(drag.cancel_gtk()); + assert!(!drag.cancel_gtk()); +} + +#[test] +fn throttle_reports_a_pending_terminal_apply() { + let start = Instant::now(); + let mut drag = moving(false); + let interval = Duration::from_millis(20); + assert!(drag.should_apply(start, interval)); + assert!(!drag.should_apply(start + Duration::from_millis(5), interval)); + assert!(drag.end_move().unwrap().pending_apply); +} + +#[test] +fn blocked_gtk_drag_advances_sequence_and_stays_blocked_until_end() { + let mut drag = ToolbarDrag::new(); + drag.note_gtk_offset_seq(4); + + assert!(drag.gtk_note_feedback(true, >k_offset(GtkToolbarDragPhase::Start, 9))); + assert!(drag.gtk_note_feedback(false, >k_offset(GtkToolbarDragPhase::Move, 8))); + assert_eq!(drag.gtk_offset_seq(), 9); + assert!(drag.gtk_note_feedback(false, >k_offset(GtkToolbarDragPhase::End, 10))); + assert_eq!(drag.gtk_offset_seq(), 10); + assert!(!drag.gtk_note_feedback(false, >k_offset(GtkToolbarDragPhase::Start, 11))); +} + +#[test] +fn passive_and_capture_feedback_follow_modal_policy() { + let mut drag = ToolbarDrag::new(); + drag.block_gtk_drag(); + assert!(!drag.gtk_note_feedback( + true, + &GtkToolbarFeedback::CaptureSuppressionReady { generation: 7 } + )); + let shortcut = GtkToolbarFeedback::PointerShortcut { + button: 8, + ctrl: false, + shift: false, + alt: false, + logo: false, + }; + assert!(drag.gtk_note_feedback(true, &shortcut)); + assert!(!drag.gtk_note_feedback(false, &shortcut)); + assert!(drag.gtk_note_feedback(false, >k_offset(GtkToolbarDragPhase::Move, 1))); +} + +#[test] +fn local_and_screen_events_remain_continuous_as_the_toolbar_moves() { + let mut drag = moving(false); + assert_eq!( + drag.move_to( + MoveDragKind::Top, + MoveSample::Screen((104.0, 206.0)), + (100.0, 200.0) + ), + Some((3.0, 4.0)) + ); + // Apply the delta to the toolbar origin, then return to the same local spot. + assert_eq!( + drag.move_to( + MoveDragKind::Top, + MoveSample::Local((1.0, 2.0)), + (103.0, 204.0) + ), + Some((0.0, 0.0)) + ); + assert_eq!( + drag.move_to( + MoveDragKind::Top, + MoveSample::Local((3.0, 1.0)), + (103.0, 204.0) + ), + Some((2.0, -1.0)) + ); + assert_eq!( + drag.move_to( + MoveDragKind::Top, + MoveSample::Screen((108.0, 204.0)), + (105.0, 203.0) + ), + Some((2.0, -1.0)) + ); +} + +#[test] +fn local_motion_normalizes_the_initial_sample_before_applying_offsets() { + let mut drag = moving(false); + assert_eq!( + drag.move_to( + MoveDragKind::Top, + MoveSample::Local((3.0, 4.0)), + (100.0, 200.0) + ), + Some((2.0, 2.0)) + ); + // Subsequent layer-surface motion compares against the saved screen sample. + assert_eq!( + drag.move_to( + MoveDragKind::Top, + MoveSample::Local((3.0, 4.0)), + (102.0, 202.0) + ), + Some((2.0, 2.0)) + ); +} + +#[test] +fn local_preview_deltas_ignore_changes_to_the_suppressed_surface_origin() { + let mut drag = moving(true); + assert_eq!( + drag.move_to( + MoveDragKind::Top, + MoveSample::Local((3.0, 4.0)), + (100.0, 200.0) + ), + Some((2.0, 2.0)) + ); + assert_eq!( + drag.move_to( + MoveDragKind::Top, + MoveSample::Local((4.0, 6.0)), + (102.0, 202.0) + ), + Some((1.0, 2.0)) + ); +} + +#[test] +fn preview_converts_to_screen_but_rebases_on_return_to_local_motion() { + let mut drag = moving(true); + assert_eq!( + drag.move_to( + MoveDragKind::Top, + MoveSample::Screen((104.0, 206.0)), + (100.0, 200.0) + ), + Some((3.0, 4.0)) + ); + assert_eq!( + drag.move_to( + MoveDragKind::Top, + MoveSample::Local((500.0, 600.0)), + (103.0, 204.0) + ), + None + ); + assert_eq!( + drag.move_to( + MoveDragKind::Top, + MoveSample::Local((501.0, 602.0)), + (103.0, 204.0) + ), + Some((1.0, 2.0)) + ); +} + +#[test] +fn rejected_samples_consume_the_baseline_across_coordinate_routes() { + let mut drag = moving(false); + drag.note_move(MoveDragKind::Top, MoveSample::Local((20.0, 30.0))); + assert_eq!( + drag.move_to( + MoveDragKind::Top, + MoveSample::Screen((126.0, 239.0)), + (103.0, 204.0) + ), + Some((3.0, 5.0)) + ); + drag.note_move(MoveDragKind::Top, MoveSample::Screen((300.0, 400.0))); + assert_eq!( + drag.move_to( + MoveDragKind::Top, + MoveSample::Local((197.0, 193.0)), + (106.0, 209.0) + ), + Some((3.0, 2.0)) + ); +} + +#[test] +fn rejected_preview_samples_keep_their_local_baseline() { + let mut drag = moving(true); + drag.note_move(MoveDragKind::Top, MoveSample::Local((20.0, 30.0))); + assert_eq!( + drag.move_to( + MoveDragKind::Top, + MoveSample::Local((23.0, 35.0)), + (100.0, 200.0) + ), + Some((3.0, 5.0)) + ); + drag.note_move(MoveDragKind::Top, MoveSample::Screen((300.0, 400.0))); + assert_eq!( + drag.move_to( + MoveDragKind::Top, + MoveSample::Local((20.0, 30.0)), + (100.0, 200.0) + ), + None + ); + assert_eq!( + drag.move_to( + MoveDragKind::Top, + MoveSample::Local((21.0, 32.0)), + (100.0, 200.0) + ), + Some((1.0, 2.0)) + ); +} + +#[test] +fn motion_outside_a_builtin_drag_cannot_create_a_baseline() { + let mut drag = moving(false); + drag.end_move().unwrap(); + let sample = MoveSample::Screen((300.0, 400.0)); + assert_eq!(drag.note_move(MoveDragKind::Top, sample), None); + assert_eq!(drag.move_to(MoveDragKind::Top, sample, (0.0, 0.0)), None); + drag.begin_gtk_preview(GtkToolbarKind::Top, 24.0); + assert_eq!(drag.move_to(MoveDragKind::Top, sample, (0.0, 0.0)), None); + assert_eq!(drag.frozen_base_x(), Some(24.0)); +} + +#[test] +fn gtk_preview_uses_the_base_frozen_at_drag_start_until_released() { + let mut drag = ToolbarDrag::new(); + drag.begin_gtk_preview(GtkToolbarKind::Top, 24.0); + drag.set_gtk_rebase(Some((300.0, 400.0))); + assert_eq!(drag.frozen_base_x(), Some(24.0)); + assert_eq!(drag.frozen_base_y(), None); + + let deadline = Instant::now() + Duration::from_millis(10); + drag.begin_handoff(deadline); + assert_eq!(drag.frozen_base_x(), Some(24.0)); + drag.release_gtk_frozen_base(10.0); + assert_eq!(drag.frozen_base_x(), Some(10.0)); + assert_eq!(drag.finish_handoff_if_due(deadline), Some(HandoffEnd::Gtk)); + assert_eq!(drag.frozen_base_x(), None); +} + +#[test] +fn idle_and_handoff_layouts_do_not_reuse_a_stale_builtin_frozen_base() { + let mut drag = moving(true); + assert_eq!(drag.frozen_base_x(), Some(24.0)); + assert_eq!(drag.frozen_base_y(), Some(12.0)); + assert_eq!(drag.end_move().unwrap().commit_base, Some(24.0)); + assert_eq!(drag.frozen_base_x(), None); + assert_eq!(drag.frozen_base_y(), None); + + let now = Instant::now(); + drag.begin_handoff(now); + assert_eq!(drag.frozen_base_x(), None); + assert_eq!(drag.finish_handoff_if_due(now), Some(HandoffEnd::BuiltIn)); + assert_eq!(drag.frozen_base_x(), None); +} + +#[test] +fn cancelling_or_blocking_a_gtk_drag_releases_its_frozen_base() { + let mut drag = ToolbarDrag::new(); + drag.begin_gtk_preview(GtkToolbarKind::Top, 24.0); + assert!(drag.cancel_gtk()); + assert_eq!(drag.frozen_base_x(), None); + + drag.begin_gtk_preview(GtkToolbarKind::Top, 48.0); + assert_eq!(drag.frozen_base_x(), Some(48.0)); + drag.block_gtk_drag(); + assert_eq!(drag.frozen_base_x(), None); + assert!(drag.gtk_note_feedback(false, >k_offset(GtkToolbarDragPhase::End, 1))); + assert_eq!(drag.frozen_base_x(), None); +} diff --git a/src/base64.rs b/src/base64.rs index 5d02d6205..cffccf25d 100644 --- a/src/base64.rs +++ b/src/base64.rs @@ -96,7 +96,7 @@ pub(crate) fn decode_standard(encoded: &str) -> Result, DecodeError> { let mut decoded = Vec::with_capacity(values.len() / 4 * 3 + 2); let full_groups_len = values.len() / 4 * 4; - for chunk in values[..full_groups_len].chunks_exact(4) { + for chunk in values[..full_groups_len].as_chunks::<4>().0 { decoded.push((chunk[0] << 2) | (chunk[1] >> 4)); decoded.push((chunk[1] << 4) | (chunk[2] >> 2)); decoded.push((chunk[2] << 6) | chunk[3]); diff --git a/src/capture/band_cut.rs b/src/capture/band_cut.rs index 2f247f06f..cf2112bc8 100644 --- a/src/capture/band_cut.rs +++ b/src/capture/band_cut.rs @@ -318,8 +318,10 @@ mod tests { fn ids_of(pixels: &PackedArgb32) -> Vec { pixels .data() - .chunks_exact(4) - .map(|chunk| u32::from_ne_bytes(chunk.try_into().expect("pixel"))) + .as_chunks::<4>() + .0 + .iter() + .map(|chunk| u32::from_ne_bytes(*chunk)) .collect() } diff --git a/src/capture/desktop_backdrop.rs b/src/capture/desktop_backdrop.rs index 662ee094b..5941eadb9 100644 --- a/src/capture/desktop_backdrop.rs +++ b/src/capture/desktop_backdrop.rs @@ -52,7 +52,7 @@ fn rgba_to_cairo_argb(rgba: &[u8]) -> Result, CaptureError> { } let mut argb = Vec::with_capacity(rgba.len()); - for pixel in rgba.chunks_exact(4) { + for pixel in rgba.as_chunks::<4>().0 { let r = pixel[0]; let g = pixel[1]; let b = pixel[2]; diff --git a/src/capture/sources/frozen.rs b/src/capture/sources/frozen.rs index 60ec93145..bf201b6a0 100644 --- a/src/capture/sources/frozen.rs +++ b/src/capture/sources/frozen.rs @@ -18,18 +18,18 @@ pub fn decode_image_to_argb(data: &[u8]) -> Result<(Vec, u32, u32), CaptureE let mut argb = Vec::with_capacity((info.width * info.height * 4) as usize); let iter: Box> = match info.color_type { - png::ColorType::Rgba => Box::new(buf[..info.buffer_size()].chunks_exact(4).map(|c| { - let [r, g, b, a] = *c else { - return (0, 0, 0, 0); - }; - (r, g, b, a) - })), - png::ColorType::Rgb => Box::new(buf[..info.buffer_size()].chunks_exact(3).map(|c| { - let [r, g, b] = *c else { - return (0, 0, 0, 0xFF); - }; - (r, g, b, 0xFF) - })), + png::ColorType::Rgba => Box::new(buf[..info.buffer_size()].as_chunks::<4>().0.iter().map( + |c| { + let [r, g, b, a] = *c; + (r, g, b, a) + }, + )), + png::ColorType::Rgb => Box::new(buf[..info.buffer_size()].as_chunks::<3>().0.iter().map( + |c| { + let [r, g, b] = *c; + (r, g, b, 0xFF) + }, + )), other => { return Err(CaptureError::ImageError(format!( "Unsupported PNG color type: {:?}", diff --git a/src/daemon/icons.rs b/src/daemon/icons.rs index 1dbc54cc5..c86f6ceff 100644 --- a/src/daemon/icons.rs +++ b/src/daemon/icons.rs @@ -35,7 +35,7 @@ fn decode_icon_png(bytes: &[u8]) -> Option { // expand channels to ARGB with duplicated gray. match info.color_type { png::ColorType::Rgba => { - for chunk in bytes.chunks_exact(4) { + for chunk in bytes.as_chunks::<4>().0 { data.push(chunk[3]); // A data.push(chunk[0]); // R data.push(chunk[1]); // G @@ -43,7 +43,7 @@ fn decode_icon_png(bytes: &[u8]) -> Option { } } png::ColorType::GrayscaleAlpha => { - for chunk in bytes.chunks_exact(2) { + for chunk in bytes.as_chunks::<2>().0 { let g = chunk[0]; let a = chunk[1]; data.push(a); @@ -61,7 +61,7 @@ fn decode_icon_png(bytes: &[u8]) -> Option { } } png::ColorType::Rgb => { - for chunk in bytes.chunks_exact(3) { + for chunk in bytes.as_chunks::<3>().0 { data.push(255); data.push(chunk[0]); data.push(chunk[1]); @@ -93,7 +93,13 @@ mod tests { for icon in icons { assert_eq!(icon.width, icon.height); assert_eq!(icon.data.len(), (icon.width * icon.height * 4) as usize); - assert!(icon.data.chunks_exact(4).any(|pixel| pixel[0] > 0)); + assert!( + icon.data + .as_chunks::<4>() + .0 + .iter() + .any(|pixel| pixel[0] > 0) + ); } } } diff --git a/src/daemon/protocol_v2/digest.rs b/src/daemon/protocol_v2/digest.rs index 9137a2dc9..0fc304958 100644 --- a/src/daemon/protocol_v2/digest.rs +++ b/src/daemon/protocol_v2/digest.rs @@ -37,7 +37,9 @@ mod tests { assert!(value.len().is_multiple_of(2)); value .as_bytes() - .chunks_exact(2) + .as_chunks::<2>() + .0 + .iter() .map(|pair| { let pair = std::str::from_utf8(pair).unwrap(); u8::from_str_radix(pair, 16).unwrap() diff --git a/src/draw/dirty.rs b/src/draw/dirty.rs index a333f534f..d6222472d 100644 --- a/src/draw/dirty.rs +++ b/src/draw/dirty.rs @@ -111,7 +111,7 @@ impl DirtyTracker { self.force_full_reason = None; self.regions.retain(Rect::is_valid); DirtyRegionReport { - regions: self.regions.drain(..).collect(), + regions: std::mem::take(&mut self.regions), full_reason: None, } } diff --git a/src/draw/render/image.rs b/src/draw/render/image.rs index a4e0728a1..31659e3f9 100644 --- a/src/draw/render/image.rs +++ b/src/draw/render/image.rs @@ -203,7 +203,12 @@ fn decode_surface(data: &EmbeddedImage) -> Option<(ImageSurface, usize)> { for (row, source) in image.rgba.chunks_exact(width as usize * 4).enumerate() { let offset = row * stride; let row_bytes = &mut pixels[offset..offset + width as usize * 4]; - for (pixel, out) in source.chunks_exact(4).zip(row_bytes.chunks_exact_mut(4)) { + for (pixel, out) in source + .as_chunks::<4>() + .0 + .iter() + .zip(row_bytes.as_chunks_mut::<4>().0.iter_mut()) + { let [r, g, b, a] = [pixel[0], pixel[1], pixel[2], pixel[3]]; let premul = |channel: u8| -> u8 { ((channel as u16 * a as u16 + 127) / 255).min(255) as u8 }; diff --git a/src/draw/render/pressure_strokes.rs b/src/draw/render/pressure_strokes.rs index 25aeca664..434fb6433 100644 --- a/src/draw/render/pressure_strokes.rs +++ b/src/draw/render/pressure_strokes.rs @@ -323,7 +323,9 @@ mod tests { surface .data() .unwrap() - .chunks_exact(4) + .as_chunks::<4>() + .0 + .iter() .any(|pixel| pixel[3] > 0) } diff --git a/src/image_decode.rs b/src/image_decode.rs index 94480766b..80e1a79b5 100644 --- a/src/image_decode.rs +++ b/src/image_decode.rs @@ -111,7 +111,7 @@ fn normalize_png_rgba( if data.len() != pixels * 3 { return Err("decoded PNG RGB data has an unexpected length".to_string()); } - for pixel in data.chunks_exact(3) { + for pixel in data.as_chunks::<3>().0 { rgba.extend_from_slice(&[pixel[0], pixel[1], pixel[2], 255]); } } @@ -127,7 +127,7 @@ fn normalize_png_rgba( if data.len() != pixels * 2 { return Err("decoded PNG grayscale-alpha data has an unexpected length".to_string()); } - for pixel in data.chunks_exact(2) { + for pixel in data.as_chunks::<2>().0 { rgba.extend_from_slice(&[pixel[0], pixel[0], pixel[0], pixel[1]]); } } @@ -179,7 +179,7 @@ fn decode_jpeg_rgba(bytes: &[u8]) -> Result { fn rgb_to_rgba(rgb: &[u8]) -> Vec { let mut rgba = Vec::with_capacity(rgb.len() / 3 * 4); - for pixel in rgb.chunks_exact(3) { + for pixel in rgb.as_chunks::<3>().0 { rgba.extend_from_slice(&[pixel[0], pixel[1], pixel[2], 255]); } rgba diff --git a/src/toolbar_icons/history/mod.rs b/src/toolbar_icons/history/mod.rs index 5081e9314..28b71a95f 100644 --- a/src/toolbar_icons/history/mod.rs +++ b/src/toolbar_icons/history/mod.rs @@ -111,7 +111,7 @@ mod tests { let alpha_at = |x: usize, y: usize| pixels[y * stride + x * 4 + 3]; let last = size as usize - 1; assert!( - pixels.chunks_exact(4).any(|pixel| pixel[3] != 0), + pixels.as_chunks::<4>().0.iter().any(|pixel| pixel[3] != 0), "{name} rendered empty at {size}px" ); let touches_edge = (0..=last).any(|position| { diff --git a/src/toolbar_icons/mod.rs b/src/toolbar_icons/mod.rs index 30a40cb08..9582ca0a0 100644 --- a/src/toolbar_icons/mod.rs +++ b/src/toolbar_icons/mod.rs @@ -233,7 +233,7 @@ mod painter_tests { let mut painted = false; surface .with_data(|pixels| { - painted = pixels.chunks_exact(4).any(|pixel| pixel[3] != 0); + painted = pixels.as_chunks::<4>().0.iter().any(|pixel| pixel[3] != 0); }) .expect("surface data"); assert!(painted, "{name} painted nothing at {size}px"); diff --git a/src/toolbar_icons/svg.rs b/src/toolbar_icons/svg.rs index e6d7d39a7..ab172a41e 100644 --- a/src/toolbar_icons/svg.rs +++ b/src/toolbar_icons/svg.rs @@ -674,7 +674,7 @@ mod tests { let mut has_alpha = false; surface .with_data(|pixels| { - has_alpha = pixels.chunks_exact(4).any(|pixel| pixel[3] != 0); + has_alpha = pixels.as_chunks::<4>().0.iter().any(|pixel| pixel[3] != 0); }) .expect("surface data"); assert!(has_alpha, "{name} rendered empty at {size}px"); diff --git a/tools/README.md b/tools/README.md index b734cf97e..40f309852 100644 --- a/tools/README.md +++ b/tools/README.md @@ -68,7 +68,7 @@ Helper scripts for development, installation, packaging, and release workflows. - Usage: `./tools/bump-version.sh [--dry-run] [new_version]` - **check-version-consistency.sh** - Check release metadata alignment - - Verifies Cargo manifests, the workspace lockfile, packaging metadata, flake version sourcing, and that the flake compares nixpkgs rustc to Cargo.toml rust-version + - Verifies Cargo manifests, the workspace lockfile, packaging metadata, flake version sourcing, and that the flake compares the selected Rust toolchain to Cargo.toml rust-version - Keeps the configurator's libadwaita 1.4 floor aligned across Cargo, deb, rpm, PKGBUILD, and `.SRCINFO` - With `--release-version X.Y.Z[.N]`, rejects tags that do not match Cargo or an explicit packaging hotfix of Cargo - Usage: `bash tools/check-version-consistency.sh [--release-version X.Y.Z[.N]]` diff --git a/tools/check-nixpkgs-recipe.py b/tools/check-nixpkgs-recipe.py index f08bfc491..4ff626cf1 100755 --- a/tools/check-nixpkgs-recipe.py +++ b/tools/check-nixpkgs-recipe.py @@ -27,7 +27,7 @@ CARGO_TOML = Path("Cargo.toml") RECIPE = Path("packaging/nixpkgs/package.nix") FLAKE = Path("flake.nix") -FLAKE_PACKAGE_MARKER = "wayscriber = pkgs.rustPlatform.buildRustPackage" +FLAKE_PACKAGE_MARKER = "wayscriber = rustPlatform.buildRustPackage" LINUX_TARGET = "x86_64-unknown-linux-gnu" # Native tools/hooks required by the default GTK-enabled package. diff --git a/tools/check-version-consistency.sh b/tools/check-version-consistency.sh index 00e4a9954..d82e9715e 100755 --- a/tools/check-version-consistency.sh +++ b/tools/check-version-consistency.sh @@ -350,10 +350,10 @@ if "builtins.fromTOML (builtins.readFile ./Cargo.toml)" not in flake_text: errors.append("flake.nix package version should be derived from Cargo.toml") if not all( token in flake_text - for token in ("package.rust-version", "pkgs.rustc.version", "versionAtLeast") + for token in ("package.rust-version", "rustToolchain.version", "versionAtLeast") ): errors.append( - "flake.nix should compare nixpkgs rustc against Cargo.toml rust-version" + "flake.nix should compare the selected rustc against Cargo.toml rust-version" ) # Install examples that pin a concrete tag are stale one release later.