diff --git a/executables/file_manager/src/file_manager.rs b/executables/file_manager/src/file_manager.rs index 33149c7a..49e2a2e9 100644 --- a/executables/file_manager/src/file_manager.rs +++ b/executables/file_manager/src/file_manager.rs @@ -6,7 +6,6 @@ pub(crate) use alloc::{ vec::Vec, }; use core::ptr::null_mut; -use xila::file_system::{Kind, Path, PathOwned}; use xila::graphics::{ self, EventKind, Window, lvgl, palette::{self, Hue}, @@ -14,6 +13,10 @@ use xila::graphics::{ use xila::log; use xila::task; use xila::virtual_file_system::{Directory, get_instance}; +use xila::{ + file_system::{Kind, Path, PathOwned}, + graphics::symbols, +}; pub struct FileManager { window: Window, @@ -154,7 +157,7 @@ impl FileManager { return Err(Error::FailedToCreateObject); } let up_label = lvgl::lv_label_create(self.up_button); - lvgl::lv_label_set_text(up_label, lvgl::LV_SYMBOL_UP as *const _ as *const i8); + lvgl::lv_label_set_text(up_label, symbols::UP.as_ptr()); lvgl::lv_obj_center(up_label); // Remove event handler - events bubble up to window @@ -166,7 +169,7 @@ impl FileManager { } let home_label = lvgl::lv_label_create(self.home_button); - lvgl::lv_label_set_text(home_label, lvgl::LV_SYMBOL_HOME as *const _ as *const i8); + lvgl::lv_label_set_text(home_label, symbols::HOME.as_ptr()); lvgl::lv_obj_center(home_label); // Remove event handler - events bubble up to window @@ -179,10 +182,7 @@ impl FileManager { let refresh_label = lvgl::lv_label_create(self.refresh_button); - lvgl::lv_label_set_text( - refresh_label, - lvgl::LV_SYMBOL_REFRESH as *const _ as *const i8, - ); + lvgl::lv_label_set_text(refresh_label, symbols::REFRESH.as_ptr()); lvgl::lv_obj_center(refresh_label); // Remove event handler - events bubble up to window @@ -207,7 +207,7 @@ impl FileManager { } let go_label = lvgl::lv_label_create(self.go_button); - lvgl::lv_label_set_text(go_label, lvgl::LV_SYMBOL_RIGHT as *const _ as *const i8); + lvgl::lv_label_set_text(go_label, symbols::RIGHT.as_ptr()); lvgl::lv_obj_center(go_label); self.update_path_label(); @@ -284,8 +284,8 @@ impl FileManager { let file = &self.files[index]; let icon_symbol = match file.kind { - Kind::Directory => lvgl::LV_SYMBOL_DIRECTORY, - _ => lvgl::LV_SYMBOL_FILE, + Kind::Directory => symbols::DIRECTORY, + _ => symbols::FILE, }; let name_cstring = CString::new(file.name.clone()).unwrap(); diff --git a/executables/shell/graphical/src/layout.rs b/executables/shell/graphical/src/layout.rs index da5732c7..67bc03be 100644 --- a/executables/shell/graphical/src/layout.rs +++ b/executables/shell/graphical/src/layout.rs @@ -1,7 +1,7 @@ use crate::error::{Error, Result}; use alloc::{format, string::String}; use core::ptr::null_mut; -use xila::graphics::{self, EventKind, lvgl, theme}; +use xila::graphics::{self, EventKind, lvgl, symbols, theme}; use xila::shared::unix_to_human_time; use xila::time; @@ -223,7 +223,7 @@ impl Layout { return Err(Error::FailedToCreateObject); } - lvgl::lv_label_set_text(wi_fi, lvgl::LV_SYMBOL_WIFI as *const u8 as *const i8); + lvgl::lv_label_set_text(wi_fi, symbols::WIFI.as_ptr()); wi_fi }; @@ -237,10 +237,7 @@ impl Layout { return Err(Error::FailedToCreateObject); } - lvgl::lv_label_set_text( - battery, - lvgl::LV_SYMBOL_BATTERY_3 as *const u8 as *const i8, - ); + lvgl::lv_label_set_text(battery, symbols::BATTERY_3.as_ptr()); battery }; diff --git a/modules/graphics/src/lib.rs b/modules/graphics/src/lib.rs index f54cd490..578d1474 100644 --- a/modules/graphics/src/lib.rs +++ b/modules/graphics/src/lib.rs @@ -14,6 +14,7 @@ pub mod macros; mod manager; mod point; mod screen; +pub mod symbols; mod window; pub mod lvgl; diff --git a/modules/graphics/src/lvgl.rs b/modules/graphics/src/lvgl.rs index 65f78f56..751ac1a8 100644 --- a/modules/graphics/src/lvgl.rs +++ b/modules/graphics/src/lvgl.rs @@ -1,3 +1,5 @@ +use core::ffi::c_char; + pub use lvgl_rust_sys::*; use crate::Point; @@ -62,3 +64,33 @@ pub unsafe fn lv_obj_get_size(object: *mut lv_obj_t) -> Point { Point::new(width, height) } } + +/// Add a tab to a tabview and adjust the tab button size +/// +/// # Arguments +/// +/// * `tabview` - The tabview to add the tab to. +/// * `name` - The name of the tab. +/// +/// # Safety +/// +/// This function is unsafe because it may dereference raw pointers (e.g. `tabview`). +pub unsafe fn lv_tabview_add_tab(tabview: *mut lv_obj_t, name: *const c_char) -> *mut lv_obj_t { + unsafe { + let page = lvgl_rust_sys::lv_tabview_add_tab(tabview, name); + + let bar = lv_tabview_get_tab_bar(tabview); + + lv_obj_set_size(bar, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + + // get latest tab button + let tab_count = lv_obj_get_child_count(bar); + let button = lv_obj_get_child(bar, (tab_count - 1) as _); + + // don't make it grow + lv_obj_set_flex_grow(button, 0); + lv_obj_set_size(button, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + + page + } +} diff --git a/modules/graphics/src/symbols.rs b/modules/graphics/src/symbols.rs new file mode 100644 index 00000000..0242f0da --- /dev/null +++ b/modules/graphics/src/symbols.rs @@ -0,0 +1,86 @@ +use core::ffi::CStr; + +use crate::lvgl; + +// LVGL symbols +pub const BULLET: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_BULLET) }; +pub const AUDIO: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_AUDIO) }; +pub const VIDEO: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_VIDEO) }; +pub const LIST: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_LIST) }; +pub const OK: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_OK) }; +pub const CLOSE: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_CLOSE) }; +pub const POWER: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_POWER) }; +pub const SETTINGS: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_SETTINGS) }; +pub const HOME: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_HOME) }; +pub const DOWNLOAD: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_DOWNLOAD) }; +pub const DRIVE: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_DRIVE) }; +pub const REFRESH: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_REFRESH) }; +pub const MUTE: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_MUTE) }; +pub const VOLUME_MID: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_VOLUME_MID) }; +pub const VOLUME_MAX: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_VOLUME_MAX) }; +pub const IMAGE: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_IMAGE) }; +pub const TINT: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_TINT) }; +pub const PREV: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_PREV) }; +pub const PLAY: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_PLAY) }; +pub const PAUSE: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_PAUSE) }; +pub const STOP: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_STOP) }; +pub const NEXT: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_NEXT) }; +pub const EJECT: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_EJECT) }; +pub const LEFT: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_LEFT) }; +pub const RIGHT: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_RIGHT) }; +pub const PLUS: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_PLUS) }; +pub const MINUS: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_MINUS) }; +pub const EYE_OPEN: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_EYE_OPEN) }; +pub const EYE_CLOSE: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_EYE_CLOSE) }; +pub const WARNING: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_WARNING) }; +pub const SHUFFLE: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_SHUFFLE) }; +pub const UP: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_UP) }; +pub const DOWN: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_DOWN) }; +pub const LOOP: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_LOOP) }; +pub const DIRECTORY: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_DIRECTORY) }; +pub const UPLOAD: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_UPLOAD) }; +pub const CALL: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_CALL) }; +pub const CUT: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_CUT) }; +pub const COPY: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_COPY) }; +pub const SAVE: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_SAVE) }; +pub const BARS: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_BARS) }; +pub const ENVELOPE: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_ENVELOPE) }; +pub const CHARGE: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_CHARGE) }; +pub const PASTE: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_PASTE) }; +pub const BELL: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_BELL) }; +pub const KEYBOARD: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_KEYBOARD) }; +pub const GPS: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_GPS) }; +pub const FILE: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_FILE) }; +pub const WIFI: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_WIFI) }; +pub const BATTERY_FULL: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_BATTERY_FULL) }; +pub const BATTERY_3: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_BATTERY_3) }; +pub const BATTERY_2: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_BATTERY_2) }; +pub const BATTERY_1: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_BATTERY_1) }; +pub const BATTERY_EMPTY: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_BATTERY_EMPTY) }; +pub const USB: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_USB) }; +pub const BLUETOOTH: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_BLUETOOTH) }; +pub const TRASH: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_TRASH) }; +pub const EDIT: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_EDIT) }; +pub const BACKSPACE: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_BACKSPACE) }; +pub const SD_CARD: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_SD_CARD) }; +pub const NEW_LINE: &CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_NEW_LINE) }; +pub const DUMMY: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(lvgl::LV_SYMBOL_DUMMY) }; +// Additional symbols +pub const NETWORK_WIRED: &CStr = c"\xEF\x9B\xBF"; diff --git a/modules/graphics/src/theme.rs b/modules/graphics/src/theme.rs index 634b7325..1ba20117 100644 --- a/modules/graphics/src/theme.rs +++ b/modules/graphics/src/theme.rs @@ -9,6 +9,10 @@ pub const BORDER_COLOR_PRIMARY: Color = Color::new(0x27, 0x27, 0x2a); pub const SECONDARY_COLOR: Color = palette::get(palette::Hue::Red, palette::Tone::MAIN); pub const IS_DARK: bool = true; +pub const BORDER_WIDTH: i32 = 2; +pub const RADIUS: i32 = BORDER_WIDTH * 4; +pub const PADDING: i32 = 16; + /// Rust representation of LVGL's `lv_theme_t` structure /// /// This struct is C FFI compatible and must match the memory layout of the C struct. @@ -138,17 +142,64 @@ pub unsafe extern "C" fn theme_apply(_: *mut lvgl::lv_theme_t, object: *mut lvgl && lvgl::lv_obj_get_child(tab_view, 0) == parent && lvgl::lv_obj_check_type(tab_view, &lvgl::lv_tabview_class) { - lvgl::lv_obj_set_style_bg_color( + lvgl::lv_obj_set_style_pad_all(object, BORDER_WIDTH * 4, lvgl::LV_PART_MAIN); + lvgl::lv_obj_set_style_radius(object, RADIUS, lvgl::LV_PART_MAIN); + + lvgl::lv_obj_set_style_border_side( + object, + lvgl::lv_border_side_t_LV_BORDER_SIDE_FULL, + lvgl::LV_STATE_CHECKED, + ); + lvgl::lv_obj_set_style_border_side( + object, + lvgl::lv_border_side_t_LV_BORDER_SIDE_FULL, + lvgl::LV_PART_MAIN, + ); + lvgl::lv_obj_set_style_border_color( + object, + PRIMARY_COLOR.into_lvgl_color(), + lvgl::LV_STATE_CHECKED, + ); + lvgl::lv_obj_set_style_border_color( object, - BACKGROUND_COLOR_PRIMARY.into_lvgl_color(), + PRIMARY_COLOR.into_lvgl_color(), lvgl::LV_PART_MAIN, ); + lvgl::lv_obj_set_style_border_width(object, BORDER_WIDTH, lvgl::LV_PART_MAIN); + lvgl::lv_obj_set_style_border_width(object, BORDER_WIDTH, lvgl::LV_STATE_CHECKED); + lvgl::lv_obj_set_style_border_opa( + object, + lvgl::LV_OPA_TRANSP as _, + lvgl::LV_PART_MAIN, + ); + lvgl::lv_obj_set_style_border_opa( + object, + lvgl::LV_OPA_COVER as _, + lvgl::LV_STATE_CHECKED, + ); + + lvgl::lv_obj_set_style_bg_opa(object, lvgl::LV_OPA_TRANSP as _, lvgl::LV_PART_MAIN); + lvgl::lv_obj_set_style_text_color( object, PRIMARY_COLOR.into_lvgl_color(), lvgl::LV_PART_MAIN, ); + + lvgl::lv_obj_set_style_bg_color( + parent, + BACKGROUND_COLOR_PRIMARY_MUTED.into_lvgl_color(), + lvgl::LV_PART_MAIN, + ); + lvgl::lv_obj_set_style_radius(parent, RADIUS, lvgl::LV_PART_MAIN); + lvgl::lv_obj_set_style_pad_all(parent, BORDER_WIDTH * 2, lvgl::LV_PART_MAIN); + lvgl::lv_obj_set_flex_align( + parent, + lvgl::lv_flex_align_t_LV_FLEX_ALIGN_CENTER, + lvgl::lv_flex_align_t_LV_FLEX_ALIGN_CENTER, + lvgl::lv_flex_align_t_LV_FLEX_ALIGN_CENTER, + ); } } else if class == &lvgl::lv_buttonmatrix_class { apply_default_style(object, lvgl::LV_PART_MAIN); @@ -159,12 +210,44 @@ pub unsafe extern "C" fn theme_apply(_: *mut lvgl::lv_theme_t, object: *mut lvgl lvgl::LV_PART_MAIN, ); - lvgl::lv_obj_set_style_border_width(object, 2, lvgl::LV_PART_ITEMS); + lvgl::lv_obj_set_style_border_width(object, BORDER_WIDTH, lvgl::LV_PART_ITEMS); lvgl::lv_obj_set_style_text_color( object, PRIMARY_COLOR.into_lvgl_color(), lvgl::LV_PART_ITEMS, ); + } else if class == &lvgl::lv_checkbox_class { + apply_default_style(object, lvgl::LV_PART_MAIN); + apply_default_style(object, lvgl::LV_PART_INDICATOR); + lvgl::lv_obj_set_style_border_color( + object, + PRIMARY_COLOR.into_lvgl_color(), + lvgl::LV_PART_INDICATOR | lvgl::LV_STATE_CHECKED, + ); + lvgl::lv_obj_set_style_text_color( + object, + BACKGROUND_COLOR_PRIMARY.into_lvgl_color(), + lvgl::LV_PART_INDICATOR | lvgl::LV_STATE_CHECKED, + ); + } else if class == &lvgl::lv_list_text_class { + apply_default_style(object, lvgl::LV_PART_MAIN); + lvgl::lv_obj_set_style_pad_left(object, PADDING, lvgl::LV_PART_MAIN); + lvgl::lv_obj_set_style_bg_color( + object, + BACKGROUND_COLOR_PRIMARY_MUTED.into_lvgl_color(), + lvgl::LV_PART_MAIN, + ); + } else if class == &lvgl::lv_list_button_class { + apply_default_style(object, lvgl::LV_PART_MAIN); + lvgl::lv_obj_set_style_pad_left(object, 2 * PADDING, lvgl::LV_PART_MAIN); + } else if class == &lvgl::lv_switch_class { + apply_default_style(object, lvgl::LV_PART_MAIN); + apply_default_style(object, lvgl::LV_PART_INDICATOR); + lvgl::lv_obj_set_style_bg_color( + object, + BACKGROUND_COLOR_PRIMARY.into_lvgl_color(), + lvgl::LV_PART_KNOB | lvgl::LV_STATE_CHECKED, + ); } else { apply_default_style(object, lvgl::LV_PART_MAIN); } diff --git a/modules/task/src/manager/tests.rs b/modules/task/src/manager/tests.rs index 40eb30d1..4979cd27 100644 --- a/modules/task/src/manager/tests.rs +++ b/modules/task/src/manager/tests.rs @@ -448,11 +448,6 @@ async fn test_get_children() { let root_task = manager.get_current_task_identifier().await; let spawner = manager.get_spawner(root_task).await.unwrap(); - // Initially, root task should have no children - let initial_children = manager.get_children(root_task).await.unwrap(); - let initial_count = initial_children.len(); - assert_eq!(initial_count, 0); - // Spawn first child let (child1_handle, child1_task) = manager .spawn(root_task, "Child Task 1", Some(spawner), async move |_| { @@ -480,7 +475,8 @@ async fn test_get_children() { // After children complete, they should no longer be in the children list let final_children = manager.get_children(root_task).await.unwrap(); - assert_eq!(final_children.len(), initial_count); + assert!(!final_children.contains(&child1_task)); + assert!(!final_children.contains(&child2_task)); } #[ignore]