1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
//! `Container` — the web's generic box: a styled div plus the `scroll` prop.
//! Style delegates to gpui's [`Styled`], so every gpui style method works here.
//!
//! A block box, as a `div` is on the web: children stack at their content size
//! and a scrolling one overflows rather than being squeezed. gpui defaults to
//! flex, where flexbox drops the automatic minimum size of any child hiding its
//! own overflow; `.flex()` opts back in.
use fluidattacks_designs_domain::container::Scroll;
use gpui::prelude::FluentBuilder as _;
use gpui::{
div, px, AnyElement, App, Div, InteractiveElement as _, IntoElement, ParentElement, RenderOnce,
ScrollHandle, SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Window,
};
#[derive(IntoElement)]
pub struct Container {
base: Div,
scroll: Scroll,
handle: Option<(SharedString, ScrollHandle)>,
children: Vec<AnyElement>,
}
impl Container {
#[must_use]
pub fn new() -> Self {
Self {
// block, as a `div` is on the web; gpui would default to flex
base: div().block(),
scroll: Scroll::None,
handle: None,
children: Vec::new(),
}
}
/// Let the named axes scroll rather than clip (web `scroll`).
///
/// The offset is the app's to own, so hold one `handle` per scroll area and
/// hand it over here. `name` is a string because each bar derives its own
/// id from it, and element state cannot be keyed twice.
#[must_use]
pub fn scroll(
mut self,
name: impl Into<SharedString>,
axes: Scroll,
handle: &ScrollHandle,
) -> Self {
self.scroll = axes;
self.handle = Some((name.into(), handle.clone()));
self
}
}
impl Default for Container {
fn default() -> Self {
Self::new()
}
}
impl Styled for Container {
fn style(&mut self) -> &mut StyleRefinement {
self.base.style()
}
}
impl ParentElement for Container {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements);
}
}
impl RenderOnce for Container {
fn render(mut self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let scroll = self.scroll;
let Some((name, handle)) = self.handle.take() else {
return self.base.children(self.children).into_any_element();
};
// the bars anchor to the nearest positioned ancestor, so geometry moves
// out to the wrapper and the scrolling box fills it exactly
let geometry = {
let style = self.base.style();
StyleRefinement {
position: style.position.take(),
inset: core::mem::take(&mut style.inset),
size: core::mem::take(&mut style.size),
min_size: core::mem::take(&mut style.min_size),
max_size: core::mem::take(&mut style.max_size),
margin: core::mem::take(&mut style.margin),
flex_grow: style.flex_grow.take(),
flex_shrink: style.flex_shrink.take(),
flex_basis: style.flex_basis.take(),
..StyleRefinement::default()
}
};
// Grown inside a column rather than sized at 100%, which is what lets
// one node do both jobs: taffy 0.12, in with gpui-ce, stopped falling
// back to the content size for a percentage against a parent that has
// no size of its own, so a box meant to be sized by its rows collapsed
// and scrolled what would have fitted. A basis of auto contributes that
// content height upward; growing fills a wrapper that has a height of
// its own; and the minimums are what let it shrink and scroll there.
let content = self
.base
.id(SharedString::from(format!("{name}-content")))
.flex_grow(1.0)
.min_w(px(0.0))
.min_h(px(0.0))
.when(scroll.x(), StatefulInteractiveElement::overflow_x_scroll)
.when(scroll.y(), StatefulInteractiveElement::overflow_y_scroll)
.track_scroll(&handle)
.children(self.children);
let mut wrapper = div();
*wrapper.style() = geometry;
// a column, so the box above grows and shrinks on the axis it scrolls
let wrapper = wrapper.flex().flex_col();
// each bar keeps its drag grip in element state, so the two axes cannot
// share one id
wrapper
.relative()
.child(content)
.when(scroll.y(), |el| {
el.child(crate::Scrollbar::vertical(
SharedString::from(format!("{name}-scroll-y")),
&handle,
))
})
.when(scroll.x(), |el| {
el.child(crate::Scrollbar::horizontal(
SharedString::from(format!("{name}-scroll-x")),
&handle,
))
})
.into_any_element()
}
}
#[cfg(test)]
mod tests {
/// The render impl, where the scrolling box is built. Stops at this module,
/// whose own text would otherwise answer the scan.
fn rendered() -> &'static str {
let source = include_str!("container.rs");
let start = source
.find("impl RenderOnce for Container")
.expect("the scan found no render impl, so it is not reading this file");
let body = source.split_at(start).1;
let end = body
.find("#[cfg(test)]")
.expect("the scan found no test module, so it is not reading this file");
body.split_at(end).0
}
#[test]
fn the_scrolling_box_is_grown_inside_a_column() {
let rendered = rendered();
for shape in [
".flex_col()",
".flex_grow(1.0)",
".min_w(px(0.0))",
".min_h(px(0.0))",
] {
assert!(rendered.contains(shape), "the scrolling box lost `{shape}`");
}
}
#[test]
fn the_scrolling_box_never_claims_a_percentage_of_its_parent() {
let rendered = rendered();
// taffy 0.12 stopped falling back to the content size for these, so a
// box whose parent has no height of its own collapses
for sized in [".size_full()", ".h_full()", ".w_full()"] {
assert!(!rendered.contains(sized), "`{sized}` collapses the box");
}
}
}