sctk_adwaita/
buttons.rs

1use log::{debug, warn};
2use smithay_client_toolkit::reexports::csd_frame::{WindowManagerCapabilities, WindowState};
3use tiny_skia::{FillRule, PathBuilder, PixmapMut, Rect, Stroke, Transform};
4
5use crate::{theme::ColorMap, Location, SkiaResult};
6
7/// The size of the button on the header bar in logical points.
8const BUTTON_SIZE: f32 = 24.;
9const BUTTON_MARGIN: f32 = 5.;
10const BUTTON_SPACING: f32 = 13.;
11
12#[derive(Debug)]
13pub(crate) struct Buttons {
14    // Sorted by order vec of buttons for the left and right sides
15    buttons_left: Vec<Button>,
16    buttons_right: Vec<Button>,
17    layout_config: Option<(String, String)>,
18}
19
20type ButtonLayout = (Vec<Button>, Vec<Button>);
21
22impl Default for Buttons {
23    fn default() -> Self {
24        let (buttons_left, buttons_right) = Buttons::get_default_buttons_layout();
25
26        Self {
27            buttons_left,
28            buttons_right,
29            layout_config: None,
30        }
31    }
32}
33
34impl Buttons {
35    pub fn new(layout_config: Option<(String, String)>) -> Self {
36        match Buttons::parse_button_layout(layout_config.clone()) {
37            Some((buttons_left, buttons_right)) => Self {
38                buttons_left,
39                buttons_right,
40                layout_config,
41            },
42            _ => Self::default(),
43        }
44    }
45
46    /// Rearrange the buttons with the new width.
47    pub fn arrange(&mut self, width: u32, margin_h: f32) {
48        let mut left_x = BUTTON_MARGIN + margin_h;
49        let mut right_x = width as f32 - BUTTON_MARGIN;
50
51        for button in &mut self.buttons_left {
52            button.offset = left_x;
53
54            // Add the button size plus spacing
55            left_x += BUTTON_SIZE + BUTTON_SPACING;
56        }
57
58        for button in &mut self.buttons_right {
59            // Subtract the button size.
60            right_x -= BUTTON_SIZE;
61
62            // Update it
63            button.offset = right_x;
64
65            // Subtract spacing for the next button.
66            right_x -= BUTTON_SPACING;
67        }
68    }
69
70    /// Find the coordinate of the button.
71    pub fn find_button(&self, x: f64, y: f64) -> Location {
72        let x = x as f32;
73        let y = y as f32;
74        let buttons = self.buttons_left.iter().chain(self.buttons_right.iter());
75
76        for button in buttons {
77            if button.contains(x, y) {
78                return Location::Button(button.kind);
79            }
80        }
81
82        Location::Head
83    }
84
85    pub fn update_wm_capabilities(&mut self, wm_capabilites: WindowManagerCapabilities) {
86        let supports_maximize = wm_capabilites.contains(WindowManagerCapabilities::MAXIMIZE);
87        let supports_minimize = wm_capabilites.contains(WindowManagerCapabilities::MINIMIZE);
88
89        self.update_buttons(supports_maximize, supports_minimize);
90    }
91
92    pub fn update_buttons(&mut self, supports_maximize: bool, supports_minimize: bool) {
93        let is_supported = |button: &Button| match button.kind {
94            ButtonKind::Close => true,
95            ButtonKind::Maximize => supports_maximize,
96            ButtonKind::Minimize => supports_minimize,
97        };
98
99        let (buttons_left, buttons_right) =
100            Buttons::parse_button_layout(self.layout_config.clone())
101                .unwrap_or_else(Buttons::get_default_buttons_layout);
102
103        self.buttons_left = buttons_left.into_iter().filter(is_supported).collect();
104        self.buttons_right = buttons_right.into_iter().filter(is_supported).collect();
105    }
106
107    pub fn right_buttons_start_x(&self) -> Option<f32> {
108        self.buttons_right.last().map(|button| button.x())
109    }
110
111    pub fn left_buttons_end_x(&self) -> Option<f32> {
112        self.buttons_left.last().map(|button| button.end_x())
113    }
114
115    #[allow(clippy::too_many_arguments)]
116    pub fn draw(
117        &self,
118        start_x: f32,
119        end_x: f32,
120        scale: f32,
121        colors: &ColorMap,
122        mouse_location: Location,
123        pixmap: &mut PixmapMut,
124        resizable: bool,
125        state: &WindowState,
126    ) {
127        let left_buttons_right_limit =
128            self.right_buttons_start_x().unwrap_or(end_x).min(end_x) - BUTTON_SPACING;
129        let buttons_left = self.buttons_left.iter().map(|x| (x, Side::Left));
130        let buttons_right = self.buttons_right.iter().map(|x| (x, Side::Right));
131
132        for (button, side) in buttons_left.chain(buttons_right) {
133            let is_visible = button.x() > start_x && button.end_x() < end_x
134                // If we have buttons from both sides and they overlap, prefer the right side
135                && (side == Side::Right || button.end_x() < left_buttons_right_limit);
136
137            if is_visible {
138                button.draw(scale, colors, mouse_location, pixmap, resizable, state);
139            }
140        }
141    }
142
143    fn parse_button_layout(sides: Option<(String, String)>) -> Option<ButtonLayout> {
144        let Some((left_side, right_side)) = sides else {
145            return None;
146        };
147
148        let buttons_left = Buttons::parse_button_layout_side(left_side, Side::Left);
149        let buttons_right = Buttons::parse_button_layout_side(right_side, Side::Right);
150
151        if buttons_left.is_empty() && buttons_right.is_empty() {
152            warn!("No valid buttons found in configuration");
153            return None;
154        }
155
156        Some((buttons_left, buttons_right))
157    }
158
159    fn parse_button_layout_side(config: String, side: Side) -> Vec<Button> {
160        let mut buttons: Vec<Button> = vec![];
161
162        for button in config.split(',').take(3) {
163            let button_kind = match button {
164                "close" => ButtonKind::Close,
165                "maximize" => ButtonKind::Maximize,
166                "minimize" => ButtonKind::Minimize,
167                "appmenu" => {
168                    debug!("Ignoring \"appmenu\" button");
169                    continue;
170                }
171                _ => {
172                    warn!("Ignoring unknown button type: {button}");
173                    continue;
174                }
175            };
176
177            buttons.push(Button::new(button_kind));
178        }
179
180        // For the right side, we need to revert the order
181        if side == Side::Right {
182            buttons.into_iter().rev().collect()
183        } else {
184            buttons
185        }
186    }
187
188    fn get_default_buttons_layout() -> ButtonLayout {
189        (
190            vec![],
191            vec![
192                Button::new(ButtonKind::Close),
193                Button::new(ButtonKind::Maximize),
194                Button::new(ButtonKind::Minimize),
195            ],
196        )
197    }
198}
199
200#[derive(Debug, Clone)]
201pub(crate) struct Button {
202    /// The button offset into the header bar canvas.
203    offset: f32,
204    /// The kind of the button.
205    kind: ButtonKind,
206}
207
208impl Button {
209    pub fn new(kind: ButtonKind) -> Self {
210        Self { offset: 0., kind }
211    }
212
213    pub fn radius(&self) -> f32 {
214        BUTTON_SIZE / 2.0
215    }
216
217    pub fn x(&self) -> f32 {
218        self.offset
219    }
220
221    pub fn center_x(&self) -> f32 {
222        self.offset + self.radius()
223    }
224
225    pub fn center_y(&self) -> f32 {
226        BUTTON_MARGIN + self.radius()
227    }
228
229    pub fn end_x(&self) -> f32 {
230        self.offset + BUTTON_SIZE
231    }
232
233    fn contains(&self, x: f32, y: f32) -> bool {
234        x > self.offset
235            && x < self.offset + BUTTON_SIZE
236            && y > BUTTON_MARGIN
237            && y < BUTTON_MARGIN + BUTTON_SIZE
238    }
239
240    pub fn draw(
241        &self,
242        scale: f32,
243        colors: &ColorMap,
244        mouse_location: Location,
245        pixmap: &mut PixmapMut,
246        resizable: bool,
247        state: &WindowState,
248    ) -> SkiaResult {
249        let button_bg = if mouse_location == Location::Button(self.kind)
250            && (resizable || self.kind != ButtonKind::Maximize)
251        {
252            colors.button_hover_paint()
253        } else {
254            colors.button_idle_paint()
255        };
256
257        // Convert to pixels.
258        let x = self.center_x() * scale;
259        let y = self.center_y() * scale;
260        let radius = self.radius() * scale;
261
262        // Draw the button background.
263        let circle = PathBuilder::from_circle(x, y, radius)?;
264        pixmap.fill_path(
265            &circle,
266            &button_bg,
267            FillRule::Winding,
268            Transform::identity(),
269            None,
270        );
271
272        let mut button_icon_paint = colors.button_icon_paint();
273        // Do AA only for diagonal lines.
274        button_icon_paint.anti_alias = self.kind == ButtonKind::Close;
275
276        // Draw the icon.
277        match self.kind {
278            ButtonKind::Close => {
279                let x_icon = {
280                    let size = 3.5 * scale;
281                    let mut pb = PathBuilder::new();
282
283                    {
284                        let sx = x - size;
285                        let sy = y - size;
286                        let ex = x + size;
287                        let ey = y + size;
288
289                        pb.move_to(sx, sy);
290                        pb.line_to(ex, ey);
291                        pb.close();
292                    }
293
294                    {
295                        let sx = x - size;
296                        let sy = y + size;
297                        let ex = x + size;
298                        let ey = y - size;
299
300                        pb.move_to(sx, sy);
301                        pb.line_to(ex, ey);
302                        pb.close();
303                    }
304
305                    pb.finish()?
306                };
307
308                pixmap.stroke_path(
309                    &x_icon,
310                    &button_icon_paint,
311                    &Stroke {
312                        width: 1.1 * scale,
313                        ..Default::default()
314                    },
315                    Transform::identity(),
316                    None,
317                );
318            }
319            ButtonKind::Maximize => {
320                let path2 = {
321                    let size = 8.0 * scale;
322                    let hsize = size / 2.0;
323                    let mut pb = PathBuilder::new();
324
325                    let x = x - hsize;
326                    let y = y - hsize;
327                    if state.contains(WindowState::MAXIMIZED) {
328                        let offset = 2.0 * scale;
329                        if let Some(rect) =
330                            Rect::from_xywh(x, y + offset, size - offset, size - offset)
331                        {
332                            pb.push_rect(rect);
333                            pb.move_to(rect.left() + offset, rect.top() - offset);
334                            pb.line_to(rect.right() + offset, rect.top() - offset);
335                            pb.line_to(rect.right() + offset, rect.bottom() - offset + 0.5);
336                        }
337                    } else if let Some(rect) = Rect::from_xywh(x, y, size, size) {
338                        pb.push_rect(rect);
339                    }
340
341                    pb.finish()?
342                };
343
344                pixmap.stroke_path(
345                    &path2,
346                    &button_icon_paint,
347                    &Stroke {
348                        width: 1.0 * scale,
349                        ..Default::default()
350                    },
351                    Transform::identity(),
352                    None,
353                );
354            }
355            ButtonKind::Minimize => {
356                let len = 8.0 * scale;
357                let hlen = len / 2.0;
358                pixmap.fill_rect(
359                    Rect::from_xywh(x - hlen, y + hlen, len, scale)?,
360                    &button_icon_paint,
361                    Transform::identity(),
362                    None,
363                );
364            }
365        }
366
367        Some(())
368    }
369}
370
371#[derive(Debug, Copy, Clone, PartialEq, Eq)]
372pub enum ButtonKind {
373    Close,
374    Maximize,
375    Minimize,
376}
377
378#[derive(Debug, Copy, Clone, PartialEq, Eq)]
379pub enum Side {
380    Left,
381    Right,
382}