Use cosmic::Application/cosmic::app::applet

This saves a bit of duplicated boilerplate.

Applets seem to work fairly well with this. Though we should see if any
other changes to initial window settings are needed in certain applets
for the exact behavior we want.
This commit is contained in:
Ian Douglas Scott 2023-08-03 13:22:17 -07:00
parent 3ad64df5f3
commit a23f181b7f
28 changed files with 2029 additions and 2597 deletions

716
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,6 @@
[workspace] [workspace]
members = [ members = [
"applet",
"cosmic-app-list", "cosmic-app-list",
"cosmic-applet-audio", "cosmic-applet-audio",
"cosmic-applet-battery", "cosmic-applet-battery",
@ -20,8 +19,8 @@ resolver="2"
[workspace.dependencies] [workspace.dependencies]
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "e39748e" } cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "e39748e" }
cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols", default-features = false, features = ["client"], rev = "e39748e" } cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols", default-features = false, features = ["client"], rev = "e39748e" }
cosmic-time = { git = "https://github.com/pop-os/cosmic-time", rev = "c39e737", default-features = false, features = ["libcosmic", "once_cell"] } cosmic-time = { git = "https://github.com/pop-os/cosmic-time", default-features = false, features = ["libcosmic", "once_cell"] }
libcosmic = { git = "https://github.com/pop-os/libcosmic", default-features = false, features = ["tokio", "wayland"] } libcosmic = { git = "https://github.com/pop-os/libcosmic", default-features = false, features = ["applet", "tokio", "wayland"] }
[profile.release] [profile.release]
lto = "thin" lto = "thin"

View file

@ -1,13 +0,0 @@
[package]
name = "cosmic-applet"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
libcosmic.workspace = true
ron = { version = "0.8" }
serde = { version = "1.0" }
cosmic-panel-config = { git = "https://github.com/pop-os/cosmic-panel" }
log = { version = "0.4" }

View file

@ -1,258 +0,0 @@
use std::sync::Arc;
use cosmic::{
cosmic_config::{config_subscription, CosmicConfigEntry},
cosmic_theme::util::CssColor,
iced::{
alignment::{Horizontal, Vertical},
wayland::InitialSurface,
widget::{self, Container},
window, Color, Element, Length, Limits, Rectangle, Settings,
},
iced_futures::Subscription,
iced_style, iced_widget, sctk,
theme::Button,
Renderer,
};
use cosmic_panel_config::{CosmicPanelBackground, PanelAnchor, PanelSize};
use iced_style::{button::StyleSheet, container::Appearance};
use iced_widget::runtime::command::platform_specific::wayland::{
popup::{SctkPopupSettings, SctkPositioner},
window::SctkWindowSettings,
};
use log::error;
use sctk::reexports::protocols::xdg::shell::client::xdg_positioner::{Anchor, Gravity};
pub use cosmic_panel_config;
const APPLET_PADDING: u32 = 8;
#[must_use]
pub fn applet_button_theme() -> Button {
Button::Custom {
active: Box::new(|t| iced_style::button::Appearance {
border_radius: 0.0.into(),
..t.active(&Button::Text)
}),
hover: Box::new(|t| iced_style::button::Appearance {
border_radius: 0.0.into(),
..t.hovered(&Button::Text)
}),
}
}
#[derive(Debug, Clone)]
pub struct CosmicAppletHelper {
pub size: Size,
pub anchor: PanelAnchor,
pub background: CosmicPanelBackground,
pub output_name: String,
}
#[derive(Clone, Debug)]
pub enum Size {
PanelSize(PanelSize),
// (width, height)
Hardcoded((u16, u16)),
}
impl Default for CosmicAppletHelper {
fn default() -> Self {
Self {
size: Size::PanelSize(
std::env::var("COSMIC_PANEL_SIZE")
.ok()
.and_then(|size| ron::from_str(size.as_str()).ok())
.unwrap_or(PanelSize::S),
),
anchor: std::env::var("COSMIC_PANEL_ANCHOR")
.ok()
.and_then(|size| ron::from_str(size.as_str()).ok())
.unwrap_or(PanelAnchor::Top),
background: std::env::var("COSMIC_PANEL_BACKGROUND")
.ok()
.and_then(|size| ron::from_str(size.as_str()).ok())
.unwrap_or(CosmicPanelBackground::ThemeDefault),
output_name: std::env::var("COSMIC_PANEL_OUTPUT").unwrap_or_default(),
}
}
}
impl CosmicAppletHelper {
#[must_use]
pub fn suggested_size(&self) -> (u16, u16) {
match &self.size {
Size::PanelSize(size) => match size {
PanelSize::XL => (64, 64),
PanelSize::L => (36, 36),
PanelSize::M => (24, 24),
PanelSize::S => (16, 16),
PanelSize::XS => (12, 12),
},
Size::Hardcoded((width, height)) => (*width, *height),
}
}
// Set the default window size. Helper for application init with hardcoded size.
pub fn window_size(&mut self, width: u16, height: u16) {
self.size = Size::Hardcoded((width, height));
}
#[must_use]
pub fn window_settings<F: Default>(&self) -> Settings<F> {
self.window_settings_with_flags(F::default())
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn window_settings_with_flags<F>(&self, flags: F) -> Settings<F> {
let (width, height) = self.suggested_size();
let width = u32::from(width);
let height = u32::from(height);
Settings {
initial_surface: InitialSurface::XdgWindow(SctkWindowSettings {
size: (width + APPLET_PADDING * 2, height + APPLET_PADDING * 2),
size_limits: Limits::NONE
.min_height(height as f32 + APPLET_PADDING as f32 * 2.0)
.max_height(height as f32 + APPLET_PADDING as f32 * 2.0)
.min_width(width as f32 + APPLET_PADDING as f32 * 2.0)
.max_width(width as f32 + APPLET_PADDING as f32 * 2.0),
resizable: None,
..Default::default()
}),
default_text_size: 18.0,
default_font: cosmic::font::FONT,
..Settings::with_flags(flags)
}
}
#[must_use]
pub fn icon_button<'a, Message: 'static>(
&self,
icon_name: &'a str,
) -> widget::Button<'a, Message, Renderer> {
cosmic::widget::button(cosmic::theme::Button::Text)
.icon(
cosmic::theme::Svg::Symbolic,
icon_name,
self.suggested_size().0,
)
.padding(8)
}
// TODO popup container which tracks the size of itself and requests the popup to resize to match
pub fn popup_container<'a, Message: 'static>(
&self,
content: impl Into<Element<'a, Message, Renderer>>,
) -> Container<'a, Message, Renderer> {
let (vertical_align, horizontal_align) = match self.anchor {
PanelAnchor::Left => (Vertical::Center, Horizontal::Left),
PanelAnchor::Right => (Vertical::Center, Horizontal::Right),
PanelAnchor::Top => (Vertical::Top, Horizontal::Center),
PanelAnchor::Bottom => (Vertical::Bottom, Horizontal::Center),
};
Container::<Message, Renderer>::new(Container::<Message, Renderer>::new(content).style(
cosmic::theme::Container::custom(|theme| Appearance {
text_color: Some(theme.cosmic().background.on.into()),
background: Some(Color::from(theme.cosmic().background.base).into()),
border_radius: 12.0.into(),
border_width: 0.0,
border_color: Color::TRANSPARENT,
}),
))
.width(Length::Shrink)
.height(Length::Shrink)
.align_x(horizontal_align)
.align_y(vertical_align)
}
#[must_use]
#[allow(clippy::cast_possible_wrap)]
pub fn get_popup_settings(
&self,
parent: window::Id,
id: window::Id,
size: Option<(u32, u32)>,
width_padding: Option<i32>,
height_padding: Option<i32>,
) -> SctkPopupSettings {
let (width, height) = self.suggested_size();
let pixel_offset = 8;
let (offset, anchor, gravity) = match self.anchor {
PanelAnchor::Left => ((pixel_offset, 0), Anchor::Right, Gravity::Right),
PanelAnchor::Right => ((-pixel_offset, 0), Anchor::Left, Gravity::Left),
PanelAnchor::Top => ((0, pixel_offset), Anchor::Bottom, Gravity::Bottom),
PanelAnchor::Bottom => ((0, -pixel_offset), Anchor::Top, Gravity::Top),
};
SctkPopupSettings {
parent,
id,
positioner: SctkPositioner {
anchor,
gravity,
offset,
size,
anchor_rect: Rectangle {
x: 0,
y: 0,
width: width_padding.unwrap_or(APPLET_PADDING as i32) * 2 + i32::from(width),
height: height_padding.unwrap_or(APPLET_PADDING as i32) * 2 + i32::from(height),
},
reactive: true,
constraint_adjustment: 15, // slide_y, slide_x, flip_x, flip_y
..Default::default()
},
parent_size: None,
grab: true,
}
}
pub fn theme(&self) -> cosmic::theme::Theme {
match self.background {
CosmicPanelBackground::ThemeDefault | CosmicPanelBackground::Color(_) => {
let Ok(helper) = cosmic::cosmic_config::Config::new(
cosmic::cosmic_theme::NAME,
cosmic::cosmic_theme::Theme::<CssColor>::version() as u64,
) else {
return cosmic::theme::Theme::dark();
};
let t = cosmic::cosmic_theme::Theme::get_entry(&helper)
.map(|t| t.into_srgba())
.unwrap_or_else(|(errors, theme)| {
for err in errors {
error!("{:?}", err);
}
theme.into_srgba()
});
cosmic::theme::Theme::custom(Arc::new(t))
}
CosmicPanelBackground::Dark => cosmic::theme::Theme::dark(),
CosmicPanelBackground::Light => cosmic::theme::Theme::light(),
}
}
pub fn theme_subscription(&self, id: u64) -> Subscription<cosmic::theme::Theme> {
match self.background {
CosmicPanelBackground::ThemeDefault | CosmicPanelBackground::Color(_) => {
config_subscription::<u64, cosmic::cosmic_theme::Theme<CssColor>>(
id,
cosmic::cosmic_theme::NAME.into(),
cosmic::cosmic_theme::Theme::<CssColor>::version() as u64,
)
.map(|(_, res)| {
let theme =
res.map(|theme| theme.into_srgba())
.unwrap_or_else(|(errors, theme)| {
for err in errors {
error!("{:?}", err);
}
theme.into_srgba()
});
cosmic::theme::Theme::custom(Arc::new(theme))
})
}
CosmicPanelBackground::Dark | CosmicPanelBackground::Light => Subscription::none(),
}
}
}

View file

@ -8,7 +8,6 @@ edition = "2021"
cctk.workspace = true cctk.workspace = true
cosmic-protocols.workspace = true cosmic-protocols.workspace = true
libcosmic.workspace = true libcosmic.workspace = true
cosmic-applet = { path = "../applet" }
# libcosmic = { path = "../../libcosmic", default-features = false, features = ["wayland", "tokio"] } # libcosmic = { path = "../../libcosmic", default-features = false, features = ["wayland", "tokio"] }
ron = "0.8" ron = "0.8"
futures = "0.3" futures = "0.3"

View file

