Time: Replace GTK applet with iced applet

Basic Iced applet, but it should be noted that the time update
logic is a significant improvement. The milliseconds until the
next whole minute is calculated, then via tokio the thread sleeps
until then. Meaning that the clock applet is only running
(from my testing) for 3 milliseconds a minute. This takes less
recources and is more accurate than checking every second from
app start like the old gtk applet did.
This commit is contained in:
13r0ck 2022-12-15 16:53:29 -07:00 committed by Ashley Wulber
parent 31bea66801
commit 2940341033
6 changed files with 3464 additions and 163 deletions

3271
applets/cosmic-applet-time/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -5,15 +5,29 @@ edition = "2021"
license = "GPL-3.0-or-later"
[dependencies]
cascade = "1"
chrono = "0.4"
futures = "0.3"
gtk4 = { git = "https://github.com/gtk-rs/gtk4-rs", features = [ "v4_6" ] }
adw = { git = "https://gitlab.gnome.org/World/Rust/libadwaita-rs", package = "libadwaita"}
libcosmic = { git = "https://github.com/pop-os/libcosmic", default-features = false }
libcosmic-applet = { path = "../../libcosmic-applet" }
once_cell = "1.12"
serde = "1"
zbus = "2.0.1"
zbus_names = "2"
zvariant = "3"
icon-loader = { version = "0.3.6", features = ["gtk"] }
tokio = { version = "1.20.1", features=["full"] }
libcosmic = { git = "https://github.com/pop-os/libcosmic/", branch = "master", default-features = false, features = ["wayland", "applet"] }
# libcosmic = { path = "../../../../libcosmic", default-features = false, features = ["wayland", "applet"] }
# iced_sctk = { git = "https://github.com/pop-os/iced-sctk" }
# sctk = { package = "smithay-client-toolkit", git = "https://github.com/Smithay/client-toolkit", commit = "f1d9c3ef9cfbd508d986f7f98b2fc267fcc39b84" }
nix = "0.24.1"
chrono = { version = "0.4.23", features = ["clock"] }
[workspace]
resolved = "2"
[dependencies.iced]
git = "https://github.com/pop-os/iced.git"
branch = "sctk-cosmic"
# path = "../iced"
default-features = false
features = ["image", "svg", "tokio", "wayland"]
[dependencies.iced_native]
git = "https://github.com/pop-os/iced.git"
branch = "sctk-cosmic"
[dependencies.iced_futures]
git = "https://github.com/pop-os/iced.git"
branch = "sctk-cosmic"

View file

@ -1,31 +0,0 @@
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<T>(OnceCell<T>);
impl<T> DerefCell<T> {
#[track_caller]
pub fn set(&self, value: T) {
if self.0.set(value).is_err() {
panic!("Initialized twice");
}
}
}
impl<T> Default for DerefCell<T> {
fn default() -> Self {
Self(OnceCell::default())
}
}
impl<T> std::ops::Deref for DerefCell<T> {
type Target = T;
#[track_caller]
fn deref(&self) -> &T {
self.0.get().unwrap()
}
}

View file

