use gpui_kit::{
AnyElement, App, ElementId, InteractiveElement as _, IntoElement, ParentElement, RenderOnce,
StyleRefinement, Styled, TestSupportExt as _, Window, base::StyledExt as _, div,
prelude::FluentBuilder as _, px,
};
use crate::{
ActiveTheme as _, Progress, StatusSelect,
collapse::{self, Fit, Measured},
};
#[derive(IntoElement)]
#[non_exhaustive]
pub struct ContextMeter {
id: ElementId,
percent: f32,
style: StyleRefinement,
}
impl ContextMeter {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
percent: 0.,
style: StyleRefinement::default(),
}
}
pub fn percent(mut self, percent: f32) -> Self {
self.percent = percent.clamp(0., 100.);
self
}
pub fn percent_used(&self) -> f32 {
self.percent
}
}
impl Styled for ContextMeter {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl RenderOnce for ContextMeter {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let (muted, gap) = (theme.muted_foreground(), theme.metrics.status_label_gap);
let percent = self.percent;
div()
.id(self.id.clone())
.test_support()
.flex()
.flex_shrink_0()
.items_center()
.gap(gap)
.text_color(muted)
.refine_style(&self.style)
.child(
Progress::new(ElementId::NamedChild(self.id.into(), "ring".into()))
.circular()
.value(percent)
.accessibility_label("Context used"),
)
.child(format!("{percent:.0}%"))
}
}
#[derive(IntoElement)]
#[non_exhaustive]
pub struct ComposerStatusTab {
id: ElementId,
items: Vec<Entry>,
trailing: Option<AnyElement>,
style: StyleRefinement,
}
enum Entry {
Select(Box<StatusSelect>),
Other(AnyElement),
}
impl ComposerStatusTab {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
items: Vec::new(),
trailing: None,
style: StyleRefinement::default(),
}
}
pub fn select(mut self, select: StatusSelect) -> Self {
self.items.push(Entry::Select(Box::new(select)));
self
}
pub fn context(mut self, percent: f32) -> Self {
let id = ElementId::NamedChild(self.id.clone().into(), "context".into());
self.trailing = Some(ContextMeter::new(id).percent(percent).into_any_element());
self
}
pub fn trailing(mut self, element: impl IntoElement) -> Self {
self.trailing = Some(element.into_any_element());
self
}
}
impl Styled for ComposerStatusTab {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl ParentElement for ComposerStatusTab {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.items.extend(elements.into_iter().map(Entry::Other));
}
}
impl RenderOnce for ComposerStatusTab {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let metrics = &theme.metrics;
let (height, inset, gap) = (
metrics.status_tab_height,
metrics.status_tab_inset,
metrics.status_tab_gap,
);
let (pad_left, pad_right) = (metrics.status_tab_inset, theme.base.spacing.lg);
let tight = theme.base.spacing.sm;
let (fill, radius, text) = (theme.sidebar, theme.radius_lg(), theme.text_control);
let widths = window
.use_keyed_state(
ElementId::NamedChild(self.id.clone().into(), "widths".into()),
cx,
|_, _| (Measured::default(), Measured::default()),
)
.read(cx)
.clone();
let (tab_width, trailing_width) = widths;
let needs: Vec<_> = self
.items
.iter()
.filter_map(|entry| match entry {
Entry::Select(select) => Some(select.need(window, cx)),
Entry::Other(_) => None,
})
.collect();
let order: Vec<usize> = (0..needs.len()).rev().collect();
let trailing_gap = if self.trailing.is_some() { gap } else { px(0.) };
let available =
tab_width.width() - pad_left - pad_right - trailing_width.width() - trailing_gap;
let collapsed = collapse::plan(
if tab_width.width() > px(0.) {
available
} else {
px(0.)
},
gap,
tight,
&needs,
&order,
);
let gap = if collapsed.iter().any(|fit| fit.is_icon()) {
tight
} else {
gap
};
let mut selects = 0;
let items: Vec<AnyElement> = self
.items
.into_iter()
.map(|entry| match entry {
Entry::Select(select) => {
let fit = collapsed.get(selects).copied().unwrap_or(Fit::Natural);
selects += 1;
match fit {
Fit::Icon => select.icon_only(true),
Fit::Shrunk(width) => select.width(width),
Fit::Natural => *select,
}
.into_any_element()
}
Entry::Other(element) => element,
})
.collect();
div()
.id(self.id)
.test_support()
.relative()
.flex()
.items_center()
.justify_between()
.h(height)
.mx(inset)
.pl(pad_left)
.pr(pad_right)
.gap(gap)
.rounded_t(radius)
.bg(fill)
.text_size(text.size)
.line_height(text.line_height)
.refine_style(&self.style)
.child(tab_width.probe())
.child(
div()
.flex()
.flex_1()
.min_w_0()
.items_center()
.gap(gap)
.children(items),
)
.when_some(self.trailing, |this, trailing| {
this.child(
div()
.relative()
.flex_shrink_0()
.child(trailing)
.child(trailing_width.probe()),
)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_percent_clamps() {
assert_eq!(ContextMeter::new("m").percent(57.).percent_used(), 57.);
assert_eq!(ContextMeter::new("m").percent(180.).percent_used(), 100.);
assert_eq!(ContextMeter::new("m").percent(-1.).percent_used(), 0.);
}
}