Add Live Activities as configurable bar items (#30)
* Add Live Activities as configurable bar items * Fix activity owner monitoring and Morph list interactions * Keep activity rows stable during updates and tighten their layout * Use subtle theme tint for activity cards * Tighten activity title and progress spacing * Use theme accent for active progress rings * Fit compact activities to measured titles and badge * Expose Live Activities display controls in Settings * Open activity terminals and streamline the Morph header * Give Morph continuous icons and prioritize workspace navigation * Show activity progress inside its ring * Inset activity progress rings and strengthen their stroke * Fix activity lifecycle, command output handling and list navigation * Reduce activity update work and harden publisher lifetimes * Set up Live Activities for the detected user shell during installation * Document Activities setup, daily use and troubleshooting * Preserve Morph idle width across empty activity updates * Fix dimming flicker, hide inactive music tab and consolidate Activities docs * Fix panel clearance across styles and match previews to theme settings * Prevent panel blur from repainting neighboring islands --------- Co-authored-by: Samuel Onoja <[email protected]>
This commit is contained in:
1 parent
7a7df0106c
commit
6b0d903ad0
107 files changed
+9340
-291
No files matched your search
@@ -163,6 +163,7 @@ fn status_label(kind: Menu, s: &Snapshot) -> String {
|
||||
Some(n) => format!("Notifications: {} unread", n.count),
|
||||
_ => "Notifications: unavailable".into(),
|
||||
},
|
||||
Menu::Activities => "Activities".into(),
|
||||
Menu::Workspaces => "Workspaces".into(),
|
||||
Menu::Calendar => "Calendar".into(),
|
||||
Menu::Overflow => "More panel items".into(),
|
||||
@@ -243,6 +244,7 @@ pub(super) fn status_icon(kind: Menu, s: &Snapshot) -> (&'static [u8], bool) {
|
||||
}
|
||||
}
|
||||
Menu::Workspaces => (ferese_theme::icons::OVERVIEW, true),
|
||||
Menu::Activities => (ferese_theme::icons::FERESE, true),
|
||||
Menu::System => (ferese_theme::icons::CONTROL_CENTER, true),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,7 @@ impl FereseShell {
|
||||
|
||||
if kind == Menu::Notifications && self.notifications.ready {
|
||||
self.notifications.toggle_history();
|
||||
self.sync_activity_presentations();
|
||||
}
|
||||
|
||||
let notifications = self.sync_notification_surface();
|
||||
|
||||
@@ -32,6 +32,7 @@ struct MenuStyle {
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Menu {
|
||||
Activities,
|
||||
Workspaces,
|
||||
Network,
|
||||
Bluetooth,
|
||||
@@ -75,6 +76,7 @@ impl Menu {
|
||||
|
||||
fn title(self) -> &'static str {
|
||||
match self {
|
||||
Self::Activities => "Activities",
|
||||
Self::Workspaces => "Workspaces",
|
||||
Self::System => "Control Center",
|
||||
Self::Overflow => "Panel items",
|
||||
@@ -97,7 +99,7 @@ impl Menu {
|
||||
Self::Battery => status.battery.is_some(),
|
||||
Self::Calendar | Self::Recording | Self::Media => true,
|
||||
Self::Notifications => status.notifications.is_some(),
|
||||
Self::System | Self::Overflow | Self::Workspaces => true,
|
||||
Self::System | Self::Overflow | Self::Workspaces | Self::Activities => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -281,7 +283,7 @@ impl FereseShell {
|
||||
style.primary,
|
||||
opacity,
|
||||
)),
|
||||
Menu::Overflow | Menu::Workspaces => rows,
|
||||
Menu::Activities | Menu::Overflow | Menu::Workspaces => rows,
|
||||
};
|
||||
if !kind.available(&self.status) {
|
||||
rows = rows.push(
|
||||
@@ -389,6 +391,7 @@ impl FereseShell {
|
||||
Menu::Battery => battery::view(self, rows, style),
|
||||
Menu::Calendar => rows.push(calendar::view(self.render, self.calendar_offset, theme, p)),
|
||||
Menu::Notifications => notification_controls(self, rows, style, false),
|
||||
Menu::Activities => rows.push(self.activity_list(menu.anchor.parent, p)),
|
||||
Menu::Recording | Menu::Workspaces => rows,
|
||||
Menu::Overflow => self.view_panel_overflow(rows),
|
||||
Menu::Media => media::view(self, rows, style),
|
||||
@@ -414,7 +417,7 @@ impl FereseShell {
|
||||
}
|
||||
|
||||
let compositor_material = menu.effects.is_some();
|
||||
let body: Element<'_, cosmic::Action<Message>> = if kind == Menu::Overflow {
|
||||
let body: Element<'_, cosmic::Action<Message>> = if matches!(kind, Menu::Overflow | Menu::Activities) {
|
||||
cosmic::widget::scrollable(rows).height(Length::Shrink).into()
|
||||
} else {
|
||||
rows.into()
|
||||
@@ -510,6 +513,131 @@ fn notification_controls<'a>(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn activity_popups_scroll_to_the_last_row_in_regular_panels() {
|
||||
use cosmic::iced::advanced::{Layout, Shell, clipboard, layout, renderer::Headless, widget};
|
||||
use cosmic::iced::{Event, Rectangle, Size, Vector, mouse};
|
||||
use ferese_activities::{Data, Owner, Source, Store, Template, Value};
|
||||
#[derive(Default)]
|
||||
struct ScrollBounds(Option<(Rectangle, Rectangle, Vector)>);
|
||||
impl widget::Operation for ScrollBounds {
|
||||
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn widget::Operation)) {
|
||||
visit(self);
|
||||
}
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
_: Option<&widget::Id>,
|
||||
bounds: Rectangle,
|
||||
content: Rectangle,
|
||||
translation: Vector,
|
||||
_: &mut dyn widget::operation::Scrollable,
|
||||
) {
|
||||
self.0 = Some((bounds, content, translation));
|
||||
}
|
||||
}
|
||||
let renderer = cosmic::iced::futures::executor::block_on(cosmic::Renderer::new(
|
||||
cosmic::iced::Font::default(),
|
||||
cosmic::iced::Pixels(14.),
|
||||
Some("tiny-skia"),
|
||||
))
|
||||
.unwrap();
|
||||
for style in [
|
||||
ferese_panel::PanelStyle::Flat,
|
||||
ferese_panel::PanelStyle::Floating,
|
||||
ferese_panel::PanelStyle::Adaptive,
|
||||
] {
|
||||
let mut app = crate::tests::shell_with_measured_panel();
|
||||
let bar = app.outputs[0].bar;
|
||||
app.config.panels[0].style = style;
|
||||
app.snapshot.outputs = vec![crate::control::OutputSnapshot {
|
||||
id: 1,
|
||||
name: "test".into(),
|
||||
active_workspace: 1,
|
||||
focused: true,
|
||||
}];
|
||||
app.outputs[0]
|
||||
.runtime
|
||||
.reset(&app.config.panels[0], &app.snapshot, Some("test"));
|
||||
let mut store = Store::new(1);
|
||||
for n in 0..24 {
|
||||
store
|
||||
.start(
|
||||
Owner { pid: n + 1, birth: 1 },
|
||||
n.to_string(),
|
||||
Data {
|
||||
source: Source::Publisher,
|
||||
output: Some("test".into()),
|
||||
priority: 100,
|
||||
icon: "hammer".into(),
|
||||
title: format!("Task {n}"),
|
||||
value: Value::Indeterminate,
|
||||
template: Template::Progress,
|
||||
},
|
||||
0,
|
||||
0,
|
||||
None,
|
||||
true,
|
||||
true,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
app.activities.snapshot = std::sync::Arc::new(ferese_ipc::activities::Snapshot {
|
||||
activities: store.entries().cloned().collect(),
|
||||
..Default::default()
|
||||
});
|
||||
app.sync_activity_presentations();
|
||||
drop(app.open_menu(
|
||||
Menu::Activities,
|
||||
PopoverAnchor {
|
||||
parent: bar,
|
||||
panel: app.config.panels[0].id.clone(),
|
||||
item: None,
|
||||
rectangle: Rectangle {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 100,
|
||||
height: 28,
|
||||
},
|
||||
},
|
||||
));
|
||||
let mut view = app.view_status_menu_at(Instant::now() + Duration::from_secs(1));
|
||||
let mut tree = widget::Tree::new(&view);
|
||||
let node = view.as_widget_mut().layout(
|
||||
&mut tree,
|
||||
&renderer,
|
||||
&layout::Limits::new(Size::ZERO, Size::new(300., 500.)),
|
||||
);
|
||||
let mut before = ScrollBounds::default();
|
||||
view.as_widget_mut()
|
||||
.operate(&mut tree, Layout::new(&node), &renderer, &mut before);
|
||||
let (viewport, content, _) = before.0.expect("activity popup must be scrollable");
|
||||
assert!(content.height > viewport.height);
|
||||
let mut messages = Vec::new();
|
||||
view.as_widget_mut().update(
|
||||
&mut tree,
|
||||
&Event::Mouse(mouse::Event::WheelScrolled {
|
||||
delta: mouse::ScrollDelta::Lines { x: 0., y: -1000. },
|
||||
}),
|
||||
Layout::new(&node),
|
||||
mouse::Cursor::Available(viewport.center()),
|
||||
&renderer,
|
||||
&mut clipboard::Null,
|
||||
&mut Shell::new(&mut messages),
|
||||
&node.bounds(),
|
||||
);
|
||||
let mut after = ScrollBounds::default();
|
||||
view.as_widget_mut()
|
||||
.operate(&mut tree, Layout::new(&node), &renderer, &mut after);
|
||||
let (viewport, content, translation) = after.0.unwrap();
|
||||
assert!(translation.y > 0., "{style:?} did not scroll");
|
||||
assert!(
|
||||
(translation.y - (content.height - viewport.height)).abs() < 1.,
|
||||
"last row must be reachable"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_controls_only_show_errors_in_related_popups() {
|
||||
for (action, relevant) in [
|
||||
|
||||
Reference in new issue
Block a user