use alloc::boxed::Box;
use core::cell::UnsafeCell;
use core::ffi::c_void;
use embassy_time::{Duration, Timer};
use oxivgl_sys::*;
use crate::{
display::{lvgl_disp_init, LvglBuffers, DISPLAY_READY},
driver::LvglDriver,
enums::EventCode,
event::Event,
widgets::{Obj, ScreenAnim, WidgetError},
};
const LVGL_TICK_MS: u64 = LV_DEF_REFR_PERIOD as u64 / 4;
pub trait View: Sized + 'static {
fn create(&mut self, container: &Obj<'static>) -> Result<(), WidgetError>;
fn update(&mut self) -> Result<NavAction, WidgetError> {
Ok(NavAction::None)
}
fn on_event(&mut self, _event: &Event) -> NavAction {
NavAction::None
}
fn register_events(&mut self) {
register_event_on(self, unsafe { lv_screen_active() });
}
fn will_hide(&mut self) {}
fn did_show(&mut self) {}
}
pub enum NavAction {
None,
Push(Box<dyn AnyView>, Option<ScreenAnim>),
Pop(Option<ScreenAnim>),
Replace(Box<dyn AnyView>, Option<ScreenAnim>),
Modal(Box<dyn AnyView>),
DismissModal,
}
impl NavAction {
pub fn push(view: impl View, anim: Option<ScreenAnim>) -> Self {
Self::Push(Box::new(view), anim)
}
pub fn replace(view: impl View, anim: Option<ScreenAnim>) -> Self {
Self::Replace(Box::new(view), anim)
}
pub fn modal(view: impl View) -> Self {
Self::Modal(Box::new(view))
}
pub fn is_none(&self) -> bool {
matches!(self, Self::None)
}
}
pub trait AnyView: 'static {
fn create(&mut self, container: &Obj<'static>) -> Result<(), WidgetError>;
fn update(&mut self) -> Result<NavAction, WidgetError>;
fn on_event(&mut self, event: &Event) -> NavAction;
fn register_events(&mut self);
fn will_hide(&mut self);
fn did_show(&mut self);
}
impl<T: View> AnyView for T {
fn create(&mut self, container: &Obj<'static>) -> Result<(), WidgetError> {
View::create(self, container)
}
fn update(&mut self) -> Result<NavAction, WidgetError> {
View::update(self)
}
fn on_event(&mut self, event: &Event) -> NavAction {
View::on_event(self, event)
}
fn register_events(&mut self) {
View::register_events(self)
}
fn will_hide(&mut self) {
View::will_hide(self)
}
fn did_show(&mut self) {
View::did_show(self)
}
}
#[derive(Debug)]
pub enum NavigationError {
StackEmpty,
NoActiveModal,
CreateFailed(WidgetError),
}
impl core::fmt::Display for NavigationError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::StackEmpty => write!(f, "cannot pop the root view"),
Self::NoActiveModal => write!(f, "no active modal to dismiss"),
Self::CreateFailed(e) => write!(f, "view creation failed: {:?}", e),
}
}
}
struct SyncCell(UnsafeCell<Option<NavAction>>);
unsafe impl Sync for SyncCell {}
static PENDING_EVENT_ACTION: SyncCell = SyncCell(UnsafeCell::new(None));
pub(crate) fn take_pending_event_action() -> Option<NavAction> {
unsafe { (*PENDING_EVENT_ACTION.0.get()).take() }
}
pub fn register_view_events<V: View>(view: &mut V) {
view.register_events();
}
pub fn register_event_on<V: View>(view: &mut V, obj: *mut lv_obj_t) {
assert!(!obj.is_null(), "register_event_on: obj must not be null");
let view_ptr = view as *mut V as *mut c_void;
unsafe {
lv_obj_add_event_cb(
obj,
Some(view_event_trampoline::<V>),
EventCode::ALL.0,
view_ptr,
);
};
}
unsafe extern "C" fn view_event_trampoline<V: View>(e: *mut lv_event_t) {
if e.is_null() {
return;
}
unsafe {
let view = lv_event_get_user_data(e) as *mut V;
if !view.is_null() {
let event = Event::from_raw(e);
let action = (*view).on_event(&event);
if !action.is_none() {
let slot = &mut *PENDING_EVENT_ACTION.0.get();
if slot.is_none() {
*slot = Some(action);
}
}
}
}
}
pub async fn run_app<V: View, const BYTES: usize>(
w: i32,
h: i32,
bufs: &'static mut LvglBuffers<BYTES>,
mut view: V,
) -> ! {
info!("UI task started");
let driver = LvglDriver::init(w, h);
unsafe { lvgl_disp_init(w, h, bufs) };
DISPLAY_READY.wait().await;
info!("Display ready");
let screen_handle = unsafe { lv_screen_active() };
assert!(!screen_handle.is_null(), "no active screen after display init");
let container = Obj::from_raw_non_owning(screen_handle);
if let Err(e) = view.create(&container) {
warn!("Could not create LVGL widgets: {:?}, disabling UI", e);
loop {
Timer::after(Duration::from_secs(60)).await;
}
}
register_view_events(&mut view);
loop {
debug!("Rendering UI loop iteration");
let action = view.update()
.unwrap_or_else(|e| { warn!("Failed to update widgets: {:?}", e); NavAction::None });
debug_assert!(action.is_none(), "NavAction ignored in run_app — use run_app_nav for navigation");
for _ in 0..4 {
debug!("LVGL tick/timer handler");
driver.timer_handler();
Timer::after(Duration::from_millis(LVGL_TICK_MS)).await;
}
let _event_action = take_pending_event_action();
}
}
pub async fn run_app_nav<const BYTES: usize>(
w: i32,
h: i32,
bufs: &'static mut LvglBuffers<BYTES>,
initial: impl View,
) -> ! {
info!("UI task started (navigator)");
let driver = LvglDriver::init(w, h);
unsafe { lvgl_disp_init(w, h, bufs) };
DISPLAY_READY.wait().await;
info!("Display ready");
let mut nav = crate::navigator::Navigator::new();
nav.push_root(initial);
loop {
let action = nav
.active_view_mut()
.map(|v| v.update())
.unwrap_or(Ok(NavAction::None))
.unwrap_or_else(|e| {
warn!("view update: {:?}", e);
NavAction::None
});
let modal_action = nav
.active_modal_mut()
.map(|m| m.update())
.unwrap_or(Ok(NavAction::None))
.unwrap_or_else(|e| {
warn!("modal update: {:?}", e);
NavAction::None
});
for _ in 0..4 {
driver.timer_handler();
Timer::after(Duration::from_millis(LVGL_TICK_MS)).await;
}
let event_handled = nav.process_pending_event_action();
if !event_handled {
if !action.is_none() {
nav.process_action(action);
}
if !modal_action.is_none() {
nav.process_action(modal_action);
}
}
}
}