From fd0071f2af1f49bbaf67a8bf6d4960a81167c9d4 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 19 Mar 2024 10:16:18 -0700 Subject: [PATCH] Add an animation to the LSP checking indicator (#9463) Spinner go spinny. Extra thanks to @kvark for helping me with the shaders. https://github.com/zed-industries/zed/assets/2280405/9d5f4f4e-0d43-44d2-a089-5d69939938e9 Release Notes: - Added a spinning animation to the LSP checking indicator --------- Co-authored-by: Dzmitry Malyshau --- crates/diagnostics/src/items.rs | 18 +- crates/gpui/Cargo.toml | 1 + crates/gpui/build.rs | 1 + crates/gpui/examples/animation.rs | 74 ++++++++ crates/gpui/examples/image/arrow_circle.svg | 6 + crates/gpui/src/elements/animation.rs | 188 ++++++++++++++++++++ crates/gpui/src/elements/mod.rs | 2 + crates/gpui/src/elements/svg.rs | 100 ++++++++++- crates/gpui/src/geometry.rs | 129 +++++++++++++- crates/gpui/src/key_dispatch.rs | 4 + crates/gpui/src/platform/blade/shaders.wgsl | 16 +- crates/gpui/src/platform/mac/shaders.metal | 30 +++- crates/gpui/src/scene.rs | 106 ++++++++++- crates/gpui/src/window.rs | 42 +++-- crates/gpui/src/window/element_cx.rs | 12 +- crates/ui/src/components/icon.rs | 11 +- 16 files changed, 708 insertions(+), 32 deletions(-) create mode 100644 crates/gpui/examples/animation.rs create mode 100644 crates/gpui/examples/image/arrow_circle.svg create mode 100644 crates/gpui/src/elements/animation.rs diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index d823ad52af..03d46ed599 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -1,8 +1,10 @@ +use std::time::Duration; + use collections::HashSet; use editor::Editor; use gpui::{ - rems, EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, View, - ViewContext, WeakView, + percentage, rems, Animation, AnimationExt, EventEmitter, IntoElement, ParentElement, Render, + Styled, Subscription, Transformation, View, ViewContext, WeakView, }; use language::Diagnostic; use lsp::LanguageServerId; @@ -66,7 +68,17 @@ impl Render for DiagnosticIndicator { Some( h_flex() .gap_2() - .child(Icon::new(IconName::ArrowCircle).size(IconSize::Small)) + .child( + Icon::new(IconName::ArrowCircle) + .size(IconSize::Small) + .with_animation( + "arrow-circle", + Animation::new(Duration::from_secs(2)).repeat(), + |icon, delta| { + icon.transform(Transformation::rotate(percentage(delta))) + }, + ), + ) .child( Label::new("Checking…") .size(LabelSize::Small) diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 77f93469ff..528e312574 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -11,6 +11,7 @@ license = "Apache-2.0" workspace = true [features] +default = [] test-support = [ "backtrace", "collections/test-support", diff --git a/crates/gpui/build.rs b/crates/gpui/build.rs index 8f63787c8b..c9c483d6b4 100644 --- a/crates/gpui/build.rs +++ b/crates/gpui/build.rs @@ -87,6 +87,7 @@ fn generate_shader_bindings() -> PathBuf { "PathSprite".into(), "SurfaceInputIndex".into(), "SurfaceBounds".into(), + "TransformationMatrix".into(), ]); config.no_includes = true; config.enumeration.prefix_with_name = true; diff --git a/crates/gpui/examples/animation.rs b/crates/gpui/examples/animation.rs new file mode 100644 index 0000000000..b4e0f4c0a3 --- /dev/null +++ b/crates/gpui/examples/animation.rs @@ -0,0 +1,74 @@ +use std::time::Duration; + +use gpui::*; + +struct Assets {} + +impl AssetSource for Assets { + fn load(&self, path: &str) -> Result> { + std::fs::read(path).map(Into::into).map_err(Into::into) + } + + fn list(&self, path: &str) -> Result> { + Ok(std::fs::read_dir(path)? + .filter_map(|entry| { + Some(SharedString::from( + entry.ok()?.path().to_string_lossy().to_string(), + )) + }) + .collect::>()) + } +} + +struct AnimationExample {} + +impl Render for AnimationExample { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + div().flex().flex_col().size_full().justify_around().child( + div().flex().flex_row().w_full().justify_around().child( + div() + .flex() + .bg(rgb(0x2e7d32)) + .size(Length::Definite(Pixels(300.0).into())) + .justify_center() + .items_center() + .shadow_lg() + .text_xl() + .text_color(black()) + .child("hello") + .child( + svg() + .size_8() + .path("examples/image/arrow_circle.svg") + .text_color(black()) + .with_animation( + "image_circle", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(bounce(ease_in_out)), + |svg, delta| { + svg.with_transformation(Transformation::rotate(percentage( + delta, + ))) + }, + ), + ), + ), + ) + } +} + +fn main() { + App::new() + .with_assets(Assets {}) + .run(|cx: &mut AppContext| { + let options = WindowOptions { + bounds: Some(Bounds::centered(size(px(300.), px(300.)), cx)), + ..Default::default() + }; + cx.open_window(options, |cx| { + cx.activate(false); + cx.new_view(|_cx| AnimationExample {}) + }); + }); +} diff --git a/crates/gpui/examples/image/arrow_circle.svg b/crates/gpui/examples/image/arrow_circle.svg new file mode 100644 index 0000000000..90e352bdea --- /dev/null +++ b/crates/gpui/examples/image/arrow_circle.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/crates/gpui/src/elements/animation.rs b/crates/gpui/src/elements/animation.rs new file mode 100644 index 0000000000..4c3379cfb1 --- /dev/null +++ b/crates/gpui/src/elements/animation.rs @@ -0,0 +1,188 @@ +use std::time::{Duration, Instant}; + +use crate::{AnyElement, Element, ElementId, IntoElement}; + +pub use easing::*; + +/// An animation that can be applied to an element. +pub struct Animation { + /// The amount of time for which this animation should run + pub duration: Duration, + /// Whether to repeat this animation when it finishes + pub oneshot: bool, + /// 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. + pub easing: Box f32>, +} + +impl Animation { + /// Create a new animation with the given duration. + /// By default the animation will only run once and will use a linear easing function. + pub fn new(duration: Duration) -> Self { + Self { + duration, + oneshot: true, + easing: Box::new(linear), + } + } + + /// Set the animation to loop when it finishes. + pub fn repeat(mut self) -> Self { + self.oneshot = false; + self + } + + /// Set the easing function to use for this animation. + /// 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 = Box::new(easing); + self + } +} + +/// An extension trait for adding the animation wrapper to both Elements and Components +pub trait AnimationExt { + /// Render this component or element with an animation + fn with_animation( + self, + id: impl Into, + animation: Animation, + animator: impl Fn(Self, f32) -> Self + 'static, + ) -> AnimationElement + where + Self: Sized, + { + AnimationElement { + id: id.into(), + element: Some(self), + animator: Box::new(animator), + animation, + } + } +} + +impl AnimationExt for E {} + +/// A GPUI element that applies an animation to another element +pub struct AnimationElement { + id: ElementId, + element: Option, + animation: Animation, + animator: Box E + 'static>, +} + +impl IntoElement for AnimationElement { + type Element = AnimationElement; + + fn into_element(self) -> Self::Element { + self + } +} + +struct AnimationState { + start: Instant, +} + +impl Element for AnimationElement { + type BeforeLayout = AnyElement; + + type AfterLayout = (); + + fn before_layout( + &mut self, + cx: &mut crate::ElementContext, + ) -> (crate::LayoutId, Self::BeforeLayout) { + cx.with_element_state(Some(self.id.clone()), |state, cx| { + let state = state.unwrap().unwrap_or_else(|| AnimationState { + start: Instant::now(), + }); + let mut delta = + state.start.elapsed().as_secs_f32() / self.animation.duration.as_secs_f32(); + + let mut done = false; + if delta > 1.0 { + if self.animation.oneshot { + done = true; + delta = 1.0; + } else { + delta = delta % 1.0; + } + } + let delta = (self.animation.easing)(delta); + + debug_assert!( + delta >= 0.0 && delta <= 1.0, + "delta should always be between 0 and 1" + ); + + let element = self.element.take().expect("should only be called once"); + let mut element = (self.animator)(element, delta).into_any_element(); + + if !done { + let parent_id = cx.parent_view_id(); + cx.on_next_frame(move |cx| { + if let Some(parent_id) = parent_id { + cx.notify(parent_id) + } else { + cx.refresh() + } + }) + } + + ((element.before_layout(cx), element), Some(state)) + }) + } + + fn after_layout( + &mut self, + _bounds: crate::Bounds, + element: &mut Self::BeforeLayout, + cx: &mut crate::ElementContext, + ) -> Self::AfterLayout { + element.after_layout(cx); + } + + fn paint( + &mut self, + _bounds: crate::Bounds, + element: &mut Self::BeforeLayout, + _: &mut Self::AfterLayout, + cx: &mut crate::ElementContext, + ) { + element.paint(cx); + } +} + +mod easing { + /// The linear easing function, or delta itself + pub fn linear(delta: f32) -> f32 { + delta + } + + /// The quadratic easing function, delta * delta + pub fn quadratic(delta: f32) -> f32 { + delta * delta + } + + /// The quadratic ease-in-out function, which starts and ends slowly but speeds up in the middle + pub fn ease_in_out(delta: f32) -> f32 { + if delta < 0.5 { + 2.0 * delta * delta + } else { + let x = -2.0 * delta + 2.0; + 1.0 - x * x / 2.0 + } + } + + /// Apply the given easing function, first in the forward direction and then in the reverse direction + pub fn bounce(easing: impl Fn(f32) -> f32) -> impl Fn(f32) -> f32 { + move |delta| { + if delta < 0.5 { + easing(delta * 2.0) + } else { + easing((1.0 - delta) * 2.0) + } + } + } +} diff --git a/crates/gpui/src/elements/mod.rs b/crates/gpui/src/elements/mod.rs index ee8f8964e1..9b0d1c9d80 100644 --- a/crates/gpui/src/elements/mod.rs +++ b/crates/gpui/src/elements/mod.rs @@ -1,3 +1,4 @@ +mod animation; mod canvas; mod deferred; mod div; @@ -8,6 +9,7 @@ mod svg; mod text; mod uniform_list; +pub use animation::*; pub use canvas::*; pub use deferred::*; pub use div::*; diff --git a/crates/gpui/src/elements/svg.rs b/crates/gpui/src/elements/svg.rs index cd215ebac1..d00c47e317 100644 --- a/crates/gpui/src/elements/svg.rs +++ b/crates/gpui/src/elements/svg.rs @@ -1,12 +1,14 @@ use crate::{ - Bounds, Element, ElementContext, Hitbox, InteractiveElement, Interactivity, IntoElement, - LayoutId, Pixels, SharedString, StyleRefinement, Styled, + geometry::Negate as _, point, px, radians, size, Bounds, Element, ElementContext, Hitbox, + InteractiveElement, Interactivity, IntoElement, LayoutId, Pixels, Point, Radians, SharedString, + Size, StyleRefinement, Styled, TransformationMatrix, }; use util::ResultExt; /// An SVG element. pub struct Svg { interactivity: Interactivity, + transformation: Option, path: Option, } @@ -14,6 +16,7 @@ pub struct Svg { pub fn svg() -> Svg { Svg { interactivity: Interactivity::default(), + transformation: None, path: None, } } @@ -24,6 +27,13 @@ impl Svg { self.path = Some(path.into()); self } + + /// Transform the SVG element with the given transformation. + /// Note that this won't effect the hitbox or layout of the element, only the rendering. + pub fn with_transformation(mut self, transformation: Transformation) -> Self { + self.transformation = Some(transformation); + self + } } impl Element for Svg { @@ -59,7 +69,16 @@ impl Element for Svg { self.interactivity .paint(bounds, hitbox.as_ref(), cx, |style, cx| { if let Some((path, color)) = self.path.as_ref().zip(style.text.color) { - cx.paint_svg(bounds, path.clone(), color).log_err(); + let transformation = self + .transformation + .as_ref() + .map(|transformation| { + transformation.into_matrix(bounds.center(), cx.scale_factor()) + }) + .unwrap_or_default(); + + cx.paint_svg(bounds, path.clone(), transformation, color) + .log_err(); } }) } @@ -84,3 +103,78 @@ impl InteractiveElement for Svg { &mut self.interactivity } } + +/// A transformation to apply to an SVG element. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Transformation { + scale: Size, + translate: Point, + rotate: Radians, +} + +impl Default for Transformation { + fn default() -> Self { + Self { + scale: size(1.0, 1.0), + translate: point(px(0.0), px(0.0)), + rotate: radians(0.0), + } + } +} + +impl Transformation { + /// Create a new Transformation with the specified scale along each axis. + pub fn scale(scale: Size) -> Self { + Self { + scale, + translate: point(px(0.0), px(0.0)), + rotate: radians(0.0), + } + } + + /// Create a new Transformation with the specified translation. + pub fn translate(translate: Point) -> Self { + Self { + scale: size(1.0, 1.0), + translate, + rotate: radians(0.0), + } + } + + /// Create a new Transformation with the specified rotation in radians. + pub fn rotate(rotate: impl Into) -> Self { + let rotate = rotate.into(); + Self { + scale: size(1.0, 1.0), + translate: point(px(0.0), px(0.0)), + rotate, + } + } + + /// Update the scaling factor of this transformation. + pub fn with_scaling(mut self, scale: Size) -> Self { + self.scale = scale; + self + } + + /// Update the translation value of this transformation. + pub fn with_translation(mut self, translate: Point) -> Self { + self.translate = translate; + self + } + + /// Update the rotation angle of this transformation. + pub fn with_rotation(mut self, rotate: impl Into) -> Self { + self.rotate = rotate.into(); + self + } + + fn into_matrix(self, center: Point, scale_factor: f32) -> TransformationMatrix { + //Note: if you read this as a sequence of matrix mulitplications, start from the bottom + TransformationMatrix::unit() + .translate(center.scale(scale_factor) + self.translate.scale(scale_factor)) + .rotate(self.rotate) + .scale(self.scale) + .translate(center.scale(scale_factor).negate()) + } +} diff --git a/crates/gpui/src/geometry.rs b/crates/gpui/src/geometry.rs index 9833ae9f72..41d330cdf1 100644 --- a/crates/gpui/src/geometry.rs +++ b/crates/gpui/src/geometry.rs @@ -26,7 +26,7 @@ pub enum Axis { impl Axis { /// Swap this axis to the opposite axis. - pub fn invert(&self) -> Self { + pub fn invert(self) -> Self { match self { Axis::Vertical => Axis::Horizontal, Axis::Horizontal => Axis::Vertical, @@ -160,6 +160,12 @@ impl Along for Point { } } +impl Negate for Point { + fn negate(self) -> Self { + self.map(Negate::negate) + } +} + impl Point { /// Scales the point by a given factor, which is typically derived from the resolution /// of a target display to ensure proper sizing of UI elements. @@ -421,6 +427,19 @@ where } } +impl Size +where + T: Clone + Default + Debug + Half, +{ + /// Compute the center point of the size.g + pub fn center(&self) -> Point { + Point { + x: self.width.half(), + y: self.height.half(), + } + } +} + impl Size { /// Scales the size by a given factor. /// @@ -1970,6 +1989,66 @@ impl From for Corners { } } +/// Represents an angle in Radians +#[derive( + Clone, + Copy, + Default, + Add, + AddAssign, + Sub, + SubAssign, + Neg, + Div, + DivAssign, + PartialEq, + Serialize, + Deserialize, + Debug, +)] +#[repr(transparent)] +pub struct Radians(pub f32); + +/// Create a `Radian` from a raw value +pub fn radians(value: f32) -> Radians { + Radians(value) +} + +/// A type representing a percentage value. +#[derive( + Clone, + Copy, + Default, + Add, + AddAssign, + Sub, + SubAssign, + Neg, + Div, + DivAssign, + PartialEq, + Serialize, + Deserialize, + Debug, +)] +#[repr(transparent)] +pub struct Percentage(pub f32); + +/// Generate a `Radian` from a percentage of a full circle. +pub fn percentage(value: f32) -> Percentage { + debug_assert!( + value >= 0.0 && value <= 1.0, + "Percentage must be between 0 and 1" + ); + Percentage(value) +} + +impl From for Radians { + fn from(value: Percentage) -> Self { + radians(value.0 * std::f32::consts::PI * 2.0) + } +} + /// Represents a length in pixels, the base unit of measurement in the UI framework. /// /// `Pixels` is a value type that represents an absolute length in pixels, which is used @@ -2761,6 +2840,54 @@ impl Half for GlobalPixels { } } +/// Provides a trait for types that can negate their values. +pub trait Negate { + /// Returns the negation of the given value + fn negate(self) -> Self; +} + +impl Negate for i32 { + fn negate(self) -> Self { + -self + } +} + +impl Negate for f32 { + fn negate(self) -> Self { + -self + } +} + +impl Negate for DevicePixels { + fn negate(self) -> Self { + Self(-self.0) + } +} + +impl Negate for ScaledPixels { + fn negate(self) -> Self { + Self(-self.0) + } +} + +impl Negate for Pixels { + fn negate(self) -> Self { + Self(-self.0) + } +} + +impl Negate for Rems { + fn negate(self) -> Self { + Self(-self.0) + } +} + +impl Negate for GlobalPixels { + fn negate(self) -> Self { + Self(-self.0) + } +} + /// A trait for checking if a value is zero. /// /// This trait provides a method to determine if a value is considered to be zero. diff --git a/crates/gpui/src/key_dispatch.rs b/crates/gpui/src/key_dispatch.rs index e85b170a2a..62392983ee 100644 --- a/crates/gpui/src/key_dispatch.rs +++ b/crates/gpui/src/key_dispatch.rs @@ -202,6 +202,10 @@ impl DispatchTree { self.focusable_node_ids.insert(focus_id, node_id); } + pub fn parent_view_id(&mut self) -> Option { + self.view_stack.last().copied() + } + pub fn set_view_id(&mut self, view_id: EntityId) { if self.view_stack.last().copied() != Some(view_id) { let node_id = *self.node_stack.last().unwrap(); diff --git a/crates/gpui/src/platform/blade/shaders.wgsl b/crates/gpui/src/platform/blade/shaders.wgsl index 756d96197a..c29f6bc67b 100644 --- a/crates/gpui/src/platform/blade/shaders.wgsl +++ b/crates/gpui/src/platform/blade/shaders.wgsl @@ -49,6 +49,11 @@ struct AtlasTile { bounds: AtlasBounds, } +struct TransformationMatrix { + rotation_scale: mat2x2, + translation: vec2, +} + fn to_device_position_impl(position: vec2) -> vec4 { let device_position = position / globals.viewport_size * vec2(2.0, -2.0) + vec2(-1.0, 1.0); return vec4(device_position, 0.0, 1.0); @@ -59,6 +64,13 @@ fn to_device_position(unit_vertex: vec2, bounds: Bounds) -> vec4 { return to_device_position_impl(position); } +fn to_device_position_transformed(unit_vertex: vec2, bounds: Bounds, transform: TransformationMatrix) -> vec4 { + let position = unit_vertex * vec2(bounds.size) + bounds.origin; + //Note: Rust side stores it as row-major, so transposing here + let transformed = transpose(transform.rotation_scale) * position + transform.translation; + return to_device_position_impl(transformed); +} + fn to_tile_position(unit_vertex: vec2, tile: AtlasTile) -> vec2 { let atlas_size = vec2(textureDimensions(t_sprite, 0)); return (vec2(tile.bounds.origin) + unit_vertex * vec2(tile.bounds.size)) / atlas_size; @@ -476,6 +488,7 @@ struct MonochromeSprite { content_mask: Bounds, color: Hsla, tile: AtlasTile, + transformation: TransformationMatrix, } var b_mono_sprites: array; @@ -492,7 +505,8 @@ fn vs_mono_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index let sprite = b_mono_sprites[instance_id]; var out = MonoSpriteVarying(); - out.position = to_device_position(unit_vertex, sprite.bounds); + out.position = to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation); + out.tile_position = to_tile_position(unit_vertex, sprite.tile); out.color = hsla_to_rgba(sprite.color); out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask); diff --git a/crates/gpui/src/platform/mac/shaders.metal b/crates/gpui/src/platform/mac/shaders.metal index beadd83021..c1089dbbec 100644 --- a/crates/gpui/src/platform/mac/shaders.metal +++ b/crates/gpui/src/platform/mac/shaders.metal @@ -6,6 +6,10 @@ using namespace metal; float4 hsla_to_rgba(Hsla hsla); float4 to_device_position(float2 unit_vertex, Bounds_ScaledPixels bounds, constant Size_DevicePixels *viewport_size); +float4 to_device_position_transformed(float2 unit_vertex, Bounds_ScaledPixels bounds, + TransformationMatrix transformation, + constant Size_DevicePixels *input_viewport_size); + float2 to_tile_position(float2 unit_vertex, AtlasTile tile, constant Size_DevicePixels *atlas_size); float4 distance_from_clip_rect(float2 unit_vertex, Bounds_ScaledPixels bounds, @@ -301,7 +305,7 @@ vertex MonochromeSpriteVertexOutput monochrome_sprite_vertex( float2 unit_vertex = unit_vertices[unit_vertex_id]; MonochromeSprite sprite = sprites[sprite_id]; float4 device_position = - to_device_position(unit_vertex, sprite.bounds, viewport_size); + to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation, viewport_size); float4 clip_distance = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask.bounds); float2 tile_position = to_tile_position(unit_vertex, sprite.tile, atlas_size); @@ -582,6 +586,30 @@ float4 to_device_position(float2 unit_vertex, Bounds_ScaledPixels bounds, return float4(device_position, 0., 1.); } +float4 to_device_position_transformed(float2 unit_vertex, Bounds_ScaledPixels bounds, + TransformationMatrix transformation, + constant Size_DevicePixels *input_viewport_size) { + float2 position = + unit_vertex * float2(bounds.size.width, bounds.size.height) + + float2(bounds.origin.x, bounds.origin.y); + + // Apply the transformation matrix to the position via matrix multiplication. + float2 transformed_position = float2(0, 0); + transformed_position[0] = position[0] * transformation.rotation_scale[0][0] + position[1] * transformation.rotation_scale[0][1]; + transformed_position[1] = position[0] * transformation.rotation_scale[1][0] + position[1] * transformation.rotation_scale[1][1]; + + // Add in the translation component of the transformation matrix. + transformed_position[0] += transformation.translation[0]; + transformed_position[1] += transformation.translation[1]; + + float2 viewport_size = float2((float)input_viewport_size->width, + (float)input_viewport_size->height); + float2 device_position = + transformed_position / viewport_size * float2(2., -2.) + float2(-1., 1.); + return float4(device_position, 0., 1.); +} + + float2 to_tile_position(float2 unit_vertex, AtlasTile tile, constant Size_DevicePixels *atlas_size) { float2 tile_origin = float2(tile.bounds.origin.x, tile.bounds.origin.y); diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index 844b854903..9d27c8fa45 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -3,7 +3,7 @@ use crate::{ bounds_tree::BoundsTree, point, AtlasTextureId, AtlasTile, Bounds, ContentMask, Corners, Edges, - Hsla, Pixels, Point, ScaledPixels, + Hsla, Pixels, Point, Radians, ScaledPixels, Size, }; use std::{fmt::Debug, iter::Peekable, ops::Range, slice}; @@ -504,6 +504,109 @@ impl From for Primitive { } } +/// A data type representing a 2 dimensional transformation that can be applied to an element. +#[derive(Debug, Clone, Copy, PartialEq)] +#[repr(C)] +pub struct TransformationMatrix { + /// 2x2 matrix containing rotation and scale, + /// stored row-major + pub rotation_scale: [[f32; 2]; 2], + /// translation vector + pub translation: [f32; 2], +} + +impl Eq for TransformationMatrix {} + +impl TransformationMatrix { + /// The unit matrix, has no effect. + pub fn unit() -> Self { + Self { + rotation_scale: [[1.0, 0.0], [0.0, 1.0]], + translation: [0.0, 0.0], + } + } + + /// Move the origin by a given point + pub fn translate(mut self, point: Point) -> Self { + self.compose(Self { + rotation_scale: [[1.0, 0.0], [0.0, 1.0]], + translation: [point.x.0, point.y.0], + }) + } + + /// Clockwise rotation in radians around the origin + pub fn rotate(self, angle: Radians) -> Self { + self.compose(Self { + rotation_scale: [ + [angle.0.cos(), -angle.0.sin()], + [angle.0.sin(), angle.0.cos()], + ], + translation: [0.0, 0.0], + }) + } + + /// Scale around the origin + pub fn scale(self, size: Size) -> Self { + self.compose(Self { + rotation_scale: [[size.width, 0.0], [0.0, size.height]], + translation: [0.0, 0.0], + }) + } + + /// Perform matrix multiplication with another transformation + /// to produce a new transformation that is the result of + /// applying both transformations: first, `other`, then `self`. + #[inline] + pub fn compose(self, other: TransformationMatrix) -> TransformationMatrix { + if other == Self::unit() { + return self; + } + // Perform matrix multiplication + TransformationMatrix { + rotation_scale: [ + [ + self.rotation_scale[0][0] * other.rotation_scale[0][0] + + self.rotation_scale[0][1] * other.rotation_scale[1][0], + self.rotation_scale[0][0] * other.rotation_scale[0][1] + + self.rotation_scale[0][1] * other.rotation_scale[1][1], + ], + [ + self.rotation_scale[1][0] * other.rotation_scale[0][0] + + self.rotation_scale[1][1] * other.rotation_scale[1][0], + self.rotation_scale[1][0] * other.rotation_scale[0][1] + + self.rotation_scale[1][1] * other.rotation_scale[1][1], + ], + ], + translation: [ + self.translation[0] + + self.rotation_scale[0][0] * other.translation[0] + + self.rotation_scale[0][1] * other.translation[1], + self.translation[1] + + self.rotation_scale[1][0] * other.translation[0] + + self.rotation_scale[1][1] * other.translation[1], + ], + } + } + + /// Apply transformation to a point, mainly useful for debugging + pub fn apply(&self, point: Point) -> Point { + let input = [point.x.0, point.y.0]; + let mut output = self.translation; + for i in 0..2 { + for k in 0..2 { + output[i] += self.rotation_scale[i][k] * input[k]; + } + } + Point::new(output[0].into(), output[1].into()) + } +} + +impl Default for TransformationMatrix { + fn default() -> Self { + Self::unit() + } +} + #[derive(Clone, Debug, Eq, PartialEq)] #[repr(C)] pub(crate) struct MonochromeSprite { @@ -513,6 +616,7 @@ pub(crate) struct MonochromeSprite { pub content_mask: ContentMask, pub color: Hsla, pub tile: AtlasTile, + pub transformation: TransformationMatrix, } impl Ord for MonochromeSprite { diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index fcdb36e026..fa12861828 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -631,6 +631,28 @@ impl<'a> WindowContext<'a> { } } + /// Indicate that this view has changed, which will invoke any observers and also mark the window as dirty. + /// If this view or any of its ancestors are *cached*, notifying it will cause it or its ancestors to be redrawn. + pub fn notify(&mut self, view_id: EntityId) { + for view_id in self + .window + .rendered_frame + .dispatch_tree + .view_path(view_id) + .into_iter() + .rev() + { + if !self.window.dirty_views.insert(view_id) { + break; + } + } + + if self.window.draw_phase == DrawPhase::None { + self.window.dirty.set(true); + self.app.push_effect(Effect::Notify { emitter: view_id }); + } + } + /// Close this window. pub fn remove_window(&mut self) { self.window.removed = true; @@ -2159,25 +2181,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { /// Indicate that this view has changed, which will invoke any observers and also mark the window as dirty. /// If this view or any of its ancestors are *cached*, notifying it will cause it or its ancestors to be redrawn. pub fn notify(&mut self) { - for view_id in self - .window - .rendered_frame - .dispatch_tree - .view_path(self.view.entity_id()) - .into_iter() - .rev() - { - if !self.window.dirty_views.insert(view_id) { - break; - } - } - - if self.window.draw_phase == DrawPhase::None { - self.window_cx.window.dirty.set(true); - self.window_cx.app.push_effect(Effect::Notify { - emitter: self.view.model.entity_id, - }); - } + self.window_cx.notify(self.view.entity_id()); } /// Register a callback to be invoked when the window is resized. diff --git a/crates/gpui/src/window/element_cx.rs b/crates/gpui/src/window/element_cx.rs index c62ab96fea..bc007c5bc8 100644 --- a/crates/gpui/src/window/element_cx.rs +++ b/crates/gpui/src/window/element_cx.rs @@ -35,8 +35,8 @@ use crate::{ GlyphId, Hsla, ImageData, InputHandler, IsZero, KeyContext, KeyEvent, LayoutId, LineLayoutIndex, MonochromeSprite, MouseEvent, PaintQuad, Path, Pixels, PlatformInputHandler, Point, PolychromeSprite, Quad, RenderGlyphParams, RenderImageParams, RenderSvgParams, Scene, - Shadow, SharedString, Size, StrikethroughStyle, Style, TextStyleRefinement, Underline, - UnderlineStyle, Window, WindowContext, SUBPIXEL_VARIANTS, + Shadow, SharedString, Size, StrikethroughStyle, Style, TextStyleRefinement, + TransformationMatrix, Underline, UnderlineStyle, Window, WindowContext, SUBPIXEL_VARIANTS, }; pub(crate) type AnyMouseListener = @@ -1007,6 +1007,7 @@ impl<'a> ElementContext<'a> { content_mask, color, tile, + transformation: TransformationMatrix::unit(), }); } Ok(()) @@ -1072,6 +1073,7 @@ impl<'a> ElementContext<'a> { &mut self, bounds: Bounds, path: SharedString, + transformation: TransformationMatrix, color: Hsla, ) -> Result<()> { let scale_factor = self.scale_factor(); @@ -1103,6 +1105,7 @@ impl<'a> ElementContext<'a> { content_mask, color, tile, + transformation, }); Ok(()) @@ -1266,6 +1269,11 @@ impl<'a> ElementContext<'a> { self.window.next_frame.dispatch_tree.set_view_id(view_id); } + /// Get the last view id for the current element + pub fn parent_view_id(&mut self) -> Option { + self.window.next_frame.dispatch_tree.parent_view_id() + } + /// Sets an input handler, such as [`ElementInputHandler`][element_input_handler], which interfaces with the /// platform to receive textual input with proper integration with concerns such /// as IME interactions. This handler will be active for the upcoming frame until the following frame is diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index bd1f795bce..9a0326bdcd 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -1,4 +1,4 @@ -use gpui::{svg, IntoElement, Rems}; +use gpui::{svg, IntoElement, Rems, Transformation}; use strum::EnumIter; use crate::prelude::*; @@ -219,6 +219,7 @@ pub struct Icon { path: SharedString, color: Color, size: IconSize, + transformation: Transformation, } impl Icon { @@ -227,6 +228,7 @@ impl Icon { path: icon.path().into(), color: Color::default(), size: IconSize::default(), + transformation: Transformation::default(), } } @@ -235,6 +237,7 @@ impl Icon { path: path.into(), color: Color::default(), size: IconSize::default(), + transformation: Transformation::default(), } } @@ -247,11 +250,17 @@ impl Icon { self.size = size; self } + + pub fn transform(mut self, transformation: Transformation) -> Self { + self.transformation = transformation; + self + } } impl RenderOnce for Icon { fn render(self, cx: &mut WindowContext) -> impl IntoElement { svg() + .with_transformation(self.transformation) .size(self.size.rems()) .flex_none() .path(self.path)