Merge remote-tracking branch 'origin/main' into perf-2

This commit is contained in:
Nathan Sobo
2023-12-14 14:16:59 -07:00
68 changed files with 2701 additions and 1149 deletions
@@ -2,7 +2,6 @@ use gpui::{relative, DefiniteLength};
use gpui::{rems, transparent_black, AnyElement, AnyView, ClickEvent, Div, Hsla, Rems, Stateful};
use smallvec::SmallVec;
use crate::h_stack;
use crate::prelude::*;
pub trait ButtonCommon: Clickable + Disableable {
@@ -250,6 +249,7 @@ impl ButtonSize {
/// 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,
@@ -264,6 +264,7 @@ pub struct ButtonLike {
impl ButtonLike {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
base: div(),
id: id.into(),
style: ButtonStyle::default(),
disabled: false,
@@ -331,6 +332,13 @@ impl ButtonCommon for ButtonLike {
}
}
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
@@ -341,7 +349,8 @@ impl RenderOnce for ButtonLike {
type Rendered = Stateful<Div>;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
h_stack()
self.base
.h_flex()
.id(self.id.clone())
.group("")
.flex_none()
@@ -98,6 +98,13 @@ impl ButtonCommon for IconButton {
}
}
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 {
type Rendered = ButtonLike;
+3 -2
View File
@@ -255,6 +255,9 @@ impl Render for ContextMenu {
};
ListItem::new(label.clone())
.inset(true)
.selected(Some(ix) == self.selected_index)
.on_click(move |_, cx| handler(cx))
.child(
h_stack()
.w_full()
@@ -265,8 +268,6 @@ impl Render for ContextMenu {
.map(|binding| div().ml_1().child(binding))
})),
)
.selected(Some(ix) == self.selected_index)
.on_click(move |_, cx| handler(cx))
.into_any_element()
}
},
+2 -1
View File
@@ -1,6 +1,7 @@
use crate::{prelude::*, Color, Icon, IconButton, IconSize};
use gpui::ClickEvent;
use crate::{prelude::*, Color, Icon, IconButton, IconSize};
#[derive(IntoElement)]
pub struct Disclosure {
is_open: bool,
+44 -22
View File
@@ -1,12 +1,18 @@
use crate::{h_stack, prelude::*, Disclosure, Icon, IconElement, IconSize, Label};
use crate::{h_stack, prelude::*, Disclosure, Label};
use gpui::{AnyElement, ClickEvent, Div};
use smallvec::SmallVec;
#[derive(IntoElement)]
pub struct ListHeader {
/// The label of the header.
label: SharedString,
left_icon: Option<Icon>,
meta: SmallVec<[AnyElement; 2]>,
/// 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,
@@ -17,8 +23,9 @@ impl ListHeader {
pub fn new(label: impl Into<SharedString>) -> Self {
Self {
label: label.into(),
left_icon: None,
meta: SmallVec::new(),
start_slot: None,
end_slot: None,
end_hover_slot: None,
inset: false,
toggle: None,
on_toggle: None,
@@ -39,13 +46,23 @@ impl ListHeader {
self
}
pub fn left_icon(mut self, left_icon: impl Into<Option<Icon>>) -> Self {
self.left_icon = left_icon.into();
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 meta(mut self, meta: impl IntoElement) -> Self {
self.meta.push(meta.into_any_element());
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
}
}
@@ -61,9 +78,9 @@ impl RenderOnce for ListHeader {
type Rendered = Div;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
h_stack().w_full().relative().child(
h_stack().w_full().relative().group("list_header").child(
div()
.h_5()
.h_7()
.when(self.inset, |this| this.px_2())
.when(self.selected, |this| {
this.bg(cx.theme().colors().ghost_element_selected)
@@ -77,24 +94,29 @@ impl RenderOnce for ListHeader {
.child(
h_stack()
.gap_1()
.children(
self.toggle
.map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)),
)
.child(
div()
.flex()
.gap_1()
.items_center()
.children(self.left_icon.map(|i| {
IconElement::new(i)
.color(Color::Muted)
.size(IconSize::Small)
}))
.children(self.start_slot)
.child(Label::new(self.label.clone()).color(Color::Muted)),
)
.children(
self.toggle
.map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)),
),
)
.child(h_stack().gap_2().items_center().children(self.meta)),
.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),
)
}),
)
}
}
+109 -54
View File
@@ -1,19 +1,24 @@
use crate::{prelude::*, Avatar, Disclosure, Icon, IconElement, IconSize};
use gpui::{
px, AnyElement, AnyView, ClickEvent, Div, ImageSource, MouseButton, MouseDownEvent, Pixels,
Stateful,
px, AnyElement, AnyView, ClickEvent, Div, MouseButton, MouseDownEvent, Pixels, Stateful,
};
use smallvec::SmallVec;
use crate::{prelude::*, Disclosure};
#[derive(IntoElement)]
pub struct ListItem {
id: ElementId,
selected: bool,
// TODO: Reintroduce this
// disclosure_control_style: DisclosureControlVisibility,
indent_level: usize,
indent_step_size: Pixels,
left_slot: Option<AnyElement>,
/// 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>>,
@@ -30,7 +35,9 @@ impl ListItem {
selected: false,
indent_level: 0,
indent_step_size: px(12.),
left_slot: None,
start_slot: None,
end_slot: None,
end_hover_slot: None,
toggle: None,
inset: false,
on_click: None,
@@ -87,23 +94,18 @@ impl ListItem {
self
}
pub fn left_child(mut self, left_content: impl IntoElement) -> Self {
self.left_slot = Some(left_content.into_any_element());
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 left_icon(mut self, left_icon: Icon) -> Self {
self.left_slot = Some(
IconElement::new(left_icon)
.size(IconSize::Small)
.color(Color::Muted)
.into_any_element(),
);
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 left_avatar(mut self, left_avatar: impl Into<ImageSource>) -> Self {
self.left_slot = Some(Avatar::new(left_avatar).into_any_element());
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
}
}
@@ -125,49 +127,102 @@ impl RenderOnce for ListItem {
type Rendered = Stateful<Div>;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
div()
h_stack()
.id(self.id)
.w_full()
.relative()
// TODO: Add focus state
// .when(self.state == InteractionState::Focused, |this| {
// this.border()
// .border_color(cx.theme().colors().border_focused)
// })
.when(self.inset, |this| this.rounded_md())
.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 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_1()
})
.when_some(self.on_click, |this, on_click| {
this.cursor_pointer().on_click(move |event, cx| {
// HACK: GPUI currently fires `on_click` with any mouse button,
// but we only care about the left button.
if event.down.button == MouseButton::Left {
(on_click)(event, cx)
}
})
.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)
})
})
.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))
.child(
div()
.when(self.inset, |this| this.px_2())
.ml(self.indent_level as f32 * self.indent_step_size)
.flex()
.gap_1()
.items_center()
h_stack()
.id("inner_list_item")
.w_full()
.relative()
.children(
self.toggle
.map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)),
.gap_1()
.px_2()
.group("list_item")
.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)
})
})
.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.))
.visible_on_hover("")
.child(Disclosure::new(is_open).on_toggle(self.on_toggle))
}))
.child(
h_stack()
.flex_1()
.gap_1()
.children(self.start_slot)
.children(self.children),
)
.children(self.left_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),
)
}),
)
}
}
@@ -9,6 +9,10 @@ impl RenderOnce for ListSeparator {
type Rendered = Div;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
div().h_px().w_full().bg(cx.theme().colors().border_variant)
div()
.h_px()
.w_full()
.my_1()
.bg(cx.theme().colors().border_variant)
}
}
@@ -6,7 +6,7 @@ use crate::{h_stack, Icon, IconElement, IconSize, Label};
#[derive(IntoElement)]
pub struct ListSubHeader {
label: SharedString,
left_icon: Option<Icon>,
start_slot: Option<Icon>,
inset: bool,
}
@@ -14,13 +14,13 @@ impl ListSubHeader {
pub fn new(label: impl Into<SharedString>) -> Self {
Self {
label: label.into(),
left_icon: None,
start_slot: None,
inset: false,
}
}
pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
self.left_icon = left_icon;
self.start_slot = left_icon;
self
}
}
@@ -44,7 +44,7 @@ impl RenderOnce for ListSubHeader {
.flex()
.gap_1()
.items_center()
.children(self.left_icon.map(|i| {
.children(self.start_slot.map(|i| {
IconElement::new(i)
.color(Color::Muted)
.size(IconSize::Small)
@@ -15,19 +15,19 @@ impl Render for ListHeaderStory {
.child(Story::label("Default"))
.child(ListHeader::new("Section 1"))
.child(Story::label("With left icon"))
.child(ListHeader::new("Section 2").left_icon(Icon::Bell))
.child(ListHeader::new("Section 2").start_slot(IconElement::new(Icon::Bell)))
.child(Story::label("With left icon and meta"))
.child(
ListHeader::new("Section 3")
.left_icon(Icon::BellOff)
.meta(IconButton::new("action_1", Icon::Bolt)),
.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")
.meta(IconButton::new("action_1", Icon::Bolt))
.meta(IconButton::new("action_2", Icon::ExclamationTriangle))
.meta(IconButton::new("action_3", Icon::Plus)),
.end_slot(IconButton::new("action_1", Icon::Bolt))
.end_slot(IconButton::new("action_2", Icon::ExclamationTriangle))
.end_slot(IconButton::new("action_3", Icon::Plus)),
)
}
}
+64 -8
View File
@@ -1,7 +1,7 @@
use gpui::{Div, Render};
use story::Story;
use crate::prelude::*;
use crate::{prelude::*, Avatar};
use crate::{Icon, ListItem};
pub struct ListItemStory;
@@ -9,24 +9,80 @@ pub struct ListItemStory;
impl Render for ListItemStory {
type Element = Div;
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
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("With left icon"))
.child(Story::label("Inset"))
.child(
ListItem::new("with_left_icon")
ListItem::new("inset_list_item")
.inset(true)
.start_slot(
IconElement::new(Icon::Bell)
.size(IconSize::Small)
.color(Color::Muted),
)
.child("Hello, world!")
.left_icon(Icon::Bell),
.end_slot(
IconElement::new(Icon::Bell)
.size(IconSize::Small)
.color(Color::Muted),
),
)
.child(Story::label("With left avatar"))
.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!")
.left_avatar(SharedString::from(
.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(
+1 -2
View File
@@ -158,7 +158,6 @@ impl RenderOnce for Tab {
)
.child(
h_stack()
.invisible()
.w_3()
.h_3()
.justify_center()
@@ -167,7 +166,7 @@ impl RenderOnce for Tab {
TabCloseSide::Start => this.left_1(),
TabCloseSide::End => this.right_1(),
})
.group_hover("", |style| style.visible())
.visible_on_hover("")
.children(self.end_slot),
)
.children(self.children),
+1
View File
@@ -9,6 +9,7 @@ pub use crate::clickable::*;
pub use crate::disableable::*;
pub use crate::fixed::*;
pub use crate::selectable::*;
pub use crate::visible_on_hover::*;
pub use crate::{h_stack, v_stack};
pub use crate::{Button, ButtonSize, ButtonStyle, IconButton};
pub use crate::{ButtonCommon, Color, StyledExt};
+13 -3
View File
@@ -118,16 +118,26 @@ pub trait StyledExt: Styled + Sized {
elevated(self, cx, ElevationIndex::ModalSurface)
}
/// The theme's primary border color.
fn border_primary(self, cx: &mut WindowContext) -> Self {
self.border_color(cx.theme().colors().border)
}
/// The theme's secondary or muted border color.
fn border_muted(self, cx: &mut WindowContext) -> Self {
self.border_color(cx.theme().colors().border_variant)
}
fn debug_bg_red(self) -> Self {
self.bg(gpui::red())
self.bg(hsla(0. / 360., 1., 0.5, 1.))
}
fn debug_bg_green(self) -> Self {
self.bg(gpui::green())
self.bg(hsla(120. / 360., 1., 0.5, 1.))
}
fn debug_bg_blue(self) -> Self {
self.bg(gpui::blue())
self.bg(hsla(240. / 360., 1., 0.5, 1.))
}
fn debug_bg_yellow(self) -> Self {
+2
View File
@@ -21,6 +21,7 @@ mod selectable;
mod styled_ext;
mod styles;
pub mod utils;
mod visible_on_hover;
pub use clickable::*;
pub use components::*;
@@ -30,3 +31,4 @@ pub use prelude::*;
pub use selectable::*;
pub use styled_ext::*;
pub use styles::*;
pub use visible_on_hover::*;
+15
View File
@@ -0,0 +1,15 @@
use gpui::{InteractiveElement, SharedString, Styled};
pub trait VisibleOnHover {
/// Sets the element to only be visible when the specified group is hovered.
///
/// Pass `""` as the `group_name` to use the global group.
fn visible_on_hover(self, group_name: impl Into<SharedString>) -> Self;
}
impl<E: InteractiveElement + Styled> VisibleOnHover for E {
fn visible_on_hover(self, group_name: impl Into<SharedString>) -> Self {
self.invisible()
.group_hover(group_name, |style| style.visible())
}
}