Merge remote-tracking branch 'origin/main' into surfaces

# Conflicts:
#	crates/ui2/src/components/avatar.rs
This commit is contained in:
Antonio Scandurra 2023-11-30 10:43:45 +01:00
commit cc0bc444b1
139 changed files with 8384 additions and 6398 deletions

View file

@ -0,0 +1,7 @@
use gpui::{ClickEvent, WindowContext};
/// A trait for elements that can be clicked.
pub trait Clickable {
/// Sets the click handler that will fire whenever the element is clicked.
fn on_click(self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self;
}

View file

@ -5,15 +5,11 @@ mod context_menu;
mod disclosure;
mod divider;
mod icon;
mod icon_button;
mod input;
mod keybinding;
mod label;
mod list;
mod popover;
mod slot;
mod stack;
mod toggle;
mod tooltip;
#[cfg(feature = "stories")]
@ -26,15 +22,11 @@ pub use context_menu::*;
pub use disclosure::*;
pub use divider::*;
pub use icon::*;
pub use icon_button::*;
pub use input::*;
pub use keybinding::*;
pub use label::*;
pub use list::*;
pub use popover::*;
pub use slot::*;
pub use stack::*;
pub use toggle::*;
pub use tooltip::*;
#[cfg(feature = "stories")]

View file

@ -1,7 +1,7 @@
use std::sync::Arc;
use crate::prelude::*;
use gpui::{img, ImageData, ImageSource, Img, IntoElement};
use gpui::{img, rems, Div, ImageData, ImageSource, IntoElement, Styled};
#[derive(Debug, Default, PartialEq, Clone)]
pub enum Shape {
@ -13,13 +13,14 @@ pub enum Shape {
#[derive(IntoElement)]
pub struct Avatar {
src: ImageSource,
is_available: Option<bool>,
shape: Shape,
}
impl RenderOnce for Avatar {
type Rendered = Img;
type Rendered = Div;
fn render(self, _: &mut WindowContext) -> Self::Rendered {
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
let mut img = img(self.src);
if self.shape == Shape::Circle {
@ -28,9 +29,28 @@ impl RenderOnce for Avatar {
img = img.rounded_md();
}
img.size_4()
// todo!(Pull the avatar fallback background from the theme.)
.bg(gpui::red())
let size = rems(1.0);
div()
.size(size)
.child(
img.size(size)
// todo!(Pull the avatar fallback background from the theme.)
.bg(gpui::red()),
)
.children(self.is_available.map(|is_free| {
// HACK: non-integer sizes result in oval indicators.
let indicator_size = (size.0 * cx.rem_size() * 0.4).round();
div()
.absolute()
.z_index(1)
.bg(if is_free { gpui::green() } else { gpui::red() })
.size(indicator_size)
.rounded(indicator_size)
.bottom_0()
.right_0()
}))
}
}
@ -39,17 +59,30 @@ impl Avatar {
Self {
src: src.into().into(),
shape: Shape::Circle,
is_available: None,
}
}
pub fn data(src: Arc<ImageData>) -> Self {
Self {
src: src.into(),
shape: Shape::Circle,
is_available: None,
}
}
pub fn source(src: ImageSource) -> Self {
Self {
src,
shape: Shape::Circle,
is_available: None,
}
}
pub fn shape(mut self, shape: Shape) -> Self {
self.shape = shape;
self
}
pub fn availability_indicator(mut self, is_available: impl Into<Option<bool>>) -> Self {
self.is_available = is_available.into();
self
}
}

View file

@ -1,233 +0,0 @@
use std::rc::Rc;
use gpui::{
DefiniteLength, Div, Hsla, IntoElement, MouseButton, MouseDownEvent,
StatefulInteractiveElement, WindowContext,
};
use crate::prelude::*;
use crate::{h_stack, Color, Icon, IconButton, IconElement, Label, LineHeightStyle};
/// Provides the flexibility to use either a standard
/// button or an icon button in a given context.
pub enum ButtonOrIconButton {
Button(Button),
IconButton(IconButton),
}
impl From<Button> for ButtonOrIconButton {
fn from(value: Button) -> Self {
Self::Button(value)
}
}
impl From<IconButton> for ButtonOrIconButton {
fn from(value: IconButton) -> Self {
Self::IconButton(value)
}
}
#[derive(Default, PartialEq, Clone, Copy)]
pub enum IconPosition {
#[default]
Left,
Right,
}
#[derive(Default, Copy, Clone, PartialEq)]
pub enum ButtonVariant {
#[default]
Ghost,
Filled,
}
impl ButtonVariant {
pub fn bg_color(&self, cx: &mut WindowContext) -> Hsla {
match self {
ButtonVariant::Ghost => cx.theme().colors().ghost_element_background,
ButtonVariant::Filled => cx.theme().colors().element_background,
}
}
pub fn bg_color_hover(&self, cx: &mut WindowContext) -> Hsla {
match self {
ButtonVariant::Ghost => cx.theme().colors().ghost_element_hover,
ButtonVariant::Filled => cx.theme().colors().element_hover,
}
}
pub fn bg_color_active(&self, cx: &mut WindowContext) -> Hsla {
match self {
ButtonVariant::Ghost => cx.theme().colors().ghost_element_active,
ButtonVariant::Filled => cx.theme().colors().element_active,
}
}
}
#[derive(IntoElement)]
pub struct Button {
disabled: bool,
click_handler: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext)>>,
icon: Option<Icon>,
icon_position: Option<IconPosition>,
label: SharedString,
variant: ButtonVariant,
width: Option<DefiniteLength>,
color: Option<Color>,
}
impl RenderOnce for Button {
type Rendered = gpui::Stateful<Div>;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
let (icon_color, label_color) = match (self.disabled, self.color) {
(true, _) => (Color::Disabled, Color::Disabled),
(_, None) => (Color::Default, Color::Default),
(_, Some(color)) => (Color::from(color), color),
};
let mut button = h_stack()
.id(SharedString::from(format!("{}", self.label)))
.relative()
.p_1()
.text_ui()
.rounded_md()
.bg(self.variant.bg_color(cx))
.cursor_pointer()
.hover(|style| style.bg(self.variant.bg_color_hover(cx)))
.active(|style| style.bg(self.variant.bg_color_active(cx)));
match (self.icon, self.icon_position) {
(Some(_), Some(IconPosition::Left)) => {
button = button
.gap_1()
.child(self.render_label(label_color))
.children(self.render_icon(icon_color))
}
(Some(_), Some(IconPosition::Right)) => {
button = button
.gap_1()
.children(self.render_icon(icon_color))
.child(self.render_label(label_color))
}
(_, _) => button = button.child(self.render_label(label_color)),
}
if let Some(width) = self.width {
button = button.w(width).justify_center();
}
if let Some(click_handler) = self.click_handler.clone() {
button = button.on_mouse_down(MouseButton::Left, move |event, cx| {
click_handler(event, cx);
});
}
button
}
}
impl Button {
pub fn new(label: impl Into<SharedString>) -> Self {
Self {
disabled: false,
click_handler: None,
icon: None,
icon_position: None,
label: label.into(),
variant: Default::default(),
width: Default::default(),
color: None,
}
}
pub fn ghost(label: impl Into<SharedString>) -> Self {
Self::new(label).variant(ButtonVariant::Ghost)
}
pub fn variant(mut self, variant: ButtonVariant) -> Self {
self.variant = variant;
self
}
pub fn icon(mut self, icon: Icon) -> Self {
self.icon = Some(icon);
self
}
pub fn icon_position(mut self, icon_position: IconPosition) -> Self {
if self.icon.is_none() {
panic!("An icon must be present if an icon_position is provided.");
}
self.icon_position = Some(icon_position);
self
}
pub fn width(mut self, width: Option<DefiniteLength>) -> Self {
self.width = width;
self
}
pub fn on_click(
mut self,
handler: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
) -> Self {
self.click_handler = Some(Rc::new(handler));
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn color(mut self, color: Option<Color>) -> Self {
self.color = color;
self
}
pub fn label_color(&self, color: Option<Color>) -> Color {
if self.disabled {
Color::Disabled
} else if let Some(color) = color {
color
} else {
Default::default()
}
}
fn render_label(&self, color: Color) -> Label {
Label::new(self.label.clone())
.color(color)
.line_height_style(LineHeightStyle::UILabel)
}
fn render_icon(&self, icon_color: Color) -> Option<IconElement> {
self.icon.map(|i| IconElement::new(i).color(icon_color))
}
}
#[derive(IntoElement)]
pub struct ButtonGroup {
buttons: Vec<Button>,
}
impl RenderOnce for ButtonGroup {
type Rendered = Div;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
let mut group = h_stack();
for button in self.buttons.into_iter() {
group = group.child(button.render(cx));
}
group
}
}
impl ButtonGroup {
pub fn new(buttons: Vec<Button>) -> Self {
Self { buttons }
}
}

View file

@ -0,0 +1,91 @@
use gpui::AnyView;
use crate::prelude::*;
use crate::{ButtonCommon, ButtonLike, ButtonSize2, ButtonStyle2, Label, LineHeightStyle};
#[derive(IntoElement)]
pub struct Button {
base: ButtonLike,
label: SharedString,
label_color: Option<Color>,
}
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,
}
}
pub fn color(mut self, label_color: impl Into<Option<Color>>) -> Self {
self.label_color = label_color.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 ButtonCommon for Button {
fn id(&self) -> &ElementId {
self.base.id()
}
fn style(mut self, style: ButtonStyle2) -> Self {
self.base = self.base.style(style);
self
}
fn size(mut self, size: ButtonSize2) -> 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 {
type Rendered = ButtonLike;
fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
let label_color = if self.base.disabled {
Color::Disabled
} else if self.base.selected {
Color::Selected
} else {
Color::Default
};
self.base.child(
Label::new(self.label)
.color(label_color)
.line_height_style(LineHeightStyle::UILabel),
)
}
}

View file

@ -0,0 +1,273 @@
use gpui::{rems, AnyElement, AnyView, ClickEvent, Div, Hsla, Rems, Stateful};
use smallvec::SmallVec;
use crate::h_stack;
use crate::prelude::*;
pub trait ButtonCommon: Clickable + Disableable {
fn id(&self) -> &ElementId;
fn style(self, style: ButtonStyle2) -> Self;
fn size(self, size: ButtonSize2) -> Self;
fn tooltip(self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self;
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
pub enum ButtonStyle2 {
#[default]
Filled,
// Tinted,
Subtle,
Transparent,
}
#[derive(Debug, Clone)]
pub struct ButtonStyle {
pub background: Hsla,
pub border_color: Hsla,
pub label_color: Hsla,
pub icon_color: Hsla,
}
impl ButtonStyle2 {
pub fn enabled(self, cx: &mut WindowContext) -> ButtonStyle {
match self {
ButtonStyle2::Filled => ButtonStyle {
background: cx.theme().colors().element_background,
border_color: gpui::transparent_black(),
label_color: Color::Default.color(cx),
icon_color: Color::Default.color(cx),
},
ButtonStyle2::Subtle => ButtonStyle {
background: cx.theme().colors().ghost_element_background,
border_color: gpui::transparent_black(),
label_color: Color::Default.color(cx),
icon_color: Color::Default.color(cx),
},
ButtonStyle2::Transparent => ButtonStyle {
background: gpui::transparent_black(),
border_color: gpui::transparent_black(),
label_color: Color::Default.color(cx),
icon_color: Color::Default.color(cx),
},
}
}
pub fn hovered(self, cx: &mut WindowContext) -> ButtonStyle {
match self {
ButtonStyle2::Filled => ButtonStyle {
background: cx.theme().colors().element_hover,
border_color: gpui::transparent_black(),
label_color: Color::Default.color(cx),
icon_color: Color::Default.color(cx),
},
ButtonStyle2::Subtle => ButtonStyle {
background: cx.theme().colors().ghost_element_hover,
border_color: gpui::transparent_black(),
label_color: Color::Default.color(cx),
icon_color: Color::Default.color(cx),
},
ButtonStyle2::Transparent => ButtonStyle {
background: gpui::transparent_black(),
border_color: gpui::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 fn active(self, cx: &mut WindowContext) -> ButtonStyle {
match self {
ButtonStyle2::Filled => ButtonStyle {
background: cx.theme().colors().element_active,
border_color: gpui::transparent_black(),
label_color: Color::Default.color(cx),
icon_color: Color::Default.color(cx),
},
ButtonStyle2::Subtle => ButtonStyle {
background: cx.theme().colors().ghost_element_active,
border_color: gpui::transparent_black(),
label_color: Color::Default.color(cx),
icon_color: Color::Default.color(cx),
},
ButtonStyle2::Transparent => ButtonStyle {
background: gpui::transparent_black(),
border_color: gpui::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 fn focused(self, cx: &mut WindowContext) -> ButtonStyle {
match self {
ButtonStyle2::Filled => ButtonStyle {
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),
},
ButtonStyle2::Subtle => ButtonStyle {
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),
},
ButtonStyle2::Transparent => ButtonStyle {
background: gpui::transparent_black(),
border_color: cx.theme().colors().border_focused,
label_color: Color::Accent.color(cx),
icon_color: Color::Accent.color(cx),
},
}
}
pub fn disabled(self, cx: &mut WindowContext) -> ButtonStyle {
match self {
ButtonStyle2::Filled => ButtonStyle {
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),
},
ButtonStyle2::Subtle => ButtonStyle {
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),
},
ButtonStyle2::Transparent => ButtonStyle {
background: gpui::transparent_black(),
border_color: gpui::transparent_black(),
label_color: Color::Disabled.color(cx),
icon_color: Color::Disabled.color(cx),
},
}
}
}
#[derive(Default, PartialEq, Clone, Copy)]
pub enum ButtonSize2 {
#[default]
Default,
Compact,
None,
}
impl ButtonSize2 {
fn height(self) -> Rems {
match self {
ButtonSize2::Default => rems(22. / 16.),
ButtonSize2::Compact => rems(18. / 16.),
ButtonSize2::None => rems(16. / 16.),
}
}
}
#[derive(IntoElement)]
pub struct ButtonLike {
id: ElementId,
pub(super) style: ButtonStyle2,
pub(super) disabled: bool,
pub(super) selected: bool,
size: ButtonSize2,
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 {
id: id.into(),
style: ButtonStyle2::default(),
disabled: false,
selected: false,
size: ButtonSize2::Default,
tooltip: None,
children: SmallVec::new(),
on_click: None,
}
}
}
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 ButtonCommon for ButtonLike {
fn id(&self) -> &ElementId {
&self.id
}
fn style(mut self, style: ButtonStyle2) -> Self {
self.style = style;
self
}
fn size(mut self, size: ButtonSize2) -> 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 ParentElement for ButtonLike {
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
&mut self.children
}
}
impl RenderOnce for ButtonLike {
type Rendered = Stateful<Div>;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
h_stack()
.id(self.id.clone())
.h(self.size.height())
.rounded_md()
.cursor_pointer()
.gap_1()
.px_1()
.bg(self.style.enabled(cx).background)
.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_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)
}
}

View file

@ -0,0 +1,102 @@
use gpui::{Action, AnyView};
use crate::prelude::*;
use crate::{ButtonCommon, ButtonLike, ButtonSize2, ButtonStyle2, Icon, IconElement, IconSize};
#[derive(IntoElement)]
pub struct IconButton {
base: ButtonLike,
icon: Icon,
icon_size: IconSize,
icon_color: Color,
}
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,
}
}
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 action(self, action: Box<dyn Action>) -> Self {
self.on_click(move |_event, cx| cx.dispatch_action(action.boxed_clone()))
}
}
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 ButtonCommon for IconButton {
fn id(&self) -> &ElementId {
self.base.id()
}
fn style(mut self, style: ButtonStyle2) -> Self {
self.base = self.base.style(style);
self
}
fn size(mut self, size: ButtonSize2) -> 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 IconButton {
type Rendered = ButtonLike;
fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
let icon_color = if self.base.disabled {
Color::Disabled
} else if self.base.selected {
Color::Selected
} else {
self.icon_color
};
self.base.child(
IconElement::new(self.icon)
.size(self.icon_size)
.color(icon_color),
)
}
}

