Fix compositor output routing and pointer hints; format imports

This commit is contained in:
Samuel Onoja committed 2026-09-30 06:39:58 +01:00
1 parent 88e4f3c135
commit 46509f880a
108 files changed
+3682 -8371

No files matched your search

+5 -10
View File
@@ -1,9 +1,8 @@
use cosmic::iced::{Background, Color};
use cosmic::theme;
use cosmic::widget::button;
use crate::{composite, foreground};
use cosmic::{
iced::{Background, Color},
theme,
widget::button,
};
#[derive(Clone, Copy)]
pub(crate) enum State {
@@ -13,11 +12,7 @@ pub(crate) enum State {
Disabled,
}
pub(crate) fn accent_button_style(
theme: &cosmic::Theme,
state: State,
focused: bool,
) -> button::Style {
pub(crate) fn accent_button_style(theme: &cosmic::Theme, state: State, focused: bool) -> button::Style {
let native = theme.cosmic();
let component = &native.accent_button;
let fill: Color = match state {
+21 -54
View File
@@ -1,10 +1,7 @@
//! 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 cosmic::iced::{Alignment, Background, Border, Length};
use cosmic::widget::{column, container, row};
use cosmic::{Element, theme};
use jiff::Zoned;
pub fn format_bar_time(now: &Zoned) -> String {
@@ -41,10 +38,7 @@ pub fn stacked_digits(label: &str) -> Option<[String; 2]> {
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())
|| !hour.bytes().chain(minute.bytes()).all(|digit| digit.is_ascii_digit())
{
return None;
}
@@ -104,13 +98,9 @@ pub fn grid<'a, M: Clone + 'a>(
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),
container(crate::text(label, font).size(11).class(theme::Text::Color(muted)))
.width(30)
.center_x(30),
);
}
grid = grid.push(weekdays);
@@ -118,26 +108,21 @@ pub fn grid<'a, M: Clone + 'a>(
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 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 })),
)
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))),
background: is_today.then_some(Background::Color(palette.accent.scale_alpha(0.2 * opacity))),
border: Border {
radius: palette.radius.min(12.).into(),
..Default::default()
@@ -155,14 +140,8 @@ pub fn grid<'a, M: Clone + 'a>(
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_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());
@@ -171,18 +150,9 @@ mod tests {
#[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",
),
("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);
}
@@ -194,10 +164,7 @@ mod tests {
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)
);
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));
+11 -42
View File
@@ -76,27 +76,12 @@ pub fn apply(native: &mut cosmic::cosmic_theme::Theme, preferred: Color) {
let surface: Color = native.primary(false).base.into();
for component in [&mut native.accent, &mut native.accent_button] {
let (base, on) = accent_pair(composite(component.base.into(), surface), preferred);
component.hover = rgba(readable_fill(
composite(component.hover.into(), surface),
base,
on,
));
component.pressed = rgba(readable_fill(
composite(component.pressed.into(), surface),
base,
on,
));
component.selected = rgba(readable_fill(
composite(component.selected.into(), surface),
base,
on,
));
component.hover = rgba(readable_fill(composite(component.hover.into(), surface), base, on));
component.pressed = rgba(readable_fill(composite(component.pressed.into(), surface), base, on));
component.selected = rgba(readable_fill(composite(component.selected.into(), surface), base, on));
component.base = rgba(base);
component.on = rgba(on);
component.selected_text = rgba(foreground(
composite(component.selected.into(), surface),
on,
));
component.selected_text = rgba(foreground(composite(component.selected.into(), surface), on));
component.disabled = rgba(composite(Color { a: 0.5, ..base }, surface));
component.on_disabled = rgba(foreground(composite(Color { a: 0.5, ..base }, surface), on));
}
@@ -125,9 +110,10 @@ fn rgba(c: Color) -> cosmic::cosmic_theme::palette::Srgba {
#[cfg(test)]
mod tests {
use cosmic::iced::Background;
use super::*;
use crate::button::{State, accent_button_style};
use cosmic::iced::Background;
#[test]
fn reference_contrast_values_use_linear_srgb() {
assert!((contrast(Color::BLACK, Color::WHITE) - 21.).abs() < 0.00001);
@@ -139,10 +125,7 @@ mod tests {
let preferred = Color::from_rgb8(240, 242, 246);
assert_eq!(foreground(Color::BLACK, preferred), preferred);
assert_eq!(foreground(Color::WHITE, preferred), Color::BLACK);
assert_eq!(
foreground(Color::from_rgb8(61, 123, 230), preferred),
Color::BLACK
);
assert_eq!(foreground(Color::from_rgb8(61, 123, 230), preferred), Color::BLACK);
}
#[test]
@@ -170,12 +153,7 @@ mod tests {
.build();
apply(&mut native, preferred);
let theme = cosmic::Theme::custom(std::sync::Arc::new(native));
for state in [
State::Active,
State::Hovered,
State::Pressed,
State::Disabled,
] {
for state in [State::Active, State::Hovered, State::Pressed, State::Disabled] {
let actual = accent_button_style(&theme, state, false);
assert_eq!(actual.text_color, Some(preferred));
assert_eq!(actual.icon_color, Some(preferred));
@@ -223,20 +201,11 @@ mod tests {
for fill in [component.base, component.hover, component.pressed] {
assert!(contrast(fill.into(), component.on.into()) >= 4.5);
}
assert!(
contrast(component.selected.into(), component.selected_text.into()) >= 4.5
);
assert!(
contrast(component.disabled.into(), component.on_disabled.into()) >= 4.5
);
assert!(contrast(component.selected.into(), component.selected_text.into()) >= 4.5);
assert!(contrast(component.disabled.into(), component.on_disabled.into()) >= 4.5);
}
let theme = cosmic::Theme::custom(std::sync::Arc::new(native));
for state in [
State::Active,
State::Hovered,
State::Pressed,
State::Disabled,
] {
for state in [State::Active, State::Hovered, State::Pressed, State::Disabled] {
let style = accent_button_style(&theme, state, false);
let Some(Background::Color(fill)) = style.background else {
panic!("No fill")
+8 -24
View File
@@ -1,9 +1,8 @@
use cosmic::iced::{Background, Border, Color, Vector};
use cosmic::widget::{button, container};
use cosmic::{theme, widget};
use crate::{Palette, mix};
use cosmic::{
iced::{Background, Border, Color, Vector},
theme,
widget::{self, button, container},
};
pub fn surface(background: Color, radius: f32) -> theme::Container<'static> {
theme::Container::custom(move |_| surface_appearance(background, radius))
@@ -85,11 +84,7 @@ fn styled_button(p: Palette, selected: bool, navigation: bool) -> theme::Button
icon_color: Some(on),
border_radius: p.radius.min(9.).into(),
border_width: if selected { 1. } else { 0. },
border_color: if selected {
p.accent
} else {
Color::TRANSPARENT
},
border_color: if selected { p.accent } else { Color::TRANSPARENT },
outline_width: 0.,
outline_color: Color::TRANSPARENT,
overlay: None,
@@ -136,11 +131,7 @@ pub fn authentication_input(palette: Palette) -> theme::TextInput {
}
}
pub fn lock_input(
radius: f32,
accent: cosmic::iced::Color,
surface: cosmic::iced::Color,
) -> theme::TextInput {
pub fn lock_input(radius: f32, accent: cosmic::iced::Color, surface: cosmic::iced::Color) -> theme::TextInput {
let appearance = move |focused: bool| widget::text_input::Appearance {
background: cosmic::iced::Color::from_rgba(
(surface.r + 1.) * 0.5,
@@ -190,9 +181,7 @@ pub fn shell_button(
a: if pressed {
0.20 * opacity
} else {
((if selected { 0.14 } else { 0.0 })
+ progress * if selected { 0.02 } else { 0.08 })
* opacity
((if selected { 0.14 } else { 0.0 }) + progress * if selected { 0.02 } else { 0.08 }) * opacity
},
..foreground
})),
@@ -211,12 +200,7 @@ pub fn surface_appearance(background: Color, radius: f32) -> container::Style {
}
}
pub fn notification_button(
foreground: Color,
hover: Color,
radius: f32,
filled: bool,
) -> theme::Button {
pub fn notification_button(foreground: Color, hover: Color, radius: f32, filled: bool) -> theme::Button {
let style = move |active: bool| button::Style {
text_color: Some(foreground),
icon_color: Some(foreground),
+7 -13
View File
@@ -1,9 +1,7 @@
//! Canonical SVG assets, sizing and semantic tint rules for native Ferese UIs.
use cosmic::{
iced::{Color, ContentFit, Length},
theme,
widget::icon,
};
use cosmic::iced::{Color, ContentFit, Length};
use cosmic::theme;
use cosmic::widget::icon;
pub const ARROW: &[u8] = include_bytes!("../assets/icons/arrow.svg");
pub const BATTERY: &[u8] = include_bytes!("../assets/icons/battery.svg");
@@ -18,8 +16,7 @@ pub const BATTERY_75: &[u8] = include_bytes!("../assets/icons/status/battery-75.
pub const BATTERY_CHARGING: &[u8] = include_bytes!("../assets/icons/status/battery-charging.svg");
pub const BATTERY_EMPTY: &[u8] = include_bytes!("../assets/icons/status/battery-empty.svg");
pub const BATTERY_FULL: &[u8] = include_bytes!("../assets/icons/status/battery-full.svg");
pub const BLUETOOTH_CONNECTED: &[u8] =
include_bytes!("../assets/icons/status/bluetooth-connected.svg");
pub const BLUETOOTH_CONNECTED: &[u8] = include_bytes!("../assets/icons/status/bluetooth-connected.svg");
pub const BLUETOOTH_OFF: &[u8] = include_bytes!("../assets/icons/status/bluetooth-off.svg");
pub const BLUETOOTH_ON: &[u8] = include_bytes!("../assets/icons/status/bluetooth-on.svg");
pub const BRIGHTNESS: &[u8] = include_bytes!("../assets/icons/status/brightness.svg");
@@ -67,10 +64,7 @@ pub fn tinted(source: &'static [u8], size: u16, foreground: Color) -> icon::Icon
}
pub fn symbolic(source: &'static [u8], size: u16) -> icon::Icon {
icon::from_svg_bytes(source)
.symbolic(true)
.icon()
.size(size)
icon::from_svg_bytes(source).symbolic(true).icon().size(size)
}
pub fn accented(source: &'static [u8], size: u16, foreground: Color, accent: Color) -> icon::Icon {
@@ -88,8 +82,8 @@ pub fn accented(source: &'static [u8], size: u16, foreground: Color, accent: Col
.size(size)
.width(Length::Fixed(f32::from(size) * aspect))
.content_fit(ContentFit::Contain)
.class(theme::Svg::custom(move |_| {
cosmic::iced::widget::svg::Style { color: None }
.class(theme::Svg::custom(move |_| cosmic::iced::widget::svg::Style {
color: None,
}))
}
+9 -17
View File
@@ -1,14 +1,10 @@
use std::sync::Arc;
use ferese_protocols::material::v1::client::{
ferese_material_manager_v1::FereseMaterialManagerV1,
ferese_surface_material_v1::FereseSurfaceMaterialV1,
};
use wayland_client::{
Connection, Dispatch, Proxy, QueueHandle, delegate_noop,
globals::{GlobalListContents, registry_queue_init},
protocol::{wl_registry, wl_surface},
};
use ferese_protocols::material::v1::client::ferese_material_manager_v1::FereseMaterialManagerV1;
use ferese_protocols::material::v1::client::ferese_surface_material_v1::FereseSurfaceMaterialV1;
use wayland_client::globals::{GlobalListContents, registry_queue_init};
use wayland_client::protocol::{wl_registry, wl_surface};
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle, delegate_noop};
/// Keeps a compositor-owned dialog material attached for the window lifetime.
#[derive(Clone, Debug)]
@@ -37,9 +33,8 @@ impl ModalMaterial {
return Err("Dialog materials require Wayland".into());
};
// Iced lends live native handles; this backend borrows its connection.
let backend = unsafe {
wayland_client::backend::Backend::from_foreign_display(display.display.as_ptr().cast())
};
let backend =
unsafe { wayland_client::backend::Backend::from_foreign_display(display.display.as_ptr().cast()) };
let connection = Connection::from_backend(backend);
let id = unsafe {
wayland_client::backend::ObjectId::from_ptr(
@@ -48,8 +43,7 @@ impl ModalMaterial {
)
}
.map_err(|error| error.to_string())?;
let surface =
wl_surface::WlSurface::from_id(&connection, id).map_err(|error| error.to_string())?;
let surface = wl_surface::WlSurface::from_id(&connection, id).map_err(|error| error.to_string())?;
let (globals, mut queue) =
registry_queue_init::<MaterialState>(&connection).map_err(|error| error.to_string())?;
let qh = queue.handle();
@@ -63,9 +57,7 @@ impl ModalMaterial {
material,
};
let mut state = MaterialState::default();
queue
.roundtrip(&mut state)
.map_err(|error| error.to_string())?;
queue.roundtrip(&mut state).map_err(|error| error.to_string())?;
if !state.ready {
return Err("Dialog material attachment was not acknowledged".into());
}
+10 -31
View File
@@ -1,9 +1,7 @@
//! Shared menu layouts and paint; callers own actions and animation timing.
use cosmic::{
Element,
iced::{Alignment, Color, Length},
widget::{button, container, row as iced_row},
};
use cosmic::Element;
use cosmic::iced::{Alignment, Color, Length};
use cosmic::widget::{button, container, row as iced_row};
pub fn button<'a, M: Clone + 'a>(
label: impl Into<std::borrow::Cow<'a, str>> + 'a,
@@ -20,12 +18,9 @@ pub fn row<'a, M: 'a>(
trailing: Element<'a, M>,
font: cosmic::font::Font,
) -> Element<'a, M> {
iced_row![
crate::text(label, font).width(Length::Fill).size(14),
trailing
]
.align_y(Alignment::Center)
.into()
iced_row![crate::text(label, font).width(Length::Fill).size(14), trailing]
.align_y(Alignment::Center)
.into()
}
pub fn heading<'a, M: 'a>(
@@ -38,12 +33,7 @@ pub fn heading<'a, M: 'a>(
.align_y(Alignment::Center)
}
pub fn badge<'a, M: 'a>(
content: Element<'a, M>,
accent: Color,
opacity: f32,
radius: f32,
) -> Element<'a, M> {
pub fn badge<'a, M: 'a>(content: Element<'a, M>, accent: Color, opacity: f32, radius: f32) -> Element<'a, M> {
container(content)
.width(36)
.height(36)
@@ -56,12 +46,7 @@ pub fn badge<'a, M: 'a>(
.into()
}
pub fn section<'a, M: 'a>(
content: Element<'a, M>,
foreground: Color,
opacity: f32,
radius: f32,
) -> Element<'a, M> {
pub fn section<'a, M: 'a>(content: Element<'a, M>, foreground: Color, opacity: f32, radius: f32) -> Element<'a, M> {
container(content)
.padding(10)
.width(Length::Fill)
@@ -87,10 +72,7 @@ pub fn section_label<'a>(
pub fn separator<'a, M: 'a>(foreground: Color, opacity: f32) -> Element<'a, M> {
container(cosmic::widget::Space::new().width(Length::Fill).height(1))
.class(crate::controls::surface(
foreground.scale_alpha(0.12 * opacity),
0.,
))
.class(crate::controls::surface(foreground.scale_alpha(0.12 * opacity), 0.))
.into()
}
@@ -112,10 +94,7 @@ pub fn switch<'a, M: 'a>(enabled: bool, palette: crate::Palette, opacity: f32) -
.width(16)
.height(16)
.class(crate::controls::surface(
Color {
a: opacity,
..thumb
},
Color { a: opacity, ..thumb },
palette.radius.min(8.),
));
container(knob)
+3 -9
View File
@@ -102,11 +102,7 @@ pub fn parse_color(value: &str) -> Option<Color> {
}
pub fn mix(a: Color, b: Color, t: f32) -> Color {
Color::from_rgb(
a.r + (b.r - a.r) * t,
a.g + (b.g - a.g) * t,
a.b + (b.b - a.b) * t,
)
Color::from_rgb(a.r + (b.r - a.r) * t, a.g + (b.g - a.g) * t, a.b + (b.b - a.b) * t)
}
pub fn surface_shade(base: Color) -> Color {
@@ -179,10 +175,8 @@ mod tests {
("translucent", 2., 1.),
("translucent", -1., 0.),
] {
let document = Document::parse(&format!(
"theme {{ material {{ style {style}; opacity {opacity}; }} }}"
))
.unwrap();
let document =
Document::parse(&format!("theme {{ material {{ style {style}; opacity {opacity}; }} }}")).unwrap();
assert_eq!(material_opacity(Some(&document)), expected);
}
}
+2 -4
View File
@@ -1,7 +1,5 @@
use std::{
collections::HashMap,
sync::{Mutex, OnceLock},
};
use std::collections::HashMap;
use std::sync::{Mutex, OnceLock};
pub fn font(family: Option<&str>) -> cosmic::font::Font {
let Some(family) = family else {