cosmic-applets/applets/cosmic-app-list/src/app.rs

446 lines
19 KiB
Rust
Raw Normal View History

use std::ffi::OsStr;
use std::path::PathBuf;
use crate::config;
use crate::config::AppListConfig;
use crate::toplevel_subscription::toplevel_subscription;
use crate::toplevel_subscription::ToplevelRequest;
use crate::toplevel_subscription::ToplevelUpdate;
use calloop::channel::Sender;
use cctk::toplevel_info::ToplevelInfo;
2022-12-13 19:58:00 -05:00
use cctk::wayland_client::protocol::wl_seat::WlSeat;
use cosmic::applet::CosmicAppletHelper;
2022-12-13 19:58:00 -05:00
use cosmic::iced;
use cosmic::iced::wayland::popup::destroy_popup;
use cosmic::iced::wayland::popup::get_popup;
use cosmic::iced::wayland::SurfaceIdWrapper;
use cosmic::iced::widget::{column, row};
use cosmic::iced::{executor, window, Application, Command, Subscription};
2022-12-13 19:58:00 -05:00
use cosmic::iced_native::subscription::events_with;
use cosmic::iced_style::application::{self, Appearance};
use cosmic::iced_style::Color;
use cosmic::theme::Button;
use cosmic::widget::{horizontal_rule, vertical_rule};
use cosmic::{Element, Theme};
use cosmic_panel_config::PanelAnchor;
use cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1;
use freedesktop_desktop_entry::DesktopEntry;
use iced::wayland::window::resize_window;
use iced::widget::container;
use iced::widget::horizontal_space;
use iced::widget::svg;
use iced::widget::Image;
2022-12-13 19:58:00 -05:00
use iced::Alignment;
use iced::Background;
use iced::Length;
use itertools::Itertools;
pub fn run() -> cosmic::iced::Result {
let helper = CosmicAppletHelper::default();
CosmicAppList::run(helper.window_settings())
}
#[derive(Debug, Clone, Default)]
struct Toplevel {
toplevels: Vec<(ZcosmicToplevelHandleV1, ToplevelInfo)>,
app_id: String,
icon_path: PathBuf,
}
#[derive(Clone, Default)]
struct CosmicAppList {
theme: Theme,
popup: Option<window::Id>,
id_ctr: u32,
subscription_ctr: u32,
toplevel_list: Vec<Toplevel>,
config: AppListConfig,
toplevel_sender: Option<Sender<ToplevelRequest>>,
applet_helper: CosmicAppletHelper,
2022-12-13 19:58:00 -05:00
seat: Option<WlSeat>,
}
// TODO DnD after sctk merges DnD
#[derive(Debug, Clone)]
enum Message {
Toplevel(ToplevelUpdate),
Favorite(String),
UnFavorite(String),
TogglePopup(usize),
Activate(Option<ZcosmicToplevelHandleV1>),
Quit(ZcosmicToplevelHandleV1),
Errored(String),
Ignore,
2022-12-13 19:58:00 -05:00
NewSeat(WlSeat),
RemovedSeat(WlSeat),
}
fn icon_for_app_ids(mut app_ids: Vec<String>) -> Vec<(String, PathBuf)> {
let mut ret = freedesktop_desktop_entry::Iter::new(freedesktop_desktop_entry::default_paths())
.filter_map(|path| {
std::fs::read_to_string(&path).ok().and_then(|input| {
DesktopEntry::decode(&path, &input).ok().and_then(|de| {
if let Some(i) = app_ids.iter().position(|s| s == de.appid) {
let id = app_ids.remove(i);
freedesktop_icons::lookup(de.icon().unwrap_or(de.appid))
.with_size(128)
.with_cache()
.find()
.map(|buf| (id, buf))
} else {
None
}
})
})
})
.collect_vec();
ret.append(
&mut app_ids
.into_iter()
.map(|id| (id, Default::default()))
.collect_vec(),
);
ret
}
impl Application for CosmicAppList {
type Message = Message;
type Theme = Theme;
type Executor = executor::Default;
type Flags = ();
fn new(_flags: ()) -> (Self, Command<Message>) {
let config = config::AppListConfig::load().unwrap_or_default();
(
CosmicAppList {
toplevel_list: icon_for_app_ids(config.favorites.clone())
.into_iter()
.map(|e| Toplevel {
toplevels: Default::default(),
app_id: e.0,
icon_path: e.1,
})
.collect(),
config,
..Default::default()
},
Command::none(),
)
}
fn title(&self) -> String {
config::APP_ID.to_string()
}
fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::Errored(_) => {
// TODO log errors
}
Message::TogglePopup(_) => {
if let Some(p) = self.popup.take() {
return destroy_popup(p);
} else {
self.id_ctr += 1;
let new_id = window::Id::new(self.id_ctr);
self.popup.replace(new_id);
let popup_settings = self.applet_helper.get_popup_settings(
window::Id::new(0),
new_id,
(400, 240),
None,
None,
);
return get_popup(popup_settings);
}
}
Message::Favorite(id) => {
let _ = self.config.add_favorite(id);
}
Message::UnFavorite(id) => {
let _ = self.config.remove_favorite(id);
}
Message::Activate(handle) => {
2022-12-13 19:58:00 -05:00
if let (Some(tx), Some(seat), Some(handle)) = (self.toplevel_sender.as_ref(), self.seat.as_ref(), handle) {
let _ = tx.send(ToplevelRequest::Activate(handle, seat.clone()));
}
}
Message::Quit(handle) => {
if let Some(tx) = self.toplevel_sender.as_ref() {
let _ = tx.send(ToplevelRequest::Quit(handle));
}
}
Message::Toplevel(event) => {
// dbg!(&self.toplevel_list);
match event {
ToplevelUpdate::AddToplevel(handle, info) => {
if let Some(i) = self
.toplevel_list
.iter()
.position(|Toplevel { app_id, .. }| app_id == &info.app_id)
{
self.toplevel_list[i].toplevels.push((handle, info));
} else {
let (app_id, icon_name) =
icon_for_app_ids(vec![info.app_id.clone()]).remove(0);
self.toplevel_list.push(Toplevel {
toplevels: vec![(handle, info)],
app_id,
icon_path: icon_name,
});
// TODO better way of setting window size?
let pixel_size = self.applet_helper.suggested_icon_size();
let padding = 8;
let dot_size = 4;
let spacing = 4;
2022-12-13 19:58:00 -05:00
let length = self
.toplevel_list
.iter()
.map(|t| {
(pixel_size + 2 * padding)
.max((dot_size + spacing) * t.toplevels.len() as u16)
as u32
+ spacing as u32
})
.sum();
let thickness = (pixel_size + 2 * padding + dot_size + spacing) as u32;
let (w, h) = match self.applet_helper.anchor {
PanelAnchor::Left | PanelAnchor::Right => (thickness, length),
PanelAnchor::Top | PanelAnchor::Bottom => (length, thickness),
};
return resize_window(window::Id::new(0), w, h);
}
}
ToplevelUpdate::Init(tx) => {
self.toplevel_sender.replace(tx);
}
ToplevelUpdate::Finished => {
self.subscription_ctr += 1;
2022-12-13 19:58:00 -05:00
for t in &mut self.toplevel_list {
t.toplevels.clear();
}
}
ToplevelUpdate::RemoveToplevel(handle) => {
if let Some(i) = self.toplevel_list.iter_mut().position(
|Toplevel {
toplevels, app_id, ..
}| {
if let Some(ret) = toplevels.iter().position(|t| &t.0 == &handle) {
toplevels.remove(ret);
toplevels.is_empty() && self.config.favorites.contains(app_id)
} else {
false
}
},
) {
self.toplevel_list.remove(i);
}
// TODO better way of setting window size?
let pixel_size = self.applet_helper.suggested_icon_size();
let padding = 8;
let dot_size = 4;
let spacing = 4;
2022-12-13 19:58:00 -05:00
let length = self
.toplevel_list
.iter()
.map(|t| {
(pixel_size + 2 * padding)
.max((dot_size + spacing) * t.toplevels.len() as u16)
as u32
+ spacing as u32
})
.sum();
let thickness = (pixel_size + 2 * padding + dot_size + spacing) as u32;
let (w, h) = match self.applet_helper.anchor {
PanelAnchor::Left | PanelAnchor::Right => (thickness, length),
PanelAnchor::Top | PanelAnchor::Bottom => (length, thickness),
};
return resize_window(window::Id::new(0), w, h);
}
ToplevelUpdate::UpdateToplevel(handle, info) => {
'toplevel_loop: for toplevel_list in &mut self.toplevel_list {
for (t_handle, t_info) in &mut toplevel_list.toplevels {
if &handle == t_handle {
*t_info = info;
break 'toplevel_loop;
}
}
}
// TODO better way of setting window size?
let pixel_size = self.applet_helper.suggested_icon_size();
let padding = 8;
let dot_size = 4;
let spacing = 4;
2022-12-13 19:58:00 -05:00
let length = self
.toplevel_list
.iter()
.map(|t| {
(pixel_size + 2 * padding)
.max((dot_size + spacing) * t.toplevels.len() as u16)
as u32
+ spacing as u32
})
.sum();
let thickness = (pixel_size + 2 * padding + dot_size + spacing) as u32;
let (w, h) = match self.applet_helper.anchor {
PanelAnchor::Left | PanelAnchor::Right => (thickness, length),
PanelAnchor::Top | PanelAnchor::Bottom => (length, thickness),
};
return resize_window(window::Id::new(0), w, h);
}
}
}
Message::Ignore => {}
2022-12-13 19:58:00 -05:00
Message::NewSeat(s) => {
self.seat.replace(s);
},
Message::RemovedSeat(_) => {
self.seat.take();
},
}
Command::none()
}
fn view(&self, id: SurfaceIdWrapper) -> Element<Message> {
match id {
SurfaceIdWrapper::LayerSurface(_) => unimplemented!(),
SurfaceIdWrapper::Window(_) => {
let (favorites, running) = self.toplevel_list.iter().enumerate().fold(
(Vec::new(), Vec::new()),
|(mut favorites, mut running),
(
i,
Toplevel {
toplevels,
app_id,
icon_path,
},
)| {
let icon = if icon_path.extension() == Some(&OsStr::new("svg")) {
let handle = svg::Handle::from_path(icon_path);
svg::Svg::new(handle)
.width(Length::Units(self.applet_helper.suggested_icon_size()))
.height(Length::Units(self.applet_helper.suggested_icon_size()))
.into()
} else {
Image::new(icon_path)
.width(Length::Units(self.applet_helper.suggested_icon_size()))
.height(Length::Units(self.applet_helper.suggested_icon_size()))
.into()
};
2022-12-13 22:46:30 -05:00
let dot_radius = 2;
let dots = (0..toplevels.len())
.into_iter()
.map(|_| {
container(horizontal_space(Length::Units(0)))
2022-12-13 19:58:00 -05:00
.padding(dot_radius)
.style(<Self::Theme as container::StyleSheet>::Style::Custom(
|theme| container::Appearance {
text_color: Some(Color::TRANSPARENT),
2022-12-13 19:58:00 -05:00
background: Some(Background::Color(
theme.cosmic().on_bg_color().into(),
)),
border_radius: 4.0,
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
))
.into()
})
.collect_vec();
let icon_wrapper = match &self.applet_helper.anchor {
2022-12-13 19:58:00 -05:00
PanelAnchor::Left => row(vec![column(dots).spacing(4).into(), icon])
.align_items(iced::Alignment::Center)
.spacing(4)
.into(),
PanelAnchor::Right => row(vec![icon, column(dots).spacing(4).into()])
.align_items(iced::Alignment::Center)
.spacing(4)
.into(),
PanelAnchor::Top => column(vec![row(dots).spacing(4).into(), icon])
.align_items(iced::Alignment::Center)
.spacing(4)
.into(),
PanelAnchor::Bottom => column(vec![icon, row(dots).spacing(4).into()])
.align_items(iced::Alignment::Center)
.spacing(4)
.into(),
};
// TODO tooltip on hover
let icon_button = cosmic::widget::button(Button::Text)
2022-12-13 19:58:00 -05:00
.custom(vec![icon_wrapper])
.on_press(Message::Activate(toplevels.first().map(|t| t.0.clone())))
.padding(8)
.into();
if self.config.favorites.contains(&app_id) {
favorites.push(icon_button)
} else {
running.push(icon_button);
}
(favorites, running)
},
);
match &self.applet_helper.anchor {
PanelAnchor::Left | PanelAnchor::Right => {
2022-12-13 19:58:00 -05:00
column![column(favorites), horizontal_rule(1), column(running)]
.spacing(4)
.align_items(Alignment::Center)
.height(Length::Fill)
.width(Length::Fill)
.into()
}
PanelAnchor::Top | PanelAnchor::Bottom => {
2022-12-13 19:58:00 -05:00
row![row(favorites), vertical_rule(1), row(running)]
.spacing(4)
.align_items(Alignment::Center)
.height(Length::Fill)
.width(Length::Fill)
.into()
}
}
}
SurfaceIdWrapper::Popup(_) => {
todo!();
}
}
}
fn subscription(&self) -> Subscription<Message> {
Subscription::batch(vec![
2022-12-13 19:58:00 -05:00
toplevel_subscription(self.subscription_ctr).map(|(_, event)| Message::Toplevel(event)),
events_with(|e, status| match e {
cosmic::iced_native::Event::PlatformSpecific(
cosmic::iced_native::event::PlatformSpecific::Wayland(
cosmic::iced_native::event::wayland::Event::Seat(e, seat),
),
) => match e {
cosmic::iced_native::event::wayland::SeatEvent::Enter => {
Some(Message::NewSeat(seat))
},
cosmic::iced_native::event::wayland::SeatEvent::Leave => {
Some(Message::RemovedSeat(seat))
},
},
_ => None
}),
])
}
fn theme(&self) -> Theme {
self.theme
}
fn close_requested(&self, _id: SurfaceIdWrapper) -> Self::Message {
Message::Ignore
}
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
<Self::Theme as application::StyleSheet>::Style::Custom(|theme| Appearance {
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().on_bg_color().into(),
})
}
}