use frust::authoring::{
BoxConstraints, BuildCtx, ChangeFlags, LayoutCtx, PaintCtx, PaintScene, View, Widget,
};
use frust::{Theme, divider as base_divider};
use kurbo::{Point, Size};
use peniko::Color;
const DEFAULT_THICKNESS: f64 = 1.0;
const FALLBACK_OUTLINE_VARIANT: Color = Color::from_rgb8(0xCA, 0xC4, 0xD0);
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DividerView {
vertical: bool,
thickness: f64,
indent: f64,
end_indent: f64,
color: Option<Color>,
}
pub fn divider() -> DividerView {
DividerView {
vertical: false,
thickness: DEFAULT_THICKNESS,
indent: 0.0,
end_indent: 0.0,
color: None,
}
}
impl DividerView {
pub fn vertical(mut self) -> Self {
self.vertical = true;
self
}
pub fn thickness(mut self, thickness: f64) -> Self {
self.thickness = thickness;
self
}
pub fn indent(mut self, indent: f64) -> Self {
self.indent = indent;
self
}
pub fn end_indent(mut self, end_indent: f64) -> Self {
self.end_indent = end_indent;
self
}
pub fn color(mut self, color: Color) -> Self {
self.color = Some(color);
self
}
}
pub struct DividerWidget {
vertical: bool,
thickness: f64,
indent: f64,
end_indent: f64,
color: Option<Color>,
}
impl<State: 'static> View<State> for DividerView {
type Element = DividerWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> DividerWidget {
DividerWidget {
vertical: self.vertical,
thickness: self.thickness,
indent: self.indent,
end_indent: self.end_indent,
color: self.color,
}
}
fn rebuild(
&self,
prev: &Self,
element: &mut DividerWidget,
_ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
let mut flags = ChangeFlags::NONE;
if prev.color != self.color {
element.color = self.color;
flags |= ChangeFlags::PAINT;
}
if prev.vertical != self.vertical
|| prev.thickness != self.thickness
|| prev.indent != self.indent
|| prev.end_indent != self.end_indent
{
element.vertical = self.vertical;
element.thickness = self.thickness;
element.indent = self.indent;
element.end_indent = self.end_indent;
flags |= ChangeFlags::LAYOUT;
}
flags
}
}
impl DividerWidget {
fn insets(&self) -> (f64, f64) {
(self.indent.max(0.0), self.end_indent.max(0.0))
}
fn base(&self, color: Color) -> frust::DividerView {
let mut view = base_divider(color).thickness(self.thickness);
if self.vertical {
view = view.vertical();
}
view
}
fn resolve_color(&self, theme: Option<&Theme>) -> Color {
if let Some(color) = self.color {
return color;
}
match theme {
Some(theme) => theme.scheme().outline_variant,
None => FALLBACK_OUTLINE_VARIANT,
}
}
}
fn build_line<State: 'static>(view: &frust::DividerView) -> frust::DividerWidget {
let mut counter = 0u64;
View::<State>::build(view, &mut BuildCtx::new(&mut counter))
}
impl Widget for DividerWidget {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
let (before, after) = self.insets();
let total = before + after;
let inner_bc = if self.vertical {
BoxConstraints::new(
Size::new(bc.min().width, (bc.min().height - total).max(0.0)),
Size::new(bc.max().width, (bc.max().height - total).max(0.0)),
)
} else {
BoxConstraints::new(
Size::new((bc.min().width - total).max(0.0), bc.min().height),
Size::new((bc.max().width - total).max(0.0), bc.max().height),
)
};
let view = self.base(FALLBACK_OUTLINE_VARIANT); let mut line = build_line::<()>(&view);
let inner_size = line.layout(ctx, &inner_bc);
let full = if self.vertical {
Size::new(inner_size.width, inner_size.height + total)
} else {
Size::new(inner_size.width + total, inner_size.height)
};
bc.constrain(full)
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
let theme = Theme::from_paint_ctx(ctx);
let color = self.resolve_color(theme);
let (before, after) = self.insets();
let total = before + after;
let (inner_origin, inner_size) = if self.vertical {
(
Point::new(ctx.origin().x, ctx.origin().y + before),
Size::new(ctx.size().width, (ctx.size().height - total).max(0.0)),
)
} else {
(
Point::new(ctx.origin().x + before, ctx.origin().y),
Size::new((ctx.size().width - total).max(0.0), ctx.size().height),
)
};
let view = self.base(color);
let mut line = build_line::<()>(&view);
let mut inner_ctx = PaintCtx::new(inner_origin, inner_size);
line.paint(&mut inner_ctx, scene);
}
}
#[cfg(test)]
mod tests {
use super::*;
use kurbo::Point;
fn build(view: &DividerView) -> DividerWidget {
let mut counter = 0u64;
View::<()>::build(view, &mut BuildCtx::new(&mut counter))
}
fn themed(outline_variant: Color) -> Theme {
let mut theme = Theme::neutral();
theme.light.outline_variant = outline_variant;
theme
}
#[test]
fn horizontal_default_fills_width_and_takes_default_thickness_on_height() {
let mut w = build(÷r());
let mut lctx = LayoutCtx::new();
let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(320.0, 640.0)));
assert_eq!(size, Size::new(320.0, DEFAULT_THICKNESS));
}
#[test]
fn vertical_fills_height_and_takes_thickness_on_width() {
let mut w = build(÷r().vertical());
let mut lctx = LayoutCtx::new();
let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(320.0, 640.0)));
assert_eq!(size, Size::new(DEFAULT_THICKNESS, 640.0));
}
#[test]
fn custom_thickness_is_honored() {
let mut w = build(÷r().thickness(4.0));
let mut lctx = LayoutCtx::new();
let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(200.0, 200.0)));
assert_eq!(size, Size::new(200.0, 4.0));
}
#[test]
fn insets_shrink_the_line_but_the_wrapper_still_reports_the_full_fill_size() {
let mut w = build(÷r().indent(8.0).end_indent(4.0));
let mut lctx = LayoutCtx::new();
let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(100.0, 50.0)));
assert_eq!(size, Size::new(100.0, DEFAULT_THICKNESS));
}
#[test]
fn vertical_insets_apply_to_the_fill_axis_too() {
let mut w = build(÷r().vertical().indent(10.0).end_indent(5.0));
let mut lctx = LayoutCtx::new();
let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(50.0, 200.0)));
assert_eq!(size, Size::new(DEFAULT_THICKNESS, 200.0));
}
#[test]
fn a_tight_constraint_forces_the_divider_to_it_regardless_of_thickness() {
let mut w = build(÷r());
let mut lctx = LayoutCtx::new();
let size = w.layout(&mut lctx, &BoxConstraints::tight(Size::new(50.0, 50.0)));
assert_eq!(size, Size::new(50.0, 50.0));
}
#[derive(Default)]
struct Recorder {
rects: Vec<(Point, Size, Color)>,
}
impl PaintScene for Recorder {
fn fill_rect(&mut self, origin: Point, size: Size, color: Color) {
self.rects.push((origin, size, color));
}
fn draw_text(&mut self, _origin: Point, _text: &str) {}
}
#[test]
fn paint_with_no_theme_uses_the_unthemed_fallback() {
let mut w = build(÷r());
let mut lctx = LayoutCtx::new();
let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(100.0, 100.0)));
let mut rec = Recorder::default();
let mut pctx = PaintCtx::new(Point::ZERO, size);
w.paint(&mut pctx, &mut rec);
assert_eq!(
rec.rects,
vec![(Point::ZERO, size, FALLBACK_OUTLINE_VARIANT)]
);
}
#[test]
fn paint_resolves_the_theme_s_outline_variant_when_no_explicit_color() {
let mut w = build(÷r());
let mut lctx = LayoutCtx::new();
let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(100.0, 100.0)));
let expected = Color::from_rgb8(0x11, 0x22, 0x33);
let theme = themed(expected);
let mut rec = Recorder::default();
let mut pctx = PaintCtx::new(Point::ZERO, size).with_theme(&theme);
w.paint(&mut pctx, &mut rec);
assert_eq!(rec.rects, vec![(Point::ZERO, size, expected)]);
}
#[test]
fn explicit_color_wins_over_the_theme() {
let explicit = Color::from_rgb8(0xAA, 0xBB, 0xCC);
let mut w = build(÷r().color(explicit));
let mut lctx = LayoutCtx::new();
let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(100.0, 100.0)));
let theme = themed(Color::from_rgb8(0x11, 0x22, 0x33));
let mut rec = Recorder::default();
let mut pctx = PaintCtx::new(Point::ZERO, size).with_theme(&theme);
w.paint(&mut pctx, &mut rec);
assert_eq!(rec.rects, vec![(Point::ZERO, size, explicit)]);
}
#[test]
fn horizontal_insets_offset_and_shrink_the_painted_line() {
let mut w = build(÷r().indent(8.0).end_indent(4.0));
let mut lctx = LayoutCtx::new();
let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(100.0, 50.0)));
let mut rec = Recorder::default();
let mut pctx = PaintCtx::new(Point::new(2.0, 3.0), size);
w.paint(&mut pctx, &mut rec);
assert_eq!(
rec.rects,
vec![(
Point::new(2.0 + 8.0, 3.0),
Size::new(100.0 - 8.0 - 4.0, DEFAULT_THICKNESS),
FALLBACK_OUTLINE_VARIANT
)]
);
}
#[test]
fn vertical_insets_offset_and_shrink_the_painted_line() {
let mut w = build(÷r().vertical().indent(10.0).end_indent(5.0));
let mut lctx = LayoutCtx::new();
let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(50.0, 200.0)));
let mut rec = Recorder::default();
let mut pctx = PaintCtx::new(Point::ZERO, size);
w.paint(&mut pctx, &mut rec);
assert_eq!(
rec.rects,
vec![(
Point::new(0.0, 10.0),
Size::new(DEFAULT_THICKNESS, 200.0 - 10.0 - 5.0),
FALLBACK_OUTLINE_VARIANT
)]
);
}
#[test]
fn insets_larger_than_the_line_collapse_to_zero_rather_than_going_negative() {
let mut w = build(÷r().indent(80.0).end_indent(80.0));
let mut lctx = LayoutCtx::new();
let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(100.0, 50.0)));
let mut rec = Recorder::default();
let mut pctx = PaintCtx::new(Point::ZERO, size);
w.paint(&mut pctx, &mut rec);
assert_eq!(rec.rects[0].1.width, 0.0);
}
}