Detailed changes
@@ -65,6 +65,7 @@ dependencies = [
"clock",
"collections",
"command_palette_hooks",
+ "component",
"context_server",
"convert_case 0.8.0",
"db",
@@ -85,6 +86,7 @@ dependencies = [
"language",
"language_model",
"language_model_selector",
+ "linkme",
"log",
"lsp",
"markdown",
@@ -32,6 +32,7 @@ client.workspace = true
clock.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
+component.workspace = true
context_server.workspace = true
convert_case.workspace = true
db.workspace = true
@@ -51,6 +52,7 @@ itertools.workspace = true
language.workspace = true
language_model.workspace = true
language_model_selector.workspace = true
+linkme.workspace = true
log.workspace = true
lsp.workspace = true
markdown.workspace = true
@@ -85,9 +87,9 @@ ui.workspace = true
ui_input.workspace = true
util.workspace = true
uuid.workspace = true
+workspace-hack.workspace = true
workspace.workspace = true
zed_actions.workspace = true
-workspace-hack.workspace = true
[dev-dependencies]
buffer_diff = { workspace = true, features = ["test-support"] }
@@ -1,5 +1,7 @@
mod agent_notification;
mod context_pill;
+mod user_spending;
pub use agent_notification::*;
pub use context_pill::*;
+// pub use user_spending::*;
@@ -0,0 +1,186 @@
+use gpui::{Entity, Render};
+use ui::{ProgressBar, prelude::*};
+
+#[derive(RegisterComponent)]
+pub struct UserSpending {
+ free_tier_current: u32,
+ free_tier_cap: u32,
+ over_tier_current: u32,
+ over_tier_cap: u32,
+ free_tier_progress: Entity<ProgressBar>,
+ over_tier_progress: Entity<ProgressBar>,
+}
+
+impl UserSpending {
+ pub fn new(
+ free_tier_current: u32,
+ free_tier_cap: u32,
+ over_tier_current: u32,
+ over_tier_cap: u32,
+ cx: &mut App,
+ ) -> Self {
+ let free_tier_capped = free_tier_current == free_tier_cap;
+ let free_tier_near_capped =
+ free_tier_current as f32 / 100.0 >= free_tier_cap as f32 / 100.0 * 0.9;
+ let over_tier_capped = over_tier_current == over_tier_cap;
+ let over_tier_near_capped =
+ over_tier_current as f32 / 100.0 >= over_tier_cap as f32 / 100.0 * 0.9;
+
+ let free_tier_progress = cx.new(|cx| {
+ ProgressBar::new(
+ "free_tier",
+ free_tier_current as f32,
+ free_tier_cap as f32,
+ cx,
+ )
+ });
+ let over_tier_progress = cx.new(|cx| {
+ ProgressBar::new(
+ "over_tier",
+ over_tier_current as f32,
+ over_tier_cap as f32,
+ cx,
+ )
+ });
+
+ if free_tier_capped {
+ free_tier_progress.update(cx, |progress_bar, cx| {
+ progress_bar.fg_color(cx.theme().status().error);
+ });
+ } else if free_tier_near_capped {
+ free_tier_progress.update(cx, |progress_bar, cx| {
+ progress_bar.fg_color(cx.theme().status().warning);
+ });
+ }
+
+ if over_tier_capped {
+ over_tier_progress.update(cx, |progress_bar, cx| {
+ progress_bar.fg_color(cx.theme().status().error);
+ });
+ } else if over_tier_near_capped {
+ over_tier_progress.update(cx, |progress_bar, cx| {
+ progress_bar.fg_color(cx.theme().status().warning);
+ });
+ }
+
+ Self {
+ free_tier_current,
+ free_tier_cap,
+ over_tier_current,
+ over_tier_cap,
+ free_tier_progress,
+ over_tier_progress,
+ }
+ }
+}
+
+impl Render for UserSpending {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ let formatted_free_tier = format!(
+ "${} / ${}",
+ self.free_tier_current as f32 / 100.0,
+ self.free_tier_cap as f32 / 100.0
+ );
+ let formatted_over_tier = format!(
+ "${} / ${}",
+ self.over_tier_current as f32 / 100.0,
+ self.over_tier_cap as f32 / 100.0
+ );
+
+ v_group()
+ .elevation_2(cx)
+ .py_1p5()
+ .px_2p5()
+ .w(px(360.))
+ .child(
+ v_flex()
+ .child(
+ v_flex()
+ .p_1p5()
+ .gap_0p5()
+ .child(
+ h_flex()
+ .justify_between()
+ .child(Label::new("Free Tier Usage").size(LabelSize::Small))
+ .child(
+ Label::new(formatted_free_tier)
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ ),
+ )
+ .child(self.free_tier_progress.clone()),
+ )
+ .child(
+ v_flex()
+ .p_1p5()
+ .gap_0p5()
+ .child(
+ h_flex()
+ .justify_between()
+ .child(Label::new("Current Spending").size(LabelSize::Small))
+ .child(
+ Label::new(formatted_over_tier)
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ ),
+ )
+ .child(self.over_tier_progress.clone()),
+ ),
+ )
+ }
+}
+
+impl Component for UserSpending {
+ fn scope() -> ComponentScope {
+ ComponentScope::None
+ }
+
+ fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
+ let new_user = cx.new(|cx| UserSpending::new(0, 2000, 0, 2000, cx));
+ let free_capped = cx.new(|cx| UserSpending::new(2000, 2000, 0, 2000, cx));
+ let free_near_capped = cx.new(|cx| UserSpending::new(1800, 2000, 0, 2000, cx));
+ let over_near_capped = cx.new(|cx| UserSpending::new(2000, 2000, 1800, 2000, cx));
+ let over_capped = cx.new(|cx| UserSpending::new(1000, 2000, 2000, 2000, cx));
+
+ Some(
+ v_flex()
+ .gap_6()
+ .p_4()
+ .children(vec![example_group(vec![
+ single_example(
+ "New User",
+ div().size_full().child(new_user.clone()).into_any_element(),
+ ),
+ single_example(
+ "Free Tier Capped",
+ div()
+ .size_full()
+ .child(free_capped.clone())
+ .into_any_element(),
+ ),
+ single_example(
+ "Free Tier Near Capped",
+ div()
+ .size_full()
+ .child(free_near_capped.clone())
+ .into_any_element(),
+ ),
+ single_example(
+ "Over Tier Near Capped",
+ div()
+ .size_full()
+ .child(over_near_capped.clone())
+ .into_any_element(),
+ ),
+ single_example(
+ "Over Tier Capped",
+ div()
+ .size_full()
+ .child(over_capped.clone())
+ .into_any_element(),
+ ),
+ ])])
+ .into_any_element(),
+ )
+ }
+}
@@ -187,22 +187,20 @@ impl ComponentPreview {
let mut entries = Vec::new();
- let known_scopes = [
- ComponentScope::Layout,
- ComponentScope::Input,
- ComponentScope::Editor,
- ComponentScope::Notification,
- ComponentScope::Collaboration,
- ComponentScope::VersionControl,
- ComponentScope::None,
- ];
-
// Always show all components first
entries.push(PreviewEntry::AllComponents);
entries.push(PreviewEntry::Separator);
- for scope in known_scopes.iter() {
- if let Some(components) = scope_groups.remove(scope) {
+ let mut scopes: Vec<_> = scope_groups
+ .keys()
+ .filter(|scope| !matches!(**scope, ComponentScope::None))
+ .cloned()
+ .collect();
+
+ scopes.sort_by_key(|s| s.to_string());
+
+ for scope in scopes {
+ if let Some(components) = scope_groups.remove(&scope) {
if !components.is_empty() {
entries.push(PreviewEntry::SectionHeader(scope.to_string().into()));
let mut sorted_components = components;
@@ -215,6 +213,7 @@ impl ComponentPreview {
}
}
+ // Add uncategorized components last
if let Some(components) = scope_groups.get(&ComponentScope::None) {
if !components.is_empty() {
entries.push(PreviewEntry::Separator);
@@ -272,7 +271,12 @@ impl ComponentPreview {
.into_any_element()
}
PreviewEntry::Separator => ListItem::new(ix)
- .child(h_flex().pt_3().child(Divider::horizontal_dashed()))
+ .child(
+ h_flex()
+ .occlude()
+ .pt_3()
+ .child(Divider::horizontal_dashed()),
+ )
.into_any_element(),
}
}
@@ -22,6 +22,7 @@ mod notification;
mod numeric_stepper;
mod popover;
mod popover_menu;
+mod progress;
mod radio;
mod right_click_menu;
mod scrollbar;
@@ -61,6 +62,7 @@ pub use notification::*;
pub use numeric_stepper::*;
pub use popover::*;
pub use popover_menu::*;
+pub use progress::*;
pub use radio::*;
pub use right_click_menu::*;
pub use scrollbar::*;
@@ -267,7 +267,7 @@ impl RenderOnce for IconWithIndicator {
impl Component for Icon {
fn scope() -> ComponentScope {
- ComponentScope::None
+ ComponentScope::Images
}
fn description() -> Option<&'static str> {
@@ -26,7 +26,7 @@ impl RenderOnce for DecoratedIcon {
impl Component for DecoratedIcon {
fn scope() -> ComponentScope {
- ComponentScope::None
+ ComponentScope::Images
}
fn description() -> Option<&'static str> {
@@ -199,7 +199,7 @@ impl RenderOnce for Label {
impl Component for Label {
fn scope() -> ComponentScope {
- ComponentScope::None
+ ComponentScope::Typography
}
fn description() -> Option<&'static str> {
@@ -0,0 +1,2 @@
+mod progress_bar;
+pub use progress_bar::*;
@@ -0,0 +1,159 @@
+use documented::Documented;
+use gpui::{Hsla, point};
+
+use crate::components::Label;
+use crate::prelude::*;
+
+/// A progress bar is a horizontal bar that communicates the status of a process.
+///
+/// A progress bar should not be used to represent indeterminate progress.
+#[derive(RegisterComponent, Documented)]
+pub struct ProgressBar {
+ id: ElementId,
+ value: f32,
+ max_value: f32,
+ bg_color: Hsla,
+ fg_color: Hsla,
+}
+
+impl ProgressBar {
+ /// Create a new progress bar with the given value and maximum value.
+ pub fn new(
+ id: impl Into<ElementId>,
+ value: f32,
+ max_value: f32,
+ cx: &mut Context<Self>,
+ ) -> Self {
+ Self {
+ id: id.into(),
+ value,
+ max_value,
+ bg_color: cx.theme().colors().background,
+ fg_color: cx.theme().status().info,
+ }
+ }
+
+ /// Set the current value of the progress bar.
+ pub fn value(&mut self, value: f32) -> &mut Self {
+ self.value = value;
+ self
+ }
+
+ /// Set the maximum value of the progress bar.
+ pub fn max_value(&mut self, max_value: f32) -> &mut Self {
+ self.max_value = max_value;
+ self
+ }
+
+ /// Set the background color of the progress bar.
+ pub fn bg_color(&mut self, color: Hsla) -> &mut Self {
+ self.bg_color = color;
+ self
+ }
+
+ /// Set the foreground color of the progress bar.
+ pub fn fg_color(&mut self, color: Hsla) -> &mut Self {
+ self.fg_color = color;
+ self
+ }
+}
+
+impl Render for ProgressBar {
+ fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
+ let fill_width = (self.value / self.max_value).clamp(0.02, 1.0);
+
+ div()
+ .id(self.id.clone())
+ .w_full()
+ .h(px(8.0))
+ .rounded_full()
+ .py(px(2.0))
+ .px(px(4.0))
+ .bg(self.bg_color)
+ .shadow(smallvec::smallvec![gpui::BoxShadow {
+ color: gpui::black().opacity(0.08),
+ offset: point(px(0.), px(1.)),
+ blur_radius: px(0.),
+ spread_radius: px(0.),
+ }])
+ .child(
+ div()
+ .h_full()
+ .rounded_full()
+ .bg(self.fg_color)
+ .w(relative(fill_width)),
+ )
+ }
+}
+
+impl Component for ProgressBar {
+ fn scope() -> ComponentScope {
+ ComponentScope::Status
+ }
+
+ fn description() -> Option<&'static str> {
+ Some(Self::DOCS)
+ }
+
+ fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
+ let max_value = 180.0;
+
+ let empty_progress_bar = cx.new(|cx| ProgressBar::new("empty", 0.0, max_value, cx));
+ let partial_progress_bar =
+ cx.new(|cx| ProgressBar::new("partial", max_value * 0.35, max_value, cx));
+ let filled_progress_bar = cx.new(|cx| ProgressBar::new("filled", max_value, max_value, cx));
+
+ Some(
+ div()
+ .flex()
+ .flex_col()
+ .gap_4()
+ .p_4()
+ .w(px(240.0))
+ .child(div().child("Progress Bar"))
+ .child(
+ div()
+ .flex()
+ .flex_col()
+ .gap_2()
+ .child(
+ div()
+ .flex()
+ .justify_between()
+ .child(Label::new("0%"))
+ .child(Label::new("Empty")),
+ )
+ .child(empty_progress_bar.clone()),
+ )
+ .child(
+ div()
+ .flex()
+ .flex_col()
+ .gap_2()
+ .child(
+ div()
+ .flex()
+ .justify_between()
+ .child(Label::new("38%"))
+ .child(Label::new("Partial")),
+ )
+ .child(partial_progress_bar.clone()),
+ )
+ .child(
+ div()
+ .flex()
+ .flex_col()
+ .gap_2()
+ .child(
+ div()
+ .flex()
+ .justify_between()
+ .child(Label::new("100%"))
+ .child(Label::new("Complete")),
+ )
+ .child(filled_progress_bar.clone()),
+ )
+ .into_any_element(),
+ )
+ }
+}
@@ -235,7 +235,7 @@ impl Headline {
impl Component for Headline {
fn scope() -> ComponentScope {
- ComponentScope::None
+ ComponentScope::Typography
}
fn description() -> Option<&'static str> {