Add live config reload, session locking and managed daemons
Refine focus gradients, shell workspace controls and popup motion. Correct floating stacking and preserve client-chosen dialog sizes. Add lock protocol probes and session integration.
This commit is contained in:
1 parent
b422448d88
commit
672b4e2b60
41 files changed
+2694
-298
No files matched your search
@@ -6,6 +6,322 @@ use cosmic::{Element, Theme};
|
||||
|
||||
pub type Regions = std::sync::Arc<std::sync::Mutex<Vec<[i32; 5]>>>;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, serde::Deserialize)]
|
||||
#[serde(default)]
|
||||
pub(crate) struct Settings {
|
||||
enabled: bool,
|
||||
reduced_motion: bool,
|
||||
speed: f64,
|
||||
}
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
reduced_motion: false,
|
||||
speed: 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Settings {
|
||||
fn hover_duration(self) -> std::time::Duration {
|
||||
self.duration(120.0)
|
||||
}
|
||||
pub(crate) fn duration(self, milliseconds: f64) -> std::time::Duration {
|
||||
if !self.enabled || self.reduced_motion {
|
||||
return std::time::Duration::ZERO;
|
||||
}
|
||||
let speed = if self.speed.is_finite() && self.speed > 0.0 {
|
||||
self.speed.clamp(0.01, 100.0)
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
std::time::Duration::from_secs_f64(milliseconds / 1000.0 / speed)
|
||||
}
|
||||
}
|
||||
static SETTINGS: std::sync::OnceLock<std::sync::RwLock<Settings>> = std::sync::OnceLock::new();
|
||||
pub(crate) fn configure(settings: Settings) {
|
||||
*SETTINGS
|
||||
.get_or_init(|| std::sync::RwLock::new(Settings::default()))
|
||||
.write()
|
||||
.unwrap() = settings;
|
||||
}
|
||||
|
||||
pub(crate) struct PopupMotion {
|
||||
start: f32,
|
||||
target: f32,
|
||||
started: Option<std::time::Instant>,
|
||||
settings: Settings,
|
||||
}
|
||||
impl PopupMotion {
|
||||
pub(crate) fn update_settings(&mut self, settings: Settings) {
|
||||
let now = std::time::Instant::now();
|
||||
self.start = self.progress_at(now);
|
||||
self.settings = settings;
|
||||
self.started = Some(now);
|
||||
}
|
||||
pub(crate) fn new(settings: Settings) -> Self {
|
||||
Self {
|
||||
start: 0.0,
|
||||
target: 1.0,
|
||||
started: None,
|
||||
settings,
|
||||
}
|
||||
}
|
||||
pub(crate) fn begin(&mut self, now: std::time::Instant) {
|
||||
if self.started.is_none() {
|
||||
self.started = Some(now);
|
||||
}
|
||||
}
|
||||
pub(crate) fn progress_at(&self, now: std::time::Instant) -> f32 {
|
||||
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;
|
||||
}
|
||||
self.start + (self.target - self.start) * t * t * (3.0 - 2.0 * t)
|
||||
}
|
||||
pub(crate) fn progress(&self) -> f32 {
|
||||
self.progress_at(std::time::Instant::now())
|
||||
}
|
||||
pub(crate) fn closing(&self) -> bool {
|
||||
self.target == 0.0
|
||||
}
|
||||
pub(crate) fn animating(&self) -> bool {
|
||||
self.progress() != self.target
|
||||
}
|
||||
pub(crate) fn retarget(&mut self, target: f32, now: std::time::Instant) {
|
||||
self.start = self.progress_at(now);
|
||||
self.target = target;
|
||||
self.started = Some(now);
|
||||
}
|
||||
}
|
||||
|
||||
/// Animate paint only; the original button retains its layout, hit target,
|
||||
/// keyboard handling, pressed state, and accessibility identity.
|
||||
pub(crate) fn button<'a, M: Clone + 'a>(
|
||||
button: cosmic::widget::button::Button<'a, M>,
|
||||
foreground: cosmic::iced::Color,
|
||||
selected: bool,
|
||||
opacity: f32,
|
||||
) -> Element<'a, M> {
|
||||
let progress = std::rc::Rc::new(std::cell::Cell::new(0.0));
|
||||
let active = progress.clone();
|
||||
let hovered = progress.clone();
|
||||
let paint = move |progress: f32, pressed: bool| cosmic::widget::button::Style {
|
||||
text_color: Some(foreground),
|
||||
icon_color: Some(foreground),
|
||||
border_radius: 6.0.into(),
|
||||
background: Some(cosmic::iced::Background::Color(cosmic::iced::Color {
|
||||
a: if pressed {
|
||||
0.20 * opacity
|
||||
} else {
|
||||
((if selected { 0.14 } else { 0.0 })
|
||||
+ progress * if selected { 0.02 } else { 0.08 })
|
||||
* opacity
|
||||
},
|
||||
..foreground
|
||||
})),
|
||||
..Default::default()
|
||||
};
|
||||
let content = button
|
||||
.class(cosmic::theme::Button::Custom {
|
||||
active: Box::new(move |_, _| paint(active.get(), false)),
|
||||
hovered: Box::new(move |_, _| paint(hovered.get(), false)),
|
||||
pressed: Box::new(move |_, _| paint(1.0, true)),
|
||||
disabled: Box::new(move |_| paint(0.0, false)),
|
||||
})
|
||||
.into();
|
||||
Element::new(Hover {
|
||||
content,
|
||||
progress,
|
||||
duration: SETTINGS
|
||||
.get()
|
||||
.map_or_else(Settings::default, |settings| *settings.read().unwrap())
|
||||
.hover_duration(),
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct HoverState {
|
||||
current: f32,
|
||||
start: f32,
|
||||
target: f32,
|
||||
started: Option<std::time::Instant>,
|
||||
}
|
||||
impl HoverState {
|
||||
fn advance(
|
||||
&mut self,
|
||||
target: f32,
|
||||
now: std::time::Instant,
|
||||
duration: std::time::Duration,
|
||||
) -> bool {
|
||||
if self.target != target {
|
||||
self.start = self.current;
|
||||
self.target = target;
|
||||
self.started = Some(now);
|
||||
}
|
||||
let t = if duration.is_zero() {
|
||||
1.0
|
||||
} else {
|
||||
self.started.map_or(1.0, |start| {
|
||||
(now.saturating_duration_since(start).as_secs_f32() / duration.as_secs_f32())
|
||||
.min(1.0)
|
||||
})
|
||||
};
|
||||
self.current = self.start + (self.target - self.start) * t * t * (3.0 - 2.0 * t);
|
||||
if t == 1.0 {
|
||||
self.current = target;
|
||||
self.started = None;
|
||||
}
|
||||
self.current != target
|
||||
}
|
||||
}
|
||||
struct Hover<'a, M> {
|
||||
content: Element<'a, M>,
|
||||
progress: std::rc::Rc<std::cell::Cell<f32>>,
|
||||
duration: std::time::Duration,
|
||||
}
|
||||
impl<M> Widget<M, Theme, cosmic::Renderer> for Hover<'_, M> {
|
||||
fn a11y_nodes(
|
||||
&self,
|
||||
layout: Layout<'_>,
|
||||
tree: &widget::Tree,
|
||||
cursor: mouse::Cursor,
|
||||
) -> iced_accessibility::A11yTree {
|
||||
self.content
|
||||
.as_widget()
|
||||
.a11y_nodes(layout, &tree.children[0], cursor)
|
||||
}
|
||||
fn tag(&self) -> widget::tree::Tag {
|
||||
widget::tree::Tag::of::<HoverState>()
|
||||
}
|
||||
fn state(&self) -> widget::tree::State {
|
||||
widget::tree::State::new(HoverState::default())
|
||||
}
|
||||
fn children(&self) -> Vec<widget::Tree> {
|
||||
vec![widget::Tree::new(&self.content)]
|
||||
}
|
||||
fn diff(&mut self, tree: &mut widget::Tree) {
|
||||
tree.diff_children(std::slice::from_mut(&mut self.content));
|
||||
}
|
||||
fn size(&self) -> Size<Length> {
|
||||
self.content.as_widget().size()
|
||||
}
|
||||
fn layout(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
renderer: &cosmic::Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
self.content
|
||||
.as_widget_mut()
|
||||
.layout(&mut tree.children[0], renderer, limits)
|
||||
}
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &cosmic::Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, M>,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
let now = std::time::Instant::now();
|
||||
let hovered = cursor.is_over(layout.bounds()) && cursor.is_over(*viewport);
|
||||
let state = tree.state.downcast_mut::<HoverState>();
|
||||
if state.advance(if hovered { 1.0 } else { 0.0 }, now, self.duration) {
|
||||
shell.request_redraw_at(now + std::time::Duration::from_millis(16));
|
||||
}
|
||||
self.progress.set(state.current);
|
||||
self.content.as_widget_mut().update(
|
||||
&mut tree.children[0],
|
||||
event,
|
||||
layout,
|
||||
cursor,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &widget::Tree,
|
||||
renderer: &mut cosmic::Renderer,
|
||||
theme: &Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
self.progress
|
||||
.set(tree.state.downcast_ref::<HoverState>().current);
|
||||
self.content.as_widget().draw(
|
||||
&tree.children[0],
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout,
|
||||
cursor,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &widget::Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
renderer: &cosmic::Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.content.as_widget().mouse_interaction(
|
||||
&tree.children[0],
|
||||
layout,
|
||||
cursor,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
}
|
||||
fn operate(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &cosmic::Renderer,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
self.content
|
||||
.as_widget_mut()
|
||||
.operate(&mut tree.children[0], layout, renderer, operation);
|
||||
}
|
||||
fn overlay<'a>(
|
||||
&'a mut self,
|
||||
tree: &'a mut widget::Tree,
|
||||
layout: Layout<'a>,
|
||||
renderer: &cosmic::Renderer,
|
||||
viewport: &Rectangle,
|
||||
translation: Vector,
|
||||
) -> Option<cosmic::iced::advanced::overlay::Element<'a, M, Theme, cosmic::Renderer>> {
|
||||
self.content.as_widget_mut().overlay(
|
||||
&mut tree.children[0],
|
||||
layout,
|
||||
renderer,
|
||||
viewport,
|
||||
translation,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn animated<'a, M: 'a>(
|
||||
content: Element<'a, M>,
|
||||
progress: f32,
|
||||
@@ -181,6 +497,127 @@ impl widget::Operation for CollectRegions {
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn popup_waits_for_configuration_then_opens_and_closes_faster() {
|
||||
let now = std::time::Instant::now();
|
||||
let mut motion = PopupMotion::new(Settings::default());
|
||||
assert_eq!(
|
||||
motion.progress_at(now + std::time::Duration::from_secs(30)),
|
||||
0.0
|
||||
);
|
||||
motion.begin(now);
|
||||
// Later configure/focus notifications must not restart the clock.
|
||||
motion.begin(now + std::time::Duration::from_millis(90));
|
||||
assert!(
|
||||
(motion.progress_at(now + std::time::Duration::from_millis(100)) - 0.5).abs() < 0.00001
|
||||
);
|
||||
assert_eq!(
|
||||
motion.progress_at(now + std::time::Duration::from_millis(200)),
|
||||
1.0
|
||||
);
|
||||
motion.retarget(0.0, now + std::time::Duration::from_millis(200));
|
||||
assert!(motion.closing());
|
||||
assert!(
|
||||
(motion.progress_at(now + std::time::Duration::from_millis(270)) - 0.5).abs() < 0.00001
|
||||
);
|
||||
assert_eq!(
|
||||
motion.progress_at(now + std::time::Duration::from_millis(340)),
|
||||
0.0
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn popup_reversal_and_motion_policy_are_consistent() {
|
||||
let now = std::time::Instant::now();
|
||||
let mut motion = PopupMotion::new(Settings {
|
||||
speed: 0.75,
|
||||
..Settings::default()
|
||||
});
|
||||
motion.begin(now);
|
||||
let reverse = now + std::time::Duration::from_millis(120);
|
||||
let before = motion.progress_at(reverse);
|
||||
motion.retarget(0.0, reverse);
|
||||
assert_eq!(motion.progress_at(reverse), before);
|
||||
let reopen = reverse + std::time::Duration::from_millis(60);
|
||||
let before = motion.progress_at(reopen);
|
||||
motion.retarget(1.0, reopen);
|
||||
assert_eq!(motion.progress_at(reopen), before);
|
||||
assert_eq!(
|
||||
motion.progress_at(reopen + std::time::Duration::from_secs(1)),
|
||||
1.0
|
||||
);
|
||||
for settings in [
|
||||
Settings {
|
||||
reduced_motion: true,
|
||||
..Settings::default()
|
||||
},
|
||||
Settings {
|
||||
enabled: false,
|
||||
..Settings::default()
|
||||
},
|
||||
] {
|
||||
let mut motion = PopupMotion::new(settings);
|
||||
assert_eq!(motion.progress_at(now), 1.0);
|
||||
motion.retarget(0.0, now);
|
||||
assert_eq!(motion.progress_at(now), 0.0);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn hover_fades_settle_and_idle_does_not_schedule_frames() {
|
||||
let now = std::time::Instant::now();
|
||||
let duration = std::time::Duration::from_millis(120);
|
||||
let mut hover = HoverState::default();
|
||||
assert!(!hover.advance(0.0, now, duration));
|
||||
assert!(hover.advance(1.0, now + std::time::Duration::from_secs(10), duration));
|
||||
assert_eq!(hover.current, 0.0);
|
||||
let start = now + std::time::Duration::from_secs(10);
|
||||
assert!(hover.advance(1.0, start + duration / 2, duration));
|
||||
assert!((hover.current - 0.5).abs() < 0.00001);
|
||||
assert!(!hover.advance(1.0, start + duration, duration));
|
||||
assert_eq!(hover.current, 1.0);
|
||||
assert!(!hover.advance(1.0, start + duration * 10, duration));
|
||||
assert!(hover.advance(0.0, start + duration * 11, duration));
|
||||
assert!(!hover.advance(0.0, start + duration * 12, duration));
|
||||
assert_eq!(hover.current, 0.0);
|
||||
}
|
||||
#[test]
|
||||
fn hover_reversals_preserve_current_paint() {
|
||||
let now = std::time::Instant::now();
|
||||
let duration = std::time::Duration::from_millis(120);
|
||||
let mut hover = HoverState::default();
|
||||
hover.advance(1.0, now, duration);
|
||||
hover.advance(1.0, now + duration / 2, duration);
|
||||
let before = hover.current;
|
||||
assert!(hover.advance(0.0, now + duration / 2, duration));
|
||||
assert_eq!(hover.current, before);
|
||||
hover.advance(0.0, now + duration, duration);
|
||||
assert!(hover.current > 0.0 && hover.current < before);
|
||||
}
|
||||
#[test]
|
||||
fn hover_obeys_global_motion_policy() {
|
||||
assert_eq!(
|
||||
Settings {
|
||||
speed: 0.75,
|
||||
..Settings::default()
|
||||
}
|
||||
.hover_duration(),
|
||||
std::time::Duration::from_millis(160)
|
||||
);
|
||||
for settings in [
|
||||
Settings {
|
||||
enabled: false,
|
||||
..Settings::default()
|
||||
},
|
||||
Settings {
|
||||
reduced_motion: true,
|
||||
..Settings::default()
|
||||
},
|
||||
] {
|
||||
assert!(settings.hover_duration().is_zero());
|
||||
let mut hover = HoverState::default();
|
||||
assert!(!hover.advance(1.0, std::time::Instant::now(), settings.hover_duration()));
|
||||
assert_eq!(hover.current, 1.0);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn material_regions_include_cards_but_not_the_parent_or_gaps() {
|
||||
let mut collector = CollectRegions(Vec::new());
|
||||
let id = widget::Id::new("ferese-blur-card");
|
||||
|
||||
Reference in new issue
Block a user