Add hooks for custom window decorations

This is a first use of the new hooks system, which allows customizing
cosmic-comp at compile-time.
In this case, the view() function of CosmicWindow / CosmicStack is
hooked and the hook can change what is rendered as the header bar.

Signed-off-by: Yureka <yuka@yuka.dev>
This commit is contained in:
Yureka 2025-09-29 17:34:30 +02:00 • committed by Victoria Brekenfeld
parent d6e11de1f1
commit a74b6e3a9b
5 changed files with 119 additions and 46 deletions

47
src/hooks.rs Normal file
View file

@ -0,0 +1,47 @@
// SPDX-License-Identifier: GPL-3.0-only
use crate::shell::element::stack::{
CosmicStackInternal, DefaultDecorations as DefaultStackDecorations, Message as StackMessage,
};
use crate::shell::element::window::{
CosmicWindowInternal, DefaultDecorations as DefaultWindowDecorations, Message as WindowMessage,
};
use std::sync::{Arc, OnceLock};
/// An _unstable_ interface to customize cosmic-comp at compile-time by providing
/// hooks to be run in specific code paths.
#[derive(Default, Debug, Clone)]
pub struct Hooks {
pub window_decorations:
Option<Arc<dyn Decorations<CosmicWindowInternal, WindowMessage> + Send + Sync>>,
pub stack_decorations:
Option<Arc<dyn Decorations<CosmicStackInternal, StackMessage> + Send + Sync>>,
}
pub static HOOKS: OnceLock<Hooks> = OnceLock::new();
pub trait Decorations<Internal, Message>: std::fmt::Debug {
fn view(&self, state: &Internal) -> cosmic::Element<'_, Message>;
}
impl Decorations<CosmicWindowInternal, WindowMessage>
for Option<Arc<dyn Decorations<CosmicWindowInternal, WindowMessage> + Send + Sync>>
{
fn view(&self, window: &CosmicWindowInternal) -> cosmic::Element<'_, WindowMessage> {
match self {
None => DefaultWindowDecorations.view(window),
Some(deco) => deco.view(window),
}
}
}
impl Decorations<CosmicStackInternal, StackMessage>
for Option<Arc<dyn Decorations<CosmicStackInternal, StackMessage> + Send + Sync>>
{
fn view(&self, window: &CosmicStackInternal) -> cosmic::Element<'_, StackMessage> {
match self {
None => DefaultStackDecorations.view(window),
Some(deco) => deco.view(window),
}
}
}

View file

