Merge remote-tracking branch 'origin/main' into surfaces
# Conflicts: # crates/ui2/src/components/avatar.rs
This commit is contained in:
commit
cc0bc444b1
139 changed files with 8384 additions and 6398 deletions
7
crates/ui2/src/clickable.rs
Normal file
7
crates/ui2/src/clickable.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
use gpui::{ClickEvent, WindowContext};
|
||||
|
||||
/// A trait for elements that can be clicked.
|
||||
pub trait Clickable {
|
||||
/// Sets the click handler that will fire whenever the element is clicked.
|
||||
fn on_click(self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self;
|
||||
}
|
|
@ -5,15 +5,11 @@ mod context_menu;
|
|||
mod disclosure;
|
||||
mod divider;
|
||||
mod icon;
|
||||
mod icon_button;
|
||||
mod input;
|
||||
mod keybinding;
|
||||
mod label;
|
||||
mod list;
|
||||
mod popover;
|
||||
mod slot;
|
||||
mod stack;
|
||||
mod toggle;
|
||||
mod tooltip;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
|
@ -26,15 +22,11 @@ pub use context_menu::*;
|
|||
pub use disclosure::*;
|
||||
pub use divider::*;
|
||||
pub use icon::*;
|
||||
pub use icon_button::*;
|
||||
pub use input::*;
|
||||
pub use keybinding::*;
|
||||
pub use label::*;
|
||||
pub use list::*;
|
||||
pub use popover::*;
|
||||
pub use slot::*;
|
||||
pub use stack::*;
|
||||
pub use toggle::*;
|
||||
pub use tooltip::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use crate::prelude::*;
|
||||
use gpui::{img, ImageData, ImageSource, Img, IntoElement};
|
||||
use gpui::{img, rems, Div, ImageData, ImageSource, IntoElement, Styled};
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Clone)]
|
||||
pub enum Shape {
|
||||
|
@ -13,13 +13,14 @@ pub enum Shape {
|
|||
#[derive(IntoElement)]
|
||||
pub struct Avatar {
|
||||
src: ImageSource,
|
||||
is_available: Option<bool>,
|
||||
shape: Shape,
|
||||
}
|
||||
|
||||
impl RenderOnce for Avatar {
|
||||
type Rendered = Img;
|
||||
type Rendered = Div;
|
||||
|
||||
fn render(self, _: &mut WindowContext) -> Self::Rendered {
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
let mut img = img(self.src);
|
||||
|
||||
if self.shape == Shape::Circle {
|
||||
|
@ -28,9 +29,28 @@ impl RenderOnce for Avatar {
|
|||
img = img.rounded_md();
|
||||
}
|
||||
|
||||
img.size_4()
|
||||
// todo!(Pull the avatar fallback background from the theme.)
|
||||
.bg(gpui::red())
|
||||
let size = rems(1.0);
|
||||
|
||||
div()
|
||||
.size(size)
|
||||
.child(
|
||||
img.size(size)
|
||||
// todo!(Pull the avatar fallback background from the theme.)
|
||||
.bg(gpui::red()),
|
||||
)
|
||||
.children(self.is_available.map(|is_free| {
|
||||
// HACK: non-integer sizes result in oval indicators.
|
||||
let indicator_size = (size.0 * cx.rem_size() * 0.4).round();
|
||||
|
||||
div()
|
||||
.absolute()
|
||||
.z_index(1)
|
||||
.bg(if is_free { gpui::green() } else { gpui::red() })
|
||||
.size(indicator_size)
|
||||
.rounded(indicator_size)
|
||||
.bottom_0()
|
||||
.right_0()
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -39,17 +59,30 @@ impl Avatar {
|
|||
Self {
|
||||
src: src.into().into(),
|
||||
shape: Shape::Circle,
|
||||
is_available: None,
|
||||
}
|
||||
}
|
||||
pub fn data(src: Arc<ImageData>) -> Self {
|
||||
Self {
|
||||
src: src.into(),
|
||||
shape: Shape::Circle,
|
||||
is_available: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn source(src: ImageSource) -> Self {
|
||||
Self {
|
||||
src,
|
||||
shape: Shape::Circle,
|
||||
is_available: None,
|
||||
}
|
||||
}
|
||||
pub fn shape(mut self, shape: Shape) -> Self {
|
||||
self.shape = shape;
|
||||
self
|
||||
}
|
||||
pub fn availability_indicator(mut self, is_available: impl Into<Option<bool>>) -> Self {
|
||||
self.is_available = is_available.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,233 +0,0 @@
|
|||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
DefiniteLength, Div, Hsla, IntoElement, MouseButton, MouseDownEvent,
|
||||
StatefulInteractiveElement, WindowContext,
|
||||
};
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{h_stack, Color, Icon, IconButton, IconElement, Label, LineHeightStyle};
|
||||
|
||||
/// Provides the flexibility to use either a standard
|
||||
/// button or an icon button in a given context.
|
||||
pub enum ButtonOrIconButton {
|
||||
Button(Button),
|
||||
IconButton(IconButton),
|
||||
}
|
||||
|
||||
impl From<Button> for ButtonOrIconButton {
|
||||
fn from(value: Button) -> Self {
|
||||
Self::Button(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IconButton> for ButtonOrIconButton {
|
||||
fn from(value: IconButton) -> Self {
|
||||
Self::IconButton(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Clone, Copy)]
|
||||
pub enum IconPosition {
|
||||
#[default]
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Default, Copy, Clone, PartialEq)]
|
||||
pub enum ButtonVariant {
|
||||
#[default]
|
||||
Ghost,
|
||||
Filled,
|
||||
}
|
||||
|
||||
impl ButtonVariant {
|
||||
pub fn bg_color(&self, cx: &mut WindowContext) -> Hsla {
|
||||
match self {
|
||||
ButtonVariant::Ghost => cx.theme().colors().ghost_element_background,
|
||||
ButtonVariant::Filled => cx.theme().colors().element_background,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bg_color_hover(&self, cx: &mut WindowContext) -> Hsla {
|
||||
match self {
|
||||
ButtonVariant::Ghost => cx.theme().colors().ghost_element_hover,
|
||||
ButtonVariant::Filled => cx.theme().colors().element_hover,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bg_color_active(&self, cx: &mut WindowContext) -> Hsla {
|
||||
match self {
|
||||
ButtonVariant::Ghost => cx.theme().colors().ghost_element_active,
|
||||
ButtonVariant::Filled => cx.theme().colors().element_active,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Button {
|
||||
disabled: bool,
|
||||
click_handler: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext)>>,
|
||||
icon: Option<Icon>,
|
||||
icon_position: Option<IconPosition>,
|
||||
label: SharedString,
|
||||
variant: ButtonVariant,
|
||||
width: Option<DefiniteLength>,
|
||||
color: Option<Color>,
|
||||
}
|
||||
|
||||
impl RenderOnce for Button {
|
||||
type Rendered = gpui::Stateful<Div>;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
let (icon_color, label_color) = match (self.disabled, self.color) {
|
||||
(true, _) => (Color::Disabled, Color::Disabled),
|
||||
(_, None) => (Color::Default, Color::Default),
|
||||
(_, Some(color)) => (Color::from(color), color),
|
||||
};
|
||||
|
||||
let mut button = h_stack()
|
||||
.id(SharedString::from(format!("{}", self.label)))
|
||||
.relative()
|
||||
.p_1()
|
||||
.text_ui()
|
||||
.rounded_md()
|
||||
.bg(self.variant.bg_color(cx))
|
||||
.cursor_pointer()
|
||||
.hover(|style| style.bg(self.variant.bg_color_hover(cx)))
|
||||
.active(|style| style.bg(self.variant.bg_color_active(cx)));
|
||||
|
||||
match (self.icon, self.icon_position) {
|
||||
(Some(_), Some(IconPosition::Left)) => {
|
||||
button = button
|
||||
.gap_1()
|
||||
.child(self.render_label(label_color))
|
||||
.children(self.render_icon(icon_color))
|
||||
}
|
||||
(Some(_), Some(IconPosition::Right)) => {
|
||||
button = button
|
||||
.gap_1()
|
||||
.children(self.render_icon(icon_color))
|
||||
.child(self.render_label(label_color))
|
||||
}
|
||||
(_, _) => button = button.child(self.render_label(label_color)),
|
||||
}
|
||||
|
||||
if let Some(width) = self.width {
|
||||
button = button.w(width).justify_center();
|
||||
}
|
||||
|
||||
if let Some(click_handler) = self.click_handler.clone() {
|
||||
button = button.on_mouse_down(MouseButton::Left, move |event, cx| {
|
||||
click_handler(event, cx);
|
||||
});
|
||||
}
|
||||
|
||||
button
|
||||
}
|
||||
}
|
||||
|
||||
impl Button {
|
||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
disabled: false,
|
||||
click_handler: None,
|
||||
icon: None,
|
||||
icon_position: None,
|
||||
label: label.into(),
|
||||
variant: Default::default(),
|
||||
width: Default::default(),
|
||||
color: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ghost(label: impl Into<SharedString>) -> Self {
|
||||
Self::new(label).variant(ButtonVariant::Ghost)
|
||||
}
|
||||
|
||||
pub fn variant(mut self, variant: ButtonVariant) -> Self {
|
||||
self.variant = variant;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn icon(mut self, icon: Icon) -> Self {
|
||||
self.icon = Some(icon);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn icon_position(mut self, icon_position: IconPosition) -> Self {
|
||||
if self.icon.is_none() {
|
||||
panic!("An icon must be present if an icon_position is provided.");
|
||||
}
|
||||
self.icon_position = Some(icon_position);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn width(mut self, width: Option<DefiniteLength>) -> Self {
|
||||
self.width = width;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
handler: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
|
||||
) -> Self {
|
||||
self.click_handler = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: Option<Color>) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn label_color(&self, color: Option<Color>) -> Color {
|
||||
if self.disabled {
|
||||
Color::Disabled
|
||||
} else if let Some(color) = color {
|
||||
color
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_label(&self, color: Color) -> Label {
|
||||
Label::new(self.label.clone())
|
||||
.color(color)
|
||||
.line_height_style(LineHeightStyle::UILabel)
|
||||
}
|
||||
|
||||
fn render_icon(&self, icon_color: Color) -> Option<IconElement> {
|
||||
self.icon.map(|i| IconElement::new(i).color(icon_color))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ButtonGroup {
|
||||
buttons: Vec<Button>,
|
||||
}
|
||||
|
||||
impl RenderOnce for ButtonGroup {
|
||||
type Rendered = Div;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
let mut group = h_stack();
|
||||
|
||||
for button in self.buttons.into_iter() {
|
||||
group = group.child(button.render(cx));
|
||||
}
|
||||
|
||||
group
|
||||
}
|
||||
}
|
||||
|
||||
impl ButtonGroup {
|
||||
pub fn new(buttons: Vec<Button>) -> Self {
|
||||
Self { buttons }
|
||||
}
|
||||
}
|
91
crates/ui2/src/components/button/button.rs
Normal file
91
crates/ui2/src/components/button/button.rs
Normal file
|
@ -0,0 +1,91 @@
|
|||
use gpui::AnyView;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{ButtonCommon, ButtonLike, ButtonSize2, ButtonStyle2, Label, LineHeightStyle};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Button {
|
||||
base: ButtonLike,
|
||||
label: SharedString,
|
||||
label_color: Option<Color>,
|
||||
}
|
||||
|
||||
impl Button {
|
||||
pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
base: ButtonLike::new(id),
|
||||
label: label.into(),
|
||||
label_color: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn color(mut self, label_color: impl Into<Option<Color>>) -> Self {
|
||||
self.label_color = label_color.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Selectable for Button {
|
||||
fn selected(mut self, selected: bool) -> Self {
|
||||
self.base = self.base.selected(selected);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Disableable for Button {
|
||||
fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.base = self.base.disabled(disabled);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Clickable for Button {
|
||||
fn on_click(
|
||||
mut self,
|
||||
handler: impl Fn(&gpui::ClickEvent, &mut WindowContext) + 'static,
|
||||
) -> Self {
|
||||
self.base = self.base.on_click(handler);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ButtonCommon for Button {
|
||||
fn id(&self) -> &ElementId {
|
||||
self.base.id()
|
||||
}
|
||||
|
||||
fn style(mut self, style: ButtonStyle2) -> Self {
|
||||
self.base = self.base.style(style);
|
||||
self
|
||||
}
|
||||
|
||||
fn size(mut self, size: ButtonSize2) -> Self {
|
||||
self.base = self.base.size(size);
|
||||
self
|
||||
}
|
||||
|
||||
fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
|
||||
self.base = self.base.tooltip(tooltip);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Button {
|
||||
type Rendered = ButtonLike;
|
||||
|
||||
fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
|
||||
let label_color = if self.base.disabled {
|
||||
Color::Disabled
|
||||
} else if self.base.selected {
|
||||
Color::Selected
|
||||
} else {
|
||||
Color::Default
|
||||
};
|
||||
|
||||
self.base.child(
|
||||
Label::new(self.label)
|
||||
.color(label_color)
|
||||
.line_height_style(LineHeightStyle::UILabel),
|
||||
)
|
||||
}
|
||||
}
|
273
crates/ui2/src/components/button/button_like.rs
Normal file
273
crates/ui2/src/components/button/button_like.rs
Normal file
|
@ -0,0 +1,273 @@
|
|||
use gpui::{rems, AnyElement, AnyView, ClickEvent, Div, Hsla, Rems, Stateful};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::h_stack;
|
||||
use crate::prelude::*;
|
||||
|
||||
pub trait ButtonCommon: Clickable + Disableable {
|
||||
fn id(&self) -> &ElementId;
|
||||
fn style(self, style: ButtonStyle2) -> Self;
|
||||
fn size(self, size: ButtonSize2) -> Self;
|
||||
fn tooltip(self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self;
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
|
||||
pub enum ButtonStyle2 {
|
||||
#[default]
|
||||
Filled,
|
||||
// Tinted,
|
||||
Subtle,
|
||||
Transparent,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ButtonStyle {
|
||||
pub background: Hsla,
|
||||
pub border_color: Hsla,
|
||||
pub label_color: Hsla,
|
||||
pub icon_color: Hsla,
|
||||
}
|
||||
|
||||
impl ButtonStyle2 {
|
||||
pub fn enabled(self, cx: &mut WindowContext) -> ButtonStyle {
|
||||
match self {
|
||||
ButtonStyle2::Filled => ButtonStyle {
|
||||
background: cx.theme().colors().element_background,
|
||||
border_color: gpui::transparent_black(),
|
||||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle2::Subtle => ButtonStyle {
|
||||
background: cx.theme().colors().ghost_element_background,
|
||||
border_color: gpui::transparent_black(),
|
||||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle2::Transparent => ButtonStyle {
|
||||
background: gpui::transparent_black(),
|
||||
border_color: gpui::transparent_black(),
|
||||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hovered(self, cx: &mut WindowContext) -> ButtonStyle {
|
||||
match self {
|
||||
ButtonStyle2::Filled => ButtonStyle {
|
||||
background: cx.theme().colors().element_hover,
|
||||
border_color: gpui::transparent_black(),
|
||||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle2::Subtle => ButtonStyle {
|
||||
background: cx.theme().colors().ghost_element_hover,
|
||||
border_color: gpui::transparent_black(),
|
||||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle2::Transparent => ButtonStyle {
|
||||
background: gpui::transparent_black(),
|
||||
border_color: gpui::transparent_black(),
|
||||
// TODO: These are not great
|
||||
label_color: Color::Muted.color(cx),
|
||||
// TODO: These are not great
|
||||
icon_color: Color::Muted.color(cx),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn active(self, cx: &mut WindowContext) -> ButtonStyle {
|
||||
match self {
|
||||
ButtonStyle2::Filled => ButtonStyle {
|
||||
background: cx.theme().colors().element_active,
|
||||
border_color: gpui::transparent_black(),
|
||||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle2::Subtle => ButtonStyle {
|
||||
background: cx.theme().colors().ghost_element_active,
|
||||
border_color: gpui::transparent_black(),
|
||||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle2::Transparent => ButtonStyle {
|
||||
background: gpui::transparent_black(),
|
||||
border_color: gpui::transparent_black(),
|
||||
// TODO: These are not great
|
||||
label_color: Color::Muted.color(cx),
|
||||
// TODO: These are not great
|
||||
icon_color: Color::Muted.color(cx),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focused(self, cx: &mut WindowContext) -> ButtonStyle {
|
||||
match self {
|
||||
ButtonStyle2::Filled => ButtonStyle {
|
||||
background: cx.theme().colors().element_background,
|
||||
border_color: cx.theme().colors().border_focused,
|
||||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle2::Subtle => ButtonStyle {
|
||||
background: cx.theme().colors().ghost_element_background,
|
||||
border_color: cx.theme().colors().border_focused,
|
||||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle2::Transparent => ButtonStyle {
|
||||
background: gpui::transparent_black(),
|
||||
border_color: cx.theme().colors().border_focused,
|
||||
label_color: Color::Accent.color(cx),
|
||||
icon_color: Color::Accent.color(cx),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disabled(self, cx: &mut WindowContext) -> ButtonStyle {
|
||||
match self {
|
||||
ButtonStyle2::Filled => ButtonStyle {
|
||||
background: cx.theme().colors().element_disabled,
|
||||
border_color: cx.theme().colors().border_disabled,
|
||||
label_color: Color::Disabled.color(cx),
|
||||
icon_color: Color::Disabled.color(cx),
|
||||
},
|
||||
ButtonStyle2::Subtle => ButtonStyle {
|
||||
background: cx.theme().colors().ghost_element_disabled,
|
||||
border_color: cx.theme().colors().border_disabled,
|
||||
label_color: Color::Disabled.color(cx),
|
||||
icon_color: Color::Disabled.color(cx),
|
||||
},
|
||||
ButtonStyle2::Transparent => ButtonStyle {
|
||||
background: gpui::transparent_black(),
|
||||
border_color: gpui::transparent_black(),
|
||||
label_color: Color::Disabled.color(cx),
|
||||
icon_color: Color::Disabled.color(cx),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Clone, Copy)]
|
||||
pub enum ButtonSize2 {
|
||||
#[default]
|
||||
Default,
|
||||
Compact,
|
||||
None,
|
||||
}
|
||||
|
||||
impl ButtonSize2 {
|
||||
fn height(self) -> Rems {
|
||||
match self {
|
||||
ButtonSize2::Default => rems(22. / 16.),
|
||||
ButtonSize2::Compact => rems(18. / 16.),
|
||||
ButtonSize2::None => rems(16. / 16.),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ButtonLike {
|
||||
id: ElementId,
|
||||
pub(super) style: ButtonStyle2,
|
||||
pub(super) disabled: bool,
|
||||
pub(super) selected: bool,
|
||||
size: ButtonSize2,
|
||||
tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
|
||||
on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl ButtonLike {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
style: ButtonStyle2::default(),
|
||||
disabled: false,
|
||||
selected: false,
|
||||
size: ButtonSize2::Default,
|
||||
tooltip: None,
|
||||
children: SmallVec::new(),
|
||||
on_click: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Disableable for ButtonLike {
|
||||
fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Selectable for ButtonLike {
|
||||
fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Clickable for ButtonLike {
|
||||
fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
|
||||
self.on_click = Some(Box::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ButtonCommon for ButtonLike {
|
||||
fn id(&self) -> &ElementId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn style(mut self, style: ButtonStyle2) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
fn size(mut self, size: ButtonSize2) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
|
||||
self.tooltip = Some(Box::new(tooltip));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for ButtonLike {
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
|
||||
&mut self.children
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ButtonLike {
|
||||
type Rendered = Stateful<Div>;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
h_stack()
|
||||
.id(self.id.clone())
|
||||
.h(self.size.height())
|
||||
.rounded_md()
|
||||
.cursor_pointer()
|
||||
.gap_1()
|
||||
.px_1()
|
||||
.bg(self.style.enabled(cx).background)
|
||||
.hover(|hover| hover.bg(self.style.hovered(cx).background))
|
||||
.active(|active| active.bg(self.style.active(cx).background))
|
||||
.when_some(
|
||||
self.on_click.filter(|_| !self.disabled),
|
||||
|this, on_click| {
|
||||
this.on_click(move |event, cx| {
|
||||
cx.stop_propagation();
|
||||
(on_click)(event, cx)
|
||||
})
|
||||
},
|
||||
)
|
||||
.when_some(self.tooltip, |this, tooltip| {
|
||||
this.tooltip(move |cx| tooltip(cx))
|
||||
})
|
||||
.children(self.children)
|
||||
}
|
||||
}
|
102
crates/ui2/src/components/button/icon_button.rs
Normal file
102
crates/ui2/src/components/button/icon_button.rs
Normal file
|
@ -0,0 +1,102 @@
|
|||
use gpui::{Action, AnyView};
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{ButtonCommon, ButtonLike, ButtonSize2, ButtonStyle2, Icon, IconElement, IconSize};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct IconButton {
|
||||
base: ButtonLike,
|
||||
icon: Icon,
|
||||
icon_size: IconSize,
|
||||
icon_color: Color,
|
||||
}
|
||||
|
||||
impl IconButton {
|
||||
pub fn new(id: impl Into<ElementId>, icon: Icon) -> Self {
|
||||
Self {
|
||||
base: ButtonLike::new(id),
|
||||
icon,
|
||||
icon_size: IconSize::default(),
|
||||
icon_color: Color::Default,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icon_size(mut self, icon_size: IconSize) -> Self {
|
||||
self.icon_size = icon_size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn icon_color(mut self, icon_color: Color) -> Self {
|
||||
self.icon_color = icon_color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn action(self, action: Box<dyn Action>) -> Self {
|
||||
self.on_click(move |_event, cx| cx.dispatch_action(action.boxed_clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Disableable for IconButton {
|
||||
fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.base = self.base.disabled(disabled);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Selectable for IconButton {
|
||||
fn selected(mut self, selected: bool) -> Self {
|
||||
self.base = self.base.selected(selected);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Clickable for IconButton {
|
||||
fn on_click(
|
||||
mut self,
|
||||
handler: impl Fn(&gpui::ClickEvent, &mut WindowContext) + 'static,
|
||||
) -> Self {
|
||||
self.base = self.base.on_click(handler);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ButtonCommon for IconButton {
|
||||
fn id(&self) -> &ElementId {
|
||||
self.base.id()
|
||||
}
|
||||
|
||||
fn style(mut self, style: ButtonStyle2) -> Self {
|
||||
self.base = self.base.style(style);
|
||||
self
|
||||
}
|
||||
|
||||
fn size(mut self, size: ButtonSize2) -> Self {
|
||||
self.base = self.base.size(size);
|
||||
self
|
||||
}
|
||||
|
||||
fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
|
||||
self.base = self.base.tooltip(tooltip);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for IconButton {
|
||||
type Rendered = ButtonLike;
|
||||
|
||||
fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
|
||||
let icon_color = if self.base.disabled {
|
||||
Color::Disabled
|
||||
} else if self.base.selected {
|
||||
Color::Selected
|
||||
} else {
|
||||
self.icon_color
|
||||
};
|
||||
|
||||
self.base.child(
|
||||
IconElement::new(self.icon)
|
||||
.size(self.icon_size)
|
||||
.color(icon_color),
|
||||
)
|
||||
}
|
||||
}
|
7
crates/ui2/src/components/button/mod.rs
Normal file
7
crates/ui2/src/components/button/mod.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
mod button;
|
||||
mod button_like;
|
||||
mod icon_button;
|
||||
|
||||
pub use button::*;
|
||||
pub use button_like::*;
|
||||
pub use icon_button::*;
|
|
@ -1,7 +1,6 @@
|
|||
use gpui::{div, prelude::*, Div, Element, ElementId, IntoElement, Styled, WindowContext};
|
||||
|
||||
use theme2::ActiveTheme;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{Color, Icon, IconElement, Selection};
|
||||
|
||||
pub type CheckHandler = Box<dyn Fn(&Selection, &mut WindowContext) + 'static>;
|
||||
|
|
|
@ -1,23 +1,28 @@
|
|||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{prelude::*, v_stack, Label, List};
|
||||
use crate::{ListItem, ListSeparator, ListSubHeader};
|
||||
use gpui::{
|
||||
overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, ClickEvent, DismissEvent,
|
||||
DispatchPhase, Div, EventEmitter, FocusHandle, FocusableView, IntoElement, LayoutId,
|
||||
ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render, View, VisualContext,
|
||||
use crate::{
|
||||
h_stack, prelude::*, v_stack, KeyBinding, Label, List, ListItem, ListSeparator, ListSubHeader,
|
||||
};
|
||||
use gpui::{
|
||||
overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, DismissEvent, DispatchPhase,
|
||||
Div, EventEmitter, FocusHandle, FocusableView, IntoElement, LayoutId, ManagedView, MouseButton,
|
||||
MouseDownEvent, Pixels, Point, Render, View, VisualContext,
|
||||
};
|
||||
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
pub enum ContextMenuItem {
|
||||
Separator,
|
||||
Header(SharedString),
|
||||
Entry(SharedString, Rc<dyn Fn(&ClickEvent, &mut WindowContext)>),
|
||||
Entry {
|
||||
label: SharedString,
|
||||
handler: Rc<dyn Fn(&mut WindowContext)>,
|
||||
key_binding: Option<KeyBinding>,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct ContextMenu {
|
||||
items: Vec<ContextMenuItem>,
|
||||
focus_handle: FocusHandle,
|
||||
selected_index: Option<usize>,
|
||||
}
|
||||
|
||||
impl FocusableView for ContextMenu {
|
||||
|
@ -39,6 +44,7 @@ impl ContextMenu {
|
|||
Self {
|
||||
items: Default::default(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
selected_index: None,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
|
@ -58,27 +64,90 @@ impl ContextMenu {
|
|||
pub fn entry(
|
||||
mut self,
|
||||
label: impl Into<SharedString>,
|
||||
on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
|
||||
on_click: impl Fn(&mut WindowContext) + 'static,
|
||||
) -> Self {
|
||||
self.items
|
||||
.push(ContextMenuItem::Entry(label.into(), Rc::new(on_click)));
|
||||
self.items.push(ContextMenuItem::Entry {
|
||||
label: label.into(),
|
||||
handler: Rc::new(on_click),
|
||||
key_binding: None,
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
pub fn action(self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
|
||||
// todo: add the keybindings to the list entry
|
||||
self.entry(label.into(), move |_, cx| {
|
||||
cx.dispatch_action(action.boxed_clone())
|
||||
})
|
||||
pub fn action(
|
||||
mut self,
|
||||
label: impl Into<SharedString>,
|
||||
action: Box<dyn Action>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Self {
|
||||
self.items.push(ContextMenuItem::Entry {
|
||||
label: label.into(),
|
||||
key_binding: KeyBinding::for_action(&*action, cx),
|
||||
handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
||||
// todo!()
|
||||
cx.emit(DismissEvent::Dismiss);
|
||||
if let Some(ContextMenuItem::Entry { handler, .. }) =
|
||||
self.selected_index.and_then(|ix| self.items.get(ix))
|
||||
{
|
||||
(handler)(cx)
|
||||
}
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(DismissEvent::Dismiss);
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
|
||||
self.selected_index = self.items.iter().position(|item| item.is_selectable());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
|
||||
for (ix, item) in self.items.iter().enumerate().rev() {
|
||||
if item.is_selectable() {
|
||||
self.selected_index = Some(ix);
|
||||
cx.notify();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
|
||||
if let Some(ix) = self.selected_index {
|
||||
for (ix, item) in self.items.iter().enumerate().skip(ix + 1) {
|
||||
if item.is_selectable() {
|
||||
self.selected_index = Some(ix);
|
||||
cx.notify();
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.select_first(&Default::default(), cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
|
||||
if let Some(ix) = self.selected_index {
|
||||
for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
|
||||
if item.is_selectable() {
|
||||
self.selected_index = Some(ix);
|
||||
cx.notify();
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.select_last(&Default::default(), cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextMenuItem {
|
||||
fn is_selectable(&self) -> bool {
|
||||
matches!(self, Self::Entry { .. })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -90,38 +159,51 @@ impl Render for ContextMenu {
|
|||
v_stack()
|
||||
.min_w(px(200.))
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_mouse_down_out(
|
||||
cx.listener(|this: &mut Self, _, cx| this.cancel(&Default::default(), cx)),
|
||||
)
|
||||
// .on_action(ContextMenu::select_first)
|
||||
// .on_action(ContextMenu::select_last)
|
||||
// .on_action(ContextMenu::select_next)
|
||||
// .on_action(ContextMenu::select_prev)
|
||||
.on_mouse_down_out(cx.listener(|this, _, cx| this.cancel(&Default::default(), cx)))
|
||||
.key_context("menu")
|
||||
.on_action(cx.listener(ContextMenu::select_first))
|
||||
.on_action(cx.listener(ContextMenu::select_last))
|
||||
.on_action(cx.listener(ContextMenu::select_next))
|
||||
.on_action(cx.listener(ContextMenu::select_prev))
|
||||
.on_action(cx.listener(ContextMenu::confirm))
|
||||
.on_action(cx.listener(ContextMenu::cancel))
|
||||
.flex_none()
|
||||
// .bg(cx.theme().colors().elevated_surface_background)
|
||||
// .border()
|
||||
// .border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
List::new().children(self.items.iter().map(|item| match item {
|
||||
ContextMenuItem::Separator => ListSeparator::new().into_any_element(),
|
||||
ContextMenuItem::Header(header) => {
|
||||
ListSubHeader::new(header.clone()).into_any_element()
|
||||
}
|
||||
ContextMenuItem::Entry(entry, callback) => {
|
||||
let callback = callback.clone();
|
||||
let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent::Dismiss));
|
||||
List::new().children(self.items.iter().enumerate().map(
|
||||
|(ix, item)| match item {
|
||||
ContextMenuItem::Separator => ListSeparator.into_any_element(),
|
||||
ContextMenuItem::Header(header) => {
|
||||
ListSubHeader::new(header.clone()).into_any_element()
|
||||
}
|
||||
ContextMenuItem::Entry {
|
||||
label: entry,
|
||||
handler: callback,
|
||||
key_binding,
|
||||
} => {
|
||||
let callback = callback.clone();
|
||||
let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent));
|
||||
|
||||
ListItem::new(entry.clone())
|
||||
.child(Label::new(entry.clone()))
|
||||
.on_click(move |event, cx| {
|
||||
callback(event, cx);
|
||||
dismiss(event, cx)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
})),
|
||||
ListItem::new(entry.clone())
|
||||
.child(
|
||||
h_stack()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(Label::new(entry.clone()))
|
||||
.children(
|
||||
key_binding
|
||||
.clone()
|
||||
.map(|binding| div().ml_1().child(binding)),
|
||||
),
|
||||
)
|
||||
.selected(Some(ix) == self.selected_index)
|
||||
.on_click(move |event, cx| {
|
||||
callback(cx);
|
||||
dismiss(event, cx)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
},
|
||||
)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -177,6 +259,7 @@ pub struct MenuHandleState<M> {
|
|||
child_element: Option<AnyElement>,
|
||||
menu_element: Option<AnyElement>,
|
||||
}
|
||||
|
||||
impl<M: ManagedView> Element for MenuHandle<M> {
|
||||
type State = MenuHandleState<M>;
|
||||
|
||||
|
@ -264,11 +347,9 @@ impl<M: ManagedView> Element for MenuHandle<M> {
|
|||
|
||||
let new_menu = (builder)(cx);
|
||||
let menu2 = menu.clone();
|
||||
cx.subscribe(&new_menu, move |modal, e, cx| match e {
|
||||
&DismissEvent::Dismiss => {
|
||||
*menu2.borrow_mut() = None;
|
||||
cx.notify();
|
||||
}
|
||||
cx.subscribe(&new_menu, move |_modal, _: &DismissEvent, cx| {
|
||||
*menu2.borrow_mut() = None;
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
cx.focus_view(&new_menu);
|
||||
|
|
|
@ -1,19 +1,48 @@
|
|||
use gpui::{div, Element, ParentElement};
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{Color, Icon, IconElement, IconSize, Toggle};
|
||||
use gpui::ClickEvent;
|
||||
|
||||
pub fn disclosure_control(toggle: Toggle) -> impl Element {
|
||||
match (toggle.is_toggleable(), toggle.is_toggled()) {
|
||||
(false, _) => div(),
|
||||
(_, true) => div().child(
|
||||
IconElement::new(Icon::ChevronDown)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small),
|
||||
),
|
||||
(_, false) => div().child(
|
||||
IconElement::new(Icon::ChevronRight)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small),
|
||||
),
|
||||
use crate::prelude::*;
|
||||
use crate::{Color, Icon, IconButton, IconSize};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Disclosure {
|
||||
is_open: bool,
|
||||
on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||
}
|
||||
|
||||
impl Disclosure {
|
||||
pub fn new(is_open: bool) -> Self {
|
||||
Self {
|
||||
is_open,
|
||||
on_toggle: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_toggle(
|
||||
mut self,
|
||||
handler: impl Into<Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>>,
|
||||
) -> Self {
|
||||
self.on_toggle = handler.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Disclosure {
|
||||
type Rendered = IconButton;
|
||||
|
||||
fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
|
||||
IconButton::new(
|
||||
"toggle",
|
||||
match self.is_open {
|
||||
true => Icon::ChevronDown,
|
||||
false => Icon::ChevronRight,
|
||||
},
|
||||
)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_size(IconSize::Small)
|
||||
.when_some(self.on_toggle, move |this, on_toggle| {
|
||||
this.on_click(move |event, cx| on_toggle(event, cx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,17 +49,4 @@ impl Divider {
|
|||
self.inset = true;
|
||||
self
|
||||
}
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> impl Element {
|
||||
div()
|
||||
.map(|this| match self.direction {
|
||||
DividerDirection::Horizontal => {
|
||||
this.h_px().w_full().when(self.inset, |this| this.mx_1p5())
|
||||
}
|
||||
DividerDirection::Vertical => {
|
||||
this.w_px().h_full().when(self.inset, |this| this.my_1p5())
|
||||
}
|
||||
})
|
||||
.bg(cx.theme().colors().border_variant)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,8 @@ pub enum IconSize {
|
|||
pub enum Icon {
|
||||
Ai,
|
||||
ArrowLeft,
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
ArrowRight,
|
||||
ArrowUpRight,
|
||||
AtSign,
|
||||
|
@ -61,6 +63,7 @@ pub enum Icon {
|
|||
Mic,
|
||||
MicMute,
|
||||
Plus,
|
||||
Public,
|
||||
Quote,
|
||||
Replace,
|
||||
ReplaceAll,
|
||||
|
@ -71,6 +74,11 @@ pub enum Icon {
|
|||
Terminal,
|
||||
WholeWord,
|
||||
XCircle,
|
||||
Command,
|
||||
Control,
|
||||
Shift,
|
||||
Option,
|
||||
Return,
|
||||
}
|
||||
|
||||
impl Icon {
|
||||
|
@ -79,6 +87,8 @@ impl Icon {
|
|||
Icon::Ai => "icons/ai.svg",
|
||||
Icon::ArrowLeft => "icons/arrow_left.svg",
|
||||
Icon::ArrowRight => "icons/arrow_right.svg",
|
||||
Icon::ArrowUp => "icons/arrow_up.svg",
|
||||
Icon::ArrowDown => "icons/arrow_down.svg",
|
||||
Icon::ArrowUpRight => "icons/arrow_up_right.svg",
|
||||
Icon::AtSign => "icons/at-sign.svg",
|
||||
Icon::AudioOff => "icons/speaker-off.svg",
|
||||
|
@ -125,6 +135,7 @@ impl Icon {
|
|||
Icon::Mic => "icons/mic.svg",
|
||||
Icon::MicMute => "icons/mic-mute.svg",
|
||||
Icon::Plus => "icons/plus.svg",
|
||||
Icon::Public => "icons/public.svg",
|
||||
Icon::Quote => "icons/quote.svg",
|
||||
Icon::Replace => "icons/replace.svg",
|
||||
Icon::ReplaceAll => "icons/replace_all.svg",
|
||||
|
@ -135,6 +146,11 @@ impl Icon {
|
|||
Icon::Terminal => "icons/terminal.svg",
|
||||
Icon::WholeWord => "icons/word_search.svg",
|
||||
Icon::XCircle => "icons/error.svg",
|
||||
Icon::Command => "icons/command.svg",
|
||||
Icon::Control => "icons/control.svg",
|
||||
Icon::Shift => "icons/shift.svg",
|
||||
Icon::Option => "icons/option.svg",
|
||||
Icon::Return => "icons/return.svg",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -151,8 +167,8 @@ impl RenderOnce for IconElement {
|
|||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
let svg_size = match self.size {
|
||||
IconSize::Small => rems(0.75),
|
||||
IconSize::Medium => rems(0.9375),
|
||||
IconSize::Small => rems(14. / 16.),
|
||||
IconSize::Medium => rems(16. / 16.),
|
||||
};
|
||||
|
||||
svg()
|
||||
|
@ -189,17 +205,4 @@ impl IconElement {
|
|||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> impl Element {
|
||||
let svg_size = match self.size {
|
||||
IconSize::Small => rems(0.75),
|
||||
IconSize::Medium => rems(0.9375),
|
||||
};
|
||||
|
||||
svg()
|
||||
.size(svg_size)
|
||||
.flex_none()
|
||||
.path(self.path)
|
||||
.text_color(self.color.color(cx))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,129 +0,0 @@
|
|||
use crate::{h_stack, prelude::*, Icon, IconElement};
|
||||
use gpui::{prelude::*, Action, AnyView, Div, MouseButton, MouseDownEvent, Stateful};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct IconButton {
|
||||
id: ElementId,
|
||||
icon: Icon,
|
||||
color: Color,
|
||||
variant: ButtonVariant,
|
||||
state: InteractionState,
|
||||
selected: bool,
|
||||
tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>>,
|
||||
on_mouse_down: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
|
||||
}
|
||||
|
||||
impl RenderOnce for IconButton {
|
||||
type Rendered = Stateful<Div>;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
let icon_color = match (self.state, self.color) {
|
||||
(InteractionState::Disabled, _) => Color::Disabled,
|
||||
(InteractionState::Active, _) => Color::Selected,
|
||||
_ => self.color,
|
||||
};
|
||||
|
||||
let (mut bg_color, bg_hover_color, bg_active_color) = match self.variant {
|
||||
ButtonVariant::Filled => (
|
||||
cx.theme().colors().element_background,
|
||||
cx.theme().colors().element_hover,
|
||||
cx.theme().colors().element_active,
|
||||
),
|
||||
ButtonVariant::Ghost => (
|
||||
cx.theme().colors().ghost_element_background,
|
||||
cx.theme().colors().ghost_element_hover,
|
||||
cx.theme().colors().ghost_element_active,
|
||||
),
|
||||
};
|
||||
|
||||
if self.selected {
|
||||
bg_color = cx.theme().colors().element_selected;
|
||||
}
|
||||
|
||||
let mut button = h_stack()
|
||||
.id(self.id.clone())
|
||||
.justify_center()
|
||||
.rounded_md()
|
||||
.p_1()
|
||||
.bg(bg_color)
|
||||
.cursor_pointer()
|
||||
// Nate: Trying to figure out the right places we want to show a
|
||||
// hover state here. I think it is a bit heavy to have it on every
|
||||
// place we use an icon button.
|
||||
// .hover(|style| style.bg(bg_hover_color))
|
||||
.active(|style| style.bg(bg_active_color))
|
||||
.child(IconElement::new(self.icon).color(icon_color));
|
||||
|
||||
if let Some(click_handler) = self.on_mouse_down {
|
||||
button = button.on_mouse_down(MouseButton::Left, move |event, cx| {
|
||||
cx.stop_propagation();
|
||||
click_handler(event, cx);
|
||||
})
|
||||
}
|
||||
|
||||
if let Some(tooltip) = self.tooltip {
|
||||
if !self.selected {
|
||||
button = button.tooltip(move |cx| tooltip(cx))
|
||||
}
|
||||
}
|
||||
|
||||
button
|
||||
}
|
||||
}
|
||||
|
||||
impl IconButton {
|
||||
pub fn new(id: impl Into<ElementId>, icon: Icon) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
icon,
|
||||
color: Color::default(),
|
||||
variant: ButtonVariant::default(),
|
||||
state: InteractionState::default(),
|
||||
selected: false,
|
||||
tooltip: None,
|
||||
on_mouse_down: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icon(mut self, icon: Icon) -> Self {
|
||||
self.icon = icon;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: Color) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn variant(mut self, variant: ButtonVariant) -> Self {
|
||||
self.variant = variant;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn state(mut self, state: InteractionState) -> Self {
|
||||
self.state = state;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
|
||||
self.tooltip = Some(Box::new(tooltip));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
handler: impl 'static + Fn(&MouseDownEvent, &mut WindowContext),
|
||||
) -> Self {
|
||||
self.on_mouse_down = Some(Box::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn action(self, action: Box<dyn Action>) -> Self {
|
||||
self.on_click(move |this, cx| cx.dispatch_action(action.boxed_clone()))
|
||||
}
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
use crate::{prelude::*, Label};
|
||||
use gpui::{prelude::*, Div, IntoElement, Stateful};
|
||||
|
||||
#[derive(Default, PartialEq)]
|
||||
pub enum InputVariant {
|
||||
#[default]
|
||||
Ghost,
|
||||
Filled,
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Input {
|
||||
placeholder: SharedString,
|
||||
value: String,
|
||||
state: InteractionState,
|
||||
variant: InputVariant,
|
||||
disabled: bool,
|
||||
is_active: bool,
|
||||
}
|
||||
|
||||
impl RenderOnce for Input {
|
||||
type Rendered = Stateful<Div>;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
let (input_bg, input_hover_bg, input_active_bg) = match self.variant {
|
||||
InputVariant::Ghost => (
|
||||
cx.theme().colors().ghost_element_background,
|
||||
cx.theme().colors().ghost_element_hover,
|
||||
cx.theme().colors().ghost_element_active,
|
||||
),
|
||||
InputVariant::Filled => (
|
||||
cx.theme().colors().element_background,
|
||||
cx.theme().colors().element_hover,
|
||||
cx.theme().colors().element_active,
|
||||
),
|
||||
};
|
||||
|
||||
let placeholder_label = Label::new(self.placeholder.clone()).color(if self.disabled {
|
||||
Color::Disabled
|
||||
} else {
|
||||
Color::Placeholder
|
||||
});
|
||||
|
||||
let label = Label::new(self.value.clone()).color(if self.disabled {
|
||||
Color::Disabled
|
||||
} else {
|
||||
Color::Default
|
||||
});
|
||||
|
||||
div()
|
||||
.id("input")
|
||||
.h_7()
|
||||
.w_full()
|
||||
.px_2()
|
||||
.border()
|
||||
.border_color(cx.theme().styles.system.transparent)
|
||||
.bg(input_bg)
|
||||
.hover(|style| style.bg(input_hover_bg))
|
||||
.active(|style| style.bg(input_active_bg))
|
||||
.flex()
|
||||
.items_center()
|
||||
.child(div().flex().items_center().text_ui_sm().map(move |this| {
|
||||
if self.value.is_empty() {
|
||||
this.child(placeholder_label)
|
||||
} else {
|
||||
this.child(label)
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Input {
|
||||
pub fn new(placeholder: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
placeholder: placeholder.into(),
|
||||
value: "".to_string(),
|
||||
state: InteractionState::default(),
|
||||
variant: InputVariant::default(),
|
||||
disabled: false,
|
||||
is_active: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn value(mut self, value: String) -> Self {
|
||||
self.value = value;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn state(mut self, state: InteractionState) -> Self {
|
||||
self.state = state;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn variant(mut self, variant: InputVariant) -> Self {
|
||||
self.variant = variant;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_active(mut self, is_active: bool) -> Self {
|
||||
self.is_active = is_active;
|
||||
self
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
use crate::prelude::*;
|
||||
use gpui::{Action, Div, IntoElement};
|
||||
use crate::{h_stack, prelude::*, Icon, IconElement, IconSize};
|
||||
use gpui::{relative, rems, Action, Div, IntoElement, Keystroke};
|
||||
|
||||
#[derive(IntoElement, Clone)]
|
||||
pub struct KeyBinding {
|
||||
|
@ -14,19 +14,35 @@ impl RenderOnce for KeyBinding {
|
|||
type Rendered = Div;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
div()
|
||||
.flex()
|
||||
h_stack()
|
||||
.flex_none()
|
||||
.gap_2()
|
||||
.children(self.key_binding.keystrokes().iter().map(|keystroke| {
|
||||
div()
|
||||
.flex()
|
||||
.gap_1()
|
||||
let key_icon = Self::icon_for_key(&keystroke);
|
||||
|
||||
h_stack()
|
||||
.flex_none()
|
||||
.gap_0p5()
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.p_0p5()
|
||||
.rounded_sm()
|
||||
.when(keystroke.modifiers.function, |el| el.child(Key::new("fn")))
|
||||
.when(keystroke.modifiers.control, |el| el.child(Key::new("^")))
|
||||
.when(keystroke.modifiers.alt, |el| el.child(Key::new("⌥")))
|
||||
.when(keystroke.modifiers.command, |el| el.child(Key::new("⌘")))
|
||||
.when(keystroke.modifiers.shift, |el| el.child(Key::new("⇧")))
|
||||
.child(Key::new(keystroke.key.clone()))
|
||||
.when(keystroke.modifiers.control, |el| {
|
||||
el.child(KeyIcon::new(Icon::Control))
|
||||
})
|
||||
.when(keystroke.modifiers.alt, |el| {
|
||||
el.child(KeyIcon::new(Icon::Option))
|
||||
})
|
||||
.when(keystroke.modifiers.command, |el| {
|
||||
el.child(KeyIcon::new(Icon::Command))
|
||||
})
|
||||
.when(keystroke.modifiers.shift, |el| {
|
||||
el.child(KeyIcon::new(Icon::Shift))
|
||||
})
|
||||
.when_some(key_icon, |el, icon| el.child(KeyIcon::new(icon)))
|
||||
.when(key_icon.is_none(), |el| {
|
||||
el.child(Key::new(keystroke.key.to_uppercase().clone()))
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
@ -39,6 +55,22 @@ impl KeyBinding {
|
|||
Some(Self::new(key_binding))
|
||||
}
|
||||
|
||||
fn icon_for_key(keystroke: &Keystroke) -> Option<Icon> {
|
||||
let mut icon: Option<Icon> = None;
|
||||
|
||||
if keystroke.key == "left".to_string() {
|
||||
icon = Some(Icon::ArrowLeft);
|
||||
} else if keystroke.key == "right".to_string() {
|
||||
icon = Some(Icon::ArrowRight);
|
||||
} else if keystroke.key == "up".to_string() {
|
||||
icon = Some(Icon::ArrowUp);
|
||||
} else if keystroke.key == "down".to_string() {
|
||||
icon = Some(Icon::ArrowDown);
|
||||
}
|
||||
|
||||
icon
|
||||
}
|
||||
|
||||
pub fn new(key_binding: gpui::KeyBinding) -> Self {
|
||||
Self { key_binding }
|
||||
}
|
||||
|
@ -53,13 +85,18 @@ impl RenderOnce for Key {
|
|||
type Rendered = Div;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
let single_char = self.key.len() == 1;
|
||||
|
||||
div()
|
||||
.px_2()
|
||||
.py_0()
|
||||
.rounded_md()
|
||||
.text_ui_sm()
|
||||
.when(single_char, |el| {
|
||||
el.w(rems(14. / 16.)).flex().flex_none().justify_center()
|
||||
})
|
||||
.when(!single_char, |el| el.px_0p5())
|
||||
.h(rems(14. / 16.))
|
||||
.text_ui()
|
||||
.line_height(relative(1.))
|
||||
.text_color(cx.theme().colors().text)
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.child(self.key.clone())
|
||||
}
|
||||
}
|
||||
|
@ -69,3 +106,24 @@ impl Key {
|
|||
Self { key: key.into() }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct KeyIcon {
|
||||
icon: Icon,
|
||||
}
|
||||
|
||||
impl RenderOnce for KeyIcon {
|
||||
type Rendered = Div;
|
||||
|
||||
fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
|
||||
div()
|
||||
.w(rems(14. / 16.))
|
||||
.child(IconElement::new(self.icon).size(IconSize::Small))
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyIcon {
|
||||
pub fn new(icon: Icon) -> Self {
|
||||
Self { icon }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::prelude::*;
|
||||
use crate::styled_ext::StyledExt;
|
||||
use gpui::{relative, Div, Hsla, IntoElement, StyledText, TextRun, WindowContext};
|
||||
use gpui::{relative, Div, IntoElement, StyledText, TextRun, WindowContext};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
|
||||
pub enum LabelSize {
|
||||
|
@ -182,9 +182,3 @@ impl HighlightedLabel {
|
|||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A run of text that receives the same style.
|
||||
struct Run {
|
||||
pub text: String,
|
||||
pub color: Hsla,
|
||||
}
|
||||
|
|
|
@ -1,409 +1,18 @@
|
|||
use gpui::{
|
||||
div, px, AnyElement, ClickEvent, Div, IntoElement, Stateful, StatefulInteractiveElement,
|
||||
};
|
||||
mod list_header;
|
||||
mod list_item;
|
||||
mod list_separator;
|
||||
mod list_sub_header;
|
||||
|
||||
use gpui::{AnyElement, Div};
|
||||
use smallvec::SmallVec;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{
|
||||
disclosure_control, h_stack, v_stack, Avatar, Icon, IconElement, IconSize, Label, Toggle,
|
||||
};
|
||||
use crate::{prelude::*, GraphicSlot};
|
||||
use crate::prelude::*;
|
||||
use crate::{v_stack, Label};
|
||||
|
||||
#[derive(Clone, Copy, Default, Debug, PartialEq)]
|
||||
pub enum ListItemVariant {
|
||||
/// The list item extends to the far left and right of the list.
|
||||
FullWidth,
|
||||
#[default]
|
||||
Inset,
|
||||
}
|
||||
|
||||
pub enum ListHeaderMeta {
|
||||
// TODO: These should be IconButtons
|
||||
Tools(Vec<Icon>),
|
||||
// TODO: This should be a button
|
||||
Button(Label),
|
||||
Text(Label),
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ListHeader {
|
||||
label: SharedString,
|
||||
left_icon: Option<Icon>,
|
||||
meta: Option<ListHeaderMeta>,
|
||||
variant: ListItemVariant,
|
||||
toggle: Toggle,
|
||||
}
|
||||
|
||||
impl RenderOnce for ListHeader {
|
||||
type Rendered = Div;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
let disclosure_control = disclosure_control(self.toggle);
|
||||
|
||||
let meta = match self.meta {
|
||||
Some(ListHeaderMeta::Tools(icons)) => div().child(
|
||||
h_stack()
|
||||
.gap_2()
|
||||
.items_center()
|
||||
.children(icons.into_iter().map(|i| {
|
||||
IconElement::new(i)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small)
|
||||
})),
|
||||
),
|
||||
Some(ListHeaderMeta::Button(label)) => div().child(label),
|
||||
Some(ListHeaderMeta::Text(label)) => div().child(label),
|
||||
None => div(),
|
||||
};
|
||||
|
||||
h_stack()
|
||||
.w_full()
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
.relative()
|
||||
.child(
|
||||
div()
|
||||
.h_5()
|
||||
.when(self.variant == ListItemVariant::Inset, |this| this.px_2())
|
||||
.flex()
|
||||
.flex_1()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.children(self.left_icon.map(|i| {
|
||||
IconElement::new(i)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small)
|
||||
}))
|
||||
.child(Label::new(self.label.clone()).color(Color::Muted)),
|
||||
)
|
||||
.child(disclosure_control),
|
||||
)
|
||||
.child(meta),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ListHeader {
|
||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
left_icon: None,
|
||||
meta: None,
|
||||
variant: ListItemVariant::default(),
|
||||
toggle: Toggle::NotToggleable,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle(mut self, toggle: Toggle) -> Self {
|
||||
self.toggle = toggle;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
|
||||
self.left_icon = left_icon;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn meta(mut self, meta: Option<ListHeaderMeta>) -> Self {
|
||||
self.meta = meta;
|
||||
self
|
||||
}
|
||||
|
||||
// before_ship!("delete")
|
||||
// fn render<V: 'static>(self, cx: &mut WindowContext) -> impl Element<V> {
|
||||
// let disclosure_control = disclosure_control(self.toggle);
|
||||
|
||||
// let meta = match self.meta {
|
||||
// Some(ListHeaderMeta::Tools(icons)) => div().child(
|
||||
// h_stack()
|
||||
// .gap_2()
|
||||
// .items_center()
|
||||
// .children(icons.into_iter().map(|i| {
|
||||
// IconElement::new(i)
|
||||
// .color(TextColor::Muted)
|
||||
// .size(IconSize::Small)
|
||||
// })),
|
||||
// ),
|
||||
// Some(ListHeaderMeta::Button(label)) => div().child(label),
|
||||
// Some(ListHeaderMeta::Text(label)) => div().child(label),
|
||||
// None => div(),
|
||||
// };
|
||||
|
||||
// h_stack()
|
||||
// .w_full()
|
||||
// .bg(cx.theme().colors().surface_background)
|
||||
// // TODO: Add focus state
|
||||
// // .when(self.state == InteractionState::Focused, |this| {
|
||||
// // this.border()
|
||||
// // .border_color(cx.theme().colors().border_focused)
|
||||
// // })
|
||||
// .relative()
|
||||
// .child(
|
||||
// div()
|
||||
// .h_5()
|
||||
// .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
|
||||
// .flex()
|
||||
// .flex_1()
|
||||
// .items_center()
|
||||
// .justify_between()
|
||||
// .w_full()
|
||||
// .gap_1()
|
||||
// .child(
|
||||
// h_stack()
|
||||
// .gap_1()
|
||||
// .child(
|
||||
// div()
|
||||
// .flex()
|
||||
// .gap_1()
|
||||
// .items_center()
|
||||
// .children(self.left_icon.map(|i| {
|
||||
// IconElement::new(i)
|
||||
// .color(TextColor::Muted)
|
||||
// .size(IconSize::Small)
|
||||
// }))
|
||||
// .child(Label::new(self.label.clone()).color(TextColor::Muted)),
|
||||
// )
|
||||
// .child(disclosure_control),
|
||||
// )
|
||||
// .child(meta),
|
||||
// )
|
||||
// }
|
||||
}
|
||||
|
||||
#[derive(IntoElement, Clone)]
|
||||
pub struct ListSubHeader {
|
||||
label: SharedString,
|
||||
left_icon: Option<Icon>,
|
||||
variant: ListItemVariant,
|
||||
}
|
||||
|
||||
impl ListSubHeader {
|
||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
left_icon: None,
|
||||
variant: ListItemVariant::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
|
||||
self.left_icon = left_icon;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ListSubHeader {
|
||||
type Rendered = Div;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
h_stack().flex_1().w_full().relative().py_1().child(
|
||||
div()
|
||||
.h_6()
|
||||
.when(self.variant == ListItemVariant::Inset, |this| this.px_2())
|
||||
.flex()
|
||||
.flex_1()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.children(self.left_icon.map(|i| {
|
||||
IconElement::new(i)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small)
|
||||
}))
|
||||
.child(Label::new(self.label.clone()).color(Color::Muted)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Copy, Clone)]
|
||||
pub enum ListEntrySize {
|
||||
#[default]
|
||||
Small,
|
||||
Medium,
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ListItem {
|
||||
id: ElementId,
|
||||
disabled: bool,
|
||||
// TODO: Reintroduce this
|
||||
// disclosure_control_style: DisclosureControlVisibility,
|
||||
indent_level: u32,
|
||||
left_slot: Option<GraphicSlot>,
|
||||
overflow: OverflowStyle,
|
||||
size: ListEntrySize,
|
||||
toggle: Toggle,
|
||||
variant: ListItemVariant,
|
||||
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl ListItem {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
disabled: false,
|
||||
indent_level: 0,
|
||||
left_slot: None,
|
||||
overflow: OverflowStyle::Hidden,
|
||||
size: ListEntrySize::default(),
|
||||
toggle: Toggle::NotToggleable,
|
||||
variant: ListItemVariant::default(),
|
||||
on_click: Default::default(),
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
|
||||
self.on_click = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn variant(mut self, variant: ListItemVariant) -> Self {
|
||||
self.variant = variant;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn indent_level(mut self, indent_level: u32) -> Self {
|
||||
self.indent_level = indent_level;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn toggle(mut self, toggle: Toggle) -> Self {
|
||||
self.toggle = toggle;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn left_content(mut self, left_content: GraphicSlot) -> Self {
|
||||
self.left_slot = Some(left_content);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn left_icon(mut self, left_icon: Icon) -> Self {
|
||||
self.left_slot = Some(GraphicSlot::Icon(left_icon));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn left_avatar(mut self, left_avatar: impl Into<SharedString>) -> Self {
|
||||
self.left_slot = Some(GraphicSlot::Avatar(left_avatar.into()));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn size(mut self, size: ListEntrySize) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ListItem {
|
||||
type Rendered = Stateful<Div>;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
let left_content = match self.left_slot.clone() {
|
||||
Some(GraphicSlot::Icon(i)) => Some(
|
||||
h_stack().child(
|
||||
IconElement::new(i)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
),
|
||||
Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::uri(src))),
|
||||
Some(GraphicSlot::PublicActor(src)) => Some(h_stack().child(Avatar::uri(src))),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let sized_item = match self.size {
|
||||
ListEntrySize::Small => div().h_6(),
|
||||
ListEntrySize::Medium => div().h_7(),
|
||||
};
|
||||
div()
|
||||
.id(self.id)
|
||||
.relative()
|
||||
.hover(|mut style| {
|
||||
style.background = Some(cx.theme().colors().editor_background.into());
|
||||
style
|
||||
})
|
||||
.on_click({
|
||||
let on_click = self.on_click.clone();
|
||||
move |event, cx| {
|
||||
if let Some(on_click) = &on_click {
|
||||
(on_click)(event, cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
// TODO: Add focus state
|
||||
// .when(self.state == InteractionState::Focused, |this| {
|
||||
// this.border()
|
||||
// .border_color(cx.theme().colors().border_focused)
|
||||
// })
|
||||
.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
|
||||
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
|
||||
.child(
|
||||
sized_item
|
||||
.when(self.variant == ListItemVariant::Inset, |this| this.px_2())
|
||||
// .ml(rems(0.75 * self.indent_level as f32))
|
||||
.children((0..self.indent_level).map(|_| {
|
||||
div()
|
||||
.w(px(4.))
|
||||
.h_full()
|
||||
.flex()
|
||||
.justify_center()
|
||||
.group_hover("", |style| style.bg(cx.theme().colors().border_focused))
|
||||
.child(
|
||||
h_stack()
|
||||
.child(div().w_px().h_full())
|
||||
.child(div().w_px().h_full().bg(cx.theme().colors().border)),
|
||||
)
|
||||
}))
|
||||
.flex()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.relative()
|
||||
.child(disclosure_control(self.toggle))
|
||||
.children(left_content)
|
||||
.children(self.children),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for ListItem {
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
|
||||
&mut self.children
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement, Clone)]
|
||||
pub struct ListSeparator;
|
||||
|
||||
impl ListSeparator {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ListSeparator {
|
||||
type Rendered = Div;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
div().h_px().w_full().bg(cx.theme().colors().border_variant)
|
||||
}
|
||||
}
|
||||
pub use list_header::*;
|
||||
pub use list_item::*;
|
||||
pub use list_separator::*;
|
||||
pub use list_sub_header::*;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct List {
|
||||
|
@ -411,34 +20,16 @@ pub struct List {
|
|||
/// Defaults to "No items"
|
||||
empty_message: SharedString,
|
||||
header: Option<ListHeader>,
|
||||
toggle: Toggle,
|
||||
toggle: Option<bool>,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl RenderOnce for List {
|
||||
type Rendered = Div;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
let list_content = match (self.children.is_empty(), self.toggle) {
|
||||
(false, _) => div().children(self.children),
|
||||
(true, Toggle::Toggled(false)) => div(),
|
||||
(true, _) => div().child(Label::new(self.empty_message.clone()).color(Color::Muted)),
|
||||
};
|
||||
|
||||
v_stack()
|
||||
.w_full()
|
||||
.py_1()
|
||||
.children(self.header.map(|header| header))
|
||||
.child(list_content)
|
||||
}
|
||||
}
|
||||
|
||||
impl List {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
empty_message: "No items".into(),
|
||||
header: None,
|
||||
toggle: Toggle::NotToggleable,
|
||||
toggle: None,
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
|
@ -453,8 +44,8 @@ impl List {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn toggle(mut self, toggle: Toggle) -> Self {
|
||||
self.toggle = toggle;
|
||||
pub fn toggle(mut self, toggle: impl Into<Option<bool>>) -> Self {
|
||||
self.toggle = toggle.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
@ -464,3 +55,19 @@ impl ParentElement for List {
|
|||
&mut self.children
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for List {
|
||||
type Rendered = Div;
|
||||
|
||||
fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
|
||||
v_stack()
|
||||
.w_full()
|
||||
.py_1()
|
||||
.children(self.header.map(|header| header))
|
||||
.map(|this| match (self.children.is_empty(), self.toggle) {
|
||||
(false, _) => this.children(self.children),
|
||||
(true, Some(false)) => this,
|
||||
(true, _) => this.child(Label::new(self.empty_message.clone()).color(Color::Muted)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
124
crates/ui2/src/components/list/list_header.rs
Normal file
124
crates/ui2/src/components/list/list_header.rs
Normal file
|
@ -0,0 +1,124 @@
|
|||
use std::rc::Rc;
|
||||
|
||||
use gpui::{ClickEvent, Div};
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{h_stack, Disclosure, Icon, IconButton, IconElement, IconSize, Label};
|
||||
|
||||
pub enum ListHeaderMeta {
|
||||
Tools(Vec<IconButton>),
|
||||
// TODO: This should be a button
|
||||
Button(Label),
|
||||
Text(Label),
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ListHeader {
|
||||
label: SharedString,
|
||||
left_icon: Option<Icon>,
|
||||
meta: Option<ListHeaderMeta>,
|
||||
toggle: Option<bool>,
|
||||
on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||
inset: bool,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
impl ListHeader {
|
||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
left_icon: None,
|
||||
meta: None,
|
||||
inset: false,
|
||||
toggle: None,
|
||||
on_toggle: None,
|
||||
selected: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle(mut self, toggle: impl Into<Option<bool>>) -> Self {
|
||||
self.toggle = toggle.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_toggle(
|
||||
mut self,
|
||||
on_toggle: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
|
||||
) -> Self {
|
||||
self.on_toggle = Some(Rc::new(on_toggle));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
|
||||
self.left_icon = left_icon;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn right_button(self, button: IconButton) -> Self {
|
||||
self.meta(Some(ListHeaderMeta::Tools(vec![button])))
|
||||
}
|
||||
|
||||
pub fn meta(mut self, meta: Option<ListHeaderMeta>) -> Self {
|
||||
self.meta = meta;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ListHeader {
|
||||
type Rendered = Div;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
let meta = match self.meta {
|
||||
Some(ListHeaderMeta::Tools(icons)) => div().child(
|
||||
h_stack()
|
||||
.gap_2()
|
||||
.items_center()
|
||||
.children(icons.into_iter().map(|i| i.icon_color(Color::Muted))),
|
||||
),
|
||||
Some(ListHeaderMeta::Button(label)) => div().child(label),
|
||||
Some(ListHeaderMeta::Text(label)) => div().child(label),
|
||||
None => div(),
|
||||
};
|
||||
|
||||
h_stack().w_full().relative().child(
|
||||
div()
|
||||
.h_5()
|
||||
.when(self.inset, |this| this.px_2())
|
||||
.when(self.selected, |this| {
|
||||
this.bg(cx.theme().colors().ghost_element_selected)
|
||||
})
|
||||
.flex()
|
||||
.flex_1()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.children(self.left_icon.map(|i| {
|
||||
IconElement::new(i)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small)
|
||||
}))
|
||||
.child(Label::new(self.label.clone()).color(Color::Muted)),
|
||||
)
|
||||
.children(
|
||||
self.toggle
|
||||
.map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)),
|
||||
),
|
||||
)
|
||||
.child(meta),
|
||||
)
|
||||
}
|
||||
}
|
166
crates/ui2/src/components/list/list_item.rs
Normal file
166
crates/ui2/src/components/list/list_item.rs
Normal file
|
@ -0,0 +1,166 @@
|
|||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
px, AnyElement, ClickEvent, Div, ImageSource, MouseButton, MouseDownEvent, Pixels, Stateful,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{Avatar, Disclosure, Icon, IconElement, IconSize};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ListItem {
|
||||
id: ElementId,
|
||||
selected: bool,
|
||||
// TODO: Reintroduce this
|
||||
// disclosure_control_style: DisclosureControlVisibility,
|
||||
indent_level: usize,
|
||||
indent_step_size: Pixels,
|
||||
left_slot: Option<AnyElement>,
|
||||
toggle: Option<bool>,
|
||||
inset: bool,
|
||||
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||
on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||
on_secondary_mouse_down: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl ListItem {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
selected: false,
|
||||
indent_level: 0,
|
||||
indent_step_size: px(12.),
|
||||
left_slot: None,
|
||||
toggle: None,
|
||||
inset: false,
|
||||
on_click: None,
|
||||
on_secondary_mouse_down: None,
|
||||
on_toggle: None,
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
|
||||
self.on_click = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_secondary_mouse_down(
|
||||
mut self,
|
||||
handler: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
|
||||
) -> Self {
|
||||
self.on_secondary_mouse_down = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn inset(mut self, inset: bool) -> Self {
|
||||
self.inset = inset;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn indent_level(mut self, indent_level: usize) -> Self {
|
||||
self.indent_level = indent_level;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn indent_step_size(mut self, indent_step_size: Pixels) -> Self {
|
||||
self.indent_step_size = indent_step_size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn toggle(mut self, toggle: impl Into<Option<bool>>) -> Self {
|
||||
self.toggle = toggle.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_toggle(
|
||||
mut self,
|
||||
on_toggle: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
|
||||
) -> Self {
|
||||
self.on_toggle = Some(Rc::new(on_toggle));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn left_child(mut self, left_content: impl IntoElement) -> Self {
|
||||
self.left_slot = Some(left_content.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn left_icon(mut self, left_icon: Icon) -> Self {
|
||||
self.left_slot = Some(
|
||||
IconElement::new(left_icon)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted)
|
||||
.into_any_element(),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn left_avatar(mut self, left_avatar: impl Into<ImageSource>) -> Self {
|
||||
self.left_slot = Some(Avatar::source(left_avatar.into()).into_any_element());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for ListItem {
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
|
||||
&mut self.children
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ListItem {
|
||||
type Rendered = Stateful<Div>;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
div()
|
||||
.id(self.id)
|
||||
.relative()
|
||||
// TODO: Add focus state
|
||||
// .when(self.state == InteractionState::Focused, |this| {
|
||||
// this.border()
|
||||
// .border_color(cx.theme().colors().border_focused)
|
||||
// })
|
||||
.when(self.inset, |this| this.rounded_md())
|
||||
.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
|
||||
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
|
||||
.when(self.selected, |this| {
|
||||
this.bg(cx.theme().colors().ghost_element_selected)
|
||||
})
|
||||
.when_some(self.on_click, |this, on_click| {
|
||||
this.cursor_pointer().on_click(move |event, cx| {
|
||||
// HACK: GPUI currently fires `on_click` with any mouse button,
|
||||
// but we only care about the left button.
|
||||
if event.down.button == MouseButton::Left {
|
||||
(on_click)(event, cx)
|
||||
}
|
||||
})
|
||||
})
|
||||
.when_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
|
||||
this.on_mouse_down(MouseButton::Right, move |event, cx| {
|
||||
(on_mouse_down)(event, cx)
|
||||
})
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.when(self.inset, |this| this.px_2())
|
||||
.ml(self.indent_level as f32 * self.indent_step_size)
|
||||
.flex()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.relative()
|
||||
.children(
|
||||
self.toggle
|
||||
.map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)),
|
||||
)
|
||||
.children(self.left_slot)
|
||||
.children(self.children),
|
||||
)
|
||||
}
|
||||
}
|
14
crates/ui2/src/components/list/list_separator.rs
Normal file
14
crates/ui2/src/components/list/list_separator.rs
Normal file
|
@ -0,0 +1,14 @@
|
|||
use gpui::Div;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ListSeparator;
|
||||
|
||||
impl RenderOnce for ListSeparator {
|
||||
type Rendered = Div;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
div().h_px().w_full().bg(cx.theme().colors().border_variant)
|
||||
}
|
||||
}
|
56
crates/ui2/src/components/list/list_sub_header.rs
Normal file
56
crates/ui2/src/components/list/list_sub_header.rs
Normal file
|
@ -0,0 +1,56 @@
|
|||
use gpui::Div;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{h_stack, Icon, IconElement, IconSize, Label};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ListSubHeader {
|
||||
label: SharedString,
|
||||
left_icon: Option<Icon>,
|
||||
inset: bool,
|
||||
}
|
||||
|
||||
impl ListSubHeader {
|
||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
left_icon: None,
|
||||
inset: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
|
||||
self.left_icon = left_icon;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ListSubHeader {
|
||||
type Rendered = Div;
|
||||
|
||||
fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
|
||||
h_stack().flex_1().w_full().relative().py_1().child(
|
||||
div()
|
||||
.h_6()
|
||||
.when(self.inset, |this| this.px_2())
|
||||
.flex()
|
||||
.flex_1()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.children(self.left_icon.map(|i| {
|
||||
IconElement::new(i)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small)
|
||||
}))
|
||||
.child(Label::new(self.label.clone()).color(Color::Muted)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -3,9 +3,9 @@ use gpui::{
|
|||
WindowContext,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use theme2::ActiveTheme;
|
||||
|
||||
use crate::{v_stack, StyledExt};
|
||||
use crate::prelude::*;
|
||||
use crate::v_stack;
|
||||
|
||||
/// A popover is used to display a menu or show some options.
|
||||
///
|
||||
|
|
|
@ -2,18 +2,22 @@ mod avatar;
|
|||
mod button;
|
||||
mod checkbox;
|
||||
mod context_menu;
|
||||
mod disclosure;
|
||||
mod icon;
|
||||
mod input;
|
||||
mod icon_button;
|
||||
mod keybinding;
|
||||
mod label;
|
||||
mod list;
|
||||
mod list_item;
|
||||
|
||||
pub use avatar::*;
|
||||
pub use button::*;
|
||||
pub use checkbox::*;
|
||||
pub use context_menu::*;
|
||||
pub use disclosure::*;
|
||||
pub use icon::*;
|
||||
pub use input::*;
|
||||
pub use icon_button::*;
|
||||
pub use keybinding::*;
|
||||
pub use label::*;
|
||||
pub use list::*;
|
||||
pub use list_item::*;
|
||||
|
|
|
@ -9,7 +9,7 @@ pub struct AvatarStory;
|
|||
impl Render for AvatarStory {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container()
|
||||
.child(Story::title_for::<Avatar>())
|
||||
.child(Story::label("Default"))
|
||||
|
@ -19,5 +19,13 @@ impl Render for AvatarStory {
|
|||
.child(Avatar::uri(
|
||||
"https://avatars.githubusercontent.com/u/326587?v=4",
|
||||
))
|
||||
.child(
|
||||
Avatar::uri("https://avatars.githubusercontent.com/u/326587?v=4")
|
||||
.availability_indicator(true),
|
||||
)
|
||||
.child(
|
||||
Avatar::uri("https://avatars.githubusercontent.com/u/326587?v=4")
|
||||
.availability_indicator(false),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,145 +1,22 @@
|
|||
use gpui::{rems, Div, Render};
|
||||
use gpui::{Div, Render};
|
||||
use story::Story;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{h_stack, v_stack, Button, Icon, IconPosition, Label};
|
||||
use crate::{Button, ButtonStyle2};
|
||||
|
||||
pub struct ButtonStory;
|
||||
|
||||
impl Render for ButtonStory {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let states = InteractionState::iter();
|
||||
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container()
|
||||
.child(Story::title_for::<Button>())
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_8()
|
||||
.child(
|
||||
div()
|
||||
.child(Story::label("Ghost (Default)"))
|
||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
||||
v_stack()
|
||||
.gap_1()
|
||||
.child(Label::new(state.to_string()).color(Color::Muted))
|
||||
.child(
|
||||
Button::new("Label").variant(ButtonVariant::Ghost), // .state(state),
|
||||
)
|
||||
})))
|
||||
.child(Story::label("Ghost – Left Icon"))
|
||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
||||
v_stack()
|
||||
.gap_1()
|
||||
.child(Label::new(state.to_string()).color(Color::Muted))
|
||||
.child(
|
||||
Button::new("Label")
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.icon(Icon::Plus)
|
||||
.icon_position(IconPosition::Left), // .state(state),
|
||||
)
|
||||
})))
|
||||
.child(Story::label("Ghost – Right Icon"))
|
||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
||||
v_stack()
|
||||
.gap_1()
|
||||
.child(Label::new(state.to_string()).color(Color::Muted))
|
||||
.child(
|
||||
Button::new("Label")
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.icon(Icon::Plus)
|
||||
.icon_position(IconPosition::Right), // .state(state),
|
||||
)
|
||||
}))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.child(Story::label("Filled"))
|
||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
||||
v_stack()
|
||||
.gap_1()
|
||||
.child(Label::new(state.to_string()).color(Color::Muted))
|
||||
.child(
|
||||
Button::new("Label").variant(ButtonVariant::Filled), // .state(state),
|
||||
)
|
||||
})))
|
||||
.child(Story::label("Filled – Left Button"))
|
||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
||||
v_stack()
|
||||
.gap_1()
|
||||
.child(Label::new(state.to_string()).color(Color::Muted))
|
||||
.child(
|
||||
Button::new("Label")
|
||||
.variant(ButtonVariant::Filled)
|
||||
.icon(Icon::Plus)
|
||||
.icon_position(IconPosition::Left), // .state(state),
|
||||
)
|
||||
})))
|
||||
.child(Story::label("Filled – Right Button"))
|
||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
||||
v_stack()
|
||||
.gap_1()
|
||||
.child(Label::new(state.to_string()).color(Color::Muted))
|
||||
.child(
|
||||
Button::new("Label")
|
||||
.variant(ButtonVariant::Filled)
|
||||
.icon(Icon::Plus)
|
||||
.icon_position(IconPosition::Right), // .state(state),
|
||||
)
|
||||
}))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.child(Story::label("Fixed With"))
|
||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
||||
v_stack()
|
||||
.gap_1()
|
||||
.child(Label::new(state.to_string()).color(Color::Muted))
|
||||
.child(
|
||||
Button::new("Label")
|
||||
.variant(ButtonVariant::Filled)
|
||||
// .state(state)
|
||||
.width(Some(rems(6.).into())),
|
||||
)
|
||||
})))
|
||||
.child(Story::label("Fixed With – Left Icon"))
|
||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
||||
v_stack()
|
||||
.gap_1()
|
||||
.child(Label::new(state.to_string()).color(Color::Muted))
|
||||
.child(
|
||||
Button::new("Label")
|
||||
.variant(ButtonVariant::Filled)
|
||||
// .state(state)
|
||||
.icon(Icon::Plus)
|
||||
.icon_position(IconPosition::Left)
|
||||
.width(Some(rems(6.).into())),
|
||||
)
|
||||
})))
|
||||
.child(Story::label("Fixed With – Right Icon"))
|
||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
||||
v_stack()
|
||||
.gap_1()
|
||||
.child(Label::new(state.to_string()).color(Color::Muted))
|
||||
.child(
|
||||
Button::new("Label")
|
||||
.variant(ButtonVariant::Filled)
|
||||
// .state(state)
|
||||
.icon(Icon::Plus)
|
||||
.icon_position(IconPosition::Right)
|
||||
.width(Some(rems(6.).into())),
|
||||
)
|
||||
}))),
|
||||
),
|
||||
)
|
||||
.child(Story::label("Button with `on_click`"))
|
||||
.child(
|
||||
Button::new("Label")
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.on_click(|_, cx| println!("Button clicked.")),
|
||||
)
|
||||
.child(Story::label("Default"))
|
||||
.child(Button::new("default_filled", "Click me"))
|
||||
.child(Story::label("Default (Subtle)"))
|
||||
.child(Button::new("default_subtle", "Click me").style(ButtonStyle2::Subtle))
|
||||
.child(Story::label("Default (Transparent)"))
|
||||
.child(Button::new("default_transparent", "Click me").style(ButtonStyle2::Transparent))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,11 +10,11 @@ fn build_menu(cx: &mut WindowContext, header: impl Into<SharedString>) -> View<C
|
|||
ContextMenu::build(cx, |menu, _| {
|
||||
menu.header(header)
|
||||
.separator()
|
||||
.entry("Print current time", |v, cx| {
|
||||
.entry("Print current time", |cx| {
|
||||
println!("dispatching PrintCurrentTime action");
|
||||
cx.dispatch_action(PrintCurrentDate.boxed_clone())
|
||||
})
|
||||
.entry("Print best foot", |v, cx| {
|
||||
.entry("Print best foot", |cx| {
|
||||
cx.dispatch_action(PrintBestFood.boxed_clone())
|
||||
})
|
||||
})
|
||||
|
@ -25,7 +25,7 @@ pub struct ContextMenuStory;
|
|||
impl Render for ContextMenuStory {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container()
|
||||
.on_action(|_: &PrintCurrentDate, _| {
|
||||
println!("printing unix time!");
|
||||
|
|
20
crates/ui2/src/components/stories/disclosure.rs
Normal file
20
crates/ui2/src/components/stories/disclosure.rs
Normal file
|
@ -0,0 +1,20 @@
|
|||
use gpui::{Div, Render};
|
||||
use story::Story;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::Disclosure;
|
||||
|
||||
pub struct DisclosureStory;
|
||||
|
||||
impl Render for DisclosureStory {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container()
|
||||
.child(Story::title_for::<Disclosure>())
|
||||
.child(Story::label("Toggled"))
|
||||
.child(Disclosure::new(true))
|
||||
.child(Story::label("Not Toggled"))
|
||||
.child(Disclosure::new(false))
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@ pub struct IconStory;
|
|||
impl Render for IconStory {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let icons = Icon::iter();
|
||||
|
||||
Story::container()
|
||||
|
|
47
crates/ui2/src/components/stories/icon_button.rs
Normal file
47
crates/ui2/src/components/stories/icon_button.rs
Normal file
|
@ -0,0 +1,47 @@
|
|||
use gpui::{Div, Render};
|
||||
use story::Story;
|
||||
|
||||
use crate::{prelude::*, Tooltip};
|
||||
use crate::{Icon, IconButton};
|
||||
|
||||
pub struct IconButtonStory;
|
||||
|
||||
impl Render for IconButtonStory {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container()
|
||||
.child(Story::title_for::<IconButton>())
|
||||
.child(Story::label("Default"))
|
||||
.child(div().w_8().child(IconButton::new("icon_a", Icon::Hash)))
|
||||
.child(Story::label("Selected"))
|
||||
.child(
|
||||
div()
|
||||
.w_8()
|
||||
.child(IconButton::new("icon_a", Icon::Hash).selected(true)),
|
||||
)
|
||||
.child(Story::label("Disabled"))
|
||||
.child(
|
||||
div()
|
||||
.w_8()
|
||||
.child(IconButton::new("icon_a", Icon::Hash).disabled(true)),
|
||||
)
|
||||
.child(Story::label("With `on_click`"))
|
||||
.child(
|
||||
div()
|
||||
.w_8()
|
||||
.child(
|
||||
IconButton::new("with_on_click", Icon::Ai).on_click(|_event, _cx| {
|
||||
println!("Clicked!");
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(Story::label("With `tooltip`"))
|
||||
.child(
|
||||
div().w_8().child(
|
||||
IconButton::new("with_tooltip", Icon::MessageBubbles)
|
||||
.tooltip(|cx| Tooltip::text("Open messages", cx)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
use gpui::{Div, Render};
|
||||
use story::Story;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::Input;
|
||||
|
||||
pub struct InputStory;
|
||||
|
||||
impl Render for InputStory {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container()
|
||||
.child(Story::title_for::<Input>())
|
||||
.child(Story::label("Default"))
|
||||
.child(div().flex().child(Input::new("Search")))
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@ pub fn binding(key: &str) -> gpui::KeyBinding {
|
|||
impl Render for KeybindingStory {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let all_modifier_permutations = ["ctrl", "alt", "cmd", "shift"].into_iter().permutations(2);
|
||||
|
||||
Story::container()
|
||||
|
|
|
@ -9,7 +9,7 @@ pub struct LabelStory;
|
|||
impl Render for LabelStory {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container()
|
||||
.child(Story::title_for::<Label>())
|
||||
.child(Story::label("Default"))
|
||||
|
|
38
crates/ui2/src/components/stories/list.rs
Normal file
38
crates/ui2/src/components/stories/list.rs
Normal file
|
@ -0,0 +1,38 @@
|
|||
use gpui::{Div, Render};
|
||||
use story::Story;
|
||||
|
||||
use crate::{prelude::*, ListHeader, ListSeparator, ListSubHeader};
|
||||
use crate::{List, ListItem};
|
||||
|
||||
pub struct ListStory;
|
||||
|
||||
impl Render for ListStory {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container()
|
||||
.child(Story::title_for::<List>())
|
||||
.child(Story::label("Default"))
|
||||
.child(
|
||||
List::new()
|
||||
.child(ListItem::new("apple").child("Apple"))
|
||||
.child(ListItem::new("banana").child("Banana"))
|
||||
.child(ListItem::new("cherry").child("Cherry")),
|
||||
)
|
||||
.child(Story::label("With sections"))
|
||||
.child(
|
||||
List::new()
|
||||
.child(ListHeader::new("Fruits"))
|
||||
.child(ListItem::new("apple").child("Apple"))
|
||||
.child(ListItem::new("banana").child("Banana"))
|
||||
.child(ListItem::new("cherry").child("Cherry"))
|
||||
.child(ListSeparator)
|
||||
.child(ListHeader::new("Vegetables"))
|
||||
.child(ListSubHeader::new("Root Vegetables"))
|
||||
.child(ListItem::new("carrot").child("Carrot"))
|
||||
.child(ListItem::new("potato").child("Potato"))
|
||||
.child(ListSubHeader::new("Leafy Vegetables"))
|
||||
.child(ListItem::new("kale").child("Kale")),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -2,18 +2,32 @@ use gpui::{Div, Render};
|
|||
use story::Story;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::ListItem;
|
||||
use crate::{Icon, ListItem};
|
||||
|
||||
pub struct ListItemStory;
|
||||
|
||||
impl Render for ListItemStory {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container()
|
||||
.child(Story::title_for::<ListItem>())
|
||||
.child(Story::label("Default"))
|
||||
.child(ListItem::new("hello_world").child("Hello, world!"))
|
||||
.child(Story::label("With left icon"))
|
||||
.child(
|
||||
ListItem::new("with_left_icon")
|
||||
.child("Hello, world!")
|
||||
.left_icon(Icon::Bell),
|
||||
)
|
||||
.child(Story::label("With left avatar"))
|
||||
.child(
|
||||
ListItem::new("with_left_avatar")
|
||||
.child("Hello, world!")
|
||||
.left_avatar(SharedString::from(
|
||||
"https://avatars.githubusercontent.com/u/1714999?v=4",
|
||||
)),
|
||||
)
|
||||
.child(Story::label("With `on_click`"))
|
||||
.child(
|
||||
ListItem::new("with_on_click")
|
||||
|
@ -22,5 +36,13 @@ impl Render for ListItemStory {
|
|||
println!("Clicked!");
|
||||
}),
|
||||
)
|
||||
.child(Story::label("With `on_secondary_mouse_down`"))
|
||||
.child(
|
||||
ListItem::new("with_on_secondary_mouse_down")
|
||||
.child("Right click me")
|
||||
.on_secondary_mouse_down(|_event, _cx| {
|
||||
println!("Right mouse down!");
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
/// Whether the entry is toggleable, and if so, whether it is currently toggled.
|
||||
///
|
||||
/// To make an element toggleable, simply add a `Toggle::Toggled(_)` and handle it's cases.
|
||||
///
|
||||
/// You can check if an element is toggleable with `.is_toggleable()`
|
||||
///
|
||||
/// Possible values:
|
||||
/// - `Toggle::NotToggleable` - The entry is not toggleable
|
||||
/// - `Toggle::Toggled(true)` - The entry is toggleable and toggled
|
||||
/// - `Toggle::Toggled(false)` - The entry is toggleable and not toggled
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Toggle {
|
||||
NotToggleable,
|
||||
Toggled(bool),
|
||||
}
|
||||
|
||||
impl Toggle {
|
||||
/// Returns true if the entry is toggled (or is not toggleable.)
|
||||
///
|
||||
/// As element that isn't toggleable is always "expanded" or "enabled"
|
||||
/// returning true in that case makes sense.
|
||||
pub fn is_toggled(&self) -> bool {
|
||||
match self {
|
||||
Self::Toggled(false) => false,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_toggleable(&self) -> bool {
|
||||
match self {
|
||||
Self::Toggled(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for Toggle {
|
||||
fn from(toggled: bool) -> Self {
|
||||
Toggle::Toggled(toggled)
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
use gpui::{overlay, Action, AnyView, IntoElement, Overlay, Render, VisualContext};
|
||||
use settings2::Settings;
|
||||
use theme2::{ActiveTheme, ThemeSettings};
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{h_stack, v_stack, Color, KeyBinding, Label, LabelSize, StyledExt};
|
||||
|
@ -13,7 +13,7 @@ pub struct Tooltip {
|
|||
|
||||
impl Tooltip {
|
||||
pub fn text(title: impl Into<SharedString>, cx: &mut WindowContext) -> AnyView {
|
||||
cx.build_view(|cx| Self {
|
||||
cx.build_view(|_cx| Self {
|
||||
title: title.into(),
|
||||
meta: None,
|
||||
key_binding: None,
|
||||
|
|
5
crates/ui2/src/disableable.rs
Normal file
5
crates/ui2/src/disableable.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
/// A trait for elements that can be disabled.
|
||||
pub trait Disableable {
|
||||
/// Sets whether the element is disabled.
|
||||
fn disabled(self, disabled: bool) -> Self;
|
||||
}
|
10
crates/ui2/src/fixed.rs
Normal file
10
crates/ui2/src/fixed.rs
Normal file
|
@ -0,0 +1,10 @@
|
|||
use gpui::DefiniteLength;
|
||||
|
||||
/// A trait for elements that have a fixed with.
|
||||
pub trait FixedWidth {
|
||||
/// Sets the width of the element.
|
||||
fn width(self, width: DefiniteLength) -> Self;
|
||||
|
||||
/// Sets the element's width to the full width of its container.
|
||||
fn full_width(self) -> Self;
|
||||
}
|
|
@ -1,70 +1,12 @@
|
|||
pub use gpui::prelude::*;
|
||||
pub use gpui::{
|
||||
div, Element, ElementId, InteractiveElement, ParentElement, RenderOnce, SharedString, Styled,
|
||||
ViewContext, WindowContext,
|
||||
};
|
||||
|
||||
pub use crate::StyledExt;
|
||||
pub use crate::{ButtonVariant, Color};
|
||||
pub use theme2::ActiveTheme;
|
||||
|
||||
use strum::EnumIter;
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
|
||||
pub enum IconSide {
|
||||
#[default]
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, EnumIter)]
|
||||
pub enum OverflowStyle {
|
||||
Hidden,
|
||||
Wrap,
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Copy, Clone, EnumIter, strum::Display)]
|
||||
pub enum InteractionState {
|
||||
/// An element that is enabled and not hovered, active, focused, or disabled.
|
||||
///
|
||||
/// This is often referred to as the "default" state.
|
||||
#[default]
|
||||
Enabled,
|
||||
/// An element that is hovered.
|
||||
Hovered,
|
||||
/// An element has an active mouse down or touch start event on it.
|
||||
Active,
|
||||
/// An element that is focused using the keyboard.
|
||||
Focused,
|
||||
/// An element that is disabled.
|
||||
Disabled,
|
||||
/// A toggleable element that is selected, like the active button in a
|
||||
/// button toggle group.
|
||||
Selected,
|
||||
}
|
||||
|
||||
impl InteractionState {
|
||||
pub fn if_enabled(&self, enabled: bool) -> Self {
|
||||
if enabled {
|
||||
*self
|
||||
} else {
|
||||
InteractionState::Disabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)]
|
||||
pub enum Selection {
|
||||
#[default]
|
||||
Unselected,
|
||||
Indeterminate,
|
||||
Selected,
|
||||
}
|
||||
|
||||
impl Selection {
|
||||
pub fn inverse(&self) -> Self {
|
||||
match self {
|
||||
Self::Unselected | Self::Indeterminate => Self::Selected,
|
||||
Self::Selected => Self::Unselected,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub use crate::clickable::*;
|
||||
pub use crate::disableable::*;
|
||||
pub use crate::fixed::*;
|
||||
pub use crate::selectable::*;
|
||||
pub use crate::{ButtonCommon, Color, StyledExt};
|
||||
pub use theme::ActiveTheme;
|
||||
|
|
22
crates/ui2/src/selectable.rs
Normal file
22
crates/ui2/src/selectable.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
/// A trait for elements that can be selected.
|
||||
pub trait Selectable {
|
||||
/// Sets whether the element is selected.
|
||||
fn selected(self, selected: bool) -> Self;
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)]
|
||||
pub enum Selection {
|
||||
#[default]
|
||||
Unselected,
|
||||
Indeterminate,
|
||||
Selected,
|
||||
}
|
||||
|
||||
impl Selection {
|
||||
pub fn inverse(&self) -> Self {
|
||||
match self {
|
||||
Self::Unselected | Self::Indeterminate => Self::Selected,
|
||||
Self::Selected => Self::Unselected,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,14 +1,12 @@
|
|||
use gpui::SharedString;
|
||||
use gpui::{ImageSource, SharedString};
|
||||
|
||||
use crate::Icon;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// A slot utility that provides a way to to pass either
|
||||
/// an icon or an image to a component.
|
||||
///
|
||||
/// Can be filled with a []
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum GraphicSlot {
|
||||
Icon(Icon),
|
||||
Avatar(SharedString),
|
||||
Avatar(ImageSource),
|
||||
PublicActor(SharedString),
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
use gpui::{px, Styled, WindowContext};
|
||||
use theme2::ActiveTheme;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{ElevationIndex, UITextSize};
|
||||
|
||||
fn elevated<E: Styled>(this: E, cx: &mut WindowContext, index: ElevationIndex) -> E {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use gpui::{Hsla, WindowContext};
|
||||
use theme2::ActiveTheme;
|
||||
use theme::ActiveTheme;
|
||||
|
||||
#[derive(Default, PartialEq, Copy, Clone)]
|
||||
#[derive(Debug, Default, PartialEq, Copy, Clone)]
|
||||
pub enum Color {
|
||||
#[default]
|
||||
Default,
|
||||
|
|
|
@ -11,16 +11,24 @@
|
|||
#![doc = include_str!("../docs/hello-world.md")]
|
||||
#![doc = include_str!("../docs/building-ui.md")]
|
||||
#![doc = include_str!("../docs/todo.md")]
|
||||
// TODO: Fix warnings instead of supressing.
|
||||
#![allow(dead_code, unused_variables)]
|
||||
|
||||
mod clickable;
|
||||
mod components;
|
||||
mod disableable;
|
||||
mod fixed;
|
||||
pub mod prelude;
|
||||
mod selectable;
|
||||
mod slot;
|
||||
mod styled_ext;
|
||||
mod styles;
|
||||
pub mod utils;
|
||||
|
||||
pub use clickable::*;
|
||||
pub use components::*;
|
||||
pub use disableable::*;
|
||||
pub use fixed::*;
|
||||
pub use prelude::*;
|
||||
pub use selectable::*;
|
||||
pub use slot::*;
|
||||
pub use styled_ext::*;
|
||||
pub use styles::*;
|
||||
|
|
|
@ -16,55 +16,54 @@ fn distance_in_seconds(date: NaiveDateTime, base_date: NaiveDateTime) -> i64 {
|
|||
fn distance_string(distance: i64, include_seconds: bool, add_suffix: bool) -> String {
|
||||
let suffix = if distance < 0 { " from now" } else { " ago" };
|
||||
|
||||
let d = distance.abs();
|
||||
let distance = distance.abs();
|
||||
|
||||
let minutes = d / 60;
|
||||
let hours = d / 3600;
|
||||
let days = d / 86400;
|
||||
let months = d / 2592000;
|
||||
let years = d / 31536000;
|
||||
let minutes = distance / 60;
|
||||
let hours = distance / 3_600;
|
||||
let days = distance / 86_400;
|
||||
let months = distance / 2_592_000;
|
||||
|
||||
let string = if d < 5 && include_seconds {
|
||||
let string = if distance < 5 && include_seconds {
|
||||
"less than 5 seconds".to_string()
|
||||
} else if d < 10 && include_seconds {
|
||||
} else if distance < 10 && include_seconds {
|
||||
"less than 10 seconds".to_string()
|
||||
} else if d < 20 && include_seconds {
|
||||
} else if distance < 20 && include_seconds {
|
||||
"less than 20 seconds".to_string()
|
||||
} else if d < 40 && include_seconds {
|
||||
} else if distance < 40 && include_seconds {
|
||||
"half a minute".to_string()
|
||||
} else if d < 60 && include_seconds {
|
||||
} else if distance < 60 && include_seconds {
|
||||
"less than a minute".to_string()
|
||||
} else if d < 90 && include_seconds {
|
||||
} else if distance < 90 && include_seconds {
|
||||
"1 minute".to_string()
|
||||
} else if d < 30 {
|
||||
} else if distance < 30 {
|
||||
"less than a minute".to_string()
|
||||
} else if d < 90 {
|
||||
} else if distance < 90 {
|
||||
"1 minute".to_string()
|
||||
} else if d < 2700 {
|
||||
} else if distance < 2_700 {
|
||||
format!("{} minutes", minutes)
|
||||
} else if d < 5400 {
|
||||
} else if distance < 5_400 {
|
||||
"about 1 hour".to_string()
|
||||
} else if d < 86400 {
|
||||
} else if distance < 86_400 {
|
||||
format!("about {} hours", hours)
|
||||
} else if d < 172800 {
|
||||
} else if distance < 172_800 {
|
||||
"1 day".to_string()
|
||||
} else if d < 2592000 {
|
||||
} else if distance < 2_592_000 {
|
||||
format!("{} days", days)
|
||||
} else if d < 5184000 {
|
||||
} else if distance < 5_184_000 {
|
||||
"about 1 month".to_string()
|
||||
} else if d < 7776000 {
|
||||
} else if distance < 7_776_000 {
|
||||
"about 2 months".to_string()
|
||||
} else if d < 31540000 {
|
||||
} else if distance < 31_540_000 {
|
||||
format!("{} months", months)
|
||||
} else if d < 39425000 {
|
||||
} else if distance < 39_425_000 {
|
||||
"about 1 year".to_string()
|
||||
} else if d < 55195000 {
|
||||
} else if distance < 55_195_000 {
|
||||
"over 1 year".to_string()
|
||||
} else if d < 63080000 {
|
||||
} else if distance < 63_080_000 {
|
||||
"almost 2 years".to_string()
|
||||
} else {
|
||||
let years = d / 31536000;
|
||||
let remaining_months = (d % 31536000) / 2592000;
|
||||
let years = distance / 31_536_000;
|
||||
let remaining_months = (distance % 31_536_000) / 2_592_000;
|
||||
|
||||
if remaining_months < 3 {
|
||||
format!("about {} years", years)
|
||||
|
@ -76,7 +75,7 @@ fn distance_string(distance: i64, include_seconds: bool, add_suffix: bool) -> St
|
|||
};
|
||||
|
||||
if add_suffix {
|
||||
return format!("{}{}", string, suffix);
|
||||
format!("{}{}", string, suffix)
|
||||
} else {
|
||||
string
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue