Skip to main content

gpui_ai/
decoration.rs

1//! Layers an application paints into a component's frame.
2//!
3//! A component owns its structure; what fills the space behind and in front
4//! of that structure is the application's business. This is the slot for it:
5//! one element under the content, one over it, both clipped to the frame's
6//! own shape, neither taking any part in layout.
7//!
8//! # What can go in one
9//!
10//! Anything that is an element. A `div` with a patterned background, an
11//! `img` — including an animated GIF or WebP — a `canvas` painting arbitrary
12//! geometry, or a webview. This crate ships no effects of its own: a shake, a
13//! ripple, a halftone field are an application's expression, not a library's.
14//!
15//! # Corners
16//!
17//! A layer that paints all the way to the edge has to carry the frame's
18//! radius itself, because GPUI cannot clip a subtree to a rounded rectangle -
19//! its `ContentMask` is a `Bounds`, so `overflow_hidden` clips to the box and
20//! never to a corner. [`frame_radius`] is that radius. A layer that does not
21//! reach the corners - scattered dots, a ring in the middle - needs nothing.
22//!
23//! # Example
24//!
25//! ```
26//! use gpui_ai::prelude::{Decoration, decoration};
27//! use gpui::{App, div, prelude::*};
28//!
29//! # fn example(cx: &App) -> Decoration {
30//! Decoration::behind(div().size_full().rounded(decoration::frame_radius(cx)))
31//!     .and_above(div().size_full())
32//! # }
33//! ```
34//!
35//! # Motion
36//!
37//! [`animated`] drives a decoration from a looping 0…1 and stops it when the
38//! frame it sits in scrolls out of view or the reader has asked for less
39//! motion. A decoration that animates on its own clock would keep a scrolled
40//! away panel paying for itself forever.
41
42use crate::motion::VisibleAnimationExt as _;
43use gpui::{AnyElement, App, ElementId, IntoElement, ParentElement, Pixels, Styled, Window, div};
44use gpui_component::ActiveTheme as _;
45use std::time::Duration;
46
47/// The layers an application paints into one component's frame.
48///
49/// Both slots are optional and independent. A decoration never affects the
50/// component's size: the frame measures its content, and these are painted
51/// into the space that measurement produced.
52#[derive(Default)]
53pub struct Decoration {
54    behind: Option<AnyElement>,
55    above: Option<AnyElement>,
56}
57
58impl Decoration {
59    /// A layer painted under the component's content, over its background.
60    pub fn behind(element: impl IntoElement) -> Self {
61        Self {
62            behind: Some(element.into_any_element()),
63            above: None,
64        }
65    }
66
67    /// A layer painted over the component's content.
68    ///
69    /// Passes pointer input through to the content underneath, so a veil, a
70    /// scanline, or a vignette never costs the component a click.
71    pub fn above(element: impl IntoElement) -> Self {
72        Self {
73            behind: None,
74            above: Some(element.into_any_element()),
75        }
76    }
77
78    /// Adds the layer painted over the component's content.
79    pub fn and_above(mut self, element: impl IntoElement) -> Self {
80        self.above = Some(element.into_any_element());
81        self
82    }
83
84    /// Adds the layer painted under the component's content.
85    ///
86    /// So that either layer can be named first: paint order is fixed by the
87    /// component, not by which of these was called.
88    pub fn and_behind(mut self, element: impl IntoElement) -> Self {
89        self.behind = Some(element.into_any_element());
90        self
91    }
92
93    /// Takes the under layer, placed over the component's own background.
94    fn take_under(&mut self) -> Option<impl IntoElement> {
95        self.behind.take().map(clipped)
96    }
97
98    /// Takes the over layer, placed over the component's content.
99    fn take_over(&mut self) -> Option<impl IntoElement> {
100        self.above.take().map(clipped)
101    }
102}
103
104/// The corner radius a decoration must carry to reach a component's edge.
105///
106/// Every framed component in this library rounds its frame by the theme's
107/// large radius, so a layer that paints to the edge rounds itself by this and
108/// lands on the frame exactly. It follows the active theme, including a custom
109/// JSON one, which is why it is a call rather than a constant.
110///
111/// It exists because the alternative does not: GPUI's `ContentMask` is a
112/// `Bounds`, so nothing here can clip a layer to a rounded shape on the
113/// caller's behalf. Per-element radii on a `div` background or an `img` are
114/// resolved in the shader and do work - and note that `ObjectFit::Cover` hands
115/// a sprite bounds larger than the element, which puts those radii on corners
116/// that are off screen. A covered photograph has to be cropped to the frame,
117/// not fitted into it.
118pub fn frame_radius(cx: &App) -> Pixels {
119    cx.theme().semantic_tokens().radius.lg
120}
121
122/// Places one layer over the component's own background, clipped to its box.
123///
124/// Rectangularly, and that is a real limit rather than an oversight - see
125/// [`frame_radius`] for why, and for the radius a layer rounds itself by.
126///
127/// Painting the corners back in the frame's colour was tried and is wrong: the
128/// sliver outside the radius is where whatever sits *behind* the component
129/// shows through, so filling it with the component's own surface draws a blob
130/// over the backdrop rather than a rounded corner.
131fn clipped(layer: AnyElement) -> impl IntoElement {
132    div().absolute().inset_0().overflow_hidden().child(layer)
133}
134
135/// Places a component's decoration layers.
136///
137/// Two calls rather than one, because paint order is child order: the under
138/// layer has to be the frame's first child and the over layer its last. The
139/// `every_framed_component_places_both_decoration_layers` test is what keeps
140/// a component from quietly honouring only one of them.
141pub(crate) trait DecoratedExt: Sized {
142    /// Places the under layer. Call before adding any content.
143    fn decoration_under(self, decoration: &mut Decoration) -> Self;
144
145    /// Places the over layer. Call after adding all content.
146    fn decoration_over(self, decoration: &mut Decoration) -> Self;
147}
148
149impl<E: ParentElement + Styled + Sized> DecoratedExt for E {
150    fn decoration_under(mut self, decoration: &mut Decoration) -> Self {
151        if let Some(layer) = decoration.take_under() {
152            // The frame becomes the positioning context for both layers. Only
153            // when there is one: a component with no decoration keeps exactly
154            // the layout it had before this existed.
155            self.style().position = Some(gpui::Position::Relative);
156            self.extend(std::iter::once(layer.into_any_element()));
157        }
158        self
159    }
160
161    fn decoration_over(mut self, decoration: &mut Decoration) -> Self {
162        if let Some(layer) = decoration.take_over() {
163            self.style().position = Some(gpui::Position::Relative);
164            self.extend(std::iter::once(layer.into_any_element()));
165        }
166        self
167    }
168}
169
170/// A decoration that animates, and stops when nobody can see it.
171///
172/// `build` is handed a 0…1 that loops over `period`, and returns the element
173/// to paint at that point in the loop. The loop runs only while the region is
174/// on screen and unclipped, and holds at 0 when the reader has asked for less
175/// motion — so an application's own effect answers to the same preference
176/// every animation in this library does, without the application arranging it.
177///
178/// # Example
179///
180/// ```
181/// use gpui_ai::prelude::decoration;
182/// use gpui::{div, prelude::*, px};
183/// use std::time::Duration;
184///
185/// let drifting = decoration::animated("tide", Duration::from_secs(8), |delta| {
186///     div().absolute().top(px(delta * 40.)).size_full()
187/// });
188/// ```
189pub fn animated<E>(
190    id: impl Into<ElementId>,
191    period: Duration,
192    build: impl Fn(f32) -> E + 'static,
193) -> impl IntoElement
194where
195    E: IntoElement + 'static,
196{
197    let id = id.into();
198    // The animator receives the container, not the caller's element, so the
199    // caller's closure runs once per frame and its result is the child.
200    div().size_full().with_visible_animation(
201        id,
202        gpui::Animation::new(period).repeat(),
203        move |container, delta| container.child(build(delta).into_any_element()),
204    )
205}
206
207/// Drives a decoration from a value the application already has.
208///
209/// Where [`animated`] loops on its own, this eases towards `target` whenever
210/// the application moves it — a ripple started by a press, a tilt that
211/// follows a pointer, a shake that grows as a value approaches its limit.
212/// Returns `target` immediately when the reader has asked for less motion.
213pub fn toward(
214    id: impl Into<gpui_base::motion::TransitionId>,
215    target: f32,
216    window: &mut Window,
217    cx: &mut App,
218) -> f32 {
219    if !crate::motion::motion_is_full(cx) {
220        return target;
221    }
222    let standard = crate::motion::MotionTokens::read(cx).standard();
223    gpui_base::motion::transition(
224        id,
225        target,
226        gpui_base::motion::Transition::new(standard).ease(crate::motion::ease_out_quint),
227        window,
228        cx,
229    )
230}