View file

@ -0,0 +1,7 @@
mod button;
mod button_like;
mod icon_button;
pub use button::*;
pub use button_like::*;
pub use icon_button::*;

View file

@ -1,7 +1,6 @@
use gpui::{div, prelude::*, Div, Element, ElementId, IntoElement, Styled, WindowContext};
use theme2::ActiveTheme;
use crate::prelude::*;
use crate::{Color, Icon, IconElement, Selection};
pub type CheckHandler = Box<dyn Fn(&Selection, &mut WindowContext) + 'static>;

View file

@ -1,23 +1,28 @@
use std::cell::RefCell;
use std::rc::Rc;
use crate::{prelude::*, v_stack, Label, List};
use crate::{ListItem, ListSeparator, ListSubHeader};
use gpui::{
overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, ClickEvent, DismissEvent,
DispatchPhase, Div, EventEmitter, FocusHandle, FocusableView, IntoElement, LayoutId,
ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render, View, VisualContext,
use crate::{
h_stack, prelude::*, v_stack, KeyBinding, Label, List, ListItem, ListSeparator, ListSubHeader,
};
use gpui::{
overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, DismissEvent, DispatchPhase,
Div, EventEmitter, FocusHandle, FocusableView, IntoElement, LayoutId, ManagedView, MouseButton,
MouseDownEvent, Pixels, Point, Render, View, VisualContext,
};
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
use std::{cell::RefCell, rc::Rc};
pub enum ContextMenuItem {
Separator,
Header(SharedString),
Entry(SharedString, Rc<dyn Fn(&ClickEvent, &mut WindowContext)>),
Entry {
label: SharedString,
handler: Rc<dyn Fn(&mut WindowContext)>,
key_binding: Option<KeyBinding>,
},
}
pub struct ContextMenu {
items: Vec<ContextMenuItem>,
focus_handle: FocusHandle,
selected_index: Option<usize>,
}
impl FocusableView for ContextMenu {
@ -39,6 +44,7 @@ impl ContextMenu {
Self {
items: Default::default(),
focus_handle: cx.focus_handle(),
selected_index: None,
},
cx,
)
@ -58,27 +64,90 @@ impl ContextMenu {
pub fn entry(
mut self,
label: impl Into<SharedString>,
on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
on_click: impl Fn(&mut WindowContext) + 'static,
) -> Self {
self.items
.push(ContextMenuItem::Entry(label.into(), Rc::new(on_click)));
self.items.push(ContextMenuItem::Entry {
label: label.into(),
handler: Rc::new(on_click),
key_binding: None,
});
self
}
pub fn action(self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
// todo: add the keybindings to the list entry
self.entry(label.into(), move |_, cx| {
cx.dispatch_action(action.boxed_clone())
})
pub fn action(
mut self,
label: impl Into<SharedString>,
action: Box<dyn Action>,
cx: &mut WindowContext,
) -> Self {
self.items.push(ContextMenuItem::Entry {
label: label.into(),
key_binding: KeyBinding::for_action(&*action, cx),
handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())),
});
self
}
pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
// todo!()
cx.emit(DismissEvent::Dismiss);
if let Some(ContextMenuItem::Entry { handler, .. }) =
self.selected_index.and_then(|ix| self.items.get(ix))
{
(handler)(cx)
}
cx.emit(DismissEvent);
}
pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
cx.emit(DismissEvent::Dismiss);
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();
}
fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
for (ix, item) in self.items.iter().enumerate().rev() {
if item.is_selectable() {
self.selected_index = Some(ix);
cx.notify();
break;
}
}
}
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.select_last(&Default::default(), cx);
}
}
}
impl ContextMenuItem {
fn is_selectable(&self) -> bool {
matches!(self, Self::Entry { .. })
}
}
@ -90,38 +159,51 @@ impl Render for ContextMenu {
v_stack()
.min_w(px(200.))
.track_focus(&self.focus_handle)
.on_mouse_down_out(
cx.listener(|this: &mut Self, _, cx| this.cancel(&Default::default(), cx)),
)
// .on_action(ContextMenu::select_first)
// .on_action(ContextMenu::select_last)
// .on_action(ContextMenu::select_next)
// .on_action(ContextMenu::select_prev)
.on_mouse_down_out(cx.listener(|this, _, cx| this.cancel(&Default::default(), cx)))
.key_context("menu")
.on_action(cx.listener(ContextMenu::select_first))
.on_action(cx.listener(ContextMenu::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))
.flex_none()
// .bg(cx.theme().colors().elevated_surface_background)
// .border()
// .border_color(cx.theme().colors().border)
.child(
List::new().children(self.items.iter().map(|item| match item {
ContextMenuItem::Separator => ListSeparator::new().into_any_element(),
ContextMenuItem::Header(header) => {
ListSubHeader::new(header.clone()).into_any_element()
}
ContextMenuItem::Entry(entry, callback) => {
let callback = callback.clone();
let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent::Dismiss));
List::new().children(self.items.iter().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: entry,
handler: callback,
key_binding,
} => {
let callback = callback.clone();
let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent));
ListItem::new(entry.clone())
.child(Label::new(entry.clone()))
.on_click(move |event, cx| {
callback(event, cx);
dismiss(event, cx)
})
.into_any_element()
}
})),
ListItem::new(entry.clone())
.child(
h_stack()
.w_full()
.justify_between()
.child(Label::new(entry.clone()))
.children(
key_binding
.clone()
.map(|binding| div().ml_1().child(binding)),
),
)
.selected(Some(ix) == self.selected_index)
.on_click(move |event, cx| {
callback(cx);
dismiss(event, cx)
})
.into_any_element()
}
},
)),
),
)
}
@ -177,6 +259,7 @@ pub struct MenuHandleState<M> {
child_element: Option<AnyElement>,
menu_element: Option<AnyElement>,
}
impl<M: ManagedView> Element for MenuHandle<M> {
type State = MenuHandleState<M>;
@ -264,11 +347,9 @@ impl<M: ManagedView> Element for MenuHandle<M> {
let new_menu = (builder)(cx);
let menu2 = menu.clone();
cx.subscribe(&new_menu, move |modal, e, cx| match e {
&DismissEvent::Dismiss => {
*menu2.borrow_mut() = None;
cx.notify();
}
cx.subscribe(&new_menu, move |_modal, _: &DismissEvent, cx| {
*menu2.borrow_mut() = None;
cx.notify();
})
.detach();
cx.focus_view(&new_menu);

View file

@ -1,19 +1,48 @@
use gpui::{div, Element, ParentElement};
use std::rc::Rc;
use crate::{Color, Icon, IconElement, IconSize, Toggle};
use gpui::ClickEvent;
pub fn disclosure_control(toggle: Toggle) -> impl Element {
match (toggle.is_toggleable(), toggle.is_toggled()) {
(false, _) => div(),
(_, true) => div().child(
IconElement::new(Icon::ChevronDown)
.color(Color::Muted)
.size(IconSize::Small),
),
(_, false) => div().child(
IconElement::new(Icon::ChevronRight)
.color(Color::Muted)
.size(IconSize::Small),
),
use crate::prelude::*;
use crate::{Color, Icon, IconButton, IconSize};
#[derive(IntoElement)]
pub struct Disclosure {
is_open: bool,
on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
}
impl Disclosure {
pub fn new(is_open: bool) -> Self {
Self {
is_open,
on_toggle: None,
}
}
pub fn on_toggle(
mut self,
handler: impl Into<Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>>,
) -> Self {
self.on_toggle = handler.into();
self
}
}
impl RenderOnce for Disclosure {
type Rendered = IconButton;
fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
IconButton::new(
"toggle",
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))
})
}
}

View file

@ -49,17 +49,4 @@ impl Divider {
self.inset = true;
self
}
fn render(self, cx: &mut WindowContext) -> impl Element {
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(cx.theme().colors().border_variant)
}
}

View file

@ -14,6 +14,8 @@ pub enum IconSize {
pub enum Icon {
Ai,
ArrowLeft,
ArrowUp,
ArrowDown,
ArrowRight,
ArrowUpRight,
AtSign,
@ -61,6 +63,7 @@ pub enum Icon {
Mic,
MicMute,
Plus,
Public,
Quote,
Replace,
ReplaceAll,
@ -71,6 +74,11 @@ pub enum Icon {
Terminal,
WholeWord,
XCircle,
Command,
Control,
Shift,
Option,
Return,
}
impl Icon {
@ -79,6 +87,8 @@ impl Icon {
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",
@ -125,6 +135,7 @@ impl Icon {
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",
@ -135,6 +146,11 @@ impl Icon {
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",
}
}
}
@ -151,8 +167,8 @@ impl RenderOnce for IconElement {
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
let svg_size = match self.size {
IconSize::Small => rems(0.75),
IconSize::Medium => rems(0.9375),
IconSize::Small => rems(14. / 16.),
IconSize::Medium => rems(16. / 16.),
};
svg()
@ -189,17 +205,4 @@ impl IconElement {
self.size = size;
self
}
fn render(self, cx: &mut WindowContext) -> impl Element {
let svg_size = match self.size {
IconSize::Small => rems(0.75),
IconSize::Medium => rems(0.9375),
};
svg()
.size(svg_size)
.flex_none()
.path(self.path)
.text_color(self.color.color(cx))
}
}

View file

@ -1,129 +0,0 @@
use crate::{h_stack, prelude::*, Icon, IconElement};
use gpui::{prelude::*, Action, AnyView, Div, MouseButton, MouseDownEvent, Stateful};
#[derive(IntoElement)]
pub struct IconButton {
id: ElementId,
icon: Icon,
color: Color,
variant: ButtonVariant,
state: InteractionState,
selected: bool,
tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>>,
on_mouse_down: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
}
impl RenderOnce for IconButton {
type Rendered = Stateful<Div>;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
let icon_color = match (self.state, self.color) {
(InteractionState::Disabled, _) => Color::Disabled,
(InteractionState::Active, _) => Color::Selected,
_ => self.color,
};
let (mut bg_color, bg_hover_color, bg_active_color) = match self.variant {
ButtonVariant::Filled => (
cx.theme().colors().element_background,
cx.theme().colors().element_hover,
cx.theme().colors().element_active,
),
ButtonVariant::Ghost => (
cx.theme().colors().ghost_element_background,
cx.theme().colors().ghost_element_hover,
cx.theme().colors().ghost_element_active,
),
};
if self.selected {
bg_color = cx.theme().colors().element_selected;
}
let mut button = h_stack()
.id(self.id.clone())
.justify_center()
.rounded_md()
.p_1()
.bg(bg_color)
.cursor_pointer()
// Nate: Trying to figure out the right places we want to show a
// hover state here. I think it is a bit heavy to have it on every
// place we use an icon button.
// .hover(|style| style.bg(bg_hover_color))
.active(|style| style.bg(bg_active_color))
.child(IconElement::new(self.icon).color(icon_color));
if let Some(click_handler) = self.on_mouse_down {
button = button.on_mouse_down(MouseButton::Left, move |event, cx| {
cx.stop_propagation();
click_handler(event, cx);
})
}
if let Some(tooltip) = self.tooltip {
if !self.selected {
button = button.tooltip(move |cx| tooltip(cx))
}
}
button
}
}
impl IconButton {
pub fn new(id: impl Into<ElementId>, icon: Icon) -> Self {
Self {
id: id.into(),
icon,
color: Color::default(),
variant: ButtonVariant::default(),
state: InteractionState::default(),
selected: false,
tooltip: None,
on_mouse_down: None,
}
}
pub fn icon(mut self, icon: Icon) -> Self {
self.icon = icon;
self
}
pub fn color(mut self, color: Color) -> Self {
self.color = color;
self
}
pub fn variant(mut self, variant: ButtonVariant) -> Self {
self.variant = variant;
self
}
pub fn state(mut self, state: InteractionState) -> Self {
self.state = state;
self
}
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
self.tooltip = Some(Box::new(tooltip));
self
}
pub fn on_click(
mut self,
handler: impl 'static + Fn(&MouseDownEvent, &mut WindowContext),
) -> Self {
self.on_mouse_down = Some(Box::new(handler));
self
}
pub fn action(self, action: Box<dyn Action>) -> Self {
self.on_click(move |this, cx| cx.dispatch_action(action.boxed_clone()))
}
}

View file

@ -1,108 +0,0 @@
use crate::{prelude::*, Label};
use gpui::{prelude::*, Div, IntoElement, Stateful};
#[derive(Default, PartialEq)]
pub enum InputVariant {
#[default]
Ghost,
Filled,
}
#[derive(IntoElement)]
pub struct Input {
placeholder: SharedString,
value: String,
state: InteractionState,
variant: InputVariant,
disabled: bool,
is_active: bool,
}
impl RenderOnce for Input {
type Rendered = Stateful<Div>;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
let (input_bg, input_hover_bg, input_active_bg) = match self.variant {
InputVariant::Ghost => (
cx.theme().colors().ghost_element_background,
cx.theme().colors().ghost_element_hover,
cx.theme().colors().ghost_element_active,
),
InputVariant::Filled => (
cx.theme().colors().element_background,
cx.theme().colors().element_hover,
cx.theme().colors().element_active,
),
};
let placeholder_label = Label::new(self.placeholder.clone()).color(if self.disabled {
Color::Disabled
} else {
Color::Placeholder
});
let label = Label::new(self.value.clone()).color(if self.disabled {
Color::Disabled
} else {
Color::Default
});
div()
.id("input")
.h_7()
.w_full()
.px_2()
.border()
.border_color(cx.theme().styles.system.transparent)
.bg(input_bg)
.hover(|style| style.bg(input_hover_bg))
.active(|style| style.bg(input_active_bg))
.flex()
.items_center()
.child(div().flex().items_center().text_ui_sm().map(move |this| {
if self.value.is_empty() {
this.child(placeholder_label)
} else {
this.child(label)
}
}))
}
}
impl Input {
pub fn new(placeholder: impl Into<SharedString>) -> Self {
Self {
placeholder: placeholder.into(),
value: "".to_string(),
state: InteractionState::default(),
variant: InputVariant::default(),
disabled: false,
is_active: false,
}
}
pub fn value(mut self, value: String) -> Self {
self.value = value;
self
}
pub fn state(mut self, state: InteractionState) -> Self {
self.state = state;
self
}
pub fn variant(mut self, variant: InputVariant) -> Self {
self.variant = variant;
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn is_active(mut self, is_active: bool) -> Self {
self.is_active = is_active;
self
}
}

View file

@ -1,5 +1,5 @@
use crate::prelude::*;
use gpui::{Action, Div, IntoElement};
use crate::{h_stack, prelude::*, Icon, IconElement, IconSize};
use gpui::{relative, rems, Action, Div, IntoElement, Keystroke};
#[derive(IntoElement, Clone)]
pub struct KeyBinding {
@ -14,19 +14,35 @@ impl RenderOnce for KeyBinding {
type Rendered = Div;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
div()
.flex()
h_stack()
.flex_none()
.gap_2()
.children(self.key_binding.keystrokes().iter().map(|keystroke| {
div()
.flex()
.gap_1()
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(Key::new("^")))
.when(keystroke.modifiers.alt, |el| el.child(Key::new("")))
.when(keystroke.modifiers.command, |el| el.child(Key::new("")))
.when(keystroke.modifiers.shift, |el| el.child(Key::new("")))
.child(Key::new(keystroke.key.clone()))
.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()))
})
}))
}
}
@ -39,6 +55,22 @@ impl KeyBinding {
Some(Self::new(key_binding))
}
fn icon_for_key(keystroke: &Keystroke) -> Option<Icon> {
let mut icon: Option<Icon> = None;
if keystroke.key == "left".to_string() {
icon = Some(Icon::ArrowLeft);
} else if keystroke.key == "right".to_string() {
icon = Some(Icon::ArrowRight);
} else if keystroke.key == "up".to_string() {
icon = Some(Icon::ArrowUp);
} else if keystroke.key == "down".to_string() {
icon = Some(Icon::ArrowDown);
}
icon
}
pub fn new(key_binding: gpui::KeyBinding) -> Self {
Self { key_binding }
}
@ -53,13 +85,18 @@ impl RenderOnce for Key {
type Rendered = Div;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
let single_char = self.key.len() == 1;
div()
.px_2()
.py_0()
.rounded_md()
.text_ui_sm()
.when(single_char, |el| {
el.w(rems(14. / 16.)).flex().flex_none().justify_center()
})
.when(!single_char, |el| el.px_0p5())
.h(rems(14. / 16.))
.text_ui()
.line_height(relative(1.))
.text_color(cx.theme().colors().text)
.bg(cx.theme().colors().element_background)
.child(self.key.clone())
}
}
@ -69,3 +106,24 @@ impl Key {
Self { key: key.into() }
}
}
#[derive(IntoElement)]
pub struct KeyIcon {
icon: Icon,
}
impl RenderOnce for KeyIcon {
type Rendered = Div;
fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
div()
.w(rems(14. / 16.))
.child(IconElement::new(self.icon).size(IconSize::Small))
}
}
impl KeyIcon {
pub fn new(icon: Icon) -> Self {
Self { icon }
}
}

