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:
parent
31bea66801
commit
2940341033
6 changed files with 3464 additions and 163 deletions
3271
applets/cosmic-applet-time/Cargo.lock
generated
Normal file
3271
applets/cosmic-applet-time/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
1
debian/control
vendored
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue