Skip to main content

frus_widgets/
placeholder.rs

1//! [`Placeholder`]: a crossed box standing in for a widget not written yet.
2//!
3//! A design tool rather than a control: it takes whatever room it is given, draws a
4//! rectangle with its two diagonals, and makes the shape of a layout readable before any
5//! of it is built. The reference's defaults — blue grey, a 2 px stroke, and 400×400 when
6//! nothing constrains it — are the defaults here, and all three are overridable.
7
8use frus_core::{BorderRadius, Color, Path, Point, Rect, Scene};
9use frus_layout::{Dimension, Style};
10
11use crate::interaction::Status;
12use crate::theme::Theme;
13use crate::widget::Widget;
14
15/// The reference's colour: blue grey 700.
16pub const PLACEHOLDER_COLOR: Color = Color {
17    r: 0x45 as f32 / 255.0,
18    g: 0x5A as f32 / 255.0,
19    b: 0x64 as f32 / 255.0,
20    a: 1.0,
21};
22/// The reference's stroke width.
23pub const PLACEHOLDER_STROKE: f32 = 2.0;
24/// The size it takes when nothing constrains it.
25pub const PLACEHOLDER_FALLBACK: f32 = 400.0;
26
27/// A crossed box standing in for a widget not written yet.
28pub struct Placeholder {
29    color: Option<Color>,
30    stroke: f32,
31    width: f32,
32    height: f32,
33}
34
35impl Default for Placeholder {
36    fn default() -> Self {
37        Self::new()
38    }
39}
40
41impl Placeholder {
42    /// A placeholder at the reference's defaults.
43    pub fn new() -> Self {
44        Self {
45            color: None,
46            stroke: PLACEHOLDER_STROKE,
47            width: PLACEHOLDER_FALLBACK,
48            height: PLACEHOLDER_FALLBACK,
49        }
50    }
51
52    /// The colour of the box and its diagonals.
53    pub fn color(mut self, color: Color) -> Self {
54        self.color = Some(color);
55        self
56    }
57
58    /// The stroke width.
59    pub fn stroke(mut self, stroke: f32) -> Self {
60        self.stroke = stroke;
61        self
62    }
63
64    /// The size to take when nothing constrains it.
65    pub fn fallback(mut self, width: f32, height: f32) -> Self {
66        self.width = width;
67        self.height = height;
68        self
69    }
70}
71
72impl<Msg> Widget<Msg> for Placeholder {
73    fn style(&self) -> Style {
74        Style {
75            width: Dimension::Length(self.width),
76            height: Dimension::Length(self.height),
77            // It is a stand-in: it should take the room on offer rather than insist on
78            // 400 px and push the layout it is meant to be illustrating out of shape.
79            flex_grow: 1.0,
80            min_width: Dimension::Length(0.0),
81            min_height: Dimension::Length(0.0),
82            ..Default::default()
83        }
84    }
85
86    fn children(&self) -> &[Box<dyn Widget<Msg>>] {
87        &[]
88    }
89
90    fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
91        let color = self.color.unwrap_or(PLACEHOLDER_COLOR).fade(status.opacity);
92        let _ = theme;
93        scene.draw_rect(
94            bounds,
95            Color::TRANSPARENT,
96            BorderRadius::ZERO,
97            self.stroke,
98            color,
99        );
100        // The two diagonals, as strokes rather than a filled shape, so a thin placeholder
101        // still reads as a cross rather than as a solid wedge.
102        let half = self.stroke / 2.0;
103        let (right, bottom) = (bounds.x + bounds.width, bounds.y + bounds.height);
104        for (x0, y0, x1, y1) in [
105            (bounds.x, bounds.y, right, bottom),
106            (bounds.x, bottom, right, bounds.y),
107        ] {
108            let (dx, dy) = (x1 - x0, y1 - y0);
109            let len = (dx * dx + dy * dy).sqrt().max(f32::EPSILON);
110            let (nx, ny) = (-dy / len * half, dx / len * half);
111            let path = Path::new()
112                .move_to(Point::new(x0 + nx, y0 + ny))
113                .line_to(Point::new(x1 + nx, y1 + ny))
114                .line_to(Point::new(x1 - nx, y1 - ny))
115                .line_to(Point::new(x0 - nx, y0 - ny))
116                .close();
117            scene.fill_path(&path, color);
118        }
119    }
120
121    fn on_click(&self) -> Option<Msg> {
122        None
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use frus_core::{Primitive, Size};
130
131    fn primitives(w: &dyn Widget<()>) -> Vec<Primitive> {
132        let mut scene = Scene::new();
133        w.paint(
134            Rect::new(0.0, 0.0, 100.0, 50.0),
135            Status::default(),
136            &Theme::default(),
137            &mut scene,
138        );
139        let _ = Size::new(0.0, 0.0);
140        scene.primitives().to_vec()
141    }
142
143    /// A box and two diagonals — the reference's picture.
144    #[test]
145    fn it_draws_a_box_and_its_two_diagonals() {
146        let p = Placeholder::new();
147        let painted = primitives(&p);
148        let rects = painted
149            .iter()
150            .filter(|x| matches!(x, Primitive::Rect { .. }))
151            .count();
152        let paths = painted
153            .iter()
154            .filter(|x| matches!(x, Primitive::Path { .. }))
155            .count();
156        assert_eq!(rects, 1, "the outline");
157        assert_eq!(paths, 2, "the two diagonals");
158    }
159
160    /// The colour is the caller's when they gave one, and the reference's otherwise.
161    #[test]
162    fn the_colour_is_overridable() {
163        let default_color = match &primitives(&Placeholder::new())[0] {
164            Primitive::Rect { border_color, .. } => *border_color,
165            other => panic!("{other:?}"),
166        };
167        assert_eq!(default_color, PLACEHOLDER_COLOR);
168
169        let asked = Color::rgb(1.0, 0.0, 0.0);
170        let mine = match &primitives(&Placeholder::new().color(asked))[0] {
171            Primitive::Rect { border_color, .. } => *border_color,
172            other => panic!("{other:?}"),
173        };
174        assert_eq!(mine, asked);
175    }
176
177    /// It takes the room on offer rather than insisting on its 400 px fallback.
178    #[test]
179    fn it_gives_way_to_the_layout_around_it() {
180        let style = Widget::<()>::style(&Placeholder::new());
181        assert_eq!(style.flex_grow, 1.0);
182        assert_eq!(style.min_width, Dimension::Length(0.0));
183        assert_eq!(style.width, Dimension::Length(PLACEHOLDER_FALLBACK));
184    }
185}