diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index 5b2c7d6172..6d13e8a71a 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -1,6 +1,7 @@ mod avatar; mod button; mod checkbox; +mod content_group; mod context_menu; mod disclosure; mod divider; @@ -36,6 +37,7 @@ mod stories; pub use avatar::*; pub use button::*; pub use checkbox::*; +pub use content_group::*; pub use context_menu::*; pub use disclosure::*; pub use divider::*; diff --git a/crates/ui/src/components/content_group.rs b/crates/ui/src/components/content_group.rs new file mode 100644 index 0000000000..b8ba5b8860 --- /dev/null +++ b/crates/ui/src/components/content_group.rs @@ -0,0 +1,135 @@ +use crate::prelude::*; +use gpui::{AnyElement, IntoElement, ParentElement, StyleRefinement, Styled}; +use smallvec::SmallVec; + +/// Creates a new [ContentGroup]. +pub fn content_group() -> ContentGroup { + ContentGroup::new() +} + +/// A [ContentGroup] that vertically stacks its children. +/// +/// This is a convenience function that simply combines [`ContentGroup`] and [`v_flex`](crate::v_flex). +pub fn v_group() -> ContentGroup { + content_group().v_flex() +} + +/// Creates a new horizontal [ContentGroup]. +/// +/// This is a convenience function that simply combines [`ContentGroup`] and [`h_flex`](crate::h_flex). +pub fn h_group() -> ContentGroup { + content_group().h_flex() +} + +/// A flexible container component that can hold other elements. +#[derive(IntoElement)] +pub struct ContentGroup { + base: Div, + border: bool, + fill: bool, + children: SmallVec<[AnyElement; 2]>, +} + +impl ContentGroup { + /// Creates a new [ContentBox]. + pub fn new() -> Self { + Self { + base: div(), + border: true, + fill: true, + children: SmallVec::new(), + } + } + + /// Removes the border from the [ContentBox]. + pub fn borderless(mut self) -> Self { + self.border = false; + self + } + + /// Removes the background fill from the [ContentBox]. + pub fn unfilled(mut self) -> Self { + self.fill = false; + self + } +} + +impl ParentElement for ContentGroup { + fn extend(&mut self, elements: impl IntoIterator) { + self.children.extend(elements) + } +} + +impl Styled for ContentGroup { + fn style(&mut self) -> &mut StyleRefinement { + self.base.style() + } +} + +impl RenderOnce for ContentGroup { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + // TODO: + // Baked in padding will make scrollable views inside of content boxes awkward. + // + // Do we make the padding optional, or do we push to use a different component? + + self.base + .when(self.fill, |this| { + this.bg(cx.theme().colors().text.opacity(0.05)) + }) + .when(self.border, |this| { + this.border_1().border_color(cx.theme().colors().border) + }) + .rounded_md() + .p_2() + .children(self.children) + } +} + +impl ComponentPreview for ContentGroup { + fn description() -> impl Into> { + "A flexible container component that can hold other elements. It can be customized with or without a border and background fill." + } + + fn example_label_side() -> ExampleLabelSide { + ExampleLabelSide::Bottom + } + + fn examples(_: &WindowContext) -> Vec> { + vec![example_group(vec![ + single_example( + "Default", + ContentGroup::new() + .flex_1() + .items_center() + .justify_center() + .h_48() + .child(Label::new("Default ContentBox")), + ) + .grow(), + single_example( + "Without Border", + ContentGroup::new() + .flex_1() + .items_center() + .justify_center() + .h_48() + .borderless() + .child(Label::new("Borderless ContentBox")), + ) + .grow(), + single_example( + "Without Fill", + ContentGroup::new() + .flex_1() + .items_center() + .justify_center() + .h_48() + .unfilled() + .child(Label::new("Unfilled ContentBox")), + ) + .grow(), + ]) + .grow()] + } +} diff --git a/crates/ui/src/prelude.rs b/crates/ui/src/prelude.rs index 0e79475e00..e763d0b663 100644 --- a/crates/ui/src/prelude.rs +++ b/crates/ui/src/prelude.rs @@ -16,7 +16,7 @@ pub use crate::traits::selectable::*; pub use crate::traits::styled_ext::*; pub use crate::traits::visible_on_hover::*; pub use crate::DynamicSpacing; -pub use crate::{h_flex, v_flex}; +pub use crate::{h_flex, h_group, v_flex, v_group}; pub use crate::{Button, ButtonSize, ButtonStyle, IconButton, SelectableButton}; pub use crate::{ButtonCommon, Color}; pub use crate::{Headline, HeadlineSize}; diff --git a/crates/ui/src/traits/component_preview.rs b/crates/ui/src/traits/component_preview.rs index 1cb577a97f..eefc1e8228 100644 --- a/crates/ui/src/traits/component_preview.rs +++ b/crates/ui/src/traits/component_preview.rs @@ -32,6 +32,10 @@ pub trait ComponentPreview: IntoElement { fn examples(_cx: &WindowContext) -> Vec>; + fn custom_example(_cx: &WindowContext) -> impl Into> { + None:: + } + fn component_previews(cx: &WindowContext) -> Vec { Self::examples(cx) .into_iter() @@ -47,7 +51,8 @@ pub trait ComponentPreview: IntoElement { let description = Self::description().into(); v_flex() - .gap_3() + .w_full() + .gap_6() .p_4() .border_1() .border_color(cx.theme().colors().border) @@ -73,18 +78,23 @@ pub trait ComponentPreview: IntoElement { ) }), ) + .when_some(Self::custom_example(cx).into(), |this, custom_example| { + this.child(custom_example) + }) .children(Self::component_previews(cx)) .into_any_element() } fn render_example_group(group: ComponentExampleGroup) -> AnyElement { v_flex() - .gap_2() + .gap_6() + .when(group.grow, |this| this.w_full().flex_1()) .when_some(group.title, |this, title| { this.child(Label::new(title).size(LabelSize::Small)) }) .child( h_flex() + .w_full() .gap_6() .children(group.examples.into_iter().map(Self::render_example)) .into_any_element(), @@ -103,6 +113,7 @@ pub trait ComponentPreview: IntoElement { }; base.gap_1() + .when(example.grow, |this| this.flex_1()) .child(example.element) .child( Label::new(example.variant_name) @@ -117,6 +128,7 @@ pub trait ComponentPreview: IntoElement { pub struct ComponentExample { variant_name: SharedString, element: T, + grow: bool, } impl ComponentExample { @@ -125,14 +137,22 @@ impl ComponentExample { Self { variant_name: variant_name.into(), element: example, + grow: false, } } + + /// Set the example to grow to fill the available horizontal space. + pub fn grow(mut self) -> Self { + self.grow = true; + self + } } /// A group of component examples. pub struct ComponentExampleGroup { pub title: Option, pub examples: Vec>, + pub grow: bool, } impl ComponentExampleGroup { @@ -141,15 +161,24 @@ impl ComponentExampleGroup { Self { title: None, examples, + grow: false, } } + /// Create a new group of examples with the given title. pub fn with_title(title: impl Into, examples: Vec>) -> Self { Self { title: Some(title.into()), examples, + grow: false, } } + + /// Set the group to grow to fill the available horizontal space. + pub fn grow(mut self) -> Self { + self.grow = true; + self + } } /// Create a single example diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index a644fc22dc..c8d5bf6dfc 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -267,13 +267,8 @@ impl Render for WelcomePage { ), ) .child( - v_flex() - .p_3() + v_group() .gap_2() - .bg(cx.theme().colors().element_background) - .border_1() - .border_color(cx.theme().colors().border_variant) - .rounded_md() .child(CheckboxWithLabel::new( "enable-vim", Label::new("Enable Vim Mode"), diff --git a/crates/workspace/src/theme_preview.rs b/crates/workspace/src/theme_preview.rs index fef4dfc86e..337e603d84 100644 --- a/crates/workspace/src/theme_preview.rs +++ b/crates/workspace/src/theme_preview.rs @@ -5,8 +5,8 @@ use theme::all_theme_colors; use ui::{ element_cell, prelude::*, string_cell, utils::calculate_contrast_ratio, AudioStatus, Availability, Avatar, AvatarAudioStatusIndicator, AvatarAvailabilityIndicator, ButtonLike, - Checkbox, CheckboxWithLabel, DecoratedIcon, ElevationIndex, Facepile, IconDecoration, - Indicator, Table, TintColor, Tooltip, + Checkbox, CheckboxWithLabel, ContentGroup, DecoratedIcon, ElevationIndex, Facepile, + IconDecoration, Indicator, Table, TintColor, Tooltip, }; use crate::{Item, Workspace}; @@ -510,6 +510,7 @@ impl ThemePreview { .overflow_scroll() .size_full() .gap_2() + .child(ContentGroup::render_component_previews(cx)) .child(IconDecoration::render_component_previews(cx)) .child(DecoratedIcon::render_component_previews(cx)) .child(Checkbox::render_component_previews(cx))