Refine status popovers and toggle radios through status icons
This commit is contained in:
1 parent
64c95f0b97
commit
a0fd760737
1 file changed
+130
-80
@@ -4,9 +4,13 @@ use cosmic::iced::{Alignment, Rectangle};
|
|||||||
use cosmic::widget::{column, slider};
|
use cosmic::widget::{column, slider};
|
||||||
use status::{Action, Snapshot};
|
use status::{Action, Snapshot};
|
||||||
|
|
||||||
const WIDTH: f32 = 320.0;
|
const DEVICE_LIST_HEIGHT: f32 = 180.0;
|
||||||
const OPEN_MS: f32 = 160.0;
|
const OPEN_MS: f32 = 160.0;
|
||||||
|
|
||||||
|
fn device_list_height(count: usize) -> Option<f32> {
|
||||||
|
(count > 4).then_some(DEVICE_LIST_HEIGHT)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum Menu {
|
pub enum Menu {
|
||||||
Network,
|
Network,
|
||||||
@@ -17,6 +21,14 @@ pub enum Menu {
|
|||||||
System,
|
System,
|
||||||
}
|
}
|
||||||
impl Menu {
|
impl Menu {
|
||||||
|
fn width(self) -> f32 {
|
||||||
|
match self {
|
||||||
|
Self::System => 320.0,
|
||||||
|
Self::Network | Self::Bluetooth => 280.0,
|
||||||
|
Self::Battery => 250.0,
|
||||||
|
Self::Audio | Self::Notifications => 290.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
pub(super) fn material_role(self) -> Option<ferese_surface_effects_v1::Role> {
|
pub(super) fn material_role(self) -> Option<ferese_surface_effects_v1::Role> {
|
||||||
// Popovers contain independent cards, with no shared material backing.
|
// Popovers contain independent cards, with no shared material backing.
|
||||||
None
|
None
|
||||||
@@ -93,8 +105,8 @@ impl FereseShell {
|
|||||||
gravity: 6u32.try_into().unwrap(), // bottom-left
|
gravity: 6u32.try_into().unwrap(), // bottom-left
|
||||||
offset: (anchor.width / 2, 4),
|
offset: (anchor.width / 2, 4),
|
||||||
size_limits: Limits::NONE
|
size_limits: Limits::NONE
|
||||||
.min_width(WIDTH)
|
.min_width(kind.width())
|
||||||
.max_width(WIDTH)
|
.max_width(kind.width())
|
||||||
.max_height(720.0),
|
.max_height(720.0),
|
||||||
constraint_adjustment: 3, // slide X/Y, never flip above the bar
|
constraint_adjustment: 3, // slide X/Y, never flip above the bar
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -232,30 +244,10 @@ impl FereseShell {
|
|||||||
};
|
};
|
||||||
let theme = self.config.theme;
|
let theme = self.config.theme;
|
||||||
let p = menu.progress();
|
let p = menu.progress();
|
||||||
|
let kind = menu.kind;
|
||||||
let primary = color_with_opacity(theme.text_primary, p);
|
let primary = color_with_opacity(theme.text_primary, p);
|
||||||
let muted = color_with_opacity(theme.text_muted, p);
|
let muted = color_with_opacity(theme.text_muted, p);
|
||||||
let mut rows = column::with_capacity(12).spacing(8).width(Length::Fill);
|
let mut rows = column::with_capacity(12).spacing(8).width(Length::Fill);
|
||||||
let radio = match menu.kind {
|
|
||||||
Menu::Network => self
|
|
||||||
.status
|
|
||||||
.network
|
|
||||||
.as_ref()
|
|
||||||
.map(|n| (n.enabled, Action::Wifi(!n.enabled))),
|
|
||||||
Menu::Bluetooth => self
|
|
||||||
.status
|
|
||||||
.bluetooth
|
|
||||||
.as_ref()
|
|
||||||
.map(|b| (b.enabled, Action::Bluetooth(!b.enabled))),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
if let Some((enabled, action)) = radio {
|
|
||||||
rows = rows.push(menu_button(
|
|
||||||
if enabled { "Turn off" } else { "Turn on" },
|
|
||||||
Message::Control(action),
|
|
||||||
primary,
|
|
||||||
p,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if let Some(action) = &menu.confirm {
|
if let Some(action) = &menu.confirm {
|
||||||
let title = match action {
|
let title = match action {
|
||||||
Action::Poweroff => "Power off this computer?",
|
Action::Poweroff => "Power off this computer?",
|
||||||
@@ -352,6 +344,7 @@ impl FereseShell {
|
|||||||
muted,
|
muted,
|
||||||
color_with_opacity(theme.accent, p),
|
color_with_opacity(theme.accent, p),
|
||||||
p,
|
p,
|
||||||
|
Some((n.enabled, Action::Wifi(!n.enabled))),
|
||||||
));
|
));
|
||||||
if connected {
|
if connected {
|
||||||
let strength = match n.signal {
|
let strength = match n.signal {
|
||||||
@@ -360,7 +353,7 @@ impl FereseShell {
|
|||||||
25..=49 => "Fair",
|
25..=49 => "Fair",
|
||||||
_ => "Weak",
|
_ => "Weak",
|
||||||
};
|
};
|
||||||
rows = rows.push(control_card(
|
rows = rows.push(
|
||||||
column![
|
column![
|
||||||
row![
|
row![
|
||||||
text("Signal strength").size(12).width(Length::Fill),
|
text("Signal strength").size(12).width(Length::Fill),
|
||||||
@@ -368,11 +361,8 @@ impl FereseShell {
|
|||||||
],
|
],
|
||||||
level_meter(n.signal, primary, p),
|
level_meter(n.signal, primary, p),
|
||||||
]
|
]
|
||||||
.spacing(8)
|
.spacing(8),
|
||||||
.into(),
|
);
|
||||||
primary,
|
|
||||||
p,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -396,9 +386,10 @@ impl FereseShell {
|
|||||||
muted,
|
muted,
|
||||||
color_with_opacity(theme.accent, p),
|
color_with_opacity(theme.accent, p),
|
||||||
p,
|
p,
|
||||||
|
Some((b.enabled, Action::Bluetooth(!b.enabled))),
|
||||||
));
|
));
|
||||||
if b.enabled && !b.devices.is_empty() {
|
if b.enabled && !b.devices.is_empty() {
|
||||||
let mut devices = column::with_capacity(b.devices.len()).spacing(12);
|
let mut devices = column::with_capacity(b.devices.len()).spacing(8);
|
||||||
for device in &b.devices {
|
for device in &b.devices {
|
||||||
devices = devices.push(
|
devices = devices.push(
|
||||||
column![
|
column![
|
||||||
@@ -408,7 +399,16 @@ impl FereseShell {
|
|||||||
.spacing(3),
|
.spacing(3),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
rows = rows.push(control_card(devices.into(), primary, p));
|
let list: Element<'_, cosmic::Action<Message>> =
|
||||||
|
if let Some(height) = device_list_height(b.devices.len()) {
|
||||||
|
cosmic::iced::widget::scrollable(devices)
|
||||||
|
.height(height)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
devices.into()
|
||||||
|
};
|
||||||
|
rows = rows.push(list);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -436,7 +436,12 @@ impl FereseShell {
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
.spacing(4);
|
.spacing(4);
|
||||||
rows = rows.push(control_card(audio.into(), primary, p));
|
let audio: Element<'_, cosmic::Action<Message>> = if combined {
|
||||||
|
control_card(audio.into(), primary, p)
|
||||||
|
} else {
|
||||||
|
audio.into()
|
||||||
|
};
|
||||||
|
rows = rows.push(audio);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if combined {
|
if combined {
|
||||||
@@ -458,17 +463,25 @@ impl FereseShell {
|
|||||||
}
|
}
|
||||||
if combined || menu.kind == Menu::Notifications {
|
if combined || menu.kind == Menu::Notifications {
|
||||||
if let Some(n) = &self.status.notifications {
|
if let Some(n) = &self.status.notifications {
|
||||||
rows = rows.push(control_card(
|
let toggle =
|
||||||
toggle_row("Do Not Disturb", n.dnd, Action::Dnd(!n.dnd), primary, p),
|
toggle_row("Do Not Disturb", n.dnd, Action::Dnd(!n.dnd), primary, p);
|
||||||
primary,
|
rows = rows.push(if combined {
|
||||||
p,
|
control_card(toggle, primary, p)
|
||||||
));
|
} else {
|
||||||
|
toggle
|
||||||
|
});
|
||||||
if menu.kind == Menu::Notifications {
|
if menu.kind == Menu::Notifications {
|
||||||
rows = rows
|
rows = rows
|
||||||
.push(
|
.push(
|
||||||
text(format!("{} notifications", n.count))
|
text(if n.count == 0 {
|
||||||
.size(13)
|
"No notifications".to_owned()
|
||||||
.class(theme::Text::Color(muted)),
|
} else if n.count == 1 {
|
||||||
|
"1 notification".to_owned()
|
||||||
|
} else {
|
||||||
|
format!("{} notifications", n.count)
|
||||||
|
})
|
||||||
|
.size(13)
|
||||||
|
.class(theme::Text::Color(muted)),
|
||||||
)
|
)
|
||||||
.push(menu_button(
|
.push(menu_button(
|
||||||
"Open notification center",
|
"Open notification center",
|
||||||
@@ -494,17 +507,17 @@ impl FereseShell {
|
|||||||
rows = rows.push(
|
rows = rows.push(
|
||||||
container(
|
container(
|
||||||
row![
|
row![
|
||||||
text(format!("{}%", b.percent)).size(40).width(Length::Fill),
|
text(format!("{}%", b.percent)).size(24).width(Length::Fill),
|
||||||
accented_icon(
|
accented_icon(
|
||||||
status_icon(Menu::Battery, &self.status).0,
|
status_icon(Menu::Battery, &self.status).0,
|
||||||
40,
|
24,
|
||||||
battery_color,
|
battery_color,
|
||||||
color_with_opacity(theme.accent, p),
|
color_with_opacity(theme.accent, p),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
.align_y(Alignment::Center),
|
.align_y(Alignment::Center),
|
||||||
)
|
)
|
||||||
.padding([10, 0])
|
.padding([2, 0])
|
||||||
.class(theme::Container::custom(move |_| container::Style {
|
.class(theme::Container::custom(move |_| container::Style {
|
||||||
text_color: Some(battery_color),
|
text_color: Some(battery_color),
|
||||||
icon_color: Some(battery_color),
|
icon_color: Some(battery_color),
|
||||||
@@ -512,7 +525,6 @@ impl FereseShell {
|
|||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
rows = rows.push(level_meter(b.percent, battery_color, p));
|
rows = rows.push(level_meter(b.percent, battery_color, p));
|
||||||
rows = rows.push(text(&b.status).size(14));
|
|
||||||
rows = rows.push(
|
rows = rows.push(
|
||||||
text(match b.status.as_str() {
|
text(match b.status.as_str() {
|
||||||
"Charging" => "Connected to power",
|
"Charging" => "Connected to power",
|
||||||
@@ -582,19 +594,25 @@ impl FereseShell {
|
|||||||
..Color::from_rgb8(230, 172, 90)
|
..Color::from_rgb8(230, 172, 90)
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
let panel = container(rows)
|
let content: Element<'_, cosmic::Action<Message>> = if kind == Menu::System {
|
||||||
.width(WIDTH)
|
rows.into()
|
||||||
.padding(12)
|
} else {
|
||||||
.class(theme::Container::custom(move |_| container::Style {
|
control_card(rows.into(), primary, p)
|
||||||
background: None,
|
};
|
||||||
text_color: Some(primary),
|
let panel =
|
||||||
icon_color: Some(primary),
|
container(content)
|
||||||
border: Border {
|
.width(kind.width())
|
||||||
radius: theme.material_radius.into(),
|
.padding(12)
|
||||||
|
.class(theme::Container::custom(move |_| container::Style {
|
||||||
|
background: None,
|
||||||
|
text_color: Some(primary),
|
||||||
|
icon_color: Some(primary),
|
||||||
|
border: Border {
|
||||||
|
radius: theme.material_radius.into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
}));
|
||||||
..Default::default()
|
|
||||||
}));
|
|
||||||
// Keep the outer surface transparent; only inner cards paint a background.
|
// Keep the outer surface transparent; only inner cards paint a background.
|
||||||
cosmic::widget::autosize::autosize(
|
cosmic::widget::autosize::autosize(
|
||||||
super::motion::animated(panel.into(), p, menu.regions.clone()),
|
super::motion::animated(panel.into(), p, menu.regions.clone()),
|
||||||
@@ -602,8 +620,8 @@ impl FereseShell {
|
|||||||
)
|
)
|
||||||
.limits(
|
.limits(
|
||||||
Limits::NONE
|
Limits::NONE
|
||||||
.min_width(WIDTH)
|
.min_width(kind.width())
|
||||||
.max_width(WIDTH)
|
.max_width(kind.width())
|
||||||
.max_height(720.0),
|
.max_height(720.0),
|
||||||
)
|
)
|
||||||
.into()
|
.into()
|
||||||
@@ -618,39 +636,52 @@ fn status_summary<'a>(
|
|||||||
muted: Color,
|
muted: Color,
|
||||||
accent: Color,
|
accent: Color,
|
||||||
opacity: f32,
|
opacity: f32,
|
||||||
|
toggle: Option<(bool, Action)>,
|
||||||
) -> Element<'a, cosmic::Action<Message>> {
|
) -> Element<'a, cosmic::Action<Message>> {
|
||||||
let badge = container(accented_icon(source, 24, primary, accent))
|
let enabled = toggle.as_ref().is_some_and(|(enabled, _)| *enabled);
|
||||||
.width(48)
|
let badge_color = if enabled { accent } else { primary };
|
||||||
.height(48)
|
let badge = container(accented_icon(source, 22, primary, accent))
|
||||||
|
.width(32)
|
||||||
|
.height(32)
|
||||||
.align_x(alignment::Horizontal::Center)
|
.align_x(alignment::Horizontal::Center)
|
||||||
.align_y(alignment::Vertical::Center)
|
.align_y(alignment::Vertical::Center)
|
||||||
.class(theme::Container::custom(move |_| container::Style {
|
.class(theme::Container::custom(move |_| container::Style {
|
||||||
background: Some(Background::Color(Color {
|
background: Some(Background::Color(Color {
|
||||||
a: 0.06 * opacity,
|
a: if enabled {
|
||||||
..primary
|
0.22 * opacity
|
||||||
|
} else {
|
||||||
|
0.06 * opacity
|
||||||
|
},
|
||||||
|
..badge_color
|
||||||
})),
|
})),
|
||||||
border: Border {
|
border: Border {
|
||||||
radius: 14.0.into(),
|
radius: 9.0.into(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}));
|
}));
|
||||||
container(
|
let badge: Element<'_, cosmic::Action<Message>> = if let Some((_, action)) = toggle {
|
||||||
row![
|
button::custom(badge)
|
||||||
badge,
|
.padding(0)
|
||||||
column![
|
.name(if enabled { "Turn off" } else { "Turn on" })
|
||||||
text(title).size(18),
|
.class(button_style(badge_color, false, opacity))
|
||||||
text(subtitle).size(12).class(theme::Text::Color(muted)),
|
.on_press(cosmic::Action::App(Message::Control(action)))
|
||||||
]
|
.into()
|
||||||
.spacing(4)
|
} else {
|
||||||
.width(Length::Fill)
|
badge.into()
|
||||||
|
};
|
||||||
|
let summary = row![
|
||||||
|
badge,
|
||||||
|
column![
|
||||||
|
text(title).size(14),
|
||||||
|
text(subtitle).size(12).class(theme::Text::Color(muted)),
|
||||||
]
|
]
|
||||||
.spacing(12)
|
.spacing(4)
|
||||||
.align_y(Alignment::Center),
|
.width(Length::Fill)
|
||||||
)
|
]
|
||||||
.id("ferese-blur-card")
|
.spacing(8)
|
||||||
.padding([12, 0])
|
.align_y(Alignment::Center);
|
||||||
.into()
|
summary.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn level_meter(
|
fn level_meter(
|
||||||
@@ -1017,6 +1048,25 @@ fn status_icon(kind: Menu, s: &Snapshot) -> (&'static [u8], bool) {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn smaller_popups_have_content_specific_widths() {
|
||||||
|
assert_eq!(Menu::System.width(), 320.0);
|
||||||
|
assert_eq!(Menu::Network.width(), 280.0);
|
||||||
|
assert_eq!(Menu::Bluetooth.width(), 280.0);
|
||||||
|
assert_eq!(Menu::Battery.width(), 250.0);
|
||||||
|
assert_eq!(Menu::Audio.width(), 290.0);
|
||||||
|
assert_eq!(Menu::Notifications.width(), 290.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn short_device_lists_shrink_and_long_lists_are_bounded() {
|
||||||
|
for count in 0..=4 {
|
||||||
|
assert_eq!(device_list_height(count), None);
|
||||||
|
}
|
||||||
|
for count in [5, 20, 1000] {
|
||||||
|
assert_eq!(device_list_height(count), Some(180.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
use super::*;
|
use super::*;
|
||||||
#[test]
|
#[test]
|
||||||
fn completed_open_stops_requesting_animation_ticks() {
|
fn completed_open_stops_requesting_animation_ticks() {
|
||||||
|
|||||||
Reference in new issue
Block a user