zaxis 0.1.0

Animation

Retained time-based transitions, keyframes, and application-defined motion.

Transitions in immediate mode

Call the same channel each visible pass. Its first appearance uses the target immediately; subsequent target changes animate from its currently sampled value. transition_from supplies an initial value when the first appearance should move.

use std::time::Duration;
use zaxis::{Color, Easing, TweenOptions};

let target = if enabled { Color::WHITE } else { Color::gray(100) };
let color = ui.transition("indicator-color", target,
    TweenOptions::new(Duration::from_millis(160)).easing(Easing::QuadOut)).value;

Ui scopes these IDs like widgets. Context takes an explicit Id; use widget_id.with("color"), widget_id.with("angle"), etc. for separate properties. Use ui.animation_id(source) to obtain a channel ID for pause/cancel controls. Repeated calls with the same target never restart, even after completion or cancellation. A new target restarts a paused/cancelled transition from its frozen value. Changed options apply on the next target change. Avoid NaN value targets: transition identity follows the type's PartialEq implementation.

Explicit tracks

animate(id, factory) creates a track once. Completed and cancelled channels keep their value and status, but release the track. The factory runs again only after the channel disappeared for a complete UI pass or an explicit restart.

use std::time::Duration;
use zaxis::{Easing, Repeat, Tween};

let motion = ui.animate("pulse", ||
    Tween::new(0.5_f32, 1.0, Duration::from_millis(300))
        .delay(Duration::from_millis(100))
        .easing(Easing::SineInOut)
        .repeat(Repeat::Count(3))
        .auto_reverse(true));
if motion.just_completed {
    // One completion event for the entire track, rather than for each cycle.
}
RuleBehavior
StartClock starts at the creating/restarting UI pass; the first sample is elapsed zero
DelayInitial value remains visible; one future deadline, before the first cycle only
DurationTime of a forward leg; elapsed time, never a frame counter
RepeatOnce, Count(n) total cycles including the first, or Forever; count zero panics
Auto-reverseEach cycle has forward and backward legs; finishes at the exact initial value
Cycle boundaryRepeated forward-only tracks reset to their start; reverse tracks return continuously
Zero durationCompletes at the end of delay, even with Forever; reduced motion skips the delay
CompletionAt or beyond total duration; exact endpoint clone, Completed, no next deadline
PauseSamples at this pass's time, freezes value and elapsed time including remaining delay
ResumeContinues frozen elapsed time; paused wall time is excluded; completed/cancelled tracks stay stopped
CancelSamples and freezes the current value, discards the track; no completion event
RestartReplaces the track, resets elapsed time, pause/cancel state and pending completion; may jump to its new start
FinishApplies Animation::finish() exactly, even when paused, and emits completion once

Use pause_animation, resume_animation, cancel_animation, finish_animation, restart_animation and sample_animation on Context. Controls are idempotent; their boolean return indicates whether the channel exists. Apply them during a UI pass; outside a pass they use the latest frame_time. Restart must be followed by an animate/sample call on the same pass to mark the channel visible. Animated exposes value, status, running(), completed() and just_completed. The completion flag is consumed by the first read, including multiple reads within a pass. Cancelling is distinct from completing.

Rate, direction and scrubbing

A running or paused channel can change how its track time advances:

CallEffect
set_animation_rate(id, rate)Speed multiplier (2.0 is twice as fast, negative plays backwards). Position is kept; zero or non-finite panics, use pause to stop
reverse_animation(id)Flips the sign of the rate from the current pose
seek_animation(id, elapsed)Jumps to track time. A paused channel shows the pose and stays paused; past the end it completes
animation_elapsed, animation_rate, animation_durationRead back track time, rate and exact length, e.g. to drive a scrubber

Playing backwards completes at the start pose (not finish()), with one completion event. Completed and cancelled channels hold no track and cannot be seeked or reversed; restart them, which also resets the rate to 1.0. Wake-ups of sleeping stages are converted to wall time, so a half-speed Delay wakes at twice the remaining track time without polling.

MotionStyle::time_scale multiplies the speed of every channel (0.25 is slow motion for debugging). Changing it mid-flight does not jump; non-positive or non-finite values mean 1.0. It scales animation channels only, not cursor blink, scrolling or other timers.

Testing tracks

Tracks are pure functions of elapsed time. zaxis::animation::testing checks an application track at several frame rates without a window: assert_exact_finish (not complete at zero, exact finish() at every rate, exact end after a long suspension, repeatable samples), assert_monotonic and frames. For retained channels, drive Context::run_at with explicit instants.

Keyframes and curves

use std::time::Duration;
use zaxis::{Easing, Keyframe, Keyframes};

let motion = ui.animate("sequence", || Keyframes::new([
    Keyframe::new(Duration::ZERO, 0.0_f32),
    Keyframe::new(Duration::from_millis(100), 8.0).easing(Easing::QuadOut),
    Keyframe::new(Duration::from_millis(280), 0.0).easing(Easing::QuintOut),
]));

The easing on each point controls the segment arriving at that point. Exact point times return the original value. At least two strictly increasing points starting at zero are required; invalid input panics at construction. Keyframes support delay, repeat and auto-reverse with the same cycle rules as tweens.

Built-in curves are Linear and the In/Out/InOut forms of Quad, Cubic, Quint, Sine, Expo, Circ, Back, Elastic and Bounce. In and InOut are mirrors of each family's Out curve, so In(t) + Out(1 - t) = 1 and InOut passes exactly through 0.5. Back and Elastic overshoot [0, 1] by design. Circ has a vertical tangent at one end.

Easing::cubic_bezier(x1, y1, x2, y2) is the CSS timing function; x1 and x2 must lie in [0, 1] (time stays monotonic) while y1 and y2 may overshoot. Easing::ease() and Easing::ease_in_out() are the CSS presets. Easing::steps(n) makes n equal jumps at the end of each interval.

Easing::custom takes a reusable Fn(f32) -> f32 + Send + Sync + 'static, which can capture curve data. Input is clamped to [0, 1], output must be finite and may overshoot. Endpoints stay exact for every curve.

Paths

Path::new(start) builds a 2D path with line_to, quad_to and cubic_to; curves are flattened once into a polyline with cumulative arc length, so point_at(fraction) moves at constant speed however the control points are placed. angle_at(fraction) is the direction of travel in radians, pose_at returns both as a PathPose, length() is the total, and points() is the polyline for drawing it.

PathFollow::new(path, duration) is an Animation<PathPose> with the tween builders easing, delay, repeat and auto_reverse; it completes exactly on the pose at the end. For a draw-on stroke animate a fraction and paint path.trimmed(0.0, fraction), which returns the polyline between two fractions (corners included).

Interpolation and external motion

Interpolate supports f32, f64, Vec2, Color, Border, CornerRadius, Shadow, Rect, Padding, Transform, Gradient, arrays, pairs and triples of supported types, plus Option<T> and Vec<T>. Some to Some and equal-length vectors interpolate element-wise; appearing, disappearing or a changed length is discrete (the from state is kept until the end). Color RGB interpolates in linear-light sRGB, alpha linearly in straight-alpha space. Results are encoded to sRGB and rounded to bytes. For example, black to white at 50% is Color::gray(188). Gradient direction is discrete and changes at its endpoint. Numeric interpolation allows custom-curve overshoot.

Oklab(color) is a wrapper that blends in the perceptual Oklab space: black to white at 50% is mid grey (about 99) rather than 188, and saturated pairs such as blue to yellow keep their lightness instead of dipping through grey. Use Tween<Oklab> or transition(.., Oklab(target), ..) and read .0 for the color. Endpoints are exact in both spaces.

Structs whose fields are all supported implement the traits with a macro instead of by hand:

#[derive(Clone, Debug, PartialEq)]
struct Pose { offset: Vec2, angle: f32 }
zaxis::impl_interpolate!(Pose { offset, angle });
zaxis::impl_spring_value!(Pose { offset, angle }); // for springs and Decay

Application value types implement Interpolate: Clone + PartialEq + 'static. Application effects implement Animation<T> with two methods:

use std::time::Duration;
use zaxis::{Animation, AnimationSample};

