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
@@ -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;
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user