Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions docs/codebase-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Daemon mode therefore provides a persistent background service that reacts to us
- Communicate with `capture::CaptureManager` for screenshot actions.
- Exit when `InputState.should_exit` is set (Escape, tray close, etc.).

`WaylandState` coordinates the runtime owners handlers need. `ProtocolGlobals` owns bound globals and toolkit handler state; `PointerRuntime` owns pointer, cursor, pointer-lock, and single-contact touch protocol lifecycles; `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 input and toolbar routing.
`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.

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.

Expand All @@ -93,8 +93,9 @@ Freeze capture waits for the overlay-suppression frame, then selects `wlr-screen
- Key presses can emit typed backend work; the event loop drains the ordered
`InputEffectOutbox` runtime batch through `InputState::drain_input_effects`.

2. **Mouse events (`handlers/pointer.rs`)**
- Update `current_mouse_x/y`.
2. **Mouse events (`handlers/pointer/`)**
- Classify canvas, layer-shell toolbar, and foreign surfaces once through `SurfaceRouter`; toolbar-local positions are converted to overlay screen coordinates.
- Update the pointer position.
- Call `InputState::on_mouse_press`, `on_mouse_motion`, `on_mouse_release`.
- Adjust pen thickness or font size via scroll wheel + modifiers; scrolling over a Spotlight loupe adjusts its magnification instead.
- Keep touchpad-finger Spotlight adjustments in one undo gesture until Wayland reports `axis_stop`; wheel-like sources use a quiet-period fallback when no stop arrives.
Expand Down Expand Up @@ -178,8 +179,8 @@ The result is a predictable pipeline: Wayland → handlers → `InputState` →
selection; `capture_region_interactive` enters review so Copy, Save, Both,
or Board can choose the terminal request. Interactive review can apply
sequential band cuts through `capture::band_cut` after flattening a
snapshot. Live cut previews run on `WaylandState.region_cut_preview`, a
capacity-one `RuntimeOperationController` independent of `CaptureManager`,
snapshot. Live cut previews run on the capacity-one controller owned by
`RegionCaptureRuntime`, independent of `CaptureManager`,
so a replaceable preview cannot occupy the capture reservation slot.
Flatten, cut, and PNG encode stay off the event loop; terminal Copy, Save,
Both, or Board still submit through `CaptureManager`.
Expand Down
32 changes: 21 additions & 11 deletions src/backend/wayland/backend/event_loop/capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ pub(super) fn capture_timeout(state: &WaylandState, now: Instant) -> Option<Dura
state.frozen.portal_timeout(now),
super::min_timeout(
state.zoom.portal_timeout(now),
state.xdg_frozen_fullscreen_timeout(now),
state.surface.placement().xdg_frozen().timeout(now),
),
),
),
Expand All @@ -62,15 +62,23 @@ fn handle_pending_frozen_image(state: &mut WaylandState, now: Instant) {
return;
}
if state.surface.is_xdg_window() {
if state.xdg_fullscreen() {
if state.surface.placement().xdg_fullscreen() {
state.activate_pending_frozen_image_for_current_surface();
return;
}
if !state.xdg_frozen_fullscreen_requested() && state.begin_xdg_frozen_fullscreen() {
if !state.surface.placement().xdg_frozen().requested()
&& state.begin_xdg_frozen_fullscreen()
{
return;
}
if state.xdg_frozen_fullscreen_pending_configure() {
if state.xdg_frozen_fullscreen_timed_out(now) {
if state.surface.placement().xdg_frozen().pending_configure() {
if state
.surface
.placement()
.xdg_frozen()
.timeout(now)
.is_some_and(|timeout| timeout.is_zero())
{
warn!("Frozen xdg fullscreen configure timed out; cancelling freeze");
state.restore_xdg_after_frozen();
if state.frozen.has_acquisition_attempt() {
Expand Down Expand Up @@ -254,9 +262,9 @@ fn frozen_toggle_pass_decision(
fn handle_frozen_toggle(state: &mut WaylandState, user_requested: bool) {
let decision = frozen_toggle_pass_decision(
user_requested,
state.screen_acquisition_slot(),
state.acquisition.slot(),
state.input_state.frozen_active(),
state.frozen_enabled(),
state.frozen.enabled(),
);
match decision.user_action {
FrozenUserToggleAction::None => {}
Expand Down Expand Up @@ -288,12 +296,14 @@ fn handle_frozen_toggle(state: &mut WaylandState, user_requested: bool) {
);
}
FrozenUserToggleAction::RequestUserFreeze => {
let _ = state.request_screen_acquisition(ScreenAcquisitionOwner::UserFreeze);
let _ = state
.acquisition
.request(ScreenAcquisitionOwner::UserFreeze);
}
}

let record = if decision.user_action == FrozenUserToggleAction::RequestUserFreeze {
state.queued_screen_acquisition()
state.acquisition.queued()
} else {
decision.queued_to_start
};
Expand All @@ -311,7 +321,7 @@ fn handle_frozen_toggle(state: &mut WaylandState, user_requested: bool) {
}
match state.frozen.start_capture_for(record.id, record.owner) {
Ok(()) => {
state.mark_screen_acquisition_started(record.id, record.owner);
state.acquisition.mark_started(record.id, record.owner);
}
Err(err) => {
warn!("Frozen capture failed to start: {err}");
Expand Down Expand Up @@ -615,7 +625,7 @@ fn handle_capture_results(state: &mut WaylandState) {
// Exit-after-capture is intentional teardown. Mark it explicit so XDG
// stay-mode cannot clear should_exit while the overlay is unfocused
// (for example after a portal dialog stole focus during capture).
state.mark_xdg_explicit_close_requested();
state.focus.mark_xdg_explicit_close_requested();
state.input_state.should_exit = true;
}
}
Expand Down
20 changes: 11 additions & 9 deletions src/backend/wayland/backend/event_loop/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ pub(super) fn run_event_loop(
let capture_active = state.capture.is_in_progress()
|| state.frozen.is_in_progress()
|| state.zoom.is_in_progress()
|| state.overlay_blocks_event_loop();
|| state.suppression.blocks_event_loop();
let timeout = event_loop_timeout(state, capture_active, last_render_time);
if let Err(e) =
dispatch::dispatch_events(event_queue, state, runtime_wake, signal_state, timeout)
Expand Down Expand Up @@ -273,7 +273,7 @@ fn event_loop_timeout(
state.input_state.ocr_scan_wake_after(now),
);
let autosave_timeout = session_save::autosave_timeout(state, now);
let focus_exit_timeout = state.focus_exit_timeout(now);
let focus_exit_timeout = state.focus.exit_timeout(now);
let base_timeout = if should_block {
min_timeout(autosave_timeout, focus_exit_timeout)
} else if !vsync_enabled && state.input_state.needs_redraw {
Expand Down Expand Up @@ -318,15 +318,15 @@ fn event_loop_timeout(
fn handle_xdg_focus_loss(state: &mut WaylandState, qh: &wayland_client::QueueHandle<WaylandState>) {
if !state.surface.is_xdg_window()
|| state.input_state.should_exit
|| state.has_keyboard_focus()
|| state.focus.keyboard_focused()
|| state.desktop_open_in_progress()
|| !state.focus_exit_suppression_expired(Instant::now())
|| !state.focus.exit_suppression_expired(Instant::now())
{
return;
}
if state.xdg_focus_loss_exits_overlay() {
warn!("Keyboard focus not restored after clipboard action; exiting overlay");
state.clear_focus_exit_suppression();
state.focus.clear_exit_suppression();
notification::send_notification_async(
&state.tokio_handle,
"Wayscriber lost focus".to_string(),
Expand All @@ -338,8 +338,10 @@ fn handle_xdg_focus_loss(state: &mut WaylandState, qh: &wayland_client::QueueHan
warn!(
"Keyboard focus not restored after clipboard action; keeping overlay open (ui.xdg_focus_loss_behavior=stay)"
);
state.clear_focus_exit_suppression();
state.set_xdg_close_guard_for(Duration::from_millis(2500));
state.focus.clear_exit_suppression();
state
.focus
.guard_xdg_close_for(Instant::now(), Duration::from_millis(2500));
state.request_xdg_activation(qh);
}
}
Expand Down Expand Up @@ -395,12 +397,12 @@ fn break_on_requested_exit(state: &mut WaylandState) -> bool {
if !state.input_state.should_exit {
return false;
}
let explicit_xdg_close_requested = state.take_xdg_explicit_close_requested()
let explicit_xdg_close_requested = state.focus.take_xdg_explicit_close_requested()
|| state.input_state.take_explicit_exit_requested();
if should_defer_xdg_unfocused_exit(
state.surface.is_xdg_window(),
!state.xdg_focus_loss_exits_overlay(),
state.has_keyboard_focus(),
state.focus.keyboard_focused(),
explicit_xdg_close_requested,
) {
warn!("Exit requested while unfocused in xdg stay mode; keeping overlay open");
Expand Down
4 changes: 2 additions & 2 deletions src/backend/wayland/backend/event_loop/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,14 @@ pub(super) fn maybe_render(
state.input_state.status_hud.hover(),
state.input_state.zoom_chip.hover(),
);
if chrome_hover_before != chrome_hover_after && state.has_pointer_focus() {
if chrome_hover_before != chrome_hover_after && state.focus.pointer_focused() {
// Layout can move under a stationary pointer (for example,
// Fit removes the zoom-chip Lock button). The render pass
// reclassifies hover; publish the matching Wayland cursor
// now instead of waiting for another motion event. Pointer
// focus is required so a leave-triggered redraw cannot
// publish or cache a cursor for stale coordinates.
state.update_pointer_cursor(state.pointer_over_toolbar(), conn);
state.update_pointer_cursor(state.toolbar_chrome.pointer_over_toolbar(), conn);
}
state.record_perf_render_complete(
render_start,
Expand Down
6 changes: 3 additions & 3 deletions src/backend/wayland/backend/event_loop/session_save.rs
Original file line number Diff line number Diff line change
Expand Up @@ -536,9 +536,9 @@ fn record_persistence_transport_failure(
pub(in crate::backend::wayland) fn should_defer_for_interaction(state: &WaylandState) -> bool {
persistence_interaction_active(
input_persistence_interaction_active(&state.input_state),
state.toolbar_dragging(),
state.is_move_dragging(),
state.board_panning_active(),
state.toolbar_drag.item_dragging(),
state.toolbar_drag.is_moving(),
state.pointer.board_pan_active(),
state.zoom_panning_active(),
stylus_tip_down(state),
)
Expand Down
2 changes: 1 addition & 1 deletion src/backend/wayland/backend/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ where
}

fn take_toolbar_drag_flush_requested(&mut self) -> bool {
self.state.take_toolbar_drag_flush_requested()
self.state.toolbar_drag.take_flush_requested()
}

fn flush(&mut self) -> Result<()> {
Expand Down
12 changes: 11 additions & 1 deletion src/backend/wayland/backend/state_init/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ use super::WaylandBackend;
use super::runtime_wake::RuntimeWakeSource;
use super::setup::WaylandSetup;
use crate::backend::wayland::portal_capture::screenshot_portal_available;
use crate::env_vars::{DESKTOP_SESSION_ENV, XDG_CURRENT_DESKTOP_ENV, XDG_SESSION_DESKTOP_ENV};
use crate::env_vars::{
DESKTOP_SESSION_ENV, XDG_ACTIVATION_TOKEN_ENV, XDG_CURRENT_DESKTOP_ENV, XDG_SESSION_DESKTOP_ENV,
};
use crate::{
capture::CaptureManager,
config::Config,
Expand Down Expand Up @@ -103,6 +105,13 @@ pub(super) fn init_state(backend: &WaylandBackend, setup: WaylandSetup) -> Resul
let direct_capture_supported = screencopy_supported || image_copy_capture_supported;
let frozen_supported = direct_capture_supported || portal_freeze_supported;
let tokio_handle = backend.tokio_runtime.handle().clone();
let startup_activation_token = env::var(XDG_ACTIVATION_TOKEN_ENV)
.ok()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty());
if startup_activation_token.is_some() {
info!("Received startup activation token from launcher environment");
}

// Set compositor capabilities based on detected Wayland protocols
input_state.compositor_capabilities = CompositorCapabilities {
Expand Down Expand Up @@ -174,6 +183,7 @@ pub(super) fn init_state(backend: &WaylandBackend, setup: WaylandSetup) -> Resul
globals: setup.state_globals,
config,
input_state,
startup_activation_token,
onboarding,
palette_recents,
capture_manager,
Expand Down
10 changes: 6 additions & 4 deletions src/backend/wayland/backend/surface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ pub(super) fn create_overlay_surface(
// Create surface using layer-shell when available, otherwise fall back to xdg-shell
let wl_surface = state.protocol.compositor().create_surface(qh);
if state.protocol.layer_shell().is_some() {
state.begin_main_layer_focus_acquisition();
state.focus.begin_main_layer_acquisition();
}
if let Some(layer_shell) = state.protocol.layer_shell() {
let layer = state.main_surface_layer();
let layer = state.surface.placement().layer();
info!("Creating layer shell surface in {:?} layer", layer);
let layer_surface = layer_shell.create_layer_surface(
qh,
Expand All @@ -39,15 +39,17 @@ pub(super) fn create_overlay_surface(
layer_surface.commit();

state.surface.set_layer_surface(layer_surface);
state.set_current_keyboard_interactivity(Some(desired_keyboard_mode));
state
.focus
.set_keyboard_interactivity(Some(desired_keyboard_mode));
info!("Layer shell surface created");
} else if let Some(xdg_shell) = state.protocol.xdg_shell() {
info!("Layer shell missing; creating xdg-shell window");
let window = xdg_shell.create_window(wl_surface, WindowDecorations::None, qh);
window.set_title("wayscriber overlay");
let app_id = runtime_app_id();
window.set_app_id(&app_id);
if state.xdg_fullscreen() {
if state.surface.placement().xdg_fullscreen() {
if let Some(output) = state.preferred_fullscreen_output() {
info!("Requesting fullscreen on preferred output");
window.set_fullscreen(Some(&output));
Expand Down
Loading
Loading