1use frust::authoring::{
71 BoxConstraints, BuildCtx, ChangeFlags, LayoutCtx, PaintCtx, PaintScene, View, Widget,
72};
73use frust::{Theme, divider as base_divider};
74use kurbo::{Point, Size};
75use peniko::Color;
76
77const DEFAULT_THICKNESS: f64 = 1.0;
81
82const FALLBACK_OUTLINE_VARIANT: Color = Color::from_rgb8(0xCA, 0xC4, 0xD0);
86
87#[derive(Clone, Copy, Debug, PartialEq)]
89pub struct DividerView {
90 vertical: bool,
91 thickness: f64,
92 indent: f64,
93 end_indent: f64,
94 color: Option<Color>,
95}
96
97pub fn divider() -> DividerView {
100 DividerView {
101 vertical: false,
102 thickness: DEFAULT_THICKNESS,
103 indent: 0.0,
104 end_indent: 0.0,
105 color: None,
106 }
107}
108
109impl DividerView {
110 pub fn vertical(mut self) -> Self {
113 self.vertical = true;
114 self
115 }
116
117 pub fn thickness(mut self, thickness: f64) -> Self {
120 self.thickness = thickness;
121 self
122 }
123
124 pub fn indent(mut self, indent: f64) -> Self {
127 self.indent = indent;
128 self
129 }
130
131 pub fn end_indent(mut self, end_indent: f64) -> Self {
136 self.end_indent = end_indent;
137 self
138 }
139
140 pub fn color(mut self, color: Color) -> Self {
143 self.color = Some(color);
144 self
145 }
146}
147
148pub struct DividerWidget {
150 vertical: bool,
151 thickness: f64,
152 indent: f64,
153 end_indent: f64,
154 color: Option<Color>,
155}
156
157impl<State: 'static> View<State> for DividerView {
158 type Element = DividerWidget;
159
160 fn build(&self, _ctx: &mut BuildCtx<'_>) -> DividerWidget {
161 DividerWidget {
162 vertical: self.vertical,
163 thickness: self.thickness,
164 indent: self.indent,
165 end_indent: self.end_indent,
166 color: self.color,
167 }
168 }
169
170 fn rebuild(
171 &self,
172 prev: &Self,
173 element: &mut DividerWidget,
174 _ctx: &mut BuildCtx<'_>,
175 ) -> ChangeFlags {
176 let mut flags = ChangeFlags::NONE;
177 if prev.color != self.color {
178 element.color = self.color;
179 flags |= ChangeFlags::PAINT;
180 }
181 if prev.vertical != self.vertical
182 || prev.thickness != self.thickness
183 || prev.indent != self.indent
184 || prev.end_indent != self.end_indent
185 {
186 element.vertical = self.vertical;
187 element.thickness = self.thickness;
188 element.indent = self.indent;
189 element.end_indent = self.end_indent;
190 flags |= ChangeFlags::LAYOUT;
191 }
192 flags
193 }
194}
195
196impl DividerWidget {
197 fn insets(&self) -> (f64, f64) {
200 (self.indent.max(0.0), self.end_indent.max(0.0))
201 }
202
203 fn base(&self, color: Color) -> frust::DividerView {
208 let mut view = base_divider(color).thickness(self.thickness);
209 if self.vertical {
210 view = view.vertical();
211 }
212 view
213 }
214
215 fn resolve_color(&self, theme: Option<&Theme>) -> Color {
220 if let Some(color) = self.color {
221 return color;
222 }
223 match theme {
224 Some(theme) => theme.scheme().outline_variant,
225 None => FALLBACK_OUTLINE_VARIANT,
226 }
227 }
228}
229
230fn build_line<State: 'static>(view: &frust::DividerView) -> frust::DividerWidget {
235 let mut counter = 0u64;
236 View::<State>::build(view, &mut BuildCtx::new(&mut counter))
237}
238
239impl Widget for DividerWidget {
240 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
241 let (before, after) = self.insets();
242 let total = before + after;
243 let inner_bc = if self.vertical {
244 BoxConstraints::new(
245 Size::new(bc.min().width, (bc.min().height - total).max(0.0)),
246 Size::new(bc.max().width, (bc.max().height - total).max(0.0)),
247 )
248 } else {
249 BoxConstraints::new(
250 Size::new((bc.min().width - total).max(0.0), bc.min().height),
251 Size::new((bc.max().width - total).max(0.0), bc.max().height),
252 )
253 };
254 let view = self.base(FALLBACK_OUTLINE_VARIANT); let mut line = build_line::<()>(&view);
256 let inner_size = line.layout(ctx, &inner_bc);
257 let full = if self.vertical {
258 Size::new(inner_size.width, inner_size.height + total)
259 } else {
260 Size::new(inner_size.width + total, inner_size.height)
261 };
262 bc.constrain(full)
263 }
264
265 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
266 let theme = Theme::from_paint_ctx(ctx);
267 let color = self.resolve_color(theme);
268 let (before, after) = self.insets();
269 let total = before + after;
270 let (inner_origin, inner_size) = if self.vertical {
271 (
272 Point::new(ctx.origin().x, ctx.origin().y + before),
273 Size::new(ctx.size().width, (ctx.size().height - total).max(0.0)),
274 )
275 } else {
276 (
277 Point::new(ctx.origin().x + before, ctx.origin().y),
278 Size::new((ctx.size().width - total).max(0.0), ctx.size().height),
279 )
280 };
281 let view = self.base(color);
282 let mut line = build_line::<()>(&view);
283 let mut inner_ctx = PaintCtx::new(inner_origin, inner_size);
288 line.paint(&mut inner_ctx, scene);
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295 use kurbo::Point;
296
297 fn build(view: &DividerView) -> DividerWidget {
298 let mut counter = 0u64;
299 View::<()>::build(view, &mut BuildCtx::new(&mut counter))
300 }
301
302 fn themed(outline_variant: Color) -> Theme {
306 let mut theme = Theme::neutral();
307 theme.light.outline_variant = outline_variant;
308 theme
309 }
310
311 #[test]
314 fn horizontal_default_fills_width_and_takes_default_thickness_on_height() {
315 let mut w = build(÷r());
316 let mut lctx = LayoutCtx::new();
317 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(320.0, 640.0)));
318 assert_eq!(size, Size::new(320.0, DEFAULT_THICKNESS));
319 }
320
321 #[test]
322 fn vertical_fills_height_and_takes_thickness_on_width() {
323 let mut w = build(÷r().vertical());
324 let mut lctx = LayoutCtx::new();
325 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(320.0, 640.0)));
326 assert_eq!(size, Size::new(DEFAULT_THICKNESS, 640.0));
327 }
328
329 #[test]
330 fn custom_thickness_is_honored() {
331 let mut w = build(÷r().thickness(4.0));
332 let mut lctx = LayoutCtx::new();
333 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(200.0, 200.0)));
334 assert_eq!(size, Size::new(200.0, 4.0));
335 }
336
337 #[test]
338 fn insets_shrink_the_line_but_the_wrapper_still_reports_the_full_fill_size() {
339 let mut w = build(÷r().indent(8.0).end_indent(4.0));
340 let mut lctx = LayoutCtx::new();
341 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(100.0, 50.0)));
342 assert_eq!(size, Size::new(100.0, DEFAULT_THICKNESS));
345 }
346
347 #[test]
348 fn vertical_insets_apply_to_the_fill_axis_too() {
349 let mut w = build(÷r().vertical().indent(10.0).end_indent(5.0));
350 let mut lctx = LayoutCtx::new();
351 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(50.0, 200.0)));
352 assert_eq!(size, Size::new(DEFAULT_THICKNESS, 200.0));
353 }
354
355 #[test]
356 fn a_tight_constraint_forces_the_divider_to_it_regardless_of_thickness() {
357 let mut w = build(÷r());
358 let mut lctx = LayoutCtx::new();
359 let size = w.layout(&mut lctx, &BoxConstraints::tight(Size::new(50.0, 50.0)));
360 assert_eq!(size, Size::new(50.0, 50.0));
361 }
362
363 #[derive(Default)]
366 struct Recorder {
367 rects: Vec<(Point, Size, Color)>,
368 }
369
370 impl PaintScene for Recorder {
371 fn fill_rect(&mut self, origin: Point, size: Size, color: Color) {
372 self.rects.push((origin, size, color));
373 }
374 fn draw_text(&mut self, _origin: Point, _text: &str) {}
375 }
376
377 #[test]
378 fn paint_with_no_theme_uses_the_unthemed_fallback() {
379 let mut w = build(÷r());
380 let mut lctx = LayoutCtx::new();
381 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(100.0, 100.0)));
382
383 let mut rec = Recorder::default();
384 let mut pctx = PaintCtx::new(Point::ZERO, size);
385 w.paint(&mut pctx, &mut rec);
386
387 assert_eq!(
388 rec.rects,
389 vec![(Point::ZERO, size, FALLBACK_OUTLINE_VARIANT)]
390 );
391 }
392
393 #[test]
394 fn paint_resolves_the_theme_s_outline_variant_when_no_explicit_color() {
395 let mut w = build(÷r());
396 let mut lctx = LayoutCtx::new();
397 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(100.0, 100.0)));
398
399 let expected = Color::from_rgb8(0x11, 0x22, 0x33);
400 let theme = themed(expected);
401 let mut rec = Recorder::default();
402 let mut pctx = PaintCtx::new(Point::ZERO, size).with_theme(&theme);
403 w.paint(&mut pctx, &mut rec);
404
405 assert_eq!(rec.rects, vec![(Point::ZERO, size, expected)]);
406 }
407
408 #[test]
409 fn explicit_color_wins_over_the_theme() {
410 let explicit = Color::from_rgb8(0xAA, 0xBB, 0xCC);
411 let mut w = build(÷r().color(explicit));
412 let mut lctx = LayoutCtx::new();
413 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(100.0, 100.0)));
414
415 let theme = themed(Color::from_rgb8(0x11, 0x22, 0x33));
416 let mut rec = Recorder::default();
417 let mut pctx = PaintCtx::new(Point::ZERO, size).with_theme(&theme);
418 w.paint(&mut pctx, &mut rec);
419
420 assert_eq!(rec.rects, vec![(Point::ZERO, size, explicit)]);
421 }
422
423 #[test]
424 fn horizontal_insets_offset_and_shrink_the_painted_line() {
425 let mut w = build(÷r().indent(8.0).end_indent(4.0));
426 let mut lctx = LayoutCtx::new();
427 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(100.0, 50.0)));
428
429 let mut rec = Recorder::default();
430 let mut pctx = PaintCtx::new(Point::new(2.0, 3.0), size);
431 w.paint(&mut pctx, &mut rec);
432
433 assert_eq!(
434 rec.rects,
435 vec![(
436 Point::new(2.0 + 8.0, 3.0),
437 Size::new(100.0 - 8.0 - 4.0, DEFAULT_THICKNESS),
438 FALLBACK_OUTLINE_VARIANT
439 )]
440 );
441 }
442
443 #[test]
444 fn vertical_insets_offset_and_shrink_the_painted_line() {
445 let mut w = build(÷r().vertical().indent(10.0).end_indent(5.0));
446 let mut lctx = LayoutCtx::new();
447 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(50.0, 200.0)));
448
449 let mut rec = Recorder::default();
450 let mut pctx = PaintCtx::new(Point::ZERO, size);
451 w.paint(&mut pctx, &mut rec);
452
453 assert_eq!(
454 rec.rects,
455 vec![(
456 Point::new(0.0, 10.0),
457 Size::new(DEFAULT_THICKNESS, 200.0 - 10.0 - 5.0),
458 FALLBACK_OUTLINE_VARIANT
459 )]
460 );
461 }
462
463 #[test]
464 fn insets_larger_than_the_line_collapse_to_zero_rather_than_going_negative() {
465 let mut w = build(÷r().indent(80.0).end_indent(80.0));
466 let mut lctx = LayoutCtx::new();
467 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(100.0, 50.0)));
468
469 let mut rec = Recorder::default();
470 let mut pctx = PaintCtx::new(Point::ZERO, size);
471 w.paint(&mut pctx, &mut rec);
472
473 assert_eq!(rec.rects[0].1.width, 0.0);
474 }
475}