added transitions (#29)
This commit is contained in:
@@ -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
|
||||
# ============================================================================
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user