View file

@ -1,6 +1,6 @@
use crate::prelude::*;
use crate::styled_ext::StyledExt;
use gpui::{relative, Div, Hsla, IntoElement, StyledText, TextRun, WindowContext};
use gpui::{relative, Div, IntoElement, StyledText, TextRun, WindowContext};
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
pub enum LabelSize {
@ -182,9 +182,3 @@ impl HighlightedLabel {
self
}
}
/// A run of text that receives the same style.
struct Run {
pub text: String,
pub color: Hsla,
}

View file

@ -1,409 +1,18 @@
use gpui::{
div, px, AnyElement, ClickEvent, Div, IntoElement, Stateful, StatefulInteractiveElement,
};
mod list_header;
mod list_item;
mod list_separator;
mod list_sub_header;
use gpui::{AnyElement, Div};
use smallvec::SmallVec;
use std::rc::Rc;
use crate::{
disclosure_control, h_stack, v_stack, Avatar, Icon, IconElement, IconSize, Label, Toggle,
};
use crate::{prelude::*, GraphicSlot};
use crate::prelude::*;
use crate::{v_stack, Label};
#[derive(Clone, Copy, Default, Debug, PartialEq)]
pub enum ListItemVariant {
/// The list item extends to the far left and right of the list.
FullWidth,
#[default]
Inset,
}
pub enum ListHeaderMeta {
// TODO: These should be IconButtons
Tools(Vec<Icon>),
// TODO: This should be a button
Button(Label),
Text(Label),
}
#[derive(IntoElement)]
pub struct ListHeader {
label: SharedString,
left_icon: Option<Icon>,
meta: Option<ListHeaderMeta>,
variant: ListItemVariant,
toggle: Toggle,
}
impl RenderOnce for ListHeader {
type Rendered = Div;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
let disclosure_control = disclosure_control(self.toggle);
let meta = match self.meta {
Some(ListHeaderMeta::Tools(icons)) => div().child(
h_stack()
.gap_2()
.items_center()
.children(icons.into_iter().map(|i| {
IconElement::new(i)
.color(Color::Muted)
.size(IconSize::Small)
})),
),
Some(ListHeaderMeta::Button(label)) => div().child(label),
Some(ListHeaderMeta::Text(label)) => div().child(label),
None => div(),
};
h_stack()
.w_full()
.bg(cx.theme().colors().surface_background)
.relative()
.child(
div()
.h_5()
.when(self.variant == ListItemVariant::Inset, |this| this.px_2())
.flex()
.flex_1()
.items_center()
.justify_between()
.w_full()
.gap_1()
.child(
h_stack()
.gap_1()
.child(
div()
.flex()
.gap_1()
.items_center()
.children(self.left_icon.map(|i| {
IconElement::new(i)
.color(Color::Muted)
.size(IconSize::Small)
}))
.child(Label::new(self.label.clone()).color(Color::Muted)),
)
.child(disclosure_control),
)
.child(meta),
)
}
}
impl ListHeader {
pub fn new(label: impl Into<SharedString>) -> Self {
Self {
label: label.into(),
left_icon: None,
meta: None,
variant: ListItemVariant::default(),
toggle: Toggle::NotToggleable,
}
}
pub fn toggle(mut self, toggle: Toggle) -> Self {
self.toggle = toggle;
self
}
pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
self.left_icon = left_icon;
self
}
pub fn meta(mut self, meta: Option<ListHeaderMeta>) -> Self {
self.meta = meta;
self
}
// before_ship!("delete")
// fn render<V: 'static>(self, cx: &mut WindowContext) -> impl Element<V> {
// let disclosure_control = disclosure_control(self.toggle);
// let meta = match self.meta {
// Some(ListHeaderMeta::Tools(icons)) => div().child(
// h_stack()
// .gap_2()
// .items_center()
// .children(icons.into_iter().map(|i| {
// IconElement::new(i)
// .color(TextColor::Muted)
// .size(IconSize::Small)
// })),
// ),
// Some(ListHeaderMeta::Button(label)) => div().child(label),
// Some(ListHeaderMeta::Text(label)) => div().child(label),
// None => div(),
// };
// h_stack()
// .w_full()
// .bg(cx.theme().colors().surface_background)
// // TODO: Add focus state
// // .when(self.state == InteractionState::Focused, |this| {
// // this.border()
// // .border_color(cx.theme().colors().border_focused)
// // })
// .relative()
// .child(
// div()
// .h_5()
// .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
// .flex()
// .flex_1()
// .items_center()
// .justify_between()
// .w_full()
// .gap_1()
// .child(
// h_stack()
// .gap_1()
// .child(
// div()
// .flex()
// .gap_1()
// .items_center()
// .children(self.left_icon.map(|i| {
// IconElement::new(i)
// .color(TextColor::Muted)
// .size(IconSize::Small)
// }))
// .child(Label::new(self.label.clone()).color(TextColor::Muted)),
// )
// .child(disclosure_control),
// )
// .child(meta),
// )
// }
}
#[derive(IntoElement, Clone)]
pub struct ListSubHeader {
label: SharedString,
left_icon: Option<Icon>,
variant: ListItemVariant,
}
impl ListSubHeader {
pub fn new(label: impl Into<SharedString>) -> Self {
Self {
label: label.into(),
left_icon: None,
variant: ListItemVariant::default(),
}
}
pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
self.left_icon = left_icon;
self
}
}
impl RenderOnce for ListSubHeader {
type Rendered = Div;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
h_stack().flex_1().w_full().relative().py_1().child(
div()
.h_6()
.when(self.variant == ListItemVariant::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.left_icon.map(|i| {
IconElement::new(i)
.color(Color::Muted)
.size(IconSize::Small)
}))
.child(Label::new(self.label.clone()).color(Color::Muted)),
),
)
}
}
#[derive(Default, PartialEq, Copy, Clone)]
pub enum ListEntrySize {
#[default]
Small,
Medium,
}
#[derive(IntoElement)]
pub struct ListItem {
id: ElementId,
disabled: bool,
// TODO: Reintroduce this
// disclosure_control_style: DisclosureControlVisibility,
indent_level: u32,
left_slot: Option<GraphicSlot>,
overflow: OverflowStyle,
size: ListEntrySize,
toggle: Toggle,
variant: ListItemVariant,
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
children: SmallVec<[AnyElement; 2]>,
}
impl ListItem {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
disabled: false,
indent_level: 0,
left_slot: None,
overflow: OverflowStyle::Hidden,
size: ListEntrySize::default(),
toggle: Toggle::NotToggleable,
variant: ListItemVariant::default(),
on_click: Default::default(),
children: SmallVec::new(),
}
}
pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
self.on_click = Some(Rc::new(handler));
self
}
pub fn variant(mut self, variant: ListItemVariant) -> Self {
self.variant = variant;
self
}
pub fn indent_level(mut self, indent_level: u32) -> Self {
self.indent_level = indent_level;
self
}
pub fn toggle(mut self, toggle: Toggle) -> Self {
self.toggle = toggle;
self
}
pub fn left_content(mut self, left_content: GraphicSlot) -> Self {
self.left_slot = Some(left_content);
self
}
pub fn left_icon(mut self, left_icon: Icon) -> Self {
self.left_slot = Some(GraphicSlot::Icon(left_icon));
self
}
pub fn left_avatar(mut self, left_avatar: impl Into<SharedString>) -> Self {
self.left_slot = Some(GraphicSlot::Avatar(left_avatar.into()));
self
}
pub fn size(mut self, size: ListEntrySize) -> Self {
self.size = size;
self
}
}
impl RenderOnce for ListItem {
type Rendered = Stateful<Div>;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
let left_content = match self.left_slot.clone() {
Some(GraphicSlot::Icon(i)) => Some(
h_stack().child(
IconElement::new(i)
.size(IconSize::Small)
.color(Color::Muted),
),
),
Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::uri(src))),
Some(GraphicSlot::PublicActor(src)) => Some(h_stack().child(Avatar::uri(src))),
None => None,
};
let sized_item = match self.size {
ListEntrySize::Small => div().h_6(),
ListEntrySize::Medium => div().h_7(),
};
div()
.id(self.id)
.relative()
.hover(|mut style| {
style.background = Some(cx.theme().colors().editor_background.into());
style
})
.on_click({
let on_click = self.on_click.clone();
move |event, cx| {
if let Some(on_click) = &on_click {
(on_click)(event, cx)
}
}
})
// 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))
.child(
sized_item
.when(self.variant == ListItemVariant::Inset, |this| this.px_2())
// .ml(rems(0.75 * self.indent_level as f32))
.children((0..self.indent_level).map(|_| {
div()
.w(px(4.))
.h_full()
.flex()
.justify_center()
.group_hover("", |style| style.bg(cx.theme().colors().border_focused))
.child(
h_stack()
.child(div().w_px().h_full())
.child(div().w_px().h_full().bg(cx.theme().colors().border)),
)
}))
.flex()
.gap_1()
.items_center()
.relative()
.child(disclosure_control(self.toggle))
.children(left_content)
.children(self.children),
)
}
}
impl ParentElement for ListItem {
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
&mut self.children
}
}
#[derive(IntoElement, Clone)]
pub struct ListSeparator;
impl ListSeparator {
pub fn new() -> Self {
Self
}
}
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)
}
}
pub use list_header::*;
pub use list_item::*;
pub use list_separator::*;
pub use list_sub_header::*;
#[derive(IntoElement)]
pub struct List {
@ -411,34 +20,16 @@ pub struct List {
/// Defaults to "No items"
empty_message: SharedString,
header: Option<ListHeader>,
toggle: Toggle,
toggle: Option<bool>,
children: SmallVec<[AnyElement; 2]>,
}
impl RenderOnce for List {
type Rendered = Div;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
let list_content = match (self.children.is_empty(), self.toggle) {
(false, _) => div().children(self.children),
(true, Toggle::Toggled(false)) => div(),
(true, _) => div().child(Label::new(self.empty_message.clone()).color(Color::Muted)),
};
v_stack()
.w_full()
.py_1()
.children(self.header.map(|header| header))
.child(list_content)
}
}
impl List {
pub fn new() -> Self {
Self {
empty_message: "No items".into(),
header: None,
toggle: Toggle::NotToggleable,
toggle: None,
children: SmallVec::new(),
}
}
@ -453,8 +44,8 @@ impl List {
self
}
pub fn toggle(mut self, toggle: Toggle) -> Self {
self.toggle = toggle;
pub fn toggle(mut self, toggle: impl Into<Option<bool>>) -> Self {
self.toggle = toggle.into();
self
}
}
@ -464,3 +55,19 @@ impl ParentElement for List {
&mut self.children
}
}
impl RenderOnce for List {
type Rendered = Div;
fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
v_stack()
.w_full()
.py_1()
.children(self.header.map(|header| 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)),
})
}
}

