1use std::error::Error;
2use std::mem;
3use std::num::NonZeroU32;
4use std::sync::atomic::{AtomicBool, Ordering};
5use std::sync::Arc;
6use std::time::Duration;
7
8use tiny_skia::{
9 Color, FillRule, Mask, Path, PathBuilder, Pixmap, PixmapMut, PixmapPaint, Point, Rect,
10 Transform,
11};
12
13use smithay_client_toolkit::reexports::client::backend::ObjectId;
14use smithay_client_toolkit::reexports::client::protocol::wl_shm;
15use smithay_client_toolkit::reexports::client::protocol::wl_subsurface::WlSubsurface;
16use smithay_client_toolkit::reexports::client::protocol::wl_surface::WlSurface;
17use smithay_client_toolkit::reexports::client::{Dispatch, Proxy, QueueHandle};
18use smithay_client_toolkit::reexports::csd_frame::{
19 CursorIcon, DecorationsFrame, FrameAction, FrameClick, WindowManagerCapabilities, WindowState,
20};
21
22use smithay_client_toolkit::compositor::{CompositorState, Region, SurfaceData};
23use smithay_client_toolkit::shell::WaylandSurface;
24use smithay_client_toolkit::shm::{slot::SlotPool, Shm};
25use smithay_client_toolkit::subcompositor::SubcompositorState;
26use smithay_client_toolkit::subcompositor::SubsurfaceData;
27
28mod buttons;
29mod config;
30mod parts;
31mod pointer;
32mod shadow;
33pub mod theme;
34mod title;
35mod wl_typed;
36
37use crate::theme::{
38 ColorMap, ColorTheme, BORDER_SIZE, CORNER_RADIUS, HEADER_SIZE, RESIZE_HANDLE_CORNER_SIZE,
39 VISIBLE_BORDER_SIZE,
40};
41
42use buttons::Buttons;
43use config::get_button_layout_config;
44use parts::DecorationParts;
45use pointer::{Location, MouseState};
46use shadow::Shadow;
47use title::TitleText;
48use wl_typed::WlTyped;
49
50/// XXX this is not result, so `must_use` when needed.
51type SkiaResult = Option<()>;
52
53/// Whether the frame draws libadwaita's window edge in place of its own border: the
54/// radius, box shadows and outline of libadwaita 1.7's `window.csd`, round a body whose
55/// bottom corners the application cuts to match.
56pub static LIBADWAITA_EDGE: AtomicBool = AtomicBool::new(false);
57
58/// libadwaita's `--window-radius`, in logical pixels.
59pub const WINDOW_RADIUS: f32 = 15.;
60
61/// `window.csd`'s box shadows focused and not, as y offset, blur, spread and alpha of black;
62/// the last is its 1 px ring.
63const WINDOW_SHADOWS: [[[f32; 4]; 4]; 2] = [
64 [
65 [2., 8., 2., 0.13],
66 [3., 20., 10., 0.09],
67 [6., 32., 16., 0.04],
68 [0., 0., 1., 0.005],
69 ],
70 [
71 [1., 3., 3., 0.09],
72 [2., 14., 5., 0.05],
73 [4., 28., 12., 0.03],
74 [0., 0., 1., 0.02],
75 ],
76];
77
78/// The alpha of `window.csd`'s white outline, 1 px inside its edge.
79const WINDOW_OUTLINE: f32 = 0.07;
80
81/// Set once a frame is made. Its header has no height, so the application draws the title
82/// bar, and its window buttons, itself.
83pub static FRAMED: AtomicBool = AtomicBool::new(false);
84
85fn libadwaita_edge() -> bool {
86 LIBADWAITA_EDGE.load(Ordering::Relaxed)
87}
88
89/// A simple set of decorations
90#[derive(Debug)]
91pub struct AdwaitaFrame<State> {
92 /// The base surface used to create the window.
93 base_surface: WlTyped<WlSurface, SurfaceData>,
94
95 compositor: Arc<CompositorState>,
96
97 /// Subcompositor to create/drop subsurfaces ondemand.
98 subcompositor: Arc<SubcompositorState>,
99
100 /// Queue handle to perform object creation.
101 queue_handle: QueueHandle<State>,
102
103 /// The drawable decorations, `None` when hidden.
104 decorations: Option<DecorationParts>,
105
106 /// Memory pool to allocate the buffers for the decorations.
107 pool: SlotPool,
108
109 /// Whether the frame should be redrawn.
110 dirty: bool,
111
112 /// Whether the drawing should be synced with the main surface.
113 should_sync: bool,
114
115 /// Scale factor used for the surface.
116 scale_factor: u32,
117
118 /// Wether the frame is resizable.
119 resizable: bool,
120
121 buttons: Buttons,
122 state: WindowState,
123 wm_capabilities: WindowManagerCapabilities,
124 mouse: MouseState,
125 theme: ColorTheme,
126 title: Option<String>,
127 title_text: Option<TitleText>,
128 shadow: Shadow,
129}
130
131impl<State> AdwaitaFrame<State>
132where
133 State: Dispatch<WlSurface, SurfaceData> + Dispatch<WlSubsurface, SubsurfaceData> + 'static,
134{
135 pub fn new(
136 base_surface: &impl WaylandSurface,
137 shm: &Shm,
138 compositor: Arc<CompositorState>,
139 subcompositor: Arc<SubcompositorState>,
140 queue_handle: QueueHandle<State>,
141 frame_config: FrameConfig,
142 ) -> Result<Self, Box<dyn Error>> {
143 let base_surface = WlTyped::wrap::<State>(base_surface.wl_surface().clone());
144
145 let pool = SlotPool::new(1, shm)?;
146 FRAMED.store(true, Ordering::Relaxed);
147
148 let decorations = Some(DecorationParts::new(
149 &base_surface,
150 &subcompositor,
151 &queue_handle,
152 ));
153
154 let theme = frame_config.theme;
155
156 Ok(AdwaitaFrame {
157 base_surface,
158 decorations,
159 pool,
160 compositor,
161 subcompositor,
162 queue_handle,
163 dirty: true,
164 scale_factor: 1,
165 should_sync: true,
166 title: None,
167 title_text: TitleText::new(theme.active.font_color),
168 theme,
169 buttons: Buttons::new(get_button_layout_config()),
170 mouse: Default::default(),
171 state: WindowState::empty(),
172 wm_capabilities: WindowManagerCapabilities::all(),
173 resizable: true,
174 shadow: Shadow::default(),
175 })
176 }
177
178 /// Update the current frame config.
179 pub fn set_config(&mut self, config: FrameConfig) {
180 self.theme = config.theme;
181 self.dirty = true;
182 }
183
184 fn precise_location(
185 &self,
186 location: Location,
187 decoration: &DecorationParts,
188 x: f64,
189 y: f64,
190 ) -> Location {
191 let header_width = decoration.header().surface_rect.width;
192 let side_height = decoration.side_height();
193
194 let left_corner_x = BORDER_SIZE + RESIZE_HANDLE_CORNER_SIZE;
195 let right_corner_x = (header_width + BORDER_SIZE).saturating_sub(RESIZE_HANDLE_CORNER_SIZE);
196 let top_corner_y = RESIZE_HANDLE_CORNER_SIZE;
197 let bottom_corner_y = side_height.saturating_sub(RESIZE_HANDLE_CORNER_SIZE);
198 match location {
199 Location::Head | Location::Button(_) => self.buttons.find_button(x, y),
200 Location::Top | Location::TopLeft | Location::TopRight => {
201 if x <= f64::from(left_corner_x) {
202 Location::TopLeft
203 } else if x >= f64::from(right_corner_x) {
204 Location::TopRight
205 } else {
206 Location::Top
207 }
208 }
209 Location::Bottom | Location::BottomLeft | Location::BottomRight => {
210 if x <= f64::from(left_corner_x) {
211 Location::BottomLeft
212 } else if x >= f64::from(right_corner_x) {
213 Location::BottomRight
214 } else {
215 Location::Bottom
216 }
217 }
218 Location::Left => {
219 if y <= f64::from(top_corner_y) {
220 Location::TopLeft
221 } else if y >= f64::from(bottom_corner_y) {
222 Location::BottomLeft
223 } else {
224 Location::Left
225 }
226 }
227 Location::Right => {
228 if y <= f64::from(top_corner_y) {
229 Location::TopRight
230 } else if y >= f64::from(bottom_corner_y) {
231 Location::BottomRight
232 } else {
233 Location::Right
234 }
235 }
236 other => other,
237 }
238 }
239
240 fn redraw_inner(&mut self) -> Option<bool> {
241 let decorations = self.decorations.as_mut()?;
242
243 // Reset the dirty bit.
244 self.dirty = false;
245 let should_sync = mem::take(&mut self.should_sync);
246
247 // Don't draw borders if the frame explicitly hidden or fullscreened.
248 if self.state.contains(WindowState::FULLSCREEN) {
249 decorations.hide();
250 return Some(true);
251 }
252
253 let colors = if self.state.contains(WindowState::ACTIVATED) {
254 &self.theme.active
255 } else {
256 &self.theme.inactive
257 };
258
259 let draw_borders = if self.state.contains(WindowState::MAXIMIZED) {
260 // Don't draw the borders.
261 decorations.hide_borders();
262 false
263 } else {
264 true
265 };
266 let border_paint = colors.border_paint();
267 let libadwaita = libadwaita_edge() && !self.state.intersects(WindowState::TILED);
268 let window = (
269 decorations.header().surface_rect.width,
270 decorations.side_height(),
271 );
272
273 // Draw the borders.
274 for (idx, part) in decorations.parts().filter(|(idx, part)| {
275 (*idx == DecorationParts::HEADER || draw_borders) && part.surface_rect.height > 0
276 }) {
277 let scale = self.scale_factor;
278
279 let mut rect = part.surface_rect;
280 // XXX to perfectly align the visible borders we draw them with
281 // the header, otherwise rounded corners won't look 'smooth' at the
282 // start. To achieve that, we enlargen the width of the header by
283 // 2 * `VISIBLE_BORDER_SIZE`, and move `x` by `VISIBLE_BORDER_SIZE`
284 // to the left.
285 if idx == DecorationParts::HEADER && draw_borders && !libadwaita {
286 rect.width += 2 * VISIBLE_BORDER_SIZE;
287 rect.x -= VISIBLE_BORDER_SIZE as i32;
288 }
289 // libadwaita's outline lies inside the body, and its bottom corners are cut from
290 // it: the sides overlap the body by the outline, the bottom by the radius.
291 let (inset, reach) = match idx {
292 _ if !libadwaita || !draw_borders => (0, 0),
293 DecorationParts::LEFT | DecorationParts::RIGHT => (1, 0),
294 DecorationParts::BOTTOM => (0, WINDOW_RADIUS as u32),
295 _ => (0, 0),
296 };
297 if idx == DecorationParts::RIGHT {
298 rect.x -= inset as i32;
299 }
300 rect.width += inset;
301 rect.y -= reach as i32;
302 rect.height += reach;
303 // Without a header the body's top corners are cut too, and the top reaches over them.
304 if libadwaita && draw_borders && idx == DecorationParts::TOP {
305 rect.height += WINDOW_RADIUS as u32;
306 }
307
308 rect.width *= scale;
309 rect.height *= scale;
310
311 let (buffer, canvas) = match self.pool.create_buffer(
312 rect.width as i32,
313 rect.height as i32,
314 rect.width as i32 * 4,
315 wl_shm::Format::Argb8888,
316 ) {
317 Ok((buffer, canvas)) => (buffer, canvas),
318 Err(_) => continue,
319 };
320
321 // Create the pixmap and fill with transparent color.
322 let mut pixmap = PixmapMut::from_bytes(canvas, rect.width, rect.height)?;
323
324 // Fill everything with transparent background, since we draw rounded corners and
325 // do invisible borders to enlarge the input zone.
326 pixmap.fill(Color::TRANSPARENT);
327
328 let edge = libadwaita && draw_borders;
329 let origin = [rect.x, rect.y + HEADER_SIZE as i32].map(|v| (v * scale as i32) as f32);
330 let active = self.state.contains(WindowState::ACTIVATED);
331 let edge_layer = |pixmap: &mut PixmapMut, outline| {
332 draw_libadwaita_edge(
333 pixmap,
334 idx,
335 origin,
336 [window.0 * scale, window.1 * scale],
337 scale as f32,
338 active,
339 outline,
340 )
341 };
342 if edge {
343 edge_layer(&mut pixmap, false);
344 } else if !self.state.intersects(WindowState::TILED) {
345 self.shadow.draw(
346 &mut pixmap,
347 scale,
348 self.state.contains(WindowState::ACTIVATED),
349 idx,
350 );
351 }
352
353 match idx {
354 DecorationParts::HEADER => {
355 if let Some(title_text) = self.title_text.as_mut() {
356 title_text.update_scale(scale);
357 title_text.update_color(colors.font_color);
358 }
359
360 draw_headerbar(
361 &mut pixmap,
362 self.title_text.as_ref().map(|t| t.pixmap()).unwrap_or(None),
363 scale as f32,
364 self.resizable,
365 &self.state,
366 &self.theme,
367 &self.buttons,
368 self.mouse.location,
369 );
370 }
371 _ if edge => {}
372 border => {
373 // The visible border is one pt.
374 let visible_border_size = VISIBLE_BORDER_SIZE * scale;
375
376 // XXX we do all the match using integral types and then convert to f32 in the
377 // end to ensure that result is finite.
378 let border_rect = match border {
379 DecorationParts::LEFT => {
380 let x = (rect.x.unsigned_abs() * scale) - visible_border_size;
381 let y = rect.y.unsigned_abs() * scale;
382 Rect::from_xywh(
383 x as f32,
384 y as f32,
385 visible_border_size as f32,
386 (rect.height - y) as f32,
387 )
388 }
389 DecorationParts::RIGHT => {
390 let y = rect.y.unsigned_abs() * scale;
391 Rect::from_xywh(
392 0.,
393 y as f32,
394 visible_border_size as f32,
395 (rect.height - y) as f32,
396 )
397 }
398 // Without a header, the top has its border too.
399 DecorationParts::TOP => {
400 let x = (rect.x.unsigned_abs() * scale) - visible_border_size;
401 Rect::from_xywh(
402 x as f32,
403 (rect.height - visible_border_size) as f32,
404 (rect.width - 2 * x) as f32,
405 visible_border_size as f32,
406 )
407 }
408 DecorationParts::BOTTOM => {
409 let x = (rect.x.unsigned_abs() * scale) - visible_border_size;
410 Rect::from_xywh(
411 x as f32,
412 0.,
413 (rect.width - 2 * x) as f32,
414 visible_border_size as f32,
415 )
416 }
417 _ => None,
418 };
419
420 // Fill the visible border, if present.
421 if let Some(border_rect) = border_rect {
422 pixmap.fill_rect(border_rect, &border_paint, Transform::identity(), None);
423 }
424 }
425 };
426
427 if edge {
428 edge_layer(&mut pixmap, true);
429 }
430
431 if should_sync {
432 part.subsurface.set_sync();
433 } else {
434 part.subsurface.set_desync();
435 }
436
437 part.surface.set_buffer_scale(scale as i32);
438
439 part.subsurface.set_position(rect.x, rect.y);
440 buffer.attach_to(&part.surface).ok()?;
441
442 if part.surface.version() >= 4 {
443 part.surface.damage_buffer(0, 0, i32::MAX, i32::MAX);
444 } else {
445 part.surface.damage(0, 0, i32::MAX, i32::MAX);
446 }
447
448 if let Some(input_rect) = part.input_rect {
449 let input_region = Region::new(&*self.compositor).ok()?;
450 input_region.add(
451 input_rect.x
452 + if idx == DecorationParts::RIGHT {
453 inset as i32
454 } else {
455 0
456 },
457 input_rect.y + reach as i32,
458 input_rect.width as i32,
459 input_rect.height as i32,
460 );
461
462 part.surface
463 .set_input_region(Some(input_region.wl_region()));
464 }
465
466 part.surface.commit();
467 }
468
469 Some(should_sync)
470 }
471}
472
473impl<State> DecorationsFrame for AdwaitaFrame<State>
474where
475 State: Dispatch<WlSurface, SurfaceData> + Dispatch<WlSubsurface, SubsurfaceData> + 'static,
476{
477 fn update_state(&mut self, state: WindowState) {
478 let difference = self.state.symmetric_difference(state);
479 self.state = state;
480 self.dirty |= difference.intersects(
481 WindowState::ACTIVATED
482 | WindowState::FULLSCREEN
483 | WindowState::MAXIMIZED
484 | WindowState::TILED,
485 );
486 }
487
488 fn update_wm_capabilities(&mut self, wm_capabilities: WindowManagerCapabilities) {
489 self.dirty |= self.wm_capabilities != wm_capabilities;
490 self.wm_capabilities = wm_capabilities;
491 self.buttons.update_wm_capabilities(wm_capabilities);
492 }
493
494 fn set_hidden(&mut self, hidden: bool) {
495 if hidden {
496 self.dirty = false;
497 let _ = self.pool.resize(1);
498 self.decorations = None;
499 } else if self.decorations.is_none() {
500 self.decorations = Some(DecorationParts::new(
501 &self.base_surface,
502 &self.subcompositor,
503 &self.queue_handle,
504 ));
505 self.dirty = true;
506 self.should_sync = true;
507 }
508 }
509
510 fn set_resizable(&mut self, resizable: bool) {
511 self.dirty |= self.resizable != resizable;
512 self.resizable = resizable;
513 }
514
515 fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) {
516 let Some(decorations) = self.decorations.as_mut() else {
517 log::error!("trying to resize the hidden frame.");
518 return;
519 };
520
521 decorations.resize(width.get(), height.get());
522 self.buttons
523 .arrange(width.get(), get_margin_h_lp(&self.state));
524 self.dirty = true;
525 self.should_sync = true;
526 }
527
528 fn draw(&mut self) -> bool {
529 self.redraw_inner().unwrap_or(true)
530 }
531
532 fn subtract_borders(
533 &self,
534 width: NonZeroU32,
535 height: NonZeroU32,
536 ) -> (Option<NonZeroU32>, Option<NonZeroU32>) {
537 if self.decorations.is_none() || self.state.contains(WindowState::FULLSCREEN) {
538 (Some(width), Some(height))
539 } else {
540 (
541 Some(width),
542 NonZeroU32::new(height.get().saturating_sub(HEADER_SIZE)),
543 )
544 }
545 }
546
547 fn add_borders(&self, width: u32, height: u32) -> (u32, u32) {
548 if self.decorations.is_none() || self.state.contains(WindowState::FULLSCREEN) {
549 (width, height)
550 } else {
551 (width, height + HEADER_SIZE)
552 }
553 }
554
555 fn location(&self) -> (i32, i32) {
556 if self.decorations.is_none() || self.state.contains(WindowState::FULLSCREEN) {
557 (0, 0)
558 } else {
559 (0, -(HEADER_SIZE as i32))
560 }
561 }
562
563 fn set_title(&mut self, title: impl Into<String>) {
564 let new_title = title.into();
565 if let Some(title_text) = self.title_text.as_mut() {
566 title_text.update_title(new_title.clone());
567 }
568
569 self.title = Some(new_title);
570 self.dirty = true;
571 }
572
573 fn on_click(
574 &mut self,
575 timestamp: Duration,
576 click: FrameClick,
577 pressed: bool,
578 ) -> Option<FrameAction> {
579 match click {
580 FrameClick::Normal => self.mouse.click(
581 timestamp,
582 pressed,
583 self.resizable,
584 &self.state,
585 &self.wm_capabilities,
586 ),
587 FrameClick::Alternate => self.mouse.alternate_click(pressed, &self.wm_capabilities),
588 _ => None,
589 }
590 }
591
592 fn set_scaling_factor(&mut self, scale_factor: f64) {
593 // NOTE: Clamp it just in case to some ok-ish range.
594 self.scale_factor = scale_factor.clamp(0.1, 64.).ceil() as u32;
595 self.dirty = true;
596 self.should_sync = true;
597 }
598
599 fn click_point_moved(
600 &mut self,
601 _timestamp: Duration,
602 surface: &ObjectId,
603 x: f64,
604 y: f64,
605 ) -> Option<CursorIcon> {
606 let decorations = self.decorations.as_ref()?;
607 let location = decorations.find_surface(surface);
608 if location == Location::None {
609 return None;
610 }
611
612 let old_location = self.mouse.location;
613
614 let location = self.precise_location(location, decorations, x, y);
615 let new_cursor = self.mouse.moved(location, x, y, self.resizable);
616
617 // Set dirty if we moved the cursor between the buttons.
618 self.dirty |= (matches!(old_location, Location::Button(_))
619 || matches!(self.mouse.location, Location::Button(_)))
620 && old_location != self.mouse.location;
621
622 Some(new_cursor)
623 }
624
625 fn click_point_left(&mut self) {
626 self.mouse.left()
627 }
628
629 fn is_dirty(&self) -> bool {
630 self.dirty
631 }
632
633 fn is_hidden(&self) -> bool {
634 self.decorations.is_none()
635 }
636}
637
638/// The configuration for the [`AdwaitaFrame`] frame.
639#[derive(Debug, Clone)]
640pub struct FrameConfig {
641 pub theme: ColorTheme,
642}
643
644impl FrameConfig {
645 /// Create the new configuration with the given `theme`.
646 pub fn new(theme: ColorTheme) -> Self {
647 Self { theme }
648 }
649
650 /// This is equivalent of calling `FrameConfig::new(ColorTheme::auto())`.
651 ///
652 /// For details see [`ColorTheme::auto`].
653 pub fn auto() -> Self {
654 Self {
655 theme: ColorTheme::auto(),
656 }
657 }
658
659 /// This is equivalent of calling `FrameConfig::new(ColorTheme::light())`.
660 ///
661 /// For details see [`ColorTheme::light`].
662 pub fn light() -> Self {
663 Self {
664 theme: ColorTheme::light(),
665 }
666 }
667
668 /// This is equivalent of calling `FrameConfig::new(ColorTheme::dark())`.
669 ///
670 /// For details see [`ColorTheme::dark`].
671 pub fn dark() -> Self {
672 Self {
673 theme: ColorTheme::dark(),
674 }
675 }
676}
677
678#[allow(clippy::too_many_arguments)]
679fn draw_headerbar(
680 pixmap: &mut PixmapMut,
681 text_pixmap: Option<&Pixmap>,
682 scale: f32,
683 resizable: bool,
684 state: &WindowState,
685 theme: &ColorTheme,
686 buttons: &Buttons,
687 mouse: Location,
688) {
689 let colors = theme.for_state(state.contains(WindowState::ACTIVATED));
690
691 let _ = draw_headerbar_bg(pixmap, scale, colors, state);
692
693 // Horizontal margin.
694 let margin_h = get_margin_h_lp(state) * 2.0;
695
696 let canvas_w = pixmap.width() as f32;
697 let canvas_h = pixmap.height() as f32;
698
699 let header_w = canvas_w - margin_h * 2.0;
700 let header_h = canvas_h;
701
702 if let Some(text_pixmap) = text_pixmap {
703 const TEXT_OFFSET: f32 = 10.;
704 let offset_x = TEXT_OFFSET * scale;
705
706 let text_w = text_pixmap.width() as f32;
707 let text_h = text_pixmap.height() as f32;
708
709 let x = margin_h + header_w / 2. - text_w / 2.;
710 let y = header_h / 2. - text_h / 2.;
711
712 let left_buttons_end_x = buttons.left_buttons_end_x().unwrap_or(0.0) * scale;
713 let right_buttons_start_x =
714 buttons.right_buttons_start_x().unwrap_or(header_w / scale) * scale;
715
716 {
717 // We have enough space to center text
718 let (x, y, text_canvas_start_x) = if (x + text_w < right_buttons_start_x - offset_x)
719 && (x > left_buttons_end_x + offset_x)
720 {
721 let text_canvas_start_x = x;
722
723 (x, y, text_canvas_start_x)
724 } else {
725 let x = left_buttons_end_x + offset_x;
726 let text_canvas_start_x = left_buttons_end_x + offset_x;
727
728 (x, y, text_canvas_start_x)
729 };
730
731 let text_canvas_end_x = right_buttons_start_x - x - offset_x;
732 // Ensure that text start within the bounds.
733 let x = x.max(margin_h + offset_x);
734
735 if let Some(clip) =
736 Rect::from_xywh(text_canvas_start_x, 0., text_canvas_end_x, canvas_h)
737 {
738 if let Some(mut mask) = Mask::new(canvas_w as u32, canvas_h as u32) {
739 mask.fill_path(
740 &PathBuilder::from_rect(clip),
741 FillRule::Winding,
742 false,
743 Transform::identity(),
744 );
745 pixmap.draw_pixmap(
746 x.round() as i32,
747 y as i32,
748 text_pixmap.as_ref(),
749 &PixmapPaint::default(),
750 Transform::identity(),
751 Some(&mask),
752 );
753 } else {
754 log::error!(
755 "Invalid mask width and height: w: {}, h: {}",
756 canvas_w as u32,
757 canvas_h as u32
758 );
759 }
760 }
761 }
762 }
763
764 // Draw the buttons.
765 buttons.draw(
766 margin_h, header_w, scale, colors, mouse, pixmap, resizable, state,
767 );
768}
769
770#[must_use]
771fn draw_headerbar_bg(
772 pixmap: &mut PixmapMut,
773 scale: f32,
774 colors: &ColorMap,
775 state: &WindowState,
776) -> SkiaResult {
777 let w = pixmap.width() as f32;
778 let h = pixmap.height() as f32;
779
780 let radius = if state.intersects(WindowState::MAXIMIZED | WindowState::TILED) {
781 0.
782 } else if libadwaita_edge() {
783 WINDOW_RADIUS * scale
784 } else {
785 CORNER_RADIUS as f32 * scale
786 };
787
788 let bg = rounded_headerbar_shape(0., 0., w, h, radius)?;
789
790 pixmap.fill_path(
791 &bg,
792 &colors.headerbar_paint(),
793 FillRule::Winding,
794 Transform::identity(),
795 None,
796 );
797
798 Some(())
799}
800
801/// One layer of libadwaita's window edge in part `idx`, whose pixmap's origin lies at
802/// `origin` from the header's top left in a `window` of the header and body together, all
803/// in device pixels: the box shadows outside the window, under the part's content, or the
804/// outline inside it, over the content. Each part draws only where the others do not.
805fn draw_libadwaita_edge(
806 pixmap: &mut PixmapMut,
807 idx: usize,
808 origin: [f32; 2],
809 window: [u32; 2],
810 scale: f32,
811 active: bool,
812 outline: bool,
813) {
814 let [width, height] = window.map(|v| v as f32);
815 let radius = WINDOW_RADIUS * scale;
816 // The top owns the body's top corners where no header rounds them.
817 let top = (HEADER_SIZE as f32 * scale).max(radius);
818 let owned = |x: f32, y: f32| match idx {
819 DecorationParts::LEFT => x < 0. || (y >= top && y < height - radius),
820 DecorationParts::RIGHT => x >= width || (y >= top && y < height - radius),
821 DecorationParts::BOTTOM => y >= height || (0. ..width).contains(&x),
822 _ => true,
823 };
824 // Signed distance from the window grown by `spread` and moved down by `offset`.
825 let distance = |x: f32, y: f32, offset: f32, spread: f32| {
826 let radius = radius + spread;
827 let [qx, qy] = [
828 (x - width / 2.).abs() - width / 2. - spread + radius,
829 (y - offset - height / 2.).abs() - height / 2. - spread + radius,
830 ];
831 qx.max(0.).hypot(qy.max(0.)) + qx.max(qy).min(0.) - radius
832 };
833 let shadows = &WINDOW_SHADOWS[usize::from(!active)];
834 let columns = pixmap.width() as usize;
835 for (index, pixel) in pixmap.pixels_mut().iter_mut().enumerate() {
836 let [x, y] = [
837 (index % columns) as f32 + 0.5 + origin[0],
838 (index / columns) as f32 + 0.5 + origin[1],
839 ];
840 if !owned(x, y) {
841 continue;
842 }
843 let edge = distance(x, y, 0., 0.);
844 let (color, alpha) = if outline {
845 // The share of the pixel inside the band 1 px wide inside the edge.
846 let cover = (edge + 0.5).min(0.) - (edge - 0.5).max(-scale);
847 (255., WINDOW_OUTLINE * cover.max(0.))
848 } else {
849 let clear = shadows
850 .iter()
851 .fold(1., |clear, [offset, blur, spread, alpha]| {
852 let reach = distance(x, y, offset * scale, spread * scale);
853 // A Gaussian blur of standard deviation half the blur radius, as GSK's.
854 let cover = if *blur == 0. {
855 (0.5 - reach).clamp(0., 1.)
856 } else {
857 0.5 * erfc(reach / (blur * scale / 2. * std::f32::consts::SQRT_2))
858 };
859 clear * (1. - alpha * cover)
860 });
861 // Box shadows show only outside the window.
862 (0., (1. - clear) * (edge + 0.5).clamp(0., 1.))
863 };
864 if alpha <= 0. {
865 continue;
866 }
867 let over = |channel: u8| (color * alpha + f32::from(channel) * (1. - alpha)).round() as u8;
868 let alpha_over = (alpha * 255. + f32::from(pixel.alpha()) * (1. - alpha)).round() as u8;
869 if let Some(blended) = tiny_skia::PremultipliedColorU8::from_rgba(
870 over(pixel.red()).min(alpha_over),
871 over(pixel.green()).min(alpha_over),
872 over(pixel.blue()).min(alpha_over),
873 alpha_over,
874 ) {
875 *pixel = blended;
876 }
877 }
878}
879
880/// The complementary error function, to within 1.5e-7 (Abramowitz and Stegun 7.1.26).
881fn erfc(x: f32) -> f32 {
882 let t = 1. / (1. + 0.327_591_1 * x.abs());
883 let poly = t
884 * (0.254_829_6
885 + t * (-0.284_496_74 + t * (1.421_413_7 + t * (-1.453_152_1 + t * 1.061_405_4))));
886 let erfc = poly * (-x * x).exp();
887 if x >= 0. {
888 erfc
889 } else {
890 2. - erfc
891 }
892}
893
894fn rounded_headerbar_shape(x: f32, y: f32, width: f32, height: f32, radius: f32) -> Option<Path> {
895 // https://stackoverflow.com/a/27863181
896 let cubic_bezier_circle = 0.552_284_8 * radius;
897
898 let mut pb = PathBuilder::new();
899 let mut cursor = Point::from_xy(x, y);
900
901 // !!!
902 // This code is heavily "inspired" by https://gitlab.com/snakedye/snui/
903 // So technically it should be licensed under MPL-2.0, sorry about that 🥺 👉👈
904 // !!!
905
906 // Positioning the cursor
907 cursor.y += radius;
908 pb.move_to(cursor.x, cursor.y);
909
910 // Drawing the outline
911 let next = Point::from_xy(cursor.x + radius, cursor.y - radius);
912 pb.cubic_to(
913 cursor.x,
914 cursor.y - cubic_bezier_circle,
915 next.x - cubic_bezier_circle,
916 next.y,
917 next.x,
918 next.y,
919 );
920 cursor = next;
921 pb.line_to(
922 {
923 cursor.x = x + width - radius;
924 cursor.x
925 },
926 cursor.y,
927 );
928 let next = Point::from_xy(cursor.x + radius, cursor.y + radius);
929 pb.cubic_to(
930 cursor.x + cubic_bezier_circle,
931 cursor.y,
932 next.x,
933 next.y - cubic_bezier_circle,
934 next.x,
935 next.y,
936 );
937 cursor = next;
938 pb.line_to(cursor.x, {
939 cursor.y = y + height;
940 cursor.y
941 });
942 pb.line_to(
943 {
944 cursor.x = x;
945 cursor.x
946 },
947 cursor.y,
948 );
949
950 pb.close();
951
952 pb.finish()
953}
954
955// returns horizontal margin, logical points
956fn get_margin_h_lp(state: &WindowState) -> f32 {
957 if state.intersects(WindowState::MAXIMIZED | WindowState::TILED) || libadwaita_edge() {
958 0.
959 } else {
960 VISIBLE_BORDER_SIZE as f32
961 }
962}