1use 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
15pub 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};
22pub const PLACEHOLDER_STROKE: f32 = 2.0;
24pub const PLACEHOLDER_FALLBACK: f32 = 400.0;
26
27pub 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 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 pub fn color(mut self, color: Color) -> Self {
54 self.color = Some(color);
55 self
56 }
57
58 pub fn stroke(mut self, stroke: f32) -> Self {
60 self.stroke = stroke;
61 self
62 }
63
64 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 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 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 #[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 #[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 #[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}