Skip to main content

cranpose_ui/
safe_area.rs

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