Remove desktop clock and sticky-note widgets

This commit is contained in:
Samuel Onoja committed 2026-10-10 23:33:44 +01:00
1 parent 78c7168a92
commit b356f63b93
21 files changed
+136 -3021

No files matched your search

+50 -381
View File
@@ -5,98 +5,32 @@ use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
use std::os::unix::ffi::OsStrExt;
use std::os::unix::net::UnixStream;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use cosmic::iced::{Subscription, futures};
use ferese_config::desktop::Clock;
use jiff::{Timestamp, Zoned, tz::TimeZone};
const SECOND: i128 = 1_000_000_000;
const MINUTE: i128 = 60 * SECOND;
const UNNAMED_ZONE: &str = "ferese/unnamed-system-zone";
// Fractional formats keep the previous sampling rate; nanosecond formats must
// not turn into a nanosecond timer that continuously rebuilds the shell.
const FRACTION_SAMPLE: i128 = SECOND / 2;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Labels {
pub bar: String,
pub desktop: (String, String),
pub fn current_time() -> String {
labels(Timestamp::now(), &zones().0)
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
struct Format {
time: String,
date: Option<String>,
zone: Option<String>,
lowercase: bool,
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
struct Settings {
bar: bool,
desktop: Option<Arc<Format>>,
}
#[derive(Default)]
pub struct Service {
desktop: Option<Arc<Format>>,
}
impl Service {
pub fn new(clock: &Clock) -> Self {
let mut service = Self::default();
service.configure(clock);
service
}
pub fn configure(&mut self, clock: &Clock) {
if !clock.enabled {
self.desktop = None;
return;
}
let date = clock.show_date.then_some(clock.date_format.as_str());
let zone = clock.time_zone.as_deref().filter(|zone| !zone.is_empty());
if self.desktop.as_ref().is_some_and(|format| {
format.time == clock.time_format
&& format.date.as_deref() == date
&& format.zone.as_deref() == zone
&& format.lowercase == clock.lowercase
}) {
return;
}
self.desktop = Some(Arc::new(Format {
time: clock.time_format.clone(),
date: date.map(str::to_owned),
zone: zone.map(str::to_owned),
lowercase: clock.lowercase,
}));
}
pub fn subscription(&self, bar: bool, desktop_visible: bool) -> Subscription<Labels> {
self.settings(bar, desktop_visible)
.map_or_else(Subscription::none, |settings| Subscription::run_with(settings, stream))
}
fn settings(&self, bar: bool, desktop_visible: bool) -> Option<Settings> {
let desktop = if desktop_visible { self.desktop.clone() } else { None };
if !bar && desktop.is_none() {
return None;
}
Some(Settings { bar, desktop })
pub fn subscription(visible: bool) -> Subscription<String> {
if visible {
Subscription::run_with((), stream)
} else {
Subscription::none()
}
}
fn stream(settings: &Settings) -> impl futures::Stream<Item = Labels> + use<> {
let settings = settings.clone();
fn stream(_: &()) -> impl futures::Stream<Item = String> + use<> {
let (send, receive) = tokio::sync::mpsc::channel(1);
let stop = match UnixStream::pair() {
Ok((stop, cancel)) => {
match std::thread::Builder::new().name("ferese-clock".into()).spawn(move || {
if let Err(error) = watch(&settings, send, cancel) {
if let Err(error) = watch(send, cancel) {
eprintln!("ferese-shell: clock subscription: {error}");
}
}) {
@@ -118,22 +52,22 @@ fn stream(settings: &Settings) -> impl futures::Stream<Item = Labels> + use<> {
})
}
fn watch(settings: &Settings, send: tokio::sync::mpsc::Sender<Labels>, cancel: UnixStream) -> io::Result<()> {
fn watch(send: tokio::sync::mpsc::Sender<String>, cancel: UnixStream) -> io::Result<()> {
let timer =
owned_fd(unsafe { libc::timerfd_create(libc::CLOCK_REALTIME, libc::TFD_CLOEXEC | libc::TFD_NONBLOCK) })?;
let (mut system, mut desktop, mut paths) = zones(settings);
let (mut system, mut paths) = zones();
let mut timezone = ZoneWatch::new(&paths)?;
let mut previous = None;
loop {
let now = Timestamp::now();
let labels = labels(settings, now, &system, &desktop);
let labels = labels(now, &system);
if previous.as_ref() != Some(&labels) {
previous = Some(labels.clone());
if send.blocking_send(labels).is_err() {
return Ok(());
}
}
if arm(&timer, deadline(settings, now, &system, &desktop))? {
if arm(&timer, zone_deadline(&now.to_zoned(system.clone())))? {
continue;
}
let mut fds = [
@@ -170,7 +104,7 @@ fn watch(settings: &Settings, send: tokio::sync::mpsc::Sender<Labels>, cancel: U
}
if fds[1].revents != 0 && timezone.changed()? {
jiff::tz::db().reset();
(system, desktop, paths) = zones(settings);
(system, paths) = zones();
timezone = ZoneWatch::new(&paths)?;
}
}
@@ -220,7 +154,7 @@ fn read_timer(timer: &OwnedFd) -> io::Result<()> {
Ok(())
}
fn zones(settings: &Settings) -> (TimeZone, TimeZone, Vec<PathBuf>) {
fn zones() -> (TimeZone, Vec<PathBuf>) {
let mut paths = vec![PathBuf::from("/etc/localtime"), PathBuf::from("/etc/timezone")];
// Load the file directly: Jiff's system timezone cache lasts five minutes.
let system = match std::env::var("TZ") {
@@ -228,13 +162,7 @@ fn zones(settings: &Settings) -> (TimeZone, TimeZone, Vec<PathBuf>) {
Ok(zone) => named_zone(zone.trim_start_matches(':'), &mut paths).unwrap_or_else(TimeZone::system),
Err(_) => file_zone(Path::new("/etc/localtime"), &mut paths).unwrap_or_else(TimeZone::system),
};
let desktop = settings
.desktop
.as_ref()
.and_then(|format| format.zone.as_ref())
.and_then(|zone| named_zone(zone, &mut paths))
.unwrap_or_else(|| system.clone());
(system, desktop, paths)
(system, paths)
}
fn named_zone(name: &str, paths: &mut Vec<PathBuf>) -> Option<TimeZone> {
@@ -270,93 +198,20 @@ fn file_zone(path: &Path, paths: &mut Vec<PathBuf>) -> Option<TimeZone> {
Some(zone)
}
fn labels(settings: &Settings, now: Timestamp, system: &TimeZone, desktop: &TimeZone) -> Labels {
let bar = if settings.bar {
ferese_theme::calendar::format_bar_time(&now.to_zoned(system.clone()))
} else {
String::new()
};
let desktop = settings
.desktop
.as_ref()
.map(|format| {
let now = now.to_zoned(desktop.clone());
let mut broken = jiff::fmt::strtime::BrokenDownTime::from(&now);
if desktop.iana_name() == Some(UNNAMED_ZONE) {
// Copied localtime files have no IANA name. Preserve Jiff's
// %Q offset fallback while retaining their DST/abbreviation.
broken.set_iana_time_zone(None);
}
let time = broken.to_string(&format.time).unwrap_or_default();
let date = format
.date
.as_ref()
.map(|date| broken.to_string(date).unwrap_or_default())
.unwrap_or_default();
if format.lowercase {
(time.to_lowercase(), date.to_lowercase())
} else {
(time, date)
}
})
.unwrap_or_default();
Labels { bar, desktop }
fn labels(now: Timestamp, system: &TimeZone) -> String {
ferese_theme::calendar::format_bar_time(&now.to_zoned(system.clone()))
}
fn deadline(settings: &Settings, now: Timestamp, system: &TimeZone, desktop: &TimeZone) -> i128 {
let mut next = i128::MAX;
if settings.bar {
next = next.min(zone_deadline(&now.to_zoned(system.clone()), MINUTE));
}
if let Some(format) = &settings.desktop {
let precision =
format_precision(&format.time).min(format.date.as_deref().map(format_precision).unwrap_or(MINUTE));
next = next.min(zone_deadline(&now.to_zoned(desktop.clone()), precision));
}
next
}
fn zone_deadline(now: &Zoned, precision: i128) -> i128 {
let stamp = now.timestamp().as_nanosecond();
let next = if precision == MINUTE {
stamp + MINUTE - i128::from(now.second()) * SECOND - i128::from(now.subsec_nanosecond())
} else {
(stamp.div_euclid(precision) + 1) * precision
};
fn zone_deadline(now: &Zoned) -> i128 {
let next = now.timestamp().as_nanosecond() + MINUTE
- i128::from(now.second()) * SECOND
- i128::from(now.subsec_nanosecond());
now.time_zone()
.following(now.timestamp())
.next()
.map_or(next, |transition| next.min(transition.timestamp().as_nanosecond()))
}
fn format_precision(format: &str) -> i128 {
let mut precision = MINUTE;
let mut chars = format.chars();
while let Some(character) = chars.next() {
if character != '%' {
continue;
}
let Some(mut directive) = chars.next() else {
break;
};
if directive == '%' {
continue;
}
while matches!(directive, '_' | '-' | '0' | '^' | '#' | ':' | '.') || directive.is_ascii_digit() {
let Some(next) = chars.next() else {
return precision;
};
directive = next;
}
precision = precision.min(match directive {
'f' | 'N' => FRACTION_SAMPLE,
'S' | 's' | 'T' | 'c' | 'r' | 'X' => SECOND,
_ => MINUTE,
});
}
precision
}
struct ZoneWatch {
fd: OwnedFd,
names: Vec<(libc::c_int, OsString)>,
@@ -433,142 +288,10 @@ impl ZoneWatch {
mod tests {
use super::*;
use std::time::Duration;
fn settings(time: Option<&str>, date: Option<&str>) -> Settings {
Settings {
bar: true,
desktop: time.map(|time| {
Arc::new(Format {
time: time.into(),
date: date.map(str::to_owned),
zone: None,
lowercase: false,
})
}),
}
}
#[test]
fn subscription_rebuilds_and_style_changes_keep_the_cached_format() {
let mut clock = Clock {
enabled: true,
time_zone: Some("Asia/Kathmandu".into()),
..Clock::default()
};
let mut service = Service::new(&clock);
let first = service.settings(true, true).unwrap();
let format = first.desktop.as_ref().unwrap();
for _ in 0..100 {
let current = service.settings(true, true).unwrap();
assert!(Arc::ptr_eq(format, current.desktop.as_ref().unwrap()));
assert_eq!(first, current);
}
clock.time_size += 8.;
clock.margin_x += 10;
service.configure(&clock);
assert!(Arc::ptr_eq(format, service.desktop.as_ref().unwrap()));
assert!(service.settings(true, false).unwrap().desktop.is_none());
assert!(service.settings(false, false).is_none());
assert!(Arc::ptr_eq(
format,
service.settings(false, true).unwrap().desktop.as_ref().unwrap()
));
}
#[test]
fn format_changes_replace_subscription_identity_without_mutating_the_worker_settings() {
let mut clock = Clock {
enabled: true,
..Clock::default()
};
let mut service = Service::new(&clock);
let initial = service.settings(true, true).unwrap();
clock.time_format = "%H:%M:%S".into();
service.configure(&clock);
let seconds = service.settings(true, true).unwrap();
assert_ne!(initial, seconds);
assert_eq!(initial.desktop.as_ref().unwrap().time, "%-I:%M %p");
clock.show_date = false;
service.configure(&clock);
let no_date = service.settings(true, true).unwrap();
assert_ne!(seconds, no_date);
assert!(no_date.desktop.as_ref().unwrap().date.is_none());
clock.date_format = "%Y".into();
service.configure(&clock);
assert!(Arc::ptr_eq(
no_date.desktop.as_ref().unwrap(),
service.desktop.as_ref().unwrap()
));
clock.time_zone = Some("UTC".into());
service.configure(&clock);
assert_ne!(no_date, service.settings(true, true).unwrap());
clock.lowercase = !clock.lowercase;
let previous = service.settings(true, true).unwrap();
service.configure(&clock);
assert_ne!(previous, service.settings(true, true).unwrap());
clock.enabled = false;
service.configure(&clock);
assert!(service.settings(false, true).is_none());
assert!(service.settings(true, true).unwrap().desktop.is_none());
clock.enabled = true;
service.configure(&clock);
assert!(service.settings(false, true).unwrap().desktop.is_some());
}
#[test]
fn production_subscription_uses_visible_outputs_formats_and_hidden_date() {
let mut clock = Clock {
enabled: true,
time_format: "%H:%M".into(),
date_format: "%S%.f".into(),
..Clock::default()
};
assert_eq!(Service::new(&clock).subscription(false, false).units(), 0);
assert_eq!(Service::new(&clock).subscription(true, false).units(), 1);
let hidden = Service::new(&clock).settings(true, false).unwrap();
assert!(hidden.desktop.is_none());
let now: Timestamp = "2026-10-04T12:34:56.123Z".parse().unwrap();
assert_eq!(
deadline(&hidden, now, &TimeZone::UTC, &TimeZone::UTC),
"2026-10-04T12:35:00Z".parse::<Timestamp>().unwrap().as_nanosecond()
);
let visible = Service::new(&clock).settings(true, true).unwrap();
assert_eq!(
deadline(&visible, now, &TimeZone::UTC, &TimeZone::UTC),
"2026-10-04T12:34:56.5Z".parse::<Timestamp>().unwrap().as_nanosecond()
);
clock.show_date = false;
let hidden_date = Service::new(&clock).settings(true, true).unwrap();
assert_ne!(hidden_date, visible);
assert_eq!(
deadline(&hidden_date, now, &TimeZone::UTC, &TimeZone::UTC),
"2026-10-04T12:35:00Z".parse::<Timestamp>().unwrap().as_nanosecond()
);
clock.time_format = "%S".into();
assert_ne!(Service::new(&clock).settings(true, true).unwrap(), hidden_date);
clock.enabled = false;
assert_eq!(Service::new(&clock).subscription(false, true).units(), 0);
}
#[test]
fn named_zone_labels_match_configured_clock_including_aliases_and_case() {
let now: Timestamp = "2026-10-04T12:34:56.123Z".parse().unwrap();
for zone in ["America/New_York", "Etc/UTC", "Asia/Kathmandu", "UTC", "utc"] {
let clock = Clock {
enabled: true,
time_format: "%H:%M:%S%.3f %Q %Z".into(),
date_format: "%a %F".into(),
time_zone: Some(zone.into()),
..Clock::default()
};
let settings = Service::new(&clock).settings(true, true).unwrap();
let desktop = named_zone(zone, &mut Vec::new()).unwrap_or_else(|| panic!("zone {zone} unavailable"));
let actual = labels(&settings, now, &TimeZone::UTC, &desktop).desktop;
assert_eq!(actual, clock.labels(&now.to_zoned(TimeZone::UTC)).unwrap(), "{zone}");
}
fn panel_subscription_stops_when_all_panels_are_hidden() {
assert_eq!(subscription(false).units(), 0);
assert_eq!(subscription(true).units(), 1);
}
#[test]
@@ -590,54 +313,31 @@ mod tests {
std::fs::rename(replacement, &path).unwrap();
assert!(watcher.changed().unwrap());
let second = file_zone(&path, &mut Vec::new()).unwrap();
let bar_settings = settings(None, None);
assert_eq!(labels(&bar_settings, now, &first, &first).bar, "4 oct, 8:34 am");
assert_eq!(labels(&bar_settings, now, &second, &second).bar, "4 oct, 6:19 pm");
let settings = settings(Some("%Q %:Q %Z %z"), None);
assert_eq!(
labels(&settings, now, &first, &first).desktop.0,
"-0400 -04:00 EDT -0400"
);
assert_eq!(
labels(&settings, now, &second, &second).desktop.0,
"+0545 +05:45 +0545 +0545"
);
assert_eq!(labels(now, &first), "4 oct, 8:34 am");
assert_eq!(labels(now, &second), "4 oct, 6:19 pm");
}
#[test]
fn visible_format_precision_handles_seconds_escapes_and_sampled_fractions() {
for format in ["%-I:%M %p", "%a, %b %-d", "%%S %%f", "%:z %:::z", "fixed text"] {
assert_eq!(format_precision(format), MINUTE, "{format}");
fn realtime_jumps_recompute_boundaries_and_timezone_labels() {
let initial: Timestamp = "2026-10-04T12:34:56Z".parse().unwrap();
let first = labels(initial, &TimeZone::UTC);
for (stamp, expected) in [
("2026-10-04T11:20:01Z", "2026-10-04T11:21:00Z"),
("2026-10-04T15:40:01Z", "2026-10-04T15:41:00Z"),
] {
let now: Timestamp = stamp.parse().unwrap();
assert_ne!(labels(now, &TimeZone::UTC), first);
assert_eq!(
zone_deadline(&now.to_zoned(TimeZone::UTC)),
expected.parse::<Timestamp>().unwrap().as_nanosecond()
);
}
for format in ["%S", "%_3S", "%s", "%T", "%c", "%r", "%X"] {
assert_eq!(format_precision(format), SECOND, "{format}");
}
for format in ["%f", "%N", "%.f", "%.3f", "%9f", "%1f"] {
assert_eq!(format_precision(format), FRACTION_SAMPLE, "{format}");
}
let now: Timestamp = "2026-10-04T12:34:56.123Z".parse().unwrap();
let minute = "2026-10-04T12:35:00Z".parse::<Timestamp>().unwrap().as_nanosecond();
let zone = TimeZone::get("Asia/Kathmandu").unwrap();
assert_ne!(labels(initial, &zone), first);
assert_eq!(labels(initial, &zone), "4 oct, 6:19 pm");
assert_eq!(
deadline(&settings(None, None), now, &TimeZone::UTC, &TimeZone::UTC),
minute
);
assert_eq!(
deadline(
&settings(Some("%H:%M"), Some("%S")),
now,
&TimeZone::UTC,
&TimeZone::UTC
),
"2026-10-04T12:34:57Z".parse::<Timestamp>().unwrap().as_nanosecond()
);
assert_eq!(
deadline(
&settings(Some("%H:%M:%S%.3f"), None),
now,
&TimeZone::UTC,
&TimeZone::UTC
),
"2026-10-04T12:34:56.5Z".parse::<Timestamp>().unwrap().as_nanosecond()
labels(initial, &TimeZone::UTC),
labels(initial + jiff::SignedDuration::from_secs(1), &TimeZone::UTC)
);
}
@@ -649,7 +349,7 @@ mod tests {
] {
let now: Timestamp = stamp.parse().unwrap();
assert_eq!(
zone_deadline(&now.to_zoned(TimeZone::UTC), MINUTE),
zone_deadline(&now.to_zoned(TimeZone::UTC)),
expected.parse::<Timestamp>().unwrap().as_nanosecond()
);
}
@@ -660,48 +360,17 @@ mod tests {
] {
let now: Timestamp = stamp.parse().unwrap();
let expected: Timestamp = expected.parse().unwrap();
assert_eq!(
zone_deadline(&now.to_zoned(zone.clone()), MINUTE),
expected.as_nanosecond()
);
assert_eq!(labels(&settings(None, None), expected, &zone, &zone).bar, label);
assert_eq!(zone_deadline(&now.to_zoned(zone.clone())), expected.as_nanosecond());
assert_eq!(labels(expected, &zone), label);
}
let zone = TimeZone::fixed(jiff::tz::Offset::from_seconds(30).unwrap());
let now: Timestamp = "2026-10-04T12:34:20Z".parse().unwrap();
assert_eq!(
zone_deadline(&now.to_zoned(zone), MINUTE),
zone_deadline(&now.to_zoned(zone)),
"2026-10-04T12:34:30Z".parse::<Timestamp>().unwrap().as_nanosecond()
);
}
#[test]
fn realtime_jumps_recompute_boundaries_and_timezone_labels() {
let settings = settings(Some("%H:%M %Z"), None);
let initial: Timestamp = "2026-10-04T12:34:56Z".parse().unwrap();
let earlier: Timestamp = "2026-10-04T11:20:01Z".parse().unwrap();
let later: Timestamp = "2026-10-04T15:40:01Z".parse().unwrap();
let first = labels(&settings, initial, &TimeZone::UTC, &TimeZone::UTC);
for (now, expected) in [(earlier, "2026-10-04T11:21:00Z"), (later, "2026-10-04T15:41:00Z")] {
assert_ne!(labels(&settings, now, &TimeZone::UTC, &TimeZone::UTC), first);
assert_eq!(
deadline(&settings, now, &TimeZone::UTC, &TimeZone::UTC),
expected.parse::<Timestamp>().unwrap().as_nanosecond()
);
}
let zone = TimeZone::get("Asia/Kathmandu").unwrap();
assert_ne!(labels(&settings, initial, &zone, &zone), first);
assert_eq!(labels(&settings, initial, &zone, &zone).desktop.0, "18:19 +0545");
assert_eq!(
labels(&settings, initial, &TimeZone::UTC, &TimeZone::UTC),
labels(
&settings,
initial + jiff::SignedDuration::from_secs(1),
&TimeZone::UTC,
&TimeZone::UTC
)
);
}
#[test]
fn timezone_watch_filters_unrelated_files_and_survives_atomic_replacement() {
let dir = tempfile::tempdir().unwrap();
@@ -739,7 +408,7 @@ mod tests {
let (send, mut receive) = tokio::sync::mpsc::channel(1);
let (done_send, done_receive) = std::sync::mpsc::channel();
std::thread::spawn(move || {
done_send.send(watch(&settings(None, None), send, cancel)).unwrap();
done_send.send(watch(send, cancel)).unwrap();
});
assert!(receive.blocking_recv().is_some());
drop(stop);
+7 -28
View File
@@ -8,7 +8,6 @@ const DEFAULT_BACKGROUND: [u8; 3] = [11, 15, 20];
#[derive(Clone, Debug)]
pub(crate) struct ShellConfig {
pub(crate) notifications: ferese_config::notifications::NotificationConfig,
pub(crate) desktop_widgets: ferese_config::desktop::DesktopWidgets,
pub(crate) animations: crate::motion::Settings,
pub(crate) font_family: Option<String>,
pub(crate) wallpaper: WallpaperConfig,
@@ -23,7 +22,6 @@ impl Default for ShellConfig {
let status = StatusConfig::default();
Self {
notifications: Default::default(),
desktop_widgets: Default::default(),
animations: Default::default(),
font_family: None,
wallpaper: Default::default(),
@@ -186,8 +184,6 @@ struct FereseConfig {
#[serde(default)]
notifications: ferese_config::notifications::NotificationConfig,
#[serde(default)]
desktop_widgets: ferese_config::desktop::DesktopWidgets,
#[serde(default)]
animations: crate::motion::Settings,
#[serde(default)]
theme: ThemeConfig,
@@ -380,7 +376,6 @@ fn parse_document(
config.animations.validate().map_err(ferese_config::Error::from)?;
config.notifications.validate().map_err(ferese_config::Error::from)?;
config.desktop_widgets.validate().map_err(ferese_config::Error::from)?;
let mut theme = shell_theme(&config.theme);
theme.appearance = appearance;
theme.material_radius = nonnegative_or(config.theme.geometry.shell_radius.unwrap_or(14.0), 14.0);
@@ -388,7 +383,6 @@ fn parse_document(
Ok(ShellConfig {
notifications: config.notifications,
desktop_widgets: config.desktop_widgets,
animations: config.animations,
font_family: config.theme.typography.font_family,
wallpaper: config.theme.background,
@@ -625,30 +619,15 @@ mod tests {
}
}
#[test]
fn desktop_clock_parses_and_rejects_invalid_reload_values() {
let clock = parse_test_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_config::desktop::Anchor::BottomRight);
assert!(parse_test_source("desktop-widgets {\n clock {\n opacity 1.1\n }\n}\n").is_err());
assert!(parse_test_source("desktop-widgets {\n clock {\n time-format \"%\"\n }\n}\n").is_err());
}
use super::*;
#[test]
fn removed_desktop_widgets_are_rejected_at_startup_and_reload() {
let source = "desktop-widgets { clock { enabled #true; }; }";
assert!(parse_source(source).is_err());
assert!(parse_test_source(source).is_err());
}
fn parse_test_source(source: &str) -> Result<ShellConfig, ferese_config::Error> {
let document = ferese_config::Document::parse(source)?;
parse_document(&document, Default::default(), Default::default())
-738
View File
@@ -1,738 +0,0 @@
use super::{
Anchor, Background, Border, Color, Element, FereseShell, IcedMargin, IcedOutput, KeyboardInteractivity, Layer,
Length, Limits, Message, NoteDrag, SctkLayerSurfaceSettings, Task, Zoned, alignment, button, color, config,
configured_font, container, destroy_layer_surface, format_bar_time, motion, note_store, row, set_anchor,
set_margin, stacked_clock_digits, text, theme, window,
};
use cosmic::iced::border::Shape as BorderShape;
impl FereseShell {
pub(super) fn flush_note_changes(&mut self) -> Task<Message> {
if self.note_saving || self.note_pending.is_empty() {
return Task::none();
}
let Some(path) = config::config_path() else {
self.note_error = Some("Configuration path unavailable.".into());
return Task::none();
};
self.note_saving = true;
self.note_inflight = std::mem::take(&mut self.note_pending);
let edits = self.note_inflight.clone();
cosmic::task::future(async move {
let (send, receive) = cosmic::iced::futures::channel::oneshot::channel();
std::thread::spawn(move || {
let _ = send.send(note_store::save(&path, &edits));
});
Message::NotesStored(
receive
.await
.unwrap_or_else(|_| Err("Note save worker stopped.".into())),
)
})
}
pub(super) fn set_note_text(&mut self, id: &str, value: String) -> Task<Message> {
if let Some(note) = self.config.desktop_widgets.notes.iter_mut().find(|note| note.id == id)
&& (note.text != value || self.note_error.is_some())
{
note.text = value.clone();
self.note_pending.push(note_store::Edit::Text(id.to_owned(), value));
}
self.flush_note_changes()
}
pub(super) fn finish_note_edit(&mut self) -> Task<Message> {
if let Some(editor) = self.note_editor.take() {
self.set_note_text(&editor.id, editor.content.text())
} else {
self.flush_note_changes()
}
}
pub(super) fn begin_note_drag(&mut self, id: String, surface: window::Id) -> Task<Message> {
self.begin_widget_drag(Some(id), surface)
}
pub(super) fn begin_widget_drag(&mut self, id: Option<String>, surface: window::Id) -> Task<Message> {
if self.note_drag.is_some() {
return Task::none();
}
let Some(entry) = self
.outputs
.iter()
.find(|entry| entry.clock == Some(surface) || entry.notes.iter().any(|(_, window)| *window == surface))
else {
return Task::none();
};
let Some(size) = entry.size else {
return Task::none();
};
let (origin, dimensions) = if let Some(id) = &id {
let Some(note) = self.config.desktop_widgets.notes.iter().find(|note| &note.id == id) else {
return Task::none();
};
(note_origin(note, size), (note.width, note.height))
} else {
let clock = &self.config.desktop_widgets.clock;
(
widget_origin(
clock.anchor,
clock.margin_x,
clock.margin_y,
(clock.width, clock.height),
size,
),
(clock.width, clock.height),
)
};
let output = entry.output.clone();
let mut obstacles = Vec::new();
let clock = &self.config.desktop_widgets.clock;
if id.is_some() && clock.on_output(entry.name.as_deref()) {
let position = widget_origin(
clock.anchor,
clock.margin_x,
clock.margin_y,
(clock.width, clock.height),
size,
);
obstacles.push(cosmic::iced::Rectangle::new(
position,
cosmic::iced::Size::new(clock.width as f32, clock.height as f32),
));
}
for note in &self.config.desktop_widgets.notes {
if Some(&note.id) != id.as_ref() && note.on_output(entry.name.as_deref()) {
obstacles.push(cosmic::iced::Rectangle::new(
note_origin(note, size),
cosmic::iced::Size::new(note.width as f32, note.height as f32),
));
}
}
let overlay = window::Id::unique();
self.note_drag = Some(NoteDrag {
id,
source: surface,
overlay,
origin,
position: origin,
start: self.note_pointer.get(&surface).copied().unwrap_or_default(),
output_size: size,
obstacles,
});
let action = cosmic::surface::action::app_layer_shell::<Self>(
|_| Default::default(),
move |_| SctkLayerSurfaceSettings {
id: overlay,
layer: Layer::Overlay,
keyboard_interactivity: KeyboardInteractivity::None,
input_zone: Some(Vec::new()),
anchor: Anchor::TOP | Anchor::LEFT,
margin: IcedMargin {
top: origin.y.round() as i32,
left: origin.x.round() as i32,
..Default::default()
},
output: IcedOutput::Output(output.clone()),
namespace: "ferese-shell-note-drag".into(),
exclusive_zone: -1,
size: Some((Some(dimensions.0), Some(dimensions.1))),
size_limits: Limits::NONE,
},
Some(Box::new(Self::view_note_drag)),
);
Task::batch([
self.finish_note_edit(),
cosmic::task::message(cosmic::Action::Surface(action)),
])
}
pub(super) fn view_note_drag(&self) -> Element<'_, cosmic::Action<Message>> {
let Some(drag) = &self.note_drag else {
return text("").into();
};
if let Some(id) = &drag.id {
self.view_note_content(id, drag.source)
} else {
self.view_clock_content()
}
}
pub(super) fn finish_note_drag(&mut self, save: bool) -> Task<Message> {
let Some(drag) = self.note_drag.take() else {
return Task::none();
};
let mut tasks = vec![destroy_layer_surface(drag.overlay)];
if save {
if drag.id.is_none() {
let clock = &mut self.config.desktop_widgets.clock;
clock.anchor = ferese_config::desktop::Anchor::TopLeft;
clock.margin_x = drag.position.x.round() as i32;
clock.margin_y = drag.position.y.round() as i32;
for entry in &self.outputs {
if let Some(id) = entry.clock {
tasks.push(set_anchor(id, Anchor::TOP | Anchor::LEFT));
tasks.push(set_margin(id, clock.margin_y, 0, 0, clock.margin_x));
}
}
self.note_pending
.push(note_store::Edit::ClockPosition(clock.margin_x, clock.margin_y));
}
if let Some(note) = self
.config
.desktop_widgets
.notes
.iter_mut()
.find(|note| Some(&note.id) == drag.id.as_ref())
{
note.anchor = ferese_config::desktop::Anchor::TopLeft;
note.margin_x = drag.position.x.round() as i32;
note.margin_y = drag.position.y.round() as i32;
for entry in &self.outputs {
if let Some((_, id)) = entry.notes.iter().find(|(id, _)| Some(id) == drag.id.as_ref()) {
tasks.push(set_anchor(*id, Anchor::TOP | Anchor::LEFT));
tasks.push(set_margin(*id, note.margin_y, 0, 0, note.margin_x));
}
}
self.note_pending.push(note_store::Edit::Position(
note.id.clone(),
note.margin_x,
note.margin_y,
));
}
tasks.push(self.flush_note_changes());
}
Task::batch(tasks)
}
pub(super) fn rebuild_notes(&mut self, reset: bool) -> Task<Message> {
let mut tasks = Vec::new();
for entry in &mut self.outputs {
if reset {
for (_, id) in entry.notes.drain(..) {
self.note_pointer.remove(&id);
tasks.push(destroy_layer_surface(id));
}
}
for note in &self.config.desktop_widgets.notes {
if !note.on_output(entry.name.as_deref()) || entry.notes.iter().any(|(id, _)| id == &note.id) {
continue;
}
let id = window::Id::unique();
entry.notes.push((note.id.clone(), id));
let note = note.clone();
let note_id = note.id.clone();
let output = entry.output.clone();
let action = cosmic::surface::action::app_layer_shell::<Self>(
|_| Default::default(),
move |_| {
let (anchor, margin) = widget_placement(note.anchor, note.margin_x, note.margin_y);
SctkLayerSurfaceSettings {
id,
layer: Layer::Bottom,
keyboard_interactivity: if note.interactive {
KeyboardInteractivity::OnDemand
} else {
KeyboardInteractivity::None
},
input_zone: if note.interactive { None } else { Some(Vec::new()) },
anchor,
margin,
output: IcedOutput::Output(output.clone()),
namespace: "ferese-shell-sticky-note".into(),
exclusive_zone: -1,
size: Some((Some(note.width), Some(note.height))),
size_limits: Limits::NONE,
}
},
Some(Box::new(move |app| app.view_note(&note_id, id))),
);
tasks.push(cosmic::task::message(cosmic::Action::Surface(action)));
}
}
Task::batch(tasks)
}
pub(super) fn view_note(&self, id: &str, surface: window::Id) -> Element<'_, cosmic::Action<Message>> {
if self
.note_drag
.as_ref()
.is_some_and(|drag| drag.id.as_deref() == Some(id))
{
return container(text("")).width(Length::Fill).height(Length::Fill).into();
}
self.view_note_content(id, surface)
}
pub(super) fn view_note_content(&self, id: &str, surface: window::Id) -> Element<'_, cosmic::Action<Message>> {
let Some(note) = self.config.desktop_widgets.notes.iter().find(|note| note.id == id) else {
return text("").into();
};
let alignment = match note.alignment {
ferese_config::desktop::Alignment::Left => alignment::Horizontal::Left,
ferese_config::desktop::Alignment::Center => alignment::Horizontal::Center,
ferese_config::desktop::Alignment::Right => alignment::Horizontal::Right,
};
let font = configured_font(
note.font_family
.as_deref()
.filter(|s| !s.trim().is_empty())
.or(self.config.font_family.as_deref()),
);
let mut foreground = color(
note.color
.as_deref()
.and_then(config::parse_color)
.unwrap_or(self.config.theme.text_primary),
);
foreground.a *= note.opacity;
let background = if note.background.as_deref() == Some("") {
None
} else {
let mut tint = color(
note.background
.as_deref()
.and_then(config::parse_color)
.unwrap_or(self.config.theme.surface_base),
);
tint.a *= note.opacity;
Some(Background::Color(tint))
};
let mut body = cosmic::widget::column([]).spacing(note.gap).align_x(alignment);
if note.interactive {
let title = ferese_theme::text(if note.title.is_empty() { "⋮⋮" } else { &note.title }, font)
.size(note.title_size)
.class(theme::Text::Color(foreground));
let header = cosmic::widget::mouse_area(container(title).width(Length::Fill))
.interaction(cosmic::iced::mouse::Interaction::Grab)
.on_press(cosmic::Action::App(Message::BeginNoteDrag(id.to_owned(), surface)));
let editing = self.note_editor.as_ref().is_some_and(|editor| editor.id == id);
body = body.push(
row([]).push(header).push(motion::button(
button::custom(text(if editing { "Done" } else { "Edit" }))
.padding([4, 8])
.on_press(cosmic::Action::App(if editing {
Message::FinishNoteEdit
} else {
Message::BeginNoteEdit(id.to_owned())
})),
foreground,
false,
1.0,
)),
);
} else if !note.title.is_empty() {
body = body.push(
ferese_theme::text(
note.title.clone(),
cosmic::font::Font {
weight: cosmic::iced::font::Weight::Semibold,
..font
},
)
.size(note.title_size)
.width(Length::Fill)
.align_x(alignment)
.class(theme::Text::Color(foreground)),
);
}
if let Some(editor) = &self.note_editor
&& editor.id == id
{
let id = id.to_owned();
let editor_radius = ferese_theme::inner_radius(self.config.theme.material_radius, note.padding);
body = body.push(
cosmic::widget::TextEditor::new(&editor.content)
.style(move |theme, status| {
use cosmic::iced::widget::text_editor::Catalog;
let mut style = theme.style(&<cosmic::Theme as Catalog>::default(), status);
style.border.radius = editor_radius.into();
style
})
.height(Length::Fill)
.font(font)
.size(note.text_size)
.on_action(move |action| cosmic::Action::App(Message::NoteAction(id.clone(), action))),
);
} else {
body = body.push(
ferese_theme::text(note.text.clone(), font)
.size(note.text_size)
.width(Length::Fill)
.align_x(alignment)
.class(theme::Text::Color(foreground)),
);
}
if let Some(error) = &self.note_error {
body = body.push(text(error.clone()).size(10).class(theme::Text::Color(foreground)));
}
let radius = self.config.theme.material_radius;
container(body)
.padding(note.padding)
.width(Length::Fill)
.height(Length::Fill)
.class(theme::Container::custom(move |_| container::Style {
background,
border: Border {
shape: BorderShape::Continuous,
radius: radius.into(),
..Default::default()
},
..Default::default()
}))
.into()
}
pub(super) fn rebuild_clocks(&mut self, reset: bool) -> Task<Message> {
self.desktop_clock = self
.config
.desktop_widgets
.clock
.labels(&Zoned::now())
.unwrap_or_default();
let mut tasks = Vec::new();
for entry in &mut self.outputs {
if !reset && entry.clock.is_some() {
continue;
}
if let Some(id) = entry.clock.take() {
self.note_pointer.remove(&id);
tasks.push(destroy_layer_surface(id));
}
let clock = self.config.desktop_widgets.clock.clone();
if !clock.on_output(entry.name.as_deref()) {
continue;
}
let id = window::Id::unique();
entry.clock = Some(id);
let output = entry.output.clone();
let action = cosmic::surface::action::app_layer_shell::<Self>(
|_| Default::default(),
move |_| {
let (anchor, margin) = clock_placement(&clock);
SctkLayerSurfaceSettings {
id,
layer: Layer::Bottom,
keyboard_interactivity: KeyboardInteractivity::OnDemand,
input_zone: None,
anchor,
margin,
output: IcedOutput::Output(output.clone()),
namespace: "ferese-shell-desktop-clock".into(),
exclusive_zone: -1,
size: Some((Some(clock.width), Some(clock.height))),
size_limits: Limits::NONE,
}
},
Some(Box::new(move |app| app.view_desktop_clock(id))),
);
tasks.push(cosmic::task::message(cosmic::Action::Surface(action)));
}
Task::batch(tasks)
}
pub(super) fn view_desktop_clock(&self, id: window::Id) -> Element<'_, cosmic::Action<Message>> {
if self
.note_drag
.as_ref()
.is_some_and(|drag| drag.id.is_none() && drag.source == id)
{
return container(text("")).width(Length::Fill).height(Length::Fill).into();
}
cosmic::widget::mouse_area(self.view_clock_content())
.on_press(cosmic::Action::App(Message::BeginClockDrag(id)))
.interaction(cosmic::iced::mouse::Interaction::Grab)
.into()
}
pub(super) fn view_clock_content(&self) -> Element<'_, cosmic::Action<Message>> {
use ferese_config::desktop::Alignment as ClockAlignment;
let clock = &self.config.desktop_widgets.clock;
let alignment = match clock.alignment {
ClockAlignment::Left => alignment::Horizontal::Left,
ClockAlignment::Center => alignment::Horizontal::Center,
ClockAlignment::Right => alignment::Horizontal::Right,
};
let pixel = clock.style == ferese_config::desktop::ClockStyle::Pixel;
let custom_font = clock.font_family.as_deref().filter(|family| !family.trim().is_empty());
let mut font = configured_font(if pixel {
custom_font.or(Some("Cantarell"))
} else {
custom_font.or(self.config.font_family.as_deref())
});
if pixel && custom_font.is_none() {
font.weight = cosmic::iced::font::Weight::Black;
} else if clock.bold {
font.weight = cosmic::iced::font::Weight::Bold;
}
let date_font = configured_font(custom_font.or(self.config.font_family.as_deref()));
let tint = |custom: &Option<String>, fallback| {
let mut tint = color(custom.as_deref().and_then(config::parse_color).unwrap_or(fallback));
tint.a *= clock.opacity;
tint
};
let foreground = tint(&clock.color, self.config.theme.text_primary);
let accent = color(self.config.theme.accent);
let custom_color = clock.color.as_deref().and_then(config::parse_color).is_some();
let digit_colors = [0.8, 0.45].map(|text_mix| {
if custom_color {
return foreground;
}
Color {
r: foreground.r * text_mix + accent.r * (1.0 - text_mix),
g: foreground.g * text_mix + accent.g * (1.0 - text_mix),
b: foreground.b * text_mix + accent.b * (1.0 - text_mix),
a: foreground.a,
}
});
let mut labels = cosmic::widget::column([]).spacing(clock.gap).align_x(alignment);
if pixel && clock.show_date {
labels = labels.push(
ferese_theme::text(self.desktop_clock.1.clone(), date_font)
.size(clock.date_size)
.class(theme::Text::Color(tint(
&clock.date_color,
self.config.theme.text_primary,
))),
);
}
if let Some(digits) = pixel.then(|| stacked_clock_digits(&self.desktop_clock.0)).flatten() {
let available_height = (clock.height as f32
- clock.padding * 2.0
- if clock.show_date {
clock.date_size * 1.3 + clock.gap
} else {
0.0
})
.max(16.0);
let size = clock
.time_size
.min(available_height / 1.6)
.min((clock.width as f32 - clock.padding * 2.0).max(16.0) / 1.5);
let mut stack = cosmic::widget::column([]).spacing(0).align_x(alignment);
for (line, digits) in digits.into_iter().enumerate() {
let mut digit_row = row([]).spacing(0);
for (position, digit) in digits.chars().enumerate() {
digit_row = digit_row.push(
ferese_theme::text(digit.to_string(), font)
.size(size)
.line_height(cosmic::iced::widget::text::LineHeight::Relative(0.8))
.class(theme::Text::Color(digit_colors[(line + position) % 2])),
);
}
stack = stack.push(digit_row);
}
labels = labels.push(stack);
} else {
let characters = self.desktop_clock.0.chars().count().max(1) as f32;
let available_width = (clock.width as f32 - clock.padding * 2.0).max(1.0);
let available_height = (clock.height as f32
- clock.padding * 2.0
- if clock.show_date {
clock.date_size * 1.3 + clock.gap
} else {
0.0
})
.max(1.0);
let size = clock
.time_size
.min(available_width / (characters * 0.75))
.min(available_height / 1.3);
labels = labels.push(
ferese_theme::text(self.desktop_clock.0.clone(), font)
.size(size)
.width(Length::Fill)
.align_x(alignment)
.class(theme::Text::Color(foreground)),
);
}
if !pixel && clock.show_date {
labels = labels.push(
ferese_theme::text(self.desktop_clock.1.clone(), font)
.size(clock.date_size)
.width(Length::Fill)
.align_x(alignment)
.class(theme::Text::Color(tint(
&clock.date_color,
self.config.theme.text_muted,
))),
);
}
let background = clock.background.as_deref().and_then(config::parse_color).map(|rgba| {
let mut tint = color(rgba);
tint.a *= clock.opacity;
Background::Color(tint)
});
let radius = self.config.theme.material_radius;
container(labels)
.padding(clock.padding)
.width(Length::Fill)
.height(Length::Fill)
.align_x(alignment)
.center_y(Length::Fill)
.class(theme::Container::custom(move |_| container::Style {
background,
border: Border {
shape: BorderShape::Continuous,
radius: radius.into(),
..Default::default()
},
..Default::default()
}))
.into()
}
}
/// Sweep each axis, allowing edge sliding without tunnelling through a widget
/// when pointer events skip ahead. Coordinates match integer layer margins.
pub(super) fn avoid_widget_overlap(
previous: cosmic::iced::Point,
requested: cosmic::iced::Point,
size: (u32, u32),
obstacles: &[cosmic::iced::Rectangle],
) -> cosmic::iced::Point {
let mut position = previous;
let (width, height) = (size.0 as f32, size.1 as f32);
let mut x = requested.x.round();
for obstacle in obstacles {
if position.y < obstacle.y + obstacle.height && position.y + height > obstacle.y {
if x > position.x && position.x + width <= obstacle.x {
x = x.min((obstacle.x - width).floor());
} else if x < position.x && position.x >= obstacle.x + obstacle.width {
x = x.max((obstacle.x + obstacle.width).ceil());
}
}
}
position.x = x;
let mut y = requested.y.round();
for obstacle in obstacles {
if position.x < obstacle.x + obstacle.width && position.x + width > obstacle.x {
if y > position.y && position.y + height <= obstacle.y {
y = y.min((obstacle.y - height).floor());
} else if y < position.y && position.y >= obstacle.y + obstacle.height {
y = y.max((obstacle.y + obstacle.height).ceil());
}
}
}
position.y = y;
position
}
pub(super) fn clamp_note_position(
position: cosmic::iced::Point,
output: (i32, i32),
size: (u32, u32),
) -> cosmic::iced::Point {
cosmic::iced::Point::new(
position.x.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).clamp(0., 8192.)),
)
}
pub(super) fn note_origin(note: &ferese_config::desktop::StickyNote, output: (i32, i32)) -> cosmic::iced::Point {
widget_origin(
note.anchor,
note.margin_x,
note.margin_y,
(note.width, note.height),
output,
)
}
pub(super) fn widget_origin(
anchor: ferese_config::desktop::Anchor,
margin_x: i32,
margin_y: i32,
size: (u32, u32),
output: (i32, i32),
) -> cosmic::iced::Point {
use ferese_config::desktop::Anchor as A;
let x = match anchor {
A::TopLeft | A::CenterLeft | A::BottomLeft => margin_x as f32,
A::TopRight | A::CenterRight | A::BottomRight => output.0 as f32 - size.0 as f32 - margin_x as f32,
_ => (output.0 as f32 - size.0 as f32) / 2.,
};
let y = match anchor {
A::TopLeft | A::TopCenter | A::TopRight => margin_y as f32,
A::BottomLeft | A::BottomCenter | A::BottomRight => output.1 as f32 - size.1 as f32 - margin_y as f32,
_ => (output.1 as f32 - size.1 as f32) / 2.,
};
clamp_note_position(cosmic::iced::Point::new(x, y), output, size)
}
pub(super) fn clock_placement(clock: &ferese_config::desktop::Clock) -> (Anchor, IcedMargin) {
widget_placement(clock.anchor, clock.margin_x, clock.margin_y)
}
pub(super) fn widget_placement(
position: ferese_config::desktop::Anchor,
margin_x: i32,
margin_y: i32,
) -> (Anchor, IcedMargin) {
use ferese_config::desktop::Anchor as Position;
let horizontal = match position {
Position::TopLeft | Position::CenterLeft | Position::BottomLeft => Anchor::LEFT,
Position::TopRight | Position::CenterRight | Position::BottomRight => Anchor::RIGHT,
_ => Anchor::empty(),
};
let vertical = match position {
Position::TopLeft | Position::TopCenter | Position::TopRight => Anchor::TOP,
Position::BottomLeft | Position::BottomCenter | Position::BottomRight => Anchor::BOTTOM,
_ => Anchor::empty(),
};
(
horizontal | vertical,
IcedMargin {
top: if vertical == Anchor::TOP { margin_y } else { 0 },
bottom: if vertical == Anchor::BOTTOM { margin_y } else { 0 },
left: if horizontal == Anchor::LEFT { margin_x } else { 0 },
right: if horizontal == Anchor::RIGHT { margin_x } else { 0 },
},
)
}
pub(super) fn current_time() -> String {
format_bar_time(&Zoned::now())
}
+7 -282
View File
@@ -1,9 +1,7 @@
mod bar;
mod desktop_widgets;
mod surfaces;
use bar::*;
use desktop_widgets::*;
use surfaces::*;
mod clock;
@@ -14,7 +12,6 @@ mod display_mode;
mod keybinding_guide;
mod media;
mod motion;
mod note_store;
mod notification_ui;
mod notifications;
mod panel;
@@ -46,9 +43,7 @@ use cosmic::{Element, theme};
use ferese_protocols::effects::v1::client::ferese_effects_manager_v1::FereseEffectsManagerV1;
use ferese_protocols::effects::v1::client::ferese_surface_effects_v1;
use ferese_protocols::effects::v1::client::ferese_surface_effects_v1::FereseSurfaceEffectsV1;
use ferese_theme::calendar::{format_bar_time, stacked_digits as stacked_clock_digits};
use ferese_theme::icons::{accented as accented_icon, tinted as bar_icon};
use jiff::Zoned;
use wayland_client::globals::{GlobalListContents, registry_queue_init};
use wayland_client::protocol::{wl_output, wl_registry, wl_surface};
use wayland_client::{Connection, Dispatch, EventQueue, Proxy, QueueHandle, delegate_noop};
@@ -121,19 +116,6 @@ fn main() -> cosmic::iced::Result {
});
renderer::configure_shell();
cosmic::iced::advanced::graphics::text::font_system()
.write()
.unwrap()
.load_font(std::borrow::Cow::Borrowed(include_bytes!(
"../../../assets/fonts/Comfortaa-Regular.otf"
)));
cosmic::iced::advanced::graphics::text::font_system()
.write()
.unwrap()
.load_font(std::borrow::Cow::Borrowed(include_bytes!(
"../../../assets/fonts/Cantarell-ExtraBold.otf"
)));
motion::configure(config.animations, config.theme.material_radius);
let compositor_wallpaper = std::env::var_os("FERESE_COMPOSITOR_WALLPAPER").is_some();
if compositor_wallpaper {
@@ -177,8 +159,6 @@ struct FereseShell {
workspace_ui: workspace_ui::WorkspaceUi,
overview_active: bool,
clock: String,
clock_service: clock::Service,
desktop_clock: (String, String),
outputs: Vec<OutputSurfaces>,
notifications: notifications::Center,
notification_surface: Option<notification_ui::NotificationSurface>,
@@ -196,30 +176,6 @@ struct FereseShell {
guide_load: keybinding_guide::LoadState,
guide_attempts: u8,
pending_power: Option<(window::Id, system_modal::PowerAction)>,
note_editor: Option<DesktopNoteEditor>,
note_drag: Option<NoteDrag>,
note_pointer: std::collections::HashMap<window::Id, cosmic::iced::Point>,
note_pending: Vec<note_store::Edit>,
note_inflight: Vec<note_store::Edit>,
note_saving: bool,
note_error: Option<String>,
}
struct DesktopNoteEditor {
id: String,
content: cosmic::widget::text_editor::Content<cosmic::Renderer>,
revision: u64,
}
struct NoteDrag {
id: Option<String>,
source: window::Id,
overlay: window::Id,
start: cosmic::iced::Point,
origin: cosmic::iced::Point,
position: cosmic::iced::Point,
output_size: (i32, i32),
obstacles: Vec<cosmic::iced::Rectangle>,
}
struct OutputSurfaces {
@@ -227,8 +183,6 @@ struct OutputSurfaces {
name: Option<String>,
bar: window::Id,
wallpaper: Option<window::Id>,
clock: Option<window::Id>,
notes: Vec<(String, window::Id)>,
size: Option<(i32, i32)>,
effects: Option<EffectsBinding>,
bar_regions: Vec<[f32; 5]>,
@@ -244,17 +198,10 @@ enum Message {
ThemeChanged(Box<ferese_ipc::theme::Snapshot>),
ThemeMode(ferese_config::theme::Mode),
ThemeModeSet(Result<(), String>),
BeginNoteEdit(String),
NoteAction(String, cosmic::widget::text_editor::Action),
SaveNote(String, u64),
FinishNoteEdit,
BeginNoteDrag(String, window::Id),
BeginClockDrag(window::Id),
NotesStored(Result<(), String>),
WallpaperLoaded(Result<image::Handle, String>),
Event(Event, window::Id),
NativeSurface(window::Id, Result<(Connection, wl_surface::WlSurface), String>),
ClockChanged(clock::Labels),
ClockChanged(String),
RecorderEvent(recording::Event),
RecorderElapsed,
MediaUpdated(std::sync::Arc<ferese_ipc::media::Snapshot>),
@@ -330,7 +277,6 @@ impl cosmic::Application for FereseShell {
}
fn init(core: Core, (config, wallpaper): Self::Flags) -> (Self, Task<Self::Message>) {
let desktop_clock = config.desktop_widgets.clock.labels(&Zoned::now()).unwrap_or_default();
let app = Self {
core,
notifications: notifications::Center::new(config.notifications.clone()),
@@ -349,14 +295,6 @@ impl cosmic::Application for FereseShell {
guide_load: Default::default(),
guide_attempts: 0,
pending_power: None,
note_editor: None,
note_drag: None,
note_pointer: Default::default(),
note_pending: Vec::new(),
note_inflight: Vec::new(),
note_saving: false,
note_error: None,
clock_service: clock::Service::new(&config.desktop_widgets.clock),
config,
wallpaper: None,
control: ShellControl::connect()
@@ -367,8 +305,7 @@ impl cosmic::Application for FereseShell {
snapshot: ShellSnapshot::default(),
workspace_ui: Default::default(),
overview_active: false,
clock: current_time(),
desktop_clock,
clock: clock::current_time(),
outputs: Vec::new(),
};
let wallpaper_task = match wallpaper {
@@ -409,34 +346,13 @@ impl cosmic::Application for FereseShell {
)) => Some(Message::Event(event, id)),
_ => None,
}),
self.clock_service
.subscription(
self.outputs.iter().any(|output| !output.hidden),
self.outputs.iter().any(|output| {
output.clock.is_some() && self.config.desktop_widgets.clock.on_output(output.name.as_deref())
}),
)
.map(Message::ClockChanged),
clock::subscription(self.outputs.iter().any(|output| !output.hidden)).map(Message::ClockChanged),
self.media.subscription().map(Message::MediaUpdated),
self.media_tick_subscription().map(|_| Message::MediaTick),
self.media.art.subscription().map(Message::MediaArt),
self.recorder.subscription().map(Message::RecorderEvent),
self.recorder.elapsed_subscription().map(|_| Message::RecorderElapsed),
self.status_service.subscription().map(Message::StatusUpdated),
if self.config.desktop_widgets.clock.enabled
|| self
.config
.desktop_widgets
.notes
.iter()
.any(|note| note.enabled && note.interactive)
{
event::listen_with(|event, _, id| {
matches!(&event, Event::Mouse(_)).then_some(Message::Event(event, id))
})
} else {
Subscription::none()
},
])
}
@@ -555,67 +471,6 @@ impl cosmic::Application for FereseShell {
}
Task::none()
}
Message::BeginNoteEdit(id) => {
let mut tasks = vec![self.finish_note_edit()];
if let Some(note) = self.config.desktop_widgets.notes.iter().find(|note| note.id == id) {
self.note_editor = Some(DesktopNoteEditor {
id,
content: cosmic::widget::text_editor::Content::with_text(&note.text),
revision: 0,
});
}
tasks.push(Task::none());
Task::batch(tasks)
}
Message::FinishNoteEdit => self.finish_note_edit(),
Message::NoteAction(id, action) => {
if let Some(editor) = &mut self.note_editor
&& editor.id == id
{
let edited = action.is_edit();
editor.content.perform(action);
if edited {
editor.revision = editor.revision.wrapping_add(1);
let revision = editor.revision;
return cosmic::task::future(async move {
// iced executor is Tokio; do not spawn blocking sleep threads.
tokio::time::sleep(Duration::from_millis(500)).await;
Message::SaveNote(id, revision)
});
}
}
Task::none()
}
Message::SaveNote(id, revision) => {
if let Some(editor) = &self.note_editor
&& editor.id == id
&& editor.revision == revision
{
let value = editor.content.text();
self.set_note_text(&id, value)
} else {
Task::none()
}
}
Message::BeginNoteDrag(id, surface) => self.begin_note_drag(id, surface),
Message::BeginClockDrag(surface) => self.begin_widget_drag(None, surface),
Message::NotesStored(result) => {
self.note_saving = false;
match result {
Ok(()) => {
self.note_inflight.clear();
self.note_error = None;
self.flush_note_changes()
}
Err(error) => {
self.note_pending.splice(0..0, self.note_inflight.drain(..));
self.note_error = Some(error);
Task::none()
}
}
}
Message::WallpaperLoaded(result) => {
match result {
Ok(handle) => self.wallpaper = Some(handle),
@@ -855,8 +710,7 @@ impl cosmic::Application for FereseShell {
}
Message::Event(event, id) => self.handle_event(event, id),
Message::ClockChanged(labels) => {
self.clock = labels.bar;
self.desktop_clock = labels.desktop;
self.clock = labels;
Task::none()
}
Message::RecorderEvent(event) => {
@@ -1024,40 +878,13 @@ impl FereseShell {
let edge_changed = self.config.panels[0].edge != config.panels[0].edge;
let old = self.config.theme;
let old_geometry = self.config.panels[0].geometry;
let old_clock = &self.config.desktop_widgets.clock;
let old_notes = &self.config.desktop_widgets.notes;
let new_notes = &config.desktop_widgets.notes;
let notes_changed = old_notes.len() != new_notes.len()
|| old_notes.iter().zip(new_notes).any(|(old, new)| !old.same_surface(new));
let new_clock = &config.desktop_widgets.clock;
let clock_changed = old_clock.enabled != new_clock.enabled
|| old_clock.outputs != new_clock.outputs
|| old_clock.anchor != new_clock.anchor
|| old_clock.width != new_clock.width
|| old_clock.height != new_clock.height
|| old_clock.margin_x != new_clock.margin_x
|| old_clock.margin_y != new_clock.margin_y;
let theme = config.theme;
let geometry = config.panels[0].geometry;
let geometry_changed = edge_changed || old_geometry != geometry;
self.notifications.configure(config.notifications.clone());
self.clock_service.configure(&config.desktop_widgets.clock);
self.config = config;
let workspace_task = self.update_workspace_ui();
self.desktop_clock = self
.config
.desktop_widgets
.clock
.labels(&Zoned::now())
.unwrap_or_default();
let mut tasks = vec![
workspace_task,
if clock_changed {
self.rebuild_clocks(true)
} else {
Task::none()
},
];
let mut tasks = vec![workspace_task];
if composition_changed || geometry_changed {
tasks.push(self.destroy_menu());
@@ -1082,25 +909,6 @@ impl FereseShell {
tasks.push(cosmic::command::set_theme(theme.palette().native_theme()));
}
if clock_changed && self.note_drag.as_ref().is_some_and(|drag| drag.id.is_none()) {
tasks.push(self.finish_note_drag(false));
}
if notes_changed {
tasks.push(self.finish_note_drag(false));
if self.note_editor.as_ref().is_some_and(|editor| {
!self
.config
.desktop_widgets
.notes
.iter()
.any(|note| note.id == editor.id && note.interactive)
}) {
self.note_editor = None;
}
tasks.push(self.rebuild_notes(true));
}
for entry in self.outputs.iter().filter(|_| geometry_changed) {
let (anchor, _) = surfaces::panel_placement(self.config.panels[0].edge, geometry.edge_margin, 0);
tasks.push(set_anchor(entry.bar, anchor | Anchor::LEFT | Anchor::RIGHT));
@@ -1190,14 +998,6 @@ mod tests {
guide_load: Default::default(),
guide_attempts: 0,
pending_power: None,
note_editor: None,
note_drag: None,
note_pointer: Default::default(),
note_pending: Vec::new(),
note_inflight: Vec::new(),
note_saving: false,
note_error: None,
clock_service: clock::Service::new(&config.desktop_widgets.clock),
config,
wallpaper: None,
control: None,
@@ -1205,14 +1005,11 @@ mod tests {
workspace_ui: Default::default(),
overview_active: false,
clock: String::new(),
desktop_clock: Default::default(),
outputs: vec![OutputSurfaces {
output,
name: Some("test".into()),
bar: window::Id::unique(),
wallpaper: None,
clock: None,
notes: Vec::new(),
size: Some((1200, 800)),
effects: None,
bar_regions: vec![[0., 0., 600., 36., 8.]],
@@ -1379,17 +1176,13 @@ mod tests {
}
#[test]
fn late_open_checks_preserve_owned_desktop_widgets_and_tooltips() {
fn late_open_checks_preserve_owned_wallpaper_and_tooltips() {
let mut shell = shell_with_measured_panel();
let wallpaper = window::Id::unique();
let clock = window::Id::unique();
let note = window::Id::unique();
let tooltip = window::Id::unique();
shell.outputs[0].wallpaper = Some(wallpaper);
shell.outputs[0].clock = Some(clock);
shell.outputs[0].notes.push(("note".into(), note));
shell.workspace_ui.tooltip = Some(tooltip);
for id in [wallpaper, clock, note, tooltip] {
for id in [wallpaper, tooltip] {
assert!(shell.owns_surface(id));
assert_eq!(
shell
@@ -1421,74 +1214,6 @@ mod tests {
assert_eq!(shell.outputs[0].bar_margin_horizontal, 240);
}
#[test]
fn widget_drag_stops_at_edges_without_tunnelling_and_slides() {
use cosmic::iced::{Point, Rectangle};
let obstacles = [Rectangle {
x: 100.,
y: 0.,
width: 40.,
height: 100.,
}];
assert_eq!(
super::avoid_widget_overlap(Point::new(0., 0.), Point::new(300., 0.), (20, 20), &obstacles),
Point::new(80., 0.)
);
assert_eq!(
super::avoid_widget_overlap(Point::new(80., 0.), Point::new(120., 50.), (20, 20), &obstacles),
Point::new(80., 50.)
);
assert_eq!(
super::avoid_widget_overlap(Point::new(160., 0.), Point::new(0., 0.), (20, 20), &obstacles),
Point::new(140., 0.)
);
assert_eq!(
super::avoid_widget_overlap(Point::new(100., 120.), Point::new(100., 0.), (20, 20), &obstacles),
Point::new(100., 100.)
);
assert_eq!(
super::avoid_widget_overlap(Point::new(0., 100.), Point::new(200., 100.), (20, 20), &obstacles),
Point::new(200., 100.)
);
}
#[test]
fn note_drag_origin_and_bounds_use_logical_output_coordinates() {
use cosmic::iced::Point;
let mut note = ferese_config::desktop::StickyNote::default();
assert_eq!(super::note_origin(&note, (1920, 1080)), Point::new(1552., 80.));
note.anchor = ferese_config::desktop::Anchor::Center;
assert_eq!(super::note_origin(&note, (1920, 1080)), Point::new(800., 420.));
assert_eq!(
super::clamp_note_position(Point::new(-20., 1200.), (1920, 1080), (320, 240)),
Point::new(0., 840.)
);
assert_eq!(
super::clamp_note_position(Point::new(20., 30.), (200, 100), (320, 240)),
Point::ORIGIN
);
}
#[test]
fn desktop_clock_anchor_margins_match_only_anchored_edges() {
use ferese_config::desktop::{Anchor as Position, Clock};
let clock = Clock {
anchor: Position::BottomRight,
margin_x: 30,
margin_y: 40,
..Clock::default()
};
let (anchor, margin) = super::clock_placement(&clock);
assert_eq!(anchor, super::Anchor::BOTTOM | super::Anchor::RIGHT);
assert_eq!((margin.top, margin.right, margin.bottom, margin.left), (0, 30, 40, 0));
let (anchor, margin) = super::clock_placement(&Clock {
anchor: Position::Center,
..clock
});
assert!(anchor.is_empty());
assert_eq!((margin.top, margin.right, margin.bottom, margin.left), (0, 0, 0, 0));
}
#[test]
fn shell_font_respects_configured_family() {
assert_eq!(
-138
View File
@@ -1,138 +0,0 @@
//! Atomic, conflict-checked note edits; always called from a worker thread.
use std::fs;
use std::io::Write;
use std::path::Path;
#[derive(Clone, Debug)]
pub enum Edit {
Text(String, String),
Position(String, i32, i32),
ClockPosition(i32, i32),
}
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 = ferese_config::Document::parse(&source).map_err(|e| e.to_string())?;
for edit in edits {
if let Edit::ClockPosition(x, y) = edit {
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 id = match edit {
Edit::Text(id, _) | Edit::Position(id, _, _) => id,
Edit::ClockPosition(..) => unreachable!(),
};
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.")?;
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();
if updated.len() > 60 * 1024 {
return Err("Configuration exceeds 60 KiB.".into());
}
crate::config::parse_source(&updated).map_err(|e| e.to_string())?;
let parent = path.parent().ok_or("Config has no parent directory.")?;
let mut file = tempfile::NamedTempFile::new_in(parent).map_err(|e| e.to_string())?;
file.as_file()
.set_permissions(fs::metadata(path).map_err(|e| e.to_string())?.permissions())
.map_err(|e| e.to_string())?;
file.write_all(updated.as_bytes()).map_err(|e| e.to_string())?;
file.as_file().sync_all().map_err(|e| e.to_string())?;
if fs::read_to_string(path).map_err(|e| e.to_string())? != source {
return Err("Config changed while saving; your note remains open. Edit again to retry.".into());
}
file.persist(path).map_err(|e| e.to_string())?;
fs::File::open(parent)
.and_then(|dir| dir.sync_all())
.map_err(|e| e.to_string())?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn clock_position_saves_without_a_notes_table() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.kdl");
fs::write(
&path,
"desktop-widgets {\n clock {\n enabled #true\n time-size 80\n }\n}\n",
)
.unwrap();
save(&path, &[Edit::ClockPosition(120, 240)]).unwrap();
let config = crate::config::parse_source(&fs::read_to_string(path).unwrap()).unwrap();
assert_eq!(
(
config.desktop_widgets.clock.margin_x,
config.desktop_widgets.clock.margin_y
),
(120, 240)
);
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.kdl");
fs::write(
&path,
r##"// keep this
theme {
colors {
accent "#123456"
}
}
desktop-widgets {
note id="a" text="old"
}
"##,
)
.unwrap();
save(
&path,
&[
Edit::Text("a".into(), "one\ntwo".into()),
Edit::Position("a".into(), 50, 60),
],
)
.unwrap();
let source = fs::read_to_string(&path).unwrap();
assert!(source.contains("// keep this"));
assert!(source.contains("#123456"));
let notes = crate::config::parse_source(&source).unwrap().desktop_widgets.notes;
assert_eq!(notes[0].text, "one\ntwo");
assert_eq!((notes[0].margin_x, notes[0].margin_y), (50, 60));
assert_eq!(notes[0].anchor, ferese_config::desktop::Anchor::TopLeft);
}
}
+8 -103
View File
@@ -2,9 +2,9 @@ use super::{
Anchor, Background, BarMetrics, Color, Connection, ContentFit, Dispatch, EFFECT_FRAME_PENDING, Element, Event,
EventQueue, FereseEffectsManagerV1, FereseShell, FereseSurfaceEffectsV1, GlobalListContents, IcedMargin,
IcedOutput, KeyboardInteractivity, Layer, Length, Limits, Message, Ordering, OutputSurfaces, PlatformSpecific,
Proxy, QueueHandle, SctkLayerSurfaceSettings, Task, WallpaperMode, avoid_widget_overlap, clamp_note_position,
config, container, delegate_noop, destroy_layer_surface, ferese_surface_effects_v1, image, output_bar_hidden,
registry_queue_init, set_margin, status_ui, text, theme, wayland, window, wl_output, wl_registry, wl_surface,
Proxy, QueueHandle, SctkLayerSurfaceSettings, Task, WallpaperMode, config, container, delegate_noop,
destroy_layer_surface, ferese_surface_effects_v1, image, output_bar_hidden, registry_queue_init, status_ui, text,
theme, wayland, window, wl_output, wl_registry, wl_surface,
};
pub(super) fn panel_placement(edge: ferese_config::panel::Edge, margin: i32, side: i32) -> (Anchor, IcedMargin) {
@@ -39,25 +39,9 @@ impl FereseShell {
menu.chain(destroy_layer_surface(entry.bar)),
self.rebuild_system_modal(),
];
if self.note_drag.as_ref().is_some_and(|drag| {
entry.clock == Some(drag.source) || entry.notes.iter().any(|(_, id)| *id == drag.source)
}) {
tasks.push(self.finish_note_drag(false));
}
if let Some(wallpaper) = entry.wallpaper {
tasks.push(destroy_layer_surface(wallpaper));
}
if let Some(clock) = entry.clock {
tasks.push(destroy_layer_surface(clock));
}
for (_, id) in entry.notes {
self.note_pointer.remove(&id);
tasks.push(destroy_layer_surface(id));
}
return Task::batch(tasks);
}
@@ -81,7 +65,6 @@ impl FereseShell {
entry.size = size;
}
let changed = name.is_some() && entry.name != name;
if name.is_some() {
entry.name = name;
}
@@ -98,17 +81,7 @@ impl FereseShell {
} else {
Task::none()
};
return if changed {
Task::batch([
margin,
popup,
tooltip,
self.rebuild_clocks(true),
self.rebuild_notes(true),
])
} else {
Task::batch([margin, popup, tooltip])
};
return Task::batch([margin, popup, tooltip]);
}
let bar_surface_id = window::Id::unique();
@@ -138,8 +111,6 @@ impl FereseShell {
bar_regions: Vec::new(),
panel_resolution: Default::default(),
bar_margin_horizontal: 0,
clock: None,
notes: Vec::new(),
size,
hidden,
});
@@ -196,27 +167,22 @@ impl FereseShell {
Task::batch([
Task::batch(tasks),
self.rebuild_clocks(false),
self.rebuild_notes(false),
self.rebuild_system_modal(),
cosmic::task::message(cosmic::Action::App(Message::ShowGuide)),
])
}
pub(super) fn owns_surface(&self, id: window::Id) -> bool {
self.outputs.iter().any(|entry| {
entry.bar == id
|| entry.wallpaper == Some(id)
|| entry.clock == Some(id)
|| entry.notes.iter().any(|(_, surface)| *surface == id)
}) || self.menu.as_ref().is_some_and(|menu| menu.id == id)
self.outputs
.iter()
.any(|entry| entry.bar == id || entry.wallpaper == Some(id))
|| self.menu.as_ref().is_some_and(|menu| menu.id == id)
|| self.system_modal.as_ref().is_some_and(|modal| modal.contains(id))
|| self
.notification_surface
.as_ref()
.is_some_and(|surface| surface.id == id)
|| self.workspace_ui.tooltip == Some(id)
|| self.note_drag.as_ref().is_some_and(|drag| drag.overlay == id)
}
pub(super) fn handle_event(&mut self, event: Event, id: window::Id) -> Task<Message> {
@@ -230,59 +196,6 @@ impl FereseShell {
modal.focus_visible = false;
}
if let Event::Mouse(mouse) = &event {
if !self
.outputs
.iter()
.any(|entry| entry.clock == Some(id) || entry.notes.iter().any(|(_, surface)| *surface == id))
{
return Task::none();
}
match mouse {
cosmic::iced::mouse::Event::CursorMoved { position } => {
self.note_pointer.insert(id, *position);
if let Some(drag) = &mut self.note_drag
&& drag.source == id
{
let delta = *position - drag.start;
let dimensions = if let Some(id) = &drag.id {
self.config
.desktop_widgets
.notes
.iter()
.find(|note| &note.id == id)
.map(|note| (note.width, note.height))
} else {
let clock = &self.config.desktop_widgets.clock;
Some((clock.width, clock.height))
};
if let Some(dimensions) = dimensions {
let requested = clamp_note_position(drag.origin + delta, drag.output_size, dimensions);
drag.position = avoid_widget_overlap(drag.position, requested, dimensions, &drag.obstacles);
// Move a compact, cached buffer in the compositor instead of
// repainting an output-sized buffer for every pointer event.
return set_margin(
drag.overlay,
drag.position.y.round() as i32,
0,
0,
drag.position.x.round() as i32,
);
}
}
}
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);
}
_ => {}
}
}
if let Event::Keyboard(cosmic::iced::keyboard::Event::KeyPressed {
key: cosmic::iced::keyboard::Key::Named(cosmic::iced::keyboard::key::Named::Enter),
..
@@ -337,14 +250,6 @@ impl FereseShell {
if self.system_modal.is_some() {
return self.close_system_modal();
}
if self.note_drag.is_some() {
return self.finish_note_drag(false);
}
if self.note_editor.is_some() {
return self.finish_note_edit();
}
if self.notifications.history_open {
return self.close_notification_history();
}
+1 -1
View File
@@ -668,7 +668,7 @@ impl FereseShell {
}
}
// Restrict Tab traversal to the modal actions, excluding the bar and desktop widgets.
// Restrict Tab traversal to the modal actions, excluding the panel.
fn modal_focus_operation<T: Send + 'static>(backwards: bool) -> Box<dyn cosmic::iced::advanced::widget::Operation<T>> {
use cosmic::iced::advanced::widget::{Id, operation};
let target = Id::new("ferese-system-modal-controls");