Added system locale support (#1427)

* add system locale hint, query & event

* update changelog

* cleanup

* fix multiple hint formatting, changelog

* locale formatting optimization, additional formatting tests

* Update changelog.md

Co-authored-by: Antoni Spaanderman <49868160+antonilol@users.noreply.github.com>

* remove redundant `Result` return type from `format_locale_hint`

---------

Co-authored-by: Antoni Spaanderman <49868160+antonilol@users.noreply.github.com>
This commit is contained in:
renski
2025-03-19 17:59:34 +02:00
committed by GitHub
parent dc6f555c31
commit fca5c3782e
5 changed files with 216 additions and 1 deletions

View File

@@ -27,6 +27,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

View File

@@ -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

View File

@@ -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<T: std::borrow::Borrow<Locale>>(
locales: impl IntoIterator<Item = T>,
) -> bool {
set(PREFERRED_LOCALES, &format_locale_hint(locales))
}
fn format_locale_hint<T: std::borrow::Borrow<Locale>>(
locales: impl IntoIterator<Item = T>,
) -> 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"
);
}
}

View File

@@ -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;

92
src/sdl2/locale.rs Normal file
View File

@@ -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<String>,
}
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::<Vec<_>>();
/// 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<Self::Item> {
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<Locale> {
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<String> {
if ptr.is_null() {
None
} else {
Some(std::ffi::CStr::from_ptr(ptr).to_string_lossy().into_owned())
}
}