#[derive(Clone)]
struct Dial { angle: f32 }
struct Wobble { target: f32 }
impl Animation<Dial> for Wobble {
    fn sample(&self, elapsed: Duration) -> AnimationSample<Dial> {
        if elapsed >= Duration::from_secs(1) {
            return AnimationSample::completed(self.finish());
        }
        let t = elapsed.as_secs_f32();
        AnimationSample::running(Dial {
            angle: self.target * (1.0 - (-8.0 * t).exp() * (14.0 * t).cos()),
        })
    }
    fn finish(&self) -> Dial { Dial { angle: self.target } }
}

sample is a pure function of elapsed time and returns value, completion and scheduling information. It must handle arbitrary elapsed times and large gaps. finish supplies the exact state for explicit finish and reduced motion. AnimationSample::after(value, delay) lets procedural timers sleep until their next meaningful change. Procedural::new(final_value, closure) is a closure adapter. Animation<T> only requires T: Clone + 'static in the context; a custom track need not implement interpolation or equality. No unsafe, closed effect enum, plugin registration, or worker thread is involved.

The custom_animation example defines an external Pose, its Interpolate and SpringValue implementations and a custom easing curve. It uses the built-in analytic spring, including physical velocity on retarget, pause, resume, cancellation and completion events. Wobble above is an arbitrary external time function; it does not model spring velocity.

Style and built-in components

Style::motion contains reduced_motion, hover, expand, page and frame_interval. Defaults follow Rayfield's motion: hover/colors/reveal/chevron use 160 ms Quad Out, directional pages use 280 ms Quint Out. The sampling interval defaults to 16 ms for Immediate and timer-driven hosts. The built-in Vsync runner requests each next presented frame during continuous motion, without a second timer throttle. Duration and easing can be overridden per API call. Built-in component presets should use Repeat::Once without auto-reverse, so their final value represents their state.

Button, Checkbox, Slider and ColorPicker share resolved hover transitions for fill, gradient, border, shadow and text. TextEdit animates its background; its cursor uses a sleeping procedural timer through the same engine. Slider values, pointer capture, text selection and window drag/resize remain immediate. ColorPicker's inline editor reveals under a matching clip and disables interaction as soon as closing starts. Its real line chevron turns through half a rotation.

Pair tab_bar with ui.tab_pages(source, selected_index, size, build) for page slides. This fixed-size container enters from the right for higher indices and from the left for lower indices. It completes the current slide and retains only the latest pending selection. During a slide, both visible pages can be built and their controls are disabled. Keep external side effects out of page build callbacks. Hidden pages are not built. Paint and hit regions use the same translated layout; the container clip remains fixed.

Set style.motion.reduced_motion = true to immediately finish decorative motion, including delayed or paused tracks. Infinite decorative tracks use their declared final state. Essential timers can opt out with animate_with(..., AnimationOptions { decorative: false }, ...). Cursor blinking uses that option. Reduced motion never discards the final UI state.

Host scheduling and lifecycle

Only channels sampled during the current visible UI pass request deadlines. Ui helpers check their clip; built-in controls also check their own rectangle. For custom drawings, call animation APIs only in the visible region that needs them. Paused, completed, cancelled and disappeared channels have no deadlines. Manual request_repaint_after timers remain independent. An early native redraw does not postpone a delayed track's start. No animation call invalidates the whole context: changed paint descriptions rebuild affected elements, and completed geometry is reused normally.

Runner uses WaitUntil for deadlines and Wait in idle. Occluded, minimized, zero-size and suspended windows do not wake for animation. On restoration, elapsed time catches up directly; time does not automatically pause. Hidden tabs stop building their channels and remove them; reappearing transitions snap to their current target. Explicit animate starts a new visible lifetime.

Custom hosts use Context::run_at(now, build), frame_time() and needs_repaint_at(now) with the same monotonic Instant clock. Vsync hosts can also use wants_animation_frame() after presenting; it excludes sleeping timers and stopped/hidden channels. One time is fixed for all animation/timer computations in a pass. Backwards timestamps clamp to the previous pass. Pass future deadlines to your event loop, and suppress wakeups while the viewport is hidden. Normal Context::run samples Instant::now() once. Do not mix a simulated clock with real-time needs_repaint().

