Merge branch 'main' into refine-keybindings

This commit is contained in:
Nate Butler
2023-11-28 14:00:09 -05:00
250 changed files with 16329 additions and 14656 deletions
+37 -41
View File
@@ -1,27 +1,25 @@
use gpui::img;
use std::sync::Arc;
use crate::prelude::*;
use gpui::{img, ImageData, ImageSource, Img, IntoElement};
#[derive(Component)]
#[derive(Debug, Default, PartialEq, Clone)]
pub enum Shape {
#[default]
Circle,
RoundedRectangle,
}
#[derive(IntoElement)]
pub struct Avatar {
src: SharedString,
src: ImageSource,
shape: Shape,
}
impl Avatar {
pub fn new(src: impl Into<SharedString>) -> Self {
Self {
src: src.into(),
shape: Shape::Circle,
}
}
impl RenderOnce for Avatar {
type Rendered = Img;
pub fn shape(mut self, shape: Shape) -> Self {
self.shape = shape;
self
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
fn render(self, _: &mut WindowContext) -> Self::Rendered {
let mut img = img();
if self.shape == Shape::Circle {
@@ -30,37 +28,35 @@ impl Avatar {
img = img.rounded_md();
}
img.uri(self.src.clone())
img.source(self.src.clone())
.size_4()
// todo!(Pull the avatar fallback background from the theme.)
.bg(gpui::red())
}
}
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use super::*;
use crate::Story;
use gpui::{Div, Render};
pub struct AvatarStory;
impl Render for AvatarStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container(cx)
.child(Story::title_for::<_, Avatar>(cx))
.child(Story::label(cx, "Default"))
.child(Avatar::new(
"https://avatars.githubusercontent.com/u/1714999?v=4",
))
.child(Avatar::new(
"https://avatars.githubusercontent.com/u/326587?v=4",
))
impl Avatar {
pub fn uri(src: impl Into<SharedString>) -> Self {
Self {
src: src.into().into(),
shape: Shape::Circle,
}
}
pub fn data(src: Arc<ImageData>) -> Self {
Self {
src: src.into(),
shape: Shape::Circle,
}
}
pub fn source(src: ImageSource) -> Self {
Self {
src,
shape: Shape::Circle,
}
}
pub fn shape(mut self, shape: Shape) -> Self {
self.shape = shape;
self
}
}
+95 -264
View File
@@ -1,25 +1,28 @@
use std::sync::Arc;
use std::rc::Rc;
use gpui::{DefiniteLength, Hsla, MouseButton, StatefulInteractiveComponent, WindowContext};
use gpui::{
DefiniteLength, Div, Hsla, IntoElement, MouseButton, MouseDownEvent,
StatefulInteractiveElement, WindowContext,
};
use crate::prelude::*;
use crate::{h_stack, Icon, IconButton, IconElement, Label, LineHeightStyle, TextColor};
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<V: 'static> {
Button(Button<V>),
IconButton(IconButton<V>),
pub enum ButtonOrIconButton {
Button(Button),
IconButton(IconButton),
}
impl<V: 'static> From<Button<V>> for ButtonOrIconButton<V> {
fn from(value: Button<V>) -> Self {
impl From<Button> for ButtonOrIconButton {
fn from(value: Button) -> Self {
Self::Button(value)
}
}
impl<V: 'static> From<IconButton<V>> for ButtonOrIconButton<V> {
fn from(value: IconButton<V>) -> Self {
impl From<IconButton> for ButtonOrIconButton {
fn from(value: IconButton) -> Self {
Self::IconButton(value)
}
}
@@ -61,38 +64,74 @@ impl ButtonVariant {
}
}
pub type ClickHandler<V> = Arc<dyn Fn(&mut V, &mut ViewContext<V>)>;
struct ButtonHandlers<V: 'static> {
click: Option<ClickHandler<V>>,
}
unsafe impl<S> Send for ButtonHandlers<S> {}
unsafe impl<S> Sync for ButtonHandlers<S> {}
impl<V: 'static> Default for ButtonHandlers<V> {
fn default() -> Self {
Self { click: None }
}
}
#[derive(Component)]
pub struct Button<V: 'static> {
#[derive(IntoElement)]
pub struct Button {
disabled: bool,
handlers: ButtonHandlers<V>,
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<TextColor>,
color: Option<Color>,
}
impl<V: 'static> Button<V> {
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,
handlers: ButtonHandlers::default(),
click_handler: None,
icon: None,
icon_position: None,
label: label.into(),
@@ -129,8 +168,11 @@ impl<V: 'static> Button<V> {
self
}
pub fn on_click(mut self, handler: ClickHandler<V>) -> Self {
self.handlers.click = Some(handler);
pub fn on_click(
mut self,
handler: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
) -> Self {
self.click_handler = Some(Rc::new(handler));
self
}
@@ -139,14 +181,14 @@ impl<V: 'static> Button<V> {
self
}
pub fn color(mut self, color: Option<TextColor>) -> Self {
pub fn color(mut self, color: Option<Color>) -> Self {
self.color = color;
self
}
pub fn label_color(&self, color: Option<TextColor>) -> TextColor {
pub fn label_color(&self, color: Option<Color>) -> Color {
if self.disabled {
TextColor::Disabled
Color::Disabled
} else if let Some(color) = color {
color
} else {
@@ -154,249 +196,38 @@ impl<V: 'static> Button<V> {
}
}
fn render_label(&self, color: TextColor) -> Label {
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: TextColor) -> Option<IconElement> {
fn render_icon(&self, icon_color: Color) -> Option<IconElement> {
self.icon.map(|i| IconElement::new(i).color(icon_color))
}
}
pub fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
let (icon_color, label_color) = match (self.disabled, self.color) {
(true, _) => (TextColor::Disabled, TextColor::Disabled),
(_, None) => (TextColor::Default, TextColor::Default),
(_, Some(color)) => (TextColor::from(color), color),
};
#[derive(IntoElement)]
pub struct ButtonGroup {
buttons: Vec<Button>,
}
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)));
impl RenderOnce for ButtonGroup {
type Rendered = Div;
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)),
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));
}
if let Some(width) = self.width {
button = button.w(width).justify_center();
}
if let Some(click_handler) = self.handlers.click.clone() {
button = button.on_mouse_down(MouseButton::Left, move |state, event, cx| {
click_handler(state, cx);
});
}
button
group
}
}
#[derive(Component)]
pub struct ButtonGroup<V: 'static> {
buttons: Vec<Button<V>>,
}
impl<V: 'static> ButtonGroup<V> {
pub fn new(buttons: Vec<Button<V>>) -> Self {
impl ButtonGroup {
pub fn new(buttons: Vec<Button>) -> Self {
Self { buttons }
}
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
let mut el = h_stack().text_ui();
for button in self.buttons {
el = el.child(button.render(_view, cx));
}
el
}
}
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use super::*;
use crate::{h_stack, v_stack, Story, TextColor};
use gpui::{rems, Div, Render};
use strum::IntoEnumIterator;
pub struct ButtonStory;
impl Render for ButtonStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let states = InteractionState::iter();
Story::container(cx)
.child(Story::title_for::<_, Button<Self>>(cx))
.child(
div()
.flex()
.gap_8()
.child(
div()
.child(Story::label(cx, "Ghost (Default)"))
.child(h_stack().gap_2().children(states.clone().map(|state| {
v_stack()
.gap_1()
.child(
Label::new(state.to_string()).color(TextColor::Muted),
)
.child(
Button::new("Label").variant(ButtonVariant::Ghost), // .state(state),
)
})))
.child(Story::label(cx, "Ghost – Left Icon"))
.child(h_stack().gap_2().children(states.clone().map(|state| {
v_stack()
.gap_1()
.child(
Label::new(state.to_string()).color(TextColor::Muted),
)
.child(
Button::new("Label")
.variant(ButtonVariant::Ghost)
.icon(Icon::Plus)
.icon_position(IconPosition::Left), // .state(state),
)
})))
.child(Story::label(cx, "Ghost – Right Icon"))
.child(h_stack().gap_2().children(states.clone().map(|state| {
v_stack()
.gap_1()
.child(
Label::new(state.to_string()).color(TextColor::Muted),
)
.child(
Button::new("Label")
.variant(ButtonVariant::Ghost)
.icon(Icon::Plus)
.icon_position(IconPosition::Right), // .state(state),
)
}))),
)
.child(
div()
.child(Story::label(cx, "Filled"))
.child(h_stack().gap_2().children(states.clone().map(|state| {
v_stack()
.gap_1()
.child(
Label::new(state.to_string()).color(TextColor::Muted),
)
.child(
Button::new("Label").variant(ButtonVariant::Filled), // .state(state),
)
})))
.child(Story::label(cx, "Filled – Left Button"))
.child(h_stack().gap_2().children(states.clone().map(|state| {
v_stack()
.gap_1()
.child(
Label::new(state.to_string()).color(TextColor::Muted),
)
.child(
Button::new("Label")
.variant(ButtonVariant::Filled)
.icon(Icon::Plus)
.icon_position(IconPosition::Left), // .state(state),
)
})))
.child(Story::label(cx, "Filled – Right Button"))
.child(h_stack().gap_2().children(states.clone().map(|state| {
v_stack()
.gap_1()
.child(
Label::new(state.to_string()).color(TextColor::Muted),
)
.child(
Button::new("Label")
.variant(ButtonVariant::Filled)
.icon(Icon::Plus)
.icon_position(IconPosition::Right), // .state(state),
)
}))),
)
.child(
div()
.child(Story::label(cx, "Fixed With"))
.child(h_stack().gap_2().children(states.clone().map(|state| {
v_stack()
.gap_1()
.child(
Label::new(state.to_string()).color(TextColor::Muted),
)
.child(
Button::new("Label")
.variant(ButtonVariant::Filled)
// .state(state)
.width(Some(rems(6.).into())),
)
})))
.child(Story::label(cx, "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(TextColor::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(cx, "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(TextColor::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(cx, "Button with `on_click`"))
.child(
Button::new("Label")
.variant(ButtonVariant::Ghost)
.on_click(Arc::new(|_view, _cx| println!("Button clicked."))),
)
}
}
}
+154 -94
View File
@@ -1,48 +1,28 @@
use gpui::{div, prelude::*, Component, ElementId, Styled, ViewContext};
use std::sync::Arc;
use gpui::{div, prelude::*, Div, Element, ElementId, IntoElement, Styled, WindowContext};
use theme2::ActiveTheme;
use crate::{Icon, IconElement, Selection, TextColor};
use crate::{Color, Icon, IconElement, Selection};
pub type CheckHandler<V> = Arc<dyn Fn(Selection, &mut V, &mut ViewContext<V>) + Send + Sync>;
pub type CheckHandler = Box<dyn Fn(&Selection, &mut WindowContext) + 'static>;
/// # Checkbox
///
/// Checkboxes are used for multiple choices, not for mutually exclusive choices.
/// Each checkbox works independently from other checkboxes in the list,
/// therefore checking an additional box does not affect any other selections.
#[derive(Component)]
pub struct Checkbox<V: 'static> {
#[derive(IntoElement)]
pub struct Checkbox {
id: ElementId,
checked: Selection,
disabled: bool,
on_click: Option<CheckHandler<V>>,
on_click: Option<CheckHandler>,
}
impl<V: 'static> Checkbox<V> {
pub fn new(id: impl Into<ElementId>, checked: Selection) -> Self {
Self {
id: id.into(),
checked,
disabled: false,
on_click: None,
}
}
impl RenderOnce for Checkbox {
type Rendered = gpui::Stateful<Div>;
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn on_click(
mut self,
handler: impl 'static + Fn(Selection, &mut V, &mut ViewContext<V>) + Send + Sync,
) -> Self {
self.on_click = Some(Arc::new(handler));
self
}
pub fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
let group_id = format!("checkbox_group_{:?}", self.id);
let icon = match self.checked {
@@ -54,9 +34,9 @@ impl<V: 'static> Checkbox<V> {
.color(
// If the checkbox is disabled we change the color of the icon.
if self.disabled {
TextColor::Disabled
Color::Disabled
} else {
TextColor::Selected
Color::Selected
},
),
)
@@ -69,9 +49,9 @@ impl<V: 'static> Checkbox<V> {
.color(
// If the checkbox is disabled we change the color of the icon.
if self.disabled {
TextColor::Disabled
Color::Disabled
} else {
TextColor::Selected
Color::Selected
},
),
)
@@ -157,69 +137,149 @@ impl<V: 'static> Checkbox<V> {
)
.when_some(
self.on_click.filter(|_| !self.disabled),
|this, on_click| {
this.on_click(move |view, _, cx| on_click(self.checked.inverse(), view, cx))
},
|this, on_click| this.on_click(move |_, cx| on_click(&self.checked.inverse(), cx)),
)
}
}
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use super::*;
use crate::{h_stack, Story};
use gpui::{Div, Render};
pub struct CheckboxStory;
impl Render for CheckboxStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container(cx)
.child(Story::title_for::<_, Checkbox<Self>>(cx))
.child(Story::label(cx, "Default"))
.child(
h_stack()
.p_2()
.gap_2()
.rounded_md()
.border()
.border_color(cx.theme().colors().border)
.child(Checkbox::new("checkbox-enabled", Selection::Unselected))
.child(Checkbox::new(
"checkbox-intermediate",
Selection::Indeterminate,
))
.child(Checkbox::new("checkbox-selected", Selection::Selected)),
)
.child(Story::label(cx, "Disabled"))
.child(
h_stack()
.p_2()
.gap_2()
.rounded_md()
.border()
.border_color(cx.theme().colors().border)
.child(
Checkbox::new("checkbox-disabled", Selection::Unselected)
.disabled(true),
)
.child(
Checkbox::new(
"checkbox-disabled-intermediate",
Selection::Indeterminate,
)
.disabled(true),
)
.child(
Checkbox::new("checkbox-disabled-selected", Selection::Selected)
.disabled(true),
),
)
impl Checkbox {
pub fn new(id: impl Into<ElementId>, checked: Selection) -> Self {
Self {
id: id.into(),
checked,
disabled: false,
on_click: None,
}
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn on_click(
mut self,
handler: impl 'static + Fn(&Selection, &mut WindowContext) + Send + Sync,
) -> Self {
self.on_click = Some(Box::new(handler));
self
}
pub fn render(self, cx: &mut WindowContext) -> impl Element {
let group_id = format!("checkbox_group_{:?}", self.id);
let icon = match self.checked {
// When selected, we show a checkmark.
Selection::Selected => {
Some(
IconElement::new(Icon::Check)
.size(crate::IconSize::Small)
.color(
// If the checkbox is disabled we change the color of the icon.
if self.disabled {
Color::Disabled
} else {
Color::Selected
},
),
)
}
// In an indeterminate state, we show a dash.
Selection::Indeterminate => {
Some(
IconElement::new(Icon::Dash)
.size(crate::IconSize::Small)
.color(
// If the checkbox is disabled we change the color of the icon.
if self.disabled {
Color::Disabled
} else {
Color::Selected
},
),
)
}
// When unselected, we show nothing.
Selection::Unselected => None,
};
// A checkbox could be in an indeterminate state,
// for example the indeterminate state could represent:
// - a group of options of which only some are selected
// - an enabled option that is no longer available
// - a previously agreed to license that has been updated
//
// For the sake of styles we treat the indeterminate state as selected,
// but it's icon will be different.
let selected =
self.checked == Selection::Selected || self.checked == Selection::Indeterminate;
// We could use something like this to make the checkbox background when selected:
//
// ~~~rust
// ...
// .when(selected, |this| {
// this.bg(cx.theme().colors().element_selected)
// })
// ~~~
//
// But we use a match instead here because the checkbox might be disabled,
// and it could be disabled _while_ it is selected, as well as while it is not selected.
let (bg_color, border_color) = match (self.disabled, selected) {
(true, _) => (
cx.theme().colors().ghost_element_disabled,
cx.theme().colors().border_disabled,
),
(false, true) => (
cx.theme().colors().element_selected,
cx.theme().colors().border,
),
(false, false) => (
cx.theme().colors().element_background,
cx.theme().colors().border,
),
};
div()
.id(self.id)
// Rather than adding `px_1()` to add some space around the checkbox,
// we use a larger parent element to create a slightly larger
// click area for the checkbox.
.size_5()
// Because we've enlarged the click area, we need to create a
// `group` to pass down interactivity events to the checkbox.
.group(group_id.clone())
.child(
div()
.flex()
// This prevent the flex element from growing
// or shrinking in response to any size changes
.flex_none()
// The combo of `justify_center()` and `items_center()`
// is used frequently to center elements in a flex container.
//
// We use this to center the icon in the checkbox.
.justify_center()
.items_center()
.m_1()
.size_4()
.rounded_sm()
.bg(bg_color)
.border()
.border_color(border_color)
// We only want the interactivity states to fire when we
// are in a checkbox that isn't disabled.
.when(!self.disabled, |this| {
// Here instead of `hover()` we use `group_hover()`
// to pass it the group id.
this.group_hover(group_id.clone(), |el| {
el.bg(cx.theme().colors().element_hover)
})
})
.children(icon),
)
.when_some(
self.on_click.filter(|_| !self.disabled),
|this, on_click| this.on_click(move |_, cx| on_click(&self.checked.inverse(), cx)),
)
}
}
+96 -224
View File
@@ -1,47 +1,42 @@
use std::cell::RefCell;
use std::rc::Rc;
use crate::prelude::*;
use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHeader};
use crate::{prelude::*, v_stack, Label, List};
use crate::{ListItem, ListSeparator, ListSubHeader};
use gpui::{
overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, DispatchPhase, Div,
EventEmitter, FocusHandle, FocusableView, LayoutId, ManagedView, Manager, MouseButton,
MouseDownEvent, Pixels, Point, Render, View, VisualContext, WeakView,
overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, ClickEvent, DismissEvent,
DispatchPhase, Div, EventEmitter, FocusHandle, FocusableView, IntoElement, LayoutId,
ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render, View, VisualContext,
};
pub enum ContextMenuItem<V> {
Separator(ListSeparator),
Header(ListSubHeader),
Entry(
ListEntry<ContextMenu<V>>,
Rc<dyn Fn(&mut V, &mut ViewContext<V>)>,
),
pub enum ContextMenuItem {
Separator,
Header(SharedString),
Entry(SharedString, Rc<dyn Fn(&ClickEvent, &mut WindowContext)>),
}
pub struct ContextMenu<V> {
items: Vec<ContextMenuItem<V>>,
pub struct ContextMenu {
items: Vec<ContextMenuItem>,
focus_handle: FocusHandle,
handle: WeakView<V>,
}
impl<V: Render> FocusableView for ContextMenu<V> {
impl FocusableView for ContextMenu {
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
impl<V: Render> EventEmitter<Manager> for ContextMenu<V> {}
impl EventEmitter<DismissEvent> for ContextMenu {}
impl<V: Render> ContextMenu<V> {
impl ContextMenu {
pub fn build(
cx: &mut ViewContext<V>,
f: impl FnOnce(Self, &mut ViewContext<Self>) -> Self,
cx: &mut WindowContext,
f: impl FnOnce(Self, &mut WindowContext) -> Self,
) -> View<Self> {
let handle = cx.view().downgrade();
// let handle = cx.view().downgrade();
cx.build_view(|cx| {
f(
Self {
handle,
items: Default::default(),
focus_handle: cx.focus_handle(),
},
@@ -51,105 +46,103 @@ impl<V: Render> ContextMenu<V> {
}
pub fn header(mut self, title: impl Into<SharedString>) -> Self {
self.items
.push(ContextMenuItem::Header(ListSubHeader::new(title)));
self.items.push(ContextMenuItem::Header(title.into()));
self
}
pub fn separator(mut self) -> Self {
self.items.push(ContextMenuItem::Separator(ListSeparator));
self.items.push(ContextMenuItem::Separator);
self
}
pub fn entry(
mut self,
view: ListEntry<Self>,
on_click: impl Fn(&mut V, &mut ViewContext<V>) + 'static,
label: impl Into<SharedString>,
on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
) -> Self {
self.items
.push(ContextMenuItem::Entry(view, Rc::new(on_click)));
.push(ContextMenuItem::Entry(label.into(), Rc::new(on_click)));
self
}
pub fn action(self, view: ListEntry<Self>, action: Box<dyn Action>) -> Self {
pub fn action(self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
// todo: add the keybindings to the list entry
self.entry(view, move |_, cx| cx.dispatch_action(action.boxed_clone()))
self.entry(label.into(), move |_, cx| {
cx.dispatch_action(action.boxed_clone())
})
}
pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
// todo!()
cx.emit(Manager::Dismiss);
cx.emit(DismissEvent::Dismiss);
}
pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
cx.emit(Manager::Dismiss);
cx.emit(DismissEvent::Dismiss);
}
}
impl<V: Render> Render for ContextMenu<V> {
type Element = Div<Self>;
impl Render for ContextMenu {
type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
div().elevation_2(cx).flex().flex_row().child(
v_stack()
.min_w(px(200.))
.track_focus(&self.focus_handle)
.on_mouse_down_out(|this: &mut Self, _, cx| this.cancel(&Default::default(), cx))
.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_action(ContextMenu::confirm)
.on_action(ContextMenu::cancel)
.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(
self.items
.iter()
.map(|item| match item {
ContextMenuItem::Separator(separator) => {
ListItem::Separator(separator.clone())
}
ContextMenuItem::Header(header) => ListItem::Header(header.clone()),
ContextMenuItem::Entry(entry, callback) => {
let callback = callback.clone();
let handle = self.handle.clone();
ListItem::Entry(entry.clone().on_click(move |this, cx| {
handle.update(cx, |view, cx| callback(view, cx)).ok();
cx.emit(Manager::Dismiss);
}))
}
})
.collect(),
)),
.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));
ListItem::new(entry.clone())
.child(Label::new(entry.clone()))
.on_click(move |event, cx| {
callback(event, cx);
dismiss(event, cx)
})
.into_any_element()
}
})),
),
)
}
}
pub struct MenuHandle<V: 'static, M: ManagedView> {
id: Option<ElementId>,
child_builder: Option<Box<dyn FnOnce(bool) -> AnyElement<V> + 'static>>,
menu_builder: Option<Rc<dyn Fn(&mut V, &mut ViewContext<V>) -> View<M> + 'static>>,
pub struct MenuHandle<M: ManagedView> {
id: ElementId,
child_builder: Option<Box<dyn FnOnce(bool) -> AnyElement + 'static>>,
menu_builder: Option<Rc<dyn Fn(&mut WindowContext) -> View<M> + 'static>>,
anchor: Option<AnchorCorner>,
attach: Option<AnchorCorner>,
}
impl<V: 'static, M: ManagedView> MenuHandle<V, M> {
pub fn id(mut self, id: impl Into<ElementId>) -> Self {
self.id = Some(id.into());
self
}
pub fn menu(mut self, f: impl Fn(&mut V, &mut ViewContext<V>) -> View<M> + 'static) -> Self {
impl<M: ManagedView> MenuHandle<M> {
pub fn menu(mut self, f: impl Fn(&mut WindowContext) -> View<M> + 'static) -> Self {
self.menu_builder = Some(Rc::new(f));
self
}
pub fn child<R: Component<V>>(mut self, f: impl FnOnce(bool) -> R + 'static) -> Self {
self.child_builder = Some(Box::new(|b| f(b).render()));
pub fn child<R: IntoElement>(mut self, f: impl FnOnce(bool) -> R + 'static) -> Self {
self.child_builder = Some(Box::new(|b| f(b).into_element().into_any()));
self
}
@@ -167,9 +160,9 @@ impl<V: 'static, M: ManagedView> MenuHandle<V, M> {
}
}
pub fn menu_handle<V: 'static, M: ManagedView>() -> MenuHandle<V, M> {
pub fn menu_handle<M: ManagedView>(id: impl Into<ElementId>) -> MenuHandle<M> {
MenuHandle {
id: None,
id: id.into(),
child_builder: None,
menu_builder: None,
anchor: None,
@@ -177,26 +170,21 @@ pub fn menu_handle<V: 'static, M: ManagedView>() -> MenuHandle<V, M> {
}
}
pub struct MenuHandleState<V, M> {
pub struct MenuHandleState<M> {
menu: Rc<RefCell<Option<View<M>>>>,
position: Rc<RefCell<Point<Pixels>>>,
child_layout_id: Option<LayoutId>,
child_element: Option<AnyElement<V>>,
menu_element: Option<AnyElement<V>>,
child_element: Option<AnyElement>,
menu_element: Option<AnyElement>,
}
impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
type ElementState = MenuHandleState<V, M>;
fn element_id(&self) -> Option<gpui::ElementId> {
Some(self.id.clone().expect("menu_handle must have an id()"))
}
impl<M: ManagedView> Element for MenuHandle<M> {
type State = MenuHandleState<M>;
fn layout(
&mut self,
view_state: &mut V,
element_state: Option<Self::ElementState>,
cx: &mut crate::ViewContext<V>,
) -> (gpui::LayoutId, Self::ElementState) {
element_state: Option<Self::State>,
cx: &mut WindowContext,
) -> (gpui::LayoutId, Self::State) {
let (menu, position) = if let Some(element_state) = element_state {
(element_state.menu, element_state.position)
} else {
@@ -206,15 +194,15 @@ impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
let mut menu_layout_id = None;
let menu_element = menu.borrow_mut().as_mut().map(|menu| {
let mut overlay = overlay::<V>().snap_to_window();
let mut overlay = overlay().snap_to_window();
if let Some(anchor) = self.anchor {
overlay = overlay.anchor(anchor);
}
overlay = overlay.position(*position.borrow());
let mut view = overlay.child(menu.clone()).render();
menu_layout_id = Some(view.layout(view_state, cx));
view
let mut element = overlay.child(menu.clone()).into_any();
menu_layout_id = Some(element.layout(cx));
element
});
let mut child_element = self
@@ -224,7 +212,7 @@ impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
let child_layout_id = child_element
.as_mut()
.map(|child_element| child_element.layout(view_state, cx));
.map(|child_element| child_element.layout(cx));
let layout_id = cx.request_layout(
&gpui::Style::default(),
@@ -244,22 +232,21 @@ impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
}
fn paint(
&mut self,
self,
bounds: Bounds<gpui::Pixels>,
view_state: &mut V,
element_state: &mut Self::ElementState,
cx: &mut crate::ViewContext<V>,
element_state: &mut Self::State,
cx: &mut WindowContext,
) {
if let Some(child) = element_state.child_element.as_mut() {
child.paint(view_state, cx);
if let Some(child) = element_state.child_element.take() {
child.paint(cx);
}
if let Some(menu) = element_state.menu_element.as_mut() {
menu.paint(view_state, cx);
if let Some(menu) = element_state.menu_element.take() {
menu.paint(cx);
return;
}
let Some(builder) = self.menu_builder.clone() else {
let Some(builder) = self.menu_builder else {
return;
};
let menu = element_state.menu.clone();
@@ -267,7 +254,7 @@ impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
let attach = self.attach.clone();
let child_layout_id = element_state.child_layout_id.clone();
cx.on_mouse_event(move |view_state, event: &MouseDownEvent, phase, cx| {
cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
if phase == DispatchPhase::Bubble
&& event.button == MouseButton::Right
&& bounds.contains_point(&event.position)
@@ -275,10 +262,10 @@ impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
cx.stop_propagation();
cx.prevent_default();
let new_menu = (builder)(view_state, cx);
let new_menu = (builder)(cx);
let menu2 = menu.clone();
cx.subscribe(&new_menu, move |this, modal, e, cx| match e {
&Manager::Dismiss => {
cx.subscribe(&new_menu, move |modal, e, cx| match e {
&DismissEvent::Dismiss => {
*menu2.borrow_mut() = None;
cx.notify();
}
@@ -300,129 +287,14 @@ impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
}
}
impl<V: 'static, M: ManagedView> Component<V> for MenuHandle<V, M> {
fn render(self) -> AnyElement<V> {
AnyElement::new(self)
}
}
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use super::*;
use crate::story::Story;
use gpui::{actions, Div, Render};
impl<M: ManagedView> IntoElement for MenuHandle<M> {
type Element = Self;
actions!(PrintCurrentDate, PrintBestFood);
fn build_menu<V: Render>(
cx: &mut ViewContext<V>,
header: impl Into<SharedString>,
) -> View<ContextMenu<V>> {
let handle = cx.view().clone();
ContextMenu::build(cx, |menu, _| {
menu.header(header)
.separator()
.entry(ListEntry::new(Label::new("Print current time")), |v, cx| {
println!("dispatching PrintCurrentTime action");
cx.dispatch_action(PrintCurrentDate.boxed_clone())
})
.entry(ListEntry::new(Label::new("Print best food")), |v, cx| {
cx.dispatch_action(PrintBestFood.boxed_clone())
})
})
fn element_id(&self) -> Option<gpui::ElementId> {
Some(self.id.clone())
}
pub struct ContextMenuStory;
impl Render for ContextMenuStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container(cx)
.on_action(|_, _: &PrintCurrentDate, _| {
println!("printing unix time!");
if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() {
println!("Current Unix time is {:?}", unix_time.as_secs());
}
})
.on_action(|_, _: &PrintBestFood, _| {
println!("burrito");
})
.flex()
.flex_row()
.justify_between()
.child(
div()
.flex()
.flex_col()
.justify_between()
.child(
menu_handle()
.id("test2")
.child(|is_open| {
Label::new(if is_open {
"TOP LEFT"
} else {
"RIGHT CLICK ME"
})
.render()
})
.menu(move |_, cx| build_menu(cx, "top left")),
)
.child(
menu_handle()
.id("test1")
.child(|is_open| {
Label::new(if is_open {
"BOTTOM LEFT"
} else {
"RIGHT CLICK ME"
})
.render()
})
.anchor(AnchorCorner::BottomLeft)
.attach(AnchorCorner::TopLeft)
.menu(move |_, cx| build_menu(cx, "bottom left")),
),
)
.child(
div()
.flex()
.flex_col()
.justify_between()
.child(
menu_handle()
.id("test3")
.child(|is_open| {
Label::new(if is_open {
"TOP RIGHT"
} else {
"RIGHT CLICK ME"
})
.render()
})
.anchor(AnchorCorner::TopRight)
.menu(move |_, cx| build_menu(cx, "top right")),
)
.child(
menu_handle()
.id("test4")
.child(|is_open| {
Label::new(if is_open {
"BOTTOM RIGHT"
} else {
"RIGHT CLICK ME"
})
.render()
})
.anchor(AnchorCorner::BottomRight)
.attach(AnchorCorner::TopRight)
.menu(move |_, cx| build_menu(cx, "bottom right")),
),
)
}
fn into_element(self) -> Self::Element {
self
}
}
-78
View File
@@ -1,78 +0,0 @@
use crate::prelude::*;
use crate::{v_stack, ButtonGroup};
#[derive(Component)]
pub struct Details<V: 'static> {
text: &'static str,
meta: Option<&'static str>,
actions: Option<ButtonGroup<V>>,
}
impl<V: 'static> Details<V> {
pub fn new(text: &'static str) -> Self {
Self {
text,
meta: None,
actions: None,
}
}
pub fn meta_text(mut self, meta: &'static str) -> Self {
self.meta = Some(meta);
self
}
pub fn actions(mut self, actions: ButtonGroup<V>) -> Self {
self.actions = Some(actions);
self
}
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
v_stack()
.p_1()
.gap_0p5()
.text_ui_sm()
.text_color(cx.theme().colors().text)
.size_full()
.child(self.text)
.children(self.meta.map(|m| m))
.children(self.actions.map(|a| a))
}
}
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use super::*;
use crate::{Button, Story};
use gpui::{Div, Render};
pub struct DetailsStory;
impl Render for DetailsStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container(cx)
.child(Story::title_for::<_, Details<Self>>(cx))
.child(Story::label(cx, "Default"))
.child(Details::new("The quick brown fox jumps over the lazy dog"))
.child(Story::label(cx, "With meta"))
.child(
Details::new("The quick brown fox jumps over the lazy dog")
.meta_text("Sphinx of black quartz, judge my vow."),
)
.child(Story::label(cx, "With meta and actions"))
.child(
Details::new("The quick brown fox jumps over the lazy dog")
.meta_text("Sphinx of black quartz, judge my vow.")
.actions(ButtonGroup::new(vec![
Button::new("Decline"),
Button::new("Accept").variant(crate::ButtonVariant::Filled),
])),
)
}
}
}
+19
View File
@@ -0,0 +1,19 @@
use gpui::{div, Element, ParentElement};
use crate::{Color, Icon, IconElement, IconSize, Toggle};
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),
),
}
}
+21 -2
View File
@@ -1,3 +1,5 @@
use gpui::{Div, IntoElement};
use crate::prelude::*;
enum DividerDirection {
@@ -5,12 +7,29 @@ enum DividerDirection {
Vertical,
}
#[derive(Component)]
#[derive(IntoElement)]
pub struct Divider {
direction: DividerDirection,
inset: bool,
}
impl RenderOnce for Divider {
type Rendered = Div;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
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)
}
}
impl Divider {
pub fn horizontal() -> Self {
Self {
@@ -31,7 +50,7 @@ impl Divider {
self
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
fn render(self, cx: &mut WindowContext) -> impl Element {
div()
.map(|this| match self.direction {
DividerDirection::Horizontal => {
@@ -1,28 +0,0 @@
use gpui::Div;
use crate::{prelude::*, v_stack};
/// Create an elevated surface.
///
/// Must be used inside of a relative parent element
pub fn elevated_surface<V: 'static>(level: ElevationIndex, cx: &mut ViewContext<V>) -> Div<V> {
let colors = cx.theme().colors();
// let shadow = BoxShadow {
// color: hsla(0., 0., 0., 0.1),
// offset: point(px(0.), px(1.)),
// blur_radius: px(3.),
// spread_radius: px(0.),
// };
v_stack()
.rounded_lg()
.bg(colors.elevated_surface_background)
.border()
.border_color(colors.border)
.shadow(level.shadow())
}
pub fn modal<V: 'static>(cx: &mut ViewContext<V>) -> Div<V> {
elevated_surface(ElevationIndex::ModalSurface, cx)
}
-59
View File
@@ -1,59 +0,0 @@
use crate::prelude::*;
use crate::{Avatar, Player};
#[derive(Component)]
pub struct Facepile {
players: Vec<Player>,
}
impl Facepile {
pub fn new<P: Iterator<Item = Player>>(players: P) -> Self {
Self {
players: players.collect(),
}
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
let player_count = self.players.len();
let player_list = self.players.iter().enumerate().map(|(ix, player)| {
let isnt_last = ix < player_count - 1;
div()
.when(isnt_last, |div| div.neg_mr_1())
.child(Avatar::new(player.avatar_src().to_string()))
});
div().p_1().flex().items_center().children(player_list)
}
}
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use super::*;
use crate::{static_players, Story};
use gpui::{Div, Render};
pub struct FacepileStory;
impl Render for FacepileStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let players = static_players();
Story::container(cx)
.child(Story::title_for::<_, Facepile>(cx))
.child(Story::label(cx, "Default"))
.child(
div()
.flex()
.gap_3()
.child(Facepile::new(players.clone().into_iter().take(1)))
.child(Facepile::new(players.clone().into_iter().take(2)))
.child(Facepile::new(players.clone().into_iter().take(3))),
)
}
}
}
+53 -54
View File
@@ -1,4 +1,4 @@
use gpui::{rems, svg};
use gpui::{rems, svg, IntoElement, Svg};
use strum::EnumIter;
use crate::prelude::*;
@@ -25,6 +25,7 @@ pub enum Icon {
BellOff,
BellRing,
Bolt,
CaseSensitive,
Check,
ChevronDown,
ChevronLeft,
@@ -33,6 +34,9 @@ pub enum Icon {
Close,
Collab,
Copilot,
CopilotInit,
CopilotError,
CopilotDisabled,
Dash,
Envelope,
ExclamationTriangle,
@@ -67,6 +71,7 @@ pub enum Icon {
Split,
SplitMessage,
Terminal,
WholeWord,
XCircle,
Command,
Control,
@@ -91,6 +96,7 @@ impl Icon {
Icon::BellOff => "icons/bell-off.svg",
Icon::BellRing => "icons/bell-ring.svg",
Icon::Bolt => "icons/bolt.svg",
Icon::CaseSensitive => "icons/case_insensitive.svg",
Icon::Check => "icons/check.svg",
Icon::ChevronDown => "icons/chevron_down.svg",
Icon::ChevronLeft => "icons/chevron_left.svg",
@@ -99,6 +105,9 @@ impl Icon {
Icon::Close => "icons/x.svg",
Icon::Collab => "icons/user_group_16.svg",
Icon::Copilot => "icons/copilot.svg",
Icon::CopilotInit => "icons/copilot_init.svg",
Icon::CopilotError => "icons/copilot_error.svg",
Icon::CopilotDisabled => "icons/copilot_disabled.svg",
Icon::Dash => "icons/dash.svg",
Icon::Envelope => "icons/feedback.svg",
Icon::ExclamationTriangle => "icons/warning.svg",
@@ -133,6 +142,7 @@ impl Icon {
Icon::Split => "icons/split.svg",
Icon::SplitMessage => "icons/split_message.svg",
Icon::Terminal => "icons/terminal.svg",
Icon::WholeWord => "icons/word_search.svg",
Icon::XCircle => "icons/error.svg",
Icon::Command => "icons/command.svg",
Icon::Control => "icons/control.svg",
@@ -143,41 +153,17 @@ impl Icon {
}
}
#[derive(Component)]
#[derive(IntoElement)]
pub struct IconElement {
path: SharedString,
color: TextColor,
color: Color,
size: IconSize,
}
impl IconElement {
pub fn new(icon: Icon) -> Self {
Self {
path: icon.path().into(),
color: TextColor::default(),
size: IconSize::default(),
}
}
impl RenderOnce for IconElement {
type Rendered = Svg;
pub fn from_path(path: impl Into<SharedString>) -> Self {
Self {
path: path.into(),
color: TextColor::default(),
size: IconSize::default(),
}
}
pub fn color(mut self, color: TextColor) -> Self {
self.color = color;
self
}
pub fn size(mut self, size: IconSize) -> Self {
self.size = size;
self
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
let svg_size = match self.size {
IconSize::Small => rems(14. / 16.),
IconSize::Medium => rems(16. / 16.),
@@ -191,30 +177,43 @@ impl IconElement {
}
}
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use gpui::{Div, Render};
use strum::IntoEnumIterator;
use crate::Story;
use super::*;
pub struct IconStory;
impl Render for IconStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let icons = Icon::iter();
Story::container(cx)
.child(Story::title_for::<_, IconElement>(cx))
.child(Story::label(cx, "All Icons"))
.child(div().flex().gap_3().children(icons.map(IconElement::new)))
impl IconElement {
pub fn new(icon: Icon) -> Self {
Self {
path: icon.path().into(),
color: Color::default(),
size: IconSize::default(),
}
}
pub fn from_path(path: impl Into<SharedString>) -> Self {
Self {
path: path.into(),
color: Color::default(),
size: IconSize::default(),
}
}
pub fn color(mut self, color: Color) -> Self {
self.color = color;
self
}
pub fn size(mut self, size: IconSize) -> Self {
self.size = size;
self
}
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))
}
}
+75 -82
View File
@@ -1,40 +1,87 @@
use crate::{h_stack, prelude::*, ClickHandler, Icon, IconElement};
use gpui::{prelude::*, Action, AnyView, MouseButton};
use std::sync::Arc;
use crate::{h_stack, prelude::*, Icon, IconElement};
use gpui::{prelude::*, Action, AnyView, Div, MouseButton, MouseDownEvent, Stateful};
struct IconButtonHandlers<V: 'static> {
click: Option<ClickHandler<V>>,
}
impl<V: 'static> Default for IconButtonHandlers<V> {
fn default() -> Self {
Self { click: None }
}
}
#[derive(Component)]
pub struct IconButton<V: 'static> {
#[derive(IntoElement)]
pub struct IconButton {
id: ElementId,
icon: Icon,
color: TextColor,
color: Color,
variant: ButtonVariant,
state: InteractionState,
selected: bool,
tooltip: Option<Box<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static>>,
handlers: IconButtonHandlers<V>,
tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>>,
on_mouse_down: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
}
impl<V: 'static> IconButton<V> {
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: TextColor::default(),
color: Color::default(),
variant: ButtonVariant::default(),
state: InteractionState::default(),
selected: false,
tooltip: None,
handlers: IconButtonHandlers::default(),
on_mouse_down: None,
}
}
@@ -43,7 +90,7 @@ impl<V: 'static> IconButton<V> {
self
}
pub fn color(mut self, color: TextColor) -> Self {
pub fn color(mut self, color: Color) -> Self {
self.color = color;
self
}
@@ -63,74 +110,20 @@ impl<V: 'static> IconButton<V> {
self
}
pub fn tooltip(
mut self,
tooltip: impl Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static,
) -> 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(&mut V, &mut ViewContext<V>)) -> Self {
self.handlers.click = Some(Arc::new(handler));
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()))
}
fn render(mut self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
let icon_color = match (self.state, self.color) {
(InteractionState::Disabled, _) => TextColor::Disabled,
(InteractionState::Active, _) => TextColor::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 = bg_hover_color;
}
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.handlers.click.clone() {
button = button.on_mouse_down(MouseButton::Left, move |state, event, cx| {
cx.stop_propagation();
click_handler(state, cx);
})
}
if let Some(tooltip) = self.tooltip.take() {
if !self.selected {
button = button.tooltip(move |view: &mut V, cx| (tooltip)(view, cx))
}
}
button
}
}
-23
View File
@@ -1,23 +0,0 @@
use gpui::px;
use crate::prelude::*;
#[derive(Component)]
pub struct UnreadIndicator;
impl UnreadIndicator {
pub fn new() -> Self {
Self
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
div()
.rounded_full()
.border_2()
.border_color(cx.theme().colors().surface_background)
.w(px(9.0))
.h(px(9.0))
.z_index(2)
.bg(cx.theme().status().info)
}
}
+53 -72
View File
@@ -1,5 +1,5 @@
use crate::{prelude::*, Label};
use gpui::prelude::*;
use gpui::{prelude::*, Div, IntoElement, Stateful};
#[derive(Default, PartialEq)]
pub enum InputVariant {
@@ -8,7 +8,7 @@ pub enum InputVariant {
Filled,
}
#[derive(Component)]
#[derive(IntoElement)]
pub struct Input {
placeholder: SharedString,
value: String,
@@ -18,6 +18,57 @@ pub struct Input {
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 {
@@ -54,74 +105,4 @@ impl Input {
self.is_active = is_active;
self
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
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 {
TextColor::Disabled
} else {
TextColor::Placeholder
});
let label = Label::new(self.value.clone()).color(if self.disabled {
TextColor::Disabled
} else {
TextColor::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(|this| {
if self.value.is_empty() {
this.child(placeholder_label)
} else {
this.child(label)
}
}))
}
}
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use super::*;
use crate::Story;
use gpui::{Div, Render};
pub struct InputStory;
impl Render for InputStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container(cx)
.child(Story::title_for::<_, Input>(cx))
.child(Story::label(cx, "Default"))
.child(div().flex().child(Input::new("Search")))
}
}
}
+40 -163
View File
@@ -1,9 +1,7 @@
use gpui::{actions, relative, rems, Action, Styled};
use strum::EnumIter;
use crate::prelude::*;
use gpui::{Action, Div, IntoElement};
use crate::{h_stack, prelude::*, Icon, IconElement, IconSize};
#[derive(Component, Clone)]
#[derive(IntoElement, Clone)]
pub struct KeyBinding {
/// A keybinding consists of a key and a set of modifier keys.
/// More then one keybinding produces a chord.
@@ -12,6 +10,27 @@ pub struct KeyBinding {
key_binding: gpui::KeyBinding,
}
impl RenderOnce for KeyBinding {
type Rendered = Div;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
div()
.flex()
.gap_2()
.children(self.key_binding.keystrokes().iter().map(|keystroke| {
div()
.flex()
.gap_1()
.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()))
}))
}
}
impl KeyBinding {
pub fn for_action(action: &dyn Action, cx: &mut WindowContext) -> Option<Self> {
// todo! this last is arbitrary, we want to prefer users key bindings over defaults,
@@ -23,172 +42,30 @@ impl KeyBinding {
pub fn new(key_binding: gpui::KeyBinding) -> Self {
Self { key_binding }
}
fn icon_for_key(key: &str) -> Option<Icon> {
match key {
"left" => Some(Icon::ArrowLeft),
"right" => Some(Icon::ArrowRight),
"up" => Some(Icon::ArrowUp),
"down" => Some(Icon::ArrowDown),
_ => None,
}
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
h_stack()
.flex_none()
.gap_1()
.children(self.key_binding.keystrokes().iter().map(|keystroke| {
let key_icon = Self::icon_for_key(&keystroke.key);
h_stack()
.flex_none()
.gap_0p5()
.bg(cx.theme().colors().element_background)
.p_0p5()
.rounded_sm()
.when(keystroke.modifiers.function, |el| el.child(Key::new("fn")))
.when(keystroke.modifiers.control, |el| {
el.child(KeyIcon::new(Icon::Control))
})
.when(keystroke.modifiers.alt, |el| {
el.child(KeyIcon::new(Icon::Option))
})
.when(keystroke.modifiers.command, |el| {
el.child(KeyIcon::new(Icon::Command))
})
.when(keystroke.modifiers.shift, |el| {
el.child(KeyIcon::new(Icon::Shift))
})
.when_some(key_icon, |el, icon| el.child(KeyIcon::new(icon)))
.when(key_icon.is_none(), |el| {
el.child(Key::new(keystroke.key.to_uppercase().clone()))
})
}))
}
}
#[derive(Component)]
#[derive(IntoElement)]
pub struct Key {
key: SharedString,
}
impl RenderOnce for Key {
type Rendered = Div;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
div()
.px_2()
.py_0()
.rounded_md()
.text_ui_sm()
.text_color(cx.theme().colors().text)
.bg(cx.theme().colors().element_background)
.child(self.key.clone())
}
}
impl Key {
pub fn new(key: impl Into<SharedString>) -> Self {
Self { key: key.into() }
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
let single_char = self.key.len() == 1;
div()
// .px_0p5()
.py_0()
.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.))
// .rounded_md()
.text_ui()
.line_height(relative(1.))
.text_color(cx.theme().colors().text)
// .bg(cx.theme().colors().element_background)
.child(self.key.clone())
}
}
#[derive(Component)]
pub struct KeyIcon {
icon: Icon,
}
impl KeyIcon {
pub fn new(icon: Icon) -> Self {
Self { icon }
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
div()
.w(rems(14. / 16.))
// .bg(cx.theme().colors().element_background)
.child(IconElement::new(self.icon).size(IconSize::Small))
}
}
// NOTE: The order the modifier keys appear in this enum impacts the order in
// which they are rendered in the UI.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
pub enum ModifierKey {
Control,
Alt, // Option
Shift,
Command,
}
actions!(NoAction);
pub fn binding(key: &str) -> gpui::KeyBinding {
gpui::KeyBinding::new(key, NoAction {}, None)
}
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use super::*;
pub use crate::KeyBinding;
use crate::{binding, Story};
use gpui::{Div, Render};
use itertools::Itertools;
pub struct KeybindingStory;
impl Render for KeybindingStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let all_modifier_permutations =
["ctrl", "alt", "cmd", "shift"].into_iter().permutations(2);
Story::container(cx)
.child(Story::title_for::<_, KeyBinding>(cx))
.child(Story::label(cx, "Single Key"))
.child(KeyBinding::new(binding("Z")))
.child(Story::label(cx, "Single Key with Modifier"))
.child(
div()
.flex()
.gap_3()
.child(KeyBinding::new(binding("ctrl-c")))
.child(KeyBinding::new(binding("alt-c")))
.child(KeyBinding::new(binding("cmd-c")))
.child(KeyBinding::new(binding("shift-c"))),
)
.child(Story::label(cx, "Single Key with Modifier (Permuted)"))
.child(
div().flex().flex_col().children(
all_modifier_permutations
.chunks(4)
.into_iter()
.map(|chunk| {
div()
.flex()
.gap_4()
.py_3()
.children(chunk.map(|permutation| {
KeyBinding::new(binding(&*(permutation.join("-") + "-x")))
}))
}),
),
)
.child(Story::label(cx, "Single Key with All Modifiers"))
.child(KeyBinding::new(binding("ctrl-alt-cmd-shift-z")))
.child(Story::label(cx, "Chord"))
.child(KeyBinding::new(binding("a z")))
.child(Story::label(cx, "Chord with Modifier"))
.child(KeyBinding::new(binding("ctrl-a shift-z")))
.child(KeyBinding::new(binding("fn-s")))
}
}
}
+62 -129
View File
@@ -1,7 +1,6 @@
use gpui::{relative, Hsla, Text, TextRun, WindowContext};
use crate::prelude::*;
use crate::styled_ext::StyledExt;
use gpui::{relative, Div, Hsla, IntoElement, StyledText, TextRun, WindowContext};
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
pub enum LabelSize {
@@ -10,48 +9,6 @@ pub enum LabelSize {
Small,
}
#[derive(Default, PartialEq, Copy, Clone)]
pub enum TextColor {
#[default]
Default,
Accent,
Created,
Deleted,
Disabled,
Error,
Hidden,
Info,
Modified,
Muted,
Placeholder,
Player(u32),
Selected,
Success,
Warning,
}
impl TextColor {
pub fn color(&self, cx: &WindowContext) -> Hsla {
match self {
TextColor::Default => cx.theme().colors().text,
TextColor::Muted => cx.theme().colors().text_muted,
TextColor::Created => cx.theme().status().created,
TextColor::Modified => cx.theme().status().modified,
TextColor::Deleted => cx.theme().status().deleted,
TextColor::Disabled => cx.theme().colors().text_disabled,
TextColor::Hidden => cx.theme().status().hidden,
TextColor::Info => cx.theme().status().info,
TextColor::Placeholder => cx.theme().colors().text_placeholder,
TextColor::Accent => cx.theme().colors().text_accent,
TextColor::Player(i) => cx.theme().styles.player.0[i.clone() as usize].cursor,
TextColor::Error => cx.theme().status().error,
TextColor::Selected => cx.theme().colors().text_accent,
TextColor::Success => cx.theme().status().success,
TextColor::Warning => cx.theme().status().warning,
}
}
}
#[derive(Default, PartialEq, Copy, Clone)]
pub enum LineHeightStyle {
#[default]
@@ -60,47 +17,19 @@ pub enum LineHeightStyle {
UILabel,
}
#[derive(Clone, Component)]
#[derive(IntoElement, Clone)]
pub struct Label {
label: SharedString,
size: LabelSize,
line_height_style: LineHeightStyle,
color: TextColor,
color: Color,
strikethrough: bool,
}
impl Label {
pub fn new(label: impl Into<SharedString>) -> Self {
Self {
label: label.into(),
size: LabelSize::Default,
line_height_style: LineHeightStyle::default(),
color: TextColor::Default,
strikethrough: false,
}
}
impl RenderOnce for Label {
type Rendered = Div;
pub fn size(mut self, size: LabelSize) -> Self {
self.size = size;
self
}
pub fn color(mut self, color: TextColor) -> Self {
self.color = color;
self
}
pub fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self {
self.line_height_style = line_height_style;
self
}
pub fn set_strikethrough(mut self, strikethrough: bool) -> Self {
self.strikethrough = strikethrough;
self
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
div()
.when(self.strikethrough, |this| {
this.relative().child(
@@ -109,7 +38,7 @@ impl Label {
.top_1_2()
.w_full()
.h_px()
.bg(TextColor::Hidden.color(cx)),
.bg(Color::Hidden.color(cx)),
)
})
.map(|this| match self.size {
@@ -124,24 +53,13 @@ impl Label {
}
}
#[derive(Component)]
pub struct HighlightedLabel {
label: SharedString,
size: LabelSize,
color: TextColor,
highlight_indices: Vec<usize>,
strikethrough: bool,
}
impl HighlightedLabel {
/// shows a label with the given characters highlighted.
/// characters are identified by utf8 byte position.
pub fn new(label: impl Into<SharedString>, highlight_indices: Vec<usize>) -> Self {
impl Label {
pub fn new(label: impl Into<SharedString>) -> Self {
Self {
label: label.into(),
size: LabelSize::Default,
color: TextColor::Default,
highlight_indices,
line_height_style: LineHeightStyle::default(),
color: Color::Default,
strikethrough: false,
}
}
@@ -151,17 +69,35 @@ impl HighlightedLabel {
self
}
pub fn color(mut self, color: TextColor) -> Self {
pub fn color(mut self, color: Color) -> Self {
self.color = color;
self
}
pub fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self {
self.line_height_style = line_height_style;
self
}
pub fn set_strikethrough(mut self, strikethrough: bool) -> Self {
self.strikethrough = strikethrough;
self
}
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
#[derive(IntoElement)]
pub struct HighlightedLabel {
label: SharedString,
size: LabelSize,
color: Color,
highlight_indices: Vec<usize>,
strikethrough: bool,
}
impl RenderOnce for HighlightedLabel {
type Rendered = Div;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
let highlight_color = cx.theme().colors().text_accent;
let mut text_style = cx.text_style().clone();
@@ -207,14 +143,43 @@ impl HighlightedLabel {
.my_auto()
.w_full()
.h_px()
.bg(TextColor::Hidden.color(cx)),
.bg(Color::Hidden.color(cx)),
)
})
.map(|this| match self.size {
LabelSize::Default => this.text_ui(),
LabelSize::Small => this.text_ui_sm(),
})
.child(Text::styled(self.label, runs))
.child(StyledText::new(self.label).with_runs(runs))
}
}
impl HighlightedLabel {
/// shows a label with the given characters highlighted.
/// characters are identified by utf8 byte position.
pub fn new(label: impl Into<SharedString>, highlight_indices: Vec<usize>) -> Self {
Self {
label: label.into(),
size: LabelSize::Default,
color: Color::Default,
highlight_indices,
strikethrough: false,
}
}
pub fn size(mut self, size: LabelSize) -> Self {
self.size = size;
self
}
pub fn color(mut self, color: Color) -> Self {
self.color = color;
self
}
pub fn set_strikethrough(mut self, strikethrough: bool) -> Self {
self.strikethrough = strikethrough;
self
}
}
@@ -223,35 +188,3 @@ struct Run {
pub text: String,
pub color: Hsla,
}
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use super::*;
use crate::Story;
use gpui::{Div, Render};
pub struct LabelStory;
impl Render for LabelStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container(cx)
.child(Story::title_for::<_, Label>(cx))
.child(Story::label(cx, "Default"))
.child(Label::new("Hello, world!"))
.child(Story::label(cx, "Highlighted"))
.child(HighlightedLabel::new(
"Hello, world!",
vec![0, 1, 2, 7, 8, 12],
))
.child(HighlightedLabel::new(
"Héllo, world!",
vec![0, 1, 3, 8, 9, 13],
))
}
}
}
+188 -220
View File
@@ -1,10 +1,14 @@
use std::rc::Rc;
use gpui::{div, Div, Stateful, StatefulInteractiveComponent};
use gpui::{
div, px, AnyElement, ClickEvent, Div, ImageSource, IntoElement, MouseButton, MouseDownEvent,
Pixels, Stateful, StatefulInteractiveElement,
};
use smallvec::SmallVec;
use crate::settings::user_settings;
use crate::{
disclosure_control, h_stack, v_stack, Avatar, Icon, IconElement, IconSize, Label, Toggle,
disclosure_control, h_stack, v_stack, Avatar, Icon, IconButton, IconElement, IconSize, Label,
Toggle,
};
use crate::{prelude::*, GraphicSlot};
@@ -17,14 +21,13 @@ pub enum ListItemVariant {
}
pub enum ListHeaderMeta {
// TODO: These should be IconButtons
Tools(Vec<Icon>),
Tools(Vec<IconButton>),
// TODO: This should be a button
Button(Label),
Text(Label),
}
#[derive(Component)]
#[derive(IntoElement)]
pub struct ListHeader {
label: SharedString,
left_icon: Option<Icon>,
@@ -33,6 +36,60 @@ pub struct ListHeader {
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| i.color(Color::Muted))),
),
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 {
@@ -54,72 +111,17 @@ impl ListHeader {
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
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<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(Component, Clone)]
#[derive(IntoElement, Clone)]
pub struct ListSubHeader {
label: SharedString,
left_icon: Option<Icon>,
@@ -139,8 +141,12 @@ impl ListSubHeader {
self.left_icon = left_icon;
self
}
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
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()
@@ -158,134 +164,89 @@ impl ListSubHeader {
.items_center()
.children(self.left_icon.map(|i| {
IconElement::new(i)
.color(TextColor::Muted)
.color(Color::Muted)
.size(IconSize::Small)
}))
.child(Label::new(self.label.clone()).color(TextColor::Muted)),
.child(Label::new(self.label.clone()).color(Color::Muted)),
),
)
}
}
#[derive(Default, PartialEq, Copy, Clone)]
pub enum ListEntrySize {
#[default]
Small,
Medium,
}
#[derive(Clone)]
pub enum ListItem<V: 'static> {
Entry(ListEntry<V>),
Separator(ListSeparator),
Header(ListSubHeader),
}
impl<V: 'static> From<ListEntry<V>> for ListItem<V> {
fn from(entry: ListEntry<V>) -> Self {
Self::Entry(entry)
}
}
impl<V: 'static> From<ListSeparator> for ListItem<V> {
fn from(entry: ListSeparator) -> Self {
Self::Separator(entry)
}
}
impl<V: 'static> From<ListSubHeader> for ListItem<V> {
fn from(entry: ListSubHeader) -> Self {
Self::Header(entry)
}
}
impl<V: 'static> ListItem<V> {
fn render(self, view: &mut V, ix: usize, cx: &mut ViewContext<V>) -> impl Component<V> {
match self {
ListItem::Entry(entry) => div().child(entry.render(ix, cx)),
ListItem::Separator(separator) => div().child(separator.render(view, cx)),
ListItem::Header(header) => div().child(header.render(view, cx)),
}
}
pub fn new(label: Label) -> Self {
Self::Entry(ListEntry::new(label))
}
pub fn as_entry(&mut self) -> Option<&mut ListEntry<V>> {
if let Self::Entry(entry) = self {
Some(entry)
} else {
None
}
}
}
pub struct ListEntry<V> {
#[derive(IntoElement)]
pub struct ListItem {
id: ElementId,
disabled: bool,
selected: bool,
// TODO: Reintroduce this
// disclosure_control_style: DisclosureControlVisibility,
indent_level: u32,
label: Label,
indent_level: usize,
indent_step_size: Pixels,
left_slot: Option<GraphicSlot>,
overflow: OverflowStyle,
size: ListEntrySize,
toggle: Toggle,
variant: ListItemVariant,
on_click: Option<Rc<dyn Fn(&mut V, &mut ViewContext<V>) + 'static>>,
on_click: 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<V> Clone for ListEntry<V> {
fn clone(&self) -> Self {
Self {
disabled: self.disabled,
indent_level: self.indent_level,
label: self.label.clone(),
left_slot: self.left_slot.clone(),
overflow: self.overflow,
size: self.size,
toggle: self.toggle,
variant: self.variant,
on_click: self.on_click.clone(),
}
}
}
impl<V: 'static> ListEntry<V> {
pub fn new(label: Label) -> Self {
impl ListItem {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
disabled: false,
selected: false,
indent_level: 0,
label,
indent_step_size: px(12.),
left_slot: None,
overflow: OverflowStyle::Hidden,
size: ListEntrySize::default(),
toggle: Toggle::NotToggleable,
variant: ListItemVariant::default(),
on_click: Default::default(),
on_click: None,
on_secondary_mouse_down: None,
children: SmallVec::new(),
}
}
pub fn on_click(mut self, handler: impl Fn(&mut V, &mut ViewContext<V>) + 'static) -> Self {
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 variant(mut self, variant: ListItemVariant) -> Self {
self.variant = variant;
self
}
pub fn indent_level(mut self, indent_level: u32) -> 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: Toggle) -> Self {
self.toggle = toggle;
self
}
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
pub fn left_content(mut self, left_content: GraphicSlot) -> Self {
self.left_slot = Some(left_content);
self
@@ -296,116 +257,138 @@ impl<V: 'static> ListEntry<V> {
self
}
pub fn left_avatar(mut self, left_avatar: impl Into<SharedString>) -> Self {
pub fn left_avatar(mut self, left_avatar: impl Into<ImageSource>) -> Self {
self.left_slot = Some(GraphicSlot::Avatar(left_avatar.into()));
self
}
}
pub fn size(mut self, size: ListEntrySize) -> Self {
self.size = size;
self
}
fn render(self, ix: usize, cx: &mut ViewContext<V>) -> Stateful<V, Div<V>> {
let settings = user_settings(cx);
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(TextColor::Muted),
.color(Color::Muted),
),
),
Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::new(src))),
Some(GraphicSlot::PublicActor(src)) => Some(h_stack().child(Avatar::new(src))),
Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::source(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(ix)
.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 |view: &mut V, event, cx| {
if let Some(on_click) = &on_click {
(on_click)(view, cx)
}
}
})
.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)
// })
.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)
})
})
.child(
sized_item
div()
.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(*settings.list_indent_depth)
.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)),
)
}))
.ml(self.indent_level as f32 * self.indent_step_size)
.flex()
.gap_1()
.items_center()
.relative()
.child(disclosure_control(self.toggle))
.children(left_content)
.child(self.label),
.children(self.children)
// HACK: We need to attach the `on_click` handler to the child element in order to have the click
// event actually fire.
// Once this is fixed in GPUI we can remove this and rely on the `on_click` handler set above on the
// outer `div`.
.id("on_click_hack")
.when_some(self.on_click, |this, on_click| {
this.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)
}
})
}),
)
}
}
#[derive(Clone, Component)]
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
}
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
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)
}
}
#[derive(Component)]
pub struct List<V: 'static> {
items: Vec<ListItem<V>>,
#[derive(IntoElement)]
pub struct List {
/// Message to display when the list is empty
/// Defaults to "No items"
empty_message: SharedString,
header: Option<ListHeader>,
toggle: Toggle,
children: SmallVec<[AnyElement; 2]>,
}
impl<V: 'static> List<V> {
pub fn new(items: Vec<ListItem<V>>) -> Self {
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 {
items,
empty_message: "No items".into(),
header: None,
toggle: Toggle::NotToggleable,
children: SmallVec::new(),
}
}
@@ -423,25 +406,10 @@ impl<V: 'static> List<V> {
self.toggle = toggle;
self
}
}
fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
let list_content = match (self.items.is_empty(), self.toggle) {
(false, _) => div().children(
self.items
.into_iter()
.enumerate()
.map(|(ix, item)| item.render(view, ix, cx)),
),
(true, Toggle::Toggled(false)) => div(),
(true, _) => {
div().child(Label::new(self.empty_message.clone()).color(TextColor::Muted))
}
};
v_stack()
.w_full()
.py_1()
.children(self.header.map(|header| header))
.child(list_content)
impl ParentElement for List {
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
&mut self.children
}
}
-81
View File
@@ -1,81 +0,0 @@
use gpui::AnyElement;
use smallvec::SmallVec;
use crate::{h_stack, prelude::*, v_stack, Button, Icon, IconButton, Label};
#[derive(Component)]
pub struct Modal<V: 'static> {
id: ElementId,
title: Option<SharedString>,
primary_action: Option<Button<V>>,
secondary_action: Option<Button<V>>,
children: SmallVec<[AnyElement<V>; 2]>,
}
impl<V: 'static> Modal<V> {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
title: None,
primary_action: None,
secondary_action: None,
children: SmallVec::new(),
}
}
pub fn title(mut self, title: impl Into<SharedString>) -> Self {
self.title = Some(title.into());
self
}
pub fn primary_action(mut self, action: Button<V>) -> Self {
self.primary_action = Some(action);
self
}
pub fn secondary_action(mut self, action: Button<V>) -> Self {
self.secondary_action = Some(action);
self
}
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
v_stack()
.id(self.id.clone())
.w_96()
// .rounded_xl()
.bg(cx.theme().colors().background)
.border()
.border_color(cx.theme().colors().border)
.shadow_2xl()
.child(
h_stack()
.justify_between()
.p_1()
.border_b()
.border_color(cx.theme().colors().border)
.child(div().children(self.title.clone().map(|t| Label::new(t))))
.child(IconButton::new("close", Icon::Close)),
)
.child(v_stack().p_1().children(self.children))
.when(
self.primary_action.is_some() || self.secondary_action.is_some(),
|this| {
this.child(
h_stack()
.border_t()
.border_color(cx.theme().colors().border)
.p_1()
.justify_end()
.children(self.secondary_action)
.children(self.primary_action),
)
},
)
}
}
impl<V: 'static> ParentComponent<V> for Modal<V> {
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
&mut self.children
}
}
@@ -1,40 +0,0 @@
use gpui::rems;
use crate::prelude::*;
use crate::{h_stack, Icon};
#[derive(Component)]
pub struct NotificationToast {
label: SharedString,
icon: Option<Icon>,
}
impl NotificationToast {
pub fn new(label: SharedString) -> Self {
Self { label, icon: None }
}
pub fn icon<I>(mut self, icon: I) -> Self
where
I: Into<Option<Icon>>,
{
self.icon = icon.into();
self
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
h_stack()
.z_index(5)
.absolute()
.top_1()
.right_1()
.w(rems(9999.))
.max_w_56()
.py_1()
.px_1p5()
.rounded_lg()
.shadow_md()
.bg(cx.theme().colors().elevated_surface_background)
.child(div().size_full().child(self.label.clone()))
}
}
-204
View File
@@ -1,204 +0,0 @@
use crate::{h_stack, prelude::*, v_stack, KeyBinding, Label};
use gpui::prelude::*;
#[derive(Component)]
pub struct Palette {
id: ElementId,
input_placeholder: SharedString,
empty_string: SharedString,
items: Vec<PaletteItem>,
default_order: OrderMethod,
}
impl Palette {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
input_placeholder: "Find something...".into(),
empty_string: "No items found.".into(),
items: vec![],
default_order: OrderMethod::default(),
}
}
pub fn items(mut self, items: Vec<PaletteItem>) -> Self {
self.items = items;
self
}
pub fn placeholder(mut self, input_placeholder: impl Into<SharedString>) -> Self {
self.input_placeholder = input_placeholder.into();
self
}
pub fn empty_string(mut self, empty_string: impl Into<SharedString>) -> Self {
self.empty_string = empty_string.into();
self
}
// TODO: Hook up sort order
pub fn default_order(mut self, default_order: OrderMethod) -> Self {
self.default_order = default_order;
self
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
v_stack()
.id(self.id.clone())
.w_96()
.rounded_lg()
.bg(cx.theme().colors().elevated_surface_background)
.border()
.border_color(cx.theme().colors().border)
.child(
v_stack()
.gap_px()
.child(v_stack().py_0p5().px_1().child(div().px_2().py_0p5().child(
Label::new(self.input_placeholder.clone()).color(TextColor::Placeholder),
)))
.child(
div()
.h_px()
.w_full()
.bg(cx.theme().colors().element_background),
)
.child(
v_stack()
.id("items")
.py_0p5()
.px_1()
.grow()
.max_h_96()
.overflow_y_scroll()
.children(
vec![if self.items.is_empty() {
Some(
h_stack().justify_between().px_2().py_1().child(
Label::new(self.empty_string.clone())
.color(TextColor::Muted),
),
)
} else {
None
}]
.into_iter()
.flatten(),
)
.children(self.items.into_iter().enumerate().map(|(index, item)| {
h_stack()
.id(index)
.justify_between()
.px_2()
.py_0p5()
.rounded_lg()
.hover(|style| {
style.bg(cx.theme().colors().ghost_element_hover)
})
.active(|style| {
style.bg(cx.theme().colors().ghost_element_active)
})
.child(item)
})),
),
)
}
}
#[derive(Component)]
pub struct PaletteItem {
pub label: SharedString,
pub sublabel: Option<SharedString>,
pub key_binding: Option<KeyBinding>,
}
impl PaletteItem {
pub fn new(label: impl Into<SharedString>) -> Self {
Self {
label: label.into(),
sublabel: None,
key_binding: None,
}
}
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
self.label = label.into();
self
}
pub fn sublabel(mut self, sublabel: impl Into<Option<SharedString>>) -> Self {
self.sublabel = sublabel.into();
self
}
pub fn key_binding(mut self, key_binding: impl Into<Option<KeyBinding>>) -> Self {
self.key_binding = key_binding.into();
self
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
div()
.flex()
.flex_row()
.grow()
.justify_between()
.child(
v_stack()
.child(Label::new(self.label.clone()))
.children(self.sublabel.clone().map(|sublabel| Label::new(sublabel))),
)
.children(self.key_binding)
}
}
use gpui::ElementId;
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use gpui::{Div, Render};
use crate::{binding, Story};
use super::*;
pub struct PaletteStory;
impl Render for PaletteStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
{
Story::container(cx)
.child(Story::title_for::<_, Palette>(cx))
.child(Story::label(cx, "Default"))
.child(Palette::new("palette-1"))
.child(Story::label(cx, "With Items"))
.child(
Palette::new("palette-2")
.placeholder("Execute a command...")
.items(vec![
PaletteItem::new("theme selector: toggle")
.key_binding(KeyBinding::new(binding("cmd-k cmd-t"))),
PaletteItem::new("assistant: inline assist")
.key_binding(KeyBinding::new(binding("cmd-enter"))),
PaletteItem::new("assistant: quote selection")
.key_binding(KeyBinding::new(binding("cmd-<"))),
PaletteItem::new("assistant: toggle focus")
.key_binding(KeyBinding::new(binding("cmd-?"))),
PaletteItem::new("auto update: check"),
PaletteItem::new("auto update: view release notes"),
PaletteItem::new("branches: open recent")
.key_binding(KeyBinding::new(binding("cmd-alt-b"))),
PaletteItem::new("chat panel: toggle focus"),
PaletteItem::new("cli: install"),
PaletteItem::new("client: sign in"),
PaletteItem::new("client: sign out"),
PaletteItem::new("editor: cancel")
.key_binding(KeyBinding::new(binding("escape"))),
]),
)
}
}
}
}
-150
View File
@@ -1,150 +0,0 @@
use gpui::{prelude::*, AbsoluteLength, AnyElement};
use smallvec::SmallVec;
use crate::prelude::*;
use crate::settings::user_settings;
use crate::v_stack;
#[derive(Default, Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum PanelAllowedSides {
LeftOnly,
RightOnly,
BottomOnly,
#[default]
LeftAndRight,
All,
}
impl PanelAllowedSides {
/// Return a `HashSet` that contains the allowable `PanelSide`s.
pub fn allowed_sides(&self) -> HashSet<PanelSide> {
match self {
Self::LeftOnly => HashSet::from_iter([PanelSide::Left]),
Self::RightOnly => HashSet::from_iter([PanelSide::Right]),
Self::BottomOnly => HashSet::from_iter([PanelSide::Bottom]),
Self::LeftAndRight => HashSet::from_iter([PanelSide::Left, PanelSide::Right]),
Self::All => HashSet::from_iter([PanelSide::Left, PanelSide::Right, PanelSide::Bottom]),
}
}
}
#[derive(Default, Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum PanelSide {
#[default]
Left,
Right,
Bottom,
}
use std::collections::HashSet;
#[derive(Component)]
pub struct Panel<V: 'static> {
id: ElementId,
current_side: PanelSide,
/// Defaults to PanelAllowedSides::LeftAndRight
allowed_sides: PanelAllowedSides,
initial_width: AbsoluteLength,
width: Option<AbsoluteLength>,
children: SmallVec<[AnyElement<V>; 2]>,
}
impl<V: 'static> Panel<V> {
pub fn new(id: impl Into<ElementId>, cx: &mut WindowContext) -> Self {
let settings = user_settings(cx);
Self {
id: id.into(),
current_side: PanelSide::default(),
allowed_sides: PanelAllowedSides::default(),
initial_width: *settings.default_panel_size,
width: None,
children: SmallVec::new(),
}
}
pub fn initial_width(mut self, initial_width: AbsoluteLength) -> Self {
self.initial_width = initial_width;
self
}
pub fn width(mut self, width: AbsoluteLength) -> Self {
self.width = Some(width);
self
}
pub fn allowed_sides(mut self, allowed_sides: PanelAllowedSides) -> Self {
self.allowed_sides = allowed_sides;
self
}
pub fn side(mut self, side: PanelSide) -> Self {
let allowed_sides = self.allowed_sides.allowed_sides();
if allowed_sides.contains(&side) {
self.current_side = side;
} else {
panic!(
"The panel side {:?} was not added as allowed before it was set.",
side
);
}
self
}
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
let current_size = self.width.unwrap_or(self.initial_width);
v_stack()
.id(self.id.clone())
.flex_initial()
.map(|this| match self.current_side {
PanelSide::Left | PanelSide::Right => this.h_full().w(current_size),
PanelSide::Bottom => this,
})
.map(|this| match self.current_side {
PanelSide::Left => this.border_r(),
PanelSide::Right => this.border_l(),
PanelSide::Bottom => this.border_b().w_full().h(current_size),
})
.bg(cx.theme().colors().surface_background)
.border_color(cx.theme().colors().border)
.children(self.children)
}
}
impl<V: 'static> ParentComponent<V> for Panel<V> {
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
&mut self.children
}
}
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use super::*;
use crate::{Label, Story};
use gpui::{Div, InteractiveComponent, Render};
pub struct PanelStory;
impl Render for PanelStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container(cx)
.child(Story::title_for::<_, Panel<Self>>(cx))
.child(Story::label(cx, "Default"))
.child(
Panel::new("panel", cx).child(
div()
.id("panel-contents")
.overflow_y_scroll()
.children((0..100).map(|ix| Label::new(format!("Item {}", ix + 1)))),
),
)
}
}
}
-174
View File
@@ -1,174 +0,0 @@
use gpui::{Hsla, ViewContext};
use crate::prelude::*;
/// Represents a person with a Zed account's public profile.
/// All data in this struct should be considered public.
pub struct PublicPlayer {
pub username: SharedString,
pub avatar: SharedString,
pub is_contact: bool,
}
impl PublicPlayer {
pub fn new(username: impl Into<SharedString>, avatar: impl Into<SharedString>) -> Self {
Self {
username: username.into(),
avatar: avatar.into(),
is_contact: false,
}
}
}
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub enum PlayerStatus {
#[default]
Offline,
Online,
InCall,
Away,
DoNotDisturb,
Invisible,
}
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub enum MicStatus {
Muted,
#[default]
Unmuted,
}
impl MicStatus {
pub fn inverse(&self) -> Self {
match self {
Self::Muted => Self::Unmuted,
Self::Unmuted => Self::Muted,
}
}
}
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub enum VideoStatus {
On,
#[default]
Off,
}
impl VideoStatus {
pub fn inverse(&self) -> Self {
match self {
Self::On => Self::Off,
Self::Off => Self::On,
}
}
}
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub enum ScreenShareStatus {
Shared,
#[default]
NotShared,
}
impl ScreenShareStatus {
pub fn inverse(&self) -> Self {
match self {
Self::Shared => Self::NotShared,
Self::NotShared => Self::Shared,
}
}
}
#[derive(Clone)]
pub struct PlayerCallStatus {
pub mic_status: MicStatus,
/// Indicates if the player is currently speaking
/// And the intensity of the volume coming through
///
/// 0.0 - 1.0
pub voice_activity: f32,
pub video_status: VideoStatus,
pub screen_share_status: ScreenShareStatus,
pub in_current_project: bool,
pub disconnected: bool,
pub following: Option<Vec<Player>>,
pub followers: Option<Vec<Player>>,
}
impl PlayerCallStatus {
pub fn new() -> Self {
Self {
mic_status: MicStatus::default(),
voice_activity: 0.,
video_status: VideoStatus::default(),
screen_share_status: ScreenShareStatus::default(),
in_current_project: true,
disconnected: false,
following: None,
followers: None,
}
}
}
#[derive(PartialEq, Clone)]
pub struct Player {
index: usize,
avatar_src: String,
username: String,
status: PlayerStatus,
}
#[derive(Clone)]
pub struct PlayerWithCallStatus {
player: Player,
call_status: PlayerCallStatus,
}
impl PlayerWithCallStatus {
pub fn new(player: Player, call_status: PlayerCallStatus) -> Self {
Self {
player,
call_status,
}
}
pub fn get_player(&self) -> &Player {
&self.player
}
pub fn get_call_status(&self) -> &PlayerCallStatus {
&self.call_status
}
}
impl Player {
pub fn new(index: usize, avatar_src: String, username: String) -> Self {
Self {
index,
avatar_src,
username,
status: Default::default(),
}
}
pub fn set_status(mut self, status: PlayerStatus) -> Self {
self.status = status;
self
}
pub fn cursor_color<V: 'static>(&self, cx: &mut ViewContext<V>) -> Hsla {
cx.theme().styles.player.0[self.index % cx.theme().styles.player.0.len()].cursor
}
pub fn selection_color<V: 'static>(&self, cx: &mut ViewContext<V>) -> Hsla {
cx.theme().styles.player.0[self.index % cx.theme().styles.player.0.len()].selection
}
pub fn avatar_src(&self) -> &str {
&self.avatar_src
}
pub fn index(&self) -> usize {
self.index
}
}
-61
View File
@@ -1,61 +0,0 @@
use crate::prelude::*;
use crate::{Avatar, Facepile, PlayerWithCallStatus};
#[derive(Component)]
pub struct PlayerStack {
player_with_call_status: PlayerWithCallStatus,
}
impl PlayerStack {
pub fn new(player_with_call_status: PlayerWithCallStatus) -> Self {
Self {
player_with_call_status,
}
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
let player = self.player_with_call_status.get_player();
let followers = self
.player_with_call_status
.get_call_status()
.followers
.as_ref()
.map(|followers| followers.clone());
// if we have no followers return a slightly different element
// if mic_status == muted add a red ring to avatar
div()
.h_full()
.flex()
.flex_col()
.gap_px()
.justify_center()
.child(
div()
.flex()
.justify_center()
.w_full()
.child(div().w_4().h_0p5().rounded_sm().bg(player.cursor_color(cx))),
)
.child(
div()
.flex()
.items_center()
.justify_center()
.h_6()
.pl_1()
.rounded_lg()
.bg(if followers.is_none() {
cx.theme().styles.system.transparent
} else {
player.selection_color(cx)
})
.child(Avatar::new(player.avatar_src().to_string()))
.children(followers.map(|followers| {
div().neg_ml_2().child(Facepile::new(followers.into_iter()))
})),
)
}
}
+84
View File
@@ -0,0 +1,84 @@
use gpui::{
div, AnyElement, Div, Element, ElementId, IntoElement, ParentElement, RenderOnce, Styled,
WindowContext,
};
use smallvec::SmallVec;
use theme2::ActiveTheme;
use crate::{v_stack, StyledExt};
/// A popover is used to display a menu or show some options.
///
/// Clicking the element that launches the popover should not change the current view,
/// and the popover should be statically positioned relative to that element (not the
/// user's mouse.)
///
/// Example: A "new" menu with options like "new file", "new folder", etc,
/// Linear's "Display" menu, a profile menu that appers when you click your avatar.
///
/// Related elements:
///
/// `ContextMenu`:
///
/// Used to display a popover menu that only contains a list of items. Context menus are always
/// launched by secondary clicking on an element. The menu is positioned relative to the user's cursor.
///
/// Example: Right clicking a file in the file tree to get a list of actions, right clicking
/// a tab to in the tab bar to get a list of actions.
///
/// `Dropdown`:
///
/// Used to display a list of options when the user clicks an element. The menu is
/// positioned relative the element that was clicked, and clicking an item in the
/// dropdown should change the value of the element that was clicked.
///
/// Example: A theme select control. Displays "One Dark", clicking it opens a list of themes.
/// When one is selected, the theme select control displays the selected theme.
#[derive(IntoElement)]
pub struct Popover {
children: SmallVec<[AnyElement; 2]>,
aside: Option<AnyElement>,
}
impl RenderOnce for Popover {
type Rendered = Div;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
div()
.flex()
.gap_1()
.child(v_stack().elevation_2(cx).px_1().children(self.children))
.when_some(self.aside, |this, aside| {
this.child(
v_stack()
.elevation_2(cx)
.bg(cx.theme().colors().surface_background)
.px_1()
.child(aside),
)
})
}
}
impl Popover {
pub fn new() -> Self {
Self {
children: SmallVec::new(),
aside: None,
}
}
pub fn aside(mut self, aside: impl IntoElement) -> Self
where
Self: Sized,
{
self.aside = Some(aside.into_element().into_any());
self
}
}
impl ParentElement for Popover {
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
&mut self.children
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
use gpui::SharedString;
use gpui::{ImageSource, SharedString};
use crate::Icon;
@@ -9,6 +9,6 @@ use crate::Icon;
/// Can be filled with a []
pub enum GraphicSlot {
Icon(Icon),
Avatar(SharedString),
Avatar(ImageSource),
PublicActor(SharedString),
}
+2 -2
View File
@@ -5,13 +5,13 @@ use crate::StyledExt;
/// Horizontally stacks elements.
///
/// Sets `flex()`, `flex_row()`, `items_center()`
pub fn h_stack<V: 'static>() -> Div<V> {
pub fn h_stack() -> Div {
div().h_flex()
}
/// Vertically stacks elements.
///
/// Sets `flex()`, `flex_col()`
pub fn v_stack<V: 'static>() -> Div<V> {
pub fn v_stack() -> Div {
div().v_flex()
}
+19
View File
@@ -0,0 +1,19 @@
mod avatar;
mod button;
mod checkbox;
mod context_menu;
mod icon;
mod input;
mod keybinding;
mod label;
mod list_item;
pub use avatar::*;
pub use button::*;
pub use checkbox::*;
pub use context_menu::*;
pub use icon::*;
pub use input::*;
pub use keybinding::*;
pub use label::*;
pub use list_item::*;
@@ -0,0 +1,23 @@
use gpui::{Div, Render};
use story::Story;
use crate::prelude::*;
use crate::Avatar;
pub struct AvatarStory;
impl Render for AvatarStory {
type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container()
.child(Story::title_for::<Avatar>())
.child(Story::label("Default"))
.child(Avatar::uri(
"https://avatars.githubusercontent.com/u/1714999?v=4",
))
.child(Avatar::uri(
"https://avatars.githubusercontent.com/u/326587?v=4",
))
}
}
+145
View File
@@ -0,0 +1,145 @@
use gpui::{rems, Div, Render};
use story::Story;
use strum::IntoEnumIterator;
use crate::prelude::*;
use crate::{h_stack, v_stack, Button, Icon, IconPosition, Label};
pub struct ButtonStory;
impl Render for ButtonStory {
type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let states = InteractionState::iter();
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.")),
)
}
}
@@ -0,0 +1,49 @@
use gpui::{Div, Render, ViewContext};
use story::Story;
use crate::prelude::*;
use crate::{h_stack, Checkbox};
pub struct CheckboxStory;
impl Render for CheckboxStory {
type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container()
.child(Story::title_for::<Checkbox>())
.child(Story::label("Default"))
.child(
h_stack()
.p_2()
.gap_2()
.rounded_md()
.border()
.border_color(cx.theme().colors().border)
.child(Checkbox::new("checkbox-enabled", Selection::Unselected))
.child(Checkbox::new(
"checkbox-intermediate",
Selection::Indeterminate,
))
.child(Checkbox::new("checkbox-selected", Selection::Selected)),
)
.child(Story::label("Disabled"))
.child(
h_stack()
.p_2()
.gap_2()
.rounded_md()
.border()
.border_color(cx.theme().colors().border)
.child(Checkbox::new("checkbox-disabled", Selection::Unselected).disabled(true))
.child(
Checkbox::new("checkbox-disabled-intermediate", Selection::Indeterminate)
.disabled(true),
)
.child(
Checkbox::new("checkbox-disabled-selected", Selection::Selected)
.disabled(true),
),
)
}
}
@@ -0,0 +1,104 @@
use gpui::{actions, Action, AnchorCorner, Div, Render, View};
use story::Story;
use crate::prelude::*;
use crate::{menu_handle, ContextMenu, Label};
actions!(PrintCurrentDate, PrintBestFood);
fn build_menu(cx: &mut WindowContext, header: impl Into<SharedString>) -> View<ContextMenu> {
ContextMenu::build(cx, |menu, _| {
menu.header(header)
.separator()
.entry("Print current time", |v, cx| {
println!("dispatching PrintCurrentTime action");
cx.dispatch_action(PrintCurrentDate.boxed_clone())
})
.entry("Print best foot", |v, cx| {
cx.dispatch_action(PrintBestFood.boxed_clone())
})
})
}
pub struct ContextMenuStory;
impl Render for ContextMenuStory {
type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container()
.on_action(|_: &PrintCurrentDate, _| {
println!("printing unix time!");
if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() {
println!("Current Unix time is {:?}", unix_time.as_secs());
}
})
.on_action(|_: &PrintBestFood, _| {
println!("burrito");
})
.flex()
.flex_row()
.justify_between()
.child(
div()
.flex()
.flex_col()
.justify_between()
.child(
menu_handle("test2")
.child(|is_open| {
Label::new(if is_open {
"TOP LEFT"
} else {
"RIGHT CLICK ME"
})
})
.menu(move |cx| build_menu(cx, "top left")),
)
.child(
menu_handle("test1")
.child(|is_open| {
Label::new(if is_open {
"BOTTOM LEFT"
} else {
"RIGHT CLICK ME"
})
})
.anchor(AnchorCorner::BottomLeft)
.attach(AnchorCorner::TopLeft)
.menu(move |cx| build_menu(cx, "bottom left")),
),
)
.child(
div()
.flex()
.flex_col()
.justify_between()
.child(
menu_handle("test3")
.child(|is_open| {
Label::new(if is_open {
"TOP RIGHT"
} else {
"RIGHT CLICK ME"
})
})
.anchor(AnchorCorner::TopRight)
.menu(move |cx| build_menu(cx, "top right")),
)
.child(
menu_handle("test4")
.child(|is_open| {
Label::new(if is_open {
"BOTTOM RIGHT"
} else {
"RIGHT CLICK ME"
})
})
.anchor(AnchorCorner::BottomRight)
.attach(AnchorCorner::TopRight)
.menu(move |cx| build_menu(cx, "bottom right")),
),
)
}
}
+21
View File
@@ -0,0 +1,21 @@
use gpui::{Div, Render};
use story::Story;
use strum::IntoEnumIterator;
use crate::prelude::*;
use crate::{Icon, IconElement};
pub struct IconStory;
impl Render for IconStory {
type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let icons = Icon::iter();
Story::container()
.child(Story::title_for::<IconElement>())
.child(Story::label("All Icons"))
.child(div().flex().gap_3().children(icons.map(IconElement::new)))
}
}
@@ -0,0 +1,18 @@
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")))
}
}
@@ -0,0 +1,61 @@
use gpui::{actions, Div, Render};
use itertools::Itertools;
use story::Story;
use crate::prelude::*;
use crate::KeyBinding;
pub struct KeybindingStory;
actions!(NoAction);
pub fn binding(key: &str) -> gpui::KeyBinding {
gpui::KeyBinding::new(key, NoAction {}, None)
}
impl Render for KeybindingStory {
type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let all_modifier_permutations = ["ctrl", "alt", "cmd", "shift"].into_iter().permutations(2);
Story::container()
.child(Story::title_for::<KeyBinding>())
.child(Story::label("Single Key"))
.child(KeyBinding::new(binding("Z")))
.child(Story::label("Single Key with Modifier"))
.child(
div()
.flex()
.gap_3()
.child(KeyBinding::new(binding("ctrl-c")))
.child(KeyBinding::new(binding("alt-c")))
.child(KeyBinding::new(binding("cmd-c")))
.child(KeyBinding::new(binding("shift-c"))),
)
.child(Story::label("Single Key with Modifier (Permuted)"))
.child(
div().flex().flex_col().children(
all_modifier_permutations
.chunks(4)
.into_iter()
.map(|chunk| {
div()
.flex()
.gap_4()
.py_3()
.children(chunk.map(|permutation| {
KeyBinding::new(binding(&*(permutation.join("-") + "-x")))
}))
}),
),
)
.child(Story::label("Single Key with All Modifiers"))
.child(KeyBinding::new(binding("ctrl-alt-cmd-shift-z")))
.child(Story::label("Chord"))
.child(KeyBinding::new(binding("a z")))
.child(Story::label("Chord with Modifier"))
.child(KeyBinding::new(binding("ctrl-a shift-z")))
.child(KeyBinding::new(binding("fn-s")))
}
}
@@ -0,0 +1,27 @@
use gpui::{Div, Render};
use story::Story;
use crate::prelude::*;
use crate::{HighlightedLabel, Label};
pub struct LabelStory;
impl Render for LabelStory {
type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container()
.child(Story::title_for::<Label>())
.child(Story::label("Default"))
.child(Label::new("Hello, world!"))
.child(Story::label("Highlighted"))
.child(HighlightedLabel::new(
"Hello, world!",
vec![0, 1, 2, 7, 8, 12],
))
.child(HighlightedLabel::new(
"Héllo, world!",
vec![0, 1, 3, 8, 9, 13],
))
}
}
@@ -0,0 +1,34 @@
use gpui::{Div, Render};
use story::Story;
use crate::prelude::*;
use crate::ListItem;
pub struct ListItemStory;
impl Render for ListItemStory {
type Element = Div;
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 `on_click`"))
.child(
ListItem::new("with_on_click")
.child("Click me")
.on_click(|_event, _cx| {
println!("Clicked!");
}),
)
.child(Story::label("With `on_secondary_mouse_down`"))
.child(
ListItem::new("with_on_secondary_mouse_down").on_secondary_mouse_down(
|_event, _cx| {
println!("Right mouse down!");
},
),
)
}
}
-272
View File
@@ -1,272 +0,0 @@
use crate::prelude::*;
use crate::{Icon, IconElement, Label, TextColor};
use gpui::{prelude::*, red, Div, ElementId, Render, View};
#[derive(Component, Clone)]
pub struct Tab {
id: ElementId,
title: String,
icon: Option<Icon>,
current: bool,
dirty: bool,
fs_status: FileSystemStatus,
git_status: GitStatus,
diagnostic_status: DiagnosticStatus,
close_side: IconSide,
}
#[derive(Clone, Debug)]
struct TabDragState {
title: String,
}
impl Render for TabDragState {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
div().w_8().h_4().bg(red())
}
}
impl Tab {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
title: "untitled".to_string(),
icon: None,
current: false,
dirty: false,
fs_status: FileSystemStatus::None,
git_status: GitStatus::None,
diagnostic_status: DiagnosticStatus::None,
close_side: IconSide::Right,
}
}
pub fn current(mut self, current: bool) -> Self {
self.current = current;
self
}
pub fn title(mut self, title: String) -> Self {
self.title = title;
self
}
pub fn icon<I>(mut self, icon: I) -> Self
where
I: Into<Option<Icon>>,
{
self.icon = icon.into();
self
}
pub fn dirty(mut self, dirty: bool) -> Self {
self.dirty = dirty;
self
}
pub fn fs_status(mut self, fs_status: FileSystemStatus) -> Self {
self.fs_status = fs_status;
self
}
pub fn git_status(mut self, git_status: GitStatus) -> Self {
self.git_status = git_status;
self
}
pub fn diagnostic_status(mut self, diagnostic_status: DiagnosticStatus) -> Self {
self.diagnostic_status = diagnostic_status;
self
}
pub fn close_side(mut self, close_side: IconSide) -> Self {
self.close_side = close_side;
self
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
let has_fs_conflict = self.fs_status == FileSystemStatus::Conflict;
let is_deleted = self.fs_status == FileSystemStatus::Deleted;
let label = match (self.git_status, is_deleted) {
(_, true) | (GitStatus::Deleted, false) => Label::new(self.title.clone())
.color(TextColor::Hidden)
.set_strikethrough(true),
(GitStatus::None, false) => Label::new(self.title.clone()),
(GitStatus::Created, false) => Label::new(self.title.clone()).color(TextColor::Created),
(GitStatus::Modified, false) => {
Label::new(self.title.clone()).color(TextColor::Modified)
}
(GitStatus::Renamed, false) => Label::new(self.title.clone()).color(TextColor::Accent),
(GitStatus::Conflict, false) => Label::new(self.title.clone()),
};
let close_icon = || IconElement::new(Icon::Close).color(TextColor::Muted);
let (tab_bg, tab_hover_bg, tab_active_bg) = match self.current {
false => (
cx.theme().colors().tab_inactive_background,
cx.theme().colors().ghost_element_hover,
cx.theme().colors().ghost_element_active,
),
true => (
cx.theme().colors().tab_active_background,
cx.theme().colors().element_hover,
cx.theme().colors().element_active,
),
};
let drag_state = TabDragState {
title: self.title.clone(),
};
div()
.id(self.id.clone())
.on_drag(move |_view, cx| cx.build_view(|cx| drag_state.clone()))
.drag_over::<TabDragState>(|d| d.bg(cx.theme().colors().drop_target_background))
.on_drop(|_view, state: View<TabDragState>, cx| {
eprintln!("{:?}", state.read(cx));
})
.px_2()
.py_0p5()
.flex()
.items_center()
.justify_center()
.bg(tab_bg)
.hover(|h| h.bg(tab_hover_bg))
.active(|a| a.bg(tab_active_bg))
.child(
div()
.px_1()
.flex()
.items_center()
.gap_1p5()
.children(has_fs_conflict.then(|| {
IconElement::new(Icon::ExclamationTriangle)
.size(crate::IconSize::Small)
.color(TextColor::Warning)
}))
.children(self.icon.map(IconElement::new))
.children(if self.close_side == IconSide::Left {
Some(close_icon())
} else {
None
})
.child(label)
.children(if self.close_side == IconSide::Right {
Some(close_icon())
} else {
None
}),
)
}
}
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use super::*;
use crate::{h_stack, v_stack, Icon, Story};
use strum::IntoEnumIterator;
pub struct TabStory;
impl Render for TabStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let git_statuses = GitStatus::iter();
let fs_statuses = FileSystemStatus::iter();
Story::container(cx)
.child(Story::title_for::<_, Tab>(cx))
.child(
h_stack().child(
v_stack()
.gap_2()
.child(Story::label(cx, "Default"))
.child(Tab::new("default")),
),
)
.child(
h_stack().child(
v_stack().gap_2().child(Story::label(cx, "Current")).child(
h_stack()
.gap_4()
.child(
Tab::new("current")
.title("Current".to_string())
.current(true),
)
.child(
Tab::new("not_current")
.title("Not Current".to_string())
.current(false),
),
),
),
)
.child(
h_stack().child(
v_stack()
.gap_2()
.child(Story::label(cx, "Titled"))
.child(Tab::new("titled").title("label".to_string())),
),
)
.child(
h_stack().child(
v_stack()
.gap_2()
.child(Story::label(cx, "With Icon"))
.child(
Tab::new("with_icon")
.title("label".to_string())
.icon(Some(Icon::Envelope)),
),
),
)
.child(
h_stack().child(
v_stack()
.gap_2()
.child(Story::label(cx, "Close Side"))
.child(
h_stack()
.gap_4()
.child(
Tab::new("left")
.title("Left".to_string())
.close_side(IconSide::Left),
)
.child(Tab::new("right").title("Right".to_string())),
),
),
)
.child(
v_stack()
.gap_2()
.child(Story::label(cx, "Git Status"))
.child(h_stack().gap_4().children(git_statuses.map(|git_status| {
Tab::new("git_status")
.title(git_status.to_string())
.git_status(git_status)
}))),
)
.child(
v_stack()
.gap_2()
.child(Story::label(cx, "File System Status"))
.child(h_stack().gap_4().children(fs_statuses.map(|fs_status| {
Tab::new("file_system_status")
.title(fs_status.to_string())
.fs_status(fs_status)
}))),
)
}
}
}
-90
View File
@@ -1,90 +0,0 @@
use crate::prelude::*;
use gpui::{prelude::*, AnyElement};
use smallvec::SmallVec;
#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
pub enum ToastOrigin {
#[default]
Bottom,
BottomRight,
}
/// Don't use toast directly:
///
/// - For messages with a required action, use a `NotificationToast`.
/// - For messages that convey information, use a `StatusToast`.
///
/// A toast is a small, temporary window that appears to show a message to the user
/// or indicate a required action.
///
/// Toasts should not persist on the screen for more than a few seconds unless
/// they are actively showing the a process in progress.
///
/// Only one toast may be visible at a time.
#[derive(Component)]
pub struct Toast<V: 'static> {
origin: ToastOrigin,
children: SmallVec<[AnyElement<V>; 2]>,
}
impl<V: 'static> Toast<V> {
pub fn new(origin: ToastOrigin) -> Self {
Self {
origin,
children: SmallVec::new(),
}
}
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
let mut div = div();
if self.origin == ToastOrigin::Bottom {
div = div.right_1_2();
} else {
div = div.right_2();
}
div.z_index(5)
.absolute()
.bottom_9()
.flex()
.py_1()
.px_1p5()
.rounded_lg()
.shadow_md()
.overflow_hidden()
.bg(cx.theme().colors().elevated_surface_background)
.children(self.children)
}
}
impl<V: 'static> ParentComponent<V> for Toast<V> {
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
&mut self.children
}
}
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use gpui::{Div, Render};
use crate::{Label, Story};
use super::*;
pub struct ToastStory;
impl Render for ToastStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container(cx)
.child(Story::title_for::<_, Toast<Self>>(cx))
.child(Story::label(cx, "Default"))
.child(Toast::new(ToastOrigin::Bottom).child(Label::new("label")))
}
}
}
-20
View File
@@ -1,7 +1,3 @@
use gpui::{div, Component, ParentComponent};
use crate::{Icon, IconElement, IconSize, TextColor};
/// 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.
@@ -43,19 +39,3 @@ impl From<bool> for Toggle {
Toggle::Toggled(toggled)
}
}
pub fn disclosure_control<V: 'static>(toggle: Toggle) -> impl Component<V> {
match (toggle.is_toggleable(), toggle.is_toggled()) {
(false, _) => div(),
(_, true) => div().child(
IconElement::new(Icon::ChevronDown)
.color(TextColor::Muted)
.size(IconSize::Small),
),
(_, false) => div().child(
IconElement::new(Icon::ChevronRight)
.color(TextColor::Muted)
.size(IconSize::Small),
),
}
}
-14
View File
@@ -1,14 +0,0 @@
use crate::prelude::*;
#[derive(Component)]
pub struct ToolDivider;
impl ToolDivider {
pub fn new() -> Self {
Self
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
div().w_px().h_3().bg(cx.theme().colors().border)
}
}
+4 -8
View File
@@ -1,9 +1,9 @@
use gpui::{overlay, Action, AnyView, Overlay, Render, VisualContext};
use gpui::{overlay, Action, AnyView, IntoElement, Overlay, Render, VisualContext};
use settings2::Settings;
use theme2::{ActiveTheme, ThemeSettings};
use crate::prelude::*;
use crate::{h_stack, v_stack, KeyBinding, Label, LabelSize, StyledExt, TextColor};
use crate::{h_stack, v_stack, Color, KeyBinding, Label, LabelSize, StyledExt};
pub struct Tooltip {
title: SharedString,
@@ -68,7 +68,7 @@ impl Tooltip {
}
impl Render for Tooltip {
type Element = Overlay<Self>;
type Element = Overlay;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
@@ -90,11 +90,7 @@ impl Render for Tooltip {
}),
)
.when_some(self.meta.clone(), |this, meta| {
this.child(
Label::new(meta)
.size(LabelSize::Small)
.color(TextColor::Muted),
)
this.child(Label::new(meta).size(LabelSize::Small).color(Color::Muted))
}),
),
)