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
@@ -43,7 +43,7 @@ pub struct Display {
|
||||
}
|
||||
|
||||
pub fn load() -> Result<Vec<Display>, String> {
|
||||
let mut connection = ferese_ipc::theme::Connection::connect().map_err(|error| error.to_string())?;
|
||||
let mut connection = ferese_ipc::Connection::connect().map_err(|error| error.to_string())?;
|
||||
let value = connection.call("get-outputs", json!({}))?;
|
||||
let values = value.as_array().ok_or("Invalid display response")?;
|
||||
Ok(values
|
||||
|
||||
@@ -800,7 +800,7 @@ impl cosmic::Application for App {
|
||||
suffix += 1;
|
||||
}
|
||||
snapshot.edit(&set(&format!("theme.custom_themes.{id}.file"), path.as_str()))?;
|
||||
let mut connection = ferese_ipc::theme::Connection::connect().map_err(|e| e.to_string())?;
|
||||
let mut connection = ferese_ipc::Connection::connect().map_err(|e| e.to_string())?;
|
||||
let value = connection.call(
|
||||
"theme-preview",
|
||||
serde_json::json!({"source": snapshot.source, "directory": directory}),
|
||||
|
||||
@@ -34,13 +34,14 @@ impl std::fmt::Display for StyleChoice {
|
||||
}
|
||||
}
|
||||
|
||||
const CATALOG: [CatalogItem; 12] = [
|
||||
const CATALOG: [CatalogItem; 13] = [
|
||||
CatalogItem(ItemKind::Overview),
|
||||
CatalogItem(ItemKind::Workspaces {
|
||||
style: ferese_panel::WorkspaceStyle::Dots,
|
||||
}),
|
||||
CatalogItem(ItemKind::FocusedWindow),
|
||||
CatalogItem(ItemKind::Media),
|
||||
CatalogItem(ItemKind::Activities),
|
||||
CatalogItem(ItemKind::QuickSettings),
|
||||
CatalogItem(ItemKind::Network),
|
||||
CatalogItem(ItemKind::Audio),
|
||||
@@ -197,7 +198,45 @@ impl App {
|
||||
.into(),
|
||||
),
|
||||
);
|
||||
let maximum = self.draft.number("activities.max_visible", 2.) as u64;
|
||||
let choices = widget::row([1_u64, 2].into_iter().map(|count| {
|
||||
widget::button::custom(self.label(count.to_string(), 12.))
|
||||
.name(format!("Show up to {count} activities"))
|
||||
.padding([8, 10])
|
||||
.width(cosmic::iced::Length::Fill)
|
||||
.class(crate::visuals::panel_button(palette, count == maximum))
|
||||
.on_press_maybe(
|
||||
(count != maximum)
|
||||
.then(|| Message::Change(store::set("activities.max_visible", count))),
|
||||
)
|
||||
.into()
|
||||
}))
|
||||
.spacing(2);
|
||||
appearance = appearance.push(self.panel_divider()).push(
|
||||
self.panel_appearance_row(
|
||||
"Visible activities",
|
||||
"Maximum activities in the collapsed and peek island.",
|
||||
widget::container(choices)
|
||||
.padding(3)
|
||||
.class(crate::visuals::surface(palette.sidebar, 9.))
|
||||
.into(),
|
||||
),
|
||||
);
|
||||
}
|
||||
let show_badge = self.draft.boolean("activities.show_badge", true);
|
||||
appearance = appearance.push(self.panel_divider()).push(
|
||||
self.panel_appearance_row(
|
||||
"Show activity count",
|
||||
"Show +n for additional activities.",
|
||||
widget::container(
|
||||
ferese_theme::controls::switch(show_badge, palette)
|
||||
.name("Show activity count")
|
||||
.on_press(Message::Change(store::set("activities.show_badge", !show_badge))),
|
||||
)
|
||||
.align_right(cosmic::iced::Length::Fill)
|
||||
.into(),
|
||||
),
|
||||
);
|
||||
rows = rows.push(
|
||||
widget::container(appearance)
|
||||
.id("panel-appearance-settings")
|
||||
@@ -327,6 +366,18 @@ impl App {
|
||||
|
||||
fn panel_opacity_controls(&self) -> Element<'static, Message> {
|
||||
use cosmic::iced::{Alignment, Length};
|
||||
if self.draft.boolean("theme.accessibility.reduce_transparency", false) {
|
||||
return self.panel_appearance_row(
|
||||
"Background opacity",
|
||||
"Reduce transparency is on. Turn it off in Accessibility to change opacity.",
|
||||
self.settings_button(
|
||||
"Accessibility",
|
||||
"M9 18l6-6-6-6",
|
||||
Some(Message::Page(Page::Accessibility)),
|
||||
false,
|
||||
),
|
||||
);
|
||||
}
|
||||
let palette = crate::visuals::Palette::from_resolved(&self.resolved.presented);
|
||||
let tokens = &self.resolved.presented.tokens;
|
||||
let inherited = if tokens.material.style == "translucent" {
|
||||
@@ -809,6 +860,7 @@ pub(super) fn item_icon(kind: ItemKind, tint: cosmic::iced::Color) -> widget::ic
|
||||
ItemKind::Workspaces { .. } => icons::OVERVIEW,
|
||||
ItemKind::FocusedWindow => icons::DISPLAY,
|
||||
ItemKind::Media => icons::MEDIA,
|
||||
ItemKind::Activities => icons::FERESE,
|
||||
ItemKind::QuickSettings => icons::CONTROL_CENTER,
|
||||
ItemKind::Network => icons::WIFI_FULL,
|
||||
ItemKind::Audio => icons::VOLUME_HIGH,
|
||||
|
||||
@@ -7,6 +7,7 @@ use ferese_theme::panel::{Sample, Spacing, frame};
|
||||
use crate::{App, Element, Message, panel_controls, visuals};
|
||||
|
||||
const SAMPLE_AVAILABILITY: Availability = Availability {
|
||||
activities: true,
|
||||
media: true,
|
||||
network: true,
|
||||
audio: true,
|
||||
@@ -19,6 +20,18 @@ fn available(item: &Item) -> bool {
|
||||
item.available(SAMPLE_AVAILABILITY)
|
||||
}
|
||||
|
||||
fn panel_background(theme: &ferese_config::theme::ResolvedTheme, panel: &Panel) -> cosmic::iced::Color {
|
||||
let fill = if panel.style == ferese_panel::PanelStyle::Morph {
|
||||
&theme.tokens.colors.surface_raised
|
||||
} else {
|
||||
&theme.tokens.surface.bar.background
|
||||
};
|
||||
cosmic::iced::Color {
|
||||
a: theme.panel_opacity(panel.background_opacity),
|
||||
..visuals::color(fill, cosmic::iced::Color::TRANSPARENT)
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub(super) fn panel_preview(&self) -> Element<'_, Message> {
|
||||
let panel = match self.preview_panel() {
|
||||
@@ -115,14 +128,7 @@ impl App {
|
||||
groups: Vec::new(),
|
||||
scale: self.render_scale,
|
||||
high_contrast: palette.high_contrast,
|
||||
background: cosmic::iced::Color {
|
||||
a: if self.resolved.presented.accessibility.reduce_transparency {
|
||||
1.
|
||||
} else {
|
||||
panel.background_opacity.unwrap_or(palette.card.a)
|
||||
},
|
||||
..palette.card
|
||||
},
|
||||
background: panel_background(&self.resolved.presented, &panel),
|
||||
border: if panel.border {
|
||||
visuals::color(
|
||||
&self.resolved.presented.tokens.colors.border,
|
||||
@@ -194,7 +200,7 @@ impl App {
|
||||
ferese_panel::shape::ZonePosition::End,
|
||||
)))
|
||||
.collect();
|
||||
let opacity = self.resolved.presented.panel_opacity(panel.background_opacity);
|
||||
let background = panel_background(&self.resolved.presented, &panel);
|
||||
let height = panel.resolved_geometry().height;
|
||||
let padding = panel.resolved_geometry().inner_padding;
|
||||
let border = panel.border;
|
||||
@@ -265,10 +271,7 @@ impl App {
|
||||
groups,
|
||||
scale: self.render_scale,
|
||||
high_contrast: palette.high_contrast,
|
||||
background: cosmic::iced::Color {
|
||||
a: opacity,
|
||||
..palette.card
|
||||
},
|
||||
background,
|
||||
border: if border {
|
||||
visuals::color(
|
||||
&self.resolved.presented.tokens.colors.border,
|
||||
@@ -367,6 +370,7 @@ impl App {
|
||||
ItemKind::Workspaces { .. } => icons::OVERVIEW,
|
||||
ItemKind::FocusedWindow => icons::DISPLAY,
|
||||
ItemKind::Media => icons::MEDIA,
|
||||
ItemKind::Activities => icons::FERESE,
|
||||
ItemKind::QuickSettings => icons::CONTROL_CENTER,
|
||||
ItemKind::Network => icons::WIFI_FULL,
|
||||
ItemKind::Audio => icons::VOLUME_HIGH,
|
||||
@@ -380,6 +384,14 @@ impl App {
|
||||
let label = match (item.kind, representation) {
|
||||
(ItemKind::Workspaces { .. }, _) => "1 2 3",
|
||||
(ItemKind::FocusedWindow, _) => "Settings",
|
||||
(ItemKind::Activities, Representation::Wide) => {
|
||||
if self.draft.boolean("activities.show_badge", true) {
|
||||
"Build · 62% +1"
|
||||
} else {
|
||||
"Build · 62%"
|
||||
}
|
||||
}
|
||||
(ItemKind::Activities, Representation::Compact) => "Build",
|
||||
(ItemKind::Media, Representation::Wide) => "Song title · Artist ▷",
|
||||
(ItemKind::Media, Representation::Compact) => "Song title",
|
||||
(ItemKind::Battery { percentage: true }, Representation::Wide) => "75%",
|
||||
@@ -478,6 +490,37 @@ mod tests {
|
||||
use cosmic::iced::advanced::{Layout, Shell, clipboard, layout, mouse, renderer::Headless, widget};
|
||||
use cosmic::iced::{Event, Font, Pixels, Point, Rectangle, Size};
|
||||
|
||||
#[test]
|
||||
fn preview_uses_bar_tokens_and_inherited_opacity_for_every_available_style() {
|
||||
let mut theme = ferese_config::theme::default_theme();
|
||||
theme.tokens.colors.surface_raised = "#112233".into();
|
||||
theme.tokens.surface.bar.background = "#abcdef".into();
|
||||
theme.tokens.material.style = "translucent".into();
|
||||
theme.tokens.material.opacity = 0.35;
|
||||
for style in ferese_panel::PanelStyle::AVAILABLE {
|
||||
let mut panel = Panel::from_preset(style);
|
||||
let expected = if style == ferese_panel::PanelStyle::Morph {
|
||||
cosmic::iced::Color::from_rgb8(0x11, 0x22, 0x33)
|
||||
} else {
|
||||
cosmic::iced::Color::from_rgb8(0xab, 0xcd, 0xef)
|
||||
};
|
||||
for (override_opacity, opacity) in [(None, 0.35), (Some(0.), 0.), (Some(0.7), 0.7)] {
|
||||
panel.background_opacity = override_opacity;
|
||||
for reduce in [false, true] {
|
||||
theme.accessibility.reduce_transparency = reduce;
|
||||
assert_eq!(
|
||||
panel_background(&theme, &panel),
|
||||
cosmic::iced::Color {
|
||||
a: if reduce { 1. } else { opacity },
|
||||
..expected
|
||||
},
|
||||
"{style:?}, opacity={override_opacity:?}, reduce={reduce}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Bounds {
|
||||
targets: Vec<(widget::Id, ItemId)>,
|
||||
|
||||
@@ -232,6 +232,33 @@ pub fn reload_running() -> bool {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn activity_display_settings_roundtrip_without_changing_rules() {
|
||||
for source in [
|
||||
"",
|
||||
"// keep this rule\nactivities { rule cargo { match \"^cargo\"; title Custom; }; }\n",
|
||||
] {
|
||||
let mut snapshot = Snapshot::parse(source.into()).unwrap();
|
||||
let rules = snapshot.doc.value()["activities"]["rules"].clone();
|
||||
snapshot.edit(&set("activities.max_visible", 1)).unwrap();
|
||||
snapshot.edit(&set("activities.show_badge", false)).unwrap();
|
||||
let loaded = Snapshot::parse(snapshot.source.clone()).unwrap();
|
||||
let settings: ferese_config::activities::Settings =
|
||||
serde_json::from_value(loaded.doc.value()["activities"].clone()).unwrap();
|
||||
assert_eq!(settings.limits().max_visible, 1);
|
||||
assert!(!settings.limits().show_badge);
|
||||
if !source.is_empty() {
|
||||
assert_eq!(loaded.doc.value()["activities"]["rules"], rules);
|
||||
assert!(loaded.source.contains("// keep this rule"));
|
||||
}
|
||||
snapshot.edit(&set("activities.max_visible", 2)).unwrap();
|
||||
snapshot.edit(&set("activities.show_badge", true)).unwrap();
|
||||
let loaded = Snapshot::parse(snapshot.source).unwrap();
|
||||
assert_eq!(loaded.number("activities.max_visible", 0.), 2.);
|
||||
assert!(loaded.boolean("activities.show_badge", false));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edits_preserve_comments_custom_keys_and_other_rules() {
|
||||
let source = r#"// personal theme
|
||||
|
||||
Reference in new issue
Block a user