Momentum and velocity-preserving transitions

Decay::new(from, velocity) is a fling: velocity (units per second) falls as e^(-friction * t) and the value glides toward a rest position it only approaches. It is analytic like the spring, so sampling cost is constant after any suspension, and it knows its destination up front:

use std::time::Duration;
use zaxis::{Decay, DecayOptions};
let fling = Decay::with_options(position, release_velocity,
    DecayOptions::half_life(Duration::from_millis(350)));
let rest = fling.rest();                // from + velocity / friction
ui.context().restart_animation(id, fling);

Friction is per second (DecayOptions::friction(4.0) by default); half_life is the time for the speed to halve. The track completes once the remaining distance is under distance_threshold (0.01 by default), returns the exact rest position and reports an exact duration(). Because rest() is known, clamp or snap it before starting; to hit a chosen spot pass velocity = (spot - from) * friction. Values need SpringValue (f32, f64, Vec2 or your own type).

transition restarts its easing curve from zero speed on every retarget, which shows as a hitch when the target changes repeatedly mid-flight. transition_smooth (on Context and Ui, for SpringValue + Interpolate types) keeps the displayed velocity: the retargeted leg is a cubic Hermite curve of length options.duration that starts at the current value with the current speed and comes to rest at the new target. It ignores the easing curve for that leg (the speed is measured from the running track, including rate and time_scale); from rest, and for the first leg, the options apply unchanged. Equal targets never restart, and the first call snaps, exactly like transition. The leg can overshoot slightly when the old motion points away from the new target, which is the continuous behavior.

Physical spring

use zaxis::{vec2, SpringOptions, SpringState};
let motion = ui.spring_transition("panel-position", vec2(120.0, 0.0),
    ui.style().motion.spring);
let position = motion.value.value;
let velocity = motion.value.velocity; // logical pixels per second

// Only the first appearance uses this initial state.
let motion = ui.spring_transition_from("preview", SpringState {
    value: 0.0_f32, velocity: 12.0,
}, 100.0, SpringOptions::frequency(3.0, 1.0));

Context takes explicit IDs for the same methods. Spring::new(from, target) and Spring::with_options(...).velocity(initial_velocity) are ordinary open Animation<SpringState<T>> tracks; use all existing Context controls with them. SpringValue adds only zero, addition, subtraction, scalar multiplication and norm. Built-in implementations support f32, f64, and Vec2; application types can implement it independently of Interpolate.

The differential equation is mass * acceleration + damping * velocity + stiffness * displacement = 0. For frequency f in Hz and damping ratio r, omega = 2*pi*f, stiffness = mass*omega^2, damping = 2*mass*omega*r. frequency uses mass one. Ratios below one oscillate, one is critically damped, and above one are overdamped. Default: 3 Hz, critical damping, no bounce. The analytic solution has constant sampling cost at 30/60/144 Hz or after hours of suspension. No missed-frame integration loop runs.

Mass and thresholds must be positive, stiffness/damping nonnegative, all parameters finite; invalid parameters or overflowing coefficients panic at construction/retarget. Stiffness zero snaps to the exact target and zero velocity. Zero damping keeps oscillating unless initially within both rest thresholds; finish/cancel or stop building it to end that track. Thresholds default to 0.001 distance units and 0.001 units/second. Once both are satisfied, the channel returns the exact target, zero velocity, one completion event and no deadline.

Instead of frequency and ratio, SpringOptions::duration_bounce(duration, bounce) takes the period of one undamped oscillation and a bounce in (-1, 1): 0 is critically damped, positive values overshoot (0.15 to 0.4 feel springy), negative values are overdamped. It is frequency(1 / duration, 1 - bounce) for non-negative bounce and frequency(1 / duration, 1 / (1 + bounce)) otherwise.

Retarget first samples position and velocity at the pass time. The new spring starts at that state. Equal targets never restart; option changes apply on the next target change. First appearance without an initial state snaps silently. Pause freezes position, velocity and elapsed time; resume excludes paused wall time. Cancel freezes the sampled state and discards its track, with no completion. A new target resumes from that frozen state; restart replaces the state and may jump. Explicit finish also emits completion once. All controls use one pass clock.

