dash-frontend: App pinning (wip)

This commit is contained in:
Aleksander 2026-07-07 00:06:12 +02:00
parent cfeef62215
commit 81fbc65b51
11 changed files with 334 additions and 158 deletions

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M18.288 16.713Q18 16.425 18 16v-2h-2q-.425 0-.712-.288T15 13t.288-.712T16 12h2v-2q0-.425.288-.712T19 9t.713.288T20 10v2h2q.425 0 .713.288T23 13t-.288.713T22 14h-2v2q0 .425-.288.713T19 17t-.712-.288M1.75 23.626v-3.8zM10.3 20.2q-.35-.125-.625-.4q-1.1-1-2.6-2.275t-2.85-2.737t-2.287-3.063T1 8.475q0-2.35 1.575-3.912T6.5 3q1.3 0 2.475.538T11 5.075q.85-1 2.025-1.537T15.5 3q1.575 0 2.788.738T20.2 5.6q.275.475-.087.963T19.075 7h-.4q-2.125 0-3.9 1.713T13 13q0 1.05.338 1.988T14.4 16.7q.275.3.238.7t-.338.675l-1.975 1.75q-.275.275-.625.388t-.7.112t-.7-.125"/></svg>

After

Width:  |  Height:  |  Size: 785 B

View File

@ -1,5 +1,6 @@
<layout>
<include src="../theme.xml" />
<include src="../t_group_box.xml" />
<template name="AppEntry">
<Button
@ -39,6 +40,15 @@
</rectangle>
</div>
-->
<rectangle macro="group_box">
<div align_items="center" gap="8">
<sprite src_builtin="dashboard/heart_plus.svg" min_width="24" min_height="24"/>
<label translation="PINNED_APPS"/>
</div>
<div id="pinned_apps_parent" flex_direction="row" flex_wrap="wrap" gap="4">
<!-- filled-in at runtime -->
</div>
</rectangle>
<div
id="app_list_parent"
flex_direction="row"
@ -46,4 +56,4 @@
gap="4"
/>
</elements>
</layout>
</layout>

View File

@ -18,7 +18,7 @@
<rectangle macro="group_box" id="icon_parent" padding="16" color="#0033aa66" color2="#00000022" gradient="vertical" justify_content="center">
</rectangle>
<div flex_direction="column" gap="8" flex_grow="1" max_width="720">
<div flex_direction="column" gap="8" flex_grow="1">
<label id="label_title" weight="bold" size="32" overflow="hidden" />
<Subtext label_id="label_exec" overflow="hidden" />
<Separator />
@ -51,9 +51,13 @@
<RadioBox translation="APP_LAUNCHER.POS.STATIC" value="Static" tooltip_str="APP_LAUNCHER.POS.STATIC_HELP" />
</RadioGroup -->
<Separator />
<div flex_direction="row" justify_content="space_between" gap="16">
<CheckBox id="cb_autostart" translation="APP_LAUNCHER.AUTOSTART" />
<Button id="btn_launch" align_self="baseline" color="#44ce22FF" padding_top="4" padding_bottom="4" round="8" padding_right="12" min_height="40">
<CheckBox id="cb_autostart" translation="APP_LAUNCHER.AUTOSTART" align_self="baseline"/>
<div flex_direction="row" justify_content="end" gap="8" margin_top="8">
<Button id="btn_pin" color="#ce4477" padding_left="8" padding_right="8" round="8" min_height="40">
<sprite src_builtin="dashboard/heart_plus.svg" width="32" height="32" />
<label text="Add to favourites" weight="bold" size="17" shadow="#00000099" />
</Button>
<Button id="btn_launch" color="#44ce22" padding_left="8" padding_right="8" rround="8" min_height="40">
<sprite src_builtin="dashboard/play.svg" width="32" height="32" />
<label translation="APP_LAUNCHER.LAUNCH" weight="bold" size="17" shadow="#00000099" />
</Button>
@ -61,4 +65,4 @@
</div>
</div>
</elements>
</layout>
</layout>

View File

@ -298,5 +298,8 @@
"TARGET_PATH": "Target path",
"TERMINATE_PROCESS": "Terminate process",
"VERSION": "Version",
"WIDTH": "Width"
"WIDTH": "Width",
"SAVED_TO_FAVOURITES": "Saved to favourites",
"EMPTY": "Empty",
"PINNED_APPS": "Pinned applications"
}

