Respect motion settings and improve accessible theme transitions

This commit is contained in:
Samuel Onoja committed 2026-10-01 11:54:57 +01:00
1 parent 53bad8c672
commit 7a9fd5c864
13 files changed
+390 -67

No files matched your search

+9 -2
View File
@@ -42,6 +42,7 @@ impl Default for StatusConfig {
#[derive(Clone, Copy, Debug)]
pub(crate) struct ShellTheme {
pub(crate) appearance: ferese_config::theme::Appearance,
pub(crate) high_contrast: bool,
pub(crate) bar_background: [u8; 4],
pub(crate) bar_text_primary: [u8; 4],
pub(crate) bar_text_muted: [u8; 4],
@@ -70,6 +71,7 @@ impl Default for ShellTheme {
fn default() -> Self {
Self {
appearance: Default::default(),
high_contrast: false,
bar_background: [28, 32, 46, 255],
bar_text_primary: [240, 243, 250, 255],
bar_text_muted: [170, 180, 199, 255],
@@ -102,6 +104,7 @@ impl ShellTheme {
let surface = color(self.surface_base);
ferese_theme::Palette {
appearance: self.appearance,
high_contrast: self.high_contrast,
background: surface,
sidebar: surface,
card: surface,
@@ -339,11 +342,13 @@ pub(crate) fn load() -> ShellConfig {
pub(crate) fn parse_source(source: &str) -> Result<ShellConfig, ferese_config::Error> {
let document = ferese_config::Document::parse(source)?;
let snapshot = ferese_theme::service::current();
parse_document(
let mut config = parse_document(
&document.with_theme(&snapshot.presented),
snapshot.presented.appearance,
snapshot.mode,
)
)?;
config.apply_theme(&snapshot.presented);
Ok(config)
}
fn parse_document(
@@ -394,6 +399,7 @@ impl ShellConfig {
let config: ThemeConfig = serde_json::from_value(serde_json::to_value(&theme.tokens).unwrap()).unwrap();
self.theme = shell_theme(&config);
self.theme.appearance = theme.appearance;
self.theme.high_contrast = theme.accessibility.increase_contrast;
self.theme.material_radius = theme.tokens.geometry.shell_radius as f32;
self.theme.bar_radius = self.theme.material_radius;
self.font_family = Some(theme.tokens.typography.font_family.clone());
@@ -422,6 +428,7 @@ fn shell_theme(theme: &ThemeConfig) -> ShellTheme {
ShellTheme {
appearance: defaults.appearance,
high_contrast: defaults.high_contrast,
material_radius: defaults.material_radius,
bar_background: {
let mut background = parse_color(&theme.surface.bar.background).unwrap_or(defaults.bar_background);
+7 -4
View File
@@ -874,10 +874,13 @@ impl FereseShell {
}
motion::configure(config.animations, config.theme.material_radius);
if self.config.animations != config.animations
&& let Some(menu) = &mut self.menu
{
menu.motion.update_settings(config.animations);
if self.config.animations != config.animations {
if let Some(menu) = &mut self.menu {
menu.motion.update_settings(config.animations);
}
if let Some(modal) = &mut self.system_modal {
modal.motion.update_settings(config.animations);
}
}
self.status_service
.update_settings(config.status.settings_command.clone());
+25 -4
View File
@@ -76,7 +76,9 @@ impl PopupMotion {
let now = std::time::Instant::now();
self.start = self.progress_at(now);
self.settings = settings;
self.started = Some(now);
if self.started.is_some() {
self.started = Some(now);
}
}
pub(crate) fn new(settings: Settings) -> Self {
@@ -95,13 +97,13 @@ impl PopupMotion {
}
pub(crate) fn progress_at(&self, now: std::time::Instant) -> f32 {
let Some(started) = self.started else {
return self.start;
};
let duration = self.settings.duration(if self.target == 1.0 { 200.0 } else { 140.0 });
if duration.is_zero() {
return self.target;
}
let Some(started) = self.started else {
return self.start;
};
let t = (now.saturating_duration_since(started).as_secs_f32() / duration.as_secs_f32()).min(1.0);
if t == 1.0 {
return self.target;
@@ -422,6 +424,11 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Motion<'_, M> {
) {
let v = self.translation();
let transform = Transformation::translate(v.x, v.y);
// Layout/region collection still run before the surface starts opening.
// No child may paint an opaque control before the presentation begins.
if self.progress <= 0.0 {
return;
}
renderer.with_transformation(transform, |renderer| {
self.content
.as_widget()
@@ -556,12 +563,26 @@ mod tests {
},
] {
let mut motion = PopupMotion::new(settings);
assert_eq!(motion.progress_at(now), 0.0);
motion.begin(now);
assert_eq!(motion.progress_at(now), 1.0);
motion.retarget(0.0, now);
assert_eq!(motion.progress_at(now), 0.0);
}
}
#[test]
fn updating_motion_policy_preserves_surface_readiness() {
let mut motion = PopupMotion::new(Settings::default());
motion.update_settings(Settings {
reduced_motion: true,
..Settings::default()
});
assert_eq!(motion.progress(), 0.0);
motion.begin(std::time::Instant::now());
assert_eq!(motion.progress(), 1.0);
}
#[test]
fn hover_fades_settle_and_idle_does_not_schedule_frames() {
let now = std::time::Instant::now();
+8 -10
View File
@@ -242,13 +242,15 @@ impl FereseShell {
let Some(entry) = modal.surfaces.iter_mut().find(|entry| entry.id == id) else {
return;
};
modal.motion.begin(Instant::now());
if entry.primary && entry.effects.is_none() {
match EffectsBinding::attach_role(surface, None, 0.0) {
Ok(effects) => entry.effects = Some(effects),
Err(error) => eprintln!("ferese-shell: system modal material unavailable: {error}"),
}
}
if entry.primary {
modal.motion.begin(Instant::now());
}
}
pub(super) fn update_power_materials(&self) {
@@ -422,13 +424,8 @@ impl FereseShell {
let content: Element<'_, cosmic::Action<Message>> = if surface.primary {
let theme = self.config.theme;
let material = surface.effects.is_some();
let alpha = if material { 1.0 } else { progress };
let mut palette = theme.palette();
palette.text = palette.text.scale_alpha(alpha);
palette.muted = palette.muted.scale_alpha(alpha);
palette.sidebar = palette.sidebar.scale_alpha(alpha);
palette.card = palette.card.scale_alpha(alpha);
palette.accent = palette.accent.scale_alpha(alpha);
// Opacity belongs to the presented surface, never to semantic colors.
let palette = theme.palette();
let rows = match &modal.content {
Content::Power(action) => {
let mut rows = column![
@@ -526,7 +523,7 @@ impl FereseShell {
.padding(24)
.class(theme::Container::custom(move |_| container::Style {
background: (!material)
.then_some(Background::Color(color_with_opacity(theme.surface_popover, alpha))),
.then_some(Background::Color(color_with_opacity(theme.surface_popover, 1.0))),
text_color: Some(palette.text),
icon_color: Some(palette.text),
border: Border {
@@ -550,6 +547,7 @@ impl FereseShell {
} else {
content
};
let scrim_alpha = if surface.effects.is_some() { 1.0 } else { progress };
let centered = container(content)
.width(Length::Fill)
.height(Length::Fill)
@@ -557,7 +555,7 @@ impl FereseShell {
.align_x(cosmic::iced::Alignment::Center)
.align_y(cosmic::iced::Alignment::Center)
.class(theme::Container::custom(move |_| container::Style {
background: Some(Background::Color(Color::from_rgba(0.0, 0.0, 0.0, 0.34 * progress))),
background: Some(Background::Color(Color::from_rgba(0.0, 0.0, 0.0, 0.34 * scrim_alpha))),
..Default::default()
}));
centered.into()