From f56b16063505a0de4e840d0488479110c43d383a Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Fri, 1 Jul 2022 21:33:58 -0700 Subject: [PATCH] Library for shared applet code; use in battery applet Creates window, button, and popover with correct properties and CSS style. --- Cargo.lock | 13 +- Cargo.toml | 1 + applets/cosmic-applet-battery/Cargo.toml | 1 + applets/cosmic-applet-battery/src/main.rs | 216 +++++++++++----------- libcosmic-applet/Cargo.toml | 10 + libcosmic-applet/src/deref_cell.rs | 31 ++++ libcosmic-applet/src/lib.rs | 111 +++++++++++ 7 files changed, 269 insertions(+), 114 deletions(-) create mode 100644 libcosmic-applet/Cargo.toml create mode 100644 libcosmic-applet/src/deref_cell.rs create mode 100644 libcosmic-applet/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index e0f16129..09440f00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -356,6 +356,7 @@ version = "0.1.0" dependencies = [ "futures", "gtk4", + "libcosmic-applet", "relm4", "zbus", ] @@ -502,7 +503,7 @@ dependencies = [ [[package]] name = "cosmic-panel-config" version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-panel#49b8d8c84f9db93c72ef06778b76ccb38329a8c4" +source = "git+https://github.com/pop-os/cosmic-panel/#49b8d8c84f9db93c72ef06778b76ccb38329a8c4" dependencies = [ "anyhow", "gtk4", @@ -1560,6 +1561,16 @@ dependencies = [ "x11", ] +[[package]] +name = "libcosmic-applet" +version = "0.1.0" +dependencies = [ + "cosmic-panel-config", + "gtk4", + "once_cell", + "relm4-macros", +] + [[package]] name = "libcosmic-widgets" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 2be22a96..fbff1a15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,4 +11,5 @@ members = [ "applets/cosmic-applet-time", "applets/cosmic-app-list", "applets/cosmic-panel-button", + "libcosmic-applet", ] diff --git a/applets/cosmic-applet-battery/Cargo.toml b/applets/cosmic-applet-battery/Cargo.toml index 95b564f9..e987efa7 100644 --- a/applets/cosmic-applet-battery/Cargo.toml +++ b/applets/cosmic-applet-battery/Cargo.toml @@ -6,5 +6,6 @@ edition = "2021" [dependencies] futures = "0.3" gtk4 = { git = "https://github.com/gtk-rs/gtk4-rs" } +libcosmic-applet = { path = "../../libcosmic-applet" } relm4 = { git = "https://github.com/relm4/relm4", branch = "next", features = ["macros"] } zbus = { version = "2", no-default-features = true } diff --git a/applets/cosmic-applet-battery/src/main.rs b/applets/cosmic-applet-battery/src/main.rs index 09d06f7a..616712ed 100644 --- a/applets/cosmic-applet-battery/src/main.rs +++ b/applets/cosmic-applet-battery/src/main.rs @@ -77,125 +77,115 @@ impl SimpleComponent for AppModel { type Output = (); view! { - gtk4::Window { - set_decorated: false, - set_resizable: false, - set_width_request: 1, - set_height_request: 1, - gtk4::MenuButton { - set_has_frame: false, - #[watch] - set_icon_name: &model.icon_name, - #[wrap(Some)] - set_popover = >k4::Popover { - #[wrap(Some)] - set_child = >k4::Box { + libcosmic_applet::Applet { + #[watch] + set_button_icon_name: &model.icon_name, + #[wrap(Some)] + set_popover_child = >k4::Box { + set_orientation: gtk4::Orientation::Vertical, + + // Battery + gtk4::Box { + set_orientation: gtk4::Orientation::Horizontal, + gtk4::Image { + #[watch] + set_icon_name: Some(&model.icon_name), + }, + gtk4::Box { set_orientation: gtk4::Orientation::Vertical, - - // Battery - gtk4::Box { - set_orientation: gtk4::Orientation::Horizontal, - gtk4::Image { - #[watch] - set_icon_name: Some(&model.icon_name), - }, - gtk4::Box { - set_orientation: gtk4::Orientation::Vertical, - gtk4::Label { - set_halign: gtk4::Align::Start, - set_label: "Battery", - }, - gtk4::Label { - set_halign: gtk4::Align::Start, - // XXX time to full, fully changed, etc. - #[watch] - set_label: &format!("{} until empty ({:.0}%)", format_duration(model.time_remaining), model.battery_percent), - }, - }, + gtk4::Label { + set_halign: gtk4::Align::Start, + set_label: "Battery", }, - - gtk4::Separator { - }, - - // Limit charging - gtk4::Box { - set_orientation: gtk4::Orientation::Horizontal, - gtk4::Box { - set_orientation: gtk4::Orientation::Vertical, - gtk4::Label { - set_halign: gtk4::Align::Start, - set_label: "Limit Battery Charging", - }, - gtk4::Label { - set_halign: gtk4::Align::Start, - set_label: "Increase the lifespan of your battery by setting a maximum charge value of 80%." - }, - }, - gtk4::Switch { - set_valign: gtk4::Align::Center, - }, - }, - - gtk4::Separator { - }, - - // Brightness - gtk4::Box { + gtk4::Label { + set_halign: gtk4::Align::Start, + // XXX time to full, fully changed, etc. #[watch] - set_visible: model.backlight.is_some(), - set_orientation: gtk4::Orientation::Horizontal, - gtk4::Image { - set_icon_name: Some("display-brightness-symbolic"), - }, - gtk4::Scale { - set_hexpand: true, - set_adjustment: >k4::Adjustment::new(0., 0., 1., 1., 1., 0.), - #[watch] - set_value: model.display_brightness, - connect_change_value[sender] => move |_, _, value| { - sender.input(AppMsg::SetDisplayBrightness(value)); - gtk4::Inhibit(false) - }, - }, - gtk4::Label { - #[watch] - set_label: &format!("{:.0}%", model.display_brightness * 100.), - }, - }, - gtk4::Box { - #[watch] - set_visible: model.kbd_backlight.is_some(), - set_orientation: gtk4::Orientation::Horizontal, - gtk4::Image { - set_icon_name: Some("keyboard-brightness-symbolic"), - }, - gtk4::Scale { - set_hexpand: true, - set_adjustment: >k4::Adjustment::new(0., 0., 1., 1., 1., 0.), - #[watch] - set_value: model.keyboard_brightness, - connect_change_value[sender] => move |_, _, value| { - sender.input(AppMsg::SetKeyboardBrightness(value)); - gtk4::Inhibit(false) - }, - }, - gtk4::Label { - #[watch] - set_label: &format!("{:.0}%", model.keyboard_brightness * 100.), - }, + set_label: &format!("{} until empty ({:.0}%)", format_duration(model.time_remaining), model.battery_percent), }, + }, + }, - gtk4::Separator { - }, + gtk4::Separator { + }, - gtk4::Button { - set_label: "Power Settings...", - connect_clicked => move |_| { - // XXX open subpanel - let _ = Command::new("cosmic-settings").spawn(); - // TODO hide - } - } + // Limit charging + gtk4::Box { + set_orientation: gtk4::Orientation::Horizontal, + gtk4::Box { + set_orientation: gtk4::Orientation::Vertical, + gtk4::Label { + set_halign: gtk4::Align::Start, + set_label: "Limit Battery Charging", + }, + gtk4::Label { + set_halign: gtk4::Align::Start, + set_label: "Increase the lifespan of your battery by setting a maximum charge value of 80%." + }, + }, + gtk4::Switch { + set_valign: gtk4::Align::Center, + }, + }, + + gtk4::Separator { + }, + + // Brightness + gtk4::Box { + #[watch] + set_visible: model.backlight.is_some(), + set_orientation: gtk4::Orientation::Horizontal, + gtk4::Image { + set_icon_name: Some("display-brightness-symbolic"), + }, + gtk4::Scale { + set_hexpand: true, + set_adjustment: >k4::Adjustment::new(0., 0., 1., 1., 1., 0.), + #[watch] + set_value: model.display_brightness, + connect_change_value[sender] => move |_, _, value| { + sender.input(AppMsg::SetDisplayBrightness(value)); + gtk4::Inhibit(false) + }, + }, + gtk4::Label { + #[watch] + set_label: &format!("{:.0}%", model.display_brightness * 100.), + }, + }, + gtk4::Box { + #[watch] + set_visible: model.kbd_backlight.is_some(), + set_orientation: gtk4::Orientation::Horizontal, + gtk4::Image { + set_icon_name: Some("keyboard-brightness-symbolic"), + }, + gtk4::Scale { + set_hexpand: true, + set_adjustment: >k4::Adjustment::new(0., 0., 1., 1., 1., 0.), + #[watch] + set_value: model.keyboard_brightness, + connect_change_value[sender] => move |_, _, value| { + sender.input(AppMsg::SetKeyboardBrightness(value)); + gtk4::Inhibit(false) + }, + }, + gtk4::Label { + #[watch] + set_label: &format!("{:.0}%", model.keyboard_brightness * 100.), + }, + }, + + gtk4::Separator { + }, + + gtk4::Button { + set_label: "Power Settings...", + connect_clicked => move |_| { + // XXX open subpanel + let _ = Command::new("cosmic-settings").spawn(); + // TODO hide } } } diff --git a/libcosmic-applet/Cargo.toml b/libcosmic-applet/Cargo.toml new file mode 100644 index 00000000..4ae4e820 --- /dev/null +++ b/libcosmic-applet/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "libcosmic-applet" +version = "0.1.0" +edition = "2021" + +[dependencies] +cosmic-panel-config = { git = "https://github.com/pop-os/cosmic-panel/", features = ["gtk4"] } +gtk4 = { git = "https://github.com/gtk-rs/gtk4-rs", features = ["v4_6"] } +once_cell = "1.12.0" +relm4-macros = { git = "https://github.com/Relm4/Relm4.git", branch = "next" } diff --git a/libcosmic-applet/src/deref_cell.rs b/libcosmic-applet/src/deref_cell.rs new file mode 100644 index 00000000..dfdd4936 --- /dev/null +++ b/libcosmic-applet/src/deref_cell.rs @@ -0,0 +1,31 @@ +use once_cell::unsync::OnceCell; + +/// Wrapper around `OnceCell` implementing `Deref`, and thus also panicking +/// when not set (or set twice). +/// +/// To be used in place of `gtk::TemplateChild`, but without xml. +pub struct DerefCell(OnceCell); + +impl DerefCell { + #[track_caller] + pub fn set(&self, value: T) { + if self.0.set(value).is_err() { + panic!("Initialized twice"); + } + } +} + +impl Default for DerefCell { + fn default() -> Self { + Self(OnceCell::default()) + } +} + +impl std::ops::Deref for DerefCell { + type Target = T; + + #[track_caller] + fn deref(&self) -> &T { + self.0.get().unwrap() + } +} diff --git a/libcosmic-applet/src/lib.rs b/libcosmic-applet/src/lib.rs new file mode 100644 index 00000000..15e996c5 --- /dev/null +++ b/libcosmic-applet/src/lib.rs @@ -0,0 +1,111 @@ +use gtk4::{glib, prelude::*, subclass::prelude::*}; +use relm4_macros::view; + +mod deref_cell; +use deref_cell::DerefCell; + +// TODO make sure style fits different panel colors? +// TODO abstraction to start main loop? Work with relm4. +// TODO gir bindings +// TODO orientation, etc. +// TODO make image size dependent on CosmicPanelConfig? + +static STYLE: &str = " +window.cosmic_applet_window { + background: transparent; +} + +button.cosmic_applet_button { + border-radius: 12px; + transition: 100ms; + padding: 4px; + border-color: transparent; + background: transparent; + outline-color: transparent; +} +"; + +#[derive(Default)] +pub struct AppletInner { + menu_button: DerefCell, + popover: DerefCell, +} + +#[glib::object_subclass] +impl ObjectSubclass for AppletInner { + const NAME: &'static str = "CosmicApplet"; + type Type = Applet; + type ParentType = gtk4::Window; +} + +impl ObjectImpl for AppletInner { + fn constructed(&self, obj: &Applet) { + let window = || obj; + view! { + window() { + add_css_class: "cosmic_applet_window", + set_decorated: false, + set_resizable: false, + set_width_request: 1, + set_height_request: 1, + #[wrap(Some)] + set_child: menu_button = >k4::MenuButton { + add_css_class: "cosmic_applet_button", + set_has_frame: false, + #[wrap(Some)] + set_popover: popover = >k4::Popover { + // TODO: change if it can be positioned correctly? + set_has_arrow: false, + } + } + } + } + + let provider = gtk4::CssProvider::new(); + provider.load_from_data(STYLE.as_bytes()); + obj.style_context().add_provider(&provider, gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION); + + self.menu_button.set(menu_button); + self.popover.set(popover); + } +} + +impl WidgetImpl for AppletInner {} +impl WindowImpl for AppletInner {} + +glib::wrapper! { + pub struct Applet(ObjectSubclass) + @extends gtk4::Widget, gtk4::Window; +} + +impl Default for Applet { + fn default() -> Self { + Self::new() + } +} + +impl Applet { + pub fn new() -> Self { + glib::Object::new(&[]).unwrap() + } + + fn inner(&self) -> &AppletInner { + AppletInner::from_instance(self) + } + + pub fn set_button_child(&self, child: Option<&impl IsA>) { + self.inner().menu_button.set_child(child); + } + + pub fn set_button_icon_name(&self, name: &str) { + self.inner().menu_button.set_icon_name(name); + } + + pub fn set_button_label(&self, label: &str) { + self.inner().menu_button.set_label(label); + } + + pub fn set_popover_child(&self, child: Option<&impl IsA>) { + self.inner().popover.set_child(child); + } +}