Skip to main content

fret_ui_kit/declarative/
scroll.rs

1use fret_core::{Color, Px};
2use fret_ui::element::{
3    AnyElement, ContainerProps, InsetStyle, LayoutStyle, Length, Overflow, PositionStyle,
4    ScrollAxis, ScrollProps, ScrollbarAxis, ScrollbarProps, ScrollbarStyle, SizeStyle, StackProps,
5};
6use fret_ui::scroll::ScrollHandle;
7use fret_ui::{ElementContext, Theme, UiHost};
8
9use crate::IntoUiElement;
10use crate::LayoutRefinement;
11use crate::collect_children;
12use crate::declarative::style;
13
14/// Component-layer scroll helper (typed, declarative).
15///
16/// Fret treats scrolling as an explicit element (not a boolean overflow flag). This wrapper exists
17/// to match gpui/tailwind ergonomics while keeping the runtime contract explicit.
18#[track_caller]
19pub fn overflow_scroll<H: UiHost, I, T>(
20    cx: &mut ElementContext<'_, H>,
21    layout: LayoutRefinement,
22    show_scrollbar: bool,
23    f: impl FnOnce(&mut ElementContext<'_, H>) -> I,
24) -> AnyElement
25where
26    I: IntoIterator<Item = T>,
27    T: IntoUiElement<H>,
28{
29    let (layout, scrollbar_w, thumb, thumb_hover) = {
30        let theme = Theme::global(&*cx.app);
31        let layout = style::layout_style(theme, layout);
32
33        let scrollbar_w = theme.metric_token("metric.scrollbar.width");
34
35        let thumb = theme.color_token("scrollbar.thumb.background");
36        let thumb_hover = theme.color_token("scrollbar.thumb.hover.background");
37
38        (layout, scrollbar_w, thumb, thumb_hover)
39    };
40
41    cx.stack_props(StackProps { layout }, move |cx| {
42        let handle = cx.slot_state(ScrollHandle::default, |h| h.clone());
43        let mut scroll_layout = LayoutStyle::default();
44        scroll_layout.size.width = Length::Fill;
45        scroll_layout.size.height = Length::Fill;
46        scroll_layout.overflow = Overflow::Clip;
47
48        let scroll = cx.scroll(
49            ScrollProps {
50                layout: scroll_layout,
51                scroll_handle: Some(handle.clone()),
52                ..Default::default()
53            },
54            move |cx| {
55                let items = f(cx);
56                collect_children(cx, items)
57            },
58        );
59
60        let scroll_id = scroll.id;
61        let mut children = vec![scroll];
62        if show_scrollbar {
63            let scrollbar_layout = LayoutStyle {
64                position: PositionStyle::Absolute,
65                inset: InsetStyle {
66                    top: Some(Px(0.0)).into(),
67                    right: Some(Px(0.0)).into(),
68                    bottom: Some(Px(0.0)).into(),
69                    left: None.into(),
70                },
71                size: SizeStyle {
72                    width: Length::Px(scrollbar_w),
73                    ..Default::default()
74                },
75                ..Default::default()
76            };
77
78            children.push(cx.scrollbar(ScrollbarProps {
79                layout: scrollbar_layout,
80                axis: ScrollbarAxis::Vertical,
81                scroll_target: Some(scroll_id),
82                scroll_handle: handle,
83                style: ScrollbarStyle {
84                    thumb,
85                    thumb_hover,
86                    ..Default::default()
87                },
88            }));
89        }
90
91        children
92    })
93}
94
95pub fn overflow_scroll_with_handle<H: UiHost, I, T>(
96    cx: &mut ElementContext<'_, H>,
97    layout: LayoutRefinement,
98    show_scrollbar: bool,
99    handle: ScrollHandle,
100    f: impl FnOnce(&mut ElementContext<'_, H>) -> I,
101) -> AnyElement
102where
103    I: IntoIterator<Item = T>,
104    T: IntoUiElement<H>,
105{
106    let (layout, scrollbar_w, thumb, thumb_hover) = {
107        let theme = Theme::global(&*cx.app);
108        let layout = style::layout_style(theme, layout);
109
110        let scrollbar_w = theme.metric_token("metric.scrollbar.width");
111
112        let thumb = theme.color_token("scrollbar.thumb.background");
113        let thumb_hover = theme.color_token("scrollbar.thumb.hover.background");
114
115        (layout, scrollbar_w, thumb, thumb_hover)
116    };
117
118    cx.stack_props(StackProps { layout }, move |cx| {
119        let mut scroll_layout = LayoutStyle::default();
120        scroll_layout.size.width = Length::Fill;
121        scroll_layout.size.height = Length::Fill;
122        scroll_layout.overflow = Overflow::Clip;
123
124        let scroll = cx.scroll(
125            ScrollProps {
126                layout: scroll_layout,
127                scroll_handle: Some(handle.clone()),
128                ..Default::default()
129            },
130            move |cx| {
131                let items = f(cx);
132                collect_children(cx, items)
133            },
134        );
135
136        let scroll_id = scroll.id;
137        let mut children = vec![scroll];
138        if show_scrollbar {
139            let scrollbar_layout = LayoutStyle {
140                position: PositionStyle::Absolute,
141                inset: InsetStyle {
142                    top: Some(Px(0.0)).into(),
143                    right: Some(Px(0.0)).into(),
144                    bottom: Some(Px(0.0)).into(),
145                    left: None.into(),
146                },
147                size: SizeStyle {
148                    width: Length::Px(scrollbar_w),
149                    ..Default::default()
150                },
151                ..Default::default()
152            };
153
154            children.push(cx.scrollbar(ScrollbarProps {
155                layout: scrollbar_layout,
156                axis: ScrollbarAxis::Vertical,
157                scroll_target: Some(scroll_id),
158                scroll_handle: handle,
159                style: ScrollbarStyle {
160                    thumb,
161                    thumb_hover,
162                    ..Default::default()
163                },
164            }));
165        }
166
167        children
168    })
169}
170
171pub fn overflow_scroll_with_handle_xy<H: UiHost, I, T>(
172    cx: &mut ElementContext<'_, H>,
173    layout: LayoutRefinement,
174    show_scrollbar_x: bool,
175    show_scrollbar_y: bool,
176    handle: ScrollHandle,
177    f: impl FnOnce(&mut ElementContext<'_, H>) -> I,
178) -> AnyElement
179where
180    I: IntoIterator<Item = T>,
181    T: IntoUiElement<H>,
182{
183    let (layout, scrollbar_w, thumb, thumb_hover, corner_bg) = {
184        let theme = Theme::global(&*cx.app);
185        let layout = style::layout_style(theme, layout);
186
187        let scrollbar_w = theme.metric_token("metric.scrollbar.width");
188
189        let thumb = theme.color_token("scrollbar.thumb.background");
190        let thumb_hover = theme.color_token("scrollbar.thumb.hover.background");
191
192        let corner_bg = theme
193            .color_by_key("scrollbar.corner.background")
194            .or_else(|| theme.color_by_key("scrollbar.track.background"))
195            .unwrap_or(Color::TRANSPARENT);
196
197        (layout, scrollbar_w, thumb, thumb_hover, corner_bg)
198    };
199
200    cx.stack_props(StackProps { layout }, move |cx| {
201        let mut scroll_layout = LayoutStyle::default();
202        scroll_layout.size.width = Length::Fill;
203        scroll_layout.size.height = Length::Fill;
204        scroll_layout.overflow = Overflow::Clip;
205
206        let scroll = cx.scroll(
207            ScrollProps {
208                layout: scroll_layout,
209                axis: ScrollAxis::Both,
210                scroll_handle: Some(handle.clone()),
211                ..Default::default()
212            },
213            move |cx| {
214                let items = f(cx);
215                collect_children(cx, items)
216            },
217        );
218
219        let scroll_id = scroll.id;
220        let mut children = vec![scroll];
221
222        if show_scrollbar_y {
223            let scrollbar_layout = LayoutStyle {
224                position: PositionStyle::Absolute,
225                inset: InsetStyle {
226                    top: Some(Px(0.0)).into(),
227                    right: Some(Px(0.0)).into(),
228                    bottom: Some(if show_scrollbar_x {
229                        scrollbar_w
230                    } else {
231                        Px(0.0)
232                    })
233                    .into(),
234                    left: None.into(),
235                },
236                size: SizeStyle {
237                    width: Length::Px(scrollbar_w),
238                    ..Default::default()
239                },
240                ..Default::default()
241            };
242
243            children.push(cx.scrollbar(ScrollbarProps {
244                layout: scrollbar_layout,
245                axis: ScrollbarAxis::Vertical,
246                scroll_target: Some(scroll_id),
247                scroll_handle: handle.clone(),
248                style: ScrollbarStyle {
249                    thumb,
250                    thumb_hover,
251                    ..Default::default()
252                },
253            }));
254        }
255
256        if show_scrollbar_x {
257            let scrollbar_layout = LayoutStyle {
258                position: PositionStyle::Absolute,
259                inset: InsetStyle {
260                    top: None.into(),
261                    right: Some(if show_scrollbar_y {
262                        scrollbar_w
263                    } else {
264                        Px(0.0)
265                    })
266                    .into(),
267                    bottom: Some(Px(0.0)).into(),
268                    left: Some(Px(0.0)).into(),
269                },
270                size: SizeStyle {
271                    height: Length::Px(scrollbar_w),
272                    ..Default::default()
273                },
274                ..Default::default()
275            };
276
277            children.push(cx.scrollbar(ScrollbarProps {
278                layout: scrollbar_layout,
279                axis: ScrollbarAxis::Horizontal,
280                scroll_target: Some(scroll_id),
281                scroll_handle: handle.clone(),
282                style: ScrollbarStyle {
283                    thumb,
284                    thumb_hover,
285                    ..Default::default()
286                },
287            }));
288        }
289
290        if show_scrollbar_x && show_scrollbar_y {
291            let corner_layout = LayoutStyle {
292                position: PositionStyle::Absolute,
293                inset: InsetStyle {
294                    right: Some(Px(0.0)).into(),
295                    bottom: Some(Px(0.0)).into(),
296                    ..Default::default()
297                },
298                size: SizeStyle {
299                    width: Length::Px(scrollbar_w),
300                    height: Length::Px(scrollbar_w),
301                    ..Default::default()
302                },
303                ..Default::default()
304            };
305
306            children.push(cx.container(
307                ContainerProps {
308                    layout: corner_layout,
309                    background: Some(corner_bg),
310                    ..Default::default()
311                },
312                |_cx| vec![],
313            ));
314        }
315
316        children
317    })
318}
319
320#[track_caller]
321pub fn overflow_scrollbar<H: UiHost, I, T>(
322    cx: &mut ElementContext<'_, H>,
323    layout: LayoutRefinement,
324    f: impl FnOnce(&mut ElementContext<'_, H>) -> I,
325) -> AnyElement
326where
327    I: IntoIterator<Item = T>,
328    T: IntoUiElement<H>,
329{
330    overflow_scroll(cx, layout, true, f)
331}
332
333/// Like `overflow_scroll`, but enforces a single content root.
334///
335/// Note: `Scroll` does not lay out multiple children; if you pass a `Vec` of siblings they will
336/// overlap. Prefer this helper (or `*_vstack`) to make the intended structure explicit.
337#[track_caller]
338pub fn overflow_scroll_content<H: UiHost, T>(
339    cx: &mut ElementContext<'_, H>,
340    layout: LayoutRefinement,
341    show_scrollbar: bool,
342    content: impl FnOnce(&mut ElementContext<'_, H>) -> T,
343) -> AnyElement
344where
345    T: IntoUiElement<H>,
346{
347    overflow_scroll(cx, layout, show_scrollbar, |cx| {
348        std::iter::once(content(cx))
349    })
350}
351
352/// Horizontal scrolling with a single content root.
353#[track_caller]
354pub fn overflow_scroll_x_content<H: UiHost, T>(
355    cx: &mut ElementContext<'_, H>,
356    layout: LayoutRefinement,
357    show_scrollbar_x: bool,
358    content: impl FnOnce(&mut ElementContext<'_, H>) -> T,
359) -> AnyElement
360where
361    T: IntoUiElement<H>,
362{
363    let (layout, scrollbar_w, thumb, thumb_hover) = {
364        let theme = Theme::global(&*cx.app);
365        let layout = style::layout_style(theme, layout);
366
367        let scrollbar_w = theme.metric_token("metric.scrollbar.width");
368
369        let thumb = theme.color_token("scrollbar.thumb.background");
370        let thumb_hover = theme.color_token("scrollbar.thumb.hover.background");
371
372        (layout, scrollbar_w, thumb, thumb_hover)
373    };
374
375    cx.stack_props(StackProps { layout }, move |cx| {
376        let handle = cx.slot_state(ScrollHandle::default, |h| h.clone());
377        let mut scroll_layout = LayoutStyle::default();
378        scroll_layout.size.width = Length::Fill;
379        // For X-only scrolling, the common expectation is "height = content height" (e.g. code
380        // blocks) while width fills the viewport.
381        scroll_layout.size.height = Length::Auto;
382        scroll_layout.overflow = Overflow::Clip;
383
384        let scroll = cx.scroll(
385            ScrollProps {
386                layout: scroll_layout,
387                axis: ScrollAxis::X,
388                scroll_handle: Some(handle.clone()),
389                ..Default::default()
390            },
391            move |cx| {
392                let child = content(cx);
393                vec![IntoUiElement::into_element(child, cx)]
394            },
395        );
396
397        let scroll_id = scroll.id;
398        let mut children = vec![scroll];
399
400        if show_scrollbar_x {
401            let scrollbar_layout = LayoutStyle {
402                position: PositionStyle::Absolute,
403                inset: InsetStyle {
404                    top: None.into(),
405                    right: Some(Px(0.0)).into(),
406                    bottom: Some(Px(0.0)).into(),
407                    left: Some(Px(0.0)).into(),
408                },
409                size: SizeStyle {
410                    height: Length::Px(scrollbar_w),
411                    ..Default::default()
412                },
413                ..Default::default()
414            };
415
416            children.push(cx.scrollbar(ScrollbarProps {
417                layout: scrollbar_layout,
418                axis: ScrollbarAxis::Horizontal,
419                scroll_target: Some(scroll_id),
420                scroll_handle: handle,
421                style: ScrollbarStyle {
422                    thumb,
423                    thumb_hover,
424                    ..Default::default()
425                },
426            }));
427        }
428
429        children
430    })
431}
432
433// Horizontal scrolling with a `vstack` content root.
434// Note: older versions of this module exposed `*_vstack` helpers that depended on the legacy stack
435// helpers. Those were removed when the repo converged on
436// `fret-ui-kit::ui::*` builders as the teaching surface. Prefer `overflow_scroll_content(...)`
437// with an explicit content root (e.g. `ui::v_flex(|cx| ...)`).
438
439/// Like `overflow_scroll_with_handle_xy`, but enforces a single content root.
440pub fn overflow_scroll_with_handle_xy_content<H: UiHost, T>(
441    cx: &mut ElementContext<'_, H>,
442    layout: LayoutRefinement,
443    show_scrollbar_x: bool,
444    show_scrollbar_y: bool,
445    handle: ScrollHandle,
446    content: impl FnOnce(&mut ElementContext<'_, H>) -> T,
447) -> AnyElement
448where
449    T: IntoUiElement<H>,
450{
451    overflow_scroll_with_handle_xy(
452        cx,
453        layout,
454        show_scrollbar_x,
455        show_scrollbar_y,
456        handle,
457        |cx| std::iter::once(content(cx)),
458    )
459}