diff --git a/crates/story/src/divider_story.rs b/crates/story/src/divider_story.rs new file mode 100644 index 00000000..8a2b2816 --- /dev/null +++ b/crates/story/src/divider_story.rs @@ -0,0 +1,93 @@ +use crate::section; +use gpui::{ + App, AppContext, Context, Entity, Focusable, IntoElement, ParentElement, Render, Styled, + Window, px, +}; +use gpui_component::{ActiveTheme, divider::Divider, h_flex, label::Label, v_flex}; + +const DESCRIPTION: &str = "GPUI Component is a Rust GUI components for building fantastic cross-platform desktop application by using GPUI."; + +pub struct DividerStory { + focus_handle: gpui::FocusHandle, +} + +impl super::Story for DividerStory { + fn title() -> &'static str { + "Divider" + } + + fn description() -> &'static str { + "A divider that can be either vertical or horizontal." + } + + fn new_view(window: &mut Window, cx: &mut App) -> Entity { + Self::view(window, cx) + } +} + +impl DividerStory { + pub fn view(_window: &mut Window, cx: &mut App) -> Entity { + cx.new(|cx| Self { + focus_handle: cx.focus_handle(), + }) + } +} + +impl Focusable for DividerStory { + fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for DividerStory { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + v_flex() + .gap_6() + .child( + section("Horizontal Dividers").child( + v_flex() + .gap_4() + .w_full() + .mt_4() + .child(Divider::horizontal()) + .child(Divider::horizontal().label("With Label")) + .child(Divider::horizontal_dashed()) + .child(Divider::horizontal_dashed().label("Dashed With Label")), + ), + ) + .child( + section("Vertical Dividers").child( + h_flex() + .gap_4() + .h(px(100.)) + .child(Divider::vertical()) + .child(Divider::vertical().label("Solid")) + .child(Divider::vertical_dashed()) + .child(Divider::vertical_dashed().label("Dashed")), + ), + ) + .child( + section("Combination Dividers").child( + v_flex() + .gap_y_4() + .child( + v_flex().gap_y_2().child("Hello GPUI Component").child( + Label::new(DESCRIPTION) + .text_color(cx.theme().muted_foreground) + .text_sm(), + ), + ) + .child(Divider::horizontal()) + .child( + h_flex() + .gap_x_4() + .child("Docs") + .child(Divider::vertical().dashed()) + .child("Github") + .child(Divider::vertical().dashed()) + .child("Source"), + ), + ), + ) + } +} diff --git a/crates/story/src/lib.rs b/crates/story/src/lib.rs index 3c9cdfd3..0d9324a4 100644 --- a/crates/story/src/lib.rs +++ b/crates/story/src/lib.rs @@ -13,6 +13,7 @@ mod color_picker_story; mod date_picker_story; mod description_list_story; mod dialog_story; +mod divider_story; mod dropdown_button_story; mod form_story; mod group_box_story; @@ -73,6 +74,7 @@ pub use color_picker_story::ColorPickerStory; pub use date_picker_story::DatePickerStory; pub use description_list_story::DescriptionListStory; pub use dialog_story::DialogStory; +pub use divider_story::DividerStory; pub use dropdown_button_story::DropdownButtonStory; pub use form_story::FormStory; pub use group_box_story::GroupBoxStory; @@ -667,6 +669,7 @@ impl StoryState { "InputStory" => story!(InputStory), "ListStory" => story!(ListStory), "DialogStory" => story!(DialogStory), + "DividerStory" => story!(DividerStory), "PopoverStory" => story!(PopoverStory), "ProgressStory" => story!(ProgressStory), "ResizableStory" => story!(ResizableStory), diff --git a/crates/story/src/main.rs b/crates/story/src/main.rs index a6747b0e..0d79d331 100644 --- a/crates/story/src/main.rs +++ b/crates/story/src/main.rs @@ -51,6 +51,7 @@ impl Gallery { StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), + StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), diff --git a/crates/ui/src/divider.rs b/crates/ui/src/divider.rs index dd05cef4..fbda6f65 100644 --- a/crates/ui/src/divider.rs +++ b/crates/ui/src/divider.rs @@ -1,9 +1,17 @@ use crate::{ActiveTheme, StyledExt}; use gpui::{ - div, prelude::FluentBuilder as _, px, App, Axis, Div, Hsla, IntoElement, ParentElement, - RenderOnce, SharedString, StyleRefinement, Styled, Window, + App, Axis, Div, Hsla, IntoElement, ParentElement, PathBuilder, RenderOnce, SharedString, + StyleRefinement, Styled, Window, canvas, div, point, prelude::FluentBuilder as _, px, }; +/// The style of the divider line. +#[derive(Clone, Copy, PartialEq, Default)] +pub enum DividerStyle { + #[default] + Solid, + Dashed, +} + /// A divider that can be either vertical or horizontal. #[derive(IntoElement)] pub struct Divider { @@ -12,6 +20,7 @@ pub struct Divider { label: Option, axis: Axis, color: Option, + line_style: DividerStyle, } impl Divider { @@ -23,6 +32,7 @@ impl Divider { label: None, color: None, style: StyleRefinement::default(), + line_style: DividerStyle::Solid, } } @@ -34,9 +44,20 @@ impl Divider { label: None, color: None, style: StyleRefinement::default(), + line_style: DividerStyle::Solid, } } + /// Creates a vertical dashed divider. + pub fn vertical_dashed() -> Self { + Self::vertical().dashed() + } + + /// Creates a horizontal dashed divider. + pub fn horizontal_dashed() -> Self { + Self::horizontal().dashed() + } + /// Sets the label for the divider. pub fn label(mut self, label: impl Into) -> Self { self.label = Some(label.into()); @@ -48,6 +69,52 @@ impl Divider { self.color = Some(color.into()); self } + + /// Sets the style of the divider to dashed. + pub fn dashed(mut self) -> Self { + self.line_style = DividerStyle::Dashed; + self + } + + fn render_base(axis: Axis) -> Div { + div().absolute().map(|this| match axis { + Axis::Vertical => this.w(px(1.)).h_full(), + Axis::Horizontal => this.h(px(1.)).w_full(), + }) + } + + fn render_solid(axis: Axis, color: Hsla) -> impl IntoElement { + Self::render_base(axis).bg(color) + } + + fn render_dashed(axis: Axis, color: Hsla) -> impl IntoElement { + Self::render_base(axis).child( + canvas( + move |_, _, _| {}, + move |bounds, _, window, _| { + let mut builder = PathBuilder::stroke(px(1.)).dash_array(&[px(4.), px(2.)]); + let (start, end) = match axis { + Axis::Horizontal => { + let x = bounds.origin.x; + let y = bounds.origin.y + px(0.5); + (point(x, y), point(x + bounds.size.width, y)) + } + Axis::Vertical => { + let x = bounds.origin.x + px(0.5); + let y = bounds.origin.y; + (point(x, y), point(x, y + bounds.size.height)) + } + }; + builder.move_to(start); + builder.line_to(end); + if let Ok(line) = builder.build() { + window.paint_path(line, color); + } + }, + ) + .size_full(), + ) + } } impl Styled for Divider { @@ -58,21 +125,20 @@ impl Styled for Divider { impl RenderOnce for Divider { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { + let color = self.color.unwrap_or(cx.theme().border); + let axis = self.axis; + let line_style = self.line_style; + self.base .flex() .flex_shrink_0() .items_center() .justify_center() .refine_style(&self.style) - .child( - div() - .absolute() - .map(|this| match self.axis { - Axis::Vertical => this.w(px(1.)).h_full(), - Axis::Horizontal => this.h(px(1.)).w_full(), - }) - .bg(self.color.unwrap_or(cx.theme().border)), - ) + .child(match line_style { + DividerStyle::Solid => Self::render_solid(axis, color).into_any_element(), + DividerStyle::Dashed => Self::render_dashed(axis, color).into_any_element(), + }) .when_some(self.label, |this, label| { this.child( div()