Consolidate desktop controls with editable holidays and a running-window tray

This commit is contained in:
aria committed 2026-10-28 06:02:46 +09:00
1 parent 9908dde9f8
commit 326465952a
55 files changed
+4207 -450

No files matched your search

+188 -40
View File
@@ -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));
}
}