diff --git a/changelog.md b/changelog.md index 12808ac830..b91bd9d9f8 100644 --- a/changelog.md +++ b/changelog.md @@ -9,6 +9,8 @@ when upgrading from a version of rust-sdl2 to another. [PR #1407](https://github.com/Rust-SDL2/rust-sdl2/pull/1407) Add new use_ios_framework for linking to SDL2.framework on iOS +[PR #1427](https://github.com/Rust-SDL2/rust-sdl2/pull/1427) **BREAKING CHANGE** Add system locale support. A new event type `Event::LocaleChanged` has been added. + ### v0.37.0 [PR #1406](https://github.com/Rust-SDL2/rust-sdl2/pull/1406) Update bindings to SDL 2.0.26, add Event.is\_touch() for mouse events, upgrade wgpu to 0.20 in examples diff --git a/src/sdl2/event.rs b/src/sdl2/event.rs index 820a086b35..8c4a6a2413 100644 --- a/src/sdl2/event.rs +++ b/src/sdl2/event.rs @@ -320,6 +320,8 @@ pub enum EventType { RenderTargetsReset = SDL_EventType::SDL_RENDER_TARGETS_RESET as u32, RenderDeviceReset = SDL_EventType::SDL_RENDER_DEVICE_RESET as u32, + LocaleChanged = SDL_EventType::SDL_LOCALECHANGED as u32, + User = SDL_EventType::SDL_USEREVENT as u32, Last = SDL_EventType::SDL_LASTEVENT as u32, } @@ -897,6 +899,10 @@ pub enum Event { timestamp: u32, }, + LocaleChanged { + timestamp: u32, + }, + User { timestamp: u32, window_id: u32, @@ -1975,6 +1981,10 @@ impl Event { timestamp: raw.common.timestamp, }, + EventType::LocaleChanged => Event::LocaleChanged { + timestamp: raw.common.timestamp, + }, + EventType::First => panic!("Unused event, EventType::First, was encountered"), EventType::Last => panic!("Unusable event, EventType::Last, was encountered"), @@ -2180,6 +2190,7 @@ impl Event { Self::AudioDeviceRemoved { timestamp, .. } => timestamp, Self::RenderTargetsReset { timestamp, .. } => timestamp, Self::RenderDeviceReset { timestamp, .. } => timestamp, + Self::LocaleChanged { timestamp, .. } => timestamp, Self::User { timestamp, .. } => timestamp, Self::Unknown { timestamp, .. } => timestamp, } @@ -2573,6 +2584,27 @@ impl Event { ) } + /// Returns `true` if this is a locale event. + /// + /// # Example + /// + /// ``` + /// use sdl2::event::Event; + /// + /// let ev = Event::LocaleChanged { + /// timestamp: 0, + /// }; + /// assert!(ev.is_locale()); + /// + /// let another_ev = Event::Quit { + /// timestamp: 0, + /// }; + /// assert!(another_ev.is_locale() == false); // Not a locale event! + /// ``` + pub fn is_locale(&self) -> bool { + matches!(self, Self::LocaleChanged { .. }) + } + /// Returns `true` if this is a user event. /// /// # Example diff --git a/src/sdl2/hint.rs b/src/sdl2/hint.rs index bc9958e291..48b8b309dd 100644 --- a/src/sdl2/hint.rs +++ b/src/sdl2/hint.rs @@ -1,8 +1,9 @@ -use crate::sys; +use crate::{locale::Locale, sys}; use libc::c_char; use std::ffi::{CStr, CString}; const VIDEO_MINIMIZE_ON_FOCUS_LOSS: &str = "SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS"; +const PREFERRED_LOCALES: &str = "SDL_PREFERRED_LOCALES"; pub enum Hint { Default, @@ -74,6 +75,44 @@ pub fn get_video_minimize_on_focus_loss() -> bool { ) } +/// A hint that overrides the user's locale settings. +/// +/// [Official SDL documentation](https://wiki.libsdl.org/SDL2/SDL_HINT_PREFERRED_LOCALES) +/// +/// # Default +/// This is disabled by default. +/// +/// # Example +/// +/// See [`crate::locale::get_preferred_locales`]. +pub fn set_preferred_locales>( + locales: impl IntoIterator, +) -> bool { + set(PREFERRED_LOCALES, &format_locale_hint(locales)) +} + +fn format_locale_hint>( + locales: impl IntoIterator, +) -> String { + use std::fmt::Write; + + let mut iter = locales.into_iter(); + let (reserve, _) = iter.size_hint(); + // Assuming that most locales will be of the form "xx_yy", + // plus 1 char for the comma. + let mut formatted = String::with_capacity(reserve * 6); + + if let Some(first) = iter.next() { + write!(formatted, "{}", first.borrow()).ok(); + } + + for locale in iter { + write!(formatted, ",{}", locale.borrow()).ok(); + } + + formatted +} + #[doc(alias = "SDL_SetHint")] pub fn set(name: &str, value: &str) -> bool { let name = CString::new(name).unwrap(); @@ -126,3 +165,52 @@ pub fn set_with_priority(name: &str, value: &str, priority: &Hint) -> bool { ) == sys::SDL_bool::SDL_TRUE } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn locale() { + // Test set_preferred_locales + let locales = [Locale { + lang: "en".to_string(), + country: Some("US".to_string()), + }]; + set_preferred_locales(&locales); + set_preferred_locales(locales); + + // Test hint formatting + assert_eq!(format_locale_hint(&[]), ""); + + assert_eq!( + format_locale_hint([Locale { + lang: "en".to_string(), + country: None, + }]), + "en" + ); + + assert_eq!( + format_locale_hint([Locale { + lang: "en".to_string(), + country: Some("US".to_string()), + }]), + "en_US" + ); + + assert_eq!( + format_locale_hint([ + Locale { + lang: "en".to_string(), + country: Some("US".to_string()), + }, + Locale { + lang: "fr".to_string(), + country: Some("FR".to_string()), + }, + ]), + "en_US,fr_FR" + ); + } +} diff --git a/src/sdl2/lib.rs b/src/sdl2/lib.rs index 20ef28b11f..ee827cff31 100644 --- a/src/sdl2/lib.rs +++ b/src/sdl2/lib.rs @@ -74,6 +74,7 @@ pub mod haptic; pub mod hint; pub mod joystick; pub mod keyboard; +pub mod locale; pub mod log; pub mod messagebox; pub mod mouse; diff --git a/src/sdl2/locale.rs b/src/sdl2/locale.rs new file mode 100644 index 0000000000..d879d7608d --- /dev/null +++ b/src/sdl2/locale.rs @@ -0,0 +1,92 @@ +//! System locale information. + +/// A locale defines a user's language and (optionally) region. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Locale { + pub lang: String, + pub country: Option, +} + +impl std::fmt::Display for Locale { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.lang)?; + + if let Some(region) = &self.country { + write!(f, "_{}", region)?; + } + + Ok(()) + } +} + +/// Get the user's preferred locales. +/// +/// [Official SDL documentation](https://wiki.libsdl.org/SDL_GetPreferredLocales) +/// +/// # Example +/// ``` +/// let locales = [sdl2::locale::Locale { +/// lang: "en".to_string(), +/// country: Some("US".to_string()), +/// }]; +/// +/// sdl2::hint::set_preferred_locales(&locales); +/// +/// let preferred_locales = sdl2::locale::get_preferred_locales().collect::>(); +/// assert_eq!(preferred_locales, locales); +/// ``` +pub fn get_preferred_locales() -> LocaleIterator { + unsafe { + LocaleIterator { + raw: sys::SDL_GetPreferredLocales(), + index: 0, + } + } +} + +pub struct LocaleIterator { + raw: *mut sys::SDL_Locale, + index: isize, +} + +impl Drop for LocaleIterator { + fn drop(&mut self) { + unsafe { sys::SDL_free(self.raw as *mut _) } + } +} + +impl Iterator for LocaleIterator { + type Item = Locale; + + fn next(&mut self) -> Option { + let locale = unsafe { get_locale(self.raw.offset(self.index))? }; + self.index += 1; + Some(locale) + } +} + +unsafe fn get_locale(ptr: *const sys::SDL_Locale) -> Option { + let sdl_locale = ptr.as_ref()?; + + if sdl_locale.language.is_null() { + return None; + } + let lang = std::ffi::CStr::from_ptr(sdl_locale.language) + .to_string_lossy() + .into_owned(); + + let region = try_get_string(sdl_locale.country); + + Some(Locale { + lang, + country: region, + }) +} + +unsafe fn try_get_string(ptr: *const i8) -> Option { + if ptr.is_null() { + None + } else { + Some(std::ffi::CStr::from_ptr(ptr).to_string_lossy().into_owned()) + } +}