Enter, Exit, and measured disclosure

use zaxis::{vec2, Presence};
Presence::fade().offset(vec2(0.0, ui.style().motion.slide_distance))
    .scaling(0.98).show(ui, "tool-panel", visible, |ui| {
        ui.label("Tool output");
        ui.button("Copy");
    });
ui.reveal("details", open, |ui| {
    ui.label(&details); // actual height, including wrapping
});

ui.presence(source, visible, closure) is the fade shortcut. Standalone presets are Presence::fade(), ::slide(offset) and ::scale(scale); combine them with fading, offset, scaling, pivot, and motion. Initially hidden content is never built and never runs Exit. Enter starts from the hidden pose. Exit keeps calling the current pass's closure until opacity/pose reaches its hidden endpoint; then it stops building. No borrowed closure or application subtree is retained. Changing visibility during either transition reverses from its sampled pose.

exit_layout(true) (default) preserves the measured allocation during Exit; false releases it immediately while painting the outgoing contents as an overlay. Both choices disable outgoing input immediately, clear focus/capture at pass end, and ignore already queued widget activation. interactive_enter(true) is the default: the transformed hit regions follow the displayed content. Disable it to wait until Enter completes. Reusing the source in an entirely different area requires an appropriate stable parent push_id.

Scale uses a relative pivot in measured bounds (center by default). Layout and text measurement stay at logical size. Cached base meshes are transformed; glyphs and curves rasterize/tessellate at displayed resolution, using quarter-step scale buckets and a bounded resolution cap. A changing raster bucket rebuilds glyph geometry while retaining its shaped layout; motion within a bucket reuses it. Beyond the cap, text uses texture filtering. Parent clipping stays fixed, child clips follow geometry. Opacity multiplies every primitive's alpha, including text, borders, shadows, gradients and the blur mask. It never edits shared Style. Blur remains opt-in. Popup anchors follow their transform; panels fit the screen viewport independently and their full-viewport blockers stay fixed. Ui::visual exposes the same axis-aligned mapping and subtree opacity directly. Widget Response.rect remains a logical layout bound; after the helper returns, Ui::displayed_rect(&response) or Context::visual_rect(id, rect) obtains its visual bound. Input coordinates are mapped back for sliders and text selection.

Reveal measures arbitrary content once per pass and animates its actual height under a clip. Resizes and rapid toggles retarget from the current displayed height. Closing content loses input immediately; at zero height it is neither built nor allocated. Parent neighbors and ScrollArea content size use the animated height. reveal_with overrides Style.motion.expand. The internal ColorPicker uses this same measured placement/clip path. Presence and reveal accept finite forward tweens only (no repeat or auto-reverse). When an entire area disappears for a pass, its state/channels/deadlines are pruned. Reappearance starts fresh: hidden stays hidden; visible Presence enters; a newly opened Reveal starts from zero.

Opt-in reorder and selection marker

use zaxis::{Id, Reorder, Layout};
let mut bounds = Vec::new();
Reorder::new("results").show(ui, model.iter().map(|item| item.id), |rows| {
    for item in visible_items {
        let response = rows.item(item.id, |ui| ui.button(&item.label));
        bounds.push((item.id, rows.ui().displayed_rect(&response)));
    }
});
ui.selection_indicator("selected", selected_id, &bounds, Layout::Vertical);

Each row executes once and obtains its final measured allocation before its visual offset is applied. Later rows use final layout, not animated positions. Stable model IDs, never labels or current indexes, own the retained motion. Reordering during movement samples the displayed position before retargeting. New rows snap to their final allocation; deleted model IDs are cleaned at pass end; reinsertion after deletion starts fresh. Changed row sizes shift subsequent targets. Duplicate model/build IDs are reported as an IdCollision diagnostic.

Always supply full model membership, including rows outside the virtual range. Build only visible rows and reserve skipped layout using your existing virtual list. Membership preserves hidden row identity; hidden tracks are sampled without requesting frames. A row omitted from building for a pass snaps to the current layout when it returns, avoiding movement from an obsolete viewport position. Scroll offsets are normalized out: ordinary viewport movement never retargets every row. The container itself disappearing releases its motion state. There is no drag-and-drop or owned list model.

