Skip to main content

cranpose_services/
host_surface.rs

1//! The surface the host gives the application, and what the application may
2//! ask of it.
3//!
4//! On the web that is the canvas inside its page; on desktop the window's
5//! client area; on mobile the activity's content view. An application that
6//! wants to lay out against it — or, on a host that allows it, ask for a
7//! different size — reads observable state here instead of reaching for a
8//! platform API and a resize callback of its own.
9
10use std::sync::{
11    Arc, Mutex, OnceLock,
12    atomic::{AtomicU64, Ordering},
13};
14
15use cranpose_core::{State, rememberEventStream};
16
17use crate::registry::ServiceRegistry;
18
19/// The size of the host surface, in logical pixels, with the scale the host
20/// renders it at.
21#[derive(Clone, Copy, Debug, PartialEq)]
22pub struct HostSurfaceSize {
23    /// Logical width.
24    pub width: f32,
25    /// Logical height.
26    pub height: f32,
27    /// Physical pixels per logical pixel.
28    pub scale: f32,
29}
30
31impl Default for HostSurfaceSize {
32    fn default() -> Self {
33        Self {
34            width: 0.0,
35            height: 0.0,
36            scale: 1.0,
37        }
38    }
39}
40
41impl HostSurfaceSize {
42    /// The size in physical pixels.
43    pub fn physical(&self) -> (u32, u32) {
44        (
45            (self.width * self.scale).round().max(0.0) as u32,
46            (self.height * self.scale).round().max(0.0) as u32,
47        )
48    }
49}
50
51/// Why a resize request was refused.
52#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
53pub enum ResizeRefused {
54    /// This host does not let an application choose its surface size — a
55    /// fullscreen mobile activity, a maximised window, a fixed canvas.
56    #[error("this host does not accept surface resize requests")]
57    Unsupported,
58    /// The host accepted the idea but refused these dimensions.
59    #[error("the host refused the requested surface size")]
60    Rejected,
61}
62
63/// What an application may ask of the host's surface.
64///
65/// The surface's *size* is not asked of a backend: every host publishes it as
66/// it lays the surface out, and [`host_surface_size`] answers from that. What a
67/// backend adds is the other direction — whether this host lets the application
68/// choose a size, and how to ask for one.
69pub trait HostSurface: Send + Sync {
70    /// Whether this host accepts resize requests at all.
71    fn can_resize(&self) -> bool {
72        false
73    }
74
75    /// Asks the host for a different surface size.
76    ///
77    /// Hosts are free to clamp or ignore the request, so the answer is only
78    /// "the request was accepted": the size that actually took effect arrives
79    /// through the observable state.
80    fn request_size(&self, width: f32, height: f32) -> Result<(), ResizeRefused> {
81        let _ = (width, height);
82        Err(ResizeRefused::Unsupported)
83    }
84}
85
86/// Shared handle to a [`HostSurface`].
87pub type HostSurfaceRef = Arc<dyn HostSurface>;
88
89struct NoHostSurface;
90
91impl HostSurface for NoHostSurface {}
92
93static PLATFORM_HOST_SURFACE: ServiceRegistry<dyn HostSurface> = ServiceRegistry::new();
94
95/// Installs the platform host surface.
96pub fn set_platform_host_surface(surface: HostSurfaceRef) {
97    PLATFORM_HOST_SURFACE.set(surface);
98}
99
100/// Removes the installed host surface (tests and teardown).
101pub fn clear_platform_host_surface() {
102    PLATFORM_HOST_SURFACE.clear();
103    if let Ok(mut observers) = observers().lock() {
104        observers.clear();
105    }
106    if let Ok(mut last) = last_published().lock() {
107        *last = HostSurfaceSize::default();
108    }
109}
110
111/// Whether this host lets the application choose its surface size.
112pub fn host_surface_can_resize() -> bool {
113    host_surface().can_resize()
114}
115
116/// The installed host surface, or one that accepts no resize requests.
117pub fn host_surface() -> HostSurfaceRef {
118    PLATFORM_HOST_SURFACE
119        .get()
120        .unwrap_or_else(|| Arc::new(NoHostSurface))
121}
122
123/// The size the host last reported.
124///
125/// Read from what the host published rather than asked of a backend: every host
126/// publishes its surface as it lays it out, and a host that has no resize
127/// facility to install a backend for still reports its size. Before the first
128/// frame this is the empty surface at scale one — nothing has been measured
129/// yet — so a caller that needs the real value observes
130/// [`rememberHostSurfaceSize`] rather than sampling once at startup.
131pub fn host_surface_size() -> HostSurfaceSize {
132    last_published()
133        .lock()
134        .map(|size| *size)
135        .unwrap_or_default()
136}
137
138fn last_published() -> &'static Mutex<HostSurfaceSize> {
139    static SLOT: OnceLock<Mutex<HostSurfaceSize>> = OnceLock::new();
140    SLOT.get_or_init(|| Mutex::new(HostSurfaceSize::default()))
141}
142
143/// Asks the host for a different surface size.
144pub fn request_host_surface_size(width: f32, height: f32) -> Result<(), ResizeRefused> {
145    host_surface().request_size(width, height)
146}
147
148type Observer = Arc<dyn Fn(HostSurfaceSize) + Send + Sync>;
149
150fn observers() -> &'static Mutex<Vec<(u64, Observer)>> {
151    static SLOT: OnceLock<Mutex<Vec<(u64, Observer)>>> = OnceLock::new();
152    SLOT.get_or_init(|| Mutex::new(Vec::new()))
153}
154
155static NEXT_OBSERVER: AtomicU64 = AtomicU64::new(1);
156
157/// Keeps a host-surface observer registered until it is dropped.
158pub struct HostSurfaceObserver {
159    id: u64,
160}
161
162impl Drop for HostSurfaceObserver {
163    fn drop(&mut self) {
164        if let Ok(mut observers) = observers().lock() {
165            observers.retain(|(id, _)| *id != self.id);
166        }
167    }
168}
169
170/// Registers `observer` for host-surface size changes. Applications collect
171/// [`rememberHostSurfaceSize`] instead of calling this.
172pub fn observe_host_surface_size(
173    observer: impl Fn(HostSurfaceSize) + Send + Sync + 'static,
174) -> HostSurfaceObserver {
175    let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
176    if let Ok(mut observers) = observers().lock() {
177        observers.push((id, Arc::new(observer)));
178    }
179    HostSurfaceObserver { id }
180}
181
182/// Publishes a new host-surface size. Platform backends call this whenever the
183/// host resizes the surface.
184pub fn publish_host_surface_size(size: HostSurfaceSize) {
185    if let Ok(mut last) = last_published().lock() {
186        if *last == size {
187            return;
188        }
189        *last = size;
190    }
191    let observers = observers()
192        .lock()
193        .map(|observers| {
194            observers
195                .iter()
196                .map(|(_, observer)| Arc::clone(observer))
197                .collect::<Vec<_>>()
198        })
199        .unwrap_or_default();
200    for observer in observers {
201        observer(size);
202    }
203}
204
205/// The host surface's size, observed for as long as this call stays in the
206/// composition.
207#[expect(non_snake_case)]
208#[track_caller]
209pub fn rememberHostSurfaceSize() -> State<HostSurfaceSize> {
210    let updates = rememberEventStream((), |sender| {
211        observe_host_surface_size(move |size| sender.send(size))
212    });
213    cranpose_core::collectAsState(updates, (), host_surface_size())
214}
215
216#[cfg(test)]
217#[path = "tests/host_surface_tests.rs"]
218mod tests;