added transitions (#29)

This commit is contained in:
Cameron Campbell
2026-06-04 20:26:15 -04:00
committed by GitHub
parent 01335f7698
commit 81af1059a2
35 changed files with 1357 additions and 94 deletions
+4
View File
@@ -210,6 +210,10 @@ path = "examples/learn/animation.rs"
name = "text"
path = "examples/learn/text.rs"
[[example]]
name = "transition"
path = "examples/learn/transition.rs"
# ============================================================================
# Bench Examples - Performance benchmarks
# ============================================================================
+3 -3
View File
@@ -1,9 +1,9 @@
use std::{ops::Range, rc::Rc, time::Duration};
use gpui::{
canvas, div, point, prelude::*, px, rgb, size, uniform_list, App, Application, Bounds, Context,
MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, Render, SharedString,
UniformListScrollHandle, Window, WindowBounds, WindowOptions,
App, Application, Bounds, Context, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point,
Render, SharedString, UniformListScrollHandle, Window, WindowBounds, WindowOptions, canvas,
div, point, prelude::*, px, rgb, size, uniform_list,
};
const TOTAL_ITEMS: usize = 10000;
+3 -3
View File
@@ -1,7 +1,7 @@
use gpui::{
canvas, div, linear_color_stop, linear_gradient, point, prelude::*, px, rgb, size, Application,
Background, Bounds, ColorSpace, Context, Path, PathBuilder, Pixels, Render, TitlebarOptions,
Window, WindowBounds, WindowOptions,
Application, Background, Bounds, ColorSpace, Context, Path, PathBuilder, Pixels, Render,
TitlebarOptions, Window, WindowBounds, WindowOptions, canvas, div, linear_color_stop,
linear_gradient, point, prelude::*, px, rgb, size,
};
const DEFAULT_WINDOW_WIDTH: Pixels = px(1024.0);
+2 -2
View File
@@ -1,6 +1,6 @@
use gpui::{
div, linear_color_stop, linear_gradient, pattern_slash, prelude::*, px, rgb, size, App,
AppContext, Application, Bounds, Context, Window, WindowBounds, WindowOptions,
App, AppContext, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div,
linear_color_stop, linear_gradient, pattern_slash, prelude::*, px, rgb, size,
};
struct PatternExample;
+2 -2
View File
@@ -1,6 +1,6 @@
use gpui::{
current_platform, div, hsla, point, prelude::*, px, relative, rgb, size, App, Application,
Bounds, BoxShadow, Context, Div, SharedString, Window, WindowBounds, WindowOptions,
App, Application, Bounds, BoxShadow, Context, Div, SharedString, Window, WindowBounds,
WindowOptions, current_platform, div, hsla, point, prelude::*, px, relative, rgb, size,
};
struct Shadow {}
+2 -2
View File
@@ -15,9 +15,9 @@ use std::time::Duration;
use anyhow::Result;
use gpui::colors::Colors;
use gpui::{
bounce, div, ease_in_out, linear, percentage, prelude::*, px, rgb, size as gpui_size, svg,
Animation, AnimationExt as _, App, Application, AssetSource, Bounds, Context, Hsla,
SharedString, Transformation, Window, WindowBounds, WindowOptions,
SharedString, Transformation, Window, WindowBounds, WindowOptions, bounce, div, ease_in_out,
linear, percentage, prelude::*, px, rgb, size as gpui_size, svg,
};
struct Assets {}
+18 -16
View File
@@ -14,8 +14,8 @@ use std::time::Duration;
use gpui::colors::Colors;
use gpui::{
div, prelude::*, px, rgb, size, App, Application, Bounds, Context, Entity, Render, Task,
Window, WindowBounds, WindowOptions,
App, Application, Bounds, Context, Entity, Render, Task, Window, WindowBounds, WindowOptions,
div, prelude::*, px, rgb, size,
};
// Example 1: Simple Foreground Task
@@ -138,22 +138,24 @@ impl CancellableTaskDemo {
self.counting_task = None;
cx.notify();
} else {
self.counting_task = Some(cx.spawn(async move |this, cx| loop {
cx.background_spawn(async {
std::thread::sleep(Duration::from_millis(100));
})
.await;
let should_continue = this
.update(cx, |this, cx| {
this.counter += 1;
cx.notify();
true
self.counting_task = Some(cx.spawn(async move |this, cx| {
loop {
cx.background_spawn(async {
std::thread::sleep(Duration::from_millis(100));
})
.unwrap_or(false);
.await;
if !should_continue {
break;
let should_continue = this
.update(cx, |this, cx| {
this.counter += 1;
cx.notify();
true
})
.unwrap_or(false);
if !should_continue {
break;
}
}
}));
cx.notify();
@@ -13,8 +13,8 @@ mod example_prelude;
use example_prelude::init_example;
use gpui::colors::Colors;
use gpui::{
div, prelude::*, px, rgb, size, App, Application, Bounds, Context, Entity, IntoElement, Render,
RenderOnce, Window, WindowBounds, WindowOptions,
App, Application, Bounds, Context, Entity, IntoElement, Render, RenderOnce, Window,
WindowBounds, WindowOptions, div, prelude::*, px, rgb, size,
};
// ============================================================================
+3 -3
View File
@@ -9,9 +9,9 @@
use gpui::colors::Colors;
use gpui::{
canvas, div, fill, point, prelude::*, px, rgb, size, App, Application, Bounds, Context, Hsla,
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, PathBuilder, Pixels, Point,
Render, Rgba, Window, WindowBounds, WindowOptions,
App, Application, Bounds, Context, Hsla, MouseButton, MouseDownEvent, MouseMoveEvent,
MouseUpEvent, Path, PathBuilder, Pixels, Point, Render, Rgba, Window, WindowBounds,
WindowOptions, canvas, div, fill, point, prelude::*, px, rgb, size,
};
#[path = "../prelude.rs"]
@@ -13,9 +13,9 @@ mod example_prelude;
use example_prelude::init_example;
use gpui::colors::Colors;
use gpui::{
div, prelude::*, px, rgb, size, App, Application, Bounds, ClickEvent, Context, Entity, Half,
Hsla, IntoElement, MouseButton, MouseMoveEvent, Pixels, Point, Render, Window, WindowBounds,
WindowOptions,
App, Application, Bounds, ClickEvent, Context, Entity, Half, Hsla, IntoElement, MouseButton,
MouseMoveEvent, Pixels, Point, Render, Window, WindowBounds, WindowOptions, div, prelude::*,
px, rgb, size,
};
// ============================================================================
+2 -2
View File
@@ -12,8 +12,8 @@ mod example_prelude;
use example_prelude::init_example;
use gpui::colors::Colors;
use gpui::{
div, prelude::*, px, size, App, Application, Bounds, Context, Div, Hsla, Render, Rgba, Window,
WindowBounds, WindowOptions,
App, Application, Bounds, Context, Div, Hsla, Render, Rgba, Window, WindowBounds,
WindowOptions, div, prelude::*, px, size,
};
// Helper: Colored block for visualization
+2 -2
View File
@@ -8,8 +8,8 @@
use gpui::colors::Colors;
use gpui::{
actions, div, prelude::*, px, rgb, size, App, Application, Bounds, Context, FocusHandle, Hsla,
KeyBinding, Menu, MenuItem, Render, Rgba, Window, WindowBounds, WindowOptions,
App, Application, Bounds, Context, FocusHandle, Hsla, KeyBinding, Menu, MenuItem, Render, Rgba,
Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, rgb, size,
};
actions!(styling_example, [Quit, Tab, TabPrev]);
+3 -3
View File
@@ -14,9 +14,9 @@ mod example_prelude;
use example_prelude::init_example;
use gpui::{
colors::Colors, current_platform, div, prelude::*, px, relative, rgb, size, App, Application,
Bounds, Context, FontStyle, FontWeight, Hsla, Render, StyledText, TextOverflow, Window,
WindowBounds, WindowOptions,
App, Application, Bounds, Context, FontStyle, FontWeight, Hsla, Render, StyledText,
TextOverflow, Window, WindowBounds, WindowOptions, colors::Colors, current_platform, div,
prelude::*, px, relative, rgb, size,
};
// Text Styling Examples
+109
View File
@@ -0,0 +1,109 @@
//! Transition Example
//!
//! This example demonstrates transition capabilities in GPUI via `use_keyed_transition`.
#[path = "../prelude.rs"]
mod example_prelude;
use std::time::Duration;
use gpui::{
AnyElement, App, AppContext, Bounds, Context, ElementId, Lerp, Rgba, Window, WindowBounds,
WindowOptions, actions, div, ease_in_out, prelude::*, px, rgb, size,
};
use smallvec::SmallVec;
actions!(app, [Quit]);
#[derive(IntoElement)]
struct Button {
id: ElementId,
children: SmallVec<[AnyElement; 2]>,
}
impl Button {
fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
children: SmallVec::new(),
}
}
}
impl ParentElement for Button {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements);
}
}
impl RenderOnce for Button {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
const HOVER_STRENGTH: f32 = 0.3;
let base_color: Rgba = rgb(0x663399);
let hover_transition = window
.use_keyed_transition(
(self.id.clone(), "hover"),
cx,
Duration::from_millis(300),
|_window, _cx| 0.,
)
.with_easing(ease_in_out);
let bg_color = base_color.lerp(
&rgb(0x000),
*hover_transition.evaluate(window, cx) * HOVER_STRENGTH,
);
div()
.id(self.id)
.cursor_pointer()
.rounded(px(100.))
.pl(px(14.))
.pr(px(14.))
.pt(px(10.))
.pb(px(10.))
.bg(bg_color)
.text_color(rgb(0x110F15))
.children(self.children)
.on_hover(move |hover, _window, cx| {
hover_transition.update(cx, |this, cx| {
*this = *hover as u8 as f32;
cx.notify();
});
})
}
}
struct TransitionExample;
impl Render for TransitionExample {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.size_full()
.flex()
.justify_center()
.items_center()
.absolute()
.bg(rgb(0x110F15))
.gap(px(20.))
.p(px(100.))
.child(Button::new("btn").child("Click me!"))
}
}
fn main() {
gpui_platform::application().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(500.), px(650.)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|_| TransitionExample),
)
.expect("Failed to open window");
example_prelude::init_example(cx, "Transition");
});
}
+2 -2
View File
@@ -1,6 +1,6 @@
use gpui::{
actions, div, prelude::*, px, size, App, Application, Bounds, Context, Div, ElementId,
FocusHandle, KeyBinding, SharedString, Stateful, Window, WindowBounds, WindowOptions,
App, Application, Bounds, Context, Div, ElementId, FocusHandle, KeyBinding, SharedString,
Stateful, Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, size,
};
actions!(example, [Tab, TabPrev, Quit]);
+1 -1
View File
@@ -1,4 +1,4 @@
use gpui::{div, img, prelude::*, App, Application, Context, Render, Window, WindowOptions};
use gpui::{App, Application, Context, Render, Window, WindowOptions, div, img, prelude::*};
use std::path::PathBuf;
struct GifViewer {
+2 -2
View File
@@ -1,6 +1,6 @@
use gpui::{
canvas, div, linear_color_stop, linear_gradient, point, prelude::*, px, size, App, Application,
Bounds, ColorSpace, Context, Half, Render, Window, WindowOptions,
App, Application, Bounds, ColorSpace, Context, Half, Render, Window, WindowOptions, canvas,
div, linear_color_stop, linear_gradient, point, prelude::*, px, size,
};
struct GradientViewer {
+2 -2
View File
@@ -1,6 +1,6 @@
use gpui::{
div, prelude::*, px, rgb, size, App, Application, Bounds, Context, SharedString, Window,
WindowBounds, WindowOptions,
App, Application, Bounds, Context, SharedString, Window, WindowBounds, WindowOptions, div,
prelude::*, px, rgb, size,
};
struct HelloWorld {
+4 -4
View File
@@ -1,10 +1,10 @@
use std::{path::Path, sync::Arc, time::Duration};
use gpui::{
black, div, img, prelude::*, pulsating_between, px, red, size, Animation, AnimationExt, App,
Application, Asset, AssetLogger, AssetSource, Bounds, Context, Hsla, ImageAssetLoader,
ImageCacheError, ImgResourceLoader, Length, RenderImage, Resource, SharedString, Window,
WindowBounds, WindowOptions, LOADING_DELAY,
Animation, AnimationExt, App, Application, Asset, AssetLogger, AssetSource, Bounds, Context,
Hsla, ImageAssetLoader, ImageCacheError, ImgResourceLoader, LOADING_DELAY, Length, RenderImage,
Resource, SharedString, Window, WindowBounds, WindowOptions, black, div, img, prelude::*,
pulsating_between, px, red, size,
};
struct Assets {}
+10 -8
View File
@@ -11,20 +11,22 @@ mod example {
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use gpui::{
div, layer_shell::*, point, prelude::*, px, rems, rgba, white, App, Application, Bounds,
Context, FontWeight, Size, Window, WindowBackgroundAppearance, WindowBounds, WindowKind,
WindowOptions,
App, Application, Bounds, Context, FontWeight, Size, Window, WindowBackgroundAppearance,
WindowBounds, WindowKind, WindowOptions, div, layer_shell::*, point, prelude::*, px, rems,
rgba, white,
};
struct LayerShellExample;
impl LayerShellExample {
fn new(cx: &mut Context<Self>) -> Self {
cx.spawn(async move |this, cx| loop {
let _ = this.update(cx, |_, cx| cx.notify());
cx.background_executor()
.timer(Duration::from_millis(500))
.await;
cx.spawn(async move |this, cx| {
loop {
let _ = this.update(cx, |_, cx| cx.notify());
cx.background_executor()
.timer(Duration::from_millis(500))
.await;
}
})
.detach();
@@ -1,6 +1,6 @@
use gpui::{
actions, div, prelude::*, px, rgb, size, App, Application, Bounds, Context, FocusHandle,
KeyBinding, Window, WindowBounds, WindowOptions,
App, Application, Bounds, Context, FocusHandle, KeyBinding, Window, WindowBounds,
WindowOptions, actions, div, prelude::*, px, rgb, size,
};
actions!(example, [CloseWindow]);
+2 -2
View File
@@ -2,8 +2,8 @@ use std::{fs, path::PathBuf};
use anyhow::Result;
use gpui::{
div, hsla, img, point, prelude::*, px, rgb, size, svg, App, Application, AssetSource, Bounds,
BoxShadow, ClickEvent, Context, SharedString, Task, Window, WindowBounds, WindowOptions,
App, Application, AssetSource, Bounds, BoxShadow, ClickEvent, Context, SharedString, Task,
Window, WindowBounds, WindowOptions, div, hsla, img, point, prelude::*, px, rgb, size, svg,
};
struct Assets {
+2 -2
View File
@@ -1,6 +1,6 @@
use gpui::{
div, prelude::*, px, size, App, Application, Bounds, Context, Window, WindowBounds,
WindowOptions,
App, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px,
size,
};
struct Scrollable {}
+2 -2
View File
@@ -3,8 +3,8 @@ use std::path::PathBuf;
use anyhow::Result;
use gpui::{
current_platform, div, prelude::*, px, rgb, size, svg, App, Application, AssetSource, Bounds,
Context, SharedString, Window, WindowBounds, WindowOptions,
App, Application, AssetSource, Bounds, Context, SharedString, Window, WindowBounds,
WindowOptions, current_platform, div, prelude::*, px, rgb, size, svg,
};
struct Assets {
+2 -2
View File
@@ -1,6 +1,6 @@
use gpui::{
actions, div, prelude::*, px, size, App, Application, Bounds, Context, Div, ElementId,
FocusHandle, KeyBinding, SharedString, Stateful, Window, WindowBounds, WindowOptions,
App, Application, Bounds, Context, Div, ElementId, FocusHandle, KeyBinding, SharedString,
Stateful, Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, size,
};
actions!(example, [Tab, TabPrev]);
+2 -2
View File
@@ -3,8 +3,8 @@
use std::sync::LazyLock;
use gpui::{
div, prelude::*, px, size, App, Application, Bounds, Context, Window, WindowBounds,
WindowOptions,
App, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px,
size,
};
struct Tree {}
+2 -2
View File
@@ -1,6 +1,6 @@
use gpui::{
div, prelude::*, px, rgb, size, uniform_list, App, Application, Bounds, Context, Window,
WindowBounds, WindowOptions,
App, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px,
rgb, size, uniform_list,
};
struct UniformListExample {}
+2 -2
View File
@@ -1,6 +1,6 @@
use gpui::{
actions, div, prelude::*, px, rgb, size, App, Application, Bounds, Context, KeyBinding,
PromptButton, PromptLevel, Window, WindowBounds, WindowKind, WindowOptions,
App, Application, Bounds, Context, KeyBinding, PromptButton, PromptLevel, Window, WindowBounds,
WindowKind, WindowOptions, actions, div, prelude::*, px, rgb, size,
};
struct SubWindow {
@@ -1,7 +1,7 @@
use gpui::{
div, point, prelude::*, px, rgb, App, Application, Bounds, Context, DisplayId, Hsla, Pixels,
SharedString, Size, Window, WindowBackgroundAppearance, WindowBounds, WindowKind,
WindowOptions,
App, Application, Bounds, Context, DisplayId, Hsla, Pixels, SharedString, Size, Window,
WindowBackgroundAppearance, WindowBounds, WindowKind, WindowOptions, div, point, prelude::*,
px, rgb,
};
struct WindowContent {
+3 -3
View File
@@ -1,8 +1,8 @@
use gpui::{
black, canvas, div, green, point, prelude::*, px, rgb, size, transparent_black, white, App,
Application, Bounds, Context, CursorStyle, Decorations, HitboxBehavior, Hsla, MouseButton,
App, Application, Bounds, Context, CursorStyle, Decorations, HitboxBehavior, Hsla, MouseButton,
Pixels, Point, ResizeEdge, Size, Window, WindowBackgroundAppearance, WindowBounds,
WindowDecorations, WindowOptions,
WindowDecorations, WindowOptions, black, canvas, div, green, point, prelude::*, px, rgb, size,
transparent_black, white,
};
struct WindowShadow {}
+1 -1
View File
@@ -8,7 +8,7 @@
//! use example_prelude::init_example;
//! ```
use gpui::{actions, App, KeyBinding, Menu, MenuItem, SharedString};
use gpui::{App, KeyBinding, Menu, MenuItem, SharedString, actions};
actions!(example, [Quit, CloseWindow]);
+4
View File
@@ -30,6 +30,7 @@ mod inspector;
mod interactive;
mod key_dispatch;
mod keymap;
mod lerp;
mod path_builder;
mod platform;
pub mod prelude;
@@ -49,6 +50,7 @@ mod taffy;
#[cfg(any(test, feature = "test-support"))]
pub mod test;
mod text_system;
mod transition;
mod util;
mod view;
mod window;
@@ -104,6 +106,7 @@ pub use inspector::*;
pub use interactive::*;
use key_dispatch::*;
pub use keymap::*;
pub use lerp::*;
pub use path_builder::*;
pub use platform::*;
pub use profiler::*;
@@ -123,6 +126,7 @@ pub use taffy::{AvailableSpace, LayoutId};
#[cfg(any(test, feature = "test-support"))]
pub use test::*;
pub use text_system::*;
pub use transition::*;
pub use util::{FutureExt, Timeout};
pub use view::*;
pub use window::*;
+383
View File
@@ -0,0 +1,383 @@
//! Lerp trait defines behaviour for interpolating between two values of the same type.
use std::{
fmt::Debug,
ops::{Add, Mul, Sub},
};
use crate::{
Bounds, Corners, DevicePixels, Edges, Percentage, Pixels, Point, Radians, Rems, Rgba, Size,
colors::Colors,
};
/// A trait for types that can be linearly interpolated.
pub trait Lerp<Output = Self>
where
Self: Sized,
{
/// Interpolates between `self` and `to` based on `delta`.
fn lerp(&self, to: &Self, delta: f32) -> Output;
}
impl Lerp<f32> for bool {
fn lerp(&self, to: &Self, delta: f32) -> f32 {
lerp(*self as u8 as f32, *to as u8 as f32, delta)
}
}
macro_rules! float_lerps {
( $( $ty:ty ),+ ) => {
$(
impl Lerp for $ty {
fn lerp(&self, to: &Self, delta: f32) -> Self {
lerp(*self, *to, delta as $ty)
}
}
)+
};
}
float_lerps!(f32, f64);
macro_rules! int_lerps {
( $( $ty:ident as $ty_into:ident ),+ ) => {
$(
impl Lerp for $ty {
fn lerp(&self, to: &Self, delta: f32) -> Self {
lerp(*self as $ty_into, *to as $ty_into, delta as $ty_into) as $ty
}
}
)+
};
}
int_lerps!(
usize as f32,
u8 as f32,
u16 as f32,
u32 as f32,
u64 as f64,
u128 as f64,
isize as f32,
i8 as f32,
i16 as f32,
i32 as f32,
i64 as f64,
i128 as f64
);
macro_rules! struct_lerps {
( $( $ty:ident $( < $gen:ident > )? { $( $n:ident ),+ } ),+ $(,)? ) => {
$(
impl$(<$gen: Lerp + Clone + Debug + Default + PartialEq>)? Lerp for $ty$(<$gen>)? {
fn lerp(&self, to: &Self, delta: f32) -> Self {
$ty$(::<$gen>)? {
$(
$n: self.$n.lerp(&to.$n, delta)
),+
}
}
}
)+
};
}
struct_lerps!(
Point<T> { x, y },
Size<T> { width, height },
Edges<T> { top, right, bottom, left },
Corners<T> { top_left, top_right, bottom_right, bottom_left },
Bounds<T> { origin, size },
Rgba { r, g, b, a },
Colors { text, selected_text, background, disabled, selected, border, separator, container }
);
macro_rules! tuple_struct_lerps {
( $( $ty:ident ( $n:ty ) ),+ ) => {
$(
impl Lerp for $ty {
fn lerp(&self, to: &Self, delta: f32) -> Self {
$ty(self.0.lerp(&to.0, delta))
}
}
)+
};
}
tuple_struct_lerps!(
Radians(f32),
Percentage(f32),
DevicePixels(i32),
Rems(f32),
Pixels(f32)
);
fn lerp<T>(from: T, to: T, alpha: T) -> T
where
T: Copy + Add<Output = T> + Sub<Output = T> + Mul<Output = T>,
{
from + (to - from) * alpha
}
#[cfg(all(test, feature = "test-support"))]
mod tests {
use super::*;
use crate::px;
#[test]
fn test_f32_lerp() {
let start = 0.0_f32;
let end = 100.0_f32;
assert_eq!(start.lerp(&end, 0.0), 0.0);
assert_eq!(start.lerp(&end, 0.5), 50.0);
assert_eq!(start.lerp(&end, 1.0), 100.0);
assert_eq!(start.lerp(&end, 0.25), 25.0);
}
#[test]
fn test_f64_lerp() {
let start = 0.0_f64;
let end = 100.0_f64;
assert_eq!(start.lerp(&end, 0.0), 0.0);
assert_eq!(start.lerp(&end, 0.5), 50.0);
assert_eq!(start.lerp(&end, 1.0), 100.0);
}
#[test]
fn test_f32_lerp_negative_values() {
let start = -50.0_f32;
let end = 50.0_f32;
assert_eq!(start.lerp(&end, 0.0), -50.0);
assert_eq!(start.lerp(&end, 0.5), 0.0);
assert_eq!(start.lerp(&end, 1.0), 50.0);
}
#[test]
fn test_integer_lerp() {
// i32
assert_eq!(0_i32.lerp(&100_i32, 0.0), 0);
assert_eq!(0_i32.lerp(&100_i32, 0.5), 50);
assert_eq!(0_i32.lerp(&100_i32, 1.0), 100);
// u8
assert_eq!(0_u8.lerp(&100_u8, 0.5), 50);
// u16
assert_eq!(0_u16.lerp(&1000_u16, 0.5), 500);
// u32
assert_eq!(0_u32.lerp(&10000_u32, 0.5), 5000);
// u64
assert_eq!(0_u64.lerp(&100000_u64, 0.5), 50000);
// i64
assert_eq!((-50000_i64).lerp(&50000_i64, 0.5), 0);
// usize
assert_eq!(0_usize.lerp(&100_usize, 0.5), 50);
// isize
assert_eq!((-100_isize).lerp(&100_isize, 0.5), 0);
}
#[test]
fn test_point_lerp() {
let start: Point<f32> = Point { x: 0.0, y: 0.0 };
let end: Point<f32> = Point { x: 100.0, y: 200.0 };
let mid = start.lerp(&end, 0.5);
assert_eq!(mid.x, 50.0);
assert_eq!(mid.y, 100.0);
let at_start = start.lerp(&end, 0.0);
assert_eq!(at_start.x, 0.0);
assert_eq!(at_start.y, 0.0);
let at_end = start.lerp(&end, 1.0);
assert_eq!(at_end.x, 100.0);
assert_eq!(at_end.y, 200.0);
}
#[test]
fn test_size_lerp() {
let start: Size<f32> = Size {
width: 10.0,
height: 20.0,
};
let end: Size<f32> = Size {
width: 110.0,
height: 220.0,
};
let mid = start.lerp(&end, 0.5);
assert_eq!(mid.width, 60.0);
assert_eq!(mid.height, 120.0);
}
#[test]
fn test_edges_lerp() {
let start: Edges<f32> = Edges {
top: 0.0,
right: 0.0,
bottom: 0.0,
left: 0.0,
};
let end: Edges<f32> = Edges {
top: 10.0,
right: 20.0,
bottom: 30.0,
left: 40.0,
};
let mid = start.lerp(&end, 0.5);
assert_eq!(mid.top, 5.0);
assert_eq!(mid.right, 10.0);
assert_eq!(mid.bottom, 15.0);
assert_eq!(mid.left, 20.0);
}
#[test]
fn test_corners_lerp() {
let start: Corners<f32> = Corners {
top_left: 0.0,
top_right: 0.0,
bottom_right: 0.0,
bottom_left: 0.0,
};
let end: Corners<f32> = Corners {
top_left: 4.0,
top_right: 8.0,
bottom_right: 12.0,
bottom_left: 16.0,
};
let mid = start.lerp(&end, 0.5);
assert_eq!(mid.top_left, 2.0);
assert_eq!(mid.top_right, 4.0);
assert_eq!(mid.bottom_right, 6.0);
assert_eq!(mid.bottom_left, 8.0);
}
#[test]
fn test_bounds_lerp() {
let start: Bounds<f32> = Bounds {
origin: Point { x: 0.0, y: 0.0 },
size: Size {
width: 100.0,
height: 100.0,
},
};
let end: Bounds<f32> = Bounds {
origin: Point { x: 50.0, y: 50.0 },
size: Size {
width: 200.0,
height: 200.0,
},
};
let mid = start.lerp(&end, 0.5);
assert_eq!(mid.origin.x, 25.0);
assert_eq!(mid.origin.y, 25.0);
assert_eq!(mid.size.width, 150.0);
assert_eq!(mid.size.height, 150.0);
}
#[test]
fn test_rgba_lerp() {
let start = Rgba {
r: 0.0,
g: 0.0,
b: 0.0,
a: 1.0,
};
let end = Rgba {
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
};
let mid = start.lerp(&end, 0.5);
assert_eq!(mid.r, 0.5);
assert_eq!(mid.g, 0.5);
assert_eq!(mid.b, 0.5);
assert_eq!(mid.a, 1.0);
}
#[test]
fn test_rgba_lerp_with_alpha() {
let start = Rgba {
r: 1.0,
g: 0.0,
b: 0.0,
a: 0.0,
};
let end = Rgba {
r: 0.0,
g: 0.0,
b: 1.0,
a: 1.0,
};
let mid = start.lerp(&end, 0.5);
assert_eq!(mid.r, 0.5);
assert_eq!(mid.g, 0.0);
assert_eq!(mid.b, 0.5);
assert_eq!(mid.a, 0.5);
}
#[test]
fn test_pixels_lerp() {
let start = px(0.0);
let end = px(100.0);
let mid = start.lerp(&end, 0.5);
assert_eq!(mid, px(50.0));
let at_start = start.lerp(&end, 0.0);
assert_eq!(at_start, px(0.0));
let at_end = start.lerp(&end, 1.0);
assert_eq!(at_end, px(100.0));
}
#[test]
fn test_rems_lerp() {
let start = Rems(0.0);
let end = Rems(2.0);
let mid = start.lerp(&end, 0.5);
assert_eq!(mid.0, 1.0);
}
#[test]
fn test_device_pixels_lerp() {
let start = DevicePixels(0);
let end = DevicePixels(100);
let mid = start.lerp(&end, 0.5);
assert_eq!(mid.0, 50);
}
#[test]
fn test_percentage_lerp() {
let start = Percentage(0.0);
let end = Percentage(100.0);
let mid = start.lerp(&end, 0.5);
assert_eq!(mid.0, 50.0);
}
#[test]
fn test_radians_lerp() {
let start = Radians(0.0);
let end = Radians(std::f32::consts::PI);
let mid = start.lerp(&end, 0.5);
assert!((mid.0 - std::f32::consts::FRAC_PI_2).abs() < 0.0001);
}
}
+725
View File
@@ -0,0 +1,725 @@
use std::{
borrow::BorrowMut,
cell::{Ref, RefCell},
rc::Rc,
time::{Duration, Instant},
};
use crate::{App, Entity, EntityId, Window, lerp::Lerp, linear};
/// An animated transition between values of type `T`.
///
/// `Transition` manages the interpolation of a value from a start state to a goal
/// state over a specified duration. It supports customizable easing functions and
/// can operate in continuous or non-continuous mode.
///
/// # Type Parameters
///
/// * `T` - The type of value being transitioned. Must implement [`Lerp`], [`Clone`],
/// and [`PartialEq`].
///
/// # Continuous vs Non-Continuous Mode
///
/// By default, transitions operate in continuous mode. When the goal is updated:
/// - **Continuous mode** (`continuous = true`): The transition smoothly continues
/// from the current interpolated value to the new goal.
/// - **Non-continuous mode** (`continuous = false`): The transition restarts from
/// the initial value to the new goal.
///
/// # Example
///
/// ```ignore
/// let transition = window.use_transition(cx, Duration::from_millis(300), |_, _| 0.0_f32)
/// .with_easing(ease_in_out);
///
/// // Get the current interpolated value
/// let value = transition.evaluate(window, cx);
///
/// // Update the goal
/// transition.update(cx, |val, cx| {
/// *val = 1.0;
/// cx.notify();
/// });
/// ```
#[derive(Clone)]
pub struct Transition<T: Lerp + Clone + PartialEq + 'static> {
/// The amount of time for which this transtion should run.
duration_secs: f32,
/// A function that takes a delta between 0 and 1 and returns a new delta
/// between 0 and 1 based on the given easing function.
easing: Rc<dyn Fn(f32) -> f32>,
state: Entity<TransitionState<T>>,
/// A cached version of the transition's value.
cached_value: RefCell<Option<T>>,
/// Whether to continue the transition from the current value when the goal changes.
/// If true, transitions smoothly from current animated value to new goal.
/// If false, restarts from the original start value.
continuous: bool,
}
impl<T: Lerp + Clone + PartialEq + 'static> Transition<T> {
/// Create a new transition with the given duration using the specified state.
pub fn new(state: Entity<TransitionState<T>>, duration: Duration) -> Self {
Self {
duration_secs: duration.as_secs_f32(),
easing: Rc::new(linear),
state,
cached_value: RefCell::new(None),
continuous: true,
}
}
/// Set the easing function to use for this transition.
/// The easing function will take a time delta between 0 and 1 and return a new delta
/// between 0 and 1
pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self {
self.easing = Rc::new(easing);
self
}
/// Sets whether the transition should be continuous.
///
/// On goal updates, transitions continue from the current value by default.
/// If `continuous` is set to false, the transition will restart from its initial value.
pub fn continuous(mut self, continuous: bool) -> Self {
self.continuous = continuous;
self
}
fn default_goal_updated_at(&self) -> Instant {
Instant::now() - Duration::from_secs_f32(self.duration_secs)
}
/// Evaluates the value of the transition without using the cache.
/// Returns if the transition is finished (bool) and the evaluated value (T).
fn raw_evaluate(&self, cx: &mut App) -> (bool, T) {
let mut state_entity = self.state.as_mut(cx);
let state: &mut TransitionState<T> = state_entity.borrow_mut();
let elapsed_secs = state
.goal_last_updated_at
.unwrap_or_else(|| self.default_goal_updated_at())
.elapsed()
.as_secs_f32();
let delta = (self.easing)((elapsed_secs / self.duration_secs).min(1.));
debug_assert!(
(0.0..=1.0).contains(&delta),
"delta should always be between 0 and 1"
);
state.last_delta = delta;
let evaluated_value = state.start_goal.lerp(&state.end_goal, delta);
(delta != 1., evaluated_value)
}
/// Evaluates and returns the current interpolated value of the transition.
///
/// This method calculates the value based on the elapsed time since the last
/// goal update, applies the easing function, and caches the result. If the
/// transition is still in progress, it automatically requests an animation
/// frame to continue the animation.
///
/// The returned value is cached for the duration of the current frame to avoid
/// redundant calculations when called multiple times.
pub fn evaluate(&self, window: &mut Window, cx: &mut App) -> Ref<'_, T> {
if self.cached_value.borrow().is_none() {
let (in_progress, evaluated_value) = self.raw_evaluate(cx);
if in_progress {
window.request_animation_frame();
}
*self.cached_value.borrow_mut() = Some(evaluated_value);
}
Ref::map(self.cached_value.borrow(), |opt| opt.as_ref().unwrap())
}
/// Reads the end goal of the transitions.
pub fn read_goal<'b>(&'b self, cx: &'b mut App) -> &'b T {
&self.state.read(cx).end_goal
}
/// Reads the current value of the cached transition, if it exists.
pub fn read_cache(&self) -> Ref<'_, Option<T>> {
self.cached_value.borrow()
}
/// Evaluates and returns the current progress delta of the transition.
///
/// Returns a value between 0.0 and 1.0 representing how far the transition
/// has progressed, after applying the easing function. A value of 0.0 means
/// the transition just started, and 1.0 means it has completed.
pub fn evaluate_delta<'b>(&'b self, cx: &'b App) -> f32 {
let goal_last_updated_at = self
.state
.read(cx)
.goal_last_updated_at
.unwrap_or_else(|| self.default_goal_updated_at());
let elapsed_secs = goal_last_updated_at.elapsed().as_secs_f32();
(self.easing)((elapsed_secs / self.duration_secs).min(1.))
}
/// Updates the goal value for the transition.
///
/// The provided closure receives a mutable reference to the current goal value
/// and can modify it. If the goal changes (and continuous mode is enabled),
/// a new animation will begin from the current interpolated value toward the
/// new goal.
///
/// Returns `true` if the goal was actually updated (i.e., the new value differs
/// from the previous goal), `false` otherwise.
///
/// Note: This method does not automatically notify GPUI of changes. You should
/// call `cx.notify()` within the closure if you want to trigger a re-render.
pub fn update<R>(
&self,
cx: &mut App,
update: impl FnOnce(&mut T, &mut crate::Context<TransitionState<T>>) -> R,
) -> bool {
let mut was_updated = false;
self.state.update(cx, |state, cx| {
let last_end_goal = state.end_goal.clone();
update(&mut state.end_goal, cx);
if self.continuous && state.end_goal == last_end_goal {
return;
};
state.goal_last_updated_at = Some(Instant::now());
if self.continuous {
state.start_goal = state.start_goal.lerp(&last_end_goal, state.last_delta);
}
was_updated = true;
});
was_updated
}
/// Returns the entity ID associated with this transition's state.
///
/// This can be useful for tracking or comparing transitions.
pub fn entity_id(&self) -> EntityId {
self.state.entity_id()
}
/// Resets the transition to its initial state.
///
/// This clears all progress and sets both the start and end goals back to
/// the initial value that was provided when the transition was created.
/// The cache is also cleared.
pub fn reset(&self, cx: &mut App) {
self.state.update(cx, |state, _cx| {
state.goal_last_updated_at = None;
state.start_goal = state.initial_goal.clone();
state.end_goal = state.initial_goal.clone();
state.last_delta = 0.0;
});
*self.cached_value.borrow_mut() = None;
}
}
// Internal state container for a [`Transition`](crate::Transition).
///
/// This struct holds the data necessary to track a transition's progress,
/// including the start and end goals, timing information, and the last
/// computed delta value.
#[derive(Clone)]
pub struct TransitionState<T: Lerp + Clone + PartialEq + 'static> {
pub(crate) goal_last_updated_at: Option<Instant>,
pub(crate) initial_goal: T,
pub(crate) start_goal: T,
pub(crate) end_goal: T,
pub(crate) last_delta: f32,
}
impl<T: Lerp + Clone + PartialEq + 'static> TransitionState<T> {
/// Creates a new transition state with the given initial goal.
///
/// The start goal, end goal, and initial goal are all set to the provided value.
/// The transition begins in a "completed" state (delta = 1.0) until the goal
/// is updated.
pub fn new(initial_goal: T) -> Self {
Self {
goal_last_updated_at: None,
initial_goal: initial_goal.clone(),
start_goal: initial_goal.clone(),
end_goal: initial_goal,
last_delta: 1.,
}
}
}
#[cfg(all(test, feature = "test-support"))]
mod tests {
use crate::AppContext;
use super::*;
use gpui::{Point, Rgba, TestAppContext, px};
/// Helper to create a Transition directly without using window hooks.
/// This bypasses the render-phase restriction of use_transition/use_keyed_transition.
fn create_transition<T: Lerp + Clone + PartialEq + 'static>(
cx: &mut App,
duration: Duration,
initial: T,
) -> Transition<T> {
let state = cx.new(|_| TransitionState::new(initial));
Transition::new(state, duration)
}
#[gpui::test]
fn test_transition_creation(cx: &mut TestAppContext) {
cx.update(|cx| {
let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32);
// Read the goal - should be the initial value
let goal = transition.read_goal(cx);
assert_eq!(*goal, 0.0);
});
}
#[gpui::test]
fn test_transition_read_goal(cx: &mut TestAppContext) {
cx.update(|cx| {
let transition = create_transition(cx, Duration::from_millis(300), 42.0_f32);
let goal = transition.read_goal(cx);
assert_eq!(*goal, 42.0);
});
}
#[gpui::test]
fn test_transition_update_returns_true_on_change(cx: &mut TestAppContext) {
cx.update(|cx| {
let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32);
let was_updated = transition.update(cx, |val, _cx| {
*val = 100.0;
});
assert!(was_updated);
});
}
#[gpui::test]
fn test_transition_update_returns_false_on_no_change(cx: &mut TestAppContext) {
cx.update(|cx| {
let transition = create_transition(cx, Duration::from_millis(300), 50.0_f32);
// Update to the same value
let was_updated = transition.update(cx, |val, _cx| {
*val = 50.0;
});
assert!(!was_updated);
});
}
#[gpui::test]
fn test_transition_goal_updated_after_update(cx: &mut TestAppContext) {
cx.update(|cx| {
let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32);
transition.update(cx, |val, _cx| {
*val = 200.0;
});
let goal = transition.read_goal(cx);
assert_eq!(*goal, 200.0);
});
}
#[gpui::test]
fn test_transition_entity_id(cx: &mut TestAppContext) {
cx.update(|cx| {
let transition1 = create_transition(cx, Duration::from_millis(300), 0.0_f32);
let transition2 = create_transition(cx, Duration::from_millis(300), 0.0_f32);
// Different transitions should have different entity IDs
assert_ne!(transition1.entity_id(), transition2.entity_id());
});
}
#[gpui::test]
fn test_transition_reset(cx: &mut TestAppContext) {
cx.update(|cx| {
let transition = create_transition(cx, Duration::from_millis(300), 10.0_f32);
// Update the goal
transition.update(cx, |val, _cx| {
*val = 100.0;
});
assert_eq!(*transition.read_goal(cx), 100.0);
// Reset the transition
transition.reset(cx);
// Goal should be back to initial value
assert_eq!(*transition.read_goal(cx), 10.0);
});
}
#[gpui::test]
fn test_transition_cache_cleared_on_reset(cx: &mut TestAppContext) {
cx.update(|cx| {
let transition = create_transition(cx, Duration::from_millis(300), 25.0_f32);
// Manually populate the cache using raw_evaluate
let (_, value) = transition.raw_evaluate(cx);
*transition.cached_value.borrow_mut() = Some(value);
assert!(transition.read_cache().is_some());
// Reset
transition.reset(cx);
// Cache should be cleared
assert!(transition.read_cache().is_none());
});
}
#[gpui::test]
fn test_transition_with_custom_easing(cx: &mut TestAppContext) {
cx.update(|cx| {
// Custom easing that always returns 0.5
let transition =
create_transition(cx, Duration::from_millis(300), 0.0_f32).with_easing(|_| 0.5);
// Update goal to trigger animation
transition.update(cx, |val, _cx| {
*val = 100.0;
});
// With our custom easing, the delta should be 0.5
// So the value should be lerp(0, 100, 0.5) = 50
let (_, value) = transition.raw_evaluate(cx);
assert_eq!(value, 50.0);
});
}
#[gpui::test]
fn test_transition_with_point(cx: &mut TestAppContext) {
cx.update(|cx| {
let initial: Point<f32> = Point { x: 0.0, y: 0.0 };
let transition = create_transition(cx, Duration::from_millis(300), initial);
let goal = transition.read_goal(cx);
assert_eq!(goal.x, 0.0);
assert_eq!(goal.y, 0.0);
transition.update(cx, |point, _cx| {
point.x = 100.0;
point.y = 200.0;
});
let goal = transition.read_goal(cx);
assert_eq!(goal.x, 100.0);
assert_eq!(goal.y, 200.0);
});
}
#[gpui::test]
fn test_transition_with_rgba(cx: &mut TestAppContext) {
cx.update(|cx| {
let initial = Rgba {
r: 1.0,
g: 0.0,
b: 0.0,
a: 1.0,
};
let transition = create_transition(cx, Duration::from_millis(300), initial);
let goal = transition.read_goal(cx);
assert_eq!(goal.r, 1.0);
assert_eq!(goal.g, 0.0);
assert_eq!(goal.b, 0.0);
});
}
#[gpui::test]
fn test_transition_continuous_mode_default(cx: &mut TestAppContext) {
cx.update(|cx| {
// By default, transitions are continuous
let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32);
// First update
transition.update(cx, |val, _cx| {
*val = 50.0;
});
// Second update (in continuous mode, this should work from current interpolated position)
let was_updated = transition.update(cx, |val, _cx| {
*val = 100.0;
});
assert!(was_updated);
assert_eq!(*transition.read_goal(cx), 100.0);
});
}
#[gpui::test]
fn test_transition_non_continuous_mode(cx: &mut TestAppContext) {
cx.update(|cx| {
let transition =
create_transition(cx, Duration::from_millis(300), 0.0_f32).continuous(false);
// Update the goal
transition.update(cx, |val, _cx| {
*val = 100.0;
});
assert_eq!(*transition.read_goal(cx), 100.0);
});
}
#[gpui::test]
fn test_evaluate_delta_initial(cx: &mut TestAppContext) {
cx.update(|cx| {
let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32);
// Without any update, the transition should be "complete" (delta = 1.0)
// because it starts in completed state
let delta = transition.evaluate_delta(cx);
assert_eq!(delta, 1.0);
});
}
#[gpui::test]
fn test_evaluate_delta_after_update(cx: &mut TestAppContext) {
cx.update(|cx| {
let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32);
transition.update(cx, |val, _cx| {
*val = 100.0;
});
// Immediately after update, delta should be close to 0
let delta = transition.evaluate_delta(cx);
assert!(
delta < 0.1,
"delta should be small immediately after update"
);
});
}
#[gpui::test]
fn test_transition_clone(cx: &mut TestAppContext) {
cx.update(|cx| {
let transition = create_transition(cx, Duration::from_millis(300), 42.0_f32);
let cloned = transition.clone();
// Both should reference the same entity
assert_eq!(transition.entity_id(), cloned.entity_id());
// Updating one should affect the other
transition.update(cx, |val, _cx| {
*val = 100.0;
});
assert_eq!(*cloned.read_goal(cx), 100.0);
});
}
#[gpui::test]
fn test_transition_cache_consistency(cx: &mut TestAppContext) {
cx.update(|cx| {
let transition =
create_transition(cx, Duration::from_millis(300), 0.0_f32).with_easing(|_| 0.5); // Always return 0.5 for deterministic testing
transition.update(cx, |val, _cx| {
*val = 100.0;
});
// First evaluation using raw_evaluate
let (_, value1) = transition.raw_evaluate(cx);
// Second evaluation should return the same value
let (_, value2) = transition.raw_evaluate(cx);
assert_eq!(value1, value2);
});
}
#[gpui::test]
fn test_multiple_transitions_independent(cx: &mut TestAppContext) {
cx.update(|cx| {
let transition_a = create_transition(cx, Duration::from_millis(300), 0.0_f32);
let transition_b = create_transition(cx, Duration::from_millis(300), 100.0_f32);
// Update only transition_a
transition_a.update(cx, |val, _cx| {
*val = 50.0;
});
// transition_b should remain unchanged
assert_eq!(*transition_a.read_goal(cx), 50.0);
assert_eq!(*transition_b.read_goal(cx), 100.0);
});
}
#[gpui::test]
fn test_transition_with_pixels(cx: &mut TestAppContext) {
cx.update(|cx| {
let transition = create_transition(cx, Duration::from_millis(300), px(0.0));
let goal = transition.read_goal(cx);
assert_eq!(*goal, px(0.0));
transition.update(cx, |val, _cx| {
*val = px(100.0);
});
let goal = transition.read_goal(cx);
assert_eq!(*goal, px(100.0));
});
}
#[gpui::test]
fn test_transition_rapid_updates(cx: &mut TestAppContext) {
cx.update(|cx| {
let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32);
// Rapidly update multiple times
for i in 1..=10 {
transition.update(cx, |val, _cx| {
*val = i as f32 * 10.0;
});
}
// Final goal should be 100.0
assert_eq!(*transition.read_goal(cx), 100.0);
});
}
#[gpui::test]
fn test_raw_evaluate_in_progress(cx: &mut TestAppContext) {
cx.update(|cx| {
let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32);
transition.update(cx, |val, _cx| {
*val = 100.0;
});
// Immediately after update, the transition should be in progress
let (in_progress, _value) = transition.raw_evaluate(cx);
assert!(
in_progress,
"transition should be in progress immediately after update"
);
});
}
#[gpui::test]
fn test_raw_evaluate_completed(cx: &mut TestAppContext) {
cx.update(|cx| {
let transition = create_transition(cx, Duration::from_millis(300), 50.0_f32);
// Without any update, the transition starts as "complete"
let (in_progress, value) = transition.raw_evaluate(cx);
assert!(
!in_progress,
"transition should be complete without updates"
);
assert_eq!(value, 50.0);
});
}
#[gpui::test]
fn test_transition_interpolation_with_easing(cx: &mut TestAppContext) {
cx.update(|cx| {
// Test with different easing values
for expected_delta in [0.0, 0.25, 0.5, 0.75, 1.0] {
let transition = create_transition(cx, Duration::from_millis(300), 0.0_f32)
.with_easing(move |_| expected_delta);
transition.update(cx, |val, _cx| {
*val = 100.0;
});
let (_, value) = transition.raw_evaluate(cx);
let expected_value = 0.0_f32.lerp(&100.0_f32, expected_delta);
assert_eq!(value, expected_value);
}
});
}
#[test]
fn test_new_state_initialization() {
let state = TransitionState::new(42.0_f32);
assert_eq!(state.initial_goal, 42.0);
assert_eq!(state.start_goal, 42.0);
assert_eq!(state.end_goal, 42.0);
assert_eq!(state.last_delta, 1.0);
assert!(state.goal_last_updated_at.is_none());
}
#[test]
fn test_state_with_point() {
let initial: Point<f32> = Point { x: 10.0, y: 20.0 };
let state = TransitionState::new(initial.clone());
assert_eq!(state.initial_goal.x, 10.0);
assert_eq!(state.initial_goal.y, 20.0);
assert_eq!(state.start_goal.x, 10.0);
assert_eq!(state.end_goal.x, 10.0);
}
#[test]
fn test_state_with_rgba() {
let initial = Rgba {
r: 1.0,
g: 0.5,
b: 0.0,
a: 1.0,
};
let state = TransitionState::new(initial);
assert_eq!(state.initial_goal.r, 1.0);
assert_eq!(state.initial_goal.g, 0.5);
assert_eq!(state.initial_goal.b, 0.0);
assert_eq!(state.initial_goal.a, 1.0);
}
#[test]
fn test_state_clone() {
let state = TransitionState::new(100.0_f32);
let cloned = state.clone();
assert_eq!(state.initial_goal, cloned.initial_goal);
assert_eq!(state.start_goal, cloned.start_goal);
assert_eq!(state.end_goal, cloned.end_goal);
assert_eq!(state.last_delta, cloned.last_delta);
}
#[test]
fn test_state_with_integer() {
let state = TransitionState::new(50_i32);
assert_eq!(state.initial_goal, 50);
assert_eq!(state.start_goal, 50);
assert_eq!(state.end_goal, 50);
}
#[test]
fn test_state_starts_completed() {
let state = TransitionState::new(0.0_f32);
// last_delta should be 1.0 indicating the transition is "complete"
assert_eq!(state.last_delta, 1.0);
}
}
+43 -9
View File
@@ -7,18 +7,18 @@ use crate::{
DispatchActionListener, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity,
EntityId, EventEmitter, FileDropEvent, FontId, Global, GlobalElementId, GlyphId, GpuSpecs,
Hsla, InputHandler, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeyEvent, Keystroke,
KeystrokeEvent, LayoutId, LineLayoutIndex, Modifiers, ModifiersChangedEvent, MonochromeSprite,
MouseButton, MouseEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas,
PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, PolychromeSprite,
Priority, PromptButton, PromptLevel, Quad, Render, RenderGlyphParams, RenderImage,
RenderImageParams, RenderSvgParams, Replay, ResizeEdge, SMOOTH_SVG_SCALE_FACTOR,
KeystrokeEvent, LayoutId, Lerp, LineLayoutIndex, Modifiers, ModifiersChangedEvent,
MonochromeSprite, MouseButton, MouseEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels,
PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point,
PolychromeSprite, Priority, PromptButton, PromptLevel, Quad, Render, RenderGlyphParams,
RenderImage, RenderImageParams, RenderSvgParams, Replay, ResizeEdge, SMOOTH_SVG_SCALE_FACTOR,
SUBPIXEL_VARIANTS_X, SUBPIXEL_VARIANTS_Y, ScaledPixels, Scene, Shadow, SharedString, Size,
StrikethroughStyle, Style, SubpixelSprite, SubscriberSet, Subscription, SystemWindowTab,
SystemWindowTabController, TabStopMap, TaffyLayoutEngine, Task, TextRenderingMode, TextStyle,
TextStyleRefinement, ThermalState, TransformationMatrix, Underline, UnderlineStyle,
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations,
WindowOptions, WindowParams, WindowTextSystem, point, prelude::*, px, rems, size,
transparent_black,
TextStyleRefinement, ThermalState, TransformationMatrix, Transition, TransitionState,
Underline, UnderlineStyle, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem, point,
prelude::*, px, rems, size, transparent_black,
};
use anyhow::{Context as _, Result, anyhow};
use collections::{FxHashMap, FxHashSet};
@@ -3490,6 +3490,40 @@ impl Window {
}
}
/// Creates a new transition with automatic state management.
///
/// The state for this transition is managed internally and will be recreated
/// on each render. For persistent state across renders, use [`use_keyed_transition`](Self::use_keyed_transition).
pub fn use_transition<T: Lerp + Clone + PartialEq + 'static>(
&mut self,
cx: &mut App,
duration: Duration,
init: impl Fn(&mut Window, &mut Context<TransitionState<T>>) -> T,
) -> Transition<T> {
let state = self.use_state(cx, |window, cx| TransitionState::new(init(window, cx)));
Transition::new(state, duration)
}
/// Creates a new keyed transition with persistent state.
///
/// The state for this transition is associated with the provided key and will
/// persist across renders as long as the key remains the same. This is the
/// recommended method for most use cases where you want smooth, continuous
/// animations.
pub fn use_keyed_transition<T: Lerp + Clone + PartialEq + 'static>(
&mut self,
key: impl Into<ElementId>,
cx: &mut App,
duration: Duration,
init: impl Fn(&mut Window, &mut Context<TransitionState<T>>) -> T,
) -> Transition<T> {
let state =
self.use_keyed_state(key, cx, |window, cx| TransitionState::new(init(window, cx)));
Transition::new(state, duration)
}
/// Executes the given closure within the context of a tab group.
#[inline]
pub fn with_tab_group<R>(&mut self, index: Option<isize>, f: impl FnOnce(&mut Self) -> R) -> R {