Consolidate desktop controls with editable holidays and a running-window tray
This commit is contained in:
1 parent
9908dde9f8
commit
326465952a
55 files changed
+4207
-450
No files matched your search
@@ -36,7 +36,7 @@ pub fn month(today: jiff::civil::Date, offset: i32) -> (jiff::civil::Date, usize
|
||||
let year = month_index.div_euclid(12) as i16;
|
||||
let month = (month_index.rem_euclid(12) + 1) as i8;
|
||||
let first = jiff::civil::Date::new(year, month, 1).expect("calendar month is in range");
|
||||
let start = usize::try_from(first.weekday().since(jiff::civil::Weekday::Monday)).unwrap_or(0);
|
||||
let start = usize::try_from(first.weekday().since(jiff::civil::Weekday::Sunday)).unwrap_or(0);
|
||||
let days = usize::try_from(first.days_in_month()).unwrap_or(31);
|
||||
(first, start, days)
|
||||
}
|
||||
@@ -49,8 +49,11 @@ pub fn grid<'a, M: Clone + 'a>(
|
||||
font: cosmic::font::Font,
|
||||
opacity: f32,
|
||||
navigation: [Element<'a, M>; 3],
|
||||
settings: &ferese_config::calendar::Settings,
|
||||
) -> Element<'a, M> {
|
||||
grid_with_density(today, offset, palette, font, opacity, navigation, false)
|
||||
grid_with_density(
|
||||
today, offset, palette, font, opacity, navigation, false, settings,
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
@@ -61,8 +64,11 @@ pub fn compact_grid<'a, M: Clone + 'a>(
|
||||
font: cosmic::font::Font,
|
||||
opacity: f32,
|
||||
navigation: [Element<'a, M>; 3],
|
||||
settings: &ferese_config::calendar::Settings,
|
||||
) -> Element<'a, M> {
|
||||
grid_with_density(today, offset, palette, font, opacity, navigation, true)
|
||||
grid_with_density(
|
||||
today, offset, palette, font, opacity, navigation, true, settings,
|
||||
)
|
||||
}
|
||||
|
||||
fn grid_with_density<'a, M: Clone + 'a>(
|
||||
@@ -73,13 +79,10 @@ fn grid_with_density<'a, M: Clone + 'a>(
|
||||
opacity: f32,
|
||||
navigation: [Element<'a, M>; 3],
|
||||
compact: bool,
|
||||
settings: &ferese_config::calendar::Settings,
|
||||
) -> Element<'a, M> {
|
||||
let cell_width = if compact {
|
||||
Length::Fill
|
||||
} else {
|
||||
Length::Fixed(30.)
|
||||
};
|
||||
let cell_height = if compact { 20. } else { 25. };
|
||||
let cell_width = Length::Fill;
|
||||
let cell_height = if compact { 24. } else { 28. };
|
||||
let spacing = if compact { 2. } else { 3. };
|
||||
let months = [
|
||||
"January",
|
||||
@@ -130,7 +133,7 @@ fn grid_with_density<'a, M: Clone + 'a>(
|
||||
} else {
|
||||
Length::Shrink
|
||||
});
|
||||
for label in ["M", "T", "W", "T", "F", "S", "S"] {
|
||||
for label in weekday_labels(ferese_config::locale::resolved()) {
|
||||
weekdays = weekdays.push(
|
||||
container(
|
||||
crate::text(label, font)
|
||||
@@ -150,42 +153,187 @@ fn grid_with_density<'a, M: Clone + 'a>(
|
||||
});
|
||||
for weekday in 0..7 {
|
||||
let cell = week * 7 + weekday;
|
||||
let day = cell
|
||||
.checked_sub(start)
|
||||
.map(|day| day + 1)
|
||||
.filter(|day| *day <= days);
|
||||
let is_today =
|
||||
year == today.year() && month == today.month() && day == Some(today.day() as usize);
|
||||
let label = day.map_or(String::new(), |day| day.to_string());
|
||||
dates = dates.push(
|
||||
container(
|
||||
crate::text(label, font)
|
||||
.size(12)
|
||||
.class(theme::Text::Color(if is_today { primary } else { muted })),
|
||||
)
|
||||
.width(cell_width)
|
||||
.height(cell_height)
|
||||
.center_x(cell_width)
|
||||
.center_y(cell_height)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: is_today
|
||||
.then_some(Background::Color(palette.accent.scale_alpha(0.2 * opacity))),
|
||||
border: Border {
|
||||
shape: BorderShape::Continuous,
|
||||
radius: palette.radius.min(12.).into(),
|
||||
..Default::default()
|
||||
},
|
||||
let date = first
|
||||
.checked_add(jiff::Span::new().days(cell as i64 - start as i64))
|
||||
.ok();
|
||||
let in_month = date.is_some_and(|date| date.month() == month && date.year() == year);
|
||||
let visible = in_month || settings.show_adjacent_days;
|
||||
let date = date.filter(|_| visible);
|
||||
let is_today = date == Some(today);
|
||||
let holiday = date.and_then(|date| settings.holiday_name(date));
|
||||
let holiday_color = if palette.appearance == ferese_config::theme::Appearance::Light {
|
||||
cosmic::iced::Color::from_rgb8(184, 38, 49)
|
||||
} else {
|
||||
cosmic::iced::Color::from_rgb8(244, 110, 120)
|
||||
}
|
||||
.scale_alpha(opacity * if in_month { 1. } else { 0.65 });
|
||||
let foreground = if holiday.is_some() {
|
||||
holiday_color
|
||||
} else if is_today || in_month {
|
||||
primary
|
||||
} else {
|
||||
muted.scale_alpha(0.6)
|
||||
};
|
||||
let label = date.map_or(String::new(), |date| date.day().to_string());
|
||||
let font = cosmic::iced::Font {
|
||||
weight: if holiday.is_some() {
|
||||
cosmic::iced::font::Weight::Bold
|
||||
} else {
|
||||
cosmic::iced::font::Weight::Normal
|
||||
},
|
||||
..font
|
||||
};
|
||||
let day: Element<'a, M> = container(
|
||||
crate::text(label, font)
|
||||
.size(12)
|
||||
.class(theme::Text::Color(foreground)),
|
||||
)
|
||||
.id(cosmic::iced::advanced::widget::Id::new(date.map_or_else(
|
||||
|| format!("calendar-empty-{cell}"),
|
||||
|date| format!("calendar-date-{date}"),
|
||||
)))
|
||||
.width(cell_width)
|
||||
.height(cell_height)
|
||||
.center_x(cell_width)
|
||||
.center_y(cell_height)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: is_today
|
||||
.then_some(Background::Color(palette.accent.scale_alpha(0.2 * opacity))),
|
||||
border: Border {
|
||||
shape: BorderShape::Continuous,
|
||||
radius: palette.radius.min(12.).into(),
|
||||
..Default::default()
|
||||
})),
|
||||
);
|
||||
},
|
||||
..Default::default()
|
||||
}))
|
||||
.into();
|
||||
dates = dates.push(if let Some(name) = holiday {
|
||||
cosmic::widget::tooltip(
|
||||
day,
|
||||
crate::text(name, font).size(11),
|
||||
cosmic::widget::tooltip::Position::Top,
|
||||
)
|
||||
.into()
|
||||
} else {
|
||||
day
|
||||
});
|
||||
}
|
||||
grid = grid.push(dates);
|
||||
}
|
||||
if let Err(error) =
|
||||
ferese_config::calendar::japanese_holidays(&settings.japanese_holiday_function, year)
|
||||
{
|
||||
grid = grid.push(
|
||||
crate::text(
|
||||
ferese_config::locale::format(
|
||||
"Holiday function error: {error}",
|
||||
&[("error", error)],
|
||||
),
|
||||
font,
|
||||
)
|
||||
.size(10)
|
||||
.class(theme::Text::Color(muted)),
|
||||
);
|
||||
}
|
||||
grid.into()
|
||||
}
|
||||
|
||||
/// Sunday is always the first column; labels follow Ferese's interface language.
|
||||
#[must_use]
|
||||
pub fn weekday_labels(language: ferese_config::locale::Language) -> [&'static str; 7] {
|
||||
if language == ferese_config::locale::Language::Chinese {
|
||||
["日", "一", "二", "三", "四", "五", "六"]
|
||||
} else {
|
||||
["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn sunday_grid_localizes_weekdays_and_can_hide_adjacent_dates_without_reflow() {
|
||||
use cosmic::iced::advanced::renderer::Headless;
|
||||
use cosmic::iced::advanced::widget::{Id, Operation, Tree};
|
||||
use cosmic::iced::advanced::{Layout, layout};
|
||||
use cosmic::iced::{Font, Pixels, Rectangle, Size};
|
||||
use ferese_config::locale::Language;
|
||||
assert_eq!(
|
||||
super::weekday_labels(Language::Chinese),
|
||||
["日", "一", "二", "三", "四", "五", "六"]
|
||||
);
|
||||
assert_eq!(
|
||||
super::weekday_labels(Language::English),
|
||||
["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
|
||||
);
|
||||
#[derive(Default)]
|
||||
struct Dates {
|
||||
first: Option<Rectangle>,
|
||||
sunday: Option<Rectangle>,
|
||||
holiday: Option<Rectangle>,
|
||||
}
|
||||
impl Operation for Dates {
|
||||
fn traverse(&mut self, children: &mut dyn FnMut(&mut dyn Operation)) {
|
||||
children(self);
|
||||
}
|
||||
fn container(&mut self, id: Option<&Id>, bounds: Rectangle) {
|
||||
if id == Some(&Id::new("calendar-date-2026-10-01")) {
|
||||
self.first = Some(bounds);
|
||||
}
|
||||
if id == Some(&Id::new("calendar-date-2026-09-27")) {
|
||||
self.sunday = Some(bounds);
|
||||
}
|
||||
if id == Some(&Id::new("calendar-date-2026-10-12")) {
|
||||
self.holiday = Some(bounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.build()
|
||||
.unwrap();
|
||||
let renderer = runtime
|
||||
.block_on(<cosmic::Renderer as Headless>::new(
|
||||
Font::default(),
|
||||
Pixels(14.),
|
||||
Some("tiny-skia"),
|
||||
))
|
||||
.unwrap();
|
||||
let mut first = None;
|
||||
for adjacent in [true, false] {
|
||||
let settings = ferese_config::calendar::Settings {
|
||||
show_adjacent_days: adjacent,
|
||||
..Default::default()
|
||||
};
|
||||
let mut view: cosmic::Element<'_, ()> = super::grid(
|
||||
"2026-10-28".parse().unwrap(),
|
||||
0,
|
||||
crate::Palette::from_resolved(&ferese_config::theme::default_theme()),
|
||||
Font::default(),
|
||||
1.,
|
||||
std::array::from_fn(|_| cosmic::widget::Space::new().width(20).height(20).into()),
|
||||
&settings,
|
||||
);
|
||||
let mut tree = Tree::new(&view);
|
||||
let node = view.as_widget_mut().layout(
|
||||
&mut tree,
|
||||
&renderer,
|
||||
&layout::Limits::new(Size::ZERO, Size::new(330., 600.)),
|
||||
);
|
||||
let mut dates = Dates::default();
|
||||
view.as_widget_mut()
|
||||
.operate(&mut tree, Layout::new(&node), &renderer, &mut dates);
|
||||
assert_eq!(dates.sunday.is_some(), adjacent);
|
||||
assert!(dates.holiday.is_some());
|
||||
let current = dates.first.unwrap();
|
||||
if let Some(first) = first {
|
||||
assert_eq!(current, first);
|
||||
} else {
|
||||
first = Some(current);
|
||||
}
|
||||
if let Some(sunday) = dates.sunday {
|
||||
assert!(sunday.x < current.x && (sunday.y - current.y).abs() < 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn clock_uses_lowercase_date_and_twelve_hour_time() {
|
||||
for (stamp, expected) in [
|
||||
@@ -210,14 +358,14 @@ mod tests {
|
||||
fn calendar_aligns_leap_months_and_year_boundaries() {
|
||||
let february = jiff::civil::Date::new(2024, 2, 15).unwrap();
|
||||
let (first, start, days) = super::month(february, 0);
|
||||
assert_eq!((first.year(), first.month(), start, days), (2024, 2, 3, 29));
|
||||
assert_eq!((first.year(), first.month(), start, days), (2024, 2, 4, 29));
|
||||
let (first, start, days) = super::month(february, -2);
|
||||
assert_eq!(
|
||||
(first.year(), first.month(), start, days),
|
||||
(2023, 12, 4, 31)
|
||||
(2023, 12, 5, 31)
|
||||
);
|
||||
let june = jiff::civil::Date::new(2025, 6, 1).unwrap();
|
||||
let (_, start, days) = super::month(june, 0);
|
||||
assert_eq!((start, days), (6, 30));
|
||||
assert_eq!((start, days), (0, 30));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user