Replace user configuration with KDL and improve Rust source spacing

This commit is contained in:
Samuel Onoja committed 2026-09-27 05:46:51 +01:00
1 parent dbdfc5688e
commit 9e90de4e74
41 files changed
+1668 -559

No files matched your search

+87 -42
View File
@@ -1,4 +1,3 @@
use std::env;
use std::fs;
use std::path::PathBuf;
@@ -174,11 +173,13 @@ impl Default for BarConfig {
}
}
}
#[derive(Debug, Deserialize)]
struct PopoverConfig {
#[serde(default = "default_popover_opacity")]
opacity: f32,
}
impl Default for PopoverConfig {
fn default() -> Self {
Self {
@@ -186,6 +187,7 @@ impl Default for PopoverConfig {
}
}
}
const fn default_popover_opacity() -> f32 {
0.96
}
@@ -299,13 +301,13 @@ pub(crate) fn load() -> ShellConfig {
}
}
pub(crate) fn parse_source(source: &str) -> Result<ShellConfig, toml::de::Error> {
match toml::from_str::<FereseConfig>(source) {
pub(crate) fn parse_source(source: &str) -> Result<ShellConfig, ferese_config::Error> {
match ferese_config::from_str::<FereseConfig>(source) {
Ok(config) => {
config
.desktop_widgets
.validate()
.map_err(<toml::de::Error as serde::de::Error>::custom)?;
.map_err(ferese_config::Error::from)?;
let mut theme = shell_theme(&config.theme);
theme.material_radius =
nonnegative_or(config.appearance.corner_radius.unwrap_or(14.0), 14.0);
@@ -415,48 +417,63 @@ fn finite_or(value: f32, fallback: f32) -> f32 {
fn default_surface_base() -> String {
"#111821".to_owned()
}
fn default_text_primary() -> String {
"#F4F7FB".to_owned()
}
fn default_text_muted() -> String {
"#7F8A98".to_owned()
}
fn default_accent() -> String {
"#5B8CFF".to_owned()
}
fn default_border() -> String {
"#FFFFFF18".to_owned()
}
fn default_shadow() -> String {
"#00000055".to_owned()
}
const fn default_bar_height() -> f32 {
28.0
}
const fn default_bar_window_gap() -> i32 {
0
}
const fn default_bar_margin_top() -> i32 {
0
}
const fn default_bar_margin_horizontal() -> i32 {
0
}
const fn default_bar_radius() -> f32 {
0.0
}
const fn default_panel_padding() -> f32 {
12.0
}
const fn default_control_gap() -> f32 {
12.0
}
const fn default_shadow_offset_y() -> f32 {
4.0
}
const fn default_shadow_blur() -> f32 {
18.0
}
const fn default_shadow_opacity() -> f32 {
0.20
}
@@ -466,42 +483,59 @@ pub(crate) const fn default_background() -> [u8; 3] {
}
pub(crate) fn config_path() -> Option<PathBuf> {
if let Some(directory) = env::var_os("XDG_CONFIG_HOME").filter(|value| !value.is_empty()) {
return Some(PathBuf::from(directory).join("ferese/config.toml"));
}
env::var_os("HOME")
.filter(|value| !value.is_empty())
.map(PathBuf::from)
.map(|home| home.join(".config/ferese/config.toml"))
ferese_config::config_path()
}
#[cfg(test)]
mod tests {
#[test]
fn desktop_clock_parses_and_rejects_invalid_reload_values() {
let clock = super::parse_source("[desktop_widgets.clock]\nenabled = true\nanchor = 'bottom_right'\nfont_family = ''\ncolor = ''\ntime_zone = ''").unwrap().desktop_widgets.clock;
let clock = super::parse_source(
r#"desktop-widgets {
clock {
enabled #true
anchor "bottom_right"
font-family ""
color ""
time-zone ""
}
}
"#,
)
.unwrap()
.desktop_widgets
.clock;
assert!(clock.enabled);
assert_eq!(clock.anchor, ferese_core::desktop::Anchor::BottomRight);
assert!(super::parse_source("[desktop_widgets.clock]\nopacity = 1.1").is_err());
assert!(super::parse_source("[desktop_widgets.clock]\ntime_format = '%'").is_err());
assert!(
super::parse_source("desktop-widgets {\n clock {\n opacity 1.1\n }\n}\n")
.is_err()
);
assert!(
super::parse_source(
"desktop-widgets {\n clock {\n time-format \"%\"\n }\n}\n"
)
.is_err()
);
}
use super::*;
#[test]
fn parses_theme_without_rejecting_compositor_sections() {
let config: FereseConfig = toml::from_str(
r#"
[layout]
mode = "scrolling"
[theme.typography]
font_family = "JetBrainsMono Nerd Font"
[theme.background]
path = "/tmp/wallpaper.png"
mode = "fit"
"#,
let config: FereseConfig = ferese_config::from_str(
r#"layout {
mode "scrolling"
}
theme {
typography {
font-family "JetBrainsMono Nerd Font"
}
background {
path "/tmp/wallpaper.png"
mode "fit"
}
}
"#,
)
.unwrap();
@@ -518,7 +552,7 @@ mod tests {
#[test]
fn defaults_to_the_ferese_visual_profile() {
let config: FereseConfig = toml::from_str("").unwrap();
let config: FereseConfig = ferese_config::from_str("").unwrap();
assert_eq!(config.theme.typography.font_family, None);
assert_eq!(config.theme.background.path, None);
@@ -535,13 +569,17 @@ mod tests {
#[test]
fn bar_palette_can_be_changed_without_changing_popovers() {
let config: FereseConfig = toml::from_str(
r##"
[theme.surface.bar]
background = "#EAECEEDD"
text_primary = "#222222"
text_muted = "#666666"
"##,
let config: FereseConfig = ferese_config::from_str(
r##"theme {
surface {
bar {
background "#EAECEEDD"
text-primary "#222222"
text-muted "#666666"
}
}
}
"##,
)
.unwrap();
let theme = shell_theme(&config.theme);
@@ -553,13 +591,20 @@ mod tests {
#[test]
fn bar_transparency_and_shadow_opacity_are_independent() {
let config: FereseConfig = toml::from_str(
r#"
[theme.surface.bar]
opacity = 0.6
[theme.shadow.soft]
opacity = 0.07
"#,
let config: FereseConfig = ferese_config::from_str(
r#"theme {
surface {
bar {
opacity 0.6
}
}
shadow {
soft {
opacity 0.07
}
}
}
"#,
)
.unwrap();
let theme = shell_theme(&config.theme);
+5
View File
@@ -689,6 +689,7 @@ impl FereseShell {
}
Task::batch(tasks)
}
fn output_event(
&mut self,
event: wayland::OutputEvent,
@@ -2264,6 +2265,7 @@ impl EffectsBinding {
self.connection.flush()?;
Ok(())
}
fn set_opacity(&self, opacity: f32) -> Result<(), Box<dyn std::error::Error>> {
let opacity = (opacity.clamp(0.0, 1.0) * 1000.0).round() as u32;
if self.surface.version() >= 3 && self.opacity.get() != Some(opacity) {
@@ -2349,6 +2351,7 @@ mod tests {
Point::new(200., 100.)
);
}
#[test]
fn note_drag_origin_and_bounds_use_logical_output_coordinates() {
use cosmic::iced::Point;
@@ -2371,6 +2374,7 @@ mod tests {
Point::ORIGIN
);
}
#[test]
fn desktop_clock_anchor_margins_match_only_anchored_edges() {
use ferese_core::desktop::{Anchor as Position, Clock};
@@ -2396,6 +2400,7 @@ mod tests {
(0, 0, 0, 0)
);
}
#[test]
fn clock_uses_lowercase_date_and_twelve_hour_time() {
for (stamp, expected) in [
+44
View File
@@ -13,6 +13,7 @@ pub(crate) struct Settings {
reduced_motion: bool,
speed: f64,
}
impl Default for Settings {
fn default() -> Self {
Self {
@@ -22,10 +23,12 @@ impl Default for Settings {
}
}
}
impl Settings {
fn hover_duration(self) -> std::time::Duration {
self.duration(120.0)
}
pub(crate) fn duration(self, milliseconds: f64) -> std::time::Duration {
if !self.enabled || self.reduced_motion {
return std::time::Duration::ZERO;
@@ -52,6 +55,7 @@ pub(crate) struct PopupMotion {
started: Option<std::time::Instant>,
settings: Settings,
}
impl PopupMotion {
pub(crate) fn update_settings(&mut self, settings: Settings) {
let now = std::time::Instant::now();
@@ -59,6 +63,7 @@ impl PopupMotion {
self.settings = settings;
self.started = Some(now);
}
pub(crate) fn new(settings: Settings) -> Self {
Self {
start: 0.0,
@@ -67,11 +72,13 @@ impl PopupMotion {
settings,
}
}
pub(crate) fn begin(&mut self, now: std::time::Instant) {
if self.started.is_none() {
self.started = Some(now);
}
}
pub(crate) fn progress_at(&self, now: std::time::Instant) -> f32 {
let duration = self
.settings
@@ -89,15 +96,19 @@ impl PopupMotion {
}
self.start + (self.target - self.start) * t * t * (3.0 - 2.0 * t)
}
pub(crate) fn progress(&self) -> f32 {
self.progress_at(std::time::Instant::now())
}
pub(crate) fn closing(&self) -> bool {
self.target == 0.0
}
pub(crate) fn animating(&self) -> bool {
self.progress() != self.target
}
pub(crate) fn retarget(&mut self, target: f32, now: std::time::Instant) {
self.start = self.progress_at(now);
self.target = target;
@@ -157,6 +168,7 @@ struct HoverState {
target: f32,
started: Option<std::time::Instant>,
}
impl HoverState {
fn advance(
&mut self,
@@ -185,11 +197,13 @@ impl HoverState {
self.current != target
}
}
struct Hover<'a, M> {
content: Element<'a, M>,
progress: std::rc::Rc<std::cell::Cell<f32>>,
duration: std::time::Duration,
}
impl<M> Widget<M, Theme, cosmic::Renderer> for Hover<'_, M> {
fn a11y_nodes(
&self,
@@ -201,21 +215,27 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Hover<'_, M> {
.as_widget()
.a11y_nodes(layout, &tree.children[0], cursor)
}
fn tag(&self) -> widget::tree::Tag {
widget::tree::Tag::of::<HoverState>()
}
fn state(&self) -> widget::tree::State {
widget::tree::State::new(HoverState::default())
}
fn children(&self) -> Vec<widget::Tree> {
vec![widget::Tree::new(&self.content)]
}
fn diff(&mut self, tree: &mut widget::Tree) {
tree.diff_children(std::slice::from_mut(&mut self.content));
}
fn size(&self) -> Size<Length> {
self.content.as_widget().size()
}
fn layout(
&mut self,
tree: &mut widget::Tree,
@@ -226,6 +246,7 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Hover<'_, M> {
.as_widget_mut()
.layout(&mut tree.children[0], renderer, limits)
}
fn update(
&mut self,
tree: &mut widget::Tree,
@@ -255,6 +276,7 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Hover<'_, M> {
viewport,
);
}
fn draw(
&self,
tree: &widget::Tree,
@@ -277,6 +299,7 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Hover<'_, M> {
viewport,
);
}
fn mouse_interaction(
&self,
tree: &widget::Tree,
@@ -293,6 +316,7 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Hover<'_, M> {
renderer,
)
}
fn operate(
&mut self,
tree: &mut widget::Tree,
@@ -304,6 +328,7 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Hover<'_, M> {
.as_widget_mut()
.operate(&mut tree.children[0], layout, renderer, operation);
}
fn overlay<'a>(
&'a mut self,
tree: &'a mut widget::Tree,
@@ -333,37 +358,46 @@ pub fn animated<'a, M: 'a>(
regions,
})
}
struct Motion<'a, M> {
content: Element<'a, M>,
progress: f32,
regions: Regions,
}
impl<M> Motion<'_, M> {
fn translation(&self) -> Vector {
Vector::new(0.0, -4.0 * (1.0 - self.progress))
}
fn cursor(&self, cursor: mouse::Cursor) -> mouse::Cursor {
cursor.position().map_or(mouse::Cursor::Unavailable, |p| {
mouse::Cursor::Available(p - self.translation())
})
}
}
impl<M> Widget<M, Theme, cosmic::Renderer> for Motion<'_, M> {
fn tag(&self) -> widget::tree::Tag {
self.content.as_widget().tag()
}
fn state(&self) -> widget::tree::State {
self.content.as_widget().state()
}
fn children(&self) -> Vec<widget::Tree> {
self.content.as_widget().children()
}
fn diff(&mut self, tree: &mut widget::Tree) {
self.content.as_widget_mut().diff(tree);
}
fn size(&self) -> Size<Length> {
self.content.as_widget().size()
}
fn layout(
&mut self,
tree: &mut widget::Tree,
@@ -372,6 +406,7 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Motion<'_, M> {
) -> layout::Node {
self.content.as_widget_mut().layout(tree, renderer, limits)
}
fn update(
&mut self,
tree: &mut widget::Tree,
@@ -417,6 +452,7 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Motion<'_, M> {
tree, event, layout, cursor, renderer, clipboard, shell, viewport,
);
}
fn draw(
&self,
tree: &widget::Tree,
@@ -441,6 +477,7 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Motion<'_, M> {
)
});
}
fn mouse_interaction(
&self,
tree: &widget::Tree,
@@ -457,6 +494,7 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Motion<'_, M> {
renderer,
)
}
fn operate(
&mut self,
tree: &mut widget::Tree,
@@ -475,6 +513,7 @@ impl widget::Operation for CollectRegions {
fn traverse(&mut self, children: &mut dyn FnMut(&mut dyn widget::Operation)) {
children(self);
}
fn container(&mut self, id: Option<&widget::Id>, bounds: Rectangle) {
if id == Some(&widget::Id::new("ferese-blur-card")) {
let contains = |outer: &Rectangle, inner: &Rectangle| {
@@ -524,6 +563,7 @@ mod tests {
0.0
);
}
#[test]
fn popup_reversal_and_motion_policy_are_consistent() {
let now = std::time::Instant::now();
@@ -560,6 +600,7 @@ mod tests {
assert_eq!(motion.progress_at(now), 0.0);
}
}
#[test]
fn hover_fades_settle_and_idle_does_not_schedule_frames() {
let now = std::time::Instant::now();
@@ -578,6 +619,7 @@ mod tests {
assert!(!hover.advance(0.0, start + duration * 12, duration));
assert_eq!(hover.current, 0.0);
}
#[test]
fn hover_reversals_preserve_current_paint() {
let now = std::time::Instant::now();
@@ -591,6 +633,7 @@ mod tests {
hover.advance(0.0, now + duration, duration);
assert!(hover.current > 0.0 && hover.current < before);
}
#[test]
fn hover_obeys_global_motion_policy() {
assert_eq!(
@@ -617,6 +660,7 @@ mod tests {
assert_eq!(hover.current, 1.0);
}
}
#[test]
fn material_regions_include_cards_but_not_the_parent_or_gaps() {
let mut collector = CollectRegions(Vec::new());
+48 -37
View File
@@ -12,48 +12,45 @@ pub fn save(path: &Path, edits: &[Edit]) -> Result<(), String> {
let canonical = path.canonicalize().map_err(|e| e.to_string())?;
let path = canonical.as_path();
let source = fs::read_to_string(path).map_err(|e| e.to_string())?;
let mut doc: toml_edit::DocumentMut = source
.parse()
.map_err(|e: toml_edit::TomlError| e.to_string())?;
let mut doc = ferese_config::Document::parse(&source).map_err(|e| e.to_string())?;
for edit in edits {
if let Edit::ClockPosition(x, y) = edit {
let clock = doc
.get_mut("desktop_widgets")
.and_then(|item| item.get_mut("clock"))
.ok_or("Clock was removed from the config.")?;
clock["anchor"] = toml_edit::value("top_left");
clock["margin_x"] = toml_edit::value(i64::from(*x));
clock["margin_y"] = toml_edit::value(i64::from(*y));
if doc.get("desktop_widgets.clock").is_none() {
return Err("Clock was removed from the config.".into());
}
for (field, value) in [
("anchor", "top_left".into()),
("margin_x", i64::from(*x).into()),
("margin_y", i64::from(*y).into()),
] {
doc.set(&format!("desktop_widgets.clock.{field}"), value)
.map_err(|e| e.to_string())?;
}
continue;
}
let notes = doc
.get_mut("desktop_widgets")
.and_then(|item| item.get_mut("notes"))
.and_then(toml_edit::Item::as_array_of_tables_mut)
.ok_or("Notes were removed from the config.")?;
let id = match edit {
Edit::Text(id, _) | Edit::Position(id, _, _) => id,
Edit::ClockPosition(..) => unreachable!(),
};
let note = notes
.iter_mut()
.find(|note| {
note.get("id")
.and_then(toml_edit::Item::as_str)
.unwrap_or("note")
== id
})
let index = doc
.get("desktop_widgets.notes")
.and_then(|v| v.as_array())
.ok_or("Notes were removed from the config.")?
.iter()
.position(|note| note.get("id").and_then(|v| v.as_str()).unwrap_or("note") == id)
.ok_or("This note no longer exists in the config.")?;
match edit {
Edit::Text(_, value) => {
note["text"] = toml_edit::value(value);
}
Edit::Position(_, x, y) => {
note["anchor"] = toml_edit::value("top_left");
note["margin_x"] = toml_edit::value(i64::from(*x));
note["margin_y"] = toml_edit::value(i64::from(*y));
}
let fields = match edit {
Edit::Text(_, value) => vec![("text", value.clone().into())],
Edit::Position(_, x, y) => vec![
("anchor", "top_left".into()),
("margin_x", i64::from(*x).into()),
("margin_y", i64::from(*y).into()),
],
Edit::ClockPosition(..) => unreachable!(),
};
for (field, value) in fields {
doc.set(&format!("desktop_widgets.notes.{index}.{field}"), value)
.map_err(|e| e.to_string())?;
}
}
let updated = doc.to_string();
@@ -87,10 +84,10 @@ mod tests {
#[test]
fn clock_position_saves_without_a_notes_table() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
let path = dir.path().join("config.kdl");
fs::write(
&path,
"[desktop_widgets.clock]\nenabled = true\ntime_size = 80\n",
"desktop-widgets {\n clock {\n enabled #true\n time-size 80\n }\n}\n",
)
.unwrap();
save(&path, &[Edit::ClockPosition(120, 240)]).unwrap();
@@ -104,11 +101,25 @@ mod tests {
);
assert_eq!(config.desktop_widgets.clock.time_size, 80.);
}
#[test]
fn note_edits_preserve_unrelated_config_and_multiline_content() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
fs::write(&path, "# keep this\n[theme.colors]\naccent = '#123456'\n[[desktop_widgets.notes]]\nid = 'a'\ntext = 'old'\n").unwrap();
let path = dir.path().join("config.kdl");
fs::write(
&path,
r##"// keep this
theme {
colors {
accent "#123456"
}
}
desktop-widgets {
note id="a" text="old"
}
"##,
)
.unwrap();
save(
&path,
&[
@@ -118,7 +129,7 @@ mod tests {
)
.unwrap();
let source = fs::read_to_string(&path).unwrap();
assert!(source.contains("# keep this"));
assert!(source.contains("// keep this"));
assert!(source.contains("#123456"));
let notes = crate::config::parse_source(&source)
.unwrap()
+23
View File
@@ -23,28 +23,33 @@ pub struct Snapshot {
pub reboot: bool,
pub suspend: bool,
}
#[derive(Clone, Debug)]
pub struct Network {
pub enabled: bool,
pub connection: Option<String>,
pub signal: u8,
}
#[derive(Clone, Debug)]
pub struct Bluetooth {
pub enabled: bool,
pub devices: Vec<String>,
}
#[derive(Clone, Debug)]
pub struct Audio {
pub volume: u8,
pub muted: bool,
pub output: String,
}
#[derive(Clone, Debug)]
pub struct Battery {
pub percent: u8,
pub status: String,
}
#[derive(Clone, Debug)]
pub struct Notifications {
pub count: u32,
@@ -65,22 +70,26 @@ pub enum Action {
Reboot,
Suspend,
}
impl Action {
fn key(&self) -> std::mem::Discriminant<Self> {
std::mem::discriminant(self)
}
}
pub struct Update {
pub snapshot: Snapshot,
pub generation: u64,
pub error: Option<String>,
}
pub struct Service {
settings: Arc<Mutex<Option<Vec<String>>>>,
tx: SyncSender<(u64, Action)>,
rx: Receiver<Update>,
pub generation: u64,
}
impl Service {
pub fn start(settings: Option<Vec<String>>) -> Self {
let live_settings = Arc::new(Mutex::new(settings));
@@ -153,6 +162,7 @@ impl Service {
generation: 0,
}
}
pub fn send(&mut self, action: Action) -> Result<(), String> {
let generation = self.generation + 1;
self.tx
@@ -161,9 +171,11 @@ impl Service {
self.generation = generation;
Ok(())
}
pub fn update_settings(&self, settings: Option<Vec<String>>) {
*self.settings.lock().unwrap() = settings;
}
pub fn poll(&self) -> Option<Update> {
self.rx.try_iter().last()
}
@@ -222,6 +234,7 @@ fn poll() -> Snapshot {
suspend: can_power("CanSuspend"),
}
}
fn network() -> Option<Network> {
// No Wi-Fi device means no Wi-Fi control (wired connectivity isn't called Wi-Fi).
let devices = run("nmcli", &["-t", "-f", "TYPE", "device", "status"]).ok()?;
@@ -261,6 +274,7 @@ fn network() -> Option<Network> {
signal,
})
}
fn bluetooth() -> Option<Bluetooth> {
let info = run("bluetoothctl", &["show"]).ok()?;
let enabled = info
@@ -282,6 +296,7 @@ fn bluetooth() -> Option<Bluetooth> {
};
Some(Bluetooth { enabled, devices })
}
pub fn parse_audio(value: &str) -> Option<(u8, bool)> {
let volume = value
.strip_prefix("Volume: ")?
@@ -297,6 +312,7 @@ pub fn parse_audio(value: &str) -> Option<(u8, bool)> {
value.contains("[MUTED]"),
))
}
fn audio() -> Option<Audio> {
let (volume, muted) =
parse_audio(&run("wpctl", &["get-volume", "@DEFAULT_AUDIO_SINK@"]).ok()?)?;
@@ -312,11 +328,13 @@ fn audio() -> Option<Audio> {
output,
})
}
fn read(path: &Path, name: &str) -> Option<String> {
fs::read_to_string(path.join(name))
.ok()
.map(|s| s.trim().to_owned())
}
fn battery() -> Option<Battery> {
let entries = fs::read_dir("/sys/class/power_supply").ok()?;
// Prefer a system battery, not a mouse/headset battery.
@@ -336,6 +354,7 @@ fn battery() -> Option<Battery> {
})
})
}
fn brightness() -> Option<u8> {
let max = run("brightnessctl", &["--class=backlight", "max"])
.ok()?
@@ -348,6 +367,7 @@ fn brightness() -> Option<u8> {
(max > 0.0 && current.is_finite())
.then(|| (100.0 * current / max).round().clamp(0.0, 100.0) as u8)
}
fn notifications() -> Option<Notifications> {
// Check service ownership first: swaync-client otherwise waits indefinitely.
let owner = run(
@@ -375,6 +395,7 @@ fn notifications() -> Option<Notifications> {
};
Some(Notifications { count, dnd })
}
fn can_power(method: &str) -> bool {
run(
"busctl",
@@ -389,6 +410,7 @@ fn can_power(method: &str) -> bool {
)
.is_ok_and(|s| s == "s \"yes\"" || s == "s \"challenge\"")
}
fn execute(action: &Action, settings: Option<&[String]>) -> Result<(), String> {
match action {
Action::Wifi(on) => run("nmcli", &["radio", "wifi", if *on { "on" } else { "off" }]),
@@ -462,6 +484,7 @@ mod tests {
assert_eq!(parse_audio(value), None);
}
}
#[test]
fn default_snapshot_does_not_invent_services() {
let s = Snapshot::default();
+13
View File
@@ -19,6 +19,7 @@ pub enum Menu {
Notifications,
System,
}
impl Menu {
fn width(self) -> f32 {
match self {
@@ -28,10 +29,12 @@ impl Menu {
Self::Audio | Self::Notifications => 290.0,
}
}
pub(super) fn material_role(self) -> Option<ferese_surface_effects_v1::Role> {
// Popovers contain independent cards, with no shared material backing.
None
}
fn available(self, status: &Snapshot) -> bool {
match self {
Self::Network => status.network.is_some(),
@@ -52,10 +55,12 @@ pub struct OpenMenu {
pub effects: Option<EffectsBinding>,
pub regions: super::motion::Regions,
}
impl OpenMenu {
pub fn progress(&self) -> f32 {
self.motion.progress()
}
pub fn animating(&self) -> bool {
self.motion.animating()
}
@@ -115,6 +120,7 @@ impl FereseShell {
);
destroy.chain(cosmic::task::message(cosmic::Action::Surface(action)))
}
pub fn destroy_menu(&mut self) -> Task<Message> {
super::EFFECT_FRAME_PENDING.store(false, std::sync::atomic::Ordering::Relaxed);
self.menu.take().map_or_else(Task::none, |menu| {
@@ -123,6 +129,7 @@ impl FereseShell {
))
})
}
pub fn close_menu(&mut self) -> Task<Message> {
let Some(menu) = &mut self.menu else {
return Task::none();
@@ -142,6 +149,7 @@ impl FereseShell {
}
self.destroy_menu()
}
pub fn animate_menu(&mut self) -> Task<Message> {
if let Some(menu) = &self.menu {
if let Some(effects) = &menu.effects {
@@ -153,6 +161,7 @@ impl FereseShell {
}
Task::none()
}
pub fn optimistic_status(&mut self, action: &Action) {
match action {
Action::Volume(v) => {
@@ -184,6 +193,7 @@ impl FereseShell {
_ => {}
}
}
pub fn view_status_bar(&self) -> Element<'_, cosmic::Action<Message>> {
let theme = self.config.theme.for_bar();
let metrics = BarMetrics::from(theme);
@@ -861,6 +871,7 @@ fn menu_button<'a>(
opacity,
)
}
fn toggle_row<'a>(
label: &'a str,
on: bool,
@@ -987,6 +998,7 @@ fn audio_icon(volume: u8, muted: bool) -> &'static [u8] {
include_bytes!("../assets/icons/status/volume-high.svg")
}
}
fn status_icon(kind: Menu, s: &Snapshot) -> (&'static [u8], bool) {
match kind {
Menu::Network => {
@@ -1111,6 +1123,7 @@ mod tests {
assert!(!Menu::Notifications.available(&s));
assert!(Menu::System.available(&s));
}
#[test]
fn icons_follow_real_states() {
let mut s = Snapshot::default();