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 /// Insets of the system bars, display cutouts and rounded corners, in logical pixels.
12 pub safe_area: EdgeInsets,
13 /// Insets of the on-screen keyboard, in logical pixels.
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 crate::thread_state::with_environment(|locals| {
41 locals
42 .safe_area
43 .get_or_init(|| compositionLocalOf(EdgeInsets::default))
44 .clone()
45 })
46}
47
48/// CompositionLocal carrying the on-screen soft-keyboard (IME) insets in logical
49/// pixels. The `bottom` field is the height the keyboard currently covers at the
50/// bottom of the window (zero when it is hidden); the other edges are zero.
51///
52/// Mobile backends update this as the keyboard animates in and out so an
53/// application can keep the focused field visible — for example by adding
54/// `bottom` to a scroll container's bottom padding, or scrolling the caret into
55/// view. It defaults to [`EdgeInsets::default`] (zero), so desktop and web
56/// compositions (and mobile frames with no keyboard) are unaffected.
57///
58/// Like [`local_safe_area_insets`], the same `CompositionLocal` instance is
59/// returned on every call (cached per thread).
60pub fn local_ime_insets() -> CompositionLocal<EdgeInsets> {
61 crate::thread_state::with_environment(|locals| {
62 locals
63 .ime
64 .get_or_init(|| compositionLocalOf(EdgeInsets::default))
65 .clone()
66 })
67}
68
69/// Returns the framework-owned insets currently visible to the composition.
70pub fn window_insets() -> WindowInsets {
71 WindowInsets {
72 safe_area: local_safe_area_insets().current(),
73 ime: local_ime_insets().current(),
74 }
75}
76
77impl Modifier {
78 /// Adds padding for explicit platform or application insets.
79 pub fn window_insets_padding(self, insets: EdgeInsets) -> Self {
80 self.padding_each(insets.left, insets.top, insets.right, insets.bottom)
81 }
82
83 /// Adds padding for system bars, display cutouts, and rounded display edges.
84 pub fn safe_area_padding(self) -> Self {
85 self.window_insets_padding(local_safe_area_insets().current())
86 }
87}
88
89#[cfg(test)]
90#[path = "tests/safe_area_tests.rs"]
91mod tests;