All nodes

A reference of every node and its properties. If you followed the guide, you already know the pattern; this page is for looking things up.

Builders set initial values; view.add(builder) (or node.add for children) returns a Copy handle whose methods of the same names return signals.

NodeBuilderSignals
Rect.width .height .size .radiuswidth height radius
Circle.size .start_angle .end_angle .piesize start_angle end_angle — arcs, pie slices
Polygon.sides .sizesides size — sides animate
Star.points .size .innerpoints size inner
Path.data(BezPath) .svg("M0 0 …")data — morphs
Line.points .smooth .closed .start_arrow .end_arrowpoints arrow_size
Txt.text .rich([span(..)]) .font_size .weight .family .italic .align .max_widthtext spans font_size weight, morph_to
Code.code .language .theme .focus(a, b) .dimcode font_size focus dim, focus_lines
Img.src(include_bytes!(..)) .file(path) .width .heightwidth height
Layout.row .column .wrap .grid(n) .gap .padding .align .justify .width .heightgap padding width height radius
Grid.width .height .spacingwidth height spacing
Group——

Every node has x y rotation scale opacity fill stroke line_width start end, the paint and compositing properties described in Effects, and the flex item properties grow shrink basis margin align_self. start/end trim outlines — animate end from 0 to 1 to draw a shape on (on text it reveals glyph by glyph).

Shapes and morphing

Tweening Path::data between any two paths aligns them first (segment counts, start points, winding), so shapes morph without twisting.

Loading live preview…

Source of morph — edit in the playground
//! Morphing between arbitrary paths and drawing outlines on with `end`.

use oxframe::prelude::*;

const STAR: &str =
    "M0 -150 L44 -46 L150 -46 L64 18 L96 130 L0 64 L-96 130 L-64 18 L-150 -46 L-44 -46 Z";
const HEART: &str = "M0 140 C-60 80 -160 30 -160 -50 C-160 -110 -110 -150 -60 -150 C-30 -150 -10 -130 0 -110 \
                     C10 -130 30 -150 60 -150 C110 -150 160 -110 160 -50 C160 30 60 80 0 140 Z";

fn circle(r: f64) -> BezPath {
    use oxframe::core::kurbo::Shape;
    oxframe::core::kurbo::Circle::new((0.0, 0.0), r).to_path(0.1)
}

fn svg(d: &str) -> BezPath {
    BezPath::from_svg(d).expect("valid path")
}

#[oxframe::scene]
pub async fn morph(view: View) {
    // Draw the outline on, then fill it.
    let shape = view.add(
        Path::new()
            .svg(STAR)
            .fill(Color::TRANSPARENT)
            .stroke(color!("#f5c242"))
            .line_width(8.0)
            .end(0.0),
    );
    shape.end().to(1.0, 1.2).await;
    shape.fill().to(color!("#f5c242"), 0.5).await;

    // Star → circle → heart → star: alignment picks start points and
    // directions so the shapes do not twist.
    shape.data().to(circle(140.0), 1.0).await;
    all((
        shape.data().to(svg(HEART), 1.0),
        shape.fill().to(color!("#e13238"), 1.0),
        shape.stroke().to(color!("#e13238"), 1.0),
    ))
    .await;
    wait_for(0.3).await;
    all((
        shape.data().to(svg(STAR), 1.0).ease(presets::OUT_BACK),
        shape.rotation().to(144.0, 1.0),
        shape.fill().to(color!("#68abdf"), 1.0),
        shape.stroke().to(color!("#68abdf"), 1.0),
    ))
    .await;
    // Erase from the start.
    shape.fill().to(Color::TRANSPARENT, 0.4).await;
    shape.start().to(1.0, 0.8).await;
}

pub fn project() -> Project {
    Project::new("morph").scene(morph())
}

Loading live preview…

Source of shapes — edit in the playground
//! Shapes and layout: polygons, stars, arcs, smooth curves and a flexbox
//! row whose items grow and wrap.

use oxframe::prelude::*;