View file

@ -0,0 +1,124 @@
use std::rc::Rc;
use gpui::{ClickEvent, Div};
use crate::prelude::*;
use crate::{h_stack, Disclosure, Icon, IconButton, IconElement, IconSize, Label};
pub enum ListHeaderMeta {
Tools(Vec<IconButton>),
// TODO: This should be a button
Button(Label),
Text(Label),
}
#[derive(IntoElement)]
pub struct ListHeader {
label: SharedString,
left_icon: Option<Icon>,
meta: Option<ListHeaderMeta>,
toggle: Option<bool>,
on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
inset: bool,
selected: bool,
}
impl ListHeader {
pub fn new(label: impl Into<SharedString>) -> Self {
Self {
label: label.into(),
left_icon: None,
meta: 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(Rc::new(on_toggle));
self
}
pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
self.left_icon = left_icon;
self
}
pub fn right_button(self, button: IconButton) -> Self {
self.meta(Some(ListHeaderMeta::Tools(vec![button])))
}
pub fn meta(mut self, meta: Option<ListHeaderMeta>) -> Self {
self.meta = meta;
self
}
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
}
impl RenderOnce for ListHeader {
type Rendered = Div;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
let meta = match self.meta {
Some(ListHeaderMeta::Tools(icons)) => div().child(
h_stack()
.gap_2()
.items_center()
.children(icons.into_iter().map(|i| i.icon_color(Color::Muted))),
),
Some(ListHeaderMeta::Button(label)) => div().child(label),
Some(ListHeaderMeta::Text(label)) => div().child(label),
None => div(),
};
h_stack().w_full().relative().child(
div()
.h_5()
.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()
.child(
div()
.flex()
.gap_1()
.items_center()
.children(self.left_icon.map(|i| {
IconElement::new(i)
.color(Color::Muted)
.size(IconSize::Small)
}))
.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(meta),
)
}
}

