Skip to main content

cranpose_ui/
safe_area.rs

1//! Platform safe-area insets exposed to composition.
2
3use cranpose_core::{CompositionLocal, compositionLocalOf};
4use cranpose_ui_graphics::EdgeInsets;
5
6use crate::Modifier;
7
8/// Insets needed to keep content clear of system UI and the on-screen keyboard.
9#[derive(Clone, Copy, Debug, Default, PartialEq)]
10pub struct WindowInsets {
11    pub safe_area: EdgeInsets,
12    pub ime: EdgeInsets,
13}
14
15impl WindowInsets {
16    /// Combines overlapping insets by taking the largest obstruction on each edge.
17    pub fn combined(self) -> EdgeInsets {
18        EdgeInsets::from_components(
19            self.safe_area.left.max(self.ime.left),
20            self.safe_area.top.max(self.ime.top),
21            self.safe_area.right.max(self.ime.right),
22            self.safe_area.bottom.max(self.ime.bottom),
23        )
24    }
25}
26
27/// CompositionLocal carrying the platform safe-area insets in logical pixels.
28///
29/// Mobile backends provide the system-bar / notch / home-indicator insets here
30/// so applications can keep content clear of them (for example with
31/// `Modifier.padding_each`). It defaults to [`EdgeInsets::default`] (zero), so
32/// desktop and web compositions are unaffected.
33///
34/// The same `CompositionLocal` instance is returned on every call (cached per
35/// thread), so the platform that provides it and the app that reads it observe
36/// one shared local.
37pub fn local_safe_area_insets() -> CompositionLocal<EdgeInsets> {
38    crate::environment_locals::ENVIRONMENT_LOCALS.with(|locals| {
39        locals
40            .safe_area
41            .get_or_init(|| compositionLocalOf(EdgeInsets::default))
42            .clone()
43    })
44}
45
46/// CompositionLocal carrying the on-screen soft-keyboard (IME) insets in logical
47/// pixels. The `bottom` field is the height the keyboard currently covers at the
48/// bottom of the window (zero when it is hidden); the other edges are zero.
49///
50/// Mobile backends update this as the keyboard animates in and out so an
51/// application can keep the focused field visible — for example by adding
52/// `bottom` to a scroll container's bottom padding, or scrolling the caret into
53/// view. It defaults to [`EdgeInsets::default`] (zero), so desktop and web
54/// compositions (and mobile frames with no keyboard) are unaffected.
55///
56/// Like [`local_safe_area_insets`], the same `CompositionLocal` instance is
57/// returned on every call (cached per thread).
58pub fn local_ime_insets() -> CompositionLocal<EdgeInsets> {
59    crate::environment_locals::ENVIRONMENT_LOCALS.with(|locals| {
60        locals
61            .ime
62            .get_or_init(|| compositionLocalOf(EdgeInsets::default))
63            .clone()
64    })
65}
66
67/// Returns the framework-owned insets currently visible to the composition.
68pub fn window_insets() -> WindowInsets {
69    WindowInsets {
70        safe_area: local_safe_area_insets().current(),
71        ime: local_ime_insets().current(),
72    }
73}
74
75impl Modifier {
76    /// Adds padding for explicit platform or application insets.
77    pub fn window_insets_padding(self, insets: EdgeInsets) -> Self {
78        self.padding_each(insets.left, insets.top, insets.right, insets.bottom)
79    }
80
81    /// Adds padding for system bars, display cutouts, and rounded display edges.
82    pub fn safe_area_padding(self) -> Self {
83        self.window_insets_padding(local_safe_area_insets().current())
84    }
85}
86
87#[cfg(test)]
88#[path = "tests/safe_area_tests.rs"]
89mod tests;