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