1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
// SPDX-License-Identifier: MIT OR Apache-2.0
//! View navigation stack with modal overlay support.
//!
//! `Navigator` manages a stack of [`View`](crate::view::View) instances,
//! supporting push/pop/replace transitions and modal overlays. Only the
//! topmost view (and any active modal) have live LVGL widgets.
//!
//! Views cannot call Navigator methods directly (the navigator owns
//! the view). Instead, [`View::update`](crate::view::View::update) and
//! [`View::on_event`](crate::view::View::on_event) return
//! [`NavAction`](crate::view::NavAction), which the render loop dispatches.
extern crate alloc;
use alloc::boxed::Box;
use alloc::vec::Vec;
use oxivgl_sys::*;
use crate::view::{
AnyView, NavAction, NavigationError, View,
take_pending_event_action,
};
use crate::widgets::{AsLvHandle, Obj, Screen, ScreenAnim};
/// Entry on the navigation stack, pairing a type-erased view with its
/// owning screen object (if any).
struct ViewEntry {
view: Box<dyn AnyView>,
/// The LVGL screen created for this view. `None` for the root view
/// which uses LVGL's default screen.
screen: Option<Obj<'static>>,
}
/// View navigation stack with modal overlay support.
///
/// The navigator owns all view instances. Views lower in the stack
/// have their widget trees destroyed but their struct state preserved.
/// Only the topmost view (and any active modal) have live widgets.
///
/// # Usage
///
/// For single-screen applications, use
/// [`run_app`](crate::view::run_app) directly. `Navigator` is for
/// multi-screen applications that need push/pop/replace/modal.
pub struct Navigator {
/// Full-screen navigation stack. Index 0 is the root view.
stack: Vec<ViewEntry>,
/// Currently active modal, if any. Rendered on `lv_layer_top()`.
modal: Option<Box<dyn AnyView>>,
}
impl Navigator {
/// Create a new empty navigator.
pub fn new() -> Self {
Self {
stack: Vec::new(),
modal: None,
}
}
/// Push the initial root view. Called once during setup.
///
/// The root view uses the default LVGL screen. Its widgets are
/// created immediately.
pub fn push_root(&mut self, view: impl View) {
let mut boxed: Box<dyn AnyView> = Box::new(view);
// Use the default active screen as the container. Child suppresses
// Drop so the LVGL screen is never deleted by Rust.
let screen_handle = unsafe { lv_screen_active() };
assert!(!screen_handle.is_null(), "no active screen");
let container = Obj::from_raw_non_owning(screen_handle);
boxed
.create(&container)
.expect("root view create failed");
// register_events() default calls register_event_on(self, lv_screen_active()).
// lv_screen_active() is the default screen — correct at this point.
boxed.register_events();
boxed.did_show();
self.stack.push(ViewEntry {
view: boxed,
screen: None,
});
}
/// Push a new view onto the stack.
///
/// 1. Calls `will_hide()` on the current top view.
/// 2. Creates a new LVGL screen for the new view.
/// 3. Loads the new screen (makes it active).
/// 4. Calls `create(container)` on the new view.
/// 5. Registers event handlers.
/// 6. Cleans the old screen's widget tree (preserving view state).
/// 7. Calls `did_show()` on the new view.
pub fn push(&mut self, view: impl View, anim: Option<ScreenAnim>) {
self.push_boxed(Box::new(view), anim);
}
/// Push a boxed (type-erased) view.
fn push_boxed(&mut self, mut boxed: Box<dyn AnyView>, anim: Option<ScreenAnim>) {
// Notify current top view.
if let Some(top) = self.stack.last_mut() {
top.view.will_hide();
}
// Capture the old screen handle BEFORE loading the new screen,
// because lv_screen_active() will change after Screen::load.
let old_screen_h = self.stack.last().map(|top| {
top.screen
.as_ref()
.map(|s| s.lv_handle())
.unwrap_or_else(|| {
// Root view uses the LVGL default screen.
unsafe { lv_screen_active() }
})
});
// Create a new screen for the incoming view.
let new_screen = Screen::create();
// Load the new screen BEFORE create/register_events so that
// lv_screen_active() returns the new screen during those calls.
if let Some(ref a) = anim {
Screen::load(&new_screen, a, false);
} else {
Screen::load_instant(&new_screen);
}
boxed
.create(&new_screen)
.expect("pushed view create failed");
// register_events() calls register_event_on(self, lv_screen_active()).
// Since we loaded new_screen above, lv_screen_active() == new_screen.
boxed.register_events();
// Clean the old screen's children (widget tree) to free memory,
// but keep the screen object alive for potential pop animation.
// SAFETY: old_screen_h was captured above while still valid. The old
// screen object is still alive (just no longer active). lv_obj_clean
// deletes all children but keeps the screen itself. Note: any Obj
// wrappers held by the old view now contain stale pointers — their
// Drop uses lv_obj_is_valid() as a guard (see spec-memory-lifetime §8.1).
if let Some(h) = old_screen_h {
unsafe { lv_obj_clean(h) };
}
boxed.did_show();
self.stack.push(ViewEntry {
view: boxed,
screen: Some(new_screen),
});
}
/// Pop the current view and return to the previous one.
///
/// Returns `Err(NavigationError::StackEmpty)` if only the root view
/// remains (the root cannot be popped).
pub fn pop(&mut self, anim: Option<ScreenAnim>) -> Result<(), NavigationError> {
if self.stack.len() <= 1 {
return Err(NavigationError::StackEmpty);
}
// Remove the top view — will_hide + drop.
let mut popped = self.stack.pop().unwrap();
popped.view.will_hide();
// Rebuild the now-top view's widgets.
let top = self.stack.last_mut().unwrap();
// Load the restored screen BEFORE dropping the popped screen.
// This ensures lv_screen_active() returns the correct screen
// during create/register_events, and avoids the undefined state
// of having no active screen.
let container_handle = if let Some(ref top_screen) = top.screen {
if let Some(ref a) = anim {
Screen::load(top_screen, a, false);
} else {
Screen::load_instant(top_screen);
}
top_screen.lv_handle()
} else {
// Root view: load the default LVGL screen. We must get its
// handle BEFORE dropping popped (which deletes popped.screen).
// SAFETY: lv_display_get_default/lv_display_get_screen returns
// the LVGL default screen (index 0), which is always valid.
let default_screen = unsafe {
let disp = lv_display_get_default();
lv_display_get_screen_active(disp)
};
// The default screen may be behind the popped screen. Load it
// before dropping so it becomes active.
Screen::load_instant(&Obj::from_raw_non_owning(default_screen));
default_screen
};
// Now safe to drop the popped view and its screen.
drop(popped);
// Non-owning handle — the screen is owned by the ViewEntry, not
// this temporary. Child suppresses Drop so no screen deletion.
let container = Obj::from_raw_non_owning(container_handle);
top.view
.create(&container)
.map_err(NavigationError::CreateFailed)?;
top.view.register_events();
top.view.did_show();
Ok(())
}
/// Replace the current view without preserving it on the stack.
///
/// The current view is dropped. The new view takes its place at the
/// same stack depth.
pub fn replace(&mut self, view: impl View, anim: Option<ScreenAnim>) {
self.replace_boxed(Box::new(view), anim);
}
/// Replace with a boxed view.
fn replace_boxed(&mut self, mut boxed: Box<dyn AnyView>, anim: Option<ScreenAnim>) {
// Notify the view being replaced so it can save state if needed.
if let Some(top) = self.stack.last_mut() {
top.view.will_hide();
}
// Create a new screen and load it BEFORE dropping the old view,
// ensuring there is always a valid active screen.
let new_screen = Screen::create();
if let Some(ref a) = anim {
Screen::load(&new_screen, a, false);
} else {
Screen::load_instant(&new_screen);
}
// Now safe to drop the old view and its screen.
self.stack.pop();
boxed
.create(&new_screen)
.expect("replaced view create failed");
boxed.register_events();
boxed.did_show();
self.stack.push(ViewEntry {
view: boxed,
screen: Some(new_screen),
});
}
/// Show a modal overlay on top of the current view.
///
/// The current view's widget tree stays alive and visible underneath.
/// The modal's widgets are created on `lv_layer_top()`.
///
/// Only one modal can be active at a time. Calling `modal()` while
/// a modal is already open replaces it.
pub fn modal(&mut self, view: impl View) {
self.modal_boxed(Box::new(view));
}
/// Show a boxed modal.
fn modal_boxed(&mut self, mut boxed: Box<dyn AnyView>) {
// Dismiss any existing modal first.
if self.modal.is_some() {
let _ = self.dismiss_modal();
}
let layer_top = Screen::layer_top();
boxed
.create(&layer_top)
.expect("modal view create failed");
// For modals, register_events default would register on
// lv_screen_active() which is the background view's screen.
// Modal views should override register_events to register on
// the layer_top container instead, or use EVENT_BUBBLE.
// We call register_events() and trust the view's override.
boxed.register_events();
boxed.did_show();
self.modal = Some(boxed);
}
/// Dismiss the current modal overlay.
///
/// Cleans `lv_layer_top()` children. Returns `Err` if no modal is active.
pub fn dismiss_modal(&mut self) -> Result<(), NavigationError> {
if let Some(mut modal) = self.modal.take() {
modal.will_hide();
// SAFETY: lv_layer_top() returns the global overlay object (valid
// after lv_init). lv_obj_clean deletes all children. Any Obj
// wrappers in the modal view now hold stale pointers — their Drop
// uses lv_obj_is_valid() as a guard (spec-memory-lifetime §8.1).
// We clean before dropping the modal so LVGL removes its widgets
// from the display immediately.
let layer = unsafe { lv_layer_top() };
unsafe { lv_obj_clean(layer) };
// modal is dropped here — Obj::drop guards prevent double-free.
Ok(())
} else {
Err(NavigationError::NoActiveModal)
}
}
/// Whether a modal is currently showing.
pub fn has_modal(&self) -> bool {
self.modal.is_some()
}
/// Number of views on the stack.
pub fn depth(&self) -> usize {
self.stack.len()
}
/// Get a mutable reference to the active (topmost) view.
pub fn active_view_mut(&mut self) -> Option<&mut dyn AnyView> {
self.stack
.last_mut()
.map(|e| &mut *e.view as &mut dyn AnyView)
}
/// Get a mutable reference to the active modal, if any.
pub fn active_modal_mut(&mut self) -> Option<&mut dyn AnyView> {
self.modal.as_mut().map(|m| &mut **m as &mut dyn AnyView)
}
/// Process a [`NavAction`] returned by a view.
pub fn process_action(&mut self, action: NavAction) {
match action {
NavAction::None => {}
NavAction::Push(view, anim) => self.push_boxed(view, anim),
NavAction::Pop(anim) => {
if let Err(e) = self.pop(anim) {
warn!("nav pop failed: {}", e);
}
}
NavAction::Replace(view, anim) => self.replace_boxed(view, anim),
NavAction::Modal(view) => self.modal_boxed(view),
NavAction::DismissModal => {
if let Err(e) = self.dismiss_modal() {
warn!("nav dismiss_modal failed: {}", e);
}
}
}
}
/// Process any pending event action stashed by the on_event trampoline.
/// Returns `true` if an event action was processed.
pub fn process_pending_event_action(&mut self) -> bool {
if let Some(action) = take_pending_event_action() {
self.process_action(action);
true
} else {
false
}
}
}