Refine shell status icons and window controls; add installer
This commit is contained in:
1 parent
956c739940
commit
686b0477d4
28 files changed
+527
-155
No files matched your search
@@ -41,6 +41,7 @@ pub(crate) struct WorkspaceSnapshot {
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct WindowSnapshot {
|
||||
pub(crate) id: u64,
|
||||
pub(crate) workspace: u64,
|
||||
pub(crate) app_id: String,
|
||||
pub(crate) title: String,
|
||||
@@ -120,6 +121,12 @@ impl ShellControl {
|
||||
let _ = self.connection.flush();
|
||||
}
|
||||
|
||||
pub(crate) fn activate_window(&self, id: u64) {
|
||||
let (hi, lo) = split_id(id);
|
||||
self.shell.activate_window(hi, lo);
|
||||
let _ = self.connection.flush();
|
||||
}
|
||||
|
||||
pub(crate) fn set_overview_active(&self, active: bool) {
|
||||
if active {
|
||||
self.shell.enter_overview();
|
||||
@@ -218,8 +225,8 @@ impl Dispatch<FereseShellV1, ()> for ControlState {
|
||||
});
|
||||
}
|
||||
ferese_shell_v1::Event::Window {
|
||||
window_hi: _,
|
||||
window_lo: _,
|
||||
window_hi,
|
||||
window_lo,
|
||||
workspace_hi,
|
||||
workspace_lo,
|
||||
app_id,
|
||||
@@ -235,6 +242,7 @@ impl Dispatch<FereseShellV1, ()> for ControlState {
|
||||
};
|
||||
|
||||
state.pending.windows.push(WindowSnapshot {
|
||||
id: join_id(window_hi, window_lo),
|
||||
workspace: join_id(workspace_hi, workspace_lo),
|
||||
app_id,
|
||||
title,
|
||||
|
||||
+163
-40
@@ -144,6 +144,7 @@ enum Message {
|
||||
),
|
||||
Tick,
|
||||
ActivateWorkspace(u64),
|
||||
ActivateWindow(u64),
|
||||
ToggleOverview,
|
||||
StatusTick,
|
||||
AnimateMenu,
|
||||
@@ -383,6 +384,12 @@ impl cosmic::Application for FereseShell {
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::ActivateWindow(id) => {
|
||||
if let Some(control) = &self.control {
|
||||
control.activate_window(id);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::ActivateWorkspace(id) => {
|
||||
if let Some(control) = &self.control {
|
||||
control.activate_workspace(id);
|
||||
@@ -597,34 +604,43 @@ impl FereseShell {
|
||||
workspace_row = workspace_row.push(workspace_button);
|
||||
}
|
||||
|
||||
let app_name = self
|
||||
.snapshot
|
||||
.windows
|
||||
.iter()
|
||||
.find(|window| {
|
||||
window.focused
|
||||
&& focused_output
|
||||
.is_none_or(|output| window.workspace == output.active_workspace)
|
||||
})
|
||||
.map(display_app_name)
|
||||
.unwrap_or_else(|| "Desktop".to_owned());
|
||||
let foreground = color(shell_theme.text_primary);
|
||||
let app_name = if self.overview_active {
|
||||
"Overview".to_owned()
|
||||
} else {
|
||||
app_name
|
||||
};
|
||||
let left = row![
|
||||
workspace_row,
|
||||
text(app_name)
|
||||
.size(bar.text_size)
|
||||
.font(cosmic::font::semibold())
|
||||
.width(Length::Fill)
|
||||
.wrapping(cosmic::iced::core::text::Wrapping::None)
|
||||
.class(theme::Text::Color(foreground))
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(cosmic::iced::Alignment::Center);
|
||||
let mut windows = row::with_capacity(self.snapshot.windows.len())
|
||||
.spacing(4)
|
||||
.align_y(cosmic::iced::Alignment::Center);
|
||||
for window in self.snapshot.windows.iter().filter(|window| {
|
||||
focused_output.is_some_and(|output| window.workspace == output.active_workspace)
|
||||
}) {
|
||||
let label = compact_window_label(window);
|
||||
let foreground = color(if window.focused {
|
||||
shell_theme.text_primary
|
||||
} else {
|
||||
shell_theme.text_muted
|
||||
});
|
||||
windows = windows.push(
|
||||
button::custom(
|
||||
text(label)
|
||||
.size(bar.text_size)
|
||||
.wrapping(cosmic::iced::core::text::Wrapping::None)
|
||||
.class(theme::Text::Color(foreground)),
|
||||
)
|
||||
.padding([0, 8])
|
||||
.height(bar.control_height)
|
||||
.class(status_ui::button_style(foreground, window.focused, 1.0))
|
||||
.on_press(cosmic::Action::App(Message::ActivateWindow(window.id))),
|
||||
);
|
||||
}
|
||||
let windows = cosmic::iced::widget::scrollable(windows)
|
||||
.direction(cosmic::iced::widget::scrollable::Direction::Horizontal(
|
||||
cosmic::iced::widget::scrollable::Scrollbar::default()
|
||||
.width(0)
|
||||
.scroller_width(0),
|
||||
))
|
||||
.width(Length::Fill)
|
||||
.height(bar.control_height);
|
||||
let left = row![workspace_row, windows]
|
||||
.spacing(8)
|
||||
.align_y(cosmic::iced::Alignment::Center);
|
||||
let clock = container(
|
||||
text(&self.clock)
|
||||
.size(bar.text_size)
|
||||
@@ -680,21 +696,47 @@ impl FereseShell {
|
||||
}
|
||||
}
|
||||
|
||||
fn symbolic_icon(source: &'static [u8], size: u16) -> icon::Icon {
|
||||
icon::from_svg_bytes(source)
|
||||
.symbolic(true)
|
||||
.icon()
|
||||
.size(size)
|
||||
fn bar_icon(source: &'static [u8], size: u16, foreground: Color) -> icon::Icon {
|
||||
accented_icon(source, size, foreground, foreground)
|
||||
}
|
||||
|
||||
fn bar_icon(source: &'static [u8], size: u16, foreground: Color) -> icon::Icon {
|
||||
// COSMIC's transparent containers replace inherited foreground colors.
|
||||
// Pin the bar glyph color rather than relying on those container defaults.
|
||||
symbolic_icon(source, size).class(theme::Svg::custom(move |_| {
|
||||
cosmic::iced::widget::svg::Style {
|
||||
color: Some(foreground),
|
||||
}
|
||||
}))
|
||||
fn accented_icon(source: &'static [u8], size: u16, foreground: Color, accent: Color) -> icon::Icon {
|
||||
let svg = std::str::from_utf8(source).expect("embedded SVG must be UTF-8");
|
||||
// Battery canvases are wider; reserve that space instead of stretching
|
||||
// or clipping them into the square slot used by the other status icons.
|
||||
let aspect = if svg.contains("viewBox=\"0 0 32 24\"") {
|
||||
4.0 / 3.0
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
icon::from_svg_bytes(tinted_svg(source, foreground, accent))
|
||||
.symbolic(false)
|
||||
.icon()
|
||||
.size(size)
|
||||
.width(Length::Fixed(f32::from(size) * aspect))
|
||||
.content_fit(ContentFit::Contain)
|
||||
.class(theme::Svg::custom(move |_| {
|
||||
cosmic::iced::widget::svg::Style { color: None }
|
||||
}))
|
||||
}
|
||||
|
||||
fn tinted_svg(source: &[u8], foreground: Color, accent: Color) -> Vec<u8> {
|
||||
let rgba = |foreground: Color| {
|
||||
format!(
|
||||
"rgba({},{},{},{})",
|
||||
(foreground.r * 255.0).round() as u8,
|
||||
(foreground.g * 255.0).round() as u8,
|
||||
(foreground.b * 255.0).round() as u8,
|
||||
foreground.a,
|
||||
)
|
||||
};
|
||||
std::str::from_utf8(source)
|
||||
.expect("embedded icons must be UTF-8 SVG")
|
||||
.replace("currentColor", &rgba(foreground))
|
||||
// The source artwork's blue swatch is the semantic theme accent.
|
||||
// Battery warning/success swatches remain status colors.
|
||||
.replace("#3d7be6", &rgba(accent))
|
||||
.into_bytes()
|
||||
}
|
||||
|
||||
fn overview_control(
|
||||
@@ -806,6 +848,22 @@ fn current_time() -> String {
|
||||
Zoned::now().strftime("%a · %H:%M").to_string()
|
||||
}
|
||||
|
||||
fn compact_window_label(window: &crate::control::WindowSnapshot) -> String {
|
||||
let title = window.title.trim();
|
||||
let label = if title.is_empty() {
|
||||
display_app_name(window)
|
||||
} else {
|
||||
title.to_owned()
|
||||
};
|
||||
let mut characters = label.chars();
|
||||
let prefix: String = characters.by_ref().take(24).collect();
|
||||
if characters.next().is_some() {
|
||||
format!("{prefix}…")
|
||||
} else {
|
||||
prefix
|
||||
}
|
||||
}
|
||||
|
||||
fn shell_surface_style(theme: ShellTheme) -> cosmic::iced::theme::Style {
|
||||
cosmic::iced::theme::Style {
|
||||
background_color: Color::TRANSPARENT,
|
||||
@@ -967,6 +1025,70 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::control::{OutputSnapshot, WindowSnapshot};
|
||||
|
||||
#[test]
|
||||
fn icons_use_theme_accent_and_preserve_battery_status_colors() {
|
||||
for (source, accent) in [
|
||||
(
|
||||
include_bytes!("../assets/icons/status/battery-full.svg").as_slice(),
|
||||
"#63c168",
|
||||
),
|
||||
(
|
||||
include_bytes!("../assets/icons/status/battery-50.svg").as_slice(),
|
||||
"#3d7be6",
|
||||
),
|
||||
(
|
||||
include_bytes!("../assets/icons/status/battery-25.svg").as_slice(),
|
||||
"#e0654f",
|
||||
),
|
||||
(
|
||||
include_bytes!("../assets/icons/status/wifi-full.svg").as_slice(),
|
||||
"#3d7be6",
|
||||
),
|
||||
(
|
||||
include_bytes!("../assets/icons/status/control-center.svg").as_slice(),
|
||||
"#3d7be6",
|
||||
),
|
||||
] {
|
||||
let svg = String::from_utf8(tinted_svg(
|
||||
source,
|
||||
Color::from_rgb8(205, 214, 244),
|
||||
Color::from_rgb8(203, 166, 247),
|
||||
))
|
||||
.unwrap();
|
||||
assert!(!svg.contains("currentColor"));
|
||||
assert!(svg.contains("rgba(205,214,244,1)"));
|
||||
assert!(!svg.contains("#3d7be6"));
|
||||
assert!(svg.contains(if accent == "#3d7be6" {
|
||||
"rgba(203,166,247,1)"
|
||||
} else {
|
||||
accent
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_list_labels_use_titles_and_fall_back_to_the_app_name() {
|
||||
let snapshot = snapshot_with_fullscreen_window(7);
|
||||
let mut window = snapshot.windows[0].clone();
|
||||
window.title = " editor.rs — Project ".to_owned();
|
||||
assert_eq!(compact_window_label(&window), "editor.rs — Project");
|
||||
window.title = " ".to_owned();
|
||||
assert_eq!(compact_window_label(&window), display_app_name(&window));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_list_labels_truncate_unicode_without_breaking_characters() {
|
||||
let snapshot = snapshot_with_fullscreen_window(7);
|
||||
let mut window = snapshot.windows[0].clone();
|
||||
window.title = "界".repeat(25);
|
||||
assert_eq!(
|
||||
compact_window_label(&window),
|
||||
format!("{}…", "界".repeat(24))
|
||||
);
|
||||
window.title = "界".repeat(24);
|
||||
assert_eq!(compact_window_label(&window), window.title);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_surface_clear_is_transparent_and_bar_fallback_is_dark() {
|
||||
let theme = ShellTheme::default();
|
||||
@@ -1048,6 +1170,7 @@ mod tests {
|
||||
}],
|
||||
workspaces: Vec::new(),
|
||||
windows: vec![WindowSnapshot {
|
||||
id: 1,
|
||||
workspace,
|
||||
app_id: "dev.ferese.Test".to_owned(),
|
||||
title: "Test".to_owned(),
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user