Binary file not shown.

View File

@ -100,6 +100,7 @@ pub struct InitParams<'a, T> {
pub enum SoundType {
Startup,
Launch,
Save,
}
#[derive(Clone)]
@ -216,6 +217,7 @@ impl<T: 'static> Frontend<T> {
let path = match sound_type {
SoundType::Startup => "sound/startup.mp3",
SoundType::Launch => "sound/app_start.mp3",
SoundType::Save => "sound/save.mp3",
};
// try loading a custom sound; if one doesn't exist (or it failed to load), use the built-in asset

View File

@ -3,29 +3,39 @@ 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::desktop_finder::DesktopEntry;
use crate::{
frontend::{Frontend, FrontendTasks},
tab::{Tab, TabType},
util::popup_manager::PopupHolder,
util::{popup_manager::PopupHolder, wgui_simple},
views::{self},
};
struct State {
#[allow(dead_code)]
parser_state: ParserState,
view_launcher: PopupHolder<views::app_launcher::View>,
}
pub struct TabApps<T> {
#[allow(dead_code)]
parser_state: ParserState,
#[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> {
@ -34,9 +44,15 @@ impl<T> Tab<T> for TabApps<T> {
}
fn update(&mut self, frontend: &mut Frontend<T>, _time_ms: u32, data: &mut T) -> anyhow::Result<()> {
let state = self.state.borrow_mut();
let mut state = self.state.borrow_mut();
self.app_list.tick(frontend, &self.state, &mut self.parser_state)?;
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
@ -45,8 +61,67 @@ impl<T> Tab<T> for TabApps<T> {
}
}
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 mut stale_entries = Vec::<Rc<str>>::new();
let mut pinned_desktop_entries = Vec::<&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(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 desktop_entry in pinned_desktop_entries {
mount_entry(
frontend,
&mut state.parser_state,
&doc_params(frontend.globals.clone()),
self.pinned_apps_parent,
desktop_entry,
)?;
}
Ok(())
}
}
struct AppList {
//data: Vec<ParserData>,
all_entries: Vec<DesktopEntry>,
entries_to_mount: VecDeque<DesktopEntry>,
list_parent: WidgetPair,
prev_category_name: String,
@ -55,6 +130,7 @@ struct AppList {
// 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>>,
@ -65,6 +141,7 @@ fn on_app_click(
globals.clone(),
entry.clone(),
state.borrow_mut().view_launcher.clone(),
tasks.make_callback_box(Task::RefreshPinnedApps),
);
Ok(())
})
@ -81,20 +158,24 @@ fn doc_params(globals: WguiGlobals) -> ParseDocumentParams<'static> {
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 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 mut entries_sorted: Vec<_> = frontend
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);
@ -102,16 +183,21 @@ impl<T> TabApps<T> {
});
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,
parser_state,
state,
tasks,
marker: PhantomData,
pinned_apps_parent: pinned_apps_parent.id,
})
}
}
@ -225,81 +311,86 @@ fn get_category_name(entry: &DesktopEntry) -> &str {
}
}
impl AppList {
fn mount_entry<T>(
&mut self,
frontend: &mut Frontend<T>,
parser_state: &mut ParserState,
doc_params: &ParseDocumentParams,
entry: &DesktopEntry,
) -> anyhow::Result<Rc<ComponentButton>> {
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);
fn mount_entry<T>(
frontend: &mut Frontend<T>,
parser_state: &mut ParserState,
doc_params: &ParseDocumentParams,
id_parent: WidgetID,
entry: &DesktopEntry,
) -> anyhow::Result<Rc<ComponentButton>> {
{
let mut params = TemplateParams::new();
parser_state.realize_template(
doc_params,
"CategoryText",
&mut frontend.layout,
self.list_parent.id,
params,
)?;
}
// entry icon
params.insert_rc(
"src_ext",
entry
.icon_path
.as_ref()
.map_or_else(|| "".into(), |icon_path| icon_path.clone()),
);
{
let mut params = TemplateParams::new();
// entry fallback (question mark) icon
params.insert(
"src",
if entry.icon_path.is_none() {
"dashboard/terminal.svg"
} else {
""
},
);
params.insert("name", &entry.app_name);
// entry icon
params.insert_rc(
"src_ext",
entry
.icon_path
.as_ref()
.map_or_else(|| "".into(), |icon_path| icon_path.clone()),
);
let data = parser_state.realize_template(doc_params, "AppEntry", &mut frontend.layout, id_parent, params)?;
// 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,
self.list_parent.id,
params,
)?;
data.fetch_component_as::<ComponentButton>("button")
}
data.fetch_component_as::<ComponentButton>("button")
}
}
impl AppList {
fn tick<T>(
&mut self,
frontend: &mut Frontend<T>,
state: &Rc<RefCell<State>>,
parser_state: &mut ParserState,
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 button = self.mount_entry(frontend, parser_state, &doc_params(globals.clone()), &entry)?;
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,
)?;
button.on_click(on_app_click(
frontend.tasks.clone(),
tasks.clone(),
globals.clone(),
entry.clone(),
state.clone(),
rc_state.clone(),
));
} else {
break;

View File

@ -1,6 +1,5 @@
use std::{collections::HashMap, rc::Rc, str::FromStr};
use strum::{AsRefStr, EnumString, VariantNames};
use strum::VariantNames;
use wayvr_ipc::packet_client::{PositionMode, WvrProcessLaunchParams};
use wgui::{
assets::AssetPath,
@ -12,7 +11,11 @@ use wgui::{
task::Tasks,
widget::label::WidgetLabel,
};
use wlx_common::{config::GeneralConfig, dash_interface::BoxDashInterface, desktop_finder::DesktopEntry};
use wlx_common::{
config::{AppCompositorMode, AppOrientationMode, AppPosMode, AppResMode, GeneralConfig, PinnedApp},
dash_interface::BoxDashInterface,
desktop_finder::DesktopEntry,
};
use crate::{
frontend::{FrontendTask, FrontendTasks, SoundType},
@ -20,51 +23,22 @@ use crate::{
views::{ViewTrait, ViewUpdateParams},
};
#[derive(Clone, Copy, Eq, PartialEq, EnumString, VariantNames, AsRefStr)]
enum PosMode {
Floating,
Anchored,
Static,
}
#[derive(Clone, Copy, Eq, PartialEq, EnumString, VariantNames, AsRefStr)]
enum ResMode {
Res1440,
Res1080,
Res720,
Res480,
}
#[derive(Clone, Copy, Eq, PartialEq, EnumString, VariantNames, AsRefStr)]
enum OrientationMode {
Wide,
SemiWide,
Square,
SemiTall,
Tall,
}
#[derive(Clone, Copy, Eq, PartialEq, EnumString, VariantNames, AsRefStr)]
enum CompositorMode {
Cage,
Native,
}
#[derive(Clone)]
enum Task {
SetCompositor(CompositorMode),
SetRes(ResMode),
SetOrientation(OrientationMode),
SetCompositor(AppCompositorMode),
SetRes(AppResMode),
SetOrientation(AppOrientationMode),
SetAutoStart(bool),
Launch,
PinApp,
}
struct LaunchParams<'a, T> {
application: &'a DesktopEntry,
compositor_mode: CompositorMode,
pos_mode: PosMode,
res_mode: ResMode,
orientation_mode: OrientationMode,
compositor_mode: AppCompositorMode,
pos_mode: AppPosMode,
res_mode: AppResMode,
orientation_mode: AppOrientationMode,
globals: &'a WguiGlobals,
frontend_tasks: &'a FrontendTasks,
interface: &'a mut BoxDashInterface<T>,
@ -88,14 +62,15 @@ pub struct View {
#[allow(dead_code)]
radio_orientation: Rc<ComponentRadioGroup>,
compositor_mode: CompositorMode,
pos_mode: PosMode,
res_mode: ResMode,
orientation_mode: OrientationMode,
compositor_mode: AppCompositorMode,
pos_mode: AppPosMode,
res_mode: AppResMode,
orientation_mode: AppOrientationMode,
auto_start: bool,
on_launched: Option<Box<dyn FnOnce()>>,
on_close_request: Option<Box<dyn FnOnce()>>,
on_app_pinned: Option<Box<dyn Fn()>>,
}
pub struct Params<'a> {
@ -105,7 +80,8 @@ pub struct Params<'a> {
pub parent_id: WidgetID,
pub config: &'a GeneralConfig,
pub frontend_tasks: &'a FrontendTasks,
pub on_launched: Box<dyn FnOnce()>,
pub on_close_request: Box<dyn FnOnce()>,
pub on_app_pinned: Box<dyn Fn()>,
}
impl ViewTrait for View {
@ -131,6 +107,7 @@ impl View {
let cb_autostart = state.fetch_component_as::<ComponentCheckbox>("cb_autostart")?;
let btn_launch = state.fetch_component_as::<ComponentButton>("btn_launch")?;
let btn_pin = state.fetch_component_as::<ComponentButton>("btn_pin")?;
{
let mut label_exec = state.fetch_widget_as::<WidgetLabel>(&params.layout.state, "label_exec")?;
@ -144,6 +121,7 @@ impl View {
let tasks = Tasks::new();
tasks.handle_button(&btn_launch, Task::Launch);
tasks.handle_button(&btn_pin, Task::PinApp);
let id_icon_parent = state.get_widget_id("icon_parent")?;
@ -161,24 +139,24 @@ impl View {
}
let compositor_mode = if params.config.xwayland_by_default {
CompositorMode::Cage
AppCompositorMode::Cage
} else {
CompositorMode::Native
AppCompositorMode::Native
};
radio_compositor.set_value_simple(compositor_mode.as_ref())?;
tasks.push(Task::SetCompositor(compositor_mode));
let res_mode = ResMode::Res1080;
let res_mode = AppResMode::Res1080;
// TODO: configurable defaults ?
//radio_res.set_value(res_mode.as_ref())?;
//tasks.push(Task::SetRes(res_mode));
let orientation_mode = OrientationMode::Wide;
let orientation_mode = AppOrientationMode::Wide;
// TODO: configurable defaults ?
//radio_orientation.set_value(orientation_mode.as_ref())?;
//tasks.push(Task::SetOrientation(orientation_mode));
let pos_mode = PosMode::Anchored;
let pos_mode = AppPosMode::Anchored;
// TODO: configurable defaults ?
//radio_pos.set_value(pos_mode.as_ref())?;
//tasks.push(Task::SetPos(pos_mode));
@ -189,11 +167,11 @@ impl View {
let tasks = tasks.clone();
Box::new(move |_, ev| {
if let Some(mode) = ev.value.and_then(|v| {
CompositorMode::from_str(&v)
AppCompositorMode::from_str(&v)
.inspect_err(|_| {
log::error!(
"Invalid value for compositor: '{v}'. Valid values are: {:?}",
ResMode::VARIANTS
AppResMode::VARIANTS
)
})
.ok()
@ -208,11 +186,11 @@ impl View {
let tasks = tasks.clone();
Box::new(move |_, ev| {
if let Some(mode) = ev.value.and_then(|v| {
ResMode::from_str(&v)
AppResMode::from_str(&v)
.inspect_err(|_| {
log::error!(
"Invalid value for resolution: '{v}'. Valid values are: {:?}",
ResMode::VARIANTS
AppResMode::VARIANTS
)
})
.ok()
@ -246,11 +224,11 @@ impl View {
let tasks = tasks.clone();
Box::new(move |_, ev| {
if let Some(mode) = ev.value.and_then(|v| {
OrientationMode::from_str(&v)
AppOrientationMode::from_str(&v)
.inspect_err(|_| {
log::error!(
"Invalid value for orientation: '{v}'. Valid values are: {:?}",
OrientationMode::VARIANTS
AppOrientationMode::VARIANTS
)
})
.ok()
@ -290,7 +268,8 @@ impl View {
entry: params.entry,
frontend_tasks: params.frontend_tasks.clone(),
globals: params.globals.clone(),
on_launched: Some(params.on_launched),
on_close_request: Some(params.on_close_request),
on_app_pinned: Some(params.on_app_pinned),
})
}
@ -307,6 +286,10 @@ impl View {
Task::SetOrientation(mode) => self.orientation_mode = mode,
Task::SetAutoStart(auto_start) => self.auto_start = auto_start,
Task::Launch => self.action_launch(interface, data),
Task::PinApp => {
self.action_pin_app(interface.general_config(data));
interface.config_changed(data, Default::default());
}
}
}
}
@ -314,6 +297,35 @@ impl View {
Ok(())
}
fn close(&mut self) {
if let Some(c) = self.on_close_request.take() {
c();
}
}
fn action_pin_app(&mut self, config: &mut GeneralConfig) {
self
.frontend_tasks
.push(FrontendTask::PushToast(Translation::from_translation_key(
"SAVED_TO_FAVOURITES",
)));
self.frontend_tasks.push(FrontendTask::PlaySound(SoundType::Save));
config.pinned_apps.push(PinnedApp {
app_id: self.entry.app_id.clone(),
compositor_mode: self.compositor_mode,
pos_mode: self.pos_mode,
orientation_mode: self.orientation_mode,
res_mode: self.res_mode,
});
if let Some(c) = &self.on_app_pinned {
c();
}
self.close();
}
fn action_launch<T>(&mut self, interface: &mut BoxDashInterface<T>, data: &mut T) {
View::try_launch(LaunchParams {
application: &self.entry,
@ -326,7 +338,7 @@ impl View {
auto_start: self.auto_start,
interface,
data,
on_launched: self.on_launched.take(),
on_launched: self.on_close_request.take(),
});
}
@ -347,7 +359,7 @@ impl View {
fn launch<T>(mut params: LaunchParams<T>) -> anyhow::Result<()> {
let mut env = Vec::<String>::new();
if params.compositor_mode == CompositorMode::Native {
if params.compositor_mode == AppCompositorMode::Native {
// This list could be larger, feel free to expand it
env.push("QT_QPA_PLATFORM=wayland".into());
env.push("GDK_BACKEND=wayland".into());
@ -357,19 +369,19 @@ impl View {
}
let args = match params.compositor_mode {
CompositorMode::Cage => format!("-- {} {}", params.application.exec_path, params.application.exec_args),
CompositorMode::Native => params.application.exec_args.to_string(),
AppCompositorMode::Cage => format!("-- {} {}", params.application.exec_path, params.application.exec_args),
AppCompositorMode::Native => params.application.exec_args.to_string(),
};
let exec = match params.compositor_mode {
CompositorMode::Cage => "cage".to_string(),
CompositorMode::Native => params.application.exec_path.to_string(),
AppCompositorMode::Cage => "cage".to_string(),
AppCompositorMode::Native => params.application.exec_path.to_string(),
};
let pos_mode = match params.pos_mode {
PosMode::Floating => PositionMode::Float,
PosMode::Anchored => PositionMode::Anchor,
PosMode::Static => PositionMode::Static,
AppPosMode::Floating => PositionMode::Float,
AppPosMode::Anchored => PositionMode::Anchor,
AppPosMode::Static => PositionMode::Static,
};
let mut userdata = HashMap::new();
@ -408,20 +420,20 @@ impl View {
Ok(())
}
fn calculate_resolution(res_mode: ResMode, orientation_mode: OrientationMode) -> [u32; 2] {
fn calculate_resolution(res_mode: AppResMode, orientation_mode: AppOrientationMode) -> [u32; 2] {
let total_pixels = match res_mode {
ResMode::Res1440 => 2560 * 1440,
ResMode::Res1080 => 1920 * 1080,
ResMode::Res720 => 1280 * 720,
ResMode::Res480 => 854 * 480,
AppResMode::Res1440 => 2560 * 1440,
AppResMode::Res1080 => 1920 * 1080,
AppResMode::Res720 => 1280 * 720,
AppResMode::Res480 => 854 * 480,
};
let (ratio_w, ratio_h) = match orientation_mode {
OrientationMode::Wide => (16, 9),
OrientationMode::SemiWide => (3, 2),
OrientationMode::Square => (1, 1),
OrientationMode::SemiTall => (2, 3),
OrientationMode::Tall => (9, 16),
AppOrientationMode::Wide => (16, 9),
AppOrientationMode::SemiWide => (3, 2),
AppOrientationMode::Square => (1, 1),
AppOrientationMode::SemiTall => (2, 3),
AppOrientationMode::Tall => (9, 16),
};
let k = ((total_pixels as f64) / (ratio_w * ratio_h) as f64).sqrt();
@ -433,13 +445,19 @@ impl View {
}
}
pub fn mount_popup(frontend_tasks: FrontendTasks, globals: WguiGlobals, entry: DesktopEntry, popup: PopupHolder<View>) {
pub fn mount_popup(
frontend_tasks: FrontendTasks,
globals: WguiGlobals,
entry: DesktopEntry,
popup: PopupHolder<View>,
on_app_pinned: Box<dyn Fn()>,
) {
frontend_tasks
.clone()
.push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new(
Translation::from_raw_text(&entry.app_name),
Box::new(move |data| {
let on_launched = popup.get_close_callback(data.layout);
let on_close_request = popup.get_close_callback(data.layout);
let view = View::new(Params {
entry: entry.clone(),
globals: &globals,
@ -447,7 +465,8 @@ pub fn mount_popup(frontend_tasks: FrontendTasks, globals: WguiGlobals, entry: D
parent_id: data.id_content,
frontend_tasks: &frontend_tasks,
config: data.config,
on_launched,
on_close_request,
on_app_pinned,
})?;
popup.set_view(data.handle, view, None);

View File

@ -1,9 +1,9 @@
use std::{collections::HashMap, sync::Arc};
use std::{collections::HashMap, rc::Rc, sync::Arc};
use chrono::Offset;
use idmap::IdMap;
use serde::{Deserialize, Serialize};
use strum::{AsRefStr, EnumProperty, EnumString, VariantArray};
use strum::{AsRefStr, EnumProperty, EnumString, VariantArray, VariantNames};
use wayvr_ipc::packet_client::WvrProcessLaunchParams;
use wgui::drawing::{self, HsvColor};
@ -151,6 +151,44 @@ pub struct SerializedWindowSet {
pub hidden_overlays: SerializedWindowStates,
}
#[derive(Serialize, Deserialize, Clone, Copy, Eq, PartialEq, EnumString, VariantNames, AsRefStr)]
pub enum AppPosMode {
Floating,
Anchored,
Static,
}
#[derive(Serialize, Deserialize, Clone, Copy, Eq, PartialEq, EnumString, VariantNames, AsRefStr)]
pub enum AppOrientationMode {
Wide,
SemiWide,
Square,
SemiTall,
Tall,
}
#[derive(Serialize, Deserialize, Clone, Copy, Eq, PartialEq, EnumString, VariantNames, AsRefStr)]
pub enum AppCompositorMode {
Cage,
Native,
}
#[derive(Serialize, Deserialize, Clone, Copy, Eq, PartialEq, EnumString, VariantNames, AsRefStr)]
pub enum AppResMode {
Res1440,
Res1080,
Res720,
Res480,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct PinnedApp {
pub app_id: Rc<str>, // desktop entry app id, for example "libreoffice-draw"
pub compositor_mode: AppCompositorMode,
pub pos_mode: AppPosMode,
pub orientation_mode: AppOrientationMode,
pub res_mode: AppResMode,
}
pub const fn def_pw_tokens() -> PwTokenMap {
AStrMap::new()
}
@ -401,6 +439,9 @@ pub struct GeneralConfig {
#[serde(default)]
pub autostart_apps: Vec<WvrProcessLaunchParams>,
#[serde(default)]
pub pinned_apps: Vec<PinnedApp>,
#[serde(default)]
pub last_set: u32,

View File

@ -78,11 +78,12 @@ pub trait DashInterface<T> {
fn get_feats(&mut self, data: &mut T) -> InterfaceFeats;
}
#[derive(Clone, Copy)]
#[derive(Default, Clone, Copy)]
pub enum ConfigChangeKind {
OverlayConfig,
EnvironmentBlend,
/// Marks the config for saving but doesn't notify any components
#[default]
Other,
}

View File

@ -17,6 +17,7 @@ use serde::{Deserialize, Serialize};
use walkdir::WalkDir;
struct DesktopEntryOwned {
app_id: String,
exec_path: String,
exec_args: String,
app_name: String,
@ -28,6 +29,7 @@ struct DesktopEntryOwned {
pub struct DesktopEntry {
pub exec_path: Rc<str>,
pub exec_args: Rc<str>,
pub app_id: Rc<str>,
pub app_name: Rc<str>,
pub icon_path: Option<Rc<str>>,
pub categories: Vec<Rc<str>>,
@ -36,6 +38,7 @@ pub struct DesktopEntry {
impl From<DesktopEntryOwned> for DesktopEntry {
fn from(value: DesktopEntryOwned) -> Self {
Self {
app_id: value.app_id.into(),
exec_path: value.exec_path.into(),
exec_args: value.exec_args.into(),
app_name: value.app_name.into(),
@ -242,8 +245,9 @@ impl DesktopFinder {
known_files.insert(file_name.to_string());
entries.insert(
app_id,
app_id.clone(),
DesktopEntryOwned {
app_id,
app_name: String::from(app_name),
exec_path: String::from(exec_path),
exec_args: exec_args.join(" "),