Refine shell status icons and window controls; add installer

This commit is contained in:
Samuel Onoja committed 2026-09-26 05:46:15 +01:00
1 parent 956c739940
commit 686b0477d4
28 files changed
+527 -155

No files matched your search

+139 -33
View File
@@ -152,33 +152,26 @@ impl FereseShell {
let metrics = BarMetrics::from(theme);
let mut controls = row::with_capacity(6).spacing(3).align_y(Alignment::Center);
for kind in [
Menu::System,
Menu::Network,
Menu::Bluetooth,
Menu::Audio,
Menu::Battery,
Menu::Notifications,
Menu::System,
Menu::Battery,
] {
if !kind.available(&self.status) {
continue;
}
let (source, active) = status_icon(kind, &self.status);
let warning = kind == Menu::Battery
&& self.status.battery.as_ref().is_some_and(|b| {
b.percent < self.config.status.low_battery_threshold && b.status != "Charging"
});
let foreground = if warning {
Color::from_rgb8(245, 193, 105)
} else {
color(if active {
theme.text_primary
} else {
theme.text_muted
})
};
let mut content = row![bar_icon(source, metrics.icon_size, foreground)]
.spacing(4)
.align_y(Alignment::Center);
let (source, _) = status_icon(kind, &self.status);
let foreground = color(theme.text_primary);
let mut content = row![accented_icon(
source,
metrics.icon_size,
foreground,
color(theme.accent)
)]
.spacing(4)
.align_y(Alignment::Center);
if kind == Menu::Battery && self.config.status.battery_percentage {
if let Some(battery) = &self.status.battery {
content = content.push(
@@ -210,11 +203,16 @@ impl FereseShell {
// A fixed button height does not center its child in libcosmic.
// Keep the visual content centered inside the entire click target.
let content = container(content)
.height(metrics.control_height)
.align_x(alignment::Horizontal::Center)
.height(metrics.height)
.align_y(alignment::Vertical::Center);
let control = button::custom(content)
.padding([0, 7])
.height(metrics.control_height)
.name(status_label(kind, &self.status))
.padding([
0.0,
((metrics.height - f32::from(metrics.icon_size)) * 0.5).max(7.0),
])
.height(metrics.height)
.class(button_style(foreground, selected, 1.0))
.on_press_with_rectangle(move |offset, bounds| {
cosmic::Action::App(Message::OpenMenu(
@@ -365,6 +363,7 @@ impl FereseShell {
},
primary,
muted,
color_with_opacity(theme.accent, p),
p,
));
if connected {
@@ -408,6 +407,7 @@ impl FereseShell {
},
primary,
muted,
color_with_opacity(theme.accent, p),
p,
));
if b.enabled && !b.devices.is_empty() {
@@ -439,7 +439,14 @@ impl FereseShell {
]
.align_y(Alignment::Center),
text(&a.output).size(12).class(theme::Text::Color(muted)),
slider_row(audio_icon(a.volume, a.muted), a.volume, false, primary, p,)
slider_row(
audio_icon(a.volume, a.muted),
a.volume,
false,
primary,
color_with_opacity(theme.accent, p),
p,
)
]
.spacing(4);
rows = rows.push(control_card(audio.into(), primary, p));
@@ -454,6 +461,7 @@ impl FereseShell {
value,
true,
primary,
color_with_opacity(theme.accent, p),
p,
)
]
@@ -502,7 +510,12 @@ impl FereseShell {
container(
row![
text(format!("{}%", b.percent)).size(40).width(Length::Fill),
symbolic_icon(status_icon(Menu::Battery, &self.status).0, 40),
accented_icon(
status_icon(Menu::Battery, &self.status).0,
40,
battery_color,
color_with_opacity(theme.accent, p),
),
]
.align_y(Alignment::Center),
)
@@ -639,9 +652,10 @@ fn status_summary<'a>(
subtitle: &'a str,
primary: Color,
muted: Color,
accent: Color,
opacity: f32,
) -> Element<'a, cosmic::Action<Message>> {
let badge = container(symbolic_icon(source, 24))
let badge = container(accented_icon(source, 24, primary, accent))
.width(48)
.height(48)
.align_x(alignment::Horizontal::Center)
@@ -742,8 +756,9 @@ fn connection_control<'a>(
opacity: f32,
) -> Element<'a, cosmic::Action<Message>> {
let foreground = color_with_opacity(theme.text_primary, opacity);
let icon_accent = color_with_opacity(theme.accent, opacity);
let accent = color_with_opacity(theme.accent, opacity * 0.14);
let icon = container(symbolic_icon(source, 18))
let icon = container(accented_icon(source, 18, foreground, icon_accent))
.width(32)
.height(32)
.align_x(alignment::Horizontal::Center)
@@ -850,6 +865,7 @@ fn slider_row(
value: u8,
brightness: bool,
foreground: Color,
icon_accent: Color,
opacity: f32,
) -> Element<'static, cosmic::Action<Message>> {
let style = std::rc::Rc::new(move |_: &cosmic::Theme| {
@@ -900,7 +916,7 @@ fn slider_row(
dragging: style,
});
row![
symbolic_icon(source, 18),
accented_icon(source, 18, foreground, icon_accent),
control,
text(format!("{value}%")).size(12).width(36)
]
@@ -909,6 +925,36 @@ fn slider_row(
.into()
}
fn status_label(kind: Menu, s: &Snapshot) -> String {
match kind {
Menu::Network => match &s.network {
Some(n) if n.enabled => format!(
"Wi-Fi: {}",
n.connection.as_deref().unwrap_or("not connected")
),
_ => "Wi-Fi: off".into(),
},
Menu::Bluetooth => match &s.bluetooth {
Some(b) if b.enabled => format!("Bluetooth: on, {} devices connected", b.devices.len()),
_ => "Bluetooth: off".into(),
},
Menu::Audio => match &s.audio {
Some(a) if !a.muted && a.volume > 0 => format!("Audio: {}%", a.volume),
_ => "Audio: muted".into(),
},
Menu::Notifications => match &s.notifications {
Some(n) if n.dnd => "Notifications: do not disturb".into(),
Some(n) => format!("Notifications: {} unread", n.count),
_ => "Notifications: unavailable".into(),
},
Menu::Battery => s.battery.as_ref().map_or_else(
|| "Battery: unavailable".into(),
|b| format!("Battery: {}%, {}", b.percent, b.status),
),
Menu::System => "Control Center".into(),
}
}
fn audio_icon(volume: u8, muted: bool) -> &'static [u8] {
if muted || volume == 0 {
include_bytes!("../assets/icons/status/volume-mute.svg")
@@ -940,7 +986,7 @@ fn status_icon(kind: Menu, s: &Snapshot) -> (&'static [u8], bool) {
}
}
Menu::Bluetooth => match &s.bluetooth {
Some(b) if !b.devices.is_empty() => (
Some(b) if b.enabled && !b.devices.is_empty() => (
include_bytes!("../assets/icons/status/bluetooth-connected.svg"),
true,
),
@@ -965,13 +1011,13 @@ fn status_icon(kind: Menu, s: &Snapshot) -> (&'static [u8], bool) {
};
let icon: &'static [u8] = if b.status == "Charging" {
include_bytes!("../assets/icons/status/battery-charging.svg")
} else if b.percent > 87 {
} else if b.percent >= 80 {
include_bytes!("../assets/icons/status/battery-full.svg")
} else if b.percent > 62 {
} else if b.percent >= 50 {
include_bytes!("../assets/icons/status/battery-75.svg")
} else if b.percent > 37 {
} else if b.percent >= 20 {
include_bytes!("../assets/icons/status/battery-50.svg")
} else if b.percent > 12 {
} else if b.percent > 0 {
include_bytes!("../assets/icons/status/battery-25.svg")
} else {
include_bytes!("../assets/icons/status/battery-empty.svg")
@@ -980,7 +1026,10 @@ fn status_icon(kind: Menu, s: &Snapshot) -> (&'static [u8], bool) {
}
Menu::Notifications => {
if s.notifications.as_ref().is_some_and(|n| n.dnd) {
(include_bytes!("../assets/icons/status/dnd.svg"), false)
(
include_bytes!("../assets/icons/status/notifications-off.svg"),
false,
)
} else {
(
include_bytes!("../assets/icons/status/notifications.svg"),
@@ -1042,5 +1091,62 @@ mod tests {
status_icon(Menu::Battery, &s).0,
include_bytes!("../assets/icons/status/battery-charging.svg")
);
s.notifications = Some(status::Notifications {
count: 3,
dnd: true,
});
assert_eq!(
status_icon(Menu::Notifications, &s).0,
include_bytes!("../assets/icons/status/notifications-off.svg")
);
assert_eq!(
status_label(Menu::Notifications, &s),
"Notifications: do not disturb"
);
s.bluetooth = Some(status::Bluetooth {
enabled: false,
devices: vec!["Headphones".into()],
});
assert_eq!(
status_icon(Menu::Bluetooth, &s).0,
include_bytes!("../assets/icons/status/bluetooth-off.svg")
);
}
#[test]
fn battery_bands_and_live_label_follow_charge_state() {
let mut s = Snapshot::default();
for (percent, expected) in [
(
0,
include_bytes!("../assets/icons/status/battery-empty.svg").as_slice(),
),
(
19,
include_bytes!("../assets/icons/status/battery-25.svg").as_slice(),
),
(
20,
include_bytes!("../assets/icons/status/battery-50.svg").as_slice(),
),
(
50,
include_bytes!("../assets/icons/status/battery-75.svg").as_slice(),
),
(
80,
include_bytes!("../assets/icons/status/battery-full.svg").as_slice(),
),
] {
s.battery = Some(status::Battery {
percent,
status: "Discharging".into(),
});
assert_eq!(status_icon(Menu::Battery, &s).0, expected);
assert_eq!(
status_label(Menu::Battery, &s),
format!("Battery: {percent}%, Discharging")
);
}
}
}