extern crate alloc;
use alloc::boxed::Box;
use alloc::collections::VecDeque;
use alloc::vec::Vec;
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, channel::Channel};
use core::time::Duration;
use oxivgl_sys::*;
use crate::driver::get_tick_ms;
use crate::view::{
AnyView, NavAction, NavigationError, View,
take_pending_event_action,
};
use crate::widgets::{AsLvHandle, Obj, Screen, ScreenAnim};
pub const TOAST_MARGIN_PX: i32 = 2;
pub const TOAST_SHADOW_WIDTH_PX: i32 = 12;
pub const TOAST_SHADOW_OPA: u8 = 80;
struct ViewEntry {
view: Box<dyn AnyView>,
screen: Option<Obj<'static>>,
}
pub struct Navigator {
stack: Vec<ViewEntry>,
modal: Option<Box<dyn AnyView>>,
modal_backdrop: Option<Obj<'static>>,
saved_focus: Option<SavedFocus>,
toast: Option<Box<dyn AnyView>>,
toast_container: Option<Obj<'static>>,
toast_deadline_ms: Option<u32>,
toast_queue: VecDeque<PendingToast>,
}
struct PendingToast {
view: Box<dyn AnyView>,
duration: Option<Duration>,
}
const TOAST_PENDING_CAPACITY: usize = 4;
impl Navigator {
pub fn new() -> Self {
Self {
stack: Vec::new(),
modal: None,
modal_backdrop: None,
saved_focus: None,
toast: None,
toast_container: None,
toast_deadline_ms: None,
toast_queue: VecDeque::new(),
}
}
pub fn push_root(&mut self, view: impl View) {
let mut boxed: Box<dyn AnyView> = Box::new(view);
let new_screen = Screen::create();
Screen::load_instant(&new_screen);
boxed
.create(&new_screen)
.expect("root view create failed");
boxed.register_events_on(&new_screen);
boxed.did_show();
activate_view_group(boxed.input_group());
self.stack.push(ViewEntry {
view: boxed,
screen: Some(new_screen),
});
}
pub fn push(&mut self, view: impl View, anim: Option<ScreenAnim>) {
self.push_boxed(Box::new(view), anim);
}
fn push_boxed(&mut self, mut boxed: Box<dyn AnyView>, anim: Option<ScreenAnim>) {
if let Some(top) = self.stack.last_mut() {
top.view.will_hide();
}
let old_screen_h = self.stack.last().map(|top| {
top.screen
.as_ref()
.map(|s| s.lv_handle())
.unwrap_or_else(|| {
unsafe { lv_screen_active() }
})
});
let new_screen = Screen::create();
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");
boxed.register_events_on(&new_screen);
self.reattach_toast();
if let Some(h) = old_screen_h {
unsafe { lv_obj_clean(h) };
}
boxed.did_show();
activate_view_group(boxed.input_group());
self.stack.push(ViewEntry {
view: boxed,
screen: Some(new_screen),
});
}
pub fn pop(&mut self, anim: Option<ScreenAnim>) -> Result<(), NavigationError> {
if self.stack.len() <= 1 {
return Err(NavigationError::StackEmpty);
}
let mut popped = self.stack.pop().unwrap();
popped.view.will_hide();
let container_handle = {
let top = self.stack.last_mut().unwrap();
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 {
debug_assert!(false, "pop: top ViewEntry has no screen — invariant broken");
let default_screen = unsafe {
let disp = lv_display_get_default();
lv_display_get_screen_active(disp)
};
Screen::load_instant(&Obj::from_raw_non_owning(default_screen));
default_screen
}
};
self.reattach_toast();
drop(popped);
let container = Obj::from_raw_non_owning(container_handle);
{
let top = self.stack.last_mut().unwrap();
top.view
.create(&container)
.map_err(NavigationError::CreateFailed)?;
top.view.register_events_on(&container);
top.view.did_show();
activate_view_group(top.view.input_group());
}
self.reattach_toast();
Ok(())
}
pub fn replace(&mut self, view: impl View, anim: Option<ScreenAnim>) {
self.replace_boxed(Box::new(view), anim);
}
fn replace_boxed(&mut self, mut boxed: Box<dyn AnyView>, anim: Option<ScreenAnim>) {
if let Some(top) = self.stack.last_mut() {
top.view.will_hide();
}
let new_screen = Screen::create();
if let Some(ref a) = anim {
Screen::load(&new_screen, a, false);
} else {
Screen::load_instant(&new_screen);
}
self.reattach_toast();
self.stack.pop();
boxed
.create(&new_screen)
.expect("replaced view create failed");
boxed.register_events_on(&new_screen);
boxed.did_show();
activate_view_group(boxed.input_group());
self.reattach_toast();
self.stack.push(ViewEntry {
view: boxed,
screen: Some(new_screen),
});
}
pub fn modal(&mut self, view: impl View) {
self.modal_boxed(Box::new(view));
}
fn modal_boxed(&mut self, mut boxed: Box<dyn AnyView>) {
if self.modal.is_some() {
let _ = self.dismiss_modal();
}
let layer_top_h = unsafe { lv_layer_top() };
assert!(!layer_top_h.is_null(), "lv_layer_top returned NULL");
let backdrop_h = unsafe { lv_obj_create(layer_top_h) };
assert!(!backdrop_h.is_null(), "modal backdrop creation failed");
let backdrop = Obj::from_raw(backdrop_h);
unsafe {
let pct100 = lv_pct(100);
lv_obj_set_size(backdrop_h, pct100, pct100);
lv_obj_set_style_bg_opa(backdrop_h, 0, 0);
lv_obj_set_style_border_width(backdrop_h, 0, 0);
lv_obj_set_style_pad_all(backdrop_h, 0, 0);
lv_obj_add_flag(
backdrop_h,
crate::enums::ObjFlag::CLICKABLE.0,
);
lv_obj_remove_flag(
backdrop_h,
crate::enums::ObjFlag::SCROLLABLE.0,
);
}
boxed
.create(&backdrop)
.expect("modal view create failed");
boxed.register_events_on(&backdrop);
boxed.did_show();
if let Some(modal_group) = boxed.input_group() {
self.saved_focus = Some(SavedFocus::capture());
modal_group.set_default();
modal_group.assign_to_keyboard_indevs();
}
self.modal = Some(boxed);
self.modal_backdrop = Some(backdrop);
self.reattach_toast();
}
pub fn dismiss_modal(&mut self) -> Result<(), NavigationError> {
let mut modal = match self.modal.take() {
Some(m) => m,
None => return Err(NavigationError::NoActiveModal),
};
modal.will_hide();
let backdrop = self.modal_backdrop.take();
self.reattach_toast();
drop(backdrop);
drop(modal);
if let Some(saved) = self.saved_focus.take() {
saved.restore();
}
Ok(())
}
pub fn has_modal(&self) -> bool {
self.modal.is_some()
}
pub fn show_toast(&mut self, view: impl View, duration: Option<Duration>) {
self.show_toast_boxed(Box::new(view), duration);
}
fn show_toast_boxed(&mut self, boxed: Box<dyn AnyView>, duration: Option<Duration>) {
match duration {
None => {
self.toast_queue.clear();
if self.toast.is_some() {
let _ = self.teardown_active_toast();
}
self.display_toast_now(boxed, None);
}
Some(_) => {
if self.toast.is_some() {
self.enqueue_toast(boxed, duration);
} else {
self.display_toast_now(boxed, duration);
}
}
}
}
fn enqueue_toast(&mut self, boxed: Box<dyn AnyView>, duration: Option<Duration>) {
if self.toast_queue.len() >= TOAST_PENDING_CAPACITY {
warn!(
"nav show_toast: pending queue full ({}), toast dropped",
TOAST_PENDING_CAPACITY,
);
return;
}
self.toast_queue.push_back(PendingToast { view: boxed, duration });
}
fn current_toast_surface(&self) -> *mut lv_obj_t {
if let Some(backdrop) = self.modal_backdrop.as_ref() {
backdrop.lv_handle()
} else {
unsafe { lv_screen_active() }
}
}
fn reattach_toast(&self) {
if let Some(container) = self.toast_container.as_ref() {
let handle = container.lv_handle();
let surface = self.current_toast_surface();
unsafe {
if lv_obj_is_valid(handle) && !surface.is_null() {
lv_obj_set_parent(handle, surface);
}
}
}
}
fn display_toast_now(&mut self, mut boxed: Box<dyn AnyView>, duration: Option<Duration>) -> bool {
debug_assert!(self.toast.is_none(), "display_toast_now: slot not empty");
let surface = self.current_toast_surface();
assert!(!surface.is_null(), "toast surface is NULL");
let container_handle = unsafe { lv_obj_create(surface) };
assert!(!container_handle.is_null(), "toast container creation failed");
let container = Obj::from_raw(container_handle);
unsafe {
lv_obj_set_width(container_handle, lv_pct(100));
lv_obj_set_style_margin_left(container_handle, TOAST_MARGIN_PX, 0);
lv_obj_set_style_margin_right(container_handle, TOAST_MARGIN_PX, 0);
lv_obj_set_height(container_handle, crate::style::LV_SIZE_CONTENT);
lv_obj_align(
container_handle,
lv_align_t_LV_ALIGN_BOTTOM_MID as lv_align_t,
0,
-TOAST_MARGIN_PX,
);
lv_obj_set_style_shadow_width(container_handle, TOAST_SHADOW_WIDTH_PX, 0);
lv_obj_set_style_shadow_opa(container_handle, TOAST_SHADOW_OPA, 0);
}
if let Err(e) = boxed.create(&container) {
warn!("nav show_toast: create failed: {:?}", e);
drop(container);
return false;
}
unsafe { remove_clickable_recursive(container_handle) };
unsafe { lv_obj_update_layout(container_handle) };
container.invalidate();
boxed.did_show();
self.toast = Some(boxed);
self.toast_container = Some(container);
self.toast_deadline_ms = duration.map(|d| {
let ms = d.as_millis().min(u32::MAX as u128) as u32;
get_tick_ms().wrapping_add(ms)
});
true
}
fn promote_next_toast(&mut self) {
if self.toast.is_some() {
return;
}
while let Some(next) = self.toast_queue.pop_front() {
if self.display_toast_now(next.view, next.duration) {
break;
}
}
}
fn teardown_active_toast(&mut self) -> Result<(), NavigationError> {
let mut toast = match self.toast.take() {
Some(t) => t,
None => return Err(NavigationError::NoActiveToast),
};
self.toast_deadline_ms = None;
toast.will_hide();
if let Some(container) = self.toast_container.take() {
let handle = container.lv_handle();
core::mem::forget(container);
unsafe {
if lv_obj_is_valid(handle) {
lv_obj_delete(handle);
}
}
}
drop(toast);
Ok(())
}
pub fn dismiss_toast(&mut self) -> Result<(), NavigationError> {
self.teardown_active_toast()?;
self.promote_next_toast();
Ok(())
}
pub fn has_toast(&self) -> bool {
self.toast.is_some()
}
#[doc(hidden)]
pub fn toast_container_handle(&self) -> Option<*mut lv_obj_t> {
self.toast_container.as_ref().map(|c| c.lv_handle())
}
pub fn active_toast_mut(&mut self) -> Option<&mut dyn AnyView> {
self.toast.as_mut().map(|t| &mut **t as &mut dyn AnyView)
}
pub fn tick_toast(&mut self) {
if self.toast.is_none() {
return;
}
if let Some(container) = self.toast_container.as_ref() {
let handle = container.lv_handle();
if !unsafe { lv_obj_is_valid(handle) } {
if let Some(mut t) = self.toast.take() {
t.will_hide();
}
if let Some(orphan) = self.toast_container.take() {
core::mem::forget(orphan);
}
self.toast_deadline_ms = None;
self.promote_next_toast();
return;
}
}
if let Some(deadline) = self.toast_deadline_ms
&& get_tick_ms().wrapping_sub(deadline) as i32 >= 0
{
let _ = self.dismiss_toast();
}
}
pub fn depth(&self) -> usize {
self.stack.len()
}
pub fn active_view_mut(&mut self) -> Option<&mut dyn AnyView> {
self.stack
.last_mut()
.map(|e| &mut *e.view as &mut dyn AnyView)
}
pub fn active_modal_mut(&mut self) -> Option<&mut dyn AnyView> {
self.modal.as_mut().map(|m| &mut **m as &mut dyn AnyView)
}
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);
}
}
NavAction::ShowToast(view, duration) => self.show_toast_boxed(view, duration),
NavAction::DismissToast => {
if let Err(e) = self.dismiss_toast() {
warn!("nav dismiss_toast failed: {}", e);
}
}
}
}
pub fn process_pending_event_action(&mut self) -> bool {
if let Some(action) = take_pending_event_action() {
self.process_action(action);
true
} else {
false
}
}
pub fn drain_toast_requests(&mut self) {
while let Ok(req) = TOAST_CHANNEL.try_receive() {
match req {
ToastRequest::Show(make, duration) => {
self.show_toast_boxed(make(), duration);
}
ToastRequest::Dismiss => {
let _ = self.dismiss_toast();
}
}
}
}
}
impl Default for Navigator {
fn default() -> Self {
Self::new()
}
}
fn activate_view_group(group: Option<crate::group::GroupRef>) {
if let Some(g) = group {
g.set_default();
g.assign_to_keyboard_indevs();
}
}
const TOAST_QUEUE_CAPACITY: usize = 4;
type ToastBuilder = Box<dyn FnOnce() -> Box<dyn AnyView> + Send>;
enum ToastRequest {
Show(ToastBuilder, Option<Duration>),
Dismiss,
}
static TOAST_CHANNEL: Channel<CriticalSectionRawMutex, ToastRequest, TOAST_QUEUE_CAPACITY> =
Channel::new();
pub fn post_toast_with<V: View>(
make: impl FnOnce() -> V + Send + 'static,
duration: Option<Duration>,
) {
let req = ToastRequest::Show(Box::new(move || Box::new(make()) as Box<dyn AnyView>), duration);
if TOAST_CHANNEL.try_send(req).is_err() {
warn!("post_toast: queue full ({}), request dropped", TOAST_QUEUE_CAPACITY);
}
}
pub fn post_toast<V: View + Send>(view: V, duration: Option<Duration>) {
post_toast_with(move || view, duration);
}
pub fn post_dismiss_toast() {
if TOAST_CHANNEL.try_send(ToastRequest::Dismiss).is_err() {
warn!(
"post_dismiss_toast: queue full ({}), request dropped",
TOAST_QUEUE_CAPACITY,
);
}
}
struct SavedFocus {
prev_default: *mut lv_group_t,
prev_indev_groups: Vec<(*mut lv_indev_t, *mut lv_group_t)>,
}
impl SavedFocus {
fn capture() -> Self {
let prev_default = unsafe { lv_group_get_default() };
let mut prev_indev_groups = Vec::new();
unsafe {
let mut indev = lv_indev_get_next(core::ptr::null_mut());
while !indev.is_null() {
let kind = lv_indev_get_type(indev);
if kind == lv_indev_type_t_LV_INDEV_TYPE_KEYPAD
|| kind == lv_indev_type_t_LV_INDEV_TYPE_ENCODER
{
prev_indev_groups.push((indev, lv_indev_get_group(indev)));
}
indev = lv_indev_get_next(indev);
}
}
Self { prev_default, prev_indev_groups }
}
fn restore(self) {
unsafe {
lv_group_set_default(self.prev_default);
for (indev, group) in self.prev_indev_groups {
lv_indev_set_group(indev, group);
}
}
}
}
unsafe fn remove_clickable_recursive(obj: *mut lv_obj_t) {
if obj.is_null() {
return;
}
let flags = crate::enums::ObjFlag::CLICKABLE.0 | crate::enums::ObjFlag::CLICK_FOCUSABLE.0;
unsafe {
lv_obj_remove_flag(obj, flags);
let n = lv_obj_get_child_count(obj);
for i in 0..n {
let child = lv_obj_get_child(obj, i as i32);
remove_clickable_recursive(child);
}
}
}