nandi/jolt-nativepublic Fork 0
e0d90292c3d0c00acad1fb5cdc100d728938759f
Commits
Clone
git clone https://git.rickub.com/nandi/jolt-native.git
git clone ssh://git@rickub.com/nandi/jolt-native.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

chrome.rs · 299 lines · 10.3 KBRust Blame HistoryRaw
Bring vidya in cfd3e36 nandi 19d ago1//! System chrome (status bar / nav bar) safe areas.
2//!
3//! Android NativeActivity draws **edge-to-edge**: the window fills under the
4//! system status bar (clock, battery, …) and the gesture/nav bar. Without an
5//! explicit reserve, app chrome and labels sit under those system widgets.
6//!
7//! Call [`reserve_system_chrome`] once at the start of every frame (before any
8//! other `TopBottomPanel` / `SidePanel` / `CentralPanel`), or use
9//! [`top_header`] which does that for you.
10//!
11//! Prefer injecting measured insets via [`set_system_chrome`] or
12//! [`sync_system_chrome_from_android`] (from `AndroidApp::content_rect` or
13//! WindowInsets) so reserves match the device — hardcoded fallbacks are
14//! intentionally tight for modern gesture-nav phones.
15
16use egui::{Align2, Context, Frame, Id, Margin, Sense, Ui, WidgetText, Window};
17
18use crate::Theme;
19
20/// Temp-data id for app-supplied measured insets (see [`set_system_chrome`]).
21const SYSTEM_CHROME_ID: &str = "vidya.system_chrome";
Clear the clippy backlog the new CI enforces 4956d1e nandi 13d ago22/// Last measured nav-band height when the keyboard was hidden. Only read on
23/// Android, where the insets come from the activity.
24#[cfg(target_os = "android")]
Bring vidya in cfd3e36 nandi 19d ago25const NAV_HINT_ID: &str = "vidya.nav_bottom_hint";
26
27/// Insets for system status / navigation chrome on edge-to-edge surfaces.
28#[derive(Debug, Clone, Copy, PartialEq)]
29pub struct SystemChrome {
30 /// Space under the status bar (clock, indicators).
31 pub top: f32,
32 /// Space above the system gesture / 3-button nav bar.
33 pub nav_bottom: f32,
34 /// Extra reserve when the soft keyboard is visible (sits above [`nav_bottom`]).
35 pub ime_bottom: f32,
36}
37
38impl SystemChrome {
39 pub const ZERO: Self = Self {
40 top: 0.0,
41 nav_bottom: 0.0,
42 ime_bottom: 0.0,
43 };
44
45 /// Combined bottom inset (nav + IME).
46 #[inline]
47 pub fn bottom(self) -> f32 {
48 self.nav_bottom + self.ime_bottom
49 }
50
51 pub fn is_zero(self) -> bool {
52 self.top <= 0.0 && self.bottom() <= 0.0
53 }
54
55 /// Build from status + nav insets (no IME reserve).
56 pub fn from_insets(top: f32, nav_bottom: f32) -> Self {
57 Self {
58 top: top.max(0.0),
59 nav_bottom: nav_bottom.max(0.0),
60 ime_bottom: 0.0,
61 }
62 }
63}
64
65/// Inject measured system insets for this frame (egui points).
66///
67/// Call once per frame **before** [`reserve_system_chrome`]. When set,
68/// [`system_chrome`] uses these values instead of the platform fallbacks and
69/// does **not** add focus-based IME padding on top (measured insets already
70/// reflect keyboard visibility from `content_rect` / WindowInsets).
71pub fn set_system_chrome(ctx: &Context, chrome: SystemChrome) {
72 ctx.data_mut(|d| d.insert_temp(Id::new(SYSTEM_CHROME_ID), chrome));
73}
74
75/// Read measured insets from [`AndroidApp::content_rect`] and call
76/// [`set_system_chrome`].
77///
78/// Call once per frame on Android **before** [`reserve_system_chrome`]. When the
79/// soft keyboard is open, `content_rect` shrinks and the derived bottom inset
80/// includes the IME height so layout tracks the real keyboard band — even when
81/// the user dismisses the keyboard via the IME close button while a text field
82/// still holds focus.
83#[cfg(target_os = "android")]
84pub fn sync_system_chrome_from_android(
85 ctx: &Context,
86 app: &winit::platform::android::activity::AndroidApp,
87) {
88 const NAV_FALLBACK: f32 = 20.0;
89 /// Minimum extra inset beyond the nav band before treating the IME as visible.
90 const IME_VISIBLE_THRESHOLD: f32 = 48.0;
91
92 let rect = app.content_rect();
93 let ppp = ctx.pixels_per_point().max(0.01);
94 let screen = ctx.screen_rect();
95
96 let top = (rect.top as f32 / ppp).max(0.0);
97 let content_bottom_pt = rect.bottom as f32 / ppp;
98 let total_bottom = (screen.height() - content_bottom_pt).max(0.0);
99
100 let nav_hint = ctx
101 .data(|d| d.get_temp::<f32>(Id::new(NAV_HINT_ID)))
102 .unwrap_or(NAV_FALLBACK);
103
104 // Derive keyboard visibility from content_rect, not wants_keyboard_input():
105 // dismissing the IME hides the keyboard but often leaves text focus.
106 let keyboard_visible = total_bottom > nav_hint + IME_VISIBLE_THRESHOLD;
107
108 if keyboard_visible {
109 let nav = nav_hint.clamp(0.0, total_bottom);
110 set_system_chrome(
111 ctx,
112 SystemChrome {
113 top,
114 nav_bottom: nav,
115 ime_bottom: (total_bottom - nav).max(0.0),
116 },
117 );
118 } else {
119 ctx.data_mut(|d| d.insert_temp(Id::new(NAV_HINT_ID), total_bottom));
120 set_system_chrome(ctx, SystemChrome::from_insets(top, total_bottom));
121 }
122}
123
124/// Platform defaults for edge-to-edge drawing.
125///
126/// Fallback values are **tight** for modern gesture-nav phones (≈24–28 dp
127/// status, ≈16–24 dp gesture handle). Prefer [`set_system_chrome`] or
128/// [`sync_system_chrome_from_android`] with measured `content_rect` /
129/// WindowInsets when available.
130///
131/// When a text field holds focus (`Context::wants_keyboard_input`), the bottom
132/// inset grows so bottom bars / compose fields sit **above** the soft keyboard
133/// (NativeActivity rarely resizes the GL surface for IME).
134pub fn system_chrome(ctx: &Context) -> SystemChrome {
135 #[cfg(target_os = "android")]
136 {
137 let measured = ctx.data(|d| d.get_temp::<SystemChrome>(Id::new(SYSTEM_CHROME_ID)));
138 const TOP_FALLBACK: f32 = 36.0;
139 const NAV_FALLBACK: f32 = 20.0;
140 let top = match measured {
141 Some(c) if c.top >= 8.0 => c.top,
142 Some(c) => c.top.max(TOP_FALLBACK),
143 None => TOP_FALLBACK,
144 };
145
146 if let Some(m) = measured {
147 let chrome = SystemChrome {
148 top,
149 nav_bottom: m.nav_bottom.max(0.0),
150 ime_bottom: m.ime_bottom.max(0.0),
151 };
152 // content_rect / WindowInsets may still be animating with the IME.
153 if chrome.ime_bottom > NAV_FALLBACK {
154 ctx.request_repaint();
155 }
156 return chrome;
157 }
158
159 let mut nav_bottom = NAV_FALLBACK;
160 let mut ime_bottom = 0.0;
161
162 if ctx.wants_keyboard_input() {
163 let h = ctx.screen_rect().height();
164 let ime_fallback = (h * 0.40).clamp(240.0, h * 0.52);
165 nav_bottom = NAV_FALLBACK.min(ime_fallback);
166 ime_bottom = (ime_fallback - nav_bottom).max(0.0);
167 ctx.request_repaint();
168 }
169
170 SystemChrome {
171 top,
172 nav_bottom,
173 ime_bottom,
174 }
175 }
176 #[cfg(not(target_os = "android"))]
177 {
178 let _ = ctx;
179 SystemChrome::ZERO
180 }
181}
182
183/// Reserve top/bottom strips so **no** subsequent panel or central content can
184/// paint under the system status or navigation bars.
185///
186/// Call this **once per frame**, before other panels. Safe to call when insets
187/// are zero (no-op on desktop).
188///
189/// When the soft keyboard is open, the IME reserve does **not** absorb pointer
190/// events so swipe typing on the system keyboard is not blocked. Only the nav
191/// strip keeps a hover sink so widgets behind the gesture bar cannot steal taps.
192pub fn reserve_system_chrome(ctx: &Context, theme: &Theme) {
193 let chrome = system_chrome(ctx);
194 if chrome.is_zero() {
195 return;
196 }
197
198 let top_band = Frame::new()
199 .fill(theme.palette.headerbar_bg)
200 .inner_margin(Margin::ZERO);
201 let nav_band = Frame::new()
202 .fill(theme.palette.window_bg)
203 .inner_margin(Margin::ZERO);
204 // Transparent — only reserves layout; must not paint over the IME.
205 let ime_band = Frame::NONE;
206
207 if chrome.top > 0.0 {
208 egui::TopBottomPanel::top("vidya_system_chrome_top")
209 .exact_height(chrome.top)
210 .frame(top_band)
211 .show_separator_line(false)
212 .show(ctx, |_ui| {});
213 }
214
215 // Bottom panels stack upward: declare nav first (screen edge), then IME.
216 if chrome.nav_bottom > 0.0 {
217 egui::TopBottomPanel::bottom("vidya_system_chrome_nav")
218 .exact_height(chrome.nav_bottom)
219 .frame(nav_band)
220 .show_separator_line(false)
221 .show(ctx, |ui| {
222 ui.allocate_exact_size(ui.available_size(), Sense::hover());
223 });
224 }
225
226 if chrome.ime_bottom > 0.0 {
227 egui::TopBottomPanel::bottom("vidya_system_chrome_ime")
228 .exact_height(chrome.ime_bottom)
229 .frame(ime_band)
230 .show_separator_line(false)
231 .show(ctx, |ui| {
232 // Pass touches through to the system keyboard (swipe / glide typing).
233 ui.allocate_exact_size(ui.available_size(), Sense::empty());
234 });
235 }
236}
237
238/// Top app header with system status bar already reserved.
239///
240/// Preferred entry for shell chrome: apps cannot place title/status text under
241/// the clock / indicators. Also reserves the bottom system nav band.
242///
243/// ```ignore
244/// vidya::top_header(ctx, &theme, |ui| {
245/// ui.horizontal(|ui| {
246/// vidya::title(ui, &theme, "My App");
247/// });
248/// });
249/// ```
250pub fn top_header(ctx: &Context, theme: &Theme, add_contents: impl FnOnce(&mut Ui)) {
251 reserve_system_chrome(ctx, theme);
252 egui::TopBottomPanel::top("vidya_app_header")
253 .frame(theme.header_frame())
254 .show_separator_line(false)
255 .show(ctx, add_contents);
256}
257
258/// Centered modal-style window with themed card chrome.
259///
260/// Defaults: non-collapsible, **resizable**, centered, [`Theme::card_frame`].
261/// Chain `.default_size` / `.min_width` / `.resizable(false)` as needed, then
262/// `.show`.
263///
264/// ```ignore
265/// vidya::dialog("Rename", &theme)
266/// .default_width(360.0)
267/// .min_width(280.0)
268/// .show(ctx, |ui| { /* … */ });
269/// ```
270pub fn dialog<'a>(title: impl Into<WidgetText> + 'a, theme: &Theme) -> Window<'a> {
271 Window::new(title)
272 .collapsible(false)
273 .resizable(true)
274 .anchor(Align2::CENTER_CENTER, [0.0, 0.0])
275 .frame(theme.card_frame())
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 #[test]
283 fn system_chrome_bottom_sums_nav_and_ime() {
284 let c = SystemChrome {
285 top: 36.0,
286 nav_bottom: 20.0,
287 ime_bottom: 400.0,
288 };
289 assert!((c.bottom() - 420.0).abs() < f32::EPSILON);
290 assert!(!c.is_zero());
291 }
292
293 #[test]
294 fn from_insets_zeroes_ime() {
295 let c = SystemChrome::from_insets(36.0, 20.0);
296 assert_eq!(c.ime_bottom, 0.0);
297 assert_eq!(c.nav_bottom, 20.0);
298 }
299}