cosmic/widget/context_drawer/
widget.rs

1// Copyright 2023 System76 <info@system76.com>
2// SPDX-License-Identifier: MPL-2.0
3
4use std::borrow::Cow;
5
6use crate::widget::{LayerContainer, button, column, container, icon, row, scrollable, text};
7use crate::{Apply, Element, Renderer, Theme};
8
9use super::overlay::Overlay;
10
11use iced_core::Alignment;
12use iced_core::event::{self, Event};
13use iced_core::widget::{Operation, Tree};
14use iced_core::{
15    Clipboard, Layout, Length, Rectangle, Shell, Vector, Widget, layout, mouse,
16    overlay as iced_overlay, renderer,
17};
18
19#[must_use]
20pub struct ContextDrawer<'a, Message> {
21    id: Option<iced_core::widget::Id>,
22    content: Element<'a, Message>,
23    drawer: Element<'a, Message>,
24    on_close: Option<Message>,
25}
26
27impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> {
28    pub fn new_inner<Drawer>(
29        title: Option<Cow<'a, str>>,
30        header_actions: Vec<Element<'a, Message>>,
31        header_opt: Option<Element<'a, Message>>,
32        footer_opt: Option<Element<'a, Message>>,
33        drawer: Drawer,
34        on_close: Message,
35        max_width: f32,
36    ) -> Element<'a, Message>
37    where
38        Drawer: Into<Element<'a, Message>>,
39    {
40        #[inline(never)]
41        fn inner<'a, Message: Clone + 'static>(
42            title: Option<Cow<'a, str>>,
43            header_actions: Vec<Element<'a, Message>>,
44            header_opt: Option<Element<'a, Message>>,
45            footer_opt: Option<Element<'a, Message>>,
46            drawer: Element<'a, Message>,
47            on_close: Message,
48            max_width: f32,
49        ) -> Element<'a, Message> {
50            let cosmic_theme::Spacing {
51                space_xxs,
52                space_s,
53                space_m,
54                space_l,
55                ..
56            } = crate::theme::spacing();
57
58            let (horizontal_padding, title_portion, side_portion) = if max_width < 392.0 {
59                (space_s, 1, 1)
60            } else {
61                (space_l, 2, 1)
62            };
63
64            let title = title.map(|title| {
65                text::heading(title)
66                    .width(Length::FillPortion(title_portion))
67                    .center()
68            });
69
70            let (actions_width, close_width) = if title.is_some() {
71                (
72                    Length::FillPortion(side_portion),
73                    Length::FillPortion(side_portion),
74                )
75            } else {
76                (Length::Fill, Length::Shrink)
77            };
78
79            let header_row = row::with_capacity(3)
80                .width(Length::Fixed(480.0))
81                .align_y(Alignment::Center)
82                .push(
83                    row::with_children(header_actions)
84                        .spacing(space_xxs)
85                        .width(actions_width),
86                )
87                .push_maybe(title)
88                .push(
89                    button::text("Close")
90                        .trailing_icon(icon::from_name("go-next-symbolic"))
91                        .on_press(on_close)
92                        .apply(container)
93                        .width(close_width)
94                        .align_x(Alignment::End),
95                );
96            let header = column::with_capacity(2)
97                .width(Length::Fixed(480.0))
98                .align_x(Alignment::Center)
99                .spacing(space_m)
100                .padding([space_m, horizontal_padding])
101                .push(header_row)
102                .push_maybe(header_opt);
103            let footer = footer_opt.map(|element| {
104                container(element)
105                    .width(Length::Fixed(480.0))
106                    .align_y(Alignment::Center)
107                    .padding([space_xxs, horizontal_padding])
108            });
109            let pane = column::with_capacity(3)
110                .push(header)
111                .push(
112                    scrollable(container(drawer).padding([
113                        0,
114                        horizontal_padding,
115                        if footer.is_some() { 0 } else { space_l },
116                        horizontal_padding,
117                    ]))
118                    .height(Length::Fill)
119                    .width(Length::Shrink),
120                )
121                .push_maybe(footer);
122
123            // XXX new limits do not exactly handle the max width well for containers
124            // XXX this is a hack to get around that
125            container(
126                LayerContainer::new(pane)
127                    .layer(cosmic_theme::Layer::Primary)
128                    .class(crate::style::Container::ContextDrawer)
129                    .width(Length::Fill)
130                    .height(Length::Fill)
131                    .max_width(max_width),
132            )
133            .width(Length::Fill)
134            .height(Length::Fill)
135            .align_x(Alignment::End)
136            .into()
137        }
138
139        inner(
140            title,
141            header_actions,
142            header_opt,
143            footer_opt,
144            drawer.into(),
145            on_close,
146            max_width,
147        )
148    }
149
150    /// Creates an empty [`ContextDrawer`].
151    pub fn new<Content, Drawer>(
152        title: Option<Cow<'a, str>>,
153        header_actions: Vec<Element<'a, Message>>,
154        header_opt: Option<Element<'a, Message>>,
155        footer_opt: Option<Element<'a, Message>>,
156        content: Content,
157        drawer: Drawer,
158        on_close: Message,
159        max_width: f32,
160    ) -> Self
161    where
162        Content: Into<Element<'a, Message>>,
163        Drawer: Into<Element<'a, Message>>,
164    {
165        let drawer = Self::new_inner(
166            title,
167            header_actions,
168            header_opt,
169            footer_opt,
170            drawer,
171            on_close,
172            max_width,
173        );
174
175        ContextDrawer {
176            id: None,
177            content: content.into(),
178            drawer,
179            on_close: None,
180        }
181    }
182
183    /// Sets the [`Id`] of the [`ContextDrawer`].
184    #[inline]
185    pub fn id(mut self, id: iced_core::widget::Id) -> Self {
186        self.id = Some(id);
187        self
188    }
189
190    /// Map the message type of the context drawer to another
191    #[inline]
192    pub fn map<Out: Clone + 'static>(
193        mut self,
194        on_message: fn(Message) -> Out,
195    ) -> ContextDrawer<'a, Out> {
196        ContextDrawer {
197            id: self.id,
198            content: self.content.map(on_message),
199            drawer: self.drawer.map(on_message),
200            on_close: self.on_close.map(on_message),
201        }
202    }
203
204    /// Optionally assigns message to `on_close` event.
205    #[inline]
206    pub fn on_close_maybe(mut self, message: Option<Message>) -> Self {
207        self.on_close = message;
208        self
209    }
210}
211
212impl<Message: Clone> Widget<Message, crate::Theme, Renderer> for ContextDrawer<'_, Message> {
213    fn children(&self) -> Vec<Tree> {
214        vec![Tree::new(&self.content), Tree::new(&self.drawer)]
215    }
216
217    fn diff(&mut self, tree: &mut Tree) {
218        tree.diff_children(&mut [&mut self.content, &mut self.drawer]);
219    }
220
221    fn size(&self) -> iced_core::Size<Length> {
222        self.content.as_widget().size()
223    }
224
225    fn layout(
226        &self,
227        tree: &mut Tree,
228        renderer: &Renderer,
229        limits: &layout::Limits,
230    ) -> layout::Node {
231        self.content
232            .as_widget()
233            .layout(&mut tree.children[0], renderer, limits)
234    }
235
236    fn operate(
237        &self,
238        tree: &mut Tree,
239        layout: Layout<'_>,
240        renderer: &Renderer,
241        operation: &mut dyn Operation<()>,
242    ) {
243        self.content
244            .as_widget()
245            .operate(&mut tree.children[0], layout, renderer, operation);
246    }
247
248    fn on_event(
249        &mut self,
250        tree: &mut Tree,
251        event: Event,
252        layout: Layout<'_>,
253        cursor: mouse::Cursor,
254        renderer: &Renderer,
255        clipboard: &mut dyn Clipboard,
256        shell: &mut Shell<'_, Message>,
257        viewport: &Rectangle,
258    ) -> event::Status {
259        self.content.as_widget_mut().on_event(
260            &mut tree.children[0],
261            event,
262            layout,
263            cursor,
264            renderer,
265            clipboard,
266            shell,
267            viewport,
268        )
269    }
270
271    fn mouse_interaction(
272        &self,
273        tree: &Tree,
274        layout: Layout<'_>,
275        cursor: mouse::Cursor,
276        viewport: &Rectangle,
277        renderer: &Renderer,
278    ) -> mouse::Interaction {
279        self.content.as_widget().mouse_interaction(
280            &tree.children[0],
281            layout,
282            cursor,
283            viewport,
284            renderer,
285        )
286    }
287
288    fn draw(
289        &self,
290        tree: &Tree,
291        renderer: &mut Renderer,
292        theme: &Theme,
293        renderer_style: &renderer::Style,
294        layout: Layout<'_>,
295        cursor: mouse::Cursor,
296        viewport: &Rectangle,
297    ) {
298        self.content.as_widget().draw(
299            &tree.children[0],
300            renderer,
301            theme,
302            renderer_style,
303            layout,
304            cursor,
305            viewport,
306        );
307    }
308
309    fn overlay<'b>(
310        &'b mut self,
311        tree: &'b mut Tree,
312        layout: Layout<'_>,
313        _renderer: &Renderer,
314        translation: Vector,
315    ) -> Option<iced_overlay::Element<'b, Message, crate::Theme, Renderer>> {
316        let bounds = layout.bounds();
317
318        let mut position = layout.position();
319        position.x += translation.x;
320        position.y += translation.y;
321
322        Some(iced_overlay::Element::new(Box::new(Overlay {
323            content: &mut self.drawer,
324            tree: &mut tree.children[1],
325            width: bounds.width,
326            position,
327        })))
328    }
329
330    #[cfg(feature = "a11y")]
331    /// get the a11y nodes for the widget
332    fn a11y_nodes(
333        &self,
334        layout: Layout<'_>,
335        state: &Tree,
336        p: mouse::Cursor,
337    ) -> iced_accessibility::A11yTree {
338        let c_state = &state.children[0];
339        self.content.as_widget().a11y_nodes(layout, c_state, p)
340    }
341
342    fn drag_destinations(
343        &self,
344        state: &Tree,
345        layout: Layout<'_>,
346        renderer: &Renderer,
347        dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
348    ) {
349        self.content.as_widget().drag_destinations(
350            &state.children[0],
351            layout,
352            renderer,
353            dnd_rectangles,
354        );
355    }
356
357    fn id(&self) -> Option<iced_core::widget::Id> {
358        self.id.clone()
359    }
360
361    fn set_id(&mut self, id: iced_core::widget::Id) {
362        self.id = Some(id);
363    }
364}
365
366impl<'a, Message: 'a + Clone> From<ContextDrawer<'a, Message>> for Element<'a, Message> {
367    fn from(widget: ContextDrawer<'a, Message>) -> Element<'a, Message> {
368        Element::new(widget)
369    }
370}