use std::cell::RefCell;
use std::rc::{Rc, Weak};
use dinamika_cpu::Color;
use crate::signal::{Signal, Tweenable};
use crate::timeline::{Timeline, TimelineState};
mod code;
mod layout;
mod text;
mod tween;
pub use code::{Language, Palette};
pub use layout::{Align, Direction, Justify, Length, Padding};
pub use text::{infinite, line, TextAlign, TextPos};
pub use tween::{HighlightEdit, PaddingTween, TextEdit, Tween};
use code::CodeData;
use text::{insert_at, rewrite_range, TextData};
const NOT_TEXT: &str = "this property is only available on a text shape — create it via Shape::text(...)";
const NOT_CODE: &str = "this property is only available on a code shape — create it via Shape::code(...)";
const CODE_HAS_NO_COLOR: &str =
"a code shape has no color — set the highlight palette via .palette(...)";
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ShapeKind {
Rect,
Circle,
Layout,
Text,
Code,
}
#[derive(Copy, Clone)]
enum Axis {
Width,
Height,
}
pub(crate) struct ShapeData {
pub kind: ShapeKind,
pub x: Signal<f32>,
pub y: Signal<f32>,
pub width: Signal<f32>,
pub height: Signal<f32>,
pub min_width: Signal<f32>,
pub max_width: Signal<f32>,
pub min_height: Signal<f32>,
pub max_height: Signal<f32>,
pub width_percent: Option<f32>,
pub height_percent: Option<f32>,
pub background: Signal<Color>,
pub radius: Signal<f32>,
pub opacity: Signal<f32>,
pub rotation: Signal<f32>,
pub scale: Signal<f32>,
pub gap: Signal<f32>,
pub pad_top: Signal<f32>,
pub pad_right: Signal<f32>,
pub pad_bottom: Signal<f32>,
pub pad_left: Signal<f32>,
pub direction: Direction,
pub justify: Justify,
pub align: Align,
pub children: Vec<Shape>,
pub text: Option<TextData>,
pub code: Option<CodeData>,
timeline: RefCell<Weak<TimelineState>>,
}
#[derive(Clone)]
pub struct Shape {
pub(crate) inner: Rc<RefCell<ShapeData>>,
}
impl Shape {
pub fn rect() -> Shape {
Shape::new(ShapeKind::Rect)
}
pub fn circle() -> Shape {
Shape::new(ShapeKind::Circle)
}
pub fn layout() -> Shape {
Shape::new(ShapeKind::Layout)
}
pub fn text(content: impl Into<String>) -> Shape {
let shape = Shape::new(ShapeKind::Text);
{
let mut d = shape.inner.borrow_mut();
d.background.set(Color::TRANSPARENT);
d.text = Some(TextData::new(content.into()));
}
shape
}
pub fn code(content: impl Into<String>) -> Shape {
let shape = Shape::new(ShapeKind::Code);
{
let mut d = shape.inner.borrow_mut();
d.background.set(Color::TRANSPARENT);
d.text = Some(TextData::new(content.into()));
d.code = Some(CodeData::new());
}
shape
}
fn new(kind: ShapeKind) -> Shape {
Shape {
inner: Rc::new(RefCell::new(ShapeData {
kind,
x: Signal::new(0.0),
y: Signal::new(0.0),
width: Signal::new(0.0),
height: Signal::new(0.0),
min_width: Signal::new(0.0),
max_width: Signal::new(0.0),
min_height: Signal::new(0.0),
max_height: Signal::new(0.0),
width_percent: None,
height_percent: None,
background: Signal::new(Color::WHITE),
radius: Signal::new(0.0),
opacity: Signal::new(1.0),
rotation: Signal::new(0.0),
scale: Signal::new(1.0),
gap: Signal::new(0.0),
pad_top: Signal::new(0.0),
pad_right: Signal::new(0.0),
pad_bottom: Signal::new(0.0),
pad_left: Signal::new(0.0),
direction: Direction::Row,
justify: Justify::Start,
align: Align::Start,
children: Vec::new(),
text: None,
code: None,
timeline: RefCell::new(Weak::new()),
})),
}
}
pub fn at(&self, x: f32, y: f32) -> Self {
{
let d = self.inner.borrow();
d.x.set(x);
d.y.set(y);
}
self.clone()
}
pub fn size(&self, w: f32, h: f32) -> Self {
{
let mut d = self.inner.borrow_mut();
d.width.set(w);
d.height.set(h);
d.width_percent = None;
d.height_percent = None;
}
self.clone()
}
pub fn direction(&self, d: Direction) -> Self {
self.inner.borrow_mut().direction = d;
self.clone()
}
pub fn justify(&self, j: Justify) -> Self {
self.inner.borrow_mut().justify = j;
self.clone()
}
pub fn align(&self, a: Align) -> Self {
self.inner.borrow_mut().align = a;
self.clone()
}
pub fn child(&self, c: impl Into<Shape>) -> Self {
self.inner.borrow_mut().children.push(c.into());
self.clone()
}
pub fn children<C: IntoChildren>(&self, cs: C) -> Self {
self.inner.borrow_mut().children.extend(cs.into_children());
self.clone()
}
pub fn on(&self, tl: &Timeline) -> Self {
tl.register_shape(self.clone());
self.bind_timeline(&tl.weak());
self.clone()
}
fn bind_timeline(&self, tl: &Weak<TimelineState>) {
let children = {
let d = self.inner.borrow();
*d.timeline.borrow_mut() = tl.clone();
d.children.clone()
};
for child in &children {
child.bind_timeline(tl);
}
}
pub(crate) fn timeline_weak(&self) -> Weak<TimelineState> {
self.inner.borrow().timeline.borrow().clone()
}
fn set_prop<T: Tweenable>(&self, signal: Signal<T>, value: T) -> Tween<T> {
let from = signal.get();
signal.set(value.clone());
Tween::new(self.clone(), signal, from, value)
}
fn set_length(&self, axis: Axis, value: Length) -> Tween<f32> {
let signal = match axis {
Axis::Width => self.width_signal(),
Axis::Height => self.height_signal(),
};
match value {
Length::Pixel(v) => {
{
let mut d = self.inner.borrow_mut();
match axis {
Axis::Width => d.width_percent = None,
Axis::Height => d.height_percent = None,
}
}
self.set_prop(signal, v)
}
Length::Percent(p) => {
let fraction = p / 100.0;
{
let mut d = self.inner.borrow_mut();
match axis {
Axis::Width => d.width_percent = Some(fraction),
Axis::Height => d.height_percent = Some(fraction),
}
}
let cur = signal.get();
Tween::new(self.clone(), signal, cur, cur)
}
}
}
pub fn x(&self, value: f32) -> Tween<f32> {
self.set_prop(self.x_signal(), value)
}
pub fn y(&self, value: f32) -> Tween<f32> {
self.set_prop(self.y_signal(), value)
}
pub fn width(&self, value: Length) -> Tween<f32> {
self.set_length(Axis::Width, value)
}
pub fn height(&self, value: Length) -> Tween<f32> {
self.set_length(Axis::Height, value)
}
pub fn min_width(&self, value: f32) -> Tween<f32> {
self.set_prop(self.min_width_signal(), value)
}
pub fn max_width(&self, value: f32) -> Tween<f32> {
self.set_prop(self.max_width_signal(), value)
}
pub fn min_height(&self, value: f32) -> Tween<f32> {
self.set_prop(self.min_height_signal(), value)
}
pub fn max_height(&self, value: f32) -> Tween<f32> {
self.set_prop(self.max_height_signal(), value)
}
pub fn background(&self, value: Color) -> Tween<Color> {
self.set_prop(self.background_signal(), value)
}
pub fn radius(&self, value: f32) -> Tween<f32> {
self.set_prop(self.radius_signal(), value)
}
pub fn opacity(&self, value: f32) -> Tween<f32> {
self.set_prop(self.opacity_signal(), value)
}
pub fn rotation(&self, value: f32) -> Tween<f32> {
self.set_prop(self.rotation_signal(), value)
}
pub fn scale(&self, value: f32) -> Tween<f32> {
self.set_prop(self.scale_signal(), value)
}
pub fn gap(&self, value: f32) -> Tween<f32> {
self.set_prop(self.gap_signal(), value)
}
pub fn pad_top(&self, value: f32) -> Tween<f32> {
self.set_prop(self.pad_top_signal(), value)
}
pub fn pad_right(&self, value: f32) -> Tween<f32> {
self.set_prop(self.pad_right_signal(), value)
}
pub fn pad_bottom(&self, value: f32) -> Tween<f32> {
self.set_prop(self.pad_bottom_signal(), value)
}
pub fn pad_left(&self, value: f32) -> Tween<f32> {
self.set_prop(self.pad_left_signal(), value)
}
pub fn padding<P: Into<Padding>>(&self, value: P) -> PaddingTween {
let to: Padding = value.into();
let from = self.current_padding();
self.set_padding(to);
PaddingTween::new(self.clone(), from, to)
}
pub fn content(&self, content: impl Into<String>) -> TextEdit {
let content = content.into();
self.edit_text(|_old| content)
}
pub fn append(&self, content: impl Into<String>) -> TextEdit {
let content = content.into();
self.edit_text(|old| {
let mut out = String::with_capacity(old.len() + content.len());
out.push_str(old);
out.push_str(&content);
out
})
}
pub fn prepend(&self, content: impl Into<String>) -> TextEdit {
let content = content.into();
self.edit_text(|old| {
let mut out = String::with_capacity(old.len() + content.len());
out.push_str(&content);
out.push_str(old);
out
})
}
pub fn insert(&self, char_index: usize, content: impl Into<String>) -> TextEdit {
let content = content.into();
self.edit_text(|old| insert_at(old, char_index, &content))
}
pub fn rewrite(
&self,
from: impl Into<TextPos>,
to: impl Into<TextPos>,
content: impl Into<String>,
) -> TextEdit {
let from = from.into();
let to = to.into();
let content = content.into();
self.edit_text(|old| rewrite_range(old, from, to, &content))
}
fn edit_text(&self, f: impl FnOnce(&str) -> String) -> TextEdit {
let (old, new) = {
let d = self.inner.borrow();
let text = d.text.as_ref().expect(NOT_TEXT);
let old = text.get_text();
let new = f(&old);
text.set_text(new.clone());
(old, new)
};
TextEdit::new(self.clone(), old, new)
}
pub fn font(&self, bytes: impl Into<Rc<Vec<u8>>>) -> Self {
{
let d = self.inner.borrow();
d.text.as_ref().expect(NOT_TEXT).set_font(bytes.into(), 0);
}
self.clone()
}
pub fn font_collection(&self, bytes: impl Into<Rc<Vec<u8>>>, index: u32) -> Self {
{
let d = self.inner.borrow();
d.text.as_ref().expect(NOT_TEXT).set_font(bytes.into(), index);
}
self.clone()
}
pub fn text_align(&self, align: TextAlign) -> Self {
{
let d = self.inner.borrow();
d.text.as_ref().expect(NOT_TEXT).align.set(align);
}
self.clone()
}
pub fn font_size(&self, value: f32) -> Tween<f32> {
self.set_prop(self.font_size_signal(), value)
}
pub fn color(&self, value: Color) -> Tween<Color> {
assert!(self.inner.borrow().kind != ShapeKind::Code, "{CODE_HAS_NO_COLOR}");
self.set_prop(self.color_signal(), value)
}
pub fn palette(&self, palette: Palette) -> Self {
{
let d = self.inner.borrow();
d.code.as_ref().expect(NOT_CODE).set_palette(palette);
}
self.clone()
}
pub fn language(&self, language: Language) -> Self {
{
let d = self.inner.borrow();
d.code.as_ref().expect(NOT_CODE).set_language(language);
}
self.clone()
}
pub fn letter_spacing(&self, value: f32) -> Tween<f32> {
self.set_prop(self.letter_spacing_signal(), value)
}
pub fn line_height(&self, value: f32) -> Tween<f32> {
self.set_prop(self.line_height_signal(), value)
}
pub fn highlight(&self, from: impl Into<TextPos>, to: impl Into<TextPos>) -> HighlightEdit {
let (old, new) = {
let d = self.inner.borrow();
let text = d.text.as_ref().expect(NOT_TEXT);
let old = text.get_highlights();
text.add_highlight(from.into(), to.into());
(old, text.get_highlights())
};
HighlightEdit::new(self.clone(), old, new)
}
pub fn clear_highlight(&self) -> HighlightEdit {
let old = {
let d = self.inner.borrow();
let text = d.text.as_ref().expect(NOT_TEXT);
let old = text.get_highlights();
text.clear_highlights();
old
};
HighlightEdit::new(self.clone(), old, Vec::new())
}
pub fn font_size_signal(&self) -> Signal<f32> {
self.inner.borrow().text.as_ref().expect(NOT_TEXT).size.clone()
}
pub fn color_signal(&self) -> Signal<Color> {
self.inner.borrow().text.as_ref().expect(NOT_TEXT).color.clone()
}
pub fn letter_spacing_signal(&self) -> Signal<f32> {
self.inner.borrow().text.as_ref().expect(NOT_TEXT).letter_spacing.clone()
}
pub fn line_height_signal(&self) -> Signal<f32> {
self.inner.borrow().text.as_ref().expect(NOT_TEXT).line_height.clone()
}
fn current_padding(&self) -> Padding {
let d = self.inner.borrow();
Padding {
top: d.pad_top.get(),
right: d.pad_right.get(),
bottom: d.pad_bottom.get(),
left: d.pad_left.get(),
}
}
fn set_padding(&self, p: Padding) {
let d = self.inner.borrow();
d.pad_top.set(p.top);
d.pad_right.set(p.right);
d.pad_bottom.set(p.bottom);
d.pad_left.set(p.left);
}
pub fn x_signal(&self) -> Signal<f32> {
self.inner.borrow().x.clone()
}
pub fn y_signal(&self) -> Signal<f32> {
self.inner.borrow().y.clone()
}
pub fn width_signal(&self) -> Signal<f32> {
self.inner.borrow().width.clone()
}
pub fn height_signal(&self) -> Signal<f32> {
self.inner.borrow().height.clone()
}
pub fn min_width_signal(&self) -> Signal<f32> {
self.inner.borrow().min_width.clone()
}
pub fn max_width_signal(&self) -> Signal<f32> {
self.inner.borrow().max_width.clone()
}
pub fn min_height_signal(&self) -> Signal<f32> {
self.inner.borrow().min_height.clone()
}
pub fn max_height_signal(&self) -> Signal<f32> {
self.inner.borrow().max_height.clone()
}
pub fn background_signal(&self) -> Signal<Color> {
self.inner.borrow().background.clone()
}
pub fn radius_signal(&self) -> Signal<f32> {
self.inner.borrow().radius.clone()
}
pub fn opacity_signal(&self) -> Signal<f32> {
self.inner.borrow().opacity.clone()
}
pub fn rotation_signal(&self) -> Signal<f32> {
self.inner.borrow().rotation.clone()
}
pub fn scale_signal(&self) -> Signal<f32> {
self.inner.borrow().scale.clone()
}
pub fn gap_signal(&self) -> Signal<f32> {
self.inner.borrow().gap.clone()
}
pub fn pad_top_signal(&self) -> Signal<f32> {
self.inner.borrow().pad_top.clone()
}
pub fn pad_right_signal(&self) -> Signal<f32> {
self.inner.borrow().pad_right.clone()
}
pub fn pad_bottom_signal(&self) -> Signal<f32> {
self.inner.borrow().pad_bottom.clone()
}
pub fn pad_left_signal(&self) -> Signal<f32> {
self.inner.borrow().pad_left.clone()
}
}
pub trait IntoChildren {
fn into_children(self) -> Vec<Shape>;
}
impl IntoChildren for Shape {
fn into_children(self) -> Vec<Shape> {
vec![self]
}
}
impl<T: Tweenable> IntoChildren for Tween<T> {
fn into_children(self) -> Vec<Shape> {
vec![self.into()]
}
}
impl IntoChildren for PaddingTween {
fn into_children(self) -> Vec<Shape> {
vec![self.into()]
}
}
impl IntoChildren for TextEdit {
fn into_children(self) -> Vec<Shape> {
vec![self.into()]
}
}
impl IntoChildren for HighlightEdit {
fn into_children(self) -> Vec<Shape> {
vec![self.into()]
}
}
impl<I> IntoChildren for I
where
I: IntoIterator,
I::Item: Into<Shape>,
{
fn into_children(self) -> Vec<Shape> {
self.into_iter().map(Into::into).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::easing::Easing;
use crate::timeline::Timeline;
#[test]
fn builder_sets_initial_values() {
let s = Shape::rect().at(10.0, 20.0).size(100.0, 50.0).radius(8.0);
assert_eq!(s.x_signal().get(), 10.0);
assert_eq!(s.y_signal().get(), 20.0);
assert_eq!(s.width_signal().get(), 100.0);
assert_eq!(s.height_signal().get(), 50.0);
assert_eq!(s.radius_signal().get(), 8.0);
}
#[test]
fn property_setter_sets_and_returns_handle() {
let s = Shape::rect();
let same = s.gap(12.0);
assert_eq!(s.gap_signal().get(), 12.0);
same.gap(34.0);
assert_eq!(s.gap_signal().get(), 34.0);
}
#[test]
fn over_builds_tween_from_previous_value() {
let tl = Timeline::new(64, 64, Color::BLACK, 30.0);
let s = Shape::rect().gap(20.0).on(&tl);
tl.parallel(vec![s.gap(56.0).over(1.0, Easing::Linear)]);
tl.seek(0.0);
assert!((s.gap_signal().get() - 20.0).abs() < 1e-3, "got {}", s.gap_signal().get());
tl.seek(0.5);
assert!((s.gap_signal().get() - 38.0).abs() < 1e-3, "got {}", s.gap_signal().get());
tl.seek(1.0);
assert!((s.gap_signal().get() - 56.0).abs() < 1e-3, "got {}", s.gap_signal().get());
}
#[test]
fn padding_shorthands() {
let uniform = Shape::rect().padding(8.0);
assert_eq!(uniform.pad_top_signal().get(), 8.0);
assert_eq!(uniform.pad_right_signal().get(), 8.0);
assert_eq!(uniform.pad_bottom_signal().get(), 8.0);
assert_eq!(uniform.pad_left_signal().get(), 8.0);
let vh = Shape::rect().padding((10.0, 20.0));
assert_eq!(vh.pad_top_signal().get(), 10.0);
assert_eq!(vh.pad_bottom_signal().get(), 10.0);
assert_eq!(vh.pad_left_signal().get(), 20.0);
assert_eq!(vh.pad_right_signal().get(), 20.0);
let trbl = Shape::rect().padding((1.0, 2.0, 3.0, 4.0));
assert_eq!(trbl.pad_top_signal().get(), 1.0);
assert_eq!(trbl.pad_right_signal().get(), 2.0);
assert_eq!(trbl.pad_bottom_signal().get(), 3.0);
assert_eq!(trbl.pad_left_signal().get(), 4.0);
}
#[test]
fn padding_over_animates_all_sides() {
let tl = Timeline::new(64, 64, Color::BLACK, 30.0);
let s = Shape::rect().padding(10.0).on(&tl);
tl.parallel(vec![s.padding(20.0).over(1.0, Easing::Linear)]);
tl.seek(0.5);
assert!((s.pad_top_signal().get() - 15.0).abs() < 1e-3);
assert!((s.pad_left_signal().get() - 15.0).abs() < 1e-3);
tl.seek(1.0);
assert!((s.pad_bottom_signal().get() - 20.0).abs() < 1e-3);
}
#[test]
fn scale_defaults_to_one_and_setter_returns_handle() {
let s = Shape::rect();
assert_eq!(s.scale_signal().get(), 1.0);
let same = s.scale(2.0);
assert_eq!(s.scale_signal().get(), 2.0);
same.scale(0.5);
assert_eq!(s.scale_signal().get(), 0.5);
}
#[test]
fn scale_over_animates_from_previous_value() {
let tl = Timeline::new(64, 64, Color::BLACK, 30.0);
let s = Shape::rect().scale(1.0).on(&tl);
tl.parallel(vec![s.scale(2.0).over(1.0, Easing::Linear)]);
tl.seek(0.0);
assert!((s.scale_signal().get() - 1.0).abs() < 1e-3);
tl.seek(0.5);
assert!((s.scale_signal().get() - 1.5).abs() < 1e-3);
tl.seek(1.0);
assert!((s.scale_signal().get() - 2.0).abs() < 1e-3);
}
#[test]
fn min_max_setters_set_and_return_handles() {
let s = Shape::rect()
.min_width(10.0)
.max_width(200.0)
.min_height(20.0)
.max_height(300.0);
assert_eq!(s.min_width_signal().get(), 10.0);
assert_eq!(s.max_width_signal().get(), 200.0);
assert_eq!(s.min_height_signal().get(), 20.0);
assert_eq!(s.max_height_signal().get(), 300.0);
}
#[test]
fn min_max_default_to_unbounded() {
let s = Shape::rect();
assert_eq!(s.min_width_signal().get(), 0.0);
assert_eq!(s.max_width_signal().get(), 0.0);
assert_eq!(s.min_height_signal().get(), 0.0);
assert_eq!(s.max_height_signal().get(), 0.0);
}
#[test]
fn max_width_animates_like_any_property() {
let tl = Timeline::new(64, 64, Color::BLACK, 30.0);
let s = Shape::rect().max_width(100.0).on(&tl);
tl.parallel(vec![s.max_width(200.0).over(1.0, Easing::Linear)]);
tl.seek(0.0);
assert!((s.max_width_signal().get() - 100.0).abs() < 1e-3);
tl.seek(0.5);
assert!((s.max_width_signal().get() - 150.0).abs() < 1e-3);
tl.seek(1.0);
assert!((s.max_width_signal().get() - 200.0).abs() < 1e-3);
}
#[test]
fn percent_length_stores_fraction() {
let s = Shape::rect()
.width(Length::percent(100.0))
.height(Length::percent(50.0));
assert_eq!(s.inner.borrow().width_percent, Some(1.0));
assert_eq!(s.inner.borrow().height_percent, Some(0.5));
let plain = Shape::rect();
assert_eq!(plain.inner.borrow().width_percent, None);
assert_eq!(plain.inner.borrow().height_percent, None);
}
#[test]
fn pixel_length_sets_signal_and_clears_percent() {
let s = Shape::rect()
.width(Length::percent(50.0))
.width(Length::pixel(120.0));
assert_eq!(s.width_signal().get(), 120.0);
assert_eq!(s.inner.borrow().width_percent, None);
}
#[test]
fn size_clears_percent() {
let s = Shape::rect()
.width(Length::percent(50.0))
.height(Length::percent(50.0))
.size(40.0, 30.0);
assert_eq!(s.inner.borrow().width_percent, None);
assert_eq!(s.inner.borrow().height_percent, None);
}
#[test]
fn accessor_shares_signal_with_shape() {
let s = Shape::rect();
let x = s.x_signal();
x.set(123.0);
assert_eq!(s.x_signal().get(), 123.0);
}
#[test]
fn circle_has_circle_kind_and_shared_defaults() {
let c = Shape::circle().size(64.0, 64.0).background(Color::from_rgba8(1, 2, 3, 255));
assert_eq!(c.inner.borrow().kind, ShapeKind::Circle);
assert_eq!(c.width_signal().get(), 64.0);
assert_eq!(c.height_signal().get(), 64.0);
assert_eq!(Shape::rect().inner.borrow().kind, ShapeKind::Rect);
}
#[test]
fn layout_has_layout_kind_and_shared_defaults() {
let l = Shape::layout()
.direction(Direction::Column)
.gap(8.0)
.child(Shape::rect().size(40.0, 40.0))
.child(Shape::rect().size(40.0, 40.0));
assert_eq!(l.inner.borrow().kind, ShapeKind::Layout);
assert_eq!(l.gap_signal().get(), 8.0);
assert_eq!(l.inner.borrow().children.len(), 2);
}
#[test]
fn text_has_text_kind_and_css_defaults() {
let t = Shape::text("Hello");
assert_eq!(t.inner.borrow().kind, ShapeKind::Text);
assert_eq!(t.background_signal().get(), Color::TRANSPARENT);
assert_eq!(t.font_size_signal().get(), 32.0);
assert_eq!(t.color_signal().get(), Color::BLACK);
assert_eq!(t.letter_spacing_signal().get(), 0.0);
assert_eq!(t.line_height_signal().get(), 1.0);
}
#[test]
fn text_setters_set_and_return_handles() {
let t = Shape::text("Hi")
.font_size(48.0)
.color(Color::from_rgba8(10, 20, 30, 255))
.letter_spacing(2.0)
.line_height(1.5)
.text_align(TextAlign::Center)
.content("Bye");
assert_eq!(t.font_size_signal().get(), 48.0);
assert_eq!(t.color_signal().get(), Color::from_rgba8(10, 20, 30, 255));
assert_eq!(t.letter_spacing_signal().get(), 2.0);
assert_eq!(t.line_height_signal().get(), 1.5);
}
#[test]
fn text_size_animates_like_any_property() {
let tl = Timeline::new(64, 64, Color::BLACK, 30.0);
let t = Shape::text("x").font_size(20.0).on(&tl);
tl.parallel(vec![t.font_size(40.0).over(1.0, Easing::Linear)]);
tl.seek(0.0);
assert!((t.font_size_signal().get() - 20.0).abs() < 1e-3);
tl.seek(0.5);
assert!((t.font_size_signal().get() - 30.0).abs() < 1e-3);
tl.seek(1.0);
assert!((t.font_size_signal().get() - 40.0).abs() < 1e-3);
}
#[test]
#[should_panic(expected = "text shape")]
fn text_methods_panic_on_non_text_shape() {
Shape::rect().font_size(10.0);
}
#[test]
fn highlight_commits_ranges() {
let t = Shape::text("abcdef");
t.highlight(0, 2);
let same = t.highlight(4, infinite()).font_size(40.0);
assert_eq!(same.font_size_signal().get(), 40.0);
let committed = t.inner.borrow().text.as_ref().unwrap().get_highlights();
assert_eq!(committed.len(), 2);
t.clear_highlight();
assert_eq!(t.inner.borrow().text.as_ref().unwrap().get_highlights().len(), 0);
}
#[test]
fn parallel_highlights_merge_to_common_morph() {
let tl = Timeline::new(64, 64, Color::BLACK, 30.0);
let code = Shape::text("abcdef").on(&tl);
let stage = code.inner.borrow().text.as_ref().unwrap().highlight_stage_handle();
tl.parallel(vec![
code.highlight(0, 2).over(0.5, Easing::Linear),
code.highlight(4, 6).over(0.5, Easing::Linear),
]);
assert_eq!(code.inner.borrow().text.as_ref().unwrap().get_highlights().len(), 2);
tl.seek(0.0);
match &*stage.borrow() {
text::HighlightStage::Morph { from, to, p } => {
assert!(from.is_empty(), "group base — no highlighting, from={from:?}");
assert_eq!(to.len(), 2, "group final — both ranges");
assert!((*p - 0.0).abs() < 1e-3, "p={p}");
}
other => panic!("expected Morph, got {other:?}"),
}
tl.seek(0.5);
match &*stage.borrow() {
text::HighlightStage::Morph { from, to, p } => {
assert!(from.is_empty(), "from={from:?}");
assert_eq!(to.len(), 2);
assert!((*p - 1.0).abs() < 1e-3, "p={p}");
}
other => panic!("{other:?}"),
};
}
#[test]
fn later_smooth_edit_does_not_suppress_earlier_highlight() {
let tl = Timeline::new(64, 64, Color::BLACK, 30.0);
let code = Shape::text("abcdef").on(&tl);
let stage = code.inner.borrow().text.as_ref().unwrap().stage_handle();
tl.pause(1.0);
tl.parallel(vec![code.highlight(0, 2).over(0.5, Easing::Linear)]);
tl.pause(1.0);
tl.parallel(vec![code.append("XYZ").smooth(0.5, Easing::Linear)]);
tl.seek(1.25);
assert!(
!matches!(&*stage.borrow(), text::TextStage::Crossfade { .. }),
"within the highlight window the text stage must not be Crossfade: {:?}",
&*stage.borrow()
);
}
#[test]
fn highlight_over_drives_stage_on_timeline() {
let tl = Timeline::new(64, 64, Color::BLACK, 30.0);
let code = Shape::text("abcd").on(&tl);
let stage = code.inner.borrow().text.as_ref().unwrap().highlight_stage_handle();
tl.pause(1.0);
tl.parallel(vec![code.highlight(0, 2).over(0.5, Easing::Linear)]);
assert_eq!(code.inner.borrow().text.as_ref().unwrap().get_highlights().len(), 1);
tl.seek(0.5);
match &*stage.borrow() {
text::HighlightStage::Morph { from, to, p } => {
assert!(from.is_empty(), "before the start there should be no highlight");
assert_eq!(to.len(), 1);
assert!((*p - 0.0).abs() < 1e-3, "p={p}");
}
other => panic!("expected Morph, got {other:?}"),
}
tl.seek(1.25);
match &*stage.borrow() {
text::HighlightStage::Morph { p, .. } => assert!((*p - 0.5).abs() < 1e-3, "p={p}"),
other => panic!("{other:?}"),
}
tl.seek(1.5);
match &*stage.borrow() {
text::HighlightStage::Morph { to, p, .. } => {
assert_eq!(to.len(), 1);
assert!((*p - 1.0).abs() < 1e-3, "p={p}");
}
other => panic!("{other:?}"),
};
}
#[test]
#[should_panic(expected = "text shape")]
fn highlight_panics_on_non_text_shape() {
Shape::rect().highlight(0, 1);
}
#[test]
fn code_shares_text_defaults_and_kind() {
let c = Shape::code("fn main() {}");
assert_eq!(c.inner.borrow().kind, ShapeKind::Code);
assert_eq!(c.background_signal().get(), Color::TRANSPARENT);
assert_eq!(c.font_size_signal().get(), 32.0);
assert!(c.inner.borrow().text.is_some());
assert!(c.inner.borrow().code.is_some());
}
#[test]
fn code_uses_text_edit_methods_like_text() {
let c = Shape::code("a").font_size(48.0).append("b").content("c\nd");
assert_eq!(c.font_size_signal().get(), 48.0);
assert_eq!(committed(&c), "c\nd");
}
#[test]
fn code_palette_and_language_setters_return_shape() {
let c = Shape::code("let x = 1;")
.language(Language::Rust)
.palette(Palette::new(Color::WHITE).keyword(Color::from_rgba8(1, 2, 3, 255)))
.font_size(20.0);
assert_eq!(c.inner.borrow().kind, ShapeKind::Code);
assert_eq!(c.font_size_signal().get(), 20.0);
}
#[test]
#[should_panic(expected = "palette")]
fn code_color_panics() {
Shape::code("x").color(Color::BLACK);
}
#[test]
#[should_panic(expected = "code shape")]
fn palette_panics_on_non_code_shape() {
Shape::text("x").palette(Palette::default());
}
fn committed(s: &Shape) -> String {
s.inner.borrow().text.as_ref().unwrap().get_text()
}
#[test]
fn edit_methods_update_committed_text() {
let t = Shape::text("Hello");
t.append(" world");
assert_eq!(committed(&t), "Hello world");
t.prepend(">> ");
assert_eq!(committed(&t), ">> Hello world");
t.content("abc\ndef");
assert_eq!(committed(&t), "abc\ndef");
t.insert(3, "X");
assert_eq!(committed(&t), "abcX\ndef");
t.rewrite(0, line(1), "Z");
assert_eq!(committed(&t), "Zdef");
}
#[test]
fn rewrite_supports_char_line_and_infinite_bounds() {
let t = Shape::text("foo\nbar\nbaz");
t.rewrite(line(1), infinite(), "X");
assert_eq!(committed(&t), "foo\nX");
let u = Shape::text("abcdef");
u.rewrite(1, 4, "_");
assert_eq!(committed(&u), "a_ef");
}
#[test]
fn text_edit_handle_derefs_to_shape() {
let t = Shape::text("x").content("y").font_size(40.0).letter_spacing(1.0);
assert_eq!(t.font_size_signal().get(), 40.0);
assert_eq!(committed(&t), "y");
}
#[test]
fn typing_reveals_progressively() {
let tl = Timeline::new(64, 64, Color::BLACK, 30.0);
let t = Shape::text("").on(&tl);
let stage = t.inner.borrow().text.as_ref().unwrap().stage_handle();
tl.parallel(vec![t.content("abcd").typing(1.0, Easing::Linear)]);
tl.seek(0.0);
match &stage.borrow().clone() {
text::TextStage::Typing { text, visible } => {
assert_eq!(text, "abcd");
assert!((*visible - 0.0).abs() < 1e-3, "visible={visible}");
}
other => panic!("expected Typing, got {other:?}"),
}
tl.seek(0.5);
match &stage.borrow().clone() {
text::TextStage::Typing { visible, .. } => {
assert!((*visible - 2.0).abs() < 1e-3, "visible={visible}")
}
other => panic!("{other:?}"),
}
tl.seek(1.0);
match &stage.borrow().clone() {
text::TextStage::Typing { visible, .. } => {
assert!((*visible - 4.0).abs() < 1e-3, "visible={visible}")
}
other => panic!("{other:?}"),
};
}
#[test]
fn typing_keeps_common_prefix() {
let tl = Timeline::new(64, 64, Color::BLACK, 30.0);
let t = Shape::text("Hello").on(&tl);
let stage = t.inner.borrow().text.as_ref().unwrap().stage_handle();
tl.parallel(vec![t.append(" world").typing(1.0, Easing::Linear)]);
tl.seek(0.0);
match &stage.borrow().clone() {
text::TextStage::Typing { text, visible } => {
assert_eq!(text, "Hello world");
assert!((*visible - 5.0).abs() < 1e-3, "visible={visible}");
}
other => panic!("{other:?}"),
};
}
#[test]
fn smooth_drives_crossfade_progress() {
let tl = Timeline::new(64, 64, Color::BLACK, 30.0);
let t = Shape::text("old").on(&tl);
let stage = t.inner.borrow().text.as_ref().unwrap().stage_handle();
let progress = |from_want: &str, to_want: &str| match &stage.borrow().clone() {
text::TextStage::Crossfade { from, to, p } => {
assert_eq!(from, from_want);
assert_eq!(to, to_want);
*p
}
other => panic!("expected Crossfade, got {other:?}"),
};
tl.parallel(vec![t.content("new").smooth(1.0, Easing::Linear)]);
tl.seek(0.0);
assert!((progress("old", "new") - 0.0).abs() < 1e-3);
tl.seek(0.25);
assert!((progress("old", "new") - 0.25).abs() < 1e-3);
tl.seek(0.5);
assert!((progress("old", "new") - 0.5).abs() < 1e-3);
tl.seek(1.0);
assert!((progress("old", "new") - 1.0).abs() < 1e-3);
}
#[test]
fn parallel_text_edits_merge_into_one_morph() {
let tl = Timeline::new(64, 64, Color::BLACK, 30.0);
let code = Shape::text("X").on(&tl);
let stage = code.inner.borrow().text.as_ref().unwrap().stage_handle();
tl.pause(1.0);
tl.parallel(vec![
code.prepend("{").smooth(0.5, Easing::Linear),
code.append("}").smooth(0.5, Easing::Linear),
]);
assert_eq!(committed(&code), "{X}");
tl.seek(0.5);
match &stage.borrow().clone() {
text::TextStage::Shown(s) => assert_eq!(s, "X", "before the start there should be no braces"),
other => panic!("expected Shown(\"X\"), got {other:?}"),
}
tl.seek(1.25);
match &stage.borrow().clone() {
text::TextStage::Crossfade { from, to, p } => {
assert_eq!(from, "X");
assert_eq!(to, "{X}");
assert!((*p - 0.5).abs() < 1e-3, "p={p}");
}
other => panic!("{other:?}"),
};
}
#[test]
fn parallel_text_merge_is_order_independent() {
let tl = Timeline::new(64, 64, Color::BLACK, 30.0);
let code = Shape::text("X").on(&tl);
let stage = code.inner.borrow().text.as_ref().unwrap().stage_handle();
let app = code.append("}").smooth(0.5, Easing::Linear);
let pre = code.prepend("{").smooth(0.5, Easing::Linear);
tl.parallel(vec![pre, app]);
tl.seek(0.0);
match &stage.borrow().clone() {
text::TextStage::Crossfade { from, to, .. } => {
assert_eq!(from, "X");
assert_eq!(to, "{X}");
}
other => panic!("{other:?}"),
};
}
#[test]
fn spawn_swaps_instantly_at_its_moment() {
let tl = Timeline::new(64, 64, Color::BLACK, 30.0);
let t = Shape::text("old").on(&tl);
let stage = t.inner.borrow().text.as_ref().unwrap().stage_handle();
tl.pause(1.0);
tl.sequence(vec![t.content("new").spawn()]);
tl.seek(0.5); match &stage.borrow().clone() {
text::TextStage::Shown(s) => assert_eq!(s, "old"),
other => panic!("{other:?}"),
}
tl.seek(1.0); match &stage.borrow().clone() {
text::TextStage::Shown(s) => assert_eq!(s, "new"),
other => panic!("{other:?}"),
};
}
#[test]
fn later_text_block_does_not_leak_before_earlier_one() {
let tl = Timeline::new(64, 64, Color::BLACK, 30.0);
let code = Shape::text("println!();").on(&tl);
let stage = code.inner.borrow().text.as_ref().unwrap().stage_handle();
tl.pause(1.0);
tl.parallel(vec![
code.prepend("{\n ").smooth(0.5, Easing::Linear),
code.append("\n}").smooth(0.5, Easing::Linear),
]);
tl.pause(1.0);
tl.parallel(vec![
code.insert(2, " let x = 1;\n").smooth(0.5, Easing::Linear),
code.rewrite(0, 1, "(").smooth(0.5, Easing::Linear),
]);
tl.seek(0.5);
match &stage.borrow().clone() {
text::TextStage::Shown(s) => {
assert_eq!(s, "println!();", "before the start there should be no edits");
}
other => panic!("expected Shown of the original text, got {other:?}"),
}
tl.seek(2.0);
match &stage.borrow().clone() {
text::TextStage::Crossfade { from, to, p } => {
assert_eq!(from, "println!();");
assert_eq!(to, "{\n println!();\n}");
assert!((*p - 1.0).abs() < 1e-3, "p={p}");
assert!(!to.contains("let x"), "the second block's insert leaked: {to:?}");
}
other => panic!("{other:?}"),
};
}
#[test]
fn children_are_stored() {
let s = Shape::rect()
.child(Shape::rect())
.children(vec![Shape::rect(), Shape::rect()]);
assert_eq!(s.inner.borrow().children.len(), 3);
}
#[test]
fn children_accepts_single_shape() {
let group = Shape::rect().children(vec![Shape::circle(), Shape::circle()]);
let outer = Shape::rect().children(group);
assert_eq!(outer.inner.borrow().children.len(), 1);
assert_eq!(outer.inner.borrow().children[0].inner.borrow().children.len(), 2);
}
#[test]
fn shapes_nest_arbitrarily_deep() {
let depth = 64;
let mut node = Shape::rect().size(8.0, 8.0);
for _ in 0..depth {
node = Shape::rect().children(node);
}
let mut levels = 0;
let mut cur = node;
loop {
let next = {
let d = cur.inner.borrow();
d.children.first().cloned()
};
match next {
Some(child) => {
levels += 1;
cur = child;
}
None => break,
}
}
assert_eq!(levels, depth);
}
}