Paint and hits use one visual offset. The final model order is paint order; later rows win overlaps in both drawing and hit testing. No duplicate interactive instances are created. Reorder is explicit; streaming text, editor state and ordinary layout changes outside this container stay immediate.

SelectionIndicator takes stable selected ID plus current actual bounds; use displayed_rect for reordered/transformed controls. Selected model value and keyboard focus change immediately. Only the decorative marker retargets, including width/height changes, rapid switches and scrolling. Missing selection bounds remove its channel immediately. For per-use settings use SelectionIndicator::new with orientation, motion, color and thickness, then show.

Busy indicators and change highlight

ui.skeleton(height) (or Skeleton::new(height).width(w).corner_radius(r).period(p)) is a loading placeholder: a muted rounded block with a soft highlight sweeping across it. It is decorative and takes no input. Under reduced motion, when clipped or hidden, it is a static block and requests no frames.

use zaxis::{Loader, Progress, ProgressState};
ui.add(Loader::new().active(working).size(24.0).stroke(2.0));
ui.add(Progress::new(match actual_progress {
    Some(value) => ProgressState::Determinate(value),
    None => ProgressState::Indeterminate { active: working },
}));
ui.pulse("busy-label", working, |ui| { ui.label("Agent is working"); });
ui.highlight("file-change", revision, rect, current_base_fill, accent);

Loader uses the current Lucide loader geometry: eight strokes with round caps in a 24x24 viewbox. Full Lucide ISC and original Feather MIT notices are bundled in assets/LUCIDE-LICENSE. No SVG parser, browser, network request or image runtime is needed. Cached geometry rotates around its own center with a linear phase and constant speed. Numeric angle wraps modulo one turn, but the displayed pose is continuous. Builders set size, color, stroke, period and active state; defaults come from motion and theme. It registers no hit region. Rotation never changes allocation. While active it is a busy progress indicator to screen readers, named by Loader::label("Loading messages"); the rotation never changes what they are told. An inactive loader is not announced.

ProgressState::Determinate applies the application's value immediately; Complete fills the track and stops animation. Indeterminate has no percentage. Screen readers get the same state: a percentage for determinate progress, "busy" for active indeterminate work, never the animated segment. Name the bar with .accessible_label(..). Inactive/reduced-motion indeterminate shows a fixed segment as a busy symbol. Pulse quietly varies opacity between 0.65 and one; text never moves. Override with pulse_with(..., Pulse::new(period).minimum(...), ...). Rotation and Pulse also implement the public Animation trait for direct composition.

All convenience cycles require an external active flag. Inactive or omitted cycles release their channels, visible clipping suppresses deadlines, and reduced motion draws a static symbol. Loader/Progress restart at phase zero after a clipped/hidden/inactive lifetime. Pulse keeps time while built but clipped, without waking the host; omission or inactive resets it. Window occlusion pauses host redraw, not monotonic time; restoration samples the current phase directly. Returning from reduced motion starts convenience cycles fresh. Direct animate tracks retain the standard completed-channel rule and need explicit restart.

Highlight compares an explicit revision: first appearance establishes a baseline, each later change triggers once. It smoothly attacks from the current accent amount to one (hover timing), then returns to zero (highlight timing). A repeat event restarts that attack from the displayed amount without a jump. Base colors are supplied fresh each pass, so theme/hover changes never restore an old fill. highlight_with overrides release timing per use. Reduced motion keeps a subtle static accent after an event until the area is removed, with no animation deadline.

Composition

use std::time::Duration;
use zaxis::{Sequence, Parallel, Tween, Delay, Stagger, Id};
let track = Sequence::new(0.0_f32)
    .then(Tween::new(0.0, 1.0, Duration::from_millis(120)))
    .delay(1.0, Duration::from_millis(250))
    .then(Tween::new(1.0, 0.0, Duration::from_millis(180)));
let group = Parallel::new().with(track).with(Delay::new(0.0, Duration::from_secs(1)));
let mut stagger = Stagger::new(Duration::from_millis(25))
    .max_delay(Duration::from_millis(240));
stagger.insert(Id::new("task-a"), Duration::ZERO,
    Tween::new(0.0_f32, 1.0, Duration::from_millis(120)));

These are ordinary Animation implementations, sampled by the existing Context Slot. Pause/resume/cancel/finish/restart and single-consumer completion events apply to the entire composition. No additional scheduler or script language. Sequence consumes residual elapsed time and skips completed known-duration stages after long gaps. Exact stage boundaries return exact finished values. Zero stages are skipped; empty Sequence finishes at its explicit initial value. A child that finishes before an explicit bound holds its endpoint until that bound, sleeping. Delay returns Wake::After(remaining) and never spins continuous frames.

Animation::duration() defaults to None, preserving external implementations. Tween/Keyframes/Delay/compositions supply known lengths. An unbounded or unspecified Sequence child blocks subsequent stages; if it reports completion while its length is unknown, an intermediate stage holds without continuous frames. Use then_for(duration, custom_track) to define an exact stage boundary, including for a spring or infinite child. Explicit finish always applies the final child's finish value. Parallel returns children in insertion order and completes after all children. Empty Parallel completes immediately; an infinite child prevents normal completion. Stopped children do not add wakeups to the running group.

Parallel::at(offset, track) starts a child offset after the group begins. Until then it holds its initial value and schedules one wake-up for the offset, never continuous frames; with(track) is at(Duration::ZERO, track). duration() counts the offsets. Sequence::mark("name") names the current end of a sequence (every earlier stage needs a known length, otherwise it panics) and Sequence::time_of("name") reads it back, to place other tracks or to seek_animation to a named moment. Read marks before moving the sequence into a channel.

Stagger returns (Id, value) pairs. Insert with composition elapsed time; existing IDs keep their original track/schedule. Reorder changes output order only. Delete only on model deletion, never viewport omission; reinserting a removed ID schedules a new child. Each insertion adds step * current child count, capped at 240 ms by default (configurable), to its insertion time. Pause elapsed time before passing new insertion times. Stage storage belongs to this open composition; callers can update it before moving it into Context, or use a custom Animation wrapper for live model changes. Ordinary Context restart resets the composition clock.

The animation gallery continuously previews all built-in tracks and effects in the unchanged grey theme. Existing hover/expand/page durations remain untouched: new presence and reorder defaults are 240/200 ms, displacement 8 px. All motion is opt-in; typing, selection, cursor input, dragging/resizing and streaming tokens remain immediate.

Dock motion

Dock uses retained spring channels keyed by stable panel IDs. Layout first computes final SplitPane rectangles; existing panels travel from their displayed rectangle to those targets. Retargeting preserves current velocity. The default critical damping prevents overshoot from rest; a reversal while moving preserves momentum instead of jumping to zero velocity. An activated tab inherits its group's motion. A focus handoff rebases the offset velocity against both panels' velocities, preserving the ring's velocity in the viewport.

Panel contents build at the final size, then translate and clip into the displayed rectangle. This avoids repeated text wrapping/layout at intermediate sizes and keeps text at its normal scale. It is not a bitmap cache: visible viewers still run once each pass and geometry/text caches supply reuse. A closing panel replays ordinary paints only, with no interaction or semantics, during a restrained 100 ms fade and small shrink. Material passes and nested deferred scroll paints are not frozen into exit replays. New regions reveal close to their destination.

One focus ring spans docked and floating panels. Its base is the displayed panel rectangle; a spring animates the remaining offset on a focus change. Radius and color also use spring channels. Stroke edges and width snap to physical pixels. The inactive color fades more slowly (2 Hz) after viewport/child focus loss. Dragging changes only the preview spring until a drop commits the layout.

Snappy (default, 6 Hz) and Smooth (4.5 Hz) use critical damping, mass 1 and 0.2 px / 0.8 px/s settle thresholds. Off and reduced motion apply target values immediately. Settled channels keep no repaint deadline. The optional rotating contour requests frames only while enabled, focused and motion is permitted; turning it off, losing focus or reducing motion releases its track. Removing a Dock retires its transient geometry, input, animation and owned floating layers.

Edit on GitHub

On this page