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 crate::registry::ServiceRegistry;
11use cranpose_core::{rememberEventStream, State};
12use std::sync::atomic::{AtomicU64, Ordering};
13use std::sync::{Arc, Mutex, OnceLock};
14
15/// The size of the host surface, in logical pixels, with the scale the host
16/// renders it at.
17#[derive(Clone, Copy, Debug, PartialEq)]
18pub struct HostSurfaceSize {
19    /// Logical width.
20    pub width: f32,
21    /// Logical height.
22    pub height: f32,
23    /// Physical pixels per logical pixel.
24    pub scale: f32,
25}
26
27impl Default for HostSurfaceSize {
28    fn default() -> Self {
29        Self {
30            width: 0.0,
31            height: 0.0,
32            scale: 1.0,
33        }
34    }
35}
36
37impl HostSurfaceSize {
38    /// The size in physical pixels.
39    pub fn physical(&self) -> (u32, u32) {
40        (
41            (self.width * self.scale).round().max(0.0) as u32,
42            (self.height * self.scale).round().max(0.0) as u32,
43        )
44    }
45}
46
47/// Why a resize request was refused.
48#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
49pub enum ResizeRefused {
50    /// This host does not let an application choose its surface size — a
51    /// fullscreen mobile activity, a maximised window, a fixed canvas.
52    #[error("this host does not accept surface resize requests")]
53    Unsupported,
54    /// The host accepted the idea but refused these dimensions.
55    #[error("the host refused the requested surface size")]
56    Rejected,
57}
58
59/// What an application may ask of the host's surface.
60///
61/// The surface's *size* is not asked of a backend: every host publishes it as
62/// it lays the surface out, and [`host_surface_size`] answers from that. What a
63/// backend adds is the other direction — whether this host lets the application
64/// choose a size, and how to ask for one.
65pub trait HostSurface: Send + Sync {
66    /// Whether this host accepts resize requests at all.
67    fn can_resize(&self) -> bool {
68        false
69    }
70
71    /// Asks the host for a different surface size.
72    ///
73    /// Hosts are free to clamp or ignore the request, so the answer is only
74    /// "the request was accepted": the size that actually took effect arrives
75    /// through the observable state.
76    fn request_size(&self, width: f32, height: f32) -> Result<(), ResizeRefused> {
77        let _ = (width, height);
78        Err(ResizeRefused::Unsupported)
79    }
80}
81
82/// Shared handle to a [`HostSurface`].
83pub type HostSurfaceRef = Arc<dyn HostSurface>;
84
85struct NoHostSurface;
86
87/// A host that owns its own window: a fullscreen activity, a maximised window,
88/// a canvas the page sizes.
89impl HostSurface for NoHostSurface {}
90
91static PLATFORM_HOST_SURFACE: ServiceRegistry<dyn HostSurface> = ServiceRegistry::new();
92
93/// Installs the platform host surface.
94pub fn set_platform_host_surface(surface: HostSurfaceRef) {
95    PLATFORM_HOST_SURFACE.set(surface);
96}
97
98/// Removes the installed host surface (tests and teardown).
99pub fn clear_platform_host_surface() {
100    PLATFORM_HOST_SURFACE.clear();
101    if let Ok(mut observers) = observers().lock() {
102        observers.clear();
103    }
104    if let Ok(mut last) = last_published().lock() {
105        *last = HostSurfaceSize::default();
106    }
107}
108
109/// Whether this host lets the application choose its surface size.
110pub fn host_surface_can_resize() -> bool {
111    host_surface().can_resize()
112}
113
114/// The installed host surface, or one that accepts no resize requests.
115pub fn host_surface() -> HostSurfaceRef {
116    PLATFORM_HOST_SURFACE
117        .get()
118        .unwrap_or_else(|| Arc::new(NoHostSurface))
119}
120
121/// The size the host last reported.
122///
123/// Read from what the host published rather than asked of a backend: every host
124/// publishes its surface as it lays it out, and a host that has no resize
125/// facility to install a backend for still reports its size. Before the first
126/// frame this is the empty surface at scale one — nothing has been measured
127/// yet — so a caller that needs the real value observes
128/// [`rememberHostSurfaceSize`] rather than sampling once at startup.
129pub fn host_surface_size() -> HostSurfaceSize {
130    last_published()
131        .lock()
132        .map(|size| *size)
133        .unwrap_or_default()
134}
135
136fn last_published() -> &'static Mutex<HostSurfaceSize> {
137    static SLOT: OnceLock<Mutex<HostSurfaceSize>> = OnceLock::new();
138    SLOT.get_or_init(|| Mutex::new(HostSurfaceSize::default()))
139}
140
141/// Asks the host for a different surface size.
142pub fn request_host_surface_size(width: f32, height: f32) -> Result<(), ResizeRefused> {
143    host_surface().request_size(width, height)
144}
145
146type Observer = Arc<dyn Fn(HostSurfaceSize) + Send + Sync>;
147
148fn observers() -> &'static Mutex<Vec<(u64, Observer)>> {
149    static SLOT: OnceLock<Mutex<Vec<(u64, Observer)>>> = OnceLock::new();
150    SLOT.get_or_init(|| Mutex::new(Vec::new()))
151}
152
153static NEXT_OBSERVER: AtomicU64 = AtomicU64::new(1);
154
155/// Keeps a host-surface observer registered until it is dropped.
156pub struct HostSurfaceObserver {
157    id: u64,
158}
159
160impl Drop for HostSurfaceObserver {
161    fn drop(&mut self) {
162        if let Ok(mut observers) = observers().lock() {
163            observers.retain(|(id, _)| *id != self.id);
164        }
165    }
166}
167
168/// Registers `observer` for host-surface size changes. Applications collect
169/// [`rememberHostSurfaceSize`] instead of calling this.
170pub fn observe_host_surface_size(
171    observer: impl Fn(HostSurfaceSize) + Send + Sync + 'static,
172) -> HostSurfaceObserver {
173    let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
174    if let Ok(mut observers) = observers().lock() {
175        observers.push((id, Arc::new(observer)));
176    }
177    HostSurfaceObserver { id }
178}
179
180/// Publishes a new host-surface size. Platform backends call this whenever the
181/// host resizes the surface.
182pub fn publish_host_surface_size(size: HostSurfaceSize) {
183    if let Ok(mut last) = last_published().lock() {
184        if *last == size {
185            return;
186        }
187        *last = size;
188    }
189    let observers = observers()
190        .lock()
191        .map(|observers| {
192            observers
193                .iter()
194                .map(|(_, observer)| Arc::clone(observer))
195                .collect::<Vec<_>>()
196        })
197        .unwrap_or_default();
198    for observer in observers {
199        observer(size);
200    }
201}
202
203/// The host surface's size, observed for as long as this call stays in the
204/// composition.
205#[allow(non_snake_case)]
206pub fn rememberHostSurfaceSize() -> State<HostSurfaceSize> {
207    let updates = rememberEventStream((), |sender| {
208        observe_host_surface_size(move |size| sender.send(size))
209    });
210    cranpose_core::collectAsState(updates, (), host_surface_size())
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    struct FixedSurface;
218
219    /// A host that owns its own window and answers no to being resized.
220    impl HostSurface for FixedSurface {}
221
222    struct ResizableSurface {
223        requested: Mutex<Option<(f32, f32)>>,
224    }
225
226    impl HostSurface for ResizableSurface {
227        fn can_resize(&self) -> bool {
228            true
229        }
230
231        fn request_size(&self, width: f32, height: f32) -> Result<(), ResizeRefused> {
232            if !(width.is_finite() && height.is_finite()) || width <= 0.0 || height <= 0.0 {
233                return Err(ResizeRefused::Rejected);
234            }
235            *self
236                .requested
237                .lock()
238                .unwrap_or_else(|error| error.into_inner()) = Some((width, height));
239            Ok(())
240        }
241    }
242
243    #[test]
244    fn a_host_that_has_not_drawn_yet_reports_an_empty_surface() {
245        let _guard = crate::registry::test_service_guard();
246        clear_platform_host_surface();
247        let size = host_surface_size();
248        assert_eq!(size, HostSurfaceSize::default());
249        assert_eq!(size.scale, 1.0, "a scale of zero would divide by zero");
250        assert!(!host_surface_can_resize());
251        assert_eq!(
252            request_host_surface_size(320.0, 200.0),
253            Err(ResizeRefused::Unsupported)
254        );
255    }
256
257    #[test]
258    fn the_surface_size_is_whatever_the_host_last_published() {
259        let _guard = crate::registry::test_service_guard();
260        clear_platform_host_surface();
261        publish_host_surface_size(HostSurfaceSize {
262            width: 640.0,
263            height: 480.0,
264            scale: 2.0,
265        });
266        let size = host_surface_size();
267        assert_eq!(size.width, 640.0);
268        assert_eq!(size.physical(), (1280, 960));
269        clear_platform_host_surface();
270    }
271
272    #[test]
273    fn a_host_that_owns_its_window_refuses_resize_requests() {
274        let _guard = crate::registry::test_service_guard();
275        set_platform_host_surface(Arc::new(FixedSurface));
276        assert!(!host_surface_can_resize());
277        assert_eq!(
278            request_host_surface_size(320.0, 200.0),
279            Err(ResizeRefused::Unsupported)
280        );
281        clear_platform_host_surface();
282    }
283
284    #[test]
285    fn a_resizable_surface_takes_the_request_and_refuses_nonsense() {
286        let _guard = crate::registry::test_service_guard();
287        let surface = Arc::new(ResizableSurface {
288            requested: Mutex::new(None),
289        });
290        set_platform_host_surface(surface.clone());
291        assert!(host_surface_can_resize());
292        assert_eq!(request_host_surface_size(275.0, 116.0), Ok(()));
293        assert_eq!(
294            *surface
295                .requested
296                .lock()
297                .unwrap_or_else(|error| error.into_inner()),
298            Some((275.0, 116.0))
299        );
300        assert_eq!(
301            request_host_surface_size(0.0, 116.0),
302            Err(ResizeRefused::Rejected)
303        );
304        assert_eq!(
305            request_host_surface_size(f32::NAN, 116.0),
306            Err(ResizeRefused::Rejected)
307        );
308        clear_platform_host_surface();
309    }
310
311    #[test]
312    fn observers_see_published_sizes_and_stop_when_dropped() {
313        let _guard = crate::registry::test_service_guard();
314        clear_platform_host_surface();
315        let seen = Arc::new(Mutex::new(Vec::new()));
316        let recorder = Arc::clone(&seen);
317        let registration = observe_host_surface_size(move |size| {
318            recorder
319                .lock()
320                .unwrap_or_else(|error| error.into_inner())
321                .push(size)
322        });
323        let first = HostSurfaceSize {
324            width: 100.0,
325            height: 50.0,
326            scale: 1.0,
327        };
328        publish_host_surface_size(first);
329        // The same size again is not a change, so nothing is told about it.
330        publish_host_surface_size(first);
331        assert_eq!(
332            seen.lock().unwrap_or_else(|e| e.into_inner()).as_slice(),
333            [first]
334        );
335        drop(registration);
336        publish_host_surface_size(HostSurfaceSize {
337            width: 200.0,
338            ..first
339        });
340        assert_eq!(seen.lock().unwrap_or_else(|e| e.into_inner()).len(), 1);
341        clear_platform_host_surface();
342    }
343}