View file

@ -0,0 +1,166 @@
use std::rc::Rc;
use gpui::{
px, AnyElement, ClickEvent, Div, ImageSource, MouseButton, MouseDownEvent, Pixels, Stateful,
};
use smallvec::SmallVec;
use crate::prelude::*;
use crate::{Avatar, Disclosure, Icon, IconElement, IconSize};
#[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>,
toggle: Option<bool>,
inset: bool,
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
on_secondary_mouse_down: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
children: SmallVec<[AnyElement; 2]>,
}
impl ListItem {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
selected: false,
indent_level: 0,
indent_step_size: px(12.),
left_slot: None,
toggle: None,
inset: false,
on_click: None,
on_secondary_mouse_down: None,
on_toggle: None,
children: SmallVec::new(),
}
}
pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
self.on_click = Some(Rc::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(Rc::new(handler));
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(Rc::new(on_toggle));
self
}
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
pub fn left_child(mut self, left_content: impl IntoElement) -> Self {
self.left_slot = Some(left_content.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(),
);
self
}
pub fn left_avatar(mut self, left_avatar: impl Into<ImageSource>) -> Self {
self.left_slot = Some(Avatar::source(left_avatar.into()).into_any_element());
self
}
}
impl ParentElement for ListItem {
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
&mut self.children
}
}
impl RenderOnce for ListItem {
type Rendered = Stateful<Div>;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
div()
.id(self.id)
.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_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_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
this.on_mouse_down(MouseButton::Right, move |event, cx| {
(on_mouse_down)(event, cx)
})
})
.child(
div()
.when(self.inset, |this| this.px_2())
.ml(self.indent_level as f32 * self.indent_step_size)
.flex()
.gap_1()
.items_center()
.relative()
.children(
self.toggle
.map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)),
)
.children(self.left_slot)
.children(self.children),
)
}
}

