Consolidate desktop controls with editable holidays and a running-window tray
This commit is contained in:
1 parent
9908dde9f8
commit
326465952a
55 files changed
+4207
-450
No files matched your search
@@ -30,14 +30,22 @@ pub(super) fn view<'a>(
|
||||
]
|
||||
.align_y(Alignment::Center);
|
||||
if combined {
|
||||
volume_heading = volume_heading.push(shell_switch(
|
||||
shell.render,
|
||||
"Mute",
|
||||
a.muted,
|
||||
Action::Mute(!a.muted),
|
||||
theme,
|
||||
p,
|
||||
));
|
||||
volume_heading = volume_heading
|
||||
.push(
|
||||
shell
|
||||
.render
|
||||
.text(ferese_config::tr!("Mute"))
|
||||
.size(11)
|
||||
.class(theme::Text::Color(muted)),
|
||||
)
|
||||
.push(shell_switch(
|
||||
shell.render,
|
||||
"Mute",
|
||||
a.muted,
|
||||
Action::Mute(!a.muted),
|
||||
theme,
|
||||
p,
|
||||
));
|
||||
}
|
||||
let audio = column![
|
||||
volume_heading,
|
||||
@@ -63,6 +71,18 @@ pub(super) fn view<'a>(
|
||||
audio.into()
|
||||
};
|
||||
rows = rows.push(audio);
|
||||
} else {
|
||||
rows = rows.push(
|
||||
column![
|
||||
shell.render.text(ferese_config::tr!("Volume")).size(13),
|
||||
shell
|
||||
.render
|
||||
.text(ferese_config::tr!("No audio output available"))
|
||||
.size(11)
|
||||
.class(theme::Text::Color(muted)),
|
||||
]
|
||||
.spacing(4),
|
||||
);
|
||||
}
|
||||
rows
|
||||
}
|
||||
|
||||
@@ -60,39 +60,20 @@ impl FereseShell {
|
||||
} else {
|
||||
theme.text_muted
|
||||
});
|
||||
let mut indicator = if kind == Menu::Battery {
|
||||
let indicator = if kind == Menu::Notifications {
|
||||
notification_indicator(
|
||||
!self.notifications.entries.is_empty()
|
||||
|| self
|
||||
.status
|
||||
.notifications
|
||||
.as_ref()
|
||||
.is_some_and(|n| n.count > 0),
|
||||
)
|
||||
} else if kind == Menu::Battery {
|
||||
self.battery_indicator(theme, 1.0, battery_percentage)
|
||||
} else {
|
||||
accented_icon(source, metrics.icon_size, foreground, color(theme.accent)).into()
|
||||
};
|
||||
if kind == Menu::Notifications {
|
||||
let unread = self
|
||||
.status
|
||||
.notifications
|
||||
.as_ref()
|
||||
.is_some_and(|n| n.count > 0 && !n.dnd);
|
||||
let dot = container(cosmic::widget::Space::new())
|
||||
.width(4)
|
||||
.height(4)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: unread.then_some(Background::Color(foreground)),
|
||||
border: Border {
|
||||
shape: BorderShape::Continuous,
|
||||
radius: self.render.radius(2.0).into(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}));
|
||||
// Keep the popup anchor stable when reading clears the badge.
|
||||
indicator = cosmic::iced::widget::stack![
|
||||
indicator,
|
||||
container(dot)
|
||||
.width(metrics.icon_size)
|
||||
.height(metrics.icon_size)
|
||||
.align_x(Alignment::End),
|
||||
]
|
||||
.into();
|
||||
}
|
||||
let mut content = row![indicator].spacing(4).align_y(Alignment::Center);
|
||||
|
||||
if kind == Menu::Recording
|
||||
@@ -107,7 +88,9 @@ impl FereseShell {
|
||||
}
|
||||
let content = bar_content(content, metrics.group_item_height);
|
||||
let recording_busy = kind == Menu::Recording && self.recorder.busy();
|
||||
let icon_width = if kind == Menu::Battery {
|
||||
let icon_width = if kind == Menu::Notifications {
|
||||
3.
|
||||
} else if kind == Menu::Battery {
|
||||
crate::battery_indicator::WIDTH
|
||||
} else {
|
||||
f32::from(metrics.icon_size)
|
||||
@@ -120,7 +103,14 @@ impl FereseShell {
|
||||
} else {
|
||||
status_label(kind, &self.status)
|
||||
})
|
||||
.padding([0.0, ((metrics.height - icon_width) * 0.5).max(4.0)])
|
||||
.padding([
|
||||
0.0,
|
||||
if kind == Menu::Notifications {
|
||||
4.
|
||||
} else {
|
||||
((metrics.height - icon_width) * 0.5).max(4.0)
|
||||
},
|
||||
])
|
||||
.height(metrics.group_item_height)
|
||||
.on_press_with_rectangle(move |offset, bounds| {
|
||||
if kind == Menu::Recording {
|
||||
@@ -144,6 +134,27 @@ impl FereseShell {
|
||||
}
|
||||
}
|
||||
|
||||
fn notification_indicator<'a>(has_notifications: bool) -> Element<'a, cosmic::Action<Message>> {
|
||||
container(cosmic::widget::Space::new())
|
||||
.id("notification-indicator")
|
||||
.width(3)
|
||||
.height(13)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: Some(Background::Color(if has_notifications {
|
||||
cosmic::iced::Color::WHITE
|
||||
} else {
|
||||
cosmic::iced::Color::TRANSPARENT
|
||||
})),
|
||||
border: Border {
|
||||
shape: BorderShape::Continuous,
|
||||
radius: 1.5.into(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}))
|
||||
.into()
|
||||
}
|
||||
|
||||
fn status_label(kind: Menu, s: &Snapshot) -> String {
|
||||
match kind {
|
||||
Menu::Tray => "Application tray".into(),
|
||||
@@ -303,11 +314,12 @@ mod tests {
|
||||
let ordinary = measure(&shell, Menu::System, false);
|
||||
for (count, dnd) in [(0, false), (3, false), (0, false), (3, true)] {
|
||||
shell.status.notifications = Some(status::Notifications { count, dnd });
|
||||
let notification = measure(&shell, Menu::Notifications, false);
|
||||
assert_eq!(
|
||||
measure(&shell, Menu::Notifications, false),
|
||||
ordinary,
|
||||
"notification badges must stay inside the normal icon hitbox"
|
||||
notification.width, 11.,
|
||||
"notification mark keeps its narrow stable allocation"
|
||||
);
|
||||
assert_eq!(notification.height, ordinary.height);
|
||||
}
|
||||
for percent in [0, 9, 50, 100] {
|
||||
shell.status.battery = Some(status::Battery {
|
||||
|
||||
@@ -23,21 +23,38 @@ pub(super) fn view<'a>(
|
||||
rows = rows.push(
|
||||
shell
|
||||
.render
|
||||
.text(match b.status.as_str() {
|
||||
.text(ferese_config::locale::text(match b.status.as_str() {
|
||||
"Charging" => "Connected to power",
|
||||
"Discharging" => "Running on battery power",
|
||||
"Full" => "Battery fully charged",
|
||||
_ => "Battery status reported by the system",
|
||||
})
|
||||
}))
|
||||
.size(12)
|
||||
.class(theme::Text::Color(muted)),
|
||||
);
|
||||
}
|
||||
rows = rows.push(ferese_theme::menus::section_label(
|
||||
"Power mode",
|
||||
shell.render.font,
|
||||
power_controls(shell, rows, style, true)
|
||||
}
|
||||
|
||||
pub(super) fn power_controls<'a>(
|
||||
shell: &'a FereseShell,
|
||||
mut rows: MenuRows<'a>,
|
||||
style: MenuStyle,
|
||||
show_unavailable: bool,
|
||||
) -> MenuRows<'a> {
|
||||
let MenuStyle {
|
||||
theme,
|
||||
muted,
|
||||
));
|
||||
opacity: p,
|
||||
..
|
||||
} = style;
|
||||
if shell.status.power_profiles.is_some() || show_unavailable {
|
||||
rows = rows.push(ferese_theme::menus::section_label(
|
||||
ferese_config::tr!("Power mode"),
|
||||
shell.render.font,
|
||||
muted,
|
||||
));
|
||||
}
|
||||
if let Some(profiles) = &shell.status.power_profiles {
|
||||
let mut modes = row::with_capacity(3).spacing(6).width(Length::Fill);
|
||||
for (index, (label, icon, profile)) in [
|
||||
@@ -69,7 +86,7 @@ pub(super) fn view<'a>(
|
||||
}
|
||||
}
|
||||
rows = rows.push(modes);
|
||||
} else {
|
||||
} else if show_unavailable {
|
||||
rows = rows.push(
|
||||
shell
|
||||
.render
|
||||
@@ -114,7 +131,7 @@ pub(super) fn view<'a>(
|
||||
|
||||
if shell.status.reboot || shell.status.poweroff || shell.status.suspend {
|
||||
rows = rows.push(ferese_theme::menus::section_label(
|
||||
"Power",
|
||||
ferese_config::tr!("Power"),
|
||||
shell.render.font,
|
||||
muted,
|
||||
));
|
||||
@@ -171,9 +188,12 @@ fn power_tile<'a>(
|
||||
opacity,
|
||||
);
|
||||
let tile = container(
|
||||
column![bar_icon(icon, 19, foreground), render.text(label).size(11)]
|
||||
.spacing(3)
|
||||
.align_x(Alignment::Center),
|
||||
column![
|
||||
bar_icon(icon, 19, foreground),
|
||||
render.text(ferese_config::locale::text(label)).size(11)
|
||||
]
|
||||
.spacing(3)
|
||||
.align_x(Alignment::Center),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.center_x(Length::Fill);
|
||||
@@ -184,7 +204,13 @@ fn power_tile<'a>(
|
||||
profile,
|
||||
))));
|
||||
|
||||
motion::button(render, btn, foreground, selected, opacity)
|
||||
motion::button(
|
||||
render.with_radius_limit(6.),
|
||||
btn,
|
||||
foreground,
|
||||
selected,
|
||||
opacity,
|
||||
)
|
||||
}
|
||||
|
||||
fn power_tile_action<'a>(
|
||||
@@ -197,9 +223,12 @@ fn power_tile_action<'a>(
|
||||
) -> Element<'a, cosmic::Action<Message>> {
|
||||
let foreground = color_with_opacity(theme.text_primary, opacity);
|
||||
let tile = container(
|
||||
column![bar_icon(icon, 19, foreground), render.text(label).size(11)]
|
||||
.spacing(3)
|
||||
.align_x(Alignment::Center),
|
||||
column![
|
||||
bar_icon(icon, 19, foreground),
|
||||
render.text(ferese_config::locale::text(label)).size(11)
|
||||
]
|
||||
.spacing(3)
|
||||
.align_x(Alignment::Center),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.center_x(Length::Fill);
|
||||
@@ -208,5 +237,11 @@ fn power_tile_action<'a>(
|
||||
.padding([8, 2])
|
||||
.on_press(cosmic::Action::App(Message::ConfirmPower(action)));
|
||||
|
||||
motion::button(render, btn, foreground, false, opacity)
|
||||
motion::button(
|
||||
render.with_radius_limit(6.),
|
||||
btn,
|
||||
foreground,
|
||||
false,
|
||||
opacity,
|
||||
)
|
||||
}
|
||||
@@ -9,6 +9,7 @@ pub(super) fn view<'a>(
|
||||
offset: i32,
|
||||
theme: ShellTheme,
|
||||
opacity: f32,
|
||||
settings: &ferese_config::calendar::Settings,
|
||||
) -> Element<'a, cosmic::Action<Message>> {
|
||||
let primary = color_with_opacity(theme.text_primary, opacity);
|
||||
ferese_theme::calendar::grid(
|
||||
@@ -40,6 +41,7 @@ pub(super) fn view<'a>(
|
||||
opacity,
|
||||
),
|
||||
],
|
||||
settings,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -48,6 +50,7 @@ pub(super) fn hub<'a>(
|
||||
offset: i32,
|
||||
theme: ShellTheme,
|
||||
opacity: f32,
|
||||
settings: &ferese_config::calendar::Settings,
|
||||
) -> Element<'a, cosmic::Action<Message>> {
|
||||
let primary = color_with_opacity(theme.text_primary, opacity);
|
||||
let navigation = |label: &'static str, message: Message| {
|
||||
@@ -76,8 +79,9 @@ pub(super) fn hub<'a>(
|
||||
opacity,
|
||||
[
|
||||
navigation("‹", Message::CalendarMonth(-1)),
|
||||
navigation("Today", Message::CalendarToday),
|
||||
navigation(ferese_config::tr!("Today"), Message::CalendarToday),
|
||||
navigation("›", Message::CalendarMonth(1)),
|
||||
],
|
||||
settings,
|
||||
)
|
||||
}
|
||||
@@ -151,7 +151,15 @@ pub(super) fn shell_switch<'a>(
|
||||
render,
|
||||
button::custom(toggle)
|
||||
.padding(4)
|
||||
.name(format!("{label}: {}", if enabled { "on" } else { "off" }))
|
||||
.name(format!(
|
||||
"{}: {}",
|
||||
ferese_config::locale::translate(label),
|
||||
if enabled {
|
||||
ferese_config::tr!("On")
|
||||
} else {
|
||||
ferese_config::tr!("Off")
|
||||
}
|
||||
))
|
||||
.on_press(cosmic::Action::App(Message::Control(action))),
|
||||
color(palette.text_primary),
|
||||
false,
|
||||
@@ -168,7 +176,7 @@ pub(super) fn toggle_row<'a>(
|
||||
opacity: f32,
|
||||
) -> Element<'a, cosmic::Action<Message>> {
|
||||
ferese_theme::menus::row(
|
||||
label,
|
||||
ferese_config::locale::translate(label),
|
||||
shell_switch(render, label, on, action, palette, opacity),
|
||||
render.font,
|
||||
)
|
||||
|
||||
@@ -286,16 +286,16 @@ mod tests {
|
||||
#[test]
|
||||
fn clicking_the_same_control_at_new_bounds_repositions_instead_of_toggling() {
|
||||
let mut shell = crate::tests::shell_with_measured_panel();
|
||||
shell.status.network = Some(crate::status::Network {
|
||||
enabled: true,
|
||||
connection: Some("Test network".into()),
|
||||
signal: 90,
|
||||
shell.status.audio = Some(crate::status::Audio {
|
||||
volume: 50,
|
||||
muted: false,
|
||||
output: String::new(),
|
||||
});
|
||||
let bar = shell.outputs[0].bar;
|
||||
let mut anchor = PopoverAnchor {
|
||||
parent: bar,
|
||||
panel: shell.config.panels[0].id.clone(),
|
||||
item: Some(crate::panel::ItemId("network".into())),
|
||||
item: Some(crate::panel::ItemId("audio".into())),
|
||||
rectangle: Rectangle {
|
||||
x: 1000,
|
||||
y: 0,
|
||||
@@ -303,15 +303,15 @@ mod tests {
|
||||
height: 36,
|
||||
},
|
||||
};
|
||||
drop(shell.open_menu(Menu::Network, anchor.clone()));
|
||||
drop(shell.open_menu(Menu::Audio, anchor.clone()));
|
||||
let popup = shell.menu.as_ref().unwrap().id;
|
||||
anchor.rectangle.x = 800;
|
||||
drop(shell.open_menu(Menu::Network, anchor.clone()));
|
||||
drop(shell.open_menu(Menu::Audio, anchor.clone()));
|
||||
let menu = shell.menu.as_ref().unwrap();
|
||||
assert_eq!(menu.id, popup);
|
||||
assert_eq!(menu.anchor, anchor);
|
||||
assert!(!menu.motion.closing());
|
||||
drop(shell.open_menu(Menu::Network, anchor));
|
||||
drop(shell.open_menu(Menu::Audio, anchor));
|
||||
assert!(shell.menu.is_none());
|
||||
}
|
||||
|
||||
@@ -443,7 +443,7 @@ mod tests {
|
||||
let anchor = PopoverAnchor {
|
||||
parent: window::Id::unique(),
|
||||
panel: panel.id.clone(),
|
||||
item: Some(crate::panel::ItemId("network".into())),
|
||||
item: Some(crate::panel::ItemId("audio".into())),
|
||||
rectangle: Rectangle {
|
||||
x: 0,
|
||||
y: 0,
|
||||
@@ -453,10 +453,7 @@ mod tests {
|
||||
};
|
||||
assert!(anchor.belongs_to(&panel));
|
||||
let items = &mut panel.end.groups[0].items;
|
||||
let network = items
|
||||
.iter()
|
||||
.position(|item| item.id.0 == "network")
|
||||
.unwrap();
|
||||
let network = items.iter().position(|item| item.id.0 == "audio").unwrap();
|
||||
items[network].visible = false;
|
||||
assert!(!anchor.belongs_to(&panel));
|
||||
panel.end.groups[0].items.remove(network);
|
||||
|
||||
@@ -58,7 +58,7 @@ pub enum Menu {
|
||||
|
||||
impl Menu {
|
||||
fn action_error(self, error: &status::ActionError) -> Option<&str> {
|
||||
let relevant = self == Self::System
|
||||
let relevant = matches!(self, Self::System | Self::Calendar)
|
||||
|| matches!(
|
||||
(self, &error.action),
|
||||
(
|
||||
@@ -78,16 +78,15 @@ impl Menu {
|
||||
|
||||
pub(crate) fn width(self) -> f32 {
|
||||
match self {
|
||||
Self::System => 360.0,
|
||||
Self::System | Self::Calendar => 360.0,
|
||||
Self::Battery => 328.0,
|
||||
Self::Calendar => 268.0,
|
||||
Self::Notifications | Self::Media => 368.0,
|
||||
_ => 300.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn height_limit(self) -> f32 {
|
||||
if self == Self::Calendar { 270.0 } else { 720.0 }
|
||||
720.0
|
||||
}
|
||||
|
||||
fn title(self) -> &'static str {
|
||||
@@ -236,12 +235,18 @@ impl FereseShell {
|
||||
Menu::Bluetooth => bluetooth::view(self, rows, style, true),
|
||||
Menu::Audio => audio::view(self, rows, style, false),
|
||||
Menu::Battery => battery::view(self, rows, style),
|
||||
Menu::Calendar => rows.push(calendar::hub(
|
||||
self.render,
|
||||
self.calendar_offset,
|
||||
theme,
|
||||
opacity,
|
||||
)),
|
||||
Menu::Calendar => system::view(
|
||||
self,
|
||||
rows.push(calendar::hub(
|
||||
self.render,
|
||||
self.calendar_offset,
|
||||
theme,
|
||||
opacity,
|
||||
&self.config.status.calendar,
|
||||
)),
|
||||
style,
|
||||
true,
|
||||
),
|
||||
Menu::Media => media::hub(self, rows, style),
|
||||
Menu::Notifications => {
|
||||
let mut rows = notification_controls(self, rows, style, true);
|
||||
@@ -471,9 +476,18 @@ impl FereseShell {
|
||||
Menu::Bluetooth => bluetooth::view(self, rows, style, false),
|
||||
Menu::Audio => audio::view(self, rows, style, false),
|
||||
Menu::Battery => battery::view(self, rows, style),
|
||||
Menu::Calendar => {
|
||||
rows.push(calendar::view(self.render, self.calendar_offset, theme, p))
|
||||
}
|
||||
Menu::Calendar => system::view(
|
||||
self,
|
||||
rows.push(calendar::view(
|
||||
self.render,
|
||||
self.calendar_offset,
|
||||
theme,
|
||||
p,
|
||||
&self.config.status.calendar,
|
||||
)),
|
||||
style,
|
||||
true,
|
||||
),
|
||||
Menu::Notifications => notification_controls(self, rows, style, false),
|
||||
Menu::Activities => rows.push(self.activity_list(menu.anchor.parent, p)),
|
||||
Menu::CaptureStatus => rows.push(self.view_hub_menu(kind, p)),
|
||||
@@ -844,7 +858,8 @@ mod tests {
|
||||
] {
|
||||
assert_eq!(
|
||||
menu.action_error(&error),
|
||||
(menu == relevant || menu == Menu::System).then_some("control failed"),
|
||||
(menu == relevant || matches!(menu, Menu::System | Menu::Calendar))
|
||||
.then_some("control failed"),
|
||||
"{menu:?} showed the wrong control error: {:?}",
|
||||
error.action
|
||||
);
|
||||
@@ -858,8 +873,8 @@ mod tests {
|
||||
assert_eq!(Menu::Network.width(), 300.0);
|
||||
assert_eq!(Menu::Bluetooth.width(), 300.0);
|
||||
assert_eq!(Menu::Battery.width(), 328.0);
|
||||
assert_eq!(Menu::Calendar.width(), 268.0);
|
||||
assert_eq!(Menu::Calendar.height_limit(), 270.0);
|
||||
assert_eq!(Menu::Calendar.width(), 360.0);
|
||||
assert_eq!(Menu::Calendar.height_limit(), 720.0);
|
||||
assert_eq!(Menu::Audio.width(), 300.0);
|
||||
assert_eq!(Menu::Notifications.width(), 368.0);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use cosmic::{Element, theme};
|
||||
|
||||
use super::bar::status_icon;
|
||||
use super::controls::control_card;
|
||||
use super::{Menu, MenuRows, MenuStyle, audio, battery, notification_controls};
|
||||
use super::{Menu, MenuRows, MenuStyle, audio, battery};
|
||||
use crate::status::{self, Action};
|
||||
use crate::{FereseShell, Message, accented_icon, color, color_with_opacity, motion};
|
||||
|
||||
@@ -28,11 +28,17 @@ pub(super) fn view<'a>(
|
||||
(Mode::Auto, "◐ Auto"),
|
||||
] {
|
||||
modes = modes.push(motion::button(
|
||||
shell.render,
|
||||
button::custom(shell.render.text(label).font(shell.render.font).size(13))
|
||||
.width(Length::Fill)
|
||||
.padding(9)
|
||||
.on_press(cosmic::Action::App(Message::ThemeMode(mode))),
|
||||
shell.render.with_radius_limit(6.),
|
||||
button::custom(
|
||||
shell
|
||||
.render
|
||||
.text(ferese_config::locale::text(label))
|
||||
.font(shell.render.font)
|
||||
.size(13),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.padding(9)
|
||||
.on_press(cosmic::Action::App(Message::ThemeMode(mode))),
|
||||
primary,
|
||||
shell.config.theme_mode == mode,
|
||||
p,
|
||||
@@ -80,9 +86,9 @@ pub(super) fn view<'a>(
|
||||
shell
|
||||
.render
|
||||
.text(if theme.appearance == Appearance::Light {
|
||||
"Auto · currently light"
|
||||
ferese_config::tr!("Auto · currently light")
|
||||
} else {
|
||||
"Auto · currently dark"
|
||||
ferese_config::tr!("Auto · currently dark")
|
||||
})
|
||||
.font(shell.render.font)
|
||||
.size(12)
|
||||
@@ -96,9 +102,11 @@ pub(super) fn view<'a>(
|
||||
shell.render,
|
||||
"Wi-Fi",
|
||||
if n.enabled {
|
||||
n.connection.as_deref().unwrap_or("Not connected")
|
||||
n.connection
|
||||
.as_deref()
|
||||
.unwrap_or(ferese_config::tr!("Not connected"))
|
||||
} else {
|
||||
"Off"
|
||||
ferese_config::tr!("Off")
|
||||
},
|
||||
status_icon(Menu::Network, &shell.status).0,
|
||||
n.enabled,
|
||||
@@ -112,9 +120,12 @@ pub(super) fn view<'a>(
|
||||
shell.render,
|
||||
"Bluetooth",
|
||||
if b.enabled {
|
||||
b.devices.first().map(String::as_str).unwrap_or("On")
|
||||
b.devices
|
||||
.first()
|
||||
.map(String::as_str)
|
||||
.unwrap_or(ferese_config::tr!("On"))
|
||||
} else {
|
||||
"Off"
|
||||
ferese_config::tr!("Off")
|
||||
},
|
||||
status_icon(Menu::Bluetooth, &shell.status).0,
|
||||
b.enabled,
|
||||
@@ -155,8 +166,7 @@ pub(super) fn view<'a>(
|
||||
}
|
||||
rows = audio::view(shell, rows, style, true);
|
||||
rows = battery::brightness(shell, rows, style);
|
||||
rows = super::night_light::view(shell, rows, style);
|
||||
rows = notification_controls(shell, rows, style, true);
|
||||
rows = battery::power_controls(shell, rows, style, false);
|
||||
rows
|
||||
}
|
||||
|
||||
@@ -246,12 +256,18 @@ fn connection_control<'a>(
|
||||
.class(ferese_theme::controls::filled_button(
|
||||
fill,
|
||||
foreground,
|
||||
render.radius(14.),
|
||||
render.radius(6.),
|
||||
opacity,
|
||||
))
|
||||
.into()
|
||||
} else {
|
||||
motion::button(render, button, foreground, false, opacity)
|
||||
motion::button(
|
||||
render.with_radius_limit(6.),
|
||||
button,
|
||||
foreground,
|
||||
false,
|
||||
opacity,
|
||||
)
|
||||
};
|
||||
container(tile).width(Length::FillPortion(1)).into()
|
||||
}
|
||||
@@ -296,6 +312,122 @@ pub(super) fn heading<'a>(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn combined_controls_include_audio_and_confirmed_power_without_dnd_or_night_light() {
|
||||
use cosmic::iced::advanced::renderer::Headless;
|
||||
use cosmic::iced::advanced::{Layout, Shell, clipboard, layout, widget};
|
||||
use cosmic::iced::{Event, Rectangle, Size, mouse};
|
||||
#[derive(Default)]
|
||||
struct Labels(Vec<(String, Rectangle)>);
|
||||
impl widget::Operation for Labels {
|
||||
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn widget::Operation)) {
|
||||
visit(self);
|
||||
}
|
||||
fn text(&mut self, _: Option<&widget::Id>, bounds: Rectangle, text: &str) {
|
||||
self.0.push((text.into(), bounds));
|
||||
}
|
||||
}
|
||||
let renderer = cosmic::iced::futures::executor::block_on(cosmic::Renderer::new(
|
||||
cosmic::iced::Font::default(),
|
||||
cosmic::iced::Pixels(14.),
|
||||
Some("tiny-skia"),
|
||||
))
|
||||
.unwrap();
|
||||
let mut app = crate::tests::shell_with_measured_panel();
|
||||
app.status.audio = Some(status::Audio {
|
||||
volume: 45,
|
||||
muted: false,
|
||||
output: "Test output".into(),
|
||||
});
|
||||
app.status.power_profiles = Some(status::PowerProfiles {
|
||||
active: "balanced".into(),
|
||||
available: [true; 3],
|
||||
});
|
||||
app.status.notifications = Some(status::Notifications {
|
||||
count: 1,
|
||||
dnd: false,
|
||||
});
|
||||
app.status.poweroff = true;
|
||||
app.status.reboot = true;
|
||||
app.status.suspend = true;
|
||||
let theme = app.config.theme;
|
||||
let style = MenuStyle {
|
||||
theme,
|
||||
primary: color(theme.text_primary),
|
||||
muted: color(theme.text_muted),
|
||||
opacity: 1.,
|
||||
};
|
||||
let mut view: Element<'_, cosmic::Action<Message>> =
|
||||
view(&app, column([]), style, true).into();
|
||||
let mut tree = widget::Tree::new(&view);
|
||||
let node = view.as_widget_mut().layout(
|
||||
&mut tree,
|
||||
&renderer,
|
||||
&layout::Limits::new(Size::ZERO, Size::new(328., 1000.)),
|
||||
);
|
||||
let mut labels = Labels::default();
|
||||
view.as_widget_mut()
|
||||
.operate(&mut tree, Layout::new(&node), &renderer, &mut labels);
|
||||
for key in [
|
||||
"Volume",
|
||||
"Power mode",
|
||||
"Balanced",
|
||||
"Power off",
|
||||
"Restart",
|
||||
"Suspend",
|
||||
] {
|
||||
assert!(
|
||||
labels
|
||||
.0
|
||||
.iter()
|
||||
.any(|(text, _)| text == ferese_config::locale::text(key)),
|
||||
"missing {key}"
|
||||
);
|
||||
}
|
||||
for key in ["Do Not Disturb", "Night light"] {
|
||||
assert!(
|
||||
!labels
|
||||
.0
|
||||
.iter()
|
||||
.any(|(text, _)| text == ferese_config::locale::text(key))
|
||||
);
|
||||
}
|
||||
let point = labels
|
||||
.0
|
||||
.iter()
|
||||
.find(|(text, _)| text == ferese_config::tr!("Power off"))
|
||||
.unwrap()
|
||||
.1
|
||||
.center();
|
||||
let mut messages = Vec::new();
|
||||
for event in [
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
mouse::Event::ButtonReleased(mouse::Button::Left),
|
||||
] {
|
||||
view.as_widget_mut().update(
|
||||
&mut tree,
|
||||
&Event::Mouse(event),
|
||||
Layout::new(&node),
|
||||
mouse::Cursor::Available(point),
|
||||
&renderer,
|
||||
&mut clipboard::Null,
|
||||
&mut Shell::new(&mut messages),
|
||||
&node.bounds(),
|
||||
);
|
||||
}
|
||||
assert!(messages.iter().any(|message| matches!(
|
||||
message,
|
||||
cosmic::Action::App(Message::ConfirmPower(Action::Poweroff))
|
||||
)));
|
||||
assert!(
|
||||
!messages
|
||||
.iter()
|
||||
.any(|message| matches!(message, cosmic::Action::App(Message::ExecutePower)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connection_captions_fit_one_short_line() {
|
||||
assert_eq!(super::connection_caption("88%"), "88%");
|
||||
|
||||
Reference in new issue
Block a user