wayvr: baked toasts

This commit is contained in:
Aleksander 2026-08-09 12:21:33 +02:00 committed by galister
parent 669043035e
commit f13562c7dc
12 changed files with 165 additions and 80 deletions

View File

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

View File

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

View File

@ -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<S: 'static>(
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<S: 'static>(
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, _| {

View File

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

View File

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

View File

@ -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<Arc<str>> = 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<Translation>,
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<Translation>, 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<OverlayWindowConfig> {
fn new_toast(toast: BakedToast, app: &mut AppState) -> Option<OverlayWindowConfig> {
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<OverlayWindowConfig> {
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<OverlayWindowConfig> {
}
};
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, ()| {

View File

@ -132,8 +132,10 @@ pub fn create_whisper(app: &mut AppState) -> anyhow::Result<OverlayWindowConfig>
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<OverlayWindowConfig>
} 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)

View File

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

View File

@ -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<Box<dyn HidProvider>, 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))

View File

@ -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<Toast>,
tx_toast: mpsc::SyncSender<Toast>,
rx_toast: mpsc::Receiver<BakedToast>,
tx_toast: mpsc::SyncSender<BakedToast>,
running: Arc<AtomicBool>,
}
@ -29,7 +34,7 @@ impl NotificationManager {
}
}
pub fn drain_pending(&self, session: &AppSession) -> Vec<Toast> {
pub fn drain_pending(&self, session: &AppSession) -> Vec<BakedToast> {
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<Toast> {
fn parse_dbus(msg: &dbus::Message) -> anyhow::Result<BakedToast> {
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<Toast> {
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
}

View File

@ -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<T> OverlayWindowManager<T> {
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);

View File

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