View file

@ -0,0 +1,14 @@
use gpui::Div;
use crate::prelude::*;
#[derive(IntoElement)]
pub struct ListSeparator;
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)
}
}

View file

@ -0,0 +1,56 @@
use gpui::Div;
use crate::prelude::*;
use crate::{h_stack, Icon, IconElement, IconSize, Label};
#[derive(IntoElement)]
pub struct ListSubHeader {
label: SharedString,
left_icon: Option<Icon>,
inset: bool,
}
impl ListSubHeader {
pub fn new(label: impl Into<SharedString>) -> Self {
Self {
label: label.into(),
left_icon: None,
inset: false,
}
}
pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
self.left_icon = left_icon;
self
}
}
impl RenderOnce for ListSubHeader {
type Rendered = Div;
fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
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.left_icon.map(|i| {
IconElement::new(i)
.color(Color::Muted)
.size(IconSize::Small)
}))
.child(Label::new(self.label.clone()).color(Color::Muted)),
),
)
}
}

View file

@ -3,9 +3,9 @@ use gpui::{
WindowContext,
};
use smallvec::SmallVec;
use theme2::ActiveTheme;
use crate::{v_stack, StyledExt};
use crate::prelude::*;
use crate::v_stack;
/// A popover is used to display a menu or show some options.
///

View file

@ -2,18 +2,22 @@ mod avatar;
mod button;
mod checkbox;
mod context_menu;
mod disclosure;
mod icon;
mod input;
mod icon_button;
mod keybinding;
mod label;
mod list;
mod list_item;
pub use avatar::*;
pub use button::*;
pub use checkbox::*;
pub use context_menu::*;
pub use disclosure::*;
pub use icon::*;
pub use input::*;
pub use icon_button::*;
pub use keybinding::*;
pub use label::*;
pub use list::*;
pub use list_item::*;

View file

@ -9,7 +9,7 @@ pub struct AvatarStory;
impl Render for AvatarStory {
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()
.child(Story::title_for::<Avatar>())
.child(Story::label("Default"))
@ -19,5 +19,13 @@ impl Render for AvatarStory {
.child(Avatar::uri(
"https://avatars.githubusercontent.com/u/326587?v=4",
))
.child(
Avatar::uri("https://avatars.githubusercontent.com/u/326587?v=4")
.availability_indicator(true),
)
.child(
Avatar::uri("https://avatars.githubusercontent.com/u/326587?v=4")
.availability_indicator(false),
)
}
}

View file