@ -1,19 +1,169 @@
use cascade::cascade;
use gtk4::{glib, prelude::*};
use cosmic::applet::CosmicAppletHelper;
use cosmic::iced::wayland::{
popup::{destroy_popup, get_popup},
SurfaceIdWrapper,
};
use cosmic::iced::{
executor, time,
widget::{button, column, text},
window, Alignment, Application, Color, Command, Subscription,
};
use cosmic::iced_style::application::{self, Appearance};
use cosmic::{Element, Theme};
mod deref_cell;
mod time_button;
use time_button::TimeButton;
use chrono::{DateTime, Local, Timelike};
use std::time::Duration;
fn main() {
let _monitors = libcosmic::init();
cascade! {
libcosmic_applet::AppletWindow::new();
..set_child(Some(&TimeButton::new()));
..show();
};
let main_loop = glib::MainLoop::new(None, false);
main_loop.run();
pub fn main() -> cosmic::iced::Result {
let helper = CosmicAppletHelper::default();
Time::run(helper.window_settings())
}
struct Time {
applet_helper: CosmicAppletHelper,
theme: Theme,
popup: Option<window::Id>,
id_ctr: u32,
update_at: Every,
now: DateTime<Local>,
}
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(),
}
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
enum Every {
Minute,
Second,
}
#[derive(Debug, Clone)]
enum Message {
TogglePopup,
Tick,
Ignore,
}
impl Application for Time {
type Message = Message;
type Theme = Theme;
type Executor = executor::Default;
type Flags = ();
fn new(_flags: ()) -> (Time, Command<Message>) {
(Time::default(), Command::none())
}
fn title(&self) -> String {
String::from("Time")
}
fn theme(&self) -> Theme {
self.theme
}
fn close_requested(&self, _id: SurfaceIdWrapper) -> Self::Message {
Message::Ignore
}
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
<Self::Theme as application::StyleSheet>::Style::Custom(|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> {
const FALLBACK_DELAY: u64 = 500;
let update_delay = match self.update_at {
Every::Minute => chrono::Duration::minutes(1),
Every::Second => chrono::Duration::seconds(1),
};
// Calculate the time until next second/minute so we can sleep the thread until then.
let now = Local::now().time();
let next = (now + update_delay)
.with_second(0)
.expect("Setting seconds to 0 should always be possible")
.with_nanosecond(0)
.expect("Setting nanoseconds to 0 should always be possible.");
let wait = (next - now).num_milliseconds();
time::every(Duration::from_millis(
wait.try_into().unwrap_or(FALLBACK_DELAY),
))
.map(|_| Message::Tick)
}
fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::TogglePopup => {
if let Some(p) = self.popup.take() {
destroy_popup(p)
} else {
self.id_ctr += 1;
let new_id = window::Id::new(self.id_ctr);
self.popup.replace(new_id);
let popup_settings = self.applet_helper.get_popup_settings(
window::Id::new(0),
new_id,
(400, 300),
None,
None,
);
get_popup(popup_settings)
}
}
Message::Tick => {
self.now = Local::now();
Command::none()
}
Message::Ignore => Command::none(),
}
}
fn view(&self, id: SurfaceIdWrapper) -> Element<Message> {
match id {
SurfaceIdWrapper::LayerSurface(_) => unimplemented!(),
SurfaceIdWrapper::Window(_) => {
button(text(self.now.format("%b %-d %-I:%M %p").to_string()))
.on_press(Message::TogglePopup)
.into()
}
SurfaceIdWrapper::Popup(_) => {
use std::os::unix::process::ExitStatusExt;
let calendar = std::str::from_utf8(
&std::process::Command::new("happiness")
.output()
.unwrap_or(std::process::Output {
stdout: "`sudo apt install happiness`".as_bytes().to_vec(),
stderr: Vec::new(),
status: std::process::ExitStatus::from_raw(0),
})
.stdout,
)
.unwrap()
.to_string();
let content = column![]
.align_items(Alignment::Start)
.spacing(12)
.padding([24, 0])
.push(text(calendar));
self.applet_helper.popup_container(content).into()
}
}
}
}

View file

@ -1,104 +0,0 @@
use cascade::cascade;
use gtk4::{
glib::{self, clone},
pango,
prelude::*,
subclass::prelude::*,
};
use crate::deref_cell::DerefCell;
#[derive(Default)]
pub struct TimeButtonInner {
calendar: DerefCell<gtk4::Calendar>,
button: DerefCell<libcosmic_applet::AppletButton>,
label: DerefCell<gtk4::Label>,
}
#[glib::object_subclass]
impl ObjectSubclass for TimeButtonInner {
const NAME: &'static str = "S76TimeButton";
type ParentType = gtk4::Widget;
type Type = TimeButton;
fn class_init(klass: &mut Self::Class) {
klass.set_layout_manager_type::<gtk4::BinLayout>();
}
}
impl ObjectImpl for TimeButtonInner {
fn constructed(&self, obj: &TimeButton) {
let calendar = cascade! {
gtk4::Calendar::new();
};
let label = cascade! {
gtk4::Label::new(None);
..set_attributes(Some(&cascade! {
pango::AttrList::new();
..insert(pango::AttrInt::new_weight(pango::Weight::Bold));
}));
};
let button = cascade! {
libcosmic_applet::AppletButton::new();
..set_parent(obj);
..connect_activate(clone!(@strong obj => move |_| obj.opening()));
..set_button_child(Some(&label));
..set_popover_child(Some(&cascade! {
gtk4::Box::new(gtk4::Orientation::Horizontal, 0);
..append(&calendar);
}));
};
self.calendar.set(calendar);
self.button.set(button);
self.label.set(label);
// TODO: better way to do this?
glib::timeout_add_seconds_local(
1,
clone!(@weak obj => @default-return glib::Continue(false), move || {
obj.update_time();
glib::Continue(true)
}),
);
obj.update_time();
}
fn dispose(&self, _obj: &TimeButton) {
self.button.unparent();
}
}
impl WidgetImpl for TimeButtonInner {}
glib::wrapper! {
pub struct TimeButton(ObjectSubclass<TimeButtonInner>)
@extends gtk4::Widget;
}
impl TimeButton {
pub fn new() -> Self {
glib::Object::new::<Self>(&[]).unwrap()
}
fn inner(&self) -> &TimeButtonInner {
TimeButtonInner::from_instance(self)
}
fn opening(&self) {
let date = glib::DateTime::now(&glib::TimeZone::local()).unwrap();
self.inner().calendar.clear_marks();
self.inner().calendar.select_day(&date);
}
fn update_time(&self) {
// TODO: Locale-based formatting?
let time = chrono::Local::now();
self.inner()
.label
.set_label(&time.format("%b %-d %-I:%M %p").to_string());
// time.format("%B %-d %Y")
}
}

1
debian/control vendored
View file

@ -19,6 +19,7 @@ Homepage: https://github.com/pop-os/cosmic-applets
Package: cosmic-applets
Architecture: amd64 arm64
Depends:
happiness,
${misc:Depends},
${shlibs:Depends}
Description: Cosmic Applets