App launcher view

This commit is contained in:
Aleksander
2025-11-30 15:28:05 +01:00
parent e8528735c7
commit 6b4039a764
11 changed files with 302 additions and 89 deletions

View File

@@ -2,8 +2,6 @@
<include src="theme.xml" /> <include src="theme.xml" />
<macro name="group_box" <macro name="group_box"
min_width="200"
flex_grow="1"
flex_direction="column" flex_direction="column"
align_items="baseline" align_items="baseline"
border="2" border="2"

View File

@@ -0,0 +1,5 @@
<layout>
<template name="Separator">
<rectangle width="100%" height="1" color="#FFFFFF77" />
</template>
</layout>

View File

@@ -1,7 +1,40 @@
<layout> <layout>
<template name="Subtext">
<div flex_direction="row" gap="8">
<label weight="bold" text="${title}" />
<label text="foo" />
</div>
</template>
<template name="ApplicationIcon">
<sprite src_ext="${path}" width="128" height="128" />
</template>
<include src="../t_separator.xml" />
<include src="../t_group_box.xml" />
<elements> <elements>
<rectangle new_pass="1" color="#ff000099" width="100%" height="100%"> <div flex_direction="row" gap="16" width="100%">
<label text="TEST" size="80" /> <rectangle macro="group_box" id="icon_parent" height="100%" padding="8" color="#0033aa66" color2="#00000022" gradient="vertical" justify_content="center">
</rectangle> </rectangle>
<div flex_direction="column" gap="8" width="100%" align_items="baseline">
<label id="label_title" weight="bold" size="32" />
<Subtext title="Exec:" />
<Subtext title="Args:" />
<Separator />
<CheckBox text="Run in X11 mode (cage)" />
<CheckBox text="Run in Wayland mode" checked="1" />
<Separator />
<Button color="#44ce22FF" padding_top="4" padding_bottom="4" round="8" padding_right="12">
<sprite src="dashboard/play.svg" width="32" height="32" />
<label text="Launch embedded" weight="bold" size="17" shadow="#00000099" />
</Button>
<Separator />
<rectangle macro="group_box">
<label size="16" weight="bold" text="Or launch it detached" />
</rectangle>
</div>
</div>
</elements> </elements>
</layout> </layout>

View File

@@ -28,14 +28,14 @@
</Button> </Button>
<!-- Title --> <!-- Title -->
<label id="popup_title" weight="bold" size="18" text="Pop-up title" /> <label id="popup_title" weight="bold" size="18" />
</div> </div>
</rectangle> </rectangle>
<!-- Content --> <!-- Content -->
<rectangle width="100%" height="100%" <rectangle width="100%" height="100%"
color="#010310cc" color="#010310ee"
color2="#061e4acc" color2="#062a5eee"
gradient="vertical" gradient="vertical"
padding="16" padding="16"
id="content"> id="content">

View File