@ -1,145 +1,22 @@
use gpui::{rems, Div, Render};
use gpui::{Div, Render};
use story::Story;
use strum::IntoEnumIterator;
use crate::prelude::*;
use crate::{h_stack, v_stack, Button, Icon, IconPosition, Label};
use crate::{Button, ButtonStyle2};
pub struct ButtonStory;
impl Render for ButtonStory {
type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let states = InteractionState::iter();
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
Story::container()
.child(Story::title_for::<Button>())
.child(
div()
.flex()
.gap_8()
.child(
div()
.child(Story::label("Ghost (Default)"))
.child(h_stack().gap_2().children(states.clone().map(|state| {
v_stack()
.gap_1()
.child(Label::new(state.to_string()).color(Color::Muted))
.child(
Button::new("Label").variant(ButtonVariant::Ghost), // .state(state),
)
})))
.child(Story::label("Ghost Left Icon"))
.child(h_stack().gap_2().children(states.clone().map(|state| {
v_stack()
.gap_1()
.child(Label::new(state.to_string()).color(Color::Muted))
.child(
Button::new("Label")
.variant(ButtonVariant::Ghost)
.icon(Icon::Plus)
.icon_position(IconPosition::Left), // .state(state),
)
})))
.child(Story::label("Ghost Right Icon"))
.child(h_stack().gap_2().children(states.clone().map(|state| {
v_stack()
.gap_1()
.child(Label::new(state.to_string()).color(Color::Muted))
.child(
Button::new("Label")
.variant(ButtonVariant::Ghost)
.icon(Icon::Plus)
.icon_position(IconPosition::Right), // .state(state),
)
}))),
)
.child(
div()
.child(Story::label("Filled"))
.child(h_stack().gap_2().children(states.clone().map(|state| {
v_stack()
.gap_1()
.child(Label::new(state.to_string()).color(Color::Muted))
.child(
Button::new("Label").variant(ButtonVariant::Filled), // .state(state),
)
})))
.child(Story::label("Filled Left Button"))
.child(h_stack().gap_2().children(states.clone().map(|state| {
v_stack()
.gap_1()
.child(Label::new(state.to_string()).color(Color::Muted))
.child(
Button::new("Label")
.variant(ButtonVariant::Filled)
.icon(Icon::Plus)
.icon_position(IconPosition::Left), // .state(state),
)
})))
.child(Story::label("Filled Right Button"))
.child(h_stack().gap_2().children(states.clone().map(|state| {
v_stack()
.gap_1()
.child(Label::new(state.to_string()).color(Color::Muted))
.child(
Button::new("Label")
.variant(ButtonVariant::Filled)
.icon(Icon::Plus)
.icon_position(IconPosition::Right), // .state(state),
)
}))),
)
.child(
div()
.child(Story::label("Fixed With"))
.child(h_stack().gap_2().children(states.clone().map(|state| {
v_stack()
.gap_1()
.child(Label::new(state.to_string()).color(Color::Muted))
.child(
Button::new("Label")
.variant(ButtonVariant::Filled)
// .state(state)
.width(Some(rems(6.).into())),
)
})))
.child(Story::label("Fixed With Left Icon"))
.child(h_stack().gap_2().children(states.clone().map(|state| {
v_stack()
.gap_1()
.child(Label::new(state.to_string()).color(Color::Muted))
.child(
Button::new("Label")
.variant(ButtonVariant::Filled)
// .state(state)
.icon(Icon::Plus)
.icon_position(IconPosition::Left)
.width(Some(rems(6.).into())),
)
})))
.child(Story::label("Fixed With Right Icon"))
.child(h_stack().gap_2().children(states.clone().map(|state| {
v_stack()
.gap_1()
.child(Label::new(state.to_string()).color(Color::Muted))
.child(
Button::new("Label")
.variant(ButtonVariant::Filled)
// .state(state)
.icon(Icon::Plus)
.icon_position(IconPosition::Right)
.width(Some(rems(6.).into())),
)
}))),
),
)
.child(Story::label("Button with `on_click`"))
.child(
Button::new("Label")
.variant(ButtonVariant::Ghost)
.on_click(|_, cx| println!("Button clicked.")),
)
.child(Story::label("Default"))
.child(Button::new("default_filled", "Click me"))
.child(Story::label("Default (Subtle)"))
.child(Button::new("default_subtle", "Click me").style(ButtonStyle2::Subtle))
.child(Story::label("Default (Transparent)"))
.child(Button::new("default_transparent", "Click me").style(ButtonStyle2::Transparent))
}
}

View file

