diff --git a/wayvr/src/backend/openvr/mod.rs b/wayvr/src/backend/openvr/mod.rs index 107e17a5..5c729869 100644 --- a/wayvr/src/backend/openvr/mod.rs +++ b/wayvr/src/backend/openvr/mod.rs @@ -12,6 +12,7 @@ use ovr_overlay::{ }; use smallvec::smallvec; use vulkano::{Handle, VulkanObject, device::physical::PhysicalDevice}; +use wgui::i18n::Translation; use wlx_common::{dash_interface::InterfaceFeats, overlays::ToastTopic}; use crate::{ @@ -195,8 +196,8 @@ pub fn openvr_run(args: &Args) -> Result<(), BackendError> { log::info!("IPD: {:.1} mm -> {:.1} mm", app.input_state.ipd, ipd); Toast::new( ToastTopic::IpdChange, - "IPD".into(), - format!("{ipd:.1} mm"), + Some(Translation::from_translation_key("IPD")), + Translation::from_raw_text_string(format!("{ipd:.1} mm")), ) .submit(&mut app); } diff --git a/wayvr/src/backend/openxr/mod.rs b/wayvr/src/backend/openxr/mod.rs index aa6b87ac..adbe5bdd 100644 --- a/wayvr/src/backend/openxr/mod.rs +++ b/wayvr/src/backend/openxr/mod.rs @@ -10,6 +10,7 @@ use input::OpenXrInputSource; use openxr as xr; use skybox::{Skybox, create_skybox}; use vulkano::{Handle, VulkanObject}; +use wgui::i18n::Translation; use wlx_common::{ dash_interface::InterfaceFeats, overlays::{StereoMode, ToastTopic}, @@ -322,8 +323,12 @@ pub fn openxr_run(args: &Args) -> Result<(), BackendError> { if (app.input_state.ipd - ipd).abs() > 0.05 { log::info!("IPD changed: {} -> {}", app.input_state.ipd, ipd); app.input_state.ipd = ipd; - Toast::new(ToastTopic::IpdChange, "IPD".into(), format!("{ipd:.1} mm")) - .submit(&mut app); + Toast::new( + ToastTopic::IpdChange, + Some(Translation::from_translation_key("IPD")), + Translation::from_raw_text_string(format!("{ipd:.1} mm")), + ) + .submit(&mut app); } overlays.values_mut().for_each(|o| o.config.tick(&mut app)); diff --git a/wayvr/src/gui/panel/button.rs b/wayvr/src/gui/panel/button.rs index dab192c7..4d4b5d4b 100644 --- a/wayvr/src/gui/panel/button.rs +++ b/wayvr/src/gui/panel/button.rs @@ -14,6 +14,7 @@ use wgui::{ CallbackData, CallbackMetadata, EventCallback, EventListenerKind, MouseButtonIndex, StyleSetRequest, }, + i18n::Translation, layout::Layout, log::LogErr, parser::{self, AttribPair, CustomAttribsInfoOwned, Fetchable, ParserState, TemplateParams}, @@ -613,13 +614,14 @@ pub(super) fn setup_custom_button( let now = Instant::now(); for i in 0..duration_secs { + let text = globals.i18n().translate_and_replace( + "TOAST.FIXING_FLOOR_IN_X_SECS", + ("{SECONDS}", &format!("{}", duration_secs - i)), + ); Toast::new( ToastTopic::System, - globals.i18n().translate_and_replace( - "TOAST.FIXING_FLOOR_IN_X_SECS", - ("{SECONDS}", &format!("{}", duration_secs - i)), - ), - "TOAST.ONE_CONTROLLER_ON_FLOOR".into(), + Some(Translation::from_raw_text_string(text)), + Translation::from_translation_key("TOAST.ONE_CONTROLLER_ON_FLOOR"), ) .with_timeout(1.0) .with_lerp_amount(1.0) @@ -635,9 +637,13 @@ pub(super) fn setup_custom_button( app.tasks .enqueue_at(TaskType::Playspace(PlayspaceTask::FixFloor), deadline); - Toast::new(ToastTopic::System, "DONE".into(), String::new()) - .with_timeout(2.0) - .submit_at(app, deadline); + Toast::new( + ToastTopic::System, + Some(Translation::from_translation_key("DONE")), + Translation::from_raw_text(""), + ) + .with_timeout(2.0) + .submit_at(app, deadline); Ok(EventResult::Consumed) }), "::Shutdown" => Box::new(move |_common, data, app, _| { diff --git a/wayvr/src/overlays/keyboard/mod.rs b/wayvr/src/overlays/keyboard/mod.rs index 4acc728d..ff4fc44f 100644 --- a/wayvr/src/overlays/keyboard/mod.rs +++ b/wayvr/src/overlays/keyboard/mod.rs @@ -36,6 +36,7 @@ use smallvec::SmallVec; use wgui::{ color::WguiColor, event::{InternalStateChangeEvent, MouseButtonEvent, MouseButtonIndex}, + i18n::Translation, layout::WidgetID, }; use wlx_common::windowing::{OverlayWindowState, Positioning}; @@ -520,8 +521,10 @@ fn handle_press( if layouts.is_empty() { Toast::new( ToastTopic::System, - "TOAST.NO_KEYMAPS_CONFIGURED".into(), - "TOAST.NO_KEYMAPS_CONFIGURED_HELP".into(), + Some(Translation::from_translation_key( + "TOAST.NO_KEYMAPS_CONFIGURED", + )), + Translation::from_translation_key("TOAST.NO_KEYMAPS_CONFIGURED_HELP"), ) .with_timeout(5.) .submit(app); diff --git a/wayvr/src/overlays/screen/pw.rs b/wayvr/src/overlays/screen/pw.rs index 6e5eaa58..3d612fdb 100644 --- a/wayvr/src/overlays/screen/pw.rs +++ b/wayvr/src/overlays/screen/pw.rs @@ -377,8 +377,10 @@ fn check(mut par: CheckParams, finalize_fn: ScreenCastFinalizeFn) { log::warn!("Failed to create mirror due to PipeWire error: {e:?}"); Toast::new( ToastTopic::Error, - "TOAST.TITLE_SCREENCAST_FAIL".into(), - format!("{e}"), + Some(Translation::from_translation_key( + "TOAST.TITLE_SCREENCAST_FAIL", + )), + Translation::from_raw_text_string(format!("{e}")), ) .submit(par.app); } diff --git a/wayvr/src/overlays/toast.rs b/wayvr/src/overlays/toast.rs index 37fb4c88..dc067248 100644 --- a/wayvr/src/overlays/toast.rs +++ b/wayvr/src/overlays/toast.rs @@ -6,7 +6,10 @@ use std::{ use anyhow::Context; use glam::{Affine3A, Quat, Vec3, vec3}; -use wgui::{i18n::Translation, widget::label::WidgetLabel}; +use wgui::{ + i18n::{I18n, Translation}, + widget::label::WidgetLabel, +}; use wlx_common::{ common::LeftRight, overlays::{ToastDisplayMethod, ToastTopic}, @@ -22,9 +25,8 @@ use crate::{ static TOAST_NAME: LazyLock> = LazyLock::new(|| "toast".into()); -pub struct Toast { - pub title: String, - pub body: String, +#[derive(Clone)] +pub struct ToastParams { pub opacity: f32, pub timeout: f32, pub lerp_amount: f32, @@ -32,44 +34,102 @@ pub struct Toast { pub topic: ToastTopic, } +pub struct Toast { + pub title: Option, + pub body: Translation, + pub params: ToastParams, +} + +pub struct BakedToast { + title_raw: String, // ready-to-display text + body_raw: String, // ready-to-display text + params: ToastParams, +} + #[allow(dead_code)] impl Toast { - pub const fn new(topic: ToastTopic, title: String, body: String) -> Self { + pub const fn new(topic: ToastTopic, title: Option, body: Translation) -> Self { Self { title, body, - opacity: 1.0, - lerp_amount: 0.1, - timeout: 3.0, - sound: false, - topic, + params: ToastParams { + opacity: 1.0, + lerp_amount: 0.1, + timeout: 3.0, + sound: false, + topic, + }, } } + pub const fn with_timeout(mut self, timeout: f32) -> Self { - self.timeout = timeout; + self.params.timeout = timeout; self } + pub const fn with_lerp_amount(mut self, lerp: f32) -> Self { - self.lerp_amount = lerp; + self.params.lerp_amount = lerp; self } + pub const fn with_opacity(mut self, opacity: f32) -> Self { - self.opacity = opacity; + self.params.opacity = opacity; self } + pub const fn with_sound(mut self, sound: bool) -> Self { - self.sound = sound; + self.params.sound = sound; self } + + pub fn submit(self, app: &mut AppState) { + self.submit_at(app, Instant::now()); + } + + pub fn submit_at(self, app: &mut AppState, instant: Instant) { + let globals = app.wgui_globals.clone(); + let baked_toast = self.build(&mut globals.i18n()); + baked_toast.submit_at(app, instant); + } + + // bake it + pub fn build(&self, lang: &mut I18n) -> BakedToast { + let title = if let Some(title) = &self.title { + title.clone() + } else { + Translation::from_translation_key("TOAST.DEFAULT_TITLE") + }; + + BakedToast { + title_raw: String::from(title.generate(lang).as_ref()), + body_raw: String::from(self.body.generate(lang).as_ref()), + params: self.params.clone(), + } + } + + pub fn build_raw(&self) -> BakedToast { + BakedToast { + title_raw: String::from(if let Some(title) = &self.title { + title.text.as_ref() + } else { + "" + }), + body_raw: String::from(self.body.text.as_ref()), + params: self.params.clone(), + } + } +} + +impl BakedToast { pub fn submit(self, app: &mut AppState) { self.submit_at(app, Instant::now()); } pub fn submit_at(self, app: &mut AppState, instant: Instant) { let selector = OverlaySelector::Name(TOAST_NAME.clone()); - let destroy_at = instant.add(std::time::Duration::from_secs_f32(self.timeout)); + let destroy_at = instant.add(std::time::Duration::from_secs_f32(self.params.timeout)); - if self.sound && app.session.config.notifications_sound_enabled { + if self.params.sound && app.session.config.notifications_sound_enabled { app.audio_sample_player .play_sample(&mut app.audio_system, "toast"); } @@ -106,11 +166,11 @@ impl Toast { } } -fn new_toast(toast: Toast, app: &mut AppState) -> Option { +fn new_toast(toast: BakedToast, app: &mut AppState) -> Option { let current_method = app .session .toast_topics - .get(toast.topic) + .get(toast.params.topic) .copied() .unwrap_or(ToastDisplayMethod::Hide); @@ -123,7 +183,7 @@ fn new_toast(toast: Toast, app: &mut AppState) -> Option { vec3(0., -0.2, -0.5), Quat::IDENTITY, Positioning::FollowHead { - lerp: toast.lerp_amount, + lerp: toast.params.lerp_amount, }, ), ToastDisplayMethod::Watch => { @@ -135,19 +195,8 @@ fn new_toast(toast: Toast, app: &mut AppState) -> Option { } }; - let title = if toast.title.is_empty() { - Translation::from_translation_key("TOAST.DEFAULT_TITLE") - } else if matches!(toast.topic, ToastTopic::System | ToastTopic::Error) { - Translation::from_translation_key(&toast.title) - } else { - Translation::from_raw_text(&toast.title) - }; - - let body = if matches!(toast.topic, ToastTopic::System) { - Translation::from_translation_key(&toast.body) - } else { - Translation::from_raw_text(&toast.body) - }; + let title = Translation::from_raw_text(&toast.title_raw); + let body = Translation::from_raw_text(&toast.body_raw); let on_custom_id: OnCustomIdFunc<()> = Box::new(move |id, widget, _doc_params, layout, _parser_state, ()| { diff --git a/wayvr/src/overlays/whisper.rs b/wayvr/src/overlays/whisper.rs index d2bf43b4..ded085bd 100644 --- a/wayvr/src/overlays/whisper.rs +++ b/wayvr/src/overlays/whisper.rs @@ -132,8 +132,10 @@ pub fn create_whisper(app: &mut AppState) -> anyhow::Result Err(e) => { Toast::new( ToastTopic::System, - "WHISPER.INIT_ERROR".into(), - e.to_string(), + Some(Translation::from_translation_key( + "WHISPER.INIT_ERROR", + )), + Translation::from_raw_text_string(e.to_string()), ) .with_timeout(5.) .with_sound(true) @@ -144,8 +146,10 @@ pub fn create_whisper(app: &mut AppState) -> anyhow::Result } else { Toast::new( ToastTopic::System, - "WHISPER.MODEL_NOT_DOWNLOADED".into(), - "WHISPER.DOWNLOAD_GUIDANCE".into(), + Some(Translation::from_translation_key( + "WHISPER.MODEL_NOT_DOWNLOADED", + )), + Translation::from_translation_key("WHISPER.DOWNLOAD_GUIDANCE"), ) .with_timeout(5.) .with_sound(true) diff --git a/wayvr/src/subsystem/hid/provider/uinput.rs b/wayvr/src/subsystem/hid/provider/uinput.rs index 0b4de4bb..3833eed3 100644 --- a/wayvr/src/subsystem/hid/provider/uinput.rs +++ b/wayvr/src/subsystem/hid/provider/uinput.rs @@ -13,6 +13,7 @@ use std::fs::File; use std::mem::transmute; use std::sync::atomic::AtomicBool; use strum::IntoEnumIterator; +use wgui::i18n::Translation; use wlx_common::overlays::ToastTopic; pub struct UInputProvider { @@ -247,8 +248,8 @@ Check if you're in input group, run: id -nG"; log::info!("{UINPUT_DISABLED}"); return Err(Toast::new( ToastTopic::System, - String::with_capacity(0), - String::from(UINPUT_DISABLED), + None, + Translation::from_raw_text(UINPUT_DISABLED), ) .with_timeout(5.0)); } @@ -272,8 +273,8 @@ Check if you're in input group, run: id -nG"; } Err(Toast::new( ToastTopic::Error, - String::with_capacity(0), - full_uinput_error, + None, + Translation::from_raw_text_string(full_uinput_error), ) .with_timeout(30.0)) } diff --git a/wayvr/src/subsystem/hid/provider/wl_virtual.rs b/wayvr/src/subsystem/hid/provider/wl_virtual.rs index 5ec664f7..ebb0a6b8 100644 --- a/wayvr/src/subsystem/hid/provider/wl_virtual.rs +++ b/wayvr/src/subsystem/hid/provider/wl_virtual.rs @@ -15,6 +15,7 @@ use wayland_client::protocol::wl_registry::WlRegistry; use wayland_client::protocol::wl_seat::{WlSeat}; use wayland_protocols_misc::zwp_virtual_keyboard_v1::client::zwp_virtual_keyboard_manager_v1::ZwpVirtualKeyboardManagerV1; use wayland_protocols_misc::zwp_virtual_keyboard_v1::client::zwp_virtual_keyboard_v1::ZwpVirtualKeyboardV1; +use wgui::i18n::Translation; use xkbcommon::xkb::{KEYMAP_FORMAT_TEXT_V1}; use wlx_common::overlays::ToastTopic; use crate::overlays::toast::Toast; @@ -275,8 +276,8 @@ pub fn initialize_wl_virtual() -> anyhow::Result, Toast> { let provider = WlVirtualProvider::try_new().map_err(|e| { Toast::new( ToastTopic::System, - String::with_capacity(0), - format!("Could not initialize wl_virtual: {e}"), + None, + Translation::from_raw_text_string(format!("Could not initialize wl_virtual: {e}")), ) })?; Ok(Box::new(provider)) diff --git a/wayvr/src/subsystem/notifications.rs b/wayvr/src/subsystem/notifications.rs index 4bdd61d1..a05a206c 100644 --- a/wayvr/src/subsystem/notifications.rs +++ b/wayvr/src/subsystem/notifications.rs @@ -9,13 +9,18 @@ use std::{ }, time::Duration, }; +use wgui::i18n::Translation; use wlx_common::overlays::ToastTopic; -use crate::{overlays::toast::Toast, state::AppSession, subsystem::dbus::DbusConnector}; +use crate::{ + overlays::toast::{BakedToast, Toast}, + state::AppSession, + subsystem::dbus::DbusConnector, +}; pub struct NotificationManager { - rx_toast: mpsc::Receiver, - tx_toast: mpsc::SyncSender, + rx_toast: mpsc::Receiver, + tx_toast: mpsc::SyncSender, running: Arc, } @@ -29,7 +34,7 @@ impl NotificationManager { } } - pub fn drain_pending(&self, session: &AppSession) -> Vec { + pub fn drain_pending(&self, session: &AppSession) -> Vec { if session.config.notifications_enabled { self.rx_toast.try_iter().collect() } else { @@ -125,13 +130,15 @@ impl NotificationManager { let toast = Toast::new( ToastTopic::XSNotification, - msg.title, - msg.content.unwrap_or(String::new()), + Some(Translation::from_raw_text_string(msg.title)), + Translation::from_raw_text_string(msg.content.unwrap_or(String::new())), ) .with_timeout(msg.timeout.unwrap_or(5.)) .with_sound(msg.volume.unwrap_or(-1.) >= 0.); // XSOverlay still plays at 0, - match sender.try_send(toast) { + match sender.try_send( + toast.build_raw(), /* we don't use translation keys here */ + ) { Ok(()) => {} Err(e) => { log::error!("Failed to send notification: {e:?}"); @@ -150,7 +157,7 @@ impl Drop for NotificationManager { } } -fn parse_dbus(msg: &dbus::Message) -> anyhow::Result { +fn parse_dbus(msg: &dbus::Message) -> anyhow::Result { let mut args = msg.iter_init(); let app_name: String = args.read()?; let _replaces_id: u32 = args.read()?; @@ -164,9 +171,15 @@ fn parse_dbus(msg: &dbus::Message) -> anyhow::Result { summary }; - Ok(Toast::new(ToastTopic::DesktopNotification, title, body) - .with_timeout(5.0) - .with_opacity(1.0)) + let toast = Toast::new( + ToastTopic::DesktopNotification, + Some(Translation::from_raw_text_string(title)), + Translation::from_raw_text_string(body), + ) + .with_timeout(5.0) + .with_opacity(1.0); + + Ok(toast.build_raw()) // leave the audio part to the desktop env } diff --git a/wayvr/src/windowing/manager.rs b/wayvr/src/windowing/manager.rs index 524a7b0e..0f3a6c3e 100644 --- a/wayvr/src/windowing/manager.rs +++ b/wayvr/src/windowing/manager.rs @@ -7,7 +7,7 @@ use std::{ use anyhow::Context; use glam::{Affine3A, Vec3, Vec3A}; use slotmap::{Key, SecondaryMap, SlotMap}; -use wgui::log::LogErr; +use wgui::{i18n::Translation, log::LogErr}; use wlx_common::{ astr_containers::{AStrMap, AStrMapExt}, config::SerializedWindowSet, @@ -287,8 +287,8 @@ where if new_idx >= MAX_OVERLAY_SETS { Toast::new( ToastTopic::System, - "TOAST.CANNOT_ADD_SET".into(), - "TOAST.MAXIMUM_SETS_REACHED".into(), + Some(Translation::from_translation_key("TOAST.CANNOT_ADD_SET")), + Translation::from_translation_key("TOAST.MAXIMUM_SETS_REACHED"), ) .with_timeout(5.) .with_sound(true) @@ -305,8 +305,8 @@ where let Some(set) = self.current_set else { Toast::new( ToastTopic::System, - "TOAST.CANNOT_REMOVE_SET".into(), - "TOAST.NO_SET_SELECTED".into(), + Some(Translation::from_translation_key("TOAST.CANNOT_REMOVE_SET")), + Translation::from_translation_key("TOAST.NO_SET_SELECTED"), ) .with_timeout(5.) .with_sound(true) @@ -317,8 +317,8 @@ where if self.sets.len() <= 1 { Toast::new( ToastTopic::System, - "TOAST.CANNOT_REMOVE_SET".into(), - "TOAST.LAST_EXISTING_SET".into(), + Some(Translation::from_translation_key("TOAST.CANNOT_REMOVE_SET")), + Translation::from_translation_key("TOAST.LAST_EXISTING_SET"), ) .with_timeout(5.) .with_sound(true) @@ -1077,8 +1077,8 @@ impl OverlayWindowManager { if !self.edit_mode && self.initialized && num_overlays < 1 { Toast::new( ToastTopic::System, - "TOAST.EMPTY_SET".into(), - "TOAST.LETS_ADD_OVERLAYS".into(), + Some(Translation::from_translation_key("TOAST.EMPTY_SET")), + Translation::from_translation_key("TOAST.LETS_ADD_OVERLAYS"), ) .with_timeout(3.) .submit(app); diff --git a/wgui/src/i18n.rs b/wgui/src/i18n.rs index 08fc6dbd..f3caeecc 100644 --- a/wgui/src/i18n.rs +++ b/wgui/src/i18n.rs @@ -49,9 +49,9 @@ impl Translation { } } - pub fn from_translation_key(translated: &str) -> Self { + pub fn from_translation_key(key: &str) -> Self { Self { - text: Rc::from(translated), + text: Rc::from(key), translated: true, } }