#[oxframe::scene]
pub async fn shapes(view: View) {
    let poly = view.add(
        Polygon::new()
            .sides(3.0)
            .size(220.0)
            .position((-600.0, -250.0))
            .fill(color!("#68abdf")),
    );
    let star = view.add(
        Star::new()
            .points(5.0)
            .size(240.0)
            .position((-250.0, -250.0))
            .fill(color!("#f5c242")),
    );
    let ring = view.add(
        Circle::new()
            .size(220.0)
            .position((100.0, -250.0))
            .end_angle(0.0)
            .fill(Color::TRANSPARENT)
            .stroke(color!("#e13238"))
            .line_width(18.0)
            .line_cap(LineCap::Round),
    );
    let pie = view.add(
        Circle::new()
            .size(220.0)
            .position((450.0, -250.0))
            .pie(true)
            .end_angle(0.0)
            .fill(color!("#2ecc71")),
    );
    let wave = view.add(
        Line::new()
            .points([
                (-700.0, 60.0),
                (-450.0, -40.0),
                (-200.0, 120.0),
                (50.0, 0.0),
                (300.0, 110.0),
                (600.0, 20.0),
            ])
            .smooth(true)
            .stroke(color!("#68abdf"))
            .line_width(8.0)
            .end(0.0),
    );
    all((
        poly.sides().to(8.0, 2.0),
        star.points().to(9.0, 2.0),
        star.inner().to(0.7, 2.0),
        ring.end_angle().to(300.0, 2.0),
        pie.end_angle().to(290.0, 2.0),
        wave.end().to(1.0, 2.0),
    ))
    .await;

    // Flexbox: the middle card grows, the row wraps when it gets narrow.
    let row = view.add(
        Layout::new()
            .width(1200.0)
            .gap(20.0)
            .padding(20.0)
            .radius(24.0)
            .fill(color!("#26282e"))
            .wrap(true)
            .y(300.0),
    );
    let colors = ["#e13238", "#f5c242", "#68abdf", "#2ecc71", "#9b59b6"];
    let mut cards = Vec::new();
    for (i, c) in colors.iter().enumerate() {
        let card = row.add(
            Rect::new()
                .width(160.0)
                .height(100.0)
                .radius(16.0)
                .fill(Color::from_hex(c).unwrap()),
        );
        if i == 1 {
            card.grow().set(1.0);
        }
        cards.push(card);
    }
    wait_for(0.6).await;
    row.width().to(Some(600.0), 1.5).await;
    wait_for(0.6).await;
    row.width().to(Some(1200.0), 1.5).await;
    wait_for(0.6).await;
}

pub fn project() -> Project {
    Project::new("shapes").scene(shapes())
}

Layout

Layout is real flexbox (and CSS grid with .grid(columns)), computed by taffy. Children take .grow, .shrink, .basis, .margin and .align_self; LayoutAlign::Stretch resizes rects, images and nested layouts. Layout properties are signals: animate the width or the gap and everything re-flows.

Loading live preview…

Source of layout — edit in the playground
//! Layout: rows and columns size themselves to content; animating a
//! child's size re-flows its siblings.

use oxframe::prelude::*;

#[oxframe::scene]
pub async fn layout(view: View) {
    let colors = [
        color!("#e13238"),
        color!("#f5c242"),
        color!("#68abdf"),
        color!("#2ecc71"),
    ];
    let row = view.add(
        Layout::new()
            .gap(24.0)
            .padding(24.0)
            .radius(24.0)
            .fill(color!("#262626")),
    );
    let mut cards = Vec::new();
    for (i, c) in colors.into_iter().enumerate() {
        let card = row.add(
            Layout::new()
                .column()
                .gap(12.0)
                .padding(20.0)
                .radius(16.0)
                .fill(c)
                .opacity(0.0)
                .child(
                    Txt::new()
                        .text(format!("{}", i + 1))
                        .font_size(64.0)
                        .weight(800.0),
                )
                .child(Txt::new().text("card").font_size(28.0)),
        );
        cards.push(card);
    }
    // Staggered fade-in.
    sequence(
        0.15,
        cards
            .iter()
            .map(|c| -> oxframe::BoxFuture { Box::pin(c.opacity().to(1.0, 0.5)) })
            .collect(),
    )
    .await;

    // Growing the gap and one card's padding pushes the others aside.
    all((row.gap().to(60.0, 0.8), cards[1].padding().to(60.0, 0.8))).await;
    wait_for(0.4).await;
    all((row.gap().to(24.0, 0.8), cards[1].padding().to(20.0, 0.8))).await;

    // Removing a card re-flows the row.
    cards[2].opacity().to(0.0, 0.4).await;
    cards[2].remove();
    wait_for(0.6).await;
}

