Flatten nested guards in swipe cleanup, recording updates and popup dismissal. Use an eager optional fallback for workspace layouts and an array in the malformed-opacity test.
286 lines
9.6 KiB
Rust
286 lines
9.6 KiB
Rust
use std::time::Instant;
|
|
|
|
use cosmic::app::Task;
|
|
use cosmic::iced::platform_specific::runtime::wayland::popup::{SctkPopupSettings, SctkPositioner};
|
|
use cosmic::iced::{Limits, Rectangle, window};
|
|
use wayland_client::Proxy;
|
|
|
|
use super::{Menu, OpenMenu};
|
|
use crate::status::Action;
|
|
use crate::{FereseShell, Message};
|
|
|
|
impl OpenMenu {
|
|
fn switch_panel(&mut self, kind: Menu, settings: crate::motion::Settings, now: Instant) {
|
|
self.kind = kind;
|
|
// Keep the Wayland popup and its material binding, but give the new
|
|
// panel its own entrance instead of reusing the settled old panel.
|
|
self.motion = crate::motion::PopupMotion::new(settings);
|
|
self.motion.begin(now);
|
|
}
|
|
}
|
|
|
|
impl FereseShell {
|
|
pub fn open_menu(&mut self, kind: Menu, anchor: Rectangle<i32>) -> Task<Message> {
|
|
if self.menu.as_ref().is_some_and(|menu| menu.kind == kind) {
|
|
if let Some(menu) = &mut self.menu
|
|
&& menu.motion.closing()
|
|
{
|
|
menu.motion.retarget(1.0, Instant::now());
|
|
return Task::none();
|
|
}
|
|
|
|
return self.close_menu();
|
|
}
|
|
|
|
if kind == Menu::Calendar {
|
|
self.calendar_offset = 0;
|
|
}
|
|
|
|
if !kind.available(&self.status) {
|
|
return Task::none();
|
|
}
|
|
if self.menu.as_ref().is_some_and(|menu| menu.kind == Menu::Notifications) {
|
|
self.notifications.history_open = false;
|
|
self.notifications.hovered = None;
|
|
}
|
|
|
|
if kind == Menu::Notifications && self.notifications.ready {
|
|
self.notifications.toggle_history();
|
|
}
|
|
|
|
let notifications = self.sync_notification_surface();
|
|
crate::EFFECT_FRAME_PENDING.store(true, std::sync::atomic::Ordering::Relaxed);
|
|
self.status_error = None;
|
|
let parent = self.bar_surface_id;
|
|
let anchor = if kind == Menu::System {
|
|
self.outputs
|
|
.iter()
|
|
.find(|output| output.bar == parent)
|
|
.and_then(|output| output.size)
|
|
.map(|(width, _)| {
|
|
let theme = self.config.theme;
|
|
Rectangle {
|
|
x: (width - theme.bar_margin_horizontal * 2 - theme.panel_padding.round() as i32 - 1).max(0),
|
|
y: 0,
|
|
width: 1,
|
|
height: theme.bar_height.round() as i32,
|
|
}
|
|
})
|
|
.unwrap_or(anchor)
|
|
} else {
|
|
anchor
|
|
};
|
|
let height_limit = if kind == Menu::Notifications {
|
|
self.notification_history_height_limit()
|
|
} else {
|
|
kind.height_limit()
|
|
};
|
|
let positioner = SctkPositioner {
|
|
anchor_rect: anchor,
|
|
anchor: 2u32.try_into().unwrap(),
|
|
gravity: 6u32.try_into().unwrap(),
|
|
offset: (anchor.width / 2, 8),
|
|
// The view's autosize widget supplies the active panel's bounds.
|
|
// Keep initial popup limits broad enough for later panel changes.
|
|
size_limits: Limits::NONE.max_width(368.0).max_height(720.0_f32.max(height_limit)),
|
|
constraint_adjustment: 3,
|
|
..Default::default()
|
|
};
|
|
if let Some(menu) = &mut self.menu {
|
|
menu.switch_panel(kind, self.config.animations, Instant::now());
|
|
return Task::batch([
|
|
notifications,
|
|
cosmic::iced::platform_specific::shell::commands::popup::reposition(menu.id, positioner),
|
|
]);
|
|
}
|
|
let id = window::Id::unique();
|
|
self.menu = Some(OpenMenu {
|
|
id,
|
|
kind,
|
|
motion: crate::motion::PopupMotion::new(self.config.animations),
|
|
effects: None,
|
|
regions: Default::default(),
|
|
});
|
|
let action = cosmic::surface::action::app_popup::<Self>(
|
|
|_| Default::default(),
|
|
move |_| SctkPopupSettings {
|
|
id,
|
|
parent,
|
|
parent_size: None,
|
|
grab: true,
|
|
close_with_children: false,
|
|
input_zone: None,
|
|
positioner: positioner.clone(),
|
|
},
|
|
Some(Box::new(Self::view_status_menu)),
|
|
);
|
|
notifications.chain(cosmic::task::message(cosmic::Action::Surface(action)))
|
|
}
|
|
|
|
pub fn destroy_menu(&mut self) -> Task<Message> {
|
|
crate::EFFECT_FRAME_PENDING.store(false, std::sync::atomic::Ordering::Relaxed);
|
|
let Some(menu) = self.menu.take() else {
|
|
return Task::none();
|
|
};
|
|
if menu.kind == Menu::Notifications {
|
|
self.notifications.history_open = false;
|
|
self.notifications.hovered = None;
|
|
}
|
|
Task::batch([
|
|
cosmic::task::message(cosmic::Action::Surface(cosmic::surface::action::destroy_popup(menu.id))),
|
|
self.sync_notification_surface(),
|
|
])
|
|
}
|
|
|
|
pub fn close_menu(&mut self) -> Task<Message> {
|
|
let Some(menu) = &mut self.menu else {
|
|
return Task::none();
|
|
};
|
|
if menu.motion.closing() {
|
|
return Task::none();
|
|
}
|
|
if menu
|
|
.effects
|
|
.as_ref()
|
|
.is_some_and(|effects| effects.surface.version() >= 3)
|
|
{
|
|
menu.motion.retarget(0.0, Instant::now());
|
|
if menu.animating() {
|
|
return Task::none();
|
|
}
|
|
}
|
|
self.destroy_menu()
|
|
}
|
|
|
|
pub fn animate_menu(&mut self) -> Task<Message> {
|
|
if let Some(menu) = &self.menu
|
|
&& menu.motion.closing()
|
|
&& !menu.animating()
|
|
{
|
|
return self.destroy_menu();
|
|
}
|
|
Task::none()
|
|
}
|
|
|
|
pub fn optimistic_status(&mut self, action: &Action) {
|
|
match action {
|
|
Action::Volume(v) => {
|
|
if let Some(a) = &mut self.status.audio {
|
|
a.volume = *v;
|
|
}
|
|
}
|
|
Action::Mute(v) => {
|
|
if let Some(a) = &mut self.status.audio {
|
|
a.muted = *v;
|
|
}
|
|
}
|
|
Action::Brightness(v) => self.status.brightness = Some(*v),
|
|
Action::Wifi(v) => {
|
|
if let Some(n) = &mut self.status.network {
|
|
n.enabled = *v;
|
|
}
|
|
}
|
|
Action::Bluetooth(v) => {
|
|
if let Some(b) = &mut self.status.bluetooth {
|
|
b.enabled = *v;
|
|
}
|
|
}
|
|
Action::Dnd(v) => {
|
|
if let Some(n) = &mut self.status.notifications {
|
|
n.dnd = *v;
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::motion::{PopupMotion, Settings};
|
|
use std::time::Duration;
|
|
|
|
#[test]
|
|
fn switching_panels_replays_entrance_on_the_same_surface() {
|
|
let now = Instant::now();
|
|
let settings = Settings::default();
|
|
let mut menu = OpenMenu {
|
|
id: window::Id::unique(),
|
|
kind: Menu::Battery,
|
|
motion: PopupMotion::new(settings),
|
|
effects: None,
|
|
regions: Default::default(),
|
|
};
|
|
let id = menu.id;
|
|
let regions = menu.regions.clone();
|
|
menu.motion.begin(now);
|
|
for (index, kind) in [
|
|
Menu::Network,
|
|
Menu::Bluetooth,
|
|
Menu::Audio,
|
|
Menu::Calendar,
|
|
Menu::Recording,
|
|
Menu::Notifications,
|
|
Menu::System,
|
|
Menu::Battery,
|
|
]
|
|
.into_iter()
|
|
.enumerate()
|
|
{
|
|
let at = now + Duration::from_secs(10 * (index as u64 + 1));
|
|
assert_eq!(menu.motion.progress_at(at), 1.0);
|
|
menu.switch_panel(kind, settings, at);
|
|
assert_eq!(menu.kind, kind);
|
|
assert_eq!(menu.id, id);
|
|
assert!(std::sync::Arc::ptr_eq(®ions, &menu.regions));
|
|
assert_eq!(menu.motion.progress_at(at), 0.0);
|
|
assert!(menu.motion.frame_active(at));
|
|
let progress = menu.motion.progress_at(at + Duration::from_millis(30));
|
|
assert!(progress > 0.0 && progress < 1.0);
|
|
}
|
|
let at = now + Duration::from_secs(90);
|
|
menu.motion.retarget(0.0, at);
|
|
menu.switch_panel(Menu::Network, settings, at + Duration::from_millis(30));
|
|
assert!(!menu.motion.closing());
|
|
assert!(menu.motion.frame_active(at + Duration::from_millis(30)));
|
|
}
|
|
|
|
#[test]
|
|
fn panel_switch_respects_reduced_motion_and_speed() {
|
|
let now = Instant::now();
|
|
let mut menu = OpenMenu {
|
|
id: window::Id::unique(),
|
|
kind: Menu::Battery,
|
|
motion: PopupMotion::new(Settings::default()),
|
|
effects: None,
|
|
regions: Default::default(),
|
|
};
|
|
for settings in [
|
|
Settings {
|
|
reduced_motion: true,
|
|
..Default::default()
|
|
},
|
|
Settings {
|
|
enabled: false,
|
|
..Default::default()
|
|
},
|
|
] {
|
|
menu.switch_panel(Menu::Network, settings, now);
|
|
assert_eq!(menu.motion.progress_at(now), 1.0);
|
|
assert!(!menu.motion.frame_active(now));
|
|
}
|
|
menu.switch_panel(Menu::Battery, Settings::default(), now);
|
|
let normal = menu.motion.progress_at(now + Duration::from_millis(30));
|
|
menu.switch_panel(
|
|
Menu::Network,
|
|
Settings {
|
|
speed: 0.5,
|
|
..Default::default()
|
|
},
|
|
now,
|
|
);
|
|
assert_eq!(menu.motion.progress_at(now + Duration::from_millis(60)), normal);
|
|
}
|
|
}
|