Merge pull request #438 from wlx-team/feat-language-selector

Settings: Language selector, "Requires restart" info
This commit is contained in:
oo8dev
2026-02-13 08:33:32 +01:00
committed by GitHub
21 changed files with 366 additions and 134 deletions

View File

@@ -10,7 +10,6 @@
"APP_SETTINGS": {
"HIDE_USERNAME": "Benutzernamen ausblenden",
"OPAQUE_BACKGROUND": "Undurchsichtiger Hintergrund",
"WLX": {},
"LOOK_AND_FEEL": "Aussehen und Verhalten",
"HIDE_GRAB_HELP": "Greif-Hilfe ausblenden",
"ANIMATION_SPEED": "UI-Animationsgeschwindigkeit",
@@ -89,7 +88,9 @@
"RESET_PLAYSPACE": "Spielbereich zurücksetzen",
"RESET_PLAYSPACE_HELP": "Den Abstand des Spielbereichs zurücksetzen.",
"BLOCK_POSES_ON_KBD_INTERACTION": "Posen beim Interagieren mit der Tastatur blockieren",
"BLOCK_POSES_ON_KBD_INTERACTION_HELP": "Verhindert, dass das Spiel Posen empfängt, wenn die Tastatur angefahren wird und „Spieleingabe blockieren“ aktiviert ist"
"BLOCK_POSES_ON_KBD_INTERACTION_HELP": "Verhindert, dass das Spiel Posen empfängt, wenn die Tastatur angefahren wird und „Spieleingabe blockieren“ aktiviert ist",
"LANGUAGE": "Sprache",
"REQUIRES_RESTART": "Erfordert Neustart"
},
"HELLO": "Hallo!",
"AUDIO": {

View File

@@ -63,6 +63,7 @@
"KEYBOARD_MIDDLE_CLICK": "Keyboard middle click",
"KEYBOARD_MIDDLE_CLICK_HELP": "Modifier to use when typing\nwith purple laser",
"KEYBOARD_SOUND_ENABLED": "Keyboard sounds",
"LANGUAGE": "Language",
"LEFT_HANDED_MOUSE": "Left-handed mouse",
"LEFT_HANDED_MOUSE_HELP": "Use this if mouse buttons are swapped",
"LONG_PRESS_DURATION": "Long press duration",
@@ -74,10 +75,10 @@
"OPTION": {
"AUTO": "Automatic",
"AUTO_HELP": "ScreenCopy GPU if supported,\notherwise PipeWire GPU.",
"EYE_PINCH": "Eye + pinch",
"HMD_PINCH": "HMD + pinch",
"EYE_ONLY": "Eye only",
"EYE_PINCH": "Eye + pinch",
"HMD_ONLY": "HMD only",
"HMD_PINCH": "HMD + pinch",
"NONE": "None",
"PIPEWIRE_HELP": "Fast GPU capture,\nstandard on all desktops.",
"PW_FALLBACK_HELP": "Slow method with high CPU usage.\nTry in case PipeWire GPU doesn't work",
@@ -85,6 +86,7 @@
"SCREENCOPY_HELP": "Slow, no screen share popups.\nWorks on: Hyprland, Niri, River, Sway"
},
"POINTER_LERP_FACTOR": "Pointer smoothing",
"REQUIRES_RESTART": "Requires restart",
"RESET_PLAYSPACE": "Reset playspace",
"RESET_PLAYSPACE_HELP": "Clear the stage space offset.",
"RESTART_SOFTWARE": "Restart software",
@@ -148,8 +150,8 @@
"RESOLUTION": "Resolution"
},
"PROCESS": {
"STOP": "Stop",
"FORCE_KILL": "Force-kill"
"FORCE_KILL": "Force-kill",
"STOP": "Stop"
},
"PROCESS_LIST": "Process list",
"REFRESH": "Refresh",

View File

@@ -10,7 +10,6 @@
"APP_SETTINGS": {
"HIDE_USERNAME": "Ocultar nombre de usuario",
"OPAQUE_BACKGROUND": "Fondo opaco",
"WLX": {},
"LOOK_AND_FEEL": "Apariencia y estilo",
"HIDE_GRAB_HELP": "Ocultar ayuda para agarrar",
"ANIMATION_SPEED": "Velocidad de animación de la IU",
@@ -89,7 +88,9 @@
"RESET_PLAYSPACE": "Restablecer espacio de juego",
"RESET_PLAYSPACE_HELP": "Borrar el desplazamiento del espacio de juego.",
"BLOCK_POSES_ON_KBD_INTERACTION": "Bloquear poses al interactuar con el teclado",
"BLOCK_POSES_ON_KBD_INTERACTION_HELP": "Bloquea que el juego reciba poses cuando el teclado está sobre él y 'Bloquear entrada del juego' está habilitado"
"BLOCK_POSES_ON_KBD_INTERACTION_HELP": "Bloquea que el juego reciba poses cuando el teclado está sobre él y 'Bloquear entrada del juego' está habilitado",
"LANGUAGE": "Idioma",
"REQUIRES_RESTART": "Requiere reinicio"
},
"HELLO": "¡Hola!",
"AUDIO": {

View File

@@ -109,7 +109,9 @@
"RESET_PLAYSPACE": "Ripristina playspace",
"RESET_PLAYSPACE_HELP": "Cancella l'offset dello spazio di gioco.",
"BLOCK_POSES_ON_KBD_INTERACTION": "Blocca le pose durante l'interazione con la tastiera",
"BLOCK_POSES_ON_KBD_INTERACTION_HELP": "Impedisce al gioco di ricevere pose quando la tastiera è evidenziata e 'Blocca input di gioco' è abilitato"
"BLOCK_POSES_ON_KBD_INTERACTION_HELP": "Impedisce al gioco di ricevere pose quando la tastiera è evidenziata e 'Blocca input di gioco' è abilitato",
"LANGUAGE": "Lingua",
"REQUIRES_RESTART": "Richiede riavvio"
},
"APPLICATION_LAUNCHER": "Lanciatore applicazioni",
"APPLICATION_STARTED": "Applicazione avviata",

View File