@ -10,11 +10,11 @@ fn build_menu(cx: &mut WindowContext, header: impl Into<SharedString>) -> View<C
ContextMenu::build(cx, |menu, _| {
menu.header(header)
.separator()
.entry("Print current time", |v, cx| {
.entry("Print current time", |cx| {
println!("dispatching PrintCurrentTime action");
cx.dispatch_action(PrintCurrentDate.boxed_clone())
})
.entry("Print best foot", |v, cx| {
.entry("Print best foot", |cx| {
cx.dispatch_action(PrintBestFood.boxed_clone())
})
})
@ -25,7 +25,7 @@ pub struct ContextMenuStory;
impl Render for ContextMenuStory {
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()
.on_action(|_: &PrintCurrentDate, _| {
println!("printing unix time!");

View file

@ -0,0 +1,20 @@
use gpui::{Div, Render};
use story::Story;
use crate::prelude::*;
use crate::Disclosure;
pub struct DisclosureStory;
impl Render for DisclosureStory {
type Element = Div;
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
Story::container()
.child(Story::title_for::<Disclosure>())
.child(Story::label("Toggled"))
.child(Disclosure::new(true))
.child(Story::label("Not Toggled"))
.child(Disclosure::new(false))
}
}

View file

@ -10,7 +10,7 @@ pub struct IconStory;
impl Render for IconStory {
type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
let icons = Icon::iter();
Story::container()

View file

@ -0,0 +1,47 @@
use gpui::{Div, Render};
use story::Story;
use crate::{prelude::*, Tooltip};
use crate::{Icon, IconButton};
pub struct IconButtonStory;
impl Render for IconButtonStory {
type Element = Div;
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::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("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)),
),
)
}
}

View file

@ -1,18 +0,0 @@
use gpui::{Div, Render};
use story::Story;
use crate::prelude::*;
use crate::Input;
pub struct InputStory;
impl Render for InputStory {
type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container()
.child(Story::title_for::<Input>())
.child(Story::label("Default"))
.child(div().flex().child(Input::new("Search")))
}
}

View file

@ -16,7 +16,7 @@ pub fn binding(key: &str) -> gpui::KeyBinding {
impl Render for KeybindingStory {
type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
let all_modifier_permutations = ["ctrl", "alt", "cmd", "shift"].into_iter().permutations(2);
Story::container()

View file

@ -9,7 +9,7 @@ pub struct LabelStory;
impl Render for LabelStory {
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()
.child(Story::title_for::<Label>())
.child(Story::label("Default"))

View file

@ -0,0 +1,38 @@
use gpui::{Div, Render};
use story::Story;
use crate::{prelude::*, ListHeader, ListSeparator, ListSubHeader};
use crate::{List, ListItem};
pub struct ListStory;
impl Render for ListStory {
type Element = Div;
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
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()
.child(ListHeader::new("Fruits"))
.child(ListItem::new("apple").child("Apple"))
.child(ListItem::new("banana").child("Banana"))
.child(ListItem::new("cherry").child("Cherry"))
.child(ListSeparator)
.child(ListHeader::new("Vegetables"))
.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")),
)
}
}

View file

@ -2,18 +2,32 @@ use gpui::{Div, Render};
use story::Story;
use crate::prelude::*;
use crate::ListItem;
use crate::{Icon, ListItem};
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()
.child(Story::title_for::<ListItem>())
.child(Story::label("Default"))
.child(ListItem::new("hello_world").child("Hello, world!"))
.child(Story::label("With left icon"))
.child(
ListItem::new("with_left_icon")
.child("Hello, world!")
.left_icon(Icon::Bell),
)
.child(Story::label("With left avatar"))
.child(
ListItem::new("with_left_avatar")
.child("Hello, world!")
.left_avatar(SharedString::from(
"https://avatars.githubusercontent.com/u/1714999?v=4",
)),
)
.child(Story::label("With `on_click`"))
.child(
ListItem::new("with_on_click")
@ -22,5 +36,13 @@ impl Render for ListItemStory {
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!");
}),
)
}
}

View file

@ -1,41 +0,0 @@
/// Whether the entry is toggleable, and if so, whether it is currently toggled.
///
/// To make an element toggleable, simply add a `Toggle::Toggled(_)` and handle it's cases.
///
/// You can check if an element is toggleable with `.is_toggleable()`
///
/// Possible values:
/// - `Toggle::NotToggleable` - The entry is not toggleable
/// - `Toggle::Toggled(true)` - The entry is toggleable and toggled
/// - `Toggle::Toggled(false)` - The entry is toggleable and not toggled
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Toggle {
NotToggleable,
Toggled(bool),
}
impl Toggle {
/// Returns true if the entry is toggled (or is not toggleable.)
///
/// As element that isn't toggleable is always "expanded" or "enabled"
/// returning true in that case makes sense.
pub fn is_toggled(&self) -> bool {
match self {
Self::Toggled(false) => false,
_ => true,
}
}
pub fn is_toggleable(&self) -> bool {
match self {
Self::Toggled(_) => true,
_ => false,
}
}
}
impl From<bool> for Toggle {
fn from(toggled: bool) -> Self {
Toggle::Toggled(toggled)
}
}

View file

@ -1,6 +1,6 @@
use gpui::{overlay, Action, AnyView, IntoElement, Overlay, Render, VisualContext};
use settings2::Settings;
use theme2::{ActiveTheme, ThemeSettings};
use settings::Settings;
use theme::ThemeSettings;
use crate::prelude::*;
use crate::{h_stack, v_stack, Color, KeyBinding, Label, LabelSize, StyledExt};
@ -13,7 +13,7 @@ pub struct Tooltip {
impl Tooltip {
pub fn text(title: impl Into<SharedString>, cx: &mut WindowContext) -> AnyView {
cx.build_view(|cx| Self {
cx.build_view(|_cx| Self {
title: title.into(),
meta: None,
key_binding: None,

View file

@ -0,0 +1,5 @@
/// A trait for elements that can be disabled.
pub trait Disableable {
/// Sets whether the element is disabled.
fn disabled(self, disabled: bool) -> Self;
}

10
crates/ui2/src/fixed.rs Normal file
View file

@ -0,0 +1,10 @@
use gpui::DefiniteLength;
/// A trait for elements that have a fixed with.
pub trait FixedWidth {
/// Sets the width of the element.
fn width(self, width: DefiniteLength) -> Self;
/// Sets the element's width to the full width of its container.
fn full_width(self) -> Self;
}

View file

@ -1,70 +1,12 @@
pub use gpui::prelude::*;
pub use gpui::{
div, Element, ElementId, InteractiveElement, ParentElement, RenderOnce, SharedString, Styled,
ViewContext, WindowContext,
};
pub use crate::StyledExt;
pub use crate::{ButtonVariant, Color};
pub use theme2::ActiveTheme;
use strum::EnumIter;
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
pub enum IconSide {
#[default]
Left,
Right,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, EnumIter)]
pub enum OverflowStyle {
Hidden,
Wrap,
}
#[derive(Default, PartialEq, Copy, Clone, EnumIter, strum::Display)]
pub enum InteractionState {
/// An element that is enabled and not hovered, active, focused, or disabled.
///
/// This is often referred to as the "default" state.
#[default]
Enabled,
/// An element that is hovered.
Hovered,
/// An element has an active mouse down or touch start event on it.
Active,
/// An element that is focused using the keyboard.
Focused,
/// An element that is disabled.
Disabled,
/// A toggleable element that is selected, like the active button in a
/// button toggle group.
Selected,
}
impl InteractionState {
pub fn if_enabled(&self, enabled: bool) -> Self {
if enabled {
*self
} else {
InteractionState::Disabled
}
}
}
#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)]
pub enum Selection {
#[default]
Unselected,
Indeterminate,
Selected,
}
impl Selection {
pub fn inverse(&self) -> Self {
match self {
Self::Unselected | Self::Indeterminate => Self::Selected,
Self::Selected => Self::Unselected,
}
}
}
pub use crate::clickable::*;
pub use crate::disableable::*;
pub use crate::fixed::*;
pub use crate::selectable::*;
pub use crate::{ButtonCommon, Color, StyledExt};
pub use theme::ActiveTheme;

View file

@ -0,0 +1,22 @@
/// A trait for elements that can be selected.
pub trait Selectable {
/// Sets whether the element is selected.
fn selected(self, selected: bool) -> Self;
}
#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)]
pub enum Selection {
#[default]
Unselected,
Indeterminate,
Selected,
}
impl Selection {
pub fn inverse(&self) -> Self {
match self {
Self::Unselected | Self::Indeterminate => Self::Selected,
Self::Selected => Self::Unselected,
}
}
}

View file

@ -1,14 +1,12 @@
use gpui::SharedString;
use gpui::{ImageSource, SharedString};
use crate::Icon;
#[derive(Debug, Clone)]
/// A slot utility that provides a way to to pass either
/// an icon or an image to a component.
///
/// Can be filled with a []
#[derive(Debug, Clone)]
pub enum GraphicSlot {
Icon(Icon),
Avatar(SharedString),
Avatar(ImageSource),
PublicActor(SharedString),
}

View file

@ -1,6 +1,6 @@
use gpui::{px, Styled, WindowContext};
use theme2::ActiveTheme;
use crate::prelude::*;
use crate::{ElevationIndex, UITextSize};
fn elevated<E: Styled>(this: E, cx: &mut WindowContext, index: ElevationIndex) -> E {

View file

@ -1,7 +1,7 @@
use gpui::{Hsla, WindowContext};
use theme2::ActiveTheme;
use theme::ActiveTheme;
#[derive(Default, PartialEq, Copy, Clone)]
#[derive(Debug, Default, PartialEq, Copy, Clone)]
pub enum Color {
#[default]
Default,

View file

@ -11,16 +11,24 @@
#![doc = include_str!("../docs/hello-world.md")]
#![doc = include_str!("../docs/building-ui.md")]
#![doc = include_str!("../docs/todo.md")]
// TODO: Fix warnings instead of supressing.
#![allow(dead_code, unused_variables)]
mod clickable;
mod components;
mod disableable;
mod fixed;
pub mod prelude;
mod selectable;
mod slot;
mod styled_ext;
mod styles;
pub mod utils;
pub use clickable::*;
pub use components::*;
pub use disableable::*;
pub use fixed::*;
pub use prelude::*;
pub use selectable::*;
pub use slot::*;
pub use styled_ext::*;
pub use styles::*;

View file

@ -16,55 +16,54 @@ fn distance_in_seconds(date: NaiveDateTime, base_date: NaiveDateTime) -> i64 {
fn distance_string(distance: i64, include_seconds: bool, add_suffix: bool) -> String {
let suffix = if distance < 0 { " from now" } else { " ago" };
let d = distance.abs();
let distance = distance.abs();
let minutes = d / 60;
let hours = d / 3600;
let days = d / 86400;
let months = d / 2592000;
let years = d / 31536000;
let minutes = distance / 60;
let hours = distance / 3_600;
let days = distance / 86_400;
let months = distance / 2_592_000;
let string = if d < 5 && include_seconds {
let string = if distance < 5 && include_seconds {
"less than 5 seconds".to_string()
} else if d < 10 && include_seconds {
} else if distance < 10 && include_seconds {
"less than 10 seconds".to_string()
} else if d < 20 && include_seconds {
} else if distance < 20 && include_seconds {
"less than 20 seconds".to_string()
} else if d < 40 && include_seconds {
} else if distance < 40 && include_seconds {
"half a minute".to_string()
} else if d < 60 && include_seconds {
} else if distance < 60 && include_seconds {
"less than a minute".to_string()
} else if d < 90 && include_seconds {
} else if distance < 90 && include_seconds {
"1 minute".to_string()
} else if d < 30 {
} else if distance < 30 {
"less than a minute".to_string()
} else if d < 90 {
} else if distance < 90 {
"1 minute".to_string()
} else if d < 2700 {
} else if distance < 2_700 {
format!("{} minutes", minutes)
} else if d < 5400 {
} else if distance < 5_400 {
"about 1 hour".to_string()
} else if d < 86400 {
} else if distance < 86_400 {
format!("about {} hours", hours)
} else if d < 172800 {
} else if distance < 172_800 {
"1 day".to_string()
} else if d < 2592000 {
} else if distance < 2_592_000 {
format!("{} days", days)
} else if d < 5184000 {
} else if distance < 5_184_000 {
"about 1 month".to_string()
} else if d < 7776000 {
} else if distance < 7_776_000 {
"about 2 months".to_string()
} else if d < 31540000 {
} else if distance < 31_540_000 {
format!("{} months", months)
} else if d < 39425000 {
} else if distance < 39_425_000 {
"about 1 year".to_string()
} else if d < 55195000 {
} else if distance < 55_195_000 {
"over 1 year".to_string()
} else if d < 63080000 {
} else if distance < 63_080_000 {
"almost 2 years".to_string()
} else {
let years = d / 31536000;
let remaining_months = (d % 31536000) / 2592000;
let years = distance / 31_536_000;
let remaining_months = (distance % 31_536_000) / 2_592_000;
if remaining_months < 3 {
format!("about {} years", years)
@ -76,7 +75,7 @@ fn distance_string(distance: i64, include_seconds: bool, add_suffix: bool) -> St
};
if add_suffix {
return format!("{}{}", string, suffix);
format!("{}{}", string, suffix)
} else {
string
}