cosmic/widget/context_drawer/
widget.rs

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