@@ -10,7 +10,6 @@
"APP_SETTINGS": {
"HIDE_USERNAME": "ユーザー名を非表示",
"OPAQUE_BACKGROUND": "不透明な背景",
"WLX": {},
"LOOK_AND_FEEL": "外観",
"HIDE_GRAB_HELP": "グリップ動作中にのヘルプを非表示",
"ANIMATION_SPEED": "UIアニメーション速度",
@@ -89,7 +88,9 @@
"RESET_PLAYSPACE": "プレイエリアをリセット",
"RESET_PLAYSPACE_HELP": "プレイエリアのオフセットをクリアします。",
"BLOCK_POSES_ON_KBD_INTERACTION": "キーボード操作時のポーズをブロック",
"BLOCK_POSES_ON_KBD_INTERACTION_HELP": "キーボードがホバーされ、「ゲーム入力をブロック」が有効になっている場合、ゲームがポーズを受信することをブロックします"
"BLOCK_POSES_ON_KBD_INTERACTION_HELP": "キーボードがホバーされ、「ゲーム入力をブロック」が有効になっている場合、ゲームがポーズを受信することをブロックします",
"LANGUAGE": "言語",
"REQUIRES_RESTART": "再起動が必要です"
},
"HELLO": "こんにちは!",
"AUDIO": {

View File

@@ -5,7 +5,6 @@
"APP_SETTINGS": {
"HIDE_USERNAME": "Ukryj nazwę użytkownika",
"OPAQUE_BACKGROUND": "Nieprzezroczyste tło",
"WLX": {},
"LOOK_AND_FEEL": "Wygląd i działanie",
"HIDE_GRAB_HELP": "Ukryj pomoc dotyczącą chwytania",
"ANIMATION_SPEED": "Prędkość animacji UI",
@@ -34,7 +33,6 @@
"XR_CLICK_SENSITIVITY": "Czułość kliknięć XR",
"XR_CLICK_SENSITIVITY_RELEASE": "Czułość zwalniania XR",
"CLICK_FREEZE_TIME_MS": "Czas zamrożenia po kliknięciu (ms)",
"MISC": "Różne",
"XWAYLAND_BY_DEFAULT": "Uruchamiaj aplikacje domyślnie w trybie kompatybilności",
"UPRIGHT_SCREEN_FIX": "Naprawa pozycji ekranu",
"DOUBLE_CURSOR_FIX": "Naprawa podwójnego kursora",
@@ -55,7 +53,7 @@
"CLEAR_SAVED_STATE": "Wyczyść zapisany stan",
"CLEAR_PIPEWIRE_TOKENS": "Wyczyść tokeny PipeWire",
"DELETE_ALL_CONFIGS": "Wyczyść konfigurację",
"RESTART_SOFTWARE": "Uruchom ponownie oprogramowanie",
"RESTART_SOFTWARE": "Restartuj WayVR",
"CLEAR_SAVED_STATE_HELP": "Zresetuj zestawy i pozycje nakładek",
"CLEAR_PIPEWIRE_TOKENS_HELP": "Zapytaj o wybór ekranu przy następnym uruchomieniu",
"DELETE_ALL_CONFIGS_HELP": "Usuń wszystkie pliki konfiguracyjne z katalogu conf.d",
@@ -72,19 +70,22 @@
"SCREENCOPY_GPU_HELP": "Szybkie działanie, brak wyskakujących okien z informacją o udostępnianiu ekranu.\nDziała na: Hyprland, Niri, River, Sway",
"SCREENCOPY_HELP": "Wolne, bez wyskakujących okienek udostępniania ekranu.\nDziała na: Hyprland, Niri, River, Sway",
"NONE": "Brak",
"HMD_PINCH": "HMD + szczyknięcie",
"HMD_PINCH": "HMD + ściśnięcie placami",
"EYE_PINCH": "Ściśnięcie palcami + oko",
"EYE_ONLY": "Tylko oko",
"HMD_ONLY": "Tylko HMD"
},
"AUTOSTART_APPS": "Aplikacje do uruchomienia przy starcie",
"AUTOSTART_APPS": "Aplikacje auto-start",
"HANDSFREE_POINTER": "Tryb bez użycia rąk",
"HANDSFREE_POINTER_HELP": "Wejście do użycia, gdy kontrolery ruchu\nsą niedostępne. Lewy szczyptak to chwyt,\nprawy to kliknięcie.",
"UI_GRADIENT_INTENSITY": "Intensywność gradientu UI",
"RESET_PLAYSPACE": "Zresetuj przestrzeń gry",
"RESET_PLAYSPACE_HELP": "Wyczyść przesunięcie przestrzeni gry.",
"BLOCK_POSES_ON_KBD_INTERACTION": "Blokuj pozy podczas interakcji z klawiaturą",
"BLOCK_POSES_ON_KBD_INTERACTION_HELP": "Blokuje odbieranie póz przez grę, gdy kursor myszy znajduje się nad klawiaturą i włączona jest opcja 'Blokuj dane wejściowe z gry'"
"BLOCK_POSES_ON_KBD_INTERACTION_HELP": "Blokuje odbieranie póz przez grę, gdy kursor myszy znajduje się nad klawiaturą i włączona jest opcja 'Blokuj dane wejściowe z gry'",
"LANGUAGE": "Język",
"REQUIRES_RESTART": "Wymaga restartu",
"MISC": "Różne"
},
"APPLICATION_LAUNCHER": "Uruchamiacz aplikacji",
"APPLICATIONS": "Aplikacje",

View File

@@ -109,7 +109,9 @@
"RESET_PLAYSPACE": "重置游戏空间",
"RESET_PLAYSPACE_HELP": "清除舞台空间偏移。",
"BLOCK_POSES_ON_KBD_INTERACTION": "与键盘交互时阻止姿势",
"BLOCK_POSES_ON_KBD_INTERACTION_HELP": "当键盘悬停且启用“阻止游戏输入”时,阻止游戏接收姿势"
"BLOCK_POSES_ON_KBD_INTERACTION_HELP": "当键盘悬停且启用“阻止游戏输入”时,阻止游戏接收姿势",
"LANGUAGE": "语言",
"REQUIRES_RESTART": "需要重启"
},
"APPLICATION_LAUNCHER": "应用启动器",
"APPLICATION_STARTED": "应用已启动",

View File

