cosmic-applets/cosmic-applet-status-area/src/components/app.rs

547 lines
21 KiB
Rust
Raw Normal View History

2024-05-06 15:39:04 +02:00
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use cosmic::{
2025-08-12 21:38:51 +02:00
Element, Task, app,
2024-04-02 14:36:02 -07:00
applet::cosmic_panel_config::PanelAnchor,
2025-08-12 21:38:51 +02:00
applet::token::subscription::{TokenRequest, TokenUpdate, activation_token_subscription},
cctk::sctk::reexports::calloop,
iced::{
2025-08-12 21:38:51 +02:00
self, Subscription,
2024-10-30 22:51:08 -04:00
platform_specific::shell::commands::popup::{destroy_popup, get_popup},
2025-08-12 21:38:51 +02:00
window,
},
2025-03-14 13:14:51 -04:00
surface,
2025-03-04 13:24:44 -05:00
widget::{container, mouse_area},
};
use std::collections::BTreeMap;
use crate::{components::status_menu, subscriptions::status_notifier_watcher};
#[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),
Hovered(usize),
2025-03-14 13:14:51 -04:00
Surface(surface::Action),
2025-05-19 23:50:00 -04:00
ToggleOverflow,
HoveredOverflow,
Token(TokenUpdate),
}
#[derive(Default)]
pub(crate) struct App {
core: app::Core,
connection: Option<zbus::Connection>,
menus: BTreeMap<usize, status_menu::State>,
open_menu: Option<usize>,
max_menu_id: usize,
popup: Option<window::Id>,
2025-05-19 23:50:00 -04:00
overflow_popup: Option<window::Id>,
token_tx: Option<calloop::channel::Sender<TokenRequest>>,
}
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 {
2023-12-11 14:45:36 -05:00
window::Id::unique()
}
2024-10-30 22:51:08 -04:00
fn resize_window(&self) -> app::Task<Msg> {
2024-04-15 18:37:00 -04:00
let icon_size = self.core.applet.suggested_size(true).0 as u32
+ self.core.applet.suggested_padding(true).1 as u32 * 2;
let n = self.menus.len() as u32;
2024-10-30 22:51:08 -04:00
window::resize(
self.core.main_window_id().unwrap(),
iced::Size::new(1.max(icon_size * n) as f32, icon_size as f32),
)
}
2025-05-19 23:50:00 -04:00
fn overflow_index(&self) -> Option<usize> {
let max_major_axis_len = self.core.applet.suggested_bounds.as_ref().map(|c| {
2025-05-19 23:50:00 -04:00
// if we have a configure for width and height, we're in a overflow popup
match self.core.applet.anchor {
PanelAnchor::Top | PanelAnchor::Bottom => c.width as u32,
PanelAnchor::Left | PanelAnchor::Right => c.height as u32,
}
})?;
2025-05-19 23:50:00 -04:00
let button_total_size = self.core.applet.suggested_size(true).0
+ self.core.applet.suggested_padding(true).1 * 2;
2025-05-19 23:50:00 -04:00
let menu_count = self.menus.len();
let btn_count = max_major_axis_len / button_total_size as u32;
if btn_count >= menu_count as u32 {
None
} else {
Some(
(btn_count.saturating_sub(1) as usize)
.min(menu_count)
.max(1),
)
}
}
fn view_overflow_popup(&self) -> cosmic::Element<'_, Msg> {
// Render the overflow popup with the menus that are not shown in the main view
let overflow_index = self.overflow_index().unwrap_or(0);
let children = self.menus.iter().skip(overflow_index).map(|(id, menu)| {
mouse_area(
menu_icon_button(&self.core.applet, &menu).on_press_down(Msg::TogglePopup(*id)),
2025-05-19 23:50:00 -04:00
)
.on_enter(Msg::Hovered(*id))
.into()
});
let theme = self.core.system_theme();
let cosmic = theme.cosmic();
let corners = cosmic.corner_radii;
2025-05-19 23:50:00 -04:00
let pad = corners.radius_m[0];
self.core
.applet
.popup_container(container(
if matches!(
self.core.applet.anchor,
PanelAnchor::Left | PanelAnchor::Right
) {
Element::from(iced::widget::column(children))
} else {
Element::from(iced::widget::row(children))
},
))
.into()
}
}
impl cosmic::Application for App {
type Message = Msg;
type Executor = cosmic::SingleThreadExecutor;
type Flags = ();
const APP_ID: &'static str = "com.system76.CosmicAppletStatusArea";
2024-10-30 22:51:08 -04:00
fn init(core: app::Core, _flags: ()) -> (Self, app::Task<Msg>) {
(
Self {
core,
..Self::default()
},
2024-10-30 22:51:08 -04:00
Task::none(),
)
}
fn core(&self) -> &app::Core {
&self.core
}
fn core_mut(&mut self) -> &mut app::Core {
&mut self.core
}
2024-10-30 22:51:08 -04:00
fn style(&self) -> Option<cosmic::iced_runtime::Appearance> {
2023-09-18 08:31:27 -07:00
Some(cosmic::applet::style())
}
2024-10-30 22:51:08 -04:00
fn update(&mut self, message: Msg) -> app::Task<Msg> {
match message {
Msg::Closed(surface) => {
if self.popup == Some(surface) {
self.popup = None;
self.open_menu = None;
}
2024-10-30 22:51:08 -04:00
Task::none()
}
Msg::StatusMenu((id, msg)) => match self.menus.get_mut(&id) {
Some(state) => state
.update(msg, id, self.token_tx.as_ref())
2025-03-14 13:14:51 -04:00
.map(move |msg| cosmic::action::app(Msg::StatusMenu((id, msg)))),
2024-10-30 22:51:08 -04:00
None => Task::none(),
},
Msg::StatusNotifier(event) => match event {
status_notifier_watcher::Event::Connected(connection) => {
self.connection = Some(connection);
2024-10-30 22:51:08 -04:00
Task::none()
}
status_notifier_watcher::Event::Registered(name) => {
let (state, cmd) = status_menu::State::new(name);
if let Some((id, m)) = self
.menus
.iter_mut()
.find(|(_, prev_state)| prev_state.name() == state.name())
{
*m = state;
let id = *id;
2025-03-14 13:14:51 -04:00
return cmd.map(move |msg| cosmic::action::app(Msg::StatusMenu((id, msg))));
}
let id = self.next_menu_id();
self.menus.insert(id, state);
2024-10-30 22:51:08 -04:00
app::Task::batch([
self.resize_window(),
2025-03-14 13:14:51 -04:00
cmd.map(move |msg| cosmic::action::app(Msg::StatusMenu((id, msg)))),
])
}
status_notifier_watcher::Event::Unregistered(name) => {
2023-11-17 07:04:57 +00:00
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}");
2024-10-30 22:51:08 -04:00
Task::none()
}
},
Msg::TogglePopup(id) => {
self.open_menu = if self.open_menu.is_none() {
Some(id)
} else {
None
};
if self.open_menu.is_some() {
self.menus[&id].opened();
let mut cmds = Vec::new();
if let Some(popup_id) = self.popup.take() {
cmds.push(destroy_popup(popup_id));
}
let popup_id = self.next_popup_id();
let i = self.menus.keys().position(|&i| i == id).unwrap();
2025-05-19 23:50:00 -04:00
let (i, parent) = self
.overflow_index()
.and_then(|overflow_i| {
if overflow_i <= i {
Some(i - overflow_i).zip(self.overflow_popup)
2025-05-19 23:50:00 -04:00
} else {
Some((i, self.core.main_window_id().unwrap()))
}
})
.unwrap_or((0, self.core.main_window_id().unwrap()));
2025-05-19 23:50:00 -04:00
let mut popup_settings = self
.core
.applet
.get_popup_settings(parent, popup_id, None, None, None);
self.popup = Some(popup_id);
if matches!(
self.core.applet.anchor,
PanelAnchor::Left | PanelAnchor::Right
) {
let suggested_size = self.core.applet.suggested_size(false).1
+ 2 * self.core.applet.suggested_padding(false).1;
popup_settings.positioner.anchor_rect.y = i as i32 * suggested_size as i32;
} else {
let suggested_size = self.core.applet.suggested_size(false).0
+ 2 * self.core.applet.suggested_padding(false).1;
popup_settings.positioner.anchor_rect.x = i as i32 * suggested_size as i32;
}
cmds.push(get_popup(popup_settings));
2024-10-30 22:51:08 -04:00
return app::Task::batch(cmds);
} else if let Some(popup_id) = self.popup {
self.menus[&id].closed();
return destroy_popup(popup_id);
}
2024-10-30 22:51:08 -04:00
Task::none()
}
Msg::Token(u) => match u {
TokenUpdate::Init(tx) => {
self.token_tx = Some(tx);
return Task::none();
}
TokenUpdate::Finished => {
self.token_tx = None;
return Task::none();
}
TokenUpdate::ActivationToken { token, exec: id } => {
if let Some(((state, id), token)) = str::parse(&id)
.ok()
.and_then(|id: usize| self.menus.get_mut(&id).map(|m| (m, id)))
.zip(token)
{
return state
.update(
status_menu::Msg::ClickToken(token),
id,
self.token_tx.as_ref(),
)
.map(move |msg| cosmic::action::app(Msg::StatusMenu((id, msg))));
}
return Task::none();
}
},
Msg::Hovered(id) => {
let mut cmds = Vec::new();
if let Some(old_id) = self.open_menu.take() {
if old_id != id {
if let Some(popup_id) = self.popup.take() {
cmds.push(destroy_popup(popup_id));
}
self.open_menu = Some(id);
} else {
self.open_menu = Some(old_id);
2024-10-30 22:51:08 -04:00
return Task::none();
}
} else {
2024-10-30 22:51:08 -04:00
return Task::none();
}
2025-05-19 23:50:00 -04:00
let popup_id = self.next_popup_id();
let i = self.menus.keys().position(|&i| i == id).unwrap();
let (i, parent) = self
.overflow_index()
.and_then(|overflow_i| {
if overflow_i <= i {
Some(i - overflow_i).zip(self.overflow_popup)
2025-05-19 23:50:00 -04:00
} else {
Some((i, self.core.main_window_id().unwrap()))
}
})
.unwrap_or((0, self.core.main_window_id().unwrap()));
2025-05-19 23:50:00 -04:00
let mut popup_settings = self
.core
.applet
.get_popup_settings(parent, popup_id, None, None, None);
self.popup = Some(popup_id);
if matches!(
self.core.applet.anchor,
PanelAnchor::Left | PanelAnchor::Right
) {
let suggested_size = self.core.applet.suggested_size(false).1
+ 2 * self.core.applet.suggested_padding(false).1;
2025-05-19 23:50:00 -04:00
popup_settings.positioner.anchor_rect.y = i as i32 * suggested_size as i32;
} else {
let suggested_size = self.core.applet.suggested_size(false).0
+ 2 * self.core.applet.suggested_padding(false).1;
2025-05-19 23:50:00 -04:00
popup_settings.positioner.anchor_rect.x = i as i32 * suggested_size as i32;
}
cmds.push(get_popup(popup_settings));
Task::batch(cmds)
}
Msg::Surface(a) => {
return cosmic::task::message(cosmic::Action::Cosmic(
cosmic::app::Action::Surface(a),
));
}
Msg::ToggleOverflow => {
if let Some(popup_id) = self.overflow_popup.take() {
self.popup = None;
self.open_menu = None;
return destroy_popup(popup_id);
} else if let Some(overflow_index) = self.overflow_index() {
// If we don't have an overflow, create it
let popup_id = self.next_popup_id();
let mut popup_settings = self.core.applet.get_popup_settings(
self.core.main_window_id().unwrap(),
popup_id,
None,
None,
None,
);
popup_settings.close_with_children = false;
if matches!(
self.core.applet.anchor,
PanelAnchor::Left | PanelAnchor::Right
) {
let suggested_size = self.core.applet.suggested_size(false).1
+ 2 * self.core.applet.suggested_padding(false).1;
2025-05-19 23:50:00 -04:00
popup_settings.positioner.anchor_rect.y =
overflow_index as i32 * suggested_size as i32;
} else {
let suggested_size = self.core.applet.suggested_size(false).0
+ 2 * self.core.applet.suggested_padding(false).1;
2025-05-19 23:50:00 -04:00
popup_settings.positioner.anchor_rect.x =
overflow_index as i32 * suggested_size as i32;
}
self.overflow_popup = Some(popup_id);
return get_popup(popup_settings);
2025-05-19 23:50:00 -04:00
} else {
return Task::none();
}
}
Msg::HoveredOverflow => {
let mut cmds = Vec::new();
if self.overflow_popup.is_some() {
// If we already have an overflow popup, do nothing
return Task::none();
} else if self.open_menu.is_some() {
// If we have an open menu, close it
if let Some(popup_id) = self.popup.take() {
cmds.push(destroy_popup(popup_id));
}
} else {
return Task::none();
}
let popup_id = self.next_popup_id();
let mut popup_settings = self.core.applet.get_popup_settings(
2024-10-30 22:51:08 -04:00
self.core.main_window_id().unwrap(),
popup_id,
None,
None,
None,
);
self.popup = Some(popup_id);
2025-05-19 23:50:00 -04:00
let Some(i) = self.overflow_index() else {
return Task::batch(cmds);
};
if matches!(
self.core.applet.anchor,
PanelAnchor::Left | PanelAnchor::Right
) {
let suggested_size = self.core.applet.suggested_size(false).1
+ 2 * self.core.applet.suggested_padding(false).1;
popup_settings.positioner.anchor_rect.y = i as i32 * suggested_size as i32;
} else {
let suggested_size = self.core.applet.suggested_size(false).0
+ 2 * self.core.applet.suggested_padding(false).1;
popup_settings.positioner.anchor_rect.x = i as i32 * suggested_size as i32;
}
cmds.push(get_popup(popup_settings));
2025-03-14 13:14:51 -04:00
Task::batch(cmds)
}
}
}
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 {
subscriptions.push(menu.subscription().with(*id).map(Msg::StatusMenu));
}
subscriptions.push(activation_token_subscription(0).map(Msg::Token));
iced::Subscription::batch(subscriptions)
}
fn view(&self) -> cosmic::Element<'_, Msg> {
2025-05-19 23:50:00 -04:00
let overflow_index = self.overflow_index();
let children = self
.menus
.iter()
.take(overflow_index.unwrap_or(self.menus.len()))
.map(|(id, menu)| {
mouse_area(menu_icon_button(&self.core.applet, &menu).on_press_down(
if menu.item.menu_proxy().is_some() {
Msg::TogglePopup(*id)
} else {
Msg::StatusMenu((*id, status_menu::Msg::Click(0, true)))
},
))
2025-05-19 23:50:00 -04:00
.on_enter(Msg::Hovered(*id))
.into()
});
2024-10-30 22:51:08 -04:00
self.core
.applet
.autosize_window(
if matches!(
self.core.applet.anchor,
PanelAnchor::Left | PanelAnchor::Right
) {
Element::from(iced::widget::column(children))
} else {
2025-05-19 23:50:00 -04:00
iced::widget::row(children)
.push_maybe(overflow_index.map(|_| {
mouse_area(
self.core
.applet
.icon_button(match self.core.applet.anchor {
PanelAnchor::Bottom => "go-up-symbolic",
PanelAnchor::Left => "go-next-symbolic",
PanelAnchor::Right => "go-previous-symbolic",
PanelAnchor::Top => "go-down-symbolic",
})
.on_press_down(Msg::ToggleOverflow),
)
.on_enter(Msg::HoveredOverflow)
}))
.into()
2024-10-30 22:51:08 -04:00
},
)
.into()
}
2025-05-19 23:50:00 -04:00
fn view_window(&self, surface: window::Id) -> cosmic::Element<'_, Msg> {
if self
.overflow_popup
.as_ref()
.is_some_and(|id| *id == surface)
{
return self.view_overflow_popup();
}
2025-03-04 13:24:44 -05:00
let theme = self.core.system_theme();
let cosmic = theme.cosmic();
let corners = cosmic.corner_radii;
2025-03-04 13:24:44 -05:00
let pad = corners.radius_m[0];
match self.open_menu {
Some(id) => match self.menus.get(&id) {
Some(menu) => self
.core
2023-09-18 08:31:27 -07:00
.applet
2025-03-04 13:24:44 -05:00
.popup_container(
container(menu.popup_view().map(move |msg| Msg::StatusMenu((id, msg))))
.padding([pad, 0.]),
)
.into(),
None => unreachable!(),
},
None => iced::widget::text("").into(),
}
}
fn on_close_requested(&self, id: window::Id) -> Option<Msg> {
Some(Msg::Closed(id))
}
}
fn menu_icon_button<'a>(
applet: &'a cosmic::applet::Context,
menu: &'a status_menu::State,
) -> cosmic::widget::Button<'a, Msg> {
match (menu.icon_pixmap(), menu.icon_name(), menu.icon_theme_path()) {
(Some(icon), "", _) => applet.icon_button_from_handle(icon.clone().symbolic(true)),
(_, name, Some(theme_path)) if name != "" => {
let mut path = theme_path.to_owned();
// XXX right way to lookup icon in dir?
path.push(name.to_owned() + ".svg");
if !path.exists() {
path.pop();
path.push(name.to_owned() + ".png");
}
let icon = cosmic::widget::icon::from_path(path).symbolic(true);
applet.icon_button_from_handle(icon)
}
(_, name, _) => applet.icon_button(name),
}
}
pub fn main() -> iced::Result {
2024-10-30 22:51:08 -04:00
cosmic::applet::run::<App>(())
}