This is based on the GTK version of the status area applet that was previously in this repository. This exposes app indicators found over dbus. As used in applications like nm-applet and steam.
215 lines
6.8 KiB
Rust
215 lines
6.8 KiB
Rust
use cosmic::{
|
|
app::{self, Command},
|
|
iced::{
|
|
self,
|
|
wayland::{
|
|
popup::{destroy_popup, get_popup},
|
|
window::resize_window,
|
|
},
|
|
window, Subscription,
|
|
},
|
|
iced_style::application,
|
|
Theme,
|
|
};
|
|
use std::collections::BTreeMap;
|
|
|
|
use crate::{components::status_menu, subscriptions::status_notifier_watcher};
|
|
|
|
// XXX copied from libcosmic
|
|
const APPLET_PADDING: u32 = 8;
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub enum Msg {
|
|
Closed(window::Id),
|
|
// XXX don't use index (unique window id? or I guess that's created and destroyed)
|
|
StatusMenu((usize, status_menu::Msg)),
|
|
StatusNotifier(status_notifier_watcher::Event),
|
|
TogglePopup(usize),
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct App {
|
|
core: app::Core,
|
|
connection: Option<zbus::Connection>,
|
|
menus: BTreeMap<usize, status_menu::State>,
|
|
open_menu: Option<usize>,
|
|
max_menu_id: usize,
|
|
max_popup_id: u128,
|
|
popup: Option<window::Id>,
|
|
}
|
|
|
|
impl App {
|
|
fn next_menu_id(&mut self) -> usize {
|
|
self.max_menu_id += 1;
|
|
self.max_menu_id
|
|
}
|
|
|
|
fn next_popup_id(&mut self) -> window::Id {
|
|
self.max_popup_id += 1;
|
|
window::Id(self.max_popup_id)
|
|
}
|
|
|
|
fn resize_window(&self) -> Command<Msg> {
|
|
let icon_size = self.core.applet_helper.suggested_size().0 as u32 + APPLET_PADDING * 2;
|
|
let n = self.menus.len() as u32;
|
|
resize_window(window::Id(0), 1.max(icon_size * n), icon_size)
|
|
}
|
|
}
|
|
|
|
impl cosmic::Application for App {
|
|
type Message = Msg;
|
|
type Executor = iced::executor::Default;
|
|
type Flags = ();
|
|
const APP_ID: &'static str = "com.system76.CosmicAppletStatusArea";
|
|
|
|
fn init(core: app::Core, _flags: ()) -> (Self, app::Command<Msg>) {
|
|
(
|
|
Self {
|
|
core,
|
|
..Self::default()
|
|
},
|
|
Command::none(),
|
|
)
|
|
}
|
|
|
|
fn core(&self) -> &app::Core {
|
|
&self.core
|
|
}
|
|
|
|
fn core_mut(&mut self) -> &mut app::Core {
|
|
&mut self.core
|
|
}
|
|
|
|
fn style(&self) -> Option<<Theme as application::StyleSheet>::Style> {
|
|
Some(app::applet::style())
|
|
}
|
|
|
|
fn update(&mut self, message: Msg) -> Command<Msg> {
|
|
match message {
|
|
Msg::Closed(surface) => {
|
|
if self.popup == Some(surface) {
|
|
self.popup = None;
|
|
self.open_menu = None;
|
|
}
|
|
Command::none()
|
|
}
|
|
Msg::StatusMenu((id, msg)) => match self.menus.get_mut(&id) {
|
|
Some(state) => state
|
|
.update(msg)
|
|
.map(move |msg| app::message::app(Msg::StatusMenu((id, msg)))),
|
|
None => Command::none(),
|
|
},
|
|
Msg::StatusNotifier(event) => match event {
|
|
status_notifier_watcher::Event::Connected(connection) => {
|
|
self.connection = Some(connection);
|
|
Command::none()
|
|
}
|
|
status_notifier_watcher::Event::Registered(name) => {
|
|
let (state, cmd) = status_menu::State::new(name);
|
|
let id = self.next_menu_id();
|
|
self.menus.insert(id, state);
|
|
Command::batch([
|
|
self.resize_window(),
|
|
cmd.map(move |msg| app::message::app(Msg::StatusMenu((id, msg)))),
|
|
])
|
|
}
|
|
status_notifier_watcher::Event::Unregistered(name) => {
|
|
if let Some((id, _)) =
|
|
self.menus.iter().find(|(_id, menu)| menu.name() == &name)
|
|
{
|
|
let id = *id;
|
|
self.menus.remove(&id);
|
|
if self.open_menu == Some(id) {
|
|
self.open_menu = None;
|
|
if let Some(popup_id) = self.popup {
|
|
return destroy_popup(popup_id);
|
|
}
|
|
}
|
|
}
|
|
self.resize_window()
|
|
}
|
|
status_notifier_watcher::Event::Error(err) => {
|
|
eprintln!("Status notifier error: {}", err);
|
|
Command::none()
|
|
}
|
|
},
|
|
Msg::TogglePopup(id) => {
|
|
self.open_menu = if self.open_menu != Some(id) {
|
|
Some(id)
|
|
} else {
|
|
None
|
|
};
|
|
// Reuse popup if a different menu is opened.
|
|
// Had issue creating new one. Does it make a difference?
|
|
if self.open_menu.is_some() {
|
|
if self.popup.is_none() {
|
|
let id = self.next_popup_id();
|
|
let popup_settings = self.core.applet_helper.get_popup_settings(
|
|
window::Id(0),
|
|
id,
|
|
None,
|
|
None,
|
|
None,
|
|
);
|
|
self.popup = Some(id);
|
|
return get_popup(popup_settings);
|
|
}
|
|
} else if let Some(id) = self.popup {
|
|
return destroy_popup(id);
|
|
}
|
|
Command::none()
|
|
}
|
|
}
|
|
}
|
|
|
|
fn subscription(&self) -> Subscription<Msg> {
|
|
let mut subscriptions = Vec::new();
|
|
|
|
subscriptions.push(status_notifier_watcher::subscription().map(Msg::StatusNotifier));
|
|
|
|
for (id, menu) in self.menus.iter() {
|
|
subscriptions.push(menu.subscription().with(*id).map(Msg::StatusMenu));
|
|
}
|
|
|
|
iced::Subscription::batch(subscriptions)
|
|
}
|
|
|
|
fn view(&self) -> cosmic::Element<'_, Msg> {
|
|
// XXX connect open event
|
|
iced::widget::row(
|
|
self.menus
|
|
.iter()
|
|
.map(|(id, menu)| {
|
|
self.core
|
|
.applet_helper
|
|
.icon_button(menu.icon_name())
|
|
.on_press(Msg::TogglePopup(*id))
|
|
.into()
|
|
})
|
|
.collect(),
|
|
)
|
|
.into()
|
|
}
|
|
|
|
fn view_window(&self, _surface: window::Id) -> cosmic::Element<'_, Msg> {
|
|
match self.open_menu {
|
|
Some(id) => match self.menus.get(&id) {
|
|
Some(menu) => self
|
|
.core
|
|
.applet_helper
|
|
.popup_container(menu.popup_view().map(move |msg| Msg::StatusMenu((id, msg))))
|
|
.into(),
|
|
None => unreachable!(),
|
|
},
|
|
None => iced::widget::text("").into(),
|
|
}
|
|
}
|
|
|
|
fn on_close_requested(&self, id: window::Id) -> Option<Msg> {
|
|
Some(Msg::Closed(id))
|
|
}
|
|
}
|
|
|
|
pub fn main() -> iced::Result {
|
|
app::applet::run::<App>(true, ())
|
|
}
|