1 | use log::{debug, warn}; |
2 | use smithay_client_toolkit::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; |
3 | use tiny_skia::{FillRule, PathBuilder, PixmapMut, Rect, Stroke, Transform}; |
4 | |
5 | use crate::{theme::ColorMap, Location, SkiaResult}; |
6 | |
7 | /// The size of the button on the header bar in logical points. |
8 | const BUTTON_SIZE: f32 = 24.; |
9 | const BUTTON_MARGIN: f32 = 5.; |
10 | const BUTTON_SPACING: f32 = 13.; |
11 | |
12 | #[derive (Debug)] |
13 | pub(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 | |
20 | type ButtonLayout = (Vec<Button>, Vec<Button>); |
21 | |
22 | impl Default for Buttons { |
23 | fn default() -> Self { |
24 | let (buttons_left: Vec, buttons_right: Vec) = Buttons::get_default_buttons_layout(); |
25 | |
26 | Self { |
27 | buttons_left, |
28 | buttons_right, |
29 | layout_config: None, |
30 | } |
31 | } |
32 | } |
33 | |
34 | impl 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)] |
201 | pub(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 | |
208 | impl 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)] |
372 | pub enum ButtonKind { |
373 | Close, |
374 | Maximize, |
375 | Minimize, |
376 | } |
377 | |
378 | #[derive (Debug, Copy, Clone, PartialEq, Eq)] |
379 | pub enum Side { |
380 | Left, |
381 | Right, |
382 | } |
383 | |