Speed up per-frame layout, hit testing, and stacking paths
Split ScrollingLayout validation so the per-frame geometry solve only checks scalars and keeps the full tree validation behind debug_assert, removing a HashSet build from every frame. Replace the per-region Vec in decode_regions with a stack array via as_chunks, and skip the window hit test in PointerMotion when Overview is not presenting, where visual_scale_for_window returns 1.0 regardless. Guard sync_window_stacking behind a check that the space already matches the target order with uniform z_index, avoiding an O(n^2) raise_element loop on every redraw. Narrow window_under_visual to the space iterator outside Overview, keeping the collected and sorted candidate list for the Overview branch where front-to-back order is load-bearing. Raise rust-version to 1.98 and clear the resulting Clippy findings across the workspace: let-chain collapses, clamp, and while-let loops.
This commit is contained in:
1 parent
391f6b4e37
commit
67192c0c04
25 files changed
+886
-700
No files matched your search
@@ -8,14 +8,6 @@ mod recording;
|
||||
mod status;
|
||||
mod status_ui;
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
// Older libcosmic backends expose native surfaces in frame events. Keep
|
||||
// that compatibility path gated; current backends use Window::Opened and
|
||||
// window::run to retrieve handles without updates at the display refresh rate.
|
||||
static EFFECT_FRAME_PENDING: AtomicBool = AtomicBool::new(true);
|
||||
|
||||
use cosmic::Element;
|
||||
use cosmic::app::{Core, Settings, Task};
|
||||
use cosmic::iced::alignment;
|
||||
@@ -37,6 +29,8 @@ use ferese_protocols::effects::v1::client::{
|
||||
ferese_surface_effects_v1::{self, FereseSurfaceEffectsV1},
|
||||
};
|
||||
use jiff::Zoned;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
use wayland_client::{
|
||||
Connection, Dispatch, EventQueue, Proxy, QueueHandle, delegate_noop,
|
||||
globals::{GlobalListContents, registry_queue_init},
|
||||
@@ -48,6 +42,10 @@ use crate::control::{ShellControl, ShellSnapshot};
|
||||
|
||||
const APP_ID: &str = "dev.ferese.Shell";
|
||||
|
||||
// Older libcosmic backends expose native surfaces in frame events. Keep
|
||||
// that compatibility path gated; current backends use Window::Opened and
|
||||
// window::run to retrieve handles without updates at the display refresh rate.
|
||||
static EFFECT_FRAME_PENDING: AtomicBool = AtomicBool::new(true);
|
||||
static SHELL_FONT: std::sync::OnceLock<std::sync::RwLock<cosmic::font::Font>> =
|
||||
std::sync::OnceLock::new();
|
||||
|
||||
@@ -117,10 +115,11 @@ fn stacked_clock_digits(label: &str) -> Option<[String; 2]> {
|
||||
let mut parts = label.split_whitespace();
|
||||
let time = parts.next()?;
|
||||
|
||||
if let Some(period) = parts.next() {
|
||||
if !period.eq_ignore_ascii_case("am") && !period.eq_ignore_ascii_case("pm") {
|
||||
return None;
|
||||
}
|
||||
if let Some(period) = parts.next()
|
||||
&& !period.eq_ignore_ascii_case("am")
|
||||
&& !period.eq_ignore_ascii_case("pm")
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
if parts.next().is_some() {
|
||||
@@ -650,8 +649,7 @@ impl cosmic::Application for FereseShell {
|
||||
let open = self.open_menu(kind, anchor);
|
||||
Task::batch([destroy, open])
|
||||
} else {
|
||||
let open = self.open_menu(kind, anchor);
|
||||
open
|
||||
self.open_menu(kind, anchor)
|
||||
}
|
||||
}
|
||||
Message::ConfirmPower(action) => {
|
||||
@@ -680,10 +678,11 @@ impl cosmic::Application for FereseShell {
|
||||
.status_service
|
||||
.send(status::Action::PowerProfile(profile))
|
||||
.err();
|
||||
if self.status_error.is_none() {
|
||||
if let Some(profiles) = &mut self.status.power_profiles {
|
||||
profiles.active = profile.to_owned();
|
||||
}
|
||||
|
||||
if self.status_error.is_none()
|
||||
&& let Some(profiles) = &mut self.status.power_profiles
|
||||
{
|
||||
profiles.active = profile.to_owned();
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
@@ -706,22 +705,27 @@ impl cosmic::Application for FereseShell {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
self.status_error = self.status_service.send(action.clone()).err();
|
||||
|
||||
if self.status_error.is_none() {
|
||||
self.optimistic_status(&action);
|
||||
}
|
||||
|
||||
if matches!(
|
||||
action,
|
||||
status::Action::Notifications | status::Action::Settings
|
||||
) {
|
||||
return self.close_menu();
|
||||
}
|
||||
|
||||
Task::none()
|
||||
}
|
||||
Message::Event(event, id) => self.handle_event(event, id),
|
||||
Message::Tick => {
|
||||
self.recorder.poll();
|
||||
self.clock = current_time();
|
||||
|
||||
if self.config.desktop_widgets.clock.enabled {
|
||||
self.desktop_clock = self
|
||||
.config
|
||||
@@ -730,6 +734,7 @@ impl cosmic::Application for FereseShell {
|
||||
.labels(&Zoned::now())
|
||||
.unwrap_or_default();
|
||||
}
|
||||
|
||||
let mut reload_task = Task::none();
|
||||
if let Some(control) = &self.control {
|
||||
let poll = control.poll();
|
||||
@@ -737,15 +742,19 @@ impl cosmic::Application for FereseShell {
|
||||
if poll.disconnected {
|
||||
return cosmic::iced::exit();
|
||||
}
|
||||
|
||||
if let Some(active) = poll.overview_active {
|
||||
self.overview_active = active;
|
||||
}
|
||||
|
||||
if let Some(source) = poll.config {
|
||||
reload_task = self.reload_config(source);
|
||||
}
|
||||
|
||||
if let Some(snapshot) = poll.snapshot {
|
||||
self.snapshot = snapshot;
|
||||
let mut tasks = vec![reload_task];
|
||||
|
||||
for entry in &mut self.outputs {
|
||||
let output = self
|
||||
.snapshot
|
||||
@@ -754,16 +763,16 @@ impl cosmic::Application for FereseShell {
|
||||
.find(|output| Some(output.name.as_str()) == entry.name.as_deref())
|
||||
.map(|output| output.id);
|
||||
let hidden = output_bar_hidden(&self.snapshot, output);
|
||||
|
||||
if entry.hidden == hidden {
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.hidden = hidden;
|
||||
if let Some(effects) = &entry.effects {
|
||||
if let Err(error) = effects.set_visible(!hidden) {
|
||||
eprintln!(
|
||||
"ferese-shell: could not update panel material: {error}"
|
||||
);
|
||||
}
|
||||
if let Some(effects) = &entry.effects
|
||||
&& let Err(error) = effects.set_visible(!hidden)
|
||||
{
|
||||
eprintln!("ferese-shell: could not update panel material: {error}");
|
||||
}
|
||||
tasks.push(set_input_zone(
|
||||
entry.bar,
|
||||
@@ -1058,7 +1067,6 @@ impl FereseShell {
|
||||
// accounts for the top margin separately.
|
||||
exclusive_zone: (bar.height.round() as i32)
|
||||
.saturating_add(shell_theme.bar_window_gap),
|
||||
..Default::default()
|
||||
},
|
||||
Some(Box::new(move |app| app.view_layer(bar_surface_id))),
|
||||
);
|
||||
@@ -1113,12 +1121,11 @@ impl FereseShell {
|
||||
.notes
|
||||
.iter_mut()
|
||||
.find(|note| note.id == id)
|
||||
&& (note.text != value || self.note_error.is_some())
|
||||
{
|
||||
if note.text != value || self.note_error.is_some() {
|
||||
note.text = value.clone();
|
||||
self.note_pending
|
||||
.push(note_store::Edit::Text(id.to_owned(), value));
|
||||
}
|
||||
note.text = value.clone();
|
||||
self.note_pending
|
||||
.push(note_store::Edit::Text(id.to_owned(), value));
|
||||
}
|
||||
self.flush_note_changes()
|
||||
}
|
||||
@@ -1227,7 +1234,6 @@ impl FereseShell {
|
||||
exclusive_zone: -1,
|
||||
size: Some((Some(dimensions.0), Some(dimensions.1))),
|
||||
size_limits: Limits::NONE,
|
||||
..Default::default()
|
||||
},
|
||||
Some(Box::new(Self::view_note_drag)),
|
||||
);
|
||||
@@ -1356,7 +1362,6 @@ impl FereseShell {
|
||||
exclusive_zone: -1,
|
||||
size: Some((Some(note.width), Some(note.height))),
|
||||
size_limits: Limits::NONE,
|
||||
..Default::default()
|
||||
}
|
||||
},
|
||||
Some(Box::new(move |app| app.view_note(¬e_id, id))),
|
||||
@@ -1581,7 +1586,6 @@ impl FereseShell {
|
||||
exclusive_zone: -1,
|
||||
size: Some((Some(clock.width), Some(clock.height))),
|
||||
size_limits: Limits::NONE,
|
||||
..Default::default()
|
||||
}
|
||||
},
|
||||
Some(Box::new(move |app| app.view_desktop_clock(id))),
|
||||
@@ -1854,14 +1858,13 @@ impl FereseShell {
|
||||
}
|
||||
}
|
||||
|
||||
cosmic::iced::mouse::Event::ButtonReleased(cosmic::iced::mouse::Button::Left) => {
|
||||
cosmic::iced::mouse::Event::ButtonReleased(cosmic::iced::mouse::Button::Left)
|
||||
if self
|
||||
.note_drag
|
||||
.as_ref()
|
||||
.is_some_and(|drag| drag.source == id)
|
||||
{
|
||||
return self.finish_note_drag(true);
|
||||
}
|
||||
.is_some_and(|drag| drag.source == id) =>
|
||||
{
|
||||
return self.finish_note_drag(true);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -1987,20 +1990,20 @@ impl FereseShell {
|
||||
surface,
|
||||
frame_id,
|
||||
))) if self.menu.as_ref().is_some_and(|menu| menu.id == frame_id) => {
|
||||
if let Some(menu) = &mut self.menu {
|
||||
if menu.effects.is_none() {
|
||||
match EffectsBinding::attach_role(
|
||||
&surface,
|
||||
menu.kind.material_role(),
|
||||
menu.progress(),
|
||||
) {
|
||||
Ok(binding) => menu.effects = Some(binding),
|
||||
Err(error) => {
|
||||
eprintln!("ferese-shell: popover material unavailable: {error}")
|
||||
}
|
||||
if let Some(menu) = &mut self.menu
|
||||
&& menu.effects.is_none()
|
||||
{
|
||||
match EffectsBinding::attach_role(
|
||||
&surface,
|
||||
menu.kind.material_role(),
|
||||
menu.progress(),
|
||||
) {
|
||||
Ok(binding) => menu.effects = Some(binding),
|
||||
Err(error) => {
|
||||
eprintln!("ferese-shell: popover material unavailable: {error}")
|
||||
}
|
||||
EFFECT_FRAME_PENDING.store(false, Ordering::Relaxed);
|
||||
}
|
||||
EFFECT_FRAME_PENDING.store(false, Ordering::Relaxed);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
@@ -2022,6 +2025,7 @@ impl FereseShell {
|
||||
.is_some_and(|entry| entry.id == layer_id) =>
|
||||
{
|
||||
let entry = self.notification_surface.as_mut().unwrap();
|
||||
|
||||
if entry.effects.is_none() {
|
||||
match EffectsBinding::attach_role(&surface, None, 1.0) {
|
||||
Ok(binding) => entry.effects = Some(binding),
|
||||
@@ -2049,6 +2053,7 @@ impl FereseShell {
|
||||
let Some(entry) = self.outputs.iter_mut().find(|entry| entry.bar == id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if entry.effects.is_some() {
|
||||
return;
|
||||
}
|
||||
@@ -2493,10 +2498,10 @@ fn clamp_note_position(
|
||||
cosmic::iced::Point::new(
|
||||
position
|
||||
.x
|
||||
.clamp(0., ((output.0 as f32 - size.0 as f32).max(0.)).min(8192.)),
|
||||
.clamp(0., (output.0 as f32 - size.0 as f32).clamp(0., 8192.)),
|
||||
position
|
||||
.y
|
||||
.clamp(0., ((output.1 as f32 - size.1 as f32).max(0.)).min(8192.)),
|
||||
.clamp(0., (output.1 as f32 - size.1 as f32).clamp(0., 8192.)),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -520,7 +520,6 @@ impl FereseShell {
|
||||
color: color_with_opacity(palette.text_muted, 0.18 * opacity),
|
||||
width: if history { 0.0 } else { 1.0 },
|
||||
radius: palette.material_radius.into(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}));
|
||||
|
||||
@@ -129,7 +129,9 @@ impl Server {
|
||||
title: limited(summary, 160),
|
||||
body: limited(body, 500),
|
||||
actions: actions
|
||||
.chunks_exact(2)
|
||||
.as_chunks::<2>()
|
||||
.0
|
||||
.iter()
|
||||
.take(8)
|
||||
.map(|pair| (limited(&pair[0], 128), limited(&pair[1], 40)))
|
||||
.collect(),
|
||||
|
||||
@@ -80,18 +80,20 @@ impl Recorder {
|
||||
let (send, receive) = mpsc::sync_channel(8);
|
||||
self.updates = Some(receive);
|
||||
self.state = State::Selecting;
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let mut output = BufReader::new(output);
|
||||
|
||||
loop {
|
||||
let mut line = String::new();
|
||||
match output.by_ref().take(65537).read_line(&mut line) {
|
||||
Ok(0) | Err(_) => break,
|
||||
Ok(_) if line.len() > 65536 => break,
|
||||
Ok(_) => {
|
||||
if let Ok(update) = serde_json::from_str::<Update>(&line) {
|
||||
if send.send(Event::Update(update)).is_err() {
|
||||
break;
|
||||
}
|
||||
if let Ok(update) = serde_json::from_str::<Update>(&line)
|
||||
&& send.send(Event::Update(update)).is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,13 +158,10 @@ impl Service {
|
||||
let mut network = dbus_cache::Cache::new("org.freedesktop.NetworkManager");
|
||||
let mut bluetooth = dbus_cache::Cache::new("org.bluez");
|
||||
|
||||
loop {
|
||||
// A poll overlapping a write is discarded below. Wait for the
|
||||
// write to finish before retrying instead of launching commands
|
||||
// repeatedly while a slow control operation is still running.
|
||||
let Some(before) = wait_for_poll(&polling) else {
|
||||
break;
|
||||
};
|
||||
// A poll overlapping a write is discarded below. Wait for the
|
||||
// write to finish before retrying instead of launching commands
|
||||
// repeatedly while a slow control operation is still running.
|
||||
while let Some(before) = wait_for_poll(&polling) {
|
||||
let mut snapshot = poll(&mut system_bus, &mut session_bus);
|
||||
snapshot.network = network.read(before, network::read);
|
||||
snapshot.bluetooth = bluetooth.read(before, bluetooth::read);
|
||||
@@ -186,16 +183,14 @@ impl Service {
|
||||
});
|
||||
|
||||
thread::spawn(move || {
|
||||
loop {
|
||||
let first = match commands.recv() {
|
||||
Ok(command) => command,
|
||||
Err(_) => break,
|
||||
};
|
||||
while let Ok(first) = commands.recv() {
|
||||
let mut pending = vec![first];
|
||||
|
||||
for command in commands.try_iter() {
|
||||
pending.retain(|(_, action)| action.key() != command.1.key());
|
||||
pending.push(command);
|
||||
}
|
||||
|
||||
for (id, action) in pending {
|
||||
{
|
||||
let mut state = shared.0.lock().unwrap();
|
||||
@@ -210,6 +205,7 @@ impl Service {
|
||||
shared.1.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
shared.0.lock().unwrap().3 = true;
|
||||
shared.1.notify_one();
|
||||
});
|
||||
|
||||
+267
-234
@@ -89,24 +89,29 @@ impl OpenMenu {
|
||||
impl FereseShell {
|
||||
pub fn open_menu(&mut self, kind: Menu, anchor: Rectangle<i32>) -> Task<Message> {
|
||||
if self.menu.as_ref().is_some_and(|menu| menu.kind == kind) {
|
||||
if let Some(menu) = &mut self.menu {
|
||||
if menu.motion.closing() {
|
||||
menu.motion.retarget(1.0, Instant::now());
|
||||
return Task::none();
|
||||
}
|
||||
if let Some(menu) = &mut self.menu
|
||||
&& menu.motion.closing()
|
||||
{
|
||||
menu.motion.retarget(1.0, Instant::now());
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
return self.close_menu();
|
||||
}
|
||||
|
||||
if kind == Menu::Calendar {
|
||||
self.calendar_offset = 0;
|
||||
}
|
||||
|
||||
let destroy = self.destroy_menu();
|
||||
if !kind.available(&self.status) {
|
||||
return destroy;
|
||||
}
|
||||
|
||||
if kind == Menu::Notifications && self.notifications.ready {
|
||||
self.notifications.toggle_history();
|
||||
}
|
||||
|
||||
let notifications = self.sync_notification_surface();
|
||||
let id = window::Id::unique();
|
||||
self.menu = Some(OpenMenu {
|
||||
@@ -303,6 +308,7 @@ impl FereseShell {
|
||||
)]
|
||||
.spacing(4)
|
||||
.align_y(Alignment::Center);
|
||||
|
||||
if kind == Menu::Recording
|
||||
&& let Some(elapsed) = self.recorder.elapsed()
|
||||
{
|
||||
@@ -312,14 +318,16 @@ impl FereseShell {
|
||||
.class(theme::Text::Color(foreground)),
|
||||
);
|
||||
}
|
||||
if kind == Menu::Battery && self.config.status.battery_percentage {
|
||||
if let Some(battery) = &self.status.battery {
|
||||
content = content.push(
|
||||
text(format!("{}%", battery.percent))
|
||||
.size(metrics.text_size)
|
||||
.class(theme::Text::Color(foreground)),
|
||||
);
|
||||
}
|
||||
|
||||
if kind == Menu::Battery
|
||||
&& self.config.status.battery_percentage
|
||||
&& let Some(battery) = &self.status.battery
|
||||
{
|
||||
content = content.push(
|
||||
text(format!("{}%", battery.percent))
|
||||
.size(metrics.text_size)
|
||||
.class(theme::Text::Color(foreground)),
|
||||
);
|
||||
}
|
||||
if kind == Menu::Notifications
|
||||
&& self
|
||||
@@ -492,6 +500,7 @@ impl FereseShell {
|
||||
let combined = menu.kind == Menu::System;
|
||||
if combined {
|
||||
let mut connections = row::with_capacity(3).spacing(8).width(Length::Fill);
|
||||
|
||||
if let Some(n) = &self.status.network {
|
||||
connections = connections.push(connection_control(
|
||||
"Wi-Fi",
|
||||
@@ -507,6 +516,7 @@ impl FereseShell {
|
||||
p,
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(b) = &self.status.bluetooth {
|
||||
connections = connections.push(connection_control(
|
||||
"Bluetooth",
|
||||
@@ -522,6 +532,7 @@ impl FereseShell {
|
||||
p,
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(b) = &self.status.battery {
|
||||
connections = connections.push(connection_control(
|
||||
"Battery",
|
||||
@@ -533,6 +544,7 @@ impl FereseShell {
|
||||
p,
|
||||
));
|
||||
}
|
||||
|
||||
if self.status.network.is_some()
|
||||
|| self.status.bluetooth.is_some()
|
||||
|| self.status.battery.is_some()
|
||||
@@ -540,97 +552,102 @@ impl FereseShell {
|
||||
rows = rows.push(control_card(connections.into(), primary, p));
|
||||
}
|
||||
}
|
||||
if menu.kind == Menu::Network {
|
||||
if let Some(n) = &self.status.network {
|
||||
let connected = n.enabled && n.connection.is_some();
|
||||
rows = rows.push(status_summary(
|
||||
status_icon(Menu::Network, &self.status).0,
|
||||
if !n.enabled {
|
||||
"Wi-Fi is off"
|
||||
} else {
|
||||
n.connection.as_deref().unwrap_or("Not connected")
|
||||
},
|
||||
if connected {
|
||||
"Connected"
|
||||
} else if n.enabled {
|
||||
"No active wireless connection"
|
||||
} else {
|
||||
"Wireless connections are paused"
|
||||
},
|
||||
primary,
|
||||
muted,
|
||||
color_with_opacity(theme.accent, p),
|
||||
p,
|
||||
Some((n.enabled, Action::Wifi(!n.enabled))),
|
||||
));
|
||||
|
||||
if menu.kind == Menu::Network
|
||||
&& let Some(n) = &self.status.network
|
||||
{
|
||||
let connected = n.enabled && n.connection.is_some();
|
||||
rows = rows.push(status_summary(
|
||||
status_icon(Menu::Network, &self.status).0,
|
||||
if !n.enabled {
|
||||
"Wi-Fi is off"
|
||||
} else {
|
||||
n.connection.as_deref().unwrap_or("Not connected")
|
||||
},
|
||||
if connected {
|
||||
let strength = match n.signal {
|
||||
75.. => "Excellent",
|
||||
50..=74 => "Good",
|
||||
25..=49 => "Fair",
|
||||
_ => "Weak",
|
||||
};
|
||||
rows = rows.push(
|
||||
"Connected"
|
||||
} else if n.enabled {
|
||||
"No active wireless connection"
|
||||
} else {
|
||||
"Wireless connections are paused"
|
||||
},
|
||||
primary,
|
||||
muted,
|
||||
color_with_opacity(theme.accent, p),
|
||||
p,
|
||||
Some((n.enabled, Action::Wifi(!n.enabled))),
|
||||
));
|
||||
if connected {
|
||||
let strength = match n.signal {
|
||||
75.. => "Excellent",
|
||||
50..=74 => "Good",
|
||||
25..=49 => "Fair",
|
||||
_ => "Weak",
|
||||
};
|
||||
rows = rows.push(
|
||||
column![
|
||||
row![
|
||||
text("Signal strength").size(12).width(Length::Fill),
|
||||
text(strength).size(12)
|
||||
],
|
||||
level_meter(n.signal, primary, p),
|
||||
]
|
||||
.spacing(8),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if menu.kind == Menu::Bluetooth
|
||||
&& let Some(b) = &self.status.bluetooth
|
||||
{
|
||||
rows = rows.push(status_summary(
|
||||
status_icon(Menu::Bluetooth, &self.status).0,
|
||||
if b.enabled {
|
||||
"Bluetooth is on"
|
||||
} else {
|
||||
"Bluetooth is off"
|
||||
},
|
||||
if !b.enabled {
|
||||
"Device connections are paused"
|
||||
} else if b.devices.is_empty() {
|
||||
"No devices connected"
|
||||
} else {
|
||||
"Your connected devices"
|
||||
},
|
||||
primary,
|
||||
muted,
|
||||
color_with_opacity(theme.accent, p),
|
||||
p,
|
||||
Some((b.enabled, Action::Bluetooth(!b.enabled))),
|
||||
));
|
||||
|
||||
if b.enabled && !b.devices.is_empty() {
|
||||
let mut devices = column::with_capacity(b.devices.len()).spacing(8);
|
||||
for device in &b.devices {
|
||||
devices = devices.push(
|
||||
column![
|
||||
row![
|
||||
text("Signal strength").size(12).width(Length::Fill),
|
||||
text(strength).size(12)
|
||||
],
|
||||
level_meter(n.signal, primary, p),
|
||||
text(device).size(14),
|
||||
text("Connected").size(12).class(theme::Text::Color(muted)),
|
||||
]
|
||||
.spacing(8),
|
||||
.spacing(3),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if menu.kind == Menu::Bluetooth {
|
||||
if let Some(b) = &self.status.bluetooth {
|
||||
rows = rows.push(status_summary(
|
||||
status_icon(Menu::Bluetooth, &self.status).0,
|
||||
if b.enabled {
|
||||
"Bluetooth is on"
|
||||
|
||||
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 {
|
||||
"Bluetooth is off"
|
||||
},
|
||||
if !b.enabled {
|
||||
"Device connections are paused"
|
||||
} else if b.devices.is_empty() {
|
||||
"No devices connected"
|
||||
} else {
|
||||
"Your connected devices"
|
||||
},
|
||||
primary,
|
||||
muted,
|
||||
color_with_opacity(theme.accent, p),
|
||||
p,
|
||||
Some((b.enabled, Action::Bluetooth(!b.enabled))),
|
||||
));
|
||||
if b.enabled && !b.devices.is_empty() {
|
||||
let mut devices = column::with_capacity(b.devices.len()).spacing(8);
|
||||
for device in &b.devices {
|
||||
devices = devices.push(
|
||||
column![
|
||||
text(device).size(14),
|
||||
text("Connected").size(12).class(theme::Text::Color(muted)),
|
||||
]
|
||||
.spacing(3),
|
||||
);
|
||||
}
|
||||
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);
|
||||
}
|
||||
devices.into()
|
||||
};
|
||||
rows = rows.push(list);
|
||||
}
|
||||
}
|
||||
if combined || menu.kind == Menu::Audio {
|
||||
if let Some(a) = &self.status.audio {
|
||||
|
||||
if (combined || menu.kind == Menu::Audio)
|
||||
&& let Some(a) = &self.status.audio
|
||||
{
|
||||
let audio = column![
|
||||
row![
|
||||
text("Volume").size(13).width(Length::Fill),
|
||||
@@ -661,55 +678,57 @@ impl FereseShell {
|
||||
rows = rows.push(audio);
|
||||
}
|
||||
}
|
||||
if combined {
|
||||
if let Some(value) = self.status.brightness {
|
||||
let brightness = column![
|
||||
text("Brightness").size(13),
|
||||
slider_row(
|
||||
include_bytes!("../assets/icons/status/brightness.svg"),
|
||||
value,
|
||||
true,
|
||||
primary,
|
||||
color_with_opacity(theme.accent, p),
|
||||
p,
|
||||
|
||||
if combined && let Some(value) = self.status.brightness {
|
||||
let brightness = column![
|
||||
text("Brightness").size(13),
|
||||
slider_row(
|
||||
include_bytes!("../assets/icons/status/brightness.svg"),
|
||||
value,
|
||||
true,
|
||||
primary,
|
||||
color_with_opacity(theme.accent, p),
|
||||
p,
|
||||
)
|
||||
]
|
||||
.spacing(6);
|
||||
rows = rows.push(control_card(brightness.into(), primary, p));
|
||||
}
|
||||
|
||||
if (combined || menu.kind == Menu::Notifications)
|
||||
&& let Some(n) = &self.status.notifications
|
||||
{
|
||||
let toggle = toggle_row("Do Not Disturb", n.dnd, Action::Dnd(!n.dnd), theme, p);
|
||||
rows = rows.push(if combined {
|
||||
control_card(toggle, primary, p)
|
||||
} else {
|
||||
toggle
|
||||
});
|
||||
|
||||
if menu.kind == Menu::Notifications {
|
||||
rows = rows
|
||||
.push(
|
||||
text(if n.count == 0 {
|
||||
"No notifications".to_owned()
|
||||
} else if n.count == 1 {
|
||||
"1 notification".to_owned()
|
||||
} else {
|
||||
format!("{} notifications", n.count)
|
||||
})
|
||||
.size(13)
|
||||
.class(theme::Text::Color(muted)),
|
||||
)
|
||||
]
|
||||
.spacing(6);
|
||||
rows = rows.push(control_card(brightness.into(), primary, p));
|
||||
.push(menu_button(
|
||||
"Open notification center",
|
||||
Message::Control(Action::Notifications),
|
||||
primary,
|
||||
p,
|
||||
));
|
||||
}
|
||||
}
|
||||
if combined || menu.kind == Menu::Notifications {
|
||||
if let Some(n) = &self.status.notifications {
|
||||
let toggle = toggle_row("Do Not Disturb", n.dnd, Action::Dnd(!n.dnd), theme, p);
|
||||
rows = rows.push(if combined {
|
||||
control_card(toggle, primary, p)
|
||||
} else {
|
||||
toggle
|
||||
});
|
||||
if menu.kind == Menu::Notifications {
|
||||
rows = rows
|
||||
.push(
|
||||
text(if n.count == 0 {
|
||||
"No notifications".to_owned()
|
||||
} else if n.count == 1 {
|
||||
"1 notification".to_owned()
|
||||
} else {
|
||||
format!("{} notifications", n.count)
|
||||
})
|
||||
.size(13)
|
||||
.class(theme::Text::Color(muted)),
|
||||
)
|
||||
.push(menu_button(
|
||||
"Open notification center",
|
||||
Message::Control(Action::Notifications),
|
||||
primary,
|
||||
p,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
if menu.kind == Menu::Battery {
|
||||
if let Some(b) = &self.status.battery {
|
||||
|
||||
if menu.kind == Menu::Battery
|
||||
&& let Some(b) = &self.status.battery
|
||||
{
|
||||
let battery_color = if b.percent < self.config.status.low_battery_threshold
|
||||
&& b.status != "Charging"
|
||||
{
|
||||
@@ -752,95 +771,103 @@ impl FereseShell {
|
||||
.class(theme::Text::Color(muted)),
|
||||
);
|
||||
}
|
||||
}
|
||||
if menu.kind == Menu::Battery {
|
||||
rows = rows.push(text("Power mode").size(13).class(theme::Text::Color(muted)));
|
||||
if let Some(profiles) = &self.status.power_profiles {
|
||||
let mut modes = row::with_capacity(3).spacing(6).width(Length::Fill);
|
||||
for (index, (label, icon, profile)) in [
|
||||
|
||||
if menu.kind == Menu::Battery {
|
||||
rows = rows.push(text("Power mode").size(13).class(theme::Text::Color(muted)));
|
||||
if let Some(profiles) = &self.status.power_profiles {
|
||||
let mut modes = row::with_capacity(3).spacing(6).width(Length::Fill);
|
||||
for (index, (label, icon, profile)) in [
|
||||
(
|
||||
"Power saver",
|
||||
include_bytes!("../assets/icons/status/power-saver.svg").as_slice(),
|
||||
"power-saver",
|
||||
),
|
||||
(
|
||||
"Balanced",
|
||||
include_bytes!("../assets/icons/status/power-balanced.svg")
|
||||
.as_slice(),
|
||||
"balanced",
|
||||
),
|
||||
(
|
||||
"Performance",
|
||||
include_bytes!("../assets/icons/status/power-performance.svg")
|
||||
.as_slice(),
|
||||
"performance",
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
if profiles.available[index] {
|
||||
modes = modes.push(power_tile(
|
||||
label,
|
||||
icon,
|
||||
profile,
|
||||
profiles.active == profile,
|
||||
theme,
|
||||
p,
|
||||
));
|
||||
}
|
||||
}
|
||||
// TODO: confirm column align fix
|
||||
rows = rows.push(modes.width(Length::Fill).align_y(Alignment::Center));
|
||||
} else {
|
||||
rows = rows.push(
|
||||
text("Power Profiles service unavailable")
|
||||
.size(12)
|
||||
.class(theme::Text::Color(muted)),
|
||||
);
|
||||
}
|
||||
|
||||
let mut power = row::with_capacity(3).spacing(6).width(Length::Fill);
|
||||
for (enabled, label, icon, action) in [
|
||||
(
|
||||
"Battery saver",
|
||||
include_bytes!("../assets/icons/status/power-saver.svg").as_slice(),
|
||||
"power-saver",
|
||||
self.status.reboot,
|
||||
"Restart",
|
||||
include_bytes!("../assets/icons/status/restart.svg").as_slice(),
|
||||
Action::Reboot,
|
||||
),
|
||||
(
|
||||
"Balanced",
|
||||
include_bytes!("../assets/icons/status/power-balanced.svg").as_slice(),
|
||||
"balanced",
|
||||
self.status.poweroff,
|
||||
"Power off",
|
||||
include_bytes!("../assets/icons/status/power-off.svg").as_slice(),
|
||||
Action::Poweroff,
|
||||
),
|
||||
(
|
||||
"Performance",
|
||||
include_bytes!("../assets/icons/status/power-performance.svg")
|
||||
.as_slice(),
|
||||
"performance",
|
||||
self.status.suspend,
|
||||
"Suspend",
|
||||
include_bytes!("../assets/icons/status/suspend.svg").as_slice(),
|
||||
Action::Suspend,
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
if profiles.available[index] {
|
||||
modes = modes.push(power_tile(
|
||||
label,
|
||||
icon,
|
||||
profile,
|
||||
profiles.active == profile,
|
||||
theme,
|
||||
p,
|
||||
));
|
||||
] {
|
||||
if enabled {
|
||||
power = power.push(power_tile_action(label, icon, action, theme, p));
|
||||
}
|
||||
}
|
||||
rows = rows.push(modes);
|
||||
} else {
|
||||
|
||||
if self.status.reboot || self.status.poweroff || self.status.suspend {
|
||||
rows = rows.push(text("Power").size(13).class(theme::Text::Color(muted)));
|
||||
rows = rows.push(power.align_y(Alignment::Center).width(Length::Fill));
|
||||
}
|
||||
}
|
||||
|
||||
if !menu.kind.available(&self.status) {
|
||||
rows = rows.push(
|
||||
text("Power Profiles service unavailable")
|
||||
.size(12)
|
||||
text("Service unavailable")
|
||||
.size(13)
|
||||
.class(theme::Text::Color(muted)),
|
||||
);
|
||||
}
|
||||
let mut power = row::with_capacity(3).spacing(6).width(Length::Fill);
|
||||
for (enabled, label, icon, action) in [
|
||||
(
|
||||
self.status.reboot,
|
||||
"Restart",
|
||||
include_bytes!("../assets/icons/status/restart.svg").as_slice(),
|
||||
Action::Reboot,
|
||||
),
|
||||
(
|
||||
self.status.poweroff,
|
||||
"Power off",
|
||||
include_bytes!("../assets/icons/status/power-off.svg").as_slice(),
|
||||
Action::Poweroff,
|
||||
),
|
||||
(
|
||||
self.status.suspend,
|
||||
"Suspend",
|
||||
include_bytes!("../assets/icons/status/suspend.svg").as_slice(),
|
||||
Action::Suspend,
|
||||
),
|
||||
] {
|
||||
if enabled {
|
||||
power = power.push(power_tile_action(label, icon, action, theme, p));
|
||||
}
|
||||
}
|
||||
if self.status.reboot || self.status.poweroff || self.status.suspend {
|
||||
rows = rows.push(text("Power").size(13).class(theme::Text::Color(muted)));
|
||||
rows = rows.push(power);
|
||||
}
|
||||
}
|
||||
if !menu.kind.available(&self.status) {
|
||||
rows = rows.push(
|
||||
text("Service unavailable")
|
||||
.size(13)
|
||||
.class(theme::Text::Color(muted)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(error) = &self.status_error {
|
||||
rows = rows.push(text(error).size(12).class(theme::Text::Color(Color {
|
||||
a: p,
|
||||
..Color::from_rgb8(230, 172, 90)
|
||||
})));
|
||||
}
|
||||
|
||||
let compositor_material = menu.effects.is_some();
|
||||
let panel = container(rows)
|
||||
.id("ferese-blur-card")
|
||||
@@ -856,10 +883,10 @@ impl FereseShell {
|
||||
color: color_with_opacity(theme.text_muted, 0.18 * p),
|
||||
width: 1.0,
|
||||
radius: theme.material_radius.into(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}));
|
||||
|
||||
cosmic::widget::autosize::autosize(
|
||||
super::motion::animated(
|
||||
panel.into(),
|
||||
@@ -1111,20 +1138,20 @@ fn power_tile<'a>(
|
||||
},
|
||||
opacity,
|
||||
);
|
||||
let tile = column![bar_icon(icon, 19, foreground), text(label).size(11)]
|
||||
.spacing(3)
|
||||
.align_x(Alignment::Center);
|
||||
motion::button(
|
||||
button::custom(tile)
|
||||
.width(Length::FillPortion(1))
|
||||
.padding([8, 2])
|
||||
.on_press(cosmic::Action::App(Message::Control(Action::PowerProfile(
|
||||
profile,
|
||||
)))),
|
||||
foreground,
|
||||
selected,
|
||||
opacity,
|
||||
)
|
||||
let tile = column![
|
||||
bar_icon(icon, 19, foreground),
|
||||
text(label).size(11).align_x(Alignment::Center)
|
||||
]
|
||||
.spacing(3)
|
||||
.align_x(Alignment::Center);
|
||||
let btn = button::custom(tile.align_x(Alignment::Center))
|
||||
.width(Length::FillPortion(1))
|
||||
.padding([8, 2])
|
||||
.on_press(cosmic::Action::App(Message::Control(Action::PowerProfile(
|
||||
profile,
|
||||
))));
|
||||
|
||||
motion::button(btn, foreground, selected, opacity)
|
||||
}
|
||||
|
||||
fn power_tile_action<'a>(
|
||||
@@ -1135,6 +1162,7 @@ fn power_tile_action<'a>(
|
||||
opacity: f32,
|
||||
) -> Element<'a, cosmic::Action<Message>> {
|
||||
let foreground = color_with_opacity(theme.text_primary, opacity);
|
||||
// TODO: confirm column align fix
|
||||
motion::button(
|
||||
button::custom(
|
||||
column![bar_icon(icon, 19, foreground), text(label).size(11)]
|
||||
@@ -1623,12 +1651,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn icons_follow_real_states() {
|
||||
let mut s = Snapshot::default();
|
||||
s.audio = Some(status::Audio {
|
||||
volume: 80,
|
||||
muted: true,
|
||||
output: String::new(),
|
||||
});
|
||||
let mut s = Snapshot {
|
||||
audio: Some(status::Audio {
|
||||
volume: 80,
|
||||
muted: true,
|
||||
output: String::new(),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
status_icon(Menu::Audio, &s),
|
||||
(
|
||||
@@ -1636,6 +1666,7 @@ mod tests {
|
||||
false
|
||||
)
|
||||
);
|
||||
|
||||
s.battery = Some(status::Battery {
|
||||
percent: 2,
|
||||
status: "Charging".into(),
|
||||
@@ -1644,6 +1675,7 @@ 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,
|
||||
@@ -1656,6 +1688,7 @@ mod tests {
|
||||
status_label(Menu::Notifications, &s),
|
||||
"Notifications: do not disturb"
|
||||
);
|
||||
|
||||
s.bluetooth = Some(status::Bluetooth {
|
||||
enabled: false,
|
||||
devices: vec!["Headphones".into()],
|
||||
|
||||
Reference in new issue
Block a user