@@ -18,6 +18,7 @@ use wgui::{
use wlx_common::{
audio,
dash_interface::{BoxDashInterface, RecenterMode},
locale::WayVRLangProvider,
timestep::{self, Timestep},
};
@@ -81,8 +82,9 @@ pub struct FrontendUpdateResult {
pub sounds_to_play: Vec<SoundType>,
}
pub struct InitParams<T> {
pub struct InitParams<'a, T> {
pub interface: BoxDashInterface<T>,
pub lang_provider: &'a WayVRLangProvider,
pub has_monado: bool,
}
@@ -118,6 +120,7 @@ impl<T: 'static> Frontend<T> {
let globals = WguiGlobals::new(
assets,
params.lang_provider,
wgui::globals::Defaults::default(),
&WguiFontConfig {
binaries: vec![&font_binary_regular, &font_binary_bold, &font_binary_light],

View File

@@ -10,14 +10,20 @@ use wgui::{
slider::ComponentSlider,
tabs::ComponentTabs,
},
drawing,
event::{CallbackDataCommon, EventAlterables},
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, WidgetID},
log::LogErr,
parser::{Fetchable, ParseDocumentParams, ParserState},
renderer_vk::text::{FontWeight, TextStyle},
taffy::{self, prelude::length},
task::Tasks,
widget::label::WidgetLabel,
widget::{
div::WidgetDiv,
label::{WidgetLabel, WidgetLabelParams},
},
windowing::context_menu::{self, Blueprint, ContextMenu, TickResult},
};
use wlx_common::{config::GeneralConfig, config_io::ConfigRoot, dash_interface::RecenterMode};
@@ -202,46 +208,48 @@ impl<T> Tab<T> for TabSettings<T> {
}
}
// Sorted alphabetically
#[allow(clippy::enum_variant_names)]
#[derive(Clone, Copy, AsRefStr, EnumString)]
enum SettingType {
UiAnimationSpeed,
UiGradientIntensity,
UiRoundMultiplier,
InvertScrollDirectionX,
InvertScrollDirectionY,
ScrollSpeed,
LongPressDuration,
NotificationsEnabled,
NotificationsSoundEnabled,
KeyboardSoundEnabled,
UprightScreenFix,
DoubleCursorFix,
SetsOnWatch,
HideGrabHelp,
XrClickSensitivity,
XrClickSensitivityRelease,
AllowSliding,
ClickFreezeTimeMs,
FocusFollowsMouseMode,
LeftHandedMouse,
BlockGameInput,
BlockGameInputIgnoreWatch,
BlockPosesOnKbdInteraction,
SpaceDragMultiplier,
UseSkybox,
UsePassthrough,
ScreenRenderDown,
CaptureMethod,
ClickFreezeTimeMs,
Clock12h,
DoubleCursorFix,
FocusFollowsMouseMode,
HandsfreePointer,
HideGrabHelp,
HideUsername,
InvertScrollDirectionX,
InvertScrollDirectionY,
KeyboardMiddleClick,
KeyboardSoundEnabled,
Language,
LeftHandedMouse,
LongPressDuration,
NotificationsEnabled,
NotificationsSoundEnabled,
OpaqueBackground,
PointerLerpFactor,
ScreenRenderDown,
ScrollSpeed,
SetsOnWatch,
SpaceDragMultiplier,
SpaceDragUnlocked,
SpaceRotateUnlocked,
Clock12h,
HideUsername,
OpaqueBackground,
UiAnimationSpeed,
UiGradientIntensity,
UiRoundMultiplier,
UprightScreenFix,
UsePassthrough,
UseSkybox,
XrClickSensitivity,
XrClickSensitivityRelease,
XwaylandByDefault,
CaptureMethod,
KeyboardMiddleClick,
HandsfreePointer,
}
impl SettingType {
@@ -309,6 +317,9 @@ impl SettingType {
Self::HandsfreePointer => {
config.handsfree_pointer = wlx_common::config::HandsfreePointer::from_str(value).expect("Invalid enum value!")
}
Self::Language => {
config.language = Some(wlx_common::locale::Language::from_str(value).expect("Invalid enum value!"))
}
_ => panic!("Requested enum for non-enum SettingType"),
}
}
@@ -318,6 +329,10 @@ impl SettingType {
Self::CaptureMethod => Self::get_enum_title_inner(config.capture_method),
Self::KeyboardMiddleClick => Self::get_enum_title_inner(config.keyboard_middle_click_mode),
Self::HandsfreePointer => Self::get_enum_title_inner(config.handsfree_pointer),
Self::Language => match &config.language {
Some(lang) => Self::get_enum_title_inner(*lang),
None => Translation::from_translation_key("APP_SETTINGS.OPTION.AUTO"),
},
_ => panic!("Requested enum for non-enum SettingType"),
}
}
@@ -341,70 +356,71 @@ impl SettingType {
}
/// Ok is translation, Err is raw text
/// `match` sorted alphabetically
fn get_translation(self) -> Result<&'static str, &'static str> {
match self {
Self::UiAnimationSpeed => Ok("APP_SETTINGS.ANIMATION_SPEED"),
Self::UiGradientIntensity => Ok("APP_SETTINGS.UI_GRADIENT_INTENSITY"),
Self::UiRoundMultiplier => Ok("APP_SETTINGS.ROUND_MULTIPLIER"),
Self::InvertScrollDirectionX => Ok("APP_SETTINGS.INVERT_SCROLL_DIRECTION_X"),
Self::InvertScrollDirectionY => Ok("APP_SETTINGS.INVERT_SCROLL_DIRECTION_Y"),
Self::ScrollSpeed => Ok("APP_SETTINGS.SCROLL_SPEED"),
Self::LongPressDuration => Ok("APP_SETTINGS.LONG_PRESS_DURATION"),
Self::NotificationsEnabled => Ok("APP_SETTINGS.NOTIFICATIONS_ENABLED"),
Self::NotificationsSoundEnabled => Ok("APP_SETTINGS.NOTIFICATIONS_SOUND_ENABLED"),
Self::KeyboardSoundEnabled => Ok("APP_SETTINGS.KEYBOARD_SOUND_ENABLED"),
Self::UprightScreenFix => Ok("APP_SETTINGS.UPRIGHT_SCREEN_FIX"),
Self::DoubleCursorFix => Ok("APP_SETTINGS.DOUBLE_CURSOR_FIX"),
Self::SetsOnWatch => Ok("APP_SETTINGS.SETS_ON_WATCH"),
Self::HideGrabHelp => Ok("APP_SETTINGS.HIDE_GRAB_HELP"),
Self::XrClickSensitivity => Ok("APP_SETTINGS.XR_CLICK_SENSITIVITY"),
Self::XrClickSensitivityRelease => Ok("APP_SETTINGS.XR_CLICK_SENSITIVITY_RELEASE"),
Self::AllowSliding => Ok("APP_SETTINGS.ALLOW_SLIDING"),
Self::ClickFreezeTimeMs => Ok("APP_SETTINGS.CLICK_FREEZE_TIME_MS"),
Self::FocusFollowsMouseMode => Ok("APP_SETTINGS.FOCUS_FOLLOWS_MOUSE_MODE"),
Self::LeftHandedMouse => Ok("APP_SETTINGS.LEFT_HANDED_MOUSE"),
Self::BlockGameInput => Ok("APP_SETTINGS.BLOCK_GAME_INPUT"),
Self::BlockGameInputIgnoreWatch => Ok("APP_SETTINGS.BLOCK_GAME_INPUT_IGNORE_WATCH"),
Self::BlockPosesOnKbdInteraction => Ok("APP_SETTINGS.BLOCK_POSES_ON_KBD_INTERACTION"),
Self::SpaceDragMultiplier => Ok("APP_SETTINGS.SPACE_DRAG_MULTIPLIER"),
Self::UseSkybox => Ok("APP_SETTINGS.USE_SKYBOX"),
Self::UsePassthrough => Ok("APP_SETTINGS.USE_PASSTHROUGH"),
Self::ScreenRenderDown => Ok("APP_SETTINGS.SCREEN_RENDER_DOWN"),
Self::CaptureMethod => Ok("APP_SETTINGS.CAPTURE_METHOD"),
Self::ClickFreezeTimeMs => Ok("APP_SETTINGS.CLICK_FREEZE_TIME_MS"),
Self::Clock12h => Ok("APP_SETTINGS.CLOCK_12H"),
Self::DoubleCursorFix => Ok("APP_SETTINGS.DOUBLE_CURSOR_FIX"),
Self::FocusFollowsMouseMode => Ok("APP_SETTINGS.FOCUS_FOLLOWS_MOUSE_MODE"),
Self::HandsfreePointer => Ok("APP_SETTINGS.HANDSFREE_POINTER"),
Self::HideGrabHelp => Ok("APP_SETTINGS.HIDE_GRAB_HELP"),
Self::HideUsername => Ok("APP_SETTINGS.HIDE_USERNAME"),
Self::InvertScrollDirectionX => Ok("APP_SETTINGS.INVERT_SCROLL_DIRECTION_X"),
Self::InvertScrollDirectionY => Ok("APP_SETTINGS.INVERT_SCROLL_DIRECTION_Y"),
Self::KeyboardMiddleClick => Ok("APP_SETTINGS.KEYBOARD_MIDDLE_CLICK"),
Self::KeyboardSoundEnabled => Ok("APP_SETTINGS.KEYBOARD_SOUND_ENABLED"),
Self::Language => Ok("APP_SETTINGS.LANGUAGE"),
Self::LeftHandedMouse => Ok("APP_SETTINGS.LEFT_HANDED_MOUSE"),
Self::LongPressDuration => Ok("APP_SETTINGS.LONG_PRESS_DURATION"),
Self::NotificationsEnabled => Ok("APP_SETTINGS.NOTIFICATIONS_ENABLED"),
Self::NotificationsSoundEnabled => Ok("APP_SETTINGS.NOTIFICATIONS_SOUND_ENABLED"),
Self::OpaqueBackground => Ok("APP_SETTINGS.OPAQUE_BACKGROUND"),
Self::PointerLerpFactor => Ok("APP_SETTINGS.POINTER_LERP_FACTOR"),
Self::ScreenRenderDown => Ok("APP_SETTINGS.SCREEN_RENDER_DOWN"),
Self::ScrollSpeed => Ok("APP_SETTINGS.SCROLL_SPEED"),
Self::SetsOnWatch => Ok("APP_SETTINGS.SETS_ON_WATCH"),
Self::SpaceDragMultiplier => Ok("APP_SETTINGS.SPACE_DRAG_MULTIPLIER"),
Self::SpaceDragUnlocked => Ok("APP_SETTINGS.SPACE_DRAG_UNLOCKED"),
Self::SpaceRotateUnlocked => Ok("APP_SETTINGS.SPACE_ROTATE_UNLOCKED"),
Self::Clock12h => Ok("APP_SETTINGS.CLOCK_12H"),
Self::HideUsername => Ok("APP_SETTINGS.HIDE_USERNAME"),
Self::OpaqueBackground => Ok("APP_SETTINGS.OPAQUE_BACKGROUND"),
Self::UiAnimationSpeed => Ok("APP_SETTINGS.ANIMATION_SPEED"),
Self::UiGradientIntensity => Ok("APP_SETTINGS.UI_GRADIENT_INTENSITY"),
Self::UiRoundMultiplier => Ok("APP_SETTINGS.ROUND_MULTIPLIER"),
Self::UprightScreenFix => Ok("APP_SETTINGS.UPRIGHT_SCREEN_FIX"),
Self::UsePassthrough => Ok("APP_SETTINGS.USE_PASSTHROUGH"),
Self::UseSkybox => Ok("APP_SETTINGS.USE_SKYBOX"),
Self::XrClickSensitivity => Ok("APP_SETTINGS.XR_CLICK_SENSITIVITY"),
Self::XrClickSensitivityRelease => Ok("APP_SETTINGS.XR_CLICK_SENSITIVITY_RELEASE"),
Self::XwaylandByDefault => Ok("APP_SETTINGS.XWAYLAND_BY_DEFAULT"),
Self::CaptureMethod => Ok("APP_SETTINGS.CAPTURE_METHOD"),
Self::KeyboardMiddleClick => Ok("APP_SETTINGS.KEYBOARD_MIDDLE_CLICK"),
Self::HandsfreePointer => Ok("APP_SETTINGS.HANDSFREE_POINTER"),
}
}
/// `match` sorted alphabetically
fn get_tooltip(self) -> Option<&'static str> {
match self {
Self::UprightScreenFix => Some("APP_SETTINGS.UPRIGHT_SCREEN_FIX_HELP"),
Self::DoubleCursorFix => Some("APP_SETTINGS.DOUBLE_CURSOR_FIX_HELP"),
Self::XrClickSensitivity => Some("APP_SETTINGS.XR_CLICK_SENSITIVITY_HELP"),
Self::XrClickSensitivityRelease => Some("APP_SETTINGS.XR_CLICK_SENSITIVITY_RELEASE_HELP"),
Self::FocusFollowsMouseMode => Some("APP_SETTINGS.FOCUS_FOLLOWS_MOUSE_MODE_HELP"),
Self::LeftHandedMouse => Some("APP_SETTINGS.LEFT_HANDED_MOUSE_HELP"),
Self::BlockGameInput => Some("APP_SETTINGS.BLOCK_GAME_INPUT_HELP"),
Self::BlockGameInputIgnoreWatch => Some("APP_SETTINGS.BLOCK_GAME_INPUT_IGNORE_WATCH_HELP"),
Self::BlockPosesOnKbdInteraction => Some("APP_SETTINGS.BLOCK_POSES_ON_KBD_INTERACTION_HELP"),
Self::UseSkybox => Some("APP_SETTINGS.USE_SKYBOX_HELP"),
Self::UsePassthrough => Some("APP_SETTINGS.USE_PASSTHROUGH_HELP"),
Self::ScreenRenderDown => Some("APP_SETTINGS.SCREEN_RENDER_DOWN_HELP"),
Self::CaptureMethod => Some("APP_SETTINGS.CAPTURE_METHOD_HELP"),
Self::KeyboardMiddleClick => Some("APP_SETTINGS.KEYBOARD_MIDDLE_CLICK_HELP"),
Self::DoubleCursorFix => Some("APP_SETTINGS.DOUBLE_CURSOR_FIX_HELP"),
Self::HandsfreePointer => Some("APP_SETTINGS.HANDSFREE_POINTER_HELP"),
Self::KeyboardMiddleClick => Some("APP_SETTINGS.KEYBOARD_MIDDLE_CLICK_HELP"),
Self::LeftHandedMouse => Some("APP_SETTINGS.LEFT_HANDED_MOUSE_HELP"),
Self::ScreenRenderDown => Some("APP_SETTINGS.SCREEN_RENDER_DOWN_HELP"),
Self::UprightScreenFix => Some("APP_SETTINGS.UPRIGHT_SCREEN_FIX_HELP"),
Self::UsePassthrough => Some("APP_SETTINGS.USE_PASSTHROUGH_HELP"),
Self::UseSkybox => Some("APP_SETTINGS.USE_SKYBOX_HELP"),
Self::XrClickSensitivity => Some("APP_SETTINGS.XR_CLICK_SENSITIVITY_HELP"),
Self::XrClickSensitivityRelease => Some("APP_SETTINGS.XR_CLICK_SENSITIVITY_RELEASE_HELP"),
_ => None,
}
}
//TODO: incorporate this
fn requires_restart(self) -> bool {
match self {
Self::UiAnimationSpeed
@@ -412,6 +428,7 @@ impl SettingType {
| Self::UprightScreenFix
| Self::DoubleCursorFix
| Self::ScreenRenderDown
| Self::Language
| Self::CaptureMethod => true,
_ => false,
}
@@ -426,6 +443,42 @@ impl SettingType {
}
}
// creates a simple div with horizontal, centered flow
fn horiz_cell(layout: &mut Layout, parent: WidgetID) -> anyhow::Result<WidgetID> {
let (pair, _) = layout.add_child(
parent,
WidgetDiv::create(),
taffy::Style {
flex_direction: taffy::FlexDirection::Row,
align_items: Some(taffy::AlignItems::Center),
gap: length(8.0),
..Default::default()
},
)?;
Ok(pair.id)
}
fn mount_requires_restart(layout: &mut Layout, parent: WidgetID) -> anyhow::Result<()> {
let content = Translation::from_translation_key("APP_SETTINGS.REQUIRES_RESTART");
let label = WidgetLabel::create(
&mut layout.state.globals.get(),
WidgetLabelParams {
content,
style: TextStyle {
wrap: false,
color: Some(drawing::Color::new(1.0, 0.5, 0.5, 1.0)),
weight: Some(FontWeight::Bold),
size: Some(10.0),
..Default::default()
},
},
);
layout.add_child(parent, label, Default::default())?;
Ok(())
}
macro_rules! category {
($pe:expr, $root:expr, $translation:expr, $icon:expr) => {{
let id = $pe.idx.to_string();
@@ -464,9 +517,15 @@ macro_rules! checkbox {
let checked = if *$setting.mut_bool($mp.config) { "1" } else { "0" };
params.insert(Rc::from("checked"), Rc::from(checked));
let id_cell = horiz_cell($mp.layout, $root)?;
$mp
.parser_state
.instantiate_template($mp.doc_params, "CheckBoxSetting", $mp.layout, $root, params)?;
.instantiate_template($mp.doc_params, "CheckBoxSetting", $mp.layout, id_cell, params)?;
if $setting.requires_restart() {
mount_requires_restart($mp.layout, id_cell)?;
}
let checkbox = $mp.parser_state.fetch_component_as::<ComponentCheckbox>(&id)?;
checkbox.on_toggle(Box::new({
@@ -502,9 +561,15 @@ macro_rules! slider_f32 {
params.insert(Rc::from("max"), Rc::from($max.to_string()));
params.insert(Rc::from("step"), Rc::from($step.to_string()));
let id_cell = horiz_cell($mp.layout, $root)?;
$mp
.parser_state
.instantiate_template($mp.doc_params, "SliderSetting", $mp.layout, $root, params)?;
.instantiate_template($mp.doc_params, "SliderSetting", $mp.layout, id_cell, params)?;
if $setting.requires_restart() {
mount_requires_restart($mp.layout, id_cell)?;
}
let slider = $mp.parser_state.fetch_component_as::<ComponentSlider>(&id)?;
slider.on_value_changed(Box::new({
@@ -534,6 +599,8 @@ macro_rules! slider_i32 {
params.insert(Rc::from("tooltip"), Rc::from(tooltip));
}
let id_cell = horiz_cell($mp.layout, $root)?;
let value = $setting.mut_i32($mp.config).to_string();
params.insert(Rc::from("value"), Rc::from(value));
params.insert(Rc::from("min"), Rc::from($min.to_string()));
@@ -542,7 +609,11 @@ macro_rules! slider_i32 {
$mp
.parser_state
.instantiate_template($mp.doc_params, "SliderSetting", $mp.layout, $root, params)?;
.instantiate_template($mp.doc_params, "SliderSetting", $mp.layout, id_cell, params)?;
if $setting.requires_restart() {
mount_requires_restart($mp.layout, id_cell)?;
}
let slider = $mp.parser_state.fetch_component_as::<ComponentSlider>(&id)?;
slider.on_value_changed(Box::new({
@@ -556,7 +627,7 @@ macro_rules! slider_i32 {
}
macro_rules! dropdown {
($mp:expr, $root:expr, $setting:expr, $options:expr) => {
($mp:expr /* `MacroParams` struct */, $root:expr, $setting:expr, $options:expr) => {
let id = $mp.idx.to_string();
$mp.idx += 1;
@@ -572,9 +643,15 @@ macro_rules! dropdown {
params.insert(Rc::from("tooltip"), Rc::from(tooltip));
}
let id_cell = horiz_cell($mp.layout, $root)?;
$mp
.parser_state
.instantiate_template($mp.doc_params, "DropdownButton", $mp.layout, $root, params)?;
.instantiate_template($mp.doc_params, "DropdownButton", $mp.layout, id_cell, params)?;
if $setting.requires_restart() {
mount_requires_restart($mp.layout, id_cell)?;
}
let setting_str = $setting.as_ref();
let title = $setting.get_enum_title($mp.config);
@@ -709,6 +786,7 @@ impl<T> TabSettings<T> {
match name {
TabNameEnum::LookAndFeel => {
let c = category!(mp, root, "APP_SETTINGS.LOOK_AND_FEEL", "dashboard/palette.svg")?;
dropdown!(mp, c, SettingType::Language, wlx_common::locale::Language::VARIANTS);
checkbox!(mp, c, SettingType::OpaqueBackground);
checkbox!(mp, c, SettingType::HideUsername);
checkbox!(mp, c, SettingType::HideGrabHelp);

View File

@@ -12,6 +12,7 @@ use wgui::{
layout::{Layout, LayoutParams, LayoutUpdateParams},
parser::{ParseDocumentParams, ParserState},
};
use wlx_common::locale::WayVRLangProvider;
pub struct TestbedAny {
pub layout: Layout,
@@ -28,8 +29,11 @@ impl TestbedAny {
AssetPath::BuiltIn(&format!("gui/{name}.xml"))
};
let lang_provider = WayVRLangProvider::default();
let globals = WguiGlobals::new(
assets,
&lang_provider,
wgui::globals::Defaults::default(),
&WguiFontConfig::default(),
PathBuf::new(), // cwd

View File

@@ -1,7 +1,7 @@
use crate::testbed::{Testbed, TestbedUpdateParams};
use dash_frontend::frontend::{self, FrontendUpdateParams};
use wgui::layout::Layout;
use wlx_common::dash_interface_emulated::DashInterfaceEmulated;
use wlx_common::{dash_interface_emulated::DashInterfaceEmulated, locale::WayVRLangProvider};
pub struct TestbedDashboard {
frontend: frontend::Frontend<()>,
@@ -10,11 +10,13 @@ pub struct TestbedDashboard {
impl TestbedDashboard {
pub fn new() -> anyhow::Result<Self> {
let interface = DashInterfaceEmulated::new();
let lang_provider = WayVRLangProvider::default();
let frontend = frontend::Frontend::new(
frontend::InitParams {
interface: Box::new(interface),
has_monado: true,
lang_provider: &lang_provider,
},
&mut (),
)?;

View File

@@ -27,6 +27,7 @@ use wgui::{
window::{WguiWindow, WguiWindowParams, WguiWindowParamsExtra},
},
};
use wlx_common::locale::WayVRLangProvider;
#[derive(Clone)]
pub enum TestbedTask {
@@ -85,8 +86,11 @@ impl TestbedGeneric {
}
pub fn new(assets: Box<assets::Asset>) -> anyhow::Result<Self> {
let lang_provider = WayVRLangProvider::default();
let globals = WguiGlobals::new(
assets,
&lang_provider,
wgui::globals::Defaults::default(),
&WguiFontConfig::default(),
PathBuf::new(), // cwd

View File

@@ -10,6 +10,7 @@ use wlx_common::{
SerializedWindowStates,
},
config_io,
locale::Language,
overlays::BackendAttribValue,
};
@@ -143,6 +144,7 @@ pub struct AutoSettings {
pub keyboard_middle_click_mode: AltModifier,
pub autostart_apps: Vec<WvrProcessLaunchParams>,
pub handsfree_pointer: HandsfreePointer,
pub language: Option<Language>,
}
fn get_settings_path() -> PathBuf {
@@ -192,6 +194,7 @@ pub fn save_settings(config: &GeneralConfig) -> anyhow::Result<()> {
keyboard_middle_click_mode: config.keyboard_middle_click_mode,
autostart_apps: config.autostart_apps.clone(),
handsfree_pointer: config.handsfree_pointer,
language: config.language,
};
let json = serde_json::to_string_pretty(&conf).unwrap(); // want panic

View File

@@ -17,6 +17,7 @@ use wgui::{
};
use wlx_common::{
dash_interface::{self, DashInterface, RecenterMode},
locale::WayVRLangProvider,
overlays::{BackendAttrib, BackendAttribValue},
};
use wlx_common::{
@@ -79,6 +80,7 @@ impl DashFrontend {
let frontend = frontend::Frontend::new(
frontend::InitParams {
interface: Box::new(interface),
lang_provider: &WayVRLangProvider::from_config(&app.session.config),
has_monado: matches!(app.xr_backend, XrBackend::OpenXR),
},
app,

View File

@@ -7,6 +7,7 @@ use wgui::{
drawing, font_config::WguiFontConfig, gfx::WGfx, globals::WguiGlobals, parser::parse_color_hex,
renderer_vk::context::SharedContext as WSharedContext,
};
use wlx_common::locale::WayVRLangProvider;
use wlx_common::{
audio,
config::GeneralConfig,
@@ -138,6 +139,8 @@ impl AppState {
let mut desktop_finder = DesktopFinder::new();
desktop_finder.refresh();
let lang_provider = WayVRLangProvider::from_config(&session.config);
Ok(Self {
session,
tasks,
@@ -153,6 +156,7 @@ impl AppState {
anchor_grabbed: false,
wgui_globals: WguiGlobals::new(
assets,
&lang_provider,
defaults,
&WguiFontConfig::default(),
get_config_file_path(&theme),

View File

@@ -3,6 +3,8 @@ use std::ffi::OsStr;
use std::io::Read;
use std::path::{Component, Path, PathBuf};
use crate::i18n::LangsList;
#[derive(Debug, Clone, Copy)]
pub enum AssetPath<'a> {
WguiInternal(&'a str), // tied to internal wgui AssetProvider. Used internally
@@ -83,6 +85,11 @@ impl Default for AssetPathOwned {
}
}
pub trait LangProvider {
fn langs_list(&self) -> &dyn LangsList;
fn forced_lang(&self) -> Option<&str>;
}
pub trait AssetProvider {
fn load_from_path(&mut self, path: &str) -> anyhow::Result<Vec<u8>>;
fn load_from_path_gzip(&mut self, path: &str) -> anyhow::Result<Vec<u8>> {

View File

@@ -10,7 +10,7 @@ use anyhow::Context;
use regex::Regex;
use crate::{
assets::{AssetPath, AssetProvider},
assets::{AssetPath, AssetProvider, LangProvider},
assets_internal, drawing,
font_config::{WguiFontConfig, WguiFontSystem},
i18n::I18n,
@@ -66,11 +66,12 @@ pub struct WguiGlobals(Rc<RefCell<Globals>>);
impl WguiGlobals {
pub fn new(
mut assets_builtin: Box<dyn AssetProvider>,
lang_provider: &dyn LangProvider,
defaults: Defaults,
font_config: &WguiFontConfig,
asset_folder: PathBuf,
) -> anyhow::Result<Self> {
let i18n_builtin = I18n::new(&mut assets_builtin)?;
let i18n_builtin = I18n::new(assets_builtin.as_mut(), lang_provider)?;
let assets_internal = Box::new(assets_internal::AssetInternal {});
Ok(Self(Rc::new(RefCell::new(Globals {

View File

@@ -2,7 +2,7 @@ use std::{fmt::Display, rc::Rc, str};
use anyhow::Context;
use crate::assets::AssetProvider;
use crate::assets::{AssetProvider, LangProvider};
// a string which optionally has translation key in it
// it will hopefully support dynamic language changing soon
@@ -63,50 +63,76 @@ pub struct Locale {
matched: String,
}
pub trait LangsList {
fn all_locale(&self) -> &'static [&'static str]; // "en", "de", "es" (...)
fn default_lang(&self) -> &'static str; // "en"
}
impl Locale {
pub fn all_locale() -> &'static [&'static str] {
&["de", "en", "es", "ja", "it", "pl", "zh_CN"]
}
pub fn default_lang() -> &'static str {
"en"
}
fn match_locale<'o>(lang: &str, region: Option<&str>, all_locales: &[&'o str]) -> &'o str {
fn match_locale<'o>(
default_lang: &'static str,
lang: &str,
region: Option<&str>,
all_locales: &[&'o str],
) -> &'o str {
if let Some(region) = region {
let locale_str = format!("{lang}_{region}");
if let Some(locale) = all_locales.iter().find(|&&l| l == locale_str) {
return locale;
}
log::warn!("Unsupported locale \"{locale_str}\", trying \"{lang}\".");
};
}
if let Some(locale) = all_locales.iter().find(|&&l| l == lang) {
return locale;
}
let prefix = format!("{lang}_");
if let Some(locale) = all_locales.iter().find(|&&l| l.starts_with(&prefix)) {
return locale;
}
let locale = Self::default_lang();
log::warn!("Unsupported language \"{lang}\", defaulting to \"{locale}\".");
locale
log::warn!("Unsupported language \"{lang}\", defaulting to \"{default_lang}\".");
default_lang
}
pub fn new(lang: String, region: Option<String>) -> Self {
let matched = Self::match_locale(&lang, region.as_deref(), Self::all_locale()).to_string();
pub fn new(langs_list: &dyn LangsList, lang: String, region: Option<String>) -> Self {
let matched = Self::match_locale(
langs_list.default_lang(),
&lang,
region.as_deref(),
langs_list.all_locale(),
)
.to_string();
Self { lang, region, matched }
}
pub fn parse_str(locale: &str) -> Self {
let base = locale.split(|c| c == '.' || c == '@').next().unwrap_or(locale);
let parts: Vec<&str> = base.split(|c| c == '_' || c == '-').collect();
pub fn parse_str(langs_list: &dyn LangsList, locale: &str) -> Self {
let base = locale.split(['.', '@']).next().unwrap_or(locale);
let parts: Vec<&str> = base.split(['_', '-']).collect();
// Ensures the format is lang_REGION
match parts.as_slice() {
[lang, region, ..] => Self::new(lang.to_lowercase(), Some(region.to_uppercase())),
[lang] if !lang.is_empty() => Self::new(lang.to_lowercase(), None),
_ => Self::new("en".to_string(), None),
[lang, region, ..] => Self::new(langs_list, lang.to_lowercase(), Some(region.to_uppercase())),
[lang] if !lang.is_empty() => Self::new(langs_list, lang.to_lowercase(), None),
_ => Self::new(langs_list, langs_list.default_lang().to_string(), None),
}
}
pub fn from_env() -> Self {
pub fn from_env(lang_provider: &dyn LangProvider) -> Self {
let default_lang = lang_provider.langs_list().default_lang();
// check if forced language is set
if let Some(forced_lang) = lang_provider.forced_lang() {
let matched =
Self::match_locale(default_lang, forced_lang, None, lang_provider.langs_list().all_locale()).to_string();
return Self {
lang: forced_lang.to_string(),
region: None,
matched,
};
}
// fallback to environment variables
use std::env;
let vars = ["LC_ALL", "LC_MESSAGES", "LANG"];
let full_locale = vars
@@ -114,13 +140,10 @@ impl Locale {
.find_map(|&v| env::var(v).ok())
.filter(|v| !v.is_empty() && v != "C" && v != "POSIX")
.unwrap_or_else(|| {
log::warn!(
"LC_ALL/LC_MESSAGES/LANG is not set, defaulting to \"{}\"",
Self::default_lang()
);
Self::default_lang().to_string()
log::warn!("LC_ALL/LC_MESSAGES/LANG is not set, defaulting to \"{default_lang}\"");
default_lang.to_string()
});
Self::parse_str(&full_locale)
Self::parse_str(lang_provider.langs_list(), &full_locale)
}
pub fn get_matched(&self) -> &str {
&self.matched
@@ -155,13 +178,13 @@ fn find_translation<'a>(translation: &str, mut val: &'a serde_json::Value) -> Op
}
impl I18n {
pub fn new(provider: &mut Box<dyn AssetProvider>) -> anyhow::Result<Self> {
let locale = Locale::from_env();
pub fn new(asset_provider: &mut dyn AssetProvider, lang_provider: &dyn LangProvider) -> anyhow::Result<Self> {
let locale = Locale::from_env(lang_provider);
log::info!("Guessed system language: {locale}");
let data_english = provider.load_from_path("lang/en.json")?;
let data_english = asset_provider.load_from_path("lang/en.json")?;
let path = format!("lang/{}.json", locale.get_matched());
let data_translated = provider
let data_translated = asset_provider
.load_from_path(&path)
.with_context(|| path.clone())
.context("Could not load translation file")?;
@@ -189,7 +212,7 @@ impl I18n {
})
}
pub fn get_locale(&self) -> &Locale {
pub const fn get_locale(&self) -> &Locale {
&self.locale
}
@@ -202,7 +225,7 @@ impl I18n {
}
if let Some(translated_fallback) = find_translation(translation_key, &self.json_root_fallback) {
log::warn!("missing translation for key \"{translation_key}\", using \"en\" instead");
log::warn!("missing translation for key \"{translation_key}\", using fallback instead");
return Rc::from(format_translated(translated_fallback, sections));
}

View File

@@ -7,9 +7,7 @@ use strum::{AsRefStr, EnumProperty, EnumString, VariantArray};
use wayvr_ipc::packet_client::WvrProcessLaunchParams;
use crate::{
astr_containers::{AStrMap, AStrSet},
overlays::{BackendAttribValue, ToastDisplayMethod, ToastTopic},
windowing::OverlayWindowState,
astr_containers::{AStrMap, AStrSet}, locale::{self}, overlays::{BackendAttribValue, ToastDisplayMethod, ToastTopic}, windowing::OverlayWindowState
};
pub type PwTokenMap = AStrMap<String>;
@@ -140,6 +138,8 @@ const fn def_max_height() -> u16 {
1440
}
#[derive(Deserialize, Serialize)]
pub struct GeneralConfig {
#[serde(default = "def_theme_path")]
@@ -151,6 +151,8 @@ pub struct GeneralConfig {
pub color_faded: Option<String>,
pub color_background: Option<String>,
pub language: Option<locale::Language>, // auto-detected at runtime if unset
#[serde(default = "def_one")]
#[serde(alias = "ui_animation_speed", alias = "animation_speed" /* old name */)]
pub ui_animation_speed: f32,

View File

@@ -8,6 +8,7 @@ pub mod dash_interface;
pub mod dash_interface_emulated;
pub mod desktop_finder;
mod handle;
pub mod locale;
pub mod overlays;
pub mod timestep;
pub mod windowing;

88
wlx-common/src/locale.rs Normal file
View File

@@ -0,0 +1,88 @@
use std::rc::Rc;
use serde::{Deserialize, Serialize};
use strum::{AsRefStr, EnumProperty, EnumString, VariantArray};
use crate::config::GeneralConfig;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, AsRefStr, EnumString, EnumProperty, VariantArray)]
pub enum Language {
#[strum(props(Text = "English"))]
English,
#[strum(props(Text = "Polski"))]
Polish,
#[strum(props(Text = "日本語"))]
Japanese,
#[strum(props(Text = "German"))]
German,
#[strum(props(Text = "Italiano"))]
Italian,
#[strum(props(Text = "简体中文"))]
ChineseSimplified,
#[strum(props(Text = "Español"))]
Spanish,
}
impl Language {
pub const fn code(&self) -> &'static str {
match self {
Language::English => "en",
Language::Polish => "pl",
Language::Japanese => "ja",
Language::German => "de",
Language::Italian => "it",
Language::ChineseSimplified => "zh_CN",
Language::Spanish => "es",
}
}
pub const fn get_default() -> Self {
Self::English
}
pub const fn all_codes() -> &'static [&'static str] {
&["en", "pl", "ja", "de", "it", "zh_CN", "es"]
}
}
pub struct WayVRLangsList {}
impl wgui::i18n::LangsList for WayVRLangsList {
fn all_locale(&self) -> &'static [&'static str] {
Language::all_codes()
}
fn default_lang(&self) -> &'static str {
Language::get_default().code()
}
}
// static
const G_LANGS_LIST: WayVRLangsList = WayVRLangsList {};
#[derive(Default)]
pub struct WayVRLangProvider {
forced_lang: Option<Rc<str>>,
}
impl wgui::assets::LangProvider for WayVRLangProvider {
fn langs_list(&self) -> &dyn wgui::i18n::LangsList {
&G_LANGS_LIST
}
fn forced_lang(&self) -> Option<&str> {
self.forced_lang.as_ref().map(|lang| lang.as_ref())
}
}
impl WayVRLangProvider {
pub fn from_config(config: &GeneralConfig) -> Self {
if let Some(lang) = &config.language {
return Self {
forced_lang: Some(lang.code().into()),
};
}
Self::default()
}
}