diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 88fc23004b..6815d7882a 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -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 # ============================================================================ diff --git a/crates/gpui/examples/bench/data_table.rs b/crates/gpui/examples/bench/data_table.rs index 04a7e8dd0a..14e78dbb78 100644 --- a/crates/gpui/examples/bench/data_table.rs +++ b/crates/gpui/examples/bench/data_table.rs @@ -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; diff --git a/crates/gpui/examples/bench/paths_bench.rs b/crates/gpui/examples/bench/paths_bench.rs index e2beb307b8..2ec007e0e9 100644 --- a/crates/gpui/examples/bench/paths_bench.rs +++ b/crates/gpui/examples/bench/paths_bench.rs @@ -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); diff --git a/crates/gpui/examples/bench/pattern.rs b/crates/gpui/examples/bench/pattern.rs index e9c8884c6c..638ca2a913 100644 --- a/crates/gpui/examples/bench/pattern.rs +++ b/crates/gpui/examples/bench/pattern.rs @@ -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; diff --git a/crates/gpui/examples/bench/shadow.rs b/crates/gpui/examples/bench/shadow.rs index 92a2f13366..08f85ed333 100644 --- a/crates/gpui/examples/bench/shadow.rs +++ b/crates/gpui/examples/bench/shadow.rs @@ -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 {} diff --git a/crates/gpui/examples/learn/animation.rs b/crates/gpui/examples/learn/animation.rs index e462623029..ca719b0be8 100644 --- a/crates/gpui/examples/learn/animation.rs +++ b/crates/gpui/examples/learn/animation.rs @@ -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 {} diff --git a/crates/gpui/examples/learn/async_tasks.rs b/crates/gpui/examples/learn/async_tasks.rs index 966737ba27..96732f0a8c 100644 --- a/crates/gpui/examples/learn/async_tasks.rs +++ b/crates/gpui/examples/learn/async_tasks.rs @@ -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(); diff --git a/crates/gpui/examples/learn/creating_components.rs b/crates/gpui/examples/learn/creating_components.rs index 075733d5aa..37f549e11e 100644 --- a/crates/gpui/examples/learn/creating_components.rs +++ b/crates/gpui/examples/learn/creating_components.rs @@ -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, }; // ============================================================================ diff --git a/crates/gpui/examples/learn/custom_drawing.rs b/crates/gpui/examples/learn/custom_drawing.rs index 730eb97665..1f4dd9c5fe 100644 --- a/crates/gpui/examples/learn/custom_drawing.rs +++ b/crates/gpui/examples/learn/custom_drawing.rs @@ -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"] diff --git a/crates/gpui/examples/learn/interactive_elements.rs b/crates/gpui/examples/learn/interactive_elements.rs index 359e1faa0f..b36bc6413f 100644 --- a/crates/gpui/examples/learn/interactive_elements.rs +++ b/crates/gpui/examples/learn/interactive_elements.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, }; // ============================================================================ diff --git a/crates/gpui/examples/learn/layout.rs b/crates/gpui/examples/learn/layout.rs index 62b17a256b..f72d7016a6 100644 --- a/crates/gpui/examples/learn/layout.rs +++ b/crates/gpui/examples/learn/layout.rs @@ -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 diff --git a/crates/gpui/examples/learn/styling.rs b/crates/gpui/examples/learn/styling.rs index 64b6842ef5..21f6031624 100644 --- a/crates/gpui/examples/learn/styling.rs +++ b/crates/gpui/examples/learn/styling.rs @@ -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]); diff --git a/crates/gpui/examples/learn/text.rs b/crates/gpui/examples/learn/text.rs index de020a26fb..5e724f8f1c 100644 --- a/crates/gpui/examples/learn/text.rs +++ b/crates/gpui/examples/learn/text.rs @@ -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 diff --git a/crates/gpui/examples/learn/transition.rs b/crates/gpui/examples/learn/transition.rs new file mode 100644 index 0000000000..ce5f667b97 --- /dev/null +++ b/crates/gpui/examples/learn/transition.rs @@ -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) -> Self { + Self { + id: id.into(), + children: SmallVec::new(), + } + } +} + +impl ParentElement for Button { + fn extend(&mut self, elements: impl IntoIterator) { + 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) -> 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"); + }); +} diff --git a/crates/gpui/examples/legacy/focus_visible.rs b/crates/gpui/examples/legacy/focus_visible.rs index 2000007e99..e704d82506 100644 --- a/crates/gpui/examples/legacy/focus_visible.rs +++ b/crates/gpui/examples/legacy/focus_visible.rs @@ -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]); diff --git a/crates/gpui/examples/legacy/gif_viewer.rs b/crates/gpui/examples/legacy/gif_viewer.rs index 3fd0f804fe..bec9047b4e 100644 --- a/crates/gpui/examples/legacy/gif_viewer.rs +++ b/crates/gpui/examples/legacy/gif_viewer.rs @@ -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 { diff --git a/crates/gpui/examples/legacy/gradient.rs b/crates/gpui/examples/legacy/gradient.rs index 45b89c696a..8b1f9970ef 100644 --- a/crates/gpui/examples/legacy/gradient.rs +++ b/crates/gpui/examples/legacy/gradient.rs @@ -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 { diff --git a/crates/gpui/examples/legacy/hello_world.rs b/crates/gpui/examples/legacy/hello_world.rs index 98c6e3038f..32706515eb 100644 --- a/crates/gpui/examples/legacy/hello_world.rs +++ b/crates/gpui/examples/legacy/hello_world.rs @@ -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 { diff --git a/crates/gpui/examples/legacy/image_loading.rs b/crates/gpui/examples/legacy/image_loading.rs index f85c4e1d9d..ec0f2ea1d5 100644 --- a/crates/gpui/examples/legacy/image_loading.rs +++ b/crates/gpui/examples/legacy/image_loading.rs @@ -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 {} diff --git a/crates/gpui/examples/legacy/layer_shell.rs b/crates/gpui/examples/legacy/layer_shell.rs index cbf9840610..0f8baa989a 100644 --- a/crates/gpui/examples/legacy/layer_shell.rs +++ b/crates/gpui/examples/legacy/layer_shell.rs @@ -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 { - 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(); diff --git a/crates/gpui/examples/legacy/on_window_close_quit.rs b/crates/gpui/examples/legacy/on_window_close_quit.rs index 577217e415..c615bfe627 100644 --- a/crates/gpui/examples/legacy/on_window_close_quit.rs +++ b/crates/gpui/examples/legacy/on_window_close_quit.rs @@ -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]); diff --git a/crates/gpui/examples/legacy/opacity.rs b/crates/gpui/examples/legacy/opacity.rs index e31f4e0be1..d7104f9202 100644 --- a/crates/gpui/examples/legacy/opacity.rs +++ b/crates/gpui/examples/legacy/opacity.rs @@ -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 { diff --git a/crates/gpui/examples/legacy/scrollable.rs b/crates/gpui/examples/legacy/scrollable.rs index 4a3845004a..2e42bde94c 100644 --- a/crates/gpui/examples/legacy/scrollable.rs +++ b/crates/gpui/examples/legacy/scrollable.rs @@ -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 {} diff --git a/crates/gpui/examples/legacy/svg/svg.rs b/crates/gpui/examples/legacy/svg/svg.rs index 5cddc06d59..daa1c4d06e 100644 --- a/crates/gpui/examples/legacy/svg/svg.rs +++ b/crates/gpui/examples/legacy/svg/svg.rs @@ -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 { diff --git a/crates/gpui/examples/legacy/tab_stop.rs b/crates/gpui/examples/legacy/tab_stop.rs index 534ef4c101..8e126b7b96 100644 --- a/crates/gpui/examples/legacy/tab_stop.rs +++ b/crates/gpui/examples/legacy/tab_stop.rs @@ -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]); diff --git a/crates/gpui/examples/legacy/tree.rs b/crates/gpui/examples/legacy/tree.rs index de1b253593..506632ed56 100644 --- a/crates/gpui/examples/legacy/tree.rs +++ b/crates/gpui/examples/legacy/tree.rs @@ -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 {} diff --git a/crates/gpui/examples/legacy/uniform_list.rs b/crates/gpui/examples/legacy/uniform_list.rs index 5c470d4b55..e84d74b049 100644 --- a/crates/gpui/examples/legacy/uniform_list.rs +++ b/crates/gpui/examples/legacy/uniform_list.rs @@ -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 {} diff --git a/crates/gpui/examples/legacy/window.rs b/crates/gpui/examples/legacy/window.rs index e7c49f63e4..d2a0bc3797 100644 --- a/crates/gpui/examples/legacy/window.rs +++ b/crates/gpui/examples/legacy/window.rs @@ -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 { diff --git a/crates/gpui/examples/legacy/window_positioning.rs b/crates/gpui/examples/legacy/window_positioning.rs index 3adfe22850..40606f0bf1 100644 --- a/crates/gpui/examples/legacy/window_positioning.rs +++ b/crates/gpui/examples/legacy/window_positioning.rs @@ -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 { diff --git a/crates/gpui/examples/legacy/window_shadow.rs b/crates/gpui/examples/legacy/window_shadow.rs index 4fa3b33702..fb96d8cbac 100644 --- a/crates/gpui/examples/legacy/window_shadow.rs +++ b/crates/gpui/examples/legacy/window_shadow.rs @@ -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 {} diff --git a/crates/gpui/examples/prelude.rs b/crates/gpui/examples/prelude.rs index 63b6c50f89..466abf185f 100644 --- a/crates/gpui/examples/prelude.rs +++ b/crates/gpui/examples/prelude.rs @@ -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]); diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 12aeebfb95..172749816a 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -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::*; diff --git a/crates/gpui/src/lerp.rs b/crates/gpui/src/lerp.rs new file mode 100644 index 0000000000..9fdc6f4a84 --- /dev/null +++ b/crates/gpui/src/lerp.rs @@ -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 +where + Self: Sized, +{ + /// Interpolates between `self` and `to` based on `delta`. + fn lerp(&self, to: &Self, delta: f32) -> Output; +} + +impl Lerp 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 { x, y }, + Size { width, height }, + Edges { top, right, bottom, left }, + Corners { top_left, top_right, bottom_right, bottom_left }, + Bounds { 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(from: T, to: T, alpha: T) -> T +where + T: Copy + Add + Sub + Mul, +{ + 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 = Point { x: 0.0, y: 0.0 }; + let end: Point = 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 = Size { + width: 10.0, + height: 20.0, + }; + let end: Size = 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 = Edges { + top: 0.0, + right: 0.0, + bottom: 0.0, + left: 0.0, + }; + let end: Edges = 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 = Corners { + top_left: 0.0, + top_right: 0.0, + bottom_right: 0.0, + bottom_left: 0.0, + }; + let end: Corners = 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 = Bounds { + origin: Point { x: 0.0, y: 0.0 }, + size: Size { + width: 100.0, + height: 100.0, + }, + }; + let end: Bounds = 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); + } +} diff --git a/crates/gpui/src/transition.rs b/crates/gpui/src/transition.rs new file mode 100644 index 0000000000..a7d4424429 --- /dev/null +++ b/crates/gpui/src/transition.rs @@ -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 { + /// 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 f32>, + + state: Entity>, + + /// A cached version of the transition's value. + cached_value: RefCell>, + + /// 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 Transition { + /// Create a new transition with the given duration using the specified state. + pub fn new(state: Entity>, 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 = 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> { + 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( + &self, + cx: &mut App, + update: impl FnOnce(&mut T, &mut crate::Context>) -> 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 { + pub(crate) goal_last_updated_at: Option, + pub(crate) initial_goal: T, + pub(crate) start_goal: T, + pub(crate) end_goal: T, + pub(crate) last_delta: f32, +} + +impl TransitionState { + /// 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( + cx: &mut App, + duration: Duration, + initial: T, + ) -> Transition { + 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 = 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 = 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); + } +} diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index f529284029..122f79ea1e 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -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( + &mut self, + cx: &mut App, + duration: Duration, + init: impl Fn(&mut Window, &mut Context>) -> T, + ) -> Transition { + 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( + &mut self, + key: impl Into, + cx: &mut App, + duration: Duration, + init: impl Fn(&mut Window, &mut Context>) -> T, + ) -> Transition { + 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(&mut self, index: Option, f: impl FnOnce(&mut Self) -> R) -> R {