Skip to main content

material_ui_rs/widget/component/
viewport.rs

1//! Reusable viewport helpers for clipped animated content.
2
3use std::fmt;
4
5use iced_widget::core::widget::{self, Tree, tree};
6use iced_widget::core::{
7    Clipboard, Element, Event, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget,
8    alignment, layout, mouse, overlay, renderer,
9};
10
11use crate::Theme;
12
13/// A viewport that clips its child to a visible size while optionally laying it
14/// out at a larger internal size.
15///
16/// This is useful for Material size transitions where content should keep its
17/// final layout while the visible viewport expands or collapses.
18pub struct Viewport<'a, Message, Renderer = iced_widget::Renderer> {
19    content: Element<'a, Message, Theme, Renderer>,
20    width: Length,
21    height: Length,
22    layout_width: Option<f32>,
23    layout_height: Option<f32>,
24    align_y: alignment::Vertical,
25}
26
27impl<'a, Message, Renderer> Viewport<'a, Message, Renderer>
28where
29    Message: 'a,
30    Renderer: iced_widget::core::Renderer + 'a,
31{
32    pub fn new(content: impl Into<Element<'a, Message, Theme, Renderer>>) -> Self {
33        let content = content.into();
34        let size = content.as_widget().size();
35
36        Self {
37            content,
38            width: size.width,
39            height: size.height,
40            layout_width: None,
41            layout_height: None,
42            align_y: alignment::Vertical::Top,
43        }
44    }
45
46    pub fn fixed_height(
47        content: impl Into<Element<'a, Message, Theme, Renderer>>,
48        visible_height: f32,
49        layout_height: f32,
50    ) -> Self {
51        Self::new(content)
52            .height(Length::Fixed(visible_height))
53            .layout_height(layout_height)
54    }
55
56    pub fn width(mut self, width: impl Into<Length>) -> Self {
57        self.width = width.into();
58        self
59    }
60
61    pub fn height(mut self, height: impl Into<Length>) -> Self {
62        self.height = height.into();
63        self
64    }
65
66    pub fn layout_width(mut self, layout_width: f32) -> Self {
67        self.layout_width = Some(layout_width);
68        self
69    }
70
71    pub fn layout_height(mut self, layout_height: f32) -> Self {
72        self.layout_height = Some(layout_height);
73        self
74    }
75
76    pub fn align_y(mut self, alignment: alignment::Vertical) -> Self {
77        self.align_y = alignment;
78        self
79    }
80}
81
82/// Creates a vertically clipped viewport with a fixed visible height and a
83/// fixed internal layout height.
84pub fn fixed_height<'a, Message, Renderer>(
85    content: impl Into<Element<'a, Message, Theme, Renderer>>,
86    visible_height: f32,
87    layout_height: f32,
88) -> Element<'a, Message, Theme, Renderer>
89where
90    Message: 'a,
91    Renderer: iced_widget::core::Renderer + 'a,
92{
93    Viewport::fixed_height(content, visible_height, layout_height).into()
94}
95
96impl<Message, Renderer> fmt::Debug for Viewport<'_, Message, Renderer> {
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        f.debug_struct("Viewport")
99            .field("width", &self.width)
100            .field("height", &self.height)
101            .field("layout_width", &self.layout_width)
102            .field("layout_height", &self.layout_height)
103            .finish_non_exhaustive()
104    }
105}
106
107impl<Message, Renderer> Widget<Message, Theme, Renderer> for Viewport<'_, Message, Renderer>
108where
109    Renderer: iced_widget::core::Renderer,
110{
111    fn tag(&self) -> tree::Tag {
112        self.content.as_widget().tag()
113    }
114
115    fn state(&self) -> tree::State {
116        self.content.as_widget().state()
117    }
118
119    fn children(&self) -> Vec<Tree> {
120        self.content.as_widget().children()
121    }
122
123    fn diff(&self, tree: &mut Tree) {
124        self.content.as_widget().diff(tree);
125    }
126
127    fn size(&self) -> Size<Length> {
128        Size {
129            width: self.width,
130            height: self.height,
131        }
132    }
133
134    fn size_hint(&self) -> Size<Length> {
135        self.size()
136    }
137
138    fn layout(
139        &mut self,
140        tree: &mut Tree,
141        renderer: &Renderer,
142        limits: &layout::Limits,
143    ) -> layout::Node {
144        let size = limits.resolve(self.width, self.height, Size::ZERO);
145        let content_size = Size::new(
146            self.layout_width.unwrap_or(size.width).max(size.width),
147            self.layout_height.unwrap_or(size.height).max(size.height),
148        );
149        let content_limits = layout::Limits::new(content_size, content_size);
150        let content = self
151            .content
152            .as_widget_mut()
153            .layout(tree, renderer, &content_limits)
154            .move_to(Point::new(
155                0.0,
156                match self.align_y {
157                    alignment::Vertical::Top => 0.0,
158                    alignment::Vertical::Center => (size.height - content_size.height) / 2.0,
159                    alignment::Vertical::Bottom => size.height - content_size.height,
160                },
161            ));
162
163        layout::Node::with_children(size, vec![content])
164    }
165
166    fn operate(
167        &mut self,
168        tree: &mut Tree,
169        layout: Layout<'_>,
170        renderer: &Renderer,
171        operation: &mut dyn widget::Operation,
172    ) {
173        if let Some(content_layout) = layout.children().next() {
174            self.content
175                .as_widget_mut()
176                .operate(tree, content_layout, renderer, operation);
177        }
178    }
179
180    fn update(
181        &mut self,
182        tree: &mut Tree,
183        event: &Event,
184        layout: Layout<'_>,
185        cursor: mouse::Cursor,
186        renderer: &Renderer,
187        clipboard: &mut dyn Clipboard,
188        shell: &mut Shell<'_, Message>,
189        viewport: &Rectangle,
190    ) {
191        let Some(visible_viewport) = layout.bounds().intersection(viewport) else {
192            return;
193        };
194        let Some(content_layout) = layout.children().next() else {
195            return;
196        };
197        let cursor = if cursor.is_over(visible_viewport) {
198            cursor
199        } else {
200            mouse::Cursor::Unavailable
201        };
202
203        self.content.as_widget_mut().update(
204            tree,
205            event,
206            content_layout,
207            cursor,
208            renderer,
209            clipboard,
210            shell,
211            &visible_viewport,
212        );
213    }
214
215    fn mouse_interaction(
216        &self,
217        tree: &Tree,
218        layout: Layout<'_>,
219        cursor: mouse::Cursor,
220        viewport: &Rectangle,
221        renderer: &Renderer,
222    ) -> mouse::Interaction {
223        let Some(visible_viewport) = layout.bounds().intersection(viewport) else {
224            return mouse::Interaction::None;
225        };
226        let Some(content_layout) = layout.children().next() else {
227            return mouse::Interaction::None;
228        };
229
230        if !cursor.is_over(visible_viewport) {
231            return mouse::Interaction::None;
232        }
233
234        self.content.as_widget().mouse_interaction(
235            tree,
236            content_layout,
237            cursor,
238            &visible_viewport,
239            renderer,
240        )
241    }
242
243    fn draw(
244        &self,
245        tree: &Tree,
246        renderer: &mut Renderer,
247        theme: &Theme,
248        style: &renderer::Style,
249        layout: Layout<'_>,
250        cursor: mouse::Cursor,
251        viewport: &Rectangle,
252    ) {
253        let Some(visible_viewport) = layout.bounds().intersection(viewport) else {
254            return;
255        };
256        let Some(content_layout) = layout.children().next() else {
257            return;
258        };
259
260        renderer.with_layer(visible_viewport, |renderer| {
261            self.content.as_widget().draw(
262                tree,
263                renderer,
264                theme,
265                style,
266                content_layout,
267                cursor,
268                &visible_viewport,
269            );
270        });
271    }
272
273    fn overlay<'b>(
274        &'b mut self,
275        tree: &'b mut Tree,
276        layout: Layout<'b>,
277        renderer: &Renderer,
278        viewport: &Rectangle,
279        translation: Vector,
280    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
281        let visible_viewport = layout.bounds().intersection(viewport)?;
282        let content_layout = layout.children().next()?;
283
284        self.content.as_widget_mut().overlay(
285            tree,
286            content_layout,
287            renderer,
288            &visible_viewport,
289            translation,
290        )
291    }
292}
293
294impl<'a, Message, Renderer> From<Viewport<'a, Message, Renderer>>
295    for Element<'a, Message, Theme, Renderer>
296where
297    Message: 'a,
298    Renderer: iced_widget::core::Renderer + 'a,
299{
300    fn from(viewport: Viewport<'a, Message, Renderer>) -> Self {
301        Element::new(viewport)
302    }
303}
304
305#[cfg(test)]
306#[path = "../../../tests/widget/component/viewport.rs"]
307mod tests;