From d68488de471bd40d8b4d3d34245aa174e23f7926 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Thu, 14 Mar 2024 16:16:51 +0100 Subject: [PATCH] feat(widget): add calendar widget --- Cargo.toml | 9 +- examples/application/src/main.rs | 5 +- examples/calendar/Cargo.toml | 14 +++ examples/calendar/src/main.rs | 136 ++++++++++++++++++++++++++ src/widget/calendar.rs | 157 +++++++++++++++++++++++++++++++ src/widget/mod.rs | 3 + 6 files changed, 314 insertions(+), 10 deletions(-) create mode 100644 examples/calendar/Cargo.toml create mode 100644 examples/calendar/src/main.rs create mode 100644 src/widget/calendar.rs diff --git a/Cargo.toml b/Cargo.toml index d85e0a90..5794963a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,13 +37,7 @@ smol = ["iced/smol", "zbus?/async-io"] # Tokio async runtime tokio = ["dep:tokio", "ashpd?/tokio", "iced/tokio", "rfd?/tokio", "zbus?/tokio"] # Wayland window support -wayland = [ - "ashpd?/wayland", - "iced_runtime/wayland", - "iced/wayland", - "iced_sctk", - "cctk", -] +wayland = ["ashpd?/wayland", "iced_runtime/wayland", "iced/wayland", "iced_sctk", "cctk"] # multi-window support multi-window = ["iced/multi-window"] # Render with wgpu @@ -61,6 +55,7 @@ apply = "0.3.0" ashpd = { version = "0.7.0", default-features = false, optional = true } async-fs = { version = "2.1", optional = true } cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "e65fa5e", optional = true } +chrono = "0.4.35" cosmic-config = { path = "cosmic-config" } cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings", branch = "cosmic-settings-daemon", optional = true } css-color = "0.2.5" diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index 142363b5..48270c69 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -32,7 +32,7 @@ impl Page { fn main() -> Result<(), Box> { tracing_subscriber::fmt::init(); let _ = tracing_log::LogTracer::init(); - + let input = vec![ (Page::Page1, "🖖 Hello from libcosmic.".into()), (Page::Page2, "🌟 This is an example application.".into()), @@ -124,8 +124,7 @@ impl cosmic::Application for App { let page_content = self .nav_model .active_data::() - .map(String::as_str) - .unwrap_or("No page selected"); + .map_or("No page selected", String::as_str); let text = cosmic::widget::text(page_content); diff --git a/examples/calendar/Cargo.toml b/examples/calendar/Cargo.toml new file mode 100644 index 00000000..8eadab14 --- /dev/null +++ b/examples/calendar/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "calendar" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +chrono = "0.4.35" + +[dependencies.libcosmic] +path = "../../" +default-features = false +features = ["debug", "winit", "tokio", "xdg-portal", "wgpu"] \ No newline at end of file diff --git a/examples/calendar/src/main.rs b/examples/calendar/src/main.rs new file mode 100644 index 00000000..81fe20b2 --- /dev/null +++ b/examples/calendar/src/main.rs @@ -0,0 +1,136 @@ +// Copyright 2024 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! Calendar widget example + +use chrono::{Datelike, Days, Local, Months, NaiveDate}; +use cosmic::app::{Command, Core, Settings}; +use cosmic::{executor, iced, ApplicationExt, Element}; + +/// Runs application with these settings +#[rustfmt::skip] +fn main() -> Result<(), Box> { + cosmic::app::run::(Settings::default(), ())?; + + Ok(()) +} + +/// Messages that are used specifically by our [`App`]. +#[derive(Clone, Debug)] +pub enum Message { + PrevMonth, + NextMonth, + DaySelected(u32), +} + +/// The [`App`] stores application-specific state. +pub struct App { + core: Core, + date_selected: NaiveDate, +} + +/// Implement [`cosmic::Application`] to integrate with COSMIC. +impl cosmic::Application for App { + /// Default async executor to use with the app. + type Executor = executor::Default; + + /// Argument received [`cosmic::Application::new`]. + type Flags = (); + + /// Message type specific to our [`App`]. + type Message = Message; + + /// The unique application ID to supply to the window manager. + const APP_ID: &'static str = "org.cosmic.AppDemo"; + + fn core(&self) -> &Core { + &self.core + } + + fn core_mut(&mut self) -> &mut Core { + &mut self.core + } + + /// Creates the application, and optionally emits command on initialize. + fn init(core: Core, _input: Self::Flags) -> (Self, Command) { + let now = Local::now(); + + let mut app = App { + core, + date_selected: NaiveDate::from(now.naive_local()), + }; + + let command = app.update_title(); + + (app, command) + } + + /// Handle application events here. + fn update(&mut self, message: Self::Message) -> Command { + match message { + Message::DaySelected(day) => { + let current = self.date_selected.day(); + + let new_date = if current < day { + self.date_selected + .checked_add_days(Days::new((day - current) as u64)) + } else if current > day { + self.date_selected + .checked_sub_days(Days::new((current - day) as u64)) + } else { + None + }; + + if let Some(new) = new_date { + self.date_selected = new; + } + } + Message::PrevMonth => { + self.date_selected = self + .date_selected + .checked_sub_months(Months::new(1)) + .expect("valid naivedate"); + } + Message::NextMonth => { + self.date_selected = self + .date_selected + .checked_add_months(Months::new(1)) + .expect("valid naivedate"); + } + } + + Command::none() + } + + /// Creates a view after each update. + fn view(&self) -> Element { + let mut content = cosmic::widget::column().spacing(12); + + let calendar = cosmic::widget::calendar( + &self.date_selected, + Message::PrevMonth, + Message::NextMonth, + |day| Message::DaySelected(day), + ); + + content = content.push(cosmic::widget::container(calendar).width(350)); + + let centered = cosmic::widget::container(content) + .width(iced::Length::Fill) + .height(iced::Length::Shrink) + .align_x(iced::alignment::Horizontal::Center) + .align_y(iced::alignment::Vertical::Center); + + Element::from(centered) + } +} + +impl App +where + Self: cosmic::Application, +{ + fn update_title(&mut self) -> Command { + self.set_header_title(String::from("Calendar Demo")); + self.set_window_title(String::from("Calendar Demo")) + } +} diff --git a/src/widget/calendar.rs b/src/widget/calendar.rs new file mode 100644 index 00000000..3e380af6 --- /dev/null +++ b/src/widget/calendar.rs @@ -0,0 +1,157 @@ +// Copyright 2024 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::iced_core::{Length, Padding}; +use crate::widget::{button, column, grid, icon, row, text, Grid}; +use chrono::{Datelike, Days, NaiveDate, Weekday}; +use iced::alignment::{Horizontal, Vertical}; + +pub fn calendar( + selected: &NaiveDate, + on_prev_month: M, + on_next_month: M, + on_select: impl Fn(u32) -> M + 'static, +) -> Calendar { + Calendar { + selected, + on_prev_month, + on_next_month, + on_select: Box::new(on_select), + } +} + +pub struct Calendar<'a, M> { + selected: &'a NaiveDate, + on_prev_month: M, + on_next_month: M, + on_select: Box M>, +} + +impl<'a, Message> From> for crate::Element<'a, Message> +where + Message: Clone + 'static, +{ + fn from(this: Calendar<'a, Message>) -> Self { + let date = text(this.selected.format("%B %-d, %Y").to_string()).size(18); + let day_of_week = text(this.selected.format("%A").to_string()).size(14); + + let month_controls = row::with_capacity(2) + .push( + button::icon(icon::from_name("go-previous-symbolic")) + .padding([0, 12]) + .on_press(this.on_prev_month), + ) + .push( + button::icon(icon::from_name("go-next-symbolic")) + .padding([0, 12]) + .on_press(this.on_next_month), + ); + + // Calender + let mut calendar_grid: Grid<'_, Message> = + grid().padding([0, 12].into()).width(Length::Fill); + + let mut first_day_of_week = Weekday::Sun; // TODO: Configurable + for _ in 0..7 { + calendar_grid = calendar_grid.push( + text(first_day_of_week.to_string()) + .size(12) + .width(Length::Fixed(36.0)) + .horizontal_alignment(Horizontal::Center), + ); + + first_day_of_week = first_day_of_week.succ(); + } + calendar_grid = calendar_grid.insert_row(); + + let monday = get_calender_first( + this.selected.year(), + this.selected.month(), + first_day_of_week, + ); + let mut day_iter = monday.iter_days(); + for i in 0..42 { + if i > 0 && i % 7 == 0 { + calendar_grid = calendar_grid.insert_row(); + } + + let date = day_iter.next().unwrap(); + let is_month = + date.month() == this.selected.month() && date.year_ce() == this.selected.year_ce(); + let is_day = date.day() == this.selected.day() && is_month; + + calendar_grid = + calendar_grid.push(date_button(date.day(), is_month, is_day, &this.on_select)); + } + + let content_list = column::with_children(vec![ + row::with_children(vec![ + column::with_children(vec![date.into(), day_of_week.into()]).into(), + crate::widget::Space::with_width(Length::Fill).into(), + month_controls.into(), + ]) + .padding([12, 20]) + .into(), + calendar_grid.into(), + padded_control(crate::widget::divider::horizontal::default()).into(), + ]) + .padding([8, 0]); + + Self::new(content_list) + } +} + +fn date_button( + day: u32, + is_month: bool, + is_day: bool, + on_select: &dyn Fn(u32) -> Message, +) -> crate::widget::Button<'static, Message, crate::Theme, crate::Renderer> { + let style = if is_day { + crate::widget::button::Style::Suggested + } else { + crate::widget::button::Style::Text + }; + + let button = button( + text(format!("{day}")) + .horizontal_alignment(Horizontal::Center) + .vertical_alignment(Vertical::Center), + ) + .style(style) + .height(Length::Fixed(36.0)) + .width(Length::Fixed(36.0)); + + if is_month { + button.on_press((on_select)(day)) + } else { + button + } +} + +/// Gets the first date that will be visible on the calender +#[must_use] +pub fn get_calender_first(year: i32, month: u32, from_weekday: Weekday) -> NaiveDate { + let date = NaiveDate::from_ymd_opt(year, month, 1).unwrap(); + let num_days = (date.weekday() as u32 + 7 - from_weekday as u32) % 7; // chrono::Weekday.num_days_from + date.checked_sub_days(Days::new(num_days as u64)).unwrap() +} + +// TODO: Refactor to use same function from applet module. +fn padded_control<'a, Message>( + content: impl Into>, +) -> crate::widget::container::Container<'a, Message, crate::Theme, crate::Renderer> { + crate::widget::container(content) + .padding(menu_control_padding()) + .width(Length::Fill) +} + +fn menu_control_padding() -> Padding { + crate::theme::THEME + .with(|t| { + let t = t.borrow(); + let cosmic = t.cosmic(); + [cosmic.space_xxs(), cosmic.space_m()] + }) + .into() +} diff --git a/src/widget/mod.rs b/src/widget/mod.rs index b41c532e..ccf7f542 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -26,6 +26,9 @@ pub use button::{button, Button, IconButton, LinkButton, TextButton}; pub(crate) mod common; +pub mod calendar; +pub use calendar::{calendar, Calendar}; + pub mod card; pub use card::*;