Detailed changes
@@ -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)
@@ -11,6 +11,7 @@ license = "Apache-2.0"
workspace = true
[features]
+default = []
test-support = [
"backtrace",
"collections/test-support",
@@ -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;
@@ -0,0 +1,74 @@
+use std::time::Duration;
+
+use gpui::*;
+
+struct Assets {}
+
+impl AssetSource for Assets {
+ fn load(&self, path: &str) -> Result<std::borrow::Cow<'static, [u8]>> {
+ std::fs::read(path).map(Into::into).map_err(Into::into)
+ }
+
+ fn list(&self, path: &str) -> Result<Vec<SharedString>> {
+ Ok(std::fs::read_dir(path)?
+ .filter_map(|entry| {
+ Some(SharedString::from(
+ entry.ok()?.path().to_string_lossy().to_string(),
+ ))
+ })
+ .collect::<Vec<_>>())
+ }
+}
+
+struct AnimationExample {}
+
+impl Render for AnimationExample {
+ fn render(&mut self, _cx: &mut ViewContext<Self>) -> 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 {})
+ });
+ });
+}
@@ -0,0 +1,6 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3 8C3 6.67392 3.52678 5.40215 4.46446 4.46447C5.40214 3.52679 6.67391 3.00001 7.99999 3.00001C9.39779 3.00527 10.7394 3.55069 11.7444 4.52223L13 5.77778" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13 3.00001V5.77778H10.2222" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13 8C13 9.32608 12.4732 10.5978 11.5355 11.5355C10.5978 12.4732 9.32607 13 7.99999 13C6.60219 12.9947 5.26054 12.4493 4.25555 11.4778L3 10.2222" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.77777 10.2222H3V13" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -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<dyn Fn(f32) -> 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<ElementId>,
+ animation: Animation,
+ animator: impl Fn(Self, f32) -> Self + 'static,
+ ) -> AnimationElement<Self>
+ where
+ Self: Sized,
+ {
+ AnimationElement {
+ id: id.into(),
+ element: Some(self),
+ animator: Box::new(animator),
+ animation,
+ }
+ }
+}
+
+impl<E> AnimationExt for E {}
+
+/// A GPUI element that applies an animation to another element
+pub struct AnimationElement<E> {
+ id: ElementId,
+ element: Option<E>,
+ animation: Animation,
+ animator: Box<dyn Fn(E, f32) -> E + 'static>,
+}
+
+impl<E: IntoElement + 'static> IntoElement for AnimationElement<E> {
+ type Element = AnimationElement<E>;
+
+ fn into_element(self) -> Self::Element {
+ self
+ }
+}
+
+struct AnimationState {
+ start: Instant,
+}
+
+impl<E: IntoElement + 'static> Element for AnimationElement<E> {
+ 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<crate::Pixels>,
+ element: &mut Self::BeforeLayout,
+ cx: &mut crate::ElementContext,
+ ) -> Self::AfterLayout {
+ element.after_layout(cx);
+ }
+
+ fn paint(
+ &mut self,
+ _bounds: crate::Bounds<crate::Pixels>,
+ 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)
+ }
+ }
+ }
+}
@@ -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::*;
@@ -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<Transformation>,
path: Option<SharedString>,
}
@@ -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<f32>,
+ translate: Point<Pixels>,
+ 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<f32>) -> 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<Pixels>) -> 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<Radians>) -> 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<f32>) -> Self {
+ self.scale = scale;
+ self
+ }
+
+ /// Update the translation value of this transformation.
+ pub fn with_translation(mut self, translate: Point<Pixels>) -> Self {
+ self.translate = translate;
+ self
+ }
+
+ /// Update the rotation angle of this transformation.
+ pub fn with_rotation(mut self, rotate: impl Into<Radians>) -> Self {
+ self.rotate = rotate.into();
+ self
+ }
+
+ fn into_matrix(self, center: Point<Pixels>, 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())
+ }
+}
@@ -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<T: Clone + Debug + Default> Along for Point<T> {
}
}
+impl<T: Clone + Debug + Default + Negate> Negate for Point<T> {
+ fn negate(self) -> Self {
+ self.map(Negate::negate)
+ }
+}
+
impl Point<Pixels> {
/// 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<T> Size<T>
+where
+ T: Clone + Default + Debug + Half,
+{
+ /// Compute the center point of the size.g
+ pub fn center(&self) -> Point<T> {
+ Point {
+ x: self.width.half(),
+ y: self.height.half(),
+ }
+ }
+}
+
impl Size<Pixels> {
/// Scales the size by a given factor.
///
@@ -1970,6 +1989,66 @@ impl From<Pixels> for Corners<Pixels> {
}
}
+/// 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<Percentage> 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.
@@ -202,6 +202,10 @@ impl DispatchTree {
self.focusable_node_ids.insert(focus_id, node_id);
}
+ pub fn parent_view_id(&mut self) -> Option<EntityId> {
+ 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();
@@ -49,6 +49,11 @@ struct AtlasTile {
bounds: AtlasBounds,
}
+struct TransformationMatrix {
+ rotation_scale: mat2x2<f32>,
+ translation: vec2<f32>,
+}
+
fn to_device_position_impl(position: vec2<f32>) -> vec4<f32> {
let device_position = position / globals.viewport_size * vec2<f32>(2.0, -2.0) + vec2<f32>(-1.0, 1.0);
return vec4<f32>(device_position, 0.0, 1.0);
@@ -59,6 +64,13 @@ fn to_device_position(unit_vertex: vec2<f32>, bounds: Bounds) -> vec4<f32> {
return to_device_position_impl(position);
}
+fn to_device_position_transformed(unit_vertex: vec2<f32>, bounds: Bounds, transform: TransformationMatrix) -> vec4<f32> {
+ let position = unit_vertex * vec2<f32>(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<f32>, tile: AtlasTile) -> vec2<f32> {
let atlas_size = vec2<f32>(textureDimensions(t_sprite, 0));
return (vec2<f32>(tile.bounds.origin) + unit_vertex * vec2<f32>(tile.bounds.size)) / atlas_size;
@@ -476,6 +488,7 @@ struct MonochromeSprite {
content_mask: Bounds,
color: Hsla,
tile: AtlasTile,
+ transformation: TransformationMatrix,
}
var<storage, read> b_mono_sprites: array<MonochromeSprite>;
@@ -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);
@@ -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);
@@ -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<Shadow> 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<ScaledPixels>) -> 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<f32>) -> 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<Pixels>) -> Point<Pixels> {
+ 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<ScaledPixels>,
pub color: Hsla,
pub tile: AtlasTile,
+ pub transformation: TransformationMatrix,
}
impl Ord for MonochromeSprite {
@@ -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.
@@ -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<Pixels>,
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<EntityId> {
+ 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
@@ -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)