From c18bb0d4cb301c422e6f1312c77a3bc5831cd3c2 Mon Sep 17 00:00:00 2001 From: Fenrir Date: Sat, 28 Apr 2018 22:27:17 -0600 Subject: [PATCH 1/6] Initial software keyboard support --- ctru-rs/src/applet/mod.rs | 1 + ctru-rs/src/applet/swkbd.rs | 193 ++++++++++++++++++++++++++++++++++++ ctru-rs/src/lib.rs | 1 + 3 files changed, 195 insertions(+) create mode 100644 ctru-rs/src/applet/mod.rs create mode 100644 ctru-rs/src/applet/swkbd.rs diff --git a/ctru-rs/src/applet/mod.rs b/ctru-rs/src/applet/mod.rs new file mode 100644 index 0000000..73469a3 --- /dev/null +++ b/ctru-rs/src/applet/mod.rs @@ -0,0 +1 @@ +pub mod swkbd; diff --git a/ctru-rs/src/applet/swkbd.rs b/ctru-rs/src/applet/swkbd.rs new file mode 100644 index 0000000..27b08ee --- /dev/null +++ b/ctru-rs/src/applet/swkbd.rs @@ -0,0 +1,193 @@ +use std::mem; +use std::str; + +use libctru::{self, SwkbdState, swkbdInit, swkbdSetFeatures, swkbdInputText}; + +use libc; + +/// An instance of the software keyboard. +pub struct Swkbd { + state: Box, +} + +/// The kind of keyboard to be initialized. +/// +/// Normal is the full keyboard with several pages (QUERTY/accents/symbol/mobile) +/// Querty is a QUERTY-only keyboard. +/// Numpad is a number pad. +/// Western is a text keyboard without japanese symbols (only applies to JPN systems). For other +/// systems it's the same as a Normal keyboard. +#[derive(Copy, Clone, Debug)] +pub enum Kind { + Normal, + Querty, + Numpad, + Western, +} + +/// Represents which button the user pressed to close the software keyboard. +#[derive(Copy, Clone, Debug)] +pub enum Button { + Left, + Middle, + Right, +} + +/// Error type for the software keyboard. +#[derive(Copy, Clone, Debug)] +pub enum Error { + InvalidInput, + OutOfMem, + HomePressed, + ResetPressed, + PowerPressed, + ParentalOk, + ParentalFail, + BannedInput, +} + +/// Restrictions on keyboard input +#[derive(Copy, Clone, Debug)] +pub enum ValidInput { + Anything, + NotEmpty, + NotEmptyNotBlank, + NotBlank, + FixedLen, +} + +/// Keyboard feature flags +bitflags! { + pub struct Features: u32 { + const PARENTAL_PIN = 1 << 0; + const DARKEN_TOP_SCREEN = 1 << 1; + const PREDICTIVE_INPUT = 1 << 2; + const MULTILINE = 1 << 3; + const FIXED_WIDTH = 1 << 4; + const ALLOW_HOME = 1 << 5; + const ALLOW_RESET = 1 << 6; + const ALLOW_POWER = 1 << 7; + const DEFAULT_QUERTY = 1 << 8; + } +} + +/// Keyboard input filtering flags +bitflags! { + pub struct Filters: u32 { + const DIGITS = 1 << 0; + const AT = 1 << 1; + const PERCENT = 1 << 2; + const BACKSLASH = 1 << 3; + const PROFANITY = 1 << 4; + const CALLBACK = 1 << 5; + } +} + +impl Swkbd { + /// Initializes a software keyboard of the specified type and the chosen number of buttons + /// (from 1-3). + pub fn init(keyboard_type: Kind, num_buttons: i32) -> Self { + unsafe { + let mut state = Box::new(mem::uninitialized::()); + swkbdInit(state.as_mut(), keyboard_type as u32, num_buttons, -1); + Swkbd { state } + } + } + + /// Gets input from this keyboard and appends it to the provided string. + /// + /// The text received from the keyboard can be up to 2048 bytes in length. + pub fn get_utf8(&mut self, buf: &mut String) -> Result { + // 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. + const MAX_BYTES: usize = 2048; + let mut tmp = [0u8; MAX_BYTES]; + let button = unsafe { self.get_bytes(&mut tmp)? }; + + // Make sure we haven't overflowed our buffer. libctru might already check this, + // but we'll do it here too just in case + let len = unsafe { libc::strlen(tmp.as_ptr()) }; + assert!(len <= MAX_BYTES); + + // Not sure if this is falliable or not in this stage of the process, + // but catch any decoding errors to be sure + let utf8 = match str::from_utf8(&tmp[..len]) { + Ok(parsed) => parsed, + Err(_) => return Err(Error::InvalidInput), + }; + + // Finally, copy the validated input into the user's `String` + *buf += utf8; + Ok(button) + } + + /// Fills the provided buffer with a NUL-terminated sequence of bytes from the software + /// keyboard + /// + /// # Unsafety + /// + /// The received bytes are nominally UTF-8 formatted, but the provided buffer must be large + /// enough to receive both the text from the software keyboard along with a NUL-terminator. + /// Otherwise undefined behavior can result. + pub unsafe fn get_bytes(&mut self, buf: &mut [u8]) -> Result { + match swkbdInputText(self.state.as_mut(), buf.as_mut_ptr(), buf.len()) { + libctru::SWKBD_BUTTON_NONE => Err(self.parse_swkbd_error()), + libctru::SWKBD_BUTTON_LEFT => Ok(Button::Left), + libctru::SWKBD_BUTTON_MIDDLE => Ok(Button::Middle), + libctru::SWKBD_BUTTON_RIGHT => Ok(Button::Right), + _ => unreachable!(), + } + } + + /// Sets special features for this keyboard + pub fn set_features(&mut self, features: Features) { + unsafe { + swkbdSetFeatures(self.state.as_mut(), features.bits) + } + } + + /// Configures input validation for this keyboard + pub fn set_validation(&mut self, validation: ValidInput, + filters: Filters) { + self.state.valid_input = validation as i32; + self.state.filter_flags = filters.bits; + } + + /// Configures the maximum number of digits that can be entered in the keyboard when the + /// `Filters::DIGITS` flag is enabled + pub fn set_max_digits(&mut self, digits: u16) { + self.state.max_digits = digits; + } + + /// Configures the maximum number of UTF-16 code units that can be entered into the software + /// keyboard. By default the limit is 0xFDE8 code units. + /// + /// Note that 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 + /// receivable by the `get_utf8` and `get_bytes` functions. + 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 { + libctru::SWKBD_INVALID_INPUT => Error::InvalidInput, + libctru::SWKBD_OUTOFMEM => Error::OutOfMem, + libctru::SWKBD_HOMEPRESSED => Error::HomePressed, + libctru::SWKBD_RESETPRESSED => Error::ResetPressed, + libctru::SWKBD_POWERPRESSED => Error::PowerPressed, + libctru::SWKBD_PARENTAL_OK => Error::ParentalOk, + libctru::SWKBD_PARENTAL_FAIL => Error::ParentalFail, + libctru::SWKBD_BANNED_INPUT => Error::BannedInput, + _ => unreachable!(), + } + } +} + +impl Default for Swkbd { + fn default() -> Self { + Swkbd::init(Kind::Normal, 2) + } +} diff --git a/ctru-rs/src/lib.rs b/ctru-rs/src/lib.rs index 56ac370..c98ca65 100644 --- a/ctru-rs/src/lib.rs +++ b/ctru-rs/src/lib.rs @@ -11,6 +11,7 @@ extern crate widestring; extern crate ctru_sys as libctru; +pub mod applet; pub mod console; pub mod error; pub mod srv; From 74de604c4c26ddde8b4937a46b3cc2b1a27e432b Mon Sep 17 00:00:00 2001 From: Fenrir Date: Sun, 29 Apr 2018 14:01:36 -0600 Subject: [PATCH 2/6] make `get_bytes` a safe function libctru seems to ensure that output from the software keyboard is always well-formed --- ctru-rs/src/applet/swkbd.rs | 48 ++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/ctru-rs/src/applet/swkbd.rs b/ctru-rs/src/applet/swkbd.rs index 27b08ee..7f3dcfd 100644 --- a/ctru-rs/src/applet/swkbd.rs +++ b/ctru-rs/src/applet/swkbd.rs @@ -96,7 +96,8 @@ impl Swkbd { /// Gets input from this keyboard and appends it to the provided string. /// - /// The text received from the keyboard can be up to 2048 bytes in length. + /// The text received from the keyboard will be truncated if it is greater than 2048 bytes + /// in length. pub fn get_utf8(&mut self, buf: &mut String) -> Result { // 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 @@ -104,40 +105,33 @@ impl Swkbd { // you have to set some upper limit on the potential size of the user's input. const MAX_BYTES: usize = 2048; let mut tmp = [0u8; MAX_BYTES]; - let button = unsafe { self.get_bytes(&mut tmp)? }; + let button = self.get_bytes(&mut tmp)?; - // Make sure we haven't overflowed our buffer. libctru might already check this, - // but we'll do it here too just in case + // 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 + // should be safe. let len = unsafe { libc::strlen(tmp.as_ptr()) }; - assert!(len <= MAX_BYTES); + let utf8 = unsafe { str::from_utf8_unchecked(&tmp[..len]) }; - // Not sure if this is falliable or not in this stage of the process, - // but catch any decoding errors to be sure - let utf8 = match str::from_utf8(&tmp[..len]) { - Ok(parsed) => parsed, - Err(_) => return Err(Error::InvalidInput), - }; - - // Finally, copy the validated input into the user's `String` + // Copy the input into the user's `String` *buf += utf8; Ok(button) } - /// Fills the provided buffer with a NUL-terminated sequence of bytes from the software - /// keyboard - /// - /// # Unsafety + /// Fills the provided buffer with a UTF-8 encoded, NUL-terminated sequence of bytes from + /// this software keyboard. /// - /// The received bytes are nominally UTF-8 formatted, but the provided buffer must be large - /// enough to receive both the text from the software keyboard along with a NUL-terminator. - /// Otherwise undefined behavior can result. - pub unsafe fn get_bytes(&mut self, buf: &mut [u8]) -> Result { - match swkbdInputText(self.state.as_mut(), buf.as_mut_ptr(), buf.len()) { - libctru::SWKBD_BUTTON_NONE => Err(self.parse_swkbd_error()), - libctru::SWKBD_BUTTON_LEFT => Ok(Button::Left), - libctru::SWKBD_BUTTON_MIDDLE => Ok(Button::Middle), - libctru::SWKBD_BUTTON_RIGHT => Ok(Button::Right), - _ => unreachable!(), + /// If the buffer is too small to contain the entire sequence received from the keyboard, + /// the output will be truncated but should still be well-formed UTF-8 + pub fn get_bytes(&mut self, buf: &mut [u8]) -> Result { + unsafe { + match swkbdInputText(self.state.as_mut(), buf.as_mut_ptr(), buf.len()) { + libctru::SWKBD_BUTTON_NONE => Err(self.parse_swkbd_error()), + libctru::SWKBD_BUTTON_LEFT => Ok(Button::Left), + libctru::SWKBD_BUTTON_MIDDLE => Ok(Button::Middle), + libctru::SWKBD_BUTTON_RIGHT => Ok(Button::Right), + _ => unreachable!(), + } } } From c490600f137775f5114b987d9aaef54ee319bd98 Mon Sep 17 00:00:00 2001 From: Fenrir Date: Sun, 29 Apr 2018 14:09:34 -0600 Subject: [PATCH 3/6] rename `applet` module to `applets` --- ctru-rs/src/{applet => applets}/mod.rs | 0 ctru-rs/src/{applet => applets}/swkbd.rs | 0 ctru-rs/src/lib.rs | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename ctru-rs/src/{applet => applets}/mod.rs (100%) rename ctru-rs/src/{applet => applets}/swkbd.rs (100%) diff --git a/ctru-rs/src/applet/mod.rs b/ctru-rs/src/applets/mod.rs similarity index 100% rename from ctru-rs/src/applet/mod.rs rename to ctru-rs/src/applets/mod.rs diff --git a/ctru-rs/src/applet/swkbd.rs b/ctru-rs/src/applets/swkbd.rs similarity index 100% rename from ctru-rs/src/applet/swkbd.rs rename to ctru-rs/src/applets/swkbd.rs diff --git a/ctru-rs/src/lib.rs b/ctru-rs/src/lib.rs index c98ca65..c3caf62 100644 --- a/ctru-rs/src/lib.rs +++ b/ctru-rs/src/lib.rs @@ -11,7 +11,7 @@ extern crate widestring; extern crate ctru_sys as libctru; -pub mod applet; +pub mod applets; pub mod console; pub mod error; pub mod srv; From 4f776778f1958b80aeca653b9fc53aaad9d6085e Mon Sep 17 00:00:00 2001 From: Fenrir Date: Sun, 29 Apr 2018 14:49:36 -0600 Subject: [PATCH 4/6] add `set_hint_text` and `configure_button` --- ctru-rs/src/applets/swkbd.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/ctru-rs/src/applets/swkbd.rs b/ctru-rs/src/applets/swkbd.rs index 7f3dcfd..ee6d99a 100644 --- a/ctru-rs/src/applets/swkbd.rs +++ b/ctru-rs/src/applets/swkbd.rs @@ -1,7 +1,8 @@ use std::mem; use std::str; -use libctru::{self, SwkbdState, swkbdInit, swkbdSetFeatures, swkbdInputText}; +use libctru::{self, SwkbdState, swkbdInit, swkbdSetFeatures, swkbdSetHintText, swkbdInputText, + swkbdSetButton}; use libc; @@ -155,6 +156,26 @@ impl Swkbd { self.state.max_digits = digits; } + /// Sets the hint text for this software keyboard (that is, the help text that is displayed + /// when the textbox is empty) + pub fn set_hint_text(&mut self, text: &str) { + unsafe { + swkbdSetHintText(self.state.as_mut(), text.as_ptr()); + } + } + + /// Configures the look and behavior of a button for this keyboard. + /// + /// `button` is the `Button` to be configured + /// `text` configures the display text for the button + /// `submit` configures whether pressing the button will accept the keyboard's input or + /// discard it. + pub fn configure_button(&mut self, button: Button, text: &str, submit: bool) { + unsafe { + swkbdSetButton(self.state.as_mut(), button as u32, text.as_ptr(), submit); + } + } + /// Configures the maximum number of UTF-16 code units that can be entered into the software /// keyboard. By default the limit is 0xFDE8 code units. /// From d166c7358ffafcee8e56d2709bb8d43dd6c8a63f Mon Sep 17 00:00:00 2001 From: Fenrir Date: Sun, 29 Apr 2018 16:02:54 -0600 Subject: [PATCH 5/6] Add software keyboard example --- examples/src/bin/software-keyboard.rs | 47 +++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 examples/src/bin/software-keyboard.rs diff --git a/examples/src/bin/software-keyboard.rs b/examples/src/bin/software-keyboard.rs new file mode 100644 index 0000000..cb1b058 --- /dev/null +++ b/examples/src/bin/software-keyboard.rs @@ -0,0 +1,47 @@ +extern crate ctru; + +use ctru::gfx::Gfx; +use ctru::console::Console; +use ctru::services::apt::Apt; +use ctru::services::hid::{Hid, KeyPad}; +use ctru::applets::swkbd::{Swkbd, Button}; + +fn main() { + let apt = Apt::init().unwrap(); + let hid = Hid::init().unwrap(); + let gfx = Gfx::default(); + let _console = Console::default(); + + println!("Press A to enter some text or press Start to quit"); + + while apt.main_loop() { + gfx.flush_buffers(); + gfx.swap_buffers(); + gfx.wait_for_vblank(); + + hid.scan_input(); + + if hid.keys_down().contains(KeyPad::KEY_A) { + // Prepares a software keyboard with two buttons: One to cancel input and one + // to accept it. You can also use `Swkbd::init()` to launch the keyboard in different + // configurations. + let mut keyboard = Swkbd::default(); + + // String used to store text received from the keyboard + let mut text = String::new(); + + // Raise the software keyboard. You can perform different actions depending on which + // software button the user pressed + match keyboard.get_utf8(&mut text) { + Ok(Button::Right) => println!("You entered: {}", text), + Ok(Button::Left) => println!("Cancelled"), + Ok(Button::Middle) => println!("How did you even press this?"), + Err(_) => println!("Oh noes, an error happened!"), + } + } + + if hid.keys_down().contains(KeyPad::KEY_START) { + break; + } + } +} From 741ef60a24193ef18ae688eb2034347260e9d78c Mon Sep 17 00:00:00 2001 From: Fenrir Date: Sun, 29 Apr 2018 17:31:52 -0600 Subject: [PATCH 6/6] Add nul-terminatator to input that expects it --- ctru-rs/src/applets/swkbd.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ctru-rs/src/applets/swkbd.rs b/ctru-rs/src/applets/swkbd.rs index ee6d99a..515dd92 100644 --- a/ctru-rs/src/applets/swkbd.rs +++ b/ctru-rs/src/applets/swkbd.rs @@ -1,3 +1,4 @@ +use std::iter::once; use std::mem; use std::str; @@ -160,7 +161,8 @@ impl Swkbd { /// when the textbox is empty) pub fn set_hint_text(&mut self, text: &str) { unsafe { - swkbdSetHintText(self.state.as_mut(), text.as_ptr()); + let nul_terminated: String = text.chars().chain(once('\0')).collect(); + swkbdSetHintText(self.state.as_mut(), nul_terminated.as_ptr()); } } @@ -172,7 +174,8 @@ impl Swkbd { /// discard it. pub fn configure_button(&mut self, button: Button, text: &str, submit: bool) { unsafe { - swkbdSetButton(self.state.as_mut(), button as u32, text.as_ptr(), submit); + let nul_terminated: String = text.chars().chain(once('\0')).collect(); + swkbdSetButton(self.state.as_mut(), button as u32, nul_terminated.as_ptr(), submit); } }