Unify shell opacity and add native authentication and system controls

This commit is contained in:
Samuel Onoja committed 2026-09-28 09:26:14 +01:00
1 parent 8832a9bf2e
commit c8e89fb758
35 files changed
+1674 -133

No files matched your search

+136 -3
View File
@@ -59,12 +59,15 @@ fn configured_font(family: Option<&str>) -> cosmic::font::Font {
std::sync::Mutex<std::collections::HashMap<String, cosmic::font::Font>>,
> = std::sync::OnceLock::new();
let mut fonts = FONTS.get_or_init(Default::default).lock().unwrap();
if let Some(font) = fonts.get(family) {
return *font;
}
if fonts.len() >= 64 {
return cosmic::font::default();
}
let family = Box::leak(family.to_owned().into_boxed_str());
let font = cosmic::font::Font::with_name(family);
fonts.insert(family.to_owned(), font);
@@ -113,14 +116,17 @@ impl From<ShellTheme> for BarMetrics {
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 parts.next().is_some() {
return None;
}
let (hour, minute) = time.split_once(':')?;
if !(1..=2).contains(&hour.len())
|| minute.len() != 2
@@ -131,6 +137,7 @@ fn stacked_clock_digits(label: &str) -> Option<[String; 2]> {
{
return None;
}
Some([format!("{hour:0>2}"), minute.to_owned()])
}
@@ -142,6 +149,7 @@ fn main() -> cosmic::iced::Result {
// toolkit threads are started.
unsafe { std::env::set_var("ICED_BACKEND", "tiny-skia,wgpu") };
}
cosmic::iced::advanced::graphics::text::font_system()
.write()
.unwrap()
@@ -154,6 +162,7 @@ fn main() -> cosmic::iced::Result {
.load_font(std::borrow::Cow::Borrowed(include_bytes!(
"../../../assets/fonts/Cantarell-ExtraBold.otf"
)));
let mut config = config::load();
motion::configure(config.animations, config.theme.material_radius);
let compositor_wallpaper = std::env::var_os("FERESE_COMPOSITOR_WALLPAPER").is_some();
@@ -207,6 +216,7 @@ struct FereseShell {
status_service: status::Service,
status: status::Snapshot,
recorder: recording::Recorder,
calendar_offset: i32,
status_error: Option<String>,
menu: Option<status_ui::OpenMenu>,
note_editor: Option<DesktopNoteEditor>,
@@ -284,6 +294,8 @@ enum Message {
Control(status::Action),
ConfirmPower(status::Action),
CancelPower,
CalendarMonth(i32),
CalendarToday,
}
impl cosmic::Application for FereseShell {
@@ -316,6 +328,7 @@ impl cosmic::Application for FereseShell {
status_service: status::Service::start(config.status.settings_command.clone()),
status: status::Snapshot::default(),
recorder: recording::Recorder::default(),
calendar_offset: 0,
status_error: None,
menu: None,
note_editor: None,
@@ -348,6 +361,7 @@ impl cosmic::Application for FereseShell {
}),
None => Task::none(),
};
(app, wallpaper_task)
}
@@ -420,22 +434,28 @@ impl cosmic::Application for FereseShell {
if let Err(error) = effects.set_opacity(menu.progress()) {
eprintln!("ferese-shell: could not update popup opacity: {error}");
}
let regions = menu.regions.lock().unwrap().clone();
if let Err(error) = effects.set_regions(&regions) {
eprintln!("ferese-shell: could not update card materials: {error}");
}
}
if let Some(surface) = &self.notification_surface
&& let Some(effects) = &surface.effects
{
let regions = surface.regions.lock().unwrap().clone();
if let Err(error) = effects.set_regions(&regions) {
eprintln!("ferese-shell: could not update notification materials: {error}");
}
}
match message {
Message::BeginNoteEdit(id) => {
let mut tasks = vec![self.finish_note_edit()];
if let Some(note) = self
.config
.desktop_widgets
@@ -459,6 +479,7 @@ impl cosmic::Application for FereseShell {
{
let edited = action.is_edit();
editor.content.perform(action);
if edited {
editor.revision = editor.revision.wrapping_add(1);
let revision = editor.revision;
@@ -525,6 +546,7 @@ impl cosmic::Application for FereseShell {
),
}
}
if let Some(menu) = &mut self.menu
&& menu.id == id
&& menu.effects.is_none()
@@ -539,6 +561,7 @@ impl cosmic::Application for FereseShell {
eprintln!("ferese-shell: popover material unavailable: {error}")
}
}
if let Some(effects) = &menu.effects {
let _ = effects.set_opacity(menu.progress());
}
@@ -566,6 +589,7 @@ impl cosmic::Application for FereseShell {
self.status_error = update.error;
}
self.notifications.tick();
if self.notifications.ready {
self.status.notifications = Some(status::Notifications {
count: self.notifications.unread(),
@@ -636,6 +660,14 @@ impl cosmic::Application for FereseShell {
}
Task::none()
}
Message::CalendarMonth(delta) => {
self.calendar_offset = (self.calendar_offset + delta).clamp(-1200, 1200);
Task::none()
}
Message::CalendarToday => {
self.calendar_offset = 0;
Task::none()
}
Message::CancelPower => {
if let Some(menu) = &mut self.menu {
menu.confirm = None;
@@ -643,6 +675,18 @@ impl cosmic::Application for FereseShell {
Task::none()
}
Message::Control(action) => {
if let status::Action::PowerProfile(profile) = action {
self.status_error = self
.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();
}
}
return Task::none();
}
if self.notifications.ready {
match action {
status::Action::Dnd(value) => {
@@ -774,9 +818,11 @@ impl FereseShell {
return Task::none();
}
};
if std::env::var_os("FERESE_COMPOSITOR_WALLPAPER").is_some() {
config.wallpaper.path = None;
}
if self.config.font_family != config.font_family {
*SHELL_FONT
.get_or_init(|| std::sync::RwLock::new(cosmic::font::default()))
@@ -784,6 +830,7 @@ impl FereseShell {
.unwrap() = configured_font(config.font_family.as_deref());
}
motion::configure(config.animations, config.theme.material_radius);
if self.config.animations != config.animations
&& let Some(menu) = &mut self.menu
{
@@ -791,6 +838,7 @@ impl FereseShell {
}
self.status_service
.update_settings(config.status.settings_command.clone());
let old = self.config.theme;
let old_clock = &self.config.desktop_widgets.clock;
let old_notes = &self.config.desktop_widgets.notes;
@@ -826,6 +874,7 @@ impl FereseShell {
} else {
Task::none()
}];
if clock_changed
&& self
.note_drag
@@ -834,6 +883,7 @@ impl FereseShell {
{
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| {
@@ -848,6 +898,7 @@ impl FereseShell {
}
tasks.push(self.rebuild_notes(true));
}
for entry in self.outputs.iter().filter(|_| geometry_changed) {
tasks.push(set_size(
entry.bar,
@@ -866,6 +917,7 @@ impl FereseShell {
(theme.bar_height.round() as i32).saturating_add(theme.bar_window_gap),
));
}
Task::batch(tasks)
}
@@ -882,6 +934,7 @@ impl FereseShell {
} else {
Task::none()
};
let mut tasks = vec![destroy_layer_surface(entry.bar), menu];
if self.note_drag.as_ref().is_some_and(|drag| {
entry.clock == Some(drag.source)
@@ -889,20 +942,26 @@ impl FereseShell {
}) {
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);
}
return Task::none();
}
let size = match &event {
wayland::OutputEvent::Created(info) => info.as_ref().and_then(|info| info.logical_size),
wayland::OutputEvent::InfoUpdate(info) => info.logical_size,
@@ -913,20 +972,24 @@ impl FereseShell {
wayland::OutputEvent::InfoUpdate(info) => info.name,
_ => None,
};
if let Some(entry) = self.outputs.iter_mut().find(|entry| entry.output == output) {
if size.is_some() {
entry.size = size;
}
let changed = name.is_some() && entry.name != name;
if name.is_some() {
entry.name = name;
}
return if changed {
Task::batch([self.rebuild_clocks(true), self.rebuild_notes(true)])
} else {
Task::none()
};
}
let bar_surface_id = window::Id::unique();
let wallpaper_surface_id = window::Id::unique();
let shell_theme = self.config.theme;
@@ -1005,6 +1068,7 @@ impl FereseShell {
if std::env::var_os("FERESE_COMPOSITOR_WALLPAPER").is_none() {
surfaces.push(wallpaper_action);
}
let tasks = surfaces
.into_iter()
.map(cosmic::Action::Surface)
@@ -1021,6 +1085,7 @@ impl FereseShell {
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();
@@ -1109,6 +1174,7 @@ impl FereseShell {
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,
@@ -1122,6 +1188,7 @@ impl FereseShell {
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(
@@ -1130,6 +1197,7 @@ impl FereseShell {
));
}
}
let overlay = window::Id::unique();
self.note_drag = Some(NoteDrag {
id,
@@ -1161,8 +1229,10 @@ impl FereseShell {
size_limits: Limits::NONE,
..Default::default()
},
Some(Box::new(Self::view_note_drag)),
);
Task::batch([
self.finish_note_edit(),
cosmic::task::message(cosmic::Action::Surface(action)),
@@ -1173,6 +1243,7 @@ impl FereseShell {
let Some(drag) = &self.note_drag else {
return text("").into();
};
if let Some(id) = &drag.id {
self.view_note_content(id, drag.source)
} else {
@@ -1185,6 +1256,7 @@ impl FereseShell {
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;
@@ -1202,6 +1274,7 @@ impl FereseShell {
clock.margin_y,
));
}
if let Some(note) = self
.config
.desktop_widgets
@@ -1212,6 +1285,7 @@ impl FereseShell {
note.anchor = ferese_core::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
@@ -1222,12 +1296,14 @@ impl FereseShell {
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)
@@ -1235,6 +1311,7 @@ impl FereseShell {
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(..) {
@@ -1242,12 +1319,14 @@ impl FereseShell {
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();
@@ -1281,11 +1360,14 @@ impl FereseShell {
..Default::default()
}
},
Some(Box::new(move |app| app.view_note(&note_id, id))),
);
tasks.push(cosmic::task::message(cosmic::Action::Surface(action)));
}
}
Task::batch(tasks)
}
@@ -1300,6 +1382,7 @@ impl FereseShell {
.height(Length::Fill)
.into();
}
self.view_note_content(id, surface)
}
@@ -1334,7 +1417,9 @@ impl FereseShell {
.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 {
@@ -1350,6 +1435,7 @@ impl FereseShell {
let mut body = cosmic::widget::column([])
.spacing(note.gap)
.align_x(alignment);
if note.interactive {
let title = cosmic::widget::text(if note.title.is_empty() {
"⋮⋮"
@@ -1369,6 +1455,7 @@ impl FereseShell {
.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" }))
@@ -1396,6 +1483,7 @@ impl FereseShell {
.class(theme::Text::Color(foreground)),
);
}
if let Some(editor) = &self.note_editor
&& editor.id == id
{
@@ -1425,6 +1513,7 @@ impl FereseShell {
.class(theme::Text::Color(foreground)),
);
}
if let Some(error) = &self.note_error {
body = body.push(
text(error.clone())
@@ -1432,7 +1521,9 @@ impl FereseShell {
.class(theme::Text::Color(foreground)),
);
}
let radius = self.config.theme.material_radius;
container(body)
.padding(note.padding)
.width(Length::Fill)
@@ -1456,18 +1547,23 @@ impl FereseShell {
.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();
@@ -1490,10 +1586,13 @@ impl FereseShell {
..Default::default()
}
},
Some(Box::new(move |app| app.view_desktop_clock(id))),
);
tasks.push(cosmic::task::message(cosmic::Action::Surface(action)));
}
Task::batch(tasks)
}
@@ -1508,6 +1607,7 @@ impl FereseShell {
.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)
@@ -1602,6 +1702,7 @@ impl FereseShell {
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(
cosmic::widget::text(digit.to_string())
@@ -1611,8 +1712,10 @@ impl FereseShell {
.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;
@@ -1629,6 +1732,7 @@ impl FereseShell {
.time_size
.min(available_width / (characters * 0.75))
.min(available_height / 1.3);
labels = labels.push(
cosmic::widget::text(self.desktop_clock.0.clone())
.font(font)
@@ -1662,6 +1766,7 @@ impl FereseShell {
Background::Color(tint)
});
let radius = self.config.theme.material_radius;
container(labels)
.padding(clock.padding)
.width(Length::Fill)
@@ -1706,9 +1811,11 @@ impl FereseShell {
}) {
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
{
@@ -1724,6 +1831,7 @@ impl FereseShell {
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,
@@ -1748,6 +1856,7 @@ impl FereseShell {
}
}
}
cosmic::iced::mouse::Event::ButtonReleased(cosmic::iced::mouse::Button::Left) => {
if self
.note_drag
@@ -1770,13 +1879,16 @@ impl FereseShell {
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();
}
}
match event {
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Output(
event,
@@ -1800,6 +1912,7 @@ impl FereseShell {
let _ = effects.set_opacity(menu.progress());
}
}
window::run(id, native_wayland_surface)
.map(move |surface| cosmic::Action::App(Message::NativeSurface(id, surface)))
}
@@ -1978,6 +2091,7 @@ impl FereseShell {
let mut workspace_buttons = row::with_capacity(self.snapshot.workspaces.len())
.spacing(1)
.align_y(cosmic::iced::Alignment::Center);
for workspace in &self.snapshot.workspaces {
let active = workspace_active_on_bar(workspace.id, focused_output);
let owner = workspace
@@ -2040,9 +2154,28 @@ impl FereseShell {
let (date, time) = self.clock.split_once(", ").unwrap_or(("", &self.clock));
let clock = container(
row![
text(date)
.size(12)
.class(theme::Text::Color(color(shell_theme.text_muted))),
motion::button(
button::custom(text(date).size(12))
.padding([4, 2])
.name("Open calendar")
.on_press_with_rectangle(move |offset, bounds| {
cosmic::Action::App(Message::OpenMenuOn(
id,
status_ui::Menu::Calendar,
cosmic::iced::Rectangle {
x: (bounds.x - offset.x).round() as i32,
y: (bounds.y - offset.y).round() as i32,
width: bounds.width.round() as i32,
height: bounds.height.round() as i32,
},
))
}),
color(shell_theme.text_muted),
self.menu
.as_ref()
.is_some_and(|menu| menu.kind == status_ui::Menu::Calendar),
1.0
),
text(time)
.size(bar.text_size)
.class(theme::Text::Color(foreground)),