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 <kvark@fastmail.com>
This commit is contained in:
Mikayla Maki 2024-03-19 10:16:18 -07:00 committed by GitHub
parent 56bd96bc64
commit fd0071f2af
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 708 additions and 32 deletions

View File

@ -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)

View File

@ -11,6 +11,7 @@ license = "Apache-2.0"
workspace = true
[features]
default = []
test-support = [
"backtrace",
"collections/test-support",

View File

@ -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;

View File

@ -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 {})
});
});
}

View File

@ -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>

After

Width:  |  Height:  |  Size: 748 B

View File

@ -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)
}
}
}
}

View File

@ -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::*;

View File

@ -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())
}
}

View File

@ -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.

View File

@ -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();

View File

@ -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);

View File

@ -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);

View File

@ -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 {

View File

@ -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.

View File

@ -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

View File

@ -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)