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;
#[cfg(feature = "debug")]
pub mod debug;
pub mod hooks;
pub mod input;
mod logger;
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 mut cursor = raw_args.cursor();
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();
// 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
let mut event_loop = EventLoop::try_new().with_context(|| "Failed to initialize event loop")?;
// init wayland

View file

@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-only
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);
std::process::exit(1);
}

View file

@ -4,6 +4,7 @@ use super::{
};
use crate::{
backend::render::cursor::CursorState,
hooks::{Decorations, HOOKS},
shell::{
focus::target::PointerFocusTarget,
grabs::{ReleaseMode, ResizeEdge},
@ -1007,12 +1008,57 @@ impl Program for CosmicStackInternal {
}
fn view(&self) -> CosmicElement<'_, Self::Message> {
let windows = self.windows.lock().unwrap();
if self.geometry.lock().unwrap().is_none() {
HOOKS.get().unwrap().stack_decorations.view(self)
}
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();
};
let active = self.active.load(Ordering::SeqCst);
let group_focused = self.group_focused.load(Ordering::SeqCst);
let active = stack.active.load(Ordering::SeqCst);
let group_focused = stack.group_focused.load(Ordering::SeqCst);
let elements = vec![
cosmic_widget::icon::from_name("window-stack-symbolic")
@ -1057,7 +1103,8 @@ impl Program for CosmicStackInternal {
)
.id(SCROLLABLE_ID.clone())
.force_visible(
self.scroll_to_focus
stack
.scroll_to_focus
.load(Ordering::SeqCst)
.then_some(active),
)
@ -1079,7 +1126,7 @@ impl Program for CosmicStackInternal {
} else {
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)
.height(TAB_HEIGHT as u16)
@ -1109,42 +1156,6 @@ impl Program for CosmicStackInternal {
}))
.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 {

View file

@ -1,5 +1,6 @@
use crate::{
backend::render::cursor::CursorState,
hooks::{Decorations, HOOKS},
shell::{
focus::target::PointerFocusTarget,
grabs::{ReleaseMode, ResizeEdge},
@ -554,11 +555,20 @@ impl Program for CosmicWindowInternal {
}
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()
.title(self.last_title.lock().unwrap().clone())
.title(win.last_title.lock().unwrap().clone())
.on_drag(Message::DragStart)
.on_close(Message::Close)
.focused(self.window.is_activated(false))
.focused(win.window.is_activated(false))
.on_double_click(Message::Maximize)
.on_right_click(Message::Menu)
.is_ssd(true);