bar app icons & tooltips

This commit is contained in:
galister
2026-01-05 15:45:19 +09:00
parent b86525d65d
commit b56aa1a8de
30 changed files with 291 additions and 129 deletions

8
Cargo.lock generated
View File

@@ -1428,20 +1428,16 @@ dependencies = [
"async-native-tls",
"chrono",
"glam",
"glob",
"http-body-util",
"hyper",
"identicons-svg",
"keyvalues-parser",
"log",
"rust-embed",
"rust-ini",
"serde",
"serde_json",
"smol",
"smol-hyper",
"strum",
"walkdir",
"wayvr-ipc",
"wgui",
"wlx-common",
@@ -6923,12 +6919,16 @@ dependencies = [
"anyhow",
"chrono",
"glam",
"glob",
"identicons-svg",
"idmap",
"idmap-derive",
"log",
"rodio",
"rust-ini",
"serde",
"smol",
"walkdir",
"wayvr-ipc",
"wgui",
"xdg 3.0.0",

View File

@@ -24,7 +24,3 @@ hyper = { version = "1.8.1", features = ["client", "http1", "http2"] }
http-body-util = "0.1.3"
async-native-tls = "0.5.0"
smol-hyper = "0.1.1"
glob = "0.3.3"
walkdir = "2.5.0"
rust-ini = "0.21.3"
identicons-svg = "0.1.0"

View File

@@ -19,11 +19,10 @@ use wlx_common::{audio, dash_interface::BoxDashInterface, timestep::Timestep};
use crate::{
assets, settings,
tab::{
Tab, TabType, apps::TabApps, games::TabGames, home::TabHome, monado::TabMonado, processes::TabProcesses,
settings::TabSettings,
apps::TabApps, games::TabGames, home::TabHome, monado::TabMonado, processes::TabProcesses, settings::TabSettings,
Tab, TabType,
},
util::{
desktop_finder::DesktopFinder,
popup_manager::{MountPopupParams, PopupManager, PopupManagerParams},
toast_manager::ToastManager,
various::AsyncExecutor,
@@ -64,8 +63,6 @@ pub struct Frontend<T> {
window_audio_settings: WguiWindow,
view_audio_settings: Option<views::audio_settings::View>,
pub(crate) desktop_finder: DesktopFinder,
}
pub struct FrontendUpdateParams<'a, T> {
@@ -137,9 +134,6 @@ impl<T: 'static> Frontend<T> {
let timestep = Timestep::new(60.0);
let mut desktop_finder = DesktopFinder::new();
desktop_finder.refresh();
let mut frontend = Self {
layout,
state,
@@ -159,7 +153,6 @@ impl<T: 'static> Frontend<T> {
window_audio_settings: WguiWindow::default(),
view_audio_settings: None,
executor: Rc::new(smol::LocalExecutor::new()),
desktop_finder,
};
// init some things first
@@ -292,7 +285,7 @@ impl<T: 'static> Frontend<T> {
fn process_task(&mut self, data: &mut T, task: FrontendTask) -> anyhow::Result<()> {
match task {
FrontendTask::SetTab(tab_type) => self.set_tab(tab_type)?,
FrontendTask::SetTab(tab_type) => self.set_tab(data, tab_type)?,
FrontendTask::RefreshClock => self.update_time()?,
FrontendTask::RefreshBackground => self.update_background()?,
FrontendTask::MountPopup(params) => self.mount_popup(params)?,
@@ -305,14 +298,14 @@ impl<T: 'static> Frontend<T> {
Ok(())
}
fn set_tab(&mut self, tab_type: TabType) -> anyhow::Result<()> {
fn set_tab(&mut self, data: &mut T, tab_type: TabType) -> anyhow::Result<()> {
log::info!("Setting tab to {tab_type:?}");
let widget_content = self.state.fetch_widget(&self.layout.state, "content")?;
self.layout.remove_children(widget_content.id);
let tab: Box<dyn Tab<T>> = match tab_type {
TabType::Home => Box::new(TabHome::new(self, widget_content.id)?),
TabType::Apps => Box::new(TabApps::new(self, widget_content.id)?),
TabType::Apps => Box::new(TabApps::new(self, widget_content.id, data)?),
TabType::Games => Box::new(TabGames::new(self, widget_content.id)?),
TabType::Monado => Box::new(TabMonado::new(self, widget_content.id)?),
TabType::Processes => Box::new(TabProcesses::new(self, widget_content.id)?),

View File

@@ -14,14 +14,12 @@ use wgui::{
parser::{Fetchable, ParseDocumentParams, ParserState},
task::Tasks,
};
use wlx_common::desktop_finder::DesktopEntry;
use crate::{
frontend::{Frontend, FrontendTask, FrontendTasks},
tab::{Tab, TabType},
util::{
desktop_finder::DesktopEntry,
popup_manager::{MountPopupParams, PopupHandle},
},
util::popup_manager::{MountPopupParams, PopupHandle},
views::{self, app_launcher},
};
@@ -127,16 +125,16 @@ fn doc_params(globals: WguiGlobals) -> ParseDocumentParams<'static> {
}
impl<T> TabApps<T> {
pub fn new(frontend: &mut Frontend<T>, parent_id: WidgetID) -> anyhow::Result<Self> {
pub fn new(frontend: &mut Frontend<T>, parent_id: WidgetID, data: &mut T) -> anyhow::Result<Self> {
let globals = frontend.layout.state.globals.clone();
let tasks = Tasks::new();
let state = Rc::new(RefCell::new(State { view_launcher: None }));
let mut entries = frontend.desktop_finder.find_entries();
let mut entries = frontend.interface.desktop_finder(data).find_entries();
let parser_state = wgui::parser::parse_from_assets(&doc_params(globals.clone()), &mut frontend.layout, parent_id)?;
let app_list_parent = parser_state.fetch_widget(&frontend.layout.state, "app_list_parent")?;
let app_list = AppList {
entries_to_mount: entries.drain(..).collect(),
entries_to_mount: entries.into_values().collect(),
list_parent: app_list_parent,
};

View File

@@ -1,5 +1,4 @@
pub mod cached_fetcher;
pub mod desktop_finder;
pub mod http_client;
pub mod pactl_wrapper;
pub mod popup_manager;

View File

@@ -11,11 +11,10 @@ use wgui::{
sprite::{WidgetSprite, WidgetSpriteParams},
},
};
use wlx_common::desktop_finder;
pub type AsyncExecutor = Rc<smol::LocalExecutor<'static>>;
use crate::util::desktop_finder;
// the compiler wants to scream
#[allow(irrefutable_let_patterns)]
pub fn get_desktop_file_icon_path(desktop_file: &desktop_finder::DesktopEntry) -> AssetPathOwned {

View File

@@ -12,12 +12,11 @@ use wgui::{
task::Tasks,
widget::label::WidgetLabel,
};
use wlx_common::dash_interface::BoxDashInterface;
use wlx_common::{dash_interface::BoxDashInterface, desktop_finder::DesktopEntry};
use crate::{
frontend::{FrontendTask, FrontendTasks},
settings::SettingsIO,
util::desktop_finder::DesktopEntry,
};
#[derive(Clone, Copy, Eq, PartialEq, EnumString, VariantNames, AsRefStr)]
@@ -325,6 +324,7 @@ impl View {
name: params.application.app_name.to_string(),
args,
resolution,
icon: params.application.icon_path.as_ref().map(|x| x.as_ref().to_string()),
userdata,
},
)?;

View File

@@ -20,13 +20,9 @@ use wgui::{
ConstructEssentials,
},
};
use wlx_common::dash_interface::BoxDashInterface;
use wlx_common::{dash_interface::BoxDashInterface, desktop_finder::DesktopEntry};
use crate::util::{
self,
desktop_finder::{self},
various::get_desktop_file_icon_path,
};
use crate::util::{self, various::get_desktop_file_icon_path};
#[derive(Clone)]
enum Task {
@@ -94,13 +90,13 @@ impl View {
}
}
fn get_desktop_entry_from_process(process: &packet_server::WvrProcess) -> Option<desktop_finder::DesktopEntry> {
fn get_desktop_entry_from_process(process: &packet_server::WvrProcess) -> Option<DesktopEntry> {
// TODO: refactor this after we ditch old wayvr-dashboard completely
let Some(dfile_str) = process.userdata.get("desktop-entry") else {
return None;
};
let Ok(desktop_file) = serde_json::from_str::<desktop_finder::DesktopEntry>(dfile_str) else {
let Ok(desktop_file) = serde_json::from_str::<DesktopEntry>(dfile_str) else {
debug_assert!(false); // invalid json???
return None;
};

View File

@@ -28,6 +28,7 @@ pub struct WvrProcessLaunchParams {
pub exec: String,
pub env: Vec<String>,
pub args: String,
pub icon: Option<String>,
pub resolution: [u32; 2],
pub userdata: HashMap<String, String>,
}

View File

@@ -1,5 +1,7 @@
use std::rc::Rc;
use anyhow::Context;
use crate::assets::AssetProvider;
// a string which optionally has translation key in it
@@ -98,10 +100,27 @@ impl I18n {
}
let data_english = provider.load_from_path("lang/en.json")?;
let data_translated = provider.load_from_path(&format!("lang/{lang}.json"))?;
let path = format!("lang/{lang}.json");
let data_translated = provider
.load_from_path(&path)
.with_context(|| path.clone())
.context("Could not load translation file")?;
let json_root_fallback = serde_json::from_str(str::from_utf8(&data_english)?)?;
let json_root_translated = serde_json::from_str(str::from_utf8(&data_translated)?)?;
let json_root_fallback = serde_json::from_str(
str::from_utf8(&data_english)
.with_context(|| path.clone())
.context("Translation file not valid UTF-8")?,
)
.with_context(|| path.clone())
.context("Translation file not valid JSON")?;
let json_root_translated = serde_json::from_str(
str::from_utf8(&data_translated)
.with_context(|| path.clone())
.context("Translation file not valid UTF-8")?,
)
.with_context(|| path.clone())
.context("Translation file not valid JSON")?;
Ok(Self {
json_root_translated,
@@ -110,22 +129,16 @@ impl I18n {
}
pub fn translate(&mut self, translation_key_full: &str) -> Rc<str> {
let translation_key = translation_key_full
.split_once(';')
.map_or(translation_key_full, |(a, _)| a);
let mut sections = translation_key_full.split(';');
let translation_key = sections.next().map_or(translation_key_full, |a| a);
if let Some(translated) = find_translation(translation_key, &self.json_root_translated) {
return Rc::from(translated);
return Rc::from(format_translated(translated, sections));
}
if let Some(translated_fallback) = find_translation(translation_key, &self.json_root_fallback) {
log::warn!("missing translation for key \"{translation_key}\", using \"en\" instead");
return Rc::from(translated_fallback);
}
// not even found in fallback, check if the translation contains ";" (to be used as "MY_TRANSLATION_KEY;A fallback text")
if let Some((idx, _)) = translation_key_full.match_indices(';').next() {
return Rc::from(&translation_key_full[idx + 1..]);
return Rc::from(format_translated(translated_fallback, sections));
}
log::error!("missing translation for key \"{translation_key}\"");
@@ -137,3 +150,28 @@ impl I18n {
translated.replace(to_replace.0, to_replace.1)
}
}
fn format_translated<'a, I>(format: &str, args: I) -> String
where
I: IntoIterator<Item = &'a str>,
{
let mut result = String::new();
let mut args = args.into_iter();
let mut chars = format.chars().peekable();
while let Some(c) = chars.next() {
if c == '{' && chars.peek() == Some(&'}') {
chars.next(); //consume }
if let Some(arg) = args.next() {
result.push_str(arg);
} else {
// no more args → keep literal {}
result.push_str("{}");
}
} else {
result.push(c);
}
}
result
}

View File

@@ -32,6 +32,7 @@ pub mod gfx;
pub mod globals;
pub mod i18n;
pub mod layout;
pub mod log;
pub mod parser;
pub mod renderer_vk;
pub mod sound;

53
wgui/src/log.rs Normal file
View File

@@ -0,0 +1,53 @@
use std::fmt::Debug;
pub trait LogErr {
fn log_err(self) -> Self;
fn log_err_with(self, additional: &str) -> Self;
fn log_warn(self) -> Self;
fn log_warn_with(self, additional: &str) -> Self;
}
impl<T, E> LogErr for Result<T, E>
where
E: Debug + Send + Sync + 'static,
{
fn log_warn(self) -> Result<T, E> {
match self {
Ok(ok) => Ok(ok),
Err(error) => {
log::warn!("{error:?}");
Err(error)
}
}
}
fn log_warn_with(self, additional: &str) -> Result<T, E> {
match self {
Ok(ok) => Ok(ok),
Err(error) => {
log::warn!("{additional}: {error:?}");
Err(error)
}
}
}
fn log_err(self) -> Result<T, E> {
match self {
Ok(ok) => Ok(ok),
Err(error) => {
log::error!("{error:?}");
Err(error)
}
}
}
fn log_err_with(self, additional: &str) -> Result<T, E> {
match self {
Ok(ok) => Ok(ok),
Err(error) => {
log::error!("{additional}: {error:?}");
Err(error)
}
}
}
}

View File

@@ -22,3 +22,7 @@ rodio = { version = "0.21.1", default-features = false, features = [
"mp3",
"hound",
] }
glob = "0.3.3"
walkdir = "2.5.0"
rust-ini = "0.21.3"
identicons-svg = "0.1.0"

View File

@@ -9,33 +9,3 @@ pub enum LeftRight {
Left,
Right,
}
pub trait LogErr {
fn log_err(self) -> Self;
fn log_warn(self) -> Self;
}
impl<T, E> LogErr for Result<T, E>
where
E: Debug + Send + Sync + 'static,
{
fn log_warn(self) -> Result<T, E> {
match self {
Ok(ok) => Ok(ok),
Err(error) => {
log::warn!("{error:?}");
Err(error)
}
}
}
fn log_err(self) -> Result<T, E> {
match self {
Ok(ok) => Ok(ok),
Err(error) => {
log::error!("{error:?}");
Err(error)
}
}
}
}

View File

@@ -3,6 +3,8 @@ use wayvr_ipc::{
packet_server::{WvrProcess, WvrProcessHandle, WvrWindow, WvrWindowHandle},
};
use crate::desktop_finder::DesktopFinder;
pub trait DashInterface<T> {
fn window_list(&mut self, data: &mut T) -> anyhow::Result<Vec<WvrWindow>>;
fn window_set_visible(&mut self, data: &mut T, handle: WvrWindowHandle, visible: bool) -> anyhow::Result<()>;
@@ -12,6 +14,7 @@ pub trait DashInterface<T> {
fn process_list(&mut self, data: &mut T) -> anyhow::Result<Vec<WvrProcess>>;
fn process_terminate(&mut self, data: &mut T, handle: WvrProcessHandle) -> anyhow::Result<()>;
fn recenter_playspace(&mut self, data: &mut T) -> anyhow::Result<()>;
fn desktop_finder<'a>(&'a mut self, data: &'a mut T) -> &'a mut DesktopFinder;
}
pub type BoxDashInterface<T> = Box<dyn DashInterface<T>>;

View File

@@ -3,7 +3,7 @@ use wayvr_ipc::{
packet_server::{WvrProcess, WvrProcessHandle, WvrWindow, WvrWindowHandle},
};
use crate::{dash_interface::DashInterface, gen_id};
use crate::{dash_interface::DashInterface, desktop_finder::DesktopFinder, gen_id};
#[derive(Debug)]
pub struct EmuProcess {
@@ -54,6 +54,7 @@ gen_id!(EmuProcessVec, EmuProcess, EmuProcessCell, EmuProcessHandle);
pub struct DashInterfaceEmulated {
processes: EmuProcessVec,
windows: EmuWindowVec,
desktop_finder: DesktopFinder,
}
impl DashInterfaceEmulated {
@@ -69,7 +70,14 @@ impl DashInterfaceEmulated {
visible: true,
});
Self { processes, windows }
let mut desktop_finder = DesktopFinder::new();
desktop_finder.refresh();
Self {
processes,
windows,
desktop_finder,
}
}
}
@@ -157,4 +165,8 @@ impl DashInterface<()> for DashInterfaceEmulated {
// stub!
Ok(())
}
fn desktop_finder<'a>(&'a mut self, _: &'a mut ()) -> &'a mut DesktopFinder {
&mut self.desktop_finder
}
}

View File

@@ -1,12 +1,12 @@
use std::{
collections::HashSet, ffi::OsStr, fmt::Debug, fs::exists, path::Path, rc::Rc, sync::Arc, thread::JoinHandle,
collections::{HashMap, HashSet}, ffi::OsStr, fmt::Debug, fs::exists, path::Path, rc::Rc, sync::Arc, thread::JoinHandle,
time::Instant,
};
use ini::Ini;
use serde::{Deserialize, Serialize};
use walkdir::WalkDir;
use wlx_common::cache_dir;
use crate::cache_dir;
struct DesktopEntryOwned {
exec_path: String,
@@ -50,8 +50,8 @@ struct DesktopFinderParams {
pub struct DesktopFinder {
params: Arc<DesktopFinderParams>,
entry_cache: Vec<DesktopEntry>,
bg_task: Option<JoinHandle<Vec<DesktopEntryOwned>>>,
entry_cache: HashMap<String,DesktopEntry>,
bg_task: Option<JoinHandle<HashMap<String,DesktopEntryOwned>>>,
}
impl DesktopFinder {
@@ -98,16 +98,16 @@ impl DesktopFinder {
icon_folders,
size_preferences,
}),
entry_cache: Vec::new(),
entry_cache: HashMap::new(),
bg_task: None,
}
}
fn build_cache(params: Arc<DesktopFinderParams>) -> Vec<DesktopEntryOwned> {
fn build_cache(params: Arc<DesktopFinderParams>) -> HashMap<String, DesktopEntryOwned> {
let start = Instant::now();
let mut known_files = HashSet::new();
let mut entries = Vec::<DesktopEntryOwned>::new();
let mut entries = HashMap::<String, DesktopEntryOwned>::new();
let icons_folder = cache_dir::get_path("icons");
if !std::fs::exists(&icons_folder).unwrap_or(false) {
@@ -131,6 +131,9 @@ impl DesktopFinder {
}
let file_name = entry.file_name().to_string_lossy();
let Some(app_id) = Path::new(entry.file_name()).file_stem().map(|x| x.to_string_lossy().to_string()) else {
continue;
};
if known_files.contains(file_name.as_ref()) {
// as per xdg spec, user entries of the same filename will override system entries
@@ -220,7 +223,7 @@ impl DesktopFinder {
known_files.insert(file_name.to_string());
entries.push(DesktopEntryOwned {
entries.insert(app_id, DesktopEntryOwned {
app_name: String::from(app_name),
exec_path: String::from(exec_path),
exec_args: exec_args.join(" "),
@@ -265,7 +268,7 @@ impl DesktopFinder {
None
}
fn create_icon(desktop_entry_name: &str) -> anyhow::Result<String> {
pub fn create_icon(desktop_entry_name: &str) -> anyhow::Result<String> {
let relative_path = format!("icons/{}.svg", desktop_entry_name);
let file_path = cache_dir::get_path(&relative_path).to_string_lossy().to_string();
@@ -295,12 +298,16 @@ impl DesktopFinder {
};
self.entry_cache.clear();
for entry in entries {
self.entry_cache.push(entry.into());
for (app_id, entry) in entries {
self.entry_cache.insert(app_id, entry.into());
}
}
pub fn find_entries(&mut self) -> Vec<DesktopEntry> {
pub fn get_cached_entry(&self, app_id: &str) -> Option<&DesktopEntry> {
self.entry_cache.get(app_id)
}
pub fn find_entries(&mut self) -> HashMap<String, DesktopEntry> {
self.wait_for_entries();
self.entry_cache.clone()
}

View File

@@ -5,6 +5,7 @@ pub mod common;
pub mod config;
pub mod dash_interface;
pub mod dash_interface_emulated;
pub mod desktop_finder;
mod handle;
pub mod overlays;
pub mod timestep;

View File

@@ -1,3 +1,5 @@
use std::sync::Arc;
use idmap_derive::IntegerId;
use serde::{Deserialize, Serialize};
@@ -21,12 +23,15 @@ pub enum ToastDisplayMethod {
pub enum BackendAttrib {
Stereo,
MouseTransform,
Icon,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum BackendAttribValue {
Stereo(StereoMode),
MouseTransform(MouseTransform),
#[serde(skip_serializing, skip_deserializing)]
Icon(Arc<str>),
}
#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize)]

View File

@@ -88,14 +88,14 @@
<!-- An app with a single icon. -->
<template name="App">
<Button macro="button_style" id="overlay_${idx}" _press="::OverlayToggle ${name}" tooltip="${name}" tooltip_side="bottom">
<sprite width="56" height="56" color="~text_color" src_ext="${src}" />
<Button macro="button_style" id="overlay_${idx}" _short_release="::OverlayToggle ${name}" tooltip="OVERLAY_TOOLTIP.APP;${name}" tooltip_side="bottom">
<sprite width="56" height="56" color="~text_color" src_ext="${icon}" />
</Button>
</template>
<!-- A screen with a shortened connector name, e.g. "H1" for HDMI-A-1 or "D2" for DP-2 -->
<template name="Screen">
<Button macro="button_style" id="overlay_${idx}" _press="::OverlayToggle ${name}" tooltip="${name}" tooltip_side="bottom">
<Button macro="button_style" id="overlay_${idx}" _short_release="::OverlayToggle ${name}" tooltip="OVERLAY_TOOLTIP.SCREEN;${name}" tooltip_side="bottom">
<sprite width="56" height="56" color="~text_color" src_builtin="edit/screen.svg" />
<div position="absolute" margin_top="-10">
<label text="${display}" size="26" color="~color_faded_20" weight="bold" />
@@ -104,13 +104,13 @@
</template>
<template name="Panel">
<Button macro="button_style" id="overlay_${idx}" _press="::OverlayToggle ${name}" tooltip="${name}" tooltip_side="bottom">
<Button macro="button_style" id="overlay_${idx}" _short_release="::OverlayToggle ${name}" tooltip="OVERLAY_TOOLTIP.PANEL;${name}" tooltip_side="bottom">
<sprite width="56" height="56" color="~text_color" src_builtin="edit/panel.svg" />
</Button>
</template>
<template name="Mirror">
<Button macro="button_style" id="overlay_${idx}" _press="::OverlayToggle ${name}" tooltip="${name}" tooltip_side="bottom">
<Button macro="button_style" id="overlay_${idx}" _short_release="::OverlayToggle ${name}" tooltip="OVERLAY_TOOLTIP.MIRROR;${name}" tooltip_side="bottom">
<sprite width="56" height="56" color="~text_color" src_builtin="edit/mirror.svg" />
<div position="absolute" margin_top="7" margin_left="20">
<label text="${display}" size="26" color="~color_faded_20" weight="bold" />
@@ -119,7 +119,7 @@
</template>
<template name="Set">
<Button macro="button_style" id="set_${idx}" _press="::SetSwitch ${idx}" tooltip="WATCH.SWITCH_TO_SET" tooltip_side="bottom">
<Button macro="button_style" id="set_${idx}" _release="::SetSwitch ${idx}" tooltip="WATCH.SWITCH_TO_SET" tooltip_side="bottom">
<sprite width="56" height="56" color="~text_color" src_builtin="watch/set2.svg" />
<div position="absolute" margin_top="16" margin_left="-8">
<label text="${display}" size="26" color="~color_faded_20" weight="bold" />
@@ -139,9 +139,9 @@
</div>
<VerticalSeparator />
<div id="apps_root" gap="8">
<App id="test1" name="Blender" src="/usr/share/icons/hicolor/scalable/apps/blender-5.0.svg" />
<App id="test2" name="Inkscape" src="/usr/share/icons/hicolor/scalable/apps/org.inkscape.Inkscape.svg" />
<App id="test3" name="GIMP" src="/usr/share/icons/hicolor/scalable/apps/gimp.svg" />
<App id="test1" name="Blender" icon="/usr/share/icons/hicolor/scalable/apps/blender-5.0.svg" />
<App id="test2" name="Inkscape" icon="/usr/share/icons/hicolor/scalable/apps/org.inkscape.Inkscape.svg" />
<App id="test3" name="GIMP" icon="/usr/share/icons/hicolor/scalable/apps/gimp.svg" />
</div>
</div>
<div id="tray_root" flex_direction="row" gap="16">

View File

@@ -4,6 +4,12 @@
},
"DEFAULT": "Default",
"DISABLED": "Disabled",
"OVERLAY_TOOLTIP": {
"SCREEN": "Screen {}",
"MIRROR": "Mirror {}",
"PANEL": "Panel {}",
"APP": "{}"
},
"EDIT_MODE": {
"ADJUST_CURVATURE": "Adjust curvature",
"ALPHA_BLEND_MODE": "Alpha blend mode",

View File

@@ -19,6 +19,7 @@ use smithay::{
shm::ShmState,
}
};
use wlx_common::desktop_finder::DesktopFinder;
use std::{
cell::RefCell,
collections::{HashMap, HashSet},
@@ -301,23 +302,57 @@ impl WvrServerState {
continue;
};
let [size_x, size_y] = match wvr_server.processes.get(&process_handle) {
Some(Process::Managed(p)) => p.resolution,
_ => [1920, 1080],
// Size, icon & fallback title comes from process
let ([size_x, size_y], fallback_title, icon, is_cage) = match wvr_server.processes.get(&process_handle) {
Some(Process::Managed(p)) => (p.resolution, Some(p.app_name.clone()), p.icon.as_ref().cloned(), p.exec_path.ends_with("cage")),
_ => ([1920, 1080], None, None, false)
};
let window_handle = wvr_server
.wm
.create_window(toplevel.clone(), process_handle, size_x, size_y);
let title: Arc<str> = with_states(toplevel.wl_surface(), |states| {
states
.data_map
.get::<XdgToplevelSurfaceData>()
.and_then(|t| t.lock().unwrap().title.clone())
.map_or_else(|| format!("P{}", client.pid).into(), String::into)
});
let mut title: Arc<str> = fallback_title.unwrap_or_else(|| format!("P{}", client.pid)).into();
let mut icon = icon;
// Try to get title from xdg_toplevel, unless it's running in cage
if !is_cage {
let mut needs_title = true;
let (xdg_title, app_id): (Option<String>, Option<String>) = with_states(toplevel.wl_surface(), |states| {
states
.data_map
.get::<XdgToplevelSurfaceData>()
.map(|t| {
let t = t.lock().unwrap();
(t.title.clone(), t.app_id.clone())
})
.unwrap_or((None, None))
});
if let Some(xdg_title) = xdg_title {
needs_title = false;
title = xdg_title.into();
}
// Try to get title & icon from desktop entry
if let Some(app_id) = app_id
&& let Some(desktop_entry) = app.desktop_finder.get_cached_entry(&app_id) {
if needs_title {
title = desktop_entry.app_name.as_ref().into();
}
if icon.is_none()
&& let Some(icon_path) = desktop_entry.icon_path.as_ref() {
icon = Some(icon_path.as_ref().into());
}
}
}
// Fall back to identicon
let icon = match icon {
Some(icon) => icon,
None => DesktopFinder::create_icon(&*title)?.into(),
};
app.tasks.enqueue(TaskType::Overlay(OverlayTask::Create(
OverlaySelector::Nothing,
Box::new(move |app: &mut AppState| {
@@ -325,7 +360,8 @@ impl WvrServerState {
title,
app,
window_handle,
size_x.max(size_y),
icon,
[size_x, size_y],
).context("Could not create WvrWindow overlay").inspect_err(|e| log::warn!("{e:?}")).ok()
}),
)));
@@ -500,11 +536,13 @@ impl WvrServerState {
pub fn spawn_process(
&mut self,
app_name: &str,
exec_path: &str,
args: &[&str],
env: &[(&str, &str)],
resolution: [u32; 2],
working_dir: Option<&str>,
icon: Option<&str>,
userdata: HashMap<String, String>,
) -> anyhow::Result<process::ProcessHandle> {
let auth_key = generate_auth_key();
@@ -528,6 +566,7 @@ impl WvrServerState {
auth_key,
child,
exec_path: String::from(exec_path),
app_name: String::from(app_name),
userdata,
args: args.iter().map(|x| String::from(*x)).collect(),
working_dir: working_dir.map(String::from),
@@ -535,6 +574,7 @@ impl WvrServerState {
.iter()
.map(|(a, b)| (String::from(*a), String::from(*b)))
.collect(),
icon: icon.map(Arc::from),
resolution,
}));

View File

@@ -1,4 +1,4 @@
use std::{collections::HashMap, io::Read};
use std::{collections::HashMap, io::Read, sync::Arc};
use wayvr_ipc::packet_server;
@@ -9,12 +9,13 @@ use crate::gen_id;
pub struct WayVRProcess {
pub auth_key: String,
pub child: std::process::Child,
pub app_name: String,
pub exec_path: String,
pub args: Vec<String>,
pub env: Vec<(String, String)>,
pub working_dir: Option<String>,
pub resolution: [u32; 2],
pub icon: Option<Arc<str>>,
pub userdata: HashMap<String, String>,
}
@@ -47,8 +48,11 @@ impl Process {
pub fn get_name(&self) -> String {
match self {
Self::Managed(p) => p.get_name().unwrap_or_else(|| String::from("unknown")),
Self::External(p) => p.get_name().unwrap_or_else(|| String::from("unknown")),
Self::Managed(p) => p.get_name()
.or_else(|| p.exec_path.split('/').last().map(String::from))
.unwrap_or_else(|| String::from("unknown")),
Self::External(p) => p.get_name()
.unwrap_or_else(|| String::from("unknown")),
}
}
@@ -71,7 +75,8 @@ impl Process {
impl Drop for WayVRProcess {
fn drop(&mut self) {
log::info!(
"Sending SIGTERM (graceful exit) to process {}",
"Sending SIGTERM (graceful exit) to process {} ({})",
self.child.id(),
self.exec_path.as_str()
);
self.terminate();

View File

@@ -239,11 +239,13 @@ impl Connection {
let env_vec = gen_env_vec(&packet_params.env);
let res = params.wvr_server.spawn_process(
&packet_params.name,
&packet_params.exec,
&args_vec,
&env_vec,
packet_params.resolution,
None,
packet_params.icon.as_deref(),
packet_params.userdata,
);

View File

@@ -3,7 +3,6 @@ use dash_frontend::{
settings::{self, SettingsIO},
};
use glam::{Affine2, Affine3A, Vec2, vec2, vec3};
use tracing::instrument::WithSubscriber;
use wayvr_ipc::{
packet_client::WvrProcessLaunchParams,
packet_server::{WvrProcess, WvrProcessHandle, WvrWindow, WvrWindowHandle},
@@ -376,11 +375,13 @@ impl DashInterface<AppState> for DashInterfaceLive {
wvr_server
.spawn_process(
&params.name,
&params.exec,
&args_vec,
&env_vec,
params.resolution,
None,
params.icon.as_deref(),
params.userdata,
)
.map(|x| x.as_packet())
@@ -434,4 +435,11 @@ impl DashInterface<AppState> for DashInterfaceLive {
.enqueue(TaskType::Playspace(PlayspaceTask::Recenter));
Ok(())
}
fn desktop_finder<'a>(
&'a mut self,
data: &'a mut AppState,
) -> &'a mut wlx_common::desktop_finder::DesktopFinder {
&mut data.desktop_finder
}
}

View File

@@ -9,7 +9,7 @@ use crate::{
use anyhow::Context;
use glam::{FloatExt, Mat4, Vec2, vec2, vec3};
use wgui::{
animation::{Animation, AnimationEasing}, assets::AssetPath, components::button::ComponentButton, drawing::{self, Color}, event::{self, CallbackDataCommon, CallbackMetadata, EventAlterables, EventListenerKind}, layout::LayoutUpdateParams, parser::{Fetchable, ParseDocumentExtra, ParseDocumentParams}, renderer_vk::util, taffy::{self, prelude::length}, widget::{div::WidgetDiv, rectangle::WidgetRectangle, EventResult}
animation::{Animation, AnimationEasing}, assets::AssetPath, components::button::ComponentButton, drawing::{self, Color}, event::{self, CallbackDataCommon, CallbackMetadata, EventAlterables, EventListenerKind}, layout::LayoutUpdateParams, parser::{Fetchable, ParseDocumentParams}, renderer_vk::util, taffy::{self, prelude::length}, widget::{div::WidgetDiv, rectangle::WidgetRectangle, EventResult}
};
use super::{
@@ -319,6 +319,7 @@ pub(super) fn create_keyboard_panel(
("Panel", panels_root)
},
OverlayCategory::WayVR => {
params.insert("icon".into(), meta.icon.as_ref().expect("WayVR overlay without Icon attribute!").as_ref().into());
("App", apps_root)
},
_ => continue

View File

@@ -51,9 +51,11 @@ pub fn create_wl_window_overlay(
name: Arc<str>,
app: &mut AppState,
window: wayvr::window::WindowHandle,
size_major: u32,
icon: Arc<str>,
size: [u32; 2],
) -> anyhow::Result<OverlayWindowConfig> {
let scale = size_major as f32 / 1920.0;
let scale = size[0].max(size[1]) as f32 / 1920.0;
let curve_scale = size[0] as f32 / 1920.0;
Ok(OverlayWindowConfig {
name: name.clone(),
@@ -61,7 +63,7 @@ pub fn create_wl_window_overlay(
grabbable: true,
interactable: true,
positioning: Positioning::Floating,
curvature: Some(0.15),
curvature: Some(0.15 * curve_scale),
transform: Affine3A::from_scale_rotation_translation(
Vec3::ONE * scale,
Quat::IDENTITY,
@@ -72,12 +74,15 @@ pub fn create_wl_window_overlay(
keyboard_focus: Some(KeyboardFocus::WayVR),
category: OverlayCategory::WayVR,
show_on_spawn: true,
..OverlayWindowConfig::from_backend(Box::new(WvrWindowBackend::new(name, app, window)?))
..OverlayWindowConfig::from_backend(Box::new(WvrWindowBackend::new(
name, app, window, icon,
)?))
})
}
pub struct WvrWindowBackend {
name: Arc<str>,
icon: Arc<str>,
pipeline: Option<ScreenPipeline>,
popups_pipeline: Arc<WGfxPipeline<Vert2Uv>>,
interaction_transform: Option<Affine2>,
@@ -100,6 +105,7 @@ impl WvrWindowBackend {
name: Arc<str>,
app: &mut AppState,
window: wayvr::window::WindowHandle,
icon: Arc<str>,
) -> anyhow::Result<Self> {
let popups_pipeline = app.gfx.create_pipeline(
app.gfx_extras.shaders.get("vert_quad").unwrap(), // want panic
@@ -170,6 +176,7 @@ impl WvrWindowBackend {
Ok(Self {
name,
icon,
pipeline: None,
window,
popups: vec![],
@@ -508,6 +515,7 @@ impl OverlayBackend for WvrWindowBackend {
fn get_attrib(&self, attrib: BackendAttrib) -> Option<BackendAttribValue> {
match attrib {
BackendAttrib::Stereo => self.stereo.map(BackendAttribValue::Stereo),
BackendAttrib::Icon => Some(BackendAttribValue::Icon(self.icon.clone())),
_ => None,
}
}

View File

@@ -9,6 +9,7 @@ use wgui::{
use wlx_common::{
audio,
config::GeneralConfig,
desktop_finder::DesktopFinder,
overlays::{ToastDisplayMethod, ToastTopic},
};
@@ -57,6 +58,8 @@ pub struct AppState {
pub ipc_server: ipc_server::WayVRServer,
pub wayvr_signals: SyncEventQueue<WayVRSignal>,
pub desktop_finder: DesktopFinder,
#[cfg(feature = "osc")]
pub osc_sender: Option<OscSender>,
@@ -136,6 +139,9 @@ impl AppState {
let ipc_server = ipc_server::WayVRServer::new()?;
let mut desktop_finder = DesktopFinder::new();
desktop_finder.refresh();
Ok(Self {
session,
tasks,
@@ -159,6 +165,7 @@ impl AppState {
xr_backend,
ipc_server,
wayvr_signals,
desktop_finder,
#[cfg(feature = "osc")]
osc_sender,

View File

@@ -112,6 +112,7 @@ pub struct OverlayMeta {
pub name: Arc<str>,
pub category: OverlayCategory,
pub visible: bool,
pub icon: Option<Arc<str>>,
}
#[allow(clippy::enum_variant_names)]

View File

@@ -5,8 +5,9 @@ use std::{
use anyhow::Context;
use glam::{Affine3A, Vec3, Vec3A};
use slotmap::{HopSlotMap, Key, SecondaryMap};
use wgui::log::LogErr;
use wlx_common::{
astr_containers::{AStrMap, AStrMapExt}, common::LogErr, config::SerializedWindowSet, overlays::{BackendAttrib, ToastTopic}
astr_containers::{AStrMap, AStrMapExt}, config::SerializedWindowSet, overlays::{BackendAttrib, BackendAttribValue, ToastTopic}
};
use crate::{
@@ -773,11 +774,18 @@ impl<T> OverlayWindowManager<T> {
if matches!(data.config.category, OverlayCategory::Internal) {
continue;
}
let icon = if let Some(BackendAttribValue::Icon(icon)) = data.config.backend.get_attrib(BackendAttrib::Icon) {
Some(icon)
} else {
None
};
meta.push(OverlayMeta {
id,
name: data.config.name.clone(),
category: data.config.category,
visible: data.config.is_active(),
icon,
});
}