mirror of https://github.com/wayvr-org/wayvr.git
430 lines
9.4 KiB
Rust
430 lines
9.4 KiB
Rust
use std::{cell::RefCell, collections::VecDeque, marker::PhantomData, rc::Rc};
|
|
use wgui::{
|
|
assets::AssetPath,
|
|
components::button::{ButtonClickCallback, ComponentButton},
|
|
globals::WguiGlobals,
|
|
i18n::Translation,
|
|
layout::{WidgetID, WidgetPair},
|
|
parser::{Fetchable, ParseDocumentParams, ParserState, TemplateParams},
|
|
task::Tasks,
|
|
};
|
|
use wlx_common::{config::PinnedApp, desktop_finder::DesktopEntry};
|
|
|
|
use crate::{
|
|
frontend::{Frontend, FrontendTasks},
|
|
tab::{Tab, TabType},
|
|
util::{popup_manager::PopupHolder, wgui_simple},
|
|
views::{self},
|
|
};
|
|
|
|
struct State {
|
|
#[allow(dead_code)]
|
|
parser_state: ParserState,
|
|
view_launcher: PopupHolder<views::app_launcher::View>,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
enum Task {
|
|
RefreshPinnedApps,
|
|
}
|
|
|
|
pub struct TabApps<T> {
|
|
state: Rc<RefCell<State>>,
|
|
app_list: AppList,
|
|
marker: PhantomData<T>,
|
|
|
|
pinned_apps_parent: WidgetID,
|
|
|
|
tasks: Tasks<Task>,
|
|
}
|
|
|
|
impl<T> Tab<T> for TabApps<T> {
|
|
fn get_type(&self) -> TabType {
|
|
TabType::Apps
|
|
}
|
|
|
|
fn update(&mut self, frontend: &mut Frontend<T>, _time_ms: u32, data: &mut T) -> anyhow::Result<()> {
|
|
let mut state = self.state.borrow_mut();
|
|
|
|
self.app_list.tick(frontend, &self.tasks, &mut state, &self.state)?;
|
|
|
|
for task in self.tasks.drain() {
|
|
match task {
|
|
Task::RefreshPinnedApps => self.refresh_pinned_apps(&mut state, frontend, data)?,
|
|
}
|
|
}
|
|
|
|
state
|
|
.view_launcher
|
|
.with_view_res(|view| view.update(&mut frontend.interface, data))?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn find_entry_from_app_name<'a>(app_id: &str, entries: &'a [DesktopEntry]) -> Option<&'a DesktopEntry> {
|
|
entries.iter().find(|&entry| *entry.app_id == *app_id)
|
|
}
|
|
|
|
impl<T> TabApps<T> {
|
|
fn refresh_pinned_apps(&self, state: &mut State, frontend: &mut Frontend<T>, data: &mut T) -> anyhow::Result<()> {
|
|
frontend.layout.remove_children(self.pinned_apps_parent);
|
|
let globals = frontend.globals.clone();
|
|
|
|
let mut stale_entries = Vec::<Rc<str>>::new();
|
|
|
|
let mut pinned_desktop_entries = Vec::<(PinnedApp, &DesktopEntry)>::new();
|
|
|
|
// collect pinned desktop entries
|
|
{
|
|
let config = frontend.interface.general_config(data);
|
|
for pinned_app in &config.pinned_apps {
|
|
let Some(desktop_entry) = find_entry_from_app_name(&pinned_app.app_id, &self.app_list.all_entries) else {
|
|
stale_entries.push(pinned_app.app_id.clone());
|
|
continue;
|
|
};
|
|
|
|
pinned_desktop_entries.push((pinned_app.clone(), desktop_entry));
|
|
}
|
|
// cleanup:
|
|
// remove non-existent app ids from pinned apps
|
|
config.pinned_apps.retain(|pinned_app| {
|
|
for app_id in &stale_entries {
|
|
if *app_id == pinned_app.app_id {
|
|
return false;
|
|
}
|
|
}
|
|
true
|
|
});
|
|
}
|
|
|
|
if pinned_desktop_entries.is_empty() {
|
|
wgui_simple::create_label(
|
|
&mut frontend.layout,
|
|
self.pinned_apps_parent,
|
|
Translation::from_translation_key("EMPTY"),
|
|
)?;
|
|
}
|
|
|
|
// mount pinned desktop entries
|
|
for (pinned_app, desktop_entry) in pinned_desktop_entries {
|
|
let tooltip_string = format!(
|
|
"{}\n{}\n{}",
|
|
pinned_app.compositor_mode.as_ref(),
|
|
pinned_app.orientation_mode.as_ref(),
|
|
pinned_app.res_mode.as_ref()
|
|
);
|
|
|
|
let button = mount_entry(
|
|
frontend,
|
|
&mut state.parser_state,
|
|
&doc_params(frontend.globals.clone()),
|
|
self.pinned_apps_parent,
|
|
desktop_entry,
|
|
Some(tooltip_string),
|
|
)?;
|
|
|
|
button.on_click(on_app_click(
|
|
frontend.tasks.clone(),
|
|
self.tasks.clone(),
|
|
globals.clone(),
|
|
desktop_entry.clone(),
|
|
self.state.clone(),
|
|
Some(pinned_app.clone()),
|
|
));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
struct AppList {
|
|
//data: Vec<ParserData>,
|
|
all_entries: Vec<DesktopEntry>,
|
|
entries_to_mount: VecDeque<DesktopEntry>,
|
|
list_parent: WidgetPair,
|
|
prev_category_name: String,
|
|
}
|
|
|
|
// called after the user clicks any desktop entry
|
|
fn on_app_click(
|
|
frontend_tasks: FrontendTasks,
|
|
tasks: Tasks<Task>,
|
|
globals: WguiGlobals,
|
|
entry: DesktopEntry,
|
|
state: Rc<RefCell<State>>,
|
|
pinned_app: Option<PinnedApp>,
|
|
) -> ButtonClickCallback {
|
|
Rc::new(move |_common, _evt| {
|
|
views::app_launcher::mount_popup(
|
|
frontend_tasks.clone(),
|
|
globals.clone(),
|
|
entry.clone(),
|
|
state.borrow_mut().view_launcher.clone(),
|
|
tasks.make_callback_box(Task::RefreshPinnedApps),
|
|
pinned_app.clone(),
|
|
);
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
fn doc_params(globals: WguiGlobals) -> ParseDocumentParams<'static> {
|
|
ParseDocumentParams {
|
|
globals,
|
|
path: AssetPath::BuiltIn("gui/tab/apps.xml"),
|
|
extra: Default::default(),
|
|
}
|
|
}
|
|
|
|
impl<T> TabApps<T> {
|
|
pub fn new(frontend: &mut Frontend<T>, parent_id: WidgetID, data: &mut T) -> anyhow::Result<Self> {
|
|
let globals = frontend.layout.state.globals.clone();
|
|
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 pinned_apps_parent = parser_state.fetch_widget(&frontend.layout.state, "pinned_apps_parent")?;
|
|
|
|
let state = Rc::new(RefCell::new(State {
|
|
view_launcher: Default::default(),
|
|
parser_state,
|
|
}));
|
|
|
|
let entries: Vec<_> = frontend
|
|
.interface
|
|
.desktop_finder(data)
|
|
.find_entries()
|
|
.into_values()
|
|
.collect();
|
|
|
|
let mut entries_sorted = entries.clone();
|
|
entries_sorted.sort_by(|a, b| {
|
|
let cat_name_a = get_category_name(a);
|
|
let cat_name_b = get_category_name(b);
|
|
cat_name_a.cmp(cat_name_b)
|
|
});
|
|
|
|
let app_list = AppList {
|
|
all_entries: entries,
|
|
entries_to_mount: entries_sorted.drain(..).collect(),
|
|
list_parent: app_list_parent,
|
|
prev_category_name: String::new(),
|
|
};
|
|
|
|
let tasks = Tasks::<Task>::new();
|
|
tasks.push(Task::RefreshPinnedApps);
|
|
|
|
Ok(Self {
|
|
app_list,
|
|
state,
|
|
tasks,
|
|
marker: PhantomData,
|
|
pinned_apps_parent: pinned_apps_parent.id,
|
|
})
|
|
}
|
|
}
|
|
|
|
enum Scores {
|
|
Empty,
|
|
Unknown,
|
|
XFooBar, // X-something
|
|
Xfce,
|
|
Gnome,
|
|
Kde,
|
|
Gtk,
|
|
Qt,
|
|
Settings,
|
|
Application,
|
|
System,
|
|
Utility,
|
|
FileTools,
|
|
Filesystem,
|
|
FileManager,
|
|
Graphics,
|
|
Office,
|
|
Game,
|
|
VR, // best score (of course!)
|
|
}
|
|
|
|
fn get_category_name_score(name: &str) -> u8 {
|
|
if name.starts_with("X-") {
|
|
return Scores::XFooBar as u8;
|
|
}
|
|
|
|
match name {
|
|
"" => {
|
|
return Scores::Empty as u8;
|
|
}
|
|
"VR" => {
|
|
return Scores::VR as u8;
|
|
}
|
|
"Game" => {
|
|
return Scores::Game as u8;
|
|
}
|
|
"FileManager" => {
|
|
return Scores::FileManager as u8;
|
|
}
|
|
"Utility" => {
|
|
return Scores::Utility as u8;
|
|
}
|
|
"FileTools" => {
|
|
return Scores::FileTools as u8;
|
|
}
|
|
"Filesystem" => {
|
|
return Scores::Filesystem as u8;
|
|
}
|
|
"System" => {
|
|
return Scores::System as u8;
|
|
}
|
|
"Office" => {
|
|
return Scores::Office as u8;
|
|
}
|
|
"Settings" => {
|
|
return Scores::Settings as u8;
|
|
}
|
|
"Application" => {
|
|
return Scores::Application as u8;
|
|
}
|
|
"GTK" => {
|
|
return Scores::Gtk as u8;
|
|
}
|
|
"Qt" => {
|
|
return Scores::Qt as u8;
|
|
}
|
|
"XFCE" => {
|
|
return Scores::Xfce as u8;
|
|
}
|
|
"GNOME" => {
|
|
return Scores::Gnome as u8;
|
|
}
|
|
"KDE" => {
|
|
return Scores::Kde as u8;
|
|
}
|
|
"Graphics" => {
|
|
return Scores::Graphics as u8;
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
Scores::Unknown as u8
|
|
}
|
|
|
|
fn get_best_category_name(categories: &[Rc<str>]) -> Option<&Rc<str>> {
|
|
let mut best_score: u8 = 0;
|
|
let mut best_category: Option<&Rc<str>> = None;
|
|
|
|
for cat in categories {
|
|
let score = get_category_name_score(cat);
|
|
if score > best_score {
|
|
best_category = Some(cat);
|
|
best_score = score;
|
|
}
|
|
}
|
|
|
|
best_category
|
|
}
|
|
|
|
fn get_category_name(entry: &DesktopEntry) -> &str {
|
|
//log::info!("{:?}", entry.categories);
|
|
|
|
match get_best_category_name(&entry.categories) {
|
|
Some(cat) => cat,
|
|
None => "Other",
|
|
}
|
|
}
|
|
|
|
fn mount_entry<T>(
|
|
frontend: &mut Frontend<T>,
|
|
parser_state: &mut ParserState,
|
|
doc_params: &ParseDocumentParams,
|
|
id_parent: WidgetID,
|
|
entry: &DesktopEntry,
|
|
tooltip: Option<String>,
|
|
) -> anyhow::Result<Rc<ComponentButton>> {
|
|
{
|
|
let mut params = TemplateParams::new();
|
|
|
|
if let Some(tooltip) = tooltip {
|
|
params.insert_str("tooltip", tooltip);
|
|
};
|
|
|
|
// entry icon
|
|
params.insert_rc(
|
|
"src_ext",
|
|
entry
|
|
.icon_path
|
|
.as_ref()
|
|
.map_or_else(|| "".into(), |icon_path| icon_path.clone()),
|
|
);
|
|
|
|
// entry fallback (question mark) icon
|
|
params.insert(
|
|
"src",
|
|
if entry.icon_path.is_none() {
|
|
"dashboard/terminal.svg"
|
|
} else {
|
|
""
|
|
},
|
|
);
|
|
params.insert("name", &entry.app_name);
|
|
|
|
let data = parser_state.realize_template(doc_params, "AppEntry", &mut frontend.layout, id_parent, params)?;
|
|
|
|
data.fetch_component_as::<ComponentButton>("button")
|
|
}
|
|
}
|
|
|
|
impl AppList {
|
|
fn tick<T>(
|
|
&mut self,
|
|
frontend: &mut Frontend<T>,
|
|
tasks: &Tasks<Task>,
|
|
state: &mut State,
|
|
rc_state: &Rc<RefCell<State>>,
|
|
) -> anyhow::Result<()> {
|
|
let parser_state = &mut state.parser_state;
|
|
|
|
// load 30 entries for a single frame at most
|
|
for _ in 0..30 {
|
|
if let Some(entry) = self.entries_to_mount.pop_front() {
|
|
let globals = frontend.layout.state.globals.clone();
|
|
|
|
let category_name = get_category_name(&entry);
|
|
if category_name != self.prev_category_name {
|
|
self.prev_category_name = String::from(category_name);
|
|
let mut params = TemplateParams::new();
|
|
params.insert("text", category_name);
|
|
|
|
parser_state.realize_template(
|
|
&doc_params(globals.clone()),
|
|
"CategoryText",
|
|
&mut frontend.layout,
|
|
self.list_parent.id,
|
|
params,
|
|
)?;
|
|
}
|
|
|
|
let button = mount_entry(
|
|
frontend,
|
|
parser_state,
|
|
&doc_params(globals.clone()),
|
|
self.list_parent.id,
|
|
&entry,
|
|
None,
|
|
)?;
|
|
|
|
button.on_click(on_app_click(
|
|
frontend.tasks.clone(),
|
|
tasks.clone(),
|
|
globals.clone(),
|
|
entry.clone(),
|
|
rc_state.clone(),
|
|
None,
|
|
));
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|