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