From 1413bfa2016a8ea63a98a0cdd5e7ce5e62534bf6 Mon Sep 17 00:00:00 2001 From: Andrea Ciliberti Date: Wed, 3 Jan 2024 20:53:04 +0100 Subject: [PATCH] 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) }