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: Vec
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}
383