Centralize native palettes, presets, typography, controls, menus, calendar rendering, and icon assets in ferese-theme. Reuse the shared components across the shell, settings, lock screen, authentication prompt, and screen-sharing chooser. Attach themed dialogs to compositor-owned materials, correct backdrop capture and sampling across output transforms, smooth patterned backgrounds, and prevent clipped picker buttons with the configured shell font.
206 lines
6.5 KiB
Rust
206 lines
6.5 KiB
Rust
//! Date/time labels and a calendar grid; callers own navigation and popup behavior.
|
|
use cosmic::{
|
|
Element,
|
|
iced::{Alignment, Background, Border, Length},
|
|
theme,
|
|
widget::{column, container, row},
|
|
};
|
|
use jiff::Zoned;
|
|
|
|
pub fn format_bar_time(now: &Zoned) -> String {
|
|
let months = [
|
|
"jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sept", "oct", "nov", "dec",
|
|
];
|
|
let hour = now.hour();
|
|
let hour12 = if hour % 12 == 0 { 12 } else { hour % 12 };
|
|
format!(
|
|
"{} {}, {}:{:02} {}",
|
|
now.day(),
|
|
months[(now.month() - 1) as usize],
|
|
hour12,
|
|
now.minute(),
|
|
if hour < 12 { "am" } else { "pm" }
|
|
)
|
|
}
|
|
|
|
pub fn stacked_digits(label: &str) -> Option<[String; 2]> {
|
|
let mut parts = label.split_whitespace();
|
|
let time = parts.next()?;
|
|
|
|
if let Some(period) = parts.next()
|
|
&& !period.eq_ignore_ascii_case("am")
|
|
&& !period.eq_ignore_ascii_case("pm")
|
|
{
|
|
return None;
|
|
}
|
|
|
|
if parts.next().is_some() {
|
|
return None;
|
|
}
|
|
|
|
let (hour, minute) = time.split_once(':')?;
|
|
if !(1..=2).contains(&hour.len())
|
|
|| minute.len() != 2
|
|
|| !hour
|
|
.bytes()
|
|
.chain(minute.bytes())
|
|
.all(|digit| digit.is_ascii_digit())
|
|
{
|
|
return None;
|
|
}
|
|
|
|
Some([format!("{hour:0>2}"), minute.to_owned()])
|
|
}
|
|
|
|
pub fn month(today: jiff::civil::Date, offset: i32) -> (jiff::civil::Date, usize, usize) {
|
|
let month_index = i32::from(today.year()) * 12 + i32::from(today.month()) - 1 + offset;
|
|
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 days = usize::try_from(first.days_in_month()).unwrap_or(31);
|
|
(first, start, days)
|
|
}
|
|
|
|
pub fn grid<'a, M: Clone + 'a>(
|
|
today: jiff::civil::Date,
|
|
offset: i32,
|
|
palette: crate::Palette,
|
|
font: cosmic::font::Font,
|
|
opacity: f32,
|
|
navigation: [Element<'a, M>; 3],
|
|
) -> Element<'a, M> {
|
|
let months = [
|
|
"January",
|
|
"February",
|
|
"March",
|
|
"April",
|
|
"May",
|
|
"June",
|
|
"July",
|
|
"August",
|
|
"September",
|
|
"October",
|
|
"November",
|
|
"December",
|
|
];
|
|
let (first, start, days) = month(today, offset);
|
|
let year = first.year();
|
|
let month = first.month();
|
|
let primary = palette.text.scale_alpha(opacity);
|
|
let muted = palette.muted.scale_alpha(opacity);
|
|
let [previous, today_button, next] = navigation;
|
|
let header = row![
|
|
crate::text(format!("{} {year}", months[(month - 1) as usize]), font)
|
|
.size(15)
|
|
.width(Length::Fill),
|
|
previous,
|
|
today_button,
|
|
next,
|
|
]
|
|
.align_y(Alignment::Center)
|
|
.spacing(2);
|
|
let mut grid = column::with_capacity(8).spacing(3).push(header);
|
|
let mut weekdays = row::with_capacity(7).spacing(3);
|
|
for label in ["M", "T", "W", "T", "F", "S", "S"] {
|
|
weekdays = weekdays.push(
|
|
container(
|
|
crate::text(label, font)
|
|
.size(11)
|
|
.class(theme::Text::Color(muted)),
|
|
)
|
|
.width(30)
|
|
.center_x(30),
|
|
);
|
|
}
|
|
grid = grid.push(weekdays);
|
|
for week in 0..(start + days).div_ceil(7) {
|
|
let mut dates = row::with_capacity(7).spacing(3);
|
|
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(30)
|
|
.height(25)
|
|
.center_x(30)
|
|
.center_y(25)
|
|
.class(theme::Container::custom(move |_| container::Style {
|
|
background: is_today
|
|
.then_some(Background::Color(palette.accent.scale_alpha(0.2 * opacity))),
|
|
border: Border {
|
|
radius: palette.radius.min(12.).into(),
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
})),
|
|
);
|
|
}
|
|
grid = grid.push(dates);
|
|
}
|
|
grid.into()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
#[test]
|
|
fn pixel_clock_stacks_only_hour_and_minute_formats() {
|
|
assert_eq!(
|
|
super::stacked_digits("2:45 pm"),
|
|
Some(["02".into(), "45".into()])
|
|
);
|
|
assert_eq!(
|
|
super::stacked_digits("14:45"),
|
|
Some(["14".into(), "45".into()])
|
|
);
|
|
assert!(super::stacked_digits("14:45:30").is_none());
|
|
assert!(super::stacked_digits("Today 14:45").is_none());
|
|
assert!(super::stacked_digits("14:45 pm extra").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn clock_uses_lowercase_date_and_twelve_hour_time() {
|
|
for (stamp, expected) in [
|
|
(
|
|
"2026-09-28T21:32:00+01:00[Africa/Lagos]",
|
|
"28 sept, 9:32 pm",
|
|
),
|
|
(
|
|
"2026-09-28T00:05:00+01:00[Africa/Lagos]",
|
|
"28 sept, 12:05 am",
|
|
),
|
|
(
|
|
"2026-09-28T12:00:00+01:00[Africa/Lagos]",
|
|
"28 sept, 12:00 pm",
|
|
),
|
|
] {
|
|
assert_eq!(super::format_bar_time(&stamp.parse().unwrap()), expected);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
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));
|
|
let (first, start, days) = super::month(february, -2);
|
|
assert_eq!(
|
|
(first.year(), first.month(), start, days),
|
|
(2023, 12, 4, 31)
|
|
);
|
|
let june = jiff::civil::Date::new(2025, 6, 1).unwrap();
|
|
let (_, start, days) = super::month(june, 0);
|
|
assert_eq!((start, days), (6, 30));
|
|
}
|
|
}
|