Merge branch 'main' into project_search2

This commit is contained in:
Piotr Osiewicz 2023-12-11 16:47:48 +01:00
commit ddbd5cf2cb
145 changed files with 16798 additions and 12900 deletions

View file

@ -13,6 +13,7 @@ mod popover;
mod popover_menu;
mod right_click_menu;
mod stack;
mod tab;
mod tooltip;
#[cfg(feature = "stories")]
@ -33,6 +34,7 @@ pub use popover::*;
pub use popover_menu::*;
pub use right_click_menu::*;
pub use stack::*;
pub use tab::*;
pub use tooltip::*;
#[cfg(feature = "stories")]

View file

@ -1,7 +1,6 @@
use std::sync::Arc;
use crate::prelude::*;
use gpui::{img, rems, Div, ImageData, ImageSource, IntoElement, Styled};
use gpui::{img, Div, Hsla, ImageData, ImageSource, Img, IntoElement, Styled};
use std::sync::Arc;
#[derive(Debug, Default, PartialEq, Clone)]
pub enum Shape {
@ -12,35 +11,39 @@ pub enum Shape {
#[derive(IntoElement)]
pub struct Avatar {
src: ImageSource,
image: Img,
border_color: Option<Hsla>,
is_available: Option<bool>,
shape: Shape,
}
impl RenderOnce for Avatar {
type Rendered = Div;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
let mut img = img(self.src);
if self.shape == Shape::Circle {
img = img.rounded_full();
} else {
img = img.rounded_md();
fn render(mut self, cx: &mut WindowContext) -> Self::Rendered {
if self.image.style().corner_radii.top_left.is_none() {
self = self.shape(Shape::Circle);
}
let size = rems(1.0);
let size = cx.rem_size();
div()
.size(size)
.size(size + px(2.))
.map(|mut div| {
div.style().corner_radii = self.image.style().corner_radii.clone();
div
})
.when_some(self.border_color, |this, color| {
this.border().border_color(color)
})
.child(
img.size(size)
self.image
.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();
let indicator_size = (size * 0.4).round();
div()
.absolute()
@ -56,31 +59,39 @@ impl RenderOnce for Avatar {
impl Avatar {
pub fn uri(src: impl Into<SharedString>) -> Self {
Self {
src: src.into().into(),
shape: Shape::Circle,
is_available: None,
}
Self::source(src.into().into())
}
pub fn data(src: Arc<ImageData>) -> Self {
Self {
src: src.into(),
shape: Shape::Circle,
is_available: None,
}
Self::source(src.into())
}
pub fn source(src: ImageSource) -> Self {
Self {
src,
shape: Shape::Circle,
image: img(src),
is_available: None,
border_color: None,
}
}
pub fn shape(mut self, shape: Shape) -> Self {
self.shape = shape;
self.image = match shape {
Shape::Circle => self.image.rounded_full(),
Shape::RoundedRectangle => self.image.rounded_md(),
};
self
}
pub fn grayscale(mut self, grayscale: bool) -> Self {
self.image = self.image.grayscale(grayscale);
self
}
pub fn border_color(mut self, color: impl Into<Hsla>) -> Self {
self.border_color = Some(color.into());
self
}
pub fn availability_indicator(mut self, is_available: impl Into<Option<bool>>) -> Self {
self.is_available = is_available.into();
self

View file

@ -1,6 +1,6 @@
use gpui::{AnyView, DefiniteLength};
use crate::prelude::*;
use crate::{prelude::*, IconPosition};
use crate::{
ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, IconSize, Label, LineHeightStyle,
};
@ -14,6 +14,7 @@ pub struct Button {
label_color: Option<Color>,
selected_label: Option<SharedString>,
icon: Option<Icon>,
icon_position: Option<IconPosition>,
icon_size: Option<IconSize>,
icon_color: Option<Color>,
selected_icon: Option<Icon>,
@ -27,6 +28,7 @@ impl Button {
label_color: None,
selected_label: None,
icon: None,
icon_position: None,
icon_size: None,
icon_color: None,
selected_icon: None,
@ -48,6 +50,11 @@ impl Button {
self
}
pub fn icon_position(mut self, icon_position: impl Into<Option<IconPosition>>) -> Self {
self.icon_position = icon_position.into();
self
}
pub fn icon_size(mut self, icon_size: impl Into<Option<IconSize>>) -> Self {
self.icon_size = icon_size.into();
self
@ -141,19 +148,29 @@ impl RenderOnce for Button {
self.label_color.unwrap_or_default()
};
self.base
.children(self.icon.map(|icon| {
ButtonIcon::new(icon)
.disabled(is_disabled)
.selected(is_selected)
.selected_icon(self.selected_icon)
.size(self.icon_size)
.color(self.icon_color)
}))
.child(
Label::new(label)
.color(label_color)
.line_height_style(LineHeightStyle::UILabel),
)
self.base.child(
h_stack()
.gap_1()
.map(|this| {
if self.icon_position == Some(IconPosition::End) {
this.flex_row_reverse()
} else {
this
}
})
.child(
Label::new(label)
.color(label_color)
.line_height_style(LineHeightStyle::UILabel),
)
.children(self.icon.map(|icon| {
ButtonIcon::new(icon)
.disabled(is_disabled)
.selected(is_selected)
.selected_icon(self.selected_icon)
.size(self.icon_size)
.color(self.icon_color)
})),
)
}
}

View file

@ -30,6 +30,13 @@ pub trait ButtonCommon: Clickable + Disableable {
fn tooltip(self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self;
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
pub enum IconPosition {
#[default]
Start,
End,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
pub enum ButtonStyle {
/// A filled button with a solid background color. Provides emphasis versus
@ -342,8 +349,12 @@ impl RenderOnce for ButtonLike {
.when_some(self.width, |this, width| this.w(width))
.rounded_md()
.gap_1()
.px_1()
.map(|this| match self.size {
ButtonSize::Default | ButtonSize::Compact => this.px_1(),
ButtonSize::None => this,
})
.bg(self.style.enabled(cx).background)
.when(self.disabled, |this| this.cursor_not_allowed())
.when(!self.disabled, |this| {
this.cursor_pointer()
.hover(|hover| hover.bg(self.style.hovered(cx).background))

View file

@ -239,7 +239,6 @@ impl Render for ContextMenu {
action,
} => {
let handler = handler.clone();
let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent));
let label_element = if let Some(icon) = icon {
h_stack()
@ -263,10 +262,7 @@ impl Render for ContextMenu {
})),
)
.selected(Some(ix) == self.selected_index)
.on_click(move |event, cx| {
handler(cx);
dismiss(event, cx)
})
.on_click(move |_, cx| handler(cx))
.into_any_element()
}
},

View file

@ -51,6 +51,7 @@ pub enum Icon {
CopilotDisabled,
Dash,
Envelope,
ExternalLink,
ExclamationTriangle,
Exit,
File,
@ -124,13 +125,13 @@ impl Icon {
Icon::Close => "icons/x.svg",
Icon::Collab => "icons/user_group_16.svg",
Icon::Copilot => "icons/copilot.svg",
Icon::CopilotInit => "icons/copilot_init.svg",
Icon::CopilotError => "icons/copilot_error.svg",
Icon::CopilotDisabled => "icons/copilot_disabled.svg",
Icon::Dash => "icons/dash.svg",
Icon::Envelope => "icons/feedback.svg",
Icon::ExclamationTriangle => "icons/warning.svg",
Icon::ExternalLink => "icons/external_link.svg",
Icon::Exit => "icons/exit.svg",
Icon::File => "icons/file.svg",
Icon::FileDoc => "icons/file_icons/book.svg",

View file

@ -98,11 +98,13 @@ impl RenderOnce for Key {
div()
.py_0()
.when_else(
single_char,
|el| el.w(rems(14. / 16.)).flex().flex_none().justify_center(),
|el| el.px_0p5(),
)
.map(|this| {
if single_char {
this.w(rems(14. / 16.)).flex().flex_none().justify_center()
} else {
this.px_0p5()
}
})
.h(rems(14. / 16.))
.text_ui()
.line_height(relative(1.))

View file

@ -10,6 +10,7 @@ mod label;
mod list;
mod list_header;
mod list_item;
mod tab;
pub use avatar::*;
pub use button::*;
@ -23,3 +24,4 @@ pub use label::*;
pub use list::*;
pub use list_header::*;
pub use list_item::*;
pub use tab::*;

View file

@ -4,7 +4,7 @@ use story::Story;
use crate::prelude::*;
use crate::{right_click_menu, ContextMenu, Label};
actions!(PrintCurrentDate, PrintBestFood);
actions!(context_menu, [PrintCurrentDate, PrintBestFood]);
fn build_menu(cx: &mut WindowContext, header: impl Into<SharedString>) -> View<ContextMenu> {
ContextMenu::build(cx, |menu, _| {

View file

@ -1,4 +1,5 @@
use gpui::{actions, Div, Render};
use gpui::NoAction;
use gpui::{Div, Render};
use itertools::Itertools;
use story::Story;
@ -7,8 +8,6 @@ use crate::KeyBinding;
pub struct KeybindingStory;
actions!(NoAction);
pub fn binding(key: &str) -> gpui::KeyBinding {
gpui::KeyBinding::new(key, NoAction {}, None)
}

View file

@ -0,0 +1,114 @@
use std::cmp::Ordering;
use gpui::{Div, Render};
use story::Story;
use crate::{prelude::*, TabPosition};
use crate::{Indicator, Tab};
pub struct TabStory;
impl Render for TabStory {
type Element = Div;
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
Story::container()
.child(Story::title_for::<Tab>())
.child(Story::label("Default"))
.child(h_stack().child(Tab::new("tab_1").child("Tab 1")))
.child(Story::label("With indicator"))
.child(
h_stack().child(
Tab::new("tab_1")
.start_slot(Indicator::dot().color(Color::Warning))
.child("Tab 1"),
),
)
.child(Story::label("With close button"))
.child(
h_stack().child(
Tab::new("tab_1")
.end_slot(
IconButton::new("close_button", Icon::Close)
.icon_color(Color::Muted)
.size(ButtonSize::None)
.icon_size(IconSize::XSmall),
)
.child("Tab 1"),
),
)
.child(Story::label("List of tabs"))
.child(
h_stack()
.child(Tab::new("tab_1").child("Tab 1"))
.child(Tab::new("tab_2").child("Tab 2")),
)
.child(Story::label("List of tabs with first tab selected"))
.child(
h_stack()
.child(
Tab::new("tab_1")
.selected(true)
.position(TabPosition::First)
.child("Tab 1"),
)
.child(
Tab::new("tab_2")
.position(TabPosition::Middle(Ordering::Greater))
.child("Tab 2"),
)
.child(
Tab::new("tab_3")
.position(TabPosition::Middle(Ordering::Greater))
.child("Tab 3"),
)
.child(Tab::new("tab_4").position(TabPosition::Last).child("Tab 4")),
)
.child(Story::label("List of tabs with last tab selected"))
.child(
h_stack()
.child(
Tab::new("tab_1")
.position(TabPosition::First)
.child("Tab 1"),
)
.child(
Tab::new("tab_2")
.position(TabPosition::Middle(Ordering::Less))
.child("Tab 2"),
)
.child(
Tab::new("tab_3")
.position(TabPosition::Middle(Ordering::Less))
.child("Tab 3"),
)
.child(
Tab::new("tab_4")
.position(TabPosition::Last)
.selected(true)
.child("Tab 4"),
),
)
.child(Story::label("List of tabs with second tab selected"))
.child(
h_stack()
.child(
Tab::new("tab_1")
.position(TabPosition::First)
.child("Tab 1"),
)
.child(
Tab::new("tab_2")
.position(TabPosition::Middle(Ordering::Equal))
.selected(true)
.child("Tab 2"),
)
.child(
Tab::new("tab_3")
.position(TabPosition::Middle(Ordering::Greater))
.child("Tab 3"),
)
.child(Tab::new("tab_4").position(TabPosition::Last).child("Tab 4")),
)
}
}

View file

@ -0,0 +1,198 @@
use std::cmp::Ordering;
use std::rc::Rc;
use gpui::{AnyElement, AnyView, ClickEvent, IntoElement, MouseButton};
use smallvec::SmallVec;
use crate::prelude::*;
/// The position of a [`Tab`] within a list of tabs.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum TabPosition {
/// The tab is first in the list.
First,
/// The tab is in the middle of the list (i.e., it is not the first or last tab).
///
/// The [`Ordering`] is where this tab is positioned with respect to the selected tab.
Middle(Ordering),
/// The tab is last in the list.
Last,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum TabCloseSide {
Start,
End,
}
#[derive(IntoElement)]
pub struct Tab {
id: ElementId,
selected: bool,
position: TabPosition,
close_side: TabCloseSide,
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>>,
start_slot: Option<AnyElement>,
end_slot: Option<AnyElement>,
children: SmallVec<[AnyElement; 2]>,
}
impl Tab {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
selected: false,
position: TabPosition::First,
close_side: TabCloseSide::End,
on_click: None,
tooltip: None,
start_slot: None,
end_slot: None,
children: SmallVec::new(),
}
}
pub fn position(mut self, position: TabPosition) -> Self {
self.position = position;
self
}
pub fn close_side(mut self, close_side: TabCloseSide) -> Self {
self.close_side = close_side;
self
}
pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
self.on_click = Some(Rc::new(handler));
self
}
pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
self.tooltip = Some(Box::new(tooltip));
self
}
pub fn start_slot<E: IntoElement>(mut self, element: impl Into<Option<E>>) -> Self {
self.start_slot = element.into().map(IntoElement::into_any_element);
self
}
pub fn end_slot<E: IntoElement>(mut self, element: impl Into<Option<E>>) -> Self {
self.end_slot = element.into().map(IntoElement::into_any_element);
self
}
}
impl Selectable for Tab {
fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
}
impl ParentElement for Tab {
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
&mut self.children
}
}
impl RenderOnce for Tab {
type Rendered = Div;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
const HEIGHT_IN_REMS: f32 = 30. / 16.;
let (text_color, tab_bg, _tab_hover_bg, _tab_active_bg) = match self.selected {
false => (
cx.theme().colors().text_muted,
cx.theme().colors().tab_inactive_background,
cx.theme().colors().ghost_element_hover,
cx.theme().colors().ghost_element_active,
),
true => (
cx.theme().colors().text,
cx.theme().colors().tab_active_background,
cx.theme().colors().element_hover,
cx.theme().colors().element_active,
),
};
div()
.h(rems(HEIGHT_IN_REMS))
.bg(tab_bg)
.border_color(cx.theme().colors().border)
.map(|this| match self.position {
TabPosition::First => {
if self.selected {
this.pl_px().border_r().pb_px()
} else {
this.pl_px().pr_px().border_b()
}
}
TabPosition::Last => {
if self.selected {
this.border_l().border_r().pb_px()
} else {
this.pr_px().pl_px().border_b()
}
}
TabPosition::Middle(Ordering::Equal) => this.border_l().border_r().pb_px(),
TabPosition::Middle(Ordering::Less) => this.border_l().pr_px().border_b(),
TabPosition::Middle(Ordering::Greater) => this.border_r().pl_px().border_b(),
})
.child(
h_stack()
.group("")
.id(self.id)
.relative()
.h_full()
.px_5()
.gap_1()
.text_color(text_color)
// .hover(|style| style.bg(tab_hover_bg))
// .active(|style| style.bg(tab_active_bg))
.when_some(self.on_click, |tab, on_click| {
tab.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.tooltip, |tab, tooltip| {
tab.tooltip(move |cx| tooltip(cx))
})
.child(
h_stack()
.w_3()
.h_3()
.justify_center()
.absolute()
.map(|this| match self.close_side {
TabCloseSide::Start => this.right_1(),
TabCloseSide::End => this.left_1(),
})
.children(self.start_slot),
)
.child(
h_stack()
.invisible()
.w_3()
.h_3()
.justify_center()
.absolute()
.map(|this| match self.close_side {
TabCloseSide::Start => this.left_1(),
TabCloseSide::End => this.right_1(),
})
.group_hover("", |style| style.visible())
.children(self.end_slot),
)
.children(self.children),
)
}
}

View file

@ -12,6 +12,6 @@ pub use crate::selectable::*;
pub use crate::{h_stack, v_stack};
pub use crate::{Button, ButtonSize, ButtonStyle, IconButton};
pub use crate::{ButtonCommon, Color, StyledExt};
pub use crate::{Icon, IconElement, IconSize};
pub use crate::{Icon, IconElement, IconPosition, IconSize};
pub use crate::{Label, LabelCommon, LabelSize, LineHeightStyle};
pub use theme::ActiveTheme;