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, }; impl FereseShell { pub(super) fn output_event(&mut self, event: wayland::OutputEvent, output: wl_output::WlOutput) -> Task { if matches!(event, wayland::OutputEvent::Removed) { if let Some(index) = self.outputs.iter().position(|entry| entry.output == output) { let entry = self.outputs.remove(index); self.refresh_media_art(false); let menu = if self.bar_surface_id == entry.bar { self.destroy_menu() } else { Task::none() }; let mut tasks = vec![destroy_layer_surface(entry.bar), menu, 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); } 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, _ => None, }; let name = match event { wayland::OutputEvent::Created(info) => info.and_then(|info| info.name), 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; let bar_layout = self.config.status.bar_layout; let bar = BarMetrics::from(shell_theme); let wallpaper_output = output.clone(); let bar_output = output.clone(); let hidden = output_bar_hidden( &self.snapshot, self.snapshot .outputs .iter() .find(|output| Some(output.name.as_str()) == name.as_deref()) .map(|output| output.id), ); self.outputs.push(OutputSurfaces { output, name, bar: bar_surface_id, wallpaper: std::env::var_os("FERESE_COMPOSITOR_WALLPAPER") .is_none() .then_some(wallpaper_surface_id), effects: None, bar_regions: Vec::new(), clock: None, notes: Vec::new(), size, hidden, }); self.refresh_media_art(false); let wallpaper_action = cosmic::surface::action::app_layer_shell::( |_| Default::default(), move |_| SctkLayerSurfaceSettings { id: wallpaper_surface_id, layer: Layer::Background, keyboard_interactivity: KeyboardInteractivity::None, input_zone: Some(Vec::new()), anchor: Anchor::TOP | Anchor::RIGHT | Anchor::BOTTOM | Anchor::LEFT, output: IcedOutput::Output(wallpaper_output.clone()), namespace: "ferese-shell-wallpaper".to_owned(), // Wallpaper covers the full output, including beneath the bar // and its floating margins. Zero would use the remaining workspace. exclusive_zone: -1, size: Some((None, None)), size_limits: Limits::NONE, ..Default::default() }, Some(Box::new(Self::view_wallpaper)), ); let bar_action = cosmic::surface::action::app_layer_shell::( |_| Default::default(), move |_| SctkLayerSurfaceSettings { id: bar_surface_id, input_zone: super::bar::input_region(bar_layout, hidden, &[]), layer: Layer::Top, keyboard_interactivity: KeyboardInteractivity::None, anchor: Anchor::TOP | Anchor::LEFT | Anchor::RIGHT, output: IcedOutput::Output(bar_output.clone()), namespace: "ferese-shell-top-bar".to_owned(), margin: IcedMargin { top: shell_theme.bar_margin_top, right: shell_theme.bar_margin_horizontal, bottom: 0, left: shell_theme.bar_margin_horizontal, }, size: Some((None, Some(bar.height.round() as u32))), size_limits: Limits::NONE, // Reserve breathing room below the visible bar; layer-shell // accounts for the top margin separately. exclusive_zone: (bar.height.round() as i32).saturating_add(shell_theme.bar_window_gap), }, Some(Box::new(move |app| app.view_layer(bar_surface_id))), ); // Submit the small interactive surface before the full-screen image. let mut surfaces = vec![bar_action]; if std::env::var_os("FERESE_COMPOSITOR_WALLPAPER").is_none() { surfaces.push(wallpaper_action); } let tasks = surfaces .into_iter() .map(cosmic::Action::Surface) .map(cosmic::task::message); 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 handle_event(&mut self, event: Event, id: window::Id) -> Task { if let Event::Mouse(cosmic::iced::mouse::Event::ButtonPressed(_)) = &event && let Some(modal) = &mut self.system_modal && modal.contains(id) { 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| ¬e.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), .. }) = &event && self .system_modal .as_ref() .is_some_and(|modal| modal.is_display_mode() && modal.contains(id)) { return if self.display_mode.pending() { self.confirm_display_mode(true) } else { self.apply_display_mode() }; } if let Event::Keyboard(cosmic::iced::keyboard::Event::KeyPressed { key, .. }) = &event && self .system_modal .as_ref() .is_some_and(|modal| modal.is_display_mode() && modal.contains(id)) && matches!( key, cosmic::iced::keyboard::Key::Character(_) | cosmic::iced::keyboard::Key::Named( cosmic::iced::keyboard::key::Named::ArrowLeft | cosmic::iced::keyboard::key::Named::ArrowRight | cosmic::iced::keyboard::key::Named::ArrowUp | cosmic::iced::keyboard::key::Named::ArrowDown ) ) { return self.display_mode_key(key); } if let Event::Keyboard(cosmic::iced::keyboard::Event::KeyPressed { key: cosmic::iced::keyboard::Key::Named(cosmic::iced::keyboard::key::Named::Tab), modifiers, .. }) = &event && !modifiers.control() && self.system_modal.as_ref().is_some_and(|modal| modal.contains(id)) { return self.navigate_system_modal(modifiers.shift()); } if matches!( &event, Event::Keyboard(cosmic::iced::keyboard::Event::KeyPressed { key: cosmic::iced::keyboard::Key::Named(cosmic::iced::keyboard::key::Named::Escape), .. }) ) { 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(); } } match event { Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Output(event, output))) => { self.output_event(event, output) } Event::Window(window::Event::Opened { .. }) if self.outputs.iter().any(|entry| entry.bar == 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) => { Task::batch([ self.focus_system_modal(id), window::run(id, native_wayland_surface) .map(move |surface| cosmic::Action::App(Message::NativeSurface(id, surface))), ]) } Event::Keyboard(cosmic::iced::keyboard::Event::KeyPressed { key: cosmic::iced::keyboard::Key::Named(cosmic::iced::keyboard::key::Named::Escape), .. }) if self.menu.is_some() => self.close_menu(), Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Popup(event, surface, popup_id))) => { if let Some(menu) = &mut self.menu && menu.id == popup_id { match event { wayland::PopupEvent::Done => { if menu.kind == status_ui::Menu::Notifications { self.notifications.history_open = false; self.notifications.hovered = None; } self.menu = None; EFFECT_FRAME_PENDING.store(false, Ordering::Relaxed); return self.sync_notification_surface(); } wayland::PopupEvent::Configured { .. } => { menu.prepare_surface(&surface); } _ => {} } } Task::none() } Event::Window(window::Event::Closed) if self.menu.as_ref().is_some_and(|menu| menu.id == id) => { self.menu = None; EFFECT_FRAME_PENDING.store(false, Ordering::Relaxed); Task::none() } Event::Window(window::Event::Closed) if self.system_modal.as_ref().is_some_and(|modal| modal.contains(id)) => { self.close_system_modal() } Event::Window(window::Event::Closed) => Task::none(), Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Frame(_, surface, frame_id))) if self.menu.as_ref().is_some_and(|menu| menu.id == frame_id) => { if let Some(menu) = &mut self.menu && menu.effects.is_none() { menu.prepare_surface(&surface); } Task::none() } Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Frame(_, surface, frame_id))) if self.outputs.iter().any(|entry| entry.bar == frame_id) => { self.attach_effects(frame_id, &surface); Task::none() } Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Layer(_, surface, layer_id))) if self .notification_surface .as_ref() .is_some_and(|entry| entry.id == layer_id) => { let entry = self.notification_surface.as_mut().unwrap(); if entry.effects.is_none() { match EffectsBinding::attach_role(&surface, None, 1.0) { Ok(binding) => entry.effects = Some(binding), Err(error) => { eprintln!("ferese-shell: notification effects unavailable: {error}") } } } Task::none() } Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Layer(_, surface, layer_id))) if self.outputs.iter().any(|entry| entry.bar == layer_id) => { self.attach_effects(layer_id, &surface); self.attach_power_material(layer_id, &surface); Task::none() } _ => Task::none(), } } pub(super) fn attach_effects(&mut self, id: window::Id, surface: &wl_surface::WlSurface) { let hidden = self.bar_hidden(id); let islands = self.config.status.bar_layout == ferese_config::BarLayout::Islands; let Some(entry) = self.outputs.iter_mut().find(|entry| entry.bar == id) else { return; }; if entry.effects.is_some() { return; } EFFECT_FRAME_PENDING.store(false, Ordering::Relaxed); let binding = if islands { // Wait for measured island bounds rather than briefly painting the full bar. EffectsBinding::attach_role(surface, None, 1.0) } else { EffectsBinding::attach(surface, !hidden) }; match binding { Ok(binding) => entry.effects = Some(binding), Err(error) => eprintln!("ferese-shell: panel material unavailable: {error}"), } } pub(super) fn view_wallpaper(&self) -> Element<'_, cosmic::Action> { let Some(handle) = self.wallpaper.as_ref() else { return container(text("")) .width(Length::Fill) .height(Length::Fill) .class(theme::Container::custom(wallpaper_fallback_style)) .into(); }; let content_fit = match self.config.wallpaper.mode { WallpaperMode::Fill => ContentFit::Cover, WallpaperMode::Fit => ContentFit::Contain, }; container( image(handle.clone()) .width(Length::Fill) .height(Length::Fill) .content_fit(content_fit), ) .width(Length::Fill) .height(Length::Fill) .class(theme::Container::custom(wallpaper_fallback_style)) .into() } } // Resolve a surface while iced guarantees the native handles are alive. Keep // the borrowed-display connection alive until the effects binding takes over. pub(super) fn native_wayland_surface( window: &dyn cosmic::iced::window::Window, ) -> Result<(Connection, wl_surface::WlSurface), String> { use cosmic::iced::window::raw_window_handle::{RawDisplayHandle, RawWindowHandle}; let display = window.display_handle().map_err(|error| error.to_string())?; let handle = window.window_handle().map_err(|error| error.to_string())?; let (RawDisplayHandle::Wayland(display), RawWindowHandle::Wayland(handle)) = (display.as_raw(), handle.as_raw()) else { return Err("shell material requires a Wayland surface".to_owned()); }; // SAFETY: iced lends matching, live Wayland handles for this callback. // This backend borrows the display; it never disconnects iced's connection. // Both handles belong to the runtime, which outlives the shell bindings. let backend = unsafe { wayland_client::backend::Backend::from_foreign_display(display.display.as_ptr().cast()) }; let connection = Connection::from_backend(backend); // SAFETY: raw-window-handle identifies this pointer as a live wl_surface. let id = unsafe { wayland_client::backend::ObjectId::from_ptr(wl_surface::WlSurface::interface(), handle.surface.as_ptr().cast()) } .map_err(|error| error.to_string())?; let surface = wl_surface::WlSurface::from_id(&connection, id).map_err(|error| error.to_string())?; Ok((connection, surface)) } pub(super) fn wallpaper_fallback_style(_theme: &cosmic::Theme) -> container::Style { let [red, green, blue] = config::default_background(); container::Style { background: Some(Background::Color(Color::from_rgb8(red, green, blue))), ..container::Style::default() } } fn encode_regions(regions: &[[f32; 5]]) -> Vec { regions .iter() .flat_map(|region| region.iter().flat_map(|value| value.to_ne_bytes())) .collect() } pub(super) struct EffectsBinding { connection: Connection, _manager: FereseEffectsManagerV1, pub(super) surface: FereseSurfaceEffectsV1, _queue: EventQueue, regions: std::cell::RefCell>>, opacity: std::cell::Cell>, presentation: std::cell::RefCell, u32, Vec, ferese_surface_effects_v1::Role)>>, } impl EffectsBinding { pub(super) fn attach(surface: &wl_surface::WlSurface, visible: bool) -> Result> { Self::attach_role( surface, visible.then_some(ferese_surface_effects_v1::Role::Panel), if visible { 1.0 } else { 0.0 }, ) } pub(super) fn attach_role( surface: &wl_surface::WlSurface, role: Option, opacity: f32, ) -> Result> { let backend = surface .backend() .upgrade() .ok_or("libcosmic Wayland connection is no longer alive")?; let connection = Connection::from_backend(backend); let (globals, queue) = registry_queue_init::(&connection)?; let qh = queue.handle(); let manager = globals.bind::(&qh, 5..=5, ())?; let effects = manager.get_surface_effects(surface, &qh, ()); let opacity = (opacity.clamp(0.0, 1.0) * 1000.0).round() as u32; effects.set_opacity(opacity); if let Some(role) = role { effects.set_role(role); } else { effects.clear_role(); } connection.flush()?; Ok(Self { connection, _manager: manager, surface: effects, _queue: queue, regions: Default::default(), opacity: std::cell::Cell::new(Some(opacity)), presentation: Default::default(), }) } pub(super) fn set_presentation( &self, regions: &[[f32; 5]], opacity: f32, region_opacities: impl IntoIterator, role: ferese_surface_effects_v1::Role, ) -> Result<(), Box> { let opacity = (opacity.clamp(0.0, 1.0) * 1000.0).round() as u32; let values: Vec = region_opacities .into_iter() .take(32) .map(|value| (value.clamp(0.0, 1.0) * 1000.0).round() as u32) .collect(); let next = (regions.to_vec(), opacity, values, role); if self.presentation.borrow().as_ref() != Some(&next) { self.surface.set_presentation( role, encode_regions(regions), opacity, next.2.iter().flat_map(|value| value.to_ne_bytes()).collect(), ); self.connection.flush()?; *self.presentation.borrow_mut() = Some(next); } Ok(()) } pub(super) fn set_regions(&self, regions: &[[f32; 5]]) -> Result<(), Box> { self.set_material_regions(regions, ferese_surface_effects_v1::Role::Popover) } pub(super) fn set_material_regions( &self, regions: &[[f32; 5]], role: ferese_surface_effects_v1::Role, ) -> Result<(), Box> { if self.regions.borrow().as_deref() == Some(regions) { return Ok(()); } self.surface.set_regions(encode_regions(regions)); if regions.is_empty() { self.surface.clear_role(); } else { self.surface.set_role(role); } self.connection.flush()?; *self.regions.borrow_mut() = Some(regions.to_vec()); Ok(()) } pub(super) fn set_visible(&self, visible: bool) -> Result<(), Box> { self.set_opacity(1.0)?; if visible { self.surface.set_role(ferese_surface_effects_v1::Role::Panel); } else { self.surface.clear_role(); } self.connection.flush()?; Ok(()) } pub(super) fn set_opacity(&self, opacity: f32) -> Result<(), Box> { let opacity = (opacity.clamp(0.0, 1.0) * 1000.0).round() as u32; if self.opacity.get() != Some(opacity) { self.surface.set_opacity(opacity); self.connection.flush()?; self.opacity.set(Some(opacity)); } Ok(()) } } struct EffectsState; impl Dispatch for EffectsState { fn event( _state: &mut Self, _registry: &wl_registry::WlRegistry, _event: wl_registry::Event, _data: &GlobalListContents, _connection: &Connection, _qh: &QueueHandle, ) { } } delegate_noop!(EffectsState: ignore FereseEffectsManagerV1); delegate_noop!(EffectsState: ignore FereseSurfaceEffectsV1); #[cfg(test)] mod region_encoding_tests { #[test] fn fractional_region_encoding_preserves_values() { let regions = [[0.25, -0.75, 100.5, 40.25, 14.0]]; let bytes = super::encode_regions(®ions); let decoded: Vec<_> = bytes .as_chunks::<4>() .0 .iter() .map(|&v| f32::from_ne_bytes(v)) .collect(); assert_eq!(decoded, regions[0]); } }