diff --git a/README.md b/README.md
index e1c06b03..64d6812b 100644
--- a/README.md
+++ b/README.md
@@ -178,7 +178,7 @@ For distro-specific package details, see [Installation](#installation). For keyb
| Platform | Status | Notes |
|----------|--------|-------|
| Wayland (layer-shell) | ✅ Supported | Hyprland, Sway, River, Wayfire, Niri/Cosmic, Plasma/KWin |
-| GNOME | ⚠️ Partial | Portal fallback; overlay windowed |
+| GNOME | ⚠️ Partial | Normal overlay and Freeze via portal when available; Light Mode passthrough unavailable |
| X11 | ❌ | Not supported |
@@ -256,8 +256,8 @@ For distro-specific package details, see [Installation](#installation). For keyb
- Click highlights with configurable colors/radius/duration
- Persistent ring while click highlight tool is active
- Presenter mode (Ctrl+Shift+M): hides UI, forces click highlights
-- Light passthrough mode (layer-shell): Ctrl+Shift+L enters from the focused overlay; compositor/global shortcuts such as `wayscriber --light-toggle` keep control reliable while input is passed through
-- Screen freeze (Ctrl+Shift+F): pause display while apps run
+- Light passthrough mode (layer-shell): Ctrl+Shift+L enters from the focused overlay; compositor/global shortcuts such as `wayscriber --light-toggle` keep control reliable while input is passed through. Stock GNOME Wayland does not expose the overlay behavior this mode needs.
+- Screen freeze (Ctrl+Shift+F): pause display while apps run. On GNOME, this uses the screenshot portal when available.
### Callouts & Zoom
- **Numbered callouts:** auto-numbered arrow labels, step markers
@@ -510,6 +510,7 @@ Light passthrough controls:
- Ctrl+Shift+L is a Wayscriber in-overlay shortcut, not an OS/global shortcut. It works while the overlay is focused.
- Once light passthrough is active, normal keyboard and pointer input goes to the app underneath. Bind compositor/global shortcuts to `wayscriber --light-toggle` and `wayscriber --light-draw-toggle` for reliable control.
- Use `wayscriber --light-draw-on` on press and `wayscriber --light-draw-off` on release for draw-while-held shortcuts.
+- Stock GNOME Wayland does not support this regular-app passthrough mode. Freeze may still work for still-image capture, but it is not a live passthrough replacement. A GNOME Shell extension approach would be needed for true shell-level passthrough.
Use `--no-tray` or `WAYSCRIBER_NO_TRAY=1` if you don't have a system tray; otherwise right-click the tray icon for options:
- Toggle overlay visibility
@@ -735,7 +736,7 @@ The polygon tools are available from the toolbar picker; their default keybindin
-For light passthrough, Ctrl+Shift+L is the default Wayscriber-level binding only while the overlay has focus. Use compositor/global shortcuts that run `wayscriber --light-toggle` and related light-draw commands once passthrough is active.
+For light passthrough, Ctrl+Shift+L is the default Wayscriber-level binding only while the overlay has focus. Use compositor/global shortcuts that run `wayscriber --light-toggle` and related light-draw commands once passthrough is active. On stock GNOME Wayland, regular app windows cannot provide the required click-through shell overlay. Freeze may still work for still-image capture, but it is not a live passthrough replacement.
Arrow labels can auto-number when enabled in the arrow toolbar; reset with Ctrl+Shift+R. Step markers auto-increment and reset from the toolbar (or bind `reset_step_markers` in `config.toml`). Preset slots can be saved/cleared from the toolbar; edit names and advanced fields in `config.toml`. Blur has no default keyboard shortcut; bind `select_blur_tool` in `config.toml` if you want direct keyboard access.
diff --git a/docs/CONFIG.md b/docs/CONFIG.md
index a1ba3dc5..91f83512 100644
--- a/docs/CONFIG.md
+++ b/docs/CONFIG.md
@@ -431,7 +431,7 @@ show_toast = true
Light mode hides UI chrome and sets the overlay to click-through passthrough until drawing is explicitly enabled. `toggle_light_mode` defaults to Ctrl+Shift+L, but that is a Wayscriber in-overlay shortcut: it works while the overlay still has focus. Once passthrough is active, normal keyboard and pointer input goes to the app underneath, so compositor/global shortcuts should call the daemon commands below for reliable control.
-This mode requires layer-shell support; it is disabled on the xdg fallback because keyboard input cannot be passed through reliably there.
+This mode requires compositor overlay support through layer-shell. It is disabled on the xdg fallback because regular app windows cannot reliably stay visible as click-through shell overlays while keyboard and pointer input go to apps underneath. On stock GNOME Wayland, Freeze may still work for still-image capture when portal capture is available, but it is not a live passthrough replacement. True passthrough would require a GNOME Shell extension companion.
For compositor/global shortcuts while passthrough is active, run:
diff --git a/docs/SETUP.md b/docs/SETUP.md
index 72ae9c75..9ba6e41f 100644
--- a/docs/SETUP.md
+++ b/docs/SETUP.md
@@ -137,7 +137,7 @@ wayscriber --light-draw-off
Use `--light-toggle` for passthrough on/off and `--light-draw-toggle` for sticky drawing. Draw-while-held needs a shortcut system that can run one command on press and another on release; if your KDE shortcut UI only supports activation commands, use the sticky draw toggle instead.
-Light passthrough still requires layer-shell support. If Wayscriber reports that light mode requires layer-shell, your session is using a fallback path where passthrough is not available.
+Light passthrough requires compositor overlay support through layer-shell. If Wayscriber reports that passthrough is unavailable, your session is using a fallback path where regular app windows cannot provide a reliable click-through overlay.
### GNOME: Fedora Workstation and Ubuntu GNOME
@@ -153,7 +153,9 @@ Then use the configurator's Daemon tab, or create a GNOME custom shortcut that r
wayscriber --daemon-toggle
```
-Light passthrough mode is not currently available on GNOME sessions where Wayscriber uses the xdg-shell fallback. In that fallback, keyboard passthrough cannot be made reliable, so `--light-toggle` is intentionally disabled instead of pretending to pass input through.
+Freeze works on GNOME when the screenshot portal is available and responsive; the first use may show a desktop permission prompt. Portal capture can be slower than compositor screencopy, and mixed-DPI or multi-monitor setups may depend on client-side crop behavior.
+
+Light passthrough mode is not available in the regular app on stock GNOME Wayland. GNOME's xdg-shell fallback does not expose the shell-level overlay behavior needed to keep annotations visible while input goes to apps underneath, so `--light-toggle` is intentionally disabled instead of pretending to pass input through. A GNOME Shell extension companion would be the real path for that workflow.
### Method 3: One-Shot Mode (Alternative)
diff --git a/src/backend/wayland/backend/event_loop/capture.rs b/src/backend/wayland/backend/event_loop/capture.rs
index 3e08e265..422303ee 100644
--- a/src/backend/wayland/backend/event_loop/capture.rs
+++ b/src/backend/wayland/backend/event_loop/capture.rs
@@ -12,9 +12,40 @@ use crate::notification;
pub(super) fn poll_portal_captures(state: &mut WaylandState) {
// Apply any completed portal fallback captures without blocking.
state.frozen.poll_portal_capture(&mut state.input_state);
+ handle_pending_frozen_image(state);
state.zoom.poll_portal_capture(&mut state.input_state);
}
+fn handle_pending_frozen_image(state: &mut WaylandState) {
+ if !state.frozen.has_pending_image() {
+ return;
+ }
+ if state.surface.is_xdg_window() {
+ if state.xdg_fullscreen() {
+ state.activate_pending_frozen_image_for_current_surface();
+ return;
+ }
+ if !state.xdg_frozen_fullscreen_requested() && state.begin_xdg_frozen_fullscreen() {
+ return;
+ }
+ if state.xdg_frozen_fullscreen_pending_configure() {
+ if state.xdg_frozen_fullscreen_timed_out() {
+ warn!("Frozen xdg fullscreen configure timed out; cancelling freeze");
+ state.input_state.set_ui_toast(
+ UiToastKind::Error,
+ "Freeze failed because fullscreen was not confirmed",
+ );
+ state.restore_xdg_after_frozen();
+ state.frozen.cancel(&mut state.input_state);
+ }
+ return;
+ }
+ state.activate_pending_frozen_image_for_current_surface();
+ return;
+ }
+ state.activate_pending_frozen_image_for_current_surface();
+}
+
pub(super) fn flush_if_capture_active(conn: &Connection, capture_active: bool) {
if capture_active {
let _ = conn.flush();
@@ -61,10 +92,17 @@ fn handle_frozen_toggle(state: &mut WaylandState) {
}
if !state.frozen_enabled() {
- warn!("Frozen mode disabled on this compositor (xdg fallback); ignoring toggle");
+ warn!(
+ "Frozen mode unavailable: no screencopy backend and no screenshot portal backend; ignoring toggle"
+ );
+ state.input_state.set_ui_toast(
+ UiToastKind::Warning,
+ "Freeze is unavailable because screen capture is not available.",
+ );
} else if state.frozen.is_in_progress() {
warn!("Frozen capture already in progress; ignoring toggle");
} else if state.input_state.frozen_active() {
+ state.restore_xdg_after_frozen();
state.frozen.unfreeze(&mut state.input_state);
} else {
let use_fallback = !state.frozen.manager_available();
@@ -73,7 +111,14 @@ fn handle_frozen_toggle(state: &mut WaylandState) {
} else {
info!("Frozen mode: using screencopy fast path");
}
- state.enter_overlay_suppression(OverlaySuppression::Frozen);
+ if !state.enter_overlay_suppression(OverlaySuppression::Frozen) {
+ warn!("Frozen mode requested while overlay is suppressed; ignoring toggle");
+ state.input_state.set_ui_toast(
+ UiToastKind::Warning,
+ "Freeze is already preparing another overlay operation.",
+ );
+ return;
+ }
if let Err(err) = state
.frozen
.start_capture(use_fallback, &state.tokio_handle)
diff --git a/src/backend/wayland/backend/event_loop/mod.rs b/src/backend/wayland/backend/event_loop/mod.rs
index ae5b7a71..ee6a836b 100644
--- a/src/backend/wayland/backend/event_loop/mod.rs
+++ b/src/backend/wayland/backend/event_loop/mod.rs
@@ -167,7 +167,7 @@ pub(super) fn run_event_loop(
notification::send_notification_async(
&state.tokio_handle,
"Wayscriber lost focus".to_string(),
- "GNOME could not keep the overlay focused; closing fallback window."
+ "The desktop could not keep the overlay focused, so Wayscriber closed it."
.to_string(),
Some("dialog-warning".to_string()),
);
diff --git a/src/backend/wayland/backend/helpers.rs b/src/backend/wayland/backend/helpers.rs
index 7cdeebea..c340d215 100644
--- a/src/backend/wayland/backend/helpers.rs
+++ b/src/backend/wayland/backend/helpers.rs
@@ -26,7 +26,7 @@ pub(super) fn friendly_capture_error(error: &str) -> String {
} else if lower.contains("permission") {
"Permission denied. Enable screen sharing in system settings.".to_string()
} else if lower.contains("portal returned error code") {
- "Portal screenshot failed. If you use wlroots/Hyprland/Niri, install grim + slurp. Otherwise check xdg-desktop-portal."
+ "Screen capture failed. If you use Hyprland, Niri, or another wlroots desktop, install grim + slurp. Otherwise check the desktop screen capture service."
.to_string()
} else if lower.contains("busy") {
"Screen capture in progress. Try again in a moment.".to_string()
@@ -207,7 +207,7 @@ mod tests {
);
assert_eq!(
friendly_capture_error("portal returned error code 2"),
- "Portal screenshot failed. If you use wlroots/Hyprland/Niri, install grim + slurp. Otherwise check xdg-desktop-portal."
+ "Screen capture failed. If you use Hyprland, Niri, or another wlroots desktop, install grim + slurp. Otherwise check the desktop screen capture service."
);
assert_eq!(
friendly_capture_error("resource busy"),
diff --git a/src/backend/wayland/backend/setup.rs b/src/backend/wayland/backend/setup.rs
index 50aab128..c5e5918a 100644
--- a/src/backend/wayland/backend/setup.rs
+++ b/src/backend/wayland/backend/setup.rs
@@ -124,7 +124,7 @@ pub(super) fn setup_wayland() -> Result {
}
Err(err) => {
warn!(
- "zwlr_screencopy_manager_v1 not available (frozen mode disabled): {}",
+ "zwlr_screencopy_manager_v1 not available; frozen mode may use portal fallback: {}",
err
);
None
diff --git a/src/backend/wayland/backend/state_init/mod.rs b/src/backend/wayland/backend/state_init/mod.rs
index 508c06c5..3a7ffe89 100644
--- a/src/backend/wayland/backend/state_init/mod.rs
+++ b/src/backend/wayland/backend/state_init/mod.rs
@@ -1,15 +1,18 @@
use anyhow::Result;
use log::{info, warn};
use smithay_client_toolkit::globals::ProvidesBoundGlobal;
+use std::env;
use super::super::state::{WaylandState, WaylandStateInit};
use super::WaylandBackend;
use super::setup::WaylandSetup;
use super::tray::process_tray_action;
+use crate::backend::wayland::portal_capture::screenshot_portal_available;
use crate::{
capture::CaptureManager,
config::Config,
- input::{InputState, state::CompositorCapabilities},
+ input::InputState,
+ input::state::{CompositorCapabilities, DesktopEnvironment, ShellMode},
onboarding::{DEFERRED_HINT_REPEAT_MAX, OnboardingStore},
};
@@ -43,16 +46,29 @@ pub(super) fn init_state(backend: &WaylandBackend, setup: WaylandSetup) -> Resul
let mut input_state = input_state::build_input_state(&config);
input_state.set_session_preflight_options(session_options.clone());
+ let screencopy_supported = setup.screencopy_manager.is_some();
+ let portal_freeze_supported = screenshot_portal_available(&backend.tokio_runtime);
+ let frozen_supported = screencopy_supported || portal_freeze_supported;
+ let tokio_handle = backend.tokio_runtime.handle().clone();
// Set compositor capabilities based on detected Wayland protocols
input_state.compositor_capabilities = CompositorCapabilities {
layer_shell: setup.layer_shell_available,
- screencopy: setup.screencopy_manager.is_some(),
+ screencopy: screencopy_supported,
+ freeze_capture: frozen_supported,
pointer_constraints: setup
.state_globals
.pointer_constraints_state
.bound_global()
.is_ok(),
+ desktop_environment: desktop_environment_from_env(),
+ shell_mode: if setup.layer_shell_available {
+ ShellMode::LayerShell
+ } else if setup.state_globals.xdg_shell.is_some() {
+ ShellMode::XdgFallback
+ } else {
+ ShellMode::Unknown
+ },
};
let mut onboarding = OnboardingStore::load();
@@ -91,11 +107,10 @@ pub(super) fn init_state(backend: &WaylandBackend, setup: WaylandSetup) -> Resul
let capture_manager = CaptureManager::new(backend.tokio_runtime.handle());
info!("Capture manager initialized");
- let tokio_handle = backend.tokio_runtime.handle().clone();
-
- let frozen_supported = setup.layer_shell_available;
let freeze_on_start = if backend.freeze_on_start && !frozen_supported {
- warn!("Frozen mode is not supported on GNOME xdg fallback; ignoring --freeze");
+ warn!(
+ "Frozen mode unavailable: no screencopy backend and no screenshot portal backend; ignoring --freeze"
+ );
false
} else {
backend.freeze_on_start
@@ -144,3 +159,19 @@ fn apply_initial_mode(backend: &WaylandBackend, _config: &Config, input_state: &
}
}
}
+
+fn desktop_environment_from_env() -> DesktopEnvironment {
+ let values = [
+ env::var("XDG_CURRENT_DESKTOP").unwrap_or_default(),
+ env::var("XDG_SESSION_DESKTOP").unwrap_or_default(),
+ env::var("DESKTOP_SESSION").unwrap_or_default(),
+ ];
+ if values
+ .iter()
+ .any(|value| value.to_ascii_uppercase().contains("GNOME"))
+ {
+ DesktopEnvironment::Gnome
+ } else {
+ DesktopEnvironment::Unknown
+ }
+}
diff --git a/src/backend/wayland/frozen/capture.rs b/src/backend/wayland/frozen/capture.rs
index f45a3ca0..3c259d48 100644
--- a/src/backend/wayland/frozen/capture.rs
+++ b/src/backend/wayland/frozen/capture.rs
@@ -4,10 +4,7 @@ use smithay_client_toolkit::shm::{
Shm,
slot::{Buffer, SlotPool},
};
-use wayland_client::{
- Dispatch, QueueHandle, WEnum,
- protocol::{wl_output, wl_shm},
-};
+use wayland_client::{Dispatch, QueueHandle, WEnum, protocol::wl_shm};
use wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_frame_v1::{
Event as FrameEvent, Flags, ZwlrScreencopyFrameV1,
};
@@ -75,7 +72,7 @@ impl FrozenState {
pub fn start_capture(
&mut self,
use_fallback: bool,
- tokio_handle: &tokio::runtime::Handle,
+ _tokio_handle: &tokio::runtime::Handle,
) -> Result<()> {
if self.capture.is_some() || self.portal_in_progress || self.preflight_pending {
warn!("Frozen-mode capture already in progress; ignoring toggle");
@@ -83,14 +80,28 @@ impl FrozenState {
}
self.capture_done = false;
+ self.preflight_use_fallback = use_fallback || self.manager.is_none();
+ self.preflight_pending = true;
+ Ok(())
+ }
+ pub fn begin_preflight_capture(
+ &mut self,
+ use_fallback: bool,
+ shm: &Shm,
+ qh: &QueueHandle,
+ tokio_handle: &tokio::runtime::Handle,
+ ) -> Result<()>
+ where
+ State:
+ Dispatch + Dispatch + 'static,
+ {
if use_fallback || self.manager.is_none() {
- info!("Screencopy unavailable; using fallback portal capture for frozen mode");
- return self.capture_via_portal(tokio_handle);
+ info!("Suppression frame committed; using fallback portal capture for frozen mode");
+ self.capture_via_portal(tokio_handle)
+ } else {
+ self.begin_screencopy(shm, qh)
}
-
- self.preflight_pending = true;
- Ok(())
}
pub fn begin_screencopy(&mut self, shm: &Shm, qh: &QueueHandle) -> Result<()>
@@ -168,10 +179,7 @@ impl FrozenState {
return;
}
- input_state.set_frozen_active(true);
- input_state.dirty_tracker.mark_full();
input_state.needs_redraw = true;
- self.capture_done = true;
}
FrameEvent::Failed => {
warn!("Frozen capture failed");
@@ -268,20 +276,16 @@ impl FrozenState {
capture.frame.destroy();
- let output_transform = self
- .active_geometry
- .as_ref()
- .map(|geo| geo.transform)
- .unwrap_or(wl_output::Transform::Normal);
-
- self.set_image(
+ let source_geometry = self.active_geometry.clone();
+ self.set_pending_image(
FrozenImage {
width: capture.width,
height: capture.height,
stride: (capture.width * 4) as i32,
data,
- }
- .with_output_transform(output_transform),
+ },
+ source_geometry,
+ true,
);
Ok(())
diff --git a/src/backend/wayland/frozen/mod.rs b/src/backend/wayland/frozen/mod.rs
index 3cc57abf..41833cb1 100644
--- a/src/backend/wayland/frozen/mod.rs
+++ b/src/backend/wayland/frozen/mod.rs
@@ -6,5 +6,12 @@ mod state;
pub use image::FrozenImage;
pub use state::FrozenState;
-type PortalCaptureResult = Result<(Option, self::image::FrozenImage), String>;
+type PortalCaptureResult = Result<
+ (
+ Option,
+ Option,
+ self::image::FrozenImage,
+ ),
+ String,
+>;
type PortalCaptureRx = std::sync::mpsc::Receiver;
diff --git a/src/backend/wayland/frozen/portal.rs b/src/backend/wayland/frozen/portal.rs
index 8dc7ecc8..302cf272 100644
--- a/src/backend/wayland/frozen/portal.rs
+++ b/src/backend/wayland/frozen/portal.rs
@@ -4,10 +4,11 @@ use std::sync::mpsc;
use crate::backend::wayland::frozen::FrozenImage;
use crate::backend::wayland::portal_capture::{
- capture_via_portal_fullscreen_bytes, crop_argb, portal_output_matches,
+ capture_via_portal_fullscreen_bytes, portal_output_matches,
};
use crate::capture::sources::frozen::decode_image_to_argb;
use crate::input::InputState;
+use crate::input::state::UiToastKind;
use super::PortalCaptureResult;
use super::state::FrozenState;
@@ -29,47 +30,25 @@ impl FrozenState {
self.portal_rx = Some(rx);
self.portal_target_output_id = self.active_output_id;
- let geo = self.active_geometry.clone();
+ let source_geometry = self.active_geometry.clone();
let target_output_id = self.active_output_id;
// Notify user that portal fallback is in progress
crate::notification::send_notification_async(
tokio_handle,
"Freezing screen".to_string(),
- "Taking screenshot via portal…".to_string(),
+ "Requesting screen capture...".to_string(),
Some("camera-photo".to_string()),
);
tokio_handle.spawn(async move {
let result = async {
let bytes = capture_via_portal_fullscreen_bytes().await?;
- let (mut data, mut width, mut height) =
+ let (data, width, height) =
decode_image_to_argb(&bytes).map_err(|e| format!("Decode failed: {}", e))?;
- if let Some(geo) = geo {
- let (phys_w, phys_h) = geo.physical_size();
- let (origin_x, origin_y) = geo.physical_origin();
- if origin_x >= 0
- && origin_y >= 0
- && phys_w > 0
- && phys_h > 0
- && let Some(cropped) = crop_argb(
- &data,
- width,
- height,
- origin_x as u32,
- origin_y as u32,
- phys_w,
- phys_h,
- )
- {
- data = cropped;
- width = phys_w;
- height = phys_h;
- }
- }
-
Ok((
target_output_id,
+ source_geometry,
FrozenImage {
width,
height,
@@ -97,6 +76,10 @@ impl FrozenState {
&& start.elapsed() > std::time::Duration::from_secs(10)
{
warn!("Portal frozen capture timed out; restoring overlay");
+ input_state.set_ui_toast(
+ UiToastKind::Error,
+ "Freeze timed out while waiting for screen capture.",
+ );
input_state.set_frozen_active(false);
self.portal_in_progress = false;
self.portal_rx = None;
@@ -108,28 +91,27 @@ impl FrozenState {
if let Some(rx) = self.portal_rx.as_ref() {
match rx.try_recv() {
- Ok(Ok((target_output, image))) => {
+ Ok(Ok((target_output, source_geometry, image))) => {
let output_matches =
portal_output_matches(target_output, self.active_output_id);
if output_matches {
- self.set_image(image);
- input_state.set_frozen_active(true);
- input_state.dirty_tracker.mark_full();
- input_state.needs_redraw = true;
+ self.set_pending_image(image, source_geometry, false);
} else {
warn!("Portal capture for inactive output discarded");
input_state.set_frozen_active(false);
+ self.capture_done = true;
}
self.portal_in_progress = false;
self.portal_rx = None;
self.portal_target_output_id = None;
self.portal_started_at = None;
- self.capture_done = true;
}
Ok(Err(err)) => {
warn!("Portal frozen capture failed: {}", err);
+ input_state
+ .set_ui_toast(UiToastKind::Error, "Freeze could not capture the screen.");
input_state.set_frozen_active(false);
self.portal_in_progress = false;
self.portal_rx = None;
@@ -140,6 +122,10 @@ impl FrozenState {
Err(mpsc::TryRecvError::Empty) => {}
Err(mpsc::TryRecvError::Disconnected) => {
warn!("Portal frozen capture channel disconnected");
+ input_state.set_ui_toast(
+ UiToastKind::Error,
+ "Freeze could not capture the screen because the system capture service stopped responding.",
+ );
input_state.set_frozen_active(false);
self.portal_in_progress = false;
self.portal_rx = None;
@@ -193,6 +179,7 @@ mod tests {
frozen.portal_rx = Some(rx);
frozen.portal_in_progress = true;
tx.send(Ok((
+ None,
None,
FrozenImage {
width: 2,
@@ -205,9 +192,17 @@ mod tests {
frozen.poll_portal_capture(&mut input);
- assert!(input.frozen_active());
+ assert!(!input.frozen_active());
+ assert!(frozen.has_pending_image());
assert!(!frozen.portal_in_progress);
assert!(frozen.portal_rx.is_none());
+ assert!(!frozen.take_capture_done());
+
+ frozen
+ .activate_pending_image(2, 1, &mut input)
+ .expect("activate pending image");
+
+ assert!(input.frozen_active());
assert!(frozen.image.is_some());
assert!(frozen.take_capture_done());
}
diff --git a/src/backend/wayland/frozen/state.rs b/src/backend/wayland/frozen/state.rs
index 6f6bc50a..146c81f1 100644
--- a/src/backend/wayland/frozen/state.rs
+++ b/src/backend/wayland/frozen/state.rs
@@ -4,11 +4,18 @@ use wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_manager_v1::Z
use crate::backend::wayland::frozen::FrozenImage;
use crate::backend::wayland::frozen_geometry::OutputGeometry;
+use crate::backend::wayland::portal_capture::crop_argb;
use crate::input::InputState;
use super::PortalCaptureRx;
use super::capture::CaptureSession;
+struct PendingFrozenImage {
+ image: FrozenImage,
+ source_geometry: Option,
+ needs_output_transform: bool,
+}
+
/// End-to-end controller for frozen mode capture and image storage.
#[allow(clippy::type_complexity)]
pub struct FrozenState {
@@ -24,7 +31,9 @@ pub struct FrozenState {
pub(super) portal_target_output_id: Option,
pub(super) portal_started_at: Option,
pub(super) preflight_pending: bool,
+ pub(super) preflight_use_fallback: bool,
pub(super) capture_done: bool,
+ pending_image: Option,
}
impl FrozenState {
@@ -42,7 +51,9 @@ impl FrozenState {
portal_target_output_id: None,
portal_started_at: None,
preflight_pending: false,
+ preflight_use_fallback: false,
capture_done: false,
+ pending_image: None,
}
}
@@ -77,18 +88,42 @@ impl FrozenState {
self.bump_image_generation();
}
+ pub fn set_pending_image(
+ &mut self,
+ image: FrozenImage,
+ source_geometry: Option,
+ needs_output_transform: bool,
+ ) {
+ self.pending_image = Some(PendingFrozenImage {
+ image,
+ source_geometry,
+ needs_output_transform,
+ });
+ }
+
+ pub fn has_pending_image(&self) -> bool {
+ self.pending_image.is_some()
+ }
+
pub fn is_in_progress(&self) -> bool {
- self.capture.is_some() || self.portal_in_progress || self.preflight_pending
+ self.capture.is_some()
+ || self.portal_in_progress
+ || self.preflight_pending
+ || self.pending_image.is_some()
}
pub fn preflight_pending(&self) -> bool {
self.preflight_pending
}
- pub fn take_preflight_pending(&mut self) -> bool {
- let pending = self.preflight_pending;
+ pub fn take_preflight_pending(&mut self) -> Option {
+ if !self.preflight_pending {
+ return None;
+ }
+ let use_fallback = self.preflight_use_fallback;
self.preflight_pending = false;
- pending
+ self.preflight_use_fallback = false;
+ Some(use_fallback)
}
pub fn take_capture_done(&mut self) -> bool {
@@ -97,6 +132,84 @@ impl FrozenState {
done
}
+ pub fn activate_pending_image(
+ &mut self,
+ phys_width: u32,
+ phys_height: u32,
+ input_state: &mut InputState,
+ ) -> Result {
+ let Some(pending) = self.pending_image.take() else {
+ return Ok(false);
+ };
+ let mut image = pending.image;
+
+ if pending.needs_output_transform {
+ let output_transform = pending
+ .source_geometry
+ .as_ref()
+ .or(self.active_geometry.as_ref())
+ .map(|geo| geo.transform)
+ .unwrap_or(wl_output::Transform::Normal);
+ image = image.with_output_transform(output_transform);
+ }
+
+ if image.width != phys_width || image.height != phys_height {
+ let Some(cropped) = self.crop_pending_image(
+ image,
+ pending.source_geometry.as_ref(),
+ phys_width,
+ phys_height,
+ ) else {
+ self.capture_done = true;
+ input_state.set_frozen_active(false);
+ input_state.needs_redraw = true;
+ return Err("Freeze failed after the display changed size".to_string());
+ };
+ image = cropped;
+ }
+
+ self.set_image(image);
+ input_state.set_frozen_active(true);
+ input_state.dirty_tracker.mark_full();
+ input_state.needs_redraw = true;
+ self.capture_done = true;
+ Ok(true)
+ }
+
+ fn crop_pending_image(
+ &self,
+ image: FrozenImage,
+ source_geometry: Option<&OutputGeometry>,
+ target_width: u32,
+ target_height: u32,
+ ) -> Option {
+ if target_width == 0 || target_height == 0 {
+ return None;
+ }
+ let (origin_x, origin_y) = source_geometry
+ .or(self.active_geometry.as_ref())
+ .map(|geo| geo.physical_origin())
+ .unwrap_or((0, 0));
+ let (width, height, data) = crop_argb(
+ &image.data,
+ image.width,
+ image.height,
+ origin_x.max(0) as u32,
+ origin_y.max(0) as u32,
+ target_width,
+ target_height,
+ )?;
+ if width != target_width || height != target_height {
+ return None;
+ }
+ Some(FrozenImage {
+ width: target_width,
+ height: target_height,
+ stride: (target_width * 4) as i32,
+ data,
+ })
+ }
+
/// Drop frozen image if the surface size no longer matches.
pub fn handle_resize(
&mut self,
@@ -126,6 +239,8 @@ impl FrozenState {
capture.frame.destroy();
}
self.preflight_pending = false;
+ self.preflight_use_fallback = false;
+ self.pending_image = None;
self.capture_done = true;
input_state.set_frozen_active(false);
input_state.needs_redraw = true;
diff --git a/src/backend/wayland/handlers/compositor.rs b/src/backend/wayland/handlers/compositor.rs
index 0a380cbe..d27f738a 100644
--- a/src/backend/wayland/handlers/compositor.rs
+++ b/src/backend/wayland/handlers/compositor.rs
@@ -68,8 +68,10 @@ impl CompositorHandler for WaylandState {
);
self.surface.set_frame_callback_pending(false);
- if self.frozen.take_preflight_pending()
- && let Err(err) = self.frozen.begin_screencopy(&self.shm, qh)
+ if let Some(use_fallback) = self.frozen.take_preflight_pending()
+ && let Err(err) =
+ self.frozen
+ .begin_preflight_capture(use_fallback, &self.shm, qh, &self.tokio_handle)
{
warn!("Frozen preflight capture failed: {}", err);
self.frozen.cancel(&mut self.input_state);
diff --git a/src/backend/wayland/handlers/keyboard/mod.rs b/src/backend/wayland/handlers/keyboard/mod.rs
index cca0c951..efd19223 100644
--- a/src/backend/wayland/handlers/keyboard/mod.rs
+++ b/src/backend/wayland/handlers/keyboard/mod.rs
@@ -84,7 +84,8 @@ impl KeyboardHandler for WaylandState {
notification::send_notification_async(
&self.tokio_handle,
"Wayscriber lost focus".to_string(),
- "GNOME could not keep the overlay focused; closing fallback window.".to_string(),
+ "The desktop could not keep the overlay focused, so Wayscriber closed it."
+ .to_string(),
Some("dialog-warning".to_string()),
);
self.input_state.should_exit = true;
diff --git a/src/backend/wayland/handlers/xdg.rs b/src/backend/wayland/handlers/xdg.rs
index 6fad76b6..4b82b1a5 100644
--- a/src/backend/wayland/handlers/xdg.rs
+++ b/src/backend/wayland/handlers/xdg.rs
@@ -68,7 +68,13 @@ impl WindowHandler for WaylandState {
.map(|h| h.get())
.unwrap_or(fallback_dimensions.1);
- if self.xdg_fullscreen() {
+ if self.xdg_frozen_fullscreen_requested() {
+ if let Some(output) = self.preferred_fullscreen_output() {
+ window.set_fullscreen(Some(&output));
+ } else if !configure.is_fullscreen() {
+ window.set_fullscreen(None);
+ }
+ } else if self.xdg_fullscreen() {
if let Some(output) = self.preferred_fullscreen_output() {
// Reassert fullscreen on the preferred output every configure in case
// the compositor picked a different monitor initially.
@@ -132,6 +138,18 @@ impl WindowHandler for WaylandState {
self.frozen.set_active_geometry(Some(geo.clone()));
self.zoom.set_active_geometry(Some(geo));
}
+ if self.xdg_frozen_fullscreen_requested() && self.frozen.has_pending_image() {
+ if self.xdg_frozen_fullscreen_pending_configure() && !configure.is_fullscreen() {
+ warn!("xdg frozen fullscreen was not granted; activating freeze on current size");
+ }
+ self.activate_pending_frozen_image_for_current_surface();
+ }
+ if self.xdg_frozen_fullscreen_requested()
+ && !self.input_state.frozen_active()
+ && !self.frozen.has_pending_image()
+ {
+ self.restore_xdg_after_frozen();
+ }
self.input_state.needs_redraw = true;
// Surface is now sized; re-apply toolbar offsets so margins reflect configured bounds.
diff --git a/src/backend/wayland/portal_capture.rs b/src/backend/wayland/portal_capture.rs
index b3cf38e4..81b30dbf 100644
--- a/src/backend/wayland/portal_capture.rs
+++ b/src/backend/wayland/portal_capture.rs
@@ -1,3 +1,26 @@
+#[cfg(feature = "portal")]
+use std::time::Duration;
+
+#[cfg(feature = "portal")]
+const PORTAL_STARTUP_PROBE_TIMEOUT: Duration = Duration::from_millis(500);
+
+#[cfg(feature = "portal")]
+pub(crate) fn screenshot_portal_available(runtime: &tokio::runtime::Runtime) -> bool {
+ runtime.block_on(async {
+ tokio::time::timeout(
+ PORTAL_STARTUP_PROBE_TIMEOUT,
+ crate::capture::portal::is_portal_available(),
+ )
+ .await
+ .unwrap_or(false)
+ })
+}
+
+#[cfg(not(feature = "portal"))]
+pub(crate) fn screenshot_portal_available(_runtime: &tokio::runtime::Runtime) -> bool {
+ false
+}
+
#[cfg(feature = "portal")]
pub(crate) async fn capture_via_portal_fullscreen_bytes() -> Result, String> {
use crate::capture::sources::portal::capture_via_portal_bytes;
@@ -30,7 +53,7 @@ pub(crate) fn crop_argb(
y: u32,
crop_w: u32,
crop_h: u32,
-) -> Option> {
+) -> Option<(u32, u32, Vec)> {
if x >= width || y >= height {
return None;
}
@@ -38,6 +61,9 @@ pub(crate) fn crop_argb(
let max_h = height.saturating_sub(y);
let cw = crop_w.min(max_w);
let ch = crop_h.min(max_h);
+ if cw == 0 || ch == 0 {
+ return None;
+ }
let mut out = vec![0u8; (cw * ch * 4) as usize];
let src_stride = (width * 4) as usize;
@@ -52,7 +78,7 @@ pub(crate) fn crop_argb(
out[dst_offset..dst_offset + dst_stride]
.copy_from_slice(&data[src_offset..src_offset + dst_stride]);
}
- Some(out)
+ Some((cw, ch, out))
}
#[cfg(test)]
@@ -66,7 +92,8 @@ mod tests {
1, 2, 3, 4, 5, 6, 7, 8, //
9, 10, 11, 12, 13, 14, 15, 16,
];
- let cropped = crop_argb(&data, 2, 2, 1, 0, 1, 2).expect("crop");
+ let (width, height, cropped) = crop_argb(&data, 2, 2, 1, 0, 1, 2).expect("crop");
+ assert_eq!((width, height), (1, 2));
assert_eq!(cropped, vec![5, 6, 7, 8, 13, 14, 15, 16]);
}
diff --git a/src/backend/wayland/state.rs b/src/backend/wayland/state.rs
index a1632ca4..b9487cf5 100644
--- a/src/backend/wayland/state.rs
+++ b/src/backend/wayland/state.rs
@@ -63,7 +63,7 @@ use crate::{
};
use self::data::{MoveDrag, StateData};
-pub use self::data::{MoveDragKind, OverlaySuppression};
+pub use self::data::{MoveDragKind, OverlaySuppression, XdgFrozenFullscreenState};
use super::{
capture::{CapturePreflightRequest, CaptureState, PendingPdfExport},
clipboard::{ClipboardPasteCompletion, ClipboardPublishCompletion},
diff --git a/src/backend/wayland/state/core/accessors.rs b/src/backend/wayland/state/core/accessors.rs
index 6eed0ffa..dd1b6775 100644
--- a/src/backend/wayland/state/core/accessors.rs
+++ b/src/backend/wayland/state/core/accessors.rs
@@ -1,8 +1,10 @@
-use smithay_client_toolkit::shell::wlr_layer::Layer;
+use smithay_client_toolkit::shell::{WaylandSurface, wlr_layer::Layer};
use super::super::*;
use std::time::{Duration, Instant};
+const XDG_FROZEN_FULLSCREEN_TIMEOUT: Duration = Duration::from_millis(1500);
+
impl WaylandState {
pub(in crate::backend::wayland) fn current_mouse(&self) -> (i32, i32) {
(self.data.current_mouse_x, self.data.current_mouse_y)
@@ -213,6 +215,92 @@ impl WaylandState {
self.data.xdg_fullscreen
}
+ pub(in crate::backend::wayland) fn xdg_frozen_fullscreen_requested(&self) -> bool {
+ !matches!(
+ self.data.xdg_frozen_fullscreen_state,
+ crate::backend::wayland::state::XdgFrozenFullscreenState::Inactive
+ )
+ }
+
+ pub(in crate::backend::wayland) fn xdg_frozen_fullscreen_pending_configure(&self) -> bool {
+ matches!(
+ self.data.xdg_frozen_fullscreen_state,
+ crate::backend::wayland::state::XdgFrozenFullscreenState::PendingConfigure
+ )
+ }
+
+ pub(in crate::backend::wayland) fn xdg_frozen_fullscreen_timed_out(&self) -> bool {
+ self.xdg_frozen_fullscreen_pending_configure()
+ && self
+ .data
+ .xdg_frozen_fullscreen_requested_at
+ .is_some_and(|requested_at| requested_at.elapsed() >= XDG_FROZEN_FULLSCREEN_TIMEOUT)
+ }
+
+ pub(in crate::backend::wayland) fn begin_xdg_frozen_fullscreen(&mut self) -> bool {
+ let Some(window) = self.surface.xdg_window().cloned() else {
+ return false;
+ };
+ self.data.xdg_frozen_fullscreen_state =
+ crate::backend::wayland::state::XdgFrozenFullscreenState::PendingConfigure;
+ self.data.xdg_frozen_fullscreen_requested_at = Some(Instant::now());
+ if let Some(output) = self.preferred_fullscreen_output() {
+ window.set_fullscreen(Some(&output));
+ } else {
+ window.set_fullscreen(None);
+ }
+ window.commit();
+ true
+ }
+
+ pub(in crate::backend::wayland) fn restore_xdg_after_frozen(&mut self) {
+ if !self.xdg_frozen_fullscreen_requested() {
+ return;
+ }
+ if let Some(window) = self.surface.xdg_window().cloned() {
+ if self.xdg_fullscreen() {
+ if let Some(output) = self.preferred_fullscreen_output() {
+ window.set_fullscreen(Some(&output));
+ } else {
+ window.set_fullscreen(None);
+ }
+ } else {
+ window.unset_fullscreen();
+ window.set_maximized();
+ }
+ window.commit();
+ }
+ self.data.xdg_frozen_fullscreen_state =
+ crate::backend::wayland::state::XdgFrozenFullscreenState::Inactive;
+ self.data.xdg_frozen_fullscreen_requested_at = None;
+ }
+
+ pub(in crate::backend::wayland) fn activate_pending_frozen_image_for_current_surface(
+ &mut self,
+ ) {
+ let was_xdg_frozen_fullscreen = self.xdg_frozen_fullscreen_requested();
+ let (phys_width, phys_height) = self.surface.physical_dimensions();
+ match self
+ .frozen
+ .activate_pending_image(phys_width, phys_height, &mut self.input_state)
+ {
+ Ok(true) => {
+ if was_xdg_frozen_fullscreen {
+ self.data.xdg_frozen_fullscreen_state =
+ crate::backend::wayland::state::XdgFrozenFullscreenState::Active;
+ self.data.xdg_frozen_fullscreen_requested_at = None;
+ }
+ }
+ Ok(false) => {}
+ Err(err) => {
+ log::warn!("Frozen pending image activation failed: {}", err);
+ self.input_state
+ .set_ui_toast(crate::input::state::UiToastKind::Error, err);
+ self.restore_xdg_after_frozen();
+ }
+ }
+ }
+
pub(in crate::backend::wayland) fn main_surface_layer(&self) -> Layer {
if self.data.main_surface_uses_overlay_layer {
Layer::Overlay
diff --git a/src/backend/wayland/state/core/output.rs b/src/backend/wayland/state/core/output.rs
index e3332cae..4f390ef5 100644
--- a/src/backend/wayland/state/core/output.rs
+++ b/src/backend/wayland/state/core/output.rs
@@ -380,7 +380,7 @@ impl WaylandState {
if !self.xdg_fullscreen() {
self.input_state.set_ui_toast(
UiToastKind::Info,
- "Enable ui.xdg_fullscreen to switch outputs on xdg fallback",
+ "Enable fullscreen mode before switching outputs in this session.",
);
self.input_state.trigger_blocked_feedback();
return;
diff --git a/src/backend/wayland/state/data.rs b/src/backend/wayland/state/data.rs
index 23beb5a0..af7e6342 100644
--- a/src/backend/wayland/state/data.rs
+++ b/src/backend/wayland/state/data.rs
@@ -19,6 +19,14 @@ pub enum OverlaySuppression {
Zoom,
}
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub enum XdgFrozenFullscreenState {
+ #[default]
+ Inactive,
+ PendingConfigure,
+ Active,
+}
+
impl OverlaySuppression {
pub(in crate::backend::wayland) fn effective_for_board(
self,
@@ -107,6 +115,8 @@ pub struct StateData {
pub(super) has_seen_surface_enter: bool,
pub(super) preferred_output_identity: Option,
pub(super) xdg_fullscreen: bool,
+ pub(super) xdg_frozen_fullscreen_state: XdgFrozenFullscreenState,
+ pub(super) xdg_frozen_fullscreen_requested_at: Option,
pub(super) main_surface_uses_overlay_layer: bool,
pub(super) overlay_suppression: OverlaySuppression,
pub(super) overlay_clickthrough: bool,
@@ -181,6 +191,8 @@ impl StateData {
has_seen_surface_enter: false,
preferred_output_identity: None,
xdg_fullscreen: false,
+ xdg_frozen_fullscreen_state: XdgFrozenFullscreenState::Inactive,
+ xdg_frozen_fullscreen_requested_at: None,
main_surface_uses_overlay_layer: false,
overlay_suppression: OverlaySuppression::None,
overlay_clickthrough: false,
diff --git a/src/backend/wayland/zoom/portal.rs b/src/backend/wayland/zoom/portal.rs
index 3bd7b543..5dbcd653 100644
--- a/src/backend/wayland/zoom/portal.rs
+++ b/src/backend/wayland/zoom/portal.rs
@@ -34,7 +34,7 @@ impl ZoomState {
crate::notification::send_notification_async(
tokio_handle,
"Zoom capture".to_string(),
- "Taking screenshot via portal...".to_string(),
+ "Requesting screen capture...".to_string(),
Some("camera-photo".to_string()),
);
tokio_handle.spawn(async move {
@@ -51,7 +51,7 @@ impl ZoomState {
&& origin_y >= 0
&& phys_w > 0
&& phys_h > 0
- && let Some(cropped) = crop_argb(
+ && let Some((cropped_w, cropped_h, cropped)) = crop_argb(
&data,
width,
height,
@@ -62,8 +62,8 @@ impl ZoomState {
)
{
data = cropped;
- width = phys_w;
- height = phys_h;
+ width = cropped_w;
+ height = cropped_h;
}
}
diff --git a/src/input/state/core/base/mod.rs b/src/input/state/core/base/mod.rs
index 009cb802..b23c014f 100644
--- a/src/input/state/core/base/mod.rs
+++ b/src/input/state/core/base/mod.rs
@@ -5,11 +5,12 @@ pub use state::InputState;
pub(crate) use state::{LightModeRestore, PresenterRestore};
pub use types::{
BLOCKED_ACTION_DURATION_MS, BOARD_DELETE_CONFIRM_MS, BOARD_UNDO_EXPIRE_MS,
- CompositorCapabilities, DrawingState, MAX_STROKE_THICKNESS, MIN_STROKE_THICKNESS,
- OutputFocusAction, PAGE_DELETE_CONFIRM_MS, PAGE_UNDO_EXPIRE_MS, PRESET_FEEDBACK_DURATION_MS,
- PRESET_TOAST_DURATION_MS, PresetAction, PresetFeedbackKind, PressureThicknessEditMode,
- PressureThicknessEntryMode, SelectionAxis, SelectionHandle, TEXT_EDIT_ENTRY_DURATION_MS,
- TextInputMode, ToolbarDrawerTab, UI_TOAST_DURATION_MS, UiToastKind, ZoomAction,
+ CompositorCapabilities, DesktopEnvironment, DrawingState, MAX_STROKE_THICKNESS,
+ MIN_STROKE_THICKNESS, OutputFocusAction, PAGE_DELETE_CONFIRM_MS, PAGE_UNDO_EXPIRE_MS,
+ PRESET_FEEDBACK_DURATION_MS, PRESET_TOAST_DURATION_MS, PresetAction, PresetFeedbackKind,
+ PressureThicknessEditMode, PressureThicknessEntryMode, SelectionAxis, SelectionHandle,
+ ShellMode, TEXT_EDIT_ENTRY_DURATION_MS, TextInputMode, ToolbarDrawerTab, UI_TOAST_DURATION_MS,
+ UiToastKind, ZoomAction,
};
pub(crate) use types::{
BlockedActionFeedback, BoardPickerClickState, ClipboardFingerprint, ClipboardPasteRequest,
diff --git a/src/input/state/core/base/types.rs b/src/input/state/core/base/types.rs
index 6f715c4f..8ca6e822 100644
--- a/src/input/state/core/base/types.rs
+++ b/src/input/state/core/base/types.rs
@@ -289,7 +289,25 @@ pub(crate) enum HistoryMode {
pub struct CompositorCapabilities {
pub layer_shell: bool,
pub screencopy: bool,
+ pub freeze_capture: bool,
pub pointer_constraints: bool,
+ pub desktop_environment: DesktopEnvironment,
+ pub shell_mode: ShellMode,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub enum DesktopEnvironment {
+ Gnome,
+ #[default]
+ Unknown,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub enum ShellMode {
+ LayerShell,
+ XdgFallback,
+ #[default]
+ Unknown,
}
impl CompositorCapabilities {
@@ -300,10 +318,12 @@ impl CompositorCapabilities {
pub fn limitations_summary(&self) -> Option {
let mut issues = Vec::new();
if !self.layer_shell {
- issues.push("Toolbars limited");
+ issues.push("Toolbars limited, light passthrough unavailable");
}
- if !self.screencopy {
+ if !self.freeze_capture {
issues.push("Freeze unavailable");
+ } else if !self.screencopy {
+ issues.push("Freeze uses portal capture");
}
if !self.pointer_constraints {
issues.push("Pointer lock unavailable");
@@ -462,7 +482,10 @@ mod tests {
CompositorCapabilities {
layer_shell: true,
screencopy: true,
+ freeze_capture: true,
pointer_constraints: true,
+ desktop_environment: Default::default(),
+ shell_mode: Default::default(),
}
.limitations_summary(),
None
@@ -475,10 +498,34 @@ mod tests {
CompositorCapabilities {
layer_shell: false,
screencopy: true,
+ freeze_capture: true,
pointer_constraints: false,
+ desktop_environment: Default::default(),
+ shell_mode: Default::default(),
}
.limitations_summary(),
- Some("Toolbars limited, Pointer lock unavailable".to_string())
+ Some(
+ "Toolbars limited, light passthrough unavailable, Pointer lock unavailable"
+ .to_string()
+ )
+ );
+ }
+
+ #[test]
+ fn compositor_capabilities_reports_portal_freeze_without_hiding_limitations() {
+ let caps = CompositorCapabilities {
+ layer_shell: true,
+ screencopy: false,
+ freeze_capture: true,
+ pointer_constraints: true,
+ desktop_environment: Default::default(),
+ shell_mode: Default::default(),
+ };
+
+ assert!(!caps.all_available());
+ assert_eq!(
+ caps.limitations_summary(),
+ Some("Freeze uses portal capture".to_string())
);
}
}
diff --git a/src/input/state/core/mod.rs b/src/input/state/core/mod.rs
index 773d4263..42948f05 100644
--- a/src/input/state/core/mod.rs
+++ b/src/input/state/core/mod.rs
@@ -20,11 +20,11 @@ mod tour;
mod utility;
pub use base::{
- BLOCKED_ACTION_DURATION_MS, CompositorCapabilities, DrawingState, InputState,
- MAX_STROKE_THICKNESS, MIN_STROKE_THICKNESS, OutputFocusAction, PRESET_FEEDBACK_DURATION_MS,
- PRESET_TOAST_DURATION_MS, PresetAction, PresetFeedbackKind, PressureThicknessEditMode,
- PressureThicknessEntryMode, SelectionAxis, SelectionHandle, TextInputMode, ToolbarDrawerTab,
- UI_TOAST_DURATION_MS, UiToastKind, ZoomAction,
+ BLOCKED_ACTION_DURATION_MS, CompositorCapabilities, DesktopEnvironment, DrawingState,
+ InputState, MAX_STROKE_THICKNESS, MIN_STROKE_THICKNESS, OutputFocusAction,
+ PRESET_FEEDBACK_DURATION_MS, PRESET_TOAST_DURATION_MS, PresetAction, PresetFeedbackKind,
+ PressureThicknessEditMode, PressureThicknessEntryMode, SelectionAxis, SelectionHandle,
+ ShellMode, TextInputMode, ToolbarDrawerTab, UI_TOAST_DURATION_MS, UiToastKind, ZoomAction,
};
pub(crate) use base::{BoardPickerClickState, PolygonClickState, TextClickState};
pub(crate) use base::{
diff --git a/src/input/state/core/utility/light_mode.rs b/src/input/state/core/utility/light_mode.rs
index 2073c974..be61ca19 100644
--- a/src/input/state/core/utility/light_mode.rs
+++ b/src/input/state/core/utility/light_mode.rs
@@ -1,4 +1,6 @@
-use super::super::base::{InputState, LightModeRestore, UiToastKind};
+use super::super::base::{
+ DesktopEnvironment, InputState, LightModeRestore, ShellMode, UiToastKind,
+};
use crate::config::keybindings::Action;
use crate::input::tool::Tool;
@@ -7,6 +9,26 @@ impl InputState {
self.compositor_capabilities.layer_shell
}
+ fn light_mode_unsupported_message(&self) -> &'static str {
+ let caps = self.compositor_capabilities;
+ match (
+ caps.desktop_environment,
+ caps.shell_mode,
+ caps.freeze_capture,
+ ) {
+ (DesktopEnvironment::Gnome, ShellMode::XdgFallback, true) => {
+ "Light Mode passthrough is not supported in this GNOME Wayland session."
+ }
+ (DesktopEnvironment::Gnome, ShellMode::XdgFallback, false) => {
+ "Light Mode passthrough is not supported in this GNOME Wayland session. Screen capture is also unavailable."
+ }
+ (_, ShellMode::XdgFallback, _) => {
+ "Light Mode passthrough is not supported in this desktop session."
+ }
+ _ => "Light Mode passthrough is not supported by this compositor.",
+ }
+ }
+
pub fn light_mode_passthrough(&self) -> bool {
self.light_mode_supported() && self.light_mode && !self.light_mode_drawing
}
@@ -31,10 +53,7 @@ impl InputState {
self.exit_light_mode();
} else {
if !self.light_mode_supported() {
- self.set_ui_toast(
- UiToastKind::Warning,
- "Light Mode requires layer-shell support",
- );
+ self.set_ui_toast(UiToastKind::Warning, self.light_mode_unsupported_message());
self.needs_redraw = true;
return false;
}
@@ -56,10 +75,7 @@ impl InputState {
if !self.light_mode {
if drawing {
if !self.light_mode_supported() {
- self.set_ui_toast(
- UiToastKind::Warning,
- "Light Mode requires layer-shell support",
- );
+ self.set_ui_toast(UiToastKind::Warning, self.light_mode_unsupported_message());
self.needs_redraw = true;
return false;
}
diff --git a/src/input/state/mod.rs b/src/input/state/mod.rs
index bb2210bb..84dcc7c4 100644
--- a/src/input/state/mod.rs
+++ b/src/input/state/mod.rs
@@ -19,13 +19,14 @@ pub use core::{
COLOR_PICKER_PREVIEW_SIZE, COMMAND_PALETTE_MAX_VISIBLE, ColorPickerCursorHint,
ColorPickerPopupLayout, ColorPickerPopupState, CommandPaletteCursorHint,
CompositorCapabilities, ContextMenuCursorHint, ContextMenuEntry, ContextMenuKind,
- ContextMenuState, DrawingState, HelpOverlayCursorHint, InputState, MAX_STROKE_THICKNESS,
- MIN_STROKE_THICKNESS, OutputFocusAction, PRESET_FEEDBACK_DURATION_MS, PRESET_TOAST_DURATION_MS,
- PresetAction, PresetFeedbackKind, PressureThicknessEditMode, PressureThicknessEntryMode,
- RADIAL_COLOR_SEGMENT_COUNT, RADIAL_TOOL_LABELS, RADIAL_TOOL_SEGMENT_COUNT, RadialMenuLayout,
- RadialMenuState, RadialSegmentId, SelectionAxis, SelectionHandle, SelectionState,
- TextInputMode, ToolbarDrawerTab, TourStep, UI_TOAST_DURATION_MS, UiToastKind, ZoomAction,
- color_picker_rgb_to_hsv, radial_color_for_index, sub_ring_child_count, sub_ring_child_label,
+ ContextMenuState, DesktopEnvironment, DrawingState, HelpOverlayCursorHint, InputState,
+ MAX_STROKE_THICKNESS, MIN_STROKE_THICKNESS, OutputFocusAction, PRESET_FEEDBACK_DURATION_MS,
+ PRESET_TOAST_DURATION_MS, PresetAction, PresetFeedbackKind, PressureThicknessEditMode,
+ PressureThicknessEntryMode, RADIAL_COLOR_SEGMENT_COUNT, RADIAL_TOOL_LABELS,
+ RADIAL_TOOL_SEGMENT_COUNT, RadialMenuLayout, RadialMenuState, RadialSegmentId, SelectionAxis,
+ SelectionHandle, SelectionState, ShellMode, TextInputMode, ToolbarDrawerTab, TourStep,
+ UI_TOAST_DURATION_MS, UiToastKind, ZoomAction, color_picker_rgb_to_hsv, radial_color_for_index,
+ sub_ring_child_count, sub_ring_child_label,
};
pub(crate) use core::{
COMMAND_PALETTE_INPUT_HEIGHT, COMMAND_PALETTE_ITEM_HEIGHT, COMMAND_PALETTE_LIST_GAP,