mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-07 20:39:04 +03:00
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:
parent
56bd96bc64
commit
fd0071f2af
@ -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;
|
||||
|
74
crates/gpui/examples/animation.rs
Normal file
74
crates/gpui/examples/animation.rs
Normal 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 {})
|
||||
});
|
||||
});
|
||||
}
|
6
crates/gpui/examples/image/arrow_circle.svg
Normal file
6
crates/gpui/examples/image/arrow_circle.svg
Normal 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 |
188
crates/gpui/src/elements/animation.rs
Normal file
188
crates/gpui/src/elements/animation.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user