divider: Add support for dashed divider (#1730)

## Description

1. Add support for dashed divider.
2. Add divider story.

## Screenshot

<img width="1139" height="859" alt="SCR-20251203-nueg"
src="https://github.com/user-attachments/assets/3f2dafd9-f808-4315-bf58-c0380798eb66"
/>
This commit is contained in:
Floyd Wang 2025-12-03 16:22:36 +08:00 committed by GitHub
parent ec097ed406
commit e2f022cb59
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 174 additions and 11 deletions

View file

@ -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<impl Render> {
Self::view(window, cx)
}
}
impl DividerStory {
pub fn view(_window: &mut Window, cx: &mut App) -> Entity<Self> {
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<Self>) -> 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"),
),
),
)
}
}

View file

@ -13,6 +13,7 @@ mod color_picker_story;
mod date_picker_story; mod date_picker_story;
mod description_list_story; mod description_list_story;
mod dialog_story; mod dialog_story;
mod divider_story;
mod dropdown_button_story; mod dropdown_button_story;
mod form_story; mod form_story;
mod group_box_story; mod group_box_story;
@ -73,6 +74,7 @@ pub use color_picker_story::ColorPickerStory;
pub use date_picker_story::DatePickerStory; pub use date_picker_story::DatePickerStory;
pub use description_list_story::DescriptionListStory; pub use description_list_story::DescriptionListStory;
pub use dialog_story::DialogStory; pub use dialog_story::DialogStory;
pub use divider_story::DividerStory;
pub use dropdown_button_story::DropdownButtonStory; pub use dropdown_button_story::DropdownButtonStory;
pub use form_story::FormStory; pub use form_story::FormStory;
pub use group_box_story::GroupBoxStory; pub use group_box_story::GroupBoxStory;
@ -667,6 +669,7 @@ impl StoryState {
"InputStory" => story!(InputStory), "InputStory" => story!(InputStory),
"ListStory" => story!(ListStory), "ListStory" => story!(ListStory),
"DialogStory" => story!(DialogStory), "DialogStory" => story!(DialogStory),
"DividerStory" => story!(DividerStory),
"PopoverStory" => story!(PopoverStory), "PopoverStory" => story!(PopoverStory),
"ProgressStory" => story!(ProgressStory), "ProgressStory" => story!(ProgressStory),
"ResizableStory" => story!(ResizableStory), "ResizableStory" => story!(ResizableStory),

View file

@ -51,6 +51,7 @@ impl Gallery {
StoryContainer::panel::<DatePickerStory>(window, cx), StoryContainer::panel::<DatePickerStory>(window, cx),
StoryContainer::panel::<DescriptionListStory>(window, cx), StoryContainer::panel::<DescriptionListStory>(window, cx),
StoryContainer::panel::<DialogStory>(window, cx), StoryContainer::panel::<DialogStory>(window, cx),
StoryContainer::panel::<DividerStory>(window, cx),
StoryContainer::panel::<DropdownButtonStory>(window, cx), StoryContainer::panel::<DropdownButtonStory>(window, cx),
StoryContainer::panel::<FormStory>(window, cx), StoryContainer::panel::<FormStory>(window, cx),
StoryContainer::panel::<GroupBoxStory>(window, cx), StoryContainer::panel::<GroupBoxStory>(window, cx),

View file

@ -1,9 +1,17 @@
use crate::{ActiveTheme, StyledExt}; use crate::{ActiveTheme, StyledExt};
use gpui::{ use gpui::{
div, prelude::FluentBuilder as _, px, App, Axis, Div, Hsla, IntoElement, ParentElement, App, Axis, Div, Hsla, IntoElement, ParentElement, PathBuilder, RenderOnce, SharedString,
RenderOnce, SharedString, StyleRefinement, Styled, Window, 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. /// A divider that can be either vertical or horizontal.
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct Divider { pub struct Divider {
@ -12,6 +20,7 @@ pub struct Divider {
label: Option<SharedString>, label: Option<SharedString>,
axis: Axis, axis: Axis,
color: Option<Hsla>, color: Option<Hsla>,
line_style: DividerStyle,
} }
impl Divider { impl Divider {
@ -23,6 +32,7 @@ impl Divider {
label: None, label: None,
color: None, color: None,
style: StyleRefinement::default(), style: StyleRefinement::default(),
line_style: DividerStyle::Solid,
} }
} }
@ -34,9 +44,20 @@ impl Divider {
label: None, label: None,
color: None, color: None,
style: StyleRefinement::default(), 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. /// Sets the label for the divider.
pub fn label(mut self, label: impl Into<SharedString>) -> Self { pub fn label(mut self, label: impl Into<SharedString>) -> Self {
self.label = Some(label.into()); self.label = Some(label.into());
@ -48,6 +69,52 @@ impl Divider {
self.color = Some(color.into()); self.color = Some(color.into());
self 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 { impl Styled for Divider {
@ -58,21 +125,20 @@ impl Styled for Divider {
impl RenderOnce for Divider { impl RenderOnce for Divider {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { 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 self.base
.flex() .flex()
.flex_shrink_0() .flex_shrink_0()
.items_center() .items_center()
.justify_center() .justify_center()
.refine_style(&self.style) .refine_style(&self.style)
.child( .child(match line_style {
div() DividerStyle::Solid => Self::render_solid(axis, color).into_any_element(),
.absolute() DividerStyle::Dashed => Self::render_dashed(axis, color).into_any_element(),
.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)),
)
.when_some(self.label, |this, label| { .when_some(self.label, |this, label| {
this.child( this.child(
div() div()