@ -9,13 +9,16 @@ use cctk::sctk::reexports::calloop::channel::Sender;
use cctk::toplevel_info::ToplevelInfo; use cctk::toplevel_info::ToplevelInfo;
use cctk::wayland_client::protocol::wl_data_device_manager::DndAction; use cctk::wayland_client::protocol::wl_data_device_manager::DndAction;
use cctk::wayland_client::protocol::wl_seat::WlSeat; use cctk::wayland_client::protocol::wl_seat::WlSeat;
use cosmic::app::{
applet::{cosmic_panel_config::PanelAnchor, CosmicAppletHelper},
Command,
};
use cosmic::cosmic_config; use cosmic::cosmic_config;
use cosmic::cosmic_config::Config; use cosmic::cosmic_config::Config;
use cosmic::iced; use cosmic::iced;
use cosmic::iced::subscription::events_with; use cosmic::iced::subscription::events_with;
use cosmic::iced::wayland::actions::data_device::DataFromMimeType; use cosmic::iced::wayland::actions::data_device::DataFromMimeType;
use cosmic::iced::wayland::actions::data_device::DndIcon; use cosmic::iced::wayland::actions::data_device::DndIcon;
use cosmic::iced::wayland::actions::window::SctkWindowSettings;
use cosmic::iced::wayland::popup::destroy_popup; use cosmic::iced::wayland::popup::destroy_popup;
use cosmic::iced::wayland::popup::get_popup; use cosmic::iced::wayland::popup::get_popup;
use cosmic::iced::widget::dnd_listener; use cosmic::iced::widget::dnd_listener;
@ -23,9 +26,7 @@ use cosmic::iced::widget::vertical_rule;
use cosmic::iced::widget::vertical_space; use cosmic::iced::widget::vertical_space;
use cosmic::iced::widget::{column, dnd_source, mouse_area, row, Column, Row}; use cosmic::iced::widget::{column, dnd_source, mouse_area, row, Column, Row};
use cosmic::iced::Color; use cosmic::iced::Color;
use cosmic::iced::Limits; use cosmic::iced::{window, Subscription};
use cosmic::iced::Settings;
use cosmic::iced::{window, Application, Command, Subscription};
use cosmic::iced_runtime::core::alignment::Horizontal; use cosmic::iced_runtime::core::alignment::Horizontal;
use cosmic::iced_runtime::core::event; use cosmic::iced_runtime::core::event;
use cosmic::iced_sctk::commands::data_device::accept_mime_type; use cosmic::iced_sctk::commands::data_device::accept_mime_type;
@ -33,16 +34,13 @@ use cosmic::iced_sctk::commands::data_device::finish_dnd;
use cosmic::iced_sctk::commands::data_device::request_dnd_data; use cosmic::iced_sctk::commands::data_device::request_dnd_data;
use cosmic::iced_sctk::commands::data_device::set_actions; use cosmic::iced_sctk::commands::data_device::set_actions;
use cosmic::iced_sctk::commands::data_device::start_drag; use cosmic::iced_sctk::commands::data_device::start_drag;
use cosmic::iced_sctk::settings::InitialSurface; use cosmic::iced_style::application;
use cosmic::iced_style::application::{self, Appearance};
use cosmic::theme::Button; use cosmic::theme::Button;
use cosmic::widget::divider; use cosmic::widget::divider;
use cosmic::widget::rectangle_tracker::rectangle_tracker_subscription; use cosmic::widget::rectangle_tracker::rectangle_tracker_subscription;
use cosmic::widget::rectangle_tracker::RectangleTracker; use cosmic::widget::rectangle_tracker::RectangleTracker;
use cosmic::widget::rectangle_tracker::RectangleUpdate; use cosmic::widget::rectangle_tracker::RectangleUpdate;
use cosmic::{Element, Theme}; use cosmic::{Element, Theme};
use cosmic_applet::cosmic_panel_config::PanelAnchor;
use cosmic_applet::CosmicAppletHelper;
use cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1; use cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1;
use freedesktop_desktop_entry::DesktopEntry; use freedesktop_desktop_entry::DesktopEntry;
use futures::future::pending; use futures::future::pending;
@ -64,30 +62,7 @@ use url::Url;
static MIME_TYPE: &str = "text/uri-list"; static MIME_TYPE: &str = "text/uri-list";
pub fn run() -> cosmic::iced::Result { pub fn run() -> cosmic::iced::Result {
let helper = CosmicAppletHelper::default(); cosmic::app::applet::run::<CosmicAppList>(false, ())
let pixel_size = helper.suggested_size().0;
let padding = 8;
let dot_size = 4;
let spacing = 4;
let thickness = (pixel_size + 2 * padding + dot_size + spacing) as u32;
let (w, h) = match helper.anchor {
PanelAnchor::Top | PanelAnchor::Bottom => (2000, thickness),
PanelAnchor::Left | PanelAnchor::Right => (thickness, 2000),
};
CosmicAppList::run(Settings {
initial_surface: InitialSurface::XdgWindow(SctkWindowSettings {
autosize: true,
size_limits: Limits::NONE
.min_height(1.0)
.min_width(1.0)
.max_height(h as f32)
.max_width(w as f32),
resizable: None,
..Default::default()
}),
..Default::default()
})
} }
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
@ -150,7 +125,7 @@ impl DockItem {
.map(|_| { .map(|_| {
container(vertical_space(Length::Fixed(0.0))) container(vertical_space(Length::Fixed(0.0)))
.padding(dot_radius) .padding(dot_radius)
.style(<<CosmicAppList as cosmic::iced::Application>::Theme as container::StyleSheet>::Style::Custom(Box::new( .style(<Theme as container::StyleSheet>::Style::Custom(Box::new(
|theme| container::Appearance { |theme| container::Appearance {
text_color: Some(Color::TRANSPARENT), text_color: Some(Color::TRANSPARENT),
background: Some(Background::Color( background: Some(Background::Color(
@ -224,7 +199,7 @@ struct DndOffer {
#[derive(Clone, Default)] #[derive(Clone, Default)]
struct CosmicAppList { struct CosmicAppList {
theme: Theme, core: cosmic::app::Core,
popup: Option<(window::Id, DockItem)>, popup: Option<(window::Id, DockItem)>,
surface_id_ctr: u128, surface_id_ctr: u128,
subscription_ctr: u32, subscription_ctr: u32,
@ -234,7 +209,6 @@ struct CosmicAppList {
dnd_source: Option<(window::Id, DockItem, DndAction)>, dnd_source: Option<(window::Id, DockItem, DndAction)>,
config: AppListConfig, config: AppListConfig,
toplevel_sender: Option<Sender<ToplevelRequest>>, toplevel_sender: Option<Sender<ToplevelRequest>>,
applet_helper: CosmicAppletHelper,
seat: Option<WlSeat>, seat: Option<WlSeat>,
rectangle_tracker: Option<RectangleTracker<u32>>, rectangle_tracker: Option<RectangleTracker<u32>>,
rectangles: HashMap<u32, iced::Rectangle>, rectangles: HashMap<u32, iced::Rectangle>,
@ -268,7 +242,6 @@ enum Message {
StopListeningForDnd, StopListeningForDnd,
IncrementSubscriptionCtr, IncrementSubscriptionCtr,
ConfigUpdated(AppListConfig), ConfigUpdated(AppListConfig),
Theme(Theme),
} }
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
@ -317,7 +290,16 @@ fn desktop_info_for_app_ids(mut app_ids: Vec<String>) -> Vec<DesktopInfo> {
}) })
.collect_vec(), .collect_vec(),
); );
ret.sort_by(|a, b| app_ids_clone.iter().position(|id| id == &a.id || id.eq(&a.name)).cmp(&app_ids_clone.iter().position(|id| id == &b.id || id.eq(&b.name)))); ret.sort_by(|a, b| {
app_ids_clone
.iter()
.position(|id| id == &a.id || id.eq(&a.name))
.cmp(
&app_ids_clone
.iter()
.position(|id| id == &b.id || id.eq(&b.name)),
)
});
ret ret
} }
@ -364,17 +346,16 @@ fn index_in_list(
} }
} }
impl Application for CosmicAppList { impl cosmic::Application for CosmicAppList {
type Message = Message; type Message = Message;
type Theme = Theme;
type Executor = cosmic::SingleThreadExecutor; type Executor = cosmic::SingleThreadExecutor;
type Flags = (); type Flags = ();
const APP_ID: &'static str = config::APP_ID;
fn new(_flags: ()) -> (Self, Command<Message>) { fn init(core: cosmic::app::Core, _flags: ()) -> (Self, Command<Message>) {
let config = config::AppListConfig::load().unwrap_or_default(); let config = config::AppListConfig::load().unwrap_or_default();
let helper = CosmicAppletHelper::default();
let theme = helper.theme();
let mut self_ = CosmicAppList { let mut self_ = CosmicAppList {
core,
favorite_list: desktop_info_for_app_ids(config.favorites.clone()) favorite_list: desktop_info_for_app_ids(config.favorites.clone())
.into_iter() .into_iter()
.enumerate() .enumerate()
@ -384,9 +365,7 @@ impl Application for CosmicAppList {
desktop_info: e, desktop_info: e,
}) })
.collect(), .collect(),
applet_helper: helper,
config, config,
theme,
..Default::default() ..Default::default()
}; };
self_.item_ctr = self_.favorite_list.len() as u32; self_.item_ctr = self_.favorite_list.len() as u32;
@ -394,8 +373,12 @@ impl Application for CosmicAppList {
(self_, Command::none()) (self_, Command::none())
} }
fn title(&self) -> String { fn core(&self) -> &cosmic::app::Core {
config::APP_ID.to_string() &self.core
}
fn core_mut(&mut self) -> &mut cosmic::app::Core {
&mut self.core
} }
fn update(&mut self, message: Message) -> Command<Message> { fn update(&mut self, message: Message) -> Command<Message> {
@ -419,7 +402,7 @@ impl Application for CosmicAppList {
let new_id = window::Id(self.surface_id_ctr); let new_id = window::Id(self.surface_id_ctr);
self.popup = Some((new_id, toplevel_group.clone())); self.popup = Some((new_id, toplevel_group.clone()));
let mut popup_settings = self.applet_helper.get_popup_settings( let mut popup_settings = self.core.applet_helper.get_popup_settings(
window::Id(0), window::Id(0),
new_id, new_id,
None, None,
@ -562,8 +545,8 @@ impl Application for CosmicAppList {
} }
} }
Message::DndEnter(x, y) => { Message::DndEnter(x, y) => {
let item_size = self.applet_helper.suggested_size().0; let item_size = self.core.applet_helper.suggested_size().0;
let pos_in_list = match self.applet_helper.anchor { let pos_in_list = match self.core.applet_helper.anchor {
PanelAnchor::Top | PanelAnchor::Bottom => x, PanelAnchor::Top | PanelAnchor::Bottom => x,
PanelAnchor::Left | PanelAnchor::Right => y, PanelAnchor::Left | PanelAnchor::Right => y,
}; };
@ -593,8 +576,8 @@ impl Application for CosmicAppList {
} }
Message::DndMotion(x, y) => { Message::DndMotion(x, y) => {
if let Some(DndOffer { preview_index, .. }) = self.dnd_offer.as_mut() { if let Some(DndOffer { preview_index, .. }) = self.dnd_offer.as_mut() {
let item_size = self.applet_helper.suggested_size().0; let item_size = self.core.applet_helper.suggested_size().0;
let pos_in_list = match self.applet_helper.anchor { let pos_in_list = match self.core.applet_helper.anchor {
PanelAnchor::Top | PanelAnchor::Bottom => x, PanelAnchor::Top | PanelAnchor::Bottom => x,
PanelAnchor::Left | PanelAnchor::Right => y, PanelAnchor::Left | PanelAnchor::Right => y,
}; };
@ -646,7 +629,7 @@ impl Application for CosmicAppList {
.and_then(|o| o.dock_item.map(|i| (i, o.preview_index))) .and_then(|o| o.dock_item.map(|i| (i, o.preview_index)))
{ {
self.item_ctr += 1; self.item_ctr += 1;
if let Some((pos, is_favorite)) = self if let Some((pos, is_favorite)) = self
.active_list .active_list
.iter() .iter()
@ -674,7 +657,13 @@ impl Application for CosmicAppList {
self.favorite_list self.favorite_list
.insert(index.min(self.favorite_list.len()), dock_item); .insert(index.min(self.favorite_list.len()), dock_item);
self.config.update_favorites(self.favorite_list.iter().map(|dock_item| dock_item.desktop_info.id.clone()).collect(), &Config::new(APP_ID, 1).unwrap()); self.config.update_favorites(
self.favorite_list
.iter()
.map(|dock_item| dock_item.desktop_info.id.clone())
.collect(),
&Config::new(APP_ID, 1).unwrap(),
);
} }
return finish_dnd(); return finish_dnd();
} }
@ -715,7 +704,7 @@ impl Application for CosmicAppList {
let subscription_ctr = self.subscription_ctr; let subscription_ctr = self.subscription_ctr;
let mut rng = thread_rng(); let mut rng = thread_rng();
let rand_d = rng.gen_range(0..100); let rand_d = rng.gen_range(0..100);
return Command::perform( return iced::Command::perform(
async move { async move {
if let Some(millis) = 2u64 if let Some(millis) = 2u64
.checked_pow(subscription_ctr) .checked_pow(subscription_ctr)
@ -727,7 +716,8 @@ impl Application for CosmicAppList {
} }
}, },
|_| Message::IncrementSubscriptionCtr, |_| Message::IncrementSubscriptionCtr,
); )
.map(cosmic::app::message::app);
} }
ToplevelUpdate::RemoveToplevel(handle) => { ToplevelUpdate::RemoveToplevel(handle) => {
for t in self for t in self
@ -810,111 +800,41 @@ impl Application for CosmicAppList {
} }
// pull back configured items into the favorites list // pull back configured items into the favorites list
self.favorite_list = desktop_info_for_app_ids(self.config.favorites.clone()).into_iter().map(|new_dock_item| { self.favorite_list = desktop_info_for_app_ids(self.config.favorites.clone())
if let Some(p) = self.active_list.iter().position(|dock_item| { .into_iter()
dock_item.desktop_info.id == new_dock_item.id .map(|new_dock_item| {
}) { if let Some(p) = self
self.active_list.remove(p) .active_list
} else { .iter()
DockItem { .position(|dock_item| dock_item.desktop_info.id == new_dock_item.id)
id: self.item_ctr, {
toplevels: Default::default(), self.active_list.remove(p)
desktop_info: new_dock_item, } else {
DockItem {
id: self.item_ctr,
toplevels: Default::default(),
desktop_info: new_dock_item,
}
} }
} })
}).collect(); .collect();
}
Message::Theme(t) => {
self.theme = t;
} }
} }
Command::none() Command::none()
} }
fn view(&self, id: window::Id) -> Element<Message> { fn view(&self) -> Element<Message> {
let is_horizontal = match self.applet_helper.anchor { let is_horizontal = match self.core.applet_helper.anchor {
PanelAnchor::Top | PanelAnchor::Bottom => true, PanelAnchor::Top | PanelAnchor::Bottom => true,
PanelAnchor::Left | PanelAnchor::Right => false, PanelAnchor::Left | PanelAnchor::Right => false,
}; };
if let Some((_, item, _)) = self.dnd_source.as_ref().filter(|s| s.0 == id) {
return cosmic::widget::icon(
Path::new(&item.desktop_info.icon),
self.applet_helper.suggested_size().0,
)
.into();
}
if let Some((
_popup_id,
DockItem {
toplevels,
desktop_info,
..
},
)) = self.popup.as_ref().filter(|p| id == p.0)
{
let is_favorite = self.config.favorites.contains(&desktop_info.id)
|| self.config.favorites.contains(&desktop_info.name);
let mut content = column![
iced::widget::text(&desktop_info.name).horizontal_alignment(Horizontal::Center),
cosmic::widget::button(Button::Text)
.custom(vec![iced::widget::text(fl!("new-window")).into()])
.on_press(Message::Exec(desktop_info.exec.clone())),
]
.padding(8)
.spacing(4)
.align_items(Alignment::Center);
if !toplevels.is_empty() {
let mut list_col = column![];
for (handle, info) in toplevels {
let title = if info.title.len() > 20 {
format!("{:.24}...", &info.title)
} else {
info.title.clone()
};
list_col = list_col.push(
cosmic::widget::button(Button::Text)
.custom(vec![iced::widget::text(title).into()])
.on_press(Message::Activate(handle.clone())),
);
}
content = content.push(divider::horizontal::light());
content = content.push(list_col);
content = content.push(divider::horizontal::light());
}
content = content.push(if is_favorite {
cosmic::widget::button(Button::Text)
.custom(vec![iced::widget::text(fl!("unfavorite")).into()])
.on_press(Message::UnFavorite(desktop_info.id.clone()))
} else {
cosmic::widget::button(Button::Text)
.custom(vec![iced::widget::text(fl!("favorite")).into()])
.on_press(Message::Favorite(desktop_info.id.clone()))
});
content = match toplevels.len() {
0 => content,
1 => content.push(
cosmic::widget::button(Button::Text)
.custom(vec![iced::widget::text(fl!("quit")).into()])
.on_press(Message::Quit(desktop_info.id.clone())),
),
_ => content.push(
cosmic::widget::button(Button::Text)
.custom(vec![iced::widget::text(&fl!("quit-all")).into()])
.on_press(Message::Quit(desktop_info.id.clone())),
),
};
return self.applet_helper.popup_container(content).into();
}
let mut favorites: Vec<_> = self let mut favorites: Vec<_> = self
.favorite_list .favorite_list
.iter() .iter()
.map(|dock_item| { .map(|dock_item| {
dock_item.as_icon( dock_item.as_icon(
&self.applet_helper, &self.core.applet_helper,
self.rectangle_tracker.as_ref(), self.rectangle_tracker.as_ref(),
self.popup.is_none(), self.popup.is_none(),
) )
@ -926,13 +846,13 @@ impl Application for CosmicAppList {
.as_ref() .as_ref()
.and_then(|o| o.dock_item.as_ref().map(|item| (item, o.preview_index))) .and_then(|o| o.dock_item.as_ref().map(|item| (item, o.preview_index)))
{ {
favorites.insert(index, item.as_icon(&self.applet_helper, None, false)); favorites.insert(index, item.as_icon(&self.core.applet_helper, None, false));
} else if self.is_listening_for_dnd && self.favorite_list.is_empty() { } else if self.is_listening_for_dnd && self.favorite_list.is_empty() {
// show star indicating favorite_list is drag target // show star indicating favorite_list is drag target
favorites.push( favorites.push(
container(cosmic::widget::icon( container(cosmic::widget::icon(
"starred-symbolic.symbolic", "starred-symbolic.symbolic",
self.applet_helper.suggested_size().0, self.core.applet_helper.suggested_size().0,
)) ))
.padding(8) .padding(8)
.into(), .into(),
@ -944,7 +864,7 @@ impl Application for CosmicAppList {
.iter() .iter()
.map(|dock_item| { .map(|dock_item| {
dock_item.as_icon( dock_item.as_icon(
&self.applet_helper, &self.core.applet_helper,
self.rectangle_tracker.as_ref(), self.rectangle_tracker.as_ref(),
self.popup.is_none(), self.popup.is_none(),
) )
@ -1011,12 +931,12 @@ impl Application for CosmicAppList {
} else { } else {
vec![cosmic::widget::icon( vec![cosmic::widget::icon(
"com.system76.CosmicAppList", "com.system76.CosmicAppList",
self.applet_helper.suggested_size().0, self.core.applet_helper.suggested_size().0,
) )
.into()] .into()]
}; };
let content = match &self.applet_helper.anchor { let content = match &self.core.applet_helper.anchor {
PanelAnchor::Left | PanelAnchor::Right => container( PanelAnchor::Left | PanelAnchor::Right => container(
Column::with_children(content_list) Column::with_children(content_list)
.spacing(4) .spacing(4)
@ -1042,9 +962,83 @@ impl Application for CosmicAppList {
} }
} }
fn view_window(&self, id: window::Id) -> Element<Message> {
if let Some((_, item, _)) = self.dnd_source.as_ref().filter(|s| s.0 == id) {
cosmic::widget::icon(
Path::new(&item.desktop_info.icon),
self.core.applet_helper.suggested_size().0,
)
.into()
} else if let Some((
_popup_id,
DockItem {
toplevels,
desktop_info,
..
},
)) = self.popup.as_ref().filter(|p| id == p.0)
{
let is_favorite = self.config.favorites.contains(&desktop_info.id)
|| self.config.favorites.contains(&desktop_info.name);
let mut content = column![
iced::widget::text(&desktop_info.name).horizontal_alignment(Horizontal::Center),
cosmic::widget::button(Button::Text)
.custom(vec![iced::widget::text(fl!("new-window")).into()])
.on_press(Message::Exec(desktop_info.exec.clone())),
]
.padding(8)
.spacing(4)
.align_items(Alignment::Center);
if !toplevels.is_empty() {
let mut list_col = column![];
for (handle, info) in toplevels {
let title = if info.title.len() > 20 {
format!("{:.24}...", &info.title)
} else {
info.title.clone()
};
list_col = list_col.push(
cosmic::widget::button(Button::Text)
.custom(vec![iced::widget::text(title).into()])
.on_press(Message::Activate(handle.clone())),
);
}
content = content.push(divider::horizontal::light());
content = content.push(list_col);
content = content.push(divider::horizontal::light());
}
content = content.push(if is_favorite {
cosmic::widget::button(Button::Text)
.custom(vec![iced::widget::text(fl!("unfavorite")).into()])
.on_press(Message::UnFavorite(desktop_info.id.clone()))
} else {
cosmic::widget::button(Button::Text)
.custom(vec![iced::widget::text(fl!("favorite")).into()])
.on_press(Message::Favorite(desktop_info.id.clone()))
});
content = match toplevels.len() {
0 => content,
1 => content.push(
cosmic::widget::button(Button::Text)
.custom(vec![iced::widget::text(fl!("quit")).into()])
.on_press(Message::Quit(desktop_info.id.clone())),
),
_ => content.push(
cosmic::widget::button(Button::Text)
.custom(vec![iced::widget::text(&fl!("quit-all")).into()])
.on_press(Message::Quit(desktop_info.id.clone())),
),
};
self.core.applet_helper.popup_container(content).into()
} else {
iced::widget::text("").into()
}
}
fn subscription(&self) -> Subscription<Message> { fn subscription(&self) -> Subscription<Message> {
Subscription::batch(vec![ Subscription::batch(vec![
self.applet_helper.theme_subscription(0).map(Message::Theme),
toplevel_subscription(self.subscription_ctr).map(Message::Toplevel), toplevel_subscription(self.subscription_ctr).map(Message::Toplevel),
events_with(|e, _| match e { events_with(|e, _| match e {
cosmic::iced_runtime::core::Event::PlatformSpecific( cosmic::iced_runtime::core::Event::PlatformSpecific(
@ -1095,18 +1089,7 @@ impl Application for CosmicAppList {
]) ])
} }
fn theme(&self) -> Theme { fn style(&self) -> Option<<Theme as application::StyleSheet>::Style> {
self.theme.clone() Some(cosmic::app::applet::style())
}
fn close_requested(&self, _id: window::Id) -> Self::Message {
Message::Ignore
}
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|theme| Appearance {
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().on_bg_color().into(),
}))
} }
} }

View file

@ -12,7 +12,6 @@ libpulse-glib-binding = "2.25.0"
tokio = { version = "1.20.1", features=["full"] } tokio = { version = "1.20.1", features=["full"] }
libcosmic.workspace = true libcosmic.workspace = true
cosmic-time.workspace = true cosmic-time.workspace = true
cosmic-applet = { path = "../applet" }
log = "0.4.14" log = "0.4.14"
pretty_env_logger = "0.4.0" pretty_env_logger = "0.4.0"
# Application i18n # Application i18n

View file

@ -1,10 +1,10 @@
// SPDX-License-Identifier: MPL-2.0-only // SPDX-License-Identifier: MPL-2.0-only
use cosmic_time::once_cell::sync::Lazy;
use i18n_embed::{ use i18n_embed::{
fluent::{fluent_language_loader, FluentLanguageLoader}, fluent::{fluent_language_loader, FluentLanguageLoader},
DefaultLocalizer, LanguageLoader, Localizer, DefaultLocalizer, LanguageLoader, Localizer,
}; };
use cosmic_time::once_cell::sync::Lazy;
use rust_embed::RustEmbed; use rust_embed::RustEmbed;
#[derive(RustEmbed)] #[derive(RustEmbed)]

View file

@ -1,26 +1,26 @@
mod localize; mod localize;
use cosmic::app::Command;
use cosmic::iced::widget; use cosmic::iced::widget;
use cosmic::iced::Limits; use cosmic::iced::Limits;
use cosmic::iced_runtime::core::alignment::Horizontal; use cosmic::iced_runtime::core::alignment::Horizontal;
use cosmic::theme::Svg; use cosmic::theme::Svg;
use cosmic::app::applet::applet_button_theme;
use cosmic::widget::{button, divider, icon}; use cosmic::widget::{button, divider, icon};
use cosmic::Renderer; use cosmic::Renderer;
use cosmic_applet::{applet_button_theme, CosmicAppletHelper};
use cosmic::iced::{ use cosmic::iced::{
self, self,
widget::{column, row, slider, text}, widget::{column, row, slider, text},
window, Alignment, Application, Command, Length, Subscription, window, Alignment, Length, Subscription,
}; };
use cosmic::iced_style::application::{self, Appearance}; use cosmic::iced_style::application;
use cosmic::{Element, Theme}; use cosmic::{Element, Theme};
use cosmic_time::{anim, chain, id, once_cell::sync::Lazy, Instant, Timeline}; use cosmic_time::{anim, chain, id, once_cell::sync::Lazy, Instant, Timeline};
use iced::wayland::popup::{destroy_popup, get_popup}; use iced::wayland::popup::{destroy_popup, get_popup};
use iced::widget::container; use iced::widget::container;
use iced::Color;
mod pulse; mod pulse;
use crate::localize::localize; use crate::localize::localize;
@ -33,24 +33,22 @@ pub fn main() -> cosmic::iced::Result {
// Prepare i18n // Prepare i18n
localize(); localize();
let helper = CosmicAppletHelper::default(); cosmic::app::applet::run::<Audio>(false, ())
Audio::run(helper.window_settings())
} }
static SHOW_MEDIA_CONTROLS: Lazy<id::Toggler> = Lazy::new(id::Toggler::unique); static SHOW_MEDIA_CONTROLS: Lazy<id::Toggler> = Lazy::new(id::Toggler::unique);
#[derive(Default)] #[derive(Default)]
struct Audio { struct Audio {
core: cosmic::app::Core,
is_open: IsOpen, is_open: IsOpen,
current_output: Option<DeviceInfo>, current_output: Option<DeviceInfo>,
current_input: Option<DeviceInfo>, current_input: Option<DeviceInfo>,
outputs: Vec<DeviceInfo>, outputs: Vec<DeviceInfo>,
inputs: Vec<DeviceInfo>, inputs: Vec<DeviceInfo>,
pulse_state: PulseState, pulse_state: PulseState,
applet_helper: CosmicAppletHelper,
icon_name: String, icon_name: String,
input_icon_name: String, input_icon_name: String,
theme: Theme,
popup: Option<window::Id>, popup: Option<window::Id>,
show_media_controls_in_top_panel: bool, show_media_controls_in_top_panel: bool,
id_ctr: u128, id_ctr: u128,
@ -125,24 +123,21 @@ enum Message {
OutputChanged(String), OutputChanged(String),
InputChanged(String), InputChanged(String),
Pulse(pulse::Event), Pulse(pulse::Event),
Ignore,
TogglePopup, TogglePopup,
ToggleMediaControlsInTopPanel(chain::Toggler, bool), ToggleMediaControlsInTopPanel(chain::Toggler, bool),
Frame(Instant), Frame(Instant),
Theme(Theme),
} }
impl Application for Audio { impl cosmic::Application for Audio {
type Message = Message; type Message = Message;
type Theme = Theme;
type Executor = cosmic::SingleThreadExecutor; type Executor = cosmic::SingleThreadExecutor;
type Flags = (); type Flags = ();
const APP_ID: &'static str = "com.system76.CosmicAppletAudio";
fn new(_flags: ()) -> (Audio, Command<Message>) { fn init(core: cosmic::app::Core, _flags: ()) -> (Audio, Command<Message>) {
let applet_helper = CosmicAppletHelper::default();
let theme = applet_helper.theme();
( (
Audio { Audio {
core,
is_open: IsOpen::None, is_open: IsOpen::None,
current_output: None, current_output: None,
current_input: None, current_input: None,
@ -150,31 +145,22 @@ impl Application for Audio {
inputs: vec![], inputs: vec![],
icon_name: "audio-volume-high-symbolic".to_string(), icon_name: "audio-volume-high-symbolic".to_string(),
input_icon_name: "audio-input-microphone-symbolic".to_string(), input_icon_name: "audio-input-microphone-symbolic".to_string(),
applet_helper,
theme,
..Default::default() ..Default::default()
}, },
Command::none(), Command::none(),
) )
} }
fn title(&self) -> String { fn core(&self) -> &cosmic::app::Core {
String::from("Audio") &self.core
} }
fn theme(&self) -> Theme { fn core_mut(&mut self) -> &mut cosmic::app::Core {
self.theme.clone() &mut self.core
} }
fn close_requested(&self, _id: window::Id) -> Self::Message { fn style(&self) -> Option<<Theme as application::StyleSheet>::Style> {
Message::Ignore Some(cosmic::app::applet::style())
}
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|theme| Appearance {
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().on_bg_color().into(),
}))
} }
fn update(&mut self, message: Message) -> Command<Message> { fn update(&mut self, message: Message) -> Command<Message> {
@ -191,7 +177,7 @@ impl Application for Audio {
let new_id = window::Id(self.id_ctr); let new_id = window::Id(self.id_ctr);
self.popup.replace(new_id); self.popup.replace(new_id);
let mut popup_settings = self.applet_helper.get_popup_settings( let mut popup_settings = self.core.applet_helper.get_popup_settings(
window::Id(0), window::Id(0),
new_id, new_id,
None, None,
@ -327,14 +313,10 @@ impl Application for Audio {
} }
pulse::Event::Disconnected => self.pulse_state.disconnected(), pulse::Event::Disconnected => self.pulse_state.disconnected(),
}, },
Message::Ignore => {}
Message::ToggleMediaControlsInTopPanel(chain, enabled) => { Message::ToggleMediaControlsInTopPanel(chain, enabled) => {
self.timeline.set_chain(chain).start(); self.timeline.set_chain(chain).start();
self.show_media_controls_in_top_panel = enabled; self.show_media_controls_in_top_panel = enabled;
} }
Message::Theme(t) => {
self.theme = t;
}
}; };
Command::none() Command::none()
@ -342,7 +324,6 @@ impl Application for Audio {
fn subscription(&self) -> Subscription<Message> { fn subscription(&self) -> Subscription<Message> {
Subscription::batch(vec![ Subscription::batch(vec![
self.applet_helper.theme_subscription(0).map(Message::Theme),
pulse::connect().map(Message::Pulse), pulse::connect().map(Message::Pulse),
self.timeline self.timeline
.as_subscription() .as_subscription()
@ -350,134 +331,136 @@ impl Application for Audio {
]) ])
} }
fn view(&self, id: window::Id) -> Element<Message> { fn view(&self) -> Element<Message> {
if id == window::Id(0) { self.core
self.applet_helper .applet_helper
.icon_button(&self.icon_name) .icon_button(&self.icon_name)
.on_press(Message::TogglePopup) .on_press(Message::TogglePopup)
.into() .into()
} else { }
let audio_disabled = matches!(self.pulse_state, PulseState::Disconnected(_));
let out_f64 = VolumeLinear::from(
self.current_output
.as_ref()
.map(|o| o.volume.avg())
.unwrap_or_default(),
)
.0 * 100.0;
let in_f64 = VolumeLinear::from(
self.current_input
.as_ref()
.map(|o| o.volume.avg())
.unwrap_or_default(),
)
.0 * 100.0;
let audio_content = if audio_disabled { fn view_window(&self, _id: window::Id) -> Element<Message> {
column![text(fl!("disconnected")) let audio_disabled = matches!(self.pulse_state, PulseState::Disconnected(_));
.width(Length::Fill) let out_f64 = VolumeLinear::from(
.horizontal_alignment(Horizontal::Center) self.current_output
.size(24),] .as_ref()
} else { .map(|o| o.volume.avg())
column![ .unwrap_or_default(),
row![ )
icon(self.icon_name.as_str(), 24).style(Svg::Symbolic), .0 * 100.0;
slider(0.0..=100.0, out_f64, Message::SetOutputVolume) let in_f64 = VolumeLinear::from(
.width(Length::FillPortion(5)), self.current_input
text(format!("{}%", out_f64.round())) .as_ref()
.size(16) .map(|o| o.volume.avg())
.width(Length::FillPortion(1)) .unwrap_or_default(),
.horizontal_alignment(Horizontal::Right) )
] .0 * 100.0;
.spacing(12)
.align_items(Alignment::Center) let audio_content = if audio_disabled {
.padding([8, 24]), column![text(fl!("disconnected"))
row![ .width(Length::Fill)
icon(self.input_icon_name.as_str(), 24).style(Svg::Symbolic), .horizontal_alignment(Horizontal::Center)
slider(0.0..=100.0, in_f64, Message::SetInputVolume) .size(24),]
.width(Length::FillPortion(5)), } else {
text(format!("{}%", in_f64.round())) column![
.size(16) row![
.width(Length::FillPortion(1)) icon(self.icon_name.as_str(), 24).style(Svg::Symbolic),
.horizontal_alignment(Horizontal::Right) slider(0.0..=100.0, out_f64, Message::SetOutputVolume)
] .width(Length::FillPortion(5)),
.spacing(12) text(format!("{}%", out_f64.round()))
.align_items(Alignment::Center) .size(16)
.padding([8, 24]), .width(Length::FillPortion(1))
container(divider::horizontal::light()) .horizontal_alignment(Horizontal::Right)
.padding([12, 24])
.width(Length::Fill),
revealer(
self.is_open == IsOpen::Output,
fl!("output"),
match &self.current_output {
Some(output) => pretty_name(output.description.clone()),
None => String::from("No device selected"),
},
self.outputs
.clone()
.into_iter()
.map(|output| (
output.name.clone().unwrap_or_default(),
pretty_name(output.description)
))
.collect(),
Message::OutputToggle,
Message::OutputChanged,
),
revealer(
self.is_open == IsOpen::Input,
fl!("input"),
match &self.current_input {
Some(input) => pretty_name(input.description.clone()),
None => fl!("no-device"),
},
self.inputs
.clone()
.into_iter()
.map(|input| (
input.name.clone().unwrap_or_default(),
pretty_name(input.description)
))
.collect(),
Message::InputToggle,
Message::InputChanged,
)
] ]
.align_items(Alignment::Start) .spacing(12)
}; .align_items(Alignment::Center)
let content = column![ .padding([8, 24]),
audio_content, row![
icon(self.input_icon_name.as_str(), 24).style(Svg::Symbolic),
slider(0.0..=100.0, in_f64, Message::SetInputVolume)
.width(Length::FillPortion(5)),
text(format!("{}%", in_f64.round()))
.size(16)
.width(Length::FillPortion(1))
.horizontal_alignment(Horizontal::Right)
]
.spacing(12)
.align_items(Alignment::Center)
.padding([8, 24]),
container(divider::horizontal::light()) container(divider::horizontal::light())
.padding([12, 24]) .padding([12, 24])
.width(Length::Fill), .width(Length::Fill),
container( revealer(
anim!( self.is_open == IsOpen::Output,
// toggler fl!("output"),
SHOW_MEDIA_CONTROLS, match &self.current_output {
&self.timeline, Some(output) => pretty_name(output.description.clone()),
Some(fl!("show-media-controls")), None => String::from("No device selected"),
self.show_media_controls_in_top_panel, },
Message::ToggleMediaControlsInTopPanel, self.outputs
) .clone()
.text_size(14) .into_iter()
.map(|output| (
output.name.clone().unwrap_or_default(),
pretty_name(output.description)
))
.collect(),
Message::OutputToggle,
Message::OutputChanged,
),
revealer(
self.is_open == IsOpen::Input,
fl!("input"),
match &self.current_input {
Some(input) => pretty_name(input.description.clone()),
None => fl!("no-device"),
},
self.inputs
.clone()
.into_iter()
.map(|input| (
input.name.clone().unwrap_or_default(),
pretty_name(input.description)
))
.collect(),
Message::InputToggle,
Message::InputChanged,
) )
.padding([0, 24]),
container(divider::horizontal::light())
.padding([12, 24])
.width(Length::Fill),
button(applet_button_theme())
.custom(vec![text(fl!("sound-settings")).size(14).into()])
.padding([8, 24])
.width(Length::Fill)
] ]
.align_items(Alignment::Start) .align_items(Alignment::Start)
.padding([8, 0]); };
let content = column![
audio_content,
container(divider::horizontal::light())
.padding([12, 24])
.width(Length::Fill),
container(
anim!(
// toggler
SHOW_MEDIA_CONTROLS,
&self.timeline,
Some(fl!("show-media-controls")),
self.show_media_controls_in_top_panel,
Message::ToggleMediaControlsInTopPanel,
)
.text_size(14)
)
.padding([0, 24]),
container(divider::horizontal::light())
.padding([12, 24])
.width(Length::Fill),
button(applet_button_theme())
.custom(vec![text(fl!("sound-settings")).size(14).into()])
.padding([8, 24])
.width(Length::Fill)
]
.align_items(Alignment::Start)
.padding([8, 0]);
self.applet_helper self.core
.popup_container(container(content)) .applet_helper
.into() .popup_container(container(content))
} .into()
} }
} }

View file

@ -7,7 +7,6 @@ edition = "2021"
once_cell = "1.16.0" once_cell = "1.16.0"
libcosmic.workspace = true libcosmic.workspace = true
cosmic-time.workspace = true cosmic-time.workspace = true
cosmic-applet = { path = "../applet" }
futures = "0.3" futures = "0.3"
zbus = { version = "3.13", default-features = false, features = ["tokio"] } zbus = { version = "3.13", default-features = false, features = ["tokio"] }
log = "0.4" log = "0.4"

View file

@ -10,19 +10,18 @@ use crate::upower_device::{device_subscription, DeviceDbusEvent};
use crate::upower_kbdbacklight::{ use crate::upower_kbdbacklight::{
kbd_backlight_subscription, KeyboardBacklightRequest, KeyboardBacklightUpdate, kbd_backlight_subscription, KeyboardBacklightRequest, KeyboardBacklightUpdate,
}; };
use cosmic::app::{applet::applet_button_theme, Command};
use cosmic::iced::alignment::Horizontal; use cosmic::iced::alignment::Horizontal;
use cosmic::iced::wayland::popup::{destroy_popup, get_popup}; use cosmic::iced::wayland::popup::{destroy_popup, get_popup};
use cosmic::iced::Color;
use cosmic::iced::{ use cosmic::iced::{
widget::{column, container, row, slider, text}, widget::{column, container, row, slider, text},
window, Alignment, Application, Command, Length, Subscription, window, Alignment, Length, Subscription,
}; };
use cosmic::iced_runtime::core::layout::Limits; use cosmic::iced_runtime::core::layout::Limits;
use cosmic::iced_style::application::{self, Appearance}; use cosmic::iced_style::application;
use cosmic::theme::Svg; use cosmic::theme::Svg;
use cosmic::widget::{button, divider, icon}; use cosmic::widget::{button, divider, icon};
use cosmic::{Element, Theme}; use cosmic::{Element, Theme};
use cosmic_applet::{applet_button_theme, CosmicAppletHelper};
use cosmic_time::{anim, chain, id, once_cell::sync::Lazy, Instant, Timeline}; use cosmic_time::{anim, chain, id, once_cell::sync::Lazy, Instant, Timeline};
use log::error; use log::error;
@ -46,17 +45,16 @@ fn format_duration(duration: Duration) -> String {
} }
pub fn run() -> cosmic::iced::Result { pub fn run() -> cosmic::iced::Result {
let helper = CosmicAppletHelper::default(); cosmic::app::applet::run::<CosmicBatteryApplet>(false, ())
CosmicBatteryApplet::run(helper.window_settings())
} }
static MAX_CHARGE: Lazy<id::Toggler> = Lazy::new(id::Toggler::unique); static MAX_CHARGE: Lazy<id::Toggler> = Lazy::new(id::Toggler::unique);
#[derive(Clone, Default)] #[derive(Clone, Default)]
struct CosmicBatteryApplet { struct CosmicBatteryApplet {
core: cosmic::app::Core,
icon_name: String, icon_name: String,
display_icon_name: String, display_icon_name: String,
theme: Theme,
charging_limit: bool, charging_limit: bool,
battery_percent: f64, battery_percent: f64,
on_battery: bool, on_battery: bool,
@ -67,7 +65,6 @@ struct CosmicBatteryApplet {
id_ctr: u128, id_ctr: u128,
screen_sender: Option<UnboundedSender<ScreenBacklightRequest>>, screen_sender: Option<UnboundedSender<ScreenBacklightRequest>>,
kbd_sender: Option<UnboundedSender<KeyboardBacklightRequest>>, kbd_sender: Option<UnboundedSender<KeyboardBacklightRequest>>,
applet_helper: CosmicAppletHelper,
power_profile: Power, power_profile: Power,
power_profile_sender: Option<UnboundedSender<PowerProfileRequest>>, power_profile_sender: Option<UnboundedSender<PowerProfileRequest>>,
timeline: Timeline, timeline: Timeline,
@ -144,37 +141,36 @@ enum Message {
InitKbdBacklight(UnboundedSender<KeyboardBacklightRequest>, f64), InitKbdBacklight(UnboundedSender<KeyboardBacklightRequest>, f64),
InitScreenBacklight(UnboundedSender<ScreenBacklightRequest>, f64), InitScreenBacklight(UnboundedSender<ScreenBacklightRequest>, f64),
Errored(String), Errored(String),
Ignore,
InitProfile(UnboundedSender<PowerProfileRequest>, Power), InitProfile(UnboundedSender<PowerProfileRequest>, Power),
Profile(Power), Profile(Power),
SelectProfile(Power), SelectProfile(Power),
Frame(Instant), Frame(Instant),
Theme(Theme),
} }
impl Application for CosmicBatteryApplet { impl cosmic::Application for CosmicBatteryApplet {
type Message = Message; type Message = Message;
type Theme = Theme;
type Executor = cosmic::SingleThreadExecutor; type Executor = cosmic::SingleThreadExecutor;
type Flags = (); type Flags = ();
const APP_ID: &'static str = config::APP_ID;
fn new(_flags: ()) -> (Self, Command<Message>) { fn init(core: cosmic::app::Core, _flags: ()) -> (Self, Command<Message>) {
let applet_helper = CosmicAppletHelper::default();
let theme = applet_helper.theme();
( (
CosmicBatteryApplet { CosmicBatteryApplet {
core,
icon_name: "battery-symbolic".to_string(), icon_name: "battery-symbolic".to_string(),
display_icon_name: "display-brightness-symbolic".to_string(), display_icon_name: "display-brightness-symbolic".to_string(),
applet_helper,
theme,
..Default::default() ..Default::default()
}, },
Command::none(), Command::none(),
) )
} }
fn title(&self) -> String { fn core(&self) -> &cosmic::app::Core {
config::APP_ID.to_string() &self.core
}
fn core_mut(&mut self) -> &mut cosmic::app::Core {
&mut self.core
} }
fn update(&mut self, message: Message) -> Command<Message> { fn update(&mut self, message: Message) -> Command<Message> {
@ -217,7 +213,7 @@ impl Application for CosmicBatteryApplet {
let new_id = window::Id(self.id_ctr); let new_id = window::Id(self.id_ctr);
self.popup.replace(new_id); self.popup.replace(new_id);
let mut popup_settings = self.applet_helper.get_popup_settings( let mut popup_settings = self.core.applet_helper.get_popup_settings(
window::Id(0), window::Id(0),
new_id, new_id,
None, None,
@ -246,7 +242,6 @@ impl Application for CosmicBatteryApplet {
Message::UpdateKbdBrightness(b) => { Message::UpdateKbdBrightness(b) => {
self.kbd_brightness = b; self.kbd_brightness = b;
} }
Message::Ignore => {}
Message::InitKbdBacklight(tx, brightness) => { Message::InitKbdBacklight(tx, brightness) => {
let _ = tx.send(KeyboardBacklightRequest::Get); let _ = tx.send(KeyboardBacklightRequest::Get);
self.kbd_sender = Some(tx); self.kbd_sender = Some(tx);
@ -278,171 +273,170 @@ impl Application for CosmicBatteryApplet {
let _ = tx.send(PowerProfileRequest::Set(profile)); let _ = tx.send(PowerProfileRequest::Set(profile));
} }
} }
Message::Theme(t) => {
self.theme = t;
}
} }
Command::none() Command::none()
} }
fn view(&self, id: window::Id) -> Element<Message> {
if id == window::Id(0) { fn view(&self) -> Element<Message> {
self.applet_helper self.core
.icon_button(&self.icon_name) .applet_helper
.on_press(Message::TogglePopup) .icon_button(&self.icon_name)
.into() .on_press(Message::TogglePopup)
.into()
}
fn view_window(&self, _id: window::Id) -> Element<Message> {
let name = text(fl!("battery")).size(14);
let description = text(if !self.on_battery {
format!("{}%", self.battery_percent)
} else { } else {
let name = text(fl!("battery")).size(14); format!(
let description = text(if !self.on_battery { "{} {} ({:.0}%)",
format!("{}%", self.battery_percent) format_duration(self.time_remaining),
} else { fl!("until-empty"),
format!( self.battery_percent
"{} {} ({:.0}%)", )
format_duration(self.time_remaining), })
fl!("until-empty"), .size(10);
self.battery_percent self.core
) .applet_helper
}) .popup_container(
.size(10); column![
self.applet_helper row![
.popup_container( icon(&*self.icon_name, 24).style(Svg::Symbolic),
column![ column![name, description]
row![
icon(&*self.icon_name, 24).style(Svg::Symbolic),
column![name, description]
]
.padding([0, 24])
.spacing(8)
.align_items(Alignment::Center),
container(divider::horizontal::light())
.width(Length::Fill)
.padding([0, 12]),
button(applet_button_theme())
.custom(vec![row![
column![
text(fl!("battery")).size(14),
text(fl!("battery-desc")).size(10)
]
.width(Length::Fill),
icon("emblem-ok-symbolic", 12).size(12).style(
match self.power_profile {
Power::Battery => Svg::SymbolicActive,
_ => Svg::Default,
}
),
]
.align_items(Alignment::Center)
.into()])
.padding([8, 24])
.on_press(Message::SelectProfile(Power::Battery))
.width(Length::Fill),
button(applet_button_theme())
.custom(vec![row![
column![
text(fl!("balanced")).size(14),
text(fl!("balanced-desc")).size(10)
]
.width(Length::Fill),
icon("emblem-ok-symbolic", 12).size(12).style(
match self.power_profile {
Power::Balanced => Svg::SymbolicActive,
_ => Svg::Default,
}
),
]
.align_items(Alignment::Center)
.into()])
.padding([8, 24])
.on_press(Message::SelectProfile(Power::Balanced))
.width(Length::Fill),
button(applet_button_theme())
.custom(vec![row![
column![
text(fl!("performance")).size(14),
text(fl!("performance-desc")).size(10)
]
.width(Length::Fill),
icon("emblem-ok-symbolic", 12).size(12).style(
match self.power_profile {
Power::Performance => Svg::SymbolicActive,
_ => Svg::Default,
}
),
]
.align_items(Alignment::Center)
.into()])
.padding([8, 24])
.on_press(Message::SelectProfile(Power::Performance))
.width(Length::Fill),
container(divider::horizontal::light())
.width(Length::Fill)
.padding([0, 12]),
container(
anim!(
//toggler
MAX_CHARGE,
&self.timeline,
fl!("max-charge"),
self.charging_limit,
Message::SetChargingLimit,
)
.text_size(14)
.width(Length::Fill)
)
.padding([0, 24])
.width(Length::Fill),
container(divider::horizontal::light())
.width(Length::Fill)
.padding([0, 12]),
row![
icon(self.display_icon_name.as_str(), 24).style(Svg::Symbolic),
slider(
1..=100,
(self.screen_brightness * 100.0) as i32,
Message::SetScreenBrightness
),
text(format!("{:.0}%", self.screen_brightness * 100.0))
.size(16)
.width(Length::Fixed(40.0))
.horizontal_alignment(Horizontal::Right)
]
.padding([0, 24])
.spacing(12),
row![
icon("keyboard-brightness-symbolic", 24).style(Svg::Symbolic),
slider(
0..=100,
(self.kbd_brightness * 100.0) as i32,
Message::SetKbdBrightness
),
text(format!("{:.0}%", self.kbd_brightness * 100.0))
.size(16)
.width(Length::Fixed(40.0))
.horizontal_alignment(Horizontal::Right)
]
.padding([0, 24])
.spacing(12),
container(divider::horizontal::light())
.width(Length::Fill)
.padding([0, 12]),
button(applet_button_theme())
.custom(vec![text(fl!("power-settings"))
.size(14)
.width(Length::Fill)
.into()])
.on_press(Message::OpenBatterySettings)
.width(Length::Fill)
.padding([8, 24])
] ]
.padding([0, 24])
.spacing(8) .spacing(8)
.padding([8, 0]), .align_items(Alignment::Center),
) container(divider::horizontal::light())
.into() .width(Length::Fill)
} .padding([0, 12]),
button(applet_button_theme())
.custom(vec![row![
column![
text(fl!("battery")).size(14),
text(fl!("battery-desc")).size(10)
]
.width(Length::Fill),
icon("emblem-ok-symbolic", 12).size(12).style(
match self.power_profile {
Power::Battery => Svg::SymbolicActive,
_ => Svg::Default,
}
),
]
.align_items(Alignment::Center)
.into()])
.padding([8, 24])
.on_press(Message::SelectProfile(Power::Battery))
.width(Length::Fill),
button(applet_button_theme())
.custom(vec![row![
column![
text(fl!("balanced")).size(14),
text(fl!("balanced-desc")).size(10)
]
.width(Length::Fill),
icon("emblem-ok-symbolic", 12).size(12).style(
match self.power_profile {
Power::Balanced => Svg::SymbolicActive,
_ => Svg::Default,
}
),
]
.align_items(Alignment::Center)
.into()])
.padding([8, 24])
.on_press(Message::SelectProfile(Power::Balanced))
.width(Length::Fill),
button(applet_button_theme())
.custom(vec![row![
column![
text(fl!("performance")).size(14),
text(fl!("performance-desc")).size(10)
]
.width(Length::Fill),
icon("emblem-ok-symbolic", 12).size(12).style(
match self.power_profile {
Power::Performance => Svg::SymbolicActive,
_ => Svg::Default,
}
),
]
.align_items(Alignment::Center)
.into()])
.padding([8, 24])
.on_press(Message::SelectProfile(Power::Performance))
.width(Length::Fill),
container(divider::horizontal::light())
.width(Length::Fill)
.padding([0, 12]),
container(
anim!(
//toggler
MAX_CHARGE,
&self.timeline,
fl!("max-charge"),
self.charging_limit,
Message::SetChargingLimit,
)
.text_size(14)
.width(Length::Fill)
)
.padding([0, 24])
.width(Length::Fill),
container(divider::horizontal::light())
.width(Length::Fill)
.padding([0, 12]),
row![
icon(self.display_icon_name.as_str(), 24).style(Svg::Symbolic),
slider(
1..=100,
(self.screen_brightness * 100.0) as i32,
Message::SetScreenBrightness
),
text(format!("{:.0}%", self.screen_brightness * 100.0))
.size(16)
.width(Length::Fixed(40.0))
.horizontal_alignment(Horizontal::Right)
]
.padding([0, 24])
.spacing(12),
row![
icon("keyboard-brightness-symbolic", 24).style(Svg::Symbolic),
slider(
0..=100,
(self.kbd_brightness * 100.0) as i32,
Message::SetKbdBrightness
),
text(format!("{:.0}%", self.kbd_brightness * 100.0))
.size(16)
.width(Length::Fixed(40.0))
.horizontal_alignment(Horizontal::Right)
]
.padding([0, 24])
.spacing(12),
container(divider::horizontal::light())
.width(Length::Fill)
.padding([0, 12]),
button(applet_button_theme())
.custom(vec![text(fl!("power-settings"))
.size(14)
.width(Length::Fill)
.into()])
.on_press(Message::OpenBatterySettings)
.width(Length::Fill)
.padding([8, 24])
]
.spacing(8)
.padding([8, 0]),
)
.into()
} }
fn subscription(&self) -> Subscription<Message> { fn subscription(&self) -> Subscription<Message> {
Subscription::batch(vec![ Subscription::batch(vec![
self.applet_helper.theme_subscription(0).map(Message::Theme),
device_subscription(0).map( device_subscription(0).map(
|DeviceDbusEvent::Update { |DeviceDbusEvent::Update {
on_battery, on_battery,
@ -473,18 +467,7 @@ impl Application for CosmicBatteryApplet {
]) ])
} }
fn theme(&self) -> Theme { fn style(&self) -> Option<<Theme as application::StyleSheet>::Style> {
self.theme.clone() Some(cosmic::app::applet::style())
}
fn close_requested(&self, _id: window::Id) -> Message {
Message::Ignore
}
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|theme| Appearance {
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().on_bg_color().into(),
}))
} }
} }

View file

@ -9,7 +9,6 @@ once_cell = "1.16.0"
bluer = { version = "0.15", features = ["bluetoothd", "id"] } bluer = { version = "0.15", features = ["bluetoothd", "id"] }
futures-util = "0.3.21" futures-util = "0.3.21"
libcosmic.workspace = true libcosmic.workspace = true
cosmic-applet = { path = "../applet" }
futures = "0.3" futures = "0.3"
log = "0.4" log = "0.4"
pretty_env_logger = "0.5" pretty_env_logger = "0.5"

View file

@ -1,10 +1,12 @@
use crate::bluetooth::{BluerDeviceStatus, BluerRequest, BluerState}; use crate::bluetooth::{BluerDeviceStatus, BluerRequest, BluerState};
use cosmic::app::{applet::applet_button_theme, Command};
use cosmic::iced_style; use cosmic::iced_style;
use cosmic::{ use cosmic::{
iced::{ iced::{
self,
wayland::popup::{destroy_popup, get_popup}, wayland::popup::{destroy_popup, get_popup},
widget::{column, container, row, scrollable, text, Column}, widget::{column, container, row, scrollable, text, Column},
Alignment, Application, Color, Command, Length, Subscription, Alignment, Length, Subscription,
}, },
iced_runtime::core::{ iced_runtime::core::{
alignment::{Horizontal, Vertical}, alignment::{Horizontal, Vertical},
@ -16,7 +18,6 @@ use cosmic::{
widget::{button, divider, icon, toggler}, widget::{button, divider, icon, toggler},
Element, Theme, Element, Theme,
}; };
use cosmic_applet::{applet_button_theme, CosmicAppletHelper};
use std::collections::HashMap; use std::collections::HashMap;
use std::time::Duration; use std::time::Duration;
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
@ -25,17 +26,15 @@ use crate::bluetooth::{bluetooth_subscription, BluerDevice, BluerEvent};
use crate::{config, fl}; use crate::{config, fl};
pub fn run() -> cosmic::iced::Result { pub fn run() -> cosmic::iced::Result {
let helper = CosmicAppletHelper::default(); cosmic::app::applet::run::<CosmicBluetoothApplet>(false, ())
CosmicBluetoothApplet::run(helper.window_settings())
} }
#[derive(Default)] #[derive(Default)]
struct CosmicBluetoothApplet { struct CosmicBluetoothApplet {
core: cosmic::app::Core,
icon_name: String, icon_name: String,
theme: Theme,
popup: Option<window::Id>, popup: Option<window::Id>,
id_ctr: u128, id_ctr: u128,
applet_helper: CosmicAppletHelper,
bluer_state: BluerState, bluer_state: BluerState,
bluer_sender: Option<Sender<BluerRequest>>, bluer_sender: Option<Sender<BluerRequest>>,
// UI state // UI state
@ -63,31 +62,31 @@ enum Message {
Request(BluerRequest), Request(BluerRequest),
Cancel, Cancel,
Confirm, Confirm,
Theme(Theme),
} }
impl Application for CosmicBluetoothApplet { impl cosmic::Application for CosmicBluetoothApplet {
type Message = Message; type Message = Message;
type Theme = Theme;
type Executor = cosmic::SingleThreadExecutor; type Executor = cosmic::SingleThreadExecutor;
type Flags = (); type Flags = ();
const APP_ID: &'static str = config::APP_ID;
fn new(_flags: ()) -> (Self, Command<Message>) { fn init(core: cosmic::app::Core, _flags: ()) -> (Self, Command<Message>) {
let applet_helper = CosmicAppletHelper::default();
let theme = applet_helper.theme();
( (
CosmicBluetoothApplet { CosmicBluetoothApplet {
core,
icon_name: "bluetooth-symbolic".to_string(), icon_name: "bluetooth-symbolic".to_string(),
theme,
applet_helper,
..Default::default() ..Default::default()
}, },
Command::none(), Command::none(),
) )
} }
fn title(&self) -> String { fn core(&self) -> &cosmic::app::Core {
config::APP_ID.to_string() &self.core
}
fn core_mut(&mut self) -> &mut cosmic::app::Core {
&mut self.core
} }
fn update(&mut self, message: Message) -> Command<Message> { fn update(&mut self, message: Message) -> Command<Message> {
@ -101,7 +100,7 @@ impl Application for CosmicBluetoothApplet {
let new_id = window::Id(self.id_ctr); let new_id = window::Id(self.id_ctr);
self.popup.replace(new_id); self.popup.replace(new_id);
let mut popup_settings = self.applet_helper.get_popup_settings( let mut popup_settings = self.core.applet_helper.get_popup_settings(
window::Id(0), window::Id(0),
new_id, new_id,
None, None,
@ -116,13 +115,13 @@ impl Application for CosmicBluetoothApplet {
.max_width(400.0); .max_width(400.0);
let tx = self.bluer_sender.as_ref().cloned(); let tx = self.bluer_sender.as_ref().cloned();
return Command::batch(vec![ return Command::batch(vec![
Command::perform( iced::Command::perform(
async { async {
if let Some(tx) = tx { if let Some(tx) = tx {
let _ = tx.send(BluerRequest::StateUpdate).await; let _ = tx.send(BluerRequest::StateUpdate).await;
} }
}, },
|_| Message::Ignore, |_| cosmic::app::message::app(Message::Ignore),
), ),
get_popup(popup_settings), get_popup(popup_settings),
]); ]);
@ -148,13 +147,13 @@ impl Application for CosmicBluetoothApplet {
if self.popup.is_some() && self.bluer_sender.is_some() => if self.popup.is_some() && self.bluer_sender.is_some() =>
{ {
let tx = self.bluer_sender.as_ref().cloned().unwrap(); let tx = self.bluer_sender.as_ref().cloned().unwrap();
return Command::perform( return iced::Command::perform(
async move { async move {
// sleep for a bit before requesting state update again // sleep for a bit before requesting state update again
tokio::time::sleep(Duration::from_millis(3000)).await; tokio::time::sleep(Duration::from_millis(3000)).await;
let _ = tx.send(BluerRequest::StateUpdate).await; let _ = tx.send(BluerRequest::StateUpdate).await;
}, },
|_| Message::Ignore, |_| cosmic::app::message::app(Message::Ignore),
); );
} }
_ => {} _ => {}
@ -243,42 +242,48 @@ impl Application for CosmicBluetoothApplet {
_ => {} // TODO _ => {} // TODO
} }
if let Some(tx) = self.bluer_sender.as_mut().cloned() { if let Some(tx) = self.bluer_sender.as_mut().cloned() {
return Command::perform( return iced::Command::perform(
async move { async move {
let _ = tx.send(r).await; let _ = tx.send(r).await;
}, },
|_| Message::Ignore, // Error handling |_| cosmic::app::message::app(Message::Ignore), // Error handling
); );
} }
} }
Message::Cancel => { Message::Cancel => {
if let Some((_, _, tx)) = self.request_confirmation.take() { if let Some((_, _, tx)) = self.request_confirmation.take() {
return Command::perform( return iced::Command::perform(
async move { async move {
let _ = tx.send(false).await; let _ = tx.send(false).await;
}, },
|_| Message::Ignore, |_| cosmic::app::message::app(Message::Ignore),
); );
} }
} }
Message::Confirm => { Message::Confirm => {
if let Some((_, _, tx)) = self.request_confirmation.take() { if let Some((_, _, tx)) = self.request_confirmation.take() {
return Command::perform( return iced::Command::perform(
async move { async move {
let _ = tx.send(true).await; let _ = tx.send(true).await;
}, },
|_| Message::Ignore, |_| cosmic::app::message::app(Message::Ignore),
); );
} }
} }
Message::Theme(t) => {
self.theme = t;
}
} }
self.update_icon(); self.update_icon();
Command::none() Command::none()
} }
fn view(&self, id: window::Id) -> Element<Message> {
fn view(&self) -> Element<Message> {
self.core
.applet_helper
.icon_button(&self.icon_name)
.on_press(Message::TogglePopup)
.into()
}
fn view_window(&self, _id: window::Id) -> Element<Message> {
let button_style = || Button::Custom { let button_style = || Button::Custom {
active: Box::new(|t| iced_style::button::Appearance { active: Box::new(|t| iced_style::button::Appearance {
border_radius: 0.0.into(), border_radius: 0.0.into(),
@ -289,258 +294,234 @@ impl Application for CosmicBluetoothApplet {
..t.hovered(&Button::Text) ..t.hovered(&Button::Text)
}), }),
}; };
if id == window::Id(0) { let mut known_bluetooth = column![];
self.applet_helper for dev in self.bluer_state.devices.iter().filter(|d| {
.icon_button(&self.icon_name) !self
.on_press(Message::TogglePopup) .request_confirmation
.into() .as_ref()
} else { .map_or(false, |(dev, _, _)| d.address == dev.address)
let mut known_bluetooth = column![]; }) {
for dev in self.bluer_state.devices.iter().filter(|d| { let mut row = row![
!self icon(dev.icon.as_str(), 16).style(Svg::Symbolic),
.request_confirmation text(dev.name.clone())
.as_ref() .size(14)
.map_or(false, |(dev, _, _)| d.address == dev.address)
}) {
let mut row = row![
icon(dev.icon.as_str(), 16).style(Svg::Symbolic),
text(dev.name.clone())
.size(14)
.horizontal_alignment(Horizontal::Left)
.vertical_alignment(Vertical::Center)
.width(Length::Fill)
]
.align_items(Alignment::Center)
.spacing(12);
match &dev.status {
BluerDeviceStatus::Connected => {
row = row.push(
text(fl!("connected"))
.size(14)
.horizontal_alignment(Horizontal::Right)
.vertical_alignment(Vertical::Center),
);
}
BluerDeviceStatus::Paired => {}
BluerDeviceStatus::Connecting | BluerDeviceStatus::Disconnecting => {
row = row.push(icon("process-working-symbolic", 24).style(Svg::Symbolic));
}
BluerDeviceStatus::Disconnected | BluerDeviceStatus::Pairing => continue,
};
known_bluetooth = known_bluetooth.push(
button(applet_button_theme())
.custom(vec![row.into()])
.style(applet_button_theme())
.on_press(match dev.status {
BluerDeviceStatus::Connected => {
Message::Request(BluerRequest::DisconnectDevice(dev.address))
}
BluerDeviceStatus::Disconnected => {
Message::Request(BluerRequest::PairDevice(dev.address))
}
BluerDeviceStatus::Paired => {
Message::Request(BluerRequest::ConnectDevice(dev.address))
}
BluerDeviceStatus::Connecting => {
Message::Request(BluerRequest::CancelConnect(dev.address))
}
BluerDeviceStatus::Disconnecting => Message::Ignore, // Start connecting?
BluerDeviceStatus::Pairing => Message::Ignore, // Cancel pairing?
})
.width(Length::Fill),
);
}
let mut content = column![
column![
toggler(fl!("bluetooth"), self.bluer_state.bluetooth_enabled, |m| {
Message::Request(BluerRequest::SetBluetoothEnabled(m))
},)
.text_size(14)
.width(Length::Fill),
// these are not in the UX mockup, but they are useful imo
toggler(fl!("discoverable"), self.bluer_state.discoverable, |m| {
Message::Request(BluerRequest::SetDiscoverable(m))
},)
.text_size(14)
.width(Length::Fill),
toggler(fl!("pairable"), self.bluer_state.pairable, |m| {
Message::Request(BluerRequest::SetPairable(m))
},)
.text_size(14)
.width(Length::Fill)
]
.spacing(8)
.padding([0, 12]),
divider::horizontal::light(),
known_bluetooth,
]
.align_items(Alignment::Center)
.spacing(8)
.padding([8, 0]);
let dropdown_icon = if self.show_visible_devices {
"go-down-symbolic"
} else {
"go-next-symbolic"
};
let available_connections_btn = button(Button::Secondary)
.custom(
vec![
text(fl!("other-devices"))
.size(14)
.width(Length::Fill)
.height(Length::Fixed(24.0))
.vertical_alignment(Vertical::Center)
.into(),
container(icon(dropdown_icon, 14).style(Svg::Symbolic))
.align_x(Horizontal::Center)
.align_y(Vertical::Center)
.width(Length::Fixed(24.0))
.height(Length::Fixed(24.0))
.into(),
]
.into(),
)
.padding([8, 24])
.style(button_style())
.on_press(Message::ToggleVisibleDevices(!self.show_visible_devices));
content = content.push(available_connections_btn);
let mut list_column: Vec<Element<'_, Message>> =
Vec::with_capacity(self.bluer_state.devices.len());
if let Some((device, pin, _)) = self.request_confirmation.as_ref() {
let row = column![
icon(device.icon.as_str(), 16).style(Svg::Symbolic),
text(&device.name)
.size(14)
.horizontal_alignment(Horizontal::Left)
.vertical_alignment(Vertical::Center)
.width(Length::Fill),
text(fl!(
"confirm-pin",
HashMap::from_iter(vec![("deviceName", device.name.clone())])
))
.horizontal_alignment(Horizontal::Left) .horizontal_alignment(Horizontal::Left)
.vertical_alignment(Vertical::Center) .vertical_alignment(Vertical::Center)
.width(Length::Fill) .width(Length::Fill)
.size(14), ]
text(pin) .align_items(Alignment::Center)
.horizontal_alignment(Horizontal::Center) .spacing(12);
.vertical_alignment(Vertical::Center)
.width(Length::Fill)
.size(22),
row![
button(Button::Secondary)
.custom(
vec![text(fl!("cancel"))
.size(14)
.width(Length::Fill)
.height(Length::Fixed(24.0))
.vertical_alignment(Vertical::Center)
.into(),]
.into(),
)
.padding([8, 24])
.style(button_style())
.on_press(Message::Cancel)
.width(Length::Fill),
button(Button::Secondary)
.custom(
vec![text(fl!("confirm"))
.size(14)
.width(Length::Fill)
.height(Length::Fixed(24.0))
.vertical_alignment(Vertical::Center)
.into(),]
.into(),
)
.padding([8, 24])
.style(button_style())
.on_press(Message::Confirm)
.width(Length::Fill),
]
]
.padding([0, 24])
.spacing(12);
list_column.push(row.into());
}
let mut visible_devices_count = 0;
if self.show_visible_devices {
if self.bluer_state.bluetooth_enabled {
let mut visible_devices = column![];
for dev in self.bluer_state.devices.iter().filter(|d| {
matches!(
d.status,
BluerDeviceStatus::Disconnected | BluerDeviceStatus::Pairing
) && !self
.request_confirmation
.as_ref()
.map_or(false, |(dev, _, _)| d.address == dev.address)
}) {
let row = row![
icon(dev.icon.as_str(), 16).style(Svg::Symbolic),
text(dev.name.clone())
.horizontal_alignment(Horizontal::Left)
.size(14),
]
.width(Length::Fill)
.align_items(Alignment::Center)
.spacing(12);
visible_devices = visible_devices.push(
button(applet_button_theme())
.custom(vec![row.width(Length::Fill).into()])
.on_press(Message::Request(BluerRequest::PairDevice(
dev.address.clone(),
)))
.width(Length::Fill),
);
visible_devices_count += 1;
}
list_column.push(visible_devices.into());
}
}
let item_counter = visible_devices_count
// request confirmation is pretty big
+ if self.request_confirmation.is_some() {
5
} else {
0
};
if item_counter > 10 { match &dev.status {
content = content.push( BluerDeviceStatus::Connected => {
scrollable(Column::with_children(list_column)).height(Length::Fixed(300.0)), row = row.push(
); text(fl!("connected"))
} else { .size(14)
content = content.push(Column::with_children(list_column)); .horizontal_alignment(Horizontal::Right)
} .vertical_alignment(Vertical::Center),
self.applet_helper.popup_container(content).into() );
}
BluerDeviceStatus::Paired => {}
BluerDeviceStatus::Connecting | BluerDeviceStatus::Disconnecting => {
row = row.push(icon("process-working-symbolic", 24).style(Svg::Symbolic));
}
BluerDeviceStatus::Disconnected | BluerDeviceStatus::Pairing => continue,
};
known_bluetooth = known_bluetooth.push(
button(applet_button_theme())
.custom(vec![row.into()])
.style(applet_button_theme())
.on_press(match dev.status {
BluerDeviceStatus::Connected => {
Message::Request(BluerRequest::DisconnectDevice(dev.address))
}
BluerDeviceStatus::Disconnected => {
Message::Request(BluerRequest::PairDevice(dev.address))
}
BluerDeviceStatus::Paired => {
Message::Request(BluerRequest::ConnectDevice(dev.address))
}
BluerDeviceStatus::Connecting => {
Message::Request(BluerRequest::CancelConnect(dev.address))
}
BluerDeviceStatus::Disconnecting => Message::Ignore, // Start connecting?
BluerDeviceStatus::Pairing => Message::Ignore, // Cancel pairing?
})
.width(Length::Fill),
);
} }
let mut content = column![
column![
toggler(fl!("bluetooth"), self.bluer_state.bluetooth_enabled, |m| {
Message::Request(BluerRequest::SetBluetoothEnabled(m))
},)
.text_size(14)
.width(Length::Fill),
// these are not in the UX mockup, but they are useful imo
toggler(fl!("discoverable"), self.bluer_state.discoverable, |m| {
Message::Request(BluerRequest::SetDiscoverable(m))
},)
.text_size(14)
.width(Length::Fill),
toggler(fl!("pairable"), self.bluer_state.pairable, |m| {
Message::Request(BluerRequest::SetPairable(m))
},)
.text_size(14)
.width(Length::Fill)
]
.spacing(8)
.padding([0, 12]),
divider::horizontal::light(),
known_bluetooth,
]
.align_items(Alignment::Center)
.spacing(8)
.padding([8, 0]);
let dropdown_icon = if self.show_visible_devices {
"go-down-symbolic"
} else {
"go-next-symbolic"
};
let available_connections_btn = button(Button::Secondary)
.custom(
vec![
text(fl!("other-devices"))
.size(14)
.width(Length::Fill)
.height(Length::Fixed(24.0))
.vertical_alignment(Vertical::Center)
.into(),
container(icon(dropdown_icon, 14).style(Svg::Symbolic))
.align_x(Horizontal::Center)
.align_y(Vertical::Center)
.width(Length::Fixed(24.0))
.height(Length::Fixed(24.0))
.into(),
]
.into(),
)
.padding([8, 24])
.style(button_style())
.on_press(Message::ToggleVisibleDevices(!self.show_visible_devices));
content = content.push(available_connections_btn);
let mut list_column: Vec<Element<'_, Message>> =
Vec::with_capacity(self.bluer_state.devices.len());
if let Some((device, pin, _)) = self.request_confirmation.as_ref() {
let row = column![
icon(device.icon.as_str(), 16).style(Svg::Symbolic),
text(&device.name)
.size(14)
.horizontal_alignment(Horizontal::Left)
.vertical_alignment(Vertical::Center)
.width(Length::Fill),
text(fl!(
"confirm-pin",
HashMap::from_iter(vec![("deviceName", device.name.clone())])
))
.horizontal_alignment(Horizontal::Left)
.vertical_alignment(Vertical::Center)
.width(Length::Fill)
.size(14),
text(pin)
.horizontal_alignment(Horizontal::Center)
.vertical_alignment(Vertical::Center)
.width(Length::Fill)
.size(22),
row![
button(Button::Secondary)
.custom(
vec![text(fl!("cancel"))
.size(14)
.width(Length::Fill)
.height(Length::Fixed(24.0))
.vertical_alignment(Vertical::Center)
.into(),]
.into(),
)
.padding([8, 24])
.style(button_style())
.on_press(Message::Cancel)
.width(Length::Fill),
button(Button::Secondary)
.custom(
vec![text(fl!("confirm"))
.size(14)
.width(Length::Fill)
.height(Length::Fixed(24.0))
.vertical_alignment(Vertical::Center)
.into(),]
.into(),
)
.padding([8, 24])
.style(button_style())
.on_press(Message::Confirm)
.width(Length::Fill),
]
]
.padding([0, 24])
.spacing(12);
list_column.push(row.into());
}
let mut visible_devices_count = 0;
if self.show_visible_devices {
if self.bluer_state.bluetooth_enabled {
let mut visible_devices = column![];
for dev in self.bluer_state.devices.iter().filter(|d| {
matches!(
d.status,
BluerDeviceStatus::Disconnected | BluerDeviceStatus::Pairing
) && !self
.request_confirmation
.as_ref()
.map_or(false, |(dev, _, _)| d.address == dev.address)
}) {
let row = row![
icon(dev.icon.as_str(), 16).style(Svg::Symbolic),
text(dev.name.clone())
.horizontal_alignment(Horizontal::Left)
.size(14),
]
.width(Length::Fill)
.align_items(Alignment::Center)
.spacing(12);
visible_devices = visible_devices.push(
button(applet_button_theme())
.custom(vec![row.width(Length::Fill).into()])
.on_press(Message::Request(BluerRequest::PairDevice(
dev.address.clone(),
)))
.width(Length::Fill),
);
visible_devices_count += 1;
}
list_column.push(visible_devices.into());
}
}
let item_counter = visible_devices_count
// request confirmation is pretty big
+ if self.request_confirmation.is_some() {
5
} else {
0
};
if item_counter > 10 {
content = content
.push(scrollable(Column::with_children(list_column)).height(Length::Fixed(300.0)));
} else {
content = content.push(Column::with_children(list_column));
}
self.core.applet_helper.popup_container(content).into()
} }
fn subscription(&self) -> Subscription<Message> { fn subscription(&self) -> Subscription<Message> {
Subscription::batch(vec![ bluetooth_subscription(0).map(Message::BluetoothEvent)
self.applet_helper.theme_subscription(0).map(Message::Theme),
bluetooth_subscription(0).map(Message::BluetoothEvent),
])
} }
fn theme(&self) -> Theme { fn style(&self) -> Option<<Theme as application::StyleSheet>::Style> {
self.theme.clone() Some(cosmic::app::applet::style())
}
fn close_requested(&self, _id: window::Id) -> Self::Message {
Message::Ignore
}
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|theme| {
application::Appearance {
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().on_bg_color().into(),
}
}))
} }
} }

View file

@ -8,7 +8,6 @@ edition = "2021"
[dependencies] [dependencies]
zbus = "3.13" zbus = "3.13"
libcosmic.workspace = true libcosmic.workspace = true
cosmic-applet = { path = "../applet" }
once_cell = "1" once_cell = "1"
# Application i18n # Application i18n
i18n-embed = { version = "0.13.4", features = ["fluent-system", "desktop-requester"] } i18n-embed = { version = "0.13.4", features = ["fluent-system", "desktop-requester"] }

View file

@ -3,34 +3,8 @@ mod graphics;
mod localize; mod localize;
mod window; mod window;
use cosmic::{
iced::{wayland::InitialSurface, Application, Settings},
iced_runtime::core::layout::Limits,
};
use cosmic_applet::{cosmic_panel_config::PanelAnchor, CosmicAppletHelper};
use window::*; use window::*;
pub fn main() -> cosmic::iced::Result { pub fn main() -> cosmic::iced::Result {
let helper = CosmicAppletHelper::default(); cosmic::app::applet::run::<Window>(true, ())
let mut settings: Settings<()> = helper.window_settings();
match helper.anchor {
PanelAnchor::Top | PanelAnchor::Bottom => {
match &mut settings.initial_surface {
InitialSurface::LayerSurface(_) => todo!(),
InitialSurface::XdgWindow(w) => {
w.autosize = true;
w.resizable = None;
w.size_limits = Limits::NONE
.min_height(1.0)
.max_height(200.0)
.min_width(1.0)
.max_width(1000.0);
}
InitialSurface::None => unimplemented!(),
};
}
_ => {}
};
Window::run(settings)
} }

View file

@ -1,23 +1,24 @@
use crate::dbus::{self, PowerDaemonProxy}; use crate::dbus::{self, PowerDaemonProxy};
use crate::fl; use crate::fl;
use crate::graphics::{get_current_graphics, set_graphics, Graphics}; use crate::graphics::{get_current_graphics, set_graphics, Graphics};
use cosmic::app::{
applet::{applet_button_theme, cosmic_panel_config::PanelAnchor},
Command,
};
use cosmic::iced::wayland::popup::{destroy_popup, get_popup}; use cosmic::iced::wayland::popup::{destroy_popup, get_popup};
use cosmic::iced::Color;
use cosmic::iced_futures::Subscription;
use cosmic::iced_runtime::core::alignment::Horizontal; use cosmic::iced_runtime::core::alignment::Horizontal;
use cosmic::iced_runtime::core::Alignment; use cosmic::iced_runtime::core::Alignment;
use cosmic::iced_style::application::{self, Appearance}; use cosmic::iced_style::application;
use cosmic::theme::Button; use cosmic::theme::Button;
use cosmic::widget::icon; use cosmic::widget::icon;
use cosmic::{ use cosmic::{
iced::widget::{column, container, row, text}, iced::widget::{column, container, row, text},
iced::{self, Application, Command, Length}, iced::{self, Length},
iced_runtime::core::window, iced_runtime::core::window,
theme::{Svg, Theme}, theme::{Svg, Theme},
widget::{button, divider}, widget::{button, divider},
Element, Element,
}; };
use cosmic_applet::{applet_button_theme, cosmic_panel_config::PanelAnchor, CosmicAppletHelper};
use zbus::Connection; use zbus::Connection;
const ID: &str = "com.system76.CosmicAppletGraphics"; const ID: &str = "com.system76.CosmicAppletGraphics";
@ -41,12 +42,11 @@ impl GraphicsMode {
#[derive(Default)] #[derive(Default)]
pub struct Window { pub struct Window {
core: cosmic::app::Core,
popup: Option<window::Id>, popup: Option<window::Id>,
graphics_mode: Option<GraphicsMode>, graphics_mode: Option<GraphicsMode>,
id_ctr: u128, id_ctr: u128,
theme: Theme,
dbus: Option<(Connection, PowerDaemonProxy<'static>)>, dbus: Option<(Connection, PowerDaemonProxy<'static>)>,
applet_helper: CosmicAppletHelper,
} }
#[allow(dead_code)] #[allow(dead_code)]
@ -58,35 +58,37 @@ pub enum Message {
SelectGraphicsMode(Graphics), SelectGraphicsMode(Graphics),
TogglePopup, TogglePopup,
PopupClosed(window::Id), PopupClosed(window::Id),
Theme(Theme),
} }
impl Application for Window { impl cosmic::Application for Window {
type Executor = cosmic::SingleThreadExecutor; type Executor = cosmic::SingleThreadExecutor;
type Flags = (); type Flags = ();
type Message = Message; type Message = Message;
type Theme = Theme; const APP_ID: &'static str = ID;
fn new(_flags: ()) -> (Self, Command<Self::Message>) { fn init(core: cosmic::app::Core, _flags: ()) -> (Self, Command<Message>) {
let applet_helper = CosmicAppletHelper::default();
let theme = applet_helper.theme();
let window = Window { let window = Window {
theme, core,
applet_helper,
..Default::default() ..Default::default()
}; };
(window, Command::perform(dbus::init(), Message::DBusInit)) (
window,
iced::Command::perform(dbus::init(), |x| {
cosmic::app::message::app(Message::DBusInit(x))
}),
)
} }
fn title(&self) -> String { fn core(&self) -> &cosmic::app::Core {
String::from("Cosmic Graphics Applet") &self.core
} }
fn update(&mut self, message: Message) -> iced::Command<Self::Message> { fn core_mut(&mut self) -> &mut cosmic::app::Core {
&mut self.core
}
fn update(&mut self, message: Message) -> Command<Self::Message> {
match message { match message {
Message::Theme(t) => {
self.theme = t;
}
Message::SelectGraphicsMode(new) => { Message::SelectGraphicsMode(new) => {
if let Some((_, proxy)) = self.dbus.as_ref() { if let Some((_, proxy)) = self.dbus.as_ref() {
let prev = self let prev = self
@ -94,9 +96,14 @@ impl Application for Window {
.map(|m| m.inner()) .map(|m| m.inner())
.unwrap_or_else(|| Graphics::Integrated); .unwrap_or_else(|| Graphics::Integrated);
self.graphics_mode = Some(GraphicsMode::SelectedGraphicsMode { prev, new }); self.graphics_mode = Some(GraphicsMode::SelectedGraphicsMode { prev, new });
return Command::perform(set_graphics(proxy.clone(), new), move |success| { return iced::Command::perform(
Message::AppliedGraphics(success.ok().map(|_| new)) set_graphics(proxy.clone(), new),
}); move |success| {
cosmic::app::message::app(Message::AppliedGraphics(
success.ok().map(|_| new),
))
},
);
} }
} }
Message::TogglePopup => { Message::TogglePopup => {
@ -108,12 +115,12 @@ impl Application for Window {
self.popup.replace(new_id); self.popup.replace(new_id);
let mut commands = Vec::new(); let mut commands = Vec::new();
if let Some((_, proxy)) = self.dbus.as_ref() { if let Some((_, proxy)) = self.dbus.as_ref() {
commands.push(Command::perform( commands.push(iced::Command::perform(
get_current_graphics(proxy.clone()), get_current_graphics(proxy.clone()),
|cur_graphics| Message::CurrentGraphics(cur_graphics.ok()), |cur_graphics| Message::CurrentGraphics(cur_graphics.ok()),
)); ));
} }
let popup_settings = self.applet_helper.get_popup_settings( let popup_settings = self.core.applet_helper.get_popup_settings(
window::Id(0), window::Id(0),
new_id, new_id,
None, None,
@ -121,12 +128,12 @@ impl Application for Window {
None, None,
); );
commands.push(get_popup(popup_settings)); commands.push(get_popup(popup_settings));
return Command::batch(commands); return iced::Command::batch(commands).map(cosmic::app::message::app);
} }
} }
Message::DBusInit(dbus) => { Message::DBusInit(dbus) => {
self.dbus = dbus; self.dbus = dbus;
return Command::perform( return iced::Command::perform(
get_current_graphics(self.dbus.as_ref().unwrap().1.clone()), get_current_graphics(self.dbus.as_ref().unwrap().1.clone()),
|cur_graphics| { |cur_graphics| {
Message::CurrentGraphics(match cur_graphics { Message::CurrentGraphics(match cur_graphics {
@ -137,7 +144,8 @@ impl Application for Window {
} }
}) })
}, },
); )
.map(cosmic::app::message::app);
} }
Message::CurrentGraphics(g) => { Message::CurrentGraphics(g) => {
if let Some(g) = g { if let Some(g) = g {
@ -166,16 +174,17 @@ impl Application for Window {
// Reset to prev after failing // Reset to prev after failing
// https://github.com/pop-os/system76-power/issues/387 // https://github.com/pop-os/system76-power/issues/387
if let Some((_, proxy)) = self.dbus.as_ref() { if let Some((_, proxy)) = self.dbus.as_ref() {
return Command::perform( return iced::Command::perform(
set_graphics(proxy.clone(), prev), set_graphics(proxy.clone(), prev),
move |success| { move |success| {
Message::AppliedGraphics(success.ok().map(|_| new)) Message::AppliedGraphics(success.ok().map(|_| new))
}, },
); )
.map(cosmic::app::message::app);
} }
} }
_ => { _ => {
return Command::perform( return iced::Command::perform(
get_current_graphics(self.dbus.as_ref().unwrap().1.clone()), get_current_graphics(self.dbus.as_ref().unwrap().1.clone()),
|cur_graphics| { |cur_graphics| {
Message::CurrentGraphics(match cur_graphics { Message::CurrentGraphics(match cur_graphics {
@ -187,6 +196,7 @@ impl Application for Window {
}) })
}, },
) )
.map(cosmic::app::message::app)
} }
}; };
} }
@ -195,227 +205,207 @@ impl Application for Window {
Command::none() Command::none()
} }
fn view(&self, id: window::Id) -> Element<Message> { fn view(&self) -> Element<Message> {
if id == window::Id(0) { match self.core.applet_helper.anchor {
match self.applet_helper.anchor { PanelAnchor::Left | PanelAnchor::Right => self
PanelAnchor::Left | PanelAnchor::Right => self .core
.applet_helper .applet_helper
.icon_button(ID) .icon_button(ID)
.on_press(Message::TogglePopup) .on_press(Message::TogglePopup)
.style(Button::Text) .style(Button::Text)
.into(), .into(),
PanelAnchor::Top | PanelAnchor::Bottom => button(Button::Text) PanelAnchor::Top | PanelAnchor::Bottom => button(Button::Text)
.custom(vec![row![ .custom(vec![row![
icon(ID, self.applet_helper.suggested_size().0,).style(Svg::Symbolic), icon(ID, self.core.applet_helper.suggested_size().0,).style(Svg::Symbolic),
text(match self.graphics_mode.map(|g| g.inner()) { text(match self.graphics_mode.map(|g| g.inner()) {
Some(Graphics::Integrated) => fl!("integrated"), Some(Graphics::Integrated) => fl!("integrated"),
Some(Graphics::Nvidia) => fl!("nvidia"), Some(Graphics::Nvidia) => fl!("nvidia"),
Some(Graphics::Compute) => fl!("compute"), Some(Graphics::Compute) => fl!("compute"),
Some(Graphics::Hybrid) => fl!("hybrid"), Some(Graphics::Hybrid) => fl!("hybrid"),
None => "".into(), None => "".into(),
}) })
.size(14) .size(14)
]
.spacing(8)
.padding([0, self.core.applet_helper.suggested_size().0 / 2])
.align_items(Alignment::Center)
.into()])
.style(Button::Text)
.on_press(Message::TogglePopup)
.padding(8)
.width(Length::Shrink)
.height(Length::Shrink)
.into(),
}
}
fn view_window(&self, _id: window::Id) -> Element<Message> {
let content_list = vec![
button(applet_button_theme())
.custom(vec![row![
column![
text(format!("{} {}", fl!("integrated"), fl!("graphics"))).size(14),
text(fl!("integrated-desc")).size(12)
] ]
.spacing(8) .width(Length::Fill),
.padding([0, self.applet_helper.suggested_size().0 / 2]) icon(
.align_items(Alignment::Center) match self.graphics_mode {
.into()])
.style(Button::Text)
.on_press(Message::TogglePopup)
.padding(8)
.width(Length::Shrink)
.height(Length::Shrink)
.into(),
}
} else {
let content_list = vec![
button(applet_button_theme())
.custom(vec![row![
column![
text(format!("{} {}", fl!("integrated"), fl!("graphics"))).size(14),
text(fl!("integrated-desc")).size(12)
]
.width(Length::Fill),
icon(
match self.graphics_mode {
Some(GraphicsMode::SelectedGraphicsMode {
new: Graphics::Integrated,
..
}) => "process-working-symbolic",
_ => "emblem-ok-symbolic",
},
12
)
.size(12)
.style(match self.graphics_mode {
Some(GraphicsMode::CurrentGraphicsMode(Graphics::Integrated)) =>
Svg::SymbolicActive,
Some(GraphicsMode::AppliedGraphicsMode(Graphics::Integrated)) =>
Svg::SymbolicActive,
Some(GraphicsMode::SelectedGraphicsMode { Some(GraphicsMode::SelectedGraphicsMode {
new: Graphics::Integrated, new: Graphics::Integrated,
.. ..
}) => Svg::Symbolic, }) => "process-working-symbolic",
_ => Svg::Default, _ => "emblem-ok-symbolic",
},), },
] 12
.align_items(Alignment::Center) )
.into()]) .size(12)
.padding([8, 24]) .style(match self.graphics_mode {
.on_press(Message::SelectGraphicsMode(Graphics::Integrated)) Some(GraphicsMode::CurrentGraphicsMode(Graphics::Integrated)) =>
.width(Length::Fill) Svg::SymbolicActive,
.into(), Some(GraphicsMode::AppliedGraphicsMode(Graphics::Integrated)) =>
button(applet_button_theme()) Svg::SymbolicActive,
.custom(vec![row![ Some(GraphicsMode::SelectedGraphicsMode {
column![text(format!("{} {}", fl!("nvidia"), fl!("graphics"))).size(14),] new: Graphics::Integrated,
.width(Length::Fill), ..
icon( }) => Svg::Symbolic,
match self.graphics_mode { _ => Svg::Default,
Some(GraphicsMode::SelectedGraphicsMode { },),
new: Graphics::Nvidia, ]
.. .align_items(Alignment::Center)
}) => "process-working-symbolic", .into()])
_ => "emblem-ok-symbolic", .padding([8, 24])
}, .on_press(Message::SelectGraphicsMode(Graphics::Integrated))
12 .width(Length::Fill)
) .into(),
.size(12) button(applet_button_theme())
.style(match self.graphics_mode { .custom(vec![row![
Some(GraphicsMode::CurrentGraphicsMode(Graphics::Nvidia)) => column![text(format!("{} {}", fl!("nvidia"), fl!("graphics"))).size(14),]
Svg::SymbolicActive, .width(Length::Fill),
Some(GraphicsMode::AppliedGraphicsMode(Graphics::Nvidia)) => icon(
Svg::SymbolicActive, match self.graphics_mode {
Some(GraphicsMode::SelectedGraphicsMode { Some(GraphicsMode::SelectedGraphicsMode {
new: Graphics::Nvidia, new: Graphics::Nvidia,
.. ..
}) => Svg::Symbolic, }) => "process-working-symbolic",
_ => Svg::Default, _ => "emblem-ok-symbolic",
}), },
12
)
.size(12)
.style(match self.graphics_mode {
Some(GraphicsMode::CurrentGraphicsMode(Graphics::Nvidia)) =>
Svg::SymbolicActive,
Some(GraphicsMode::AppliedGraphicsMode(Graphics::Nvidia)) =>
Svg::SymbolicActive,
Some(GraphicsMode::SelectedGraphicsMode {
new: Graphics::Nvidia,
..
}) => Svg::Symbolic,
_ => Svg::Default,
}),
]
.align_items(Alignment::Center)
.into()])
.padding([8, 24])
.on_press(Message::SelectGraphicsMode(Graphics::Nvidia))
.width(Length::Fill)
.into(),
button(applet_button_theme())
.custom(vec![row![
column![
text(format!("{} {}", fl!("hybrid"), fl!("graphics"))).size(14),
text(fl!("hybrid-desc")).size(12)
] ]
.align_items(Alignment::Center) .width(Length::Fill),
.into()]) icon(
.padding([8, 24]) match self.graphics_mode {
.on_press(Message::SelectGraphicsMode(Graphics::Nvidia))
.width(Length::Fill)
.into(),
button(applet_button_theme())
.custom(vec![row![
column![
text(format!("{} {}", fl!("hybrid"), fl!("graphics"))).size(14),
text(fl!("hybrid-desc")).size(12)
]
.width(Length::Fill),
icon(
match self.graphics_mode {
Some(GraphicsMode::SelectedGraphicsMode {
new: Graphics::Hybrid,
..
}) => "process-working-symbolic",
_ => "emblem-ok-symbolic",
},
12
)
.size(12)
.style(match self.graphics_mode {
Some(GraphicsMode::CurrentGraphicsMode(Graphics::Hybrid)) =>
Svg::SymbolicActive,
Some(GraphicsMode::AppliedGraphicsMode(Graphics::Hybrid)) =>
Svg::SymbolicActive,
Some(GraphicsMode::SelectedGraphicsMode { Some(GraphicsMode::SelectedGraphicsMode {
new: Graphics::Hybrid, new: Graphics::Hybrid,
.. ..
}) => Svg::Symbolic, }) => "process-working-symbolic",
_ => Svg::Default, _ => "emblem-ok-symbolic",
}) },
12
)
.size(12)
.style(match self.graphics_mode {
Some(GraphicsMode::CurrentGraphicsMode(Graphics::Hybrid)) =>
Svg::SymbolicActive,
Some(GraphicsMode::AppliedGraphicsMode(Graphics::Hybrid)) =>
Svg::SymbolicActive,
Some(GraphicsMode::SelectedGraphicsMode {
new: Graphics::Hybrid,
..
}) => Svg::Symbolic,
_ => Svg::Default,
})
]
.align_items(Alignment::Center)
.into()])
.padding([8, 24])
.on_press(Message::SelectGraphicsMode(Graphics::Hybrid))
.width(Length::Fill)
.into(),
button(applet_button_theme())
.custom(vec![row![
column![
text(format!("{} {}", fl!("compute"), fl!("graphics"))).size(14),
text(fl!("compute-desc")).size(12)
] ]
.align_items(Alignment::Center) .width(Length::Fill),
.into()]) icon(
.padding([8, 24]) match self.graphics_mode {
.on_press(Message::SelectGraphicsMode(Graphics::Hybrid))
.width(Length::Fill)
.into(),
button(applet_button_theme())
.custom(vec![row![
column![
text(format!("{} {}", fl!("compute"), fl!("graphics"))).size(14),
text(fl!("compute-desc")).size(12)
]
.width(Length::Fill),
icon(
match self.graphics_mode {
Some(GraphicsMode::SelectedGraphicsMode {
new: Graphics::Compute,
..
}) => "process-working-symbolic",
_ => "emblem-ok-symbolic",
},
12
)
.size(12)
.style(match self.graphics_mode {
Some(GraphicsMode::CurrentGraphicsMode(Graphics::Compute)) =>
Svg::SymbolicActive,
Some(GraphicsMode::AppliedGraphicsMode(Graphics::Compute)) =>
Svg::SymbolicActive,
Some(GraphicsMode::SelectedGraphicsMode { Some(GraphicsMode::SelectedGraphicsMode {
new: Graphics::Compute, new: Graphics::Compute,
.. ..
}) => Svg::Symbolic, }) => "process-working-symbolic",
_ => Svg::Default, _ => "emblem-ok-symbolic",
}), },
] 12
.align_items(Alignment::Center) )
.into()]) .size(12)
.padding([8, 24]) .style(match self.graphics_mode {
.on_press(Message::SelectGraphicsMode(Graphics::Compute)) Some(GraphicsMode::CurrentGraphicsMode(Graphics::Compute)) =>
.width(Length::Fill) Svg::SymbolicActive,
.into(), Some(GraphicsMode::AppliedGraphicsMode(Graphics::Compute)) =>
]; Svg::SymbolicActive,
Some(GraphicsMode::SelectedGraphicsMode {
new: Graphics::Compute,
..
}) => Svg::Symbolic,
_ => Svg::Default,
}),
]
.align_items(Alignment::Center)
.into()])
.padding([8, 24])
.on_press(Message::SelectGraphicsMode(Graphics::Compute))
.width(Length::Fill)
.into(),
];
self.applet_helper self.core
.popup_container( .applet_helper
column(vec![ .popup_container(
text(fl!("graphics-mode")) column(vec![
.width(Length::Fill) text(fl!("graphics-mode"))
.horizontal_alignment(Horizontal::Center) .width(Length::Fill)
.size(14) .horizontal_alignment(Horizontal::Center)
.into(), .size(14)
container(divider::horizontal::light()) .into(),
.padding([0, 12]) container(divider::horizontal::light())
.width(Length::Fill) .padding([0, 12])
.into(), .width(Length::Fill)
column(content_list).into(), .into(),
]) column(content_list).into(),
.padding([8, 0]) ])
.spacing(12), .padding([8, 0])
) .spacing(12),
.into() )
} .into()
}
fn subscription(&self) -> Subscription<Message> {
self.applet_helper.theme_subscription(0).map(Message::Theme)
} }
fn close_requested(&self, id: window::Id) -> Self::Message { fn style(&self) -> Option<<Theme as application::StyleSheet>::Style> {
if id != window::Id(0) { Some(cosmic::app::applet::style())
Message::PopupClosed(id)
} else {
unimplemented!();
}
}
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|theme| Appearance {
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().background.on.into(),
}))
}
fn should_exit(&self) -> bool {
false
}
fn theme(&self) -> Theme {
self.theme.clone()
} }
} }

View file

@ -10,7 +10,6 @@ cosmic-dbus-networkmanager = { git = "https://github.com/pop-os/dbus-settings-bi
futures-util = "0.3.21" futures-util = "0.3.21"
libcosmic.workspace = true libcosmic.workspace = true
cosmic-time.workspace = true cosmic-time.workspace = true
cosmic-applet = { path = "../applet" }
futures = "0.3" futures = "0.3"
zbus = { version = "3.13", default-features = false } zbus = { version = "3.13", default-features = false }
log = "0.4" log = "0.4"

View file

@ -1,10 +1,11 @@
use cosmic::app::Command;
use cosmic::iced_style; use cosmic::iced_style;
use cosmic::iced_widget::Row; use cosmic::iced_widget::Row;
use cosmic::{ use cosmic::{
iced::{ iced::{
wayland::popup::{destroy_popup, get_popup}, wayland::popup::{destroy_popup, get_popup},
widget::{column, container, row, scrollable, text, text_input, Column}, widget::{column, container, row, scrollable, text, text_input, Column},
Alignment, Application, Color, Command, Length, Subscription, Alignment, Length, Subscription,
}, },
iced_runtime::core::{ iced_runtime::core::{
alignment::{Horizontal, Vertical}, alignment::{Horizontal, Vertical},
@ -16,7 +17,6 @@ use cosmic::{
widget::{button, divider, icon}, widget::{button, divider, icon},
Element, Theme, Element, Theme,
}; };
use cosmic_applet::CosmicAppletHelper;
use cosmic_dbus_networkmanager::interface::enums::{ActiveConnectionState, DeviceState}; use cosmic_dbus_networkmanager::interface::enums::{ActiveConnectionState, DeviceState};
use cosmic_time::{anim, chain, id, once_cell::sync::Lazy, Instant, Timeline}; use cosmic_time::{anim, chain, id, once_cell::sync::Lazy, Instant, Timeline};
@ -36,9 +36,7 @@ use crate::{
}; };
pub fn run() -> cosmic::iced::Result { pub fn run() -> cosmic::iced::Result {
let helper = CosmicAppletHelper::default(); cosmic::app::applet::run::<CosmicNetworkApplet>(false, ())
let settings = helper.window_settings();
CosmicNetworkApplet::run(settings)
} }
#[derive(Debug)] #[derive(Debug)]
@ -83,11 +81,10 @@ static AIRPLANE_MODE: Lazy<id::Toggler> = Lazy::new(id::Toggler::unique);
#[derive(Default)] #[derive(Default)]
struct CosmicNetworkApplet { struct CosmicNetworkApplet {
core: cosmic::app::Core,
icon_name: String, icon_name: String,
theme: Theme,
popup: Option<window::Id>, popup: Option<window::Id>,
id_ctr: u128, id_ctr: u128,
applet_helper: CosmicAppletHelper,
nm_state: NetworkManagerState, nm_state: NetworkManagerState,
// UI state // UI state
nm_sender: Option<UnboundedSender<NetworkManagerRequest>>, nm_sender: Option<UnboundedSender<NetworkManagerRequest>>,
@ -175,46 +172,42 @@ pub(crate) enum Message {
ToggleAirplaneMode(bool), ToggleAirplaneMode(bool),
ToggleWiFi(bool), ToggleWiFi(bool),
ToggleVisibleNetworks, ToggleVisibleNetworks,
Ignore,
NetworkManagerEvent(NetworkManagerEvent), NetworkManagerEvent(NetworkManagerEvent),
SelectWirelessAccessPoint(AccessPoint), SelectWirelessAccessPoint(AccessPoint),
CancelNewConnection, CancelNewConnection,
Password(String), Password(String),
SubmitPassword, SubmitPassword,
Frame(Instant), Frame(Instant),
Theme(Theme),
// Errored(String), // Errored(String),
} }
impl Application for CosmicNetworkApplet { impl cosmic::Application for CosmicNetworkApplet {
type Message = Message; type Message = Message;
type Theme = Theme;
type Executor = cosmic::SingleThreadExecutor; type Executor = cosmic::SingleThreadExecutor;
type Flags = (); type Flags = ();
const APP_ID: &'static str = config::APP_ID;
fn new(_flags: ()) -> (Self, Command<Message>) { fn init(core: cosmic::app::Core, _flags: ()) -> (Self, Command<Message>) {
let applet_helper = CosmicAppletHelper::default();
let theme = applet_helper.theme();
( (
CosmicNetworkApplet { CosmicNetworkApplet {
core,
icon_name: "network-offline-symbolic".to_string(), icon_name: "network-offline-symbolic".to_string(),
theme,
applet_helper,
..Default::default() ..Default::default()
}, },
Command::none(), Command::none(),
) )
} }
fn title(&self) -> String { fn core(&self) -> &cosmic::app::Core {
config::APP_ID.to_string() &self.core
}
fn core_mut(&mut self) -> &mut cosmic::app::Core {
&mut self.core
} }
fn update(&mut self, message: Message) -> Command<Message> { fn update(&mut self, message: Message) -> Command<Message> {
match message { match message {
Message::Theme(t) => {
self.theme = t;
}
Message::Frame(now) => self.timeline.now(now), Message::Frame(now) => self.timeline.now(now),
Message::TogglePopup => { Message::TogglePopup => {
if let Some(p) = self.popup.take() { if let Some(p) = self.popup.take() {
@ -226,7 +219,7 @@ impl Application for CosmicNetworkApplet {
let new_id = window::Id(self.id_ctr); let new_id = window::Id(self.id_ctr);
self.popup.replace(new_id); self.popup.replace(new_id);
let mut popup_settings = self.applet_helper.get_popup_settings( let mut popup_settings = self.core.applet_helper.get_popup_settings(
window::Id(0), window::Id(0),
new_id, new_id,
None, None,
@ -243,7 +236,6 @@ impl Application for CosmicNetworkApplet {
} }
} }
// Message::Errored(_) => todo!(), // Message::Errored(_) => todo!(),
Message::Ignore => {}
Message::ToggleAirplaneMode(enabled) => { Message::ToggleAirplaneMode(enabled) => {
self.toggle_wifi_ctr += 1; self.toggle_wifi_ctr += 1;
if let Some(tx) = self.nm_sender.as_mut() { if let Some(tx) = self.nm_sender.as_mut() {
@ -397,7 +389,16 @@ impl Application for CosmicNetworkApplet {
} }
Command::none() Command::none()
} }
fn view(&self, id: window::Id) -> Element<Message> {
fn view(&self) -> Element<Message> {
self.core
.applet_helper
.icon_button(&self.icon_name)
.on_press(Message::TogglePopup)
.into()
}
fn view_window(&self, _id: window::Id) -> Element<Message> {
let button_style = || Button::Custom { let button_style = || Button::Custom {
active: Box::new(|t| iced_style::button::Appearance { active: Box::new(|t| iced_style::button::Appearance {
border_radius: 0.0.into(), border_radius: 0.0.into(),
@ -408,369 +409,353 @@ impl Application for CosmicNetworkApplet {
..t.hovered(&Button::Text) ..t.hovered(&Button::Text)
}), }),
}; };
if id == window::Id(0) { let mut vpn_ethernet_col = column![];
self.applet_helper let mut known_wifi = column![];
.icon_button(&self.icon_name) for conn in &self.nm_state.active_conns {
.on_press(Message::TogglePopup) match conn {
.into() ActiveConnectionInfo::Vpn { name, ip_addresses } => {
} else { let mut ipv4 = Vec::with_capacity(ip_addresses.len());
let mut vpn_ethernet_col = column![]; for addr in ip_addresses {
let mut known_wifi = column![]; ipv4.push(
for conn in &self.nm_state.active_conns { text(format!("{}: {}", fl!("ipv4"), addr.to_string()))
match conn { .size(10)
ActiveConnectionInfo::Vpn { name, ip_addresses } => { .into(),
let mut ipv4 = Vec::with_capacity(ip_addresses.len());
for addr in ip_addresses {
ipv4.push(
text(format!("{}: {}", fl!("ipv4"), addr.to_string()))
.size(10)
.into(),
);
}
vpn_ethernet_col = vpn_ethernet_col
.push(column![text(name), Column::with_children(ipv4)].spacing(4));
}
ActiveConnectionInfo::Wired {
name,
hw_address: _,
speed,
ip_addresses,
} => {
let mut ipv4 = Vec::with_capacity(ip_addresses.len());
for addr in ip_addresses {
ipv4.push(
text(format!("{}: {}", fl!("ipv4"), addr.to_string()))
.size(12)
.into(),
);
}
vpn_ethernet_col = vpn_ethernet_col.push(
column![
row![
text(name),
text(format!("{speed} {}", fl!("megabits-per-second")))
]
.spacing(16),
Column::with_children(ipv4),
]
.spacing(4),
); );
} }
ActiveConnectionInfo::WiFi { vpn_ethernet_col = vpn_ethernet_col
name, .push(column![text(name), Column::with_children(ipv4)].spacing(4));
ip_addresses,
state,
strength,
..
} => {
let mut ipv4 = Vec::with_capacity(ip_addresses.len());
for addr in ip_addresses {
ipv4.push(
text(format!("{}: {}", fl!("ipv4"), addr.to_string()))
.size(12)
.into(),
);
}
let mut btn_content = vec![
icon(wifi_icon(*strength), 24).style(Svg::Symbolic).into(),
column![text(name).size(14), Column::with_children(ipv4)]
.width(Length::Fill)
.into(),
];
match state {
ActiveConnectionState::Activating
| ActiveConnectionState::Deactivating => {
btn_content.push(
icon("process-working-symbolic", 24)
.style(Svg::Symbolic)
.into(),
);
}
ActiveConnectionState::Activated => btn_content.push(
text(format!("{}", fl!("connected")))
.size(14)
.horizontal_alignment(Horizontal::Right)
.vertical_alignment(Vertical::Center)
.into(),
),
_ => {}
};
known_wifi = known_wifi.push(
column![button(Button::Secondary)
.custom(vec![Row::with_children(btn_content)
.align_items(Alignment::Center)
.spacing(8)
.into()])
.padding([8, 24])
.style(button_style())
.on_press(Message::Disconnect(name.clone()))]
.align_items(Alignment::Center),
);
}
};
}
let mut content = column![
vpn_ethernet_col,
container(
anim!(
//toggler
AIRPLANE_MODE,
&self.timeline,
fl!("airplane-mode"),
self.nm_state.airplane_mode,
|_chain, enable| { Message::ToggleAirplaneMode(enable) },
)
.text_size(14)
.width(Length::Fill)
)
.padding([0, 12]),
divider::horizontal::light(),
container(
anim!(
//toggler
WIFI,
&self.timeline,
fl!("wifi"),
self.nm_state.wifi_enabled,
|_chain, enable| { Message::ToggleWiFi(enable) },
)
.text_size(14)
.width(Length::Fill)
)
.padding([0, 12]),
divider::horizontal::light(),
]
.align_items(Alignment::Center)
.spacing(8)
.padding([8, 0]);
if self.nm_state.airplane_mode {
content = content.push(
column!(
icon("airplane-mode-symbolic", 48).style(Svg::Symbolic),
text(fl!("airplane-mode-on")).size(14),
text(fl!("turn-off-airplane-mode")).size(12)
)
.spacing(8)
.align_items(Alignment::Center)
.width(Length::Fill),
);
} else {
for known in &self.nm_state.known_access_points {
let mut btn_content = Vec::with_capacity(2);
let ssid = text(&known.ssid).size(14).width(Length::Fill);
if known.working {
btn_content.push(
icon("network-wireless-acquiring-symbolic", 24)
.style(Svg::Symbolic)
.into(),
);
btn_content.push(ssid.into());
btn_content.push(
icon("process-working-symbolic", 24)
.style(Svg::Symbolic)
.into(),
);
} else if matches!(known.state, DeviceState::Unavailable) {
btn_content.push(
icon("network-wireless-disconnected-symbolic", 24)
.style(Svg::Symbolic)
.into(),
);
btn_content.push(ssid.into());
} else {
btn_content.push(
icon(wifi_icon(known.strength), 24)
.style(Svg::Symbolic)
.into(),
);
btn_content.push(ssid.into());
}
let mut btn = button(Button::Secondary)
.custom(vec![Row::with_children(btn_content)
.align_items(Alignment::Center)
.spacing(8)
.into()])
.padding([8, 24])
.width(Length::Fill)
.style(button_style());
btn = match known.state {
DeviceState::Failed
| DeviceState::Unknown
| DeviceState::Unmanaged
| DeviceState::Disconnected
| DeviceState::NeedAuth => {
btn.on_press(Message::ActivateKnownWifi(known.ssid.clone()))
}
DeviceState::Activated => {
btn.on_press(Message::Disconnect(known.ssid.clone()))
}
_ => btn,
};
known_wifi = known_wifi.push(row![btn].align_items(Alignment::Center));
} }
content = content.push(known_wifi); ActiveConnectionInfo::Wired {
let dropdown_icon = if self.show_visible_networks { name,
"go-down-symbolic" hw_address: _,
} else { speed,
"go-next-symbolic" ip_addresses,
}; } => {
let available_connections_btn = button(Button::Secondary) let mut ipv4 = Vec::with_capacity(ip_addresses.len());
.custom( for addr in ip_addresses {
vec![ ipv4.push(
text(fl!("visible-wireless-networks")) text(format!("{}: {}", fl!("ipv4"), addr.to_string()))
.size(14) .size(12)
.width(Length::Fill)
.height(Length::Fixed(24.0))
.vertical_alignment(Vertical::Center)
.into(),
container(icon(dropdown_icon, 14).style(Svg::Symbolic))
.align_x(Horizontal::Center)
.align_y(Vertical::Center)
.width(Length::Fixed(24.0))
.height(Length::Fixed(24.0))
.into(), .into(),
);
}
vpn_ethernet_col = vpn_ethernet_col.push(
column![
row![
text(name),
text(format!("{speed} {}", fl!("megabits-per-second")))
]
.spacing(16),
Column::with_children(ipv4),
] ]
.into(), .spacing(4),
)
.padding([8, 24])
.style(button_style())
.on_press(Message::ToggleVisibleNetworks);
content = content.push(available_connections_btn);
}
if self.show_visible_networks {
if let Some(new_conn_state) = self.new_connection.as_ref() {
match new_conn_state {
NewConnectionState::EnterPassword {
access_point,
password,
} => {
let id = row![
icon("network-wireless-acquiring-symbolic", 24)
.style(Svg::Symbolic),
text(&access_point.ssid).size(14),
]
.align_items(Alignment::Center)
.width(Length::Fill)
.padding([0, 24])
.spacing(12);
content = content.push(id);
let col = column![
text(fl!("enter-password")),
text_input("", password)
.on_input(Message::Password)
.on_paste(Message::Password)
.on_submit(Message::SubmitPassword)
.password(),
container(text(fl!("router-wps-button"))).padding(8),
row![
button(Button::Secondary)
.custom(vec![container(text(fl!("cancel")))
.padding([0, 24])
.into()])
.on_press(Message::CancelNewConnection),
button(Button::Secondary)
.custom(vec![container(text(fl!("connect")))
.padding([0, 24])
.into()])
.on_press(Message::SubmitPassword)
]
.spacing(24)
]
.spacing(8)
.padding([0, 48])
.align_items(Alignment::Center);
content = content.push(col);
}
NewConnectionState::Waiting(access_point) => {
let id = row![
icon("network-wireless-acquiring-symbolic", 24)
.style(Svg::Symbolic),
text(&access_point.ssid).size(14),
]
.align_items(Alignment::Center)
.width(Length::Fill)
.spacing(12);
let connecting = row![
id,
icon("process-working-symbolic", 24).style(Svg::Symbolic),
]
.spacing(8)
.padding([0, 24]);
content = content.push(connecting);
}
NewConnectionState::Failure(access_point) => {
let id = row![
icon("network-wireless-error-symbolic", 24).style(Svg::Symbolic),
text(&access_point.ssid).size(14),
]
.align_items(Alignment::Center)
.width(Length::Fill)
.padding([0, 24])
.spacing(12);
content = content.push(id);
let col = column![
text(fl!("unable-to-connect")),
text(fl!("check-wifi-connection")),
row![
button(Button::Secondary)
.custom(vec![container(text("Cancel"))
.padding([0, 24])
.into()])
.on_press(Message::CancelNewConnection),
button(Button::Secondary)
.custom(vec![container(text("Connect"))
.padding([0, 24])
.into()])
.on_press(Message::SelectWirelessAccessPoint(
access_point.clone()
))
]
.spacing(24)
]
.spacing(16)
.padding([0, 48])
.align_items(Alignment::Center);
content = content.push(col);
}
}
} else if self.nm_state.wifi_enabled {
let mut list_col =
Vec::with_capacity(self.nm_state.wireless_access_points.len());
for ap in &self.nm_state.wireless_access_points {
if self
.nm_state
.active_conns
.iter()
.any(|a| ap.ssid == a.name())
{
continue;
}
let button = button(button_style())
.custom(vec![row![
icon(wifi_icon(ap.strength), 16).style(Svg::Symbolic),
text(&ap.ssid)
.size(14)
.height(Length::Fixed(24.0))
.vertical_alignment(Vertical::Center)
]
.align_items(Alignment::Center)
.spacing(12)
.into()])
.on_press(Message::SelectWirelessAccessPoint(ap.clone()))
.width(Length::Fill)
.padding([8, 24]);
list_col.push(button.into());
}
content = content.push(
scrollable(Column::with_children(list_col)).height(Length::Fixed(300.0)),
); );
} }
} ActiveConnectionInfo::WiFi {
self.applet_helper.popup_container(content).into() name,
ip_addresses,
state,
strength,
..
} => {
let mut ipv4 = Vec::with_capacity(ip_addresses.len());
for addr in ip_addresses {
ipv4.push(
text(format!("{}: {}", fl!("ipv4"), addr.to_string()))
.size(12)
.into(),
);
}
let mut btn_content = vec![
icon(wifi_icon(*strength), 24).style(Svg::Symbolic).into(),
column![text(name).size(14), Column::with_children(ipv4)]
.width(Length::Fill)
.into(),
];
match state {
ActiveConnectionState::Activating | ActiveConnectionState::Deactivating => {
btn_content.push(
icon("process-working-symbolic", 24)
.style(Svg::Symbolic)
.into(),
);
}
ActiveConnectionState::Activated => btn_content.push(
text(format!("{}", fl!("connected")))
.size(14)
.horizontal_alignment(Horizontal::Right)
.vertical_alignment(Vertical::Center)
.into(),
),
_ => {}
};
known_wifi = known_wifi.push(
column![button(Button::Secondary)
.custom(vec![Row::with_children(btn_content)
.align_items(Alignment::Center)
.spacing(8)
.into()])
.padding([8, 24])
.style(button_style())
.on_press(Message::Disconnect(name.clone()))]
.align_items(Alignment::Center),
);
}
};
} }
let mut content = column![
vpn_ethernet_col,
container(
anim!(
//toggler
AIRPLANE_MODE,
&self.timeline,
fl!("airplane-mode"),
self.nm_state.airplane_mode,
|_chain, enable| { Message::ToggleAirplaneMode(enable) },
)
.text_size(14)
.width(Length::Fill)
)
.padding([0, 12]),
divider::horizontal::light(),
container(
anim!(
//toggler
WIFI,
&self.timeline,
fl!("wifi"),
self.nm_state.wifi_enabled,
|_chain, enable| { Message::ToggleWiFi(enable) },
)
.text_size(14)
.width(Length::Fill)
)
.padding([0, 12]),
divider::horizontal::light(),
]
.align_items(Alignment::Center)
.spacing(8)
.padding([8, 0]);
if self.nm_state.airplane_mode {
content = content.push(
column!(
icon("airplane-mode-symbolic", 48).style(Svg::Symbolic),
text(fl!("airplane-mode-on")).size(14),
text(fl!("turn-off-airplane-mode")).size(12)
)
.spacing(8)
.align_items(Alignment::Center)
.width(Length::Fill),
);
} else {
for known in &self.nm_state.known_access_points {
let mut btn_content = Vec::with_capacity(2);
let ssid = text(&known.ssid).size(14).width(Length::Fill);
if known.working {
btn_content.push(
icon("network-wireless-acquiring-symbolic", 24)
.style(Svg::Symbolic)
.into(),
);
btn_content.push(ssid.into());
btn_content.push(
icon("process-working-symbolic", 24)
.style(Svg::Symbolic)
.into(),
);
} else if matches!(known.state, DeviceState::Unavailable) {
btn_content.push(
icon("network-wireless-disconnected-symbolic", 24)
.style(Svg::Symbolic)
.into(),
);
btn_content.push(ssid.into());
} else {
btn_content.push(
icon(wifi_icon(known.strength), 24)
.style(Svg::Symbolic)
.into(),
);
btn_content.push(ssid.into());
}
let mut btn = button(Button::Secondary)
.custom(vec![Row::with_children(btn_content)
.align_items(Alignment::Center)
.spacing(8)
.into()])
.padding([8, 24])
.width(Length::Fill)
.style(button_style());
btn = match known.state {
DeviceState::Failed
| DeviceState::Unknown
| DeviceState::Unmanaged
| DeviceState::Disconnected
| DeviceState::NeedAuth => {
btn.on_press(Message::ActivateKnownWifi(known.ssid.clone()))
}
DeviceState::Activated => btn.on_press(Message::Disconnect(known.ssid.clone())),
_ => btn,
};
known_wifi = known_wifi.push(row![btn].align_items(Alignment::Center));
}
content = content.push(known_wifi);
let dropdown_icon = if self.show_visible_networks {
"go-down-symbolic"
} else {
"go-next-symbolic"
};
let available_connections_btn = button(Button::Secondary)
.custom(
vec![
text(fl!("visible-wireless-networks"))
.size(14)
.width(Length::Fill)
.height(Length::Fixed(24.0))
.vertical_alignment(Vertical::Center)
.into(),
container(icon(dropdown_icon, 14).style(Svg::Symbolic))
.align_x(Horizontal::Center)
.align_y(Vertical::Center)
.width(Length::Fixed(24.0))
.height(Length::Fixed(24.0))
.into(),
]
.into(),
)
.padding([8, 24])
.style(button_style())
.on_press(Message::ToggleVisibleNetworks);
content = content.push(available_connections_btn);
}
if self.show_visible_networks {
if let Some(new_conn_state) = self.new_connection.as_ref() {
match new_conn_state {
NewConnectionState::EnterPassword {
access_point,
password,
} => {
let id = row![
icon("network-wireless-acquiring-symbolic", 24).style(Svg::Symbolic),
text(&access_point.ssid).size(14),
]
.align_items(Alignment::Center)
.width(Length::Fill)
.padding([0, 24])
.spacing(12);
content = content.push(id);
let col = column![
text(fl!("enter-password")),
text_input("", password)
.on_input(Message::Password)
.on_paste(Message::Password)
.on_submit(Message::SubmitPassword)
.password(),
container(text(fl!("router-wps-button"))).padding(8),
row![
button(Button::Secondary)
.custom(vec![container(text(fl!("cancel")))
.padding([0, 24])
.into()])
.on_press(Message::CancelNewConnection),
button(Button::Secondary)
.custom(vec![container(text(fl!("connect")))
.padding([0, 24])
.into()])
.on_press(Message::SubmitPassword)
]
.spacing(24)
]
.spacing(8)
.padding([0, 48])
.align_items(Alignment::Center);
content = content.push(col);
}
NewConnectionState::Waiting(access_point) => {
let id = row![
icon("network-wireless-acquiring-symbolic", 24).style(Svg::Symbolic),
text(&access_point.ssid).size(14),
]
.align_items(Alignment::Center)
.width(Length::Fill)
.spacing(12);
let connecting = row![
id,
icon("process-working-symbolic", 24).style(Svg::Symbolic),
]
.spacing(8)
.padding([0, 24]);
content = content.push(connecting);
}
NewConnectionState::Failure(access_point) => {
let id = row![
icon("network-wireless-error-symbolic", 24).style(Svg::Symbolic),
text(&access_point.ssid).size(14),
]
.align_items(Alignment::Center)
.width(Length::Fill)
.padding([0, 24])
.spacing(12);
content = content.push(id);
let col = column![
text(fl!("unable-to-connect")),
text(fl!("check-wifi-connection")),
row![
button(Button::Secondary)
.custom(vec![container(text("Cancel")).padding([0, 24]).into()])
.on_press(Message::CancelNewConnection),
button(Button::Secondary)
.custom(vec![container(text("Connect"))
.padding([0, 24])
.into()])
.on_press(Message::SelectWirelessAccessPoint(
access_point.clone()
))
]
.spacing(24)
]
.spacing(16)
.padding([0, 48])
.align_items(Alignment::Center);
content = content.push(col);
}
}
} else if self.nm_state.wifi_enabled {
let mut list_col = Vec::with_capacity(self.nm_state.wireless_access_points.len());
for ap in &self.nm_state.wireless_access_points {
if self
.nm_state
.active_conns
.iter()
.any(|a| ap.ssid == a.name())
{
continue;
}
let button = button(button_style())
.custom(vec![row![
icon(wifi_icon(ap.strength), 16).style(Svg::Symbolic),
text(&ap.ssid)
.size(14)
.height(Length::Fixed(24.0))
.vertical_alignment(Vertical::Center)
]
.align_items(Alignment::Center)
.spacing(12)
.into()])
.on_press(Message::SelectWirelessAccessPoint(ap.clone()))
.width(Length::Fill)
.padding([8, 24]);
list_col.push(button.into());
}
content = content
.push(scrollable(Column::with_children(list_col)).height(Length::Fixed(300.0)));
}
}
self.core.applet_helper.popup_container(content).into()
} }
fn subscription(&self) -> Subscription<Message> { fn subscription(&self) -> Subscription<Message> {
@ -782,7 +767,6 @@ impl Application for CosmicNetworkApplet {
if let Some(conn) = self.conn.as_ref() { if let Some(conn) = self.conn.as_ref() {
Subscription::batch(vec![ Subscription::batch(vec![
self.applet_helper.theme_subscription(0).map(Message::Theme),
timeline, timeline,
network_sub, network_sub,
active_conns_subscription(self.toggle_wifi_ctr, conn.clone()) active_conns_subscription(self.toggle_wifi_ctr, conn.clone())
@ -797,20 +781,7 @@ impl Application for CosmicNetworkApplet {
} }
} }
fn theme(&self) -> Theme { fn style(&self) -> Option<<Theme as application::StyleSheet>::Style> {
self.theme.clone() Some(cosmic::app::applet::style())
}
fn close_requested(&self, _id: window::Id) -> Self::Message {
Message::Ignore
}
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|theme| {
application::Appearance {
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().on_bg_color().into(),
}
}))
} }
} }

View file

@ -8,7 +8,6 @@ license = "GPL-3.0-or-later"
anyhow = "1" anyhow = "1"
libcosmic.workspace = true libcosmic.workspace = true
cosmic-time.workspace = true cosmic-time.workspace = true
cosmic-applet = { path = "../applet" }
nix = "0.26" nix = "0.26"
tokio = { version = "1.24.1", features = ["sync", "rt", "tracing", "macros", "net", "io-util", "io-std"] } tokio = { version = "1.24.1", features = ["sync", "rt", "tracing", "macros", "net", "io-util", "io-std"] }
cosmic-notifications-util = { git = "https://github.com/pop-os/cosmic-notifications" } cosmic-notifications-util = { git = "https://github.com/pop-os/cosmic-notifications" }

View file

@ -1,19 +1,19 @@
mod localize; mod localize;
mod subscriptions; mod subscriptions;
use cosmic::app::{applet::applet_button_theme, Command};
use cosmic::cosmic_config::{config_subscription, Config, CosmicConfigEntry}; use cosmic::cosmic_config::{config_subscription, Config, CosmicConfigEntry};
use cosmic::iced::wayland::popup::{destroy_popup, get_popup}; use cosmic::iced::wayland::popup::{destroy_popup, get_popup};
use cosmic::iced::Limits; use cosmic::iced::Limits;
use cosmic::iced::{ use cosmic::iced::{
widget::{button, column, row, text, Row}, widget::{button, column, row, text, Row},
window, Alignment, Application, Color, Command, Length, Subscription, window, Alignment, Length, Subscription,
}; };
use cosmic::iced_core::alignment::Horizontal; use cosmic::iced_core::alignment::Horizontal;
use cosmic::iced_core::image; use cosmic::iced_core::image;
use cosmic::iced_widget::image::Handle; use cosmic::iced_widget::image::Handle;
use cosmic_applet::{applet_button_theme, CosmicAppletHelper};
use cosmic::iced_style::application::{self, Appearance}; use cosmic::iced_style::application;
use cosmic::iced_widget::{horizontal_rule, scrollable, Column}; use cosmic::iced_widget::{horizontal_rule, scrollable, Column};
use cosmic::theme::Svg; use cosmic::theme::Svg;
@ -25,7 +25,7 @@ use cosmic_notifications_util::{Image, Notification};
use cosmic_time::{anim, chain, id, once_cell::sync::Lazy, Instant, Timeline}; use cosmic_time::{anim, chain, id, once_cell::sync::Lazy, Instant, Timeline};
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::{Path, PathBuf}; use std::path::PathBuf;
use std::process; use std::process;
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use tracing::info; use tracing::info;
@ -38,15 +38,13 @@ pub async fn main() -> cosmic::iced::Result {
info!("Notifications applet"); info!("Notifications applet");
let helper = CosmicAppletHelper::default(); cosmic::app::applet::run::<Notifications>(false, ())
Notifications::run(helper.window_settings())
} }
static DO_NOT_DISTURB: Lazy<id::Toggler> = Lazy::new(id::Toggler::unique); static DO_NOT_DISTURB: Lazy<id::Toggler> = Lazy::new(id::Toggler::unique);
#[derive(Default)] #[derive(Default)]
struct Notifications { struct Notifications {
applet_helper: CosmicAppletHelper, core: cosmic::app::Core,
theme: Theme,
config: NotificationsConfig, config: NotificationsConfig,
config_helper: Option<Config>, config_helper: Option<Config>,
icon_name: String, icon_name: String,
@ -88,9 +86,7 @@ enum Message {
TogglePopup, TogglePopup,
DoNotDisturb(chain::Toggler, bool), DoNotDisturb(chain::Toggler, bool),
Settings, Settings,
Ignore,
Frame(Instant), Frame(Instant),
Theme(Theme),
NotificationEvent(Notification), NotificationEvent(Notification),
Config(NotificationsConfig), Config(NotificationsConfig),
DbusEvent(subscriptions::dbus::Output), DbusEvent(subscriptions::dbus::Output),
@ -99,15 +95,13 @@ enum Message {
CardsToggled(String, bool), CardsToggled(String, bool),
} }
impl Application for Notifications { impl cosmic::Application for Notifications {
type Message = Message; type Message = Message;
type Theme = Theme;
type Executor = cosmic::SingleThreadExecutor; type Executor = cosmic::SingleThreadExecutor;
type Flags = (); type Flags = ();
const APP_ID: &'static str = "com.system76.CosmicAppletNotifications";
fn new(_flags: ()) -> (Notifications, Command<Message>) { fn init(core: cosmic::app::Core, _flags: ()) -> (Self, Command<Message>) {
let applet_helper = CosmicAppletHelper::default();
let theme = applet_helper.theme();
let helper = Config::new( let helper = Config::new(
cosmic_notifications_config::ID, cosmic_notifications_config::ID,
NotificationsConfig::version(), NotificationsConfig::version(),
@ -126,8 +120,7 @@ impl Application for Notifications {
}) })
.unwrap_or_default(); .unwrap_or_default();
let mut _self = Notifications { let mut _self = Notifications {
applet_helper, core,
theme,
config_helper: helper, config_helper: helper,
config, config,
..Default::default() ..Default::default()
@ -136,28 +129,20 @@ impl Application for Notifications {
(_self, Command::none()) (_self, Command::none())
} }
fn title(&self) -> String { fn core(&self) -> &cosmic::app::Core {
String::from("Notifications") &self.core
} }
fn theme(&self) -> Theme { fn core_mut(&mut self) -> &mut cosmic::app::Core {
self.theme.clone() &mut self.core
} }
fn close_requested(&self, _id: window::Id) -> Self::Message { fn style(&self) -> Option<<Theme as application::StyleSheet>::Style> {
Message::Ignore Some(cosmic::app::applet::style())
}
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|theme| Appearance {
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().on_bg_color().into(),
}))
} }
fn subscription(&self) -> Subscription<Message> { fn subscription(&self) -> Subscription<Message> {
Subscription::batch(vec![ Subscription::batch(vec![
self.applet_helper.theme_subscription(0).map(Message::Theme),
config_subscription::<u64, NotificationsConfig>( config_subscription::<u64, NotificationsConfig>(
0, 0,
cosmic_notifications_config::ID.into(), cosmic_notifications_config::ID.into(),
@ -182,9 +167,6 @@ impl Application for Notifications {
fn update(&mut self, message: Message) -> Command<Message> { fn update(&mut self, message: Message) -> Command<Message> {
match message { match message {
Message::Theme(t) => {
self.theme = t;
}
Message::Frame(now) => { Message::Frame(now) => {
self.timeline.now(now); self.timeline.now(now);
} }
@ -196,7 +178,7 @@ impl Application for Notifications {
let new_id = window::Id(self.id_ctr); let new_id = window::Id(self.id_ctr);
self.popup.replace(new_id); self.popup.replace(new_id);
let mut popup_settings = self.applet_helper.get_popup_settings( let mut popup_settings = self.core.applet_helper.get_popup_settings(
window::Id(0), window::Id(0),
new_id, new_id,
None, None,
@ -247,7 +229,6 @@ impl Application for Notifications {
)); ));
} }
} }
Message::Ignore => {}
Message::Config(config) => { Message::Config(config) => {
self.config = config; self.config = config;
} }
@ -320,189 +301,190 @@ impl Application for Notifications {
Command::none() Command::none()
} }
fn view(&self, id: window::Id) -> Element<Message> { fn view(&self) -> Element<Message> {
if id == window::Id(0) { self.core
self.applet_helper .applet_helper
.icon_button(&self.icon_name) .icon_button(&self.icon_name)
.on_press(Message::TogglePopup) .on_press(Message::TogglePopup)
.into() .into()
} else { }
let do_not_disturb = row![anim!(
DO_NOT_DISTURB, fn view_window(&self, _id: window::Id) -> Element<Message> {
&self.timeline, let do_not_disturb = row![anim!(
String::from(fl!("do-not-disturb")), DO_NOT_DISTURB,
self.config.do_not_disturb, &self.timeline,
Message::DoNotDisturb String::from(fl!("do-not-disturb")),
self.config.do_not_disturb,
Message::DoNotDisturb
)
.width(Length::Fill)]
.padding([0, 24]);
let settings =
row_button(vec![text(fl!("notification-settings")).into()]).on_press(Message::Settings);
let notifications = if self.cards.is_empty() {
row![container(
column![
text_icon("cosmic-applet-notification-symbolic", 40),
text(&fl!("no-notifications"))
]
.align_items(Alignment::Center)
) )
.width(Length::Fill)] .width(Length::Fill)
.padding([0, 24]); .align_x(Horizontal::Center)]
.spacing(12)
} else {
let mut notifs: Vec<Element<_>> = Vec::with_capacity(self.cards.len());
let settings = row_button(vec![text(fl!("notification-settings")).into()]) for c in self.cards.iter().rev() {
.on_press(Message::Settings); if c.1.is_empty() {
continue;
let notifications = if self.cards.is_empty() {
row![container(
column![
text_icon("cosmic-applet-notification-symbolic", 40),
text(&fl!("no-notifications"))
]
.align_items(Alignment::Center)
)
.width(Length::Fill)
.align_x(Horizontal::Center)]
.spacing(12)
} else {
let mut notifs: Vec<Element<_>> = Vec::with_capacity(self.cards.len());
for c in self.cards.iter().rev() {
if c.1.is_empty() {
continue;
}
let name = c.1[0].app_name.clone();
let notif_elems: Vec<_> =
c.1.iter()
.rev()
.map(|n| {
let app_name = text(if n.app_name.len() > 24 {
Cow::from(format!(
"{:.26}...",
n.app_name.lines().next().unwrap_or_default()
))
} else {
Cow::from(&n.app_name)
})
.size(12)
.width(Length::Fill);
let duration_since = text(duration_ago_msg(n)).size(12);
let close_notif =
button(icon("window-close-symbolic", 16).style(Svg::Symbolic))
.on_press(Message::Dismissed(n.id))
.style(cosmic::theme::Button::Text);
Element::from(
column!(
match n.image() {
Some(cosmic_notifications_util::Image::File(path)) => {
row![
icon(path.as_path(), 16),
app_name,
duration_since,
close_notif
]
.spacing(8)
.align_items(Alignment::Center)
}
Some(cosmic_notifications_util::Image::Name(name)) => {
row![
icon(name.as_str(), 16),
app_name,
duration_since,
close_notif
]
.spacing(8)
.align_items(Alignment::Center)
}
Some(cosmic_notifications_util::Image::Data {
width,
height,
data,
}) => {
let handle = image::Handle::from_pixels(
*width,
*height,
data.clone(),
);
row![
icon(handle, 16),
app_name,
duration_since,
close_notif
]
.spacing(8)
.align_items(Alignment::Center)
}
None => row![app_name, duration_since, close_notif]
.spacing(8)
.align_items(Alignment::Center),
},
column![
text(n.summary.lines().next().unwrap_or_default())
.width(Length::Fill)
.size(14),
text(n.body.lines().next().unwrap_or_default())
.width(Length::Fill)
.size(12)
]
)
.width(Length::Fill),
)
})
.collect();
let show_more_icon = c.1.last().and_then(|n| {
info!("app_icon: {:?}", &n.app_icon);
if n.app_icon.is_empty() {
match n.image().cloned() {
Some(Image::File(p)) => Some(cosmic::widget::IconSource::Path(
Cow::Owned(PathBuf::from(p)),
)),
Some(Image::Name(name)) => {
Some(cosmic::widget::IconSource::Name(Cow::Owned(name)))
}
Some(Image::Data {
width,
height,
data,
}) => Some(cosmic::widget::IconSource::Handle(
icon::Handle::Image(Handle::from_pixels(width, height, data)),
)),
None => None,
}
} else if let Some(path) = url::Url::parse(&n.app_icon)
.ok()
.and_then(|u| u.to_file_path().ok())
{
Some(cosmic::widget::IconSource::Path(Cow::Owned(path)))
} else {
Some(cosmic::widget::IconSource::Name(Cow::Borrowed(&n.app_icon)))
}
});
let card_list = anim!(
//cards
c.0.clone(),
&self.timeline,
notif_elems,
Message::ClearAll(name.clone()),
move |_, e| Message::CardsToggled(name.clone(), e),
&c.3,
"Show Less",
// &format!("Show {} More", c.1.len().saturating_sub(1)),
"Clear All",
show_more_icon,
c.2,
);
notifs.push(card_list.into());
} }
let name = c.1[0].app_name.clone();
let notif_elems: Vec<_> =
c.1.iter()
.rev()
.map(|n| {
let app_name = text(if n.app_name.len() > 24 {
Cow::from(format!(
"{:.26}...",
n.app_name.lines().next().unwrap_or_default()
))
} else {
Cow::from(&n.app_name)
})
.size(12)
.width(Length::Fill);
row!(scrollable( let duration_since = text(duration_ago_msg(n)).size(12);
Column::with_children(notifs)
.spacing(8)
.height(Length::Shrink),
)
.height(Length::Shrink))
};
let main_content = column![horizontal_rule(4), notifications, horizontal_rule(4)] let close_notif =
.padding([0, 24]) button(icon("window-close-symbolic", 16).style(Svg::Symbolic))
.spacing(12); .on_press(Message::Dismissed(n.id))
.style(cosmic::theme::Button::Text);
Element::from(
column!(
match n.image() {
Some(cosmic_notifications_util::Image::File(path)) => {
row![
icon(path.as_path(), 16),
app_name,
duration_since,
close_notif
]
.spacing(8)
.align_items(Alignment::Center)
}
Some(cosmic_notifications_util::Image::Name(name)) => {
row![
icon(name.as_str(), 16),
app_name,
duration_since,
close_notif
]
.spacing(8)
.align_items(Alignment::Center)
}
Some(cosmic_notifications_util::Image::Data {
width,
height,
data,
}) => {
let handle = image::Handle::from_pixels(
*width,
*height,
data.clone(),
);
row![
icon(handle, 16),
app_name,
duration_since,
close_notif
]
.spacing(8)
.align_items(Alignment::Center)
}
None => row![app_name, duration_since, close_notif]
.spacing(8)
.align_items(Alignment::Center),
},
column![
text(n.summary.lines().next().unwrap_or_default())
.width(Length::Fill)
.size(14),
text(n.body.lines().next().unwrap_or_default())
.width(Length::Fill)
.size(12)
]
)
.width(Length::Fill),
)
})
.collect();
let show_more_icon = c.1.last().and_then(|n| {
info!("app_icon: {:?}", &n.app_icon);
if n.app_icon.is_empty() {
match n.image().cloned() {
Some(Image::File(p)) => Some(cosmic::widget::IconSource::Path(
Cow::Owned(PathBuf::from(p)),
)),
Some(Image::Name(name)) => {
Some(cosmic::widget::IconSource::Name(Cow::Owned(name)))
}
Some(Image::Data {
width,
height,
data,
}) => Some(cosmic::widget::IconSource::Handle(icon::Handle::Image(
Handle::from_pixels(width, height, data),
))),
None => None,
}
} else if let Some(path) = url::Url::parse(&n.app_icon)
.ok()
.and_then(|u| u.to_file_path().ok())
{
Some(cosmic::widget::IconSource::Path(Cow::Owned(path)))
} else {
Some(cosmic::widget::IconSource::Name(Cow::Borrowed(&n.app_icon)))
}
});
let card_list = anim!(
//cards
c.0.clone(),
&self.timeline,
notif_elems,
Message::ClearAll(name.clone()),
move |_, e| Message::CardsToggled(name.clone(), e),
&c.3,
"Show Less",
// &format!("Show {} More", c.1.len().saturating_sub(1)),
"Clear All",
show_more_icon,
c.2,
);
notifs.push(card_list.into());
}
let content = column![do_not_disturb, main_content, settings] row!(scrollable(
.align_items(Alignment::Start) Column::with_children(notifs)
.spacing(12) .spacing(8)
.padding([16, 0]); .height(Length::Shrink),
)
.height(Length::Shrink))
};
self.applet_helper.popup_container(content).into() let main_content = column![horizontal_rule(4), notifications, horizontal_rule(4)]
} .padding([0, 24])
.spacing(12);
let content = column![do_not_disturb, main_content, settings]
.align_items(Alignment::Start)
.spacing(12)
.padding([16, 0]);
self.core.applet_helper.popup_container(content).into()
} }
} }

View file

@ -10,7 +10,7 @@ libpulse-binding = "2.26.0"
libpulse-glib-binding = "2.25.0" libpulse-glib-binding = "2.25.0"
tokio = { version = "1.20.1", features=["full"] } tokio = { version = "1.20.1", features=["full"] }
libcosmic.workspace = true libcosmic.workspace = true
cosmic-applet = { path = "../applet" } # cosmic-applet = { path = "../applet" }
nix = "0.26.2" nix = "0.26.2"
zbus = "3.13" zbus = "3.13"
logind-zbus = "3.1" logind-zbus = "3.1"

View file

@ -2,6 +2,8 @@ use std::collections::HashMap;
use std::process; use std::process;
use std::time::Duration; use std::time::Duration;
use cosmic::app::applet::applet_button_theme;
use cosmic::iced;
use cosmic::iced::alignment::{Horizontal, Vertical}; use cosmic::iced::alignment::{Horizontal, Vertical};
use cosmic::iced::event::wayland::{self, LayerEvent}; use cosmic::iced::event::wayland::{self, LayerEvent};
use cosmic::iced::event::PlatformSpecific; use cosmic::iced::event::PlatformSpecific;
@ -15,16 +17,15 @@ use cosmic::iced_sctk::commands::layer_surface::{
use cosmic::iced_widget::mouse_area; use cosmic::iced_widget::mouse_area;
use cosmic::widget::{button, divider, icon}; use cosmic::widget::{button, divider, icon};
use cosmic::Renderer; use cosmic::Renderer;
use cosmic_applet::{applet_button_theme, CosmicAppletHelper};
use cosmic::iced::Color; use cosmic::iced::Color;
use cosmic::iced::{ use cosmic::iced::{
widget::{self, column, container, row, space::Space, text, Row}, widget::{self, column, container, row, space::Space, text, Row},
window, Alignment, Application, Command, Length, Subscription, window, Alignment, Length, Subscription,
}; };
use cosmic::iced_style::application::{self, Appearance}; use cosmic::iced_style::application;
use cosmic::theme::{self, Svg}; use cosmic::theme::{self, Svg};
use cosmic::{Element, Theme}; use cosmic::{app::Command, Element, Theme};
use logind_zbus::manager::ManagerProxy; use logind_zbus::manager::ManagerProxy;
use logind_zbus::session::{SessionProxy, SessionType}; use logind_zbus::session::{SessionProxy, SessionType};
@ -41,15 +42,13 @@ use crate::cosmic_session::CosmicSessionProxy;
use crate::session_manager::SessionManagerProxy; use crate::session_manager::SessionManagerProxy;
pub fn main() -> cosmic::iced::Result { pub fn main() -> cosmic::iced::Result {
let helper = CosmicAppletHelper::default(); cosmic::app::applet::run::<Power>(false, ())
Power::run(helper.window_settings())
} }
#[derive(Default)] #[derive(Default)]
struct Power { struct Power {
applet_helper: CosmicAppletHelper, core: cosmic::app::Core,
icon_name: String, icon_name: String,
theme: Theme,
popup: Option<window::Id>, popup: Option<window::Id>,
id_ctr: u128, id_ctr: u128,
action_to_confirm: Option<(window::Id, PowerAction)>, action_to_confirm: Option<(window::Id, PowerAction)>,
@ -72,24 +71,19 @@ enum Message {
Settings, Settings,
Confirm, Confirm,
Cancel, Cancel,
Closed(window::Id),
Zbus(Result<(), zbus::Error>), Zbus(Result<(), zbus::Error>),
Theme(Theme),
} }
impl Application for Power { impl cosmic::Application for Power {
type Message = Message; type Message = Message;
type Theme = Theme;
type Executor = cosmic::SingleThreadExecutor; type Executor = cosmic::SingleThreadExecutor;
type Flags = (); type Flags = ();
const APP_ID: &'static str = "com.system76.CosmicAppletPower";
fn new(_flags: ()) -> (Power, Command<Message>) { fn init(core: cosmic::app::Core, _flags: ()) -> (Power, Command<Message>) {
let applet_helper = CosmicAppletHelper::default();
let theme = applet_helper.theme();
( (
Power { Power {
applet_helper, core,
theme,
icon_name: "system-shutdown-symbolic".to_string(), icon_name: "system-shutdown-symbolic".to_string(),
..Default::default() ..Default::default()
}, },
@ -97,46 +91,32 @@ impl Application for Power {
) )
} }
fn title(&self) -> String { fn core(&self) -> &cosmic::app::Core {
fl!("power") &self.core
} }
fn theme(&self) -> Theme { fn core_mut(&mut self) -> &mut cosmic::app::Core {
self.theme.clone() &mut self.core
} }
fn close_requested(&self, id: window::Id) -> Self::Message { fn style(&self) -> Option<<Theme as application::StyleSheet>::Style> {
Message::Closed(id) Some(cosmic::app::applet::style())
}
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|theme| Appearance {
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().on_bg_color().into(),
}))
} }
fn subscription(&self) -> Subscription<Message> { fn subscription(&self) -> Subscription<Message> {
Subscription::batch(vec![ events_with(|e, _status| match e {
self.applet_helper.theme_subscription(0).map(Message::Theme), cosmic::iced::Event::PlatformSpecific(PlatformSpecific::Wayland(
events_with(|e, _status| match e { wayland::Event::Layer(LayerEvent::Unfocused, ..),
cosmic::iced::Event::PlatformSpecific(PlatformSpecific::Wayland( )) => Some(Message::Cancel),
wayland::Event::Layer(LayerEvent::Unfocused, ..), // cosmic::iced::Event::PlatformSpecific(PlatformSpecific::Wayland(
)) => Some(Message::Cancel), // wayland::Event::Seat(wayland::SeatEvent::Leave, _),
// cosmic::iced::Event::PlatformSpecific(PlatformSpecific::Wayland( // )) => Some(Message::Cancel),
// wayland::Event::Seat(wayland::SeatEvent::Leave, _), _ => None,
// )) => Some(Message::Cancel), })
_ => None,
}),
])
} }
fn update(&mut self, message: Message) -> Command<Message> { fn update(&mut self, message: Message) -> Command<Message> {
match message { match message {
Message::Theme(t) => {
self.theme = t;
Command::none()
}
Message::TogglePopup => { Message::TogglePopup => {
if let Some(p) = self.popup.take() { if let Some(p) = self.popup.take() {
destroy_popup(p) destroy_popup(p)
@ -145,7 +125,7 @@ impl Application for Power {
let new_id = window::Id(self.id_ctr); let new_id = window::Id(self.id_ctr);
self.popup.replace(new_id); self.popup.replace(new_id);
let mut popup_settings = self.applet_helper.get_popup_settings( let mut popup_settings = self.core.applet_helper.get_popup_settings(
window::Id(0), window::Id(0),
new_id, new_id,
None, None,
@ -169,8 +149,8 @@ impl Application for Power {
let id = window::Id(self.id_ctr); let id = window::Id(self.id_ctr);
self.action_to_confirm = Some((id, action)); self.action_to_confirm = Some((id, action));
return Command::batch(vec![ return Command::batch(vec![
Command::perform(sleep(Duration::from_secs(60)), move |_| { iced::Command::perform(sleep(Duration::from_secs(60)), move |_| {
Message::Timeout(id) cosmic::app::message::app(Message::Timeout(id))
}), }),
get_layer_surface(SctkLayerSurfaceSettings { get_layer_surface(SctkLayerSurfaceSettings {
id, id,
@ -191,14 +171,15 @@ impl Application for Power {
} }
Message::Confirm => { Message::Confirm => {
if let Some((id, a)) = self.action_to_confirm.take() { if let Some((id, a)) = self.action_to_confirm.take() {
let msg = |m| cosmic::app::message::app(Message::Zbus(m));
Command::batch(vec![ Command::batch(vec![
destroy_layer_surface(id), destroy_layer_surface(id),
match a { match a {
PowerAction::Lock => Command::perform(lock(), Message::Zbus), PowerAction::Lock => iced::Command::perform(lock(), msg),
PowerAction::LogOut => Command::perform(log_out(), Message::Zbus), PowerAction::LogOut => iced::Command::perform(log_out(), msg),
PowerAction::Suspend => Command::perform(suspend(), Message::Zbus), PowerAction::Suspend => iced::Command::perform(suspend(), msg),
PowerAction::Restart => Command::perform(restart(), Message::Zbus), PowerAction::Restart => iced::Command::perform(restart(), msg),
PowerAction::Shutdown => Command::perform(shutdown(), Message::Zbus), PowerAction::Shutdown => iced::Command::perform(shutdown(), msg),
}, },
]) ])
} else { } else {
@ -211,32 +192,19 @@ impl Application for Power {
} }
Command::none() Command::none()
} }
Message::Closed(id) => {
if let Some((surface_id, _)) = self.action_to_confirm {
if id == surface_id {
self.action_to_confirm = None;
return destroy_layer_surface(id);
}
}
if id == window::Id(0) {
process::exit(0);
}
Command::none()
}
Message::Timeout(id) => { Message::Timeout(id) => {
if let Some((surface_id, a)) = self.action_to_confirm { if let Some((surface_id, a)) = self.action_to_confirm {
if id == surface_id { if id == surface_id {
self.action_to_confirm = None; self.action_to_confirm = None;
let msg = |m: zbus::Result<()>| cosmic::app::message::app(Message::Zbus(m));
return Command::batch(vec![ return Command::batch(vec![
destroy_layer_surface(id), destroy_layer_surface(id),
match a { match a {
PowerAction::Lock => Command::perform(lock(), Message::Zbus), PowerAction::Lock => iced::Command::perform(lock(), msg),
PowerAction::LogOut => Command::perform(log_out(), Message::Zbus), PowerAction::LogOut => iced::Command::perform(log_out(), msg),
PowerAction::Suspend => Command::perform(suspend(), Message::Zbus), PowerAction::Suspend => iced::Command::perform(suspend(), msg),
PowerAction::Restart => Command::perform(restart(), Message::Zbus), PowerAction::Restart => iced::Command::perform(restart(), msg),
PowerAction::Shutdown => { PowerAction::Shutdown => iced::Command::perform(shutdown(), msg),
Command::perform(shutdown(), Message::Zbus)
}
}, },
]); ]);
} }
@ -246,7 +214,15 @@ impl Application for Power {
} }
} }
fn view(&self, id: window::Id) -> Element<Message> { fn view(&self) -> Element<Message> {
self.core
.applet_helper
.icon_button(&self.icon_name)
.on_press(Message::TogglePopup)
.into()
}
fn view_window(&self, id: window::Id) -> Element<Message> {
if matches!(self.popup, Some(p) if p == id) { if matches!(self.popup, Some(p) if p == id) {
let settings = let settings =
row_button(vec![text(fl!("settings")).size(14).into()]).on_press(Message::Settings); row_button(vec![text(fl!("settings")).size(14).into()]).on_press(Message::Settings);
@ -294,7 +270,7 @@ impl Application for Power {
.spacing(12) .spacing(12)
.padding([8, 0]); .padding([8, 0]);
self.applet_helper.popup_container(content).into() self.core.applet_helper.popup_container(content).into()
} else if matches!(self.action_to_confirm, Some((c_id, _)) if c_id == id) { } else if matches!(self.action_to_confirm, Some((c_id, _)) if c_id == id) {
let action = match self.action_to_confirm.as_ref().unwrap().1 { let action = match self.action_to_confirm.as_ref().unwrap().1 {
PowerAction::Lock => "lock-screen", PowerAction::Lock => "lock-screen",
@ -350,10 +326,8 @@ impl Application for Power {
.on_middle_press(Message::Cancel) .on_middle_press(Message::Cancel)
.into() .into()
} else { } else {
self.applet_helper //panic!("no view for window {}", id.0)
.icon_button(&self.icon_name) widget::text("").into()
.on_press(Message::TogglePopup)
.into()
} }
} }
} }

View file

@ -7,6 +7,5 @@ license = "GPL-3.0-or-later"
[dependencies] [dependencies]
icon-loader = { version = "0.3.6", features = ["gtk"] } icon-loader = { version = "0.3.6", features = ["gtk"] }
libcosmic.workspace = true libcosmic.workspace = true
cosmic-applet = { path = "../applet" }
nix = "0.26.2" nix = "0.26.2"
chrono = { version = "0.4.23", features = ["clock"] } chrono = { version = "0.4.23", features = ["clock"] }

View file

@ -1,39 +1,26 @@
use cosmic::app::{self, applet::cosmic_panel_config::PanelAnchor, Command};
use cosmic::iced::wayland::popup::{destroy_popup, get_popup}; use cosmic::iced::wayland::popup::{destroy_popup, get_popup};
use cosmic::iced::Limits;
use cosmic::iced::{ use cosmic::iced::{
time, time,
wayland::InitialSurface,
widget::{button, column, text, vertical_space}, widget::{button, column, text, vertical_space},
window, Alignment, Application, Color, Command, Length, Rectangle, Subscription, window, Alignment, Length, Rectangle, Subscription,
}; };
use cosmic::iced_style::application::{self, Appearance}; use cosmic::iced_style::application;
use cosmic::theme; use cosmic::theme;
use cosmic::{ use cosmic::{
widget::{icon, rectangle_tracker::*}, widget::{icon, rectangle_tracker::*},
Element, Theme, Element, Theme,
}; };
use cosmic_applet::{cosmic_panel_config::PanelAnchor, CosmicAppletHelper};
use chrono::{DateTime, Local, Timelike}; use chrono::{DateTime, Local, Timelike};
use std::time::Duration; use std::time::Duration;
pub fn main() -> cosmic::iced::Result { pub fn main() -> cosmic::iced::Result {
let helper = CosmicAppletHelper::default(); cosmic::app::applet::run::<Time>(true, ())
let mut settings = helper.window_settings();
match &mut settings.initial_surface {
InitialSurface::XdgWindow(s) => {
s.autosize = true;
s.resizable = None;
s.size_limits = Limits::NONE.min_height(1.0).min_width(1.0);
}
_ => {}
};
Time::run(settings)
} }
struct Time { struct Time {
applet_helper: CosmicAppletHelper, core: cosmic::app::Core,
theme: Theme,
popup: Option<window::Id>, popup: Option<window::Id>,
id_ctr: u128, id_ctr: u128,
update_at: Every, update_at: Every,
@ -43,22 +30,6 @@ struct Time {
rectangle: Rectangle, rectangle: Rectangle,
} }
impl Default for Time {
fn default() -> Self {
Time {
applet_helper: CosmicAppletHelper::default(),
theme: Theme::default(),
popup: None,
id_ctr: 0,
update_at: Every::Minute,
now: Local::now(),
msg: String::new(),
rectangle_tracker: None,
rectangle: Rectangle::default(),
}
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[allow(dead_code)] #[allow(dead_code)]
enum Every { enum Every {
@ -70,47 +41,41 @@ enum Every {
enum Message { enum Message {
TogglePopup, TogglePopup,
Tick, Tick,
Ignore,
Rectangle(RectangleUpdate<u32>), Rectangle(RectangleUpdate<u32>),
Theme(Theme),
} }
impl Application for Time { impl cosmic::Application for Time {
type Message = Message; type Message = Message;
type Theme = Theme;
type Executor = cosmic::SingleThreadExecutor; type Executor = cosmic::SingleThreadExecutor;
type Flags = (); type Flags = ();
const APP_ID: &'static str = "com.system76.CosmicAppletTime";
fn new(_flags: ()) -> (Time, Command<Message>) { fn init(core: cosmic::app::Core, _flags: ()) -> (Self, app::Command<Message>) {
let applet_helper = CosmicAppletHelper::default();
let theme = applet_helper.theme();
( (
Time { Time {
applet_helper, core,
theme, popup: None,
..Default::default() id_ctr: 0,
update_at: Every::Minute,
now: Local::now(),
msg: String::new(),
rectangle_tracker: None,
rectangle: Rectangle::default(),
}, },
Command::none(), Command::none(),
) )
} }
fn title(&self) -> String { fn core(&self) -> &cosmic::app::Core {
String::from("Time") &self.core
} }
fn theme(&self) -> Theme { fn core_mut(&mut self) -> &mut cosmic::app::Core {
self.theme.clone() &mut self.core
} }
fn close_requested(&self, _id: window::Id) -> Self::Message { fn style(&self) -> Option<<Theme as application::StyleSheet>::Style> {
Message::Ignore Some(cosmic::app::applet::style())
}
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|theme| Appearance {
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().on_bg_color().into(),
}))
} }
fn subscription(&self) -> Subscription<Message> { fn subscription(&self) -> Subscription<Message> {
@ -129,7 +94,6 @@ impl Application for Time {
.expect("Setting nanoseconds to 0 should always be possible."); .expect("Setting nanoseconds to 0 should always be possible.");
let wait = 1.max((next - now).num_milliseconds()); let wait = 1.max((next - now).num_milliseconds());
Subscription::batch(vec![ Subscription::batch(vec![
self.applet_helper.theme_subscription(0).map(Message::Theme),
rectangle_tracker_subscription(0).map(|e| Message::Rectangle(e.1)), rectangle_tracker_subscription(0).map(|e| Message::Rectangle(e.1)),
time::every(Duration::from_millis( time::every(Duration::from_millis(
wait.try_into().unwrap_or(FALLBACK_DELAY), wait.try_into().unwrap_or(FALLBACK_DELAY),
@ -140,10 +104,6 @@ impl Application for Time {
fn update(&mut self, message: Message) -> Command<Message> { fn update(&mut self, message: Message) -> Command<Message> {
match message { match message {
Message::Theme(t) => {
self.theme = t;
Command::none()
}
Message::TogglePopup => { Message::TogglePopup => {
if let Some(p) = self.popup.take() { if let Some(p) = self.popup.take() {
destroy_popup(p) destroy_popup(p)
@ -166,7 +126,7 @@ impl Application for Time {
let new_id = window::Id(self.id_ctr); let new_id = window::Id(self.id_ctr);
self.popup.replace(new_id); self.popup.replace(new_id);
let mut popup_settings = self.applet_helper.get_popup_settings( let mut popup_settings = self.core.applet_helper.get_popup_settings(
window::Id(0), window::Id(0),
new_id, new_id,
None, None,
@ -192,7 +152,6 @@ impl Application for Time {
self.now = Local::now(); self.now = Local::now();
Command::none() Command::none()
} }
Message::Ignore => Command::none(),
Message::Rectangle(u) => { Message::Rectangle(u) => {
match u { match u {
RectangleUpdate::Rectangle(r) => { RectangleUpdate::Rectangle(r) => {
@ -207,57 +166,57 @@ impl Application for Time {
} }
} }
fn view(&self, id: window::Id) -> Element<Message> { fn view(&self) -> Element<Message> {
if id == window::Id(0) { let button = button(
let button = button( if matches!(
if matches!( self.core.applet_helper.anchor,
self.applet_helper.anchor, PanelAnchor::Top | PanelAnchor::Bottom
PanelAnchor::Top | PanelAnchor::Bottom ) {
) { column![text(self.now.format("%b %-d %-I:%M %p").to_string()).size(14)]
column![text(self.now.format("%b %-d %-I:%M %p").to_string()).size(14)]
} else {
let mut date_time_col = column![
icon(
"emoji-recent-symbolic",
self.applet_helper.suggested_size().0
)
.style(theme::Svg::Symbolic),
text(self.now.format("%I").to_string()).size(14),
text(self.now.format("%M").to_string()).size(14),
text(self.now.format("%p").to_string()).size(14),
vertical_space(Length::Fixed(4.0)),
// TODO better calendar icon?
icon(
"calendar-go-today-symbolic",
self.applet_helper.suggested_size().0
)
.style(theme::Svg::Symbolic),
]
.align_items(Alignment::Center)
.spacing(4);
for d in self.now.format("%x").to_string().split("/") {
date_time_col = date_time_col.push(text(d.to_string()).size(14));
}
date_time_col
},
)
.on_press(Message::TogglePopup)
.style(theme::Button::Text);
if let Some(tracker) = self.rectangle_tracker.as_ref() {
tracker.container(0, button).into()
} else { } else {
button.into() let mut date_time_col = column![
} icon(
} else { "emoji-recent-symbolic",
let content = column![] self.core.applet_helper.suggested_size().0
.align_items(Alignment::Start) )
.spacing(12) .style(theme::Svg::Symbolic),
.padding([24, 0]) text(self.now.format("%I").to_string()).size(14),
.push(text(&self.msg).size(14)) text(self.now.format("%M").to_string()).size(14),
.padding(8); text(self.now.format("%p").to_string()).size(14),
vertical_space(Length::Fixed(4.0)),
// TODO better calendar icon?
icon(
"calendar-go-today-symbolic",
self.core.applet_helper.suggested_size().0
)
.style(theme::Svg::Symbolic),
]
.align_items(Alignment::Center)
.spacing(4);
for d in self.now.format("%x").to_string().split("/") {
date_time_col = date_time_col.push(text(d.to_string()).size(14));
}
date_time_col
},
)
.on_press(Message::TogglePopup)
.style(theme::Button::Text);
self.applet_helper.popup_container(content).into() if let Some(tracker) = self.rectangle_tracker.as_ref() {
tracker.container(0, button).into()
} else {
button.into()
} }
} }
fn view_window(&self, _id: window::Id) -> Element<Message> {
let content = column![]
.align_items(Alignment::Start)
.spacing(12)
.padding([24, 0])
.push(text(&self.msg).size(14))
.padding(8);
self.core.applet_helper.popup_container(content).into()
}
} }

View file

@ -6,7 +6,6 @@ edition = "2021"
[dependencies] [dependencies]
libcosmic.workspace = true libcosmic.workspace = true
cosmic-applet = { path = "../applet" }
cctk.workspace = true cctk.workspace = true
cosmic-protocols.workspace = true cosmic-protocols.workspace = true
nix = "0.26.1" nix = "0.26.1"

View file

@ -1,19 +1,13 @@
use cctk::sctk::reexports::{calloop::channel::SyncSender, client::backend::ObjectId}; use cctk::sctk::reexports::{calloop::channel::SyncSender, client::backend::ObjectId};
use cosmic::app::{applet::cosmic_panel_config::PanelAnchor, Command};
use cosmic::iced::alignment::{Horizontal, Vertical}; use cosmic::iced::alignment::{Horizontal, Vertical};
use cosmic::iced::mouse::{self, ScrollDelta}; use cosmic::iced::mouse::{self, ScrollDelta};
use cosmic::iced::wayland::actions::window::SctkWindowSettings;
use cosmic::iced::wayland::InitialSurface;
use cosmic::iced::widget::{column, container, row, text}; use cosmic::iced::widget::{column, container, row, text};
use cosmic::iced::Color; use cosmic::iced::{subscription, widget::button, Event::Mouse, Length, Subscription};
use cosmic::iced::{ use cosmic::iced_style::application;
subscription, widget::button, window, Application, Command, Event::Mouse, Length, Settings,
Subscription,
};
use cosmic::iced_style::application::{self, Appearance};
use cosmic::theme::Button; use cosmic::theme::Button;
use cosmic::{Element, Theme}; use cosmic::{Element, Theme};
use cosmic_applet::cosmic_panel_config::PanelAnchor;
use cosmic_applet::CosmicAppletHelper;
use cosmic_protocols::workspace::v1::client::zcosmic_workspace_handle_v1; use cosmic_protocols::workspace::v1::client::zcosmic_workspace_handle_v1;
use std::cmp::Ordering; use std::cmp::Ordering;
@ -22,16 +16,7 @@ use crate::wayland::{WorkspaceEvent, WorkspaceList};
use crate::wayland_subscription::{workspaces, WorkspacesUpdate}; use crate::wayland_subscription::{workspaces, WorkspacesUpdate};
pub fn run() -> cosmic::iced::Result { pub fn run() -> cosmic::iced::Result {
let settings = Settings { cosmic::app::applet::run::<IcedWorkspacesApplet>(true, ())
initial_surface: InitialSurface::XdgWindow(SctkWindowSettings {
size: (32, 32),
autosize: true,
resizable: None,
..Default::default()
}),
..Default::default()
};
IcedWorkspacesApplet::run(settings)
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -41,11 +26,10 @@ pub enum Layout {
} }
struct IcedWorkspacesApplet { struct IcedWorkspacesApplet {
theme: Theme, core: cosmic::app::Core,
workspaces: WorkspaceList, workspaces: WorkspaceList,
workspace_tx: Option<SyncSender<WorkspaceEvent>>, workspace_tx: Option<SyncSender<WorkspaceEvent>>,
layout: Layout, layout: Layout,
helper: CosmicAppletHelper,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -53,35 +37,36 @@ enum Message {
WorkspaceUpdate(WorkspacesUpdate), WorkspaceUpdate(WorkspacesUpdate),
WorkspacePressed(ObjectId), WorkspacePressed(ObjectId),
WheelScrolled(ScrollDelta), WheelScrolled(ScrollDelta),
Theme(Theme),
Errored, Errored,
} }
impl Application for IcedWorkspacesApplet { impl cosmic::Application for IcedWorkspacesApplet {
type Message = Message; type Message = Message;
type Theme = Theme;
type Executor = cosmic::SingleThreadExecutor; type Executor = cosmic::SingleThreadExecutor;
type Flags = (); type Flags = ();
const APP_ID: &'static str = config::APP_ID;
fn new(_flags: ()) -> (Self, Command<Message>) { fn init(core: cosmic::app::Core, _flags: ()) -> (Self, Command<Message>) {
let applet_helper = CosmicAppletHelper::default();
( (
IcedWorkspacesApplet { IcedWorkspacesApplet {
layout: match &applet_helper.anchor { layout: match &core.applet_helper.anchor {
PanelAnchor::Left | PanelAnchor::Right => Layout::Column, PanelAnchor::Left | PanelAnchor::Right => Layout::Column,
PanelAnchor::Top | PanelAnchor::Bottom => Layout::Row, PanelAnchor::Top | PanelAnchor::Bottom => Layout::Row,
}, },
theme: applet_helper.theme(), core,
workspaces: Vec::new(), workspaces: Vec::new(),
workspace_tx: Default::default(), workspace_tx: Default::default(),
helper: Default::default(),
}, },
Command::none(), Command::none(),
) )
} }
fn title(&self) -> String { fn core(&self) -> &cosmic::app::Core {
config::APP_ID.to_string() &self.core
}
fn core_mut(&mut self) -> &mut cosmic::app::Core {
&mut self.core
} }
fn update(&mut self, message: Message) -> Command<Message> { fn update(&mut self, message: Message) -> Command<Message> {
@ -120,12 +105,11 @@ impl Application for IcedWorkspacesApplet {
} }
} }
Message::Errored => {} Message::Errored => {}
Message::Theme(t) => self.theme = t,
} }
Command::none() Command::none()
} }
fn view(&self, _id: window::Id) -> Element<Message> { fn view(&self) -> Element<Message> {
if self.workspaces.is_empty() { if self.workspaces.is_empty() {
return row![].padding(8).into(); return row![].padding(8).into();
} }
@ -141,8 +125,12 @@ impl Application for IcedWorkspacesApplet {
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fill), .height(Length::Fill),
) )
.width(Length::Fixed(self.helper.suggested_size().0 as f32 + 16.0)) .width(Length::Fixed(
.height(Length::Fixed(self.helper.suggested_size().0 as f32 + 16.0)) self.core.applet_helper.suggested_size().0 as f32 + 16.0,
))
.height(Length::Fixed(
self.core.applet_helper.suggested_size().0 as f32 + 16.0,
))
.on_press(Message::WorkspacePressed(w.2.clone())) .on_press(Message::WorkspacePressed(w.2.clone()))
.padding(0); .padding(0);
Some( Some(
@ -179,7 +167,6 @@ impl Application for IcedWorkspacesApplet {
fn subscription(&self) -> Subscription<Message> { fn subscription(&self) -> Subscription<Message> {
Subscription::batch( Subscription::batch(
vec![ vec![
self.helper.theme_subscription(0).map(Message::Theme),
workspaces(0).map(Message::WorkspaceUpdate), workspaces(0).map(Message::WorkspaceUpdate),
subscription::events_with(|e, _| match e { subscription::events_with(|e, _| match e {
Mouse(mouse::Event::WheelScrolled { delta }) => { Mouse(mouse::Event::WheelScrolled { delta }) => {
@ -192,18 +179,7 @@ impl Application for IcedWorkspacesApplet {
) )
} }
fn theme(&self) -> Theme { fn style(&self) -> Option<<Theme as application::StyleSheet>::Style> {
self.theme.clone() Some(cosmic::app::applet::style())
}
fn close_requested(&self, _id: window::Id) -> Self::Message {
unimplemented!()
}
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|theme| Appearance {
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().on_bg_color().into(),
}))
} }
} }

View file

@ -7,4 +7,3 @@ license = "GPL-3.0-or-later"
[dependencies] [dependencies]
freedesktop-desktop-entry = "0.5.0" freedesktop-desktop-entry = "0.5.0"
libcosmic.workspace = true libcosmic.workspace = true
cosmic-applet = { path = "../applet" }

View file

@ -1,11 +1,4 @@
use cosmic::{ use cosmic::{app, iced, iced_style::application, theme::Theme};
iced::Limits,
iced::{self, wayland::InitialSurface, Application},
iced_runtime::core::window,
iced_style::application,
theme::Theme,
};
use cosmic_applet::CosmicAppletHelper;
use freedesktop_desktop_entry::DesktopEntry; use freedesktop_desktop_entry::DesktopEntry;
use std::{env, fs, process::Command}; use std::{env, fs, process::Command};
@ -17,70 +10,47 @@ struct Desktop {
} }
struct Button { struct Button {
core: cosmic::app::Core,
desktop: Desktop, desktop: Desktop,
applet_helper: CosmicAppletHelper,
theme: Theme,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
enum Msg { enum Msg {
Press, Press,
Theme(Theme),
} }
impl iced::Application for Button { impl cosmic::Application for Button {
type Message = Msg; type Message = Msg;
type Theme = cosmic::Theme;
type Executor = cosmic::SingleThreadExecutor; type Executor = cosmic::SingleThreadExecutor;
type Flags = Desktop; type Flags = Desktop;
const APP_ID: &'static str = "com.system76.CosmicPanelButton";
fn new(desktop: Desktop) -> (Self, iced::Command<Msg>) { fn init(core: cosmic::app::Core, desktop: Desktop) -> (Self, app::Command<Msg>) {
let applet_helper = CosmicAppletHelper::default(); (Button { core, desktop }, app::Command::none())
let theme = applet_helper.theme();
(
Button {
desktop,
applet_helper,
theme,
},
iced::Command::none(),
)
} }
fn title(&self) -> String { fn core(&self) -> &cosmic::app::Core {
String::from("Button") &self.core
} }
fn close_requested(&self, _id: window::Id) -> Msg { fn core_mut(&mut self) -> &mut cosmic::app::Core {
unimplemented!() &mut self.core
} }
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style { fn style(&self) -> Option<<Theme as application::StyleSheet>::Style> {
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|theme| { Some(cosmic::app::applet::style())
application::Appearance {
background_color: iced::Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().on_bg_color().into(),
}
}))
} }
fn subscription(&self) -> iced::Subscription<Msg> { fn update(&mut self, message: Msg) -> app::Command<Msg> {
self.applet_helper.theme_subscription(0).map(Msg::Theme)
}
fn update(&mut self, message: Msg) -> iced::Command<Msg> {
match message { match message {
Msg::Press => { Msg::Press => {
let _ = Command::new("sh").arg("-c").arg(&self.desktop.exec).spawn(); let _ = Command::new("sh").arg("-c").arg(&self.desktop.exec).spawn();
} }
Msg::Theme(t) => {
self.theme = t;
}
} }
iced::Command::none() app::Command::none()
} }
fn view(&self, _id: window::Id) -> cosmic::Element<Msg> { fn view(&self) -> cosmic::Element<Msg> {
// TODO icon? // TODO icon?
cosmic::widget::button(cosmic::theme::Button::Text) cosmic::widget::button(cosmic::theme::Button::Text)
.text(&self.desktop.name) .text(&self.desktop.name)
@ -119,17 +89,5 @@ pub fn main() -> iced::Result {
let desktop = desktop.expect(&format!( let desktop = desktop.expect(&format!(
"Failed to find valid desktop file '{filename}' in search paths" "Failed to find valid desktop file '{filename}' in search paths"
)); ));
let helper = CosmicAppletHelper::default(); cosmic::app::applet::run::<Button>(true, desktop)
let mut settings = iced::Settings {
flags: desktop,
..helper.window_settings()
};
match &mut settings.initial_surface {
InitialSurface::XdgWindow(s) => {
s.autosize = true;
s.size_limits = Limits::NONE.min_height(1.0).min_width(1.0);
}
_ => unreachable!(),
};
Button::run(settings)
} }