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