From 7a86507182fc9b68b31c6901c4b2c02df8c9f33b Mon Sep 17 00:00:00 2001 From: Andrea Ciliberti Date: Mon, 27 Nov 2023 17:34:07 +0100 Subject: [PATCH 01/15] Fix todo and start working on swkbd --- ctru-rs/examples/software-keyboard.rs | 12 ++++++------ ctru-rs/src/applets/mii_selector.rs | 9 +++++---- ctru-rs/src/applets/swkbd.rs | 15 +++++++-------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/ctru-rs/examples/software-keyboard.rs b/ctru-rs/examples/software-keyboard.rs index 61c32db..cc8d81c 100644 --- a/ctru-rs/examples/software-keyboard.rs +++ b/ctru-rs/examples/software-keyboard.rs @@ -11,6 +11,11 @@ fn main() { let gfx = Gfx::new().unwrap(); let _console = Console::new(gfx.top_screen.borrow_mut()); + // Prepares a software keyboard with two buttons: one to cancel input and one + // to accept it. You can also use `SoftwareKeyboard::new()` to launch the keyboard + // with different configurations. + let mut keyboard = SoftwareKeyboard::default(); + println!("Press A to enter some text or press Start to exit."); while apt.main_loop() { @@ -22,14 +27,9 @@ fn main() { // Check if the user request to write some input. if hid.keys_down().contains(KeyPad::A) { - // Prepares a software keyboard with two buttons: One to cancel input and one - // to accept it. You can also use `SoftwareKeyboard::new()` to launch the keyboard in different - // configurations. - let mut keyboard = SoftwareKeyboard::default(); - // Raise the software keyboard. You can perform different actions depending on which // software button the user pressed. - match keyboard.get_string(2048) { + match keyboard.get_string(2048, &apt, &gfx) { Ok((text, Button::Right)) => println!("You entered: {text}"), Ok((_, Button::Left)) => println!("Cancelled"), Ok((_, Button::Middle)) => println!("How did you even press this?"), diff --git a/ctru-rs/src/applets/mii_selector.rs b/ctru-rs/src/applets/mii_selector.rs index 2353d7d..81ab2ae 100644 --- a/ctru-rs/src/applets/mii_selector.rs +++ b/ctru-rs/src/applets/mii_selector.rs @@ -4,6 +4,8 @@ //! The selected Mii is readable as a [`Mii`]. use crate::mii::Mii; +use crate::services::{apt::Apt, gfx::Gfx}; + use bitflags::bitflags; use std::{ffi::CString, fmt}; @@ -252,9 +254,8 @@ impl MiiSelector { /// Launch the Mii Selector. /// - /// Depending on the configuration, the Mii Selector window will appear either on the bottom screen (default behaviour) or the top screen (see [`Options::USE_TOP_SCREEN`]). - /// - /// TODO: UNSAFE OPERATION, LAUNCHING APPLETS REQUIRES GRAPHICS, WITHOUT AN ACTIVE GFX THIS WILL CAUSE A SEGMENTATION FAULT. + /// Depending on the configuration, the Mii Selector window will appear either + /// on the bottom screen (default behaviour) or the top screen (see [`Options::USE_TOP_SCREEN`]). /// /// # Example /// @@ -276,7 +277,7 @@ impl MiiSelector { /// # } /// ``` #[doc(alias = "miiSelectorLaunch")] - pub fn launch(&mut self) -> Result { + pub fn launch(&mut self, apt: &Apt, gfx: &Gfx) -> Result { let mut return_val = Box::::default(); unsafe { ctru_sys::miiSelectorLaunch(self.config.as_mut(), return_val.as_mut()) } diff --git a/ctru-rs/src/applets/swkbd.rs b/ctru-rs/src/applets/swkbd.rs index 69d4329..05ca659 100644 --- a/ctru-rs/src/applets/swkbd.rs +++ b/ctru-rs/src/applets/swkbd.rs @@ -5,11 +5,13 @@ // TODO: Split the Parental PIN lock operations into a different type. #![doc(alias = "keyboard")] -use bitflags::bitflags; use ctru_sys::{ self, swkbdInit, swkbdInputText, swkbdSetButton, swkbdSetFeatures, swkbdSetHintText, swkbdSetInitialText, SwkbdState, }; +use crate::services::{apt::Apt, gfx::Gfx}; + +use bitflags::bitflags; use libc; use std::fmt::Display; use std::iter::once; @@ -188,8 +190,7 @@ impl SoftwareKeyboard { /// # Notes /// /// The text received from the keyboard will be truncated if it is longer than `max_bytes`. - /// - /// TODO: UNSAFE OPERATION, LAUNCHING APPLETS REQUIRES GRAPHICS, WITHOUT AN ACTIVE GFX THIS WILL CAUSE A SEGMENTATION FAULT. + /// Use [`SoftwareKeyboard::set_max_text_len()`] to make sure the buffer can contain the input text. /// /// # Example /// @@ -207,13 +208,13 @@ impl SoftwareKeyboard { /// # } /// ``` #[doc(alias = "swkbdInputText")] - pub fn get_string(&mut self, max_bytes: usize) -> Result<(String, Button), Error> { + pub fn get_string(&mut self, max_bytes: usize, apt: &Apt, gfx: &Gfx) -> Result<(String, Button), Error> { // Unfortunately the libctru API doesn't really provide a way to get the exact length // of the string that it receieves from the software keyboard. Instead it expects you // to pass in a buffer and hope that it's big enough to fit the entire string, so // you have to set some upper limit on the potential size of the user's input. let mut tmp = vec![0u8; max_bytes]; - let button = self.write_exact(&mut tmp)?; + let button = self.write_exact(&mut tmp, apt, gfx)?; // libctru does, however, seem to ensure that the buffer will always contain a properly // terminated UTF-8 sequence even if the input has to be truncated, so these operations @@ -234,8 +235,6 @@ impl SoftwareKeyboard { /// If the buffer is too small to contain the entire sequence received from the keyboard, /// the output will be truncated. /// - /// TODO: UNSAFE OPERATION, LAUNCHING APPLETS REQUIRES GRAPHICS, WITHOUT AN ACTIVE GFX THIS WILL CAUSE A SEGMENTATION FAULT. - /// /// # Example /// /// ``` @@ -254,7 +253,7 @@ impl SoftwareKeyboard { /// # } /// ``` #[doc(alias = "swkbdInputText")] - pub fn write_exact(&mut self, buf: &mut [u8]) -> Result { + pub fn write_exact(&mut self, buf: &mut [u8], apt: &Apt, gfx: &Gfx) -> Result { unsafe { match swkbdInputText(self.state.as_mut(), buf.as_mut_ptr(), buf.len()) { ctru_sys::SWKBD_BUTTON_NONE => Err(self.parse_swkbd_error()), From ab9713941c68a657ab80f7349301f72fba31722f Mon Sep 17 00:00:00 2001 From: Andrea Ciliberti Date: Sat, 23 Dec 2023 13:03:18 +0100 Subject: [PATCH 02/15] Split parental lock into a different type --- ctru-rs/src/applets/mii_selector.rs | 1 + ctru-rs/src/applets/swkbd.rs | 117 ++++++++++++++++++++++------ 2 files changed, 94 insertions(+), 24 deletions(-) diff --git a/ctru-rs/src/applets/mii_selector.rs b/ctru-rs/src/applets/mii_selector.rs index 81ab2ae..01d5808 100644 --- a/ctru-rs/src/applets/mii_selector.rs +++ b/ctru-rs/src/applets/mii_selector.rs @@ -277,6 +277,7 @@ impl MiiSelector { /// # } /// ``` #[doc(alias = "miiSelectorLaunch")] + #[allow(unused_variables)] pub fn launch(&mut self, apt: &Apt, gfx: &Gfx) -> Result { let mut return_val = Box::::default(); unsafe { ctru_sys::miiSelectorLaunch(self.config.as_mut(), return_val.as_mut()) } diff --git a/ctru-rs/src/applets/swkbd.rs b/ctru-rs/src/applets/swkbd.rs index 05ca659..e84620e 100644 --- a/ctru-rs/src/applets/swkbd.rs +++ b/ctru-rs/src/applets/swkbd.rs @@ -2,14 +2,13 @@ //! //! This applet opens a virtual keyboard on the console's bottom screen which lets the user write UTF-16 valid text. // TODO: Implement remaining functionality (password mode, filter callbacks, etc.). Also improve "max text length" API. Improve `number of buttons` API when creating a new SoftwareKeyboard. -// TODO: Split the Parental PIN lock operations into a different type. #![doc(alias = "keyboard")] +use crate::services::{apt::Apt, gfx::Gfx}; use ctru_sys::{ self, swkbdInit, swkbdInputText, swkbdSetButton, swkbdSetFeatures, swkbdSetHintText, swkbdSetInitialText, SwkbdState, }; -use crate::services::{apt::Apt, gfx::Gfx}; use bitflags::bitflags; use libc; @@ -24,6 +23,15 @@ pub struct SoftwareKeyboard { state: Box, } +/// Configuration structure to setup the Parental Lock applet. +/// +/// Internally, the Parental Lock is just a different kind of [`SoftwareKeyboard`]. +#[doc(alias = "SwkbdState")] +#[derive(Clone)] +pub struct ParentalLock { + state: Box, +} + /// The type of keyboard used by the [`SoftwareKeyboard`]. /// /// Can be set with [`SoftwareKeyboard::new()`] @@ -75,15 +83,22 @@ pub enum Error { PowerPressed = ctru_sys::SWKBD_POWERPRESSED, /// The parental lock PIN was correct. /// - /// While this variant isn't *technically* considerable an error - /// the result of a Parental PIN operation won't return a string to the program, thus it's still exceptional behaviour. + /// This variant should never be returned by normal operations made using this module, + /// and is listed here only for compatibility purposes. + /// Refer to the return value of [`ParentalLock::launch()`] to confirm the outcome + /// of the Parental Lock PIN operation. ParentalOk = ctru_sys::SWKBD_PARENTAL_OK, /// The parental lock PIN was incorrect. + /// + /// Refer to the return value of [`ParentalLock::launch()`] to confirm the outcome + /// of the Parental Lock PIN operation. ParentalFail = ctru_sys::SWKBD_PARENTAL_FAIL, /// Input triggered the filter. /// /// You can have a look at [`Filters`] to activate custom filters. BannedInput = ctru_sys::SWKBD_BANNED_INPUT, + /// An on-screen button was pressed to exit the prompt. + ButtonPressed = ctru_sys::SWKBD_D0_CLICK, } /// Restrictions to enforce rules on the keyboard input. @@ -109,12 +124,6 @@ bitflags! { /// Special features that can be activated via [`SoftwareKeyboard::set_features()`]. #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)] pub struct Features: u32 { - /// Parental PIN mode. - /// - /// # Notes - /// - /// Refer to [`Error::ParentalOk`] and [`Error::ParentalFail`] to check whether the Parental PIN lock was successfully opened. - const PARENTAL_PIN = ctru_sys::SWKBD_PARENTAL; /// Darken top screen while the [`SoftwareKeyboard`] is active. const DARKEN_TOP_SCREEN = ctru_sys::SWKBD_DARKEN_TOP_SCREEN; /// Enable predictive input (necessary for Kanji on JPN consoles). @@ -181,7 +190,7 @@ impl SoftwareKeyboard { unsafe { let mut state = Box::::default(); swkbdInit(state.as_mut(), keyboard_type.into(), num_buttons, -1); - SoftwareKeyboard { state } + Self { state } } } @@ -208,7 +217,12 @@ impl SoftwareKeyboard { /// # } /// ``` #[doc(alias = "swkbdInputText")] - pub fn get_string(&mut self, max_bytes: usize, apt: &Apt, gfx: &Gfx) -> Result<(String, Button), Error> { + pub fn get_string( + &mut self, + max_bytes: usize, + apt: &Apt, + gfx: &Gfx, + ) -> Result<(String, Button), Error> { // Unfortunately the libctru API doesn't really provide a way to get the exact length // of the string that it receieves from the software keyboard. Instead it expects you // to pass in a buffer and hope that it's big enough to fit the entire string, so @@ -253,10 +267,11 @@ impl SoftwareKeyboard { /// # } /// ``` #[doc(alias = "swkbdInputText")] + #[allow(unused_variables)] pub fn write_exact(&mut self, buf: &mut [u8], apt: &Apt, gfx: &Gfx) -> Result { unsafe { match swkbdInputText(self.state.as_mut(), buf.as_mut_ptr(), buf.len()) { - ctru_sys::SWKBD_BUTTON_NONE => Err(self.parse_swkbd_error()), + ctru_sys::SWKBD_BUTTON_NONE => Err(self.state.result.into()), ctru_sys::SWKBD_BUTTON_LEFT => Ok(Button::Left), ctru_sys::SWKBD_BUTTON_MIDDLE => Ok(Button::Middle), ctru_sys::SWKBD_BUTTON_RIGHT => Ok(Button::Right), @@ -448,18 +463,49 @@ impl SoftwareKeyboard { pub fn set_max_text_len(&mut self, len: u16) { self.state.max_text_len = len; } +} - fn parse_swkbd_error(&self) -> Error { - match self.state.result { - ctru_sys::SWKBD_INVALID_INPUT => Error::InvalidParameters, - ctru_sys::SWKBD_OUTOFMEM => Error::OutOfMem, - ctru_sys::SWKBD_HOMEPRESSED => Error::HomePressed, - ctru_sys::SWKBD_RESETPRESSED => Error::ResetPressed, - ctru_sys::SWKBD_POWERPRESSED => Error::PowerPressed, - ctru_sys::SWKBD_PARENTAL_OK => Error::ParentalOk, - ctru_sys::SWKBD_PARENTAL_FAIL => Error::ParentalFail, - ctru_sys::SWKBD_BANNED_INPUT => Error::BannedInput, - _ => unreachable!(), +impl ParentalLock { + /// Initialize a new configuration for the Parental Lock applet. + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # fn main() { + /// # + /// use ctru::applets::swkbd::{SoftwareKeyboard, Kind}; + /// + /// // Standard keyboard. + /// let keyboard = SoftwareKeyboard::new(Kind::Normal, 2); + /// + /// // Numpad (with only the "confirm" button). + /// let keyboard = SoftwareKeyboard::new(Kind::Numpad, 1); + /// # + /// # } + #[doc(alias = "swkbdInit")] + pub fn new() -> Self { + unsafe { + let mut state = Box::::default(); + swkbdInit(state.as_mut(), Kind::Normal.into(), 1, -1); + swkbdSetFeatures(state.as_mut(), ctru_sys::SWKBD_PARENTAL); + Self { state } + } + } + + /// Launch the Parental Lock applet based on the configuration and return a result depending on whether the operation was successful or not. + #[doc(alias = "swkbdInputText")] + #[allow(unused_variables)] + pub fn launch(&mut self, apt: &Apt, gfx: &Gfx) -> Result<(), Error> { + unsafe { + let mut buf = [0; 10]; + swkbdInputText(self.state.as_mut(), buf.as_mut_ptr(), 10); + let e = self.state.result.into(); + + match e { + Error::ParentalOk => Ok(()), + _ => Err(e), + } } } } @@ -496,12 +542,35 @@ impl Display for Error { f, "input given to the software keyboard triggered the active filters" ), + Self::ButtonPressed => write!(f, "on-screen button was pressed to exit the prompt"), } } } impl std::error::Error for Error {} +impl From for Error { + fn from(value: ctru_sys::SwkbdResult) -> Self { + match value { + ctru_sys::SWKBD_INVALID_INPUT => Error::InvalidParameters, + ctru_sys::SWKBD_OUTOFMEM => Error::OutOfMem, + ctru_sys::SWKBD_HOMEPRESSED => Error::HomePressed, + ctru_sys::SWKBD_RESETPRESSED => Error::ResetPressed, + ctru_sys::SWKBD_POWERPRESSED => Error::PowerPressed, + ctru_sys::SWKBD_PARENTAL_OK => Error::ParentalOk, + ctru_sys::SWKBD_PARENTAL_FAIL => Error::ParentalFail, + ctru_sys::SWKBD_BANNED_INPUT => Error::BannedInput, + ctru_sys::SWKBD_D0_CLICK => Error::ButtonPressed, + ctru_sys::SWKBD_D1_CLICK0 => Error::ButtonPressed, + ctru_sys::SWKBD_D1_CLICK1 => Error::ButtonPressed, + ctru_sys::SWKBD_D2_CLICK0 => Error::ButtonPressed, + ctru_sys::SWKBD_D2_CLICK1 => Error::ButtonPressed, + ctru_sys::SWKBD_D2_CLICK2 => Error::ButtonPressed, + _ => unreachable!(), + } + } +} + from_impl!(Kind, ctru_sys::SwkbdType); from_impl!(Button, ctru_sys::SwkbdButton); from_impl!(Error, ctru_sys::SwkbdResult); From f39a5260fbed1ea5d09e0235505b734ee785e95d Mon Sep 17 00:00:00 2001 From: Andrea Ciliberti Date: Sat, 23 Dec 2023 13:22:37 +0100 Subject: [PATCH 03/15] Better num-buttons and docs --- ctru-rs/examples/software-keyboard.rs | 5 ++- ctru-rs/src/applets/swkbd.rs | 59 +++++++++++++++++++-------- 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/ctru-rs/examples/software-keyboard.rs b/ctru-rs/examples/software-keyboard.rs index cc8d81c..fb83df2 100644 --- a/ctru-rs/examples/software-keyboard.rs +++ b/ctru-rs/examples/software-keyboard.rs @@ -14,7 +14,10 @@ fn main() { // Prepares a software keyboard with two buttons: one to cancel input and one // to accept it. You can also use `SoftwareKeyboard::new()` to launch the keyboard // with different configurations. - let mut keyboard = SoftwareKeyboard::default(); + let mut keyboard = SoftwareKeyboard::new( + ctru::applets::swkbd::Kind::Normal, + ctru::applets::swkbd::ButtonConfig::LeftMiddleRight, + ); println!("Press A to enter some text or press Start to exit."); diff --git a/ctru-rs/src/applets/swkbd.rs b/ctru-rs/src/applets/swkbd.rs index e84620e..9841090 100644 --- a/ctru-rs/src/applets/swkbd.rs +++ b/ctru-rs/src/applets/swkbd.rs @@ -1,7 +1,7 @@ //! Software Keyboard applet. //! //! This applet opens a virtual keyboard on the console's bottom screen which lets the user write UTF-16 valid text. -// TODO: Implement remaining functionality (password mode, filter callbacks, etc.). Also improve "max text length" API. Improve `number of buttons` API when creating a new SoftwareKeyboard. +// TODO: Implement remaining functionality (password mode, filter callbacks, etc.). Also improve "max text length" API. #![doc(alias = "keyboard")] use crate::services::{apt::Apt, gfx::Gfx}; @@ -66,6 +66,18 @@ pub enum Button { Right = ctru_sys::SWKBD_BUTTON_RIGHT, } +/// Configuration to setup the on-screen buttons to exit the [`SoftwareKeyboard`] prompt. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[repr(i32)] +pub enum ButtonConfig { + /// 1 Button: considered the right button. + Right = 1, + /// 2 Buttons: left and right buttons. + LeftRight = 2, + /// 3 Buttons: left, middle and right buttons. + LeftMiddleRight = 3, +} + /// Error returned by an unsuccessful [`SoftwareKeyboard::get_string()`]. #[doc(alias = "SwkbdResult")] #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -176,20 +188,20 @@ impl SoftwareKeyboard { /// # let _runner = test_runner::GdbRunner::default(); /// # fn main() { /// # - /// use ctru::applets::swkbd::{SoftwareKeyboard, Kind}; + /// use ctru::applets::swkbd::{SoftwareKeyboard, ButtonConfig, Kind}; /// - /// // Standard keyboard. - /// let keyboard = SoftwareKeyboard::new(Kind::Normal, 2); + /// // Standard keyboard. Equivalent to `SoftwareKeyboard::default()`. + /// let keyboard = SoftwareKeyboard::new(Kind::Normal, ButtonConfig::LeftRight); /// /// // Numpad (with only the "confirm" button). - /// let keyboard = SoftwareKeyboard::new(Kind::Numpad, 1); + /// let keyboard = SoftwareKeyboard::new(Kind::Numpad, ButtonConfig::Right); /// # /// # } #[doc(alias = "swkbdInit")] - pub fn new(keyboard_type: Kind, num_buttons: i32) -> Self { + pub fn new(keyboard_type: Kind, buttons: ButtonConfig) -> Self { unsafe { let mut state = Box::::default(); - swkbdInit(state.as_mut(), keyboard_type.into(), num_buttons, -1); + swkbdInit(state.as_mut(), keyboard_type.into(), buttons.into(), -1); Self { state } } } @@ -413,10 +425,10 @@ impl SoftwareKeyboard { /// # let _runner = test_runner::GdbRunner::default(); /// # fn main() { /// # - /// use ctru::applets::swkbd::{SoftwareKeyboard, Button, Kind}; + /// use ctru::applets::swkbd::{SoftwareKeyboard, Button, ButtonConfig, Kind}; /// /// // We create a `SoftwareKeyboard` with left and right buttons. - /// let mut keyboard = SoftwareKeyboard::new(Kind::Normal, 2); + /// let mut keyboard = SoftwareKeyboard::new(Kind::Normal, ButtonConfig::LeftRight); /// /// // Set the left button text to "Cancel" and pressing it will NOT return the user's input. /// keyboard.configure_button(Button::Left, "Cancel", false); @@ -474,13 +486,9 @@ impl ParentalLock { /// # let _runner = test_runner::GdbRunner::default(); /// # fn main() { /// # - /// use ctru::applets::swkbd::{SoftwareKeyboard, Kind}; + /// use ctru::applets::swkbd::ParentalLock; /// - /// // Standard keyboard. - /// let keyboard = SoftwareKeyboard::new(Kind::Normal, 2); - /// - /// // Numpad (with only the "confirm" button). - /// let keyboard = SoftwareKeyboard::new(Kind::Numpad, 1); + /// let parental_lock = ParentalLock::new(); /// # /// # } #[doc(alias = "swkbdInit")] @@ -494,6 +502,24 @@ impl ParentalLock { } /// Launch the Parental Lock applet based on the configuration and return a result depending on whether the operation was successful or not. + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # fn main() { + /// # + /// use ctru::applets::swkbd::ParentalLock; + /// + /// let parental_lock = ParentalLock::new(); + /// + /// match parental_lock.launch() { + /// Ok(_) => println!("You can access parental-only features and settings."), + /// Err(Error::ParentalFail) => println!("Is a kid trying to access this?"), + /// Err(_) => println!("Something wrong happened during the parental lock prompt.") + /// } + /// # + /// # } #[doc(alias = "swkbdInputText")] #[allow(unused_variables)] pub fn launch(&mut self, apt: &Apt, gfx: &Gfx) -> Result<(), Error> { @@ -513,7 +539,7 @@ impl ParentalLock { /// Creates a new [`SoftwareKeyboard`] configuration set to using a [`Kind::Normal`] keyboard and 2 [`Button`]s. impl Default for SoftwareKeyboard { fn default() -> Self { - SoftwareKeyboard::new(Kind::Normal, 2) + SoftwareKeyboard::new(Kind::Normal, ButtonConfig::LeftRight) } } @@ -575,3 +601,4 @@ from_impl!(Kind, ctru_sys::SwkbdType); from_impl!(Button, ctru_sys::SwkbdButton); from_impl!(Error, ctru_sys::SwkbdResult); from_impl!(ValidInput, i32); +from_impl!(ButtonConfig, i32); From 95faa5f2a7d075af163e032b2a5ef2064c6773a6 Mon Sep 17 00:00:00 2001 From: Andrea Ciliberti Date: Sat, 23 Dec 2023 14:16:26 +0100 Subject: [PATCH 04/15] SWKBD method additions and doc tests --- ctru-rs/examples/software-keyboard.rs | 5 +- ctru-rs/src/applets/mii_selector.rs | 6 +- ctru-rs/src/applets/swkbd.rs | 116 ++++++++++++++++++++++---- 3 files changed, 104 insertions(+), 23 deletions(-) diff --git a/ctru-rs/examples/software-keyboard.rs b/ctru-rs/examples/software-keyboard.rs index fb83df2..cc8d81c 100644 --- a/ctru-rs/examples/software-keyboard.rs +++ b/ctru-rs/examples/software-keyboard.rs @@ -14,10 +14,7 @@ fn main() { // Prepares a software keyboard with two buttons: one to cancel input and one // to accept it. You can also use `SoftwareKeyboard::new()` to launch the keyboard // with different configurations. - let mut keyboard = SoftwareKeyboard::new( - ctru::applets::swkbd::Kind::Normal, - ctru::applets::swkbd::ButtonConfig::LeftMiddleRight, - ); + let mut keyboard = SoftwareKeyboard::default(); println!("Press A to enter some text or press Start to exit."); diff --git a/ctru-rs/src/applets/mii_selector.rs b/ctru-rs/src/applets/mii_selector.rs index 01d5808..079168e 100644 --- a/ctru-rs/src/applets/mii_selector.rs +++ b/ctru-rs/src/applets/mii_selector.rs @@ -262,6 +262,10 @@ impl MiiSelector { /// ```no_run /// # use std::error::Error; /// # fn main() -> Result<(), Box> { + /// # use ctru::services::{apt::Apt, gfx::Gfx}; + /// # + /// # let gfx = Gfx::new().unwrap(); + /// # let apt = Apt::new().unwrap(); /// # /// use ctru::applets::mii_selector::{MiiSelector, Options}; /// @@ -271,7 +275,7 @@ impl MiiSelector { /// let opts = Options::ENABLE_CANCEL & Options::ENABLE_GUESTS; /// mii_selector.set_options(opts); /// - /// let result = mii_selector.launch()?; + /// let result = mii_selector.launch(&apt, &gfx)?; /// # /// # Ok(()) /// # } diff --git a/ctru-rs/src/applets/swkbd.rs b/ctru-rs/src/applets/swkbd.rs index 9841090..ca72544 100644 --- a/ctru-rs/src/applets/swkbd.rs +++ b/ctru-rs/src/applets/swkbd.rs @@ -1,14 +1,11 @@ //! Software Keyboard applet. //! //! This applet opens a virtual keyboard on the console's bottom screen which lets the user write UTF-16 valid text. -// TODO: Implement remaining functionality (password mode, filter callbacks, etc.). Also improve "max text length" API. +// TODO: Implement remaining functionality (filter callbacks, etc.). Also improve "max text length" API. #![doc(alias = "keyboard")] use crate::services::{apt::Apt, gfx::Gfx}; -use ctru_sys::{ - self, swkbdInit, swkbdInputText, swkbdSetButton, swkbdSetFeatures, swkbdSetHintText, - swkbdSetInitialText, SwkbdState, -}; +use ctru_sys::{self, SwkbdState}; use bitflags::bitflags; use libc; @@ -66,6 +63,21 @@ pub enum Button { Right = ctru_sys::SWKBD_BUTTON_RIGHT, } +/// Represents the password mode to conceal the input text for the [`SoftwareKeyboard`]. +/// +/// Can be set using [`SoftwareKeyboard::set_password_mode()`]. +#[doc(alias = "SwkbdPasswordMode")] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[repr(u32)] +pub enum PasswordMode { + /// The input text will not be concealed. + None = ctru_sys::SWKBD_PASSWORD_NONE, + /// The input text will be concealed immediately after typing. + Hide = ctru_sys::SWKBD_PASSWORD_HIDE, + /// The input text will be concealed a second after typing. + HideDelay = ctru_sys::SWKBD_PASSWORD_HIDE_DELAY, +} + /// Configuration to setup the on-screen buttons to exit the [`SoftwareKeyboard`] prompt. #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[repr(i32)] @@ -201,7 +213,7 @@ impl SoftwareKeyboard { pub fn new(keyboard_type: Kind, buttons: ButtonConfig) -> Self { unsafe { let mut state = Box::::default(); - swkbdInit(state.as_mut(), keyboard_type.into(), buttons.into(), -1); + ctru_sys::swkbdInit(state.as_mut(), keyboard_type.into(), buttons.into(), -1); Self { state } } } @@ -219,11 +231,15 @@ impl SoftwareKeyboard { /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { + /// # use ctru::services::{apt::Apt, gfx::Gfx}; + /// # + /// # let gfx = Gfx::new().unwrap(); + /// # let apt = Apt::new().unwrap(); /// # /// use ctru::applets::swkbd::SoftwareKeyboard; /// let mut keyboard = SoftwareKeyboard::default(); /// - /// let (text, button) = keyboard.get_string(2048)?; + /// let (text, button) = keyboard.get_string(2048, &apt, &gfx)?; /// # /// # Ok(()) /// # } @@ -267,13 +283,17 @@ impl SoftwareKeyboard { /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { + /// # use ctru::services::{apt::Apt, gfx::Gfx}; + /// # + /// # let gfx = Gfx::new().unwrap(); + /// # let apt = Apt::new().unwrap(); /// # /// use ctru::applets::swkbd::SoftwareKeyboard; /// let mut keyboard = SoftwareKeyboard::default(); /// /// let mut buffer = vec![0; 100]; /// - /// let button = keyboard.write_exact(&mut buffer)?; + /// let button = keyboard.write_exact(&mut buffer, &apt, &gfx)?; /// # /// # Ok(()) /// # } @@ -282,7 +302,7 @@ impl SoftwareKeyboard { #[allow(unused_variables)] pub fn write_exact(&mut self, buf: &mut [u8], apt: &Apt, gfx: &Gfx) -> Result { unsafe { - match swkbdInputText(self.state.as_mut(), buf.as_mut_ptr(), buf.len()) { + match ctru_sys::swkbdInputText(self.state.as_mut(), buf.as_mut_ptr(), buf.len()) { ctru_sys::SWKBD_BUTTON_NONE => Err(self.state.result.into()), ctru_sys::SWKBD_BUTTON_LEFT => Ok(Button::Left), ctru_sys::SWKBD_BUTTON_MIDDLE => Ok(Button::Middle), @@ -310,7 +330,7 @@ impl SoftwareKeyboard { /// # } #[doc(alias = "swkbdSetFeatures")] pub fn set_features(&mut self, features: Features) { - unsafe { swkbdSetFeatures(self.state.as_mut(), features.bits()) } + unsafe { ctru_sys::swkbdSetFeatures(self.state.as_mut(), features.bits()) } } /// Configure input validation for this keyboard. @@ -383,7 +403,7 @@ impl SoftwareKeyboard { pub fn set_initial_text(&mut self, text: &str) { unsafe { let nul_terminated: String = text.chars().chain(once('\0')).collect(); - swkbdSetInitialText(self.state.as_mut(), nul_terminated.as_ptr()); + ctru_sys::swkbdSetInitialText(self.state.as_mut(), nul_terminated.as_ptr()); } } @@ -407,7 +427,57 @@ impl SoftwareKeyboard { pub fn set_hint_text(&mut self, text: &str) { unsafe { let nul_terminated: String = text.chars().chain(once('\0')).collect(); - swkbdSetHintText(self.state.as_mut(), nul_terminated.as_ptr()); + ctru_sys::swkbdSetHintText(self.state.as_mut(), nul_terminated.as_ptr()); + } + } + + /// Set a password mode for this software keyboard. + /// + /// Depending on the selected mode the input text will be concealed. + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # fn main() { + /// # + /// use ctru::applets::swkbd::{SoftwareKeyboard, PasswordMode}; + /// let mut keyboard = SoftwareKeyboard::default(); + /// + /// keyboard.set_password_mode(PasswordMode::Hide); + /// # + /// # } + #[doc(alias = "swkbdSetPasswordMode")] + pub fn set_password_mode(&mut self, mode: PasswordMode) { + unsafe { + ctru_sys::swkbdSetPasswordMode(self.state.as_mut(), mode.into()); + } + } + + /// Set the 2 custom characters to add to the keyboard while using [`Kind::Numpad`]. + /// + /// These characters will appear in their own buttons right next to the `0` key. + /// + /// # Notes + /// + /// You can set one or both of these keys to `NUL` (value 0) to avoid showing the additional buttons to the user. + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # fn main() { + /// # + /// use ctru::applets::swkbd::{SoftwareKeyboard, Kind, ButtonConfig}; + /// let mut keyboard = SoftwareKeyboard::new(Kind::Numpad, ButtonConfig::LeftRight); + /// + /// keyboard.set_numpad_keys(('#', '.')); + /// # + /// # } + #[doc(alias = "swkbdSetNumpadKeys")] + pub fn set_numpad_keys(&mut self, keys: (char, char)) { + unsafe { + ctru_sys::swkbdSetNumpadKeys(self.state.as_mut(), keys.0 as i32, keys.1 as i32); } } @@ -441,7 +511,7 @@ impl SoftwareKeyboard { pub fn configure_button(&mut self, button: Button, text: &str, submit: bool) { unsafe { let nul_terminated: String = text.chars().chain(once('\0')).collect(); - swkbdSetButton( + ctru_sys::swkbdSetButton( self.state.as_mut(), button.into(), nul_terminated.as_ptr(), @@ -454,6 +524,8 @@ impl SoftwareKeyboard { /// keyboard. By default the limit is `65000` code units. /// /// # Notes + /// + /// This action will overwrite any previously submitted [`ValidInput`] validation. /// /// Keyboard input is converted from UTF-16 to UTF-8 before being handed to Rust, /// so this code point limit does not necessarily equal the max number of UTF-8 code points @@ -474,6 +546,9 @@ impl SoftwareKeyboard { /// # } pub fn set_max_text_len(&mut self, len: u16) { self.state.max_text_len = len; + + // Activate the specific validation rule for maximum length. + self.state.valid_input = ValidInput::FixedLen.into(); } } @@ -495,8 +570,8 @@ impl ParentalLock { pub fn new() -> Self { unsafe { let mut state = Box::::default(); - swkbdInit(state.as_mut(), Kind::Normal.into(), 1, -1); - swkbdSetFeatures(state.as_mut(), ctru_sys::SWKBD_PARENTAL); + ctru_sys::swkbdInit(state.as_mut(), Kind::Normal.into(), 1, -1); + ctru_sys::swkbdSetFeatures(state.as_mut(), ctru_sys::SWKBD_PARENTAL); Self { state } } } @@ -508,12 +583,15 @@ impl ParentalLock { /// ``` /// # let _runner = test_runner::GdbRunner::default(); /// # fn main() { + /// # use ctru::services::{apt::Apt, gfx::Gfx}; /// # - /// use ctru::applets::swkbd::ParentalLock; + /// # let gfx = Gfx::new().unwrap(); + /// # let apt = Apt::new().unwrap(); + /// use ctru::applets::swkbd::{ParentalLock, Error}; /// /// let parental_lock = ParentalLock::new(); /// - /// match parental_lock.launch() { + /// match parental_lock.launch(&apt, &gfx) { /// Ok(_) => println!("You can access parental-only features and settings."), /// Err(Error::ParentalFail) => println!("Is a kid trying to access this?"), /// Err(_) => println!("Something wrong happened during the parental lock prompt.") @@ -525,7 +603,7 @@ impl ParentalLock { pub fn launch(&mut self, apt: &Apt, gfx: &Gfx) -> Result<(), Error> { unsafe { let mut buf = [0; 10]; - swkbdInputText(self.state.as_mut(), buf.as_mut_ptr(), 10); + ctru_sys::swkbdInputText(self.state.as_mut(), buf.as_mut_ptr(), 10); let e = self.state.result.into(); match e { @@ -601,4 +679,6 @@ from_impl!(Kind, ctru_sys::SwkbdType); from_impl!(Button, ctru_sys::SwkbdButton); from_impl!(Error, ctru_sys::SwkbdResult); from_impl!(ValidInput, i32); +from_impl!(ValidInput, u32); from_impl!(ButtonConfig, i32); +from_impl!(PasswordMode, u32); From fa613c25cbd44122c8db7f685dde979fef2dd0c7 Mon Sep 17 00:00:00 2001 From: Andrea Ciliberti Date: Thu, 28 Dec 2023 16:59:21 +0100 Subject: [PATCH 05/15] Working filter callback --- ctru-rs/src/applets/swkbd.rs | 59 ++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/ctru-rs/src/applets/swkbd.rs b/ctru-rs/src/applets/swkbd.rs index ca72544..6ff10fb 100644 --- a/ctru-rs/src/applets/swkbd.rs +++ b/ctru-rs/src/applets/swkbd.rs @@ -1,7 +1,6 @@ //! Software Keyboard applet. //! //! This applet opens a virtual keyboard on the console's bottom screen which lets the user write UTF-16 valid text. -// TODO: Implement remaining functionality (filter callbacks, etc.). Also improve "max text length" API. #![doc(alias = "keyboard")] use crate::services::{apt::Apt, gfx::Gfx}; @@ -9,6 +8,7 @@ use ctru_sys::{self, SwkbdState}; use bitflags::bitflags; use libc; +use std::ffi::CStr; use std::fmt::Display; use std::iter::once; use std::str; @@ -48,6 +48,21 @@ pub enum Kind { Western = ctru_sys::SWKBD_TYPE_WESTERN, } +/// The type of result returned by a custom filter callback. +/// +/// The custom callback can be set using [`SoftwareKeyboard::set_filter_callback()`]. +#[doc(alias = "SwkbdCallbackResult")] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[repr(u32)] +pub enum CallbackResult { + /// The callback yields a positive result. + Ok = ctru_sys::SWKBD_CALLBACK_OK, + /// The callback finds the input invalid, but lets the user try again. + Retry = ctru_sys::SWKBD_CALLBACK_CONTINUE, + /// The callback finds the input invalid and closes the Software Keyboard view. + Close = ctru_sys::SWKBD_CALLBACK_CLOSE, +} + /// Represents which button the user pressed to close the [`SoftwareKeyboard`]. /// /// Button text and behaviour can be customized with [`SoftwareKeyboard::configure_button()`]. @@ -357,6 +372,45 @@ impl SoftwareKeyboard { self.state.filter_flags = filters.bits(); } + /// Configure a custom filtering function to validate the input. + /// + /// The callback function will return a callback result and the error message to display when the input is invalid. + /// + /// # Notes + /// + /// This function will overwrite any currently set filter configuration. + pub fn set_filter_callback(&mut self, callback: F) + where + F: FnOnce(&str) -> CallbackResult, + { + unsafe extern "C" fn internal_callback( + user: *mut libc::c_void, + _pp_message: *mut *const libc::c_char, + text: *const libc::c_char, + _text_size: libc::size_t, + ) -> ctru_sys::SwkbdCallbackResult + where + F: FnOnce(&str) -> CallbackResult, + { + let closure = Box::from_raw(user as *mut Box); + + let text = CStr::from_ptr(text); + let text_slice: &str = text.to_str().unwrap(); + + closure(text_slice).into() + } + + let boxed_callback = Box::new(Box::new(callback)); + + unsafe { + ctru_sys::swkbdSetFilterCallback( + self.state.as_mut(), + Some(internal_callback::), + Box::into_raw(boxed_callback).cast(), + ) + }; + } + /// Configure the maximum number of digits that can be entered in the keyboard when the [`Filters::DIGITS`] flag is enabled. /// /// # Example @@ -524,7 +578,7 @@ impl SoftwareKeyboard { /// keyboard. By default the limit is `65000` code units. /// /// # Notes - /// + /// /// This action will overwrite any previously submitted [`ValidInput`] validation. /// /// Keyboard input is converted from UTF-16 to UTF-8 before being handed to Rust, @@ -682,3 +736,4 @@ from_impl!(ValidInput, i32); from_impl!(ValidInput, u32); from_impl!(ButtonConfig, i32); from_impl!(PasswordMode, u32); +from_impl!(CallbackResult, u32); From 815aa610dc202757853a6274767ebf252ce860dc Mon Sep 17 00:00:00 2001 From: Andrea Ciliberti Date: Thu, 28 Dec 2023 18:41:20 +0100 Subject: [PATCH 06/15] Still thinking about the filter callbacks... --- ctru-rs/src/applets/swkbd.rs | 67 +++++++++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 16 deletions(-) diff --git a/ctru-rs/src/applets/swkbd.rs b/ctru-rs/src/applets/swkbd.rs index 6ff10fb..771d88c 100644 --- a/ctru-rs/src/applets/swkbd.rs +++ b/ctru-rs/src/applets/swkbd.rs @@ -8,7 +8,7 @@ use ctru_sys::{self, SwkbdState}; use bitflags::bitflags; use libc; -use std::ffi::CStr; +use std::ffi::{CStr, CString}; use std::fmt::Display; use std::iter::once; use std::str; @@ -199,10 +199,6 @@ bitflags! { const BACKSLASH = ctru_sys::SWKBD_FILTER_BACKSLASH; /// Disallow the use of profanity via Nintendo's profanity filter. const PROFANITY = ctru_sys::SWKBD_FILTER_PROFANITY; - /// Use a custom callback in order to filter the input. - /// - /// TODO: It's currently impossible to setup a custom filter callback. - const CALLBACK = ctru_sys::SWKBD_FILTER_CALLBACK; } } @@ -378,26 +374,52 @@ impl SoftwareKeyboard { /// /// # Notes /// - /// This function will overwrite any currently set filter configuration. + /// The filter callback will work only for the next time the keyboard is used. After using it once, it must be set again. + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # fn main() { + /// # + /// use ctru::applets::swkbd::{SoftwareKeyboard, CallbackInput}; + /// let mut keyboard = SoftwareKeyboard::default(); + /// + /// keyboard.set_filter_callback(|text| { + /// if text.contains("boo") { + /// println!("Ah, you scared me!"); + /// } + /// + /// (CallbackResult::Ok, None) + /// }); + /// # + /// # } pub fn set_filter_callback(&mut self, callback: F) where - F: FnOnce(&str) -> CallbackResult, + F: FnOnce(&str) -> (CallbackResult, Option), { unsafe extern "C" fn internal_callback( user: *mut libc::c_void, - _pp_message: *mut *const libc::c_char, + pp_message: *mut *const libc::c_char, text: *const libc::c_char, _text_size: libc::size_t, ) -> ctru_sys::SwkbdCallbackResult where - F: FnOnce(&str) -> CallbackResult, + F: FnOnce(&str) -> (CallbackResult, Option), { let closure = Box::from_raw(user as *mut Box); let text = CStr::from_ptr(text); let text_slice: &str = text.to_str().unwrap(); - closure(text_slice).into() + let result = closure(text_slice); + + if let Some(cstr) = result.1 { + *pp_message = cstr.as_ptr(); + Box::leak(Box::new(cstr)); // Definitely SHOULD NOT do this, but as far as design goes, it's clean. + } + + result.0.into() } let boxed_callback = Box::new(Box::new(callback)); @@ -511,10 +533,10 @@ impl SoftwareKeyboard { /// Set the 2 custom characters to add to the keyboard while using [`Kind::Numpad`]. /// /// These characters will appear in their own buttons right next to the `0` key. - /// + /// /// # Notes - /// - /// You can set one or both of these keys to `NUL` (value 0) to avoid showing the additional buttons to the user. + /// + /// If `None` is passed as either key, that button will not be shown to the user. /// /// # Example /// @@ -525,13 +547,26 @@ impl SoftwareKeyboard { /// use ctru::applets::swkbd::{SoftwareKeyboard, Kind, ButtonConfig}; /// let mut keyboard = SoftwareKeyboard::new(Kind::Numpad, ButtonConfig::LeftRight); /// - /// keyboard.set_numpad_keys(('#', '.')); + /// keyboard.set_numpad_keys(Some('#'), Some('.')); + /// + /// // The right numpad key will not be shown. + /// keyboard.set_numpad_keys(Some('!'), None); /// # /// # } #[doc(alias = "swkbdSetNumpadKeys")] - pub fn set_numpad_keys(&mut self, keys: (char, char)) { + pub fn set_numpad_keys(&mut self, left_key: Option, right_key: Option) { + let mut keys = (0, 0); + + if let Some(k) = left_key { + keys.0 = k as i32; + } + + if let Some(k) = right_key { + keys.1 = k as i32; + } + unsafe { - ctru_sys::swkbdSetNumpadKeys(self.state.as_mut(), keys.0 as i32, keys.1 as i32); + ctru_sys::swkbdSetNumpadKeys(self.state.as_mut(), keys.0, keys.1); } } From 84b95a9d4c54821844a50a2c23679a06a79b1111 Mon Sep 17 00:00:00 2001 From: Andrea Ciliberti Date: Thu, 28 Dec 2023 19:16:08 +0100 Subject: [PATCH 07/15] Fmt and lints --- ctru-rs/src/applets/swkbd.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ctru-rs/src/applets/swkbd.rs b/ctru-rs/src/applets/swkbd.rs index 771d88c..b4a2ba3 100644 --- a/ctru-rs/src/applets/swkbd.rs +++ b/ctru-rs/src/applets/swkbd.rs @@ -375,21 +375,21 @@ impl SoftwareKeyboard { /// # Notes /// /// The filter callback will work only for the next time the keyboard is used. After using it once, it must be set again. - /// + /// /// # Example /// /// ``` /// # let _runner = test_runner::GdbRunner::default(); /// # fn main() { /// # - /// use ctru::applets::swkbd::{SoftwareKeyboard, CallbackInput}; + /// use ctru::applets::swkbd::{SoftwareKeyboard, CallbackResult}; /// let mut keyboard = SoftwareKeyboard::default(); - /// + /// /// keyboard.set_filter_callback(|text| { /// if text.contains("boo") { /// println!("Ah, you scared me!"); /// } - /// + /// /// (CallbackResult::Ok, None) /// }); /// # @@ -533,9 +533,9 @@ impl SoftwareKeyboard { /// Set the 2 custom characters to add to the keyboard while using [`Kind::Numpad`]. /// /// These characters will appear in their own buttons right next to the `0` key. - /// + /// /// # Notes - /// + /// /// If `None` is passed as either key, that button will not be shown to the user. /// /// # Example @@ -548,7 +548,7 @@ impl SoftwareKeyboard { /// let mut keyboard = SoftwareKeyboard::new(Kind::Numpad, ButtonConfig::LeftRight); /// /// keyboard.set_numpad_keys(Some('#'), Some('.')); - /// + /// /// // The right numpad key will not be shown. /// keyboard.set_numpad_keys(Some('!'), None); /// # @@ -678,7 +678,7 @@ impl ParentalLock { /// # let apt = Apt::new().unwrap(); /// use ctru::applets::swkbd::{ParentalLock, Error}; /// - /// let parental_lock = ParentalLock::new(); + /// let mut parental_lock = ParentalLock::new(); /// /// match parental_lock.launch(&apt, &gfx) { /// Ok(_) => println!("You can access parental-only features and settings."), From 0c302e8d23d067e3b4dfe4e079b50012751069cd Mon Sep 17 00:00:00 2001 From: Andrea Ciliberti Date: Tue, 2 Jan 2024 16:26:43 +0100 Subject: [PATCH 08/15] Fix lints --- ctru-rs/examples/file-explorer.rs | 2 +- ctru-rs/examples/mii-selector.rs | 2 +- ctru-rs/src/applets/swkbd.rs | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/ctru-rs/examples/file-explorer.rs b/ctru-rs/examples/file-explorer.rs index 3ad37c9..49855ab 100644 --- a/ctru-rs/examples/file-explorer.rs +++ b/ctru-rs/examples/file-explorer.rs @@ -165,7 +165,7 @@ impl<'a> FileExplorer<'a> { fn get_input_and_run(&mut self, action: impl FnOnce(&mut Self, String)) { let mut keyboard = SoftwareKeyboard::default(); - match keyboard.get_string(2048) { + match keyboard.get_string(2048, &self.apt, &self.gfx) { Ok((path, Button::Right)) => { // Clicked "OK". action(self, path); diff --git a/ctru-rs/examples/mii-selector.rs b/ctru-rs/examples/mii-selector.rs index 3b2b0ef..55b6b15 100644 --- a/ctru-rs/examples/mii-selector.rs +++ b/ctru-rs/examples/mii-selector.rs @@ -21,7 +21,7 @@ fn main() { mii_selector.set_title("Great Mii Selector!"); // Launch the Mii Selector and use its result to print the selected Mii's information. - match mii_selector.launch() { + match mii_selector.launch(&apt, &gfx) { Ok(result) => { println!("Mii type: {:?}", result.mii_type); println!("Name: {:?}", result.mii_data.name); diff --git a/ctru-rs/src/applets/swkbd.rs b/ctru-rs/src/applets/swkbd.rs index b4a2ba3..768d942 100644 --- a/ctru-rs/src/applets/swkbd.rs +++ b/ctru-rs/src/applets/swkbd.rs @@ -710,6 +710,12 @@ impl Default for SoftwareKeyboard { } } +impl Default for ParentalLock { + fn default() -> Self { + ParentalLock::new() + } +} + impl Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { From a727feb11c6a9f83afc7006e451313d13a7bf3d7 Mon Sep 17 00:00:00 2001 From: Andrea Ciliberti Date: Wed, 3 Jan 2024 13:53:42 +0100 Subject: [PATCH 09/15] Fixed nits --- ctru-rs/examples/file-explorer.rs | 2 +- ctru-rs/src/applets/mii_selector.rs | 3 +-- ctru-rs/src/applets/swkbd.rs | 7 +++---- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/ctru-rs/examples/file-explorer.rs b/ctru-rs/examples/file-explorer.rs index 49855ab..5c158ac 100644 --- a/ctru-rs/examples/file-explorer.rs +++ b/ctru-rs/examples/file-explorer.rs @@ -165,7 +165,7 @@ impl<'a> FileExplorer<'a> { fn get_input_and_run(&mut self, action: impl FnOnce(&mut Self, String)) { let mut keyboard = SoftwareKeyboard::default(); - match keyboard.get_string(2048, &self.apt, &self.gfx) { + match keyboard.get_string(2048, self.apt, self.gfx) { Ok((path, Button::Right)) => { // Clicked "OK". action(self, path); diff --git a/ctru-rs/src/applets/mii_selector.rs b/ctru-rs/src/applets/mii_selector.rs index 079168e..43cdbb3 100644 --- a/ctru-rs/src/applets/mii_selector.rs +++ b/ctru-rs/src/applets/mii_selector.rs @@ -281,8 +281,7 @@ impl MiiSelector { /// # } /// ``` #[doc(alias = "miiSelectorLaunch")] - #[allow(unused_variables)] - pub fn launch(&mut self, apt: &Apt, gfx: &Gfx) -> Result { + pub fn launch(&mut self, _apt: &Apt, _gfx: &Gfx) -> Result { let mut return_val = Box::::default(); unsafe { ctru_sys::miiSelectorLaunch(self.config.as_mut(), return_val.as_mut()) } diff --git a/ctru-rs/src/applets/swkbd.rs b/ctru-rs/src/applets/swkbd.rs index 768d942..c6325e7 100644 --- a/ctru-rs/src/applets/swkbd.rs +++ b/ctru-rs/src/applets/swkbd.rs @@ -310,8 +310,7 @@ impl SoftwareKeyboard { /// # } /// ``` #[doc(alias = "swkbdInputText")] - #[allow(unused_variables)] - pub fn write_exact(&mut self, buf: &mut [u8], apt: &Apt, gfx: &Gfx) -> Result { + pub fn write_exact(&mut self, buf: &mut [u8], _apt: &Apt, _gfx: &Gfx) -> Result { unsafe { match ctru_sys::swkbdInputText(self.state.as_mut(), buf.as_mut_ptr(), buf.len()) { ctru_sys::SWKBD_BUTTON_NONE => Err(self.state.result.into()), @@ -407,7 +406,7 @@ impl SoftwareKeyboard { where F: FnOnce(&str) -> (CallbackResult, Option), { - let closure = Box::from_raw(user as *mut Box); + let closure: Box> = Box::from_raw(user.cast()); let text = CStr::from_ptr(text); let text_slice: &str = text.to_str().unwrap(); @@ -691,7 +690,7 @@ impl ParentalLock { #[allow(unused_variables)] pub fn launch(&mut self, apt: &Apt, gfx: &Gfx) -> Result<(), Error> { unsafe { - let mut buf = [0; 10]; + let mut buf = [0; 0]; ctru_sys::swkbdInputText(self.state.as_mut(), buf.as_mut_ptr(), 10); let e = self.state.result.into(); From 366f4d4b1b792f4159a3832bf063582d0831bd2c Mon Sep 17 00:00:00 2001 From: Andrea Ciliberti Date: Wed, 3 Jan 2024 19:34:27 +0100 Subject: [PATCH 10/15] Zero bufsize for parental lock --- ctru-rs/src/applets/swkbd.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ctru-rs/src/applets/swkbd.rs b/ctru-rs/src/applets/swkbd.rs index c6325e7..c38764b 100644 --- a/ctru-rs/src/applets/swkbd.rs +++ b/ctru-rs/src/applets/swkbd.rs @@ -691,7 +691,7 @@ impl ParentalLock { pub fn launch(&mut self, apt: &Apt, gfx: &Gfx) -> Result<(), Error> { unsafe { let mut buf = [0; 0]; - ctru_sys::swkbdInputText(self.state.as_mut(), buf.as_mut_ptr(), 10); + ctru_sys::swkbdInputText(self.state.as_mut(), buf.as_mut_ptr(), 0); let e = self.state.result.into(); match e { From 1413bfa2016a8ea63a98a0cdd5e7ce5e62534bf6 Mon Sep 17 00:00:00 2001 From: Andrea Ciliberti Date: Wed, 3 Jan 2024 20:53:04 +0100 Subject: [PATCH 11/15] New filter callback --- ctru-rs/examples/software-keyboard.rs | 20 ++++- ctru-rs/src/applets/swkbd.rs | 114 +++++++++++++++----------- 2 files changed, 87 insertions(+), 47 deletions(-) diff --git a/ctru-rs/examples/software-keyboard.rs b/ctru-rs/examples/software-keyboard.rs index cc8d81c..c48b8a5 100644 --- a/ctru-rs/examples/software-keyboard.rs +++ b/ctru-rs/examples/software-keyboard.rs @@ -2,9 +2,12 @@ //! //! This example showcases the use of the Software Keyboard applet to receive text input from the user. -use ctru::applets::swkbd::{Button, SoftwareKeyboard}; +use ctru::applets::swkbd::{Button, CallbackResult, SoftwareKeyboard}; use ctru::prelude::*; +use std::borrow::Cow; +use std::ffi::CString; + fn main() { let apt = Apt::new().unwrap(); let mut hid = Hid::new().unwrap(); @@ -16,6 +19,21 @@ fn main() { // with different configurations. let mut keyboard = SoftwareKeyboard::default(); + // Custom filter callback to handle the given input. + // Using this callback it's possible to integrate the applet + // with custom error messages when the input is incorrect. + keyboard.set_filter_callback(Some(Box::new(|str| { + // The string is guaranteed to contain valid Unicode text, so we can safely unwrap and use it as a normal `&str`. + if str.to_str().unwrap().contains("boo") { + return ( + CallbackResult::Retry, + Some(Cow::Owned(CString::new("Ah, you scared me!").unwrap())), + ); + } + + (CallbackResult::Ok, None) + }))); + println!("Press A to enter some text or press Start to exit."); while apt.main_loop() { diff --git a/ctru-rs/src/applets/swkbd.rs b/ctru-rs/src/applets/swkbd.rs index c38764b..8f82307 100644 --- a/ctru-rs/src/applets/swkbd.rs +++ b/ctru-rs/src/applets/swkbd.rs @@ -8,16 +8,21 @@ use ctru_sys::{self, SwkbdState}; use bitflags::bitflags; use libc; -use std::ffi::{CStr, CString}; + +use std::borrow::Cow; +use std::ffi::CStr; use std::fmt::Display; use std::iter::once; use std::str; +type CallbackFunction = dyn Fn(&CStr) -> (CallbackResult, Option>); + /// Configuration structure to setup the Software Keyboard applet. #[doc(alias = "SwkbdState")] -#[derive(Clone)] -pub struct SoftwareKeyboard { +pub struct SoftwareKeyboard<'a> { state: Box, + callback: Option>, + error_message: Option>, } /// Configuration structure to setup the Parental Lock applet. @@ -202,7 +207,7 @@ bitflags! { } } -impl SoftwareKeyboard { +impl SoftwareKeyboard<'_> { /// Initialize a new configuration for the Software Keyboard applet depending on how many "exit" buttons are available to the user (1, 2 or 3). /// /// # Example @@ -225,7 +230,11 @@ impl SoftwareKeyboard { unsafe { let mut state = Box::::default(); ctru_sys::swkbdInit(state.as_mut(), keyboard_type.into(), buttons.into(), -1); - Self { state } + Self { + state, + callback: None, + error_message: None, + } } } @@ -312,6 +321,13 @@ impl SoftwareKeyboard { #[doc(alias = "swkbdInputText")] pub fn write_exact(&mut self, buf: &mut [u8], _apt: &Apt, _gfx: &Gfx) -> Result { unsafe { + // The filter callback gets reset every time the SoftwareKeyboard is used. + ctru_sys::swkbdSetFilterCallback( + self.state.as_mut(), + Some(Self::internal_callback), + (self as *mut Self).cast(), + ); + match ctru_sys::swkbdInputText(self.state.as_mut(), buf.as_mut_ptr(), buf.len()) { ctru_sys::SWKBD_BUTTON_NONE => Err(self.state.result.into()), ctru_sys::SWKBD_BUTTON_LEFT => Ok(Button::Left), @@ -369,11 +385,13 @@ impl SoftwareKeyboard { /// Configure a custom filtering function to validate the input. /// - /// The callback function will return a callback result and the error message to display when the input is invalid. - /// + /// The callback function must return a [`CallbackResult`] and the error message to display when the input is invalid. + /// /// # Notes - /// - /// The filter callback will work only for the next time the keyboard is used. After using it once, it must be set again. + /// + /// Passing [`None`] will unbind the custom filter callback. + /// + /// The error message returned by the callback should be shorter than `256` characters, otherwise it will be truncated. /// /// # Example /// @@ -384,52 +402,56 @@ impl SoftwareKeyboard { /// use ctru::applets::swkbd::{SoftwareKeyboard, CallbackResult}; /// let mut keyboard = SoftwareKeyboard::default(); /// - /// keyboard.set_filter_callback(|text| { - /// if text.contains("boo") { - /// println!("Ah, you scared me!"); + /// keyboard.set_filter_callback(Some(Box::new(|str| { + /// if str.to_str().unwrap().contains("boo") { + /// return ( + /// CallbackResult::Retry, + /// Some(Cow::Owned(CString::new("Ah, you scared me!").unwrap())), + /// ); /// } - /// + /// /// (CallbackResult::Ok, None) - /// }); + /// }))); /// # /// # } - pub fn set_filter_callback(&mut self, callback: F) - where - F: FnOnce(&str) -> (CallbackResult, Option), - { - unsafe extern "C" fn internal_callback( - user: *mut libc::c_void, - pp_message: *mut *const libc::c_char, - text: *const libc::c_char, - _text_size: libc::size_t, - ) -> ctru_sys::SwkbdCallbackResult - where - F: FnOnce(&str) -> (CallbackResult, Option), - { - let closure: Box> = Box::from_raw(user.cast()); + pub fn set_filter_callback(&mut self, callback: Option>) { + self.callback = callback; + } + + /// Internal function called by the filter callback. + extern "C" fn internal_callback( + user: *mut libc::c_void, + pp_message: *mut *const libc::c_char, + text: *const libc::c_char, + _text_size: libc::size_t, + ) -> ctru_sys::SwkbdCallbackResult { + let this: *mut SoftwareKeyboard = user.cast(); + + unsafe { + // Reset any leftover error message. + (*this).error_message = None; let text = CStr::from_ptr(text); - let text_slice: &str = text.to_str().unwrap(); - let result = closure(text_slice); + let result = { + // Run the callback if still available. + if let Some(callback) = &mut (*this).callback { + let (res, cstr) = callback(text); - if let Some(cstr) = result.1 { - *pp_message = cstr.as_ptr(); - Box::leak(Box::new(cstr)); // Definitely SHOULD NOT do this, but as far as design goes, it's clean. - } + (*this).error_message = cstr; - result.0.into() - } + if let Some(newstr) = &(*this).error_message { + *pp_message = newstr.as_ptr(); + } - let boxed_callback = Box::new(Box::new(callback)); + res + } else { + CallbackResult::Ok + } + }; - unsafe { - ctru_sys::swkbdSetFilterCallback( - self.state.as_mut(), - Some(internal_callback::), - Box::into_raw(boxed_callback).cast(), - ) - }; + result.into() + } } /// Configure the maximum number of digits that can be entered in the keyboard when the [`Filters::DIGITS`] flag is enabled. @@ -535,7 +557,7 @@ impl SoftwareKeyboard { /// /// # Notes /// - /// If `None` is passed as either key, that button will not be shown to the user. + /// If [`None`] is passed as either key, that button will not be shown to the user. /// /// # Example /// @@ -703,7 +725,7 @@ impl ParentalLock { } /// Creates a new [`SoftwareKeyboard`] configuration set to using a [`Kind::Normal`] keyboard and 2 [`Button`]s. -impl Default for SoftwareKeyboard { +impl Default for SoftwareKeyboard<'_> { fn default() -> Self { SoftwareKeyboard::new(Kind::Normal, ButtonConfig::LeftRight) } From b92074d35afd183cbd18a2a8054bae4eafb9b8b3 Mon Sep 17 00:00:00 2001 From: Andrea Ciliberti Date: Wed, 3 Jan 2024 20:57:01 +0100 Subject: [PATCH 12/15] Fmt --- ctru-rs/src/applets/swkbd.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ctru-rs/src/applets/swkbd.rs b/ctru-rs/src/applets/swkbd.rs index 8f82307..cd0229e 100644 --- a/ctru-rs/src/applets/swkbd.rs +++ b/ctru-rs/src/applets/swkbd.rs @@ -386,11 +386,11 @@ impl SoftwareKeyboard<'_> { /// Configure a custom filtering function to validate the input. /// /// The callback function must return a [`CallbackResult`] and the error message to display when the input is invalid. - /// + /// /// # Notes - /// + /// /// Passing [`None`] will unbind the custom filter callback. - /// + /// /// The error message returned by the callback should be shorter than `256` characters, otherwise it will be truncated. /// /// # Example @@ -409,7 +409,7 @@ impl SoftwareKeyboard<'_> { /// Some(Cow::Owned(CString::new("Ah, you scared me!").unwrap())), /// ); /// } - /// + /// /// (CallbackResult::Ok, None) /// }))); /// # From fcbbf0ef30c7bd93b7979accf2567da841874a23 Mon Sep 17 00:00:00 2001 From: Andrea Ciliberti Date: Wed, 3 Jan 2024 22:08:03 +0100 Subject: [PATCH 13/15] Fix tests --- ctru-rs/src/applets/swkbd.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ctru-rs/src/applets/swkbd.rs b/ctru-rs/src/applets/swkbd.rs index cd0229e..2f2c558 100644 --- a/ctru-rs/src/applets/swkbd.rs +++ b/ctru-rs/src/applets/swkbd.rs @@ -399,7 +399,10 @@ impl SoftwareKeyboard<'_> { /// # let _runner = test_runner::GdbRunner::default(); /// # fn main() { /// # + /// use std::borrow::Cow; + /// use std::ffi::CString; /// use ctru::applets::swkbd::{SoftwareKeyboard, CallbackResult}; + /// /// let mut keyboard = SoftwareKeyboard::default(); /// /// keyboard.set_filter_callback(Some(Box::new(|str| { From cdd31d30911146936482447760d10ce3e7788a5f Mon Sep 17 00:00:00 2001 From: Andrea Ciliberti Date: Wed, 3 Jan 2024 22:10:10 +0100 Subject: [PATCH 14/15] Fmt --- ctru-rs/src/applets/swkbd.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ctru-rs/src/applets/swkbd.rs b/ctru-rs/src/applets/swkbd.rs index 2f2c558..7ae533d 100644 --- a/ctru-rs/src/applets/swkbd.rs +++ b/ctru-rs/src/applets/swkbd.rs @@ -402,7 +402,7 @@ impl SoftwareKeyboard<'_> { /// use std::borrow::Cow; /// use std::ffi::CString; /// use ctru::applets::swkbd::{SoftwareKeyboard, CallbackResult}; - /// + /// /// let mut keyboard = SoftwareKeyboard::default(); /// /// keyboard.set_filter_callback(Some(Box::new(|str| { From 5948589bdba0d463ac7f4c4e383dba490f96d5ed Mon Sep 17 00:00:00 2001 From: Andrea Ciliberti Date: Fri, 5 Jan 2024 13:29:59 +0100 Subject: [PATCH 15/15] Fix suggestions --- ctru-rs/examples/software-keyboard.rs | 3 +-- ctru-rs/src/applets/swkbd.rs | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/ctru-rs/examples/software-keyboard.rs b/ctru-rs/examples/software-keyboard.rs index c48b8a5..1d24a7d 100644 --- a/ctru-rs/examples/software-keyboard.rs +++ b/ctru-rs/examples/software-keyboard.rs @@ -5,7 +5,6 @@ use ctru::applets::swkbd::{Button, CallbackResult, SoftwareKeyboard}; use ctru::prelude::*; -use std::borrow::Cow; use std::ffi::CString; fn main() { @@ -27,7 +26,7 @@ fn main() { if str.to_str().unwrap().contains("boo") { return ( CallbackResult::Retry, - Some(Cow::Owned(CString::new("Ah, you scared me!").unwrap())), + Some(CString::new("Ah, you scared me!").unwrap()), ); } diff --git a/ctru-rs/src/applets/swkbd.rs b/ctru-rs/src/applets/swkbd.rs index 7ae533d..41d40cd 100644 --- a/ctru-rs/src/applets/swkbd.rs +++ b/ctru-rs/src/applets/swkbd.rs @@ -9,20 +9,19 @@ use ctru_sys::{self, SwkbdState}; use bitflags::bitflags; use libc; -use std::borrow::Cow; -use std::ffi::CStr; +use std::ffi::{CStr, CString}; use std::fmt::Display; use std::iter::once; use std::str; -type CallbackFunction = dyn Fn(&CStr) -> (CallbackResult, Option>); +type CallbackFunction = dyn Fn(&CStr) -> (CallbackResult, Option); /// Configuration structure to setup the Software Keyboard applet. #[doc(alias = "SwkbdState")] -pub struct SoftwareKeyboard<'a> { +pub struct SoftwareKeyboard { state: Box, callback: Option>, - error_message: Option>, + error_message: Option, } /// Configuration structure to setup the Parental Lock applet. @@ -207,7 +206,7 @@ bitflags! { } } -impl SoftwareKeyboard<'_> { +impl SoftwareKeyboard { /// Initialize a new configuration for the Software Keyboard applet depending on how many "exit" buttons are available to the user (1, 2 or 3). /// /// # Example @@ -409,7 +408,7 @@ impl SoftwareKeyboard<'_> { /// if str.to_str().unwrap().contains("boo") { /// return ( /// CallbackResult::Retry, - /// Some(Cow::Owned(CString::new("Ah, you scared me!").unwrap())), + /// Some(CString::new("Ah, you scared me!").unwrap()), /// ); /// } /// @@ -441,6 +440,8 @@ impl SoftwareKeyboard<'_> { if let Some(callback) = &mut (*this).callback { let (res, cstr) = callback(text); + // Due to how `libctru` operates, the user is expected to keep the error message alive until + // the end of the Software Keyboard prompt. We ensure that happens by saving it within the configuration. (*this).error_message = cstr; if let Some(newstr) = &(*this).error_message { @@ -712,8 +713,7 @@ impl ParentalLock { /// # /// # } #[doc(alias = "swkbdInputText")] - #[allow(unused_variables)] - pub fn launch(&mut self, apt: &Apt, gfx: &Gfx) -> Result<(), Error> { + pub fn launch(&mut self, _apt: &Apt, _gfx: &Gfx) -> Result<(), Error> { unsafe { let mut buf = [0; 0]; ctru_sys::swkbdInputText(self.state.as_mut(), buf.as_mut_ptr(), 0); @@ -728,7 +728,7 @@ impl ParentalLock { } /// Creates a new [`SoftwareKeyboard`] configuration set to using a [`Kind::Normal`] keyboard and 2 [`Button`]s. -impl Default for SoftwareKeyboard<'_> { +impl Default for SoftwareKeyboard { fn default() -> Self { SoftwareKeyboard::new(Kind::Normal, ButtonConfig::LeftRight) }