@ -33,6 +33,7 @@ pub mod config;
pub mod dbus; pub mod dbus;
#[cfg(feature = "debug")] #[cfg(feature = "debug")]
pub mod debug; pub mod debug;
pub mod hooks;
pub mod input; pub mod input;
mod logger; mod logger;
pub mod session; pub mod session;
@ -103,7 +104,7 @@ impl State {
} }
} }
pub fn run() -> Result<(), Box<dyn Error>> { pub fn run(hooks: crate::hooks::Hooks) -> Result<(), Box<dyn Error>> {
let raw_args = RawArgs::from_args(); let raw_args = RawArgs::from_args();
let mut cursor = raw_args.cursor(); let mut cursor = raw_args.cursor();
let git_hash = option_env!("GIT_HASH").unwrap_or("unknown"); let git_hash = option_env!("GIT_HASH").unwrap_or("unknown");
@ -137,6 +138,10 @@ pub fn run() -> Result<(), Box<dyn Error>> {
utils::rlimit::increase_nofile_limit(); utils::rlimit::increase_nofile_limit();
// init hook globals
hooks::HOOKS.set(hooks)
.expect("Hooks global has already been initialized. Running multiple instances of COSMIC in one process is not supported.");
// init event loop // init event loop
let mut event_loop = EventLoop::try_new().with_context(|| "Failed to initialize event loop")?; let mut event_loop = EventLoop::try_new().with_context(|| "Failed to initialize event loop")?;
// init wayland // init wayland

View file

@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
fn main() { fn main() {
if let Err(err) = cosmic_comp::run() { if let Err(err) = cosmic_comp::run(Default::default()) {
tracing::error!("Error occured in main(): {}", err); tracing::error!("Error occured in main(): {}", err);
std::process::exit(1); std::process::exit(1);
} }

View file

@ -4,6 +4,7 @@ use super::{
}; };
use crate::{ use crate::{
backend::render::cursor::CursorState, backend::render::cursor::CursorState,
hooks::{Decorations, HOOKS},
shell::{ shell::{
focus::target::PointerFocusTarget, focus::target::PointerFocusTarget,
grabs::{ReleaseMode, ResizeEdge}, grabs::{ReleaseMode, ResizeEdge},
@ -1007,12 +1008,57 @@ impl Program for CosmicStackInternal {
} }
fn view(&self) -> CosmicElement<'_, Self::Message> { fn view(&self) -> CosmicElement<'_, Self::Message> {
let windows = self.windows.lock().unwrap(); HOOKS.get().unwrap().stack_decorations.view(self)
if self.geometry.lock().unwrap().is_none() { }
fn foreground(
&self,
pixels: &mut tiny_skia::PixmapMut<'_>,
damage: &[Rectangle<i32, Buffer>],
scale: f32,
theme: &Theme,
) {
if self.group_focused.load(Ordering::SeqCst) {
let border = Rectangle::new(
(0, ((TAB_HEIGHT as f32 * scale) - scale).floor() as i32).into(),
(pixels.width() as i32, scale.ceil() as i32).into(),
);
let mut paint = tiny_skia::Paint::default();
let (b, g, r, a) = theme.cosmic().accent_color().into_components();
paint.set_color(tiny_skia::Color::from_rgba(r, g, b, a).unwrap());
for rect in damage {
if let Some(overlap) = rect.intersection(border) {
pixels.fill_rect(
tiny_skia::Rect::from_xywh(
overlap.loc.x as f32,
overlap.loc.y as f32,
overlap.size.w as f32,
overlap.size.h as f32,
)
.unwrap(),
&paint,
Default::default(),
None,
)
}
}
}
}
}
#[derive(Debug)]
pub struct DefaultDecorations;
impl Decorations<CosmicStackInternal, Message> for DefaultDecorations {
fn view(&self, stack: &CosmicStackInternal) -> cosmic::Element<'_, Message> {
let windows = stack.windows.lock().unwrap();
if stack.geometry.lock().unwrap().is_none() {
return iced_widget::row(Vec::new()).into(); return iced_widget::row(Vec::new()).into();
}; };
let active = self.active.load(Ordering::SeqCst); let active = stack.active.load(Ordering::SeqCst);
let group_focused = self.group_focused.load(Ordering::SeqCst); let group_focused = stack.group_focused.load(Ordering::SeqCst);
let elements = vec![ let elements = vec![
cosmic_widget::icon::from_name("window-stack-symbolic") cosmic_widget::icon::from_name("window-stack-symbolic")
@ -1057,7 +1103,8 @@ impl Program for CosmicStackInternal {
) )
.id(SCROLLABLE_ID.clone()) .id(SCROLLABLE_ID.clone())
.force_visible( .force_visible(
self.scroll_to_focus stack
.scroll_to_focus
.load(Ordering::SeqCst) .load(Ordering::SeqCst)
.then_some(active), .then_some(active),
) )
@ -1079,7 +1126,7 @@ impl Program for CosmicStackInternal {
} else { } else {
Radius::from([8.0, 8.0, 0.0, 0.0]) Radius::from([8.0, 8.0, 0.0, 0.0])
}; };
let group_focused = self.group_focused.load(Ordering::SeqCst); let group_focused = stack.group_focused.load(Ordering::SeqCst);
iced_widget::row(elements) iced_widget::row(elements)
.height(TAB_HEIGHT as u16) .height(TAB_HEIGHT as u16)
@ -1109,42 +1156,6 @@ impl Program for CosmicStackInternal {
})) }))
.into() .into()
} }
fn foreground(
&self,
pixels: &mut tiny_skia::PixmapMut<'_>,
damage: &[Rectangle<i32, Buffer>],
scale: f32,
theme: &Theme,
) {
if self.group_focused.load(Ordering::SeqCst) {
let border = Rectangle::new(
(0, ((TAB_HEIGHT as f32 * scale) - scale).floor() as i32).into(),
(pixels.width() as i32, scale.ceil() as i32).into(),
);
let mut paint = tiny_skia::Paint::default();
let (b, g, r, a) = theme.cosmic().accent_color().into_components();
paint.set_color(tiny_skia::Color::from_rgba(r, g, b, a).unwrap());
for rect in damage {
if let Some(overlap) = rect.intersection(border) {
pixels.fill_rect(
tiny_skia::Rect::from_xywh(
overlap.loc.x as f32,
overlap.loc.y as f32,
overlap.size.w as f32,
overlap.size.h as f32,
)
.unwrap(),
&paint,
Default::default(),
None,
)
}
}
}
}
} }
impl IsAlive for CosmicStack { impl IsAlive for CosmicStack {

View file

@ -1,5 +1,6 @@
use crate::{ use crate::{
backend::render::cursor::CursorState, backend::render::cursor::CursorState,
hooks::{Decorations, HOOKS},
shell::{ shell::{
focus::target::PointerFocusTarget, focus::target::PointerFocusTarget,
grabs::{ReleaseMode, ResizeEdge}, grabs::{ReleaseMode, ResizeEdge},
@ -554,11 +555,20 @@ impl Program for CosmicWindowInternal {
} }
fn view(&self) -> cosmic::Element<'_, Self::Message> { fn view(&self) -> cosmic::Element<'_, Self::Message> {
HOOKS.get().unwrap().window_decorations.view(self)
}
}
#[derive(Debug)]
pub struct DefaultDecorations;
impl Decorations<CosmicWindowInternal, Message> for DefaultDecorations {
fn view(&self, win: &CosmicWindowInternal) -> cosmic::Element<'_, Message> {
let mut header = cosmic::widget::header_bar() let mut header = cosmic::widget::header_bar()
.title(self.last_title.lock().unwrap().clone()) .title(win.last_title.lock().unwrap().clone())
.on_drag(Message::DragStart) .on_drag(Message::DragStart)
.on_close(Message::Close) .on_close(Message::Close)
.focused(self.window.is_activated(false)) .focused(win.window.is_activated(false))
.on_double_click(Message::Maximize) .on_double_click(Message::Maximize)
.on_right_click(Message::Menu) .on_right_click(Message::Menu)
.is_ssd(true); .is_ssd(true);