veloren_voxygen/ui/ice/widget/
background_container.rs

1use iced::{
2    Clipboard, Element, Event, Hasher, Layout, Length, Point, Rectangle, Size, Widget, layout,
3};
4use std::hash::Hash;
5
6// TODO: decouple from image/compound graphic widgets (they could still use
7// common types, but the info we want here for the background is a subset of
8// what a fullblown widget might need)
9
10// Note: it might be more efficient to make this generic over the content type
11
12// Note: maybe we could just use the container styling for this (not really with
13// the aspect ratio stuff)
14
15#[derive(Copy, Clone, Hash)]
16pub struct Padding {
17    pub top: u16,
18    pub bottom: u16,
19    pub right: u16,
20    pub left: u16,
21}
22
23impl Padding {
24    pub fn new() -> Self {
25        Padding {
26            top: 0,
27            bottom: 0,
28            right: 0,
29            left: 0,
30        }
31    }
32
33    #[must_use]
34    pub fn top(mut self, pad: u16) -> Self {
35        self.top = pad;
36        self
37    }
38
39    #[must_use]
40    pub fn bottom(mut self, pad: u16) -> Self {
41        self.bottom = pad;
42        self
43    }
44
45    #[must_use]
46    pub fn right(mut self, pad: u16) -> Self {
47        self.right = pad;
48        self
49    }
50
51    #[must_use]
52    pub fn left(mut self, pad: u16) -> Self {
53        self.left = pad;
54        self
55    }
56
57    #[must_use]
58    pub fn vertical(mut self, pad: u16) -> Self {
59        self.top = pad;
60        self.bottom = pad;
61        self
62    }
63
64    #[must_use]
65    pub fn horizontal(mut self, pad: u16) -> Self {
66        self.left = pad;
67        self.right = pad;
68        self
69    }
70}
71
72impl Default for Padding {
73    fn default() -> Self { Self::new() }
74}
75
76pub trait Background<R: iced::Renderer>: Sized {
77    // The intended implementors already store the state accessed in the three
78    // functions below
79    fn width(&self) -> Length;
80    fn height(&self) -> Length;
81    fn aspect_ratio_fixed(&self) -> bool;
82    fn pixel_dims(&self, renderer: &R) -> (u16, u16);
83    fn draw(
84        &self,
85        renderer: &mut R,
86        defaults: &R::Defaults,
87        layout: Layout<'_>,
88        cursor_position: Point,
89        viewport: &Rectangle,
90    ) -> R::Output;
91}
92
93/// This widget is displays a background image behind it's content
94pub struct BackgroundContainer<'a, M, R: Renderer, B: Background<R>> {
95    max_width: u32,
96    max_height: u32,
97    background: B,
98    // Padding in same pixel units as background image
99    // Scaled relative to the background's scaling
100    padding: Padding,
101    content: Element<'a, M, R>,
102}
103
104impl<'a, M, R, B> BackgroundContainer<'a, M, R, B>
105where
106    R: Renderer,
107    B: Background<R>,
108{
109    pub fn new(background: B, content: impl Into<Element<'a, M, R>>) -> Self {
110        Self {
111            max_width: u32::MAX,
112            max_height: u32::MAX,
113            background,
114            padding: Padding::new(),
115            content: content.into(),
116        }
117    }
118
119    #[must_use]
120    pub fn padding(mut self, padding: Padding) -> Self {
121        self.padding = padding;
122        self
123    }
124
125    #[must_use]
126    pub fn max_width(mut self, max_width: u32) -> Self {
127        self.max_width = max_width;
128        self
129    }
130
131    #[must_use]
132    pub fn max_height(mut self, max_height: u32) -> Self {
133        self.max_height = max_height;
134        self
135    }
136}
137
138impl<M, R, B> Widget<M, R> for BackgroundContainer<'_, M, R, B>
139where
140    R: Renderer,
141    B: Background<R>,
142{
143    // Uses the width and height from the background
144    fn width(&self) -> Length { self.background.width() }
145
146    fn height(&self) -> Length { self.background.height() }
147
148    fn layout(&self, renderer: &R, limits: &layout::Limits) -> layout::Node {
149        let limits = limits
150            .loose() // why does iced's container do this?
151            .max_width(self.max_width)
152            .max_height(self.max_height)
153            .width(self.width())
154            .height(self.height());
155
156        let (pixel_w, pixel_h) = self.background.pixel_dims(renderer);
157        let (horizontal_pad_frac, vertical_pad_frac, top_pad_frac, left_pad_frac) = {
158            let Padding {
159                top,
160                bottom,
161                right,
162                left,
163            } = self.padding;
164            // Just in case
165            // could convert to gracefully handling
166            debug_assert!(pixel_w != 0);
167            debug_assert!(pixel_h != 0);
168            debug_assert!(top + bottom < pixel_h);
169            debug_assert!(right + left < pixel_w);
170            (
171                (right + left) as f32 / pixel_w as f32,
172                (top + bottom) as f32 / pixel_h as f32,
173                top as f32 / pixel_h as f32,
174                left as f32 / pixel_w as f32,
175            )
176        };
177
178        let (size, content) = if self.background.aspect_ratio_fixed() {
179            // To fix the aspect ratio we have to have a separate layout from the content
180            // because we can't force the content to have a specific aspect ratio
181            let aspect_ratio = pixel_w as f32 / pixel_h as f32;
182
183            // To do this we need to figure out the max width/height of the limits
184            // and then adjust one down to meet the aspect ratio
185            let max_size = limits.max();
186            let (max_width, max_height) = (max_size.width, max_size.height);
187            let max_aspect_ratio = max_width / max_height;
188            let limits = if max_aspect_ratio > aspect_ratio {
189                limits.max_width((max_height * aspect_ratio) as u32)
190            } else {
191                limits.max_height((max_width / aspect_ratio) as u32)
192            };
193            // Account for padding at max size in the limits for the children
194            let limits = limits.shrink({
195                let max = limits.max();
196                Size::new(
197                    max.width * horizontal_pad_frac,
198                    max.height * vertical_pad_frac,
199                )
200            });
201
202            // Get content size
203            // again, why is loose() used here?
204            let mut content = self.content.layout(renderer, &limits.loose());
205
206            // TODO: handle cases where self and/or children are not Length::Fill
207            // If fill use max_size
208
209            // This time we need to adjust up to meet the aspect ratio
210            // so that the container is larger than the contents
211            let mut content_size = content.size();
212            // Add minimum padding to content size (this works to ensure we have enough
213            // space for padding because the available space can only increase)
214            content_size.width /= 1.0 - horizontal_pad_frac;
215            content_size.height /= 1.0 - vertical_pad_frac;
216            let content_aspect_ratio = content_size.width / content_size.height;
217            let size = if content_aspect_ratio > aspect_ratio {
218                Size::new(content_size.width, content_size.width / aspect_ratio)
219            } else {
220                Size::new(content_size.height * aspect_ratio, content_size.height)
221            };
222
223            // Move content to account for padding
224            content.move_to(Point::new(
225                left_pad_frac * size.width,
226                top_pad_frac * size.height,
227            ));
228
229            (size, content)
230        } else {
231            // Account for padding at max size in the limits for the children
232            let limits = limits
233                .shrink({
234                    let max = limits.max();
235                    Size::new(
236                        max.width * horizontal_pad_frac,
237                        max.height * vertical_pad_frac,
238                    )
239                })
240                .loose(); // again, why is loose() used here?
241
242            let mut content = self.content.layout(renderer, &limits);
243
244            let mut size = limits.resolve(content.size());
245            // Add padding back
246            size.width /= 1.0 - horizontal_pad_frac;
247            size.height /= 1.0 - vertical_pad_frac;
248
249            // Move to account for padding
250            content.move_to(Point::new(
251                left_pad_frac * size.width,
252                top_pad_frac * size.height,
253            ));
254            // No aligning since child is currently assumed to be fill
255
256            (size, content)
257        };
258
259        layout::Node::with_children(size, vec![content])
260    }
261
262    fn draw(
263        &self,
264        renderer: &mut R,
265        defaults: &R::Defaults,
266        layout: Layout<'_>,
267        cursor_position: Point,
268        viewport: &Rectangle,
269    ) -> R::Output {
270        renderer.draw(
271            defaults,
272            &self.background,
273            layout,
274            viewport,
275            &self.content,
276            layout.children().next().unwrap(),
277            cursor_position,
278        )
279    }
280
281    fn hash_layout(&self, state: &mut Hasher) {
282        struct Marker;
283        std::any::TypeId::of::<Marker>().hash(state);
284
285        self.width().hash(state);
286        self.height().hash(state);
287        self.max_width.hash(state);
288        self.max_height.hash(state);
289        self.background.aspect_ratio_fixed().hash(state);
290        self.padding.hash(state);
291        // TODO: add pixel dims (need renderer)
292
293        self.content.hash_layout(state);
294    }
295
296    fn on_event(
297        &mut self,
298        event: Event,
299        layout: Layout<'_>,
300        cursor_position: Point,
301        renderer: &R,
302        clipboard: &mut dyn Clipboard,
303        messages: &mut Vec<M>,
304    ) -> iced::event::Status {
305        self.content.on_event(
306            event,
307            layout.children().next().unwrap(),
308            cursor_position,
309            renderer,
310            clipboard,
311            messages,
312        )
313    }
314
315    fn overlay(&mut self, layout: Layout<'_>) -> Option<iced::overlay::Element<'_, M, R>> {
316        self.content.overlay(layout.children().next().unwrap())
317    }
318}
319
320pub trait Renderer: iced::Renderer {
321    fn draw<M, B>(
322        &mut self,
323        defaults: &Self::Defaults,
324        background: &B,
325        background_layout: Layout<'_>,
326        viewport: &Rectangle,
327        content: &Element<'_, M, Self>,
328        content_layout: Layout<'_>,
329        cursor_position: Point,
330    ) -> Self::Output
331    where
332        B: Background<Self>;
333}
334
335// They got to live ¯\_(ツ)_/¯
336impl<'a, M: 'a, R: 'a, B> From<BackgroundContainer<'a, M, R, B>> for Element<'a, M, R>
337where
338    R: Renderer,
339    B: 'a + Background<R>,
340{
341    fn from(background_container: BackgroundContainer<'a, M, R, B>) -> Element<'a, M, R> {
342        Element::new(background_container)
343    }
344}