@@ -19,7 +19,7 @@ use crate::{
Tab, TabParams, TabType, apps::TabApps, games::TabGames, home::TabHome, monado::TabMonado, processes::TabProcesses, Tab, TabParams, TabType, apps::TabApps, games::TabGames, home::TabHome, monado::TabMonado, processes::TabProcesses,
settings::TabSettings, settings::TabSettings,
}, },
util::popup_manager::{PopupManager, PopupManagerParams}, util::popup_manager::{MountPopupParams, PopupManager, PopupManagerParams},
}; };
pub struct FrontendWidgets { pub struct FrontendWidgets {
@@ -27,6 +27,19 @@ pub struct FrontendWidgets {
pub id_rect_content: WidgetID, pub id_rect_content: WidgetID,
} }
#[derive(Clone)]
pub struct FrontendTasks(pub Rc<RefCell<VecDeque<FrontendTask>>>);
impl FrontendTasks {
fn new() -> Self {
Self(Rc::new(RefCell::new(VecDeque::new())))
}
pub fn push(&self, task: FrontendTask) {
self.0.borrow_mut().push_back(task);
}
}
pub struct Frontend { pub struct Frontend {
pub layout: RcLayout, pub layout: RcLayout,
globals: WguiGlobals, globals: WguiGlobals,
@@ -38,7 +51,7 @@ pub struct Frontend {
current_tab: Option<Box<dyn Tab>>, current_tab: Option<Box<dyn Tab>>,
tasks: VecDeque<FrontendTask>, pub tasks: FrontendTasks,
ticks: u32, ticks: u32,
@@ -56,7 +69,8 @@ pub enum FrontendTask {
SetTab(TabType), SetTab(TabType),
RefreshClock, RefreshClock,
RefreshBackground, RefreshBackground,
MountPopup, MountPopup(MountPopupParams),
RefreshPopupManager,
} }
impl Frontend { impl Frontend {
@@ -96,8 +110,8 @@ impl Frontend {
let rc_layout = layout.as_rc(); let rc_layout = layout.as_rc();
let mut tasks = VecDeque::<FrontendTask>::new(); let tasks = FrontendTasks::new();
tasks.push_back(FrontendTask::SetTab(TabType::Home)); tasks.push(FrontendTask::SetTab(TabType::Home));
let id_label_time = state.get_widget_id("label_time")?; let id_label_time = state.get_widget_id("label_time")?;
let id_rect_content = state.get_widget_id("rect_content")?; let id_rect_content = state.get_widget_id("rect_content")?;
@@ -129,7 +143,12 @@ impl Frontend {
} }
pub fn update(&mut self, rc_this: &RcFrontend, width: f32, height: f32, timestep_alpha: f32) -> anyhow::Result<()> { pub fn update(&mut self, rc_this: &RcFrontend, width: f32, height: f32, timestep_alpha: f32) -> anyhow::Result<()> {
while let Some(task) = self.tasks.pop_front() { let mut tasks = {
let mut tasks = self.tasks.0.borrow_mut();
std::mem::take(&mut *tasks)
};
while let Some(task) = tasks.pop_front() {
self.process_task(rc_this, task)?; self.process_task(rc_this, task)?;
} }
@@ -182,11 +201,19 @@ impl Frontend {
Ok(()) Ok(())
} }
fn mount_popup(&mut self) -> anyhow::Result<()> { fn mount_popup(&mut self, params: MountPopupParams) -> anyhow::Result<()> {
let mut layout = self.layout.borrow_mut(); let mut layout = self.layout.borrow_mut();
self
.popup_manager
.mount_popup(self.globals.clone(), &mut layout, self.tasks.clone(), params)?;
Ok(())
}
self.popup_manager.push_popup(self.globals.clone(), &mut layout)?; fn refresh_popup_manager(&mut self) -> anyhow::Result<()> {
let mut layout = self.layout.borrow_mut();
let mut c = layout.start_common();
self.popup_manager.refresh(c.common().alterables);
c.finish()?;
Ok(()) Ok(())
} }
@@ -217,16 +244,13 @@ impl Frontend {
&self.layout &self.layout
} }
pub fn push_task(&mut self, task: FrontendTask) {
self.tasks.push_back(task);
}
fn process_task(&mut self, rc_this: &RcFrontend, task: FrontendTask) -> anyhow::Result<()> { fn process_task(&mut self, rc_this: &RcFrontend, task: FrontendTask) -> anyhow::Result<()> {
match task { match task {
FrontendTask::SetTab(tab_type) => self.set_tab(tab_type, rc_this)?, FrontendTask::SetTab(tab_type) => self.set_tab(tab_type, rc_this)?,
FrontendTask::RefreshClock => self.update_time()?, FrontendTask::RefreshClock => self.update_time()?,
FrontendTask::RefreshBackground => self.update_background()?, FrontendTask::RefreshBackground => self.update_background()?,
FrontendTask::MountPopup => self.mount_popup()?, FrontendTask::MountPopup(params) => self.mount_popup(params)?,
FrontendTask::RefreshPopupManager => self.refresh_popup_manager()?,
} }
Ok(()) Ok(())
} }

View File

@@ -1,22 +1,34 @@
use std::{collections::HashMap, rc::Rc}; use std::{cell::RefCell, collections::HashMap, rc::Rc};
use wgui::{ use wgui::{
assets::AssetPath, assets::AssetPath,
components::button::ComponentButton, components::button::{ButtonClickCallback, ComponentButton},
globals::WguiGlobals,
i18n::Translation,
layout::WidgetPair, layout::WidgetPair,
parser::{Fetchable, ParseDocumentParams, ParserData, ParserState}, parser::{Fetchable, ParseDocumentParams, ParserData, ParserState},
}; };
use crate::{ use crate::{
frontend::FrontendTask, frontend::{FrontendTask, RcFrontend},
tab::{Tab, TabParams, TabType}, tab::{Tab, TabParams, TabType},
util::{self, desktop_finder::DesktopEntry}, util::{
views, self,
desktop_finder::DesktopEntry,
popup_manager::{MountPopupParams, PopupHandle},
},
views::{self, app_launcher},
}; };
struct State {
launcher: Option<(PopupHandle, views::app_launcher::View)>,
}
pub struct TabApps { pub struct TabApps {
#[allow(dead_code)] #[allow(dead_code)]
pub state: ParserState, pub parser_state: ParserState,
state: Rc<RefCell<State>>,
#[allow(dead_code)] #[allow(dead_code)]
entries: Vec<DesktopEntry>, entries: Vec<DesktopEntry>,
@@ -35,6 +47,40 @@ struct AppList {
data: Vec<ParserData>, data: Vec<ParserData>,
} }
// called after the user clicks any desktop entry
fn on_app_click(
frontend: RcFrontend,
globals: WguiGlobals,
entry: DesktopEntry,
state: Rc<RefCell<State>>,
) -> ButtonClickCallback {
Box::new(move |_common, _evt| {
frontend
.borrow_mut()
.tasks
.push(FrontendTask::MountPopup(MountPopupParams {
title: Translation::from_raw_text(&entry.app_name),
on_content: {
let state = state.clone();
let entry = entry.clone();
let globals = globals.clone();
Box::new(move |data| {
let view = app_launcher::View::new(app_launcher::Params {
entry: entry.clone(),
globals: globals.clone(),
layout: data.layout,
parent_id: data.id_content,
})?;
state.borrow_mut().launcher = Some((data.handle, view));
Ok(())
})
},
}));
Ok(())
})
}
impl TabApps { impl TabApps {
pub fn new(mut tab_params: TabParams) -> anyhow::Result<Self> { pub fn new(mut tab_params: TabParams) -> anyhow::Result<Self> {
let doc_params = &ParseDocumentParams { let doc_params = &ParseDocumentParams {
@@ -43,21 +89,39 @@ impl TabApps {
extra: Default::default(), extra: Default::default(),
}; };
let mut state = wgui::parser::parse_from_assets(doc_params, tab_params.layout, tab_params.parent_id)?;
gtk::init()?; gtk::init()?;
let entries = util::desktop_finder::find_entries()?; let entries = util::desktop_finder::find_entries()?;
let app_list_parent = state.fetch_widget(&tab_params.layout.state, "app_list_parent")?; let frontend = tab_params.frontend.clone();
let globals = tab_params.globals.clone();
let state = Rc::new(RefCell::new(State { launcher: None }));
let mut parser_state = wgui::parser::parse_from_assets(doc_params, tab_params.layout, tab_params.parent_id)?;
let app_list_parent = parser_state.fetch_widget(&tab_params.layout.state, "app_list_parent")?;
let mut app_list = AppList::default(); let mut app_list = AppList::default();
app_list.mount_entries(&entries, &mut state, doc_params, &mut tab_params, &app_list_parent)?; app_list.mount_entries(
&entries,
&mut parser_state,
doc_params,
&mut tab_params,
&app_list_parent,
|button, entry| {
// Set up the click handler for the app button
button.on_click(on_app_click(
frontend.clone(),
globals.clone(),
entry.clone(),
state.clone(),
));
},
)?;
Ok(Self { Ok(Self {
app_list, app_list,
state, parser_state,
entries, entries,
state,
}) })
} }
} }
@@ -70,7 +134,7 @@ impl AppList {
params: &mut TabParams, params: &mut TabParams,
list_parent: &WidgetPair, list_parent: &WidgetPair,
entry: &DesktopEntry, entry: &DesktopEntry,
) -> anyhow::Result<()> { ) -> anyhow::Result<Rc<ComponentButton>> {
let mut template_params = HashMap::new(); let mut template_params = HashMap::new();
// entry icon // entry icon
@@ -95,20 +159,7 @@ impl AppList {
template_params.insert(Rc::from("name"), Rc::from(entry.app_name.as_str())); template_params.insert(Rc::from("name"), Rc::from(entry.app_name.as_str()));
let data = parser_state.parse_template(doc_params, "AppEntry", params.layout, list_parent.id, template_params)?; let data = parser_state.parse_template(doc_params, "AppEntry", params.layout, list_parent.id, template_params)?;
data.fetch_component_as::<ComponentButton>("button")
let button = data.fetch_component_as::<ComponentButton>("button")?;
button.on_click({
let frontend = params.frontend.clone();
Box::new(move |_common, _evt| {
frontend.borrow_mut().push_task(FrontendTask::MountPopup);
Ok(())
})
});
self.data.push(data);
Ok(())
} }
fn mount_entries( fn mount_entries(
@@ -118,9 +169,11 @@ impl AppList {
doc_params: &ParseDocumentParams, doc_params: &ParseDocumentParams,
params: &mut TabParams, params: &mut TabParams,
list_parent: &WidgetPair, list_parent: &WidgetPair,
on_button: impl Fn(Rc<ComponentButton>, &DesktopEntry),
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
for entry in entries { for entry in entries {
self.mount_entry(parser_state, doc_params, params, list_parent, entry)?; let button = self.mount_entry(parser_state, doc_params, params, list_parent, entry)?;
on_button(button, entry);
} }
Ok(()) Ok(())
} }

View File

@@ -43,7 +43,7 @@ impl TabType {
pub fn register_button(this_rc: RcFrontend, btn: &Rc<ComponentButton>, tab: TabType) { pub fn register_button(this_rc: RcFrontend, btn: &Rc<ComponentButton>, tab: TabType) {
btn.on_click({ btn.on_click({
Box::new(move |_common, _evt| { Box::new(move |_common, _evt| {
this_rc.borrow_mut().push_task(FrontendTask::SetTab(tab)); this_rc.borrow_mut().tasks.push(FrontendTask::SetTab(tab));
Ok(()) Ok(())
}) })
}); });

View File

@@ -74,7 +74,7 @@ impl TabSettings {
state.data.fetch_component_as::<ComponentCheckbox>("cb_am_pm_clock")?, state.data.fetch_component_as::<ComponentCheckbox>("cb_am_pm_clock")?,
|settings| &mut settings.general.am_pm_clock, |settings| &mut settings.general.am_pm_clock,
Some(|frontend, _| { Some(|frontend, _| {
frontend.push_task(FrontendTask::RefreshClock); frontend.tasks.push(FrontendTask::RefreshClock);
}), }),
)?; )?;
@@ -85,7 +85,7 @@ impl TabSettings {
.fetch_component_as::<ComponentCheckbox>("cb_opaque_background")?, .fetch_component_as::<ComponentCheckbox>("cb_opaque_background")?,
|settings| &mut settings.general.opaque_background, |settings| &mut settings.general.opaque_background,
Some(|frontend, _| { Some(|frontend, _| {
frontend.push_task(FrontendTask::RefreshBackground); frontend.tasks.push(FrontendTask::RefreshBackground);
}), }),
)?; )?;

View File

@@ -8,25 +8,47 @@ use wgui::{
components::button::ComponentButton, components::button::ComponentButton,
event::{EventAlterables, StyleSetRequest}, event::{EventAlterables, StyleSetRequest},
globals::WguiGlobals, globals::WguiGlobals,
layout::{Layout, LayoutTask, WidgetID}, i18n::Translation,
layout::{Layout, LayoutTask, LayoutTasks, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState}, parser::{Fetchable, ParseDocumentParams, ParserState},
taffy::Display, taffy::Display,
widget::label::WidgetLabel,
}; };
use crate::frontend::{FrontendTask, FrontendTasks};
pub struct PopupManagerParams<'a> { pub struct PopupManagerParams<'a> {
pub globals: WguiGlobals, pub globals: WguiGlobals,
pub layout: &'a mut Layout, pub layout: &'a mut Layout,
pub parent_id: WidgetID, pub parent_id: WidgetID,
} }
struct MountedPopup { pub struct State {
#[allow(dead_code)] popup_stack: Vec<Weak<RefCell<MountedPopupState>>>,
state: ParserState,
id_root: WidgetID,
} }
pub struct State { pub struct MountedPopup {
popup_stack: Vec<MountedPopup>, #[allow(dead_code)]
state: ParserState,
id_root: WidgetID, // decorations of a popup
pub id_content: WidgetID, // content of a popup
layout_tasks: LayoutTasks,
frontend_tasks: FrontendTasks,
}
struct MountedPopupState {
mounted_popup: Option<MountedPopup>,
}
#[derive(Clone)]
pub struct PopupHandle {
state: Rc<RefCell<MountedPopupState>>,
}
impl PopupHandle {
pub fn close(&self) {
self.state.borrow_mut().mounted_popup = None; // Drop will be called
}
} }
pub struct PopupManager { pub struct PopupManager {
@@ -35,16 +57,41 @@ pub struct PopupManager {
parent_id: WidgetID, parent_id: WidgetID,
} }
pub struct PushPopupResult { pub struct PopupContentFuncData<'a> {
pub layout: &'a mut Layout,
pub handle: PopupHandle,
pub id_content: WidgetID, pub id_content: WidgetID,
} }
pub struct MountPopupParams {
pub title: Translation,
pub on_content: Box<dyn Fn(PopupContentFuncData) -> anyhow::Result<()>>,
}
impl Drop for MountedPopup {
fn drop(&mut self) {
self.layout_tasks.push(LayoutTask::RemoveWidget(self.id_root));
self.frontend_tasks.push(FrontendTask::RefreshPopupManager);
}
}
impl State { impl State {
fn refresh_stack(&mut self, alterables: &mut EventAlterables) { fn refresh_stack(&mut self, alterables: &mut EventAlterables) {
// show only the topmost popup // show only the topmost popup
self.popup_stack.retain(|weak| {
let Some(popup) = weak.upgrade() else {
return false;
};
popup.borrow_mut().mounted_popup.is_some()
});
for (idx, popup) in self.popup_stack.iter().enumerate() { for (idx, popup) in self.popup_stack.iter().enumerate() {
let popup = popup.upgrade().unwrap(); // safe
let popup = popup.borrow_mut();
let mounted_popup = popup.mounted_popup.as_ref().unwrap(); // safe;
alterables.set_style( alterables.set_style(
popup.id_root, mounted_popup.id_root,
StyleSetRequest::Display(if idx == self.popup_stack.len() - 1 { StyleSetRequest::Display(if idx == self.popup_stack.len() - 1 {
Display::Flex Display::Flex
} else { } else {
@@ -53,15 +100,6 @@ impl State {
); );
} }
} }
fn pop_popup(&mut self, alterables: &mut EventAlterables) {
let Some(popup) = self.popup_stack.pop() else {
return;
};
alterables.tasks.push(LayoutTask::RemoveWidget(popup.id_root));
self.refresh_stack(alterables);
}
} }
impl PopupManager { impl PopupManager {
@@ -75,9 +113,22 @@ impl PopupManager {
}) })
} }
pub fn push_popup(&mut self, globals: WguiGlobals, layout: &mut Layout) -> anyhow::Result<PushPopupResult> { pub fn refresh(&self, alterables: &mut EventAlterables) {
let mut state = self.state.borrow_mut();
state.refresh_stack(alterables);
}
/// Mount a new popup on top of the existing popup stack.
/// Only the topmost popup is visible.
pub fn mount_popup(
&mut self,
globals: WguiGlobals,
layout: &mut Layout,
frontend_tasks: FrontendTasks,
params: MountPopupParams,
) -> anyhow::Result<()> {
let doc_params = &ParseDocumentParams { let doc_params = &ParseDocumentParams {
globals, globals: globals.clone(),
path: AssetPath::BuiltIn("gui/view/popup_window.xml"), path: AssetPath::BuiltIn("gui/view/popup_window.xml"),
extra: Default::default(), extra: Default::default(),
}; };
@@ -86,25 +137,51 @@ impl PopupManager {
let id_root = state.get_widget_id("root")?; let id_root = state.get_widget_id("root")?;
let id_content = state.get_widget_id("content")?; let id_content = state.get_widget_id("content")?;
{
let mut label_title = state.fetch_widget_as::<WidgetLabel>(&layout.state, "popup_title")?;
label_title.set_text_simple(&mut globals.get(), params.title);
}
let but_back = state.fetch_component_as::<ComponentButton>("but_back")?; let but_back = state.fetch_component_as::<ComponentButton>("but_back")?;
let mounted_popup = MountedPopup {
state,
id_content,
id_root,
layout_tasks: layout.tasks.clone(),
frontend_tasks: frontend_tasks.clone(),
};
let mounted_popup_state = MountedPopupState {
mounted_popup: Some(mounted_popup),
};
let popup_handle = PopupHandle {
state: Rc::new(RefCell::new(mounted_popup_state)),
};
let mut state = self.state.borrow_mut();
state.popup_stack.push(Rc::downgrade(&popup_handle.state));
but_back.on_click({ but_back.on_click({
let state = self.state.clone(); let popup_handle = Rc::downgrade(&popup_handle.state);
Box::new(move |common, _evt| { Box::new(move |_common, _evt| {
state.borrow_mut().pop_popup(common.alterables); if let Some(popup_handle) = popup_handle.upgrade() {
popup_handle.borrow_mut().mounted_popup = None; // will call Drop
}
Ok(()) Ok(())
}) })
}); });
let mounted_popup = MountedPopup { state, id_root }; frontend_tasks.push(FrontendTask::RefreshPopupManager);
let mut state = self.state.borrow_mut(); // mount user-set popup content
state.popup_stack.push(mounted_popup); (*params.on_content)(PopupContentFuncData {
layout,
handle: popup_handle.clone(),
id_content,
})?;
let mut c = layout.start_common(); Ok(())
state.refresh_stack(c.common().alterables);
c.finish()?;
Ok(PushPopupResult { id_content })
} }
} }

View File

@@ -1,8 +1,12 @@
use std::{collections::HashMap, rc::Rc};
use wgui::{ use wgui::{
assets::AssetPath, assets::AssetPath,
globals::WguiGlobals, globals::WguiGlobals,
i18n::Translation,
layout::{Layout, WidgetID}, layout::{Layout, WidgetID},
parser::{ParseDocumentParams, ParserState}, parser::{Fetchable, ParseDocumentParams, ParserState},
widget::label::WidgetLabel,
}; };
use crate::util::desktop_finder::DesktopEntry; use crate::util::desktop_finder::DesktopEntry;
@@ -10,8 +14,7 @@ use crate::util::desktop_finder::DesktopEntry;
pub struct View { pub struct View {
#[allow(dead_code)] #[allow(dead_code)]
pub state: ParserState, pub state: ParserState,
//entry: DesktopEntry,
entry: DesktopEntry,
} }
pub struct Params<'a> { pub struct Params<'a> {
@@ -30,9 +33,30 @@ impl View {
}; };
let mut state = wgui::parser::parse_from_assets(doc_params, params.layout, params.parent_id)?; let mut state = wgui::parser::parse_from_assets(doc_params, params.layout, params.parent_id)?;
let id_icon_parent = state.get_widget_id("icon_parent")?;
// app icon
if let Some(icon_path) = &params.entry.icon_path {
let mut template_params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
template_params.insert("path".into(), icon_path.as_str().into());
state.instantiate_template(
doc_params,
"ApplicationIcon",
params.layout,
id_icon_parent,
template_params,
)?;
}
let mut label_title = state.fetch_widget_as::<WidgetLabel>(&params.layout.state, "label_title")?;
label_title.set_text_simple(
&mut params.globals.get(),
Translation::from_raw_text(&params.entry.app_name),
);
Ok(Self { Ok(Self {
entry: params.entry, //entry: params.entry,
state, state,
}) })
} }

View File

@@ -237,7 +237,6 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul
// force-override style // force-override style
style.flex_wrap = taffy::FlexWrap::NoWrap; style.flex_wrap = taffy::FlexWrap::NoWrap;
style.align_items = Some(AlignItems::Center); style.align_items = Some(AlignItems::Center);
style.justify_content = Some(JustifyContent::Center);
// make checkbox interaction box larger by setting padding and negative margin // make checkbox interaction box larger by setting padding and negative margin
style.padding = taffy::Rect { style.padding = taffy::Rect {