feat(widget): add calendar widget
This commit is contained in:
parent
f0585c0126
commit
d68488de47
6 changed files with 314 additions and 10 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ impl Page {
|
|||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
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::<String>()
|
||||
.map(String::as_str)
|
||||
.unwrap_or("No page selected");
|
||||
.map_or("No page selected", String::as_str);
|
||||
|
||||
let text = cosmic::widget::text(page_content);
|
||||
|
||||
|
|
|
|||
14
examples/calendar/Cargo.toml
Normal file
14
examples/calendar/Cargo.toml
Normal file
|
|
@ -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"]
|
||||
136
examples/calendar/src/main.rs
Normal file
136
examples/calendar/src/main.rs
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
// Copyright 2024 System76 <info@system76.com>
|
||||
// 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<dyn std::error::Error>> {
|
||||
cosmic::app::run::<App>(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<Self::Message>) {
|
||||
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<Self::Message> {
|
||||
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<Self::Message> {
|
||||
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<Message> {
|
||||
self.set_header_title(String::from("Calendar Demo"));
|
||||
self.set_window_title(String::from("Calendar Demo"))
|
||||
}
|
||||
}
|
||||
157
src/widget/calendar.rs
Normal file
157
src/widget/calendar.rs
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
// Copyright 2024 System76 <info@system76.com>
|
||||
// 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<M>(
|
||||
selected: &NaiveDate,
|
||||
on_prev_month: M,
|
||||
on_next_month: M,
|
||||
on_select: impl Fn(u32) -> M + 'static,
|
||||
) -> Calendar<M> {
|
||||
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<dyn Fn(u32) -> M>,
|
||||
}
|
||||
|
||||
impl<'a, Message> From<Calendar<'a, Message>> 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<Message>(
|
||||
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::Element<'a, Message>>,
|
||||
) -> 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()
|
||||
}
|
||||
|
|
@ -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::*;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue