mod anim_bridge;
mod animated_properties;
pub(in crate::widgets::container) use animated_properties::{AnimKind, AnimSlot, ContainerAnims};
mod animations;
mod box_model;
mod interaction;
mod ripple;
mod scrollable;
mod style;
#[cfg(test)]
mod characterization;
use animations::instant_transition;
pub use animations::{AdvanceResult, AnimationState, get_animated_value};
use interaction::{HitContext, untransform_point};
pub use ripple::{MAX_LIVE_RIPPLES, Ripple, RippleState};
use style::Decoration;
use std::borrow::Cow;
use std::cell::Cell;
use std::rc::Rc;
use crate::animation::{Animatable, IntoAnimated, Motion};
use crate::backdrop::BackdropBlur;
use crate::jobs::{JobRequest, JobType, RequiredJob, request_job};
use crate::layout::{Axis, Constraints, Flex, Layout, Length, Size};
use crate::pivot::Pivot;
use crate::reactive::invalidation::is_detached;
use crate::reactive::{
CursorIcon, IntoSignal, Prop, RwSignal, create_derived, create_signal, with_signal_tracking,
};
use crate::renderer::{GradientDir, PaintContext, Shadow};
use crate::transform::{Scale, Transform, Translate};
use crate::tree::{LayoutCtx, Tree, WidgetId};
use crate::widget_ref::{WidgetRef, register_widget_ref};
use super::children::ChildrenSource;
use super::control::Control;
use super::into_child::{IntoChild, IntoChildren};
use super::paint_children::{ChildPaintOptions, paint_children};
use super::scroll::{Scroll, ScrollAxis, ScrollState, ScrollbarMetrics, ScrollbarVisibility};
use super::state_layer::{
Moves, RippleConfig, StateStyle, StateWhen, Stateful, resolve_background,
};
use super::widget::{
Color, Event, EventResponse, Key, LayoutHints, Modifiers, MouseButton, Padding, Placement,
Point, Rect, ScrollSource, Widget,
};
pub type ClickCallback = Rc<dyn Fn()>;
#[doc(hidden)]
pub struct FnHandler;
#[doc(hidden)]
pub struct CallbackHandler;
#[doc(hidden)]
pub struct OptionHandler;
pub trait IntoClickHandler<Marker = FnHandler> {
fn into_click_handler(self) -> Option<ClickCallback>;
}
impl<F: Fn() + 'static> IntoClickHandler<FnHandler> for F {
fn into_click_handler(self) -> Option<ClickCallback> {
Some(Rc::new(self))
}
}
impl IntoClickHandler<CallbackHandler> for crate::reactive::Callback {
fn into_click_handler(self) -> Option<ClickCallback> {
Some(Rc::new(move || self.run()))
}
}
impl IntoClickHandler<OptionHandler> for Option<crate::reactive::Callback> {
fn into_click_handler(self) -> Option<ClickCallback> {
self.map(|cb| Rc::new(move || cb.run()) as ClickCallback)
}
}
pub type KeyCallback = Rc<dyn Fn(Key, Modifiers, bool) -> EventResponse>;
pub type KeyUpCallback = Rc<dyn Fn(Key, Modifiers) -> EventResponse>;
pub type HoverCallback = Rc<dyn Fn(bool)>;
pub type ScrollCallback = Rc<dyn Fn(f32, f32, ScrollSource)>;
pub type PointerMoveCallback = Rc<dyn Fn(f32, f32)>;
pub type MouseDownCallback = Rc<dyn Fn(f32, f32)>;
pub type MouseUpCallback = Rc<dyn Fn(f32, f32)>;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum GradientDirection {
Horizontal,
Vertical,
Diagonal,
DiagonalReverse,
}
impl From<GradientDirection> for GradientDir {
fn from(direction: GradientDirection) -> Self {
match direction {
GradientDirection::Horizontal => GradientDir::Horizontal,
GradientDirection::Vertical => GradientDir::Vertical,
GradientDirection::Diagonal => GradientDir::Diagonal,
GradientDirection::DiagonalReverse => GradientDir::DiagonalReverse,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LinearGradient {
pub start_color: Color,
pub end_color: Color,
pub direction: GradientDirection,
}
impl LinearGradient {
pub fn new(start: Color, end: Color, direction: GradientDirection) -> Self {
Self {
start_color: start,
end_color: end,
direction,
}
}
pub fn horizontal(start: Color, end: Color) -> Self {
Self::new(start, end, GradientDirection::Horizontal)
}
pub fn vertical(start: Color, end: Color) -> Self {
Self::new(start, end, GradientDirection::Vertical)
}
pub fn diagonal(start: Color, end: Color) -> Self {
Self::new(start, end, GradientDirection::Diagonal)
}
}
#[derive(Debug, Clone, Copy)]
pub struct Border {
pub width: f32,
pub color: Color,
}
impl Border {
pub fn new(width: impl crate::layout::IntoF32, color: Color) -> Self {
Self {
width: width.into_f32(),
color,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Overflow {
#[default]
Visible,
Hidden,
}
#[derive(Default)]
pub(super) struct LayerProps {
pub(super) translate: Prop<Translate>,
pub(super) rotate: Prop<f32>,
pub(super) scale: Prop<Scale>,
pub(super) pivot: Prop<Pivot>,
pub(super) opacity: Prop<f32>,
}
#[derive(Default)]
pub(super) struct DecorationProps {
pub(super) gradient: Prop<Option<LinearGradient>>,
pub(super) shadow: Prop<Shadow>,
}
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) struct InteractionFlags: u8 {
const HOVERED = 1;
const PRESSED = 2;
}
}
pub(super) struct InteractionState {
pub(super) on_click: Option<ClickCallback>,
pub(super) on_right_click: Option<ClickCallback>,
pub(super) on_middle_click: Option<ClickCallback>,
pub(super) on_key_down: Option<KeyCallback>,
pub(super) on_key_up: Option<KeyUpCallback>,
pub(super) on_hover: Option<HoverCallback>,
pub(super) on_scroll: Option<ScrollCallback>,
pub(super) on_pointer_move: Option<PointerMoveCallback>,
pub(super) on_mouse_down: Option<MouseDownCallback>,
pub(super) on_mouse_up: Option<MouseUpCallback>,
pub(super) focusable: bool,
pub(super) flags: RwSignal<InteractionFlags>,
pub(super) states: Vec<(StateWhen, StateStyle)>,
pub(super) declares_transform: Moves,
pub(super) declared_enabled: Prop<bool>,
pub(super) enabled: Prop<bool>,
pub(super) cursor: Prop<CursorIcon>,
pub(super) ripple: RippleState,
}
impl Default for InteractionState {
fn default() -> Self {
Self {
on_click: None,
on_right_click: None,
on_middle_click: None,
on_key_down: None,
on_key_up: None,
on_hover: None,
on_scroll: None,
on_pointer_move: None,
on_mouse_down: None,
on_mouse_up: None,
focusable: false,
flags: create_signal(InteractionFlags::empty()),
states: Vec::new(),
declares_transform: Moves::default(),
declared_enabled: Prop::Unset,
enabled: Prop::Unset,
cursor: Prop::Unset,
ripple: RippleState::new(),
}
}
}
impl InteractionState {
pub(super) fn is_hovered(&self) -> bool {
self.flags
.get_untracked()
.contains(InteractionFlags::HOVERED)
}
pub(super) fn is_pressed(&self) -> bool {
self.flags
.get_untracked()
.contains(InteractionFlags::PRESSED)
}
pub(super) fn set_flag(&self, flag: InteractionFlags, on: bool) {
let current = self.flags.get_untracked();
let next = if on { current | flag } else { current - flag };
if next != current {
self.flags.set(next);
}
}
pub(super) fn has_any_state(&self) -> bool {
!self.states.is_empty()
}
pub(super) fn declares(&self, when: impl Fn(&StateWhen) -> bool) -> bool {
self.states.iter().any(|(w, _)| when(w))
}
pub(super) fn ripple_config(&self) -> Option<RippleConfig> {
self.states
.iter()
.rev()
.find(|(when, s)| matches!(when, StateWhen::Pressed) && s.ripple.is_some())
.and_then(|(_, s)| s.ripple)
}
pub(super) fn as_control(&self, id: WidgetId) -> Control {
Control::new(id, self.flags, self.enabled)
}
}
pub(super) struct ScrollData {
pub(super) scroll: Scroll,
pub(super) metrics: ScrollbarMetrics,
pub(super) scrollbar_visibility: ScrollbarVisibility,
pub(super) fill_viewport: bool,
pub(super) scroll_state: ScrollState,
pub(super) v_scrollbar_track_id: Option<WidgetId>,
pub(super) v_scrollbar_handle_id: Option<WidgetId>,
pub(super) v_scrollbar_scale_anim: Option<AnimationState<f32>>,
pub(super) h_scrollbar_track_id: Option<WidgetId>,
pub(super) h_scrollbar_handle_id: Option<WidgetId>,
pub(super) h_scrollbar_scale_anim: Option<AnimationState<f32>>,
}
impl ScrollData {
fn new(scroll: Scroll) -> Self {
Self {
scroll,
metrics: ScrollbarMetrics::default(),
scrollbar_visibility: ScrollbarVisibility::Always,
fill_viewport: false,
scroll_state: ScrollState::default(),
v_scrollbar_track_id: None,
v_scrollbar_handle_id: None,
v_scrollbar_scale_anim: None,
h_scrollbar_track_id: None,
h_scrollbar_handle_id: None,
h_scrollbar_scale_anim: None,
}
}
}
pub struct Container {
pub(super) layout: Box<dyn Layout>,
pub(super) children_source: ChildrenSource,
pub(super) padding: Prop<Padding>,
pub(super) background: Prop<Color>,
pub(super) corners: Prop<crate::widgets::Corners>,
pub(super) border_width: Prop<f32>,
pub(super) border_color: Prop<Color>,
pub(super) width: Prop<Length>,
pub(super) height: Prop<Length>,
pub(super) overflow: Prop<Overflow>,
pub(super) overflow_resolved: Cell<Overflow>,
pub(super) shadow_reach: Cell<f32>,
pub(super) visible: Prop<bool>,
pub(super) layer: Option<Box<LayerProps>>,
pub(super) decoration: Option<Box<DecorationProps>>,
pub(super) interaction: Option<Box<InteractionState>>,
pub(super) widget_ref: Option<WidgetRef>,
pub(super) backdrop_blur: Prop<BackdropBlur>,
pub(super) takes_input: Prop<bool>,
pub(super) declared_control: bool,
pub(super) anims: Option<Box<ContainerAnims>>,
pub(super) scroll_axis: ScrollAxis,
pub(super) scroll_data: Option<Box<ScrollData>>,
pub(super) children_sorted_along: Option<Axis>,
}
struct Placed {
transform: Transform,
pivot: Pivot,
scroll: (f32, f32),
}
impl Container {
pub fn new() -> Self {
let children_source = ChildrenSource::default();
Self {
layout: Box::new(Flex::column()),
children_source,
padding: Prop::Unset,
background: Prop::Unset,
corners: Prop::Unset,
border_width: Prop::Unset,
border_color: Prop::Unset,
width: Prop::Unset,
height: Prop::Unset,
overflow: Prop::Unset,
overflow_resolved: Cell::new(Overflow::Visible),
shadow_reach: Cell::new(0.0),
visible: Prop::Unset,
layer: None,
decoration: None,
interaction: None,
widget_ref: None,
backdrop_blur: Prop::Unset,
takes_input: Prop::Unset,
declared_control: false,
anims: None,
scroll_axis: ScrollAxis::None,
scroll_data: None,
children_sorted_along: None,
}
}
fn publish_paint_reach(&self, tree: &mut Tree, id: WidgetId, bounds: Rect) {
let shadow_extent = self.shadow_reach.get();
tree.set_own_paint_reach(id, self.max_transform_reach(bounds, shadow_extent));
}
fn resolve_scroll(&mut self) {
let Some(data) = self.scroll_data.as_mut() else {
return;
};
let scroll = &data.scroll;
let defaults = ScrollbarMetrics::default();
let width = scroll.width.get_or(defaults.width);
data.metrics = ScrollbarMetrics {
width,
hover_width: scroll
.hover_width
.get_or(width * defaults.hover_width / defaults.width),
margin: scroll.margin.get_or(defaults.margin),
min_handle_size: scroll.min_handle_size.get_or(defaults.min_handle_size),
reserve_gutter: scroll.reserve_gutter.get_or(defaults.reserve_gutter),
};
data.scrollbar_visibility = scroll.visibility.get_or(ScrollbarVisibility::Always);
data.fill_viewport = scroll.fill_viewport.get_or(false);
}
fn placed(&self, id: WidgetId, bounds: Rect) -> Placed {
let scroll = if self.scroll_axis != ScrollAxis::None {
let sd = self.scroll_data();
(sd.scroll_state.offset_x, sd.scroll_state.offset_y)
} else {
(0.0, 0.0)
};
Placed {
transform: self.animated_transform(id, bounds),
pivot: self.resolved_pivot(id),
scroll,
}
}
fn scroll_data(&self) -> &ScrollData {
self.scroll_data.as_deref().expect("scroll_data not set")
}
fn scroll_mut(&mut self) -> &mut ScrollData {
self.scroll_data
.as_deref_mut()
.expect("scroll_data not set")
}
fn layer_mut(&mut self) -> &mut LayerProps {
self.layer.get_or_insert_with(Box::default)
}
fn decoration_mut(&mut self) -> &mut DecorationProps {
self.decoration.get_or_insert_with(Box::default)
}
pub(super) fn gradient_prop(&self) -> Prop<Option<LinearGradient>> {
self.decoration
.as_deref()
.map_or(Prop::Unset, |d| d.gradient)
}
pub(super) fn shadow_prop(&self) -> Prop<Shadow> {
self.decoration.as_deref().map_or(Prop::Unset, |d| d.shadow)
}
pub(super) fn translate_prop(&self) -> Prop<Translate> {
self.layer.as_deref().map_or(Prop::Unset, |t| t.translate)
}
pub(super) fn rotate_prop(&self) -> Prop<f32> {
self.layer.as_deref().map_or(Prop::Unset, |t| t.rotate)
}
pub(super) fn scale_prop(&self) -> Prop<Scale> {
self.layer.as_deref().map_or(Prop::Unset, |t| t.scale)
}
pub(super) fn pivot_prop(&self) -> Prop<Pivot> {
self.layer.as_deref().map_or(Prop::Unset, |t| t.pivot)
}
pub(super) fn opacity_prop(&self) -> Prop<f32> {
self.layer.as_deref().map_or(Prop::Unset, |t| t.opacity)
}
fn interact_mut(&mut self) -> &mut InteractionState {
self.interaction.get_or_insert_with(Box::default)
}
pub fn layout(mut self, layout: impl Layout + 'static) -> Self {
self.layout = Box::new(layout);
self
}
pub fn child<M>(mut self, child: impl IntoChild<M>) -> Self {
child.add_to_container(&mut self.children_source);
self
}
pub fn children<M>(mut self, children: impl IntoChildren<M>) -> Self {
children.add_to_container(&mut self.children_source);
self
}
pub fn padding<M>(mut self, value: impl IntoAnimated<Padding, M>) -> Self {
self.padding = animated_properties::declare::padding(&mut self.anims, value);
self
}
pub fn background<M>(mut self, color: impl IntoAnimated<Color, M>) -> Self {
self.background = animated_properties::declare::background(&mut self.anims, color);
self
}
pub fn corners<M>(mut self, corners: impl IntoAnimated<crate::widgets::Corners, M>) -> Self {
self.corners = animated_properties::declare::corners(&mut self.anims, corners);
self
}
pub fn backdrop_blur<M>(mut self, blur: impl IntoSignal<BackdropBlur, M>) -> Self {
self.backdrop_blur = blur.into_prop();
self
}
pub fn takes_input<M>(mut self, takes: impl IntoSignal<bool, M>) -> Self {
self.takes_input = takes.into_prop();
self
}
pub fn cursor<M>(mut self, cursor: impl IntoSignal<CursorIcon, M>) -> Self {
self.interact_mut().cursor = cursor.into_prop();
self
}
pub fn border<M1, M2>(
mut self,
width: impl IntoAnimated<f32, M1>,
color: impl IntoAnimated<Color, M2>,
) -> Self {
self.border_width = animated_properties::declare::border_width(&mut self.anims, width);
self.border_color = animated_properties::declare::border_color(&mut self.anims, color);
self
}
pub fn gradient<M>(mut self, gradient: impl IntoSignal<Option<LinearGradient>, M>) -> Self {
self.decoration_mut().gradient = gradient.into_prop();
self
}
pub fn width<M>(mut self, width: impl IntoAnimated<Length, M>) -> Self {
self.width = animated_properties::declare::width(&mut self.anims, width);
self
}
pub fn height<M>(mut self, height: impl IntoAnimated<Length, M>) -> Self {
self.height = animated_properties::declare::height(&mut self.anims, height);
self
}
pub fn overflow<M>(mut self, overflow: impl IntoSignal<Overflow, M>) -> Self {
let declared = overflow.into_prop();
self.overflow_resolved
.set(declared.get_or_untracked(Overflow::Visible));
self.overflow = declared;
self
}
pub fn visible<M>(mut self, visible: impl IntoSignal<bool, M>) -> Self {
self.visible = visible.into_prop();
self
}
pub fn enabled<M>(mut self, enabled: impl IntoSignal<bool, M>) -> Self {
self.interact_mut().declared_enabled = enabled.into_prop();
self
}
pub fn scroll(mut self, scroll: Scroll) -> Self {
self.scroll_axis = scroll.axis;
self.scroll_data = Some(Box::new(ScrollData::new(scroll)));
self
}
pub fn on_click<M>(mut self, callback: impl IntoClickHandler<M>) -> Self {
let handler = callback.into_click_handler();
if handler.is_some() || self.interaction.is_some() {
self.interact_mut().on_click = handler;
}
self
}
pub fn on_right_click<F: Fn() + 'static>(mut self, callback: F) -> Self {
self.interact_mut().on_right_click = Some(Rc::new(callback));
self
}
pub fn on_middle_click<F: Fn() + 'static>(mut self, callback: F) -> Self {
self.interact_mut().on_middle_click = Some(Rc::new(callback));
self
}
pub fn on_key_down<F: Fn(Key, Modifiers, bool) -> EventResponse + 'static>(
mut self,
callback: F,
) -> Self {
self.interact_mut().on_key_down = Some(Rc::new(callback));
self
}
pub fn on_key_up<F: Fn(Key, Modifiers) -> EventResponse + 'static>(
mut self,
callback: F,
) -> Self {
self.interact_mut().on_key_up = Some(Rc::new(callback));
self
}
pub fn focusable(mut self, focusable: bool) -> Self {
self.interact_mut().focusable = focusable;
self
}
pub fn on_hover<F: Fn(bool) + 'static>(mut self, callback: F) -> Self {
self.interact_mut().on_hover = Some(Rc::new(callback));
self
}
pub fn on_scroll<F: Fn(f32, f32, ScrollSource) + 'static>(mut self, callback: F) -> Self {
self.interact_mut().on_scroll = Some(Rc::new(callback));
self
}
pub fn on_pointer_move<F: Fn(f32, f32) + 'static>(mut self, callback: F) -> Self {
self.interact_mut().on_pointer_move = Some(Rc::new(callback));
self
}
pub fn on_mouse_down<F: Fn(f32, f32) + 'static>(mut self, callback: F) -> Self {
self.interact_mut().on_mouse_down = Some(Rc::new(callback));
self
}
pub fn on_mouse_up<F: Fn(f32, f32) + 'static>(mut self, callback: F) -> Self {
self.interact_mut().on_mouse_up = Some(Rc::new(callback));
self
}
pub fn widget_ref(mut self, r: WidgetRef) -> Self {
self.widget_ref = Some(r);
self
}
pub fn shadow<M>(mut self, shadow: impl IntoAnimated<Shadow, M>) -> Self {
let declared = animated_properties::declare::shadow(&mut self.anims, shadow);
self.decoration_mut().shadow = declared;
self
}
pub fn translate<M>(mut self, t: impl IntoAnimated<Translate, M>) -> Self {
let declared = animated_properties::declare::translate(&mut self.anims, t);
self.layer_mut().translate = declared;
self
}
pub fn rotate<M>(mut self, degrees: impl IntoAnimated<f32, M>) -> Self {
let declared = animated_properties::declare::rotate(&mut self.anims, degrees);
self.layer_mut().rotate = declared;
self
}
pub fn scale<M>(mut self, factor: impl IntoAnimated<Scale, M>) -> Self {
let declared = animated_properties::declare::scale(&mut self.anims, factor);
self.layer_mut().scale = declared;
self
}
pub fn opacity<M>(mut self, opacity: impl IntoAnimated<f32, M>) -> Self {
let declared = animated_properties::declare::opacity(&mut self.anims, opacity);
self.layer_mut().opacity = declared;
self
}
pub fn pivot<M>(mut self, origin: impl IntoSignal<Pivot, M>) -> Self {
self.layer_mut().pivot = origin.into_prop();
self
}
pub fn control(mut self) -> Self {
self.declared_control = true;
self.interact_mut();
self
}
fn fold_enabled(&mut self, tree: &Tree, id: WidgetId) {
let Some(ix) = self.interaction.as_deref_mut() else {
return;
};
let inherited = tree
.get_parent(id)
.and_then(|parent| tree.nearest_control(parent))
.map_or(Prop::Unset, |control| control.enabled_prop());
ix.enabled = match (ix.declared_enabled, inherited) {
(Prop::Unset, above) => above,
(own, Prop::Unset) => own,
(Prop::Const(own), Prop::Const(above)) => Prop::Const(own && above),
(own, above) => Prop::Reactive(create_derived(move || {
above.get_or(true) && own.get_or(true)
})),
};
}
pub(super) fn is_focusable(&self) -> bool {
self.interaction.as_ref().is_some_and(|ix| ix.focusable)
}
pub(super) fn is_control(&self) -> bool {
if self.declared_control || self.scroll_axis != ScrollAxis::None {
return true;
}
self.interaction.as_ref().is_some_and(|ix| {
ix.declared_enabled.is_set()
|| ix.has_any_state()
|| ix.on_click.is_some()
|| ix.on_right_click.is_some()
|| ix.on_middle_click.is_some()
|| ix.on_hover.is_some()
|| ix.on_scroll.is_some()
|| ix.on_pointer_move.is_some()
|| ix.on_mouse_down.is_some()
|| ix.on_mouse_up.is_some()
})
}
}
impl Stateful for Container {
type Style = StateStyle;
fn push_state_style(&mut self, when: StateWhen, style: StateStyle) {
let moves = style.moves_anything();
let ix = self.interact_mut();
ix.declares_transform.merge(moves);
ix.states.push((when, style));
}
}
impl Default for Container {
fn default() -> Self {
Self::new()
}
}
impl Widget for Container {
fn advance_animations(&mut self, tree: &mut Tree, id: WidgetId) -> bool {
let mut any_animating = false;
let leaving = self.is_leaving();
if !leaving && is_detached(id) {
return false;
}
let is_visible =
leaving || with_signal_tracking(id, JobType::Animation, || self.visible.get_or(true));
if !is_visible {
return false;
}
let now = tree.frame_instant();
any_animating |= self.advance_declared_animations(id, now);
if let Some(ref mut ix) = self.interaction
&& ix.ripple.is_active()
&& let Some(config) = ix.ripple_config()
{
let ripple_animating = ix.ripple.advance(&config, now);
if ripple_animating {
request_job(id, JobRequest::Animation(RequiredJob::Paint));
}
any_animating = any_animating || ripple_animating;
}
if let Some(ref mut sd) = self.scroll_data {
let has_scroll_velocity =
sd.scroll_state.velocity_x.abs() > 0.5 || sd.scroll_state.velocity_y.abs() > 0.5;
if has_scroll_velocity {
let scroll_animating = sd.scroll_state.advance_momentum(now);
if scroll_animating {
request_job(id, JobRequest::Animation(RequiredJob::Paint));
}
any_animating = any_animating || scroll_animating;
}
}
if self.advance_scrollbar_scale_animations_internal(id, now) {
any_animating = true;
}
if leaving
&& !self.is_exiting()
&& let Some(parent) = tree.get_parent(id)
{
request_job(parent, JobRequest::Reconcile);
}
any_animating
}
fn begin_exit(&mut self, tree: &mut Tree, id: WidgetId) -> bool {
let Some(declared) = self.anims.as_deref_mut() else {
return false;
};
if !declared.slots().any(AnimSlot::declares_exit) {
return false;
}
let now = tree.frame_instant();
let mut moves_the_box = false;
for slot in declared.slots_mut() {
if slot.begin_exit(now) {
moves_the_box |= slot.moves_the_box();
}
}
let follow_up = if moves_the_box {
RequiredJob::Layout
} else {
RequiredJob::Paint
};
request_job(id, JobRequest::Animation(follow_up));
true
}
fn is_exiting(&self) -> bool {
self.anims.as_deref().is_some_and(|declared| {
declared
.slots()
.any(|slot| slot.is_leaving() && slot.is_animating())
})
}
fn cancel_exit(&mut self, tree: &mut Tree, id: WidgetId) {
let Some(declared) = self.anims.as_deref_mut() else {
return;
};
for slot in declared.slots_mut() {
slot.cancel_exit();
}
self.advance_declared_animations(id, tree.frame_instant());
}
fn reconcile_children(&mut self, tree: &mut Tree, id: WidgetId) -> bool {
self.children_source.set_container_id(id);
self.children_source.reconcile_with_tracking(tree)
}
fn register_children(&mut self, tree: &mut Tree, id: WidgetId) {
self.children_source.set_container_id(id);
self.fold_enabled(tree, id);
tree.set_control(
id,
self.is_control()
.then(|| {
self.interaction
.as_ref()
.map(|ix| Control::new(id, ix.flags, ix.enabled))
})
.flatten(),
);
if self
.interaction
.as_ref()
.is_some_and(|ix| ix.on_key_down.is_some() || ix.on_key_up.is_some())
{
tree.listen_for_keys(id);
}
if self.visible.is_set() {
tree.declare_visible(id, self.visible);
}
if self.is_focusable() {
tree.make_tab_stop(id);
}
self.children_source.register_pending(tree, id);
}
fn layout_hints(&self) -> LayoutHints {
if !self.visible.get_or_untracked(true) {
return LayoutHints {
hidden: true,
..LayoutHints::default()
};
}
LayoutHints {
fill_width: self.width.get_or_untracked(Length::default()).is_fill(),
fill_height: self.height.get_or_untracked(Length::default()).is_fill(),
hidden: false,
}
}
fn layout(&mut self, ctx: &mut LayoutCtx, constraints: Constraints) -> Size {
let id = ctx.id();
let is_visible = self.visible.get_or(true);
if !is_visible {
ctx.tree().set_relayout_boundary(id, true);
return Size::zero();
}
self.resolve_scroll();
let boundary = self.is_relayout_boundary_for(constraints);
ctx.tree().set_relayout_boundary(id, boundary);
self.ensure_scrollbar_containers(ctx.tree(), id);
self.seed_animations(ctx, id);
let lengths = self.read_box_lengths(ctx, id, constraints);
let child = self.child_layout(ctx, &lengths, constraints);
let padding = lengths.padding;
let children = self.children_source.reconcile_and_get(ctx.tree());
let content_size = if !children.is_empty() {
self.layout
.layout(ctx, children, child.constraints, child.origin)
} else {
Size::zero()
};
if let Some((child_id, child_baseline)) = children
.iter()
.find_map(|&cid| ctx.tree_ref().baseline(cid).map(|b| (cid, b)))
&& let Some((_, child_y)) = ctx.tree_ref().get_origin(child_id)
{
ctx.tree().set_baseline(id, child_y + child_baseline);
}
self.children_sorted_along = sorted_axis(ctx.tree_ref(), children);
if self.scroll_axis != ScrollAxis::None {
let sd = self.scroll_mut();
sd.scroll_state.content_width = content_size.width + padding.horizontal_total();
sd.scroll_state.content_height = content_size.height + padding.vertical_total();
sd.scroll_state.viewport_width = child.seen_through.width;
sd.scroll_state.viewport_height = child.seen_through.height;
sd.scroll_state.clamp_offsets();
}
self.update_size_targets(ctx, id, &lengths, content_size);
let size = self.resolve_size(ctx, &lengths, constraints, content_size);
let content_least = ctx.minimum().unwrap_or(content_size);
ctx.set_minimum(self.least_size(&lengths, content_least, size));
self.layout_scrollbar_containers(ctx, id, size);
let reach = self.max_shadow_extent(id);
self.shadow_reach.set(reach);
ctx.clips_children(
self.scroll_axis != ScrollAxis::None || lengths.overflow == Overflow::Hidden,
);
if let Some(ref wr) = self.widget_ref {
register_widget_ref(id, *wr);
}
size
}
fn event(&mut self, tree: &mut Tree, id: WidgetId, event: &Event) -> EventResponse {
if !self.visible.get_or(true) {
self.switched_off(tree, id);
return EventResponse::Ignored;
}
let bounds = tree.get_bounds(id).unwrap_or_default();
let placed = self.placed(id, bounds);
let hit = HitContext {
bounds,
corners: self.animated_corners(id),
transform: placed.transform,
pivot: placed.pivot,
};
let local_event: Cow<'_, Event> = match event.coords() {
Some(at) if !hit.transform.is_identity() => Cow::Owned(
event.with_coords(untransform_point(&hit.transform, hit.pivot, hit.bounds, at)),
),
_ => Cow::Borrowed(event),
};
if let Some(ref ix) = self.interaction
&& !ix.declared_enabled.get_or(true)
{
self.switched_off(tree, id);
if hit.contains(local_event.coords()) && self.draws_its_own_surface(id) {
return EventResponse::Hit;
}
return EventResponse::Ignored;
}
self.claim_the_cursor(tree, &hit, &local_event);
let at = tree.event_instant();
let clips_children = self.overflow_resolved.get() == Overflow::Hidden
|| self.scroll_axis != ScrollAxis::None;
let in_clip = clips_children && hit.contains(local_event.coords());
if self.scroll_axis != ScrollAxis::None {
if let Some(response) =
self.handle_scrollbar_event(tree, id, &hit, &local_event, in_clip)
{
return response;
}
if let Some(response) = self.watch_for_a_content_drag(id, &hit, &local_event, at) {
return response;
}
if self
.watched_content_drag()
.is_some_and(|drag| drag.scrolling)
&& let Some(response) =
self.claim_the_content_drag(tree, id, &hit, &local_event, at)
{
return response;
}
}
self.track_pointer(tree, id, &hit, &local_event, at);
let child_event: Cow<'_, Event> = match local_event.coords() {
Some(at) => {
let child_at = hit.rebase(at).offset(placed.scroll.0, placed.scroll.1);
Cow::Owned(local_event.with_coords(Some(child_at)))
}
None => local_event.clone(),
};
let outside_clip = clips_children && local_event.coords().is_some() && !in_clip;
let children = self.children_source.get();
let withheld = outside_clip.then(|| tree.begin_withholding());
let targets = tree.event_targets(
id,
children,
&child_event,
child_event.coords().filter(|_| !outside_clip),
self.children_sorted_along,
);
let mut blind: Option<Event> = None;
let mut offer = |tree: &mut Tree, index: usize, positioned: bool| {
let child_id = children[index];
if is_detached(child_id) {
return EventResponse::Ignored;
}
let event = if positioned {
&*child_event
} else {
&*blind.get_or_insert_with(|| child_event.with_coords(None))
};
tree.with_widget_mut(child_id, |child, child_id, tree| {
child.event(tree, child_id, event)
})
.unwrap_or(EventResponse::Ignored)
};
let stopped_at = targets.into_iter().find_map(|(index, positioned)| {
let response = offer(tree, index, positioned);
(response != EventResponse::Ignored).then_some((index, response))
});
let answered = stopped_at.map_or(EventResponse::Ignored, |(_, response)| response);
if let Some((top, _)) = stopped_at
&& child_event.coords().is_some()
&& tree.owes_a_child_below(id, top)
{
let was = tree.begin_withholding();
let owed =
tree.event_targets(id, children, &child_event, None, self.children_sorted_along);
for (index, positioned) in owed.filter(|&(index, _)| index < top) {
offer(tree, index, positioned);
}
tree.end_withholding(was);
}
if let Some(was) = withheld {
tree.end_withholding(was);
}
if self.watched_content_drag().is_some()
&& let Some(response) = self.claim_the_content_drag(tree, id, &hit, &local_event, at)
{
return response;
}
if answered == EventResponse::Handled {
return EventResponse::Handled;
}
match self.handle_own_event(tree, id, &hit, event, &local_event, at) {
EventResponse::Ignored
if answered == EventResponse::Hit || self.is_hit(id, &hit, &local_event) =>
{
EventResponse::Hit
}
own => own,
}
}
fn refresh_paint_bounds(&self, tree: &mut Tree, id: WidgetId) {
let size = tree.cached_size(id).unwrap_or_default();
self.publish_paint_reach(tree, id, Rect::from_size(size));
}
fn placement(&self, tree: &Tree, id: WidgetId) -> Placement {
let Some(bounds) = tree.get_bounds(id) else {
return Placement::NONE;
};
let placed = self.placed(id, bounds);
Placement {
own: placed.transform.about(
placed.pivot,
Rect::from_size(Size::new(bounds.width, bounds.height)),
),
scroll: placed.scroll,
}
}
fn paint(&self, ctx: &mut PaintContext) {
let (tree, id) = (ctx.tree(), ctx.id());
let is_visible = self.visible.get_or(true);
if !is_visible {
return;
}
let bounds = tree.get_bounds(id).unwrap_or_default();
let placed = self.placed(id, bounds);
let (
background,
corners,
shadow,
user_transform,
pivot,
border_width,
border_color,
gradient,
backdrop_blur,
takes_input,
overflow,
opacity,
) = (
self.animated_background(id),
self.animated_corners(id),
self.animated_shadow(id),
placed.transform,
placed.pivot,
self.animated_border_width(id),
self.animated_border_color(id),
self.effective_gradient(id),
self.backdrop_blur.get(),
self.takes_input.get(),
self.overflow.get_or(Overflow::Visible),
self.animated_opacity(id),
);
self.overflow_resolved.set(overflow);
self.resync_animation_targets(id);
let (corner_radii, corner_curvature) = (corners.radii, corners.curvature);
let local_bounds = Rect::new(0.0, 0.0, bounds.width, bounds.height);
ctx.set_bounds(local_bounds);
if !user_transform.is_identity() {
ctx.apply_transform_with_pivot(user_transform, pivot);
}
ctx.set_opacity(opacity);
if let Some(blur) = backdrop_blur
&& blur.asks_for_anything()
{
ctx.draw_backdrop_blur(
local_bounds,
blur.sources,
blur.radius,
corner_radii,
corner_curvature,
);
}
if let Some(takes) = takes_input {
ctx.declare_input_region(local_bounds, corner_radii, corner_curvature, takes);
}
self.paint_decoration(
ctx,
local_bounds,
&Decoration {
background,
gradient,
corner_radii,
corner_curvature,
shadow,
border_width,
border_color,
},
);
let is_scrollable = self.scroll_axis != ScrollAxis::None;
if is_scrollable || overflow == Overflow::Hidden {
ctx.set_clip(local_bounds, corner_radii, corner_curvature);
}
let effective_cull_rect = if is_scrollable {
Some(Rect::new(
placed.scroll.0,
placed.scroll.1,
local_bounds.width,
local_bounds.height,
))
} else {
ctx.cull_rect()
};
if bounds.width < 0.5 || bounds.height < 0.5 {
return;
}
let all_children = self.children_source.get();
paint_children(
ctx,
all_children,
&ChildPaintOptions {
scroll_offset: placed.scroll,
cull_rect: effective_cull_rect,
children_sorted_along: self.children_sorted_along,
children_reach: tree.children_reach(id),
cache_requires_full_visibility: is_scrollable,
},
);
if is_scrollable {
self.paint_scrollbar_containers(ctx);
}
if let Some(ref ix) = self.interaction
&& let Some(ripple_config) = ix.ripple_config()
&& ix.ripple.iter().any(|r| r.opacity() > 0.0)
{
ctx.set_overlay_clip(local_bounds, corner_radii, corner_curvature);
let declared = ripple_config.color.get();
for ripple in ix.ripple.iter() {
let opacity = ripple.opacity();
if opacity <= 0.0 {
continue;
}
let (center_x, center_y) = ripple.center(bounds.width, bounds.height);
let radius = ripple.radius(bounds.width, bounds.height);
let ripple_color =
Color::rgba(declared.r, declared.g, declared.b, declared.a * opacity);
ctx.draw_overlay_circle(center_x, center_y, radius, ripple_color);
}
}
}
}
fn sorted_axis(tree: &Tree, children: &[WidgetId]) -> Option<Axis> {
if children.is_empty() {
return None;
}
let (mut vertical, mut horizontal) = (true, true);
let (mut bottom, mut right) = (f32::NEG_INFINITY, f32::NEG_INFINITY);
for &child in children {
let bounds = tree.get_bounds(child)?;
let (top, next_bottom) = bounds.span(Axis::Vertical);
let (left, next_right) = bounds.span(Axis::Horizontal);
vertical &= top >= bottom;
horizontal &= left >= right;
if !(vertical || horizontal) {
return None;
}
(bottom, right) = (next_bottom, next_right);
}
Some(if vertical {
Axis::Vertical
} else {
Axis::Horizontal
})
}
pub(crate) fn declare<A: Default, T: Animatable, M>(
anims: &mut Option<Box<A>>,
value: impl IntoAnimated<T, M>,
slot: impl FnOnce(&mut A) -> &mut Option<AnimationState<T>>,
) -> Prop<T> {
let (prop, installed) = declared_motion(value);
match anims.as_deref_mut() {
Some(anims) => *slot(anims) = installed,
None if installed.is_some() => *slot(anims.get_or_insert_with(Box::default)) = installed,
None => {}
}
prop
}
pub(crate) fn declare_anim<T: Animatable, M>(
anims: &mut Option<Box<ContainerAnims>>,
value: impl IntoAnimated<T, M>,
kind: AnimKind,
into_slot: fn(AnimationState<T>) -> AnimSlot,
) -> Prop<T> {
let (prop, installed) = declared_motion(value);
put(anims, kind, into_slot, installed);
prop
}
fn declared_motion<T: Animatable, M>(
value: impl IntoAnimated<T, M>,
) -> (Prop<T>, Option<AnimationState<T>>) {
let (prop, motion) = value.into_animated().into_parts();
let installed = motion.map(|motion| install(declared_seed(&prop), *motion));
(prop, installed)
}
fn declared_seed<T: Clone + 'static>(prop: &Prop<T>) -> T {
prop.get_untracked().expect(
"an `Animated` always carries a value — it is built by `into_animated`, \
inside the setter that immediately consumes it",
)
}
fn install<T: Animatable>(seed: T, motion: Motion<T>) -> AnimationState<T> {
match motion {
Motion::Ease {
config,
enter_from,
exit_to,
} => AnimationState::new(seed, config)
.with_enter_from(enter_from)
.with_exit_to(exit_to),
Motion::Play { keyframes } => {
AnimationState::new(seed, instant_transition()).with_timeline(keyframes)
}
}
}
fn put<T: Animatable>(
anims: &mut Option<Box<ContainerAnims>>,
kind: AnimKind,
into_slot: fn(AnimationState<T>) -> AnimSlot,
installed: Option<AnimationState<T>>,
) {
match (anims.as_deref_mut(), installed) {
(Some(anims), Some(anim)) => anims.set(into_slot(anim)),
(Some(anims), None) => anims.clear(kind),
(None, Some(anim)) => anims.get_or_insert_with(Box::default).set(into_slot(anim)),
(None, None) => {}
}
}
fn declare_size<M>(
anims: &mut Option<Box<ContainerAnims>>,
value: impl IntoAnimated<Length, M>,
kind: AnimKind,
into_slot: fn(AnimationState<f32>) -> AnimSlot,
) -> Prop<Length> {
let (prop, ease) = value.into_animated().into_eased();
let resolved = |length: Length| length.exact_size().or(length.min()).unwrap_or(0.0);
let installed = ease.map(|(config, enter_from, exit_to)| {
install(
resolved(declared_seed(&prop)),
Motion::Ease {
config,
enter_from: enter_from.map(resolved),
exit_to: exit_to.map(|exit_to| {
Box::new(move || resolved(exit_to())) as crate::animation::ExitTo<f32>
}),
},
)
});
put(anims, kind, into_slot, installed);
prop
}
impl Container {
pub(super) fn is_leaving(&self) -> bool {
self.anims
.as_deref()
.is_some_and(|declared| declared.slots().any(AnimSlot::is_leaving))
}
}
pub fn container() -> Container {
Container::new()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::animation::{Animate, TimingFunction, Transition};
use crate::jobs::{self, Job};
use crate::layout::Constraints;
use crate::reactive::create_signal;
#[test]
fn write_between_first_layout_and_first_paint_starts_animation() {
let open = create_signal(false);
let widget = container().scale(
(move || {
if open.get() {
Scale::NONE
} else {
Scale::new(1.0, 0.0)
}
})
.transition(Transition::new(200, TimingFunction::EaseOut)),
);
let mut tree = Tree::new();
let id = tree.register(Box::new(widget));
tree.layout_widget(id, Constraints::new(0.0, 0.0, 100.0, 100.0));
open.set(true);
let roots: rustc_hash::FxHashSet<WidgetId> = [id].into_iter().collect();
jobs::distribute_jobs(&tree, &roots);
jobs::recycle_job_buffer(jobs::drain_surface_jobs(id));
jobs::recycle_job_buffer(jobs::drain_orphan_jobs());
let mut root_node = crate::renderer::RenderNode::new(id.as_u64());
tree.paint_widget(id, &mut root_node, 1.0);
jobs::distribute_jobs(&tree, &roots);
let drained = jobs::drain_surface_jobs(id);
assert!(
drained.contains(&Job {
widget_id: id,
job_type: JobType::Animation,
}),
"paint must request an Animation job for the missed target change, got {drained:?}"
);
}
#[test]
fn a_measure_reads_animation_targets() {
let height_sig = create_signal(100.0_f32);
let widget = container().width(50.0).height(
(move || height_sig.get()).transition(Transition::new(200, TimingFunction::EaseOut)),
);
let mut tree = Tree::new();
let id = tree.register(Box::new(widget));
let render = Constraints::new(0.0, 0.0, 400.0, 400.0);
let measure = Constraints::new(0.0, 0.0, 500.0, 500.0);
tree.layout_widget(id, render);
height_sig.set(180.0);
let mid = tree.layout_widget(id, render).unwrap();
assert!(
mid.height < 180.0,
"mid-animation layout should not have reached the target yet (got {})",
mid.height
);
let fin = tree.measure_widget(id, measure).unwrap();
assert_eq!(
fin.height, 180.0,
"a measure has to report the animation target"
);
}
#[test]
fn padding_write_after_first_layout_schedules_animation() {
let pad = create_signal(4.0_f32);
let widget = container()
.padding((move || pad.get()).transition(Transition::new(200, TimingFunction::EaseOut)));
let mut tree = Tree::new();
let id = tree.register(Box::new(widget));
tree.layout_widget(id, Constraints::new(0.0, 0.0, 100.0, 100.0));
let roots: rustc_hash::FxHashSet<WidgetId> = [id].into_iter().collect();
jobs::distribute_jobs(&tree, &roots);
jobs::recycle_job_buffer(jobs::drain_surface_jobs(id));
jobs::recycle_job_buffer(jobs::drain_orphan_jobs());
pad.set(12.0);
jobs::distribute_jobs(&tree, &roots);
let drained = jobs::drain_surface_jobs(id);
assert!(
drained.contains(&Job {
widget_id: id,
job_type: JobType::Animation,
}),
"a write after first layout must schedule an Animation job \
without waiting for a paint, got {drained:?}"
);
}
#[test]
fn a_layout_property_invalidates_its_container() {
let spacing = create_signal(4.0f32);
let widget = container()
.layout(crate::layout::Flex::row().spacing(move || spacing.get()))
.child(container().width(10.0).height(10.0))
.child(container().width(10.0).height(10.0));
let mut tree = Tree::new();
let id = tree.register(Box::new(widget));
tree.with_widget_mut(id, |w, id, tree| w.register_children(tree, id));
let size = tree
.layout_widget(id, Constraints::new(0.0, 0.0, 500.0, 500.0))
.unwrap();
assert_eq!(size.width, 24.0, "10 + spacing 4 + 10");
let roots: rustc_hash::FxHashSet<WidgetId> = [id].into_iter().collect();
jobs::distribute_jobs(&tree, &roots);
jobs::recycle_job_buffer(jobs::drain_surface_jobs(id));
jobs::recycle_job_buffer(jobs::drain_orphan_jobs());
spacing.set(20.0);
jobs::distribute_jobs(&tree, &roots);
let drained = jobs::drain_surface_jobs(id);
assert!(
drained.contains(&Job {
widget_id: id,
job_type: JobType::Layout,
}),
"writing a layout's reactive property must queue a Layout job, got {drained:?}"
);
}
}