cosmic/widget/
header_bar.rs

1// Copyright 2022 System76 <info@system76.com>
2// SPDX-License-Identifier: MPL-2.0
3
4use crate::cosmic_theme::{Density, Spacing};
5use crate::{Element, theme, widget};
6use apply::Apply;
7use derive_setters::Setters;
8use iced::Length;
9use iced_core::{Vector, Widget, widget::tree};
10use std::{borrow::Cow, cmp};
11
12#[must_use]
13pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> {
14    HeaderBar {
15        title: Cow::Borrowed(""),
16        on_close: None,
17        on_drag: None,
18        on_maximize: None,
19        on_minimize: None,
20        on_right_click: None,
21        start: Vec::new(),
22        center: Vec::new(),
23        end: Vec::new(),
24        density: None,
25        focused: false,
26        maximized: false,
27        is_ssd: false,
28        on_double_click: None,
29        is_condensed: false,
30    }
31}
32
33#[derive(Setters)]
34pub struct HeaderBar<'a, Message> {
35    /// Defines the title of the window
36    #[setters(skip)]
37    title: Cow<'a, str>,
38
39    /// A message emitted when the close button is pressed.
40    #[setters(strip_option)]
41    on_close: Option<Message>,
42
43    /// A message emitted when dragged.
44    #[setters(strip_option)]
45    on_drag: Option<Message>,
46
47    /// A message emitted when the maximize button is pressed.
48    #[setters(strip_option)]
49    on_maximize: Option<Message>,
50
51    /// A message emitted when the minimize button is pressed.
52    #[setters(strip_option)]
53    on_minimize: Option<Message>,
54
55    /// A message emitted when the header is double clicked,
56    /// usually used to maximize the window.
57    #[setters(strip_option)]
58    on_double_click: Option<Message>,
59
60    /// A message emitted when the header is right clicked.
61    #[setters(strip_option)]
62    on_right_click: Option<Message>,
63
64    /// Elements packed at the start of the headerbar.
65    #[setters(skip)]
66    start: Vec<Element<'a, Message>>,
67
68    /// Elements packed in the center of the headerbar.
69    #[setters(skip)]
70    center: Vec<Element<'a, Message>>,
71
72    /// Elements packed at the end of the headerbar.
73    #[setters(skip)]
74    end: Vec<Element<'a, Message>>,
75
76    /// Controls the density of the headerbar.
77    #[setters(strip_option)]
78    density: Option<Density>,
79
80    /// Focused state of the window
81    focused: bool,
82
83    /// Maximized state of the window
84    maximized: bool,
85
86    /// HeaderBar used for server-side decorations
87    is_ssd: bool,
88
89    /// Whether the headerbar should be compact
90    is_condensed: bool,
91}
92
93impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
94    /// Defines the title of the window
95    #[must_use]
96    pub fn title(mut self, title: impl Into<Cow<'a, str>> + 'a) -> Self {
97        self.title = title.into();
98        self
99    }
100
101    /// Pushes an element to the start region.
102    #[must_use]
103    pub fn start(mut self, widget: impl Into<Element<'a, Message>> + 'a) -> Self {
104        self.start.push(widget.into());
105        self
106    }
107
108    /// Pushes an element to the center region.
109    #[must_use]
110    pub fn center(mut self, widget: impl Into<Element<'a, Message>> + 'a) -> Self {
111        self.center.push(widget.into());
112        self
113    }
114
115    /// Pushes an element to the end region.
116    #[must_use]
117    pub fn end(mut self, widget: impl Into<Element<'a, Message>> + 'a) -> Self {
118        self.end.push(widget.into());
119        self
120    }
121
122    /// Build the widget
123    #[must_use]
124    #[inline]
125    pub fn build(self) -> HeaderBarWidget<'a, Message> {
126        HeaderBarWidget {
127            header_bar_inner: self.view(),
128        }
129    }
130}
131
132pub struct HeaderBarWidget<'a, Message> {
133    header_bar_inner: Element<'a, Message>,
134}
135
136impl<Message: Clone + 'static> Widget<Message, crate::Theme, crate::Renderer>
137    for HeaderBarWidget<'_, Message>
138{
139    fn diff(&mut self, tree: &mut tree::Tree) {
140        tree.diff_children(&mut [&mut self.header_bar_inner]);
141    }
142
143    fn children(&self) -> Vec<tree::Tree> {
144        vec![tree::Tree::new(&self.header_bar_inner)]
145    }
146
147    fn size(&self) -> iced_core::Size<Length> {
148        self.header_bar_inner.as_widget().size()
149    }
150
151    fn layout(
152        &self,
153        tree: &mut tree::Tree,
154        renderer: &crate::Renderer,
155        limits: &iced_core::layout::Limits,
156    ) -> iced_core::layout::Node {
157        let child_tree = &mut tree.children[0];
158        let child = self
159            .header_bar_inner
160            .as_widget()
161            .layout(child_tree, renderer, limits);
162        iced_core::layout::Node::with_children(child.size(), vec![child])
163    }
164
165    fn draw(
166        &self,
167        tree: &tree::Tree,
168        renderer: &mut crate::Renderer,
169        theme: &crate::Theme,
170        style: &iced_core::renderer::Style,
171        layout: iced_core::Layout<'_>,
172        cursor: iced_core::mouse::Cursor,
173        viewport: &iced_core::Rectangle,
174    ) {
175        let layout_children = layout.children().next().unwrap();
176        let state_children = &tree.children[0];
177        self.header_bar_inner.as_widget().draw(
178            state_children,
179            renderer,
180            theme,
181            style,
182            layout_children,
183            cursor,
184            viewport,
185        );
186    }
187
188    fn on_event(
189        &mut self,
190        state: &mut tree::Tree,
191        event: iced_core::Event,
192        layout: iced_core::Layout<'_>,
193        cursor: iced_core::mouse::Cursor,
194        renderer: &crate::Renderer,
195        clipboard: &mut dyn iced_core::Clipboard,
196        shell: &mut iced_core::Shell<'_, Message>,
197        viewport: &iced_core::Rectangle,
198    ) -> iced_core::event::Status {
199        let child_state = &mut state.children[0];
200        let child_layout = layout.children().next().unwrap();
201        self.header_bar_inner.as_widget_mut().on_event(
202            child_state,
203            event,
204            child_layout,
205            cursor,
206            renderer,
207            clipboard,
208            shell,
209            viewport,
210        )
211    }
212
213    fn mouse_interaction(
214        &self,
215        state: &tree::Tree,
216        layout: iced_core::Layout<'_>,
217        cursor: iced_core::mouse::Cursor,
218        viewport: &iced_core::Rectangle,
219        renderer: &crate::Renderer,
220    ) -> iced_core::mouse::Interaction {
221        let child_tree = &state.children[0];
222        let child_layout = layout.children().next().unwrap();
223        self.header_bar_inner.as_widget().mouse_interaction(
224            child_tree,
225            child_layout,
226            cursor,
227            viewport,
228            renderer,
229        )
230    }
231
232    fn operate(
233        &self,
234        state: &mut tree::Tree,
235        layout: iced_core::Layout<'_>,
236        renderer: &crate::Renderer,
237        operation: &mut dyn iced_core::widget::Operation<()>,
238    ) {
239        let child_tree = &mut state.children[0];
240        let child_layout = layout.children().next().unwrap();
241        self.header_bar_inner
242            .as_widget()
243            .operate(child_tree, child_layout, renderer, operation);
244    }
245
246    fn overlay<'b>(
247        &'b mut self,
248        state: &'b mut tree::Tree,
249        layout: iced_core::Layout<'_>,
250        renderer: &crate::Renderer,
251        translation: Vector,
252    ) -> Option<iced_core::overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
253        let child_tree = &mut state.children[0];
254        let child_layout = layout.children().next().unwrap();
255        self.header_bar_inner.as_widget_mut().overlay(
256            child_tree,
257            child_layout,
258            renderer,
259            translation,
260        )
261    }
262
263    fn drag_destinations(
264        &self,
265        state: &tree::Tree,
266        layout: iced_core::Layout<'_>,
267        renderer: &crate::Renderer,
268        dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
269    ) {
270        if let Some((child_tree, child_layout)) =
271            state.children.iter().zip(layout.children()).next()
272        {
273            self.header_bar_inner.as_widget().drag_destinations(
274                child_tree,
275                child_layout,
276                renderer,
277                dnd_rectangles,
278            );
279        }
280    }
281
282    #[cfg(feature = "a11y")]
283    /// get the a11y nodes for the widget
284    fn a11y_nodes(
285        &self,
286        layout: iced_core::Layout<'_>,
287        state: &tree::Tree,
288        p: iced::mouse::Cursor,
289    ) -> iced_accessibility::A11yTree {
290        let c_layout = layout.children().next().unwrap();
291        let c_state = &state.children[0];
292        let ret = self
293            .header_bar_inner
294            .as_widget()
295            .a11y_nodes(c_layout, c_state, p);
296        ret
297    }
298}
299
300impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
301    #[allow(clippy::too_many_lines)]
302    /// Converts the headerbar builder into an Iced element.
303    pub fn view(mut self) -> Element<'a, Message> {
304        let Spacing {
305            space_xxxs,
306            space_xxs,
307            ..
308        } = theme::spacing();
309
310        // Take ownership of the regions to be packed.
311        let start = std::mem::take(&mut self.start);
312        let center = std::mem::take(&mut self.center);
313        let mut end = std::mem::take(&mut self.end);
314
315        let window_control_cnt = self.on_close.is_some() as usize
316            + self.on_maximize.is_some() as usize
317            + self.on_minimize.is_some() as usize;
318        // Also packs the window controls at the very end.
319        end.push(self.window_controls());
320
321        // Center content depending on window border
322        let padding = match self.density.unwrap_or_else(crate::config::header_size) {
323            Density::Compact => {
324                if self.maximized {
325                    [4, 8, 4, 8]
326                } else {
327                    [3, 7, 4, 7]
328                }
329            }
330            _ => {
331                if self.maximized {
332                    [8, 8, 8, 8]
333                } else {
334                    [7, 7, 8, 7]
335                }
336            }
337        };
338
339        let acc_count = |v: &[Element<'a, Message>]| {
340            v.iter().fold(0, |acc, e| {
341                acc + match e.as_widget().size().width {
342                    Length::Fixed(w) if w > 30. => (w / 30.0).ceil() as usize,
343                    _ => 1,
344                }
345            })
346        };
347
348        let left_len = acc_count(&start);
349        let right_len = acc_count(&end);
350
351        let portion = ((left_len.max(right_len + window_control_cnt) as f32
352            / center.len().max(1) as f32)
353            .round() as u16)
354            .max(1);
355        let (left_portion, right_portion) =
356            if center.is_empty() && (self.title.is_empty() || self.is_condensed) {
357                let left_to_right_ratio = left_len as f32 / right_len.max(1) as f32;
358                let right_to_left_ratio = right_len as f32 / left_len.max(1) as f32;
359                if right_to_left_ratio > 2. || left_len < 1 {
360                    (1, 2)
361                } else if left_to_right_ratio > 2. || right_len < 1 {
362                    (2, 1)
363                } else {
364                    (left_len as u16, (right_len + window_control_cnt) as u16)
365                }
366            } else {
367                (portion, portion)
368            };
369        let title_portion = cmp::max(left_portion, right_portion) * 2;
370        // Creates the headerbar widget.
371        let mut widget = widget::row::with_capacity(3)
372            // If elements exist in the start region, append them here.
373            .push(
374                widget::row::with_children(start)
375                    .spacing(space_xxxs)
376                    .align_y(iced::Alignment::Center)
377                    .apply(widget::container)
378                    .align_x(iced::Alignment::Start)
379                    .width(Length::FillPortion(left_portion)),
380            )
381            // If elements exist in the center region, use them here.
382            // This will otherwise use the title as a widget if a title was defined.
383            .push_maybe(if !center.is_empty() {
384                Some(
385                    widget::row::with_children(center)
386                        .spacing(space_xxxs)
387                        .align_y(iced::Alignment::Center)
388                        .apply(widget::container)
389                        .center_x(Length::Fill)
390                        .into(),
391                )
392            } else if !self.title.is_empty() && !self.is_condensed {
393                Some(self.title_widget(title_portion))
394            } else {
395                None
396            })
397            .push(
398                widget::row::with_children(end)
399                    .spacing(space_xxs)
400                    .align_y(iced::Alignment::Center)
401                    .apply(widget::container)
402                    .align_x(iced::Alignment::End)
403                    .width(Length::FillPortion(right_portion)),
404            )
405            .align_y(iced::Alignment::Center)
406            .height(Length::Fixed(32.0 + padding[0] as f32 + padding[2] as f32))
407            .padding(if self.is_ssd { [0, 8, 0, 8] } else { padding })
408            .spacing(8)
409            .apply(widget::container)
410            .class(crate::theme::Container::HeaderBar {
411                focused: self.focused,
412            })
413            .center_y(Length::Shrink)
414            .apply(widget::mouse_area);
415
416        // Assigns a message to emit when the headerbar is dragged.
417        if let Some(message) = self.on_drag.clone() {
418            widget = widget.on_drag(message);
419        }
420
421        // Assigns a message to emit when the headerbar is double-clicked.
422        if let Some(message) = self.on_maximize.clone() {
423            widget = widget.on_release(message);
424        }
425        if let Some(message) = self.on_double_click.clone() {
426            widget = widget.on_double_press(message);
427        }
428        if let Some(message) = self.on_right_click.clone() {
429            widget = widget.on_right_press(message);
430        }
431
432        widget.into()
433    }
434
435    fn title_widget(&mut self, title_portion: u16) -> Element<'a, Message> {
436        let mut title = Cow::default();
437        std::mem::swap(&mut title, &mut self.title);
438
439        widget::text::heading(title)
440            .apply(widget::container)
441            .center(Length::FillPortion(title_portion))
442            .into()
443    }
444
445    /// Creates the widget for window controls.
446    fn window_controls(&mut self) -> Element<'a, Message> {
447        macro_rules! icon {
448            ($name:expr, $size:expr, $on_press:expr) => {{
449                #[cfg(target_os = "linux")]
450                let icon = {
451                    widget::icon::from_name($name)
452                        .apply(widget::button::icon)
453                        .padding(8)
454                };
455
456                #[cfg(not(target_os = "linux"))]
457                let icon = {
458                    widget::icon::from_svg_bytes(include_bytes!(concat!(
459                        "../../res/icons/",
460                        $name,
461                        ".svg"
462                    )))
463                    .symbolic(true)
464                    .apply(widget::button::icon)
465                    .padding(8)
466                };
467
468                icon.class(crate::theme::Button::HeaderBar)
469                    .selected(self.focused)
470                    .icon_size($size)
471                    .on_press($on_press)
472            }};
473        }
474
475        widget::row::with_capacity(3)
476            .push_maybe(
477                self.on_minimize
478                    .take()
479                    .map(|m: Message| icon!("window-minimize-symbolic", 16, m)),
480            )
481            .push_maybe(self.on_maximize.take().map(|m| {
482                if self.maximized {
483                    icon!("window-restore-symbolic", 16, m)
484                } else {
485                    icon!("window-maximize-symbolic", 16, m)
486                }
487            }))
488            .push_maybe(
489                self.on_close
490                    .take()
491                    .map(|m| icon!("window-close-symbolic", 16, m)),
492            )
493            .spacing(theme::spacing().space_xxs)
494            .apply(widget::container)
495            .center_y(Length::Fill)
496            .into()
497    }
498}
499
500impl<'a, Message: Clone + 'static> From<HeaderBar<'a, Message>> for Element<'a, Message> {
501    fn from(headerbar: HeaderBar<'a, Message>) -> Self {
502        Element::new(headerbar.build())
503    }
504}
505
506impl<'a, Message: Clone + 'static> From<HeaderBarWidget<'a, Message>> for Element<'a, Message> {
507    fn from(headerbar: HeaderBarWidget<'a, Message>) -> Self {
508        Element::new(headerbar)
509    }
510}