pub fn project() -> Project {
    Project::new("layout").scene(layout())
}

Text

Inter and JetBrains Mono are embedded (OFL), so text renders identically everywhere. Strings tween like a typewriter.

Loading live preview…

Source of text — edit in the playground
//! Text: typewriter effect, animated weight and size, glyph-by-glyph reveal.

use oxframe::prelude::*;

#[oxframe::scene]
pub async fn text(view: View) {
    let title = view.add(Txt::new().text("").font_size(96.0).weight(300.0).y(-120.0));
    // Strings tween like a typewriter.
    title
        .text()
        .to("Hello, oxframe".into(), 1.2)
        .ease(presets::LINEAR)
        .await;
    all((
        title.weight().to(800.0, 0.8),
        title.font_size().to(120.0, 0.8),
    ))
    .await;
    // Retype: deletes back to the common prefix, then types the rest.
    title
        .text()
        .to("Hello, Rust".into(), 1.0)
        .ease(presets::LINEAR)
        .await;

    let code = view.add(
        Txt::new()
            .text("async fn intro(view: View) { .. }")
            .family(MONO)
            .font_size(44.0)
            .fill(color!("#68abdf"))
            .y(60.0)
            .end(0.0),
    );
    // `end` reveals glyph by glyph.
    code.end().to(1.0, 1.5).ease(presets::LINEAR).await;

    let note = view.add(
        Txt::new()
            .text("Кириллица тоже работает — встроенный Inter")
            .font_size(40.0)
            .fill(color!("#f5c242"))
            .y(180.0)
            .opacity(0.0),
    );
    note.opacity().to(1.0, 0.6).await;
    wait_for(1.0).await;
}

pub fn project() -> Project {
    Project::new("text").scene(text())
}

Rich text, code and morphing

Txt::new().rich([span("Signals are "), span("lazy").bold().color(color!("#f5c242"))])
Code::new().language("rust").code(src).focus(3, 4)   // code.focus_lines(5, 5, 0.4).await moves it
word.morph_to("frame", 1.0).await                    // shared letters travel, the others fade

Highlighting covers Rust, JS/TS, Python, JSON, TOML and shell, with One Dark and One Light themes.

Loading live preview…

Source of typography — edit in the playground
//! Text beyond plain strings: glyph morphing between words, rich spans and
//! code with syntax highlighting and a moving focus.

use oxframe::prelude::*;

#[oxframe::scene]
pub async fn typography(view: View) {
    // Letters shared by both words travel; the rest fade.
    let word = view.add(
        Txt::new()
            .text("oxframe")
            .font_size(140.0)
            .weight(800.0)
            .y(-300.0),
    );
    wait_for(0.4).await;
    word.morph_to("frame", 1.0).await;
    word.morph_to("flame", 1.0).await;

    let line = view.add(
        Txt::new()
            .y(-150.0)
            .font_size(44.0)
            .rich([
                span("Signals are "),
                span("lazy").bold().color(color!("#f5c242")),
                span(", tweens are "),
                span("futures").italic().color(color!("#68abdf")),
            ])
            .end(0.0),
    );
    line.end().to(1.0, 1.0).ease(presets::LINEAR).await;

    let code = view.add(
        Code::new()
            .y(150.0)
            .font_size(30.0)
            .focus(1, 1)
            .code(
                "use oxframe::prelude::*;\n\
                 \n\
                 #[oxframe::scene]\n\
                 async fn intro(view: View) {\n\
                 \x20   let r = view.add(Rect::new().size(200.0));\n\
                 \x20   r.x().to(300.0, 1.0).await; // 1 s\n\
                 }",
            )
            .opacity(0.0),
    );
    code.opacity().to(1.0, 0.5).await;
    // Walk the focus through the code.
    for (a, b) in [(3, 4), (5, 5), (6, 6), (0, 0)] {
        wait_for(0.6).await;
        code.focus_lines(a, b, 0.4).await;
    }
    wait_for(0.8).await;
}

pub fn project() -> Project {
    Project::new("typography").scene(typography())
}