diff --git a/crates/alloc/src/lib.rs b/crates/alloc/src/lib.rs index e7934422..e9752753 100644 --- a/crates/alloc/src/lib.rs +++ b/crates/alloc/src/lib.rs @@ -4,8 +4,14 @@ #![no_std] #![deny(rustdoc::broken_intra_doc_links)] -use core::alloc::{GlobalAlloc, Layout}; -use core::ffi::c_void; +use core::{ + alloc::{GlobalAlloc, Layout}, + ffi::c_void, +}; + +extern crate alloc; +// re-export all items from `alloc` so that the API user can only extern this crate +pub use alloc::*; use flipperzero_sys as sys; diff --git a/crates/build-examples.py b/crates/build-examples.py index 4705bbcb..6cba6f10 100755 --- a/crates/build-examples.py +++ b/crates/build-examples.py @@ -10,7 +10,7 @@ PYTHON = 'python' TOOLS_PATH = '../tools' INSTALL_PATH = PurePosixPath('/ext/apps/Examples') -EXAMPLES = ["dialog", "example_images", "gpio", "gui", "hello-rust", "notification", "storage"] +EXAMPLES = ["dialog", "images", "gpio", "gui", "hello-rust", "notification", "storage"] def parse_args(): @@ -27,7 +27,10 @@ def main(): for example in EXAMPLES: logging.info('Building %s', example) - run(['cargo', 'build', '--package', 'flipperzero', '--example', example, '--all-features', '--release'], check=True) + run( + ['cargo', 'build', '--package', 'flipperzero', '--example', example, '--all-features', '--release'], + check=True + ) if args.install: # Assume that the binary has the name as the @@ -35,7 +38,11 @@ def main(): target = INSTALL_PATH / f'{example}.fap' logging.info('Copying %s to %s', binary, target) - run(['cargo', 'run', '--release', '--bin', 'storage', '--', 'send', os.fspath(binary), os.fspath(target)], cwd=TOOLS_PATH, check=True) + run( + ['cargo', 'run', '--release', '--bin', 'storage', '--', 'send', os.fspath(binary), os.fspath(target)], + cwd=TOOLS_PATH, + check=True + ) if __name__ == '__main__': diff --git a/crates/flipperzero/Cargo.toml b/crates/flipperzero/Cargo.toml index e92ee59c..541f93fd 100644 --- a/crates/flipperzero/Cargo.toml +++ b/crates/flipperzero/Cargo.toml @@ -23,6 +23,8 @@ harness = false [dependencies] flipperzero-sys.workspace = true flipperzero-test.workspace = true + +# Formatting ufmt.workspace = true # HAL wrappers @@ -66,17 +68,88 @@ sha2 = { version = "0.10", default-features = false } ## ``` alloc = [] +#! ## Service features. +#! +#! These enables specific Flipper Service APIs. + +## enables all Service APIs +all-services = [ + "service-storage", + "service-notification", + "service-input", + "service-gui", + "service-dialogs", + "service-dolphin", +] +## Enables Storage Service APIs. +service-storage = [] +## Enables Notification Service APIs. +service-notification = [] +## Enables Input Service APIs. +service-input = [] +## Enables GUI APIs of Flipper. +service-gui = ["alloc", "service-input"] +## Enables Dialogs APIs of Flipper. +service-dialogs = ["service-gui"] +## Enables Dolphin APIs of Flipper. +service-dolphin = [] + +#! ## Utilities +#! +#! These APIs provide extra functionality required for specific use-cases. + +## Enables APIs for working with XBM-images. +xbm = [] + +#! ## Unstable features +#! +#! This features are unstable and are yet a subject to drastic changes. +#! They may provide experimental APIs, nightly-only features and optimizations etc. + +## Enables unstable Rust intrinsics. This requires `nightly` compiler version. +unstable_intrinsics = [] +## Enables unstable Rust lints on types provided by this crate. +## This requires `nightly` compiler version. +unstable_lints = [] + +# Internal features. + +# Enables unstable documentation features. +# This is only intended to be used by `cargo doc`. +unstable_docs = [] + [[test]] name = "dolphin" +required-features = ["service-dolphin"] harness = false [[test]] name = "string" harness = false +[[example]] +name = "storage" +required-features = ["service-storage"] + +[[example]] +name = "notification" +required-features = ["service-notification"] + +[[example]] +name = "gui" +required-features = ["service-gui", "xbm"] + +[[example]] +name = "images" +required-features = ["service-gui"] + +[[example]] +name = "view_dispatcher" +required-features = ["service-gui"] + [[example]] name = "dialog" -required-features = ["alloc"] +required-features = ["service-dialogs", "service-input"] [[example]] name = "threads" diff --git a/crates/flipperzero/examples/example_images.rs b/crates/flipperzero/examples/example_images.rs deleted file mode 100644 index 1b3379f0..00000000 --- a/crates/flipperzero/examples/example_images.rs +++ /dev/null @@ -1,125 +0,0 @@ -//! Example Images application. -//! See https://github.com/flipperdevices/flipperzero-firmware/blob/dev/applications/examples/example_images/example_images.c - -#![no_std] -#![no_main] - -use core::ffi::{c_char, c_void}; -use core::mem::{self, MaybeUninit}; - -use flipperzero_rt as rt; -use flipperzero_sys as sys; - -// Required for allocator -extern crate flipperzero_alloc; - -rt::manifest!(name = "Example: Images"); -rt::entry!(main); - -const RECORD_GUI: *const c_char = sys::c_string!("gui"); - -static mut TARGET_ICON: Icon = Icon { - width: 48, - height: 32, - frame_count: 1, - frame_rate: 0, - frames: unsafe { TARGET_FRAMES.as_ptr() }, -}; -static mut TARGET_FRAMES: [*const u8; 1] = [include_bytes!("icons/rustacean-48x32.icon").as_ptr()]; - -static mut IMAGE_POSITION: ImagePosition = ImagePosition { x: 0, y: 0 }; - -#[repr(C)] -struct ImagePosition { - pub x: u8, - pub y: u8, -} - -/// Internal icon representation. -#[repr(C)] -struct Icon { - width: u8, - height: u8, - frame_count: u8, - frame_rate: u8, - frames: *const *const u8, -} - -// Screen is 128x64 px -extern "C" fn app_draw_callback(canvas: *mut sys::Canvas, _ctx: *mut c_void) { - unsafe { - sys::canvas_clear(canvas); - sys::canvas_draw_icon( - canvas, - IMAGE_POSITION.x % 128, - IMAGE_POSITION.y % 128, - &TARGET_ICON as *const Icon as *const c_void as *const sys::Icon, - ); - } -} - -extern "C" fn app_input_callback(input_event: *mut sys::InputEvent, ctx: *mut c_void) { - unsafe { - let event_queue = ctx as *mut sys::FuriMessageQueue; - sys::furi_message_queue_put(event_queue, input_event as *mut c_void, 0); - } -} - -fn main(_args: *mut u8) -> i32 { - unsafe { - let event_queue = sys::furi_message_queue_alloc(8, mem::size_of::() as u32) - as *mut sys::FuriMessageQueue; - - // Configure view port - let view_port = sys::view_port_alloc(); - sys::view_port_draw_callback_set( - view_port, - Some(app_draw_callback), - view_port as *mut c_void, - ); - sys::view_port_input_callback_set( - view_port, - Some(app_input_callback), - event_queue as *mut c_void, - ); - - // Register view port in GUI - let gui = sys::furi_record_open(RECORD_GUI) as *mut sys::Gui; - sys::gui_add_view_port(gui, view_port, sys::GuiLayer_GuiLayerFullscreen); - - let mut event: MaybeUninit = MaybeUninit::uninit(); - - let mut running = true; - while running { - if sys::furi_message_queue_get( - event_queue, - event.as_mut_ptr() as *mut sys::InputEvent as *mut c_void, - 100, - ) == sys::FuriStatus_FuriStatusOk - { - let event = event.assume_init(); - if event.type_ == sys::InputType_InputTypePress - || event.type_ == sys::InputType_InputTypeRepeat - { - match event.key { - sys::InputKey_InputKeyLeft => IMAGE_POSITION.x -= 2, - sys::InputKey_InputKeyRight => IMAGE_POSITION.x += 2, - sys::InputKey_InputKeyUp => IMAGE_POSITION.y -= 2, - sys::InputKey_InputKeyDown => IMAGE_POSITION.y += 2, - _ => running = false, - } - } - } - sys::view_port_update(view_port); - } - - sys::view_port_enabled_set(view_port, false); - sys::gui_remove_view_port(gui, view_port); - sys::view_port_free(view_port); - sys::furi_message_queue_free(event_queue); - - sys::furi_record_close(RECORD_GUI); - } - - 0 -} diff --git a/crates/flipperzero/examples/gui.rs b/crates/flipperzero/examples/gui.rs index 7c7a77c2..9080d8a4 100644 --- a/crates/flipperzero/examples/gui.rs +++ b/crates/flipperzero/examples/gui.rs @@ -6,53 +6,129 @@ #![no_main] #![no_std] +#![forbid(unsafe_code)] + +mod xbm_images; // Required for panic handler extern crate flipperzero_rt; // Required for allocator -#[cfg(feature = "alloc")] -extern crate flipperzero_alloc; - -use core::ffi::{c_char, c_void}; -use core::ptr; -use core::time::Duration; +extern crate flipperzero_alloc as alloc; -use flipperzero::furi::thread::sleep; +use alloc::{ffi::CString, string::ToString}; +use core::{ffi::CStr, time::Duration}; +use flipperzero::{ + furi::message_queue::MessageQueue, + gui::{ + canvas::CanvasView, + view_port::{ViewPort, ViewPortCallbacks}, + Gui, GuiLayer, + }, + input::{InputEvent, InputKey, InputType}, + println, + xbm::{ByteArray, XbmImage}, +}; use flipperzero_rt::{entry, manifest}; -use flipperzero_sys as sys; - -// GUI record -const RECORD_GUI: *const c_char = sys::c_string!("gui"); -const FULLSCREEN: sys::GuiLayer = sys::GuiLayer_GuiLayerFullscreen; +use flipperzero_sys::furi::Status; manifest!(name = "Rust GUI example"); entry!(main); -/// View draw handler. -pub unsafe extern "C" fn draw_callback(canvas: *mut sys::Canvas, _context: *mut c_void) { - unsafe { - sys::canvas_draw_str(canvas, 39, 31, sys::c_string!("Hello, Rust!")); - } -} +/// An image of an 8x8 plus. +/// +/// It is important to note that byte bits are read in reverse order +/// but since this image is symmetric we don't need to reverse the bytes +/// unlike in [`RS_IMAGE`]. +const PLUS_IMAGE: XbmImage> = XbmImage::new_from_array::<8, 8>([ + 0b00_11_11_00, + 0b00_11_11_00, + 0b11_11_11_11, + 0b11_11_11_11, + 0b11_11_11_11, + 0b11_11_11_11, + 0b00_11_11_00, + 0b10_11_11_01, +]); + +/// An image of an 8x8 R and S letters. +const RS_IMAGE: XbmImage> = XbmImage::new_from_array::<8, 8>([ + 0b11100000u8.reverse_bits(), + 0b10010000u8.reverse_bits(), + 0b11100000u8.reverse_bits(), + 0b10100110u8.reverse_bits(), + 0b10011000u8.reverse_bits(), + 0b00000110u8.reverse_bits(), + 0b00000001u8.reverse_bits(), + 0b00000110u8.reverse_bits(), +]); fn main(_args: *mut u8) -> i32 { - // Currently there is no high level GUI bindings, - // so this all has to be done using the `sys` bindings. - unsafe { - let view_port = sys::view_port_alloc(); - sys::view_port_draw_callback_set(view_port, Some(draw_callback), ptr::null_mut()); + let exit_event_queue = MessageQueue::new(32); - let gui = sys::furi_record_open(RECORD_GUI) as *mut sys::Gui; - sys::gui_add_view_port(gui, view_port, FULLSCREEN); + struct State<'a> { + text: &'a CStr, + exit_event_queue: &'a MessageQueue<()>, + counter: u8, + } - sleep(Duration::from_secs(1)); + impl ViewPortCallbacks for State<'_> { + fn on_draw(&mut self, mut canvas: CanvasView) { + canvas.draw_xbm(2, 2, &PLUS_IMAGE); + canvas.draw_str(10, 31, self.text); + let bottom_text = CString::new(self.counter.to_string().as_bytes()) + .expect("should be a valid string"); + canvas.draw_str(80, 10, bottom_text); + canvas.draw_xbm(100, 50, &RS_IMAGE); + canvas.draw_xbm(0, 32, &xbm_images::ferris::IMAGE); + } - sys::view_port_enabled_set(view_port, false); - sys::gui_remove_view_port(gui, view_port); - sys::furi_record_close(RECORD_GUI); - sys::view_port_free(view_port); + fn on_input(&mut self, event: InputEvent) { + if event.r#type == InputType::Press { + match event.key { + InputKey::Up => { + self.counter = (self.counter + 1) % 10; + } + InputKey::Down => { + self.counter = if self.counter == 0 { + 10 + } else { + self.counter - 1 + }; + } + InputKey::Back => { + self.exit_event_queue + .put((), Duration::MAX) + .expect("failed to put event into the queue"); + } + _ => {} + } + } + } } + let view_port = ViewPort::new(State { + text: CStr::from_bytes_with_nul(b"Hi there!\0").expect("correct string"), + exit_event_queue: &exit_event_queue, + counter: 0, + }); + + let mut gui = Gui::new(); + let mut view_port = gui.add_view_port(view_port, GuiLayer::Fullscreen); + + let status = loop { + match exit_event_queue.get(Duration::from_millis(100)) { + Ok(()) => { + println!("Exit pressed"); + break 0; + } + Err(Status::ERR_TIMEOUT) => {} // it's okay to continue polling + Err(e) => { + println!("ERROR while receiving event: {:?}", e); + break 1; + } + } + }; + view_port.view_port_mut().set_enabled(false); - 0 + status } diff --git a/crates/flipperzero/examples/images.rs b/crates/flipperzero/examples/images.rs new file mode 100644 index 00000000..2343ac36 --- /dev/null +++ b/crates/flipperzero/examples/images.rs @@ -0,0 +1,118 @@ +//! Example Images application. +//! See + +#![no_std] +#![no_main] + +use core::time::Duration; +use flipperzero::{ + furi::message_queue::MessageQueue, + gui::{ + canvas::CanvasView, + icon::Icon, + view_port::{ViewPort, ViewPortCallbacks}, + Gui, GuiLayer, + }, + input::{InputEvent, InputKey, InputType}, +}; + +extern crate flipperzero_alloc; + +use flipperzero_rt as rt; +use flipperzero_sys as sys; + +rt::manifest!(name = "Example: Images"); +rt::entry!(main); + +// NOTE: `*mut`s are required to enforce `unsafe` since there are raw pointers involved +static mut TARGET_FRAMES: [*const u8; 1] = [include_bytes!("icons/rustacean-48x32.icon").as_ptr()]; +static mut SYS_ICON: sys::Icon = sys::Icon { + width: 48, + height: 32, + frame_count: 1, + frame_rate: 0, + frames: unsafe { TARGET_FRAMES.as_ptr() }, +}; + +#[repr(C)] +struct ImagePosition { + pub x: u8, + pub y: u8, +} + +fn main(_args: *mut u8) -> i32 { + // SAFETY: `Icon` is a read-only; + // there will be a safe API for this in this future + let icon = unsafe { Icon::from_raw(&SYS_ICON as *const _ as *mut _) }; + + // Configure view port + struct State<'a> { + exit_queue: &'a MessageQueue<()>, + image_position: ImagePosition, + target_icon: &'a Icon, + hidden: bool, + } + + impl ViewPortCallbacks for State<'_> { + fn on_draw(&mut self, mut canvas: CanvasView) { + canvas.clear(); + if !self.hidden { + // Screen is 128x64 px + canvas.draw_icon( + self.image_position.x % 128, + self.image_position.y % 64, + self.target_icon, + ); + } + } + + fn on_input(&mut self, event: InputEvent) { + if matches!(event.r#type, InputType::Press | InputType::Repeat) { + match event.key { + InputKey::Left => { + self.image_position.x = self.image_position.x.saturating_sub(2) + } + InputKey::Right => { + self.image_position.x = self.image_position.x.saturating_add(2) + } + InputKey::Up => self.image_position.y = self.image_position.y.saturating_sub(2), + InputKey::Down => { + self.image_position.y = self.image_position.y.saturating_add(2) + } + // to be a bit more creative than the original example + // we make `Ok` button (un)hide the canvas + InputKey::Ok => self.hidden = !self.hidden, + _ => { + let _ = self.exit_queue.put_now(()); + } + } + } + } + } + + // The original example has all `InputEvent`s transferred via `MessageQueue` + // While this is possible, there is no need for this + // since we do all the handling in `on_input_event` + // thus we only have to send a single object indicating shutdown + let exit_queue = MessageQueue::new(1); + let view_port = ViewPort::new(State { + exit_queue: &exit_queue, + image_position: ImagePosition { x: 0, y: 0 }, + target_icon: &icon, + hidden: false, + }); + + // Register view port in GUI + let mut gui = Gui::new(); + let mut view_port = gui.add_view_port(view_port, GuiLayer::Fullscreen); + + let mut running = true; + while running { + if exit_queue.get(Duration::from_millis(100)).is_ok() { + running = false + } + view_port.update(); + } + + 0 +} diff --git a/crates/flipperzero/examples/threads.rs b/crates/flipperzero/examples/threads.rs index 8879476d..a7ba6403 100644 --- a/crates/flipperzero/examples/threads.rs +++ b/crates/flipperzero/examples/threads.rs @@ -8,9 +8,7 @@ extern crate flipperzero_rt; // Required for allocator -extern crate flipperzero_alloc; - -extern crate alloc; +extern crate flipperzero_alloc as alloc; use alloc::borrow::ToOwned; use core::time::Duration; diff --git a/crates/flipperzero/examples/view_dispatcher.rs b/crates/flipperzero/examples/view_dispatcher.rs index 3a0d8f41..3509d16a 100644 --- a/crates/flipperzero/examples/view_dispatcher.rs +++ b/crates/flipperzero/examples/view_dispatcher.rs @@ -5,18 +5,17 @@ #![no_main] #![no_std] -extern crate alloc; -extern crate flipperzero_alloc; +extern crate flipperzero_alloc as alloc; extern crate flipperzero_rt; use alloc::boxed::Box; use core::ffi::{c_char, c_void, CStr}; use core::ptr::NonNull; use flipperzero::furi::string::FuriString; +use flipperzero::gui::Gui; use flipperzero_rt::{entry, manifest}; use flipperzero_sys as sys; - -const RECORD_GUI: *const c_char = sys::c_string!("gui"); +use flipperzero_sys::ViewDispatcher; manifest!(name = "Rust ViewDispatcher example"); entry!(main); @@ -28,7 +27,7 @@ enum AppView { struct App { name: [c_char; 16], - view_dispatcher: NonNull, + view_dispatcher: NonNull, widget: NonNull, text_input: NonNull, } @@ -71,7 +70,7 @@ pub unsafe extern "C" fn text_input_callback(context: *mut c_void) { } pub unsafe extern "C" fn navigation_event_callback(context: *mut c_void) -> bool { - let view_dispatcher = context as *mut sys::ViewDispatcher; + let view_dispatcher = context as *mut ViewDispatcher; sys::view_dispatcher_stop(view_dispatcher); sys::view_dispatcher_remove_view(view_dispatcher, AppView::Widget as u32); sys::view_dispatcher_remove_view(view_dispatcher, AppView::TextInput as u32); @@ -103,11 +102,11 @@ fn main(_args: *mut u8) -> i32 { ); } + let gui = Gui::new(); unsafe { - let gui = sys::furi_record_open(RECORD_GUI) as *mut sys::Gui; sys::view_dispatcher_attach_to_gui( app.view_dispatcher.as_ptr(), - gui, + gui.as_raw(), sys::ViewDispatcherType_ViewDispatcherTypeFullscreen, ); diff --git a/crates/flipperzero/examples/xbm_images/ferris.rs b/crates/flipperzero/examples/xbm_images/ferris.rs new file mode 100644 index 00000000..b18e6c79 --- /dev/null +++ b/crates/flipperzero/examples/xbm_images/ferris.rs @@ -0,0 +1,24 @@ +use flipperzero::xbm::{ByteArray, XbmImage}; + +pub const IMAGE: XbmImage> = flipperzero::xbm! { + #define ferris_width 48 + #define ferris_height 32 + static char ferris_bits[] = { + 0xFF, 0xFF, 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0xC0, 0xFF, 0xFF, + 0xFF, 0x3F, 0x00, 0x40, 0xFC, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0xFE, 0xFF, + 0xFF, 0x07, 0x00, 0x00, 0xD0, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0xFF, + 0xFF, 0x02, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x7F, 0x00, 0x00, 0x00, 0x10, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFE, + 0x0F, 0x00, 0x00, 0x00, 0x80, 0xF8, 0x0F, 0x00, 0x00, 0x00, 0x00, 0xF9, + 0x5F, 0x44, 0x41, 0x00, 0x10, 0xF8, 0x0F, 0x00, 0x00, 0x08, 0x8A, 0xFC, + 0x07, 0x00, 0x85, 0x01, 0x00, 0xC0, 0x1B, 0x22, 0x20, 0x50, 0x24, 0xF5, + 0x03, 0x44, 0xE0, 0x60, 0x21, 0xC5, 0x27, 0x11, 0xE2, 0x41, 0x44, 0xF4, + 0x93, 0x44, 0xF1, 0xFC, 0x31, 0xD0, 0x0B, 0x21, 0xFC, 0xFE, 0x8C, 0xEB, + 0x29, 0x44, 0xFE, 0xFE, 0x42, 0x4A, 0xC1, 0x95, 0x30, 0xB8, 0x39, 0x26, + 0xE6, 0x53, 0xBF, 0xF9, 0xA1, 0x9F, 0x98, 0xAB, 0x7B, 0x8E, 0x5E, 0x63, + 0x93, 0x53, 0xD5, 0x87, 0xD7, 0xEB, 0xAF, 0x6F, 0xA2, 0xB7, 0xE9, 0xF5, + 0x6F, 0xDF, 0xD9, 0x6E, 0xFE, 0xC7, 0x4F, 0xAE, 0xF7, 0xD7, 0xED, 0xF8, + 0x7F, 0xBD, 0x3C, 0xBF, 0xFF, 0xF5, 0xBF, 0xBC, 0xE7, 0xAC, 0x3F, 0xF6, + 0xFF, 0xFE, 0xE7, 0xFF, 0xFF, 0xFB, 0xFF, 0xF9, 0xFC, 0xFF, 0xFF, 0xFF, + }; +}; diff --git a/crates/flipperzero/examples/xbm_images/mod.rs b/crates/flipperzero/examples/xbm_images/mod.rs new file mode 100644 index 00000000..057a22c6 --- /dev/null +++ b/crates/flipperzero/examples/xbm_images/mod.rs @@ -0,0 +1 @@ +pub mod ferris; diff --git a/crates/flipperzero/src/dialogs/mod.rs b/crates/flipperzero/src/dialogs/mod.rs index a3153cd7..fb0274da 100644 --- a/crates/flipperzero/src/dialogs/mod.rs +++ b/crates/flipperzero/src/dialogs/mod.rs @@ -51,7 +51,7 @@ impl DialogsApp { /// Displays a message. pub fn show_message(&mut self, message: &DialogMessage) -> DialogMessageButton { let button_sys = - unsafe { sys::dialog_message_show(self.data.as_ptr(), message.data.as_ptr()) }; + unsafe { sys::dialog_message_show(self.data.as_raw(), message.data.as_ptr()) }; DialogMessageButton::from_sys(button_sys).expect("Invalid button") } @@ -94,6 +94,10 @@ impl<'a> DialogMessage<'a> { } } + pub fn as_raw(&self) -> *mut sys::DialogMessage { + self.data.as_ptr() + } + /// Sets the labels of the buttons. pub fn set_buttons( &mut self, @@ -125,8 +129,8 @@ impl<'a> DialogMessage<'a> { header.as_ptr(), x, y, - horizontal.to_sys(), - vertical.to_sys(), + horizontal.into(), + vertical.into(), ); } } @@ -139,8 +143,8 @@ impl<'a> DialogMessage<'a> { text.as_ptr(), x, y, - horizontal.to_sys(), - vertical.to_sys(), + horizontal.into(), + vertical.into(), ); } } @@ -182,12 +186,6 @@ impl<'a> Drop for DialogMessage<'a> { } } -impl<'a> Default for DialogMessage<'a> { - fn default() -> Self { - Self::new() - } -} - impl DialogMessageButton { fn from_sys(sys: sys::DialogMessageButton) -> Option { match sys { @@ -277,6 +275,7 @@ impl<'a> DialogFileBrowserOptions<'a> { #[cfg(feature = "alloc")] #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] pub fn alert(text: &str) { + // SAFETY: string is known to end with NUL const BUTTON_OK: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(b"OK\0") }; let text = CString::new(text.as_bytes()).unwrap(); diff --git a/crates/flipperzero/src/dolphin/mod.rs b/crates/flipperzero/src/dolphin/mod.rs index bd2723c8..577fdc84 100644 --- a/crates/flipperzero/src/dolphin/mod.rs +++ b/crates/flipperzero/src/dolphin/mod.rs @@ -33,7 +33,7 @@ impl Dolphin { /// Retrieves the dolphin's current stats. pub fn stats(&mut self) -> Stats { - unsafe { sys::dolphin_stats(self.data.as_ptr()) } + unsafe { sys::dolphin_stats(self.data.as_raw()) } } /// Upgrades the level of the dolphin, if it is ready. @@ -42,7 +42,7 @@ impl Dolphin { pub fn upgrade_level(&mut self) -> bool { let ready = self.stats().level_up_is_pending; if ready { - unsafe { sys::dolphin_upgrade_level(self.data.as_ptr()) }; + unsafe { sys::dolphin_upgrade_level(self.data.as_raw()) }; } ready } @@ -51,6 +51,6 @@ impl Dolphin { /// /// Thread safe, blocking. pub fn flush(&mut self) { - unsafe { sys::dolphin_flush(self.data.as_ptr()) }; + unsafe { sys::dolphin_flush(self.data.as_raw()) }; } } diff --git a/crates/flipperzero/src/furi/log/metadata.rs b/crates/flipperzero/src/furi/log/metadata.rs index c3b5e3b6..06f40074 100644 --- a/crates/flipperzero/src/furi/log/metadata.rs +++ b/crates/flipperzero/src/furi/log/metadata.rs @@ -5,6 +5,7 @@ use core::{cmp, fmt, str::FromStr}; +use crate::internals; use flipperzero_sys as sys; use ufmt::derive::uDebug; @@ -158,9 +159,7 @@ impl ufmt::uDisplay for Level { } } -#[cfg(feature = "std")] -#[cfg_attr(docsrs, doc(cfg(feature = "std")))] -impl std::error::Error for ParseLevelError {} +internals::macros::impl_std_error!(ParseLevelError); impl FromStr for Level { type Err = ParseLevelError; @@ -465,9 +464,7 @@ impl ufmt::uDisplay for ParseLevelFilterError { } } -#[cfg(feature = "std")] -#[cfg_attr(docsrs, doc(cfg(feature = "std")))] -impl std::error::Error for ParseLevelFilterError {} +internals::macros::impl_std_error!(ParseLevelFilterError); #[repr(usize)] #[derive(Copy, Clone, Debug, uDebug, Hash, Eq, PartialEq, PartialOrd, Ord)] diff --git a/crates/flipperzero/src/furi/message_queue.rs b/crates/flipperzero/src/furi/message_queue.rs index 8d5c6e51..581d61f9 100644 --- a/crates/flipperzero/src/furi/message_queue.rs +++ b/crates/flipperzero/src/furi/message_queue.rs @@ -27,7 +27,7 @@ impl MessageQueue { } } - // Attempts to add the message to the end of the queue, waiting up to timeout ticks. + /// Attempts to add the message to the end of the queue, waiting up to timeout ticks. pub fn put(&self, msg: M, timeout: Duration) -> furi::Result<()> { let mut msg = core::mem::ManuallyDrop::new(msg); let timeout_ticks = duration_to_ticks(timeout); @@ -44,7 +44,13 @@ impl MessageQueue { status.err_or(()) } - // Attempts to read a message from the front of the queue within timeout ticks. + /// Attempts to instantly [`put`](Self::put) the message to the end of the queue. + #[inline] + pub fn put_now(&self, msg: M) -> furi::Result<()> { + self.put(msg, Duration::ZERO) + } + + /// Attempts to read a message from the front of the queue within timeout ticks. pub fn get(&self, timeout: Duration) -> furi::Result { let timeout_ticks = duration_to_ticks(timeout); let mut out = core::mem::MaybeUninit::::uninit(); @@ -64,6 +70,12 @@ impl MessageQueue { } } + /// Attempts to instantly [`get`](Self::get) the message from the front of the queue. + #[inline] + pub fn get_now(&self) -> furi::Result { + self.get(Duration::ZERO) + } + /// Returns the capacity of the queue. pub fn capacity(&self) -> usize { unsafe { sys::furi_message_queue_get_capacity(self.hnd.as_ptr()) as usize } diff --git a/crates/flipperzero/src/gui/canvas.rs b/crates/flipperzero/src/gui/canvas.rs deleted file mode 100644 index 355c446d..00000000 --- a/crates/flipperzero/src/gui/canvas.rs +++ /dev/null @@ -1,24 +0,0 @@ -//! Canvases. - -use flipperzero_sys as sys; - -#[derive(Debug, Clone, Copy)] -pub enum Align { - Left, - Right, - Top, - Bottom, - Center, -} - -impl Align { - pub fn to_sys(&self) -> sys::Align { - match self { - Self::Left => sys::Align_AlignLeft, - Self::Right => sys::Align_AlignRight, - Self::Top => sys::Align_AlignTop, - Self::Bottom => sys::Align_AlignBottom, - Self::Center => sys::Align_AlignCenter, - } - } -} diff --git a/crates/flipperzero/src/gui/canvas/align.rs b/crates/flipperzero/src/gui/canvas/align.rs new file mode 100644 index 00000000..33d580fc --- /dev/null +++ b/crates/flipperzero/src/gui/canvas/align.rs @@ -0,0 +1,78 @@ +use core::fmt::{self, Display, Formatter}; + +use flipperzero_sys::{self as sys, Align as SysAlign}; +use ufmt::{derive::uDebug, uDisplay, uWrite, uwrite}; + +use crate::internals::macros::impl_std_error; + +/// Alignment of an object on the canvas. +/// +/// Corresponds to raw [`SysAlign`]. +#[derive(Copy, Clone, Debug, uDebug, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub enum Align { + /// The values are aligned relative to the right. + Left, + /// The values are aligned relative to the left. + Right, + /// The values are aligned relative to the top. + Top, + /// The values are aligned relative to the bottom. + Bottom, + /// The values are aligned relative to the center. + Center, +} + +impl TryFrom for Align { + type Error = FromSysAlignError; + + fn try_from(value: SysAlign) -> Result { + Ok(match value { + sys::Align_AlignLeft => Self::Left, + sys::Align_AlignRight => Self::Right, + sys::Align_AlignTop => Self::Top, + sys::Align_AlignBottom => Self::Bottom, + sys::Align_AlignCenter => Self::Center, + invalid => Err(Self::Error::Invalid(invalid))?, + }) + } +} + +impl From for SysAlign { + fn from(value: Align) -> Self { + match value { + Align::Left => sys::Align_AlignLeft, + Align::Right => sys::Align_AlignRight, + Align::Top => sys::Align_AlignTop, + Align::Bottom => sys::Align_AlignBottom, + Align::Center => sys::Align_AlignCenter, + } + } +} + +/// An error which may occur while trying +/// to convert raw [`SysAlign`] to [`Align`]. +#[non_exhaustive] +#[derive(Copy, Clone, Debug, uDebug, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub enum FromSysAlignError { + /// The [`SysAlign`] is an invalid value. + Invalid(SysAlign), +} + +impl Display for FromSysAlignError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let Self::Invalid(id) = self; + write!(f, "align ID {id} is invalid") + } +} + +impl uDisplay for FromSysAlignError { + fn fmt(&self, f: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error> + where + W: uWrite + ?Sized, + { + let Self::Invalid(id) = self; + uwrite!(f, "align ID {} is invalid", id) + } +} + +impl_std_error!(FromSysAlignError); diff --git a/crates/flipperzero/src/gui/canvas/canvas_direction.rs b/crates/flipperzero/src/gui/canvas/canvas_direction.rs new file mode 100644 index 00000000..0ca61f3e --- /dev/null +++ b/crates/flipperzero/src/gui/canvas/canvas_direction.rs @@ -0,0 +1,74 @@ +use core::fmt::{self, Display, Formatter}; + +use flipperzero_sys::{self as sys, CanvasDirection as SysCanvasDirection}; +use ufmt::{derive::uDebug, uDisplay, uWrite, uwrite}; + +use crate::internals::macros::impl_std_error; + +/// Direction of an element on the canvas. +/// +/// Corresponds to raw [`SysCanvasDirection`]. +#[derive(Copy, Clone, Debug, uDebug, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub enum CanvasDirection { + /// The direction is from left to right. + LeftToRight, + /// The direction is from top to bottom. + TopToBottom, + /// The direction is from right to left. + RightToLeft, + /// The direction is from bottom to top. + BottomToTop, +} + +impl TryFrom for CanvasDirection { + type Error = FromSysCanvasDirectionError; + + fn try_from(value: SysCanvasDirection) -> Result { + Ok(match value { + sys::CanvasDirection_CanvasDirectionLeftToRight => Self::LeftToRight, + sys::CanvasDirection_CanvasDirectionTopToBottom => Self::TopToBottom, + sys::CanvasDirection_CanvasDirectionRightToLeft => Self::RightToLeft, + sys::CanvasDirection_CanvasDirectionBottomToTop => Self::BottomToTop, + invalid => Err(Self::Error::Invalid(invalid))?, + }) + } +} + +impl From for SysCanvasDirection { + fn from(value: CanvasDirection) -> Self { + match value { + CanvasDirection::BottomToTop => sys::CanvasDirection_CanvasDirectionBottomToTop, + CanvasDirection::LeftToRight => sys::CanvasDirection_CanvasDirectionLeftToRight, + CanvasDirection::RightToLeft => sys::CanvasDirection_CanvasDirectionRightToLeft, + CanvasDirection::TopToBottom => sys::CanvasDirection_CanvasDirectionTopToBottom, + } + } +} + +/// An error which may occur while trying +/// to convert raw [`SysCanvasDirection`] to [`CanvasDirection`]. +#[non_exhaustive] +#[derive(Copy, Clone, Debug, uDebug, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub enum FromSysCanvasDirectionError { + /// The [`SysCanvasDirection`] is an invalid value. + Invalid(SysCanvasDirection), +} + +impl Display for FromSysCanvasDirectionError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let Self::Invalid(id) = self; + write!(f, "canvas direction ID {id} is invalid") + } +} + +impl uDisplay for FromSysCanvasDirectionError { + fn fmt(&self, f: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error> + where + W: uWrite + ?Sized, + { + let Self::Invalid(id) = self; + uwrite!(f, "canvas direction ID {} is invalid", id) + } +} + +impl_std_error!(FromSysCanvasDirectionError); diff --git a/crates/flipperzero/src/gui/canvas/color.rs b/crates/flipperzero/src/gui/canvas/color.rs new file mode 100644 index 00000000..61131687 --- /dev/null +++ b/crates/flipperzero/src/gui/canvas/color.rs @@ -0,0 +1,70 @@ +use core::fmt::{self, Display, Formatter}; + +use flipperzero_sys::{self as sys, Color as SysColor}; +use ufmt::{derive::uDebug, uDisplay, uWrite, uwrite}; + +use crate::internals::macros::impl_std_error; + +/// Color on the canvas. +/// +/// Corresponds to raw [`SysColor`]. +#[derive(Copy, Clone, Debug, uDebug, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub enum Color { + /// White color is used. + White, + /// Black color is used. + Black, + /// The color is inverted. + Xor, +} + +impl TryFrom for Color { + type Error = FromSysColorError; + + fn try_from(value: SysColor) -> Result { + Ok(match value { + sys::Color_ColorWhite => Self::White, + sys::Color_ColorBlack => Self::Black, + sys::Color_ColorXOR => Self::Xor, + invalid => Err(Self::Error::Invalid(invalid))?, + }) + } +} + +impl From for SysColor { + fn from(value: Color) -> Self { + match value { + Color::White => sys::Color_ColorWhite, + Color::Black => sys::Color_ColorBlack, + Color::Xor => sys::Color_ColorXOR, + } + } +} + +/// An error which may occur while trying +/// to convert raw [`SysColor`] to [`Color`]. +#[non_exhaustive] +#[derive(Copy, Clone, Debug, uDebug, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub enum FromSysColorError { + /// The [`SysColor`] is an invalid value. + Invalid(SysColor), +} + +impl Display for FromSysColorError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let Self::Invalid(id) = self; + write!(f, "color ID {id} is invalid") + } +} + +impl uDisplay for FromSysColorError { + fn fmt(&self, f: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error> + where + W: uWrite + ?Sized, + { + let Self::Invalid(id) = self; + uwrite!(f, "color ID {} is invalid", id) + } +} + +impl_std_error!(FromSysColorError); diff --git a/crates/flipperzero/src/gui/canvas/font.rs b/crates/flipperzero/src/gui/canvas/font.rs new file mode 100644 index 00000000..7cd9a11e --- /dev/null +++ b/crates/flipperzero/src/gui/canvas/font.rs @@ -0,0 +1,105 @@ +use core::fmt::{self, Display, Formatter}; + +use flipperzero_sys::{self as sys, Font as SysFont}; +use ufmt::{derive::uDebug, uDisplay, uWrite, uwrite}; + +use crate::internals::macros::impl_std_error; + +/// The font used to draw text. +/// +/// Corresponds to raw [`SysFont`]. +#[derive(Copy, Clone, Debug, uDebug, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub enum Font { + /// The primary font. + Primary, + /// The secondary font. + Secondary, + /// The keyboard font. + Keyboard, + /// The font with big numbers. + BigNumbers, +} + +impl Font { + /// Gets the total number of available fonts. + /// + /// # Example + /// + /// ``` + /// # use flipperzero::gui::canvas::Font; + /// assert_eq!(Font::total_number(), 4); + /// ``` + pub const fn total_number() -> usize { + sys::Font_FontTotalNumber as usize + } +} + +impl TryFrom for Font { + type Error = FromSysFontError; + + fn try_from(value: SysFont) -> Result { + Ok(match value { + sys::Font_FontPrimary => Self::Primary, + sys::Font_FontSecondary => Self::Secondary, + sys::Font_FontKeyboard => Self::Keyboard, + sys::Font_FontBigNumbers => Self::BigNumbers, + sys::Font_FontTotalNumber => Err(Self::Error::TotalNumber)?, + invalid => Err(Self::Error::Invalid(invalid))?, + }) + } +} + +impl From for SysFont { + fn from(value: Font) -> Self { + match value { + Font::Primary => sys::Font_FontPrimary, + Font::Secondary => sys::Font_FontSecondary, + Font::Keyboard => sys::Font_FontKeyboard, + Font::BigNumbers => sys::Font_FontBigNumbers, + } + } +} + +/// An error which may occur while trying +/// to convert raw [`SysFont`] to [`Font`]. +#[non_exhaustive] +#[derive(Copy, Clone, Debug, uDebug, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub enum FromSysFontError { + /// The [`SysFont`] is [`TotalNumber`][sys::Font_FontTotalNumber] + /// which is a meta-value used to track enum size. + TotalNumber, + /// The [`SysFont`] is an invalid value + /// other than [`TotalNumber`][sys::Font_FontTotalNumber]. + Invalid(SysFont), +} + +impl Display for FromSysFontError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::TotalNumber => write!( + f, + "font ID {} (TotalNumber) is a meta-value", + sys::Font_FontTotalNumber, + ), + Self::Invalid(id) => write!(f, "font ID {id} is invalid"), + } + } +} + +impl uDisplay for FromSysFontError { + fn fmt(&self, f: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error> + where + W: uWrite + ?Sized, + { + match self { + Self::TotalNumber => uwrite!( + f, + "font ID {} (TotalNumber) is a meta-value", + sys::Font_FontTotalNumber, + ), + Self::Invalid(id) => uwrite!(f, "font ID {} is invalid", id), + } + } +} + +impl_std_error!(FromSysFontError); diff --git a/crates/flipperzero/src/gui/canvas/font_parameters.rs b/crates/flipperzero/src/gui/canvas/font_parameters.rs new file mode 100644 index 00000000..d9078b75 --- /dev/null +++ b/crates/flipperzero/src/gui/canvas/font_parameters.rs @@ -0,0 +1,90 @@ +use core::{ + fmt::{self, Display, Formatter}, + num::NonZeroU8, +}; + +use flipperzero_sys::CanvasFontParameters as SysCanvasFontParameters; +use ufmt::{derive::uDebug, uDisplay, uWrite}; + +use crate::internals::macros::impl_std_error; + +/// Font parameters on a canvas. +/// +/// Corresponds to raw [`SysCanvasFontParameters`]. +#[derive(Copy, Clone, Debug, uDebug, Eq, PartialEq, Hash)] +pub struct CanvasFontParameters { + pub leading_default: NonZeroU8, + pub leading_min: NonZeroU8, + pub height: NonZeroU8, + pub descender: u8, +} + +impl TryFrom for CanvasFontParameters { + type Error = FromSysCanvasFontParameters; + + fn try_from(value: SysCanvasFontParameters) -> Result { + Ok(Self { + leading_default: value + .leading_default + .try_into() + .or(Err(Self::Error::ZeroLeadingDefault))?, + leading_min: value + .leading_min + .try_into() + .or(Err(Self::Error::ZeroLeadingMin))?, + height: value.height.try_into().or(Err(Self::Error::ZeroHeight))?, + descender: value.descender, + }) + } +} + +impl From for SysCanvasFontParameters { + fn from(value: CanvasFontParameters) -> Self { + Self { + leading_default: value.leading_default.into(), + leading_min: value.leading_min.into(), + height: value.height.into(), + descender: value.descender, + } + } +} + +/// An error which may occur while trying +/// to convert raw [`SysCanvasFontParameters`] to [`CanvasFontParameters`]. +/// +/// All of these correspond to errors in individual parameters. +#[non_exhaustive] +#[derive(Copy, Clone, Debug, uDebug, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub enum FromSysCanvasFontParameters { + /// [`SysCanvasFontParameters::leading_default`] field is set to `0`. + ZeroLeadingDefault, + /// [`SysCanvasFontParameters::leading_min`] field is set to `0`. + ZeroLeadingMin, + /// [`SysCanvasFontParameters::height`] field is set to `0`. + ZeroHeight, +} + +impl Display for FromSysCanvasFontParameters { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str(match self { + FromSysCanvasFontParameters::ZeroLeadingDefault => "leading_default is zero", + FromSysCanvasFontParameters::ZeroLeadingMin => "leading_min is zero", + FromSysCanvasFontParameters::ZeroHeight => "height is zero", + }) + } +} + +impl uDisplay for FromSysCanvasFontParameters { + fn fmt(&self, f: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error> + where + W: uWrite + ?Sized, + { + f.write_str(match self { + FromSysCanvasFontParameters::ZeroLeadingDefault => "leading_default is zero", + FromSysCanvasFontParameters::ZeroLeadingMin => "leading_min is zero", + FromSysCanvasFontParameters::ZeroHeight => "height is zero", + }) + } +} + +impl_std_error!(FromSysCanvasFontParameters); diff --git a/crates/flipperzero/src/gui/canvas/mod.rs b/crates/flipperzero/src/gui/canvas/mod.rs new file mode 100644 index 00000000..e18dc9bb --- /dev/null +++ b/crates/flipperzero/src/gui/canvas/mod.rs @@ -0,0 +1,403 @@ +//! Canvas-related APIs allowing to draw on it. + +mod align; +mod canvas_direction; +mod color; +mod font; +mod font_parameters; + +use core::{ + ffi::{c_char, CStr}, + marker::PhantomData, + num::NonZeroU8, + ops::Deref, + ptr::NonNull, +}; + +pub use align::*; +pub use canvas_direction::*; +pub use color::*; +use flipperzero_sys::{ + self as sys, Canvas as SysCanvas, CanvasFontParameters as SysCanvasFontParameters, +}; +pub use font::*; +pub use font_parameters::*; + +use crate::gui::{ + icon::Icon, + icon_animation::{IconAnimation, IconAnimationCallbacks}, +}; +#[cfg(feature = "xbm")] +use crate::xbm::XbmImage; + +/// System Canvas view. +pub struct CanvasView<'a> { + raw: NonNull, + _lifetime: PhantomData<&'a SysCanvas>, +} + +impl CanvasView<'_> { + /// Construct a `CanvasView` from a raw pointer. + /// + /// # Safety + /// + /// `raw` should be a valid non-null pointer to [`sys::Canvas`] + /// and the lifetime should be outlived by `raw` validity scope. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ``` + /// # use flipperzero::gui::canvas::CanvasView; + /// # let canvas_ptr: *mut flipperzero_sys::Canvas = todo!(); + /// // wrap a raw pointer to a canvas + /// let canvas = unsafe { CanvasView::from_raw(canvas_ptr) }; + /// ``` + pub unsafe fn from_raw(raw: *mut SysCanvas) -> Self { + Self { + // SAFETY: caller should provide a valid pointer + raw: unsafe { NonNull::new_unchecked(raw) }, + _lifetime: PhantomData, + } + } + + /// Resets canvas drawing tools configuration. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ``` + /// # use flipperzero::gui::canvas::{CanvasView, Color}; + /// # let mut canvas: CanvasView<'static> = todo!(); + /// // change canvas color and use it for drawing + /// canvas.set_color(Color::Xor); + /// canvas.draw_circle(10, 10, 5); + /// // reset canvas options and use defaults for drawing + /// canvas.reset(); + /// canvas.draw_circle(20, 20, 5); + /// ``` + pub fn reset(&mut self) { + let raw = self.raw.as_ptr(); + // SAFETY: `raw` is always valid + unsafe { sys::canvas_reset(raw) }; + } + + /// Commits canvas sending its buffer to display. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ``` + /// # use flipperzero::gui::canvas::{CanvasView, Color}; + /// # let mut canvas: CanvasView<'static> = todo!(); + /// // perform some draw operations on the canvas + /// canvas.draw_frame(0, 0, 51, 51); + /// canvas.draw_circle(25, 25, 10); + /// // commit changes + /// canvas.commit(); + /// ``` + pub fn commit(&mut self) { + let raw = self.raw.as_ptr(); + // SAFETY: `raw` is always valid + unsafe { sys::canvas_commit(raw) }; + } + + pub fn width(&self) -> NonZeroU8 { + let raw = self.raw.as_ptr(); + // SAFETY: `raw` is always valid + unsafe { sys::canvas_width(raw) } + .try_into() + .expect("`canvas_width` should produce a positive value") + } + + pub fn height(&self) -> NonZeroU8 { + let raw = self.raw.as_ptr(); + // SAFETY: `raw` is always valid + unsafe { sys::canvas_height(raw) } + .try_into() + .expect("`canvas_height` should produce a positive value") + } + + pub fn current_font_height(&self) -> NonZeroU8 { + let raw = self.raw.as_ptr(); + // SAFETY: `raw` is always valid + unsafe { sys::canvas_current_font_height(raw) } + .try_into() + .expect("`canvas_current_font_height` should produce a positive value") + } + + pub fn get_font_params(&self, font: Font) -> OwnedCanvasFontParameters<'_> { + let raw = self.raw.as_ptr(); + // SAFETY: `raw` is always valid + let font = font.into(); + // SAFETY: `raw` is a valid pointer + // and `font` is guaranteed to be a valid value by `From` implementation + // `cast_mut` is required since `NonNull` can only be created froma mut-pointer + let raw = unsafe { sys::canvas_get_font_params(raw, font) }.cast_mut(); + // SAFETY: `raw` is a valid pointer + let raw = unsafe { NonNull::new_unchecked(raw) }; + OwnedCanvasFontParameters { + raw, + _parent: PhantomData, + } + } + + pub fn clear(&mut self) { + let raw = self.raw.as_ptr(); + // SAFETY: `raw` is always valid + unsafe { sys::canvas_clear(raw) }; + } + + pub fn set_color(&mut self, color: Color) { + let raw = self.raw.as_ptr(); + let color = color.into(); + // SAFETY: `raw` is always valid + // and `font` is guaranteed to be a valid value by `From` implementation + unsafe { sys::canvas_set_color(raw, color) }; + } + + pub fn set_font_direction(&mut self, font_direction: CanvasDirection) { + let raw = self.raw.as_ptr(); + let font_direction = font_direction.into(); + // SAFETY: `raw` is always valid + // and `font_direction` is guaranteed to be a valid value by `From` implementation + unsafe { sys::canvas_set_font_direction(raw, font_direction) }; + } + + pub fn invert_color(&mut self) { + let raw = self.raw.as_ptr(); + // SAFETY: `raw` is always valid + unsafe { sys::canvas_invert_color(raw) }; + } + + pub fn set_font(&mut self, font: Font) { + let raw = self.raw.as_ptr(); + let font = font.into(); + // SAFETY: `raw` is always valid + // and `font` is guaranteed to be a valid value by `From` implementation + unsafe { sys::canvas_set_font(raw, font) }; + } + + pub fn draw_str(&mut self, x: u8, y: u8, string: impl AsRef) { + let raw = self.raw.as_ptr(); + let string = string.as_ref().as_ptr(); + // SAFETY: `raw` is always valid + // and `string` is guaranteed to be a valid pointer since it was created from `CStr` + unsafe { sys::canvas_draw_str(raw, x, y, string) }; + } + + pub fn draw_str_aligned( + &mut self, + x: u8, + y: u8, + horizontal: Align, + vertical: Align, + str: impl AsRef, + ) { + let raw = self.raw.as_ptr(); + let horizontal = horizontal.into(); + let vertical = vertical.into(); + let str = str.as_ref().as_ptr(); + // SAFETY: `raw` is always valid, + // `horixontal` and `vertival` are guaranteed to be valid by `From` implementation + // and `text` is guaranteed to be a valid pointer since it was created from `CStr` + unsafe { sys::canvas_draw_str_aligned(raw, x, y, horizontal, vertical, str) }; + } + + // note: for some reason, this mutates internal state + pub fn string_width(&mut self, string: impl AsRef) -> u16 { + let raw = self.raw.as_ptr(); + let string = string.as_ref().as_ptr(); + // SAFETY: `raw` is always valid + // and `string` is guaranteed to be a valid pointer since it was created from `CStr` + unsafe { sys::canvas_string_width(raw, string) } + } + + // note: for some reason, this mutates internal state + pub fn glyph_width(&mut self, glyph: c_char) -> u8 { + let raw = self.raw.as_ptr(); + // SAFETY: `raw` is always valid + unsafe { sys::canvas_glyph_width(raw, glyph) } + } + + // Note: FURI is guaranteed to correctly handle out-of-bounds draws + // so we don't need to check the bounds + + // TODO `canvas_draw_bitmap` compressed bitmap support + + pub fn draw_icon_animation<'a, 'b: 'a>( + &'a mut self, + x: u8, + y: u8, + icon_animation: &'b IconAnimation<'_, impl IconAnimationCallbacks>, + ) { + let raw = self.raw.as_ptr(); + let icon_animation = icon_animation.as_raw(); + // SAFETY: `raw` is always valid + // and `icon_animation` is always valid and outlives this canvas view + unsafe { sys::canvas_draw_icon_animation(raw, x, y, icon_animation) } + } + + pub fn draw_icon<'a, 'b: 'a>(&'a mut self, x: u8, y: u8, animation: &'b Icon) { + let raw = self.raw.as_ptr(); + let icon = animation.as_raw(); + // SAFETY: `raw` is always valid + // and `icon` is always valid and outlives this canvas view + unsafe { sys::canvas_draw_icon(raw, x, y, icon) } + } + + #[cfg(feature = "xbm")] + pub fn draw_xbm(&mut self, x: u8, y: u8, xbm: &XbmImage>) { + let raw = self.raw.as_ptr(); + let width = xbm.width(); + let height = xbm.height(); + + let data = xbm.data().as_ptr(); + + // SAFETY: `raw` is always valid + // and `data` is always valid and does not have to outlive the view + // as it is copied + unsafe { sys::canvas_draw_xbm(raw, x, y, width, height, data) }; + } + + // TODO: + // - `canvas_draw_icon` icon lifetimes + + // TODO: decide if we want to pack x-y pairs into tuples + + pub fn draw_dot(&mut self, x: u8, y: u8) { + let raw = self.raw.as_ptr(); + // SAFETY: `raw` is always valid + unsafe { sys::canvas_draw_dot(raw, x, y) } + } + + // TODO: do `width` and `height` have to be non-zero + pub fn draw_box(&mut self, x: u8, y: u8, width: u8, height: u8) { + let raw = self.raw.as_ptr(); + // SAFETY: `raw` is always valid + unsafe { sys::canvas_draw_box(raw, x, y, width, height) } + } + + // TODO: do `width` and `height` have to be non-zero + pub fn draw_frame(&mut self, x: u8, y: u8, width: u8, height: u8) { + let raw = self.raw.as_ptr(); + // SAFETY: `raw` is always valid + unsafe { sys::canvas_draw_frame(raw, x, y, width, height) } + } + + // TODO: do `x2` and `y2` have to be non-zero + pub fn draw_line(&mut self, x1: u8, y1: u8, x2: u8, y2: u8) { + let raw = self.raw.as_ptr(); + // SAFETY: `raw` is always valid + unsafe { sys::canvas_draw_line(raw, x1, y1, x2, y2) } + } + + // TODO: does `radius` have to be non-zero + pub fn draw_circle(&mut self, x: u8, y: u8, radius: u8) { + let raw = self.raw.as_ptr(); + // SAFETY: `raw` is always valid + unsafe { sys::canvas_draw_circle(raw, x, y, radius) } + } + + // TODO: does `radius` have to be non-zero + pub fn draw_disc(&mut self, x: u8, y: u8, radius: u8) { + let raw = self.raw.as_ptr(); + // SAFETY: `raw` is always valid + unsafe { sys::canvas_draw_disc(raw, x, y, radius) } + } + + // TODO: do `base` and `height` have to be non-zero + pub fn draw_triangle( + &mut self, + x: u8, + y: u8, + base: u8, + height: u8, + direction: CanvasDirection, + ) { + let raw = self.raw.as_ptr(); + let direction = direction.into(); + // SAFETY: `raw` is always valid + // and `direction` is guaranteed to be valid by `From` implementation + unsafe { sys::canvas_draw_triangle(raw, x, y, base, height, direction) } + } + + // TODO: does `character` have to be of a wrapper type + pub fn draw_glyph(&mut self, x: u8, y: u8, character: u16) { + let raw = self.raw.as_ptr(); + // SAFETY: `raw` is always valid, + unsafe { sys::canvas_draw_glyph(raw, x, y, character) } + } + + pub fn set_bitmap_mode(&mut self, alpha: bool) { + let raw = self.raw.as_ptr(); + // SAFETY: `raw` is always valid, + unsafe { sys::canvas_set_bitmap_mode(raw, alpha) } + } + + // TODO: do `width`, `height` and `radius` have to be non-zero + pub fn draw_rframe(&mut self, x: u8, y: u8, width: u8, height: u8, radius: u8) { + let raw = self.raw.as_ptr(); + // SAFETY: `raw` is always valid, + unsafe { sys::canvas_draw_rframe(raw, x, y, width, height, radius) } + } + + // TODO: do `width`, `height` and `radius` have to be non-zero + pub fn draw_rbox(&mut self, x: u8, y: u8, width: u8, height: u8, radius: u8) { + let raw = self.raw.as_ptr(); + // SAFETY: `raw` is always valid, + unsafe { sys::canvas_draw_rbox(raw, x, y, width, height, radius) } + } +} + +pub struct OwnedCanvasFontParameters<'a> { + // this wraps an effectively const pointer thus it should never be used for weiting + raw: NonNull, + _parent: PhantomData<&'a CanvasView<'a>>, +} + +impl<'a> OwnedCanvasFontParameters<'a> { + pub fn leading_default(&self) -> NonZeroU8 { + let raw = self.raw.as_ptr(); + // SAFETY: `raw` is always valid and this allways outlives its parent + unsafe { *raw } + .leading_default + .try_into() + .expect("`leading_default` should always be positive") + } + + pub fn leading_min(&self) -> NonZeroU8 { + let raw = self.raw.as_ptr(); + // SAFETY: `raw` is always valid and this allways outlives its parent + unsafe { *raw } + .leading_min + .try_into() + .expect("`leading_min` should always be positive") + } + + pub fn height(&self) -> NonZeroU8 { + let raw = self.raw.as_ptr(); + // SAFETY: `raw` is always valid and this allways outlives its parent + unsafe { *raw } + .height + .try_into() + .expect("`height` should always be positive") + } + + pub fn descender(&self) -> u8 { + let raw = self.raw.as_ptr(); + // SAFETY: `raw` is always valid and this allways outlives its parent + unsafe { *raw }.descender + } + + pub fn snapshot(&self) -> CanvasFontParameters { + let raw = self.raw.as_ptr(); + // SAFETY: `raw` is always valid and this allways outlives its parent + unsafe { *raw } + .try_into() + .expect("raw `CanvasFontParameters` should be valid") + } +} diff --git a/crates/flipperzero/src/gui/gui_layer.rs b/crates/flipperzero/src/gui/gui_layer.rs new file mode 100644 index 00000000..e1cd15bb --- /dev/null +++ b/crates/flipperzero/src/gui/gui_layer.rs @@ -0,0 +1,95 @@ +use core::fmt::{self, Display, Formatter}; + +use flipperzero_sys::{self as sys, GuiLayer as SysGuiLayer}; +use ufmt::{derive::uDebug, uDisplay, uWrite, uwrite}; + +use crate::internals::macros::impl_std_error; + +/// The font used to draw text. +/// +/// Corresponds to raw [`SysGuiLayer`]. +#[derive(Copy, Clone, Debug, uDebug, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub enum GuiLayer { + /// Desktop layer for internal use. Like fullscreen but with status bar. + Desktop, + /// Window layer, status bar is shown. + Window, + /// Status bar left-side layer, auto-layout. + StatusBarLeft, + /// Status bar right-side layer, auto-layout + StatusBarRight, + /// Fullscreen layer, no status bar. + Fullscreen, +} + +impl TryFrom for GuiLayer { + type Error = FromSysGuiLayerError; + + fn try_from(value: SysGuiLayer) -> Result { + Ok(match value { + sys::GuiLayer_GuiLayerDesktop => Self::Desktop, + sys::GuiLayer_GuiLayerWindow => Self::Window, + sys::GuiLayer_GuiLayerStatusBarLeft => Self::StatusBarLeft, + sys::GuiLayer_GuiLayerStatusBarRight => Self::StatusBarRight, + sys::GuiLayer_GuiLayerFullscreen => Self::Fullscreen, + sys::GuiLayer_GuiLayerMAX => Err(Self::Error::Max)?, + invalid => Err(Self::Error::Invalid(invalid))?, + }) + } +} + +impl From for SysGuiLayer { + fn from(value: GuiLayer) -> Self { + match value { + GuiLayer::Desktop => sys::GuiLayer_GuiLayerDesktop, + GuiLayer::Window => sys::GuiLayer_GuiLayerWindow, + GuiLayer::StatusBarLeft => sys::GuiLayer_GuiLayerStatusBarLeft, + GuiLayer::StatusBarRight => sys::GuiLayer_GuiLayerStatusBarRight, + GuiLayer::Fullscreen => sys::GuiLayer_GuiLayerFullscreen, + } + } +} + +/// An error which may occur while trying +/// to convert raw [`SysGuiLayer`] to [`GuiLayer`]. +#[non_exhaustive] +#[derive(Copy, Clone, Debug, uDebug, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub enum FromSysGuiLayerError { + /// The [`SysGuiLayer`] is [`MAX`][sys::GuiLayer_GuiLayerMAX] + /// which is a meta-value used to track enum size. + Max, + /// The [`SysGuiLayer`] is an invalid value + /// other than [`MAX`][sys::GuiLayer_GuiLayerMAX]. + Invalid(SysGuiLayer), +} + +impl Display for FromSysGuiLayerError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::Max => write!( + f, + "gui layer ID {} (MAX) is a meta-value", + sys::GuiLayer_GuiLayerMAX, + ), + Self::Invalid(id) => write!(f, "gui layer ID {id} is invalid"), + } + } +} + +impl uDisplay for FromSysGuiLayerError { + fn fmt(&self, f: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error> + where + W: uWrite + ?Sized, + { + match self { + Self::Max => uwrite!( + f, + "gui layer ID {} (MAX) is a meta-value", + sys::GuiLayer_GuiLayerMAX, + ), + Self::Invalid(id) => uwrite!(f, "gui layer ID {} is invalid", id), + } + } +} + +impl_std_error!(FromSysGuiLayerError); diff --git a/crates/flipperzero/src/gui/icon.rs b/crates/flipperzero/src/gui/icon.rs new file mode 100644 index 00000000..1b0b65eb --- /dev/null +++ b/crates/flipperzero/src/gui/icon.rs @@ -0,0 +1,68 @@ +use core::ptr::NonNull; + +use flipperzero_sys::{self as sys, Icon as SysIcon}; + +#[cfg(feature = "xbm")] +use crate::xbm::XbmImage; + +pub struct Icon { + raw: NonNull, +} + +impl Icon { + /// Construct an `Icon` from a raw non-null pointer. + /// + /// # Safety + /// + /// `raw` should be a valid non-null pointer to [`sys::Canvas`]. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ``` + /// use flipperzero::gui::icon::Icon; + /// + /// let ptr = todo!(); + /// let canvas = unsafe { Icon::from_raw(ptr) }; + /// ``` + pub unsafe fn from_raw(raw: *mut SysIcon) -> Self { + // SAFETY: the caller is required to provide the valid pointer + let raw = NonNull::new_unchecked(raw); + Self { raw } + } + + #[inline] + #[must_use] + pub fn as_raw(&self) -> *mut SysIcon { + self.raw.as_ptr() + } + + pub fn get_width(&self) -> u8 { + let raw = self.raw.as_ptr(); + // SAFETY: `raw` is always valid + unsafe { sys::icon_get_width(raw) } + } + + pub fn get_height(&self) -> u8 { + let raw = self.raw.as_ptr(); + // SAFETY: `raw` is always valid + unsafe { sys::icon_get_height(raw) } + } + + pub fn get_dimensions(&self) -> (u8, u8) { + (self.get_width(), self.get_height()) + } + + #[cfg(feature = "xbm")] + #[cfg_attr(docsrs, doc(cfg(feature = "xbm")))] + pub fn get_data(&self) -> XbmImage<&'_ [u8]> { + let (width, height) = self.get_dimensions(); + + let raw = self.raw.as_ptr(); + // SAFETY: `raw` is always valid, + // `width` and `height` are always in sync with data + // and the lifetime is based on `&self`'s + unsafe { XbmImage::from_raw(width, height, sys::icon_get_data(raw)) } + } +} diff --git a/crates/flipperzero/src/gui/icon_animation.rs b/crates/flipperzero/src/gui/icon_animation.rs new file mode 100644 index 00000000..b967c4ce --- /dev/null +++ b/crates/flipperzero/src/gui/icon_animation.rs @@ -0,0 +1,216 @@ +use core::{ + ffi::c_void, + marker::PhantomData, + ptr::{self, NonNull}, +}; + +use flipperzero_sys::{self as sys, Icon as SysIcon, IconAnimation as SysIconAnimation}; + +use crate::{gui::icon::Icon, internals::alloc::NonUniqueBox}; + +/// Icon Animation +/// which can be [started](IconAnimation::start) and [stopped](IconAnimation::stop). +pub struct IconAnimation<'a, C: IconAnimationCallbacks> { + inner: IconAnimationInner, + callbacks: NonUniqueBox, + _parent_lifetime: PhantomData<&'a mut (IconAnimationInner, C)>, +} + +impl<'a, C: IconAnimationCallbacks> IconAnimation<'a, C> { + /// Creates a new icon animation from the specified [icon](`Icon`). + pub fn new<'b: 'a>(icon: &'b Icon, callbacks: C) -> Self { + let icon = icon.as_raw().cast_const(); + // SAFETY: `icon` is a valid pointer and will outlive `inner` while remaining const + let inner = unsafe { IconAnimationInner::new(icon) }; + let callbacks = NonUniqueBox::new(callbacks); + + let icon_animation = Self { + inner, + callbacks, + _parent_lifetime: PhantomData, + }; + + { + pub unsafe extern "C" fn dispatch_update( + instance: *mut SysIconAnimation, + context: *mut c_void, + ) { + // SAFETY: `icon_animation` is guaranteed to be a valid pointer + let instance = unsafe { IconAnimationView::from_raw(instance) }; + + let context: *mut C = context.cast(); + // SAFETY: `context` is stored in a `Box` which is a member of `ViewPort` + // and the callback is accessed exclusively by this function + unsafe { &mut *context }.on_update(instance); + } + + if !ptr::eq( + C::on_update as *const c_void, + <() as IconAnimationCallbacks>::on_update as *const c_void, + ) { + let raw = icon_animation.as_raw(); + let callback = Some(dispatch_update:: as _); + let context = icon_animation.callbacks.as_ptr().cast(); + + // SAFETY: `raw` and `callback` are valid + // and `context` is valid as the box lives with this struct + unsafe { sys::icon_animation_set_update_callback(raw, callback, context) }; + } + } + + icon_animation + } + + #[inline] + #[must_use] + pub fn as_raw(&self) -> *mut SysIconAnimation { + self.inner.0.as_ptr() + } + + /// Gets the width of this icon animation. + pub fn get_width(&self) -> u8 { + let raw = self.as_raw(); + // SAFETY: `raw` is valid + unsafe { sys::icon_animation_get_width(raw) } + } + + /// Gets the height of this icon animation. + pub fn get_height(&self) -> u8 { + let raw = self.as_raw(); + // SAFETY: `raw` is valid + unsafe { sys::icon_animation_get_height(raw) } + } + + /// Gets the dimensions of this icon animation. + pub fn get_dimensions(&self) -> (u8, u8) { + (self.get_width(), self.get_height()) + } + + /// Starts this icon animation. + pub fn start(&mut self) { + let raw = self.as_raw(); + // SAFETY: `raw` is valid + unsafe { sys::icon_animation_start(raw) } + } + + /// Stops this icon animation. + pub fn stop(&mut self) { + let raw = self.as_raw(); + // SAFETY: `raw` is valid + unsafe { sys::icon_animation_stop(raw) } + } + + /// Checks if the current frame is the last one. + pub fn is_last_frame(&self) -> bool { + let raw = self.as_raw(); + // SAFETY: `raw` is valid + unsafe { sys::icon_animation_is_last_frame(raw) } + } +} + +/// Plain alloc-free wrapper over a [`SysIconAnimation`]. +struct IconAnimationInner(NonNull); + +impl IconAnimationInner { + /// Creates a new icon animation wrapper for the specified icon. + /// + /// # Safety + /// + /// `icon` should outlive the created wrapper + /// and should not mutate during this wrapper's existence. + unsafe fn new(icon: *const SysIcon) -> Self { + // SAFETY: allocation either succeeds producing the valid pointer + // or stops the system on OOM, + // `icon` is a valid pointer and `icon` outlives this animation + Self(unsafe { NonNull::new_unchecked(sys::icon_animation_alloc(icon)) }) + } +} + +impl Drop for IconAnimationInner { + fn drop(&mut self) { + let raw = self.0.as_ptr(); + // SAFETY: `raw` is a valid pointer + // which should have been created via `icon_animation_alloc` + unsafe { sys::icon_animation_free(raw) } + } +} + +/// View over system Icon Animation. +/// +/// This is passed to [callbacks](IconAnimationCallbacks) of [`IconAnimation`]. +pub struct IconAnimationView<'a> { + raw: NonNull, + _lifetime: PhantomData<&'a SysIconAnimation>, +} + +impl IconAnimationView<'_> { + /// Construct an `IconAnimationView` from a raw pointer. + /// + /// # Safety + /// + /// `raw` should be a valid non-null pointer to [`sys::Canvas`] + /// and the lifetime should be outlived by `raw` validity scope. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ``` + /// use flipperzero::gui::icon_animation::IconAnimationView; + /// + /// let ptr = todo!(); + /// let icon_animation = unsafe { IconAnimationView::from_raw(ptr) }; + /// ``` + pub unsafe fn from_raw(raw: *mut SysIconAnimation) -> Self { + Self { + // SAFETY: caller should provide a valid pointer + raw: unsafe { NonNull::new_unchecked(raw) }, + _lifetime: PhantomData, + } + } + + pub fn get_width(&self) -> u8 { + let raw = self.raw.as_ptr(); + // SAFETY: `raw` is valid + unsafe { sys::icon_animation_get_width(raw) } + } + + pub fn get_height(&self) -> u8 { + let raw = self.raw.as_ptr(); + // SAFETY: `raw` is valid + unsafe { sys::icon_animation_get_height(raw) } + } + + pub fn get_dimensions(&self) -> (u8, u8) { + (self.get_width(), self.get_height()) + } + + // TODO: decide if these methods should be available in view, + // i.e. if it is sound to call start/stop from callbacks + // pub fn start(&mut self) { + // let raw = self.raw.as_ptr(); + // // SAFETY: `raw` is valid + // unsafe { sys::icon_animation_start(raw) } + // } + // + // pub fn stop(&mut self) { + // let raw = self.raw.as_ptr(); + // // SAFETY: `raw` is valid + // unsafe { sys::icon_animation_stop(raw) } + // } + + pub fn is_last_frame(&self) -> bool { + let raw = self.raw.as_ptr(); + // SAFETY: `raw` is valid + unsafe { sys::icon_animation_is_last_frame(raw) } + } +} + +/// Callbacks of the [`IconAnimation`]. +#[allow(unused_variables)] +pub trait IconAnimationCallbacks { + fn on_update(&mut self, icon_animation: IconAnimationView) {} +} + +/// Stub implementation, use it whenever callbacks are not needed. +impl IconAnimationCallbacks for () {} diff --git a/crates/flipperzero/src/gui/mod.rs b/crates/flipperzero/src/gui/mod.rs index d4f767e8..31c3fef4 100644 --- a/crates/flipperzero/src/gui/mod.rs +++ b/crates/flipperzero/src/gui/mod.rs @@ -1,3 +1,204 @@ //! GUI service. +mod gui_layer; + pub mod canvas; +pub mod icon; +pub mod icon_animation; +pub mod view; +pub mod view_dispatcher; +pub mod view_port; + +use core::{ + ffi::c_char, + ops::{Deref, DerefMut}, +}; + +use flipperzero_sys::{self as sys, furi::UnsafeRecord, Canvas as SysCanvas, Gui as SysGui}; +pub use gui_layer::*; + +use crate::{ + gui::{ + canvas::CanvasView, + view_port::{ViewPort, ViewPortCallbacks}, + }, + input::InputEvent, +}; + +/// System GUI wrapper. +pub struct Gui { + raw: UnsafeRecord, +} + +impl Gui { + /// Furi record corresponding to GUI. + pub const RECORD: *const c_char = sys::c_string!("gui"); + + /// Creates a new GUI. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ``` + /// # use flipperzero::gui::{view_port::ViewPort, Gui, GuiLayer}; + /// let view_port = ViewPort::new(()); + /// // create a GUI with a view port added to it + /// let mut gui = Gui::new(); + /// let view_port = gui.add_view_port(view_port, GuiLayer::Desktop); + /// ``` + pub fn new() -> Self { + // SAFETY: `RECORD` is a constant + let gui = unsafe { UnsafeRecord::open(Self::RECORD) }; + + Self { raw: gui } + } + + #[inline] + #[must_use] + pub fn as_raw(&self) -> *mut SysGui { + self.raw.as_raw() + } + + pub fn add_view_port( + &mut self, + view_port: ViewPort, + layer: GuiLayer, + ) -> GuiViewPort<'_, VPC> { + let raw = self.as_raw(); + let view_port_ptr = view_port.as_raw(); + let layer = layer.into(); + + // SAFETY: all pointers are valid and `view_port` outlives this `Gui` + unsafe { sys::gui_add_view_port(raw, view_port_ptr, layer) }; + + GuiViewPort { + parent: self, + view_port, + } + } + + pub fn get_frame_buffer_size(&self) -> usize { + let raw = self.as_raw(); + // SAFETY: `raw` is a valid pointer + unsafe { sys::gui_get_framebuffer_size(raw) } + } + + pub fn set_lockdown(&self, lockdown: bool) { + let raw = self.raw.as_raw(); + // SAFETY: `raw` is a valid pointer + unsafe { sys::gui_set_lockdown(raw, lockdown) } + } + + // TODO: separate `GuiCanvas` (locking the parent) + // and `Canvas` (independent of the parent) + pub fn direct_draw_acquire(&mut self) -> ExclusiveCanvas<'_> { + let raw = self.as_raw(); + + // SAFETY: `raw` is a valid pointer + let canvas = unsafe { CanvasView::from_raw(sys::gui_direct_draw_acquire(raw)) }; + + ExclusiveCanvas { gui: self, canvas } + } + + // TODO: canvas method + // TODO: callback methods +} + +impl Default for Gui { + fn default() -> Self { + Self::new() + } +} + +/// `ViewPort` bound to a `Gui`. +pub struct GuiViewPort<'a, VPC: ViewPortCallbacks> { + parent: &'a Gui, + view_port: ViewPort, +} + +impl<'a, VPC: ViewPortCallbacks> GuiViewPort<'a, VPC> { + pub fn view_port(&self) -> &ViewPort { + &self.view_port + } + + pub fn view_port_mut(&mut self) -> &mut ViewPort { + &mut self.view_port + } + + pub fn send_to_front(&mut self) { + let gui = self.parent.raw.as_raw(); + let view_port = self.view_port.as_raw(); + + // SAFETY: `self.parent` outlives this `GuiVewPort` + unsafe { sys::gui_view_port_send_to_front(gui, view_port) }; + } + + // FIXME: `gui_view_port_send_to_back` is not present in bindings + // pub fn send_to_back(&mut self) { + // let gui = self.gui.as_raw(); + // let view_port = self.view_port.as_raw(); + // + // unsafe { sys::gui_view_port_send_to_back(gui, view_port) }; + // } + + pub fn update(&mut self) { + let view_port = self.view_port.as_raw(); + + // SAFETY: `view_port` is a valid pointer + unsafe { sys::view_port_update(view_port) } + } +} + +impl Drop for GuiViewPort<'_, VPC> { + fn drop(&mut self) { + let gui = self.parent.raw.as_raw(); + let view_port = self.view_port().as_raw(); + + // SAFETY: `gui` and `view_port` are valid pointers + // and this view port should have been added to the gui on creation + unsafe { + sys::view_port_enabled_set(view_port, false); + sys::gui_remove_view_port(gui, view_port); + // the object has to be deallocated since the ownership was transferred to the `Gui` + sys::view_port_free(view_port); + } + } +} + +pub trait GuiCallbacks { + fn on_draw(&mut self, _canvas: *mut SysCanvas) {} + fn on_input(&mut self, _event: InputEvent) {} +} + +impl GuiCallbacks for () {} + +/// Exclusively accessible canvas. +pub struct ExclusiveCanvas<'a> { + gui: &'a mut Gui, + canvas: CanvasView<'a>, +} + +impl Drop for ExclusiveCanvas<'_> { + fn drop(&mut self) { + let gui = self.gui.as_raw(); + // SAFETY: this instance should have been created from `gui` + // using `gui_direct_draw_acquire` + // and will no longer be available since it is dropped + unsafe { sys::gui_direct_draw_release(gui) }; + } +} + +impl<'a> Deref for ExclusiveCanvas<'a> { + type Target = CanvasView<'a>; + + fn deref(&self) -> &Self::Target { + &self.canvas + } +} + +impl<'a> DerefMut for ExclusiveCanvas<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.canvas + } +} diff --git a/crates/flipperzero/src/gui/view.rs b/crates/flipperzero/src/gui/view.rs new file mode 100644 index 00000000..c896d82a --- /dev/null +++ b/crates/flipperzero/src/gui/view.rs @@ -0,0 +1,55 @@ +use core::ptr::NonNull; + +use flipperzero_sys::{self as sys, View as SysView}; + +use crate::{gui::canvas::CanvasView, input::InputEvent, internals::alloc::NonUniqueBox}; + +/// UI view. +pub struct View { + inner: ViewInner, + callbacks: NonUniqueBox, +} + +impl View { + pub fn new(callbacks: C) -> Self { + let inner = ViewInner::new(); + let callbacks = NonUniqueBox::new(callbacks); + + Self { inner, callbacks } + } + + /// Creates a copy of raw pointer to the [`sys::View`]. + #[inline] + #[must_use] + pub fn as_raw(&self) -> *mut SysView { + self.inner.0.as_ptr() + } +} + +/// Plain alloc-free wrapper over a [`SysView`]. +struct ViewInner(NonNull); + +impl ViewInner { + fn new() -> Self { + // SAFETY: allocation either succeeds producing a valid non-null pointer + // or stops the system on OOM + Self(unsafe { NonNull::new_unchecked(sys::view_alloc()) }) + } +} + +impl Drop for ViewInner { + fn drop(&mut self) { + let raw = self.0.as_ptr(); + // SAFETY: `raw` is valid + unsafe { sys::view_free(raw) } + } +} + +#[allow(unused_variables)] +pub trait ViewCallbacks { + fn on_draw(&mut self, canvas: CanvasView) {} + fn on_input(&mut self, event: InputEvent) {} + // TODO: the remaining callbacks and actual usage of callbacks +} + +impl ViewCallbacks for () {} diff --git a/crates/flipperzero/src/gui/view_dispatcher/mod.rs b/crates/flipperzero/src/gui/view_dispatcher/mod.rs new file mode 100644 index 00000000..3b8ae2e3 --- /dev/null +++ b/crates/flipperzero/src/gui/view_dispatcher/mod.rs @@ -0,0 +1,414 @@ +mod r#type; + +use alloc::collections::BTreeSet; +use core::{ + ffi::c_void, + marker::PhantomData, + num::NonZeroU32, + ptr::{self, NonNull}, +}; + +use flipperzero_sys::{self as sys, ViewDispatcher as SysViewDispatcher}; +pub use r#type::*; + +use crate::{ + gui::{view::View, Gui}, + internals::alloc::NonUniqueBox, +}; + +type ViewSet = BTreeSet; + +pub mod view_id { + + /// Special view ID which hides drawing view_port. + const NONE: u32 = 0xFFFFFFFF; + + /// Special view ID which ignores navigation event. + const IGNORE: u32 = 0xFFFFFFFE; +} + +pub struct ViewDispatcher<'a, C: ViewDispatcherCallbacks, const QUEUE: bool = true> { + inner: ViewDispatcherInner, + context: NonUniqueBox>, + _phantom: PhantomData<&'a mut Gui>, +} + +struct Context { + view_dispatcher: NonNull, + callbacks: C, + // TODO: propose API to Flipper for checked view addition/removal + views: ViewSet, +} + +impl<'a, C: ViewDispatcherCallbacks, const QUEUE: bool> ViewDispatcher<'a, C, QUEUE> { + /// + /// # Examples + /// + /// Basic usage: + /// + /// ``` + /// # use flipperzero::{ + /// # gui::{ + /// # view_dispatcher::{ + /// # ViewDispatcher, ViewDispatcherCallbacks, + /// # ViewDispatcherRef, ViewDispatcherOps, ViewDispatcherType, + /// # }, + /// # Gui, + /// # }, + /// # log, + /// # }; + /// struct MyCallbacks { + /// value: u32, + /// } + /// impl ViewDispatcherCallbacks for MyCallbacks { + /// fn on_custom(&mut self, view_dispatcher: ViewDispatcherRef<'_>, event: u32) -> bool { + /// log!("{} + {} = {}", self.value, event, self.value + event); + /// true + /// } + /// } + /// let mut gui = Gui::new(); + /// let mut view_dispatcher = ViewDispatcher::new(MyCallbacks { + /// value: 10 + /// }, &mut gui, ViewDispatcherType::Fullscreen); + /// + /// view_dispatcher.send_custom_event(20); + /// // should print `10 + 20 = 30` + /// ``` + pub fn new(callbacks: C, gui: &'a Gui, kind: ViewDispatcherType) -> Self { + // discover which callbacks should be registered + let register_custom_event = !ptr::eq( + C::on_custom as *const c_void, + <() as ViewDispatcherCallbacks>::on_custom as *const c_void, + ); + let register_navigation_callback = !ptr::eq( + C::on_navigation as *const c_void, + <() as ViewDispatcherCallbacks>::on_navigation as *const c_void, + ); + let tick_period = (!ptr::eq( + C::on_tick as *const c_void, + <() as ViewDispatcherCallbacks>::on_tick as *const c_void, + )) + .then(|| callbacks.tick_period()); + + let inner = ViewDispatcherInner::new(); + let context = NonUniqueBox::new(Context { + view_dispatcher: inner.0, + callbacks, + views: BTreeSet::new(), + }); + + { + let raw = inner.0.as_ptr(); + let gui = gui.as_raw(); + let kind = kind.into(); + // SAFETY: both pointers are valid and `kind` is a valid numeric value + // and the newly created view dispatcher does not have a Gui yet + unsafe { sys::view_dispatcher_attach_to_gui(raw, gui, kind) }; + } + + // SAFETY: both pointers are guaranteed to be non-null + let view_dispatcher = Self { + inner, + context, + _phantom: PhantomData, + }; + + let raw = view_dispatcher.as_raw(); + if QUEUE { + // SAFETY: `raw` is a valid pointer + // and corresponds to a newly created `ViewDispatcher` + // which does not have a queue yet + unsafe { sys::view_dispatcher_enable_queue(raw) }; + } + + // and store context if at least one event should be registered + if register_custom_event || register_navigation_callback || tick_period.is_some() { + let context = view_dispatcher.context.as_ptr().cast(); + // SAFETY: `raw` is valid + // and `callbacks` is valid and lives with this struct + unsafe { sys::view_dispatcher_set_event_callback_context(raw, context) }; + } + + if register_custom_event { + pub unsafe extern "C" fn dispatch_custom( + context: *mut c_void, + event: u32, + ) -> bool { + let context: *mut Context = context.cast(); + // SAFETY: `context` is stored in a `Box` which is a member of `ViewDispatcher` + // and the callback is accessed exclusively by this function + let context = unsafe { &mut *context }; + context.callbacks.on_custom( + ViewDispatcherRef { + raw: context.view_dispatcher, + views: &mut context.views, + _phantom: PhantomData, + }, + event, + ) + } + + let callback = Some(dispatch_custom:: as _); + // SAFETY: `raw` is valid + // and `callbacks` is valid and lives with this struct + unsafe { sys::view_dispatcher_set_custom_event_callback(raw, callback) }; + } + if register_navigation_callback { + pub unsafe extern "C" fn dispatch_navigation( + context: *mut c_void, + ) -> bool { + let context: *mut Context = context.cast(); + // SAFETY: `context` is stored in a `Box` which is a member of `ViewDispatcher` + // and the callback is accessed exclusively by this function + let context = unsafe { &mut *context }; + context.callbacks.on_navigation(ViewDispatcherRef { + raw: context.view_dispatcher, + views: &mut context.views, + _phantom: PhantomData, + }) + } + + let callback = Some(dispatch_navigation:: as _); + // SAFETY: `raw` is valid + // and `callbacks` is valid and lives with this struct + unsafe { sys::view_dispatcher_set_navigation_event_callback(raw, callback) }; + } + if let Some(tick_period) = tick_period { + pub unsafe extern "C" fn dispatch_tick( + context: *mut c_void, + ) { + let context: *mut Context = context.cast(); + // SAFETY: `context` is stored in a `Box` which is a member of `ViewDispatcher` + // and the callback is accessed exclusively by this function + let context = unsafe { &mut *context }; + context.callbacks.on_tick(ViewDispatcherRef { + raw: context.view_dispatcher, + views: &mut context.views, + _phantom: PhantomData, + }); + } + + let tick_period = tick_period.get(); + let callback = Some(dispatch_tick:: as _); + // SAFETY: `raw` is valid + // and `callbacks` is valid and lives with this struct + unsafe { sys::view_dispatcher_set_tick_event_callback(raw, callback, tick_period) }; + } + + view_dispatcher + } + + #[inline] + #[must_use] + pub const fn as_raw(&self) -> *mut SysViewDispatcher { + self.inner.0.as_ptr() + } +} + +impl<'a, C: ViewDispatcherCallbacks> ViewDispatcher<'a, C, false> { + // /// Creates a new view dispatcher without a queue. + // /// + // /// This is equivalent to calling: [`ViewDispatcher::new`] with `QUEUE` set to `false`. + // pub fn with_no_queue(callbacks: C) -> Self { + // Self::new(callbacks) + // } + + /// Enables the queue for this view dispatcher. + /// + /// # Examples + /// + /// ``` + /// # use flipperzero::gui::view_dispatcher::ViewDispatcher; + /// // create a view dispatcher with no queue + /// let view_dispatcher = ViewDispatcher::with_no_queue(()); + /// // ... do something ... + /// // and now enable the queue for the view dispatcher + /// let view_dispatcher = view_dispatcher.enable_queue(); + /// ``` + pub fn enable_queue(self) -> ViewDispatcher<'a, C, true> { + // SAFETY: `raw` is a valid pointer + // and corresponds to a `ViewDispatcher` + // which does not have a queue yet + let raw = self.as_raw(); + unsafe { sys::view_dispatcher_enable_queue(raw) }; + + ViewDispatcher { + inner: self.inner, + context: self.context, + _phantom: self._phantom, + } + } +} + +impl<'a, C: ViewDispatcherCallbacks> ViewDispatcher<'a, C, true> { + /// Runs this view dispatcher. + /// + /// This will block until the view dispatcher gets stopped. + pub fn run(self) -> Self { + let raw = self.as_raw(); + // SAFETY: `raw` is valid + // and this is a `ViewDispatcher` with a queue + unsafe { sys::view_dispatcher_run(raw) }; + self + } +} + +/// Reference to a ViewDispatcher. +pub struct ViewDispatcherRef<'a> { + raw: NonNull, + views: &'a mut ViewSet, + _phantom: PhantomData<&'a mut SysViewDispatcher>, +} + +/// Operations on an initialized view dispatcher which has a queue associated with it. +pub trait ViewDispatcherOps: internals::InitViewDispatcherRaw { + fn send_custom_event(&mut self, event: u32) { + let raw = self.raw(); + // SAFETY: `raw` should be valid and point to a ViewDispatcher with a queue + unsafe { sys::view_dispatcher_send_custom_event(raw, event) }; + } + + /// Stops this view dispatcher. + /// + /// This will make the [ViewDispatcher::<_, true>::run] caller unfreeze. + fn stop(&mut self) { + let raw = self.raw(); + // SAFETY: `raw` should be valid and point to a ViewDispatcher with a queue + unsafe { sys::view_dispatcher_stop(raw) }; + } + + // fn add_view(&mut self, id: u32, view: &mut View<'_>) { + // if self.views().insert(id) { + // let raw = self.raw(); + // unsafe { sys::view_dispatcher_add_view(raw, id) }; + // } + // } + + fn switch_to_view(&mut self, id: u32) { + if self.views().contains(&id) { + let raw = self.raw(); + unsafe { sys::view_dispatcher_switch_to_view(raw, id) }; + } + } + + fn remove_view(&mut self, id: u32) -> Option<()> { + if self.views_mut().remove(&id) { + let raw = self.raw(); + unsafe { sys::view_dispatcher_remove_view(raw, id) } + Some(()) + } else { + None + } + } +} +impl ViewDispatcherOps for T {} + +unsafe impl internals::InitViewDispatcherRaw + for ViewDispatcher<'_, C, true> +{ + #[inline(always)] + fn raw(&self) -> *mut SysViewDispatcher { + self.inner.0.as_ptr() + } + + #[inline(always)] + fn views(&self) -> &ViewSet { + let context = self.context.as_ptr(); + // SAFETY: if this method is accessed through `ViewDispatcher` + // then no one else should be able to use it + &unsafe { &*context }.views + } + + #[inline(always)] + fn views_mut(&mut self) -> &mut ViewSet { + let context = self.context.as_ptr(); + // SAFETY: if this method is accessed through `ViewDispatcher` + // then no one else should be able to use it + &mut unsafe { &mut *context }.views + } +} + +unsafe impl internals::InitViewDispatcherRaw for ViewDispatcherRef<'_> { + #[inline(always)] + fn raw(&self) -> *mut SysViewDispatcher { + self.raw.as_ptr() + } + + #[inline(always)] + fn views(&self) -> &ViewSet { + self.views + } + + #[inline(always)] + fn views_mut(&mut self) -> &mut ViewSet { + self.views + } +} + +/// Internal representation of view dispatcher. +/// This is a thin non-null pointer to [`SysViewDispatcher`] +/// which performs its automatic [allocation][Self::new] and [deallocation](Self::drop). +struct ViewDispatcherInner(NonNull); + +impl ViewDispatcherInner { + fn new() -> Self { + // SAFETY: allocation either succeeds producing the valid pointer + // or stops the system on OOM, + Self(unsafe { NonNull::new_unchecked(sys::view_dispatcher_alloc()) }) + } +} + +impl Drop for ViewDispatcherInner { + fn drop(&mut self) { + let raw = self.0.as_ptr(); + // SAFETY: `raw` is valid + unsafe { sys::view_dispatcher_free(raw) }; + } +} + +#[allow(unused_variables)] +pub trait ViewDispatcherCallbacks { + /// Handles a custom event, + /// + /// + fn on_custom(&mut self, view_dispatcher: ViewDispatcherRef<'_>, event: u32) -> bool { + false + } + + fn on_navigation(&mut self, view_dispatcher: ViewDispatcherRef<'_>) -> bool { + false + } + + fn on_tick(&mut self, view_dispatcher: ViewDispatcherRef<'_>) {} + + #[must_use] + fn tick_period(&self) -> NonZeroU32 { + // Some arbitrary default + NonZeroU32::new(100).unwrap() + } +} + +impl ViewDispatcherCallbacks for () { + // use MAX value since this should never be used normally + fn tick_period(&self) -> NonZeroU32 { + NonZeroU32::MAX + } +} + +mod internals { + use super::{SysViewDispatcher, ViewSet}; + + /// A structure wrapping a raw [`SysViewDispatcher`] with an initialized queue. + /// + /// # Safety + /// + /// This trait should be implemented so that the provided pointer is always valid + /// and points to the [`SysViewDispatcher`] which has a queue. + pub unsafe trait InitViewDispatcherRaw { + fn raw(&self) -> *mut SysViewDispatcher; + + fn views(&self) -> &ViewSet; + + fn views_mut(&mut self) -> &mut ViewSet; + } +} diff --git a/crates/flipperzero/src/gui/view_dispatcher/type.rs b/crates/flipperzero/src/gui/view_dispatcher/type.rs new file mode 100644 index 00000000..919b4405 --- /dev/null +++ b/crates/flipperzero/src/gui/view_dispatcher/type.rs @@ -0,0 +1,69 @@ +use core::fmt::{self, Display, Formatter}; + +use flipperzero_sys::{self as sys, ViewDispatcherType as SysViewDispatcherType}; +use ufmt::{derive::uDebug, uDisplay, uWrite, uwrite}; + +use crate::internals::macros::impl_std_error; + +/// View dispatcher view port placement. +/// +/// Corresponds to raw [`SysViewDispatcherType`]. +#[derive(Copy, Clone, Debug, uDebug, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub enum ViewDispatcherType { + /// Desktop layer: fullscreen with status bar on top of it. For internal usage. + Desktop, + /// Window layer: with status bar. + Window, + /// Fullscreen layer: without status bar. + Fullscreen, +} + +impl TryFrom for ViewDispatcherType { + type Error = FromSysViewDispatcherTypeError; + + fn try_from(value: SysViewDispatcherType) -> Result { + Ok(match value { + sys::ViewDispatcherType_ViewDispatcherTypeDesktop => ViewDispatcherType::Desktop, + sys::ViewDispatcherType_ViewDispatcherTypeWindow => ViewDispatcherType::Window, + sys::ViewDispatcherType_ViewDispatcherTypeFullscreen => ViewDispatcherType::Fullscreen, + invalid => Err(Self::Error::Invalid(invalid))?, + }) + } +} + +impl From for SysViewDispatcherType { + fn from(value: ViewDispatcherType) -> Self { + match value { + ViewDispatcherType::Desktop => sys::ViewDispatcherType_ViewDispatcherTypeDesktop, + ViewDispatcherType::Window => sys::ViewDispatcherType_ViewDispatcherTypeWindow, + ViewDispatcherType::Fullscreen => sys::ViewDispatcherType_ViewDispatcherTypeFullscreen, + } + } +} + +/// An error which may occur while trying +/// to convert raw [`SysViewDispatcherType`] to [`ViewDispatcherType`]. +#[derive(Copy, Clone, Debug, uDebug, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub enum FromSysViewDispatcherTypeError { + /// The [`SysViewDispatcherType`] is an invalid value. + Invalid(SysViewDispatcherType), +} + +impl Display for FromSysViewDispatcherTypeError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let Self::Invalid(id) = self; + write!(f, "view dispatcher type ID {id} is invalid") + } +} + +impl uDisplay for FromSysViewDispatcherTypeError { + fn fmt(&self, f: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error> + where + W: uWrite + ?Sized, + { + let Self::Invalid(id) = self; + uwrite!(f, "view dispatcher type ID {} is invalid", id) + } +} + +impl_std_error!(FromSysInputTypeError); diff --git a/crates/flipperzero/src/gui/view_port/mod.rs b/crates/flipperzero/src/gui/view_port/mod.rs new file mode 100644 index 00000000..bb7e7935 --- /dev/null +++ b/crates/flipperzero/src/gui/view_port/mod.rs @@ -0,0 +1,376 @@ +//! ViewPort APIs + +mod orientation; + +use core::{ + ffi::c_void, + num::NonZeroU8, + ptr::{self, NonNull}, +}; + +use flipperzero_sys::{ + self as sys, Canvas as SysCanvas, ViewPort as SysViewPort, + ViewPortOrientation as SysViewPortOrientation, +}; +pub use orientation::*; + +use crate::{gui::canvas::CanvasView, input::InputEvent, internals::alloc::NonUniqueBox}; + +/// System ViewPort. +pub struct ViewPort { + inner: ViewPortInner, + callbacks: NonUniqueBox, +} + +impl ViewPort { + /// Creates a new `ViewPort`. + /// + /// # Example + /// + /// Basic usage: + /// + /// ``` + /// use flipperzero::gui::view_port::ViewPort; + /// + /// let view_port = ViewPort::new(()); + /// ``` + pub fn new(callbacks: C) -> Self { + let inner = ViewPortInner::new(); + let callbacks = NonUniqueBox::new(callbacks); + + let view_port = Self { inner, callbacks }; + let raw = view_port.as_raw(); + + { + pub unsafe extern "C" fn dispatch_draw( + canvas: *mut SysCanvas, + context: *mut c_void, + ) { + // SAFETY: `canvas` is guaranteed to be a valid pointer + let canvas = unsafe { CanvasView::from_raw(canvas) }; + + let context: *mut C = context.cast(); + // SAFETY: `context` is stored in a `Box` which is a member of `ViewPort` + // and the callback is accessed exclusively by this function + unsafe { &mut *context }.on_draw(canvas); + } + + if !ptr::eq( + C::on_draw as *const c_void, + <() as ViewPortCallbacks>::on_draw as *const c_void, + ) { + let context = view_port.callbacks.as_ptr().cast(); + let callback = Some(dispatch_draw:: as _); + // SAFETY: `raw` is valid + // and `callbacks` is valid and lives with this struct + unsafe { sys::view_port_draw_callback_set(raw, callback, context) }; + } + } + { + pub unsafe extern "C" fn dispatch_input( + input_event: *mut sys::InputEvent, + context: *mut c_void, + ) { + let input_event: InputEvent = (unsafe { *input_event }) + .try_into() + .expect("`input_event` should be a valid event"); + + let context: *mut C = context.cast(); + // SAFETY: `context` is stored in a pinned Box which is a member of `ViewPort` + // and the callback is accessed exclusively by this function + unsafe { &mut *context }.on_input(input_event); + } + + if !ptr::eq( + C::on_input as *const c_void, + <() as ViewPortCallbacks>::on_input as *const c_void, + ) { + let context = view_port.callbacks.as_ptr().cast(); + let callback = Some(dispatch_input:: as _); + + // SAFETY: `raw` is valid + // and `callbacks` is valid and lives with this struct + unsafe { sys::view_port_input_callback_set(raw, callback, context) }; + } + } + + view_port + } + + /// Creates a copy of the raw pointer to the [`sys::ViewPort`]. + #[inline] + #[must_use] + pub fn as_raw(&self) -> *mut SysViewPort { + self.inner.0.as_ptr() + } + + /// Sets the width of this `ViewPort`. + /// Empty `width` means automatic. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ``` + /// use std::num::NonZeroU8; + /// use flipperzero::gui::view_port::ViewPort; + /// + /// let mut view_port = ViewPort::new(()); + /// view_port.set_width(NonZeroU8::new(128u8)); + /// ``` + /// + /// Resize `ViewPort` to automatically selected width: + /// + /// ``` + /// use flipperzero::gui::view_port::ViewPort; + /// + /// let mut view_port = ViewPort::new(()); + /// view_port.set_width(None); + /// ``` + pub fn set_width(&mut self, width: Option) { + let width = width.map_or(0u8, NonZeroU8::into); + let raw = self.as_raw(); + // SAFETY: `raw` is always valid + // and there are no `width` constraints + unsafe { sys::view_port_set_width(raw, width) } + } + + /// Gets the width of this `ViewPort`. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ``` + /// use flipperzero::gui::view_port::ViewPort; + /// + /// let view_port = ViewPort::new(()); + /// let width = view_port.get_width(); + /// ``` + pub fn get_width(&self) -> Option { + let raw = self.as_raw(); + // SAFETY: `raw` is always valid + NonZeroU8::new(unsafe { sys::view_port_get_width(raw) }) + } + + /// Sets the height of this `ViewPort`. + /// Empty `height` means automatic. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ``` + /// use std::num::NonZeroU8; + /// use flipperzero::gui::view_port::ViewPort; + /// + /// let mut view_port = ViewPort::new(()); + /// view_port.set_height(NonZeroU8::new(128u8)); + /// ``` + /// + /// Resize `ViewPort` to automatically selected height: + /// + /// ``` + /// use flipperzero::gui::view_port::ViewPort; + /// + /// let mut view_port = ViewPort::new(()); + /// view_port.set_height(None); + /// ``` + pub fn set_height(&mut self, height: Option) { + let height = height.map_or(0u8, NonZeroU8::into); + let raw = self.as_raw(); + // SAFETY: `raw` is always valid + // and there are no `height` constraints + unsafe { sys::view_port_set_height(raw, height) } + } + + /// Gets the height of this `ViewPort`. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ``` + /// use flipperzero::gui::view_port::ViewPort; + /// + /// let view_port = ViewPort::new(()); + /// let height = view_port.get_height(); + /// ``` + pub fn get_height(&self) -> Option { + let raw = self.as_raw(); + // SAFETY: `raw` is always valid + NonZeroU8::new(unsafe { sys::view_port_get_height(raw) }) + } + + /// Sets the dimensions of this `ViewPort`. + /// Empty `dimensions` means automatic. + /// + /// # Example + /// + /// Basic usage: + /// + /// ``` + /// use std::num::NonZeroU8; + /// use flipperzero::gui::view_port::ViewPort; + /// + /// let mut view_port = ViewPort::new(()); + /// view_port.set_dimensions(Some((NonZeroU8::new(120).unwrap(), NonZeroU8::new(80).unwrap()))); + /// ``` + /// + /// Resize `ViewPort` to automatically selected dimensions: + /// + /// ``` + /// use flipperzero::gui::view_port::ViewPort; + /// + /// let mut view_port = ViewPort::new(()); + /// view_port.set_dimensions(None); + /// ``` + pub fn set_dimensions(&mut self, dimensions: Option<(NonZeroU8, NonZeroU8)>) { + match dimensions { + Some((width, height)) => { + self.set_width(Some(width)); + self.set_height(Some(height)); + } + None => { + self.set_width(None); + self.set_height(None); + } + } + } + + pub fn update(&mut self) { + let raw = self.as_raw(); + // SAFETY: `raw` is always valid + unsafe { sys::view_port_update(raw) } + } + + /// Gets the dimensions of this `ViewPort`. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ``` + /// use flipperzero::gui::view_port::ViewPort; + /// + /// let view_port = ViewPort::new(()); + /// let (width, height) = view_port.get_dimensions(); + /// ``` + pub fn get_dimensions(&self) -> (Option, Option) { + (self.get_width(), self.get_height()) + } + + /// Sets the orientation of this `ViewPort`. + /// + /// # Example + /// + /// Basic usage: + /// + /// ``` + /// use flipperzero::gui::view_port::{ViewPort, ViewPortOrientation}; + /// let mut view_port = ViewPort::new(()); + /// view_port.set_orientation(ViewPortOrientation::Vertical); + /// ``` + pub fn set_orientation(&mut self, orientation: ViewPortOrientation) { + let orientation = SysViewPortOrientation::from(orientation); + + let raw = self.as_raw(); + // SAFETY: `raw` is always valid + // and `orientation` is guaranteed to be valid by `From` implementation + unsafe { sys::view_port_set_orientation(raw, orientation) } + } + + /// Gets the orientation of this `ViewPort`. + /// + /// # Example + /// + /// Basic usage: + /// + /// ``` + /// use std::num::NonZeroU8; + /// use flipperzero::gui::view_port::{ViewPort, ViewPortOrientation}; + /// + /// let mut view_port = ViewPort::new(()); + /// let orientation = view_port.get_orientation(); + /// ``` + pub fn get_orientation(&self) -> ViewPortOrientation { + let raw = self.as_raw().cast_const(); + // SAFETY: `raw` is always valid + unsafe { sys::view_port_get_orientation(raw) } + .try_into() + .expect("`view_port_get_orientation` should produce a valid `ViewPort`") + } + + /// Enables or disables this `ViewPort` rendering. + /// + /// `ViewPort` is enabled by default. + /// + /// # Example + /// + /// Basic usage: + /// + /// ``` + /// use flipperzero::gui::view_port::ViewPort; + /// + /// let mut view_port = ViewPort::new(()); + /// view_port.set_enabled(false); + /// ``` + pub fn set_enabled(&mut self, enabled: bool) { + let raw = self.as_raw(); + // SAFETY: `raw` is always valid + unsafe { sys::view_port_enabled_set(raw, enabled) } + } + + /// Checks if this `ViewPort` is enabled. + /// + /// # Example + /// + /// Basic usage: + /// + /// + /// ``` + /// use flipperzero::gui::view_port::ViewPort; + /// + /// let mut view_port = ViewPort::new(()); + /// let enabled = view_port.is_enabled(); + /// ``` + pub fn is_enabled(&self) -> bool { + let raw = self.as_raw(); + // SAFETY: `raw` is always valid + unsafe { sys::view_port_is_enabled(raw) } + } +} + +impl Drop for ViewPort { + fn drop(&mut self) { + // FIXME: unregister from system (whatever this means) + self.set_enabled(false); + } +} + +/// Plain alloc-free wrapper over a [`SysViewPort`]. +struct ViewPortInner(NonNull); + +impl ViewPortInner { + fn new() -> Self { + // SAFETY: allocation either succeeds producing the valid non-null pointer + // or stops the system on OOM + Self(unsafe { NonNull::new_unchecked(sys::view_port_alloc()) }) + } +} + +impl Drop for ViewPortInner { + fn drop(&mut self) { + let raw = self.0.as_ptr(); + // SAFETY: `raw` is a valid pointer + unsafe { sys::view_port_free(raw) }; + } +} + +#[allow(unused_variables)] +pub trait ViewPortCallbacks { + fn on_draw(&mut self, canvas: CanvasView<'_>) {} + fn on_input(&mut self, event: InputEvent) {} +} + +impl ViewPortCallbacks for () {} diff --git a/crates/flipperzero/src/gui/view_port/orientation.rs b/crates/flipperzero/src/gui/view_port/orientation.rs new file mode 100644 index 00000000..79bbb39e --- /dev/null +++ b/crates/flipperzero/src/gui/view_port/orientation.rs @@ -0,0 +1,165 @@ +use core::fmt::{self, Display, Formatter}; + +use flipperzero_sys::{self as sys, ViewPortOrientation as SysViewPortOrientation}; +use ufmt::{derive::uDebug, uDisplay, uWrite, uwrite}; + +use crate::internals::macros::impl_std_error; + +/// Orientation of a view port. +/// +/// Corresponds to raw [`SysViewPortOrientation`]. +/// +/// # Examples +/// +/// Basic +/// +/// ``` +/// # use flipperzero::gui::view_port::ViewPort; +/// # use flipperzero::log; +/// let view_port = ViewPort::new(()); +/// let orientation = view_port.get_orientation(); +/// if matches!(orientation, ViewPortOrientation::Horizontal) { +/// log!("Currently in horizontal orientation") +/// } +/// ``` +#[derive(Copy, Clone, Debug, uDebug, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub enum ViewPortOrientation { + /// Horizontal orientation. + Horizontal, + /// Flipped horizontal orientation. + HorizontalFlip, + /// Vertical orientation. + Vertical, + /// Flipped vertical orientation. + VerticalFlip, +} + +impl ViewPortOrientation { + /// Checks that this orientation is horizontal. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ``` + /// # use flipperzero::gui::view_port::ViewPortOrientation; + /// assert!(ViewPortOrientation::Horizontal.is_horizontal()); + /// assert!(ViewPortOrientation::HorizontalFlip.is_horizontal()); + /// assert!(!ViewPortOrientation::Vertical.is_horizontal()); + /// assert!(!ViewPortOrientation::VerticalFlip.is_horizontal()); + /// ``` + pub const fn is_horizontal(self) -> bool { + matches!(self, Self::Horizontal | Self::HorizontalFlip) + } + + /// Checks that this orientation is vertical. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ``` + /// # use flipperzero::gui::view_port::ViewPortOrientation; + /// assert!(ViewPortOrientation::Vertical.is_vertical()); + /// assert!(ViewPortOrientation::VerticalFlip.is_vertical()); + /// assert!(!ViewPortOrientation::Horizontal.is_vertical()); + /// assert!(!ViewPortOrientation::HorizontalFlip.is_vertical()); + /// ``` + pub const fn is_vertical(self) -> bool { + matches!(self, Self::Vertical | Self::VerticalFlip) + } + + /// Checks that this orientation is flipped. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ``` + /// # use flipperzero::gui::view_port::ViewPortOrientation; + /// assert!(ViewPortOrientation::HorizontalFlip.is_flipped()); + /// assert!(ViewPortOrientation::VerticalFlip.is_flipped()); + /// assert!(!ViewPortOrientation::Horizontal.is_flipped()); + /// assert!(!ViewPortOrientation::Vertical.is_flipped()); + /// ``` + pub const fn is_flipped(self) -> bool { + matches!(self, Self::HorizontalFlip | Self::VerticalFlip) + } +} + +impl TryFrom for ViewPortOrientation { + type Error = FromSysViewPortOrientationError; + + fn try_from(value: SysViewPortOrientation) -> Result { + Ok(match value { + sys::ViewPortOrientation_ViewPortOrientationHorizontal => Self::Horizontal, + sys::ViewPortOrientation_ViewPortOrientationHorizontalFlip => Self::HorizontalFlip, + sys::ViewPortOrientation_ViewPortOrientationVertical => Self::Vertical, + sys::ViewPortOrientation_ViewPortOrientationVerticalFlip => Self::VerticalFlip, + sys::ViewPortOrientation_ViewPortOrientationMAX => Err(Self::Error::Max)?, + invalid => Err(Self::Error::Invalid(invalid))?, + }) + } +} + +impl From for SysViewPortOrientation { + fn from(value: ViewPortOrientation) -> Self { + match value { + ViewPortOrientation::Horizontal => { + sys::ViewPortOrientation_ViewPortOrientationHorizontal + } + ViewPortOrientation::HorizontalFlip => { + sys::ViewPortOrientation_ViewPortOrientationHorizontalFlip + } + ViewPortOrientation::Vertical => sys::ViewPortOrientation_ViewPortOrientationVertical, + ViewPortOrientation::VerticalFlip => { + sys::ViewPortOrientation_ViewPortOrientationVerticalFlip + } + } + } +} + +/// An error which may occur while trying +/// to convert raw [`SysViewPortOrientation`] to [`ViewPortOrientation`]. +#[non_exhaustive] +#[derive(Copy, Clone, Debug, uDebug, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub enum FromSysViewPortOrientationError { + /// The [`SysViewPortOrientation`] + /// is [`MAX`][sys::ViewPortOrientation_ViewPortOrientationMAX] + /// which is a meta-value used to track enum size. + Max, + /// The [`SysViewPortOrientation`] is an invalid value + /// other than [`MAX`][sys::ViewPortOrientation_ViewPortOrientationMAX]. + Invalid(SysViewPortOrientation), +} + +impl Display for FromSysViewPortOrientationError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::Max => write!( + f, + "view port orientation ID {} (MAX) is a meta-value", + sys::GuiLayer_GuiLayerMAX, + ), + Self::Invalid(id) => write!(f, "view port orientation ID {id} is invalid"), + } + } +} + +impl uDisplay for FromSysViewPortOrientationError { + fn fmt(&self, f: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error> + where + W: uWrite + ?Sized, + { + match self { + Self::Max => uwrite!( + f, + "view port orientation ID {} (MAX) is a meta-value", + sys::GuiLayer_GuiLayerMAX, + ), + Self::Invalid(id) => uwrite!(f, "view port orientation ID {} is invalid", id), + } + } +} + +impl_std_error!(FromSysViewPortOrientationError); diff --git a/crates/flipperzero/src/input/key.rs b/crates/flipperzero/src/input/key.rs new file mode 100644 index 00000000..113974bf --- /dev/null +++ b/crates/flipperzero/src/input/key.rs @@ -0,0 +1,118 @@ +use crate::internals::macros::impl_std_error; +use core::ffi::CStr; +use core::fmt::{self, Display, Formatter}; +use flipperzero_sys::{self as sys, InputKey as SysInputKey}; +use ufmt::{derive::uDebug, uDisplay, uWrite, uwrite}; + +/// Input key of a Flipper, i.e. its button. +/// +/// Corresponds to raw [`SysInputKey`]. +#[derive(Copy, Clone, Debug, uDebug, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub enum InputKey { + /// **Up** key (top triangle). + Up, + /// **Down** key (bottom triangle). + Down, + /// **Right** key (right triangle). + Right, + /// **Left** key (left triangle). + Left, + /// **Ok** key (central round). + Ok, + /// **Back** key (right bottom backward arrow). + Back, +} + +impl InputKey { + /// Gets the name of this input key. + /// Unlike `Debug` and `uDebug` which use Rust enu name, + /// this relies on Flipper's API intended for this purpose. + /// + /// # Example + /// + /// Basic usage: + /// + /// ``` + /// # use flipperzero::input::InputKey; + /// assert_eq!(InputKey::Up.name(), "Up"); + /// ``` + pub fn name(self) -> &'static CStr { + let this = SysInputKey::from(self); + // SAFETY: `this` is a valid enum value + // and the returned string is a static string + unsafe { CStr::from_ptr(sys::input_get_key_name(this)) } + } +} + +impl TryFrom for InputKey { + type Error = FromSysInputKeyError; + + fn try_from(value: SysInputKey) -> Result { + Ok(match value { + sys::InputKey_InputKeyUp => Self::Up, + sys::InputKey_InputKeyDown => Self::Down, + sys::InputKey_InputKeyRight => Self::Right, + sys::InputKey_InputKeyLeft => Self::Left, + sys::InputKey_InputKeyOk => Self::Ok, + sys::InputKey_InputKeyBack => Self::Back, + sys::InputKey_InputKeyMAX => Err(Self::Error::Max)?, + invalid => Err(Self::Error::Invalid(invalid))?, + }) + } +} + +impl From for SysInputKey { + fn from(value: InputKey) -> Self { + match value { + InputKey::Up => sys::InputKey_InputKeyUp, + InputKey::Down => sys::InputKey_InputKeyDown, + InputKey::Right => sys::InputKey_InputKeyRight, + InputKey::Left => sys::InputKey_InputKeyLeft, + InputKey::Ok => sys::InputKey_InputKeyOk, + InputKey::Back => sys::InputKey_InputKeyBack, + } + } +} + +/// An error which may occur while trying +/// to convert raw [`SysInputKey`] to [`InputKey`]. +#[derive(Copy, Clone, Debug, uDebug, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub enum FromSysInputKeyError { + /// The [`SysInputKey`] is [`MAX`][sys::InputKey_InputKeyMAX] + /// which is a meta-value used to track enum size. + Max, + /// The [`SysInputKey`] is an invalid value + /// other than [`MAX`][sys::InputKey_InputKeyMAX]. + Invalid(SysInputKey), +} + +impl Display for FromSysInputKeyError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::Max => write!( + f, + "input key ID {} (MAX) is a meta-value", + sys::InputKey_InputKeyMAX, + ), + Self::Invalid(id) => write!(f, "input key ID {id} is invalid"), + } + } +} + +impl uDisplay for FromSysInputKeyError { + fn fmt(&self, f: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error> + where + W: uWrite + ?Sized, + { + match self { + Self::Max => uwrite!( + f, + "input key ID {} (Max) is a meta-value", + sys::InputKey_InputKeyMAX, + ), + Self::Invalid(id) => uwrite!(f, "input key ID {} is invalid", id), + } + } +} + +impl_std_error!(FromSysInputKeyError); diff --git a/crates/flipperzero/src/input/mod.rs b/crates/flipperzero/src/input/mod.rs new file mode 100644 index 00000000..2159d142 --- /dev/null +++ b/crates/flipperzero/src/input/mod.rs @@ -0,0 +1,129 @@ +mod key; +mod r#type; + +use flipperzero_sys::{self as sys, InputEvent as SysInputEvent}; +use ufmt::derive::uDebug; +// public type alias for an anonymous union +pub use sys::InputEvent__bindgen_ty_1 as SysInputEventSequence; + +pub use key::*; +pub use r#type::*; + +/// Input event occurring on user actions. +/// +/// Corresponds to raw [`SysInputEvent`]. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct InputEvent { + /// Sequence qualifying the event. + pub sequence: InputEventSequence, + /// Physical key causing the event. + pub key: InputKey, + /// The type of the event. + pub r#type: InputType, +} + +impl TryFrom for InputEvent { + type Error = FromSysInputEventError; + + fn try_from(value: SysInputEvent) -> Result { + Ok(Self { + sequence: value.__bindgen_anon_1.into(), + key: value.key.try_into()?, + r#type: value.type_.try_into()?, + }) + } +} + +impl From for SysInputEvent { + fn from(value: InputEvent) -> Self { + Self { + __bindgen_anon_1: value.sequence.into(), + key: value.key.into(), + type_: value.r#type.into(), + } + } +} + +/// An error which may occur while trying +/// to convert raw [`SysInputEvent`] to [`InputEvent`]. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub enum FromSysInputEventError { + InvalidKey(FromSysInputKeyError), + InvalidType(FromSysInputTypeError), +} + +impl From for FromSysInputEventError { + fn from(value: FromSysInputKeyError) -> Self { + Self::InvalidKey(value) + } +} + +impl From for FromSysInputEventError { + fn from(value: FromSysInputTypeError) -> Self { + Self::InvalidType(value) + } +} + +/// [`InputEvent`] sequence. +/// +/// This is a transparent view over [`u32`](prim@u32) with the following representation: +/// +/// | Bits | 31..30 | 29..0 | +/// |---------|--------|--------| +/// | Payload | Source | Counter| +/// +/// Corresponds to raw [`SysInputEventSequence`]. +/// +/// # Example usage +/// +/// Decoding a raw `u32` value: +/// +/// ``` +/// use flipperzero::input::InputEventSequence; +/// let sequence = InputEventSequence::from(0b10__000000_10101010_11110000_11111111u32); +/// assert_eq!(0b10, sequence.source()); +/// assert_eq!(0b10101010_11110000_11111111, sequence.counter()); +/// ``` +#[repr(transparent)] +#[derive(Copy, Clone, Debug, uDebug, Hash, Eq, PartialEq, PartialOrd, Ord)] +pub struct InputEventSequence(u32); + +impl InputEventSequence { + const SOURCE_SHIFT: u32 = 30; + const SOURCE_MASK: u32 = (u32::MAX) >> Self::SOURCE_SHIFT; + const COUNTER_MASK: u32 = !(Self::SOURCE_MASK << Self::SOURCE_SHIFT); + + pub const fn source(self) -> u8 { + ((self.0 >> Self::SOURCE_SHIFT) & Self::SOURCE_MASK) as u8 + } + + pub const fn counter(self) -> u32 { + self.0 & Self::COUNTER_MASK + } +} + +impl From for InputEventSequence { + fn from(value: u32) -> Self { + Self(value) + } +} + +impl From for u32 { + fn from(value: InputEventSequence) -> Self { + value.0 + } +} + +impl From for InputEventSequence { + fn from(value: SysInputEventSequence) -> Self { + // SAFETY: both union variants are always valid + // and the bit-field one is just a typed view over the plain one + Self(unsafe { value.sequence }) + } +} + +impl From for SysInputEventSequence { + fn from(value: InputEventSequence) -> Self { + Self { sequence: value.0 } + } +} diff --git a/crates/flipperzero/src/input/type.rs b/crates/flipperzero/src/input/type.rs new file mode 100644 index 00000000..0186633d --- /dev/null +++ b/crates/flipperzero/src/input/type.rs @@ -0,0 +1,118 @@ +use crate::internals::macros::impl_std_error; +use core::ffi::CStr; +use core::fmt::{self, Display, Formatter}; +use flipperzero_sys::{self as sys, InputType as SysInputType}; +use ufmt::{derive::uDebug, uDisplay, uWrite, uwrite}; + +/// Input type of a Flipper's button describing +/// the kind of action on it (physical or logical). +/// +/// Corresponds to raw [`SysInputType`]. +#[derive(Copy, Clone, Debug, uDebug, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub enum InputType { + /// Press event, emitted after debounce. + Press, + /// Release event, emitted after debounce. + Release, + /// Short event, emitted after [`InputType::Release`] + /// done within `INPUT_LONG_PRESS` interval. + Short, + /// Long event, emitted after `INPUT_LONG_PRESS_COUNTS` interval, + /// asynchronous to [`InputType::Release`]. + Long, + /// Repeat event, emitted with `INPUT_LONG_PRESS_COUNTS` period + /// after [InputType::Long] event. + Repeat, +} + +impl InputType { + /// Gets the name of this input type. + /// Unlike `Debug` and `uDebug` which use Rust enu name, + /// this relies on Flipper's API intended for this purpose. + /// + /// # Example + /// + /// Basic usage: + /// + /// ``` + /// # use flipperzero::input::InputType; + /// assert_eq!(InputType::Release.name(), "Release"); + /// ``` + pub fn name(self) -> &'static CStr { + let this = SysInputType::from(self); + // SAFETY: `this` is a valid enum value + // and the returned string is a static string + unsafe { CStr::from_ptr(sys::input_get_type_name(this)) } + } +} + +impl TryFrom for InputType { + type Error = FromSysInputTypeError; + + fn try_from(value: SysInputType) -> Result { + Ok(match value { + sys::InputType_InputTypePress => Self::Press, + sys::InputType_InputTypeRelease => Self::Release, + sys::InputType_InputTypeShort => Self::Short, + sys::InputType_InputTypeLong => Self::Long, + sys::InputType_InputTypeRepeat => Self::Repeat, + sys::InputType_InputTypeMAX => Err(Self::Error::Max)?, + invalid => Err(Self::Error::Invalid(invalid))?, + }) + } +} + +impl From for SysInputType { + fn from(value: InputType) -> Self { + match value { + InputType::Press => sys::InputType_InputTypePress, + InputType::Release => sys::InputType_InputTypeRelease, + InputType::Short => sys::InputType_InputTypeShort, + InputType::Long => sys::InputType_InputTypeLong, + InputType::Repeat => sys::InputType_InputTypeRepeat, + } + } +} + +/// An error which may occur while trying +/// to convert raw [`SysInputType`] to [`InputType`]. +#[derive(Copy, Clone, Debug, uDebug, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub enum FromSysInputTypeError { + /// The [`SysInputType`] is [`MAX`][sys::InputType_InputTypeMAX] + /// which is a meta-value used to track enum size. + Max, + /// The [`SysInputType`] is an invalid value + /// other than [`MAX`][sys::InputType_InputTypeMAX]. + Invalid(SysInputType), +} + +impl Display for FromSysInputTypeError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::Max => write!( + f, + "input key ID {} (Max) is a meta-value", + sys::InputType_InputTypeMAX, + ), + Self::Invalid(id) => write!(f, "input key ID {id} is invalid"), + } + } +} + +impl uDisplay for FromSysInputTypeError { + fn fmt(&self, f: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error> + where + W: uWrite + ?Sized, + { + match self { + Self::Max => uwrite!( + f, + "input key ID {} (Max) is a meta-value", + sys::InputType_InputTypeMAX, + ), + Self::Invalid(id) => uwrite!(f, "input key ID {} is invalid", id), + } + } +} + +impl_std_error!(FromSysInputTypeError); diff --git a/crates/flipperzero/src/internals.rs b/crates/flipperzero/src/internals.rs new file mode 100644 index 00000000..6113b328 --- /dev/null +++ b/crates/flipperzero/src/internals.rs @@ -0,0 +1,225 @@ +//! Internal implementation details. + +use core::{marker::PhantomData, mem}; + +/// Marker type which is neither [`Send`] nor [`Sync`]. +/// This should be used until `negative_trait_bounds` Rust feature is stable. +/// +/// # Example +/// +/// Make type `Foo` `impl !Sync` and `impl !Send`: +/// +/// ```compile_fail +/// use std::marker::PhantomData; +/// struct Foo { +/// _marker: PhantomData, +/// } +/// +/// fn require_send(_: impl Send) {} +/// fn require_sync(_: impl Sync) {} +/// +/// let x = Foo { _marker: PhantomData }; +/// require_send(x); +/// require_sync(x); +/// ``` +pub(crate) struct UnsendUnsync(*const ()); + +const _: () = { + assert!( + mem::size_of::>() == 0, + "`PhantomData` should be a ZST", + ); +}; + +/// Marker type which is not [`Send`] but is [`Sync`]. +/// This should be used until `negative_trait_bounds` Rust feature is stable. +/// +/// # Example +/// +/// Make type `Foo` `impl !Send`: +/// +/// ```compile_fail +/// use std::marker::PhantomData; +/// struct Foo { +/// _marker: PhantomData, +/// } +/// +/// fn require_send(_: impl Send) {} +/// +/// let x = Foo { _marker: PhantomData }; +/// require_send(x); +/// ``` +pub(crate) struct Unsend(*const ()); + +// SAFETY: `Unsend` is just a marker struct +unsafe impl Sync for Unsend {} + +const _: () = { + assert!( + mem::size_of::>() == 0, + "`PhantomData` should be a ZST" + ); +}; + +#[cfg(feature = "alloc")] +pub(crate) mod alloc { + use alloc::boxed::Box; + use core::{mem, ptr::NonNull}; + + /// Wrapper for a [`NonNull`] created from [`Box`] + /// which does not imply uniqueness which the box does. + /// + /// # Intended use + /// + /// This is intended to be used instead of [`Box`] whenever + /// an allocation occurs on creation of a wrapper which needs + /// to store extra information on the heap, such as FFI-callback contexts, + /// in this case this struct has to be stored as a field + /// and the raw pointer provided by it should be passed to the FFI. + /// + /// The caller must guarantee that by the moment this structure is dropped + /// no one continues using the pointers. + /// + /// # Safety + /// + /// While there are no `unsafe` methods in this struct, + /// it is easy to misuse the pointers provided by its methods, namely: + /// + /// * [`NonUniqueBox::as_ptr`] + /// * [`NonUniqueBox::as_non_null`] + /// + /// so it should be used with extra care, i.e. all uses of the pointers + /// should follow the rules such as stacked borrows + /// and should never be used after the drop of this structure. + /// + /// As a rule of thumb, it should only be stored in private fields + /// of the `struct`s to help with holding a pointer to an owned allocation + /// without upholding `Box`s uniqueness guarantees. + /// + /// # Examples + /// + /// Wrapper structure for some callback: + /// ```no_run + /// # struct FfiFoo; + /// # struct Context { + /// # bar: i32, + /// # baz: u8, + /// # } + /// # extern "C" { + /// # fn foo_alloc() -> *mut FfiFoo; + /// # fn foo_set_callback(foo: *mut FfiFoo, ctx: Context); + /// # fn foo_free(foo: *mut FfiFoo); + /// # } + /// # use std::ptr::NonNull; + /// # use crate::internals::alloc::NonUniqueBox; + /// pub struct Foo { + /// inner: FooInner, + /// context: NonUniqueBox, + /// } + /// struct FooInner(NonNull); + /// impl Drop for FooInner { + /// fn drop(&mut self) { + /// let raw = self.0.as_ptr(); + /// // SAFETY: `raw` should be a valid pointer + /// unsafe { foo_free(raw) }; + /// } + /// } + /// impl Foo { + /// fn new() -> Foo { + /// let inner = FooInner( + /// // SAFETY: we uphold `foo_alloc` invariant + /// // and it is never null + /// unsafe { NonNull::new_unchecked(foo_alloc()) } + /// ); + /// let context = NonUniqueBox::new(Context { bar: 123, baz: 456 }); + /// Self { inner, context } + /// } + /// } + ///``` + #[repr(transparent)] + pub(crate) struct NonUniqueBox(NonNull); + + impl NonUniqueBox { + #[inline(always)] + pub(crate) fn new(value: T) -> Self { + let value = Box::into_raw(Box::new(value)); + // SAFETY: `value` has just been allocated via `Box` + Self(unsafe { NonNull::new_unchecked(value) }) + } + } + + impl NonUniqueBox { + #[inline(always)] + pub(crate) fn as_non_null(&self) -> NonNull { + self.0 + } + + #[inline(always)] + pub(crate) fn as_ptr(&self) -> *mut T { + self.0.as_ptr() + } + + /// Converts this back into a [`Box`]. + /// + /// # Safety + /// + /// This methods is safe since it user's responsibility + /// to correctly use the pointers created from this wrapper, + /// but it still is important to keep in mind that this is easy to misuse. + pub(crate) fn to_box(self) -> Box { + let raw = self.0.as_ptr(); + mem::forget(self); + // SAFETY: `raw` should have been created from `Box` + // and it's user's responsibility to correctly use the exposed pointer + unsafe { Box::from_raw(raw) } + } + } + + impl Drop for NonUniqueBox { + fn drop(&mut self) { + let raw = self.0.as_ptr(); + // SAFETY: `raw` should have been created from `Box` + // and it's user's responsibility to correctly use the exposed pointer + let _ = unsafe { Box::from_raw(raw) }; + } + } +} + +/// Operations which have unstable implementations +/// but still may be implemented manually on `stable` channel. +/// +/// This will use core implementations if `unstable_intrinsics` feature is enabled +/// falling back to ad-hoc implementations otherwise. +#[allow(dead_code)] // this functions may be unused if a specific feature set does not require them +pub(crate) mod ops { + pub(crate) const fn div_ceil_u8(divident: u8, divisor: u8) -> u8 { + #[cfg(feature = "unstable_intrinsics")] + { + divident.div_ceil(divisor) + } + #[cfg(not(feature = "unstable_intrinsics"))] + { + let quotient = divident / divisor; + let remainder = divident % divisor; + if remainder != 0 { + quotient + 1 + } else { + quotient + } + } + } +} + +pub(crate) mod macros { + /// Generates an implementation of `std::error::Error` for the passed type + /// hidden behind an `std` feature flag. + macro_rules! impl_std_error { + ($error_type:ident) => { + #[cfg(feature = "std")] + #[cfg_attr(docsrs, doc(cfg(feature = "std")))] + impl ::std::error::Error for $error_type {} + }; + } + + pub(crate) use impl_std_error; +} diff --git a/crates/flipperzero/src/io.rs b/crates/flipperzero/src/io.rs index b702fc72..86978743 100644 --- a/crates/flipperzero/src/io.rs +++ b/crates/flipperzero/src/io.rs @@ -1,6 +1,7 @@ use core::ffi::CStr; use core::fmt; +use crate::internals::macros::impl_std_error; use flipperzero_sys as sys; /// Stream and file system related error kinds. @@ -97,6 +98,8 @@ impl ufmt::uDisplay for Error { } } +impl_std_error!(LockError); + /// Trait comparable to `std::Read` for the Flipper Zero API pub trait Read { /// Reads some bytes from this source into the given buffer, returning how many bytes diff --git a/crates/flipperzero/src/kernel.rs b/crates/flipperzero/src/kernel.rs new file mode 100644 index 00000000..a14ceb8c --- /dev/null +++ b/crates/flipperzero/src/kernel.rs @@ -0,0 +1,101 @@ +use crate::internals::{macros::impl_std_error, Unsend}; +use core::{ + fmt::{self, Display, Formatter}, + marker::PhantomData, +}; +use flipperzero_sys::{self as sys, furi::Status}; +use ufmt::{derive::uDebug, uDebug, uDisplay, uWrite, uwrite}; + +#[inline(always)] +fn is_irq_or_masked() -> bool { + // SAFETY: this function has no invariants to uphold + unsafe { sys::furi_kernel_is_irq_or_masked() } +} + +pub fn lock() -> Result { + if is_irq_or_masked() { + Err(LockError::Interrupted) + } else { + // SAFETY: kernel is not interrupted + let status = unsafe { sys::furi_kernel_lock() }; + + Ok(match status { + 0 => LockGuard { + was_locked: false, + _marker: PhantomData, + }, + 1 => LockGuard { + was_locked: true, + _marker: PhantomData, + }, + status => Err(LockError::ErrorStatus(Status(status)))?, + }) + } +} + +#[derive(Debug)] +#[cfg_attr( + feature = "unstable_lints", + must_not_suspend = "holding a MutexGuard across suspend \ + points can cause deadlocks, delays, \ + and cause Futures to not implement `Send`" +)] +pub struct LockGuard { + was_locked: bool, + _marker: PhantomData, +} + +impl LockGuard { + pub const fn was_locked(&self) -> bool { + self.was_locked + } +} + +impl Drop for LockGuard { + fn drop(&mut self) { + // SAFETY: since `LockGuard` is `!Send` it cannot escape valid (non-interrupt) context + let _ = unsafe { sys::furi_kernel_unlock() }; + } +} + +impl uDebug for LockGuard { + fn fmt(&self, f: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error> + where + W: uWrite + ?Sized, + { + // TODO: use `derive` once `ufmt` supports `PhantomReference` + f.debug_struct("LockGuard")? + .field("was_locked", &self.was_locked)? + .finish() + } +} + +/// A type of error which can be returned whenever a lock is acquired. +#[derive(Copy, Clone, Debug, uDebug, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub enum LockError { + Interrupted, + ErrorStatus(Status), +} + +impl Display for LockError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::Interrupted => write!(f, "context is in interruption state"), + Self::ErrorStatus(status) => write!(f, "error status: {status}"), + } + } +} + +impl uDisplay for LockError { + fn fmt(&self, f: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error> + where + W: uWrite + ?Sized, + { + match self { + Self::Interrupted => uwrite!(f, "context is in interruption state"), + Self::ErrorStatus(status) => uwrite!(f, "error status: {}", status), + } + } +} + +impl_std_error!(LockError); diff --git a/crates/flipperzero/src/lib.rs b/crates/flipperzero/src/lib.rs index 9795ea39..3da60fb0 100644 --- a/crates/flipperzero/src/lib.rs +++ b/crates/flipperzero/src/lib.rs @@ -6,6 +6,8 @@ #![no_std] #![cfg_attr(test, no_main)] +#![cfg_attr(feature = "unstable_intrinsics", feature(int_roundings))] +#![cfg_attr(feature = "unstable_lints", feature(must_not_suspend))] #![cfg_attr(docsrs, feature(doc_cfg))] #![deny(rustdoc::broken_intra_doc_links)] @@ -13,16 +15,34 @@ #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] extern crate alloc; +#[cfg(feature = "service-dialogs")] +#[cfg_attr(docsrs, doc(cfg(feature = "service-dialogs")))] pub mod dialogs; +#[cfg(feature = "service-dolphin")] +#[cfg_attr(docsrs, doc(cfg(feature = "service-dolphin")))] pub mod dolphin; pub mod furi; pub mod gpio; +#[cfg(feature = "service-gui")] +#[cfg_attr(docsrs, doc(cfg(feature = "service-gui")))] pub mod gui; +#[cfg(feature = "service-input")] +#[cfg_attr(docsrs, doc(cfg(feature = "service-input")))] +pub mod input; +pub(crate) mod internals; pub mod io; +pub mod kernel; pub mod macros; +#[cfg(feature = "service-notification")] +#[cfg_attr(docsrs, doc(cfg(feature = "service-notification")))] pub mod notification; +#[cfg(feature = "service-storage")] +#[cfg_attr(docsrs, doc(cfg(feature = "service-storage")))] pub mod storage; pub mod toolbox; +#[cfg(feature = "xbm")] +#[cfg_attr(docsrs, doc(cfg(feature = "xbm")))] +pub mod xbm; #[doc(hidden)] pub mod __macro_support { @@ -66,5 +86,6 @@ flipperzero_test::tests_runner!( crate::toolbox::crc32::tests, crate::toolbox::md5::tests, crate::toolbox::sha256::tests, + crate::xbm::tests, ] ); diff --git a/crates/flipperzero/src/notification/mod.rs b/crates/flipperzero/src/notification/mod.rs index a33edaac..e3e71877 100644 --- a/crates/flipperzero/src/notification/mod.rs +++ b/crates/flipperzero/src/notification/mod.rs @@ -37,18 +37,19 @@ impl NotificationService { /// Runs a notification sequence. /// - /// #Safety + /// # Safety + /// /// Due to how rust interacts with the firmware this function is not safe to use at any time /// where the application might exit directly afterwards as the rust runtime will free the /// sequence before the firmware has finished reading it. At any time where this is an issue /// `notify_blocking` should be used instead.. pub fn notify(&mut self, sequence: &'static NotificationSequence) { - unsafe { sys::notification_message(self.data.as_ptr(), sequence.to_sys()) }; + unsafe { sys::notification_message(self.data.as_raw(), sequence.to_sys()) }; } /// Runs a notification sequence and blocks the thread. pub fn notify_blocking(&mut self, sequence: &'static NotificationSequence) { - unsafe { sys::notification_message_block(self.data.as_ptr(), sequence.to_sys()) }; + unsafe { sys::notification_message_block(self.data.as_raw(), sequence.to_sys()) }; } } diff --git a/crates/flipperzero/src/storage.rs b/crates/flipperzero/src/storage.rs index 7c7b0d3e..72840239 100644 --- a/crates/flipperzero/src/storage.rs +++ b/crates/flipperzero/src/storage.rs @@ -155,7 +155,7 @@ impl File { unsafe { let record = UnsafeRecord::open(RECORD_STORAGE); File( - NonNull::new_unchecked(sys::storage_file_alloc(record.as_ptr())), + NonNull::new_unchecked(sys::storage_file_alloc(record.as_raw())), record, ) } @@ -166,7 +166,7 @@ impl Drop for File { fn drop(&mut self) { unsafe { // `storage_file_close` calls `storage_file_sync` - // internally, so it's not necesssary to call it here. + // internally, so it's not necessary to call it here. sys::storage_file_close(self.0.as_ptr()); } } diff --git a/crates/flipperzero/src/xbm.rs b/crates/flipperzero/src/xbm.rs new file mode 100644 index 00000000..1dabbedd --- /dev/null +++ b/crates/flipperzero/src/xbm.rs @@ -0,0 +1,470 @@ +//! User-friendly wrappers of XBM images. + +use crate::internals::ops::div_ceil_u8; +use alloc::{vec, vec::Vec}; +use core::{ + ops::{Deref, DerefMut}, + slice, +}; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct XbmImage { + data: D, + width: u8, + height: u8, +} + +impl XbmImage { + pub const fn width(&self) -> u8 { + self.width + } + + pub const fn height(&self) -> u8 { + self.height + } + + pub const fn dimensions(&self) -> (u8, u8) { + (self.width, self.height) + } + + #[inline] + const fn row_bytes(width: u8) -> u8 { + div_ceil_u8(width, 8) + } + + #[inline] + const fn total_bytes(width: u8, height: u8) -> u16 { + Self::row_bytes(width) as u16 * height as u16 + } + + // IMPORTANT: XBM images have trailing bits per-rows + // rather than at the end of the whole byte-array + #[inline] + const fn offsets(&self, x: u8, y: u8) -> Option<(u8, u8)> { + if x >= self.width || y >= self.height { + None + } else { + Some(( + // Per each y we skip a row of `row_bytes` bytes + // then we also have to skip all previous + (Self::row_bytes(self.width) * y) + x / 8, + // Since all rows are aligned, only x affects the bit offset + x % 8, + )) + } + } +} + +impl> XbmImage { + pub fn data(&self) -> &D::Target { + &self.data + } + + pub fn data_mut(&mut self) -> &mut D::Target + where + D: DerefMut, + { + &mut self.data + } +} + +impl> XbmImage { + pub fn new_from(width: u8, height: u8, data: D) -> Self { + let bytes = Self::total_bytes(width, height); + + assert!( + bytes as usize == data.len(), + "width={}bits * height={}bits should correspond to {}bytes, but data has length {}", + width, + height, + bytes, + data.len() + ); + + Self { + data, + width, + height, + } + } + + pub fn get(&self, (x, y): (u8, u8)) -> Option { + if let Some((byte, shift)) = self.offsets(x, y) { + Some((self.data[byte as usize] >> shift) & 0b1 != 0) + } else { + None + } + } +} + +impl<'a> XbmImage<&'a [u8]> { + pub unsafe fn from_raw(width: u8, height: u8, data: *const u8) -> Self { + let bytes = Self::total_bytes(width, height) as usize; + + // SAFETY: the size is exactly calculated based on width and height + // and caller upholds the `data` validity invariant + let data = unsafe { slice::from_raw_parts(data, bytes) }; + + Self { + data, + width, + height, + } + } +} + +impl<'a> XbmImage<&'a mut [u8]> { + pub unsafe fn from_raw_mut(width: u8, height: u8, data: *mut u8) -> Self { + let bytes = Self::total_bytes(width, height) as usize; + + // SAFETY: the size is exactly calculated based on width and height + // and caller upholds the `data` validity invariant + let data = unsafe { slice::from_raw_parts_mut(data, bytes) }; + + Self { + data, + width, + height, + } + } +} + +impl + DerefMut> XbmImage { + pub fn set(&mut self, coordinates: (u8, u8), value: bool) -> Option<()> { + if value { + self.set_1(coordinates) + } else { + self.set_0(coordinates) + } + } + + pub fn set_1(&mut self, (x, y): (u8, u8)) -> Option<()> { + let (byte, shift) = self.offsets(x, y)?; + self.data[byte as usize] |= 1 << shift; + Some(()) + } + + pub fn set_0(&mut self, (x, y): (u8, u8)) -> Option<()> { + let (byte, shift) = self.offsets(x, y)?; + self.data[byte as usize] &= !(1 << shift); + Some(()) + } + + pub fn xor(&mut self, (x, y): (u8, u8)) -> Option<()> { + let (byte, shift) = self.offsets(x, y)?; + self.data[byte as usize] ^= 1 << shift; + Some(()) + } +} + +// Specializations + +impl XbmImage> { + pub fn new(width: u8, height: u8) -> Self { + let bytes = Self::total_bytes(width, height) as usize; + Self { + data: vec![0; bytes], + width, + height, + } + } +} + +impl XbmImage<&'static [u8]> { + /// Creates a referencing `XbmImage` from a static `u8` slice. + /// + /// This is a constant specialization of [`XbmImage::new_from`] + /// existing since the latter cannot be generically `const` + /// until `const_fn_trait_bound` Rust feature becomes stable. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```rust + /// # use flipperzero::xbm::XbmImage; + /// const IMAGE: XbmImage<&'static [u8]> = XbmImage::new_from_static(4, 4, &[0xFE, 0x12]); + /// ``` + pub const fn new_from_static(width: u8, height: u8, data: &'static [u8]) -> Self { + let bytes = Self::total_bytes(width, height); + + assert!( + bytes as usize == data.len(), + "dimensions don't match data length", + ); + + Self { + data, + width, + height, + } + } +} + +impl XbmImage> { + /// Creates a referencing `XbmImage` from a `u8` array. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```rust + /// # use flipperzero::xbm::{XbmImage, ByteArray}; + /// const IMAGE: XbmImage> = XbmImage::new_from_array::<4, 4>([0xFE, 0x12]); + /// ``` + pub const fn new_from_array(data: [u8; SIZE]) -> Self { + let bytes = Self::total_bytes(WIDTH, HEIGHT); + + assert!(bytes as usize == SIZE, "dimensions don't match data length"); + + Self { + data: ByteArray(data), + width: WIDTH, + height: HEIGHT, + } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct ByteArray(pub [u8; N]); + +impl Deref for ByteArray { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + self.0.as_slice() + } +} + +impl DerefMut for ByteArray { + fn deref_mut(&mut self) -> &mut Self::Target { + self.0.as_mut_slice() + } +} + +/// Creates a compile-time XBM image. +/// The type of this expression is [`XbmImage`] with [`ByteArray`] backend of the calculated size. +/// +/// The syntax is an [XBM image definition][XBM format] +/// optionally wrapped in an `unsafe` block. +/// +/// Unless the expression is wrapped in an `unsafe` block +/// the macro will perform identifier validation: +/// it will check that identifiers are in the order +/// `_width`, `_height`, optionally `_x_hot` and `_y_hot` and `_bits` +/// and that the `` is the same for all identifiers. +/// The `unsafe` form omits this validations +/// while still ensuring that the size constraints are valid. +/// +/// [XBM format]: https://www.fileformat.info/format/xbm/egff.htm +#[macro_export] +macro_rules! xbm { + ( + #define $width_ident:ident $width:literal + #define $height_ident:ident $height:literal + $( + #define $x_hotspot_ident:ident $_hotspot_x:literal + #define $y_hotspot_ident:ident $_hotspot_y:literal + )? + static $(unsigned)? char $bits_ident:ident[] = { + $($byte:literal),* $(,)? + }; + ) => {{ + { // name assertions + let bits_ident = stringify!($bits_ident).as_bytes(); + assert!( + matches!(bits_ident, [.., b'_', b'b', b'i', b't', b's']), + "width identifier should end with `_bits`", + ); + let significant_len = bits_ident.len() - b"_bits".len(); + + const fn str_eq(left: &[u8], right: &[u8], limit: usize) -> bool { + match (left.split_first(), right.split_first()) { + ( + Some((&left_first, left_remaining)), + Some((&right_first, right_remaining)), + ) => { + left_first == right_first + && (limit == 1 || str_eq(left_remaining, right_remaining, limit - 1)) + } + (None, None) => true, + _ => false, + } + } + + let width_ident = stringify!($width_ident).as_bytes(); + assert!( + matches!(width_ident, [.., b'_', b'w', b'i', b'd', b't', b'h']), + "width identifier should end with `_width`", + ); + assert!( + str_eq(bits_ident, width_ident, significant_len), + "bits identifier and width identifier should have the same prefix" + ); + + let height_ident = stringify!($height_ident).as_bytes(); + assert!( + matches!(height_ident, [.., b'_', b'h', b'e', b'i', b'g', b'h', b't']), + "width identifier should end with `_height`", + ); + assert!( + str_eq(bits_ident, height_ident, significant_len), + "bits identifier and height identifier should have the same prefix" + ); + + $( + let x_hotspot_ident = stringify!($x_hotspot_ident).as_bytes(); + assert!( + matches!(bits_ident, [.., b'_', b'x', b'_', b'h', b'o', b't']), + "x-hotspot identifier should end with `_x_hot`", + ); + assert!( + str_eq(bits_ident, x_hotspot_ident, significant_len), + "bits identifier and x-hotspot identifier should have the same prefix" + ); + + let y_hotspot_ident = stringify!($y_hotspot_ident).as_bytes(); + assert!( + matches!(bits_ident, [.., b'_', b'y', b'_', b'h', b'o', b't']), + "y-hotspot identifier should end with `_y_hot`", + ); + assert!( + str_eq(bits_ident, y_hotspot_ident, significant_len), + "bits identifier and y-hotspot identifier should have the same prefix" + ); + )? + } + + $crate::xbm!(unsafe { + #define $width_ident $width + #define $height_ident $height + $( + #define $x_hotspot_ident $_hotspot_x + #define $y_hotspot_ident $_hotspot_y + )? + static char $bits_ident[] = { + $($byte),* + }; + }) + }}; + ( + unsafe { + #define $_width_ident:ident $width:literal + #define $_height_ident:ident $height:literal + $( + #define $_x_hotspot_ident:ident $_hotspot_x:literal + #define $_y_hotspot_ident:ident $_hotspot_y:literal + )? + static $(unsigned)? char $_bits_ident:ident[] = { + $($byte:literal),* $(,)? + }; + } + ) => {{ + $crate::xbm::XbmImage::new_from_array::<$width, $height>([$($byte,)*]) + }}; +} + +#[flipperzero_test::tests] +mod tests { + #[test] + fn valid_byte_reading_aligned() { + // 0100110000111100 + // 0000001111111100 + let image = xbm!( + #define xbm_test_width 16 + #define xbm_test_height 2 + static char xbm_test_bits[] = { + 0x32, // 0b00110010 ~ 0b01001100 + 0x3C, // 0b00111100 ~ 0b00111100 + 0xC0, // 0b11000000 ~ 0b00000011 + 0x3F, // 0b00111111 ~ 0b11111100 + }; + ); + + assert!(!image.get((0, 0)).unwrap()); + assert!(image.get((1, 0)).unwrap()); + assert!(!image.get((2, 0)).unwrap()); + assert!(!image.get((3, 0)).unwrap()); + assert!(image.get((4, 0)).unwrap()); + assert!(image.get((5, 0)).unwrap()); + assert!(!image.get((6, 0)).unwrap()); + assert!(!image.get((7, 0)).unwrap()); + assert!(!image.get((8, 0)).unwrap()); + assert!(!image.get((9, 0)).unwrap()); + assert!(image.get((10, 0)).unwrap()); + assert!(image.get((11, 0)).unwrap()); + assert!(image.get((12, 0)).unwrap()); + assert!(image.get((13, 0)).unwrap()); + assert!(!image.get((14, 0)).unwrap()); + assert!(!image.get((15, 0)).unwrap()); + assert!(!image.get((0, 1)).unwrap()); + assert!(!image.get((1, 1)).unwrap()); + assert!(!image.get((2, 1)).unwrap()); + assert!(!image.get((3, 1)).unwrap()); + assert!(!image.get((4, 1)).unwrap()); + assert!(!image.get((5, 1)).unwrap()); + assert!(image.get((6, 1)).unwrap()); + assert!(image.get((7, 1)).unwrap()); + assert!(image.get((8, 1)).unwrap()); + assert!(image.get((9, 1)).unwrap()); + assert!(image.get((10, 1)).unwrap()); + assert!(image.get((11, 1)).unwrap()); + assert!(image.get((12, 1)).unwrap()); + assert!(image.get((13, 1)).unwrap()); + assert!(!image.get((14, 1)).unwrap()); + assert!(!image.get((15, 1)).unwrap()); + } + + #[test] + fn valid_byte_reading_misaligned() { + // 01001100 00111100 0******* + // 00000011 11111100 1******* + let image = xbm!( + #define xbm_test_width 17 + #define xbm_test_height 2 + static char xbm_test_bits[] = { + 0x32, // 0b00110010 ~ 0b01001100 + 0x3C, // 0b00111100 ~ 0b00111100 + 0xF0, // 0b00001111 ~ 0b11110000 + 0xC0, // 0b11000000 ~ 0b00000011 + 0x3F, // 0b00111111 ~ 0b11111100 + 0x0F, // 0b11110000 ~ 0b00001111 + }; + ); + + assert!(!image.get((0, 0)).unwrap()); + assert!(image.get((1, 0)).unwrap()); + assert!(!image.get((2, 0)).unwrap()); + assert!(!image.get((3, 0)).unwrap()); + assert!(image.get((4, 0)).unwrap()); + assert!(image.get((5, 0)).unwrap()); + assert!(!image.get((6, 0)).unwrap()); + assert!(!image.get((7, 0)).unwrap()); + assert!(!image.get((8, 0)).unwrap()); + assert!(!image.get((9, 0)).unwrap()); + assert!(image.get((10, 0)).unwrap()); + assert!(image.get((11, 0)).unwrap()); + assert!(image.get((12, 0)).unwrap()); + assert!(image.get((13, 0)).unwrap()); + assert!(!image.get((14, 0)).unwrap()); + assert!(!image.get((15, 0)).unwrap()); + assert!(!image.get((16, 0)).unwrap()); + assert!(!image.get((0, 1)).unwrap()); + assert!(!image.get((1, 1)).unwrap()); + assert!(!image.get((2, 1)).unwrap()); + assert!(!image.get((3, 1)).unwrap()); + assert!(!image.get((4, 1)).unwrap()); + assert!(!image.get((5, 1)).unwrap()); + assert!(image.get((6, 1)).unwrap()); + assert!(image.get((7, 1)).unwrap()); + assert!(image.get((8, 1)).unwrap()); + assert!(image.get((9, 1)).unwrap()); + assert!(image.get((10, 1)).unwrap()); + assert!(image.get((11, 1)).unwrap()); + assert!(image.get((12, 1)).unwrap()); + assert!(image.get((13, 1)).unwrap()); + assert!(!image.get((14, 1)).unwrap()); + assert!(!image.get((15, 1)).unwrap()); + assert!(image.get((16, 1)).unwrap()); + } +} diff --git a/crates/rt/Cargo.toml b/crates/rt/Cargo.toml index e9506d39..82e14bf3 100644 --- a/crates/rt/Cargo.toml +++ b/crates/rt/Cargo.toml @@ -12,11 +12,6 @@ autoexamples = false autotests = false autobenches = false -[package.metadata.docs.rs] -default-target = "thumbv7em-none-eabihf" -targets = [] -all-features = true - [lib] bench = false test = false diff --git a/crates/sys/Cargo.toml b/crates/sys/Cargo.toml index 93a56f9e..42155c0f 100644 --- a/crates/sys/Cargo.toml +++ b/crates/sys/Cargo.toml @@ -12,11 +12,6 @@ autoexamples = false autotests = false autobenches = false -[package.metadata.docs.rs] -default-target = "thumbv7em-none-eabihf" -targets = [] -all-features = true - [lib] bench = false test = false diff --git a/crates/sys/src/furi.rs b/crates/sys/src/furi.rs index 561a2123..4777cd90 100644 --- a/crates/sys/src/furi.rs +++ b/crates/sys/src/furi.rs @@ -2,13 +2,14 @@ use core::ffi::c_char; use core::fmt::Display; +use core::ptr::NonNull; use core::time::Duration; /// Operation status. /// The Furi API switches between using `enum FuriStatus`, `int32_t` and `uint32_t`. /// Since these all use the same bit representation, we can just "cast" the returns to this type. #[repr(transparent)] -#[derive(Clone, Copy, Debug, ufmt::derive::uDebug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Ord, PartialOrd, ufmt::derive::uDebug)] pub struct Status(pub i32); impl Status { @@ -94,35 +95,40 @@ impl From for Status { /// Low-level wrapper of a record handle. pub struct UnsafeRecord { name: *const c_char, - data: *mut T, + data: NonNull, } impl UnsafeRecord { /// Opens a record. /// - /// Safety: The caller must ensure that `record_name` lives for the - /// duration of the object lifetime. - /// /// # Safety /// - /// The caller must provide a valid C-string `name`. + /// The caller must ensure that `record_name` lives for the + /// duration of the object lifetime and that it is a valid C-string. pub unsafe fn open(name: *const c_char) -> Self { - Self { - name, - data: crate::furi_record_open(name) as *mut T, - } + // SAFETY: the created pointer is guaranteed to be valid + let data = unsafe { crate::furi_record_open(name) } as *mut T; + // SAFETY: the created pointer is guaranteed to be non-null + let data = unsafe { NonNull::new_unchecked(data) }; + Self { name, data } } /// Returns the record data as a raw pointer. + pub fn as_raw(&self) -> *mut T { + self.data.as_ptr() + } + + #[deprecated = "use `as_raw(&self)` instead"] pub fn as_ptr(&self) -> *mut T { - self.data + self.as_raw() } } impl Drop for UnsafeRecord { fn drop(&mut self) { unsafe { - // decrement the holders count + // SAFETY: `self.name` is valid since it was used to construct this istance + // and ownership has not been taken crate::furi_record_close(self.name); } } diff --git a/crates/sys/src/lib.rs b/crates/sys/src/lib.rs index d61e607d..bb2c1e1c 100644 --- a/crates/sys/src/lib.rs +++ b/crates/sys/src/lib.rs @@ -34,7 +34,7 @@ mod bindings; #[macro_export] macro_rules! c_string { ($str:expr $(,)?) => {{ - concat!($str, "\0").as_ptr() as *const core::ffi::c_char + ::core::concat!($str, "\0").as_ptr() as *const core::ffi::c_char }}; } @@ -45,10 +45,10 @@ macro_rules! crash { unsafe { // Crash message is passed via r12 let msg = $crate::c_string!($msg); - core::arch::asm!("", in("r12") msg, options(nomem, nostack)); + ::core::arch::asm!("", in("r12") msg, options(nomem, nostack)); $crate::__furi_crash_implementation(); - core::hint::unreachable_unchecked(); + ::core::hint::unreachable_unchecked(); } }; } diff --git a/crates/test/src/lib.rs b/crates/test/src/lib.rs index 5edc98fe..b1e93b7c 100644 --- a/crates/test/src/lib.rs +++ b/crates/test/src/lib.rs @@ -152,7 +152,7 @@ pub mod __macro_support { impl OutputFile { fn new(storage: &UnsafeRecord) -> Self { - let output_file = unsafe { sys::storage_file_alloc(storage.as_ptr()) }; + let output_file = unsafe { sys::storage_file_alloc(storage.as_raw()) }; unsafe { sys::storage_file_open( output_file,