1use gpui_kit::{
2 AnyElement, App, ElementId, InteractiveElement as _, IntoElement, ParentElement, RenderOnce,
3 StyleRefinement, Styled, TestSupportExt as _, Window, base::StyledExt as _, div,
4 prelude::FluentBuilder as _, px,
5};
6
7use crate::{
8 ActiveTheme as _, Progress, StatusSelect,
9 collapse::{self, Fit, Measured},
10};
11
12#[derive(IntoElement)]
23#[non_exhaustive]
24pub struct ContextMeter {
25 id: ElementId,
26 percent: f32,
27 style: StyleRefinement,
28}
29
30impl ContextMeter {
31 pub fn new(id: impl Into<ElementId>) -> Self {
33 Self {
34 id: id.into(),
35 percent: 0.,
36 style: StyleRefinement::default(),
37 }
38 }
39
40 pub fn percent(mut self, percent: f32) -> Self {
42 self.percent = percent.clamp(0., 100.);
43 self
44 }
45
46 pub fn percent_used(&self) -> f32 {
48 self.percent
49 }
50}
51
52impl Styled for ContextMeter {
53 fn style(&mut self) -> &mut StyleRefinement {
54 &mut self.style
55 }
56}
57
58impl RenderOnce for ContextMeter {
59 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
60 let theme = cx.theme();
61 let (muted, gap) = (theme.muted_foreground(), theme.metrics.status_label_gap);
62 let percent = self.percent;
63 div()
64 .id(self.id.clone())
65 .test_support()
66 .flex()
67 .flex_shrink_0()
68 .items_center()
69 .gap(gap)
70 .text_color(muted)
71 .refine_style(&self.style)
72 .child(
73 Progress::new(ElementId::NamedChild(self.id.into(), "ring".into()))
74 .circular()
75 .value(percent)
76 .accessibility_label("Context used"),
77 )
78 .child(format!("{percent:.0}%"))
79 }
80}
81
82#[derive(IntoElement)]
103#[non_exhaustive]
104pub struct ComposerStatusTab {
105 id: ElementId,
106 items: Vec<Entry>,
107 trailing: Option<AnyElement>,
108 style: StyleRefinement,
109}
110
111enum Entry {
114 Select(Box<StatusSelect>),
115 Other(AnyElement),
116}
117
118impl ComposerStatusTab {
119 pub fn new(id: impl Into<ElementId>) -> Self {
121 Self {
122 id: id.into(),
123 items: Vec::new(),
124 trailing: None,
125 style: StyleRefinement::default(),
126 }
127 }
128
129 pub fn select(mut self, select: StatusSelect) -> Self {
131 self.items.push(Entry::Select(Box::new(select)));
132 self
133 }
134
135 pub fn context(mut self, percent: f32) -> Self {
137 let id = ElementId::NamedChild(self.id.clone().into(), "context".into());
138 self.trailing = Some(ContextMeter::new(id).percent(percent).into_any_element());
139 self
140 }
141
142 pub fn trailing(mut self, element: impl IntoElement) -> Self {
144 self.trailing = Some(element.into_any_element());
145 self
146 }
147}
148
149impl Styled for ComposerStatusTab {
150 fn style(&mut self) -> &mut StyleRefinement {
151 &mut self.style
152 }
153}
154
155impl ParentElement for ComposerStatusTab {
159 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
160 self.items.extend(elements.into_iter().map(Entry::Other));
161 }
162}
163
164impl RenderOnce for ComposerStatusTab {
165 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
166 let theme = cx.theme();
167 let metrics = &theme.metrics;
168 let (height, inset, gap) = (
169 metrics.status_tab_height,
170 metrics.status_tab_inset,
171 metrics.status_tab_gap,
172 );
173 let (pad_left, pad_right) = (metrics.status_tab_inset, theme.base.spacing.lg);
174 let tight = theme.base.spacing.sm;
175 let (fill, radius, text) = (theme.sidebar, theme.radius_lg(), theme.text_control);
176 let widths = window
177 .use_keyed_state(
178 ElementId::NamedChild(self.id.clone().into(), "widths".into()),
179 cx,
180 |_, _| (Measured::default(), Measured::default()),
181 )
182 .read(cx)
183 .clone();
184 let (tab_width, trailing_width) = widths;
185 let needs: Vec<_> = self
186 .items
187 .iter()
188 .filter_map(|entry| match entry {
189 Entry::Select(select) => Some(select.need(window, cx)),
190 Entry::Other(_) => None,
191 })
192 .collect();
193 let order: Vec<usize> = (0..needs.len()).rev().collect();
194 let trailing_gap = if self.trailing.is_some() { gap } else { px(0.) };
195 let available =
196 tab_width.width() - pad_left - pad_right - trailing_width.width() - trailing_gap;
197 let collapsed = collapse::plan(
198 if tab_width.width() > px(0.) {
199 available
200 } else {
201 px(0.)
202 },
203 gap,
204 tight,
205 &needs,
206 &order,
207 );
208 let gap = if collapsed.iter().any(|fit| fit.is_icon()) {
209 tight
210 } else {
211 gap
212 };
213 let mut selects = 0;
214 let items: Vec<AnyElement> = self
215 .items
216 .into_iter()
217 .map(|entry| match entry {
218 Entry::Select(select) => {
219 let fit = collapsed.get(selects).copied().unwrap_or(Fit::Natural);
220 selects += 1;
221 match fit {
222 Fit::Icon => select.icon_only(true),
223 Fit::Shrunk(width) => select.width(width),
224 Fit::Natural => *select,
225 }
226 .into_any_element()
227 }
228 Entry::Other(element) => element,
229 })
230 .collect();
231 div()
232 .id(self.id)
233 .test_support()
234 .relative()
235 .flex()
236 .items_center()
237 .justify_between()
238 .h(height)
239 .mx(inset)
240 .pl(pad_left)
241 .pr(pad_right)
242 .gap(gap)
243 .rounded_t(radius)
244 .bg(fill)
245 .text_size(text.size)
246 .line_height(text.line_height)
247 .refine_style(&self.style)
248 .child(tab_width.probe())
249 .child(
250 div()
251 .flex()
252 .flex_1()
253 .min_w_0()
254 .items_center()
255 .gap(gap)
256 .children(items),
257 )
258 .when_some(self.trailing, |this, trailing| {
259 this.child(
260 div()
261 .relative()
262 .flex_shrink_0()
263 .child(trailing)
264 .child(trailing_width.probe()),
265 )
266 })
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn the_percent_clamps() {
276 assert_eq!(ContextMeter::new("m").percent(57.).percent_used(), 57.);
277 assert_eq!(ContextMeter::new("m").percent(180.).percent_used(), 100.);
278 assert_eq!(ContextMeter::new("m").percent(-1.).percent_used(), 0.);
279 }
280}