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