Remove 2 suffix for ui, storybook, text
Co-authored-by: Mikayla <mikayla@zed.dev>
This commit is contained in:
parent
0cf65223ce
commit
4305c5fdbe
142 changed files with 106 additions and 5018 deletions
91
crates/ui/src/components/avatar.rs
Normal file
91
crates/ui/src/components/avatar.rs
Normal file
|
@ -0,0 +1,91 @@
|
|||
use crate::prelude::*;
|
||||
use gpui::{img, Hsla, ImageSource, Img, IntoElement, Styled};
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Clone)]
|
||||
pub enum Shape {
|
||||
#[default]
|
||||
Circle,
|
||||
RoundedRectangle,
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Avatar {
|
||||
image: Img,
|
||||
border_color: Option<Hsla>,
|
||||
is_available: Option<bool>,
|
||||
}
|
||||
|
||||
impl RenderOnce for Avatar {
|
||||
fn render(mut self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
if self.image.style().corner_radii.top_left.is_none() {
|
||||
self = self.shape(Shape::Circle);
|
||||
}
|
||||
|
||||
let size = cx.rem_size();
|
||||
|
||||
div()
|
||||
.size(size + px(2.))
|
||||
.map(|mut div| {
|
||||
div.style().corner_radii = self.image.style().corner_radii.clone();
|
||||
div
|
||||
})
|
||||
.when_some(self.border_color, |this, color| {
|
||||
this.border().border_color(color)
|
||||
})
|
||||
.child(
|
||||
self.image
|
||||
.size(size)
|
||||
.bg(cx.theme().colors().ghost_element_background),
|
||||
)
|
||||
.children(self.is_available.map(|is_free| {
|
||||
// HACK: non-integer sizes result in oval indicators.
|
||||
let indicator_size = (size * 0.4).round();
|
||||
|
||||
div()
|
||||
.absolute()
|
||||
.z_index(1)
|
||||
.bg(if is_free {
|
||||
cx.theme().status().created
|
||||
} else {
|
||||
cx.theme().status().deleted
|
||||
})
|
||||
.size(indicator_size)
|
||||
.rounded(indicator_size)
|
||||
.bottom_0()
|
||||
.right_0()
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Avatar {
|
||||
pub fn new(src: impl Into<ImageSource>) -> Self {
|
||||
Avatar {
|
||||
image: img(src),
|
||||
is_available: None,
|
||||
border_color: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shape(mut self, shape: Shape) -> Self {
|
||||
self.image = match shape {
|
||||
Shape::Circle => self.image.rounded_full(),
|
||||
Shape::RoundedRectangle => self.image.rounded_md(),
|
||||
};
|
||||
self
|
||||
}
|
||||
|
||||
pub fn grayscale(mut self, grayscale: bool) -> Self {
|
||||
self.image = self.image.grayscale(grayscale);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn border_color(mut self, color: impl Into<Hsla>) -> Self {
|
||||
self.border_color = Some(color.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn availability_indicator(mut self, is_available: impl Into<Option<bool>>) -> Self {
|
||||
self.is_available = is_available.into();
|
||||
self
|
||||
}
|
||||
}
|
10
crates/ui/src/components/button.rs
Normal file
10
crates/ui/src/components/button.rs
Normal file
|
@ -0,0 +1,10 @@
|
|||
mod button;
|
||||
pub(self) mod button_icon;
|
||||
mod button_like;
|
||||
mod icon_button;
|
||||
mod toggle_button;
|
||||
|
||||
pub use button::*;
|
||||
pub use button_like::*;
|
||||
pub use icon_button::*;
|
||||
pub use toggle_button::*;
|
188
crates/ui/src/components/button/button.rs
Normal file
188
crates/ui/src/components/button/button.rs
Normal file
|
@ -0,0 +1,188 @@
|
|||
use gpui::{AnyView, DefiniteLength};
|
||||
|
||||
use crate::{prelude::*, IconPosition};
|
||||
use crate::{
|
||||
ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, IconSize, Label, LineHeightStyle,
|
||||
};
|
||||
|
||||
use super::button_icon::ButtonIcon;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Button {
|
||||
base: ButtonLike,
|
||||
label: SharedString,
|
||||
label_color: Option<Color>,
|
||||
label_size: Option<LabelSize>,
|
||||
selected_label: Option<SharedString>,
|
||||
icon: Option<Icon>,
|
||||
icon_position: Option<IconPosition>,
|
||||
icon_size: Option<IconSize>,
|
||||
icon_color: Option<Color>,
|
||||
selected_icon: Option<Icon>,
|
||||
}
|
||||
|
||||
impl Button {
|
||||
pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
base: ButtonLike::new(id),
|
||||
label: label.into(),
|
||||
label_color: None,
|
||||
label_size: None,
|
||||
selected_label: None,
|
||||
icon: None,
|
||||
icon_position: None,
|
||||
icon_size: None,
|
||||
icon_color: None,
|
||||
selected_icon: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn color(mut self, label_color: impl Into<Option<Color>>) -> Self {
|
||||
self.label_color = label_color.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn label_size(mut self, label_size: impl Into<Option<LabelSize>>) -> Self {
|
||||
self.label_size = label_size.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn selected_label<L: Into<SharedString>>(mut self, label: impl Into<Option<L>>) -> Self {
|
||||
self.selected_label = label.into().map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
|
||||
self.icon = icon.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn icon_position(mut self, icon_position: impl Into<Option<IconPosition>>) -> Self {
|
||||
self.icon_position = icon_position.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn icon_size(mut self, icon_size: impl Into<Option<IconSize>>) -> Self {
|
||||
self.icon_size = icon_size.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn icon_color(mut self, icon_color: impl Into<Option<Color>>) -> Self {
|
||||
self.icon_color = icon_color.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn selected_icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
|
||||
self.selected_icon = icon.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Selectable for Button {
|
||||
fn selected(mut self, selected: bool) -> Self {
|
||||
self.base = self.base.selected(selected);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Disableable for Button {
|
||||
fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.base = self.base.disabled(disabled);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Clickable for Button {
|
||||
fn on_click(
|
||||
mut self,
|
||||
handler: impl Fn(&gpui::ClickEvent, &mut WindowContext) + 'static,
|
||||
) -> Self {
|
||||
self.base = self.base.on_click(handler);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl FixedWidth for Button {
|
||||
fn width(mut self, width: DefiniteLength) -> Self {
|
||||
self.base = self.base.width(width);
|
||||
self
|
||||
}
|
||||
|
||||
fn full_width(mut self) -> Self {
|
||||
self.base = self.base.full_width();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ButtonCommon for Button {
|
||||
fn id(&self) -> &ElementId {
|
||||
self.base.id()
|
||||
}
|
||||
|
||||
fn style(mut self, style: ButtonStyle) -> Self {
|
||||
self.base = self.base.style(style);
|
||||
self
|
||||
}
|
||||
|
||||
fn size(mut self, size: ButtonSize) -> Self {
|
||||
self.base = self.base.size(size);
|
||||
self
|
||||
}
|
||||
|
||||
fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
|
||||
self.base = self.base.tooltip(tooltip);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Button {
|
||||
#[allow(refining_impl_trait)]
|
||||
fn render(self, _cx: &mut WindowContext) -> ButtonLike {
|
||||
let is_disabled = self.base.disabled;
|
||||
let is_selected = self.base.selected;
|
||||
|
||||
let label = self
|
||||
.selected_label
|
||||
.filter(|_| is_selected)
|
||||
.unwrap_or(self.label);
|
||||
|
||||
let label_color = if is_disabled {
|
||||
Color::Disabled
|
||||
} else if is_selected {
|
||||
Color::Selected
|
||||
} else {
|
||||
self.label_color.unwrap_or_default()
|
||||
};
|
||||
|
||||
self.base.child(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.when(self.icon_position.is_some(), |this| {
|
||||
this.children(self.icon.map(|icon| {
|
||||
ButtonIcon::new(icon)
|
||||
.disabled(is_disabled)
|
||||
.selected(is_selected)
|
||||
.selected_icon(self.selected_icon)
|
||||
.size(self.icon_size)
|
||||
.color(self.icon_color)
|
||||
}))
|
||||
})
|
||||
.child(
|
||||
Label::new(label)
|
||||
.color(label_color)
|
||||
.size(self.label_size.unwrap_or_default())
|
||||
.line_height_style(LineHeightStyle::UiLabel),
|
||||
)
|
||||
.when(!self.icon_position.is_some(), |this| {
|
||||
this.children(self.icon.map(|icon| {
|
||||
ButtonIcon::new(icon)
|
||||
.disabled(is_disabled)
|
||||
.selected(is_selected)
|
||||
.selected_icon(self.selected_icon)
|
||||
.size(self.icon_size)
|
||||
.color(self.icon_color)
|
||||
}))
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
82
crates/ui/src/components/button/button_icon.rs
Normal file
82
crates/ui/src/components/button/button_icon.rs
Normal file
|
@ -0,0 +1,82 @@
|
|||
use crate::{prelude::*, Icon, IconElement, IconSize};
|
||||
|
||||
/// An icon that appears within a button.
|
||||
///
|
||||
/// Can be used as either an icon alongside a label, like in [`Button`](crate::Button),
|
||||
/// or as a standalone icon, like in [`IconButton`](crate::IconButton).
|
||||
#[derive(IntoElement)]
|
||||
pub(super) struct ButtonIcon {
|
||||
icon: Icon,
|
||||
size: IconSize,
|
||||
color: Color,
|
||||
disabled: bool,
|
||||
selected: bool,
|
||||
selected_icon: Option<Icon>,
|
||||
}
|
||||
|
||||
impl ButtonIcon {
|
||||
pub fn new(icon: Icon) -> Self {
|
||||
Self {
|
||||
icon,
|
||||
size: IconSize::default(),
|
||||
color: Color::default(),
|
||||
disabled: false,
|
||||
selected: false,
|
||||
selected_icon: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(mut self, size: impl Into<Option<IconSize>>) -> Self {
|
||||
if let Some(size) = size.into() {
|
||||
self.size = size;
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: impl Into<Option<Color>>) -> Self {
|
||||
if let Some(color) = color.into() {
|
||||
self.color = color;
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn selected_icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
|
||||
self.selected_icon = icon.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Disableable for ButtonIcon {
|
||||
fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Selectable for ButtonIcon {
|
||||
fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ButtonIcon {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
let icon = self
|
||||
.selected_icon
|
||||
.filter(|_| self.selected)
|
||||
.unwrap_or(self.icon);
|
||||
|
||||
let icon_color = if self.disabled {
|
||||
Color::Disabled
|
||||
} else if self.selected {
|
||||
Color::Selected
|
||||
} else {
|
||||
self.color
|
||||
};
|
||||
|
||||
IconElement::new(icon).size(self.size).color(icon_color)
|
||||
}
|
||||
}
|
407
crates/ui/src/components/button/button_like.rs
Normal file
407
crates/ui/src/components/button/button_like.rs
Normal file
|
@ -0,0 +1,407 @@
|
|||
use gpui::{relative, DefiniteLength, MouseButton};
|
||||
use gpui::{rems, transparent_black, AnyElement, AnyView, ClickEvent, Hsla, Rems};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
pub trait ButtonCommon: Clickable + Disableable {
|
||||
/// A unique element ID to identify the button.
|
||||
fn id(&self) -> &ElementId;
|
||||
|
||||
/// The visual style of the button.
|
||||
///
|
||||
/// Most commonly will be [`ButtonStyle::Subtle`], or [`ButtonStyle::Filled`]
|
||||
/// for an emphasized button.
|
||||
fn style(self, style: ButtonStyle) -> Self;
|
||||
|
||||
/// The size of the button.
|
||||
///
|
||||
/// Most buttons will use the default size.
|
||||
///
|
||||
/// [`ButtonSize`] can also be used to help build non-button elements
|
||||
/// that are consistently sized with buttons.
|
||||
fn size(self, size: ButtonSize) -> Self;
|
||||
|
||||
/// The tooltip that shows when a user hovers over the button.
|
||||
///
|
||||
/// Nearly all interactable elements should have a tooltip. Some example
|
||||
/// exceptions might a scroll bar, or a slider.
|
||||
fn tooltip(self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self;
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
|
||||
pub enum IconPosition {
|
||||
#[default]
|
||||
Start,
|
||||
End,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
|
||||
pub enum ButtonStyle {
|
||||
/// A filled button with a solid background color. Provides emphasis versus
|
||||
/// the more common subtle button.
|
||||
Filled,
|
||||
|
||||
/// 🚧 Under construction 🚧
|
||||
///
|
||||
/// Used to emphasize a button in some way, like a selected state, or a semantic
|
||||
/// coloring like an error or success button.
|
||||
Tinted,
|
||||
|
||||
/// The default button style, used for most buttons. Has a transparent background,
|
||||
/// but has a background color to indicate states like hover and active.
|
||||
#[default]
|
||||
Subtle,
|
||||
|
||||
/// Used for buttons that only change forground color on hover and active states.
|
||||
///
|
||||
/// TODO: Better docs for this.
|
||||
Transparent,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
pub(crate) enum ButtonLikeRounding {
|
||||
All,
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ButtonLikeStyles {
|
||||
pub background: Hsla,
|
||||
#[allow(unused)]
|
||||
pub border_color: Hsla,
|
||||
#[allow(unused)]
|
||||
pub label_color: Hsla,
|
||||
#[allow(unused)]
|
||||
pub icon_color: Hsla,
|
||||
}
|
||||
|
||||
impl ButtonStyle {
|
||||
pub(crate) fn enabled(self, cx: &mut WindowContext) -> ButtonLikeStyles {
|
||||
match self {
|
||||
ButtonStyle::Filled => ButtonLikeStyles {
|
||||
background: cx.theme().colors().element_background,
|
||||
border_color: transparent_black(),
|
||||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle::Tinted => ButtonLikeStyles {
|
||||
background: gpui::red(),
|
||||
border_color: gpui::red(),
|
||||
label_color: gpui::red(),
|
||||
icon_color: gpui::red(),
|
||||
},
|
||||
ButtonStyle::Subtle => ButtonLikeStyles {
|
||||
background: cx.theme().colors().ghost_element_background,
|
||||
border_color: transparent_black(),
|
||||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle::Transparent => ButtonLikeStyles {
|
||||
background: transparent_black(),
|
||||
border_color: transparent_black(),
|
||||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn hovered(self, cx: &mut WindowContext) -> ButtonLikeStyles {
|
||||
match self {
|
||||
ButtonStyle::Filled => ButtonLikeStyles {
|
||||
background: cx.theme().colors().element_hover,
|
||||
border_color: transparent_black(),
|
||||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle::Tinted => ButtonLikeStyles {
|
||||
background: gpui::red(),
|
||||
border_color: gpui::red(),
|
||||
label_color: gpui::red(),
|
||||
icon_color: gpui::red(),
|
||||
},
|
||||
ButtonStyle::Subtle => ButtonLikeStyles {
|
||||
background: cx.theme().colors().ghost_element_hover,
|
||||
border_color: transparent_black(),
|
||||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle::Transparent => ButtonLikeStyles {
|
||||
background: transparent_black(),
|
||||
border_color: transparent_black(),
|
||||
// TODO: These are not great
|
||||
label_color: Color::Muted.color(cx),
|
||||
// TODO: These are not great
|
||||
icon_color: Color::Muted.color(cx),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn active(self, cx: &mut WindowContext) -> ButtonLikeStyles {
|
||||
match self {
|
||||
ButtonStyle::Filled => ButtonLikeStyles {
|
||||
background: cx.theme().colors().element_active,
|
||||
border_color: transparent_black(),
|
||||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle::Tinted => ButtonLikeStyles {
|
||||
background: gpui::red(),
|
||||
border_color: gpui::red(),
|
||||
label_color: gpui::red(),
|
||||
icon_color: gpui::red(),
|
||||
},
|
||||
ButtonStyle::Subtle => ButtonLikeStyles {
|
||||
background: cx.theme().colors().ghost_element_active,
|
||||
border_color: transparent_black(),
|
||||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle::Transparent => ButtonLikeStyles {
|
||||
background: transparent_black(),
|
||||
border_color: transparent_black(),
|
||||
// TODO: These are not great
|
||||
label_color: Color::Muted.color(cx),
|
||||
// TODO: These are not great
|
||||
icon_color: Color::Muted.color(cx),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) fn focused(self, cx: &mut WindowContext) -> ButtonLikeStyles {
|
||||
match self {
|
||||
ButtonStyle::Filled => ButtonLikeStyles {
|
||||
background: cx.theme().colors().element_background,
|
||||
border_color: cx.theme().colors().border_focused,
|
||||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle::Tinted => ButtonLikeStyles {
|
||||
background: gpui::red(),
|
||||
border_color: gpui::red(),
|
||||
label_color: gpui::red(),
|
||||
icon_color: gpui::red(),
|
||||
},
|
||||
ButtonStyle::Subtle => ButtonLikeStyles {
|
||||
background: cx.theme().colors().ghost_element_background,
|
||||
border_color: cx.theme().colors().border_focused,
|
||||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle::Transparent => ButtonLikeStyles {
|
||||
background: transparent_black(),
|
||||
border_color: cx.theme().colors().border_focused,
|
||||
label_color: Color::Accent.color(cx),
|
||||
icon_color: Color::Accent.color(cx),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) fn disabled(self, cx: &mut WindowContext) -> ButtonLikeStyles {
|
||||
match self {
|
||||
ButtonStyle::Filled => ButtonLikeStyles {
|
||||
background: cx.theme().colors().element_disabled,
|
||||
border_color: cx.theme().colors().border_disabled,
|
||||
label_color: Color::Disabled.color(cx),
|
||||
icon_color: Color::Disabled.color(cx),
|
||||
},
|
||||
ButtonStyle::Tinted => ButtonLikeStyles {
|
||||
background: gpui::red(),
|
||||
border_color: gpui::red(),
|
||||
label_color: gpui::red(),
|
||||
icon_color: gpui::red(),
|
||||
},
|
||||
ButtonStyle::Subtle => ButtonLikeStyles {
|
||||
background: cx.theme().colors().ghost_element_disabled,
|
||||
border_color: cx.theme().colors().border_disabled,
|
||||
label_color: Color::Disabled.color(cx),
|
||||
icon_color: Color::Disabled.color(cx),
|
||||
},
|
||||
ButtonStyle::Transparent => ButtonLikeStyles {
|
||||
background: transparent_black(),
|
||||
border_color: transparent_black(),
|
||||
label_color: Color::Disabled.color(cx),
|
||||
icon_color: Color::Disabled.color(cx),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ButtonSize can also be used to help build non-button elements
|
||||
/// that are consistently sized with buttons.
|
||||
#[derive(Default, PartialEq, Clone, Copy)]
|
||||
pub enum ButtonSize {
|
||||
Large,
|
||||
#[default]
|
||||
Default,
|
||||
Compact,
|
||||
None,
|
||||
}
|
||||
|
||||
impl ButtonSize {
|
||||
fn height(self) -> Rems {
|
||||
match self {
|
||||
ButtonSize::Large => rems(32. / 16.),
|
||||
ButtonSize::Default => rems(22. / 16.),
|
||||
ButtonSize::Compact => rems(18. / 16.),
|
||||
ButtonSize::None => rems(16. / 16.),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A button-like element that can be used to create a custom button when
|
||||
/// prebuilt buttons are not sufficient. Use this sparingly, as it is
|
||||
/// unconstrained and may make the UI feel less consistent.
|
||||
///
|
||||
/// This is also used to build the prebuilt buttons.
|
||||
#[derive(IntoElement)]
|
||||
pub struct ButtonLike {
|
||||
base: Div,
|
||||
id: ElementId,
|
||||
pub(super) style: ButtonStyle,
|
||||
pub(super) disabled: bool,
|
||||
pub(super) selected: bool,
|
||||
pub(super) width: Option<DefiniteLength>,
|
||||
size: ButtonSize,
|
||||
rounding: Option<ButtonLikeRounding>,
|
||||
tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
|
||||
on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl ButtonLike {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
base: div(),
|
||||
id: id.into(),
|
||||
style: ButtonStyle::default(),
|
||||
disabled: false,
|
||||
selected: false,
|
||||
width: None,
|
||||
size: ButtonSize::Default,
|
||||
rounding: Some(ButtonLikeRounding::All),
|
||||
tooltip: None,
|
||||
children: SmallVec::new(),
|
||||
on_click: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn rounding(mut self, rounding: impl Into<Option<ButtonLikeRounding>>) -> Self {
|
||||
self.rounding = rounding.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Disableable for ButtonLike {
|
||||
fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Selectable for ButtonLike {
|
||||
fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Clickable for ButtonLike {
|
||||
fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
|
||||
self.on_click = Some(Box::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl FixedWidth for ButtonLike {
|
||||
fn width(mut self, width: DefiniteLength) -> Self {
|
||||
self.width = Some(width);
|
||||
self
|
||||
}
|
||||
|
||||
fn full_width(mut self) -> Self {
|
||||
self.width = Some(relative(1.));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ButtonCommon for ButtonLike {
|
||||
fn id(&self) -> &ElementId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn style(mut self, style: ButtonStyle) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
fn size(mut self, size: ButtonSize) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
|
||||
self.tooltip = Some(Box::new(tooltip));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl VisibleOnHover for ButtonLike {
|
||||
fn visible_on_hover(mut self, group_name: impl Into<SharedString>) -> Self {
|
||||
self.base = self.base.visible_on_hover(group_name);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for ButtonLike {
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
|
||||
&mut self.children
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ButtonLike {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
self.base
|
||||
.h_flex()
|
||||
.id(self.id.clone())
|
||||
.group("")
|
||||
.flex_none()
|
||||
.h(self.size.height())
|
||||
.when_some(self.width, |this, width| this.w(width).justify_center())
|
||||
.when_some(self.rounding, |this, rounding| match rounding {
|
||||
ButtonLikeRounding::All => this.rounded_md(),
|
||||
ButtonLikeRounding::Left => this.rounded_l_md(),
|
||||
ButtonLikeRounding::Right => this.rounded_r_md(),
|
||||
})
|
||||
.gap_1()
|
||||
.map(|this| match self.size {
|
||||
ButtonSize::Large => this.px_2(),
|
||||
ButtonSize::Default | ButtonSize::Compact => this.px_1(),
|
||||
ButtonSize::None => this,
|
||||
})
|
||||
.bg(self.style.enabled(cx).background)
|
||||
.when(self.disabled, |this| this.cursor_not_allowed())
|
||||
.when(!self.disabled, |this| {
|
||||
this.cursor_pointer()
|
||||
.hover(|hover| hover.bg(self.style.hovered(cx).background))
|
||||
.active(|active| active.bg(self.style.active(cx).background))
|
||||
})
|
||||
.when_some(
|
||||
self.on_click.filter(|_| !self.disabled),
|
||||
|this, on_click| {
|
||||
this.on_mouse_down(MouseButton::Left, |_, cx| cx.prevent_default())
|
||||
.on_click(move |event, cx| {
|
||||
cx.stop_propagation();
|
||||
(on_click)(event, cx)
|
||||
})
|
||||
},
|
||||
)
|
||||
.when_some(self.tooltip, |this, tooltip| {
|
||||
this.tooltip(move |cx| tooltip(cx))
|
||||
})
|
||||
.children(self.children)
|
||||
}
|
||||
}
|
122
crates/ui/src/components/button/icon_button.rs
Normal file
122
crates/ui/src/components/button/icon_button.rs
Normal file
|
@ -0,0 +1,122 @@
|
|||
use gpui::{AnyView, DefiniteLength};
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, IconSize};
|
||||
|
||||
use super::button_icon::ButtonIcon;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct IconButton {
|
||||
base: ButtonLike,
|
||||
icon: Icon,
|
||||
icon_size: IconSize,
|
||||
icon_color: Color,
|
||||
selected_icon: Option<Icon>,
|
||||
}
|
||||
|
||||
impl IconButton {
|
||||
pub fn new(id: impl Into<ElementId>, icon: Icon) -> Self {
|
||||
Self {
|
||||
base: ButtonLike::new(id),
|
||||
icon,
|
||||
icon_size: IconSize::default(),
|
||||
icon_color: Color::Default,
|
||||
selected_icon: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icon_size(mut self, icon_size: IconSize) -> Self {
|
||||
self.icon_size = icon_size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn icon_color(mut self, icon_color: Color) -> Self {
|
||||
self.icon_color = icon_color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn selected_icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
|
||||
self.selected_icon = icon.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Disableable for IconButton {
|
||||
fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.base = self.base.disabled(disabled);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Selectable for IconButton {
|
||||
fn selected(mut self, selected: bool) -> Self {
|
||||
self.base = self.base.selected(selected);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Clickable for IconButton {
|
||||
fn on_click(
|
||||
mut self,
|
||||
handler: impl Fn(&gpui::ClickEvent, &mut WindowContext) + 'static,
|
||||
) -> Self {
|
||||
self.base = self.base.on_click(handler);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl FixedWidth for IconButton {
|
||||
fn width(mut self, width: DefiniteLength) -> Self {
|
||||
self.base = self.base.width(width);
|
||||
self
|
||||
}
|
||||
|
||||
fn full_width(mut self) -> Self {
|
||||
self.base = self.base.full_width();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ButtonCommon for IconButton {
|
||||
fn id(&self) -> &ElementId {
|
||||
self.base.id()
|
||||
}
|
||||
|
||||
fn style(mut self, style: ButtonStyle) -> Self {
|
||||
self.base = self.base.style(style);
|
||||
self
|
||||
}
|
||||
|
||||
fn size(mut self, size: ButtonSize) -> Self {
|
||||
self.base = self.base.size(size);
|
||||
self
|
||||
}
|
||||
|
||||
fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
|
||||
self.base = self.base.tooltip(tooltip);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl VisibleOnHover for IconButton {
|
||||
fn visible_on_hover(mut self, group_name: impl Into<SharedString>) -> Self {
|
||||
self.base = self.base.visible_on_hover(group_name);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for IconButton {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
let is_disabled = self.base.disabled;
|
||||
let is_selected = self.base.selected;
|
||||
|
||||
self.base.child(
|
||||
ButtonIcon::new(self.icon)
|
||||
.disabled(is_disabled)
|
||||
.selected(is_selected)
|
||||
.selected_icon(self.selected_icon)
|
||||
.size(self.icon_size)
|
||||
.color(self.icon_color),
|
||||
)
|
||||
}
|
||||
}
|
126
crates/ui/src/components/button/toggle_button.rs
Normal file
126
crates/ui/src/components/button/toggle_button.rs
Normal file
|
@ -0,0 +1,126 @@
|
|||
use gpui::{AnyView, ClickEvent};
|
||||
|
||||
use crate::{prelude::*, ButtonLike, ButtonLikeRounding};
|
||||
|
||||
/// The position of a [`ToggleButton`] within a group of buttons.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum ToggleButtonPosition {
|
||||
/// The toggle button is first in the group.
|
||||
First,
|
||||
|
||||
/// The toggle button is in the middle of the group (i.e., it is not the first or last toggle button).
|
||||
Middle,
|
||||
|
||||
/// The toggle button is last in the group.
|
||||
Last,
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ToggleButton {
|
||||
base: ButtonLike,
|
||||
position_in_group: Option<ToggleButtonPosition>,
|
||||
label: SharedString,
|
||||
label_color: Option<Color>,
|
||||
}
|
||||
|
||||
impl ToggleButton {
|
||||
pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
base: ButtonLike::new(id),
|
||||
position_in_group: None,
|
||||
label: label.into(),
|
||||
label_color: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn color(mut self, label_color: impl Into<Option<Color>>) -> Self {
|
||||
self.label_color = label_color.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn position_in_group(mut self, position: ToggleButtonPosition) -> Self {
|
||||
self.position_in_group = Some(position);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn first(self) -> Self {
|
||||
self.position_in_group(ToggleButtonPosition::First)
|
||||
}
|
||||
|
||||
pub fn middle(self) -> Self {
|
||||
self.position_in_group(ToggleButtonPosition::Middle)
|
||||
}
|
||||
|
||||
pub fn last(self) -> Self {
|
||||
self.position_in_group(ToggleButtonPosition::Last)
|
||||
}
|
||||
}
|
||||
|
||||
impl Selectable for ToggleButton {
|
||||
fn selected(mut self, selected: bool) -> Self {
|
||||
self.base = self.base.selected(selected);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Disableable for ToggleButton {
|
||||
fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.base = self.base.disabled(disabled);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Clickable for ToggleButton {
|
||||
fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
|
||||
self.base = self.base.on_click(handler);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ButtonCommon for ToggleButton {
|
||||
fn id(&self) -> &ElementId {
|
||||
self.base.id()
|
||||
}
|
||||
|
||||
fn style(mut self, style: ButtonStyle) -> Self {
|
||||
self.base = self.base.style(style);
|
||||
self
|
||||
}
|
||||
|
||||
fn size(mut self, size: ButtonSize) -> Self {
|
||||
self.base = self.base.size(size);
|
||||
self
|
||||
}
|
||||
|
||||
fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
|
||||
self.base = self.base.tooltip(tooltip);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ToggleButton {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
let is_disabled = self.base.disabled;
|
||||
let is_selected = self.base.selected;
|
||||
|
||||
let label_color = if is_disabled {
|
||||
Color::Disabled
|
||||
} else if is_selected {
|
||||
Color::Selected
|
||||
} else {
|
||||
self.label_color.unwrap_or_default()
|
||||
};
|
||||
|
||||
self.base
|
||||
.when_some(self.position_in_group, |this, position| match position {
|
||||
ToggleButtonPosition::First => this.rounding(ButtonLikeRounding::Left),
|
||||
ToggleButtonPosition::Middle => this.rounding(None),
|
||||
ToggleButtonPosition::Last => this.rounding(ButtonLikeRounding::Right),
|
||||
})
|
||||
.child(
|
||||
Label::new(self.label)
|
||||
.color(label_color)
|
||||
.line_height_style(LineHeightStyle::UiLabel),
|
||||
)
|
||||
}
|
||||
}
|
282
crates/ui/src/components/checkbox.rs
Normal file
282
crates/ui/src/components/checkbox.rs
Normal file
|
@ -0,0 +1,282 @@
|
|||
use gpui::{div, prelude::*, Element, ElementId, IntoElement, Styled, WindowContext};
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{Color, Icon, IconElement, Selection};
|
||||
|
||||
pub type CheckHandler = Box<dyn Fn(&Selection, &mut WindowContext) + 'static>;
|
||||
|
||||
/// # Checkbox
|
||||
///
|
||||
/// Checkboxes are used for multiple choices, not for mutually exclusive choices.
|
||||
/// Each checkbox works independently from other checkboxes in the list,
|
||||
/// therefore checking an additional box does not affect any other selections.
|
||||
#[derive(IntoElement)]
|
||||
pub struct Checkbox {
|
||||
id: ElementId,
|
||||
checked: Selection,
|
||||
disabled: bool,
|
||||
on_click: Option<CheckHandler>,
|
||||
}
|
||||
|
||||
impl RenderOnce for Checkbox {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let group_id = format!("checkbox_group_{:?}", self.id);
|
||||
|
||||
let icon = match self.checked {
|
||||
// When selected, we show a checkmark.
|
||||
Selection::Selected => {
|
||||
Some(
|
||||
IconElement::new(Icon::Check)
|
||||
.size(crate::IconSize::Small)
|
||||
.color(
|
||||
// If the checkbox is disabled we change the color of the icon.
|
||||
if self.disabled {
|
||||
Color::Disabled
|
||||
} else {
|
||||
Color::Selected
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
// In an indeterminate state, we show a dash.
|
||||
Selection::Indeterminate => {
|
||||
Some(
|
||||
IconElement::new(Icon::Dash)
|
||||
.size(crate::IconSize::Small)
|
||||
.color(
|
||||
// If the checkbox is disabled we change the color of the icon.
|
||||
if self.disabled {
|
||||
Color::Disabled
|
||||
} else {
|
||||
Color::Selected
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
// When unselected, we show nothing.
|
||||
Selection::Unselected => None,
|
||||
};
|
||||
|
||||
// A checkbox could be in an indeterminate state,
|
||||
// for example the indeterminate state could represent:
|
||||
// - a group of options of which only some are selected
|
||||
// - an enabled option that is no longer available
|
||||
// - a previously agreed to license that has been updated
|
||||
//
|
||||
// For the sake of styles we treat the indeterminate state as selected,
|
||||
// but it's icon will be different.
|
||||
let selected =
|
||||
self.checked == Selection::Selected || self.checked == Selection::Indeterminate;
|
||||
|
||||
// We could use something like this to make the checkbox background when selected:
|
||||
//
|
||||
// ~~~rust
|
||||
// ...
|
||||
// .when(selected, |this| {
|
||||
// this.bg(cx.theme().colors().element_selected)
|
||||
// })
|
||||
// ~~~
|
||||
//
|
||||
// But we use a match instead here because the checkbox might be disabled,
|
||||
// and it could be disabled _while_ it is selected, as well as while it is not selected.
|
||||
let (bg_color, border_color) = match (self.disabled, selected) {
|
||||
(true, _) => (
|
||||
cx.theme().colors().ghost_element_disabled,
|
||||
cx.theme().colors().border_disabled,
|
||||
),
|
||||
(false, true) => (
|
||||
cx.theme().colors().element_selected,
|
||||
cx.theme().colors().border,
|
||||
),
|
||||
(false, false) => (
|
||||
cx.theme().colors().element_background,
|
||||
cx.theme().colors().border,
|
||||
),
|
||||
};
|
||||
|
||||
div()
|
||||
.id(self.id)
|
||||
// Rather than adding `px_1()` to add some space around the checkbox,
|
||||
// we use a larger parent element to create a slightly larger
|
||||
// click area for the checkbox.
|
||||
.size_5()
|
||||
// Because we've enlarged the click area, we need to create a
|
||||
// `group` to pass down interactivity events to the checkbox.
|
||||
.group(group_id.clone())
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
// This prevent the flex element from growing
|
||||
// or shrinking in response to any size changes
|
||||
.flex_none()
|
||||
// The combo of `justify_center()` and `items_center()`
|
||||
// is used frequently to center elements in a flex container.
|
||||
//
|
||||
// We use this to center the icon in the checkbox.
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.m_1()
|
||||
.size_4()
|
||||
.rounded_sm()
|
||||
.bg(bg_color)
|
||||
.border()
|
||||
.border_color(border_color)
|
||||
// We only want the interactivity states to fire when we
|
||||
// are in a checkbox that isn't disabled.
|
||||
.when(!self.disabled, |this| {
|
||||
// Here instead of `hover()` we use `group_hover()`
|
||||
// to pass it the group id.
|
||||
this.group_hover(group_id.clone(), |el| {
|
||||
el.bg(cx.theme().colors().element_hover)
|
||||
})
|
||||
})
|
||||
.children(icon),
|
||||
)
|
||||
.when_some(
|
||||
self.on_click.filter(|_| !self.disabled),
|
||||
|this, on_click| this.on_click(move |_, cx| on_click(&self.checked.inverse(), cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
impl Checkbox {
|
||||
pub fn new(id: impl Into<ElementId>, checked: Selection) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
checked,
|
||||
disabled: false,
|
||||
on_click: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
handler: impl 'static + Fn(&Selection, &mut WindowContext) + Send + Sync,
|
||||
) -> Self {
|
||||
self.on_click = Some(Box::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn render(self, cx: &mut WindowContext) -> impl Element {
|
||||
let group_id = format!("checkbox_group_{:?}", self.id);
|
||||
|
||||
let icon = match self.checked {
|
||||
// When selected, we show a checkmark.
|
||||
Selection::Selected => {
|
||||
Some(
|
||||
IconElement::new(Icon::Check)
|
||||
.size(crate::IconSize::Small)
|
||||
.color(
|
||||
// If the checkbox is disabled we change the color of the icon.
|
||||
if self.disabled {
|
||||
Color::Disabled
|
||||
} else {
|
||||
Color::Selected
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
// In an indeterminate state, we show a dash.
|
||||
Selection::Indeterminate => {
|
||||
Some(
|
||||
IconElement::new(Icon::Dash)
|
||||
.size(crate::IconSize::Small)
|
||||
.color(
|
||||
// If the checkbox is disabled we change the color of the icon.
|
||||
if self.disabled {
|
||||
Color::Disabled
|
||||
} else {
|
||||
Color::Selected
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
// When unselected, we show nothing.
|
||||
Selection::Unselected => None,
|
||||
};
|
||||
|
||||
// A checkbox could be in an indeterminate state,
|
||||
// for example the indeterminate state could represent:
|
||||
// - a group of options of which only some are selected
|
||||
// - an enabled option that is no longer available
|
||||
// - a previously agreed to license that has been updated
|
||||
//
|
||||
// For the sake of styles we treat the indeterminate state as selected,
|
||||
// but it's icon will be different.
|
||||
let selected =
|
||||
self.checked == Selection::Selected || self.checked == Selection::Indeterminate;
|
||||
|
||||
// We could use something like this to make the checkbox background when selected:
|
||||
//
|
||||
// ~~~rust
|
||||
// ...
|
||||
// .when(selected, |this| {
|
||||
// this.bg(cx.theme().colors().element_selected)
|
||||
// })
|
||||
// ~~~
|
||||
//
|
||||
// But we use a match instead here because the checkbox might be disabled,
|
||||
// and it could be disabled _while_ it is selected, as well as while it is not selected.
|
||||
let (bg_color, border_color) = match (self.disabled, selected) {
|
||||
(true, _) => (
|
||||
cx.theme().colors().ghost_element_disabled,
|
||||
cx.theme().colors().border_disabled,
|
||||
),
|
||||
(false, true) => (
|
||||
cx.theme().colors().element_selected,
|
||||
cx.theme().colors().border,
|
||||
),
|
||||
(false, false) => (
|
||||
cx.theme().colors().element_background,
|
||||
cx.theme().colors().border,
|
||||
),
|
||||
};
|
||||
|
||||
div()
|
||||
.id(self.id)
|
||||
// Rather than adding `px_1()` to add some space around the checkbox,
|
||||
// we use a larger parent element to create a slightly larger
|
||||
// click area for the checkbox.
|
||||
.size_5()
|
||||
// Because we've enlarged the click area, we need to create a
|
||||
// `group` to pass down interactivity events to the checkbox.
|
||||
.group(group_id.clone())
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
// This prevent the flex element from growing
|
||||
// or shrinking in response to any size changes
|
||||
.flex_none()
|
||||
// The combo of `justify_center()` and `items_center()`
|
||||
// is used frequently to center elements in a flex container.
|
||||
//
|
||||
// We use this to center the icon in the checkbox.
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.m_1()
|
||||
.size_4()
|
||||
.rounded_sm()
|
||||
.bg(bg_color)
|
||||
.border()
|
||||
.border_color(border_color)
|
||||
// We only want the interactivity states to fire when we
|
||||
// are in a checkbox that isn't disabled.
|
||||
.when(!self.disabled, |this| {
|
||||
// Here instead of `hover()` we use `group_hover()`
|
||||
// to pass it the group id.
|
||||
this.group_hover(group_id.clone(), |el| {
|
||||
el.bg(cx.theme().colors().element_hover)
|
||||
})
|
||||
})
|
||||
.children(icon),
|
||||
)
|
||||
.when_some(
|
||||
self.on_click.filter(|_| !self.disabled),
|
||||
|this, on_click| this.on_click(move |_, cx| on_click(&self.checked.inverse(), cx)),
|
||||
)
|
||||
}
|
||||
}
|
336
crates/ui/src/components/context_menu.rs
Normal file
336
crates/ui/src/components/context_menu.rs
Normal file
|
@ -0,0 +1,336 @@
|
|||
use crate::{
|
||||
h_stack, prelude::*, v_stack, Icon, IconElement, KeyBinding, Label, List, ListItem,
|
||||
ListSeparator, ListSubHeader,
|
||||
};
|
||||
use gpui::{
|
||||
px, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
|
||||
IntoElement, Render, Subscription, View, VisualContext,
|
||||
};
|
||||
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
|
||||
use std::{rc::Rc, time::Duration};
|
||||
|
||||
enum ContextMenuItem {
|
||||
Separator,
|
||||
Header(SharedString),
|
||||
Entry {
|
||||
label: SharedString,
|
||||
icon: Option<Icon>,
|
||||
handler: Rc<dyn Fn(&mut WindowContext)>,
|
||||
action: Option<Box<dyn Action>>,
|
||||
},
|
||||
CustomEntry {
|
||||
entry_render: Box<dyn Fn(&mut WindowContext) -> AnyElement>,
|
||||
handler: Rc<dyn Fn(&mut WindowContext)>,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct ContextMenu {
|
||||
items: Vec<ContextMenuItem>,
|
||||
focus_handle: FocusHandle,
|
||||
selected_index: Option<usize>,
|
||||
delayed: bool,
|
||||
clicked: bool,
|
||||
_on_blur_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl FocusableView for ContextMenu {
|
||||
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for ContextMenu {}
|
||||
|
||||
impl ContextMenu {
|
||||
pub fn build(
|
||||
cx: &mut WindowContext,
|
||||
f: impl FnOnce(Self, &mut WindowContext) -> Self,
|
||||
) -> View<Self> {
|
||||
cx.new_view(|cx| {
|
||||
let focus_handle = cx.focus_handle();
|
||||
let _on_blur_subscription = cx.on_blur(&focus_handle, |this: &mut ContextMenu, cx| {
|
||||
this.cancel(&menu::Cancel, cx)
|
||||
});
|
||||
f(
|
||||
Self {
|
||||
items: Default::default(),
|
||||
focus_handle,
|
||||
selected_index: None,
|
||||
delayed: false,
|
||||
clicked: false,
|
||||
_on_blur_subscription,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn header(mut self, title: impl Into<SharedString>) -> Self {
|
||||
self.items.push(ContextMenuItem::Header(title.into()));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn separator(mut self) -> Self {
|
||||
self.items.push(ContextMenuItem::Separator);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn entry(
|
||||
mut self,
|
||||
label: impl Into<SharedString>,
|
||||
action: Option<Box<dyn Action>>,
|
||||
handler: impl Fn(&mut WindowContext) + 'static,
|
||||
) -> Self {
|
||||
self.items.push(ContextMenuItem::Entry {
|
||||
label: label.into(),
|
||||
handler: Rc::new(handler),
|
||||
icon: None,
|
||||
action,
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
pub fn custom_entry(
|
||||
mut self,
|
||||
entry_render: impl Fn(&mut WindowContext) -> AnyElement + 'static,
|
||||
handler: impl Fn(&mut WindowContext) + 'static,
|
||||
) -> Self {
|
||||
self.items.push(ContextMenuItem::CustomEntry {
|
||||
entry_render: Box::new(entry_render),
|
||||
handler: Rc::new(handler),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
pub fn action(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
|
||||
self.items.push(ContextMenuItem::Entry {
|
||||
label: label.into(),
|
||||
action: Some(action.boxed_clone()),
|
||||
handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())),
|
||||
icon: None,
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
pub fn link(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
|
||||
self.items.push(ContextMenuItem::Entry {
|
||||
label: label.into(),
|
||||
action: Some(action.boxed_clone()),
|
||||
handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())),
|
||||
icon: Some(Icon::Link),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
||||
match self.selected_index.and_then(|ix| self.items.get(ix)) {
|
||||
Some(
|
||||
ContextMenuItem::Entry { handler, .. }
|
||||
| ContextMenuItem::CustomEntry { handler, .. },
|
||||
) => (handler)(cx),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
|
||||
self.selected_index = self.items.iter().position(|item| item.is_selectable());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn select_last(&mut self) -> Option<usize> {
|
||||
for (ix, item) in self.items.iter().enumerate().rev() {
|
||||
if item.is_selectable() {
|
||||
self.selected_index = Some(ix);
|
||||
return Some(ix);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn handle_select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
|
||||
if self.select_last().is_some() {
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
|
||||
if let Some(ix) = self.selected_index {
|
||||
for (ix, item) in self.items.iter().enumerate().skip(ix + 1) {
|
||||
if item.is_selectable() {
|
||||
self.selected_index = Some(ix);
|
||||
cx.notify();
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.select_first(&Default::default(), cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
|
||||
if let Some(ix) = self.selected_index {
|
||||
for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
|
||||
if item.is_selectable() {
|
||||
self.selected_index = Some(ix);
|
||||
cx.notify();
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.handle_select_last(&Default::default(), cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_action_dispatch(&mut self, dispatched: &Box<dyn Action>, cx: &mut ViewContext<Self>) {
|
||||
if self.clicked {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(ix) = self.items.iter().position(|item| {
|
||||
if let ContextMenuItem::Entry {
|
||||
action: Some(action),
|
||||
..
|
||||
} = item
|
||||
{
|
||||
action.partial_eq(&**dispatched)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}) {
|
||||
self.selected_index = Some(ix);
|
||||
self.delayed = true;
|
||||
cx.notify();
|
||||
let action = dispatched.boxed_clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(50))
|
||||
.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
cx.dispatch_action(action);
|
||||
this.cancel(&menu::Cancel, cx)
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
} else {
|
||||
cx.propagate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextMenuItem {
|
||||
fn is_selectable(&self) -> bool {
|
||||
matches!(self, Self::Entry { .. } | Self::CustomEntry { .. })
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ContextMenu {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div().elevation_2(cx).flex().flex_row().child(
|
||||
v_stack()
|
||||
.min_w(px(200.))
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_mouse_down_out(cx.listener(|this, _, cx| this.cancel(&menu::Cancel, cx)))
|
||||
.key_context("menu")
|
||||
.on_action(cx.listener(ContextMenu::select_first))
|
||||
.on_action(cx.listener(ContextMenu::handle_select_last))
|
||||
.on_action(cx.listener(ContextMenu::select_next))
|
||||
.on_action(cx.listener(ContextMenu::select_prev))
|
||||
.on_action(cx.listener(ContextMenu::confirm))
|
||||
.on_action(cx.listener(ContextMenu::cancel))
|
||||
.when(!self.delayed, |mut el| {
|
||||
for item in self.items.iter() {
|
||||
if let ContextMenuItem::Entry {
|
||||
action: Some(action),
|
||||
..
|
||||
} = item
|
||||
{
|
||||
el = el.on_boxed_action(
|
||||
&**action,
|
||||
cx.listener(ContextMenu::on_action_dispatch),
|
||||
);
|
||||
}
|
||||
}
|
||||
el
|
||||
})
|
||||
.flex_none()
|
||||
.child(List::new().children(self.items.iter_mut().enumerate().map(
|
||||
|(ix, item)| match item {
|
||||
ContextMenuItem::Separator => ListSeparator.into_any_element(),
|
||||
ContextMenuItem::Header(header) => {
|
||||
ListSubHeader::new(header.clone()).into_any_element()
|
||||
}
|
||||
ContextMenuItem::Entry {
|
||||
label,
|
||||
handler,
|
||||
icon,
|
||||
action,
|
||||
} => {
|
||||
let handler = handler.clone();
|
||||
let menu = cx.view().downgrade();
|
||||
|
||||
let label_element = if let Some(icon) = icon {
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.child(Label::new(label.clone()))
|
||||
.child(IconElement::new(*icon))
|
||||
.into_any_element()
|
||||
} else {
|
||||
Label::new(label.clone()).into_any_element()
|
||||
};
|
||||
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.selected(Some(ix) == self.selected_index)
|
||||
.on_click(move |_, cx| {
|
||||
handler(cx);
|
||||
menu.update(cx, |menu, cx| {
|
||||
menu.clicked = true;
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.child(
|
||||
h_stack()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(label_element)
|
||||
.children(action.as_ref().and_then(|action| {
|
||||
KeyBinding::for_action(&**action, cx)
|
||||
.map(|binding| div().ml_1().child(binding))
|
||||
})),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
ContextMenuItem::CustomEntry {
|
||||
entry_render,
|
||||
handler,
|
||||
} => {
|
||||
let handler = handler.clone();
|
||||
let menu = cx.view().downgrade();
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.selected(Some(ix) == self.selected_index)
|
||||
.on_click(move |_, cx| {
|
||||
handler(cx);
|
||||
menu.update(cx, |menu, cx| {
|
||||
menu.clicked = true;
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.child(entry_render(cx))
|
||||
.into_any_element()
|
||||
}
|
||||
},
|
||||
))),
|
||||
)
|
||||
}
|
||||
}
|
45
crates/ui/src/components/disclosure.rs
Normal file
45
crates/ui/src/components/disclosure.rs
Normal file
|
@ -0,0 +1,45 @@
|
|||
use gpui::ClickEvent;
|
||||
|
||||
use crate::{prelude::*, Color, Icon, IconButton, IconSize};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Disclosure {
|
||||
id: ElementId,
|
||||
is_open: bool,
|
||||
on_toggle: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||
}
|
||||
|
||||
impl Disclosure {
|
||||
pub fn new(id: impl Into<ElementId>, is_open: bool) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
is_open,
|
||||
on_toggle: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_toggle(
|
||||
mut self,
|
||||
handler: impl Into<Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>>,
|
||||
) -> Self {
|
||||
self.on_toggle = handler.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Disclosure {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
IconButton::new(
|
||||
self.id,
|
||||
match self.is_open {
|
||||
true => Icon::ChevronDown,
|
||||
false => Icon::ChevronRight,
|
||||
},
|
||||
)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_size(IconSize::Small)
|
||||
.when_some(self.on_toggle, move |this, on_toggle| {
|
||||
this.on_click(move |event, cx| on_toggle(event, cx))
|
||||
})
|
||||
}
|
||||
}
|
74
crates/ui/src/components/divider.rs
Normal file
74
crates/ui/src/components/divider.rs
Normal file
|
@ -0,0 +1,74 @@
|
|||
use gpui::{Hsla, IntoElement};
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
enum DividerDirection {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub enum DividerColor {
|
||||
Border,
|
||||
#[default]
|
||||
BorderVariant,
|
||||
}
|
||||
|
||||
impl DividerColor {
|
||||
pub fn hsla(self, cx: &WindowContext) -> Hsla {
|
||||
match self {
|
||||
DividerColor::Border => cx.theme().colors().border,
|
||||
DividerColor::BorderVariant => cx.theme().colors().border_variant,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Divider {
|
||||
direction: DividerDirection,
|
||||
color: DividerColor,
|
||||
inset: bool,
|
||||
}
|
||||
|
||||
impl RenderOnce for Divider {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
div()
|
||||
.map(|this| match self.direction {
|
||||
DividerDirection::Horizontal => {
|
||||
this.h_px().w_full().when(self.inset, |this| this.mx_1p5())
|
||||
}
|
||||
DividerDirection::Vertical => {
|
||||
this.w_px().h_full().when(self.inset, |this| this.my_1p5())
|
||||
}
|
||||
})
|
||||
.bg(self.color.hsla(cx))
|
||||
}
|
||||
}
|
||||
|
||||
impl Divider {
|
||||
pub fn horizontal() -> Self {
|
||||
Self {
|
||||
direction: DividerDirection::Horizontal,
|
||||
color: DividerColor::default(),
|
||||
inset: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn vertical() -> Self {
|
||||
Self {
|
||||
direction: DividerDirection::Vertical,
|
||||
color: DividerColor::default(),
|
||||
inset: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inset(mut self) -> Self {
|
||||
self.inset = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: DividerColor) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
}
|
238
crates/ui/src/components/icon.rs
Normal file
238
crates/ui/src/components/icon.rs
Normal file
|
@ -0,0 +1,238 @@
|
|||
use gpui::{rems, svg, IntoElement, Rems};
|
||||
use strum::EnumIter;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Default, PartialEq, Copy, Clone)]
|
||||
pub enum IconSize {
|
||||
XSmall,
|
||||
Small,
|
||||
#[default]
|
||||
Medium,
|
||||
}
|
||||
|
||||
impl IconSize {
|
||||
pub fn rems(self) -> Rems {
|
||||
match self {
|
||||
IconSize::XSmall => rems(12. / 16.),
|
||||
IconSize::Small => rems(14. / 16.),
|
||||
IconSize::Medium => rems(16. / 16.),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Copy, Clone, EnumIter)]
|
||||
pub enum Icon {
|
||||
Ai,
|
||||
ArrowLeft,
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
ArrowRight,
|
||||
ArrowUpRight,
|
||||
AtSign,
|
||||
AudioOff,
|
||||
AudioOn,
|
||||
Backspace,
|
||||
Bell,
|
||||
BellOff,
|
||||
BellRing,
|
||||
Bolt,
|
||||
CaseSensitive,
|
||||
Check,
|
||||
Copy,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ChevronUp,
|
||||
Close,
|
||||
Collab,
|
||||
Copilot,
|
||||
CopilotInit,
|
||||
CopilotError,
|
||||
CopilotDisabled,
|
||||
Dash,
|
||||
Delete,
|
||||
Disconnected,
|
||||
Ellipsis,
|
||||
Envelope,
|
||||
ExternalLink,
|
||||
ExclamationTriangle,
|
||||
Exit,
|
||||
File,
|
||||
FileDoc,
|
||||
FileGeneric,
|
||||
FileGit,
|
||||
FileLock,
|
||||
FileRust,
|
||||
FileToml,
|
||||
FileTree,
|
||||
Filter,
|
||||
Folder,
|
||||
FolderOpen,
|
||||
FolderX,
|
||||
Github,
|
||||
Hash,
|
||||
InlayHint,
|
||||
Link,
|
||||
MagicWand,
|
||||
MagnifyingGlass,
|
||||
MailOpen,
|
||||
Maximize,
|
||||
Minimize,
|
||||
Menu,
|
||||
MessageBubbles,
|
||||
Mic,
|
||||
MicMute,
|
||||
Plus,
|
||||
Public,
|
||||
Quote,
|
||||
Replace,
|
||||
ReplaceAll,
|
||||
ReplaceNext,
|
||||
Screen,
|
||||
SelectAll,
|
||||
Split,
|
||||
Snip,
|
||||
Terminal,
|
||||
WholeWord,
|
||||
XCircle,
|
||||
Command,
|
||||
Control,
|
||||
Shift,
|
||||
Option,
|
||||
Return,
|
||||
Update,
|
||||
ZedXCopilot,
|
||||
}
|
||||
|
||||
impl Icon {
|
||||
pub fn path(self) -> &'static str {
|
||||
match self {
|
||||
Icon::Ai => "icons/ai.svg",
|
||||
Icon::ArrowLeft => "icons/arrow_left.svg",
|
||||
Icon::ArrowRight => "icons/arrow_right.svg",
|
||||
Icon::ArrowUp => "icons/arrow_up.svg",
|
||||
Icon::ArrowDown => "icons/arrow_down.svg",
|
||||
Icon::ArrowUpRight => "icons/arrow_up_right.svg",
|
||||
Icon::AtSign => "icons/at-sign.svg",
|
||||
Icon::AudioOff => "icons/speaker-off.svg",
|
||||
Icon::AudioOn => "icons/speaker-loud.svg",
|
||||
Icon::Backspace => "icons/backspace.svg",
|
||||
Icon::Bell => "icons/bell.svg",
|
||||
Icon::BellOff => "icons/bell-off.svg",
|
||||
Icon::BellRing => "icons/bell-ring.svg",
|
||||
Icon::Bolt => "icons/bolt.svg",
|
||||
Icon::CaseSensitive => "icons/case_insensitive.svg",
|
||||
Icon::Check => "icons/check.svg",
|
||||
Icon::Copy => "icons/copy.svg",
|
||||
Icon::ChevronDown => "icons/chevron_down.svg",
|
||||
Icon::ChevronLeft => "icons/chevron_left.svg",
|
||||
Icon::ChevronRight => "icons/chevron_right.svg",
|
||||
Icon::ChevronUp => "icons/chevron_up.svg",
|
||||
Icon::Close => "icons/x.svg",
|
||||
Icon::Collab => "icons/user_group_16.svg",
|
||||
Icon::Copilot => "icons/copilot.svg",
|
||||
Icon::CopilotInit => "icons/copilot_init.svg",
|
||||
Icon::CopilotError => "icons/copilot_error.svg",
|
||||
Icon::CopilotDisabled => "icons/copilot_disabled.svg",
|
||||
Icon::Dash => "icons/dash.svg",
|
||||
Icon::Delete => "icons/delete.svg",
|
||||
Icon::Disconnected => "icons/disconnected.svg",
|
||||
Icon::Ellipsis => "icons/ellipsis.svg",
|
||||
Icon::Envelope => "icons/feedback.svg",
|
||||
Icon::ExclamationTriangle => "icons/warning.svg",
|
||||
Icon::ExternalLink => "icons/external_link.svg",
|
||||
Icon::Exit => "icons/exit.svg",
|
||||
Icon::File => "icons/file.svg",
|
||||
Icon::FileDoc => "icons/file_icons/book.svg",
|
||||
Icon::FileGeneric => "icons/file_icons/file.svg",
|
||||
Icon::FileGit => "icons/file_icons/git.svg",
|
||||
Icon::FileLock => "icons/file_icons/lock.svg",
|
||||
Icon::FileRust => "icons/file_icons/rust.svg",
|
||||
Icon::FileToml => "icons/file_icons/toml.svg",
|
||||
Icon::FileTree => "icons/project.svg",
|
||||
Icon::Filter => "icons/filter.svg",
|
||||
Icon::Folder => "icons/file_icons/folder.svg",
|
||||
Icon::FolderOpen => "icons/file_icons/folder_open.svg",
|
||||
Icon::FolderX => "icons/stop_sharing.svg",
|
||||
Icon::Github => "icons/github.svg",
|
||||
Icon::Hash => "icons/hash.svg",
|
||||
Icon::InlayHint => "icons/inlay_hint.svg",
|
||||
Icon::Link => "icons/link.svg",
|
||||
Icon::MagicWand => "icons/magic-wand.svg",
|
||||
Icon::MagnifyingGlass => "icons/magnifying_glass.svg",
|
||||
Icon::MailOpen => "icons/mail-open.svg",
|
||||
Icon::Maximize => "icons/maximize.svg",
|
||||
Icon::Minimize => "icons/minimize.svg",
|
||||
Icon::Menu => "icons/menu.svg",
|
||||
Icon::MessageBubbles => "icons/conversations.svg",
|
||||
Icon::Mic => "icons/mic.svg",
|
||||
Icon::MicMute => "icons/mic-mute.svg",
|
||||
Icon::Plus => "icons/plus.svg",
|
||||
Icon::Public => "icons/public.svg",
|
||||
Icon::Quote => "icons/quote.svg",
|
||||
Icon::Replace => "icons/replace.svg",
|
||||
Icon::ReplaceAll => "icons/replace_all.svg",
|
||||
Icon::ReplaceNext => "icons/replace_next.svg",
|
||||
Icon::Screen => "icons/desktop.svg",
|
||||
Icon::SelectAll => "icons/select-all.svg",
|
||||
Icon::Split => "icons/split.svg",
|
||||
Icon::Snip => "icons/snip.svg",
|
||||
Icon::Terminal => "icons/terminal.svg",
|
||||
Icon::WholeWord => "icons/word_search.svg",
|
||||
Icon::XCircle => "icons/error.svg",
|
||||
Icon::Command => "icons/command.svg",
|
||||
Icon::Control => "icons/control.svg",
|
||||
Icon::Shift => "icons/shift.svg",
|
||||
Icon::Option => "icons/option.svg",
|
||||
Icon::Return => "icons/return.svg",
|
||||
Icon::Update => "icons/update.svg",
|
||||
Icon::ZedXCopilot => "icons/zed_x_copilot.svg",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct IconElement {
|
||||
path: SharedString,
|
||||
color: Color,
|
||||
size: IconSize,
|
||||
}
|
||||
|
||||
impl RenderOnce for IconElement {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
svg()
|
||||
.size(self.size.rems())
|
||||
.flex_none()
|
||||
.path(self.path)
|
||||
.text_color(self.color.color(cx))
|
||||
}
|
||||
}
|
||||
|
||||
impl IconElement {
|
||||
pub fn new(icon: Icon) -> Self {
|
||||
Self {
|
||||
path: icon.path().into(),
|
||||
color: Color::default(),
|
||||
size: IconSize::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_path(path: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
path: path.into(),
|
||||
color: Color::default(),
|
||||
size: IconSize::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: Color) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn size(mut self, size: IconSize) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
}
|
58
crates/ui/src/components/indicator.rs
Normal file
58
crates/ui/src/components/indicator.rs
Normal file
|
@ -0,0 +1,58 @@
|
|||
use gpui::Position;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub enum IndicatorStyle {
|
||||
#[default]
|
||||
Dot,
|
||||
Bar,
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Indicator {
|
||||
position: Position,
|
||||
style: IndicatorStyle,
|
||||
color: Color,
|
||||
}
|
||||
|
||||
impl Indicator {
|
||||
pub fn dot() -> Self {
|
||||
Self {
|
||||
position: Position::Relative,
|
||||
style: IndicatorStyle::Dot,
|
||||
color: Color::Default,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bar() -> Self {
|
||||
Self {
|
||||
position: Position::Relative,
|
||||
style: IndicatorStyle::Dot,
|
||||
color: Color::Default,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: Color) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn absolute(mut self) -> Self {
|
||||
self.position = Position::Absolute;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Indicator {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
div()
|
||||
.flex_none()
|
||||
.map(|this| match self.style {
|
||||
IndicatorStyle::Dot => this.w_1p5().h_1p5().rounded_full(),
|
||||
IndicatorStyle::Bar => this.w_full().h_1p5().rounded_t_md(),
|
||||
})
|
||||
.when(self.position == Position::Absolute, |this| this.absolute())
|
||||
.bg(self.color.color(cx))
|
||||
}
|
||||
}
|
131
crates/ui/src/components/keybinding.rs
Normal file
131
crates/ui/src/components/keybinding.rs
Normal file
|
@ -0,0 +1,131 @@
|
|||
use crate::{h_stack, prelude::*, Icon, IconElement, IconSize};
|
||||
use gpui::{relative, rems, Action, FocusHandle, IntoElement, Keystroke};
|
||||
|
||||
#[derive(IntoElement, Clone)]
|
||||
pub struct KeyBinding {
|
||||
/// A keybinding consists of a key and a set of modifier keys.
|
||||
/// More then one keybinding produces a chord.
|
||||
///
|
||||
/// This should always contain at least one element.
|
||||
key_binding: gpui::KeyBinding,
|
||||
}
|
||||
|
||||
impl RenderOnce for KeyBinding {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
h_stack()
|
||||
.flex_none()
|
||||
.gap_2()
|
||||
.children(self.key_binding.keystrokes().iter().map(|keystroke| {
|
||||
let key_icon = Self::icon_for_key(&keystroke);
|
||||
|
||||
h_stack()
|
||||
.flex_none()
|
||||
.gap_0p5()
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.p_0p5()
|
||||
.rounded_sm()
|
||||
.when(keystroke.modifiers.function, |el| el.child(Key::new("fn")))
|
||||
.when(keystroke.modifiers.control, |el| {
|
||||
el.child(KeyIcon::new(Icon::Control))
|
||||
})
|
||||
.when(keystroke.modifiers.alt, |el| {
|
||||
el.child(KeyIcon::new(Icon::Option))
|
||||
})
|
||||
.when(keystroke.modifiers.command, |el| {
|
||||
el.child(KeyIcon::new(Icon::Command))
|
||||
})
|
||||
.when(keystroke.modifiers.shift, |el| {
|
||||
el.child(KeyIcon::new(Icon::Shift))
|
||||
})
|
||||
.when_some(key_icon, |el, icon| el.child(KeyIcon::new(icon)))
|
||||
.when(key_icon.is_none(), |el| {
|
||||
el.child(Key::new(keystroke.key.to_uppercase().clone()))
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyBinding {
|
||||
pub fn for_action(action: &dyn Action, cx: &mut WindowContext) -> Option<Self> {
|
||||
let key_binding = cx.bindings_for_action(action).last().cloned()?;
|
||||
Some(Self::new(key_binding))
|
||||
}
|
||||
|
||||
// like for_action(), but lets you specify the context from which keybindings
|
||||
// are matched.
|
||||
pub fn for_action_in(
|
||||
action: &dyn Action,
|
||||
focus: &FocusHandle,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<Self> {
|
||||
let key_binding = cx.bindings_for_action_in(action, focus).last().cloned()?;
|
||||
Some(Self::new(key_binding))
|
||||
}
|
||||
|
||||
fn icon_for_key(keystroke: &Keystroke) -> Option<Icon> {
|
||||
match keystroke.key.as_str() {
|
||||
"left" => Some(Icon::ArrowLeft),
|
||||
"right" => Some(Icon::ArrowRight),
|
||||
"up" => Some(Icon::ArrowUp),
|
||||
"down" => Some(Icon::ArrowDown),
|
||||
"backspace" => Some(Icon::Backspace),
|
||||
"delete" => Some(Icon::Delete),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(key_binding: gpui::KeyBinding) -> Self {
|
||||
Self { key_binding }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Key {
|
||||
key: SharedString,
|
||||
}
|
||||
|
||||
impl RenderOnce for Key {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let single_char = self.key.len() == 1;
|
||||
|
||||
div()
|
||||
.py_0()
|
||||
.map(|this| {
|
||||
if single_char {
|
||||
this.w(rems(14. / 16.)).flex().flex_none().justify_center()
|
||||
} else {
|
||||
this.px_0p5()
|
||||
}
|
||||
})
|
||||
.h(rems(14. / 16.))
|
||||
.text_ui()
|
||||
.line_height(relative(1.))
|
||||
.text_color(cx.theme().colors().text)
|
||||
.child(self.key.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Key {
|
||||
pub fn new(key: impl Into<SharedString>) -> Self {
|
||||
Self { key: key.into() }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct KeyIcon {
|
||||
icon: Icon,
|
||||
}
|
||||
|
||||
impl RenderOnce for KeyIcon {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
div()
|
||||
.w(rems(14. / 16.))
|
||||
.child(IconElement::new(self.icon).size(IconSize::Small))
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyIcon {
|
||||
pub fn new(icon: Icon) -> Self {
|
||||
Self { icon }
|
||||
}
|
||||
}
|
7
crates/ui/src/components/label.rs
Normal file
7
crates/ui/src/components/label.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
mod highlighted_label;
|
||||
mod label;
|
||||
mod label_like;
|
||||
|
||||
pub use highlighted_label::*;
|
||||
pub use label::*;
|
||||
pub use label_like::*;
|
84
crates/ui/src/components/label/highlighted_label.rs
Normal file
84
crates/ui/src/components/label/highlighted_label.rs
Normal file
|
@ -0,0 +1,84 @@
|
|||
use std::ops::Range;
|
||||
|
||||
use gpui::{HighlightStyle, StyledText};
|
||||
|
||||
use crate::{prelude::*, LabelCommon, LabelLike, LabelSize, LineHeightStyle};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct HighlightedLabel {
|
||||
base: LabelLike,
|
||||
label: SharedString,
|
||||
highlight_indices: Vec<usize>,
|
||||
}
|
||||
|
||||
impl HighlightedLabel {
|
||||
/// Constructs a label with the given characters highlighted.
|
||||
/// Characters are identified by UTF-8 byte position.
|
||||
pub fn new(label: impl Into<SharedString>, highlight_indices: Vec<usize>) -> Self {
|
||||
Self {
|
||||
base: LabelLike::new(),
|
||||
label: label.into(),
|
||||
highlight_indices,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LabelCommon for HighlightedLabel {
|
||||
fn size(mut self, size: LabelSize) -> Self {
|
||||
self.base = self.base.size(size);
|
||||
self
|
||||
}
|
||||
|
||||
fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self {
|
||||
self.base = self.base.line_height_style(line_height_style);
|
||||
self
|
||||
}
|
||||
|
||||
fn color(mut self, color: Color) -> Self {
|
||||
self.base = self.base.color(color);
|
||||
self
|
||||
}
|
||||
|
||||
fn strikethrough(mut self, strikethrough: bool) -> Self {
|
||||
self.base = self.base.strikethrough(strikethrough);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for HighlightedLabel {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let highlight_color = cx.theme().colors().text_accent;
|
||||
|
||||
let mut highlight_indices = self.highlight_indices.iter().copied().peekable();
|
||||
let mut highlights: Vec<(Range<usize>, HighlightStyle)> = Vec::new();
|
||||
|
||||
while let Some(start_ix) = highlight_indices.next() {
|
||||
let mut end_ix = start_ix;
|
||||
|
||||
loop {
|
||||
end_ix = end_ix + self.label[end_ix..].chars().next().unwrap().len_utf8();
|
||||
if let Some(&next_ix) = highlight_indices.peek() {
|
||||
if next_ix == end_ix {
|
||||
end_ix = next_ix;
|
||||
highlight_indices.next();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
highlights.push((
|
||||
start_ix..end_ix,
|
||||
HighlightStyle {
|
||||
color: Some(highlight_color),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
let mut text_style = cx.text_style().clone();
|
||||
text_style.color = self.base.color.color(cx);
|
||||
|
||||
LabelLike::new().child(StyledText::new(self.label).with_highlights(&text_style, highlights))
|
||||
}
|
||||
}
|
46
crates/ui/src/components/label/label.rs
Normal file
46
crates/ui/src/components/label/label.rs
Normal file
|
@ -0,0 +1,46 @@
|
|||
use gpui::WindowContext;
|
||||
|
||||
use crate::{prelude::*, LabelCommon, LabelLike, LabelSize, LineHeightStyle};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Label {
|
||||
base: LabelLike,
|
||||
label: SharedString,
|
||||
}
|
||||
|
||||
impl Label {
|
||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
base: LabelLike::new(),
|
||||
label: label.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LabelCommon for Label {
|
||||
fn size(mut self, size: LabelSize) -> Self {
|
||||
self.base = self.base.size(size);
|
||||
self
|
||||
}
|
||||
|
||||
fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self {
|
||||
self.base = self.base.line_height_style(line_height_style);
|
||||
self
|
||||
}
|
||||
|
||||
fn color(mut self, color: Color) -> Self {
|
||||
self.base = self.base.color(color);
|
||||
self
|
||||
}
|
||||
|
||||
fn strikethrough(mut self, strikethrough: bool) -> Self {
|
||||
self.base = self.base.strikethrough(strikethrough);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Label {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
self.base.child(self.label)
|
||||
}
|
||||
}
|
102
crates/ui/src/components/label/label_like.rs
Normal file
102
crates/ui/src/components/label/label_like.rs
Normal file
|
@ -0,0 +1,102 @@
|
|||
use gpui::{relative, AnyElement, Styled};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
|
||||
pub enum LabelSize {
|
||||
#[default]
|
||||
Default,
|
||||
Small,
|
||||
XSmall,
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Copy, Clone)]
|
||||
pub enum LineHeightStyle {
|
||||
#[default]
|
||||
TextLabel,
|
||||
/// Sets the line height to 1.
|
||||
UiLabel,
|
||||
}
|
||||
|
||||
pub trait LabelCommon {
|
||||
fn size(self, size: LabelSize) -> Self;
|
||||
fn line_height_style(self, line_height_style: LineHeightStyle) -> Self;
|
||||
fn color(self, color: Color) -> Self;
|
||||
fn strikethrough(self, strikethrough: bool) -> Self;
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct LabelLike {
|
||||
size: LabelSize,
|
||||
line_height_style: LineHeightStyle,
|
||||
pub(crate) color: Color,
|
||||
strikethrough: bool,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl LabelLike {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
size: LabelSize::Default,
|
||||
line_height_style: LineHeightStyle::default(),
|
||||
color: Color::Default,
|
||||
strikethrough: false,
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LabelCommon for LabelLike {
|
||||
fn size(mut self, size: LabelSize) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self {
|
||||
self.line_height_style = line_height_style;
|
||||
self
|
||||
}
|
||||
|
||||
fn color(mut self, color: Color) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
|
||||
fn strikethrough(mut self, strikethrough: bool) -> Self {
|
||||
self.strikethrough = strikethrough;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for LabelLike {
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
|
||||
&mut self.children
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for LabelLike {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
div()
|
||||
.when(self.strikethrough, |this| {
|
||||
this.relative().child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_1_2()
|
||||
.w_full()
|
||||
.h_px()
|
||||
.bg(Color::Hidden.color(cx)),
|
||||
)
|
||||
})
|
||||
.map(|this| match self.size {
|
||||
LabelSize::Default => this.text_ui(),
|
||||
LabelSize::Small => this.text_ui_sm(),
|
||||
LabelSize::XSmall => this.text_ui_xs(),
|
||||
})
|
||||
.when(self.line_height_style == LineHeightStyle::UiLabel, |this| {
|
||||
this.line_height(relative(1.))
|
||||
})
|
||||
.text_color(self.color.color(cx))
|
||||
.children(self.children)
|
||||
}
|
||||
}
|
11
crates/ui/src/components/list.rs
Normal file
11
crates/ui/src/components/list.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
mod list;
|
||||
mod list_header;
|
||||
mod list_item;
|
||||
mod list_separator;
|
||||
mod list_sub_header;
|
||||
|
||||
pub use list::*;
|
||||
pub use list_header::*;
|
||||
pub use list_item::*;
|
||||
pub use list_separator::*;
|
||||
pub use list_sub_header::*;
|
58
crates/ui/src/components/list/list.rs
Normal file
58
crates/ui/src/components/list/list.rs
Normal file
|
@ -0,0 +1,58 @@
|
|||
use gpui::AnyElement;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{prelude::*, v_stack, Label, ListHeader};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct List {
|
||||
/// Message to display when the list is empty
|
||||
/// Defaults to "No items"
|
||||
empty_message: SharedString,
|
||||
header: Option<ListHeader>,
|
||||
toggle: Option<bool>,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl List {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
empty_message: "No items".into(),
|
||||
header: None,
|
||||
toggle: None,
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty_message(mut self, empty_message: impl Into<SharedString>) -> Self {
|
||||
self.empty_message = empty_message.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn header(mut self, header: impl Into<Option<ListHeader>>) -> Self {
|
||||
self.header = header.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn toggle(mut self, toggle: impl Into<Option<bool>>) -> Self {
|
||||
self.toggle = toggle.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for List {
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
|
||||
&mut self.children
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for List {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
v_stack().w_full().py_1().children(self.header).map(|this| {
|
||||
match (self.children.is_empty(), self.toggle) {
|
||||
(false, _) => this.children(self.children),
|
||||
(true, Some(false)) => this,
|
||||
(true, _) => this.child(Label::new(self.empty_message.clone()).color(Color::Muted)),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
124
crates/ui/src/components/list/list_header.rs
Normal file
124
crates/ui/src/components/list/list_header.rs
Normal file
|
@ -0,0 +1,124 @@
|
|||
use crate::{h_stack, prelude::*, Disclosure, Label};
|
||||
use gpui::{AnyElement, ClickEvent};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ListHeader {
|
||||
/// The label of the header.
|
||||
label: SharedString,
|
||||
/// A slot for content that appears before the label, like an icon or avatar.
|
||||
start_slot: Option<AnyElement>,
|
||||
/// A slot for content that appears after the label, usually on the other side of the header.
|
||||
/// This might be a button, a disclosure arrow, a face pile, etc.
|
||||
end_slot: Option<AnyElement>,
|
||||
/// A slot for content that appears on hover after the label
|
||||
/// It will obscure the `end_slot` when visible.
|
||||
end_hover_slot: Option<AnyElement>,
|
||||
toggle: Option<bool>,
|
||||
on_toggle: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||
inset: bool,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
impl ListHeader {
|
||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
start_slot: None,
|
||||
end_slot: None,
|
||||
end_hover_slot: None,
|
||||
inset: false,
|
||||
toggle: None,
|
||||
on_toggle: None,
|
||||
selected: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle(mut self, toggle: impl Into<Option<bool>>) -> Self {
|
||||
self.toggle = toggle.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_toggle(
|
||||
mut self,
|
||||
on_toggle: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
|
||||
) -> Self {
|
||||
self.on_toggle = Some(Box::new(on_toggle));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn start_slot<E: IntoElement>(mut self, start_slot: impl Into<Option<E>>) -> Self {
|
||||
self.start_slot = start_slot.into().map(IntoElement::into_any_element);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
|
||||
self.end_slot = end_slot.into().map(IntoElement::into_any_element);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn end_hover_slot<E: IntoElement>(mut self, end_hover_slot: impl Into<Option<E>>) -> Self {
|
||||
self.end_hover_slot = end_hover_slot.into().map(IntoElement::into_any_element);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn inset(mut self, inset: bool) -> Self {
|
||||
self.inset = inset;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Selectable for ListHeader {
|
||||
fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ListHeader {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
h_stack()
|
||||
.id(self.label.clone())
|
||||
.w_full()
|
||||
.relative()
|
||||
.group("list_header")
|
||||
.child(
|
||||
div()
|
||||
.h_7()
|
||||
.when(self.inset, |this| this.px_2())
|
||||
.when(self.selected, |this| {
|
||||
this.bg(cx.theme().colors().ghost_element_selected)
|
||||
})
|
||||
.flex()
|
||||
.flex_1()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.children(self.toggle.map(|is_open| {
|
||||
Disclosure::new("toggle", is_open).on_toggle(self.on_toggle)
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.children(self.start_slot)
|
||||
.child(Label::new(self.label.clone()).color(Color::Muted)),
|
||||
),
|
||||
)
|
||||
.child(h_stack().children(self.end_slot))
|
||||
.when_some(self.end_hover_slot, |this, end_hover_slot| {
|
||||
this.child(
|
||||
div()
|
||||
.absolute()
|
||||
.right_0()
|
||||
.visible_on_hover("list_header")
|
||||
.child(end_hover_slot),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
254
crates/ui/src/components/list/list_item.rs
Normal file
254
crates/ui/src/components/list/list_item.rs
Normal file
|
@ -0,0 +1,254 @@
|
|||
use gpui::{px, AnyElement, AnyView, ClickEvent, MouseButton, MouseDownEvent, Pixels};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{prelude::*, Disclosure};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
|
||||
pub enum ListItemSpacing {
|
||||
#[default]
|
||||
Dense,
|
||||
Sparse,
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ListItem {
|
||||
id: ElementId,
|
||||
disabled: bool,
|
||||
selected: bool,
|
||||
spacing: ListItemSpacing,
|
||||
indent_level: usize,
|
||||
indent_step_size: Pixels,
|
||||
/// A slot for content that appears before the children, like an icon or avatar.
|
||||
start_slot: Option<AnyElement>,
|
||||
/// A slot for content that appears after the children, usually on the other side of the header.
|
||||
/// This might be a button, a disclosure arrow, a face pile, etc.
|
||||
end_slot: Option<AnyElement>,
|
||||
/// A slot for content that appears on hover after the children
|
||||
/// It will obscure the `end_slot` when visible.
|
||||
end_hover_slot: Option<AnyElement>,
|
||||
toggle: Option<bool>,
|
||||
inset: bool,
|
||||
on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||
on_toggle: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||
tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>>,
|
||||
on_secondary_mouse_down: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl ListItem {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
disabled: false,
|
||||
selected: false,
|
||||
spacing: ListItemSpacing::Dense,
|
||||
indent_level: 0,
|
||||
indent_step_size: px(12.),
|
||||
start_slot: None,
|
||||
end_slot: None,
|
||||
end_hover_slot: None,
|
||||
toggle: None,
|
||||
inset: false,
|
||||
on_click: None,
|
||||
on_secondary_mouse_down: None,
|
||||
on_toggle: None,
|
||||
tooltip: None,
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spacing(mut self, spacing: ListItemSpacing) -> Self {
|
||||
self.spacing = spacing;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
|
||||
self.on_click = Some(Box::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_secondary_mouse_down(
|
||||
mut self,
|
||||
handler: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
|
||||
) -> Self {
|
||||
self.on_secondary_mouse_down = Some(Box::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
|
||||
self.tooltip = Some(Box::new(tooltip));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn inset(mut self, inset: bool) -> Self {
|
||||
self.inset = inset;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn indent_level(mut self, indent_level: usize) -> Self {
|
||||
self.indent_level = indent_level;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn indent_step_size(mut self, indent_step_size: Pixels) -> Self {
|
||||
self.indent_step_size = indent_step_size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn toggle(mut self, toggle: impl Into<Option<bool>>) -> Self {
|
||||
self.toggle = toggle.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_toggle(
|
||||
mut self,
|
||||
on_toggle: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
|
||||
) -> Self {
|
||||
self.on_toggle = Some(Box::new(on_toggle));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn start_slot<E: IntoElement>(mut self, start_slot: impl Into<Option<E>>) -> Self {
|
||||
self.start_slot = start_slot.into().map(IntoElement::into_any_element);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
|
||||
self.end_slot = end_slot.into().map(IntoElement::into_any_element);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn end_hover_slot<E: IntoElement>(mut self, end_hover_slot: impl Into<Option<E>>) -> Self {
|
||||
self.end_hover_slot = end_hover_slot.into().map(IntoElement::into_any_element);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Disableable for ListItem {
|
||||
fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Selectable for ListItem {
|
||||
fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for ListItem {
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
|
||||
&mut self.children
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ListItem {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
h_stack()
|
||||
.id(self.id)
|
||||
.w_full()
|
||||
.relative()
|
||||
// When an item is inset draw the indent spacing outside of the item
|
||||
.when(self.inset, |this| {
|
||||
this.ml(self.indent_level as f32 * self.indent_step_size)
|
||||
.px_2()
|
||||
})
|
||||
.when(!self.inset, |this| {
|
||||
this
|
||||
// TODO: Add focus state
|
||||
// .when(self.state == InteractionState::Focused, |this| {
|
||||
// this.border()
|
||||
// .border_color(cx.theme().colors().border_focused)
|
||||
// })
|
||||
.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
|
||||
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
|
||||
.when(self.selected, |this| {
|
||||
this.bg(cx.theme().colors().ghost_element_selected)
|
||||
})
|
||||
})
|
||||
.child(
|
||||
h_stack()
|
||||
.id("inner_list_item")
|
||||
.w_full()
|
||||
.relative()
|
||||
.gap_1()
|
||||
.px_2()
|
||||
.map(|this| match self.spacing {
|
||||
ListItemSpacing::Dense => this,
|
||||
ListItemSpacing::Sparse => this.py_1(),
|
||||
})
|
||||
.group("list_item")
|
||||
.when(self.inset && !self.disabled, |this| {
|
||||
this
|
||||
// TODO: Add focus state
|
||||
// .when(self.state == InteractionState::Focused, |this| {
|
||||
// this.border()
|
||||
// .border_color(cx.theme().colors().border_focused)
|
||||
// })
|
||||
.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
|
||||
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
|
||||
.when(self.selected, |this| {
|
||||
this.bg(cx.theme().colors().ghost_element_selected)
|
||||
})
|
||||
})
|
||||
.when_some(self.on_click, |this, on_click| {
|
||||
this.cursor_pointer().on_click(on_click)
|
||||
})
|
||||
.when_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
|
||||
this.on_mouse_down(MouseButton::Right, move |event, cx| {
|
||||
(on_mouse_down)(event, cx)
|
||||
})
|
||||
})
|
||||
.when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip))
|
||||
.map(|this| {
|
||||
if self.inset {
|
||||
this.rounded_md()
|
||||
} else {
|
||||
// When an item is not inset draw the indent spacing inside of the item
|
||||
this.ml(self.indent_level as f32 * self.indent_step_size)
|
||||
}
|
||||
})
|
||||
.children(self.toggle.map(|is_open| {
|
||||
div()
|
||||
.flex()
|
||||
.absolute()
|
||||
.left(rems(-1.))
|
||||
.when(is_open, |this| this.visible_on_hover(""))
|
||||
.child(Disclosure::new("toggle", is_open).on_toggle(self.on_toggle))
|
||||
}))
|
||||
.child(
|
||||
h_stack()
|
||||
// HACK: We need to set *any* width value here in order for this container to size correctly.
|
||||
// Without this the `h_stack` will overflow the parent `inner_list_item`.
|
||||
.w_px()
|
||||
.flex_1()
|
||||
.gap_1()
|
||||
.children(self.start_slot)
|
||||
.children(self.children),
|
||||
)
|
||||
.when_some(self.end_slot, |this, end_slot| {
|
||||
this.justify_between().child(
|
||||
h_stack()
|
||||
.when(self.end_hover_slot.is_some(), |this| {
|
||||
this.visible()
|
||||
.group_hover("list_item", |this| this.invisible())
|
||||
})
|
||||
.child(end_slot),
|
||||
)
|
||||
})
|
||||
.when_some(self.end_hover_slot, |this, end_hover_slot| {
|
||||
this.child(
|
||||
h_stack()
|
||||
.h_full()
|
||||
.absolute()
|
||||
.right_2()
|
||||
.top_0()
|
||||
.visible_on_hover("list_item")
|
||||
.child(end_hover_slot),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
14
crates/ui/src/components/list/list_separator.rs
Normal file
14
crates/ui/src/components/list/list_separator.rs
Normal file
|
@ -0,0 +1,14 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ListSeparator;
|
||||
|
||||
impl RenderOnce for ListSeparator {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
div()
|
||||
.h_px()
|
||||
.w_full()
|
||||
.my_1()
|
||||
.bg(cx.theme().colors().border_variant)
|
||||
}
|
||||
}
|
52
crates/ui/src/components/list/list_sub_header.rs
Normal file
52
crates/ui/src/components/list/list_sub_header.rs
Normal file
|
@ -0,0 +1,52 @@
|
|||
use crate::prelude::*;
|
||||
use crate::{h_stack, Icon, IconElement, IconSize, Label};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ListSubHeader {
|
||||
label: SharedString,
|
||||
start_slot: Option<Icon>,
|
||||
inset: bool,
|
||||
}
|
||||
|
||||
impl ListSubHeader {
|
||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
start_slot: None,
|
||||
inset: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
|
||||
self.start_slot = left_icon;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ListSubHeader {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
h_stack().flex_1().w_full().relative().py_1().child(
|
||||
div()
|
||||
.h_6()
|
||||
.when(self.inset, |this| this.px_2())
|
||||
.flex()
|
||||
.flex_1()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.children(self.start_slot.map(|i| {
|
||||
IconElement::new(i)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small)
|
||||
}))
|
||||
.child(Label::new(self.label.clone()).color(Color::Muted)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
82
crates/ui/src/components/popover.rs
Normal file
82
crates/ui/src/components/popover.rs
Normal file
|
@ -0,0 +1,82 @@
|
|||
use gpui::{
|
||||
div, AnyElement, Element, ElementId, IntoElement, ParentElement, RenderOnce, Styled,
|
||||
WindowContext,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::v_stack;
|
||||
|
||||
/// A popover is used to display a menu or show some options.
|
||||
///
|
||||
/// Clicking the element that launches the popover should not change the current view,
|
||||
/// and the popover should be statically positioned relative to that element (not the
|
||||
/// user's mouse.)
|
||||
///
|
||||
/// Example: A "new" menu with options like "new file", "new folder", etc,
|
||||
/// Linear's "Display" menu, a profile menu that appers when you click your avatar.
|
||||
///
|
||||
/// Related elements:
|
||||
///
|
||||
/// `ContextMenu`:
|
||||
///
|
||||
/// Used to display a popover menu that only contains a list of items. Context menus are always
|
||||
/// launched by secondary clicking on an element. The menu is positioned relative to the user's cursor.
|
||||
///
|
||||
/// Example: Right clicking a file in the file tree to get a list of actions, right clicking
|
||||
/// a tab to in the tab bar to get a list of actions.
|
||||
///
|
||||
/// `Dropdown`:
|
||||
///
|
||||
/// Used to display a list of options when the user clicks an element. The menu is
|
||||
/// positioned relative the element that was clicked, and clicking an item in the
|
||||
/// dropdown should change the value of the element that was clicked.
|
||||
///
|
||||
/// Example: A theme select control. Displays "One Dark", clicking it opens a list of themes.
|
||||
/// When one is selected, the theme select control displays the selected theme.
|
||||
#[derive(IntoElement)]
|
||||
pub struct Popover {
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
aside: Option<AnyElement>,
|
||||
}
|
||||
|
||||
impl RenderOnce for Popover {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
div()
|
||||
.flex()
|
||||
.gap_1()
|
||||
.child(v_stack().elevation_2(cx).px_1().children(self.children))
|
||||
.when_some(self.aside, |this, aside| {
|
||||
this.child(
|
||||
v_stack()
|
||||
.elevation_2(cx)
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
.px_1()
|
||||
.child(aside),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Popover {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
children: SmallVec::new(),
|
||||
aside: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn aside(mut self, aside: impl IntoElement) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.aside = Some(aside.into_element().into_any());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for Popover {
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
|
||||
&mut self.children
|
||||
}
|
||||
}
|
233
crates/ui/src/components/popover_menu.rs
Normal file
233
crates/ui/src/components/popover_menu.rs
Normal file
|
@ -0,0 +1,233 @@
|
|||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use gpui::{
|
||||
overlay, point, px, rems, AnchorCorner, AnyElement, Bounds, DismissEvent, DispatchPhase,
|
||||
Element, ElementId, InteractiveBounds, IntoElement, LayoutId, ManagedView, MouseDownEvent,
|
||||
ParentElement, Pixels, Point, View, VisualContext, WindowContext,
|
||||
};
|
||||
|
||||
use crate::{Clickable, Selectable};
|
||||
|
||||
pub trait PopoverTrigger: IntoElement + Clickable + Selectable + 'static {}
|
||||
|
||||
impl<T: IntoElement + Clickable + Selectable + 'static> PopoverTrigger for T {}
|
||||
|
||||
pub struct PopoverMenu<M: ManagedView> {
|
||||
id: ElementId,
|
||||
child_builder: Option<
|
||||
Box<
|
||||
dyn FnOnce(
|
||||
Rc<RefCell<Option<View<M>>>>,
|
||||
Option<Rc<dyn Fn(&mut WindowContext) -> Option<View<M>> + 'static>>,
|
||||
) -> AnyElement
|
||||
+ 'static,
|
||||
>,
|
||||
>,
|
||||
menu_builder: Option<Rc<dyn Fn(&mut WindowContext) -> Option<View<M>> + 'static>>,
|
||||
anchor: AnchorCorner,
|
||||
attach: Option<AnchorCorner>,
|
||||
offset: Option<Point<Pixels>>,
|
||||
}
|
||||
|
||||
impl<M: ManagedView> PopoverMenu<M> {
|
||||
pub fn menu(mut self, f: impl Fn(&mut WindowContext) -> Option<View<M>> + 'static) -> Self {
|
||||
self.menu_builder = Some(Rc::new(f));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn trigger<T: PopoverTrigger>(mut self, t: T) -> Self {
|
||||
self.child_builder = Some(Box::new(|menu, builder| {
|
||||
let open = menu.borrow().is_some();
|
||||
t.selected(open)
|
||||
.when_some(builder, |el, builder| {
|
||||
el.on_click({
|
||||
move |_, cx| {
|
||||
let Some(new_menu) = (builder)(cx) else {
|
||||
return;
|
||||
};
|
||||
let menu2 = menu.clone();
|
||||
let previous_focus_handle = cx.focused();
|
||||
|
||||
cx.subscribe(&new_menu, move |modal, _: &DismissEvent, cx| {
|
||||
if modal.focus_handle(cx).contains_focused(cx) {
|
||||
if previous_focus_handle.is_some() {
|
||||
cx.focus(&previous_focus_handle.as_ref().unwrap())
|
||||
}
|
||||
}
|
||||
*menu2.borrow_mut() = None;
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
cx.focus_view(&new_menu);
|
||||
*menu.borrow_mut() = Some(new_menu);
|
||||
}
|
||||
})
|
||||
})
|
||||
.into_any_element()
|
||||
}));
|
||||
self
|
||||
}
|
||||
|
||||
/// anchor defines which corner of the menu to anchor to the attachment point
|
||||
/// (by default the cursor position, but see attach)
|
||||
pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
|
||||
self.anchor = anchor;
|
||||
self
|
||||
}
|
||||
|
||||
/// attach defines which corner of the handle to attach the menu's anchor to
|
||||
pub fn attach(mut self, attach: AnchorCorner) -> Self {
|
||||
self.attach = Some(attach);
|
||||
self
|
||||
}
|
||||
|
||||
/// offset offsets the position of the content by that many pixels.
|
||||
pub fn offset(mut self, offset: Point<Pixels>) -> Self {
|
||||
self.offset = Some(offset);
|
||||
self
|
||||
}
|
||||
|
||||
fn resolved_attach(&self) -> AnchorCorner {
|
||||
self.attach.unwrap_or_else(|| match self.anchor {
|
||||
AnchorCorner::TopLeft => AnchorCorner::BottomLeft,
|
||||
AnchorCorner::TopRight => AnchorCorner::BottomRight,
|
||||
AnchorCorner::BottomLeft => AnchorCorner::TopLeft,
|
||||
AnchorCorner::BottomRight => AnchorCorner::TopRight,
|
||||
})
|
||||
}
|
||||
|
||||
fn resolved_offset(&self, cx: &WindowContext) -> Point<Pixels> {
|
||||
self.offset.unwrap_or_else(|| {
|
||||
// Default offset = 4px padding + 1px border
|
||||
let offset = rems(5. / 16.) * cx.rem_size();
|
||||
match self.anchor {
|
||||
AnchorCorner::TopRight | AnchorCorner::BottomRight => point(offset, px(0.)),
|
||||
AnchorCorner::TopLeft | AnchorCorner::BottomLeft => point(-offset, px(0.)),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn popover_menu<M: ManagedView>(id: impl Into<ElementId>) -> PopoverMenu<M> {
|
||||
PopoverMenu {
|
||||
id: id.into(),
|
||||
child_builder: None,
|
||||
menu_builder: None,
|
||||
anchor: AnchorCorner::TopLeft,
|
||||
attach: None,
|
||||
offset: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PopoverMenuState<M> {
|
||||
child_layout_id: Option<LayoutId>,
|
||||
child_element: Option<AnyElement>,
|
||||
child_bounds: Option<Bounds<Pixels>>,
|
||||
menu_element: Option<AnyElement>,
|
||||
menu: Rc<RefCell<Option<View<M>>>>,
|
||||
}
|
||||
|
||||
impl<M: ManagedView> Element for PopoverMenu<M> {
|
||||
type State = PopoverMenuState<M>;
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
element_state: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (gpui::LayoutId, Self::State) {
|
||||
let mut menu_layout_id = None;
|
||||
|
||||
let (menu, child_bounds) = if let Some(element_state) = element_state {
|
||||
(element_state.menu, element_state.child_bounds)
|
||||
} else {
|
||||
(Rc::default(), None)
|
||||
};
|
||||
|
||||
let menu_element = menu.borrow_mut().as_mut().map(|menu| {
|
||||
let mut overlay = overlay().snap_to_window().anchor(self.anchor);
|
||||
|
||||
if let Some(child_bounds) = child_bounds {
|
||||
overlay = overlay.position(
|
||||
self.resolved_attach().corner(child_bounds) + self.resolved_offset(cx),
|
||||
);
|
||||
}
|
||||
|
||||
let mut element = overlay.child(menu.clone()).into_any();
|
||||
menu_layout_id = Some(element.request_layout(cx));
|
||||
element
|
||||
});
|
||||
|
||||
let mut child_element = self
|
||||
.child_builder
|
||||
.take()
|
||||
.map(|child_builder| (child_builder)(menu.clone(), self.menu_builder.clone()));
|
||||
|
||||
let child_layout_id = child_element
|
||||
.as_mut()
|
||||
.map(|child_element| child_element.request_layout(cx));
|
||||
|
||||
let layout_id = cx.request_layout(
|
||||
&gpui::Style::default(),
|
||||
menu_layout_id.into_iter().chain(child_layout_id),
|
||||
);
|
||||
|
||||
(
|
||||
layout_id,
|
||||
PopoverMenuState {
|
||||
menu,
|
||||
child_element,
|
||||
child_layout_id,
|
||||
menu_element,
|
||||
child_bounds,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_: Bounds<gpui::Pixels>,
|
||||
element_state: &mut Self::State,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
if let Some(mut child) = element_state.child_element.take() {
|
||||
child.paint(cx);
|
||||
}
|
||||
|
||||
if let Some(child_layout_id) = element_state.child_layout_id.take() {
|
||||
element_state.child_bounds = Some(cx.layout_bounds(child_layout_id));
|
||||
}
|
||||
|
||||
if let Some(mut menu) = element_state.menu_element.take() {
|
||||
menu.paint(cx);
|
||||
|
||||
if let Some(child_bounds) = element_state.child_bounds {
|
||||
let interactive_bounds = InteractiveBounds {
|
||||
bounds: child_bounds,
|
||||
stacking_order: cx.stacking_order().clone(),
|
||||
};
|
||||
|
||||
// Mouse-downing outside the menu dismisses it, so we don't
|
||||
// want a click on the toggle to re-open it.
|
||||
cx.on_mouse_event(move |e: &MouseDownEvent, phase, cx| {
|
||||
if phase == DispatchPhase::Bubble
|
||||
&& interactive_bounds.visibly_contains(&e.position, cx)
|
||||
{
|
||||
cx.stop_propagation()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<M: ManagedView> IntoElement for PopoverMenu<M> {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<gpui::ElementId> {
|
||||
Some(self.id.clone())
|
||||
}
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
185
crates/ui/src/components/right_click_menu.rs
Normal file
185
crates/ui/src/components/right_click_menu.rs
Normal file
|
@ -0,0 +1,185 @@
|
|||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use gpui::{
|
||||
overlay, AnchorCorner, AnyElement, Bounds, DismissEvent, DispatchPhase, Element, ElementId,
|
||||
IntoElement, LayoutId, ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point,
|
||||
View, VisualContext, WindowContext,
|
||||
};
|
||||
|
||||
pub struct RightClickMenu<M: ManagedView> {
|
||||
id: ElementId,
|
||||
child_builder: Option<Box<dyn FnOnce(bool) -> AnyElement + 'static>>,
|
||||
menu_builder: Option<Rc<dyn Fn(&mut WindowContext) -> View<M> + 'static>>,
|
||||
anchor: Option<AnchorCorner>,
|
||||
attach: Option<AnchorCorner>,
|
||||
}
|
||||
|
||||
impl<M: ManagedView> RightClickMenu<M> {
|
||||
pub fn menu(mut self, f: impl Fn(&mut WindowContext) -> View<M> + 'static) -> Self {
|
||||
self.menu_builder = Some(Rc::new(f));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn trigger<E: IntoElement + 'static>(mut self, e: E) -> Self {
|
||||
self.child_builder = Some(Box::new(move |_| e.into_any_element()));
|
||||
self
|
||||
}
|
||||
|
||||
/// anchor defines which corner of the menu to anchor to the attachment point
|
||||
/// (by default the cursor position, but see attach)
|
||||
pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
|
||||
self.anchor = Some(anchor);
|
||||
self
|
||||
}
|
||||
|
||||
/// attach defines which corner of the handle to attach the menu's anchor to
|
||||
pub fn attach(mut self, attach: AnchorCorner) -> Self {
|
||||
self.attach = Some(attach);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn right_click_menu<M: ManagedView>(id: impl Into<ElementId>) -> RightClickMenu<M> {
|
||||
RightClickMenu {
|
||||
id: id.into(),
|
||||
child_builder: None,
|
||||
menu_builder: None,
|
||||
anchor: None,
|
||||
attach: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MenuHandleState<M> {
|
||||
menu: Rc<RefCell<Option<View<M>>>>,
|
||||
position: Rc<RefCell<Point<Pixels>>>,
|
||||
child_layout_id: Option<LayoutId>,
|
||||
child_element: Option<AnyElement>,
|
||||
menu_element: Option<AnyElement>,
|
||||
}
|
||||
|
||||
impl<M: ManagedView> Element for RightClickMenu<M> {
|
||||
type State = MenuHandleState<M>;
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
element_state: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (gpui::LayoutId, Self::State) {
|
||||
let (menu, position) = if let Some(element_state) = element_state {
|
||||
(element_state.menu, element_state.position)
|
||||
} else {
|
||||
(Rc::default(), Rc::default())
|
||||
};
|
||||
|
||||
let mut menu_layout_id = None;
|
||||
|
||||
let menu_element = menu.borrow_mut().as_mut().map(|menu| {
|
||||
let mut overlay = overlay().snap_to_window();
|
||||
if let Some(anchor) = self.anchor {
|
||||
overlay = overlay.anchor(anchor);
|
||||
}
|
||||
overlay = overlay.position(*position.borrow());
|
||||
|
||||
let mut element = overlay.child(menu.clone()).into_any();
|
||||
menu_layout_id = Some(element.request_layout(cx));
|
||||
element
|
||||
});
|
||||
|
||||
let mut child_element = self
|
||||
.child_builder
|
||||
.take()
|
||||
.map(|child_builder| (child_builder)(menu.borrow().is_some()));
|
||||
|
||||
let child_layout_id = child_element
|
||||
.as_mut()
|
||||
.map(|child_element| child_element.request_layout(cx));
|
||||
|
||||
let layout_id = cx.request_layout(
|
||||
&gpui::Style::default(),
|
||||
menu_layout_id.into_iter().chain(child_layout_id),
|
||||
);
|
||||
|
||||
(
|
||||
layout_id,
|
||||
MenuHandleState {
|
||||
menu,
|
||||
position,
|
||||
child_element,
|
||||
child_layout_id,
|
||||
menu_element,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<gpui::Pixels>,
|
||||
element_state: &mut Self::State,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
if let Some(mut child) = element_state.child_element.take() {
|
||||
child.paint(cx);
|
||||
}
|
||||
|
||||
if let Some(mut menu) = element_state.menu_element.take() {
|
||||
menu.paint(cx);
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(builder) = self.menu_builder.take() else {
|
||||
return;
|
||||
};
|
||||
let menu = element_state.menu.clone();
|
||||
let position = element_state.position.clone();
|
||||
let attach = self.attach.clone();
|
||||
let child_layout_id = element_state.child_layout_id.clone();
|
||||
|
||||
cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
|
||||
if phase == DispatchPhase::Bubble
|
||||
&& event.button == MouseButton::Right
|
||||
&& bounds.contains(&event.position)
|
||||
{
|
||||
cx.stop_propagation();
|
||||
cx.prevent_default();
|
||||
|
||||
let new_menu = (builder)(cx);
|
||||
let menu2 = menu.clone();
|
||||
let previous_focus_handle = cx.focused();
|
||||
|
||||
cx.subscribe(&new_menu, move |modal, _: &DismissEvent, cx| {
|
||||
if modal.focus_handle(cx).contains_focused(cx) {
|
||||
if previous_focus_handle.is_some() {
|
||||
cx.focus(&previous_focus_handle.as_ref().unwrap())
|
||||
}
|
||||
}
|
||||
*menu2.borrow_mut() = None;
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
cx.focus_view(&new_menu);
|
||||
*menu.borrow_mut() = Some(new_menu);
|
||||
|
||||
*position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() {
|
||||
attach
|
||||
.unwrap()
|
||||
.corner(cx.layout_bounds(child_layout_id.unwrap()))
|
||||
} else {
|
||||
cx.mouse_position()
|
||||
};
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<M: ManagedView> IntoElement for RightClickMenu<M> {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<gpui::ElementId> {
|
||||
Some(self.id.clone())
|
||||
}
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
19
crates/ui/src/components/stack.rs
Normal file
19
crates/ui/src/components/stack.rs
Normal file
|
@ -0,0 +1,19 @@
|
|||
use gpui::{div, Div};
|
||||
|
||||
use crate::StyledExt;
|
||||
|
||||
/// Horizontally stacks elements.
|
||||
///
|
||||
/// Sets `flex()`, `flex_row()`, `items_center()`
|
||||
#[track_caller]
|
||||
pub fn h_stack() -> Div {
|
||||
div().h_flex()
|
||||
}
|
||||
|
||||
/// Vertically stacks elements.
|
||||
///
|
||||
/// Sets `flex()`, `flex_col()`
|
||||
#[track_caller]
|
||||
pub fn v_stack() -> Div {
|
||||
div().v_flex()
|
||||
}
|
31
crates/ui/src/components/stories.rs
Normal file
31
crates/ui/src/components/stories.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
mod avatar;
|
||||
mod button;
|
||||
mod checkbox;
|
||||
mod context_menu;
|
||||
mod disclosure;
|
||||
mod icon;
|
||||
mod icon_button;
|
||||
mod keybinding;
|
||||
mod label;
|
||||
mod list;
|
||||
mod list_header;
|
||||
mod list_item;
|
||||
mod tab;
|
||||
mod tab_bar;
|
||||
mod toggle_button;
|
||||
|
||||
pub use avatar::*;
|
||||
pub use button::*;
|
||||
pub use checkbox::*;
|
||||
pub use context_menu::*;
|
||||
pub use disclosure::*;
|
||||
pub use icon::*;
|
||||
pub use icon_button::*;
|
||||
pub use keybinding::*;
|
||||
pub use label::*;
|
||||
pub use list::*;
|
||||
pub use list_header::*;
|
||||
pub use list_item::*;
|
||||
pub use tab::*;
|
||||
pub use tab_bar::*;
|
||||
pub use toggle_button::*;
|
29
crates/ui/src/components/stories/avatar.rs
Normal file
29
crates/ui/src/components/stories/avatar.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
use gpui::Render;
|
||||
use story::Story;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::Avatar;
|
||||
|
||||
pub struct AvatarStory;
|
||||
|
||||
impl Render for AvatarStory {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
Story::container()
|
||||
.child(Story::title_for::<Avatar>())
|
||||
.child(Story::label("Default"))
|
||||
.child(Avatar::new(
|
||||
"https://avatars.githubusercontent.com/u/1714999?v=4",
|
||||
))
|
||||
.child(Avatar::new(
|
||||
"https://avatars.githubusercontent.com/u/326587?v=4",
|
||||
))
|
||||
.child(
|
||||
Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
|
||||
.availability_indicator(true),
|
||||
)
|
||||
.child(
|
||||
Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
|
||||
.availability_indicator(false),
|
||||
)
|
||||
}
|
||||
}
|
38
crates/ui/src/components/stories/button.rs
Normal file
38
crates/ui/src/components/stories/button.rs
Normal file
|
@ -0,0 +1,38 @@
|
|||
use gpui::Render;
|
||||
use story::Story;
|
||||
|
||||
use crate::{prelude::*, Icon};
|
||||
use crate::{Button, ButtonStyle};
|
||||
|
||||
pub struct ButtonStory;
|
||||
|
||||
impl Render for ButtonStory {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
Story::container()
|
||||
.child(Story::title_for::<Button>())
|
||||
.child(Story::label("Default"))
|
||||
.child(Button::new("default_filled", "Click me"))
|
||||
.child(Story::label("Selected"))
|
||||
.child(Button::new("selected_filled", "Click me").selected(true))
|
||||
.child(Story::label("Selected with `selected_label`"))
|
||||
.child(
|
||||
Button::new("selected_label_filled", "Click me")
|
||||
.selected(true)
|
||||
.selected_label("I have been selected"),
|
||||
)
|
||||
.child(Story::label("With `label_color`"))
|
||||
.child(Button::new("filled_with_label_color", "Click me").color(Color::Created))
|
||||
.child(Story::label("With `icon`"))
|
||||
.child(Button::new("filled_with_icon", "Click me").icon(Icon::FileGit))
|
||||
.child(Story::label("Selected with `icon`"))
|
||||
.child(
|
||||
Button::new("filled_and_selected_with_icon", "Click me")
|
||||
.selected(true)
|
||||
.icon(Icon::FileGit),
|
||||
)
|
||||
.child(Story::label("Default (Subtle)"))
|
||||
.child(Button::new("default_subtle", "Click me").style(ButtonStyle::Subtle))
|
||||
.child(Story::label("Default (Transparent)"))
|
||||
.child(Button::new("default_transparent", "Click me").style(ButtonStyle::Transparent))
|
||||
}
|
||||
}
|
47
crates/ui/src/components/stories/checkbox.rs
Normal file
47
crates/ui/src/components/stories/checkbox.rs
Normal file
|
@ -0,0 +1,47 @@
|
|||
use gpui::{Render, ViewContext};
|
||||
use story::Story;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{h_stack, Checkbox};
|
||||
|
||||
pub struct CheckboxStory;
|
||||
|
||||
impl Render for CheckboxStory {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
Story::container()
|
||||
.child(Story::title_for::<Checkbox>())
|
||||
.child(Story::label("Default"))
|
||||
.child(
|
||||
h_stack()
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.rounded_md()
|
||||
.border()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(Checkbox::new("checkbox-enabled", Selection::Unselected))
|
||||
.child(Checkbox::new(
|
||||
"checkbox-intermediate",
|
||||
Selection::Indeterminate,
|
||||
))
|
||||
.child(Checkbox::new("checkbox-selected", Selection::Selected)),
|
||||
)
|
||||
.child(Story::label("Disabled"))
|
||||
.child(
|
||||
h_stack()
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.rounded_md()
|
||||
.border()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(Checkbox::new("checkbox-disabled", Selection::Unselected).disabled(true))
|
||||
.child(
|
||||
Checkbox::new("checkbox-disabled-intermediate", Selection::Indeterminate)
|
||||
.disabled(true),
|
||||
)
|
||||
.child(
|
||||
Checkbox::new("checkbox-disabled-selected", Selection::Selected)
|
||||
.disabled(true),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
75
crates/ui/src/components/stories/context_menu.rs
Normal file
75
crates/ui/src/components/stories/context_menu.rs
Normal file
|
@ -0,0 +1,75 @@
|
|||
use gpui::{actions, AnchorCorner, Render, View};
|
||||
use story::Story;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{right_click_menu, ContextMenu, Label};
|
||||
|
||||
actions!(context_menu, [PrintCurrentDate, PrintBestFood]);
|
||||
|
||||
fn build_menu(cx: &mut WindowContext, header: impl Into<SharedString>) -> View<ContextMenu> {
|
||||
ContextMenu::build(cx, |menu, _| {
|
||||
menu.header(header)
|
||||
.separator()
|
||||
.action("Print current time", Box::new(PrintCurrentDate))
|
||||
.entry("Print best food", Some(Box::new(PrintBestFood)), |cx| {
|
||||
cx.dispatch_action(Box::new(PrintBestFood))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub struct ContextMenuStory;
|
||||
|
||||
impl Render for ContextMenuStory {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
Story::container()
|
||||
.on_action(|_: &PrintCurrentDate, _| {
|
||||
println!("printing unix time!");
|
||||
if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() {
|
||||
println!("Current Unix time is {:?}", unix_time.as_secs());
|
||||
}
|
||||
})
|
||||
.on_action(|_: &PrintBestFood, _| {
|
||||
println!("burrito");
|
||||
})
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_between()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.justify_between()
|
||||
.child(
|
||||
right_click_menu("test2")
|
||||
.trigger(Label::new("TOP LEFT"))
|
||||
.menu(move |cx| build_menu(cx, "top left")),
|
||||
)
|
||||
.child(
|
||||
right_click_menu("test1")
|
||||
.trigger(Label::new("BOTTOM LEFT"))
|
||||
.anchor(AnchorCorner::BottomLeft)
|
||||
.attach(AnchorCorner::TopLeft)
|
||||
.menu(move |cx| build_menu(cx, "bottom left")),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.justify_between()
|
||||
.child(
|
||||
right_click_menu("test3")
|
||||
.trigger(Label::new("TOP RIGHT"))
|
||||
.anchor(AnchorCorner::TopRight)
|
||||
.menu(move |cx| build_menu(cx, "top right")),
|
||||
)
|
||||
.child(
|
||||
right_click_menu("test4")
|
||||
.trigger(Label::new("BOTTOM RIGHT"))
|
||||
.anchor(AnchorCorner::BottomRight)
|
||||
.attach(AnchorCorner::TopRight)
|
||||
.menu(move |cx| build_menu(cx, "bottom right")),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
18
crates/ui/src/components/stories/disclosure.rs
Normal file
18
crates/ui/src/components/stories/disclosure.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
use gpui::Render;
|
||||
use story::Story;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::Disclosure;
|
||||
|
||||
pub struct DisclosureStory;
|
||||
|
||||
impl Render for DisclosureStory {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
Story::container()
|
||||
.child(Story::title_for::<Disclosure>())
|
||||
.child(Story::label("Toggled"))
|
||||
.child(Disclosure::new("toggled", true))
|
||||
.child(Story::label("Not Toggled"))
|
||||
.child(Disclosure::new("not_toggled", false))
|
||||
}
|
||||
}
|
19
crates/ui/src/components/stories/icon.rs
Normal file
19
crates/ui/src/components/stories/icon.rs
Normal file
|
@ -0,0 +1,19 @@
|
|||
use gpui::Render;
|
||||
use story::Story;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{Icon, IconElement};
|
||||
|
||||
pub struct IconStory;
|
||||
|
||||
impl Render for IconStory {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let icons = Icon::iter();
|
||||
|
||||
Story::container()
|
||||
.child(Story::title_for::<IconElement>())
|
||||
.child(Story::label("All Icons"))
|
||||
.child(div().flex().gap_3().children(icons.map(IconElement::new)))
|
||||
}
|
||||
}
|
171
crates/ui/src/components/stories/icon_button.rs
Normal file
171
crates/ui/src/components/stories/icon_button.rs
Normal file
|
@ -0,0 +1,171 @@
|
|||
use gpui::Render;
|
||||
use story::{StoryContainer, StoryItem, StorySection};
|
||||
|
||||
use crate::{prelude::*, Tooltip};
|
||||
use crate::{Icon, IconButton};
|
||||
|
||||
pub struct IconButtonStory;
|
||||
|
||||
impl Render for IconButtonStory {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let default_button = StoryItem::new(
|
||||
"Default",
|
||||
IconButton::new("default_icon_button", Icon::Hash),
|
||||
)
|
||||
.description("Displays an icon button.")
|
||||
.usage(
|
||||
r#"
|
||||
IconButton::new("default_icon_button", Icon::Hash)
|
||||
"#,
|
||||
);
|
||||
|
||||
let selected_button = StoryItem::new(
|
||||
"Selected",
|
||||
IconButton::new("selected_icon_button", Icon::Hash).selected(true),
|
||||
)
|
||||
.description("Displays an icon button that is selected.")
|
||||
.usage(
|
||||
r#"
|
||||
IconButton::new("selected_icon_button", Icon::Hash).selected(true)
|
||||
"#,
|
||||
);
|
||||
|
||||
let selected_with_selected_icon = StoryItem::new(
|
||||
"Selected with `selected_icon`",
|
||||
IconButton::new("selected_with_selected_icon_button", Icon::AudioOn)
|
||||
.selected(true)
|
||||
.selected_icon(Icon::AudioOff),
|
||||
)
|
||||
.description(
|
||||
"Displays an icon button that is selected and shows a different icon when selected.",
|
||||
)
|
||||
.usage(
|
||||
r#"
|
||||
IconButton::new("selected_with_selected_icon_button", Icon::AudioOn)
|
||||
.selected(true)
|
||||
.selected_icon(Icon::AudioOff)
|
||||
"#,
|
||||
);
|
||||
|
||||
let disabled_button = StoryItem::new(
|
||||
"Disabled",
|
||||
IconButton::new("disabled_icon_button", Icon::Hash).disabled(true),
|
||||
)
|
||||
.description("Displays an icon button that is disabled.")
|
||||
.usage(
|
||||
r#"
|
||||
IconButton::new("disabled_icon_button", Icon::Hash).disabled(true)
|
||||
"#,
|
||||
);
|
||||
|
||||
let with_on_click_button = StoryItem::new(
|
||||
"With `on_click`",
|
||||
IconButton::new("with_on_click_button", Icon::Ai).on_click(|_event, _cx| {
|
||||
println!("Clicked!");
|
||||
}),
|
||||
)
|
||||
.description("Displays an icon button which triggers an event on click.")
|
||||
.usage(
|
||||
r#"
|
||||
IconButton::new("with_on_click_button", Icon::Ai).on_click(|_event, _cx| {
|
||||
println!("Clicked!");
|
||||
})
|
||||
"#,
|
||||
);
|
||||
|
||||
let with_tooltip_button = StoryItem::new(
|
||||
"With `tooltip`",
|
||||
IconButton::new("with_tooltip_button", Icon::MessageBubbles)
|
||||
.tooltip(|cx| Tooltip::text("Open messages", cx)),
|
||||
)
|
||||
.description("Displays an icon button that has a tooltip when hovered.")
|
||||
.usage(
|
||||
r#"
|
||||
IconButton::new("with_tooltip_button", Icon::MessageBubbles)
|
||||
.tooltip(|cx| Tooltip::text("Open messages", cx))
|
||||
"#,
|
||||
);
|
||||
|
||||
let selected_with_tooltip_button = StoryItem::new(
|
||||
"Selected with `tooltip`",
|
||||
IconButton::new("selected_with_tooltip_button", Icon::InlayHint)
|
||||
.selected(true)
|
||||
.tooltip(|cx| Tooltip::text("Toggle inlay hints", cx)),
|
||||
)
|
||||
.description("Displays a selected icon button with tooltip.")
|
||||
.usage(
|
||||
r#"
|
||||
IconButton::new("selected_with_tooltip_button", Icon::InlayHint)
|
||||
.selected(true)
|
||||
.tooltip(|cx| Tooltip::text("Toggle inlay hints", cx))
|
||||
"#,
|
||||
);
|
||||
|
||||
let buttons = vec![
|
||||
default_button,
|
||||
selected_button,
|
||||
selected_with_selected_icon,
|
||||
disabled_button,
|
||||
with_on_click_button,
|
||||
with_tooltip_button,
|
||||
selected_with_tooltip_button,
|
||||
];
|
||||
|
||||
StoryContainer::new(
|
||||
"Icon Button",
|
||||
"crates/ui2/src/components/stories/icon_button.rs",
|
||||
)
|
||||
.children(vec![StorySection::new().children(buttons)])
|
||||
.into_element()
|
||||
|
||||
// Story::container()
|
||||
// .child(Story::title_for::<IconButton>())
|
||||
// .child(Story::label("Default"))
|
||||
// .child(div().w_8().child(IconButton::new("icon_a", Icon::Hash)))
|
||||
// .child(Story::label("Selected"))
|
||||
// .child(
|
||||
// div()
|
||||
// .w_8()
|
||||
// .child(IconButton::new("icon_a", Icon::Hash).selected(true)),
|
||||
// )
|
||||
// .child(Story::label("Selected with `selected_icon`"))
|
||||
// .child(
|
||||
// div().w_8().child(
|
||||
// IconButton::new("icon_a", Icon::AudioOn)
|
||||
// .selected(true)
|
||||
// .selected_icon(Icon::AudioOff),
|
||||
// ),
|
||||
// )
|
||||
// .child(Story::label("Disabled"))
|
||||
// .child(
|
||||
// div()
|
||||
// .w_8()
|
||||
// .child(IconButton::new("icon_a", Icon::Hash).disabled(true)),
|
||||
// )
|
||||
// .child(Story::label("With `on_click`"))
|
||||
// .child(
|
||||
// div()
|
||||
// .w_8()
|
||||
// .child(
|
||||
// IconButton::new("with_on_click", Icon::Ai).on_click(|_event, _cx| {
|
||||
// println!("Clicked!");
|
||||
// }),
|
||||
// ),
|
||||
// )
|
||||
// .child(Story::label("With `tooltip`"))
|
||||
// .child(
|
||||
// div().w_8().child(
|
||||
// IconButton::new("with_tooltip", Icon::MessageBubbles)
|
||||
// .tooltip(|cx| Tooltip::text("Open messages", cx)),
|
||||
// ),
|
||||
// )
|
||||
// .child(Story::label("Selected with `tooltip`"))
|
||||
// .child(
|
||||
// div().w_8().child(
|
||||
// IconButton::new("selected_with_tooltip", Icon::InlayHint)
|
||||
// .selected(true)
|
||||
// .tooltip(|cx| Tooltip::text("Toggle inlay hints", cx)),
|
||||
// ),
|
||||
// )
|
||||
}
|
||||
}
|
58
crates/ui/src/components/stories/keybinding.rs
Normal file
58
crates/ui/src/components/stories/keybinding.rs
Normal file
|
@ -0,0 +1,58 @@
|
|||
use gpui::NoAction;
|
||||
use gpui::Render;
|
||||
use itertools::Itertools;
|
||||
use story::Story;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::KeyBinding;
|
||||
|
||||
pub struct KeybindingStory;
|
||||
|
||||
pub fn binding(key: &str) -> gpui::KeyBinding {
|
||||
gpui::KeyBinding::new(key, NoAction {}, None)
|
||||
}
|
||||
|
||||
impl Render for KeybindingStory {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let all_modifier_permutations = ["ctrl", "alt", "cmd", "shift"].into_iter().permutations(2);
|
||||
|
||||
Story::container()
|
||||
.child(Story::title_for::<KeyBinding>())
|
||||
.child(Story::label("Single Key"))
|
||||
.child(KeyBinding::new(binding("Z")))
|
||||
.child(Story::label("Single Key with Modifier"))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_3()
|
||||
.child(KeyBinding::new(binding("ctrl-c")))
|
||||
.child(KeyBinding::new(binding("alt-c")))
|
||||
.child(KeyBinding::new(binding("cmd-c")))
|
||||
.child(KeyBinding::new(binding("shift-c"))),
|
||||
)
|
||||
.child(Story::label("Single Key with Modifier (Permuted)"))
|
||||
.child(
|
||||
div().flex().flex_col().children(
|
||||
all_modifier_permutations
|
||||
.chunks(4)
|
||||
.into_iter()
|
||||
.map(|chunk| {
|
||||
div()
|
||||
.flex()
|
||||
.gap_4()
|
||||
.py_3()
|
||||
.children(chunk.map(|permutation| {
|
||||
KeyBinding::new(binding(&*(permutation.join("-") + "-x")))
|
||||
}))
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(Story::label("Single Key with All Modifiers"))
|
||||
.child(KeyBinding::new(binding("ctrl-alt-cmd-shift-z")))
|
||||
.child(Story::label("Chord"))
|
||||
.child(KeyBinding::new(binding("a z")))
|
||||
.child(Story::label("Chord with Modifier"))
|
||||
.child(KeyBinding::new(binding("ctrl-a shift-z")))
|
||||
.child(KeyBinding::new(binding("fn-s")))
|
||||
}
|
||||
}
|
27
crates/ui/src/components/stories/label.rs
Normal file
27
crates/ui/src/components/stories/label.rs
Normal file
|
@ -0,0 +1,27 @@
|
|||
use crate::{prelude::*, HighlightedLabel, Label};
|
||||
use gpui::Render;
|
||||
use story::Story;
|
||||
|
||||
pub struct LabelStory;
|
||||
|
||||
impl Render for LabelStory {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
Story::container()
|
||||
.child(Story::title_for::<Label>())
|
||||
.child(Story::label("Default"))
|
||||
.child(Label::new("Hello, world!"))
|
||||
.child(Story::label("Highlighted"))
|
||||
.child(HighlightedLabel::new(
|
||||
"Hello, world!",
|
||||
vec![0, 1, 2, 7, 8, 12],
|
||||
))
|
||||
.child(HighlightedLabel::new(
|
||||
"Héllo, world!",
|
||||
vec![0, 1, 3, 8, 9, 13],
|
||||
))
|
||||
.child(Story::label("Highlighted with `color`"))
|
||||
.child(
|
||||
HighlightedLabel::new("Hello, world!", vec![0, 1, 2, 7, 8, 12]).color(Color::Error),
|
||||
)
|
||||
}
|
||||
}
|
36
crates/ui/src/components/stories/list.rs
Normal file
36
crates/ui/src/components/stories/list.rs
Normal file
|
@ -0,0 +1,36 @@
|
|||
use gpui::Render;
|
||||
use story::Story;
|
||||
|
||||
use crate::{prelude::*, ListHeader, ListSeparator, ListSubHeader};
|
||||
use crate::{List, ListItem};
|
||||
|
||||
pub struct ListStory;
|
||||
|
||||
impl Render for ListStory {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
Story::container()
|
||||
.child(Story::title_for::<List>())
|
||||
.child(Story::label("Default"))
|
||||
.child(
|
||||
List::new()
|
||||
.child(ListItem::new("apple").child("Apple"))
|
||||
.child(ListItem::new("banana").child("Banana"))
|
||||
.child(ListItem::new("cherry").child("Cherry")),
|
||||
)
|
||||
.child(Story::label("With sections"))
|
||||
.child(
|
||||
List::new()
|
||||
.header(ListHeader::new("Produce"))
|
||||
.child(ListSubHeader::new("Fruits"))
|
||||
.child(ListItem::new("apple").child("Apple"))
|
||||
.child(ListItem::new("banana").child("Banana"))
|
||||
.child(ListItem::new("cherry").child("Cherry"))
|
||||
.child(ListSeparator)
|
||||
.child(ListSubHeader::new("Root Vegetables"))
|
||||
.child(ListItem::new("carrot").child("Carrot"))
|
||||
.child(ListItem::new("potato").child("Potato"))
|
||||
.child(ListSubHeader::new("Leafy Vegetables"))
|
||||
.child(ListItem::new("kale").child("Kale")),
|
||||
)
|
||||
}
|
||||
}
|
31
crates/ui/src/components/stories/list_header.rs
Normal file
31
crates/ui/src/components/stories/list_header.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
use gpui::Render;
|
||||
use story::Story;
|
||||
|
||||
use crate::{prelude::*, IconButton};
|
||||
use crate::{Icon, ListHeader};
|
||||
|
||||
pub struct ListHeaderStory;
|
||||
|
||||
impl Render for ListHeaderStory {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
Story::container()
|
||||
.child(Story::title_for::<ListHeader>())
|
||||
.child(Story::label("Default"))
|
||||
.child(ListHeader::new("Section 1"))
|
||||
.child(Story::label("With left icon"))
|
||||
.child(ListHeader::new("Section 2").start_slot(IconElement::new(Icon::Bell)))
|
||||
.child(Story::label("With left icon and meta"))
|
||||
.child(
|
||||
ListHeader::new("Section 3")
|
||||
.start_slot(IconElement::new(Icon::BellOff))
|
||||
.end_slot(IconButton::new("action_1", Icon::Bolt)),
|
||||
)
|
||||
.child(Story::label("With multiple meta"))
|
||||
.child(
|
||||
ListHeader::new("Section 4")
|
||||
.end_slot(IconButton::new("action_1", Icon::Bolt))
|
||||
.end_slot(IconButton::new("action_2", Icon::ExclamationTriangle))
|
||||
.end_slot(IconButton::new("action_3", Icon::Plus)),
|
||||
)
|
||||
}
|
||||
}
|
102
crates/ui/src/components/stories/list_item.rs
Normal file
102
crates/ui/src/components/stories/list_item.rs
Normal file
|
@ -0,0 +1,102 @@
|
|||
use gpui::Render;
|
||||
use story::Story;
|
||||
|
||||
use crate::{prelude::*, Avatar};
|
||||
use crate::{Icon, ListItem};
|
||||
|
||||
pub struct ListItemStory;
|
||||
|
||||
impl Render for ListItemStory {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
Story::container()
|
||||
.bg(cx.theme().colors().background)
|
||||
.child(Story::title_for::<ListItem>())
|
||||
.child(Story::label("Default"))
|
||||
.child(ListItem::new("hello_world").child("Hello, world!"))
|
||||
.child(Story::label("Inset"))
|
||||
.child(
|
||||
ListItem::new("inset_list_item")
|
||||
.inset(true)
|
||||
.start_slot(
|
||||
IconElement::new(Icon::Bell)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child("Hello, world!")
|
||||
.end_slot(
|
||||
IconElement::new(Icon::Bell)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(Story::label("With start slot icon"))
|
||||
.child(
|
||||
ListItem::new("with start slot_icon")
|
||||
.child("Hello, world!")
|
||||
.start_slot(
|
||||
IconElement::new(Icon::Bell)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(Story::label("With start slot avatar"))
|
||||
.child(
|
||||
ListItem::new("with_start slot avatar")
|
||||
.child("Hello, world!")
|
||||
.start_slot(Avatar::new(SharedString::from(
|
||||
"https://avatars.githubusercontent.com/u/1714999?v=4",
|
||||
))),
|
||||
)
|
||||
.child(Story::label("With end slot"))
|
||||
.child(
|
||||
ListItem::new("with_left_avatar")
|
||||
.child("Hello, world!")
|
||||
.end_slot(Avatar::new(SharedString::from(
|
||||
"https://avatars.githubusercontent.com/u/1714999?v=4",
|
||||
))),
|
||||
)
|
||||
.child(Story::label("With end hover slot"))
|
||||
.child(
|
||||
ListItem::new("with_end_hover_slot")
|
||||
.child("Hello, world!")
|
||||
.end_slot(
|
||||
h_stack()
|
||||
.gap_2()
|
||||
.child(Avatar::new(SharedString::from(
|
||||
"https://avatars.githubusercontent.com/u/1789?v=4",
|
||||
)))
|
||||
.child(Avatar::new(SharedString::from(
|
||||
"https://avatars.githubusercontent.com/u/1789?v=4",
|
||||
)))
|
||||
.child(Avatar::new(SharedString::from(
|
||||
"https://avatars.githubusercontent.com/u/1789?v=4",
|
||||
)))
|
||||
.child(Avatar::new(SharedString::from(
|
||||
"https://avatars.githubusercontent.com/u/1789?v=4",
|
||||
)))
|
||||
.child(Avatar::new(SharedString::from(
|
||||
"https://avatars.githubusercontent.com/u/1789?v=4",
|
||||
))),
|
||||
)
|
||||
.end_hover_slot(Avatar::new(SharedString::from(
|
||||
"https://avatars.githubusercontent.com/u/1714999?v=4",
|
||||
))),
|
||||
)
|
||||
.child(Story::label("With `on_click`"))
|
||||
.child(
|
||||
ListItem::new("with_on_click")
|
||||
.child("Click me")
|
||||
.on_click(|_event, _cx| {
|
||||
println!("Clicked!");
|
||||
}),
|
||||
)
|
||||
.child(Story::label("With `on_secondary_mouse_down`"))
|
||||
.child(
|
||||
ListItem::new("with_on_secondary_mouse_down")
|
||||
.child("Right click me")
|
||||
.on_secondary_mouse_down(|_event, _cx| {
|
||||
println!("Right mouse down!");
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
112
crates/ui/src/components/stories/tab.rs
Normal file
112
crates/ui/src/components/stories/tab.rs
Normal file
|
@ -0,0 +1,112 @@
|
|||
use std::cmp::Ordering;
|
||||
|
||||
use gpui::Render;
|
||||
use story::Story;
|
||||
|
||||
use crate::{prelude::*, TabPosition};
|
||||
use crate::{Indicator, Tab};
|
||||
|
||||
pub struct TabStory;
|
||||
|
||||
impl Render for TabStory {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
Story::container()
|
||||
.child(Story::title_for::<Tab>())
|
||||
.child(Story::label("Default"))
|
||||
.child(h_stack().child(Tab::new("tab_1").child("Tab 1")))
|
||||
.child(Story::label("With indicator"))
|
||||
.child(
|
||||
h_stack().child(
|
||||
Tab::new("tab_1")
|
||||
.start_slot(Indicator::dot().color(Color::Warning))
|
||||
.child("Tab 1"),
|
||||
),
|
||||
)
|
||||
.child(Story::label("With close button"))
|
||||
.child(
|
||||
h_stack().child(
|
||||
Tab::new("tab_1")
|
||||
.end_slot(
|
||||
IconButton::new("close_button", Icon::Close)
|
||||
.icon_color(Color::Muted)
|
||||
.size(ButtonSize::None)
|
||||
.icon_size(IconSize::XSmall),
|
||||
)
|
||||
.child("Tab 1"),
|
||||
),
|
||||
)
|
||||
.child(Story::label("List of tabs"))
|
||||
.child(
|
||||
h_stack()
|
||||
.child(Tab::new("tab_1").child("Tab 1"))
|
||||
.child(Tab::new("tab_2").child("Tab 2")),
|
||||
)
|
||||
.child(Story::label("List of tabs with first tab selected"))
|
||||
.child(
|
||||
h_stack()
|
||||
.child(
|
||||
Tab::new("tab_1")
|
||||
.selected(true)
|
||||
.position(TabPosition::First)
|
||||
.child("Tab 1"),
|
||||
)
|
||||
.child(
|
||||
Tab::new("tab_2")
|
||||
.position(TabPosition::Middle(Ordering::Greater))
|
||||
.child("Tab 2"),
|
||||
)
|
||||
.child(
|
||||
Tab::new("tab_3")
|
||||
.position(TabPosition::Middle(Ordering::Greater))
|
||||
.child("Tab 3"),
|
||||
)
|
||||
.child(Tab::new("tab_4").position(TabPosition::Last).child("Tab 4")),
|
||||
)
|
||||
.child(Story::label("List of tabs with last tab selected"))
|
||||
.child(
|
||||
h_stack()
|
||||
.child(
|
||||
Tab::new("tab_1")
|
||||
.position(TabPosition::First)
|
||||
.child("Tab 1"),
|
||||
)
|
||||
.child(
|
||||
Tab::new("tab_2")
|
||||
.position(TabPosition::Middle(Ordering::Less))
|
||||
.child("Tab 2"),
|
||||
)
|
||||
.child(
|
||||
Tab::new("tab_3")
|
||||
.position(TabPosition::Middle(Ordering::Less))
|
||||
.child("Tab 3"),
|
||||
)
|
||||
.child(
|
||||
Tab::new("tab_4")
|
||||
.position(TabPosition::Last)
|
||||
.selected(true)
|
||||
.child("Tab 4"),
|
||||
),
|
||||
)
|
||||
.child(Story::label("List of tabs with second tab selected"))
|
||||
.child(
|
||||
h_stack()
|
||||
.child(
|
||||
Tab::new("tab_1")
|
||||
.position(TabPosition::First)
|
||||
.child("Tab 1"),
|
||||
)
|
||||
.child(
|
||||
Tab::new("tab_2")
|
||||
.position(TabPosition::Middle(Ordering::Equal))
|
||||
.selected(true)
|
||||
.child("Tab 2"),
|
||||
)
|
||||
.child(
|
||||
Tab::new("tab_3")
|
||||
.position(TabPosition::Middle(Ordering::Greater))
|
||||
.child("Tab 3"),
|
||||
)
|
||||
.child(Tab::new("tab_4").position(TabPosition::Last).child("Tab 4")),
|
||||
)
|
||||
}
|
||||
}
|
56
crates/ui/src/components/stories/tab_bar.rs
Normal file
56
crates/ui/src/components/stories/tab_bar.rs
Normal file
|
@ -0,0 +1,56 @@
|
|||
use gpui::Render;
|
||||
use story::Story;
|
||||
|
||||
use crate::{prelude::*, Tab, TabBar, TabPosition};
|
||||
|
||||
pub struct TabBarStory;
|
||||
|
||||
impl Render for TabBarStory {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let tab_count = 20;
|
||||
let selected_tab_index = 3;
|
||||
|
||||
let tabs = (0..tab_count)
|
||||
.map(|index| {
|
||||
Tab::new(index)
|
||||
.selected(index == selected_tab_index)
|
||||
.position(if index == 0 {
|
||||
TabPosition::First
|
||||
} else if index == tab_count - 1 {
|
||||
TabPosition::Last
|
||||
} else {
|
||||
TabPosition::Middle(index.cmp(&selected_tab_index))
|
||||
})
|
||||
.child(Label::new(format!("Tab {}", index + 1)).color(
|
||||
if index == selected_tab_index {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
},
|
||||
))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Story::container()
|
||||
.child(Story::title_for::<TabBar>())
|
||||
.child(Story::label("Default"))
|
||||
.child(
|
||||
h_stack().child(
|
||||
TabBar::new("tab_bar_1")
|
||||
.start_child(
|
||||
IconButton::new("navigate_backward", Icon::ArrowLeft)
|
||||
.icon_size(IconSize::Small),
|
||||
)
|
||||
.start_child(
|
||||
IconButton::new("navigate_forward", Icon::ArrowRight)
|
||||
.icon_size(IconSize::Small),
|
||||
)
|
||||
.end_child(IconButton::new("new", Icon::Plus).icon_size(IconSize::Small))
|
||||
.end_child(
|
||||
IconButton::new("split_pane", Icon::Split).icon_size(IconSize::Small),
|
||||
)
|
||||
.children(tabs),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
95
crates/ui/src/components/stories/toggle_button.rs
Normal file
95
crates/ui/src/components/stories/toggle_button.rs
Normal file
|
@ -0,0 +1,95 @@
|
|||
use gpui::Render;
|
||||
use story::{StoryContainer, StoryItem, StorySection};
|
||||
|
||||
use crate::{prelude::*, ToggleButton};
|
||||
|
||||
pub struct ToggleButtonStory;
|
||||
|
||||
impl Render for ToggleButtonStory {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
StoryContainer::new(
|
||||
"Toggle Button",
|
||||
"crates/ui2/src/components/stories/toggle_button.rs",
|
||||
)
|
||||
.child(
|
||||
StorySection::new().child(
|
||||
StoryItem::new(
|
||||
"Default",
|
||||
ToggleButton::new("default_toggle_button", "Hello"),
|
||||
)
|
||||
.description("Displays a toggle button.")
|
||||
.usage(""),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
StorySection::new().child(
|
||||
StoryItem::new(
|
||||
"Toggle button group",
|
||||
h_stack()
|
||||
.child(
|
||||
ToggleButton::new(1, "Apple")
|
||||
.style(ButtonStyle::Filled)
|
||||
.size(ButtonSize::Large)
|
||||
.first(),
|
||||
)
|
||||
.child(
|
||||
ToggleButton::new(2, "Banana")
|
||||
.style(ButtonStyle::Filled)
|
||||
.size(ButtonSize::Large)
|
||||
.middle(),
|
||||
)
|
||||
.child(
|
||||
ToggleButton::new(3, "Cherry")
|
||||
.style(ButtonStyle::Filled)
|
||||
.size(ButtonSize::Large)
|
||||
.middle(),
|
||||
)
|
||||
.child(
|
||||
ToggleButton::new(4, "Dragonfruit")
|
||||
.style(ButtonStyle::Filled)
|
||||
.size(ButtonSize::Large)
|
||||
.last(),
|
||||
),
|
||||
)
|
||||
.description("Displays a group of toggle buttons.")
|
||||
.usage(""),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
StorySection::new().child(
|
||||
StoryItem::new(
|
||||
"Toggle button group with selection",
|
||||
h_stack()
|
||||
.child(
|
||||
ToggleButton::new(1, "Apple")
|
||||
.style(ButtonStyle::Filled)
|
||||
.size(ButtonSize::Large)
|
||||
.first(),
|
||||
)
|
||||
.child(
|
||||
ToggleButton::new(2, "Banana")
|
||||
.style(ButtonStyle::Filled)
|
||||
.size(ButtonSize::Large)
|
||||
.selected(true)
|
||||
.middle(),
|
||||
)
|
||||
.child(
|
||||
ToggleButton::new(3, "Cherry")
|
||||
.style(ButtonStyle::Filled)
|
||||
.size(ButtonSize::Large)
|
||||
.middle(),
|
||||
)
|
||||
.child(
|
||||
ToggleButton::new(4, "Dragonfruit")
|
||||
.style(ButtonStyle::Filled)
|
||||
.size(ButtonSize::Large)
|
||||
.last(),
|
||||
),
|
||||
)
|
||||
.description("Displays a group of toggle buttons.")
|
||||
.usage(""),
|
||||
),
|
||||
)
|
||||
.into_element()
|
||||
}
|
||||
}
|
174
crates/ui/src/components/tab.rs
Normal file
174
crates/ui/src/components/tab.rs
Normal file
|
@ -0,0 +1,174 @@
|
|||
use crate::prelude::*;
|
||||
use gpui::{AnyElement, IntoElement, Stateful};
|
||||
use smallvec::SmallVec;
|
||||
use std::cmp::Ordering;
|
||||
|
||||
/// The position of a [`Tab`] within a list of tabs.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum TabPosition {
|
||||
/// The tab is first in the list.
|
||||
First,
|
||||
|
||||
/// The tab is in the middle of the list (i.e., it is not the first or last tab).
|
||||
///
|
||||
/// The [`Ordering`] is where this tab is positioned with respect to the selected tab.
|
||||
Middle(Ordering),
|
||||
|
||||
/// The tab is last in the list.
|
||||
Last,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum TabCloseSide {
|
||||
Start,
|
||||
End,
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Tab {
|
||||
div: Stateful<Div>,
|
||||
selected: bool,
|
||||
position: TabPosition,
|
||||
close_side: TabCloseSide,
|
||||
start_slot: Option<AnyElement>,
|
||||
end_slot: Option<AnyElement>,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl Tab {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
div: div().id(id),
|
||||
selected: false,
|
||||
position: TabPosition::First,
|
||||
close_side: TabCloseSide::End,
|
||||
start_slot: None,
|
||||
end_slot: None,
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub const HEIGHT_IN_REMS: f32 = 30. / 16.;
|
||||
|
||||
pub fn position(mut self, position: TabPosition) -> Self {
|
||||
self.position = position;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn close_side(mut self, close_side: TabCloseSide) -> Self {
|
||||
self.close_side = close_side;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn start_slot<E: IntoElement>(mut self, element: impl Into<Option<E>>) -> Self {
|
||||
self.start_slot = element.into().map(IntoElement::into_any_element);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn end_slot<E: IntoElement>(mut self, element: impl Into<Option<E>>) -> Self {
|
||||
self.end_slot = element.into().map(IntoElement::into_any_element);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractiveElement for Tab {
|
||||
fn interactivity(&mut self) -> &mut gpui::Interactivity {
|
||||
self.div.interactivity()
|
||||
}
|
||||
}
|
||||
|
||||
impl StatefulInteractiveElement for Tab {}
|
||||
|
||||
impl Selectable for Tab {
|
||||
fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for Tab {
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
|
||||
&mut self.children
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Tab {
|
||||
#[allow(refining_impl_trait)]
|
||||
fn render(self, cx: &mut WindowContext) -> Stateful<Div> {
|
||||
let (text_color, tab_bg, _tab_hover_bg, _tab_active_bg) = match self.selected {
|
||||
false => (
|
||||
cx.theme().colors().text_muted,
|
||||
cx.theme().colors().tab_inactive_background,
|
||||
cx.theme().colors().ghost_element_hover,
|
||||
cx.theme().colors().ghost_element_active,
|
||||
),
|
||||
true => (
|
||||
cx.theme().colors().text,
|
||||
cx.theme().colors().tab_active_background,
|
||||
cx.theme().colors().element_hover,
|
||||
cx.theme().colors().element_active,
|
||||
),
|
||||
};
|
||||
|
||||
self.div
|
||||
.h(rems(Self::HEIGHT_IN_REMS))
|
||||
.bg(tab_bg)
|
||||
.border_color(cx.theme().colors().border)
|
||||
.map(|this| match self.position {
|
||||
TabPosition::First => {
|
||||
if self.selected {
|
||||
this.pl_px().border_r().pb_px()
|
||||
} else {
|
||||
this.pl_px().pr_px().border_b()
|
||||
}
|
||||
}
|
||||
TabPosition::Last => {
|
||||
if self.selected {
|
||||
this.border_l().border_r().pb_px()
|
||||
} else {
|
||||
this.pr_px().pl_px().border_b()
|
||||
}
|
||||
}
|
||||
TabPosition::Middle(Ordering::Equal) => this.border_l().border_r().pb_px(),
|
||||
TabPosition::Middle(Ordering::Less) => this.border_l().pr_px().border_b(),
|
||||
TabPosition::Middle(Ordering::Greater) => this.border_r().pl_px().border_b(),
|
||||
})
|
||||
.child(
|
||||
h_stack()
|
||||
.group("")
|
||||
.relative()
|
||||
.h_full()
|
||||
.px_5()
|
||||
.gap_1()
|
||||
.text_color(text_color)
|
||||
// .hover(|style| style.bg(tab_hover_bg))
|
||||
// .active(|style| style.bg(tab_active_bg))
|
||||
.child(
|
||||
h_stack()
|
||||
.w_3()
|
||||
.h_3()
|
||||
.justify_center()
|
||||
.absolute()
|
||||
.map(|this| match self.close_side {
|
||||
TabCloseSide::Start => this.right_1(),
|
||||
TabCloseSide::End => this.left_1(),
|
||||
})
|
||||
.children(self.start_slot),
|
||||
)
|
||||
.child(
|
||||
h_stack()
|
||||
.w_3()
|
||||
.h_3()
|
||||
.justify_center()
|
||||
.absolute()
|
||||
.map(|this| match self.close_side {
|
||||
TabCloseSide::Start => this.left_1(),
|
||||
TabCloseSide::End => this.right_1(),
|
||||
})
|
||||
.visible_on_hover("")
|
||||
.children(self.end_slot),
|
||||
)
|
||||
.children(self.children),
|
||||
)
|
||||
}
|
||||
}
|
156
crates/ui/src/components/tab_bar.rs
Normal file
156
crates/ui/src/components/tab_bar.rs
Normal file
|
@ -0,0 +1,156 @@
|
|||
use gpui::{AnyElement, ScrollHandle};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct TabBar {
|
||||
id: ElementId,
|
||||
start_children: SmallVec<[AnyElement; 2]>,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
end_children: SmallVec<[AnyElement; 2]>,
|
||||
scroll_handle: Option<ScrollHandle>,
|
||||
}
|
||||
|
||||
impl TabBar {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
start_children: SmallVec::new(),
|
||||
children: SmallVec::new(),
|
||||
end_children: SmallVec::new(),
|
||||
scroll_handle: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn track_scroll(mut self, scroll_handle: ScrollHandle) -> Self {
|
||||
self.scroll_handle = Some(scroll_handle);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn start_children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
|
||||
&mut self.start_children
|
||||
}
|
||||
|
||||
pub fn start_child(mut self, start_child: impl IntoElement) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.start_children_mut()
|
||||
.push(start_child.into_element().into_any());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn start_children(
|
||||
mut self,
|
||||
start_children: impl IntoIterator<Item = impl IntoElement>,
|
||||
) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.start_children_mut().extend(
|
||||
start_children
|
||||
.into_iter()
|
||||
.map(|child| child.into_any_element()),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn end_children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
|
||||
&mut self.end_children
|
||||
}
|
||||
|
||||
pub fn end_child(mut self, end_child: impl IntoElement) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.end_children_mut()
|
||||
.push(end_child.into_element().into_any());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn end_children(mut self, end_children: impl IntoIterator<Item = impl IntoElement>) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.end_children_mut().extend(
|
||||
end_children
|
||||
.into_iter()
|
||||
.map(|child| child.into_any_element()),
|
||||
);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for TabBar {
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
|
||||
&mut self.children
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for TabBar {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
const HEIGHT_IN_REMS: f32 = 30. / 16.;
|
||||
|
||||
div()
|
||||
.id(self.id)
|
||||
.group("tab_bar")
|
||||
.flex()
|
||||
.flex_none()
|
||||
.w_full()
|
||||
.h(rems(HEIGHT_IN_REMS))
|
||||
.bg(cx.theme().colors().tab_bar_background)
|
||||
.when(!self.start_children.is_empty(), |this| {
|
||||
this.child(
|
||||
h_stack()
|
||||
.flex_none()
|
||||
.gap_1()
|
||||
.px_1()
|
||||
.border_b()
|
||||
.border_r()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.children(self.start_children),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.relative()
|
||||
.flex_1()
|
||||
.h_full()
|
||||
.overflow_hidden_x()
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.z_index(1)
|
||||
.size_full()
|
||||
.border_b()
|
||||
.border_color(cx.theme().colors().border),
|
||||
)
|
||||
.child(
|
||||
h_stack()
|
||||
.id("tabs")
|
||||
.z_index(2)
|
||||
.flex_grow()
|
||||
.overflow_x_scroll()
|
||||
.when_some(self.scroll_handle, |cx, scroll_handle| {
|
||||
cx.track_scroll(&scroll_handle)
|
||||
})
|
||||
.children(self.children),
|
||||
),
|
||||
)
|
||||
.when(!self.end_children.is_empty(), |this| {
|
||||
this.child(
|
||||
h_stack()
|
||||
.flex_none()
|
||||
.gap_1()
|
||||
.px_1()
|
||||
.border_b()
|
||||
.border_l()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.children(self.end_children),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
97
crates/ui/src/components/tooltip.rs
Normal file
97
crates/ui/src/components/tooltip.rs
Normal file
|
@ -0,0 +1,97 @@
|
|||
use gpui::{overlay, Action, AnyView, IntoElement, Render, VisualContext};
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{h_stack, v_stack, Color, KeyBinding, Label, LabelSize, StyledExt};
|
||||
|
||||
pub struct Tooltip {
|
||||
title: SharedString,
|
||||
meta: Option<SharedString>,
|
||||
key_binding: Option<KeyBinding>,
|
||||
}
|
||||
|
||||
impl Tooltip {
|
||||
pub fn text(title: impl Into<SharedString>, cx: &mut WindowContext) -> AnyView {
|
||||
cx.new_view(|_cx| Self {
|
||||
title: title.into(),
|
||||
meta: None,
|
||||
key_binding: None,
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn for_action(
|
||||
title: impl Into<SharedString>,
|
||||
action: &dyn Action,
|
||||
cx: &mut WindowContext,
|
||||
) -> AnyView {
|
||||
cx.new_view(|cx| Self {
|
||||
title: title.into(),
|
||||
meta: None,
|
||||
key_binding: KeyBinding::for_action(action, cx),
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn with_meta(
|
||||
title: impl Into<SharedString>,
|
||||
action: Option<&dyn Action>,
|
||||
meta: impl Into<SharedString>,
|
||||
cx: &mut WindowContext,
|
||||
) -> AnyView {
|
||||
cx.new_view(|cx| Self {
|
||||
title: title.into(),
|
||||
meta: Some(meta.into()),
|
||||
key_binding: action.and_then(|action| KeyBinding::for_action(action, cx)),
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn new(title: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
title: title.into(),
|
||||
meta: None,
|
||||
key_binding: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn meta(mut self, meta: impl Into<SharedString>) -> Self {
|
||||
self.meta = Some(meta.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn key_binding(mut self, key_binding: impl Into<Option<KeyBinding>>) -> Self {
|
||||
self.key_binding = key_binding.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Tooltip {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
|
||||
overlay().child(
|
||||
// padding to avoid mouse cursor
|
||||
div().pl_2().pt_2p5().child(
|
||||
v_stack()
|
||||
.elevation_2(cx)
|
||||
.font(ui_font)
|
||||
.text_ui()
|
||||
.text_color(cx.theme().colors().text)
|
||||
.py_1()
|
||||
.px_2()
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_4()
|
||||
.child(self.title.clone())
|
||||
.when_some(self.key_binding.clone(), |this, key_binding| {
|
||||
this.justify_between().child(key_binding)
|
||||
}),
|
||||
)
|
||||
.when_some(self.meta.clone(), |this, meta| {
|
||||
this.child(Label::new(meta).size(LabelSize::Small).color(Color::Muted))
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue