Stock widgets size themselves with `Length::fluid`, whose auto-clamp `[0.7·px, 1.5·px]` is documented as logical px, yet `Canvas::resolve_geom` resolved it against the physical viewport and applied the bounds to the physical value. On any HiDPI surface whose physical short side exceeds 618 px (412 × 1.5) the cap engaged: a 1080 × 1920 phone at scale 2 got its geometry at 1.5 physical px per design px, that is 0.75 logical, while fonts, resolved by `font_px` against the logical viewport and scaled at raster time, kept growing to 2.62. Rows, paddings and icons came out at 60 % of their intended size with the text overflowing them; at scale 1 physical and logical coincide, so a winit session never showed it. `Length::resolve_physical` takes the buffer scale and multiplies `min_px` / `max_px` by it before clamping; `resolve_with_density` delegates to it with scale 1. `Canvas::resolve_geom` passes its `dpi_scale`, so every stock-widget geometry shares the surface factor of the fonts, and `overlays_reconcile` does the same for `OverlaySpec::size`, which had the same mix. A zero scale falls back to unscaled bounds. The event loop now calls `set_density( sf )` right before `App::on_scale_changed`, both on `configure` and on `scale_factor_changed` of the main surface, so `Length::dp( n )` means n logical px in every app without plumbing; an app wanting a DPI-derived density still overrides it from that callback, and crustace already sets the same value there. Two tests cover the clamp at scale 2 and the geometry/font parity on a 1080 × 2460 canvas.
502 lines
22 KiB
Rust
502 lines
22 KiB
Rust
// SPDX-License-Identifier: LGPL-2.1-only
|
|
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
|
|
|
use smithay_client_toolkit::
|
|
{
|
|
compositor::CompositorState,
|
|
output::OutputState,
|
|
registry::RegistryState,
|
|
seat::SeatState,
|
|
shell::{ wlr_layer::LayerShell, xdg::XdgShell },
|
|
shm::Shm,
|
|
session_lock::SessionLock,
|
|
subcompositor::SubcompositorState,
|
|
};
|
|
use smithay_client_toolkit::reexports::client::
|
|
{
|
|
protocol::
|
|
{
|
|
wl_keyboard::WlKeyboard,
|
|
wl_output::WlOutput,
|
|
wl_pointer::WlPointer,
|
|
wl_surface::WlSurface,
|
|
wl_touch::WlTouch,
|
|
},
|
|
QueueHandle,
|
|
};
|
|
use wayland_protocols::wp::text_input::zv3::client::
|
|
{
|
|
zwp_text_input_manager_v3::ZwpTextInputManagerV3,
|
|
zwp_text_input_v3::ZwpTextInputV3,
|
|
};
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
|
|
use crate::app::{ App, OverlayId, SubsurfaceId };
|
|
use crate::egl_context::EglContext;
|
|
use crate::types::Point;
|
|
|
|
use super::repeat::{ ButtonRepeatState, KeyRepeatState };
|
|
use super::subsurface::SubsurfaceSlot;
|
|
use super::surface::{ SurfaceFocus, SurfaceState };
|
|
use super::tooltip::{ TooltipPending, TooltipVisible };
|
|
|
|
pub struct AppData<A: App>
|
|
{
|
|
pub app: A,
|
|
pub registry_state: RegistryState,
|
|
pub seat_state: SeatState,
|
|
pub output_state: OutputState,
|
|
pub compositor_state: CompositorState,
|
|
/// `wl_subcompositor` binding for [`crate::app::App::subsurfaces`].
|
|
/// `None` when the compositor does not advertise the protocol.
|
|
pub subcompositor: Option<SubcompositorState>,
|
|
pub shm: Shm,
|
|
pub session_lock: Option<SessionLock>,
|
|
/// Process-wide EGL context (display + GLES context). `None` when EGL
|
|
/// failed to initialise or `LTK_FORCE_SOFTWARE=1` — every surface then
|
|
/// falls back to the SHM path.
|
|
pub egl_context: Option<Arc<EglContext>>,
|
|
#[ allow( dead_code ) ]
|
|
pub xdg_shell: Option<XdgShell>,
|
|
/// Shared layer-shell binding used for the main surface and every
|
|
/// overlay. `None` when the compositor does not advertise the protocol.
|
|
pub layer_shell: Option<LayerShell>,
|
|
pub keyboard: Option<WlKeyboard>,
|
|
pub pointer: Option<WlPointer>,
|
|
pub touch: Option<WlTouch>,
|
|
pub pointer_pos: Point,
|
|
/// Process-wide cursor-shape manager (`wp_cursor_shape_v1`).
|
|
/// `None` when the compositor does not advertise the protocol —
|
|
/// the runtime then leaves cursor shape to the compositor's
|
|
/// defaults.
|
|
pub cursor_shape_manager: Option<smithay_client_toolkit::seat::pointer::cursor_shape::CursorShapeManager>,
|
|
/// Per-pointer cursor-shape device, populated when the compositor
|
|
/// supports cursor-shape AND the seat has a pointer capability.
|
|
pub cursor_shape_device: Option<smithay_client_toolkit::reexports::protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::WpCursorShapeDeviceV1>,
|
|
/// Last `Enter` serial seen on a pointer event. Required by
|
|
/// `wp_cursor_shape_device_v1::set_shape` per spec — the
|
|
/// compositor tags every cursor change with the entry serial.
|
|
pub last_pointer_enter_serial: u32,
|
|
/// The cursor shape currently active on the pointer. `None`
|
|
/// means "we have not pushed any shape since the last
|
|
/// `wl_pointer.enter`" — the compositor is showing whatever the
|
|
/// previous client requested, so the next `dispatch_cursor_shape`
|
|
/// must send unconditionally to claim the cursor for our
|
|
/// surface. `Some(s)` lets the dispatch short-circuit when the
|
|
/// target equals what we already sent.
|
|
pub current_cursor_shape: Option<crate::types::CursorShape>,
|
|
pub text_input_manager: Option<ZwpTextInputManagerV3>,
|
|
pub text_input: Option<ZwpTextInputV3>,
|
|
/// Whether the currently focused text field is `secure` (password).
|
|
/// Drives the text-input-v3 content type.
|
|
pub text_input_secure: bool,
|
|
/// `xdg-activation-v1`. Present when the compositor advertises the
|
|
/// global. Used on first configure to honour an incoming
|
|
/// `XDG_ACTIVATION_TOKEN` (a launcher that spawned us wants the
|
|
/// main surface raised to focus) and exposed via
|
|
/// [`App::request_activation_token`] for outbound requests.
|
|
pub activation_state:
|
|
Option<smithay_client_toolkit::activation::ActivationState>,
|
|
/// Honour the activation token exactly once — after the first
|
|
/// successful configure of the main surface. Subsequent configures
|
|
/// (resizes, scale changes) must not re-activate.
|
|
pub activation_token_pending: Option<String>,
|
|
|
|
/// `xdg-session-management-v1` proxies plus the on-disk store; the
|
|
/// store is `None` when persistence is off (layer / lock surfaces,
|
|
/// unusable app_id, concurrent instance, or after `replaced`).
|
|
pub session: super::session::SessionRuntime,
|
|
|
|
/// `wl_data_device_manager` binding. `None` when the compositor
|
|
/// does not advertise the global, in which case copy / paste stays
|
|
/// process-local and inbound selections from other clients are
|
|
/// invisible.
|
|
pub data_device_manager:
|
|
Option<smithay_client_toolkit::data_device_manager::DataDeviceManagerState>,
|
|
/// Per-seat `wl_data_device` handle, created the first time a seat
|
|
/// with a keyboard or pointer capability appears. Required for
|
|
/// `set_selection` and to receive inbound `selection` events.
|
|
pub data_device:
|
|
Option<smithay_client_toolkit::data_device_manager::data_device::DataDevice>,
|
|
/// Currently-published outbound selection source. Held alive
|
|
/// across [`DataSourceHandler::send_request`] so the cached
|
|
/// clipboard text can be re-served on each paste from the peer.
|
|
pub clipboard_source:
|
|
Option<smithay_client_toolkit::data_device_manager::data_source::CopyPasteSource>,
|
|
/// Sender half of the cross-thread channel used to ferry inbound
|
|
/// selection bytes (a worker thread drains the read pipe).
|
|
pub clipboard_inbox_tx: std::sync::mpsc::Sender<String>,
|
|
/// Receiver half — drained once per run loop iteration.
|
|
pub clipboard_inbox_rx: std::sync::mpsc::Receiver<String>,
|
|
|
|
/// Cross-application drag-and-drop: best mime negotiated on
|
|
/// `enter` and the last pointer position (in logical surface
|
|
/// coords). Cleared on `leave` and after `drop_performed`.
|
|
pub drop_position: Option<( f64, f64 )>,
|
|
pub drop_mime: Option<String>,
|
|
pub drop_inbox_tx: std::sync::mpsc::Sender<super::data_device::DropPayload>,
|
|
pub drop_inbox_rx: std::sync::mpsc::Receiver<super::data_device::DropPayload>,
|
|
|
|
/// AccessKit / AT-SPI2 adapter. `None` when the platform adapter
|
|
/// could not be created (no AT-SPI2 daemon on the session bus,
|
|
/// missing system libraries at runtime, headless CI). The
|
|
/// runtime carries on without an accessibility tree in that
|
|
/// case; nothing else in the pipeline reads this field except
|
|
/// the per-frame tree update and the per-iteration action
|
|
/// inbox drain.
|
|
pub a11y: Option<crate::a11y::A11yState>,
|
|
/// Client-side handle to `ext-foreign-toplevel-list-v1`. Holds the
|
|
/// global proxy plus the live list of open toplevels; SCTK fans
|
|
/// events through this object into our `ForeignToplevelListHandler`
|
|
/// impl in `handlers.rs`, which then routes them to
|
|
/// `App::on_toplevel_event`. Present on every session, even when
|
|
/// the compositor does not advertise the global — internally the
|
|
/// list holds a `GlobalProxy` that simply yields no toplevels in
|
|
/// that case.
|
|
pub foreign_toplevel_list:
|
|
smithay_client_toolkit::foreign_toplevel_list::ForeignToplevelList,
|
|
pub shift_pressed: bool,
|
|
pub ctrl_pressed: bool,
|
|
/// Calloop handle for inserting timers / channels. Used by the
|
|
/// key-repeat machinery and any future feature that needs to
|
|
/// schedule work on the run loop without going through the message
|
|
/// queue.
|
|
pub loop_handle: calloop::LoopHandle<'static, Self>,
|
|
/// Repeat rate (events per second) advertised by the compositor in
|
|
/// `wl_keyboard.repeat_info`. `0` means "the compositor disabled
|
|
/// repeat" — we honour that and never start a repeat timer.
|
|
pub compositor_repeat_rate: u32,
|
|
/// Initial delay (ms) before the first repeat fires, advertised by
|
|
/// the compositor. `0` means the compositor disabled repeat.
|
|
pub compositor_repeat_delay: u32,
|
|
/// Currently-active key repeat. `Some` between the press of a
|
|
/// repeating key and either its release, the keyboard losing focus
|
|
/// or another key being pressed.
|
|
pub key_repeat: Option<KeyRepeatState>,
|
|
/// Currently-active button-press repeat. `Some` between the
|
|
/// press of a repeating-button and either its release, the
|
|
/// pointer leaving the surface, a gesture cancel, focus loss or
|
|
/// long-press promotion.
|
|
pub button_repeat: Option<ButtonRepeatState>,
|
|
/// Clipboard buffer. Copy / Cut store the selected text here;
|
|
/// Paste retrieves from here. Synchronised with the Wayland
|
|
/// selection through `wl_data_device_manager` when the compositor
|
|
/// advertises the global: outbound copies publish a
|
|
/// `CopyPasteSource`, and inbound selections from other clients
|
|
/// land here through a worker thread (see
|
|
/// [`super::data_device`]).
|
|
pub clipboard: String,
|
|
/// Timestamp of the previous press (pointer or touch). Combined
|
|
/// with [`Self::last_press_pos`] it lets the press handler
|
|
/// detect a double-click on a TextEdit and turn it into a
|
|
/// word-select.
|
|
pub last_press_time: Option<std::time::Instant>,
|
|
/// Position of the previous press. See [`Self::last_press_time`].
|
|
pub last_press_pos: Option<Point>,
|
|
pub debug_layout: bool,
|
|
pub pending_msgs: Vec<A::Message>,
|
|
/// Initial cursor positions queued when a long-press fires, to be flushed
|
|
/// to [`App::on_drag_move`] right after the paired long-press message is
|
|
/// processed. This gives the drag ghost a valid starting position even
|
|
/// before the user's finger / cursor moves — useful on mouse where the
|
|
/// pointer might sit perfectly still between press and drag.
|
|
pub pending_drag_inits: Vec<Point>,
|
|
pub tooltip_pending: Option<TooltipPending>,
|
|
pub tooltip_visible: Option<TooltipVisible>,
|
|
pub qh: QueueHandle<Self>,
|
|
/// Last pointer serial (needed for interactive move).
|
|
pub last_pointer_serial: u32,
|
|
/// Last serial from any input event. Required by `xdg_popup.grab`,
|
|
/// which only honours serials from recent input events on the same
|
|
/// seat.
|
|
pub last_input_serial: u32,
|
|
/// Set to true when the app has accepted a close request.
|
|
pub exit_requested: bool,
|
|
/// First-configure latch for `App::start_fullscreen`.
|
|
pub pending_fullscreen: bool,
|
|
/// First-configure latch: clears `max_size` after the compositor
|
|
/// has honoured the pinned initial size.
|
|
pub pending_size_hint_unpin: bool,
|
|
/// Main application surface.
|
|
pub main: SurfaceState<A::Message>,
|
|
/// Auxiliary layer-shell surfaces keyed by their stable [`OverlayId`].
|
|
/// Populated and reconciled by the run loop from [`App::overlays`]. Always
|
|
/// empty until overlay diffing is wired up.
|
|
pub overlays: HashMap<OverlayId, SurfaceState<A::Message>>,
|
|
/// Surfaces covering every output beyond the first, paired with the
|
|
/// output each one belongs to: session-lock surfaces while locked (the
|
|
/// compositor blanks any output without one), background layer surfaces
|
|
/// for a [`crate::Layer::Background`] shell (the wallpaper reaches
|
|
/// extended outputs). These render [`App::secondary_view`] and take no
|
|
/// input (the input handlers find no focus for them, and keyboard falls
|
|
/// through to `Main`).
|
|
pub secondaries: Vec<( WlOutput, SurfaceState<A::Message> )>,
|
|
/// Output the main surface was created on, so a later `new_output` for
|
|
/// the same output does not add a duplicate secondary surface.
|
|
pub main_output: Option<WlOutput>,
|
|
/// Input-transparent child surfaces keyed by their stable
|
|
/// [`SubsurfaceId`]. Reconciled each frame from [`App::subsurfaces`].
|
|
pub subsurfaces: HashMap<SubsurfaceId, SubsurfaceSlot>,
|
|
/// Shared GLES canvas reused across every subsurface raster. Held here
|
|
/// (not per-slot) so a subsurface that is dropped and re-created — e.g. a
|
|
/// lazily-emitted sliding panel — does not recompile the ~14 shader
|
|
/// programs (hundreds of ms on a mobile GPU) every time it reappears.
|
|
pub subsurface_gles_canvas: Option<crate::render::Canvas>,
|
|
/// Surface currently receiving pointer events (updated on each pointer
|
|
/// event via its `surface` field).
|
|
pub pointer_focus: SurfaceFocus,
|
|
/// Surface currently holding keyboard focus (updated on keyboard
|
|
/// enter/leave).
|
|
pub keyboard_focus: SurfaceFocus,
|
|
/// Per-touch-id tracking of which surface each active touch belongs to
|
|
/// (populated on `down`, cleared on `up`/`cancel`).
|
|
pub touch_focus: HashMap<i32, SurfaceFocus>,
|
|
/// Cached widget tree returned by [`App::view`]. Populated lazily by the
|
|
/// run loop just before drawing when `view_dirty` is set, then re-used by
|
|
/// every subsequent partial / interaction-only redraw until invalidated.
|
|
/// Dropping the rebuild on idle frames is a big win for shells that sit
|
|
/// idle most of the time.
|
|
pub cached_view: Option<crate::widget::Element<A::Message>>,
|
|
/// Cached overlay-spec list returned by [`App::overlays`]. Same lifecycle
|
|
/// as `cached_view` but driven by `overlays_dirty`.
|
|
pub cached_overlays: Option<Vec<crate::app::OverlaySpec<A::Message>>>,
|
|
/// `true` when `cached_view` is stale and must be rebuilt before the next
|
|
/// draw. Set on startup, by every `InvalidationScope` touching `Main`, and
|
|
/// every frame `App::is_animating` returns `true`.
|
|
pub view_dirty: bool,
|
|
/// Counterpart of `view_dirty` for `cached_overlays`.
|
|
pub overlays_dirty: bool,
|
|
/// Set after the first commit of a rendered buffer on the main surface.
|
|
/// Used to drive `App::on_first_frame_committed` exactly once.
|
|
pub first_frame_committed: bool,
|
|
/// Focus request deferred because the target widget was absent from
|
|
/// `widget_rects` (e.g. read_only, not yet rebuilt). Retried next draw.
|
|
pub focus_retry: Option<crate::types::WidgetId>,
|
|
/// Runtime performance guardrails (stuck-animation / poll-interval
|
|
/// diagnostics and the software animation-rate cap). See [`super::perf`].
|
|
pub perf: super::perf::PerfState,
|
|
}
|
|
|
|
impl<A: App> AppData<A>
|
|
{
|
|
/// Find which [`SurfaceFocus`] owns the given `WlSurface`, or `None` if it
|
|
/// does not correspond to any tracked surface. Safe even when the main
|
|
/// surface is still `Pending` (returns `None`).
|
|
pub( crate ) fn focus_for_surface(
|
|
&self,
|
|
wl: &WlSurface,
|
|
) -> Option<SurfaceFocus>
|
|
{
|
|
if self.main.surface.try_wl_surface() == Some( wl )
|
|
{
|
|
return Some( SurfaceFocus::Main );
|
|
}
|
|
for ( id, ss ) in self.overlays.iter()
|
|
{
|
|
if ss.surface.try_wl_surface() == Some( wl )
|
|
{
|
|
return Some( SurfaceFocus::Overlay( *id ) );
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Whether `wl` is one of the secondary per-output surfaces. Pointer and
|
|
/// touch events landing on them must be dropped rather than falling back
|
|
/// to `Main` — their coordinates would hit-test the main surface's
|
|
/// widgets. Keyboard keeps the `Main` fallback so typing works wherever
|
|
/// the compositor puts the focus.
|
|
pub( crate ) fn is_secondary_surface( &self, wl: &WlSurface ) -> bool
|
|
{
|
|
self.secondaries.iter().any( |( _, ss )| ss.surface.try_wl_surface() == Some( wl ) )
|
|
}
|
|
|
|
/// Borrow the [`SurfaceState`] identified by `focus`. Panics if `focus`
|
|
/// refers to an overlay that is not currently registered — callers must
|
|
/// only pass focus values obtained from [`focus_for_surface`] or from the
|
|
/// per-device focus fields, which the run loop keeps in sync.
|
|
#[ allow( dead_code ) ]
|
|
pub( crate ) fn surface( &self, focus: SurfaceFocus ) -> &SurfaceState<A::Message>
|
|
{
|
|
match focus
|
|
{
|
|
SurfaceFocus::Main => &self.main,
|
|
SurfaceFocus::Overlay( id ) => self.overlays.get( &id )
|
|
.expect( "surface(): overlay not registered" ),
|
|
}
|
|
}
|
|
|
|
/// Non-panicking variant of [`surface`]. Returns `None` when `focus`
|
|
/// refers to an overlay that has already been removed — callers on
|
|
/// async dispatch paths (IME Done, tooltip arm) must use this.
|
|
pub( crate ) fn try_surface( &self, focus: SurfaceFocus ) -> Option<&SurfaceState<A::Message>>
|
|
{
|
|
match focus
|
|
{
|
|
SurfaceFocus::Main => Some( &self.main ),
|
|
SurfaceFocus::Overlay( id ) => self.overlays.get( &id ),
|
|
}
|
|
}
|
|
|
|
/// Mutable counterpart of [`surface`].
|
|
#[ allow( dead_code ) ]
|
|
pub( crate ) fn surface_mut( &mut self, focus: SurfaceFocus ) -> &mut SurfaceState<A::Message>
|
|
{
|
|
match focus
|
|
{
|
|
SurfaceFocus::Main => &mut self.main,
|
|
SurfaceFocus::Overlay( id ) => self.overlays.get_mut( &id )
|
|
.expect( "surface_mut(): overlay not registered" ),
|
|
}
|
|
}
|
|
|
|
/// Non-panicking variant of [`surface_mut`].
|
|
pub( crate ) fn try_surface_mut( &mut self, focus: SurfaceFocus ) -> Option<&mut SurfaceState<A::Message>>
|
|
{
|
|
match focus
|
|
{
|
|
SurfaceFocus::Main => Some( &mut self.main ),
|
|
SurfaceFocus::Overlay( id ) => self.overlays.get_mut( &id ),
|
|
}
|
|
}
|
|
|
|
/// Synchronous overlay teardown: removes the overlay from the map
|
|
/// and rewrites every per-device focus that pointed at it so the
|
|
/// next event in the same dispatch can no longer land on a freed
|
|
/// surface. Used by the compositor-driven destruction paths
|
|
/// (`PopupHandler::done`, `LayerShellHandler::closed`) where
|
|
/// waiting for the next `reconcile_overlays` would leave a window
|
|
/// in which `surface()` / `surface_mut()` panic. Migrates an
|
|
/// in-flight long-press drag to the main surface for the same
|
|
/// reason `reconcile_overlays` does.
|
|
pub( crate ) fn discard_overlay( &mut self, id: crate::app::OverlayId )
|
|
{
|
|
if let Some( ss ) = self.overlays.remove( &id )
|
|
{
|
|
if ss.gesture.long_press_fired
|
|
{
|
|
self.main.gesture.long_press_fired = true;
|
|
self.main.gesture.long_press_origin = ss.gesture.long_press_origin;
|
|
// Keep the primary touch slot with the migrated drag so motion / release still route through the gesture machine.
|
|
if self.main.primary_touch_id.is_none()
|
|
{
|
|
self.main.primary_touch_id = ss.primary_touch_id;
|
|
}
|
|
}
|
|
}
|
|
if let SurfaceFocus::Overlay( fid ) = self.pointer_focus
|
|
{
|
|
if fid == id { self.pointer_focus = SurfaceFocus::Main; }
|
|
}
|
|
if let SurfaceFocus::Overlay( fid ) = self.keyboard_focus
|
|
{
|
|
if fid == id { self.keyboard_focus = SurfaceFocus::Main; }
|
|
}
|
|
for f in self.touch_focus.values_mut()
|
|
{
|
|
if let SurfaceFocus::Overlay( fid ) = *f
|
|
{
|
|
if fid == id { *f = SurfaceFocus::Main; }
|
|
}
|
|
}
|
|
if let Some( pending ) = self.tooltip_pending.as_ref()
|
|
{
|
|
if pending.focus == SurfaceFocus::Overlay( id ) { self.tooltip_pending = None; }
|
|
}
|
|
if let Some( visible ) = self.tooltip_visible.as_ref()
|
|
{
|
|
if visible.focus == SurfaceFocus::Overlay( id )
|
|
{
|
|
self.tooltip_visible = None;
|
|
self.overlays_dirty = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Configure the main surface size, (re)allocate its rendering target, and
|
|
// request a redraw. Routes to the GPU or SHM path inside `SurfaceState`
|
|
// according to whether `self.egl_context` is available.
|
|
pub( crate ) fn on_configure( &mut self, w: u32, h: u32 )
|
|
{
|
|
self.main.on_configure( &self.shm, self.egl_context.as_ref(), w, h );
|
|
if let Some( ref mut a ) = self.a11y
|
|
{
|
|
let sf = self.main.scale_factor.max( 1 ) as f64;
|
|
a.set_window_bounds( ( w as f64 ) * sf, ( h as f64 ) * sf );
|
|
a.set_window_focus( true );
|
|
}
|
|
// First-configure latch: honour an incoming
|
|
// `XDG_ACTIVATION_TOKEN` exactly once, after the surface has
|
|
// been mapped (`xdg_activation_v1.activate` is only meaningful
|
|
// against a configured surface). Compositors that don't
|
|
// advertise the global leave `activation_state` as `None` and
|
|
// the call drops the token silently.
|
|
if let Some( token ) = self.activation_token_pending.take()
|
|
{
|
|
if let ( Some( ref activation ), Some( wl ) ) =
|
|
( self.activation_state.as_ref(), self.main.surface.try_wl_surface() )
|
|
{
|
|
activation.activate::<Self>( &wl, token );
|
|
}
|
|
}
|
|
// `on_resize` is documented to deliver **physical** pixels, matching the
|
|
// coordinate space that the layout passes and the pointer/touch
|
|
// callbacks (`on_drag_move`, `on_drop`) work in. Wayland's
|
|
// configure.new_size is surface-local (logical), so multiply by the
|
|
// current buffer scale before handing the dimensions to the app.
|
|
let sf = self.main.scale_factor.max( 1 ) as u32;
|
|
crate::types::set_viewport_size( w * sf, h * sf );
|
|
crate::types::set_density( sf as f32 );
|
|
self.app.on_scale_changed( sf );
|
|
self.app.on_resize( w * sf, h * sf );
|
|
// `on_resize` may flip app-state that the view depends on (apps that
|
|
// branch on the new dimensions, layout caches keyed by size, …), so
|
|
// drop the cached tree to force a rebuild on the next draw.
|
|
self.dirty_caches();
|
|
}
|
|
|
|
/// Mark both view caches stale after a direct app-state mutation that
|
|
/// doesn't go through [`App::update`] — swipe progress callbacks, text-
|
|
/// input focus changes, configure-driven resize. Per-message updates use
|
|
/// the run loop's `apply_invalidation` path instead so that
|
|
/// [`App::invalidate_after`] can scope the rebuild.
|
|
pub( crate ) fn dirty_caches( &mut self )
|
|
{
|
|
self.view_dirty = true;
|
|
self.overlays_dirty = true;
|
|
}
|
|
|
|
/// Smallest remaining time until a pending long-press deadline fires.
|
|
///
|
|
/// Used by the run loop to bound `event_loop.dispatch()` so that a
|
|
/// stationary press wakes us up at the right moment even when no
|
|
/// Wayland events arrive. `Some(Duration::ZERO)` means the deadline
|
|
/// has already elapsed; `None` means nothing is pending.
|
|
pub( crate ) fn next_long_press_wakeup( &self ) -> Option<std::time::Duration>
|
|
{
|
|
let dur = self.app.long_press_duration();
|
|
let now = std::time::Instant::now();
|
|
let mut soonest: Option<std::time::Duration> = None;
|
|
let mut consider = |start: Option<std::time::Instant>|
|
|
{
|
|
if let Some( s ) = start
|
|
{
|
|
let deadline = s + dur;
|
|
let remaining = deadline.saturating_duration_since( now );
|
|
soonest = Some( match soonest
|
|
{
|
|
Some( cur ) => cur.min( remaining ),
|
|
None => remaining,
|
|
} );
|
|
}
|
|
};
|
|
consider( self.main.gesture.long_press_start );
|
|
for ss in self.overlays.values()
|
|
{
|
|
consider( ss.gesture.long_press_start );
|
|
}
|
|
soonest
|
|
}
|
|
}
|