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
91/// A host that owns its own window: a fullscreen activity, a maximised window,
92/// a canvas the page sizes.
93impl HostSurface for NoHostSurface {}
94
95static PLATFORM_HOST_SURFACE: ServiceRegistry<dyn HostSurface> = ServiceRegistry::new();
96
97/// Installs the platform host surface.
98pub fn set_platform_host_surface(surface: HostSurfaceRef) {
99    PLATFORM_HOST_SURFACE.set(surface);
100}
101
102/// Removes the installed host surface (tests and teardown).
103pub fn clear_platform_host_surface() {
104    PLATFORM_HOST_SURFACE.clear();
105    if let Ok(mut observers) = observers().lock() {
106        observers.clear();
107    }
108    if let Ok(mut last) = last_published().lock() {
109        *last = HostSurfaceSize::default();
110    }
111}
112
113/// Whether this host lets the application choose its surface size.
114pub fn host_surface_can_resize() -> bool {
115    host_surface().can_resize()
116}
117
118/// The installed host surface, or one that accepts no resize requests.
119pub fn host_surface() -> HostSurfaceRef {
120    PLATFORM_HOST_SURFACE
121        .get()
122        .unwrap_or_else(|| Arc::new(NoHostSurface))
123}
124
125/// The size the host last reported.
126///
127/// Read from what the host published rather than asked of a backend: every host
128/// publishes its surface as it lays it out, and a host that has no resize
129/// facility to install a backend for still reports its size. Before the first
130/// frame this is the empty surface at scale one — nothing has been measured
131/// yet — so a caller that needs the real value observes
132/// [`rememberHostSurfaceSize`] rather than sampling once at startup.
133pub fn host_surface_size() -> HostSurfaceSize {
134    last_published()
135        .lock()
136        .map(|size| *size)
137        .unwrap_or_default()
138}
139
140fn last_published() -> &'static Mutex<HostSurfaceSize> {
141    static SLOT: OnceLock<Mutex<HostSurfaceSize>> = OnceLock::new();
142    SLOT.get_or_init(|| Mutex::new(HostSurfaceSize::default()))
143}
144
145/// Asks the host for a different surface size.
146pub fn request_host_surface_size(width: f32, height: f32) -> Result<(), ResizeRefused> {
147    host_surface().request_size(width, height)
148}
149
150type Observer = Arc<dyn Fn(HostSurfaceSize) + Send + Sync>;
151
152fn observers() -> &'static Mutex<Vec<(u64, Observer)>> {
153    static SLOT: OnceLock<Mutex<Vec<(u64, Observer)>>> = OnceLock::new();
154    SLOT.get_or_init(|| Mutex::new(Vec::new()))
155}
156
157static NEXT_OBSERVER: AtomicU64 = AtomicU64::new(1);
158
159/// Keeps a host-surface observer registered until it is dropped.
160pub struct HostSurfaceObserver {
161    id: u64,
162}
163
164impl Drop for HostSurfaceObserver {
165    fn drop(&mut self) {
166        if let Ok(mut observers) = observers().lock() {
167            observers.retain(|(id, _)| *id != self.id);
168        }
169    }
170}
171
172/// Registers `observer` for host-surface size changes. Applications collect
173/// [`rememberHostSurfaceSize`] instead of calling this.
174pub fn observe_host_surface_size(
175    observer: impl Fn(HostSurfaceSize) + Send + Sync + 'static,
176) -> HostSurfaceObserver {
177    let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
178    if let Ok(mut observers) = observers().lock() {
179        observers.push((id, Arc::new(observer)));
180    }
181    HostSurfaceObserver { id }
182}
183
184/// Publishes a new host-surface size. Platform backends call this whenever the
185/// host resizes the surface.
186pub fn publish_host_surface_size(size: HostSurfaceSize) {
187    if let Ok(mut last) = last_published().lock() {
188        if *last == size {
189            return;
190        }
191        *last = size;
192    }
193    let observers = observers()
194        .lock()
195        .map(|observers| {
196            observers
197                .iter()
198                .map(|(_, observer)| Arc::clone(observer))
199                .collect::<Vec<_>>()
200        })
201        .unwrap_or_default();
202    for observer in observers {
203        observer(size);
204    }
205}
206
207/// The host surface's size, observed for as long as this call stays in the
208/// composition.
209#[allow(non_snake_case)]
210#[track_caller]
211pub fn rememberHostSurfaceSize() -> State<HostSurfaceSize> {
212    let updates = rememberEventStream((), |sender| {
213        observe_host_surface_size(move |size| sender.send(size))
214    });
215    cranpose_core::collectAsState(updates, (), host_surface_size())
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    struct FixedSurface;
223
224    /// A host that owns its own window and answers no to being resized.
225    impl HostSurface for FixedSurface {}
226
227    struct ResizableSurface {
228        requested: Mutex<Option<(f32, f32)>>,
229    }
230
231    impl HostSurface for ResizableSurface {
232        fn can_resize(&self) -> bool {
233            true
234        }
235
236        fn request_size(&self, width: f32, height: f32) -> Result<(), ResizeRefused> {
237            if !(width.is_finite() && height.is_finite()) || width <= 0.0 || height <= 0.0 {
238                return Err(ResizeRefused::Rejected);
239            }
240            *self
241                .requested
242                .lock()
243                .unwrap_or_else(|error| error.into_inner()) = Some((width, height));
244            Ok(())
245        }
246    }
247
248    #[test]
249    fn a_host_that_has_not_drawn_yet_reports_an_empty_surface() {
250        let _guard = crate::registry::test_service_guard();
251        clear_platform_host_surface();
252        let size = host_surface_size();
253        assert_eq!(size, HostSurfaceSize::default());
254        assert_eq!(size.scale, 1.0, "a scale of zero would divide by zero");
255        assert!(!host_surface_can_resize());
256        assert_eq!(
257            request_host_surface_size(320.0, 200.0),
258            Err(ResizeRefused::Unsupported)
259        );
260    }
261
262    #[test]
263    fn the_surface_size_is_whatever_the_host_last_published() {
264        let _guard = crate::registry::test_service_guard();
265        clear_platform_host_surface();
266        publish_host_surface_size(HostSurfaceSize {
267            width: 640.0,
268            height: 480.0,
269            scale: 2.0,
270        });
271        let size = host_surface_size();
272        assert_eq!(size.width, 640.0);
273        assert_eq!(size.physical(), (1280, 960));
274        clear_platform_host_surface();
275    }
276
277    #[test]
278    fn a_host_that_owns_its_window_refuses_resize_requests() {
279        let _guard = crate::registry::test_service_guard();
280        set_platform_host_surface(Arc::new(FixedSurface));
281        assert!(!host_surface_can_resize());
282        assert_eq!(
283            request_host_surface_size(320.0, 200.0),
284            Err(ResizeRefused::Unsupported)
285        );
286        clear_platform_host_surface();
287    }
288
289    #[test]
290    fn a_resizable_surface_takes_the_request_and_refuses_nonsense() {
291        let _guard = crate::registry::test_service_guard();
292        let surface = Arc::new(ResizableSurface {
293            requested: Mutex::new(None),
294        });
295        set_platform_host_surface(surface.clone());
296        assert!(host_surface_can_resize());
297        assert_eq!(request_host_surface_size(275.0, 116.0), Ok(()));
298        assert_eq!(
299            *surface
300                .requested
301                .lock()
302                .unwrap_or_else(|error| error.into_inner()),
303            Some((275.0, 116.0))
304        );
305        assert_eq!(
306            request_host_surface_size(0.0, 116.0),
307            Err(ResizeRefused::Rejected)
308        );
309        assert_eq!(
310            request_host_surface_size(f32::NAN, 116.0),
311            Err(ResizeRefused::Rejected)
312        );
313        clear_platform_host_surface();
314    }
315
316    #[test]
317    fn observers_see_published_sizes_and_stop_when_dropped() {
318        let _guard = crate::registry::test_service_guard();
319        clear_platform_host_surface();
320        let seen = Arc::new(Mutex::new(Vec::new()));
321        let recorder = Arc::clone(&seen);
322        let registration = observe_host_surface_size(move |size| {
323            recorder
324                .lock()
325                .unwrap_or_else(|error| error.into_inner())
326                .push(size)
327        });
328        let first = HostSurfaceSize {
329            width: 100.0,
330            height: 50.0,
331            scale: 1.0,
332        };
333        publish_host_surface_size(first);
334        // The same size again is not a change, so nothing is told about it.
335        publish_host_surface_size(first);
336        assert_eq!(
337            seen.lock().unwrap_or_else(|e| e.into_inner()).as_slice(),
338            [first]
339        );
340        drop(registration);
341        publish_host_surface_size(HostSurfaceSize {
342            width: 200.0,
343            ..first
344        });
345        assert_eq!(seen.lock().unwrap_or_else(|e| e.into_inner()).len(), 1);
346        clear_platform_host_surface();
347    }
348}