1#![deny(
2 clippy::print_stderr,
3 clippy::print_stdout,
4 clippy::dbg_macro,
5 clippy::exit,
6 clippy::unwrap_used,
7 clippy::expect_used,
8 clippy::panic,
9 clippy::indexing_slicing,
10 clippy::string_slice
11)]
12
13use std::error::Error;
14use std::mem;
15use std::num::NonZeroU32;
16use std::sync::Arc;
17use std::time::Duration;
18
19use tiny_skia::{
20 Color, FillRule, Mask, Path, PathBuilder, Pixmap, PixmapMut, PixmapPaint, Point, Rect,
21 Transform,
22};
23
24use smithay_client_toolkit::reexports::client::backend::ObjectId;
25use smithay_client_toolkit::reexports::client::protocol::wl_shm;
26use smithay_client_toolkit::reexports::client::protocol::wl_subsurface::WlSubsurface;
27use smithay_client_toolkit::reexports::client::protocol::wl_surface::WlSurface;
28use smithay_client_toolkit::reexports::client::{Dispatch, Proxy, QueueHandle};
29use smithay_client_toolkit::reexports::csd_frame::{
30 CursorIcon, DecorationsFrame, FrameAction, FrameClick, WindowManagerCapabilities, WindowState,
31};
32
33use smithay_client_toolkit::compositor::{CompositorState, Region, SurfaceData};
34use smithay_client_toolkit::shell::WaylandSurface;
35use smithay_client_toolkit::shm::{slot::SlotPool, Shm};
36use smithay_client_toolkit::subcompositor::SubcompositorState;
37use smithay_client_toolkit::subcompositor::SubsurfaceData;
38
39mod buttons;
40mod config;
41mod parts;
42mod pointer;
43mod shadow;
44pub mod theme;
45mod title;
46mod wl_typed;
47
48use crate::theme::{
49 ColorMap, ColorTheme, BORDER_SIZE, CORNER_RADIUS, HEADER_SIZE, RESIZE_HANDLE_CORNER_SIZE,
50 VISIBLE_BORDER_SIZE,
51};
52
53use buttons::Buttons;
54use config::get_button_layout_config;
55use parts::DecorationParts;
56use pointer::{Location, MouseState};
57use shadow::Shadow;
58use title::TitleText;
59use wl_typed::WlTyped;
60
61type SkiaResult = Option<()>;
63
64#[derive(Debug)]
66pub struct AdwaitaFrame<State> {
67 base_surface: WlTyped<WlSurface, SurfaceData>,
69
70 compositor: Arc<CompositorState>,
71
72 subcompositor: Arc<SubcompositorState>,
74
75 queue_handle: QueueHandle<State>,
77
78 decorations: Option<DecorationParts>,
80
81 pool: SlotPool,
83
84 dirty: bool,
86
87 should_sync: bool,
89
90 scale_factor: u32,
92
93 resizable: bool,
95
96 buttons: Buttons,
97 state: WindowState,
98 wm_capabilities: WindowManagerCapabilities,
99 mouse: MouseState,
100 theme: ColorTheme,
101 title: Option<String>,
102 title_text: Option<TitleText>,
103 shadow: Shadow,
104
105 hide_titlebar: bool,
107
108 width: NonZeroU32,
109 height: NonZeroU32,
110}
111
112impl<State> AdwaitaFrame<State>
113where
114 State: Dispatch<WlSurface, SurfaceData> + Dispatch<WlSubsurface, SubsurfaceData> + 'static,
115{
116 pub fn new(
117 base_surface: &impl WaylandSurface,
118 shm: &Shm,
119 compositor: Arc<CompositorState>,
120 subcompositor: Arc<SubcompositorState>,
121 queue_handle: QueueHandle<State>,
122 frame_config: FrameConfig,
123 ) -> Result<Self, Box<dyn Error>> {
124 let base_surface = WlTyped::wrap::<State>(base_surface.wl_surface().clone());
125
126 let pool = SlotPool::new(1, shm)?;
127
128 let decorations = Some(DecorationParts::new(
129 &base_surface,
130 &subcompositor,
131 &queue_handle,
132 frame_config.hide_titlebar,
133 ));
134
135 let theme = frame_config.theme;
136
137 Ok(AdwaitaFrame {
138 base_surface,
139 decorations,
140 pool,
141 compositor,
142 subcompositor,
143 queue_handle,
144 dirty: true,
145 scale_factor: 1,
146 should_sync: true,
147 title: None,
148 title_text: TitleText::new(theme.active.font_color),
149 theme,
150 buttons: Buttons::new(get_button_layout_config()),
151 mouse: Default::default(),
152 state: WindowState::empty(),
153 wm_capabilities: WindowManagerCapabilities::all(),
154 resizable: true,
155 shadow: Shadow::default(),
156 hide_titlebar: frame_config.hide_titlebar,
157 width: NonZeroU32::MIN,
158 height: NonZeroU32::MIN,
159 })
160 }
161
162 pub fn set_config(&mut self, config: FrameConfig) {
164 self.theme = config.theme;
165 self.dirty = true;
166
167 if self.hide_titlebar != config.hide_titlebar {
168 self.hide_titlebar = config.hide_titlebar;
169 let mut decorations = DecorationParts::new(
170 &self.base_surface,
171 &self.subcompositor,
172 &self.queue_handle,
173 self.hide_titlebar,
174 );
175 decorations.resize(self.width.get(), self.height.get());
176 self.decorations = Some(decorations);
177 }
178 }
179
180 fn precise_location(
181 &self,
182 location: Location,
183 decoration: &DecorationParts,
184 x: f64,
185 y: f64,
186 ) -> Location {
187 let header_width = decoration.header().surface_rect.width;
188 let side_height = decoration.side_height();
189
190 let left_corner_x = BORDER_SIZE + RESIZE_HANDLE_CORNER_SIZE;
191 let right_corner_x = (header_width + BORDER_SIZE).saturating_sub(RESIZE_HANDLE_CORNER_SIZE);
192 let top_corner_y = RESIZE_HANDLE_CORNER_SIZE;
193 let bottom_corner_y = side_height.saturating_sub(RESIZE_HANDLE_CORNER_SIZE);
194 match location {
195 Location::Head | Location::Button(_) => self.buttons.find_button(x, y),
196 Location::Top | Location::TopLeft | Location::TopRight => {
197 if x <= f64::from(left_corner_x) {
198 Location::TopLeft
199 } else if x >= f64::from(right_corner_x) {
200 Location::TopRight
201 } else {
202 Location::Top
203 }
204 }
205 Location::Bottom | Location::BottomLeft | Location::BottomRight => {
206 if x <= f64::from(left_corner_x) {
207 Location::BottomLeft
208 } else if x >= f64::from(right_corner_x) {
209 Location::BottomRight
210 } else {
211 Location::Bottom
212 }
213 }
214 Location::Left => {
215 if y <= f64::from(top_corner_y) {
216 Location::TopLeft
217 } else if y >= f64::from(bottom_corner_y) {
218 Location::BottomLeft
219 } else {
220 Location::Left
221 }
222 }
223 Location::Right => {
224 if y <= f64::from(top_corner_y) {
225 Location::TopRight
226 } else if y >= f64::from(bottom_corner_y) {
227 Location::BottomRight
228 } else {
229 Location::Right
230 }
231 }
232 other => other,
233 }
234 }
235
236 fn redraw_inner(&mut self) -> Option<bool> {
237 let decorations = self.decorations.as_mut()?;
238
239 self.dirty = false;
241 let should_sync = mem::take(&mut self.should_sync);
242
243 if self.state.contains(WindowState::FULLSCREEN) {
245 decorations.hide();
246 return Some(true);
247 } else {
248 decorations.show();
249 }
250
251 if self.hide_titlebar {
252 decorations.hide_titlebar();
253 }
254
255 let colors = if self.state.contains(WindowState::ACTIVATED) {
256 &self.theme.active
257 } else {
258 &self.theme.inactive
259 };
260
261 let draw_borders = if self.state.contains(WindowState::MAXIMIZED) {
262 decorations.hide_borders();
264 false
265 } else {
266 true
267 };
268 let border_paint = colors.border_paint();
269
270 for (idx, part) in decorations.parts().filter(|(_, part)| !part.hide) {
272 let scale = self.scale_factor;
273
274 let mut rect = part.surface_rect;
275 if idx == DecorationParts::HEADER && draw_borders {
281 rect.width += 2 * VISIBLE_BORDER_SIZE;
282 rect.x -= VISIBLE_BORDER_SIZE as i32;
283 }
284
285 rect.width *= scale;
286 rect.height *= scale;
287
288 let (buffer, canvas) = match self.pool.create_buffer(
289 rect.width as i32,
290 rect.height as i32,
291 rect.width as i32 * 4,
292 wl_shm::Format::Argb8888,
293 ) {
294 Ok((buffer, canvas)) => (buffer, canvas),
295 Err(_) => continue,
296 };
297
298 let mut pixmap = PixmapMut::from_bytes(canvas, rect.width, rect.height)?;
300
301 pixmap.fill(Color::TRANSPARENT);
304
305 if !self.state.intersects(WindowState::TILED) {
306 self.shadow.draw(
307 &mut pixmap,
308 scale,
309 self.state.contains(WindowState::ACTIVATED),
310 idx,
311 );
312 }
313
314 match idx {
315 DecorationParts::HEADER => {
316 if let Some(title_text) = self.title_text.as_mut() {
317 title_text.update_scale(scale);
318 title_text.update_color(colors.font_color);
319 }
320
321 draw_headerbar(
322 &mut pixmap,
323 self.title_text.as_ref().map(|t| t.pixmap()).unwrap_or(None),
324 scale as f32,
325 self.resizable,
326 &self.state,
327 &self.theme,
328 &self.buttons,
329 self.mouse.location,
330 );
331 }
332 border => {
333 let visible_border_size = VISIBLE_BORDER_SIZE * scale;
335
336 let border_rect = match border {
339 DecorationParts::LEFT => {
340 let x = (rect.x.unsigned_abs() * scale) - visible_border_size;
341 let y = rect.y.unsigned_abs() * scale;
342 Rect::from_xywh(
343 x as f32,
344 y as f32,
345 visible_border_size as f32,
346 (rect.height - y) as f32,
347 )
348 }
349 DecorationParts::RIGHT => {
350 let y = rect.y.unsigned_abs() * scale;
351 Rect::from_xywh(
352 0.,
353 y as f32,
354 visible_border_size as f32,
355 (rect.height - y) as f32,
356 )
357 }
358 DecorationParts::BOTTOM => {
361 let x = (rect.x.unsigned_abs() * scale) - visible_border_size;
362 Rect::from_xywh(
363 x as f32,
364 0.,
365 (rect.width - 2 * x) as f32,
366 visible_border_size as f32,
367 )
368 }
369 DecorationParts::TOP if self.hide_titlebar => {
371 let x = rect.x.unsigned_abs() * scale;
372 let x = x.saturating_sub(visible_border_size);
373
374 let y = rect.y.unsigned_abs() * scale;
375 let y = y.saturating_sub(visible_border_size);
376
377 Rect::from_xywh(
378 x as f32,
379 y as f32,
380 (rect.width - 2 * x) as f32,
381 visible_border_size as f32,
382 )
383 }
384 _ => None,
385 };
386
387 if let Some(border_rect) = border_rect {
389 pixmap.fill_rect(border_rect, &border_paint, Transform::identity(), None);
390 }
391 }
392 };
393
394 if should_sync {
395 part.subsurface.set_sync();
396 } else {
397 part.subsurface.set_desync();
398 }
399
400 part.surface.set_buffer_scale(scale as i32);
401
402 part.subsurface.set_position(rect.x, rect.y);
403 buffer.attach_to(&part.surface).ok()?;
404
405 if part.surface.version() >= 4 {
406 part.surface.damage_buffer(0, 0, i32::MAX, i32::MAX);
407 } else {
408 part.surface.damage(0, 0, i32::MAX, i32::MAX);
409 }
410
411 if let Some(input_rect) = part.input_rect {
412 let input_region = Region::new(&*self.compositor).ok()?;
413 input_region.add(
414 input_rect.x,
415 input_rect.y,
416 input_rect.width as i32,
417 input_rect.height as i32,
418 );
419
420 part.surface
421 .set_input_region(Some(input_region.wl_region()));
422 }
423
424 part.surface.commit();
425 }
426
427 Some(should_sync)
428 }
429}
430
431impl<State> DecorationsFrame for AdwaitaFrame<State>
432where
433 State: Dispatch<WlSurface, SurfaceData> + Dispatch<WlSubsurface, SubsurfaceData> + 'static,
434{
435 fn update_state(&mut self, state: WindowState) {
436 let difference = self.state.symmetric_difference(state);
437 self.state = state;
438 self.dirty |= difference.intersects(
439 WindowState::ACTIVATED
440 | WindowState::FULLSCREEN
441 | WindowState::MAXIMIZED
442 | WindowState::TILED,
443 );
444 }
445
446 fn update_wm_capabilities(&mut self, wm_capabilities: WindowManagerCapabilities) {
447 self.dirty |= self.wm_capabilities != wm_capabilities;
448 self.wm_capabilities = wm_capabilities;
449 self.buttons.update_wm_capabilities(wm_capabilities);
450 }
451
452 fn set_hidden(&mut self, hidden: bool) {
453 if hidden {
454 self.dirty = false;
455 let _ = self.pool.resize(1);
456 self.decorations = None;
457 } else if self.decorations.is_none() {
458 self.decorations = Some(DecorationParts::new(
459 &self.base_surface,
460 &self.subcompositor,
461 &self.queue_handle,
462 self.hide_titlebar,
463 ));
464 self.dirty = true;
465 self.should_sync = true;
466 }
467 }
468
469 fn set_resizable(&mut self, resizable: bool) {
470 self.dirty |= self.resizable != resizable;
471 self.resizable = resizable;
472 }
473
474 fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) {
475 self.width = width;
476 self.height = height;
477
478 let Some(decorations) = self.decorations.as_mut() else {
479 log::error!("trying to resize the hidden frame.");
480 return;
481 };
482
483 decorations.resize(width.get(), height.get());
484 self.buttons
485 .arrange(width.get(), get_margin_h_lp(&self.state));
486 self.dirty = true;
487 self.should_sync = true;
488 }
489
490 fn draw(&mut self) -> bool {
491 self.redraw_inner().unwrap_or(true)
492 }
493
494 fn subtract_borders(
495 &self,
496 width: NonZeroU32,
497 height: NonZeroU32,
498 ) -> (Option<NonZeroU32>, Option<NonZeroU32>) {
499 if self.decorations.is_none()
500 || self.state.contains(WindowState::FULLSCREEN)
501 || self.hide_titlebar
502 {
503 (Some(width), Some(height))
504 } else {
505 (
506 Some(width),
507 NonZeroU32::new(height.get().saturating_sub(HEADER_SIZE)),
508 )
509 }
510 }
511
512 fn add_borders(&self, width: u32, height: u32) -> (u32, u32) {
513 if self.decorations.is_none()
514 || self.state.contains(WindowState::FULLSCREEN)
515 || self.hide_titlebar
516 {
517 (width, height)
518 } else {
519 (width, height + HEADER_SIZE)
520 }
521 }
522
523 fn location(&self) -> (i32, i32) {
524 if self.decorations.is_none()
525 || self.state.contains(WindowState::FULLSCREEN)
526 || self.hide_titlebar
527 {
528 (0, 0)
529 } else {
530 (0, -(HEADER_SIZE as i32))
531 }
532 }
533
534 fn set_title(&mut self, title: impl Into<String>) {
535 let new_title = title.into();
536 if let Some(title_text) = self.title_text.as_mut() {
537 title_text.update_title(new_title.clone());
538 }
539
540 self.title = Some(new_title);
541 self.dirty = true;
542 }
543
544 fn on_click(
545 &mut self,
546 timestamp: Duration,
547 click: FrameClick,
548 pressed: bool,
549 ) -> Option<FrameAction> {
550 match click {
551 FrameClick::Normal => self.mouse.click(
552 timestamp,
553 pressed,
554 self.resizable,
555 &self.state,
556 &self.wm_capabilities,
557 ),
558 FrameClick::Alternate => self.mouse.alternate_click(pressed, &self.wm_capabilities),
559 _ => None,
560 }
561 }
562
563 fn set_scaling_factor(&mut self, scale_factor: f64) {
564 self.scale_factor = scale_factor.clamp(0.1, 64.).ceil() as u32;
566 self.dirty = true;
567 self.should_sync = true;
568 }
569
570 fn click_point_moved(
571 &mut self,
572 _timestamp: Duration,
573 surface: &ObjectId,
574 x: f64,
575 y: f64,
576 ) -> Option<CursorIcon> {
577 let decorations = self.decorations.as_ref()?;
578 let location = decorations.find_surface(surface);
579 if location == Location::None {
580 return None;
581 }
582
583 let old_location = self.mouse.location;
584
585 let location = self.precise_location(location, decorations, x, y);
586 let new_cursor = self.mouse.moved(location, x, y, self.resizable);
587
588 self.dirty |= (matches!(old_location, Location::Button(_))
590 || matches!(self.mouse.location, Location::Button(_)))
591 && old_location != self.mouse.location;
592
593 Some(new_cursor)
594 }
595
596 fn click_point_left(&mut self) {
597 self.mouse.left()
598 }
599
600 fn is_dirty(&self) -> bool {
601 self.dirty
602 }
603
604 fn is_hidden(&self) -> bool {
605 self.decorations.is_none()
606 }
607}
608
609#[derive(Debug, Clone)]
611pub struct FrameConfig {
612 pub theme: ColorTheme,
613 pub hide_titlebar: bool,
615}
616
617impl FrameConfig {
618 pub fn new(theme: ColorTheme) -> Self {
620 Self {
621 theme,
622 hide_titlebar: false,
623 }
624 }
625
626 pub fn auto() -> Self {
630 Self::new(ColorTheme::auto())
631 }
632
633 pub fn light() -> Self {
637 Self::new(ColorTheme::light())
638 }
639
640 pub fn dark() -> Self {
644 Self::new(ColorTheme::dark())
645 }
646
647 pub fn hide_titlebar(mut self, hide: bool) -> Self {
649 self.hide_titlebar = hide;
650 self
651 }
652}
653
654#[allow(clippy::too_many_arguments)]
655fn draw_headerbar(
656 pixmap: &mut PixmapMut,
657 text_pixmap: Option<&Pixmap>,
658 scale: f32,
659 resizable: bool,
660 state: &WindowState,
661 theme: &ColorTheme,
662 buttons: &Buttons,
663 mouse: Location,
664) {
665 let colors = theme.for_state(state.contains(WindowState::ACTIVATED));
666
667 let _ = draw_headerbar_bg(pixmap, scale, colors, state);
668
669 let margin_h = get_margin_h_lp(state) * 2.0;
671
672 let canvas_w = pixmap.width() as f32;
673 let canvas_h = pixmap.height() as f32;
674
675 let header_w = canvas_w - margin_h * 2.0;
676 let header_h = canvas_h;
677
678 if let Some(text_pixmap) = text_pixmap {
679 const TEXT_OFFSET: f32 = 10.;
680 let offset_x = TEXT_OFFSET * scale;
681
682 let text_w = text_pixmap.width() as f32;
683 let text_h = text_pixmap.height() as f32;
684
685 let x = margin_h + header_w / 2. - text_w / 2.;
686 let y = header_h / 2. - text_h / 2.;
687
688 let left_buttons_end_x = buttons.left_buttons_end_x().unwrap_or(0.0) * scale;
689 let right_buttons_start_x =
690 buttons.right_buttons_start_x().unwrap_or(header_w / scale) * scale;
691
692 {
693 let (x, y, text_canvas_start_x) = if (x + text_w < right_buttons_start_x - offset_x)
695 && (x > left_buttons_end_x + offset_x)
696 {
697 let text_canvas_start_x = x;
698
699 (x, y, text_canvas_start_x)
700 } else {
701 let x = left_buttons_end_x + offset_x;
702 let text_canvas_start_x = left_buttons_end_x + offset_x;
703
704 (x, y, text_canvas_start_x)
705 };
706
707 let text_canvas_end_x = right_buttons_start_x - x - offset_x;
708 let x = x.max(margin_h + offset_x);
710
711 if let Some(clip) =
712 Rect::from_xywh(text_canvas_start_x, 0., text_canvas_end_x, canvas_h)
713 {
714 if let Some(mut mask) = Mask::new(canvas_w as u32, canvas_h as u32) {
715 mask.fill_path(
716 &PathBuilder::from_rect(clip),
717 FillRule::Winding,
718 false,
719 Transform::identity(),
720 );
721 pixmap.draw_pixmap(
722 x.round() as i32,
723 y as i32,
724 text_pixmap.as_ref(),
725 &PixmapPaint::default(),
726 Transform::identity(),
727 Some(&mask),
728 );
729 } else {
730 log::error!(
731 "Invalid mask width and height: w: {}, h: {}",
732 canvas_w as u32,
733 canvas_h as u32
734 );
735 }
736 }
737 }
738 }
739
740 buttons.draw(
742 margin_h, header_w, scale, colors, mouse, pixmap, resizable, state,
743 );
744}
745
746#[must_use]
747fn draw_headerbar_bg(
748 pixmap: &mut PixmapMut,
749 scale: f32,
750 colors: &ColorMap,
751 state: &WindowState,
752) -> SkiaResult {
753 let w = pixmap.width() as f32;
754 let h = pixmap.height() as f32;
755
756 let radius = if state.intersects(WindowState::MAXIMIZED | WindowState::TILED) {
757 0.
758 } else {
759 CORNER_RADIUS as f32 * scale
760 };
761
762 let bg = rounded_headerbar_shape(0., 0., w, h, radius)?;
763
764 pixmap.fill_path(
765 &bg,
766 &colors.headerbar_paint(),
767 FillRule::Winding,
768 Transform::identity(),
769 None,
770 );
771
772 pixmap.fill_rect(
773 Rect::from_xywh(0., h - 1., w, h)?,
774 &colors.border_paint(),
775 Transform::identity(),
776 None,
777 );
778
779 Some(())
780}
781
782fn rounded_headerbar_shape(x: f32, y: f32, width: f32, height: f32, radius: f32) -> Option<Path> {
783 let cubic_bezier_circle = 0.552_284_8 * radius;
785
786 let mut pb = PathBuilder::new();
787 let mut cursor = Point::from_xy(x, y);
788
789 cursor.y += radius;
796 pb.move_to(cursor.x, cursor.y);
797
798 let next = Point::from_xy(cursor.x + radius, cursor.y - radius);
800 pb.cubic_to(
801 cursor.x,
802 cursor.y - cubic_bezier_circle,
803 next.x - cubic_bezier_circle,
804 next.y,
805 next.x,
806 next.y,
807 );
808 cursor = next;
809 pb.line_to(
810 {
811 cursor.x = x + width - radius;
812 cursor.x
813 },
814 cursor.y,
815 );
816 let next = Point::from_xy(cursor.x + radius, cursor.y + radius);
817 pb.cubic_to(
818 cursor.x + cubic_bezier_circle,
819 cursor.y,
820 next.x,
821 next.y - cubic_bezier_circle,
822 next.x,
823 next.y,
824 );
825 cursor = next;
826 pb.line_to(cursor.x, {
827 cursor.y = y + height;
828 cursor.y
829 });
830 pb.line_to(
831 {
832 cursor.x = x;
833 cursor.x
834 },
835 cursor.y,
836 );
837
838 pb.close();
839
840 pb.finish()
841}
842
843fn get_margin_h_lp(state: &WindowState) -> f32 {
845 if state.intersects(WindowState::MAXIMIZED | WindowState::TILED) {
846 0.
847 } else {
848 VISIBLE_BORDER_SIZE as f32
849 }
850}