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:
Samuel OnojaandSamuel Onoja authored and GitHub committed 2026-10-12 22:52:25 +01:00
1 parent 7a7df0106c
commit 6b0d903ad0
107 files changed
+9340 -291

No files matched your search

+11 -7
View File
@@ -33,11 +33,11 @@ struct PreviewWindow {
}
impl Ferese {
/// Morph owns the gap on its reserved edge. Keep the layout's outer gap
/// The panel owns the gap on its reserved edge. Keep the layout's outer gap
/// on the other three edges, and keep floating work-area bounds unchanged.
pub(super) fn morph_layout_bounds(&self, mut bounds: Rect, outer: f64) -> Rect {
pub(super) fn panel_layout_bounds(&self, mut bounds: Rect, outer: f64) -> Rect {
use smithay::wayland::shell::wlr_layer::{Anchor, ExclusiveZone};
if self.panel_style != ferese_panel::PanelStyle::Morph || outer <= 0. || !outer.is_finite() {
if outer <= 0. || !outer.is_finite() {
return bounds;
}
let Some(output) = self
@@ -64,7 +64,7 @@ impl Ferese {
};
let top = f64::from(output_geometry.loc.y + geometry.loc.y);
let bottom = top + f64::from(geometry.size.h);
// Only remove a gap if Morph actually borders the usable area.
// Only remove a gap if the panel actually borders the usable area.
// Another reserved bar on this edge must retain its own spacing.
if policy.anchor.contains(Anchor::TOP)
&& !policy.anchor.contains(Anchor::BOTTOM)
@@ -91,7 +91,7 @@ impl Ferese {
} else {
self.gap_config.outer
};
self.morph_layout_bounds(bounds, outer)
self.panel_layout_bounds(bounds, outer)
}
pub(super) fn visible_workspace_ids(&self) -> HashSet<WorkspaceId> {
let mut visible = self
@@ -321,7 +321,7 @@ impl Ferese {
};
let layout_bounds = self.workspace_layout_bounds(bounds, &workspace.layout);
let maximized_bounds = self.morph_layout_bounds(bounds, self.gap_config.outer);
let maximized_bounds = self.panel_layout_bounds(bounds, self.gap_config.outer);
let workspace_fullscreen = workspace.fullscreen;
let is_scrolling_layout = matches!(workspace.layout, WorkspaceLayout::Scrolling(_));
let workspace_focus = workspace.last_focused;
@@ -750,7 +750,7 @@ impl Ferese {
windows.sort_by_key(|w| w.id.0);
let input = PreviewInput {
bounds: self.workspace_layout_bounds(bounds, &workspace.layout),
maximized_bounds: self.morph_layout_bounds(bounds, self.gap_config.outer),
maximized_bounds: self.panel_layout_bounds(bounds, self.gap_config.outer),
fullscreen_bounds: self.full_output_bounds_for(output).unwrap_or(bounds),
gaps: self.gap_config,
focused: workspace.last_focused,
@@ -835,3 +835,7 @@ impl Ferese {
}
}
}
#[cfg(test)]
#[path = "panel_clearance_tests.rs"]
mod panel_clearance_tests;
+7
View File
@@ -97,6 +97,13 @@ impl Ferese {
{
return;
}
let fallback = self
.space
.outputs()
.find(|candidate| *candidate != output)
.map(|output| output.name());
self.activities.store.remove_output(&output.name(), fallback.as_deref());
self.activities.changed();
self.display_presentation.remove_output(output);
self.output_redraw_pending.retain(|pending| pending != output);
if let Some(id) = self.output_ids.get(output) {
@@ -0,0 +1,116 @@
use super::*;
use smithay::output::{Mode, PhysicalProperties, Scale, Subpixel};
use smithay::reexports::calloop::EventLoop;
use smithay::reexports::wayland_protocols_wlr::layer_shell::v1::server::zwlr_layer_shell_v1::ZwlrLayerShellV1;
use smithay::reexports::wayland_server::Resource;
use smithay::reexports::wayland_server::protocol::wl_compositor::WlCompositor;
use smithay::utils::Transform;
use std::io::Write;
use std::os::unix::net::UnixStream;
use std::sync::Arc;
fn request(wire: &mut UnixStream, object: u32, opcode: u32, args: &[u32]) {
let words = [object, (((args.len() + 2) * 4) as u32) << 16 | opcode];
wire.write_all(
&words
.into_iter()
.chain(args.iter().copied())
.flat_map(u32::to_ne_bytes)
.collect::<Vec<_>>(),
)
.unwrap();
}
#[test]
fn every_panel_style_owns_clearance_at_its_reserved_edge() {
if !crate::startup_tests::private_runtime(
"state::layout::panel_clearance_tests::every_panel_style_owns_clearance_at_its_reserved_edge",
) {
return;
}
let mut events = EventLoop::try_new().unwrap();
let mut state = crate::startup_tests::state(&mut events);
let output = Output::new(
"clearance".into(),
PhysicalProperties {
size: (0, 0).into(),
subpixel: Subpixel::Unknown,
make: "test".into(),
model: "test".into(),
},
);
state.begin_desktop_transition();
let changes = state
.publish_desktop(vec![DesktopOutput {
output: output.clone(),
identity: "clearance".into(),
mode: Mode {
size: (800, 600).into(),
refresh: 60_000,
},
transform: Transform::Normal,
scale: Scale::Fractional(1.0),
position: (0, 0).into(),
}])
.unwrap();
state.finish_desktop_transition(changes);
let (mut wire, server) = UnixStream::pair().unwrap();
let client = state
.display_handle
.insert_client(server, Arc::new(ClientState::default()))
.unwrap();
let compositor = client
.create_resource::<WlCompositor, (), Ferese>(&state.display_handle, 6, ())
.unwrap();
let layers = client
.create_resource::<ZwlrLayerShellV1, (), Ferese>(&state.display_handle, 4, ())
.unwrap();
request(&mut wire, compositor.id().protocol_id(), 0, &[2]);
let mut namespace = b"ferese-shell-top-bar\0".to_vec();
let length = namespace.len() as u32;
namespace.resize(namespace.len().div_ceil(4) * 4, 0);
let args = [3, 2, 0, 2, length]
.into_iter()
.chain(
namespace
.chunks_exact(4)
.map(|b| u32::from_ne_bytes(b.try_into().unwrap())),
)
.collect::<Vec<_>>();
request(&mut wire, layers.id().protocol_id(), 0, &args);
request(&mut wire, 3, 0, &[800, 36]);
request(&mut wire, 3, 1, &[1 | 4 | 8]); // Top, left and right.
for style in ferese_panel::PanelStyle::ALL {
state.panel_style = style;
for clearance in [0, 5, 24] {
let mut panel = ferese_panel::Panel::from_preset(style);
panel.geometry.height = Some(36.);
panel.geometry.window_clearance = Some(clearance);
panel.geometry.edge_margin = Some(8);
let geometry = panel.resolved_geometry();
request(&mut wire, 3, 2, &[panel.exclusive_zone(false) as u32]);
request(&mut wire, 3, 3, &[geometry.edge_margin as u32, 0, 0, 0]);
request(&mut wire, 2, 6, &[]);
events.dispatch(Duration::from_millis(1), &mut state).unwrap();
let bounds = state.output_bounds_for(&output).unwrap();
let edge = f64::from(geometry.edge_margin + 36 + clearance);
assert_eq!(bounds.y, edge, "{style:?}, clearance={clearance}");
for outer in [0., 12.] {
let adjusted = state.panel_layout_bounds(bounds, outer);
let window = maximized_rect(adjusted, outer);
assert_eq!(window.y, edge, "{style:?}, clearance={clearance}, outer={outer}");
assert_eq!(window.x, bounds.x + outer);
assert_eq!(window.y + window.height, bounds.y + bounds.height - outer);
}
}
}
request(&mut wire, 3, 2, &[0]);
request(&mut wire, 2, 6, &[]);
events.dispatch(Duration::from_millis(1), &mut state).unwrap();
let bounds = state.output_bounds_for(&output).unwrap();
assert_eq!(
state.panel_layout_bounds(bounds, 12.),
bounds,
"an unreserved panel must not remove gaps"
);
}
+6
View File
@@ -166,6 +166,12 @@ impl Ferese {
pub(crate) fn finish_desktop_transition(&mut self, mut changes: DesktopChanges) {
self.desktop_transition.as_mut().unwrap().publishing = true;
let activity_output = self.space.outputs().next().map(|output| output.name());
if let Some(output) = activity_output
&& self.activities.store.attach_output(&output)
{
self.activities.changed();
}
if changes.layout {
self.relayout_desktop();
}