From 899cc0f459d1dea22934e901519592d43f0c89cc Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 13 Jan 2023 18:31:59 -0500 Subject: [PATCH] refactor: applet more like mockup --- .../i18n/en/cosmic_applet_battery.ftl | 7 +- cosmic-applet-battery/src/app.rs | 151 ++++++++++++++--- cosmic-applet-battery/src/power_daemon.rs | 160 +++++++++++++++++- 3 files changed, 288 insertions(+), 30 deletions(-) diff --git a/cosmic-applet-battery/i18n/en/cosmic_applet_battery.ftl b/cosmic-applet-battery/i18n/en/cosmic_applet_battery.ftl index 40e38382..57d327ed 100644 --- a/cosmic-applet-battery/i18n/en/cosmic_applet_battery.ftl +++ b/cosmic-applet-battery/i18n/en/cosmic_applet_battery.ftl @@ -1,8 +1,13 @@ cosmic-applet-button = Cosmic Button battery = Battery +battery-desc = Reduced power usage and performance. +balanced = Balanced +balanced-desc = Standard performance and battery usage. +performance = High Performance +performance-desc = High performance and power usage. max-charge = Increase the lifespan of your battery by setting a maximum charge value of 80% seconds = s minutes = m hours = h until-empty = until empty -power-settings = Power Settings... \ No newline at end of file +power-settings = Power and Battery Settings... diff --git a/cosmic-applet-battery/src/app.rs b/cosmic-applet-battery/src/app.rs index b77f04b9..9d6f97c3 100644 --- a/cosmic-applet-battery/src/app.rs +++ b/cosmic-applet-battery/src/app.rs @@ -3,25 +3,29 @@ use crate::backlight::{ }; use crate::config; use crate::fl; +use crate::power_daemon::{ + power_profile_subscription, Power, PowerProfileRequest, PowerProfileUpdate, +}; use crate::upower_device::{device_subscription, DeviceDbusEvent}; use crate::upower_kbdbacklight::{ kbd_backlight_subscription, KeyboardBacklightRequest, KeyboardBacklightUpdate, }; -use cosmic::applet::CosmicAppletHelper; +use cosmic::applet::{CosmicAppletHelper, APPLET_BUTTON_THEME}; use cosmic::iced::alignment::Horizontal; use cosmic::iced::wayland::popup::{destroy_popup, get_popup}; use cosmic::iced::wayland::SurfaceIdWrapper; use cosmic::iced::{ executor, - widget::{button, column, row, slider, text}, + widget::{column, container, row, slider, text}, window, Alignment, Application, Command, Length, Subscription, }; use cosmic::iced_native::layout::Limits; use cosmic::iced_style::application::{self, Appearance}; use cosmic::iced_style::{svg, Color}; -use cosmic::theme::{self, Svg}; -use cosmic::widget::{horizontal_rule, icon, toggler}; -use cosmic::{iced_style, Element, Theme}; +use cosmic::theme::Svg; +use cosmic::widget::{button, horizontal_rule, icon, toggler}; +use cosmic::{Element, Theme}; +use log::error; use std::time::Duration; use tokio::sync::mpsc::UnboundedSender; @@ -60,6 +64,8 @@ struct CosmicBatteryApplet { screen_sender: Option>, kbd_sender: Option>, applet_helper: CosmicAppletHelper, + power_profile: Power, + power_profile_sender: Option>, } #[derive(Debug, Clone)] @@ -80,6 +86,9 @@ enum Message { InitScreenBacklight(UnboundedSender, f64), Errored(String), Ignore, + InitProfile(UnboundedSender, Power), + Profile(Power), + SelectProfile(Power), } impl Application for CosmicBatteryApplet { @@ -122,8 +131,8 @@ impl Application for CosmicBatteryApplet { Message::OpenBatterySettings => { // TODO Ashley } - Message::Errored(_) => { - // TODO log errors + Message::Errored(e) => { + error!("{}", e); } Message::TogglePopup => { if let Some(p) = self.popup.take() { @@ -148,10 +157,13 @@ impl Application for CosmicBatteryApplet { None, ); popup_settings.positioner.size_limits = Limits::NONE - .max_width(400) + .max_width(372) .min_width(300) .min_height(200) .max_height(1080); + if let Some(tx) = self.power_profile_sender.as_ref() { + let _ = tx.send(PowerProfileRequest::Get); + } return get_popup(popup_settings); } } @@ -181,6 +193,24 @@ impl Application for CosmicBatteryApplet { Message::UpdateScreenBrightness(b) => { self.screen_brightness = b; } + Message::InitProfile(tx, profile) => { + self.power_profile_sender.replace(tx); + self.power_profile = profile; + } + Message::Profile(profile) => { + self.power_profile = profile; + if let Some(tx) = &self.kbd_sender { + let _ = tx.send(KeyboardBacklightRequest::Get); + } + if let Some(tx) = &self.screen_sender { + let _ = tx.send(ScreenBacklightRequest::Get); + } + } + Message::SelectProfile(profile) => { + if let Some(tx) = self.power_profile_sender.as_ref() { + let _ = tx.send(PowerProfileRequest::Set(profile)); + } + } } Command::none() } @@ -223,14 +253,80 @@ impl Application for CosmicBatteryApplet { .height(Length::Units(24)), column![name, description] ] + .padding([0, 24]) .spacing(8) .align_items(Alignment::Center), - horizontal_rule(1), - toggler(fl!("max-charge"), self.charging_limit, |_| { + container(horizontal_rule(1)) + .width(Length::Fill) + .padding([0, 12]), + button(APPLET_BUTTON_THEME) + .custom(vec![row![ + column![ + text(fl!("battery")).size(14), + text(fl!("battery-desc")).size(12) + ] + .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(12) + ] + .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(12) + ] + .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(horizontal_rule(1)) + .width(Length::Fill) + .padding([0, 12]), + container(toggler(fl!("max-charge"), self.charging_limit, |_| { Message::SetChargingLimit(!self.charging_limit) - }) + })) + .padding([0, 24]) .width(Length::Fill), - horizontal_rule(1), + container(horizontal_rule(1)) + .width(Length::Fill) + .padding([0, 12]), row![ icon("display-brightness-symbolic", 24) .style(Svg::Custom(|theme| { @@ -249,6 +345,7 @@ impl Application for CosmicBatteryApplet { .width(Length::Units(40)) .horizontal_alignment(Horizontal::Right) ] + .padding([0, 24]) .spacing(12), row![ icon("keyboard-brightness-symbolic", 24) @@ -268,22 +365,21 @@ impl Application for CosmicBatteryApplet { .width(Length::Units(40)) .horizontal_alignment(Horizontal::Right) ] + .padding([0, 24]) .spacing(12), - button( - text(fl!("power-settings")) - .horizontal_alignment(Horizontal::Center) + container(horizontal_rule(1)) + .width(Length::Fill) + .padding([0, 12]), + button(APPLET_BUTTON_THEME) + .custom(vec![text(fl!("power-settings")) .width(Length::Fill) - .style(theme::Text::Custom(|theme| { - let cosmic = theme.cosmic(); - iced_style::text::Appearance { - color: Some(cosmic.accent.on.into()), - } - })) - ) - .width(Length::Fill) + .into()]) + .on_press(Message::OpenBatterySettings) + .width(Length::Fill) + .padding([8, 24]) ] - .spacing(4) - .padding(8), + .spacing(8) + .padding([8, 0]), ) .into() } @@ -311,6 +407,11 @@ impl Application for CosmicBatteryApplet { ScreenBacklightUpdate::Update(b) => Message::UpdateScreenBrightness(b), ScreenBacklightUpdate::Init(tx, b) => Message::InitScreenBacklight(tx, b), }), + power_profile_subscription(0).map(|(_, event)| match event { + PowerProfileUpdate::Update { profile } => Message::Profile(profile), + PowerProfileUpdate::Init(tx, p) => Message::InitProfile(p, tx), + PowerProfileUpdate::Error(e) => Message::Errored(e), // TODO: handle error + }), ]) } diff --git a/cosmic-applet-battery/src/power_daemon.rs b/cosmic-applet-battery/src/power_daemon.rs index 6b65ea76..2ee8f626 100644 --- a/cosmic-applet-battery/src/power_daemon.rs +++ b/cosmic-applet-battery/src/power_daemon.rs @@ -1,12 +1,33 @@ +// SPDX-License-Identifier: GPL-3.0-or-later //! # DBus interface proxy for: `com.system76.PowerDaemon` //! -//! This code was generated by `zbus-xmlgen` `2.0.1` from DBus introspection data. +//! This code was generated by `zbus-xmlgen` `3.0.0` from DBus introspection data. //! Source: `Interface '/com/system76/PowerDaemon' from service 'com.system76.PowerDaemon' on system bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the +//! [Writing a client proxy](https://dbus.pages.freedesktop.org/zbus/client.html) +//! section of the zbus documentation. +//! +//! This DBus object implements +//! [standard DBus interfaces](https://dbus.freedesktop.org/doc/dbus-specification.html), +//! (`org.freedesktop.DBus.*`) for which the following zbus proxies can be used: +//! +//! * [`zbus::fdo::IntrospectableProxy`] +//! +//! …consequently `zbus-xmlgen` did not generate code for the above interfaces. -use zbus::dbus_proxy; +use cosmic::iced; +use cosmic::iced_native::subscription; +use std::fmt::Debug; +use std::hash::Hash; +use tokio::sync::mpsc::UnboundedReceiver; +use tokio::sync::mpsc::UnboundedSender; +use zbus::Result; +use zbus::{dbus_proxy, Connection}; #[dbus_proxy( - default_service = "com.system76.PowerDaemon", interface = "com.system76.PowerDaemon", default_path = "/com/system76/PowerDaemon" )] @@ -64,4 +85,135 @@ trait PowerDaemon { fn power_profile_switch(&self, profile: &str) -> zbus::Result<()>; } -// TODO power subscription +#[derive(PartialEq, Eq, Copy, Clone, Debug, Default)] +pub enum Power { + Battery, + #[default] + Balanced, + Performance, +} + +pub async fn get_power_profile(daemon: PowerDaemonProxy<'_>) -> Result { + let power = daemon.get_profile().await?; + match power.as_str() { + "Battery" => Ok(Power::Battery), + "Balanced" => Ok(Power::Balanced), + "Performance" => Ok(Power::Performance), + _ => panic!("Unknown power profile: {}", power), + } +} + +pub async fn set_power_profile(daemon: PowerDaemonProxy<'_>, power: Power) -> Result<()> { + match power { + Power::Battery => daemon.battery().await, + Power::Balanced => daemon.balanced().await, + Power::Performance => daemon.performance().await, + } +} + +pub fn power_profile_subscription( + id: I, +) -> iced::Subscription<(I, PowerProfileUpdate)> { + subscription::unfold(id, State::Ready, move |state| start_listening(id, state)) +} + +#[derive(Debug)] +pub enum State { + Ready, + Waiting(Connection, UnboundedReceiver), + Finished, +} + +async fn start_listening(id: I, state: State) -> (Option<(I, PowerProfileUpdate)>, State) { + match state { + State::Ready => { + let conn = match Connection::system().await.map_err(|e| e.to_string()) { + Ok(conn) => conn, + Err(e) => return (Some((id, PowerProfileUpdate::Error(e))), State::Finished), + }; + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + + let power_proxy = match PowerDaemonProxy::new(&conn) + .await + .map_err(|e| e.to_string()) + { + Ok(p) => p, + Err(e) => { + return ( + Some((id, PowerProfileUpdate::Error(e))), + State::Waiting(conn, rx), + ) + } + }; + let profile = match get_power_profile(power_proxy) + .await + .map_err(|e| e.to_string()) + { + Ok(p) => p, + Err(e) => { + return ( + Some((id, PowerProfileUpdate::Error(e))), + State::Waiting(conn, rx), + ) + } + }; + + return ( + Some((id, PowerProfileUpdate::Init(profile, tx))), + State::Waiting(conn, rx), + ); + } + State::Waiting(conn, mut rx) => { + let power_proxy = match PowerDaemonProxy::new(&conn) + .await + .map_err(|e| e.to_string()) + { + Ok(p) => p, + Err(e) => { + return ( + Some((id, PowerProfileUpdate::Error(e))), + State::Waiting(conn, rx), + ) + } + }; + + match rx.recv().await { + Some(PowerProfileRequest::Get) => { + if let Ok(profile) = get_power_profile(power_proxy).await { + return ( + Some((id, PowerProfileUpdate::Update { profile })), + State::Waiting(conn, rx), + ); + } else { + return (None, State::Waiting(conn, rx)); + } + } + Some(PowerProfileRequest::Set(profile)) => { + if set_power_profile(power_proxy, profile).await.is_ok() { + return ( + Some((id, PowerProfileUpdate::Update { profile })), + State::Waiting(conn, rx), + ); + } else { + return (None, State::Waiting(conn, rx)); + } + } + None => (None, State::Finished), + } + } + State::Finished => iced::futures::future::pending().await, + } +} + +#[derive(Debug, Clone, Copy)] +pub enum PowerProfileRequest { + Get, + Set(Power), +} + +#[derive(Debug, Clone)] +pub enum PowerProfileUpdate { + Init(Power, UnboundedSender), + Update { profile: Power }, + Error(String), +}