Skip to main content

gpui_kit/layout/
scroll_fade.rs

1//! Content that fades out where it runs off the edge of its region.
2//!
3//! [`ScrollFade`] says the same thing the shadow along the top of a
4//! [`ScrollArea`](crate::layout::ScrollArea) says — there is more content past
5//! this edge — and says it where a shadow cannot: over a translucent or
6//! frosted surface, where "the colour of what is behind the window" is not a
7//! colour anything can paint. Instead of covering the content with a gradient,
8//! the fade multiplies the opacity of every primitive by how close it is to an
9//! active edge, so a glyph half inside the band is half faded rather than the
10//! whole row dimming at once.
11//!
12//! The edges are the caller's statement about overflow, not this component's
13//! guess: a region scrolled to its end fades at the start edge only, and one
14//! that fits fades at neither. Reading those from
15//! [`scroll_offset`](crate::layout::scroll_offset) keeps the fade truthful,
16//! and turning both on unconditionally would tell the reader there is content
17//! past an edge where there is none.
18//!
19//! A fade is information rather than decoration, so it is not suppressed under
20//! reduced motion; it does not animate, and nothing about it moves on its own.
21
22use gpui::{
23    AnyElement, App, Bounds, Element, GlobalElementId, InspectorElementId, IntoElement, LayoutId,
24    ParentElement, Pixels, RenderOnce, Styled, Window, div, prelude::FluentBuilder, px,
25};
26use gpui_kit_semantics::{NodeSpec, Role, Semantic};
27use gpui_kit_theme::ActiveTheme;
28
29use crate::foundation::Ident;
30
31/// Which edges of a region its content fades towards.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
33pub struct FadeEdges {
34    pub top: bool,
35    pub bottom: bool,
36    pub left: bool,
37    pub right: bool,
38}
39
40impl FadeEdges {
41    /// Both ends of a column.
42    pub fn vertical() -> Self {
43        Self {
44            top: true,
45            bottom: true,
46            ..Self::default()
47        }
48    }
49
50    /// Both ends of a row.
51    pub fn horizontal() -> Self {
52        Self {
53            left: true,
54            right: true,
55            ..Self::default()
56        }
57    }
58
59    /// Whether any edge fades at all. None of them is a region that says
60    /// nothing is hidden, which is a fade that must not be painted.
61    pub fn any(self) -> bool {
62        self.top || self.bottom || self.left || self.right
63    }
64
65    /// The edges as a stable list, for a reader that has only the tree.
66    pub fn names(self) -> Vec<&'static str> {
67        [
68            self.top.then_some("top"),
69            self.bottom.then_some("bottom"),
70            self.left.then_some("left"),
71            self.right.then_some("right"),
72        ]
73        .into_iter()
74        .flatten()
75        .collect()
76    }
77}
78
79/// A region whose content fades towards the edges it is scrolled past.
80#[derive(IntoElement)]
81pub struct ScrollFade {
82    ident: Ident,
83    edges: FadeEdges,
84    band: Option<f32>,
85    /// Whether the region is as tall as what it holds instead of as tall as
86    /// the space it is offered.
87    fit_height: bool,
88    child: Option<AnyElement>,
89}
90
91impl std::fmt::Debug for ScrollFade {
92    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93        formatter
94            .debug_struct("ScrollFade")
95            .field("ident", &self.ident)
96            .field("edges", &self.edges)
97            .field("band", &self.band)
98            .field("has_child", &self.child.is_some())
99            .finish()
100    }
101}
102
103impl ScrollFade {
104    /// A region that fades at no edge until the caller says which ones hide
105    /// something.
106    pub fn new(ident: impl Into<Ident>) -> Self {
107        Self {
108            ident: ident.into(),
109            edges: FadeEdges::default(),
110            band: None,
111            fit_height: false,
112            child: None,
113        }
114    }
115
116    pub fn edges(mut self, edges: FadeEdges) -> Self {
117        self.edges = edges;
118        self
119    }
120
121    pub fn top(mut self, fade: bool) -> Self {
122        self.edges.top = fade;
123        self
124    }
125
126    pub fn bottom(mut self, fade: bool) -> Self {
127        self.edges.bottom = fade;
128        self
129    }
130
131    pub fn left(mut self, fade: bool) -> Self {
132        self.edges.left = fade;
133        self
134    }
135
136    pub fn right(mut self, fade: bool) -> Self {
137        self.edges.right = fade;
138        self
139    }
140
141    /// How far the ramp reaches inside each active edge, in pixels, when
142    /// `effect.edgeFadeBand` is not what this region wants.
143    pub fn band(mut self, band: f32) -> Self {
144        self.band = Some(band.max(0.0));
145        self
146    }
147
148    /// Makes the region as tall as its content rather than as tall as the
149    /// space around it, which is what a caller who already bounded the scroll
150    /// area inside wants. This follows
151    /// [`ScrollArea::fit_height`](crate::layout::ScrollArea::fit_height): a
152    /// region that fills a height nobody offered has nowhere to draw, and
153    /// takes its content with it.
154    pub fn fit_height(mut self) -> Self {
155        self.fit_height = true;
156        self
157    }
158
159    pub fn child(mut self, child: impl IntoElement) -> Self {
160        self.child = Some(child.into_any_element());
161        self
162    }
163}
164
165impl RenderOnce for ScrollFade {
166    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
167        let band = self.band.unwrap_or(cx.theme().effects.edge_fade_band);
168        let edges = self.edges;
169        // Which edges are fading is a visible statement about hidden content,
170        // so it is published rather than left for a test to infer from pixels.
171        let region = div()
172            .w_full()
173            .when(!self.fit_height, |element| element.h_full())
174            .children(self.child)
175            .semantic_in(cx, {
176                let spec = NodeSpec::new(self.ident.semantic_id(), Role::Region);
177                match edges.names().as_slice() {
178                    [] => spec.value("none"),
179                    names => spec.value(names.join(" ")),
180                }
181            })
182            .into_any_element();
183
184        Faded {
185            edges,
186            band: px(band),
187            child: region,
188        }
189    }
190}
191
192/// The scope every primitive underneath is faded inside.
193struct Faded {
194    edges: FadeEdges,
195    band: Pixels,
196    child: AnyElement,
197}
198
199impl Element for Faded {
200    type RequestLayoutState = ();
201    type PrepaintState = ();
202
203    fn id(&self) -> Option<gpui::ElementId> {
204        None
205    }
206
207    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
208        None
209    }
210
211    fn request_layout(
212        &mut self,
213        _id: Option<&GlobalElementId>,
214        _inspector_id: Option<&InspectorElementId>,
215        window: &mut Window,
216        cx: &mut App,
217    ) -> (LayoutId, ()) {
218        (self.child.request_layout(window, cx), ())
219    }
220
221    fn prepaint(
222        &mut self,
223        _id: Option<&GlobalElementId>,
224        _inspector_id: Option<&InspectorElementId>,
225        _bounds: Bounds<Pixels>,
226        _request_layout: &mut Self::RequestLayoutState,
227        window: &mut Window,
228        cx: &mut App,
229    ) {
230        self.child.prepaint(window, cx);
231    }
232
233    fn paint(
234        &mut self,
235        _id: Option<&GlobalElementId>,
236        _inspector_id: Option<&InspectorElementId>,
237        bounds: Bounds<Pixels>,
238        _request_layout: &mut Self::RequestLayoutState,
239        _prepaint: &mut Self::PrepaintState,
240        window: &mut Window,
241        cx: &mut App,
242    ) {
243        let fade = (self.edges.any() && self.band > px(0.0)).then_some(gpui::EdgeFade {
244            bounds,
245            band: self.band,
246            top: self.edges.top,
247            bottom: self.edges.bottom,
248            left: self.edges.left,
249            right: self.edges.right,
250        });
251        window.with_edge_fade(fade, |window| self.child.paint(window, cx));
252    }
253}
254
255impl IntoElement for Faded {
256    type Element = Self;
257
258    fn into_element(self) -> Self::Element {
259        self
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    #[test]
268    fn a_region_that_hides_nothing_fades_at_no_edge() {
269        assert!(!FadeEdges::default().any());
270        assert!(FadeEdges::vertical().any());
271        assert_eq!(FadeEdges::horizontal().names(), vec!["left", "right"]);
272    }
273}