Merge branch 'main' into refine-keybindings
This commit is contained in:
commit
f33cd3d463
250 changed files with 16332 additions and 14659 deletions
|
@ -2,56 +2,40 @@ mod avatar;
|
|||
mod button;
|
||||
mod checkbox;
|
||||
mod context_menu;
|
||||
mod details;
|
||||
mod disclosure;
|
||||
mod divider;
|
||||
mod elevated_surface;
|
||||
mod facepile;
|
||||
mod icon;
|
||||
mod icon_button;
|
||||
mod indicator;
|
||||
mod input;
|
||||
mod keybinding;
|
||||
mod label;
|
||||
mod list;
|
||||
mod modal;
|
||||
mod notification_toast;
|
||||
mod palette;
|
||||
mod panel;
|
||||
mod player;
|
||||
mod player_stack;
|
||||
mod popover;
|
||||
mod slot;
|
||||
mod stack;
|
||||
mod tab;
|
||||
mod toast;
|
||||
mod toggle;
|
||||
mod tool_divider;
|
||||
mod tooltip;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories;
|
||||
|
||||
pub use avatar::*;
|
||||
pub use button::*;
|
||||
pub use checkbox::*;
|
||||
pub use context_menu::*;
|
||||
pub use details::*;
|
||||
pub use disclosure::*;
|
||||
pub use divider::*;
|
||||
pub use elevated_surface::*;
|
||||
pub use facepile::*;
|
||||
pub use icon::*;
|
||||
pub use icon_button::*;
|
||||
pub use indicator::*;
|
||||
pub use input::*;
|
||||
pub use keybinding::*;
|
||||
pub use label::*;
|
||||
pub use list::*;
|
||||
pub use modal::*;
|
||||
pub use notification_toast::*;
|
||||
pub use palette::*;
|
||||
pub use panel::*;
|
||||
pub use player::*;
|
||||
pub use player_stack::*;
|
||||
pub use popover::*;
|
||||
pub use slot::*;
|
||||
pub use stack::*;
|
||||
pub use tab::*;
|
||||
pub use toast::*;
|
||||
pub use toggle::*;
|
||||
pub use tool_divider::*;
|
||||
pub use tooltip::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
|
|
@ -1,27 +1,25 @@
|
|||
use gpui::img;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::prelude::*;
|
||||
use gpui::{img, ImageData, ImageSource, Img, IntoElement};
|
||||
|
||||
#[derive(Component)]
|
||||
#[derive(Debug, Default, PartialEq, Clone)]
|
||||
pub enum Shape {
|
||||
#[default]
|
||||
Circle,
|
||||
RoundedRectangle,
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Avatar {
|
||||
src: SharedString,
|
||||
src: ImageSource,
|
||||
shape: Shape,
|
||||
}
|
||||
|
||||
impl Avatar {
|
||||
pub fn new(src: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
src: src.into(),
|
||||
shape: Shape::Circle,
|
||||
}
|
||||
}
|
||||
impl RenderOnce for Avatar {
|
||||
type Rendered = Img;
|
||||
|
||||
pub fn shape(mut self, shape: Shape) -> Self {
|
||||
self.shape = shape;
|
||||
self
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
fn render(self, _: &mut WindowContext) -> Self::Rendered {
|
||||
let mut img = img();
|
||||
|
||||
if self.shape == Shape::Circle {
|
||||
|
@ -30,37 +28,35 @@ impl Avatar {
|
|||
img = img.rounded_md();
|
||||
}
|
||||
|
||||
img.uri(self.src.clone())
|
||||
img.source(self.src.clone())
|
||||
.size_4()
|
||||
// todo!(Pull the avatar fallback background from the theme.)
|
||||
.bg(gpui::red())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use super::*;
|
||||
use crate::Story;
|
||||
use gpui::{Div, Render};
|
||||
|
||||
pub struct AvatarStory;
|
||||
|
||||
impl Render for AvatarStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, Avatar>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(Avatar::new(
|
||||
"https://avatars.githubusercontent.com/u/1714999?v=4",
|
||||
))
|
||||
.child(Avatar::new(
|
||||
"https://avatars.githubusercontent.com/u/326587?v=4",
|
||||
))
|
||||
impl Avatar {
|
||||
pub fn uri(src: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
src: src.into().into(),
|
||||
shape: Shape::Circle,
|
||||
}
|
||||
}
|
||||
pub fn data(src: Arc<ImageData>) -> Self {
|
||||
Self {
|
||||
src: src.into(),
|
||||
shape: Shape::Circle,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn source(src: ImageSource) -> Self {
|
||||
Self {
|
||||
src,
|
||||
shape: Shape::Circle,
|
||||
}
|
||||
}
|
||||
pub fn shape(mut self, shape: Shape) -> Self {
|
||||
self.shape = shape;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +1,28 @@
|
|||
use std::sync::Arc;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{DefiniteLength, Hsla, MouseButton, StatefulInteractiveComponent, WindowContext};
|
||||
use gpui::{
|
||||
DefiniteLength, Div, Hsla, IntoElement, MouseButton, MouseDownEvent,
|
||||
StatefulInteractiveElement, WindowContext,
|
||||
};
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{h_stack, Icon, IconButton, IconElement, Label, LineHeightStyle, TextColor};
|
||||
use crate::{h_stack, Color, Icon, IconButton, IconElement, Label, LineHeightStyle};
|
||||
|
||||
/// Provides the flexibility to use either a standard
|
||||
/// button or an icon button in a given context.
|
||||
pub enum ButtonOrIconButton<V: 'static> {
|
||||
Button(Button<V>),
|
||||
IconButton(IconButton<V>),
|
||||
pub enum ButtonOrIconButton {
|
||||
Button(Button),
|
||||
IconButton(IconButton),
|
||||
}
|
||||
|
||||
impl<V: 'static> From<Button<V>> for ButtonOrIconButton<V> {
|
||||
fn from(value: Button<V>) -> Self {
|
||||
impl From<Button> for ButtonOrIconButton {
|
||||
fn from(value: Button) -> Self {
|
||||
Self::Button(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> From<IconButton<V>> for ButtonOrIconButton<V> {
|
||||
fn from(value: IconButton<V>) -> Self {
|
||||
impl From<IconButton> for ButtonOrIconButton {
|
||||
fn from(value: IconButton) -> Self {
|
||||
Self::IconButton(value)
|
||||
}
|
||||
}
|
||||
|
@ -61,38 +64,74 @@ impl ButtonVariant {
|
|||
}
|
||||
}
|
||||
|
||||
pub type ClickHandler<V> = Arc<dyn Fn(&mut V, &mut ViewContext<V>)>;
|
||||
|
||||
struct ButtonHandlers<V: 'static> {
|
||||
click: Option<ClickHandler<V>>,
|
||||
}
|
||||
|
||||
unsafe impl<S> Send for ButtonHandlers<S> {}
|
||||
unsafe impl<S> Sync for ButtonHandlers<S> {}
|
||||
|
||||
impl<V: 'static> Default for ButtonHandlers<V> {
|
||||
fn default() -> Self {
|
||||
Self { click: None }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct Button<V: 'static> {
|
||||
#[derive(IntoElement)]
|
||||
pub struct Button {
|
||||
disabled: bool,
|
||||
handlers: ButtonHandlers<V>,
|
||||
click_handler: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext)>>,
|
||||
icon: Option<Icon>,
|
||||
icon_position: Option<IconPosition>,
|
||||
label: SharedString,
|
||||
variant: ButtonVariant,
|
||||
width: Option<DefiniteLength>,
|
||||
color: Option<TextColor>,
|
||||
color: Option<Color>,
|
||||
}
|
||||
|
||||
impl<V: 'static> Button<V> {
|
||||
impl RenderOnce for Button {
|
||||
type Rendered = gpui::Stateful<Div>;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
let (icon_color, label_color) = match (self.disabled, self.color) {
|
||||
(true, _) => (Color::Disabled, Color::Disabled),
|
||||
(_, None) => (Color::Default, Color::Default),
|
||||
(_, Some(color)) => (Color::from(color), color),
|
||||
};
|
||||
|
||||
let mut button = h_stack()
|
||||
.id(SharedString::from(format!("{}", self.label)))
|
||||
.relative()
|
||||
.p_1()
|
||||
.text_ui()
|
||||
.rounded_md()
|
||||
.bg(self.variant.bg_color(cx))
|
||||
.cursor_pointer()
|
||||
.hover(|style| style.bg(self.variant.bg_color_hover(cx)))
|
||||
.active(|style| style.bg(self.variant.bg_color_active(cx)));
|
||||
|
||||
match (self.icon, self.icon_position) {
|
||||
(Some(_), Some(IconPosition::Left)) => {
|
||||
button = button
|
||||
.gap_1()
|
||||
.child(self.render_label(label_color))
|
||||
.children(self.render_icon(icon_color))
|
||||
}
|
||||
(Some(_), Some(IconPosition::Right)) => {
|
||||
button = button
|
||||
.gap_1()
|
||||
.children(self.render_icon(icon_color))
|
||||
.child(self.render_label(label_color))
|
||||
}
|
||||
(_, _) => button = button.child(self.render_label(label_color)),
|
||||
}
|
||||
|
||||
if let Some(width) = self.width {
|
||||
button = button.w(width).justify_center();
|
||||
}
|
||||
|
||||
if let Some(click_handler) = self.click_handler.clone() {
|
||||
button = button.on_mouse_down(MouseButton::Left, move |event, cx| {
|
||||
click_handler(event, cx);
|
||||
});
|
||||
}
|
||||
|
||||
button
|
||||
}
|
||||
}
|
||||
|
||||
impl Button {
|
||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
disabled: false,
|
||||
handlers: ButtonHandlers::default(),
|
||||
click_handler: None,
|
||||
icon: None,
|
||||
icon_position: None,
|
||||
label: label.into(),
|
||||
|
@ -129,8 +168,11 @@ impl<V: 'static> Button<V> {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn on_click(mut self, handler: ClickHandler<V>) -> Self {
|
||||
self.handlers.click = Some(handler);
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
handler: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
|
||||
) -> Self {
|
||||
self.click_handler = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -139,14 +181,14 @@ impl<V: 'static> Button<V> {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: Option<TextColor>) -> Self {
|
||||
pub fn color(mut self, color: Option<Color>) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn label_color(&self, color: Option<TextColor>) -> TextColor {
|
||||
pub fn label_color(&self, color: Option<Color>) -> Color {
|
||||
if self.disabled {
|
||||
TextColor::Disabled
|
||||
Color::Disabled
|
||||
} else if let Some(color) = color {
|
||||
color
|
||||
} else {
|
||||
|
@ -154,249 +196,38 @@ impl<V: 'static> Button<V> {
|
|||
}
|
||||
}
|
||||
|
||||
fn render_label(&self, color: TextColor) -> Label {
|
||||
fn render_label(&self, color: Color) -> Label {
|
||||
Label::new(self.label.clone())
|
||||
.color(color)
|
||||
.line_height_style(LineHeightStyle::UILabel)
|
||||
}
|
||||
|
||||
fn render_icon(&self, icon_color: TextColor) -> Option<IconElement> {
|
||||
fn render_icon(&self, icon_color: Color) -> Option<IconElement> {
|
||||
self.icon.map(|i| IconElement::new(i).color(icon_color))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
let (icon_color, label_color) = match (self.disabled, self.color) {
|
||||
(true, _) => (TextColor::Disabled, TextColor::Disabled),
|
||||
(_, None) => (TextColor::Default, TextColor::Default),
|
||||
(_, Some(color)) => (TextColor::from(color), color),
|
||||
};
|
||||
#[derive(IntoElement)]
|
||||
pub struct ButtonGroup {
|
||||
buttons: Vec<Button>,
|
||||
}
|
||||
|
||||
let mut button = h_stack()
|
||||
.id(SharedString::from(format!("{}", self.label)))
|
||||
.relative()
|
||||
.p_1()
|
||||
.text_ui()
|
||||
.rounded_md()
|
||||
.bg(self.variant.bg_color(cx))
|
||||
.cursor_pointer()
|
||||
.hover(|style| style.bg(self.variant.bg_color_hover(cx)))
|
||||
.active(|style| style.bg(self.variant.bg_color_active(cx)));
|
||||
impl RenderOnce for ButtonGroup {
|
||||
type Rendered = Div;
|
||||
|
||||
match (self.icon, self.icon_position) {
|
||||
(Some(_), Some(IconPosition::Left)) => {
|
||||
button = button
|
||||
.gap_1()
|
||||
.child(self.render_label(label_color))
|
||||
.children(self.render_icon(icon_color))
|
||||
}
|
||||
(Some(_), Some(IconPosition::Right)) => {
|
||||
button = button
|
||||
.gap_1()
|
||||
.children(self.render_icon(icon_color))
|
||||
.child(self.render_label(label_color))
|
||||
}
|
||||
(_, _) => button = button.child(self.render_label(label_color)),
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
let mut group = h_stack();
|
||||
|
||||
for button in self.buttons.into_iter() {
|
||||
group = group.child(button.render(cx));
|
||||
}
|
||||
|
||||
if let Some(width) = self.width {
|
||||
button = button.w(width).justify_center();
|
||||
}
|
||||
|
||||
if let Some(click_handler) = self.handlers.click.clone() {
|
||||
button = button.on_mouse_down(MouseButton::Left, move |state, event, cx| {
|
||||
click_handler(state, cx);
|
||||
});
|
||||
}
|
||||
|
||||
button
|
||||
group
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct ButtonGroup<V: 'static> {
|
||||
buttons: Vec<Button<V>>,
|
||||
}
|
||||
|
||||
impl<V: 'static> ButtonGroup<V> {
|
||||
pub fn new(buttons: Vec<Button<V>>) -> Self {
|
||||
impl ButtonGroup {
|
||||
pub fn new(buttons: Vec<Button>) -> Self {
|
||||
Self { buttons }
|
||||
}
|
||||
|
||||
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
let mut el = h_stack().text_ui();
|
||||
|
||||
for button in self.buttons {
|
||||
el = el.child(button.render(_view, cx));
|
||||
}
|
||||
|
||||
el
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use super::*;
|
||||
use crate::{h_stack, v_stack, Story, TextColor};
|
||||
use gpui::{rems, Div, Render};
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
pub struct ButtonStory;
|
||||
|
||||
impl Render for ButtonStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let states = InteractionState::iter();
|
||||
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, Button<Self>>(cx))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_8()
|
||||
.child(
|
||||
div()
|
||||
.child(Story::label(cx, "Ghost (Default)"))
|
||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
||||
v_stack()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new(state.to_string()).color(TextColor::Muted),
|
||||
)
|
||||
.child(
|
||||
Button::new("Label").variant(ButtonVariant::Ghost), // .state(state),
|
||||
)
|
||||
})))
|
||||
.child(Story::label(cx, "Ghost – Left Icon"))
|
||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
||||
v_stack()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new(state.to_string()).color(TextColor::Muted),
|
||||
)
|
||||
.child(
|
||||
Button::new("Label")
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.icon(Icon::Plus)
|
||||
.icon_position(IconPosition::Left), // .state(state),
|
||||
)
|
||||
})))
|
||||
.child(Story::label(cx, "Ghost – Right Icon"))
|
||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
||||
v_stack()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new(state.to_string()).color(TextColor::Muted),
|
||||
)
|
||||
.child(
|
||||
Button::new("Label")
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.icon(Icon::Plus)
|
||||
.icon_position(IconPosition::Right), // .state(state),
|
||||
)
|
||||
}))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.child(Story::label(cx, "Filled"))
|
||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
||||
v_stack()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new(state.to_string()).color(TextColor::Muted),
|
||||
)
|
||||
.child(
|
||||
Button::new("Label").variant(ButtonVariant::Filled), // .state(state),
|
||||
)
|
||||
})))
|
||||
.child(Story::label(cx, "Filled – Left Button"))
|
||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
||||
v_stack()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new(state.to_string()).color(TextColor::Muted),
|
||||
)
|
||||
.child(
|
||||
Button::new("Label")
|
||||
.variant(ButtonVariant::Filled)
|
||||
.icon(Icon::Plus)
|
||||
.icon_position(IconPosition::Left), // .state(state),
|
||||
)
|
||||
})))
|
||||
.child(Story::label(cx, "Filled – Right Button"))
|
||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
||||
v_stack()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new(state.to_string()).color(TextColor::Muted),
|
||||
)
|
||||
.child(
|
||||
Button::new("Label")
|
||||
.variant(ButtonVariant::Filled)
|
||||
.icon(Icon::Plus)
|
||||
.icon_position(IconPosition::Right), // .state(state),
|
||||
)
|
||||
}))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.child(Story::label(cx, "Fixed With"))
|
||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
||||
v_stack()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new(state.to_string()).color(TextColor::Muted),
|
||||
)
|
||||
.child(
|
||||
Button::new("Label")
|
||||
.variant(ButtonVariant::Filled)
|
||||
// .state(state)
|
||||
.width(Some(rems(6.).into())),
|
||||
)
|
||||
})))
|
||||
.child(Story::label(cx, "Fixed With – Left Icon"))
|
||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
||||
v_stack()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new(state.to_string()).color(TextColor::Muted),
|
||||
)
|
||||
.child(
|
||||
Button::new("Label")
|
||||
.variant(ButtonVariant::Filled)
|
||||
// .state(state)
|
||||
.icon(Icon::Plus)
|
||||
.icon_position(IconPosition::Left)
|
||||
.width(Some(rems(6.).into())),
|
||||
)
|
||||
})))
|
||||
.child(Story::label(cx, "Fixed With – Right Icon"))
|
||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
||||
v_stack()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new(state.to_string()).color(TextColor::Muted),
|
||||
)
|
||||
.child(
|
||||
Button::new("Label")
|
||||
.variant(ButtonVariant::Filled)
|
||||
// .state(state)
|
||||
.icon(Icon::Plus)
|
||||
.icon_position(IconPosition::Right)
|
||||
.width(Some(rems(6.).into())),
|
||||
)
|
||||
}))),
|
||||
),
|
||||
)
|
||||
.child(Story::label(cx, "Button with `on_click`"))
|
||||
.child(
|
||||
Button::new("Label")
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.on_click(Arc::new(|_view, _cx| println!("Button clicked."))),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,48 +1,28 @@
|
|||
use gpui::{div, prelude::*, Component, ElementId, Styled, ViewContext};
|
||||
use std::sync::Arc;
|
||||
use gpui::{div, prelude::*, Div, Element, ElementId, IntoElement, Styled, WindowContext};
|
||||
|
||||
use theme2::ActiveTheme;
|
||||
|
||||
use crate::{Icon, IconElement, Selection, TextColor};
|
||||
use crate::{Color, Icon, IconElement, Selection};
|
||||
|
||||
pub type CheckHandler<V> = Arc<dyn Fn(Selection, &mut V, &mut ViewContext<V>) + Send + Sync>;
|
||||
pub type CheckHandler = Box<dyn Fn(&Selection, &mut WindowContext) + 'static>;
|
||||
|
||||
/// # Checkbox
|
||||
///
|
||||
/// Checkboxes are used for multiple choices, not for mutually exclusive choices.
|
||||
/// Each checkbox works independently from other checkboxes in the list,
|
||||
/// therefore checking an additional box does not affect any other selections.
|
||||
#[derive(Component)]
|
||||
pub struct Checkbox<V: 'static> {
|
||||
#[derive(IntoElement)]
|
||||
pub struct Checkbox {
|
||||
id: ElementId,
|
||||
checked: Selection,
|
||||
disabled: bool,
|
||||
on_click: Option<CheckHandler<V>>,
|
||||
on_click: Option<CheckHandler>,
|
||||
}
|
||||
|
||||
impl<V: 'static> Checkbox<V> {
|
||||
pub fn new(id: impl Into<ElementId>, checked: Selection) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
checked,
|
||||
disabled: false,
|
||||
on_click: None,
|
||||
}
|
||||
}
|
||||
impl RenderOnce for Checkbox {
|
||||
type Rendered = gpui::Stateful<Div>;
|
||||
|
||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
handler: impl 'static + Fn(Selection, &mut V, &mut ViewContext<V>) + Send + Sync,
|
||||
) -> Self {
|
||||
self.on_click = Some(Arc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
let group_id = format!("checkbox_group_{:?}", self.id);
|
||||
|
||||
let icon = match self.checked {
|
||||
|
@ -54,9 +34,9 @@ impl<V: 'static> Checkbox<V> {
|
|||
.color(
|
||||
// If the checkbox is disabled we change the color of the icon.
|
||||
if self.disabled {
|
||||
TextColor::Disabled
|
||||
Color::Disabled
|
||||
} else {
|
||||
TextColor::Selected
|
||||
Color::Selected
|
||||
},
|
||||
),
|
||||
)
|
||||
|
@ -69,9 +49,9 @@ impl<V: 'static> Checkbox<V> {
|
|||
.color(
|
||||
// If the checkbox is disabled we change the color of the icon.
|
||||
if self.disabled {
|
||||
TextColor::Disabled
|
||||
Color::Disabled
|
||||
} else {
|
||||
TextColor::Selected
|
||||
Color::Selected
|
||||
},
|
||||
),
|
||||
)
|
||||
|
@ -157,69 +137,149 @@ impl<V: 'static> Checkbox<V> {
|
|||
)
|
||||
.when_some(
|
||||
self.on_click.filter(|_| !self.disabled),
|
||||
|this, on_click| {
|
||||
this.on_click(move |view, _, cx| on_click(self.checked.inverse(), view, cx))
|
||||
},
|
||||
|this, on_click| this.on_click(move |_, cx| on_click(&self.checked.inverse(), cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use super::*;
|
||||
use crate::{h_stack, Story};
|
||||
use gpui::{Div, Render};
|
||||
|
||||
pub struct CheckboxStory;
|
||||
|
||||
impl Render for CheckboxStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, Checkbox<Self>>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(
|
||||
h_stack()
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.rounded_md()
|
||||
.border()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(Checkbox::new("checkbox-enabled", Selection::Unselected))
|
||||
.child(Checkbox::new(
|
||||
"checkbox-intermediate",
|
||||
Selection::Indeterminate,
|
||||
))
|
||||
.child(Checkbox::new("checkbox-selected", Selection::Selected)),
|
||||
)
|
||||
.child(Story::label(cx, "Disabled"))
|
||||
.child(
|
||||
h_stack()
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.rounded_md()
|
||||
.border()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
Checkbox::new("checkbox-disabled", Selection::Unselected)
|
||||
.disabled(true),
|
||||
)
|
||||
.child(
|
||||
Checkbox::new(
|
||||
"checkbox-disabled-intermediate",
|
||||
Selection::Indeterminate,
|
||||
)
|
||||
.disabled(true),
|
||||
)
|
||||
.child(
|
||||
Checkbox::new("checkbox-disabled-selected", Selection::Selected)
|
||||
.disabled(true),
|
||||
),
|
||||
)
|
||||
impl Checkbox {
|
||||
pub fn new(id: impl Into<ElementId>, checked: Selection) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
checked,
|
||||
disabled: false,
|
||||
on_click: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
handler: impl 'static + Fn(&Selection, &mut WindowContext) + Send + Sync,
|
||||
) -> Self {
|
||||
self.on_click = Some(Box::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn render(self, cx: &mut WindowContext) -> impl Element {
|
||||
let group_id = format!("checkbox_group_{:?}", self.id);
|
||||
|
||||
let icon = match self.checked {
|
||||
// When selected, we show a checkmark.
|
||||
Selection::Selected => {
|
||||
Some(
|
||||
IconElement::new(Icon::Check)
|
||||
.size(crate::IconSize::Small)
|
||||
.color(
|
||||
// If the checkbox is disabled we change the color of the icon.
|
||||
if self.disabled {
|
||||
Color::Disabled
|
||||
} else {
|
||||
Color::Selected
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
// In an indeterminate state, we show a dash.
|
||||
Selection::Indeterminate => {
|
||||
Some(
|
||||
IconElement::new(Icon::Dash)
|
||||
.size(crate::IconSize::Small)
|
||||
.color(
|
||||
// If the checkbox is disabled we change the color of the icon.
|
||||
if self.disabled {
|
||||
Color::Disabled
|
||||
} else {
|
||||
Color::Selected
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
// When unselected, we show nothing.
|
||||
Selection::Unselected => None,
|
||||
};
|
||||
|
||||
// A checkbox could be in an indeterminate state,
|
||||
// for example the indeterminate state could represent:
|
||||
// - a group of options of which only some are selected
|
||||
// - an enabled option that is no longer available
|
||||
// - a previously agreed to license that has been updated
|
||||
//
|
||||
// For the sake of styles we treat the indeterminate state as selected,
|
||||
// but it's icon will be different.
|
||||
let selected =
|
||||
self.checked == Selection::Selected || self.checked == Selection::Indeterminate;
|
||||
|
||||
// We could use something like this to make the checkbox background when selected:
|
||||
//
|
||||
// ~~~rust
|
||||
// ...
|
||||
// .when(selected, |this| {
|
||||
// this.bg(cx.theme().colors().element_selected)
|
||||
// })
|
||||
// ~~~
|
||||
//
|
||||
// But we use a match instead here because the checkbox might be disabled,
|
||||
// and it could be disabled _while_ it is selected, as well as while it is not selected.
|
||||
let (bg_color, border_color) = match (self.disabled, selected) {
|
||||
(true, _) => (
|
||||
cx.theme().colors().ghost_element_disabled,
|
||||
cx.theme().colors().border_disabled,
|
||||
),
|
||||
(false, true) => (
|
||||
cx.theme().colors().element_selected,
|
||||
cx.theme().colors().border,
|
||||
),
|
||||
(false, false) => (
|
||||
cx.theme().colors().element_background,
|
||||
cx.theme().colors().border,
|
||||
),
|
||||
};
|
||||
|
||||
div()
|
||||
.id(self.id)
|
||||
// Rather than adding `px_1()` to add some space around the checkbox,
|
||||
// we use a larger parent element to create a slightly larger
|
||||
// click area for the checkbox.
|
||||
.size_5()
|
||||
// Because we've enlarged the click area, we need to create a
|
||||
// `group` to pass down interactivity events to the checkbox.
|
||||
.group(group_id.clone())
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
// This prevent the flex element from growing
|
||||
// or shrinking in response to any size changes
|
||||
.flex_none()
|
||||
// The combo of `justify_center()` and `items_center()`
|
||||
// is used frequently to center elements in a flex container.
|
||||
//
|
||||
// We use this to center the icon in the checkbox.
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.m_1()
|
||||
.size_4()
|
||||
.rounded_sm()
|
||||
.bg(bg_color)
|
||||
.border()
|
||||
.border_color(border_color)
|
||||
// We only want the interactivity states to fire when we
|
||||
// are in a checkbox that isn't disabled.
|
||||
.when(!self.disabled, |this| {
|
||||
// Here instead of `hover()` we use `group_hover()`
|
||||
// to pass it the group id.
|
||||
this.group_hover(group_id.clone(), |el| {
|
||||
el.bg(cx.theme().colors().element_hover)
|
||||
})
|
||||
})
|
||||
.children(icon),
|
||||
)
|
||||
.when_some(
|
||||
self.on_click.filter(|_| !self.disabled),
|
||||
|this, on_click| this.on_click(move |_, cx| on_click(&self.checked.inverse(), cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,47 +1,42 @@
|
|||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHeader};
|
||||
use crate::{prelude::*, v_stack, Label, List};
|
||||
use crate::{ListItem, ListSeparator, ListSubHeader};
|
||||
use gpui::{
|
||||
overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, DispatchPhase, Div,
|
||||
EventEmitter, FocusHandle, FocusableView, LayoutId, ManagedView, Manager, MouseButton,
|
||||
MouseDownEvent, Pixels, Point, Render, View, VisualContext, WeakView,
|
||||
overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, ClickEvent, DismissEvent,
|
||||
DispatchPhase, Div, EventEmitter, FocusHandle, FocusableView, IntoElement, LayoutId,
|
||||
ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render, View, VisualContext,
|
||||
};
|
||||
|
||||
pub enum ContextMenuItem<V> {
|
||||
Separator(ListSeparator),
|
||||
Header(ListSubHeader),
|
||||
Entry(
|
||||
ListEntry<ContextMenu<V>>,
|
||||
Rc<dyn Fn(&mut V, &mut ViewContext<V>)>,
|
||||
),
|
||||
pub enum ContextMenuItem {
|
||||
Separator,
|
||||
Header(SharedString),
|
||||
Entry(SharedString, Rc<dyn Fn(&ClickEvent, &mut WindowContext)>),
|
||||
}
|
||||
|
||||
pub struct ContextMenu<V> {
|
||||
items: Vec<ContextMenuItem<V>>,
|
||||
pub struct ContextMenu {
|
||||
items: Vec<ContextMenuItem>,
|
||||
focus_handle: FocusHandle,
|
||||
handle: WeakView<V>,
|
||||
}
|
||||
|
||||
impl<V: Render> FocusableView for ContextMenu<V> {
|
||||
impl FocusableView for ContextMenu {
|
||||
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: Render> EventEmitter<Manager> for ContextMenu<V> {}
|
||||
impl EventEmitter<DismissEvent> for ContextMenu {}
|
||||
|
||||
impl<V: Render> ContextMenu<V> {
|
||||
impl ContextMenu {
|
||||
pub fn build(
|
||||
cx: &mut ViewContext<V>,
|
||||
f: impl FnOnce(Self, &mut ViewContext<Self>) -> Self,
|
||||
cx: &mut WindowContext,
|
||||
f: impl FnOnce(Self, &mut WindowContext) -> Self,
|
||||
) -> View<Self> {
|
||||
let handle = cx.view().downgrade();
|
||||
// let handle = cx.view().downgrade();
|
||||
cx.build_view(|cx| {
|
||||
f(
|
||||
Self {
|
||||
handle,
|
||||
items: Default::default(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
},
|
||||
|
@ -51,105 +46,103 @@ impl<V: Render> ContextMenu<V> {
|
|||
}
|
||||
|
||||
pub fn header(mut self, title: impl Into<SharedString>) -> Self {
|
||||
self.items
|
||||
.push(ContextMenuItem::Header(ListSubHeader::new(title)));
|
||||
self.items.push(ContextMenuItem::Header(title.into()));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn separator(mut self) -> Self {
|
||||
self.items.push(ContextMenuItem::Separator(ListSeparator));
|
||||
self.items.push(ContextMenuItem::Separator);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn entry(
|
||||
mut self,
|
||||
view: ListEntry<Self>,
|
||||
on_click: impl Fn(&mut V, &mut ViewContext<V>) + 'static,
|
||||
label: impl Into<SharedString>,
|
||||
on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
|
||||
) -> Self {
|
||||
self.items
|
||||
.push(ContextMenuItem::Entry(view, Rc::new(on_click)));
|
||||
.push(ContextMenuItem::Entry(label.into(), Rc::new(on_click)));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn action(self, view: ListEntry<Self>, action: Box<dyn Action>) -> Self {
|
||||
pub fn action(self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
|
||||
// todo: add the keybindings to the list entry
|
||||
self.entry(view, move |_, cx| cx.dispatch_action(action.boxed_clone()))
|
||||
self.entry(label.into(), move |_, cx| {
|
||||
cx.dispatch_action(action.boxed_clone())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
||||
// todo!()
|
||||
cx.emit(Manager::Dismiss);
|
||||
cx.emit(DismissEvent::Dismiss);
|
||||
}
|
||||
|
||||
pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(Manager::Dismiss);
|
||||
cx.emit(DismissEvent::Dismiss);
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: Render> Render for ContextMenu<V> {
|
||||
type Element = Div<Self>;
|
||||
impl Render for ContextMenu {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
div().elevation_2(cx).flex().flex_row().child(
|
||||
v_stack()
|
||||
.min_w(px(200.))
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_mouse_down_out(|this: &mut Self, _, cx| this.cancel(&Default::default(), cx))
|
||||
.on_mouse_down_out(
|
||||
cx.listener(|this: &mut Self, _, cx| this.cancel(&Default::default(), cx)),
|
||||
)
|
||||
// .on_action(ContextMenu::select_first)
|
||||
// .on_action(ContextMenu::select_last)
|
||||
// .on_action(ContextMenu::select_next)
|
||||
// .on_action(ContextMenu::select_prev)
|
||||
.on_action(ContextMenu::confirm)
|
||||
.on_action(ContextMenu::cancel)
|
||||
.on_action(cx.listener(ContextMenu::confirm))
|
||||
.on_action(cx.listener(ContextMenu::cancel))
|
||||
.flex_none()
|
||||
// .bg(cx.theme().colors().elevated_surface_background)
|
||||
// .border()
|
||||
// .border_color(cx.theme().colors().border)
|
||||
.child(List::new(
|
||||
self.items
|
||||
.iter()
|
||||
.map(|item| match item {
|
||||
ContextMenuItem::Separator(separator) => {
|
||||
ListItem::Separator(separator.clone())
|
||||
}
|
||||
ContextMenuItem::Header(header) => ListItem::Header(header.clone()),
|
||||
ContextMenuItem::Entry(entry, callback) => {
|
||||
let callback = callback.clone();
|
||||
let handle = self.handle.clone();
|
||||
ListItem::Entry(entry.clone().on_click(move |this, cx| {
|
||||
handle.update(cx, |view, cx| callback(view, cx)).ok();
|
||||
cx.emit(Manager::Dismiss);
|
||||
}))
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
)),
|
||||
.child(
|
||||
List::new().children(self.items.iter().map(|item| match item {
|
||||
ContextMenuItem::Separator => ListSeparator::new().into_any_element(),
|
||||
ContextMenuItem::Header(header) => {
|
||||
ListSubHeader::new(header.clone()).into_any_element()
|
||||
}
|
||||
ContextMenuItem::Entry(entry, callback) => {
|
||||
let callback = callback.clone();
|
||||
let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent::Dismiss));
|
||||
|
||||
ListItem::new(entry.clone())
|
||||
.child(Label::new(entry.clone()))
|
||||
.on_click(move |event, cx| {
|
||||
callback(event, cx);
|
||||
dismiss(event, cx)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MenuHandle<V: 'static, M: ManagedView> {
|
||||
id: Option<ElementId>,
|
||||
child_builder: Option<Box<dyn FnOnce(bool) -> AnyElement<V> + 'static>>,
|
||||
menu_builder: Option<Rc<dyn Fn(&mut V, &mut ViewContext<V>) -> View<M> + 'static>>,
|
||||
|
||||
pub struct MenuHandle<M: ManagedView> {
|
||||
id: ElementId,
|
||||
child_builder: Option<Box<dyn FnOnce(bool) -> AnyElement + 'static>>,
|
||||
menu_builder: Option<Rc<dyn Fn(&mut WindowContext) -> View<M> + 'static>>,
|
||||
anchor: Option<AnchorCorner>,
|
||||
attach: Option<AnchorCorner>,
|
||||
}
|
||||
|
||||
impl<V: 'static, M: ManagedView> MenuHandle<V, M> {
|
||||
pub fn id(mut self, id: impl Into<ElementId>) -> Self {
|
||||
self.id = Some(id.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn menu(mut self, f: impl Fn(&mut V, &mut ViewContext<V>) -> View<M> + 'static) -> Self {
|
||||
impl<M: ManagedView> MenuHandle<M> {
|
||||
pub fn menu(mut self, f: impl Fn(&mut WindowContext) -> View<M> + 'static) -> Self {
|
||||
self.menu_builder = Some(Rc::new(f));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn child<R: Component<V>>(mut self, f: impl FnOnce(bool) -> R + 'static) -> Self {
|
||||
self.child_builder = Some(Box::new(|b| f(b).render()));
|
||||
pub fn child<R: IntoElement>(mut self, f: impl FnOnce(bool) -> R + 'static) -> Self {
|
||||
self.child_builder = Some(Box::new(|b| f(b).into_element().into_any()));
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -167,9 +160,9 @@ impl<V: 'static, M: ManagedView> MenuHandle<V, M> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn menu_handle<V: 'static, M: ManagedView>() -> MenuHandle<V, M> {
|
||||
pub fn menu_handle<M: ManagedView>(id: impl Into<ElementId>) -> MenuHandle<M> {
|
||||
MenuHandle {
|
||||
id: None,
|
||||
id: id.into(),
|
||||
child_builder: None,
|
||||
menu_builder: None,
|
||||
anchor: None,
|
||||
|
@ -177,26 +170,21 @@ pub fn menu_handle<V: 'static, M: ManagedView>() -> MenuHandle<V, M> {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct MenuHandleState<V, M> {
|
||||
pub struct MenuHandleState<M> {
|
||||
menu: Rc<RefCell<Option<View<M>>>>,
|
||||
position: Rc<RefCell<Point<Pixels>>>,
|
||||
child_layout_id: Option<LayoutId>,
|
||||
child_element: Option<AnyElement<V>>,
|
||||
menu_element: Option<AnyElement<V>>,
|
||||
child_element: Option<AnyElement>,
|
||||
menu_element: Option<AnyElement>,
|
||||
}
|
||||
impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
|
||||
type ElementState = MenuHandleState<V, M>;
|
||||
|
||||
fn element_id(&self) -> Option<gpui::ElementId> {
|
||||
Some(self.id.clone().expect("menu_handle must have an id()"))
|
||||
}
|
||||
impl<M: ManagedView> Element for MenuHandle<M> {
|
||||
type State = MenuHandleState<M>;
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
view_state: &mut V,
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut crate::ViewContext<V>,
|
||||
) -> (gpui::LayoutId, Self::ElementState) {
|
||||
element_state: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (gpui::LayoutId, Self::State) {
|
||||
let (menu, position) = if let Some(element_state) = element_state {
|
||||
(element_state.menu, element_state.position)
|
||||
} else {
|
||||
|
@ -206,15 +194,15 @@ impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
|
|||
let mut menu_layout_id = None;
|
||||
|
||||
let menu_element = menu.borrow_mut().as_mut().map(|menu| {
|
||||
let mut overlay = overlay::<V>().snap_to_window();
|
||||
let mut overlay = overlay().snap_to_window();
|
||||
if let Some(anchor) = self.anchor {
|
||||
overlay = overlay.anchor(anchor);
|
||||
}
|
||||
overlay = overlay.position(*position.borrow());
|
||||
|
||||
let mut view = overlay.child(menu.clone()).render();
|
||||
menu_layout_id = Some(view.layout(view_state, cx));
|
||||
view
|
||||
let mut element = overlay.child(menu.clone()).into_any();
|
||||
menu_layout_id = Some(element.layout(cx));
|
||||
element
|
||||
});
|
||||
|
||||
let mut child_element = self
|
||||
|
@ -224,7 +212,7 @@ impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
|
|||
|
||||
let child_layout_id = child_element
|
||||
.as_mut()
|
||||
.map(|child_element| child_element.layout(view_state, cx));
|
||||
.map(|child_element| child_element.layout(cx));
|
||||
|
||||
let layout_id = cx.request_layout(
|
||||
&gpui::Style::default(),
|
||||
|
@ -244,22 +232,21 @@ impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
|
|||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
self,
|
||||
bounds: Bounds<gpui::Pixels>,
|
||||
view_state: &mut V,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut crate::ViewContext<V>,
|
||||
element_state: &mut Self::State,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
if let Some(child) = element_state.child_element.as_mut() {
|
||||
child.paint(view_state, cx);
|
||||
if let Some(child) = element_state.child_element.take() {
|
||||
child.paint(cx);
|
||||
}
|
||||
|
||||
if let Some(menu) = element_state.menu_element.as_mut() {
|
||||
menu.paint(view_state, cx);
|
||||
if let Some(menu) = element_state.menu_element.take() {
|
||||
menu.paint(cx);
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(builder) = self.menu_builder.clone() else {
|
||||
let Some(builder) = self.menu_builder else {
|
||||
return;
|
||||
};
|
||||
let menu = element_state.menu.clone();
|
||||
|
@ -267,7 +254,7 @@ impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
|
|||
let attach = self.attach.clone();
|
||||
let child_layout_id = element_state.child_layout_id.clone();
|
||||
|
||||
cx.on_mouse_event(move |view_state, event: &MouseDownEvent, phase, cx| {
|
||||
cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
|
||||
if phase == DispatchPhase::Bubble
|
||||
&& event.button == MouseButton::Right
|
||||
&& bounds.contains_point(&event.position)
|
||||
|
@ -275,10 +262,10 @@ impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
|
|||
cx.stop_propagation();
|
||||
cx.prevent_default();
|
||||
|
||||
let new_menu = (builder)(view_state, cx);
|
||||
let new_menu = (builder)(cx);
|
||||
let menu2 = menu.clone();
|
||||
cx.subscribe(&new_menu, move |this, modal, e, cx| match e {
|
||||
&Manager::Dismiss => {
|
||||
cx.subscribe(&new_menu, move |modal, e, cx| match e {
|
||||
&DismissEvent::Dismiss => {
|
||||
*menu2.borrow_mut() = None;
|
||||
cx.notify();
|
||||
}
|
||||
|
@ -300,129 +287,14 @@ impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<V: 'static, M: ManagedView> Component<V> for MenuHandle<V, M> {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
AnyElement::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use super::*;
|
||||
use crate::story::Story;
|
||||
use gpui::{actions, Div, Render};
|
||||
|
||||
actions!(PrintCurrentDate, PrintBestFood);
|
||||
|
||||
fn build_menu<V: Render>(
|
||||
cx: &mut ViewContext<V>,
|
||||
header: impl Into<SharedString>,
|
||||
) -> View<ContextMenu<V>> {
|
||||
let handle = cx.view().clone();
|
||||
ContextMenu::build(cx, |menu, _| {
|
||||
menu.header(header)
|
||||
.separator()
|
||||
.entry(ListEntry::new(Label::new("Print current time")), |v, cx| {
|
||||
println!("dispatching PrintCurrentTime action");
|
||||
cx.dispatch_action(PrintCurrentDate.boxed_clone())
|
||||
})
|
||||
.entry(ListEntry::new(Label::new("Print best food")), |v, cx| {
|
||||
cx.dispatch_action(PrintBestFood.boxed_clone())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub struct ContextMenuStory;
|
||||
|
||||
impl Render for ContextMenuStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container(cx)
|
||||
.on_action(|_, _: &PrintCurrentDate, _| {
|
||||
println!("printing unix time!");
|
||||
if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() {
|
||||
println!("Current Unix time is {:?}", unix_time.as_secs());
|
||||
}
|
||||
})
|
||||
.on_action(|_, _: &PrintBestFood, _| {
|
||||
println!("burrito");
|
||||
})
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_between()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.justify_between()
|
||||
.child(
|
||||
menu_handle()
|
||||
.id("test2")
|
||||
.child(|is_open| {
|
||||
Label::new(if is_open {
|
||||
"TOP LEFT"
|
||||
} else {
|
||||
"RIGHT CLICK ME"
|
||||
})
|
||||
.render()
|
||||
})
|
||||
.menu(move |_, cx| build_menu(cx, "top left")),
|
||||
)
|
||||
.child(
|
||||
menu_handle()
|
||||
.id("test1")
|
||||
.child(|is_open| {
|
||||
Label::new(if is_open {
|
||||
"BOTTOM LEFT"
|
||||
} else {
|
||||
"RIGHT CLICK ME"
|
||||
})
|
||||
.render()
|
||||
})
|
||||
.anchor(AnchorCorner::BottomLeft)
|
||||
.attach(AnchorCorner::TopLeft)
|
||||
.menu(move |_, cx| build_menu(cx, "bottom left")),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.justify_between()
|
||||
.child(
|
||||
menu_handle()
|
||||
.id("test3")
|
||||
.child(|is_open| {
|
||||
Label::new(if is_open {
|
||||
"TOP RIGHT"
|
||||
} else {
|
||||
"RIGHT CLICK ME"
|
||||
})
|
||||
.render()
|
||||
})
|
||||
.anchor(AnchorCorner::TopRight)
|
||||
.menu(move |_, cx| build_menu(cx, "top right")),
|
||||
)
|
||||
.child(
|
||||
menu_handle()
|
||||
.id("test4")
|
||||
.child(|is_open| {
|
||||
Label::new(if is_open {
|
||||
"BOTTOM RIGHT"
|
||||
} else {
|
||||
"RIGHT CLICK ME"
|
||||
})
|
||||
.render()
|
||||
})
|
||||
.anchor(AnchorCorner::BottomRight)
|
||||
.attach(AnchorCorner::TopRight)
|
||||
.menu(move |_, cx| build_menu(cx, "bottom right")),
|
||||
),
|
||||
)
|
||||
}
|
||||
impl<M: ManagedView> IntoElement for MenuHandle<M> {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<gpui::ElementId> {
|
||||
Some(self.id.clone())
|
||||
}
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
use crate::prelude::*;
|
||||
use crate::{v_stack, ButtonGroup};
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct Details<V: 'static> {
|
||||
text: &'static str,
|
||||
meta: Option<&'static str>,
|
||||
actions: Option<ButtonGroup<V>>,
|
||||
}
|
||||
|
||||
impl<V: 'static> Details<V> {
|
||||
pub fn new(text: &'static str) -> Self {
|
||||
Self {
|
||||
text,
|
||||
meta: None,
|
||||
actions: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn meta_text(mut self, meta: &'static str) -> Self {
|
||||
self.meta = Some(meta);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn actions(mut self, actions: ButtonGroup<V>) -> Self {
|
||||
self.actions = Some(actions);
|
||||
self
|
||||
}
|
||||
|
||||
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
v_stack()
|
||||
.p_1()
|
||||
.gap_0p5()
|
||||
.text_ui_sm()
|
||||
.text_color(cx.theme().colors().text)
|
||||
.size_full()
|
||||
.child(self.text)
|
||||
.children(self.meta.map(|m| m))
|
||||
.children(self.actions.map(|a| a))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use super::*;
|
||||
use crate::{Button, Story};
|
||||
use gpui::{Div, Render};
|
||||
|
||||
pub struct DetailsStory;
|
||||
|
||||
impl Render for DetailsStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, Details<Self>>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(Details::new("The quick brown fox jumps over the lazy dog"))
|
||||
.child(Story::label(cx, "With meta"))
|
||||
.child(
|
||||
Details::new("The quick brown fox jumps over the lazy dog")
|
||||
.meta_text("Sphinx of black quartz, judge my vow."),
|
||||
)
|
||||
.child(Story::label(cx, "With meta and actions"))
|
||||
.child(
|
||||
Details::new("The quick brown fox jumps over the lazy dog")
|
||||
.meta_text("Sphinx of black quartz, judge my vow.")
|
||||
.actions(ButtonGroup::new(vec![
|
||||
Button::new("Decline"),
|
||||
Button::new("Accept").variant(crate::ButtonVariant::Filled),
|
||||
])),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
19
crates/ui2/src/components/disclosure.rs
Normal file
19
crates/ui2/src/components/disclosure.rs
Normal file
|
@ -0,0 +1,19 @@
|
|||
use gpui::{div, Element, ParentElement};
|
||||
|
||||
use crate::{Color, Icon, IconElement, IconSize, Toggle};
|
||||
|
||||
pub fn disclosure_control(toggle: Toggle) -> impl Element {
|
||||
match (toggle.is_toggleable(), toggle.is_toggled()) {
|
||||
(false, _) => div(),
|
||||
(_, true) => div().child(
|
||||
IconElement::new(Icon::ChevronDown)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small),
|
||||
),
|
||||
(_, false) => div().child(
|
||||
IconElement::new(Icon::ChevronRight)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small),
|
||||
),
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
use gpui::{Div, IntoElement};
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
enum DividerDirection {
|
||||
|
@ -5,12 +7,29 @@ enum DividerDirection {
|
|||
Vertical,
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
#[derive(IntoElement)]
|
||||
pub struct Divider {
|
||||
direction: DividerDirection,
|
||||
inset: bool,
|
||||
}
|
||||
|
||||
impl RenderOnce for Divider {
|
||||
type Rendered = Div;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
div()
|
||||
.map(|this| match self.direction {
|
||||
DividerDirection::Horizontal => {
|
||||
this.h_px().w_full().when(self.inset, |this| this.mx_1p5())
|
||||
}
|
||||
DividerDirection::Vertical => {
|
||||
this.w_px().h_full().when(self.inset, |this| this.my_1p5())
|
||||
}
|
||||
})
|
||||
.bg(cx.theme().colors().border_variant)
|
||||
}
|
||||
}
|
||||
|
||||
impl Divider {
|
||||
pub fn horizontal() -> Self {
|
||||
Self {
|
||||
|
@ -31,7 +50,7 @@ impl Divider {
|
|||
self
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
fn render(self, cx: &mut WindowContext) -> impl Element {
|
||||
div()
|
||||
.map(|this| match self.direction {
|
||||
DividerDirection::Horizontal => {
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
use gpui::Div;
|
||||
|
||||
use crate::{prelude::*, v_stack};
|
||||
|
||||
/// Create an elevated surface.
|
||||
///
|
||||
/// Must be used inside of a relative parent element
|
||||
pub fn elevated_surface<V: 'static>(level: ElevationIndex, cx: &mut ViewContext<V>) -> Div<V> {
|
||||
let colors = cx.theme().colors();
|
||||
|
||||
// let shadow = BoxShadow {
|
||||
// color: hsla(0., 0., 0., 0.1),
|
||||
// offset: point(px(0.), px(1.)),
|
||||
// blur_radius: px(3.),
|
||||
// spread_radius: px(0.),
|
||||
// };
|
||||
|
||||
v_stack()
|
||||
.rounded_lg()
|
||||
.bg(colors.elevated_surface_background)
|
||||
.border()
|
||||
.border_color(colors.border)
|
||||
.shadow(level.shadow())
|
||||
}
|
||||
|
||||
pub fn modal<V: 'static>(cx: &mut ViewContext<V>) -> Div<V> {
|
||||
elevated_surface(ElevationIndex::ModalSurface, cx)
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
use crate::prelude::*;
|
||||
use crate::{Avatar, Player};
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct Facepile {
|
||||
players: Vec<Player>,
|
||||
}
|
||||
|
||||
impl Facepile {
|
||||
pub fn new<P: Iterator<Item = Player>>(players: P) -> Self {
|
||||
Self {
|
||||
players: players.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
let player_count = self.players.len();
|
||||
let player_list = self.players.iter().enumerate().map(|(ix, player)| {
|
||||
let isnt_last = ix < player_count - 1;
|
||||
|
||||
div()
|
||||
.when(isnt_last, |div| div.neg_mr_1())
|
||||
.child(Avatar::new(player.avatar_src().to_string()))
|
||||
});
|
||||
div().p_1().flex().items_center().children(player_list)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use super::*;
|
||||
use crate::{static_players, Story};
|
||||
use gpui::{Div, Render};
|
||||
|
||||
pub struct FacepileStory;
|
||||
|
||||
impl Render for FacepileStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let players = static_players();
|
||||
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, Facepile>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_3()
|
||||
.child(Facepile::new(players.clone().into_iter().take(1)))
|
||||
.child(Facepile::new(players.clone().into_iter().take(2)))
|
||||
.child(Facepile::new(players.clone().into_iter().take(3))),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
use gpui::{rems, svg};
|
||||
use gpui::{rems, svg, IntoElement, Svg};
|
||||
use strum::EnumIter;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
@ -25,6 +25,7 @@ pub enum Icon {
|
|||
BellOff,
|
||||
BellRing,
|
||||
Bolt,
|
||||
CaseSensitive,
|
||||
Check,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
|
@ -33,6 +34,9 @@ pub enum Icon {
|
|||
Close,
|
||||
Collab,
|
||||
Copilot,
|
||||
CopilotInit,
|
||||
CopilotError,
|
||||
CopilotDisabled,
|
||||
Dash,
|
||||
Envelope,
|
||||
ExclamationTriangle,
|
||||
|
@ -67,6 +71,7 @@ pub enum Icon {
|
|||
Split,
|
||||
SplitMessage,
|
||||
Terminal,
|
||||
WholeWord,
|
||||
XCircle,
|
||||
Command,
|
||||
Control,
|
||||
|
@ -91,6 +96,7 @@ impl Icon {
|
|||
Icon::BellOff => "icons/bell-off.svg",
|
||||
Icon::BellRing => "icons/bell-ring.svg",
|
||||
Icon::Bolt => "icons/bolt.svg",
|
||||
Icon::CaseSensitive => "icons/case_insensitive.svg",
|
||||
Icon::Check => "icons/check.svg",
|
||||
Icon::ChevronDown => "icons/chevron_down.svg",
|
||||
Icon::ChevronLeft => "icons/chevron_left.svg",
|
||||
|
@ -99,6 +105,9 @@ impl Icon {
|
|||
Icon::Close => "icons/x.svg",
|
||||
Icon::Collab => "icons/user_group_16.svg",
|
||||
Icon::Copilot => "icons/copilot.svg",
|
||||
Icon::CopilotInit => "icons/copilot_init.svg",
|
||||
Icon::CopilotError => "icons/copilot_error.svg",
|
||||
Icon::CopilotDisabled => "icons/copilot_disabled.svg",
|
||||
Icon::Dash => "icons/dash.svg",
|
||||
Icon::Envelope => "icons/feedback.svg",
|
||||
Icon::ExclamationTriangle => "icons/warning.svg",
|
||||
|
@ -133,6 +142,7 @@ impl Icon {
|
|||
Icon::Split => "icons/split.svg",
|
||||
Icon::SplitMessage => "icons/split_message.svg",
|
||||
Icon::Terminal => "icons/terminal.svg",
|
||||
Icon::WholeWord => "icons/word_search.svg",
|
||||
Icon::XCircle => "icons/error.svg",
|
||||
Icon::Command => "icons/command.svg",
|
||||
Icon::Control => "icons/control.svg",
|
||||
|
@ -143,41 +153,17 @@ impl Icon {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
#[derive(IntoElement)]
|
||||
pub struct IconElement {
|
||||
path: SharedString,
|
||||
color: TextColor,
|
||||
color: Color,
|
||||
size: IconSize,
|
||||
}
|
||||
|
||||
impl IconElement {
|
||||
pub fn new(icon: Icon) -> Self {
|
||||
Self {
|
||||
path: icon.path().into(),
|
||||
color: TextColor::default(),
|
||||
size: IconSize::default(),
|
||||
}
|
||||
}
|
||||
impl RenderOnce for IconElement {
|
||||
type Rendered = Svg;
|
||||
|
||||
pub fn from_path(path: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
path: path.into(),
|
||||
color: TextColor::default(),
|
||||
size: IconSize::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: TextColor) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn size(mut self, size: IconSize) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
let svg_size = match self.size {
|
||||
IconSize::Small => rems(14. / 16.),
|
||||
IconSize::Medium => rems(16. / 16.),
|
||||
|
@ -191,30 +177,43 @@ impl IconElement {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use gpui::{Div, Render};
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::Story;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub struct IconStory;
|
||||
|
||||
impl Render for IconStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let icons = Icon::iter();
|
||||
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, IconElement>(cx))
|
||||
.child(Story::label(cx, "All Icons"))
|
||||
.child(div().flex().gap_3().children(icons.map(IconElement::new)))
|
||||
impl IconElement {
|
||||
pub fn new(icon: Icon) -> Self {
|
||||
Self {
|
||||
path: icon.path().into(),
|
||||
color: Color::default(),
|
||||
size: IconSize::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_path(path: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
path: path.into(),
|
||||
color: Color::default(),
|
||||
size: IconSize::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: Color) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn size(mut self, size: IconSize) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> impl Element {
|
||||
let svg_size = match self.size {
|
||||
IconSize::Small => rems(0.75),
|
||||
IconSize::Medium => rems(0.9375),
|
||||
};
|
||||
|
||||
svg()
|
||||
.size(svg_size)
|
||||
.flex_none()
|
||||
.path(self.path)
|
||||
.text_color(self.color.color(cx))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,40 +1,87 @@
|
|||
use crate::{h_stack, prelude::*, ClickHandler, Icon, IconElement};
|
||||
use gpui::{prelude::*, Action, AnyView, MouseButton};
|
||||
use std::sync::Arc;
|
||||
use crate::{h_stack, prelude::*, Icon, IconElement};
|
||||
use gpui::{prelude::*, Action, AnyView, Div, MouseButton, MouseDownEvent, Stateful};
|
||||
|
||||
struct IconButtonHandlers<V: 'static> {
|
||||
click: Option<ClickHandler<V>>,
|
||||
}
|
||||
|
||||
impl<V: 'static> Default for IconButtonHandlers<V> {
|
||||
fn default() -> Self {
|
||||
Self { click: None }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct IconButton<V: 'static> {
|
||||
#[derive(IntoElement)]
|
||||
pub struct IconButton {
|
||||
id: ElementId,
|
||||
icon: Icon,
|
||||
color: TextColor,
|
||||
color: Color,
|
||||
variant: ButtonVariant,
|
||||
state: InteractionState,
|
||||
selected: bool,
|
||||
tooltip: Option<Box<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static>>,
|
||||
handlers: IconButtonHandlers<V>,
|
||||
tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>>,
|
||||
on_mouse_down: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
|
||||
}
|
||||
|
||||
impl<V: 'static> IconButton<V> {
|
||||
impl RenderOnce for IconButton {
|
||||
type Rendered = Stateful<Div>;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
let icon_color = match (self.state, self.color) {
|
||||
(InteractionState::Disabled, _) => Color::Disabled,
|
||||
(InteractionState::Active, _) => Color::Selected,
|
||||
_ => self.color,
|
||||
};
|
||||
|
||||
let (mut bg_color, bg_hover_color, bg_active_color) = match self.variant {
|
||||
ButtonVariant::Filled => (
|
||||
cx.theme().colors().element_background,
|
||||
cx.theme().colors().element_hover,
|
||||
cx.theme().colors().element_active,
|
||||
),
|
||||
ButtonVariant::Ghost => (
|
||||
cx.theme().colors().ghost_element_background,
|
||||
cx.theme().colors().ghost_element_hover,
|
||||
cx.theme().colors().ghost_element_active,
|
||||
),
|
||||
};
|
||||
|
||||
if self.selected {
|
||||
bg_color = cx.theme().colors().element_selected;
|
||||
}
|
||||
|
||||
let mut button = h_stack()
|
||||
.id(self.id.clone())
|
||||
.justify_center()
|
||||
.rounded_md()
|
||||
.p_1()
|
||||
.bg(bg_color)
|
||||
.cursor_pointer()
|
||||
// Nate: Trying to figure out the right places we want to show a
|
||||
// hover state here. I think it is a bit heavy to have it on every
|
||||
// place we use an icon button.
|
||||
// .hover(|style| style.bg(bg_hover_color))
|
||||
.active(|style| style.bg(bg_active_color))
|
||||
.child(IconElement::new(self.icon).color(icon_color));
|
||||
|
||||
if let Some(click_handler) = self.on_mouse_down {
|
||||
button = button.on_mouse_down(MouseButton::Left, move |event, cx| {
|
||||
cx.stop_propagation();
|
||||
click_handler(event, cx);
|
||||
})
|
||||
}
|
||||
|
||||
if let Some(tooltip) = self.tooltip {
|
||||
if !self.selected {
|
||||
button = button.tooltip(move |cx| tooltip(cx))
|
||||
}
|
||||
}
|
||||
|
||||
button
|
||||
}
|
||||
}
|
||||
|
||||
impl IconButton {
|
||||
pub fn new(id: impl Into<ElementId>, icon: Icon) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
icon,
|
||||
color: TextColor::default(),
|
||||
color: Color::default(),
|
||||
variant: ButtonVariant::default(),
|
||||
state: InteractionState::default(),
|
||||
selected: false,
|
||||
tooltip: None,
|
||||
handlers: IconButtonHandlers::default(),
|
||||
on_mouse_down: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,7 +90,7 @@ impl<V: 'static> IconButton<V> {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: TextColor) -> Self {
|
||||
pub fn color(mut self, color: Color) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
|
@ -63,74 +110,20 @@ impl<V: 'static> IconButton<V> {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn tooltip(
|
||||
mut self,
|
||||
tooltip: impl Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static,
|
||||
) -> Self {
|
||||
pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
|
||||
self.tooltip = Some(Box::new(tooltip));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click(mut self, handler: impl 'static + Fn(&mut V, &mut ViewContext<V>)) -> Self {
|
||||
self.handlers.click = Some(Arc::new(handler));
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
handler: impl 'static + Fn(&MouseDownEvent, &mut WindowContext),
|
||||
) -> Self {
|
||||
self.on_mouse_down = Some(Box::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn action(self, action: Box<dyn Action>) -> Self {
|
||||
self.on_click(move |this, cx| cx.dispatch_action(action.boxed_clone()))
|
||||
}
|
||||
|
||||
fn render(mut self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
let icon_color = match (self.state, self.color) {
|
||||
(InteractionState::Disabled, _) => TextColor::Disabled,
|
||||
(InteractionState::Active, _) => TextColor::Selected,
|
||||
_ => self.color,
|
||||
};
|
||||
|
||||
let (mut bg_color, bg_hover_color, bg_active_color) = match self.variant {
|
||||
ButtonVariant::Filled => (
|
||||
cx.theme().colors().element_background,
|
||||
cx.theme().colors().element_hover,
|
||||
cx.theme().colors().element_active,
|
||||
),
|
||||
ButtonVariant::Ghost => (
|
||||
cx.theme().colors().ghost_element_background,
|
||||
cx.theme().colors().ghost_element_hover,
|
||||
cx.theme().colors().ghost_element_active,
|
||||
),
|
||||
};
|
||||
|
||||
if self.selected {
|
||||
bg_color = bg_hover_color;
|
||||
}
|
||||
|
||||
let mut button = h_stack()
|
||||
.id(self.id.clone())
|
||||
.justify_center()
|
||||
.rounded_md()
|
||||
.p_1()
|
||||
.bg(bg_color)
|
||||
.cursor_pointer()
|
||||
// Nate: Trying to figure out the right places we want to show a
|
||||
// hover state here. I think it is a bit heavy to have it on every
|
||||
// place we use an icon button.
|
||||
// .hover(|style| style.bg(bg_hover_color))
|
||||
.active(|style| style.bg(bg_active_color))
|
||||
.child(IconElement::new(self.icon).color(icon_color));
|
||||
|
||||
if let Some(click_handler) = self.handlers.click.clone() {
|
||||
button = button.on_mouse_down(MouseButton::Left, move |state, event, cx| {
|
||||
cx.stop_propagation();
|
||||
click_handler(state, cx);
|
||||
})
|
||||
}
|
||||
|
||||
if let Some(tooltip) = self.tooltip.take() {
|
||||
if !self.selected {
|
||||
button = button.tooltip(move |view: &mut V, cx| (tooltip)(view, cx))
|
||||
}
|
||||
}
|
||||
|
||||
button
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
use gpui::px;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct UnreadIndicator;
|
||||
|
||||
impl UnreadIndicator {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
div()
|
||||
.rounded_full()
|
||||
.border_2()
|
||||
.border_color(cx.theme().colors().surface_background)
|
||||
.w(px(9.0))
|
||||
.h(px(9.0))
|
||||
.z_index(2)
|
||||
.bg(cx.theme().status().info)
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
use crate::{prelude::*, Label};
|
||||
use gpui::prelude::*;
|
||||
use gpui::{prelude::*, Div, IntoElement, Stateful};
|
||||
|
||||
#[derive(Default, PartialEq)]
|
||||
pub enum InputVariant {
|
||||
|
@ -8,7 +8,7 @@ pub enum InputVariant {
|
|||
Filled,
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
#[derive(IntoElement)]
|
||||
pub struct Input {
|
||||
placeholder: SharedString,
|
||||
value: String,
|
||||
|
@ -18,6 +18,57 @@ pub struct Input {
|
|||
is_active: bool,
|
||||
}
|
||||
|
||||
impl RenderOnce for Input {
|
||||
type Rendered = Stateful<Div>;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
let (input_bg, input_hover_bg, input_active_bg) = match self.variant {
|
||||
InputVariant::Ghost => (
|
||||
cx.theme().colors().ghost_element_background,
|
||||
cx.theme().colors().ghost_element_hover,
|
||||
cx.theme().colors().ghost_element_active,
|
||||
),
|
||||
InputVariant::Filled => (
|
||||
cx.theme().colors().element_background,
|
||||
cx.theme().colors().element_hover,
|
||||
cx.theme().colors().element_active,
|
||||
),
|
||||
};
|
||||
|
||||
let placeholder_label = Label::new(self.placeholder.clone()).color(if self.disabled {
|
||||
Color::Disabled
|
||||
} else {
|
||||
Color::Placeholder
|
||||
});
|
||||
|
||||
let label = Label::new(self.value.clone()).color(if self.disabled {
|
||||
Color::Disabled
|
||||
} else {
|
||||
Color::Default
|
||||
});
|
||||
|
||||
div()
|
||||
.id("input")
|
||||
.h_7()
|
||||
.w_full()
|
||||
.px_2()
|
||||
.border()
|
||||
.border_color(cx.theme().styles.system.transparent)
|
||||
.bg(input_bg)
|
||||
.hover(|style| style.bg(input_hover_bg))
|
||||
.active(|style| style.bg(input_active_bg))
|
||||
.flex()
|
||||
.items_center()
|
||||
.child(div().flex().items_center().text_ui_sm().map(move |this| {
|
||||
if self.value.is_empty() {
|
||||
this.child(placeholder_label)
|
||||
} else {
|
||||
this.child(label)
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Input {
|
||||
pub fn new(placeholder: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
|
@ -54,74 +105,4 @@ impl Input {
|
|||
self.is_active = is_active;
|
||||
self
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
let (input_bg, input_hover_bg, input_active_bg) = match self.variant {
|
||||
InputVariant::Ghost => (
|
||||
cx.theme().colors().ghost_element_background,
|
||||
cx.theme().colors().ghost_element_hover,
|
||||
cx.theme().colors().ghost_element_active,
|
||||
),
|
||||
InputVariant::Filled => (
|
||||
cx.theme().colors().element_background,
|
||||
cx.theme().colors().element_hover,
|
||||
cx.theme().colors().element_active,
|
||||
),
|
||||
};
|
||||
|
||||
let placeholder_label = Label::new(self.placeholder.clone()).color(if self.disabled {
|
||||
TextColor::Disabled
|
||||
} else {
|
||||
TextColor::Placeholder
|
||||
});
|
||||
|
||||
let label = Label::new(self.value.clone()).color(if self.disabled {
|
||||
TextColor::Disabled
|
||||
} else {
|
||||
TextColor::Default
|
||||
});
|
||||
|
||||
div()
|
||||
.id("input")
|
||||
.h_7()
|
||||
.w_full()
|
||||
.px_2()
|
||||
.border()
|
||||
.border_color(cx.theme().styles.system.transparent)
|
||||
.bg(input_bg)
|
||||
.hover(|style| style.bg(input_hover_bg))
|
||||
.active(|style| style.bg(input_active_bg))
|
||||
.flex()
|
||||
.items_center()
|
||||
.child(div().flex().items_center().text_ui_sm().map(|this| {
|
||||
if self.value.is_empty() {
|
||||
this.child(placeholder_label)
|
||||
} else {
|
||||
this.child(label)
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use super::*;
|
||||
use crate::Story;
|
||||
use gpui::{Div, Render};
|
||||
|
||||
pub struct InputStory;
|
||||
|
||||
impl Render for InputStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, Input>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(div().flex().child(Input::new("Search")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
use gpui::{actions, relative, rems, Action, Styled};
|
||||
use strum::EnumIter;
|
||||
use crate::prelude::*;
|
||||
use gpui::{Action, Div, IntoElement};
|
||||
|
||||
use crate::{h_stack, prelude::*, Icon, IconElement, IconSize};
|
||||
|
||||
#[derive(Component, Clone)]
|
||||
#[derive(IntoElement, Clone)]
|
||||
pub struct KeyBinding {
|
||||
/// A keybinding consists of a key and a set of modifier keys.
|
||||
/// More then one keybinding produces a chord.
|
||||
|
@ -12,6 +10,27 @@ pub struct KeyBinding {
|
|||
key_binding: gpui::KeyBinding,
|
||||
}
|
||||
|
||||
impl RenderOnce for KeyBinding {
|
||||
type Rendered = Div;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
div()
|
||||
.flex()
|
||||
.gap_2()
|
||||
.children(self.key_binding.keystrokes().iter().map(|keystroke| {
|
||||
div()
|
||||
.flex()
|
||||
.gap_1()
|
||||
.when(keystroke.modifiers.function, |el| el.child(Key::new("fn")))
|
||||
.when(keystroke.modifiers.control, |el| el.child(Key::new("^")))
|
||||
.when(keystroke.modifiers.alt, |el| el.child(Key::new("⌥")))
|
||||
.when(keystroke.modifiers.command, |el| el.child(Key::new("⌘")))
|
||||
.when(keystroke.modifiers.shift, |el| el.child(Key::new("⇧")))
|
||||
.child(Key::new(keystroke.key.clone()))
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyBinding {
|
||||
pub fn for_action(action: &dyn Action, cx: &mut WindowContext) -> Option<Self> {
|
||||
// todo! this last is arbitrary, we want to prefer users key bindings over defaults,
|
||||
|
@ -23,172 +42,30 @@ impl KeyBinding {
|
|||
pub fn new(key_binding: gpui::KeyBinding) -> Self {
|
||||
Self { key_binding }
|
||||
}
|
||||
|
||||
fn icon_for_key(key: &str) -> Option<Icon> {
|
||||
match key {
|
||||
"left" => Some(Icon::ArrowLeft),
|
||||
"right" => Some(Icon::ArrowRight),
|
||||
"up" => Some(Icon::ArrowUp),
|
||||
"down" => Some(Icon::ArrowDown),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
h_stack()
|
||||
.flex_none()
|
||||
.gap_1()
|
||||
.children(self.key_binding.keystrokes().iter().map(|keystroke| {
|
||||
let key_icon = Self::icon_for_key(&keystroke.key);
|
||||
|
||||
h_stack()
|
||||
.flex_none()
|
||||
.gap_0p5()
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.p_0p5()
|
||||
.rounded_sm()
|
||||
.when(keystroke.modifiers.function, |el| el.child(Key::new("fn")))
|
||||
.when(keystroke.modifiers.control, |el| {
|
||||
el.child(KeyIcon::new(Icon::Control))
|
||||
})
|
||||
.when(keystroke.modifiers.alt, |el| {
|
||||
el.child(KeyIcon::new(Icon::Option))
|
||||
})
|
||||
.when(keystroke.modifiers.command, |el| {
|
||||
el.child(KeyIcon::new(Icon::Command))
|
||||
})
|
||||
.when(keystroke.modifiers.shift, |el| {
|
||||
el.child(KeyIcon::new(Icon::Shift))
|
||||
})
|
||||
.when_some(key_icon, |el, icon| el.child(KeyIcon::new(icon)))
|
||||
.when(key_icon.is_none(), |el| {
|
||||
el.child(Key::new(keystroke.key.to_uppercase().clone()))
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
#[derive(IntoElement)]
|
||||
pub struct Key {
|
||||
key: SharedString,
|
||||
}
|
||||
|
||||
impl RenderOnce for Key {
|
||||
type Rendered = Div;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
div()
|
||||
.px_2()
|
||||
.py_0()
|
||||
.rounded_md()
|
||||
.text_ui_sm()
|
||||
.text_color(cx.theme().colors().text)
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.child(self.key.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Key {
|
||||
pub fn new(key: impl Into<SharedString>) -> Self {
|
||||
Self { key: key.into() }
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
let single_char = self.key.len() == 1;
|
||||
|
||||
div()
|
||||
// .px_0p5()
|
||||
.py_0()
|
||||
.when(single_char, |el| {
|
||||
el.w(rems(14. / 16.)).flex().flex_none().justify_center()
|
||||
})
|
||||
.when(!single_char, |el| el.px_0p5())
|
||||
.h(rems(14. / 16.))
|
||||
// .rounded_md()
|
||||
.text_ui()
|
||||
.line_height(relative(1.))
|
||||
.text_color(cx.theme().colors().text)
|
||||
// .bg(cx.theme().colors().element_background)
|
||||
.child(self.key.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct KeyIcon {
|
||||
icon: Icon,
|
||||
}
|
||||
|
||||
impl KeyIcon {
|
||||
pub fn new(icon: Icon) -> Self {
|
||||
Self { icon }
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
div()
|
||||
.w(rems(14. / 16.))
|
||||
// .bg(cx.theme().colors().element_background)
|
||||
.child(IconElement::new(self.icon).size(IconSize::Small))
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: The order the modifier keys appear in this enum impacts the order in
|
||||
// which they are rendered in the UI.
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
|
||||
pub enum ModifierKey {
|
||||
Control,
|
||||
Alt, // Option
|
||||
Shift,
|
||||
Command,
|
||||
}
|
||||
|
||||
actions!(NoAction);
|
||||
|
||||
pub fn binding(key: &str) -> gpui::KeyBinding {
|
||||
gpui::KeyBinding::new(key, NoAction {}, None)
|
||||
}
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use super::*;
|
||||
pub use crate::KeyBinding;
|
||||
use crate::{binding, Story};
|
||||
use gpui::{Div, Render};
|
||||
use itertools::Itertools;
|
||||
pub struct KeybindingStory;
|
||||
|
||||
impl Render for KeybindingStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let all_modifier_permutations =
|
||||
["ctrl", "alt", "cmd", "shift"].into_iter().permutations(2);
|
||||
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, KeyBinding>(cx))
|
||||
.child(Story::label(cx, "Single Key"))
|
||||
.child(KeyBinding::new(binding("Z")))
|
||||
.child(Story::label(cx, "Single Key with Modifier"))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_3()
|
||||
.child(KeyBinding::new(binding("ctrl-c")))
|
||||
.child(KeyBinding::new(binding("alt-c")))
|
||||
.child(KeyBinding::new(binding("cmd-c")))
|
||||
.child(KeyBinding::new(binding("shift-c"))),
|
||||
)
|
||||
.child(Story::label(cx, "Single Key with Modifier (Permuted)"))
|
||||
.child(
|
||||
div().flex().flex_col().children(
|
||||
all_modifier_permutations
|
||||
.chunks(4)
|
||||
.into_iter()
|
||||
.map(|chunk| {
|
||||
div()
|
||||
.flex()
|
||||
.gap_4()
|
||||
.py_3()
|
||||
.children(chunk.map(|permutation| {
|
||||
KeyBinding::new(binding(&*(permutation.join("-") + "-x")))
|
||||
}))
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(Story::label(cx, "Single Key with All Modifiers"))
|
||||
.child(KeyBinding::new(binding("ctrl-alt-cmd-shift-z")))
|
||||
.child(Story::label(cx, "Chord"))
|
||||
.child(KeyBinding::new(binding("a z")))
|
||||
.child(Story::label(cx, "Chord with Modifier"))
|
||||
.child(KeyBinding::new(binding("ctrl-a shift-z")))
|
||||
.child(KeyBinding::new(binding("fn-s")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
use gpui::{relative, Hsla, Text, TextRun, WindowContext};
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::styled_ext::StyledExt;
|
||||
use gpui::{relative, Div, Hsla, IntoElement, StyledText, TextRun, WindowContext};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
|
||||
pub enum LabelSize {
|
||||
|
@ -10,48 +9,6 @@ pub enum LabelSize {
|
|||
Small,
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Copy, Clone)]
|
||||
pub enum TextColor {
|
||||
#[default]
|
||||
Default,
|
||||
Accent,
|
||||
Created,
|
||||
Deleted,
|
||||
Disabled,
|
||||
Error,
|
||||
Hidden,
|
||||
Info,
|
||||
Modified,
|
||||
Muted,
|
||||
Placeholder,
|
||||
Player(u32),
|
||||
Selected,
|
||||
Success,
|
||||
Warning,
|
||||
}
|
||||
|
||||
impl TextColor {
|
||||
pub fn color(&self, cx: &WindowContext) -> Hsla {
|
||||
match self {
|
||||
TextColor::Default => cx.theme().colors().text,
|
||||
TextColor::Muted => cx.theme().colors().text_muted,
|
||||
TextColor::Created => cx.theme().status().created,
|
||||
TextColor::Modified => cx.theme().status().modified,
|
||||
TextColor::Deleted => cx.theme().status().deleted,
|
||||
TextColor::Disabled => cx.theme().colors().text_disabled,
|
||||
TextColor::Hidden => cx.theme().status().hidden,
|
||||
TextColor::Info => cx.theme().status().info,
|
||||
TextColor::Placeholder => cx.theme().colors().text_placeholder,
|
||||
TextColor::Accent => cx.theme().colors().text_accent,
|
||||
TextColor::Player(i) => cx.theme().styles.player.0[i.clone() as usize].cursor,
|
||||
TextColor::Error => cx.theme().status().error,
|
||||
TextColor::Selected => cx.theme().colors().text_accent,
|
||||
TextColor::Success => cx.theme().status().success,
|
||||
TextColor::Warning => cx.theme().status().warning,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Copy, Clone)]
|
||||
pub enum LineHeightStyle {
|
||||
#[default]
|
||||
|
@ -60,47 +17,19 @@ pub enum LineHeightStyle {
|
|||
UILabel,
|
||||
}
|
||||
|
||||
#[derive(Clone, Component)]
|
||||
#[derive(IntoElement, Clone)]
|
||||
pub struct Label {
|
||||
label: SharedString,
|
||||
size: LabelSize,
|
||||
line_height_style: LineHeightStyle,
|
||||
color: TextColor,
|
||||
color: Color,
|
||||
strikethrough: bool,
|
||||
}
|
||||
|
||||
impl Label {
|
||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
size: LabelSize::Default,
|
||||
line_height_style: LineHeightStyle::default(),
|
||||
color: TextColor::Default,
|
||||
strikethrough: false,
|
||||
}
|
||||
}
|
||||
impl RenderOnce for Label {
|
||||
type Rendered = Div;
|
||||
|
||||
pub fn size(mut self, size: LabelSize) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: TextColor) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self {
|
||||
self.line_height_style = line_height_style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_strikethrough(mut self, strikethrough: bool) -> Self {
|
||||
self.strikethrough = strikethrough;
|
||||
self
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
div()
|
||||
.when(self.strikethrough, |this| {
|
||||
this.relative().child(
|
||||
|
@ -109,7 +38,7 @@ impl Label {
|
|||
.top_1_2()
|
||||
.w_full()
|
||||
.h_px()
|
||||
.bg(TextColor::Hidden.color(cx)),
|
||||
.bg(Color::Hidden.color(cx)),
|
||||
)
|
||||
})
|
||||
.map(|this| match self.size {
|
||||
|
@ -124,24 +53,13 @@ impl Label {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct HighlightedLabel {
|
||||
label: SharedString,
|
||||
size: LabelSize,
|
||||
color: TextColor,
|
||||
highlight_indices: Vec<usize>,
|
||||
strikethrough: bool,
|
||||
}
|
||||
|
||||
impl HighlightedLabel {
|
||||
/// shows a label with the given characters highlighted.
|
||||
/// characters are identified by utf8 byte position.
|
||||
pub fn new(label: impl Into<SharedString>, highlight_indices: Vec<usize>) -> Self {
|
||||
impl Label {
|
||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
size: LabelSize::Default,
|
||||
color: TextColor::Default,
|
||||
highlight_indices,
|
||||
line_height_style: LineHeightStyle::default(),
|
||||
color: Color::Default,
|
||||
strikethrough: false,
|
||||
}
|
||||
}
|
||||
|
@ -151,17 +69,35 @@ impl HighlightedLabel {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: TextColor) -> Self {
|
||||
pub fn color(mut self, color: Color) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self {
|
||||
self.line_height_style = line_height_style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_strikethrough(mut self, strikethrough: bool) -> Self {
|
||||
self.strikethrough = strikethrough;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
#[derive(IntoElement)]
|
||||
pub struct HighlightedLabel {
|
||||
label: SharedString,
|
||||
size: LabelSize,
|
||||
color: Color,
|
||||
highlight_indices: Vec<usize>,
|
||||
strikethrough: bool,
|
||||
}
|
||||
|
||||
impl RenderOnce for HighlightedLabel {
|
||||
type Rendered = Div;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
let highlight_color = cx.theme().colors().text_accent;
|
||||
let mut text_style = cx.text_style().clone();
|
||||
|
||||
|
@ -207,14 +143,43 @@ impl HighlightedLabel {
|
|||
.my_auto()
|
||||
.w_full()
|
||||
.h_px()
|
||||
.bg(TextColor::Hidden.color(cx)),
|
||||
.bg(Color::Hidden.color(cx)),
|
||||
)
|
||||
})
|
||||
.map(|this| match self.size {
|
||||
LabelSize::Default => this.text_ui(),
|
||||
LabelSize::Small => this.text_ui_sm(),
|
||||
})
|
||||
.child(Text::styled(self.label, runs))
|
||||
.child(StyledText::new(self.label).with_runs(runs))
|
||||
}
|
||||
}
|
||||
|
||||
impl HighlightedLabel {
|
||||
/// shows a label with the given characters highlighted.
|
||||
/// characters are identified by utf8 byte position.
|
||||
pub fn new(label: impl Into<SharedString>, highlight_indices: Vec<usize>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
size: LabelSize::Default,
|
||||
color: Color::Default,
|
||||
highlight_indices,
|
||||
strikethrough: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(mut self, size: LabelSize) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: Color) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_strikethrough(mut self, strikethrough: bool) -> Self {
|
||||
self.strikethrough = strikethrough;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -223,35 +188,3 @@ struct Run {
|
|||
pub text: String,
|
||||
pub color: Hsla,
|
||||
}
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use super::*;
|
||||
use crate::Story;
|
||||
use gpui::{Div, Render};
|
||||
|
||||
pub struct LabelStory;
|
||||
|
||||
impl Render for LabelStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, Label>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(Label::new("Hello, world!"))
|
||||
.child(Story::label(cx, "Highlighted"))
|
||||
.child(HighlightedLabel::new(
|
||||
"Hello, world!",
|
||||
vec![0, 1, 2, 7, 8, 12],
|
||||
))
|
||||
.child(HighlightedLabel::new(
|
||||
"Héllo, world!",
|
||||
vec![0, 1, 3, 8, 9, 13],
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
use std::rc::Rc;
|
||||
|
||||
use gpui::{div, Div, Stateful, StatefulInteractiveComponent};
|
||||
use gpui::{
|
||||
div, px, AnyElement, ClickEvent, Div, ImageSource, IntoElement, MouseButton, MouseDownEvent,
|
||||
Pixels, Stateful, StatefulInteractiveElement,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::settings::user_settings;
|
||||
use crate::{
|
||||
disclosure_control, h_stack, v_stack, Avatar, Icon, IconElement, IconSize, Label, Toggle,
|
||||
disclosure_control, h_stack, v_stack, Avatar, Icon, IconButton, IconElement, IconSize, Label,
|
||||
Toggle,
|
||||
};
|
||||
use crate::{prelude::*, GraphicSlot};
|
||||
|
||||
|
@ -17,14 +21,13 @@ pub enum ListItemVariant {
|
|||
}
|
||||
|
||||
pub enum ListHeaderMeta {
|
||||
// TODO: These should be IconButtons
|
||||
Tools(Vec<Icon>),
|
||||
Tools(Vec<IconButton>),
|
||||
// TODO: This should be a button
|
||||
Button(Label),
|
||||
Text(Label),
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
#[derive(IntoElement)]
|
||||
pub struct ListHeader {
|
||||
label: SharedString,
|
||||
left_icon: Option<Icon>,
|
||||
|
@ -33,6 +36,60 @@ pub struct ListHeader {
|
|||
toggle: Toggle,
|
||||
}
|
||||
|
||||
impl RenderOnce for ListHeader {
|
||||
type Rendered = Div;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
let disclosure_control = disclosure_control(self.toggle);
|
||||
|
||||
let meta = match self.meta {
|
||||
Some(ListHeaderMeta::Tools(icons)) => div().child(
|
||||
h_stack()
|
||||
.gap_2()
|
||||
.items_center()
|
||||
.children(icons.into_iter().map(|i| i.color(Color::Muted))),
|
||||
),
|
||||
Some(ListHeaderMeta::Button(label)) => div().child(label),
|
||||
Some(ListHeaderMeta::Text(label)) => div().child(label),
|
||||
None => div(),
|
||||
};
|
||||
|
||||
h_stack()
|
||||
.w_full()
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
.relative()
|
||||
.child(
|
||||
div()
|
||||
.h_5()
|
||||
.when(self.variant == ListItemVariant::Inset, |this| this.px_2())
|
||||
.flex()
|
||||
.flex_1()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.children(self.left_icon.map(|i| {
|
||||
IconElement::new(i)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small)
|
||||
}))
|
||||
.child(Label::new(self.label.clone()).color(Color::Muted)),
|
||||
)
|
||||
.child(disclosure_control),
|
||||
)
|
||||
.child(meta),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ListHeader {
|
||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
|
@ -54,72 +111,17 @@ impl ListHeader {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn right_button(self, button: IconButton) -> Self {
|
||||
self.meta(Some(ListHeaderMeta::Tools(vec![button])))
|
||||
}
|
||||
|
||||
pub fn meta(mut self, meta: Option<ListHeaderMeta>) -> Self {
|
||||
self.meta = meta;
|
||||
self
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
let disclosure_control = disclosure_control(self.toggle);
|
||||
|
||||
let meta = match self.meta {
|
||||
Some(ListHeaderMeta::Tools(icons)) => div().child(
|
||||
h_stack()
|
||||
.gap_2()
|
||||
.items_center()
|
||||
.children(icons.into_iter().map(|i| {
|
||||
IconElement::new(i)
|
||||
.color(TextColor::Muted)
|
||||
.size(IconSize::Small)
|
||||
})),
|
||||
),
|
||||
Some(ListHeaderMeta::Button(label)) => div().child(label),
|
||||
Some(ListHeaderMeta::Text(label)) => div().child(label),
|
||||
None => div(),
|
||||
};
|
||||
|
||||
h_stack()
|
||||
.w_full()
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
// TODO: Add focus state
|
||||
// .when(self.state == InteractionState::Focused, |this| {
|
||||
// this.border()
|
||||
// .border_color(cx.theme().colors().border_focused)
|
||||
// })
|
||||
.relative()
|
||||
.child(
|
||||
div()
|
||||
.h_5()
|
||||
.when(self.variant == ListItemVariant::Inset, |this| this.px_2())
|
||||
.flex()
|
||||
.flex_1()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.children(self.left_icon.map(|i| {
|
||||
IconElement::new(i)
|
||||
.color(TextColor::Muted)
|
||||
.size(IconSize::Small)
|
||||
}))
|
||||
.child(Label::new(self.label.clone()).color(TextColor::Muted)),
|
||||
)
|
||||
.child(disclosure_control),
|
||||
)
|
||||
.child(meta),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Clone)]
|
||||
#[derive(IntoElement, Clone)]
|
||||
pub struct ListSubHeader {
|
||||
label: SharedString,
|
||||
left_icon: Option<Icon>,
|
||||
|
@ -139,8 +141,12 @@ impl ListSubHeader {
|
|||
self.left_icon = left_icon;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
impl RenderOnce for ListSubHeader {
|
||||
type Rendered = Div;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
h_stack().flex_1().w_full().relative().py_1().child(
|
||||
div()
|
||||
.h_6()
|
||||
|
@ -158,134 +164,89 @@ impl ListSubHeader {
|
|||
.items_center()
|
||||
.children(self.left_icon.map(|i| {
|
||||
IconElement::new(i)
|
||||
.color(TextColor::Muted)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small)
|
||||
}))
|
||||
.child(Label::new(self.label.clone()).color(TextColor::Muted)),
|
||||
.child(Label::new(self.label.clone()).color(Color::Muted)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Copy, Clone)]
|
||||
pub enum ListEntrySize {
|
||||
#[default]
|
||||
Small,
|
||||
Medium,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ListItem<V: 'static> {
|
||||
Entry(ListEntry<V>),
|
||||
Separator(ListSeparator),
|
||||
Header(ListSubHeader),
|
||||
}
|
||||
|
||||
impl<V: 'static> From<ListEntry<V>> for ListItem<V> {
|
||||
fn from(entry: ListEntry<V>) -> Self {
|
||||
Self::Entry(entry)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> From<ListSeparator> for ListItem<V> {
|
||||
fn from(entry: ListSeparator) -> Self {
|
||||
Self::Separator(entry)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> From<ListSubHeader> for ListItem<V> {
|
||||
fn from(entry: ListSubHeader) -> Self {
|
||||
Self::Header(entry)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> ListItem<V> {
|
||||
fn render(self, view: &mut V, ix: usize, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
match self {
|
||||
ListItem::Entry(entry) => div().child(entry.render(ix, cx)),
|
||||
ListItem::Separator(separator) => div().child(separator.render(view, cx)),
|
||||
ListItem::Header(header) => div().child(header.render(view, cx)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(label: Label) -> Self {
|
||||
Self::Entry(ListEntry::new(label))
|
||||
}
|
||||
|
||||
pub fn as_entry(&mut self) -> Option<&mut ListEntry<V>> {
|
||||
if let Self::Entry(entry) = self {
|
||||
Some(entry)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ListEntry<V> {
|
||||
#[derive(IntoElement)]
|
||||
pub struct ListItem {
|
||||
id: ElementId,
|
||||
disabled: bool,
|
||||
selected: bool,
|
||||
// TODO: Reintroduce this
|
||||
// disclosure_control_style: DisclosureControlVisibility,
|
||||
indent_level: u32,
|
||||
label: Label,
|
||||
indent_level: usize,
|
||||
indent_step_size: Pixels,
|
||||
left_slot: Option<GraphicSlot>,
|
||||
overflow: OverflowStyle,
|
||||
size: ListEntrySize,
|
||||
toggle: Toggle,
|
||||
variant: ListItemVariant,
|
||||
on_click: Option<Rc<dyn Fn(&mut V, &mut ViewContext<V>) + 'static>>,
|
||||
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||
on_secondary_mouse_down: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl<V> Clone for ListEntry<V> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
disabled: self.disabled,
|
||||
indent_level: self.indent_level,
|
||||
label: self.label.clone(),
|
||||
left_slot: self.left_slot.clone(),
|
||||
overflow: self.overflow,
|
||||
size: self.size,
|
||||
toggle: self.toggle,
|
||||
variant: self.variant,
|
||||
on_click: self.on_click.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> ListEntry<V> {
|
||||
pub fn new(label: Label) -> Self {
|
||||
impl ListItem {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
disabled: false,
|
||||
selected: false,
|
||||
indent_level: 0,
|
||||
label,
|
||||
indent_step_size: px(12.),
|
||||
left_slot: None,
|
||||
overflow: OverflowStyle::Hidden,
|
||||
size: ListEntrySize::default(),
|
||||
toggle: Toggle::NotToggleable,
|
||||
variant: ListItemVariant::default(),
|
||||
on_click: Default::default(),
|
||||
on_click: None,
|
||||
on_secondary_mouse_down: None,
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_click(mut self, handler: impl Fn(&mut V, &mut ViewContext<V>) + 'static) -> Self {
|
||||
pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
|
||||
self.on_click = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_secondary_mouse_down(
|
||||
mut self,
|
||||
handler: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
|
||||
) -> Self {
|
||||
self.on_secondary_mouse_down = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn variant(mut self, variant: ListItemVariant) -> Self {
|
||||
self.variant = variant;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn indent_level(mut self, indent_level: u32) -> Self {
|
||||
pub fn indent_level(mut self, indent_level: usize) -> Self {
|
||||
self.indent_level = indent_level;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn indent_step_size(mut self, indent_step_size: Pixels) -> Self {
|
||||
self.indent_step_size = indent_step_size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn toggle(mut self, toggle: Toggle) -> Self {
|
||||
self.toggle = toggle;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn left_content(mut self, left_content: GraphicSlot) -> Self {
|
||||
self.left_slot = Some(left_content);
|
||||
self
|
||||
|
@ -296,116 +257,138 @@ impl<V: 'static> ListEntry<V> {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn left_avatar(mut self, left_avatar: impl Into<SharedString>) -> Self {
|
||||
pub fn left_avatar(mut self, left_avatar: impl Into<ImageSource>) -> Self {
|
||||
self.left_slot = Some(GraphicSlot::Avatar(left_avatar.into()));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(mut self, size: ListEntrySize) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
fn render(self, ix: usize, cx: &mut ViewContext<V>) -> Stateful<V, Div<V>> {
|
||||
let settings = user_settings(cx);
|
||||
impl RenderOnce for ListItem {
|
||||
type Rendered = Stateful<Div>;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
let left_content = match self.left_slot.clone() {
|
||||
Some(GraphicSlot::Icon(i)) => Some(
|
||||
h_stack().child(
|
||||
IconElement::new(i)
|
||||
.size(IconSize::Small)
|
||||
.color(TextColor::Muted),
|
||||
.color(Color::Muted),
|
||||
),
|
||||
),
|
||||
Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::new(src))),
|
||||
Some(GraphicSlot::PublicActor(src)) => Some(h_stack().child(Avatar::new(src))),
|
||||
Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::source(src))),
|
||||
Some(GraphicSlot::PublicActor(src)) => Some(h_stack().child(Avatar::uri(src))),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let sized_item = match self.size {
|
||||
ListEntrySize::Small => div().h_6(),
|
||||
ListEntrySize::Medium => div().h_7(),
|
||||
};
|
||||
div()
|
||||
.id(ix)
|
||||
.id(self.id)
|
||||
.relative()
|
||||
.hover(|mut style| {
|
||||
style.background = Some(cx.theme().colors().editor_background.into());
|
||||
style
|
||||
})
|
||||
.on_click({
|
||||
let on_click = self.on_click.clone();
|
||||
|
||||
move |view: &mut V, event, cx| {
|
||||
if let Some(on_click) = &on_click {
|
||||
(on_click)(view, cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
// TODO: Add focus state
|
||||
// .when(self.state == InteractionState::Focused, |this| {
|
||||
// this.border()
|
||||
// .border_color(cx.theme().colors().border_focused)
|
||||
// })
|
||||
.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
|
||||
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
|
||||
.when(self.selected, |this| {
|
||||
this.bg(cx.theme().colors().ghost_element_selected)
|
||||
})
|
||||
.when_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
|
||||
this.on_mouse_down(MouseButton::Right, move |event, cx| {
|
||||
(on_mouse_down)(event, cx)
|
||||
})
|
||||
})
|
||||
.child(
|
||||
sized_item
|
||||
div()
|
||||
.when(self.variant == ListItemVariant::Inset, |this| this.px_2())
|
||||
// .ml(rems(0.75 * self.indent_level as f32))
|
||||
.children((0..self.indent_level).map(|_| {
|
||||
div()
|
||||
.w(*settings.list_indent_depth)
|
||||
.h_full()
|
||||
.flex()
|
||||
.justify_center()
|
||||
.group_hover("", |style| style.bg(cx.theme().colors().border_focused))
|
||||
.child(
|
||||
h_stack()
|
||||
.child(div().w_px().h_full())
|
||||
.child(div().w_px().h_full().bg(cx.theme().colors().border)),
|
||||
)
|
||||
}))
|
||||
.ml(self.indent_level as f32 * self.indent_step_size)
|
||||
.flex()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.relative()
|
||||
.child(disclosure_control(self.toggle))
|
||||
.children(left_content)
|
||||
.child(self.label),
|
||||
.children(self.children)
|
||||
// HACK: We need to attach the `on_click` handler to the child element in order to have the click
|
||||
// event actually fire.
|
||||
// Once this is fixed in GPUI we can remove this and rely on the `on_click` handler set above on the
|
||||
// outer `div`.
|
||||
.id("on_click_hack")
|
||||
.when_some(self.on_click, |this, on_click| {
|
||||
this.on_click(move |event, cx| {
|
||||
// HACK: GPUI currently fires `on_click` with any mouse button,
|
||||
// but we only care about the left button.
|
||||
if event.down.button == MouseButton::Left {
|
||||
(on_click)(event, cx)
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Component)]
|
||||
impl ParentElement for ListItem {
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
|
||||
&mut self.children
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement, Clone)]
|
||||
pub struct ListSeparator;
|
||||
|
||||
impl ListSeparator {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
impl RenderOnce for ListSeparator {
|
||||
type Rendered = Div;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
div().h_px().w_full().bg(cx.theme().colors().border_variant)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct List<V: 'static> {
|
||||
items: Vec<ListItem<V>>,
|
||||
#[derive(IntoElement)]
|
||||
pub struct List {
|
||||
/// Message to display when the list is empty
|
||||
/// Defaults to "No items"
|
||||
empty_message: SharedString,
|
||||
header: Option<ListHeader>,
|
||||
toggle: Toggle,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl<V: 'static> List<V> {
|
||||
pub fn new(items: Vec<ListItem<V>>) -> Self {
|
||||
impl RenderOnce for List {
|
||||
type Rendered = Div;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
let list_content = match (self.children.is_empty(), self.toggle) {
|
||||
(false, _) => div().children(self.children),
|
||||
(true, Toggle::Toggled(false)) => div(),
|
||||
(true, _) => div().child(Label::new(self.empty_message.clone()).color(Color::Muted)),
|
||||
};
|
||||
|
||||
v_stack()
|
||||
.w_full()
|
||||
.py_1()
|
||||
.children(self.header.map(|header| header))
|
||||
.child(list_content)
|
||||
}
|
||||
}
|
||||
|
||||
impl List {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
items,
|
||||
empty_message: "No items".into(),
|
||||
header: None,
|
||||
toggle: Toggle::NotToggleable,
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -423,25 +406,10 @@ impl<V: 'static> List<V> {
|
|||
self.toggle = toggle;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
let list_content = match (self.items.is_empty(), self.toggle) {
|
||||
(false, _) => div().children(
|
||||
self.items
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, item)| item.render(view, ix, cx)),
|
||||
),
|
||||
(true, Toggle::Toggled(false)) => div(),
|
||||
(true, _) => {
|
||||
div().child(Label::new(self.empty_message.clone()).color(TextColor::Muted))
|
||||
}
|
||||
};
|
||||
|
||||
v_stack()
|
||||
.w_full()
|
||||
.py_1()
|
||||
.children(self.header.map(|header| header))
|
||||
.child(list_content)
|
||||
impl ParentElement for List {
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
|
||||
&mut self.children
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,81 +0,0 @@
|
|||
use gpui::AnyElement;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{h_stack, prelude::*, v_stack, Button, Icon, IconButton, Label};
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct Modal<V: 'static> {
|
||||
id: ElementId,
|
||||
title: Option<SharedString>,
|
||||
primary_action: Option<Button<V>>,
|
||||
secondary_action: Option<Button<V>>,
|
||||
children: SmallVec<[AnyElement<V>; 2]>,
|
||||
}
|
||||
|
||||
impl<V: 'static> Modal<V> {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
title: None,
|
||||
primary_action: None,
|
||||
secondary_action: None,
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn title(mut self, title: impl Into<SharedString>) -> Self {
|
||||
self.title = Some(title.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn primary_action(mut self, action: Button<V>) -> Self {
|
||||
self.primary_action = Some(action);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn secondary_action(mut self, action: Button<V>) -> Self {
|
||||
self.secondary_action = Some(action);
|
||||
self
|
||||
}
|
||||
|
||||
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
v_stack()
|
||||
.id(self.id.clone())
|
||||
.w_96()
|
||||
// .rounded_xl()
|
||||
.bg(cx.theme().colors().background)
|
||||
.border()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.shadow_2xl()
|
||||
.child(
|
||||
h_stack()
|
||||
.justify_between()
|
||||
.p_1()
|
||||
.border_b()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(div().children(self.title.clone().map(|t| Label::new(t))))
|
||||
.child(IconButton::new("close", Icon::Close)),
|
||||
)
|
||||
.child(v_stack().p_1().children(self.children))
|
||||
.when(
|
||||
self.primary_action.is_some() || self.secondary_action.is_some(),
|
||||
|this| {
|
||||
this.child(
|
||||
h_stack()
|
||||
.border_t()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.p_1()
|
||||
.justify_end()
|
||||
.children(self.secondary_action)
|
||||
.children(self.primary_action),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> ParentComponent<V> for Modal<V> {
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
|
||||
&mut self.children
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
use gpui::rems;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{h_stack, Icon};
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct NotificationToast {
|
||||
label: SharedString,
|
||||
icon: Option<Icon>,
|
||||
}
|
||||
|
||||
impl NotificationToast {
|
||||
pub fn new(label: SharedString) -> Self {
|
||||
Self { label, icon: None }
|
||||
}
|
||||
|
||||
pub fn icon<I>(mut self, icon: I) -> Self
|
||||
where
|
||||
I: Into<Option<Icon>>,
|
||||
{
|
||||
self.icon = icon.into();
|
||||
self
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
h_stack()
|
||||
.z_index(5)
|
||||
.absolute()
|
||||
.top_1()
|
||||
.right_1()
|
||||
.w(rems(9999.))
|
||||
.max_w_56()
|
||||
.py_1()
|
||||
.px_1p5()
|
||||
.rounded_lg()
|
||||
.shadow_md()
|
||||
.bg(cx.theme().colors().elevated_surface_background)
|
||||
.child(div().size_full().child(self.label.clone()))
|
||||
}
|
||||
}
|
|
@ -1,204 +0,0 @@
|
|||
use crate::{h_stack, prelude::*, v_stack, KeyBinding, Label};
|
||||
use gpui::prelude::*;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct Palette {
|
||||
id: ElementId,
|
||||
input_placeholder: SharedString,
|
||||
empty_string: SharedString,
|
||||
items: Vec<PaletteItem>,
|
||||
default_order: OrderMethod,
|
||||
}
|
||||
|
||||
impl Palette {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
input_placeholder: "Find something...".into(),
|
||||
empty_string: "No items found.".into(),
|
||||
items: vec![],
|
||||
default_order: OrderMethod::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn items(mut self, items: Vec<PaletteItem>) -> Self {
|
||||
self.items = items;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn placeholder(mut self, input_placeholder: impl Into<SharedString>) -> Self {
|
||||
self.input_placeholder = input_placeholder.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn empty_string(mut self, empty_string: impl Into<SharedString>) -> Self {
|
||||
self.empty_string = empty_string.into();
|
||||
self
|
||||
}
|
||||
|
||||
// TODO: Hook up sort order
|
||||
pub fn default_order(mut self, default_order: OrderMethod) -> Self {
|
||||
self.default_order = default_order;
|
||||
self
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
v_stack()
|
||||
.id(self.id.clone())
|
||||
.w_96()
|
||||
.rounded_lg()
|
||||
.bg(cx.theme().colors().elevated_surface_background)
|
||||
.border()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
v_stack()
|
||||
.gap_px()
|
||||
.child(v_stack().py_0p5().px_1().child(div().px_2().py_0p5().child(
|
||||
Label::new(self.input_placeholder.clone()).color(TextColor::Placeholder),
|
||||
)))
|
||||
.child(
|
||||
div()
|
||||
.h_px()
|
||||
.w_full()
|
||||
.bg(cx.theme().colors().element_background),
|
||||
)
|
||||
.child(
|
||||
v_stack()
|
||||
.id("items")
|
||||
.py_0p5()
|
||||
.px_1()
|
||||
.grow()
|
||||
.max_h_96()
|
||||
.overflow_y_scroll()
|
||||
.children(
|
||||
vec![if self.items.is_empty() {
|
||||
Some(
|
||||
h_stack().justify_between().px_2().py_1().child(
|
||||
Label::new(self.empty_string.clone())
|
||||
.color(TextColor::Muted),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}]
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
)
|
||||
.children(self.items.into_iter().enumerate().map(|(index, item)| {
|
||||
h_stack()
|
||||
.id(index)
|
||||
.justify_between()
|
||||
.px_2()
|
||||
.py_0p5()
|
||||
.rounded_lg()
|
||||
.hover(|style| {
|
||||
style.bg(cx.theme().colors().ghost_element_hover)
|
||||
})
|
||||
.active(|style| {
|
||||
style.bg(cx.theme().colors().ghost_element_active)
|
||||
})
|
||||
.child(item)
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct PaletteItem {
|
||||
pub label: SharedString,
|
||||
pub sublabel: Option<SharedString>,
|
||||
pub key_binding: Option<KeyBinding>,
|
||||
}
|
||||
|
||||
impl PaletteItem {
|
||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
sublabel: None,
|
||||
key_binding: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
|
||||
self.label = label.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn sublabel(mut self, sublabel: impl Into<Option<SharedString>>) -> Self {
|
||||
self.sublabel = sublabel.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn key_binding(mut self, key_binding: impl Into<Option<KeyBinding>>) -> Self {
|
||||
self.key_binding = key_binding.into();
|
||||
self
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.grow()
|
||||
.justify_between()
|
||||
.child(
|
||||
v_stack()
|
||||
.child(Label::new(self.label.clone()))
|
||||
.children(self.sublabel.clone().map(|sublabel| Label::new(sublabel))),
|
||||
)
|
||||
.children(self.key_binding)
|
||||
}
|
||||
}
|
||||
|
||||
use gpui::ElementId;
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use gpui::{Div, Render};
|
||||
|
||||
use crate::{binding, Story};
|
||||
|
||||
use super::*;
|
||||
|
||||
pub struct PaletteStory;
|
||||
|
||||
impl Render for PaletteStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
{
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, Palette>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(Palette::new("palette-1"))
|
||||
.child(Story::label(cx, "With Items"))
|
||||
.child(
|
||||
Palette::new("palette-2")
|
||||
.placeholder("Execute a command...")
|
||||
.items(vec![
|
||||
PaletteItem::new("theme selector: toggle")
|
||||
.key_binding(KeyBinding::new(binding("cmd-k cmd-t"))),
|
||||
PaletteItem::new("assistant: inline assist")
|
||||
.key_binding(KeyBinding::new(binding("cmd-enter"))),
|
||||
PaletteItem::new("assistant: quote selection")
|
||||
.key_binding(KeyBinding::new(binding("cmd-<"))),
|
||||
PaletteItem::new("assistant: toggle focus")
|
||||
.key_binding(KeyBinding::new(binding("cmd-?"))),
|
||||
PaletteItem::new("auto update: check"),
|
||||
PaletteItem::new("auto update: view release notes"),
|
||||
PaletteItem::new("branches: open recent")
|
||||
.key_binding(KeyBinding::new(binding("cmd-alt-b"))),
|
||||
PaletteItem::new("chat panel: toggle focus"),
|
||||
PaletteItem::new("cli: install"),
|
||||
PaletteItem::new("client: sign in"),
|
||||
PaletteItem::new("client: sign out"),
|
||||
PaletteItem::new("editor: cancel")
|
||||
.key_binding(KeyBinding::new(binding("escape"))),
|
||||
]),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,150 +0,0 @@
|
|||
use gpui::{prelude::*, AbsoluteLength, AnyElement};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::settings::user_settings;
|
||||
use crate::v_stack;
|
||||
|
||||
#[derive(Default, Debug, PartialEq, Eq, Hash, Clone, Copy)]
|
||||
pub enum PanelAllowedSides {
|
||||
LeftOnly,
|
||||
RightOnly,
|
||||
BottomOnly,
|
||||
#[default]
|
||||
LeftAndRight,
|
||||
All,
|
||||
}
|
||||
|
||||
impl PanelAllowedSides {
|
||||
/// Return a `HashSet` that contains the allowable `PanelSide`s.
|
||||
pub fn allowed_sides(&self) -> HashSet<PanelSide> {
|
||||
match self {
|
||||
Self::LeftOnly => HashSet::from_iter([PanelSide::Left]),
|
||||
Self::RightOnly => HashSet::from_iter([PanelSide::Right]),
|
||||
Self::BottomOnly => HashSet::from_iter([PanelSide::Bottom]),
|
||||
Self::LeftAndRight => HashSet::from_iter([PanelSide::Left, PanelSide::Right]),
|
||||
Self::All => HashSet::from_iter([PanelSide::Left, PanelSide::Right, PanelSide::Bottom]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, PartialEq, Eq, Hash, Clone, Copy)]
|
||||
pub enum PanelSide {
|
||||
#[default]
|
||||
Left,
|
||||
Right,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct Panel<V: 'static> {
|
||||
id: ElementId,
|
||||
current_side: PanelSide,
|
||||
/// Defaults to PanelAllowedSides::LeftAndRight
|
||||
allowed_sides: PanelAllowedSides,
|
||||
initial_width: AbsoluteLength,
|
||||
width: Option<AbsoluteLength>,
|
||||
children: SmallVec<[AnyElement<V>; 2]>,
|
||||
}
|
||||
|
||||
impl<V: 'static> Panel<V> {
|
||||
pub fn new(id: impl Into<ElementId>, cx: &mut WindowContext) -> Self {
|
||||
let settings = user_settings(cx);
|
||||
|
||||
Self {
|
||||
id: id.into(),
|
||||
current_side: PanelSide::default(),
|
||||
allowed_sides: PanelAllowedSides::default(),
|
||||
initial_width: *settings.default_panel_size,
|
||||
width: None,
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initial_width(mut self, initial_width: AbsoluteLength) -> Self {
|
||||
self.initial_width = initial_width;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn width(mut self, width: AbsoluteLength) -> Self {
|
||||
self.width = Some(width);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn allowed_sides(mut self, allowed_sides: PanelAllowedSides) -> Self {
|
||||
self.allowed_sides = allowed_sides;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn side(mut self, side: PanelSide) -> Self {
|
||||
let allowed_sides = self.allowed_sides.allowed_sides();
|
||||
|
||||
if allowed_sides.contains(&side) {
|
||||
self.current_side = side;
|
||||
} else {
|
||||
panic!(
|
||||
"The panel side {:?} was not added as allowed before it was set.",
|
||||
side
|
||||
);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
let current_size = self.width.unwrap_or(self.initial_width);
|
||||
|
||||
v_stack()
|
||||
.id(self.id.clone())
|
||||
.flex_initial()
|
||||
.map(|this| match self.current_side {
|
||||
PanelSide::Left | PanelSide::Right => this.h_full().w(current_size),
|
||||
PanelSide::Bottom => this,
|
||||
})
|
||||
.map(|this| match self.current_side {
|
||||
PanelSide::Left => this.border_r(),
|
||||
PanelSide::Right => this.border_l(),
|
||||
PanelSide::Bottom => this.border_b().w_full().h(current_size),
|
||||
})
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
.border_color(cx.theme().colors().border)
|
||||
.children(self.children)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> ParentComponent<V> for Panel<V> {
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
|
||||
&mut self.children
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use super::*;
|
||||
use crate::{Label, Story};
|
||||
use gpui::{Div, InteractiveComponent, Render};
|
||||
|
||||
pub struct PanelStory;
|
||||
|
||||
impl Render for PanelStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, Panel<Self>>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(
|
||||
Panel::new("panel", cx).child(
|
||||
div()
|
||||
.id("panel-contents")
|
||||
.overflow_y_scroll()
|
||||
.children((0..100).map(|ix| Label::new(format!("Item {}", ix + 1)))),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,174 +0,0 @@
|
|||
use gpui::{Hsla, ViewContext};
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Represents a person with a Zed account's public profile.
|
||||
/// All data in this struct should be considered public.
|
||||
pub struct PublicPlayer {
|
||||
pub username: SharedString,
|
||||
pub avatar: SharedString,
|
||||
pub is_contact: bool,
|
||||
}
|
||||
|
||||
impl PublicPlayer {
|
||||
pub fn new(username: impl Into<SharedString>, avatar: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
username: username.into(),
|
||||
avatar: avatar.into(),
|
||||
is_contact: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
pub enum PlayerStatus {
|
||||
#[default]
|
||||
Offline,
|
||||
Online,
|
||||
InCall,
|
||||
Away,
|
||||
DoNotDisturb,
|
||||
Invisible,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
pub enum MicStatus {
|
||||
Muted,
|
||||
#[default]
|
||||
Unmuted,
|
||||
}
|
||||
|
||||
impl MicStatus {
|
||||
pub fn inverse(&self) -> Self {
|
||||
match self {
|
||||
Self::Muted => Self::Unmuted,
|
||||
Self::Unmuted => Self::Muted,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
pub enum VideoStatus {
|
||||
On,
|
||||
#[default]
|
||||
Off,
|
||||
}
|
||||
|
||||
impl VideoStatus {
|
||||
pub fn inverse(&self) -> Self {
|
||||
match self {
|
||||
Self::On => Self::Off,
|
||||
Self::Off => Self::On,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
pub enum ScreenShareStatus {
|
||||
Shared,
|
||||
#[default]
|
||||
NotShared,
|
||||
}
|
||||
|
||||
impl ScreenShareStatus {
|
||||
pub fn inverse(&self) -> Self {
|
||||
match self {
|
||||
Self::Shared => Self::NotShared,
|
||||
Self::NotShared => Self::Shared,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PlayerCallStatus {
|
||||
pub mic_status: MicStatus,
|
||||
/// Indicates if the player is currently speaking
|
||||
/// And the intensity of the volume coming through
|
||||
///
|
||||
/// 0.0 - 1.0
|
||||
pub voice_activity: f32,
|
||||
pub video_status: VideoStatus,
|
||||
pub screen_share_status: ScreenShareStatus,
|
||||
pub in_current_project: bool,
|
||||
pub disconnected: bool,
|
||||
pub following: Option<Vec<Player>>,
|
||||
pub followers: Option<Vec<Player>>,
|
||||
}
|
||||
|
||||
impl PlayerCallStatus {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
mic_status: MicStatus::default(),
|
||||
voice_activity: 0.,
|
||||
video_status: VideoStatus::default(),
|
||||
screen_share_status: ScreenShareStatus::default(),
|
||||
in_current_project: true,
|
||||
disconnected: false,
|
||||
following: None,
|
||||
followers: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone)]
|
||||
pub struct Player {
|
||||
index: usize,
|
||||
avatar_src: String,
|
||||
username: String,
|
||||
status: PlayerStatus,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PlayerWithCallStatus {
|
||||
player: Player,
|
||||
call_status: PlayerCallStatus,
|
||||
}
|
||||
|
||||
impl PlayerWithCallStatus {
|
||||
pub fn new(player: Player, call_status: PlayerCallStatus) -> Self {
|
||||
Self {
|
||||
player,
|
||||
call_status,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_player(&self) -> &Player {
|
||||
&self.player
|
||||
}
|
||||
|
||||
pub fn get_call_status(&self) -> &PlayerCallStatus {
|
||||
&self.call_status
|
||||
}
|
||||
}
|
||||
|
||||
impl Player {
|
||||
pub fn new(index: usize, avatar_src: String, username: String) -> Self {
|
||||
Self {
|
||||
index,
|
||||
avatar_src,
|
||||
username,
|
||||
status: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_status(mut self, status: PlayerStatus) -> Self {
|
||||
self.status = status;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn cursor_color<V: 'static>(&self, cx: &mut ViewContext<V>) -> Hsla {
|
||||
cx.theme().styles.player.0[self.index % cx.theme().styles.player.0.len()].cursor
|
||||
}
|
||||
|
||||
pub fn selection_color<V: 'static>(&self, cx: &mut ViewContext<V>) -> Hsla {
|
||||
cx.theme().styles.player.0[self.index % cx.theme().styles.player.0.len()].selection
|
||||
}
|
||||
|
||||
pub fn avatar_src(&self) -> &str {
|
||||
&self.avatar_src
|
||||
}
|
||||
|
||||
pub fn index(&self) -> usize {
|
||||
self.index
|
||||
}
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
use crate::prelude::*;
|
||||
use crate::{Avatar, Facepile, PlayerWithCallStatus};
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct PlayerStack {
|
||||
player_with_call_status: PlayerWithCallStatus,
|
||||
}
|
||||
|
||||
impl PlayerStack {
|
||||
pub fn new(player_with_call_status: PlayerWithCallStatus) -> Self {
|
||||
Self {
|
||||
player_with_call_status,
|
||||
}
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
let player = self.player_with_call_status.get_player();
|
||||
|
||||
let followers = self
|
||||
.player_with_call_status
|
||||
.get_call_status()
|
||||
.followers
|
||||
.as_ref()
|
||||
.map(|followers| followers.clone());
|
||||
|
||||
// if we have no followers return a slightly different element
|
||||
// if mic_status == muted add a red ring to avatar
|
||||
|
||||
div()
|
||||
.h_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_px()
|
||||
.justify_center()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.justify_center()
|
||||
.w_full()
|
||||
.child(div().w_4().h_0p5().rounded_sm().bg(player.cursor_color(cx))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.h_6()
|
||||
.pl_1()
|
||||
.rounded_lg()
|
||||
.bg(if followers.is_none() {
|
||||
cx.theme().styles.system.transparent
|
||||
} else {
|
||||
player.selection_color(cx)
|
||||
})
|
||||
.child(Avatar::new(player.avatar_src().to_string()))
|
||||
.children(followers.map(|followers| {
|
||||
div().neg_ml_2().child(Facepile::new(followers.into_iter()))
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
84
crates/ui2/src/components/popover.rs
Normal file
84
crates/ui2/src/components/popover.rs
Normal file
|
@ -0,0 +1,84 @@
|
|||
use gpui::{
|
||||
div, AnyElement, Div, Element, ElementId, IntoElement, ParentElement, RenderOnce, Styled,
|
||||
WindowContext,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use theme2::ActiveTheme;
|
||||
|
||||
use crate::{v_stack, StyledExt};
|
||||
|
||||
/// A popover is used to display a menu or show some options.
|
||||
///
|
||||
/// Clicking the element that launches the popover should not change the current view,
|
||||
/// and the popover should be statically positioned relative to that element (not the
|
||||
/// user's mouse.)
|
||||
///
|
||||
/// Example: A "new" menu with options like "new file", "new folder", etc,
|
||||
/// Linear's "Display" menu, a profile menu that appers when you click your avatar.
|
||||
///
|
||||
/// Related elements:
|
||||
///
|
||||
/// `ContextMenu`:
|
||||
///
|
||||
/// Used to display a popover menu that only contains a list of items. Context menus are always
|
||||
/// launched by secondary clicking on an element. The menu is positioned relative to the user's cursor.
|
||||
///
|
||||
/// Example: Right clicking a file in the file tree to get a list of actions, right clicking
|
||||
/// a tab to in the tab bar to get a list of actions.
|
||||
///
|
||||
/// `Dropdown`:
|
||||
///
|
||||
/// Used to display a list of options when the user clicks an element. The menu is
|
||||
/// positioned relative the element that was clicked, and clicking an item in the
|
||||
/// dropdown should change the value of the element that was clicked.
|
||||
///
|
||||
/// Example: A theme select control. Displays "One Dark", clicking it opens a list of themes.
|
||||
/// When one is selected, the theme select control displays the selected theme.
|
||||
#[derive(IntoElement)]
|
||||
pub struct Popover {
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
aside: Option<AnyElement>,
|
||||
}
|
||||
|
||||
impl RenderOnce for Popover {
|
||||
type Rendered = Div;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
div()
|
||||
.flex()
|
||||
.gap_1()
|
||||
.child(v_stack().elevation_2(cx).px_1().children(self.children))
|
||||
.when_some(self.aside, |this, aside| {
|
||||
this.child(
|
||||
v_stack()
|
||||
.elevation_2(cx)
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
.px_1()
|
||||
.child(aside),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Popover {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
children: SmallVec::new(),
|
||||
aside: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn aside(mut self, aside: impl IntoElement) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.aside = Some(aside.into_element().into_any());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for Popover {
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
|
||||
&mut self.children
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
use gpui::SharedString;
|
||||
use gpui::{ImageSource, SharedString};
|
||||
|
||||
use crate::Icon;
|
||||
|
||||
|
@ -9,6 +9,6 @@ use crate::Icon;
|
|||
/// Can be filled with a []
|
||||
pub enum GraphicSlot {
|
||||
Icon(Icon),
|
||||
Avatar(SharedString),
|
||||
Avatar(ImageSource),
|
||||
PublicActor(SharedString),
|
||||
}
|
||||
|
|
|
@ -5,13 +5,13 @@ use crate::StyledExt;
|
|||
/// Horizontally stacks elements.
|
||||
///
|
||||
/// Sets `flex()`, `flex_row()`, `items_center()`
|
||||
pub fn h_stack<V: 'static>() -> Div<V> {
|
||||
pub fn h_stack() -> Div {
|
||||
div().h_flex()
|
||||
}
|
||||
|
||||
/// Vertically stacks elements.
|
||||
///
|
||||
/// Sets `flex()`, `flex_col()`
|
||||
pub fn v_stack<V: 'static>() -> Div<V> {
|
||||
pub fn v_stack() -> Div {
|
||||
div().v_flex()
|
||||
}
|
||||
|
|
19
crates/ui2/src/components/stories.rs
Normal file
19
crates/ui2/src/components/stories.rs
Normal file
|
@ -0,0 +1,19 @@
|
|||
mod avatar;
|
||||
mod button;
|
||||
mod checkbox;
|
||||
mod context_menu;
|
||||
mod icon;
|
||||
mod input;
|
||||
mod keybinding;
|
||||
mod label;
|
||||
mod list_item;
|
||||
|
||||
pub use avatar::*;
|
||||
pub use button::*;
|
||||
pub use checkbox::*;
|
||||
pub use context_menu::*;
|
||||
pub use icon::*;
|
||||
pub use input::*;
|
||||
pub use keybinding::*;
|
||||
pub use label::*;
|
||||
pub use list_item::*;
|
23
crates/ui2/src/components/stories/avatar.rs
Normal file
23
crates/ui2/src/components/stories/avatar.rs
Normal file
|
@ -0,0 +1,23 @@
|
|||
use gpui::{Div, Render};
|
||||
use story::Story;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::Avatar;
|
||||
|
||||
pub struct AvatarStory;
|
||||
|
||||
impl Render for AvatarStory {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container()
|
||||
.child(Story::title_for::<Avatar>())
|
||||
.child(Story::label("Default"))
|
||||
.child(Avatar::uri(
|
||||
"https://avatars.githubusercontent.com/u/1714999?v=4",
|
||||
))
|
||||
.child(Avatar::uri(
|
||||
"https://avatars.githubusercontent.com/u/326587?v=4",
|
||||
))
|
||||
}
|
||||
}
|
145
crates/ui2/src/components/stories/button.rs
Normal file
145
crates/ui2/src/components/stories/button.rs
Normal file
|
@ -0,0 +1,145 @@
|
|||
use gpui::{rems, Div, Render};
|
||||
use story::Story;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{h_stack, v_stack, Button, Icon, IconPosition, Label};
|
||||
|
||||
pub struct ButtonStory;
|
||||
|
||||
impl Render for ButtonStory {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let states = InteractionState::iter();
|
||||
|
||||
Story::container()
|
||||
.child(Story::title_for::<Button>())
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_8()
|
||||
.child(
|
||||
div()
|
||||
.child(Story::label("Ghost (Default)"))
|
||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
||||
v_stack()
|
||||
.gap_1()
|
||||
.child(Label::new(state.to_string()).color(Color::Muted))
|
||||
.child(
|
||||
Button::new("Label").variant(ButtonVariant::Ghost), // .state(state),
|
||||
)
|
||||
})))
|
||||
.child(Story::label("Ghost – Left Icon"))
|
||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
||||
v_stack()
|
||||
.gap_1()
|
||||
.child(Label::new(state.to_string()).color(Color::Muted))
|
||||
.child(
|
||||
Button::new("Label")
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.icon(Icon::Plus)
|
||||
.icon_position(IconPosition::Left), // .state(state),
|
||||
)
|
||||
})))
|
||||
.child(Story::label("Ghost – Right Icon"))
|
||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
||||
v_stack()
|
||||
.gap_1()
|
||||
.child(Label::new(state.to_string()).color(Color::Muted))
|
||||
.child(
|
||||
Button::new("Label")
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.icon(Icon::Plus)
|
||||
.icon_position(IconPosition::Right), // .state(state),
|
||||
)
|
||||
}))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.child(Story::label("Filled"))
|
||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
||||
v_stack()
|
||||
.gap_1()
|
||||
.child(Label::new(state.to_string()).color(Color::Muted))
|
||||
.child(
|
||||
Button::new("Label").variant(ButtonVariant::Filled), // .state(state),
|
||||
)
|
||||
})))
|
||||
.child(Story::label("Filled – Left Button"))
|
||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
||||
v_stack()
|
||||
.gap_1()
|
||||
.child(Label::new(state.to_string()).color(Color::Muted))
|
||||
.child(
|
||||
Button::new("Label")
|
||||
.variant(ButtonVariant::Filled)
|
||||
.icon(Icon::Plus)
|
||||
.icon_position(IconPosition::Left), // .state(state),
|
||||
)
|
||||
})))
|
||||
.child(Story::label("Filled – Right Button"))
|
||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
||||
v_stack()
|
||||
.gap_1()
|
||||
.child(Label::new(state.to_string()).color(Color::Muted))
|
||||
.child(
|
||||
Button::new("Label")
|
||||
.variant(ButtonVariant::Filled)
|
||||
.icon(Icon::Plus)
|
||||
.icon_position(IconPosition::Right), // .state(state),
|
||||
)
|
||||
}))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.child(Story::label("Fixed With"))
|
||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
||||
v_stack()
|
||||
.gap_1()
|
||||
.child(Label::new(state.to_string()).color(Color::Muted))
|
||||
.child(
|
||||
Button::new("Label")
|
||||
.variant(ButtonVariant::Filled)
|
||||
// .state(state)
|
||||
.width(Some(rems(6.).into())),
|
||||
)
|
||||
})))
|
||||
.child(Story::label("Fixed With – Left Icon"))
|
||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
||||
v_stack()
|
||||
.gap_1()
|
||||
.child(Label::new(state.to_string()).color(Color::Muted))
|
||||
.child(
|
||||
Button::new("Label")
|
||||
.variant(ButtonVariant::Filled)
|
||||
// .state(state)
|
||||
.icon(Icon::Plus)
|
||||
.icon_position(IconPosition::Left)
|
||||
.width(Some(rems(6.).into())),
|
||||
)
|
||||
})))
|
||||
.child(Story::label("Fixed With – Right Icon"))
|
||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
||||
v_stack()
|
||||
.gap_1()
|
||||
.child(Label::new(state.to_string()).color(Color::Muted))
|
||||
.child(
|
||||
Button::new("Label")
|
||||
.variant(ButtonVariant::Filled)
|
||||
// .state(state)
|
||||
.icon(Icon::Plus)
|
||||
.icon_position(IconPosition::Right)
|
||||
.width(Some(rems(6.).into())),
|
||||
)
|
||||
}))),
|
||||
),
|
||||
)
|
||||
.child(Story::label("Button with `on_click`"))
|
||||
.child(
|
||||
Button::new("Label")
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.on_click(|_, cx| println!("Button clicked.")),
|
||||
)
|
||||
}
|
||||
}
|
49
crates/ui2/src/components/stories/checkbox.rs
Normal file
49
crates/ui2/src/components/stories/checkbox.rs
Normal file
|
@ -0,0 +1,49 @@
|
|||
use gpui::{Div, Render, ViewContext};
|
||||
use story::Story;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{h_stack, Checkbox};
|
||||
|
||||
pub struct CheckboxStory;
|
||||
|
||||
impl Render for CheckboxStory {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container()
|
||||
.child(Story::title_for::<Checkbox>())
|
||||
.child(Story::label("Default"))
|
||||
.child(
|
||||
h_stack()
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.rounded_md()
|
||||
.border()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(Checkbox::new("checkbox-enabled", Selection::Unselected))
|
||||
.child(Checkbox::new(
|
||||
"checkbox-intermediate",
|
||||
Selection::Indeterminate,
|
||||
))
|
||||
.child(Checkbox::new("checkbox-selected", Selection::Selected)),
|
||||
)
|
||||
.child(Story::label("Disabled"))
|
||||
.child(
|
||||
h_stack()
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.rounded_md()
|
||||
.border()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(Checkbox::new("checkbox-disabled", Selection::Unselected).disabled(true))
|
||||
.child(
|
||||
Checkbox::new("checkbox-disabled-intermediate", Selection::Indeterminate)
|
||||
.disabled(true),
|
||||
)
|
||||
.child(
|
||||
Checkbox::new("checkbox-disabled-selected", Selection::Selected)
|
||||
.disabled(true),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
104
crates/ui2/src/components/stories/context_menu.rs
Normal file
104
crates/ui2/src/components/stories/context_menu.rs
Normal file
|
@ -0,0 +1,104 @@
|
|||
use gpui::{actions, Action, AnchorCorner, Div, Render, View};
|
||||
use story::Story;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{menu_handle, ContextMenu, Label};
|
||||
|
||||
actions!(PrintCurrentDate, PrintBestFood);
|
||||
|
||||
fn build_menu(cx: &mut WindowContext, header: impl Into<SharedString>) -> View<ContextMenu> {
|
||||
ContextMenu::build(cx, |menu, _| {
|
||||
menu.header(header)
|
||||
.separator()
|
||||
.entry("Print current time", |v, cx| {
|
||||
println!("dispatching PrintCurrentTime action");
|
||||
cx.dispatch_action(PrintCurrentDate.boxed_clone())
|
||||
})
|
||||
.entry("Print best foot", |v, cx| {
|
||||
cx.dispatch_action(PrintBestFood.boxed_clone())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub struct ContextMenuStory;
|
||||
|
||||
impl Render for ContextMenuStory {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container()
|
||||
.on_action(|_: &PrintCurrentDate, _| {
|
||||
println!("printing unix time!");
|
||||
if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() {
|
||||
println!("Current Unix time is {:?}", unix_time.as_secs());
|
||||
}
|
||||
})
|
||||
.on_action(|_: &PrintBestFood, _| {
|
||||
println!("burrito");
|
||||
})
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_between()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.justify_between()
|
||||
.child(
|
||||
menu_handle("test2")
|
||||
.child(|is_open| {
|
||||
Label::new(if is_open {
|
||||
"TOP LEFT"
|
||||
} else {
|
||||
"RIGHT CLICK ME"
|
||||
})
|
||||
})
|
||||
.menu(move |cx| build_menu(cx, "top left")),
|
||||
)
|
||||
.child(
|
||||
menu_handle("test1")
|
||||
.child(|is_open| {
|
||||
Label::new(if is_open {
|
||||
"BOTTOM LEFT"
|
||||
} else {
|
||||
"RIGHT CLICK ME"
|
||||
})
|
||||
})
|
||||
.anchor(AnchorCorner::BottomLeft)
|
||||
.attach(AnchorCorner::TopLeft)
|
||||
.menu(move |cx| build_menu(cx, "bottom left")),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.justify_between()
|
||||
.child(
|
||||
menu_handle("test3")
|
||||
.child(|is_open| {
|
||||
Label::new(if is_open {
|
||||
"TOP RIGHT"
|
||||
} else {
|
||||
"RIGHT CLICK ME"
|
||||
})
|
||||
})
|
||||
.anchor(AnchorCorner::TopRight)
|
||||
.menu(move |cx| build_menu(cx, "top right")),
|
||||
)
|
||||
.child(
|
||||
menu_handle("test4")
|
||||
.child(|is_open| {
|
||||
Label::new(if is_open {
|
||||
"BOTTOM RIGHT"
|
||||
} else {
|
||||
"RIGHT CLICK ME"
|
||||
})
|
||||
})
|
||||
.anchor(AnchorCorner::BottomRight)
|
||||
.attach(AnchorCorner::TopRight)
|
||||
.menu(move |cx| build_menu(cx, "bottom right")),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
21
crates/ui2/src/components/stories/icon.rs
Normal file
21
crates/ui2/src/components/stories/icon.rs
Normal file
|
@ -0,0 +1,21 @@
|
|||
use gpui::{Div, Render};
|
||||
use story::Story;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{Icon, IconElement};
|
||||
|
||||
pub struct IconStory;
|
||||
|
||||
impl Render for IconStory {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let icons = Icon::iter();
|
||||
|
||||
Story::container()
|
||||
.child(Story::title_for::<IconElement>())
|
||||
.child(Story::label("All Icons"))
|
||||
.child(div().flex().gap_3().children(icons.map(IconElement::new)))
|
||||
}
|
||||
}
|
18
crates/ui2/src/components/stories/input.rs
Normal file
18
crates/ui2/src/components/stories/input.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
use gpui::{Div, Render};
|
||||
use story::Story;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::Input;
|
||||
|
||||
pub struct InputStory;
|
||||
|
||||
impl Render for InputStory {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container()
|
||||
.child(Story::title_for::<Input>())
|
||||
.child(Story::label("Default"))
|
||||
.child(div().flex().child(Input::new("Search")))
|
||||
}
|
||||
}
|
61
crates/ui2/src/components/stories/keybinding.rs
Normal file
61
crates/ui2/src/components/stories/keybinding.rs
Normal file
|
@ -0,0 +1,61 @@
|
|||
use gpui::{actions, Div, Render};
|
||||
use itertools::Itertools;
|
||||
use story::Story;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::KeyBinding;
|
||||
|
||||
pub struct KeybindingStory;
|
||||
|
||||
actions!(NoAction);
|
||||
|
||||
pub fn binding(key: &str) -> gpui::KeyBinding {
|
||||
gpui::KeyBinding::new(key, NoAction {}, None)
|
||||
}
|
||||
|
||||
impl Render for KeybindingStory {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let all_modifier_permutations = ["ctrl", "alt", "cmd", "shift"].into_iter().permutations(2);
|
||||
|
||||
Story::container()
|
||||
.child(Story::title_for::<KeyBinding>())
|
||||
.child(Story::label("Single Key"))
|
||||
.child(KeyBinding::new(binding("Z")))
|
||||
.child(Story::label("Single Key with Modifier"))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_3()
|
||||
.child(KeyBinding::new(binding("ctrl-c")))
|
||||
.child(KeyBinding::new(binding("alt-c")))
|
||||
.child(KeyBinding::new(binding("cmd-c")))
|
||||
.child(KeyBinding::new(binding("shift-c"))),
|
||||
)
|
||||
.child(Story::label("Single Key with Modifier (Permuted)"))
|
||||
.child(
|
||||
div().flex().flex_col().children(
|
||||
all_modifier_permutations
|
||||
.chunks(4)
|
||||
.into_iter()
|
||||
.map(|chunk| {
|
||||
div()
|
||||
.flex()
|
||||
.gap_4()
|
||||
.py_3()
|
||||
.children(chunk.map(|permutation| {
|
||||
KeyBinding::new(binding(&*(permutation.join("-") + "-x")))
|
||||
}))
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(Story::label("Single Key with All Modifiers"))
|
||||
.child(KeyBinding::new(binding("ctrl-alt-cmd-shift-z")))
|
||||
.child(Story::label("Chord"))
|
||||
.child(KeyBinding::new(binding("a z")))
|
||||
.child(Story::label("Chord with Modifier"))
|
||||
.child(KeyBinding::new(binding("ctrl-a shift-z")))
|
||||
.child(KeyBinding::new(binding("fn-s")))
|
||||
}
|
||||
}
|
27
crates/ui2/src/components/stories/label.rs
Normal file
27
crates/ui2/src/components/stories/label.rs
Normal file
|
@ -0,0 +1,27 @@
|
|||
use gpui::{Div, Render};
|
||||
use story::Story;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{HighlightedLabel, Label};
|
||||
|
||||
pub struct LabelStory;
|
||||
|
||||
impl Render for LabelStory {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container()
|
||||
.child(Story::title_for::<Label>())
|
||||
.child(Story::label("Default"))
|
||||
.child(Label::new("Hello, world!"))
|
||||
.child(Story::label("Highlighted"))
|
||||
.child(HighlightedLabel::new(
|
||||
"Hello, world!",
|
||||
vec![0, 1, 2, 7, 8, 12],
|
||||
))
|
||||
.child(HighlightedLabel::new(
|
||||
"Héllo, world!",
|
||||
vec![0, 1, 3, 8, 9, 13],
|
||||
))
|
||||
}
|
||||
}
|
34
crates/ui2/src/components/stories/list_item.rs
Normal file
34
crates/ui2/src/components/stories/list_item.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
use gpui::{Div, Render};
|
||||
use story::Story;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::ListItem;
|
||||
|
||||
pub struct ListItemStory;
|
||||
|
||||
impl Render for ListItemStory {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container()
|
||||
.child(Story::title_for::<ListItem>())
|
||||
.child(Story::label("Default"))
|
||||
.child(ListItem::new("hello_world").child("Hello, world!"))
|
||||
.child(Story::label("With `on_click`"))
|
||||
.child(
|
||||
ListItem::new("with_on_click")
|
||||
.child("Click me")
|
||||
.on_click(|_event, _cx| {
|
||||
println!("Clicked!");
|
||||
}),
|
||||
)
|
||||
.child(Story::label("With `on_secondary_mouse_down`"))
|
||||
.child(
|
||||
ListItem::new("with_on_secondary_mouse_down").on_secondary_mouse_down(
|
||||
|_event, _cx| {
|
||||
println!("Right mouse down!");
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,272 +0,0 @@
|
|||
use crate::prelude::*;
|
||||
use crate::{Icon, IconElement, Label, TextColor};
|
||||
use gpui::{prelude::*, red, Div, ElementId, Render, View};
|
||||
|
||||
#[derive(Component, Clone)]
|
||||
pub struct Tab {
|
||||
id: ElementId,
|
||||
title: String,
|
||||
icon: Option<Icon>,
|
||||
current: bool,
|
||||
dirty: bool,
|
||||
fs_status: FileSystemStatus,
|
||||
git_status: GitStatus,
|
||||
diagnostic_status: DiagnosticStatus,
|
||||
close_side: IconSide,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct TabDragState {
|
||||
title: String,
|
||||
}
|
||||
|
||||
impl Render for TabDragState {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
div().w_8().h_4().bg(red())
|
||||
}
|
||||
}
|
||||
|
||||
impl Tab {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
title: "untitled".to_string(),
|
||||
icon: None,
|
||||
current: false,
|
||||
dirty: false,
|
||||
fs_status: FileSystemStatus::None,
|
||||
git_status: GitStatus::None,
|
||||
diagnostic_status: DiagnosticStatus::None,
|
||||
close_side: IconSide::Right,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current(mut self, current: bool) -> Self {
|
||||
self.current = current;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn title(mut self, title: String) -> Self {
|
||||
self.title = title;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn icon<I>(mut self, icon: I) -> Self
|
||||
where
|
||||
I: Into<Option<Icon>>,
|
||||
{
|
||||
self.icon = icon.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn dirty(mut self, dirty: bool) -> Self {
|
||||
self.dirty = dirty;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn fs_status(mut self, fs_status: FileSystemStatus) -> Self {
|
||||
self.fs_status = fs_status;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn git_status(mut self, git_status: GitStatus) -> Self {
|
||||
self.git_status = git_status;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn diagnostic_status(mut self, diagnostic_status: DiagnosticStatus) -> Self {
|
||||
self.diagnostic_status = diagnostic_status;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn close_side(mut self, close_side: IconSide) -> Self {
|
||||
self.close_side = close_side;
|
||||
self
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
let has_fs_conflict = self.fs_status == FileSystemStatus::Conflict;
|
||||
let is_deleted = self.fs_status == FileSystemStatus::Deleted;
|
||||
|
||||
let label = match (self.git_status, is_deleted) {
|
||||
(_, true) | (GitStatus::Deleted, false) => Label::new(self.title.clone())
|
||||
.color(TextColor::Hidden)
|
||||
.set_strikethrough(true),
|
||||
(GitStatus::None, false) => Label::new(self.title.clone()),
|
||||
(GitStatus::Created, false) => Label::new(self.title.clone()).color(TextColor::Created),
|
||||
(GitStatus::Modified, false) => {
|
||||
Label::new(self.title.clone()).color(TextColor::Modified)
|
||||
}
|
||||
(GitStatus::Renamed, false) => Label::new(self.title.clone()).color(TextColor::Accent),
|
||||
(GitStatus::Conflict, false) => Label::new(self.title.clone()),
|
||||
};
|
||||
|
||||
let close_icon = || IconElement::new(Icon::Close).color(TextColor::Muted);
|
||||
|
||||
let (tab_bg, tab_hover_bg, tab_active_bg) = match self.current {
|
||||
false => (
|
||||
cx.theme().colors().tab_inactive_background,
|
||||
cx.theme().colors().ghost_element_hover,
|
||||
cx.theme().colors().ghost_element_active,
|
||||
),
|
||||
true => (
|
||||
cx.theme().colors().tab_active_background,
|
||||
cx.theme().colors().element_hover,
|
||||
cx.theme().colors().element_active,
|
||||
),
|
||||
};
|
||||
|
||||
let drag_state = TabDragState {
|
||||
title: self.title.clone(),
|
||||
};
|
||||
|
||||
div()
|
||||
.id(self.id.clone())
|
||||
.on_drag(move |_view, cx| cx.build_view(|cx| drag_state.clone()))
|
||||
.drag_over::<TabDragState>(|d| d.bg(cx.theme().colors().drop_target_background))
|
||||
.on_drop(|_view, state: View<TabDragState>, cx| {
|
||||
eprintln!("{:?}", state.read(cx));
|
||||
})
|
||||
.px_2()
|
||||
.py_0p5()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.bg(tab_bg)
|
||||
.hover(|h| h.bg(tab_hover_bg))
|
||||
.active(|a| a.bg(tab_active_bg))
|
||||
.child(
|
||||
div()
|
||||
.px_1()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1p5()
|
||||
.children(has_fs_conflict.then(|| {
|
||||
IconElement::new(Icon::ExclamationTriangle)
|
||||
.size(crate::IconSize::Small)
|
||||
.color(TextColor::Warning)
|
||||
}))
|
||||
.children(self.icon.map(IconElement::new))
|
||||
.children(if self.close_side == IconSide::Left {
|
||||
Some(close_icon())
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.child(label)
|
||||
.children(if self.close_side == IconSide::Right {
|
||||
Some(close_icon())
|
||||
} else {
|
||||
None
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use super::*;
|
||||
use crate::{h_stack, v_stack, Icon, Story};
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
pub struct TabStory;
|
||||
|
||||
impl Render for TabStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let git_statuses = GitStatus::iter();
|
||||
let fs_statuses = FileSystemStatus::iter();
|
||||
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, Tab>(cx))
|
||||
.child(
|
||||
h_stack().child(
|
||||
v_stack()
|
||||
.gap_2()
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(Tab::new("default")),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_stack().child(
|
||||
v_stack().gap_2().child(Story::label(cx, "Current")).child(
|
||||
h_stack()
|
||||
.gap_4()
|
||||
.child(
|
||||
Tab::new("current")
|
||||
.title("Current".to_string())
|
||||
.current(true),
|
||||
)
|
||||
.child(
|
||||
Tab::new("not_current")
|
||||
.title("Not Current".to_string())
|
||||
.current(false),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_stack().child(
|
||||
v_stack()
|
||||
.gap_2()
|
||||
.child(Story::label(cx, "Titled"))
|
||||
.child(Tab::new("titled").title("label".to_string())),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_stack().child(
|
||||
v_stack()
|
||||
.gap_2()
|
||||
.child(Story::label(cx, "With Icon"))
|
||||
.child(
|
||||
Tab::new("with_icon")
|
||||
.title("label".to_string())
|
||||
.icon(Some(Icon::Envelope)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_stack().child(
|
||||
v_stack()
|
||||
.gap_2()
|
||||
.child(Story::label(cx, "Close Side"))
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_4()
|
||||
.child(
|
||||
Tab::new("left")
|
||||
.title("Left".to_string())
|
||||
.close_side(IconSide::Left),
|
||||
)
|
||||
.child(Tab::new("right").title("Right".to_string())),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_stack()
|
||||
.gap_2()
|
||||
.child(Story::label(cx, "Git Status"))
|
||||
.child(h_stack().gap_4().children(git_statuses.map(|git_status| {
|
||||
Tab::new("git_status")
|
||||
.title(git_status.to_string())
|
||||
.git_status(git_status)
|
||||
}))),
|
||||
)
|
||||
.child(
|
||||
v_stack()
|
||||
.gap_2()
|
||||
.child(Story::label(cx, "File System Status"))
|
||||
.child(h_stack().gap_4().children(fs_statuses.map(|fs_status| {
|
||||
Tab::new("file_system_status")
|
||||
.title(fs_status.to_string())
|
||||
.fs_status(fs_status)
|
||||
}))),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
use crate::prelude::*;
|
||||
use gpui::{prelude::*, AnyElement};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum ToastOrigin {
|
||||
#[default]
|
||||
Bottom,
|
||||
BottomRight,
|
||||
}
|
||||
|
||||
/// Don't use toast directly:
|
||||
///
|
||||
/// - For messages with a required action, use a `NotificationToast`.
|
||||
/// - For messages that convey information, use a `StatusToast`.
|
||||
///
|
||||
/// A toast is a small, temporary window that appears to show a message to the user
|
||||
/// or indicate a required action.
|
||||
///
|
||||
/// Toasts should not persist on the screen for more than a few seconds unless
|
||||
/// they are actively showing the a process in progress.
|
||||
///
|
||||
/// Only one toast may be visible at a time.
|
||||
#[derive(Component)]
|
||||
pub struct Toast<V: 'static> {
|
||||
origin: ToastOrigin,
|
||||
children: SmallVec<[AnyElement<V>; 2]>,
|
||||
}
|
||||
|
||||
impl<V: 'static> Toast<V> {
|
||||
pub fn new(origin: ToastOrigin) -> Self {
|
||||
Self {
|
||||
origin,
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
let mut div = div();
|
||||
|
||||
if self.origin == ToastOrigin::Bottom {
|
||||
div = div.right_1_2();
|
||||
} else {
|
||||
div = div.right_2();
|
||||
}
|
||||
|
||||
div.z_index(5)
|
||||
.absolute()
|
||||
.bottom_9()
|
||||
.flex()
|
||||
.py_1()
|
||||
.px_1p5()
|
||||
.rounded_lg()
|
||||
.shadow_md()
|
||||
.overflow_hidden()
|
||||
.bg(cx.theme().colors().elevated_surface_background)
|
||||
.children(self.children)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> ParentComponent<V> for Toast<V> {
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
|
||||
&mut self.children
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use gpui::{Div, Render};
|
||||
|
||||
use crate::{Label, Story};
|
||||
|
||||
use super::*;
|
||||
|
||||
pub struct ToastStory;
|
||||
|
||||
impl Render for ToastStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, Toast<Self>>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(Toast::new(ToastOrigin::Bottom).child(Label::new("label")))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,3 @@
|
|||
use gpui::{div, Component, ParentComponent};
|
||||
|
||||
use crate::{Icon, IconElement, IconSize, TextColor};
|
||||
|
||||
/// Whether the entry is toggleable, and if so, whether it is currently toggled.
|
||||
///
|
||||
/// To make an element toggleable, simply add a `Toggle::Toggled(_)` and handle it's cases.
|
||||
|
@ -43,19 +39,3 @@ impl From<bool> for Toggle {
|
|||
Toggle::Toggled(toggled)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disclosure_control<V: 'static>(toggle: Toggle) -> impl Component<V> {
|
||||
match (toggle.is_toggleable(), toggle.is_toggled()) {
|
||||
(false, _) => div(),
|
||||
(_, true) => div().child(
|
||||
IconElement::new(Icon::ChevronDown)
|
||||
.color(TextColor::Muted)
|
||||
.size(IconSize::Small),
|
||||
),
|
||||
(_, false) => div().child(
|
||||
IconElement::new(Icon::ChevronRight)
|
||||
.color(TextColor::Muted)
|
||||
.size(IconSize::Small),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct ToolDivider;
|
||||
|
||||
impl ToolDivider {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
div().w_px().h_3().bg(cx.theme().colors().border)
|
||||
}
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
use gpui::{overlay, Action, AnyView, Overlay, Render, VisualContext};
|
||||
use gpui::{overlay, Action, AnyView, IntoElement, Overlay, Render, VisualContext};
|
||||
use settings2::Settings;
|
||||
use theme2::{ActiveTheme, ThemeSettings};
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{h_stack, v_stack, KeyBinding, Label, LabelSize, StyledExt, TextColor};
|
||||
use crate::{h_stack, v_stack, Color, KeyBinding, Label, LabelSize, StyledExt};
|
||||
|
||||
pub struct Tooltip {
|
||||
title: SharedString,
|
||||
|
@ -68,7 +68,7 @@ impl Tooltip {
|
|||
}
|
||||
|
||||
impl Render for Tooltip {
|
||||
type Element = Overlay<Self>;
|
||||
type Element = Overlay;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
|
||||
|
@ -90,11 +90,7 @@ impl Render for Tooltip {
|
|||
}),
|
||||
)
|
||||
.when_some(self.meta.clone(), |this, meta| {
|
||||
this.child(
|
||||
Label::new(meta)
|
||||
.size(LabelSize::Small)
|
||||
.color(TextColor::Muted),
|
||||
)
|
||||
this.child(Label::new(meta).size(LabelSize::Small).color(Color::Muted))
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
|
|
@ -1,116 +1,14 @@
|
|||
use gpui::rems;
|
||||
use gpui::Rems;
|
||||
pub use gpui::{
|
||||
div, Component, Element, ElementId, InteractiveComponent, ParentComponent, SharedString,
|
||||
Styled, ViewContext, WindowContext,
|
||||
div, Element, ElementId, InteractiveElement, ParentElement, RenderOnce, SharedString, Styled,
|
||||
ViewContext, WindowContext,
|
||||
};
|
||||
|
||||
pub use crate::elevation::*;
|
||||
pub use crate::StyledExt;
|
||||
pub use crate::{ButtonVariant, TextColor};
|
||||
pub use crate::{ButtonVariant, Color};
|
||||
pub use theme2::ActiveTheme;
|
||||
|
||||
use gpui::Hsla;
|
||||
use strum::EnumIter;
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
|
||||
pub enum UITextSize {
|
||||
/// The default size for UI text.
|
||||
///
|
||||
/// `0.825rem` or `14px` at the default scale of `1rem` = `16px`.
|
||||
///
|
||||
/// Note: The absolute size of this text will change based on a user's `ui_scale` setting.
|
||||
#[default]
|
||||
Default,
|
||||
/// The small size for UI text.
|
||||
///
|
||||
/// `0.75rem` or `12px` at the default scale of `1rem` = `16px`.
|
||||
///
|
||||
/// Note: The absolute size of this text will change based on a user's `ui_scale` setting.
|
||||
Small,
|
||||
}
|
||||
|
||||
impl UITextSize {
|
||||
pub fn rems(self) -> Rems {
|
||||
match self {
|
||||
Self::Default => rems(0.875),
|
||||
Self::Small => rems(0.75),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
|
||||
pub enum FileSystemStatus {
|
||||
#[default]
|
||||
None,
|
||||
Conflict,
|
||||
Deleted,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FileSystemStatus {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::None => "None",
|
||||
Self::Conflict => "Conflict",
|
||||
Self::Deleted => "Deleted",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
|
||||
pub enum GitStatus {
|
||||
#[default]
|
||||
None,
|
||||
Created,
|
||||
Modified,
|
||||
Deleted,
|
||||
Conflict,
|
||||
Renamed,
|
||||
}
|
||||
|
||||
impl GitStatus {
|
||||
pub fn hsla(&self, cx: &WindowContext) -> Hsla {
|
||||
match self {
|
||||
Self::None => cx.theme().system().transparent,
|
||||
Self::Created => cx.theme().status().created,
|
||||
Self::Modified => cx.theme().status().modified,
|
||||
Self::Deleted => cx.theme().status().deleted,
|
||||
Self::Conflict => cx.theme().status().conflict,
|
||||
Self::Renamed => cx.theme().status().renamed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for GitStatus {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::None => "None",
|
||||
Self::Created => "Created",
|
||||
Self::Modified => "Modified",
|
||||
Self::Deleted => "Deleted",
|
||||
Self::Conflict => "Conflict",
|
||||
Self::Renamed => "Renamed",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
|
||||
pub enum DiagnosticStatus {
|
||||
#[default]
|
||||
None,
|
||||
Error,
|
||||
Warning,
|
||||
Info,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
|
||||
pub enum IconSide {
|
||||
#[default]
|
||||
|
@ -118,45 +16,6 @@ pub enum IconSide {
|
|||
Right,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
|
||||
pub enum OrderMethod {
|
||||
#[default]
|
||||
Ascending,
|
||||
Descending,
|
||||
MostRecent,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
|
||||
pub enum Shape {
|
||||
#[default]
|
||||
Circle,
|
||||
RoundedRectangle,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
|
||||
pub enum DisclosureControlVisibility {
|
||||
#[default]
|
||||
OnHover,
|
||||
Always,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
|
||||
pub enum DisclosureControlStyle {
|
||||
/// Shows the disclosure control only when hovered where possible.
|
||||
///
|
||||
/// More compact, but not available everywhere.
|
||||
ChevronOnHover,
|
||||
/// Shows an icon where possible, otherwise shows a chevron.
|
||||
///
|
||||
/// For example, in a file tree a folder or file icon is shown
|
||||
/// instead of a chevron
|
||||
Icon,
|
||||
/// Always shows a chevron.
|
||||
Chevron,
|
||||
/// Completely hides the disclosure control where possible.
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, EnumIter)]
|
||||
pub enum OverflowStyle {
|
||||
Hidden,
|
||||
|
@ -165,12 +24,22 @@ pub enum OverflowStyle {
|
|||
|
||||
#[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 {
|
||||
|
|
|
@ -1,74 +0,0 @@
|
|||
use std::ops::Deref;
|
||||
|
||||
use gpui::{rems, AbsoluteLength, AppContext, WindowContext};
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.set_global(FakeSettings::default());
|
||||
}
|
||||
|
||||
/// Returns the user settings.
|
||||
pub fn user_settings(cx: &WindowContext) -> FakeSettings {
|
||||
cx.global::<FakeSettings>().clone()
|
||||
}
|
||||
|
||||
pub fn user_settings_mut<'cx>(cx: &'cx mut WindowContext) -> &'cx mut FakeSettings {
|
||||
cx.global_mut::<FakeSettings>()
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum SettingValue<T> {
|
||||
UserDefined(T),
|
||||
Default(T),
|
||||
}
|
||||
|
||||
impl<T> Deref for SettingValue<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match self {
|
||||
Self::UserDefined(value) => value,
|
||||
Self::Default(value) => value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TitlebarSettings {
|
||||
pub show_project_owner: SettingValue<bool>,
|
||||
pub show_git_status: SettingValue<bool>,
|
||||
pub show_git_controls: SettingValue<bool>,
|
||||
}
|
||||
|
||||
impl Default for TitlebarSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
show_project_owner: SettingValue::Default(true),
|
||||
show_git_status: SettingValue::Default(true),
|
||||
show_git_controls: SettingValue::Default(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// These should be merged into settings
|
||||
#[derive(Clone)]
|
||||
pub struct FakeSettings {
|
||||
pub default_panel_size: SettingValue<AbsoluteLength>,
|
||||
pub list_disclosure_style: SettingValue<DisclosureControlStyle>,
|
||||
pub list_indent_depth: SettingValue<AbsoluteLength>,
|
||||
pub titlebar: TitlebarSettings,
|
||||
}
|
||||
|
||||
impl Default for FakeSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
titlebar: TitlebarSettings::default(),
|
||||
list_disclosure_style: SettingValue::Default(DisclosureControlStyle::ChevronOnHover),
|
||||
list_indent_depth: SettingValue::Default(rems(0.3).into()),
|
||||
default_panel_size: SettingValue::Default(rems(16.).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FakeSettings {}
|
File diff suppressed because it is too large
Load diff
|
@ -1,37 +0,0 @@
|
|||
use gpui::Div;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
pub struct Story {}
|
||||
|
||||
impl Story {
|
||||
pub fn container<V: 'static>(cx: &mut ViewContext<V>) -> Div<V> {
|
||||
div()
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.pt_2()
|
||||
.px_4()
|
||||
.bg(cx.theme().colors().background)
|
||||
}
|
||||
|
||||
pub fn title<V: 'static>(cx: &mut ViewContext<V>, title: &str) -> impl Component<V> {
|
||||
div()
|
||||
.text_xl()
|
||||
.text_color(cx.theme().colors().text)
|
||||
.child(title.to_owned())
|
||||
}
|
||||
|
||||
pub fn title_for<V: 'static, T>(cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
Self::title(cx, std::any::type_name::<T>())
|
||||
}
|
||||
|
||||
pub fn label<V: 'static>(cx: &mut ViewContext<V>, label: &str) -> impl Component<V> {
|
||||
div()
|
||||
.mt_4()
|
||||
.mb_2()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().colors().text)
|
||||
.child(label.to_owned())
|
||||
}
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
use gpui::{Styled, ViewContext};
|
||||
use gpui::{px, Styled, WindowContext};
|
||||
use theme2::ActiveTheme;
|
||||
|
||||
use crate::{ElevationIndex, UITextSize};
|
||||
|
||||
fn elevated<E: Styled, V: 'static>(this: E, cx: &mut ViewContext<V>, index: ElevationIndex) -> E {
|
||||
fn elevated<E: Styled>(this: E, cx: &mut WindowContext, index: ElevationIndex) -> E {
|
||||
this.bg(cx.theme().colors().elevated_surface_background)
|
||||
.z_index(index.z_index())
|
||||
.rounded_lg()
|
||||
.rounded(px(8.))
|
||||
.border()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.shadow(index.shadow())
|
||||
|
@ -65,7 +65,7 @@ pub trait StyledExt: Styled + Sized {
|
|||
/// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`
|
||||
///
|
||||
/// Example Elements: Title Bar, Panel, Tab Bar, Editor
|
||||
fn elevation_1<V: 'static>(self, cx: &mut ViewContext<V>) -> Self {
|
||||
fn elevation_1(self, cx: &mut WindowContext) -> Self {
|
||||
elevated(self, cx, ElevationIndex::Surface)
|
||||
}
|
||||
|
||||
|
@ -74,12 +74,10 @@ pub trait StyledExt: Styled + Sized {
|
|||
/// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`
|
||||
///
|
||||
/// Examples: Notifications, Palettes, Detached/Floating Windows, Detached/Floating Panels
|
||||
fn elevation_2<V: 'static>(self, cx: &mut ViewContext<V>) -> Self {
|
||||
fn elevation_2(self, cx: &mut WindowContext) -> Self {
|
||||
elevated(self, cx, ElevationIndex::ElevatedSurface)
|
||||
}
|
||||
|
||||
// There is no elevation 3, as the third elevation level is reserved for wash layers. See [`Elevation`](ui2::Elevation).
|
||||
|
||||
/// Modal Surfaces are used for elements that should appear above all other UI elements and are located above the wash layer. This is the maximum elevation at which UI elements can be rendered in their default state.
|
||||
///
|
||||
/// Elements rendered at this layer should have an enforced behavior: Any interaction outside of the modal will either dismiss the modal or prompt an action (Save your progress, etc) then dismiss the modal.
|
||||
|
@ -89,7 +87,7 @@ pub trait StyledExt: Styled + Sized {
|
|||
/// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`
|
||||
///
|
||||
/// Examples: Settings Modal, Channel Management, Wizards/Setup UI, Dialogs
|
||||
fn elevation_4<V: 'static>(self, cx: &mut ViewContext<V>) -> Self {
|
||||
fn elevation_3(self, cx: &mut WindowContext) -> Self {
|
||||
elevated(self, cx, ElevationIndex::ModalSurface)
|
||||
}
|
||||
}
|
||||
|
|
7
crates/ui2/src/styles.rs
Normal file
7
crates/ui2/src/styles.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
mod color;
|
||||
mod elevation;
|
||||
mod typography;
|
||||
|
||||
pub use color::*;
|
||||
pub use elevation::*;
|
||||
pub use typography::*;
|
44
crates/ui2/src/styles/color.rs
Normal file
44
crates/ui2/src/styles/color.rs
Normal file
|
@ -0,0 +1,44 @@
|
|||
use gpui::{Hsla, WindowContext};
|
||||
use theme2::ActiveTheme;
|
||||
|
||||
#[derive(Default, PartialEq, Copy, Clone)]
|
||||
pub enum Color {
|
||||
#[default]
|
||||
Default,
|
||||
Accent,
|
||||
Created,
|
||||
Deleted,
|
||||
Disabled,
|
||||
Error,
|
||||
Hidden,
|
||||
Info,
|
||||
Modified,
|
||||
Muted,
|
||||
Placeholder,
|
||||
Player(u32),
|
||||
Selected,
|
||||
Success,
|
||||
Warning,
|
||||
}
|
||||
|
||||
impl Color {
|
||||
pub fn color(&self, cx: &WindowContext) -> Hsla {
|
||||
match self {
|
||||
Color::Default => cx.theme().colors().text,
|
||||
Color::Muted => cx.theme().colors().text_muted,
|
||||
Color::Created => cx.theme().status().created,
|
||||
Color::Modified => cx.theme().status().modified,
|
||||
Color::Deleted => cx.theme().status().deleted,
|
||||
Color::Disabled => cx.theme().colors().text_disabled,
|
||||
Color::Hidden => cx.theme().status().hidden,
|
||||
Color::Info => cx.theme().status().info,
|
||||
Color::Placeholder => cx.theme().colors().text_placeholder,
|
||||
Color::Accent => cx.theme().colors().text_accent,
|
||||
Color::Player(i) => cx.theme().styles.player.0[i.clone() as usize].cursor,
|
||||
Color::Error => cx.theme().status().error,
|
||||
Color::Selected => cx.theme().colors().text_accent,
|
||||
Color::Success => cx.theme().status().success,
|
||||
Color::Warning => cx.theme().status().warning,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,27 +1,10 @@
|
|||
TODO: Originally sourced from Material Design 3, Rewrite to be more Zed specific
|
||||
|
||||
# Elevation
|
||||
|
||||
Zed applies elevation to all surfaces and components, which are categorized into levels.
|
||||
|
||||
Elevation accomplishes the following:
|
||||
- Allows surfaces to move in front of or behind others, such as content scrolling beneath app top bars.
|
||||
- Reflects spatial relationships, for instance, how a floating action button’s shadow intimates its disconnection from a collection of cards.
|
||||
- Directs attention to structures at the highest elevation, like a temporary dialog arising in front of other surfaces.
|
||||
|
||||
Elevations are the initial elevation values assigned to components by default.
|
||||
|
||||
Components may transition to a higher elevation in some cases, like user interations.
|
||||
|
||||
On such occasions, components transition to predetermined dynamic elevation offsets. These are the typical elevations to which components move when they are not at rest.
|
||||
|
||||
## Understanding Elevation
|
||||
|
||||
Elevation can be thought of as the physical closeness of an element to the user. Elements with lower elevations are physically further away from the user on the z-axis and appear to be underneath elements with higher elevations.
|
||||
|
||||
Material Design 3 has a some great visualizations of elevation that may be helpful to understanding the mental modal of elevation. [Material Design – Elevation](https://m3.material.io/styles/elevation/overview)
|
||||
|
||||
## Elevation
|
||||
## Elevation Levels
|
||||
|
||||
1. App Background (e.x.: Workspace, system window)
|
||||
1. UI Surface (e.x.: Title Bar, Panel, Tab Bar)
|
||||
|
@ -59,27 +42,3 @@ Modal Surfaces are used for elements that should appear above all other UI eleme
|
|||
Elements rendered at this layer have an enforced behavior: Any interaction outside of the modal will either dismiss the modal or prompt an action (Save your progress, etc) then dismiss the modal.
|
||||
|
||||
If the element does not have this behavior, it should be rendered at the Elevated Surface layer.
|
||||
|
||||
## Layer
|
||||
Each elevation that can contain elements has its own set of layers that are nested within the elevations.
|
||||
|
||||
1. TBD (Z -1 layer)
|
||||
1. Element (Text, button, surface, etc)
|
||||
1. Elevated Element (Popover, Context Menu, Tooltip)
|
||||
999. Dragged Element -> Highest Elevation
|
||||
|
||||
Dragged elements jump to the highest elevation the app can render. An active drag should _always_ be the most foreground element in the app at any time.
|
||||
|
||||
🚧 Work in Progress 🚧
|
||||
|
||||
## Element
|
||||
Each elevation that can contain elements has it's own set of layers:
|
||||
|
||||
1. Effects
|
||||
1. Background
|
||||
1. Tint
|
||||
1. Highlight
|
||||
1. Content
|
||||
1. Overlay
|
||||
|
||||
🚧 Work in Progress 🚧
|
|
@ -1,7 +1,7 @@
|
|||
use gpui::{hsla, point, px, BoxShadow};
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
|
||||
#[doc = include_str!("elevation.md")]
|
||||
#[doc = include_str!("docs/elevation.md")]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Elevation {
|
||||
ElevationIndex(ElevationIndex),
|
||||
|
@ -25,8 +25,8 @@ impl ElevationIndex {
|
|||
ElevationIndex::Background => 0,
|
||||
ElevationIndex::Surface => 100,
|
||||
ElevationIndex::ElevatedSurface => 200,
|
||||
ElevationIndex::Wash => 300,
|
||||
ElevationIndex::ModalSurface => 400,
|
||||
ElevationIndex::Wash => 250,
|
||||
ElevationIndex::ModalSurface => 300,
|
||||
ElevationIndex::DraggedElement => 900,
|
||||
}
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ impl ElevationIndex {
|
|||
spread_radius: px(0.),
|
||||
},
|
||||
BoxShadow {
|
||||
color: hsla(0., 0., 0., 0.16),
|
||||
color: hsla(0., 0., 0., 0.20),
|
||||
offset: point(px(3.), px(1.)),
|
||||
blur_radius: px(12.),
|
||||
spread_radius: px(0.),
|
27
crates/ui2/src/styles/typography.rs
Normal file
27
crates/ui2/src/styles/typography.rs
Normal file
|
@ -0,0 +1,27 @@
|
|||
use gpui::{rems, Rems};
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub enum UITextSize {
|
||||
/// The default size for UI text.
|
||||
///
|
||||
/// `0.825rem` or `14px` at the default scale of `1rem` = `16px`.
|
||||
///
|
||||
/// Note: The absolute size of this text will change based on a user's `ui_scale` setting.
|
||||
#[default]
|
||||
Default,
|
||||
/// The small size for UI text.
|
||||
///
|
||||
/// `0.75rem` or `12px` at the default scale of `1rem` = `16px`.
|
||||
///
|
||||
/// Note: The absolute size of this text will change based on a user's `ui_scale` setting.
|
||||
Small,
|
||||
}
|
||||
|
||||
impl UITextSize {
|
||||
pub fn rems(self) -> Rems {
|
||||
match self {
|
||||
Self::Default => rems(0.875),
|
||||
Self::Small => rems(0.75),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
mod assistant_panel;
|
||||
mod breadcrumb;
|
||||
mod buffer;
|
||||
mod buffer_search;
|
||||
mod chat_panel;
|
||||
mod collab_panel;
|
||||
mod command_palette;
|
||||
mod copilot;
|
||||
mod editor_pane;
|
||||
mod language_selector;
|
||||
mod multi_buffer;
|
||||
mod notifications_panel;
|
||||
mod panes;
|
||||
mod project_panel;
|
||||
mod recent_projects;
|
||||
mod status_bar;
|
||||
mod tab_bar;
|
||||
mod terminal;
|
||||
mod theme_selector;
|
||||
mod title_bar;
|
||||
mod toolbar;
|
||||
mod traffic_lights;
|
||||
mod workspace;
|
||||
|
||||
pub use assistant_panel::*;
|
||||
pub use breadcrumb::*;
|
||||
pub use buffer::*;
|
||||
pub use buffer_search::*;
|
||||
pub use chat_panel::*;
|
||||
pub use collab_panel::*;
|
||||
pub use command_palette::*;
|
||||
pub use copilot::*;
|
||||
pub use editor_pane::*;
|
||||
pub use language_selector::*;
|
||||
pub use multi_buffer::*;
|
||||
pub use notifications_panel::*;
|
||||
pub use panes::*;
|
||||
pub use project_panel::*;
|
||||
pub use recent_projects::*;
|
||||
pub use status_bar::*;
|
||||
pub use tab_bar::*;
|
||||
pub use terminal::*;
|
||||
pub use theme_selector::*;
|
||||
pub use title_bar::*;
|
||||
pub use toolbar::*;
|
||||
pub use traffic_lights::*;
|
||||
pub use workspace::*;
|
|
@ -1,93 +0,0 @@
|
|||
use crate::prelude::*;
|
||||
use crate::{Icon, IconButton, Label, Panel, PanelSide};
|
||||
use gpui::{prelude::*, rems, AbsoluteLength};
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct AssistantPanel {
|
||||
id: ElementId,
|
||||
current_side: PanelSide,
|
||||
}
|
||||
|
||||
impl AssistantPanel {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
current_side: PanelSide::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn side(mut self, side: PanelSide) -> Self {
|
||||
self.current_side = side;
|
||||
self
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
Panel::new(self.id.clone(), cx)
|
||||
.children(vec![div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.h_full()
|
||||
.px_2()
|
||||
.gap_2()
|
||||
// Header
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.justify_between()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.child(IconButton::new("menu", Icon::Menu))
|
||||
.child(Label::new("New Conversation")),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_px()
|
||||
.child(IconButton::new("split_message", Icon::SplitMessage))
|
||||
.child(IconButton::new("quote", Icon::Quote))
|
||||
.child(IconButton::new("magic_wand", Icon::MagicWand))
|
||||
.child(IconButton::new("plus", Icon::Plus))
|
||||
.child(IconButton::new("maximize", Icon::Maximize)),
|
||||
),
|
||||
)
|
||||
// Chat Body
|
||||
.child(
|
||||
div()
|
||||
.id("chat-body")
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.overflow_y_scroll()
|
||||
.child(Label::new("Is this thing on?")),
|
||||
)
|
||||
.render()])
|
||||
.side(self.current_side)
|
||||
.width(AbsoluteLength::Rems(rems(32.)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use super::*;
|
||||
use crate::Story;
|
||||
use gpui::{Div, Render};
|
||||
pub struct AssistantPanelStory;
|
||||
|
||||
impl Render for AssistantPanelStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, AssistantPanel>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(AssistantPanel::new("assistant-panel"))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,113 +0,0 @@
|
|||
use crate::{h_stack, prelude::*, HighlightedText};
|
||||
use gpui::{prelude::*, Div};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Symbol(pub Vec<HighlightedText>);
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct Breadcrumb {
|
||||
path: PathBuf,
|
||||
symbols: Vec<Symbol>,
|
||||
}
|
||||
|
||||
impl Breadcrumb {
|
||||
pub fn new(path: PathBuf, symbols: Vec<Symbol>) -> Self {
|
||||
Self { path, symbols }
|
||||
}
|
||||
|
||||
fn render_separator<V: 'static>(&self, cx: &WindowContext) -> Div<V> {
|
||||
div()
|
||||
.child(" › ")
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, view_state: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
let symbols_len = self.symbols.len();
|
||||
|
||||
h_stack()
|
||||
.id("breadcrumb")
|
||||
.px_1()
|
||||
.text_ui_sm()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.rounded_md()
|
||||
.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
|
||||
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
|
||||
.child(self.path.clone().to_str().unwrap().to_string())
|
||||
.child(if !self.symbols.is_empty() {
|
||||
self.render_separator(cx)
|
||||
} else {
|
||||
div()
|
||||
})
|
||||
.child(
|
||||
div().flex().children(
|
||||
self.symbols
|
||||
.iter()
|
||||
.enumerate()
|
||||
// TODO: Could use something like `intersperse` here instead.
|
||||
.flat_map(|(ix, symbol)| {
|
||||
let mut items =
|
||||
vec![div().flex().children(symbol.0.iter().map(|segment| {
|
||||
div().child(segment.text.clone()).text_color(segment.color)
|
||||
}))];
|
||||
|
||||
let is_last_segment = ix == symbols_len - 1;
|
||||
if !is_last_segment {
|
||||
items.push(self.render_separator(cx));
|
||||
}
|
||||
|
||||
items
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use super::*;
|
||||
use crate::Story;
|
||||
use gpui::Render;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub struct BreadcrumbStory;
|
||||
|
||||
impl Render for BreadcrumbStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, Breadcrumb>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(Breadcrumb::new(
|
||||
PathBuf::from_str("crates/ui/src/components/toolbar.rs").unwrap(),
|
||||
vec![
|
||||
Symbol(vec![
|
||||
HighlightedText {
|
||||
text: "impl ".to_string(),
|
||||
color: cx.theme().syntax_color("keyword"),
|
||||
},
|
||||
HighlightedText {
|
||||
text: "BreadcrumbStory".to_string(),
|
||||
color: cx.theme().syntax_color("function"),
|
||||
},
|
||||
]),
|
||||
Symbol(vec![
|
||||
HighlightedText {
|
||||
text: "fn ".to_string(),
|
||||
color: cx.theme().syntax_color("keyword"),
|
||||
},
|
||||
HighlightedText {
|
||||
text: "render".to_string(),
|
||||
color: cx.theme().syntax_color("function"),
|
||||
},
|
||||
]),
|
||||
],
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,266 +0,0 @@
|
|||
use gpui::{Hsla, WindowContext};
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{h_stack, v_stack, Icon, IconElement};
|
||||
|
||||
#[derive(Default, PartialEq, Copy, Clone)]
|
||||
pub struct PlayerCursor {
|
||||
color: Hsla,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Clone)]
|
||||
pub struct HighlightedText {
|
||||
pub text: String,
|
||||
pub color: Hsla,
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Clone)]
|
||||
pub struct HighlightedLine {
|
||||
pub highlighted_texts: Vec<HighlightedText>,
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Clone)]
|
||||
pub struct BufferRow {
|
||||
pub line_number: usize,
|
||||
pub code_action: bool,
|
||||
pub current: bool,
|
||||
pub line: Option<HighlightedLine>,
|
||||
pub cursors: Option<Vec<PlayerCursor>>,
|
||||
pub status: GitStatus,
|
||||
pub show_line_number: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BufferRows {
|
||||
pub show_line_numbers: bool,
|
||||
pub rows: Vec<BufferRow>,
|
||||
}
|
||||
|
||||
impl Default for BufferRows {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
show_line_numbers: true,
|
||||
rows: vec![BufferRow {
|
||||
line_number: 1,
|
||||
code_action: false,
|
||||
current: true,
|
||||
line: None,
|
||||
cursors: None,
|
||||
status: GitStatus::None,
|
||||
show_line_number: true,
|
||||
}],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BufferRow {
|
||||
pub fn new(line_number: usize) -> Self {
|
||||
Self {
|
||||
line_number,
|
||||
code_action: false,
|
||||
current: false,
|
||||
line: None,
|
||||
cursors: None,
|
||||
status: GitStatus::None,
|
||||
show_line_number: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_line(mut self, line: Option<HighlightedLine>) -> Self {
|
||||
self.line = line;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_cursors(mut self, cursors: Option<Vec<PlayerCursor>>) -> Self {
|
||||
self.cursors = cursors;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_cursor(mut self, cursor: PlayerCursor) -> Self {
|
||||
if let Some(cursors) = &mut self.cursors {
|
||||
cursors.push(cursor);
|
||||
} else {
|
||||
self.cursors = Some(vec![cursor]);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_status(mut self, status: GitStatus) -> Self {
|
||||
self.status = status;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_show_line_number(mut self, show_line_number: bool) -> Self {
|
||||
self.show_line_number = show_line_number;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_code_action(mut self, code_action: bool) -> Self {
|
||||
self.code_action = code_action;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_current(mut self, current: bool) -> Self {
|
||||
self.current = current;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Clone)]
|
||||
pub struct Buffer {
|
||||
id: ElementId,
|
||||
rows: Option<BufferRows>,
|
||||
readonly: bool,
|
||||
language: Option<String>,
|
||||
title: Option<String>,
|
||||
path: Option<String>,
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
rows: Some(BufferRows::default()),
|
||||
readonly: false,
|
||||
language: None,
|
||||
title: Some("untitled".to_string()),
|
||||
path: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_title<T: Into<Option<String>>>(mut self, title: T) -> Self {
|
||||
self.title = title.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_path<P: Into<Option<String>>>(mut self, path: P) -> Self {
|
||||
self.path = path.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_readonly(mut self, readonly: bool) -> Self {
|
||||
self.readonly = readonly;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_rows<R: Into<Option<BufferRows>>>(mut self, rows: R) -> Self {
|
||||
self.rows = rows.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_language<L: Into<Option<String>>>(mut self, language: L) -> Self {
|
||||
self.language = language.into();
|
||||
self
|
||||
}
|
||||
|
||||
fn render_row<V: 'static>(row: BufferRow, cx: &WindowContext) -> impl Component<V> {
|
||||
let line_background = if row.current {
|
||||
cx.theme().colors().editor_active_line_background
|
||||
} else {
|
||||
cx.theme().styles.system.transparent
|
||||
};
|
||||
|
||||
let line_number_color = if row.current {
|
||||
cx.theme().colors().text
|
||||
} else {
|
||||
cx.theme().syntax_color("comment")
|
||||
};
|
||||
|
||||
h_stack()
|
||||
.bg(line_background)
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.px_1()
|
||||
.child(
|
||||
h_stack()
|
||||
.w_4()
|
||||
.h_full()
|
||||
.px_0p5()
|
||||
.when(row.code_action, |c| {
|
||||
div().child(IconElement::new(Icon::Bolt))
|
||||
}),
|
||||
)
|
||||
.when(row.show_line_number, |this| {
|
||||
this.child(
|
||||
h_stack().justify_end().px_0p5().w_3().child(
|
||||
div()
|
||||
.text_color(line_number_color)
|
||||
.child(row.line_number.to_string()),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(div().mx_0p5().w_1().h_full().bg(row.status.hsla(cx)))
|
||||
.children(row.line.map(|line| {
|
||||
div()
|
||||
.flex()
|
||||
.children(line.highlighted_texts.iter().map(|highlighted_text| {
|
||||
div()
|
||||
.text_color(highlighted_text.color)
|
||||
.child(highlighted_text.text.clone())
|
||||
}))
|
||||
}))
|
||||
}
|
||||
|
||||
fn render_rows<V: 'static>(&self, cx: &WindowContext) -> Vec<impl Component<V>> {
|
||||
match &self.rows {
|
||||
Some(rows) => rows
|
||||
.rows
|
||||
.iter()
|
||||
.map(|row| Self::render_row(row.clone(), cx))
|
||||
.collect(),
|
||||
None => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
let rows = self.render_rows(cx);
|
||||
|
||||
v_stack()
|
||||
.flex_1()
|
||||
.w_full()
|
||||
.h_full()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.children(rows)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use super::*;
|
||||
use crate::{
|
||||
empty_buffer_example, hello_world_rust_buffer_example,
|
||||
hello_world_rust_buffer_with_status_example, Story,
|
||||
};
|
||||
use gpui::{rems, Div, Render};
|
||||
|
||||
pub struct BufferStory;
|
||||
|
||||
impl Render for BufferStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, Buffer>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(div().w(rems(64.)).h_96().child(empty_buffer_example()))
|
||||
.child(Story::label(cx, "Hello World (Rust)"))
|
||||
.child(
|
||||
div()
|
||||
.w(rems(64.))
|
||||
.h_96()
|
||||
.child(hello_world_rust_buffer_example(cx)),
|
||||
)
|
||||
.child(Story::label(cx, "Hello World (Rust) with Status"))
|
||||
.child(
|
||||
div()
|
||||
.w(rems(64.))
|
||||
.h_96()
|
||||
.child(hello_world_rust_buffer_with_status_example(cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
use gpui::{Div, Render, View, VisualContext};
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{h_stack, Icon, IconButton, Input, TextColor};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BufferSearch {
|
||||
is_replace_open: bool,
|
||||
}
|
||||
|
||||
impl BufferSearch {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
is_replace_open: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_replace(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.is_replace_open = !self.is_replace_open;
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn view(cx: &mut WindowContext) -> View<Self> {
|
||||
cx.build_view(|cx| Self::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for BufferSearch {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Div<Self> {
|
||||
h_stack()
|
||||
.bg(cx.theme().colors().toolbar_background)
|
||||
.p_2()
|
||||
.child(
|
||||
h_stack().child(Input::new("Search")).child(
|
||||
IconButton::<Self>::new("replace", Icon::Replace)
|
||||
.when(self.is_replace_open, |this| this.color(TextColor::Accent))
|
||||
.on_click(|buffer_search, cx| {
|
||||
buffer_search.toggle_replace(cx);
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,150 +0,0 @@
|
|||
use crate::{prelude::*, Icon, IconButton, Input, Label};
|
||||
use chrono::NaiveDateTime;
|
||||
use gpui::prelude::*;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct ChatPanel {
|
||||
element_id: ElementId,
|
||||
messages: Vec<ChatMessage>,
|
||||
}
|
||||
|
||||
impl ChatPanel {
|
||||
pub fn new(element_id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
element_id: element_id.into(),
|
||||
messages: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn messages(mut self, messages: Vec<ChatMessage>) -> Self {
|
||||
self.messages = messages;
|
||||
self
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
div()
|
||||
.id(self.element_id.clone())
|
||||
.flex()
|
||||
.flex_col()
|
||||
.justify_between()
|
||||
.h_full()
|
||||
.px_2()
|
||||
.gap_2()
|
||||
// Header
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.justify_between()
|
||||
.py_2()
|
||||
.child(div().flex().child(Label::new("#design")))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_px()
|
||||
.child(IconButton::new("file", Icon::File))
|
||||
.child(IconButton::new("audio_on", Icon::AudioOn)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
// Chat Body
|
||||
.child(
|
||||
div()
|
||||
.id("chat-body")
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.overflow_y_scroll()
|
||||
.children(self.messages),
|
||||
)
|
||||
// Composer
|
||||
.child(div().flex().my_2().child(Input::new("Message #design"))),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct ChatMessage {
|
||||
author: String,
|
||||
text: String,
|
||||
sent_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
impl ChatMessage {
|
||||
pub fn new(author: String, text: String, sent_at: NaiveDateTime) -> Self {
|
||||
Self {
|
||||
author,
|
||||
text,
|
||||
sent_at,
|
||||
}
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_2()
|
||||
.child(Label::new(self.author.clone()))
|
||||
.child(
|
||||
Label::new(self.sent_at.format("%m/%d/%Y").to_string())
|
||||
.color(TextColor::Muted),
|
||||
),
|
||||
)
|
||||
.child(div().child(Label::new(self.text.clone())))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use chrono::DateTime;
|
||||
use gpui::{Div, Render};
|
||||
|
||||
use crate::{Panel, Story};
|
||||
|
||||
use super::*;
|
||||
|
||||
pub struct ChatPanelStory;
|
||||
|
||||
impl Render for ChatPanelStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, ChatPanel>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(
|
||||
Panel::new("chat-panel-1-outer", cx)
|
||||
.child(ChatPanel::new("chat-panel-1-inner")),
|
||||
)
|
||||
.child(Story::label(cx, "With Mesages"))
|
||||
.child(Panel::new("chat-panel-2-outer", cx).child(
|
||||
ChatPanel::new("chat-panel-2-inner").messages(vec![
|
||||
ChatMessage::new(
|
||||
"osiewicz".to_string(),
|
||||
"is this thing on?".to_string(),
|
||||
DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z")
|
||||
.unwrap()
|
||||
.naive_local(),
|
||||
),
|
||||
ChatMessage::new(
|
||||
"maxdeviant".to_string(),
|
||||
"Reading you loud and clear!".to_string(),
|
||||
DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z")
|
||||
.unwrap()
|
||||
.naive_local(),
|
||||
),
|
||||
]),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,110 +0,0 @@
|
|||
use crate::{
|
||||
prelude::*, static_collab_panel_channels, static_collab_panel_current_call, v_stack, Icon,
|
||||
List, ListHeader, Toggle,
|
||||
};
|
||||
use gpui::prelude::*;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct CollabPanel {
|
||||
id: ElementId,
|
||||
}
|
||||
|
||||
impl CollabPanel {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self { id: id.into() }
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
v_stack()
|
||||
.id(self.id.clone())
|
||||
.h_full()
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
.child(
|
||||
v_stack()
|
||||
.id("crdb")
|
||||
.w_full()
|
||||
.overflow_y_scroll()
|
||||
.child(
|
||||
div()
|
||||
.pb_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.border_b()
|
||||
.child(
|
||||
List::new(static_collab_panel_current_call())
|
||||
.header(
|
||||
ListHeader::new("CRDB")
|
||||
.left_icon(Icon::Hash.into())
|
||||
.toggle(Toggle::Toggled(true)),
|
||||
)
|
||||
.toggle(Toggle::Toggled(true)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_stack().id("channels").py_1().child(
|
||||
List::new(static_collab_panel_channels())
|
||||
.header(ListHeader::new("CHANNELS").toggle(Toggle::Toggled(true)))
|
||||
.empty_message("No channels yet. Add a channel to get started.")
|
||||
.toggle(Toggle::Toggled(true)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_stack().id("contacts-online").py_1().child(
|
||||
List::new(static_collab_panel_current_call())
|
||||
.header(
|
||||
ListHeader::new("CONTACTS – ONLINE")
|
||||
.toggle(Toggle::Toggled(true)),
|
||||
)
|
||||
.toggle(Toggle::Toggled(true)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_stack().id("contacts-offline").py_1().child(
|
||||
List::new(static_collab_panel_current_call())
|
||||
.header(
|
||||
ListHeader::new("CONTACTS – OFFLINE")
|
||||
.toggle(Toggle::Toggled(false)),
|
||||
)
|
||||
.toggle(Toggle::Toggled(false)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.h_7()
|
||||
.px_2()
|
||||
.border_t()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.flex()
|
||||
.items_center()
|
||||
.child(
|
||||
div()
|
||||
.text_ui_sm()
|
||||
.text_color(cx.theme().colors().text_placeholder)
|
||||
.child("Find..."),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use super::*;
|
||||
use crate::Story;
|
||||
use gpui::{Div, Render};
|
||||
|
||||
pub struct CollabPanelStory;
|
||||
|
||||
impl Render for CollabPanelStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, CollabPanel>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(CollabPanel::new("collab-panel"))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
use crate::prelude::*;
|
||||
use crate::{example_editor_actions, OrderMethod, Palette};
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct CommandPalette {
|
||||
id: ElementId,
|
||||
}
|
||||
|
||||
impl CommandPalette {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self { id: id.into() }
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
div().id(self.id.clone()).child(
|
||||
Palette::new("palette")
|
||||
.items(example_editor_actions())
|
||||
.placeholder("Execute a command...")
|
||||
.empty_string("No items found.")
|
||||
.default_order(OrderMethod::Ascending),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use gpui::{Div, Render};
|
||||
|
||||
use crate::Story;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub struct CommandPaletteStory;
|
||||
|
||||
impl Render for CommandPaletteStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, CommandPalette>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(CommandPalette::new("command-palette"))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
use crate::{prelude::*, Button, Label, Modal, TextColor};
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct CopilotModal {
|
||||
id: ElementId,
|
||||
}
|
||||
|
||||
impl CopilotModal {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self { id: id.into() }
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
div().id(self.id.clone()).child(
|
||||
Modal::new("some-id")
|
||||
.title("Connect Copilot to Zed")
|
||||
.child(Label::new("You can update your settings or sign out from the Copilot menu in the status bar.").color(TextColor::Muted))
|
||||
.primary_action(Button::new("Connect to Github").variant(ButtonVariant::Filled)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use gpui::{Div, Render};
|
||||
|
||||
use crate::Story;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub struct CopilotModalStory;
|
||||
|
||||
impl Render for CopilotModalStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, CopilotModal>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(CopilotModal::new("copilot-modal"))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use gpui::{Div, Render, View, VisualContext};
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{
|
||||
hello_world_rust_editor_with_status_example, v_stack, Breadcrumb, Buffer, BufferSearch, Icon,
|
||||
IconButton, Symbol, Tab, TabBar, TextColor, Toolbar,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EditorPane {
|
||||
tabs: Vec<Tab>,
|
||||
path: PathBuf,
|
||||
symbols: Vec<Symbol>,
|
||||
buffer: Buffer,
|
||||
buffer_search: View<BufferSearch>,
|
||||
is_buffer_search_open: bool,
|
||||
}
|
||||
|
||||
impl EditorPane {
|
||||
pub fn new(
|
||||
cx: &mut ViewContext<Self>,
|
||||
tabs: Vec<Tab>,
|
||||
path: PathBuf,
|
||||
symbols: Vec<Symbol>,
|
||||
buffer: Buffer,
|
||||
) -> Self {
|
||||
Self {
|
||||
tabs,
|
||||
path,
|
||||
symbols,
|
||||
buffer,
|
||||
buffer_search: BufferSearch::view(cx),
|
||||
is_buffer_search_open: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_buffer_search(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.is_buffer_search_open = !self.is_buffer_search_open;
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn view(cx: &mut WindowContext) -> View<Self> {
|
||||
cx.build_view(|cx| hello_world_rust_editor_with_status_example(cx))
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for EditorPane {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Div<Self> {
|
||||
v_stack()
|
||||
.w_full()
|
||||
.h_full()
|
||||
.flex_1()
|
||||
.child(TabBar::new("editor-pane-tabs", self.tabs.clone()).can_navigate((false, true)))
|
||||
.child(
|
||||
Toolbar::new()
|
||||
.left_item(Breadcrumb::new(self.path.clone(), self.symbols.clone()))
|
||||
.right_items(vec![
|
||||
IconButton::<Self>::new("toggle_inlay_hints", Icon::InlayHint),
|
||||
IconButton::<Self>::new("buffer_search", Icon::MagnifyingGlass)
|
||||
.when(self.is_buffer_search_open, |this| {
|
||||
this.color(TextColor::Accent)
|
||||
})
|
||||
.on_click(|editor: &mut Self, cx| {
|
||||
editor.toggle_buffer_search(cx);
|
||||
}),
|
||||
IconButton::new("inline_assist", Icon::MagicWand),
|
||||
]),
|
||||
)
|
||||
.children(Some(self.buffer_search.clone()).filter(|_| self.is_buffer_search_open))
|
||||
.child(self.buffer.clone())
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
use crate::prelude::*;
|
||||
use crate::{OrderMethod, Palette, PaletteItem};
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct LanguageSelector {
|
||||
id: ElementId,
|
||||
}
|
||||
|
||||
impl LanguageSelector {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self { id: id.into() }
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
div().id(self.id.clone()).child(
|
||||
Palette::new("palette")
|
||||
.items(vec![
|
||||
PaletteItem::new("C"),
|
||||
PaletteItem::new("C++"),
|
||||
PaletteItem::new("CSS"),
|
||||
PaletteItem::new("Elixir"),
|
||||
PaletteItem::new("Elm"),
|
||||
PaletteItem::new("ERB"),
|
||||
PaletteItem::new("Rust (current)"),
|
||||
PaletteItem::new("Scheme"),
|
||||
PaletteItem::new("TOML"),
|
||||
PaletteItem::new("TypeScript"),
|
||||
])
|
||||
.placeholder("Select a language...")
|
||||
.empty_string("No matches")
|
||||
.default_order(OrderMethod::Ascending),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use super::*;
|
||||
use crate::Story;
|
||||
use gpui::{Div, Render};
|
||||
|
||||
pub struct LanguageSelectorStory;
|
||||
|
||||
impl Render for LanguageSelectorStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, LanguageSelector>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(LanguageSelector::new("language-selector"))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
use crate::prelude::*;
|
||||
use crate::{v_stack, Buffer, Icon, IconButton, Label};
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct MultiBuffer {
|
||||
buffers: Vec<Buffer>,
|
||||
}
|
||||
|
||||
impl MultiBuffer {
|
||||
pub fn new(buffers: Vec<Buffer>) -> Self {
|
||||
Self { buffers }
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
v_stack()
|
||||
.w_full()
|
||||
.h_full()
|
||||
.flex_1()
|
||||
.children(self.buffers.clone().into_iter().map(|buffer| {
|
||||
v_stack()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.p_4()
|
||||
.bg(cx.theme().colors().editor_subheader_background)
|
||||
.child(Label::new("main.rs"))
|
||||
.child(IconButton::new("arrow_up_right", Icon::ArrowUpRight)),
|
||||
)
|
||||
.child(buffer)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use super::*;
|
||||
use crate::{hello_world_rust_buffer_example, Story};
|
||||
use gpui::{Div, Render};
|
||||
|
||||
pub struct MultiBufferStory;
|
||||
|
||||
impl Render for MultiBufferStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, MultiBuffer>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(MultiBuffer::new(vec![
|
||||
hello_world_rust_buffer_example(cx),
|
||||
hello_world_rust_buffer_example(cx),
|
||||
hello_world_rust_buffer_example(cx),
|
||||
hello_world_rust_buffer_example(cx),
|
||||
hello_world_rust_buffer_example(cx),
|
||||
]))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,371 +0,0 @@
|
|||
use crate::{
|
||||
h_stack, prelude::*, static_new_notification_items_2, utils::naive_format_distance_from_now,
|
||||
v_stack, Avatar, ButtonOrIconButton, ClickHandler, Icon, IconElement, Label, LineHeightStyle,
|
||||
ListHeader, ListHeaderMeta, ListSeparator, PublicPlayer, TextColor, UnreadIndicator,
|
||||
};
|
||||
use gpui::prelude::*;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct NotificationsPanel {
|
||||
id: ElementId,
|
||||
}
|
||||
|
||||
impl NotificationsPanel {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self { id: id.into() }
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
div()
|
||||
.id(self.id.clone())
|
||||
.flex()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
.child(
|
||||
ListHeader::new("Notifications").meta(Some(ListHeaderMeta::Tools(vec![
|
||||
Icon::AtSign,
|
||||
Icon::BellOff,
|
||||
Icon::MailOpen,
|
||||
]))),
|
||||
)
|
||||
.child(ListSeparator::new())
|
||||
.child(
|
||||
v_stack()
|
||||
.id("notifications-panel-scroll-view")
|
||||
.py_1()
|
||||
.overflow_y_scroll()
|
||||
.flex_1()
|
||||
.child(
|
||||
div()
|
||||
.mx_2()
|
||||
.p_1()
|
||||
// TODO: Add cursor style
|
||||
// .cursor(Cursor::IBeam)
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.border()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(
|
||||
Label::new("Search...")
|
||||
.color(TextColor::Placeholder)
|
||||
.line_height_style(LineHeightStyle::UILabel),
|
||||
),
|
||||
)
|
||||
.child(v_stack().px_1().children(static_new_notification_items_2())),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NotificationAction<V: 'static> {
|
||||
button: ButtonOrIconButton<V>,
|
||||
tooltip: SharedString,
|
||||
/// Shows after action is chosen
|
||||
///
|
||||
/// For example, if the action is "Accept" the taken message could be:
|
||||
///
|
||||
/// - `(None,"Accepted")` - "Accepted"
|
||||
///
|
||||
/// - `(Some(Icon::Check),"Accepted")` - ✓ "Accepted"
|
||||
taken_message: (Option<Icon>, SharedString),
|
||||
}
|
||||
|
||||
impl<V: 'static> NotificationAction<V> {
|
||||
pub fn new(
|
||||
button: impl Into<ButtonOrIconButton<V>>,
|
||||
tooltip: impl Into<SharedString>,
|
||||
(icon, taken_message): (Option<Icon>, impl Into<SharedString>),
|
||||
) -> Self {
|
||||
Self {
|
||||
button: button.into(),
|
||||
tooltip: tooltip.into(),
|
||||
taken_message: (icon, taken_message.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ActorOrIcon {
|
||||
Actor(PublicPlayer),
|
||||
Icon(Icon),
|
||||
}
|
||||
|
||||
pub struct NotificationMeta<V: 'static> {
|
||||
items: Vec<(Option<Icon>, SharedString, Option<ClickHandler<V>>)>,
|
||||
}
|
||||
|
||||
struct NotificationHandlers<V: 'static> {
|
||||
click: Option<ClickHandler<V>>,
|
||||
}
|
||||
|
||||
impl<V: 'static> Default for NotificationHandlers<V> {
|
||||
fn default() -> Self {
|
||||
Self { click: None }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct Notification<V: 'static> {
|
||||
id: ElementId,
|
||||
slot: ActorOrIcon,
|
||||
message: SharedString,
|
||||
date_received: NaiveDateTime,
|
||||
meta: Option<NotificationMeta<V>>,
|
||||
actions: Option<[NotificationAction<V>; 2]>,
|
||||
unread: bool,
|
||||
new: bool,
|
||||
action_taken: Option<NotificationAction<V>>,
|
||||
handlers: NotificationHandlers<V>,
|
||||
}
|
||||
|
||||
impl<V> Notification<V> {
|
||||
fn new(
|
||||
id: ElementId,
|
||||
message: SharedString,
|
||||
date_received: NaiveDateTime,
|
||||
slot: ActorOrIcon,
|
||||
click_action: Option<ClickHandler<V>>,
|
||||
) -> Self {
|
||||
let handlers = if click_action.is_some() {
|
||||
NotificationHandlers {
|
||||
click: click_action,
|
||||
}
|
||||
} else {
|
||||
NotificationHandlers::default()
|
||||
};
|
||||
|
||||
Self {
|
||||
id,
|
||||
date_received,
|
||||
message,
|
||||
meta: None,
|
||||
slot,
|
||||
actions: None,
|
||||
unread: true,
|
||||
new: false,
|
||||
action_taken: None,
|
||||
handlers,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new notification with an actor slot.
|
||||
///
|
||||
/// Requires a click action.
|
||||
pub fn new_actor_message(
|
||||
id: impl Into<ElementId>,
|
||||
message: impl Into<SharedString>,
|
||||
date_received: NaiveDateTime,
|
||||
actor: PublicPlayer,
|
||||
click_action: ClickHandler<V>,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
id.into(),
|
||||
message.into(),
|
||||
date_received,
|
||||
ActorOrIcon::Actor(actor),
|
||||
Some(click_action),
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a new notification with an icon slot.
|
||||
///
|
||||
/// Requires a click action.
|
||||
pub fn new_icon_message(
|
||||
id: impl Into<ElementId>,
|
||||
message: impl Into<SharedString>,
|
||||
date_received: NaiveDateTime,
|
||||
icon: Icon,
|
||||
click_action: ClickHandler<V>,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
id.into(),
|
||||
message.into(),
|
||||
date_received,
|
||||
ActorOrIcon::Icon(icon),
|
||||
Some(click_action),
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a new notification with an actor slot
|
||||
/// and a Call To Action row.
|
||||
///
|
||||
/// Cannot take a click action due to required actions.
|
||||
pub fn new_actor_with_actions(
|
||||
id: impl Into<ElementId>,
|
||||
message: impl Into<SharedString>,
|
||||
date_received: NaiveDateTime,
|
||||
actor: PublicPlayer,
|
||||
actions: [NotificationAction<V>; 2],
|
||||
) -> Self {
|
||||
Self::new(
|
||||
id.into(),
|
||||
message.into(),
|
||||
date_received,
|
||||
ActorOrIcon::Actor(actor),
|
||||
None,
|
||||
)
|
||||
.actions(actions)
|
||||
}
|
||||
|
||||
/// Creates a new notification with an icon slot
|
||||
/// and a Call To Action row.
|
||||
///
|
||||
/// Cannot take a click action due to required actions.
|
||||
pub fn new_icon_with_actions(
|
||||
id: impl Into<ElementId>,
|
||||
message: impl Into<SharedString>,
|
||||
date_received: NaiveDateTime,
|
||||
icon: Icon,
|
||||
actions: [NotificationAction<V>; 2],
|
||||
) -> Self {
|
||||
Self::new(
|
||||
id.into(),
|
||||
message.into(),
|
||||
date_received,
|
||||
ActorOrIcon::Icon(icon),
|
||||
None,
|
||||
)
|
||||
.actions(actions)
|
||||
}
|
||||
|
||||
fn on_click(mut self, handler: ClickHandler<V>) -> Self {
|
||||
self.handlers.click = Some(handler);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn actions(mut self, actions: [NotificationAction<V>; 2]) -> Self {
|
||||
self.actions = Some(actions);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn meta(mut self, meta: NotificationMeta<V>) -> Self {
|
||||
self.meta = Some(meta);
|
||||
self
|
||||
}
|
||||
|
||||
fn render_meta_items(&self, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
if let Some(meta) = &self.meta {
|
||||
h_stack().children(
|
||||
meta.items
|
||||
.iter()
|
||||
.map(|(icon, text, _)| {
|
||||
let mut meta_el = div();
|
||||
if let Some(icon) = icon {
|
||||
meta_el = meta_el.child(IconElement::new(icon.clone()));
|
||||
}
|
||||
meta_el.child(Label::new(text.clone()).color(TextColor::Muted))
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
} else {
|
||||
div()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_slot(&self, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
match &self.slot {
|
||||
ActorOrIcon::Actor(actor) => Avatar::new(actor.avatar.clone()).render(),
|
||||
ActorOrIcon::Icon(icon) => IconElement::new(icon.clone()).render(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
div()
|
||||
.relative()
|
||||
.id(self.id.clone())
|
||||
.p_1()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.w_full()
|
||||
.children(
|
||||
Some(
|
||||
div()
|
||||
.absolute()
|
||||
.left(px(3.0))
|
||||
.top_3()
|
||||
.z_index(2)
|
||||
.child(UnreadIndicator::new()),
|
||||
)
|
||||
.filter(|_| self.unread),
|
||||
)
|
||||
.child(
|
||||
v_stack()
|
||||
.z_index(1)
|
||||
.gap_1()
|
||||
.w_full()
|
||||
.child(
|
||||
h_stack()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.child(self.render_slot(cx))
|
||||
.child(div().flex_1().child(Label::new(self.message.clone()))),
|
||||
)
|
||||
.child(
|
||||
h_stack()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new(naive_format_distance_from_now(
|
||||
self.date_received,
|
||||
true,
|
||||
true,
|
||||
))
|
||||
.color(TextColor::Muted),
|
||||
)
|
||||
.child(self.render_meta_items(cx)),
|
||||
)
|
||||
.child(match (self.actions, self.action_taken) {
|
||||
// Show nothing
|
||||
(None, _) => div(),
|
||||
// Show the taken_message
|
||||
(Some(_), Some(action_taken)) => h_stack()
|
||||
.children(action_taken.taken_message.0.map(|icon| {
|
||||
IconElement::new(icon).color(crate::TextColor::Muted)
|
||||
}))
|
||||
.child(
|
||||
Label::new(action_taken.taken_message.1.clone())
|
||||
.color(TextColor::Muted),
|
||||
),
|
||||
// Show the actions
|
||||
(Some(actions), None) => {
|
||||
h_stack().children(actions.map(|action| match action.button {
|
||||
ButtonOrIconButton::Button(button) => {
|
||||
Component::render(button)
|
||||
}
|
||||
ButtonOrIconButton::IconButton(icon_button) => {
|
||||
Component::render(icon_button)
|
||||
}
|
||||
}))
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
use chrono::NaiveDateTime;
|
||||
use gpui::{px, Styled};
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use super::*;
|
||||
use crate::{Panel, Story};
|
||||
use gpui::{Div, Render};
|
||||
|
||||
pub struct NotificationsPanelStory;
|
||||
|
||||
impl Render for NotificationsPanelStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, NotificationsPanel>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(
|
||||
Panel::new("panel", cx).child(NotificationsPanel::new("notifications_panel")),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,128 +0,0 @@
|
|||
use gpui::{hsla, red, AnyElement, ElementId, ExternalPaths, Hsla, Length, Size, View};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Default, PartialEq)]
|
||||
pub enum SplitDirection {
|
||||
#[default]
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct Pane<V: 'static> {
|
||||
id: ElementId,
|
||||
size: Size<Length>,
|
||||
fill: Hsla,
|
||||
children: SmallVec<[AnyElement<V>; 2]>,
|
||||
}
|
||||
|
||||
impl<V: 'static> Pane<V> {
|
||||
pub fn new(id: impl Into<ElementId>, size: Size<Length>) -> Self {
|
||||
// Fill is only here for debugging purposes, remove before release
|
||||
|
||||
Self {
|
||||
id: id.into(),
|
||||
size,
|
||||
fill: hsla(0.3, 0.3, 0.3, 1.),
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fill(mut self, fill: Hsla) -> Self {
|
||||
self.fill = fill;
|
||||
self
|
||||
}
|
||||
|
||||
fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
div()
|
||||
.id(self.id.clone())
|
||||
.flex()
|
||||
.flex_initial()
|
||||
.bg(self.fill)
|
||||
.w(self.size.width)
|
||||
.h(self.size.height)
|
||||
.relative()
|
||||
.child(div().z_index(0).size_full().children(self.children))
|
||||
.child(
|
||||
div()
|
||||
.z_index(1)
|
||||
.id("drag-target")
|
||||
.drag_over::<ExternalPaths>(|d| d.bg(red()))
|
||||
.on_drop(|_, files: View<ExternalPaths>, cx| {
|
||||
eprintln!("dropped files! {:?}", files.read(cx));
|
||||
})
|
||||
.absolute()
|
||||
.inset_0(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> ParentComponent<V> for Pane<V> {
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
|
||||
&mut self.children
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct PaneGroup<V: 'static> {
|
||||
groups: Vec<PaneGroup<V>>,
|
||||
panes: Vec<Pane<V>>,
|
||||
split_direction: SplitDirection,
|
||||
}
|
||||
|
||||
impl<V: 'static> PaneGroup<V> {
|
||||
pub fn new_groups(groups: Vec<PaneGroup<V>>, split_direction: SplitDirection) -> Self {
|
||||
Self {
|
||||
groups,
|
||||
panes: Vec::new(),
|
||||
split_direction,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_panes(panes: Vec<Pane<V>>, split_direction: SplitDirection) -> Self {
|
||||
Self {
|
||||
groups: Vec::new(),
|
||||
panes,
|
||||
split_direction,
|
||||
}
|
||||
}
|
||||
|
||||
fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
if !self.panes.is_empty() {
|
||||
let el = div()
|
||||
.flex()
|
||||
.flex_1()
|
||||
.gap_px()
|
||||
.w_full()
|
||||
.h_full()
|
||||
.children(self.panes.into_iter().map(|pane| pane.render(view, cx)));
|
||||
|
||||
if self.split_direction == SplitDirection::Horizontal {
|
||||
return el;
|
||||
} else {
|
||||
return el.flex_col();
|
||||
}
|
||||
}
|
||||
|
||||
if !self.groups.is_empty() {
|
||||
let el = div()
|
||||
.flex()
|
||||
.flex_1()
|
||||
.gap_px()
|
||||
.w_full()
|
||||
.h_full()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.children(self.groups.into_iter().map(|group| group.render(view, cx)));
|
||||
|
||||
if self.split_direction == SplitDirection::Horizontal {
|
||||
return el;
|
||||
} else {
|
||||
return el.flex_col();
|
||||
}
|
||||
}
|
||||
|
||||
unreachable!()
|
||||
}
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
use crate::{
|
||||
prelude::*, static_project_panel_project_items, static_project_panel_single_items, Input, List,
|
||||
ListHeader,
|
||||
};
|
||||
use gpui::prelude::*;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct ProjectPanel {
|
||||
id: ElementId,
|
||||
}
|
||||
|
||||
impl ProjectPanel {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self { id: id.into() }
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
div()
|
||||
.id(self.id.clone())
|
||||
.flex()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
.child(
|
||||
div()
|
||||
.id("project-panel-contents")
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.overflow_y_scroll()
|
||||
.child(
|
||||
List::new(static_project_panel_single_items())
|
||||
.header(ListHeader::new("FILES"))
|
||||
.empty_message("No files in directory"),
|
||||
)
|
||||
.child(
|
||||
List::new(static_project_panel_project_items())
|
||||
.header(ListHeader::new("PROJECT"))
|
||||
.empty_message("No folders in directory"),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Input::new("Find something...")
|
||||
.value("buffe".to_string())
|
||||
.state(InteractionState::Focused),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
use gpui::ElementId;
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use super::*;
|
||||
use crate::{Panel, Story};
|
||||
use gpui::{Div, Render};
|
||||
|
||||
pub struct ProjectPanelStory;
|
||||
|
||||
impl Render for ProjectPanelStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, ProjectPanel>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(
|
||||
Panel::new("project-panel-outer", cx)
|
||||
.child(ProjectPanel::new("project-panel-inner")),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
use crate::prelude::*;
|
||||
use crate::{OrderMethod, Palette, PaletteItem};
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct RecentProjects {
|
||||
id: ElementId,
|
||||
}
|
||||
|
||||
impl RecentProjects {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self { id: id.into() }
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
div().id(self.id.clone()).child(
|
||||
Palette::new("palette")
|
||||
.items(vec![
|
||||
PaletteItem::new("zed").sublabel(SharedString::from("~/projects/zed")),
|
||||
PaletteItem::new("saga").sublabel(SharedString::from("~/projects/saga")),
|
||||
PaletteItem::new("journal").sublabel(SharedString::from("~/journal")),
|
||||
PaletteItem::new("dotfiles").sublabel(SharedString::from("~/dotfiles")),
|
||||
PaletteItem::new("zed.dev").sublabel(SharedString::from("~/projects/zed.dev")),
|
||||
PaletteItem::new("laminar").sublabel(SharedString::from("~/projects/laminar")),
|
||||
])
|
||||
.placeholder("Recent Projects...")
|
||||
.empty_string("No matches")
|
||||
.default_order(OrderMethod::Ascending),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use super::*;
|
||||
use crate::Story;
|
||||
use gpui::{Div, Render};
|
||||
|
||||
pub struct RecentProjectsStory;
|
||||
|
||||
impl Render for RecentProjectsStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, RecentProjects>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(RecentProjects::new("recent-projects"))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,203 +0,0 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{Button, Icon, IconButton, TextColor, ToolDivider, Workspace};
|
||||
|
||||
#[derive(Default, PartialEq)]
|
||||
pub enum Tool {
|
||||
#[default]
|
||||
ProjectPanel,
|
||||
CollaborationPanel,
|
||||
Terminal,
|
||||
Assistant,
|
||||
Feedback,
|
||||
Diagnostics,
|
||||
}
|
||||
|
||||
struct ToolGroup {
|
||||
active_index: Option<usize>,
|
||||
tools: Vec<Tool>,
|
||||
}
|
||||
|
||||
impl Default for ToolGroup {
|
||||
fn default() -> Self {
|
||||
ToolGroup {
|
||||
active_index: None,
|
||||
tools: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
#[component(view_type = "Workspace")]
|
||||
pub struct StatusBar {
|
||||
left_tools: Option<ToolGroup>,
|
||||
right_tools: Option<ToolGroup>,
|
||||
bottom_tools: Option<ToolGroup>,
|
||||
}
|
||||
|
||||
impl StatusBar {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
left_tools: None,
|
||||
right_tools: None,
|
||||
bottom_tools: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn left_tool(mut self, tool: Tool, active_index: Option<usize>) -> Self {
|
||||
self.left_tools = {
|
||||
let mut tools = vec![tool];
|
||||
tools.extend(self.left_tools.take().unwrap_or_default().tools);
|
||||
Some(ToolGroup {
|
||||
active_index,
|
||||
tools,
|
||||
})
|
||||
};
|
||||
self
|
||||
}
|
||||
|
||||
pub fn right_tool(mut self, tool: Tool, active_index: Option<usize>) -> Self {
|
||||
self.right_tools = {
|
||||
let mut tools = vec![tool];
|
||||
tools.extend(self.left_tools.take().unwrap_or_default().tools);
|
||||
Some(ToolGroup {
|
||||
active_index,
|
||||
tools,
|
||||
})
|
||||
};
|
||||
self
|
||||
}
|
||||
|
||||
pub fn bottom_tool(mut self, tool: Tool, active_index: Option<usize>) -> Self {
|
||||
self.bottom_tools = {
|
||||
let mut tools = vec![tool];
|
||||
tools.extend(self.left_tools.take().unwrap_or_default().tools);
|
||||
Some(ToolGroup {
|
||||
active_index,
|
||||
tools,
|
||||
})
|
||||
};
|
||||
self
|
||||
}
|
||||
|
||||
fn render(
|
||||
self,
|
||||
view: &mut Workspace,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> impl Component<Workspace> {
|
||||
div()
|
||||
.py_0p5()
|
||||
.px_1()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.bg(cx.theme().colors().status_bar_background)
|
||||
.child(self.left_tools(view, cx))
|
||||
.child(self.right_tools(view, cx))
|
||||
}
|
||||
|
||||
fn left_tools(
|
||||
&self,
|
||||
workspace: &mut Workspace,
|
||||
cx: &WindowContext,
|
||||
) -> impl Component<Workspace> {
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(
|
||||
IconButton::<Workspace>::new("project_panel", Icon::FileTree)
|
||||
.when(workspace.is_project_panel_open(), |this| {
|
||||
this.color(TextColor::Accent)
|
||||
})
|
||||
.on_click(|workspace: &mut Workspace, cx| {
|
||||
workspace.toggle_project_panel(cx);
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::<Workspace>::new("collab_panel", Icon::Hash)
|
||||
.when(workspace.is_collab_panel_open(), |this| {
|
||||
this.color(TextColor::Accent)
|
||||
})
|
||||
.on_click(|workspace: &mut Workspace, cx| {
|
||||
workspace.toggle_collab_panel();
|
||||
}),
|
||||
)
|
||||
.child(ToolDivider::new())
|
||||
.child(IconButton::new("diagnostics", Icon::XCircle))
|
||||
}
|
||||
|
||||
fn right_tools(
|
||||
&self,
|
||||
workspace: &mut Workspace,
|
||||
cx: &WindowContext,
|
||||
) -> impl Component<Workspace> {
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(Button::new("116:25"))
|
||||
.child(
|
||||
Button::<Workspace>::new("Rust").on_click(Arc::new(|workspace, cx| {
|
||||
workspace.toggle_language_selector(cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(ToolDivider::new())
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(
|
||||
IconButton::new("copilot", Icon::Copilot)
|
||||
.on_click(|_, _| println!("Copilot clicked.")),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("envelope", Icon::Envelope)
|
||||
.on_click(|_, _| println!("Send Feedback clicked.")),
|
||||
),
|
||||
)
|
||||
.child(ToolDivider::new())
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(
|
||||
IconButton::<Workspace>::new("terminal", Icon::Terminal)
|
||||
.when(workspace.is_terminal_open(), |this| {
|
||||
this.color(TextColor::Accent)
|
||||
})
|
||||
.on_click(|workspace: &mut Workspace, cx| {
|
||||
workspace.toggle_terminal(cx);
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::<Workspace>::new("chat_panel", Icon::MessageBubbles)
|
||||
.when(workspace.is_chat_panel_open(), |this| {
|
||||
this.color(TextColor::Accent)
|
||||
})
|
||||
.on_click(|workspace: &mut Workspace, cx| {
|
||||
workspace.toggle_chat_panel(cx);
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::<Workspace>::new("assistant_panel", Icon::Ai)
|
||||
.when(workspace.is_assistant_panel_open(), |this| {
|
||||
this.color(TextColor::Accent)
|
||||
})
|
||||
.on_click(|workspace: &mut Workspace, cx| {
|
||||
workspace.toggle_assistant_panel(cx);
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,150 +0,0 @@
|
|||
use crate::{prelude::*, Icon, IconButton, Tab};
|
||||
use gpui::prelude::*;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct TabBar {
|
||||
id: ElementId,
|
||||
/// Backwards, Forwards
|
||||
can_navigate: (bool, bool),
|
||||
tabs: Vec<Tab>,
|
||||
}
|
||||
|
||||
impl TabBar {
|
||||
pub fn new(id: impl Into<ElementId>, tabs: Vec<Tab>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
can_navigate: (false, false),
|
||||
tabs,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_navigate(mut self, can_navigate: (bool, bool)) -> Self {
|
||||
self.can_navigate = can_navigate;
|
||||
self
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
let (can_navigate_back, can_navigate_forward) = self.can_navigate;
|
||||
|
||||
div()
|
||||
.group("tab_bar")
|
||||
.id(self.id.clone())
|
||||
.w_full()
|
||||
.flex()
|
||||
.bg(cx.theme().colors().tab_bar_background)
|
||||
// Left Side
|
||||
.child(
|
||||
div()
|
||||
.relative()
|
||||
.px_1()
|
||||
.flex()
|
||||
.flex_none()
|
||||
.gap_2()
|
||||
// Nav Buttons
|
||||
.child(
|
||||
div()
|
||||
.right_0()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_px()
|
||||
.child(
|
||||
IconButton::new("arrow_left", Icon::ArrowLeft)
|
||||
.state(InteractionState::Enabled.if_enabled(can_navigate_back)),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("arrow_right", Icon::ArrowRight).state(
|
||||
InteractionState::Enabled.if_enabled(can_navigate_forward),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().w_0().flex_1().h_full().child(
|
||||
div()
|
||||
.id("tabs")
|
||||
.flex()
|
||||
.overflow_x_scroll()
|
||||
.children(self.tabs.clone()),
|
||||
),
|
||||
)
|
||||
// Right Side
|
||||
.child(
|
||||
div()
|
||||
// We only use absolute here since we don't
|
||||
// have opacity or `hidden()` yet
|
||||
.absolute()
|
||||
.neg_top_7()
|
||||
.px_1()
|
||||
.flex()
|
||||
.flex_none()
|
||||
.gap_2()
|
||||
.group_hover("tab_bar", |this| this.top_0())
|
||||
// Nav Buttons
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_px()
|
||||
.child(IconButton::new("plus", Icon::Plus))
|
||||
.child(IconButton::new("split", Icon::Split)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
use gpui::ElementId;
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use super::*;
|
||||
use crate::Story;
|
||||
use gpui::{Div, Render};
|
||||
|
||||
pub struct TabBarStory;
|
||||
|
||||
impl Render for TabBarStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, TabBar>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(TabBar::new(
|
||||
"tab-bar",
|
||||
vec![
|
||||
Tab::new(1)
|
||||
.title("Cargo.toml".to_string())
|
||||
.current(false)
|
||||
.git_status(GitStatus::Modified),
|
||||
Tab::new(2)
|
||||
.title("Channels Panel".to_string())
|
||||
.current(false),
|
||||
Tab::new(3)
|
||||
.title("channels_panel.rs".to_string())
|
||||
.current(true)
|
||||
.git_status(GitStatus::Modified),
|
||||
Tab::new(4)
|
||||
.title("workspace.rs".to_string())
|
||||
.current(false)
|
||||
.git_status(GitStatus::Modified),
|
||||
Tab::new(5)
|
||||
.title("icon_button.rs".to_string())
|
||||
.current(false),
|
||||
Tab::new(6)
|
||||
.title("storybook.rs".to_string())
|
||||
.current(false)
|
||||
.git_status(GitStatus::Created),
|
||||
Tab::new(7).title("theme.rs".to_string()).current(false),
|
||||
Tab::new(8)
|
||||
.title("theme_registry.rs".to_string())
|
||||
.current(false),
|
||||
Tab::new(9)
|
||||
.title("styleable_helpers.rs".to_string())
|
||||
.current(false),
|
||||
],
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
use gpui::{relative, rems, Size};
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{Icon, IconButton, Pane, Tab};
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct Terminal;
|
||||
|
||||
impl Terminal {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
let can_navigate_back = true;
|
||||
let can_navigate_forward = false;
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.w_full()
|
||||
.child(
|
||||
// Terminal Tabs.
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
.child(
|
||||
div().px_1().flex().flex_none().gap_2().child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_px()
|
||||
.child(
|
||||
IconButton::new("arrow_left", Icon::ArrowLeft).state(
|
||||
InteractionState::Enabled.if_enabled(can_navigate_back),
|
||||
),
|
||||
)
|
||||
.child(IconButton::new("arrow_right", Icon::ArrowRight).state(
|
||||
InteractionState::Enabled.if_enabled(can_navigate_forward),
|
||||
)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().w_0().flex_1().h_full().child(
|
||||
div()
|
||||
.flex()
|
||||
.child(
|
||||
Tab::new(1)
|
||||
.title("zed — fish".to_string())
|
||||
.icon(Icon::Terminal)
|
||||
.close_side(IconSide::Right)
|
||||
.current(true),
|
||||
)
|
||||
.child(
|
||||
Tab::new(2)
|
||||
.title("zed — fish".to_string())
|
||||
.icon(Icon::Terminal)
|
||||
.close_side(IconSide::Right)
|
||||
.current(false),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
// Terminal Pane.
|
||||
.child(
|
||||
Pane::new(
|
||||
"terminal",
|
||||
Size {
|
||||
width: relative(1.).into(),
|
||||
height: rems(36.).into(),
|
||||
},
|
||||
)
|
||||
.child(crate::static_data::terminal_buffer(cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use super::*;
|
||||
use crate::Story;
|
||||
use gpui::{Div, Render};
|
||||
pub struct TerminalStory;
|
||||
|
||||
impl Render for TerminalStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, Terminal>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(Terminal::new())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
use crate::prelude::*;
|
||||
use crate::{OrderMethod, Palette, PaletteItem};
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct ThemeSelector {
|
||||
id: ElementId,
|
||||
}
|
||||
|
||||
impl ThemeSelector {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self { id: id.into() }
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
div().child(
|
||||
Palette::new(self.id.clone())
|
||||
.items(vec![
|
||||
PaletteItem::new("One Dark"),
|
||||
PaletteItem::new("Rosé Pine"),
|
||||
PaletteItem::new("Rosé Pine Moon"),
|
||||
PaletteItem::new("Sandcastle"),
|
||||
PaletteItem::new("Solarized Dark"),
|
||||
PaletteItem::new("Summercamp"),
|
||||
PaletteItem::new("Atelier Cave Light"),
|
||||
PaletteItem::new("Atelier Dune Light"),
|
||||
PaletteItem::new("Atelier Estuary Light"),
|
||||
PaletteItem::new("Atelier Forest Light"),
|
||||
PaletteItem::new("Atelier Heath Light"),
|
||||
])
|
||||
.placeholder("Select Theme...")
|
||||
.empty_string("No matches")
|
||||
.default_order(OrderMethod::Ascending),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use gpui::{Div, Render};
|
||||
|
||||
use crate::Story;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub struct ThemeSelectorStory;
|
||||
|
||||
impl Render for ThemeSelectorStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, ThemeSelector>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(ThemeSelector::new("theme-selector"))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,218 +0,0 @@
|
|||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::{Div, Render, View, VisualContext};
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::settings::user_settings;
|
||||
use crate::{
|
||||
Avatar, Button, Icon, IconButton, MicStatus, PlayerStack, PlayerWithCallStatus,
|
||||
ScreenShareStatus, TextColor, ToolDivider, TrafficLights,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Livestream {
|
||||
pub players: Vec<PlayerWithCallStatus>,
|
||||
pub channel: Option<String>, // projects
|
||||
// windows
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TitleBar {
|
||||
/// If the window is active from the OS's perspective.
|
||||
is_active: Arc<AtomicBool>,
|
||||
livestream: Option<Livestream>,
|
||||
mic_status: MicStatus,
|
||||
is_deafened: bool,
|
||||
screen_share_status: ScreenShareStatus,
|
||||
}
|
||||
|
||||
impl TitleBar {
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
let is_active = Arc::new(AtomicBool::new(true));
|
||||
let active = is_active.clone();
|
||||
|
||||
// cx.observe_window_activation(move |_, is_active, cx| {
|
||||
// active.store(is_active, std::sync::atomic::Ordering::SeqCst);
|
||||
// cx.notify();
|
||||
// })
|
||||
// .detach();
|
||||
|
||||
Self {
|
||||
is_active,
|
||||
livestream: None,
|
||||
mic_status: MicStatus::Unmuted,
|
||||
is_deafened: false,
|
||||
screen_share_status: ScreenShareStatus::NotShared,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_livestream(mut self, livestream: Option<Livestream>) -> Self {
|
||||
self.livestream = livestream;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_mic_muted(&self) -> bool {
|
||||
self.mic_status == MicStatus::Muted
|
||||
}
|
||||
|
||||
pub fn toggle_mic_status(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.mic_status = self.mic_status.inverse();
|
||||
|
||||
// Undeafen yourself when unmuting the mic while deafened.
|
||||
if self.is_deafened && self.mic_status == MicStatus::Unmuted {
|
||||
self.is_deafened = false;
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn toggle_deafened(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.is_deafened = !self.is_deafened;
|
||||
self.mic_status = MicStatus::Muted;
|
||||
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
pub fn toggle_screen_share_status(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.screen_share_status = self.screen_share_status.inverse();
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn view(cx: &mut WindowContext, livestream: Option<Livestream>) -> View<Self> {
|
||||
cx.build_view(|cx| Self::new(cx).set_livestream(livestream))
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for TitleBar {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Div<Self> {
|
||||
let settings = user_settings(cx);
|
||||
|
||||
// let has_focus = cx.window_is_active();
|
||||
let has_focus = true;
|
||||
|
||||
let player_list = if let Some(livestream) = &self.livestream {
|
||||
livestream.players.clone().into_iter()
|
||||
} else {
|
||||
vec![].into_iter()
|
||||
};
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.bg(cx.theme().colors().background)
|
||||
.py_1()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.h_full()
|
||||
.gap_4()
|
||||
.px_2()
|
||||
.child(TrafficLights::new().window_has_focus(has_focus))
|
||||
// === Project Info === //
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.when(*settings.titlebar.show_project_owner, |this| {
|
||||
this.child(Button::new("iamnbutler"))
|
||||
})
|
||||
.child(Button::new("zed"))
|
||||
.child(Button::new("nate/gpui2-ui-components")),
|
||||
)
|
||||
.children(player_list.map(|p| PlayerStack::new(p)))
|
||||
.child(IconButton::new("plus", Icon::Plus)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.child(
|
||||
div()
|
||||
.px_2()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(IconButton::new("folder_x", Icon::FolderX))
|
||||
.child(IconButton::new("exit", Icon::Exit)),
|
||||
)
|
||||
.child(ToolDivider::new())
|
||||
.child(
|
||||
div()
|
||||
.px_2()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(
|
||||
IconButton::<TitleBar>::new("toggle_mic_status", Icon::Mic)
|
||||
.when(self.is_mic_muted(), |this| this.color(TextColor::Error))
|
||||
.on_click(|title_bar: &mut TitleBar, cx| {
|
||||
title_bar.toggle_mic_status(cx)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::<TitleBar>::new("toggle_deafened", Icon::AudioOn)
|
||||
.when(self.is_deafened, |this| this.color(TextColor::Error))
|
||||
.on_click(|title_bar: &mut TitleBar, cx| {
|
||||
title_bar.toggle_deafened(cx)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::<TitleBar>::new("toggle_screen_share", Icon::Screen)
|
||||
.when(
|
||||
self.screen_share_status == ScreenShareStatus::Shared,
|
||||
|this| this.color(TextColor::Accent),
|
||||
)
|
||||
.on_click(|title_bar: &mut TitleBar, cx| {
|
||||
title_bar.toggle_screen_share_status(cx)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().px_2().flex().items_center().child(
|
||||
Avatar::new("https://avatars.githubusercontent.com/u/1714999?v=4")
|
||||
.shape(Shape::RoundedRectangle),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use super::*;
|
||||
use crate::Story;
|
||||
|
||||
pub struct TitleBarStory {
|
||||
title_bar: View<TitleBar>,
|
||||
}
|
||||
|
||||
impl TitleBarStory {
|
||||
pub fn view(cx: &mut WindowContext) -> View<Self> {
|
||||
cx.build_view(|cx| Self {
|
||||
title_bar: TitleBar::view(cx, None),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for TitleBarStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Div<Self> {
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, TitleBar>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(self.title_bar.clone())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,126 +0,0 @@
|
|||
use gpui::AnyElement;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ToolbarItem {}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct Toolbar<V: 'static> {
|
||||
left_items: SmallVec<[AnyElement<V>; 2]>,
|
||||
right_items: SmallVec<[AnyElement<V>; 2]>,
|
||||
}
|
||||
|
||||
impl<V: 'static> Toolbar<V> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
left_items: SmallVec::new(),
|
||||
right_items: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn left_item(mut self, child: impl Component<V>) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.left_items.push(child.render());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn left_items(mut self, iter: impl IntoIterator<Item = impl Component<V>>) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.left_items
|
||||
.extend(iter.into_iter().map(|item| item.render()));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn right_item(mut self, child: impl Component<V>) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.right_items.push(child.render());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn right_items(mut self, iter: impl IntoIterator<Item = impl Component<V>>) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.right_items
|
||||
.extend(iter.into_iter().map(|item| item.render()));
|
||||
self
|
||||
}
|
||||
|
||||
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
div()
|
||||
.bg(cx.theme().colors().toolbar_background)
|
||||
.p_2()
|
||||
.flex()
|
||||
.justify_between()
|
||||
.child(div().flex().children(self.left_items))
|
||||
.child(div().flex().children(self.right_items))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use gpui::{Div, Render};
|
||||
|
||||
use crate::{Breadcrumb, HighlightedText, Icon, IconButton, Story, Symbol};
|
||||
|
||||
use super::*;
|
||||
|
||||
pub struct ToolbarStory;
|
||||
|
||||
impl Render for ToolbarStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, Toolbar<Self>>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(
|
||||
Toolbar::new()
|
||||
.left_item(Breadcrumb::new(
|
||||
PathBuf::from_str("crates/ui/src/components/toolbar.rs").unwrap(),
|
||||
vec![
|
||||
Symbol(vec![
|
||||
HighlightedText {
|
||||
text: "impl ".to_string(),
|
||||
color: cx.theme().syntax_color("keyword"),
|
||||
},
|
||||
HighlightedText {
|
||||
text: "ToolbarStory".to_string(),
|
||||
color: cx.theme().syntax_color("function"),
|
||||
},
|
||||
]),
|
||||
Symbol(vec![
|
||||
HighlightedText {
|
||||
text: "fn ".to_string(),
|
||||
color: cx.theme().syntax_color("keyword"),
|
||||
},
|
||||
HighlightedText {
|
||||
text: "render".to_string(),
|
||||
color: cx.theme().syntax_color("function"),
|
||||
},
|
||||
]),
|
||||
],
|
||||
))
|
||||
.right_items(vec![
|
||||
IconButton::new("toggle_inlay_hints", Icon::InlayHint),
|
||||
IconButton::new("buffer_search", Icon::MagnifyingGlass),
|
||||
IconButton::new("inline_assist", Icon::MagicWand),
|
||||
]),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum TrafficLightColor {
|
||||
Red,
|
||||
Yellow,
|
||||
Green,
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
struct TrafficLight {
|
||||
color: TrafficLightColor,
|
||||
window_has_focus: bool,
|
||||
}
|
||||
|
||||
impl TrafficLight {
|
||||
fn new(color: TrafficLightColor, window_has_focus: bool) -> Self {
|
||||
Self {
|
||||
color,
|
||||
window_has_focus,
|
||||
}
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
let system_colors = &cx.theme().styles.system;
|
||||
|
||||
let fill = match (self.window_has_focus, self.color) {
|
||||
(true, TrafficLightColor::Red) => system_colors.mac_os_traffic_light_red,
|
||||
(true, TrafficLightColor::Yellow) => system_colors.mac_os_traffic_light_yellow,
|
||||
(true, TrafficLightColor::Green) => system_colors.mac_os_traffic_light_green,
|
||||
(false, _) => cx.theme().colors().element_background,
|
||||
};
|
||||
|
||||
div().w_3().h_3().rounded_full().bg(fill)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct TrafficLights {
|
||||
window_has_focus: bool,
|
||||
}
|
||||
|
||||
impl TrafficLights {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
window_has_focus: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn window_has_focus(mut self, window_has_focus: bool) -> Self {
|
||||
self.window_has_focus = window_has_focus;
|
||||
self
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(TrafficLight::new(
|
||||
TrafficLightColor::Red,
|
||||
self.window_has_focus,
|
||||
))
|
||||
.child(TrafficLight::new(
|
||||
TrafficLightColor::Yellow,
|
||||
self.window_has_focus,
|
||||
))
|
||||
.child(TrafficLight::new(
|
||||
TrafficLightColor::Green,
|
||||
self.window_has_focus,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use gpui::{Div, Render};
|
||||
|
||||
use crate::Story;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub struct TrafficLightsStory;
|
||||
|
||||
impl Render for TrafficLightsStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, TrafficLights>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(TrafficLights::new())
|
||||
.child(Story::label(cx, "Unfocused"))
|
||||
.child(TrafficLights::new().window_has_focus(false))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,398 +0,0 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use chrono::DateTime;
|
||||
use gpui::{px, relative, Div, Render, Size, View, VisualContext};
|
||||
use settings2::Settings;
|
||||
use theme2::ThemeSettings;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{
|
||||
static_livestream, v_stack, AssistantPanel, Button, ChatMessage, ChatPanel, Checkbox,
|
||||
CollabPanel, EditorPane, Label, LanguageSelector, NotificationsPanel, Pane, PaneGroup, Panel,
|
||||
PanelAllowedSides, PanelSide, ProjectPanel, SplitDirection, StatusBar, Terminal, TitleBar,
|
||||
Toast, ToastOrigin,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Gpui2UiDebug {
|
||||
pub in_livestream: bool,
|
||||
pub enable_user_settings: bool,
|
||||
pub show_toast: bool,
|
||||
}
|
||||
|
||||
impl Default for Gpui2UiDebug {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
in_livestream: false,
|
||||
enable_user_settings: false,
|
||||
show_toast: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Workspace {
|
||||
title_bar: View<TitleBar>,
|
||||
editor_1: View<EditorPane>,
|
||||
show_project_panel: bool,
|
||||
show_collab_panel: bool,
|
||||
show_chat_panel: bool,
|
||||
show_assistant_panel: bool,
|
||||
show_notifications_panel: bool,
|
||||
show_terminal: bool,
|
||||
show_debug: bool,
|
||||
show_language_selector: bool,
|
||||
test_checkbox_selection: Selection,
|
||||
debug: Gpui2UiDebug,
|
||||
}
|
||||
|
||||
impl Workspace {
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
Self {
|
||||
title_bar: TitleBar::view(cx, None),
|
||||
editor_1: EditorPane::view(cx),
|
||||
show_project_panel: true,
|
||||
show_collab_panel: false,
|
||||
show_chat_panel: false,
|
||||
show_assistant_panel: false,
|
||||
show_terminal: true,
|
||||
show_language_selector: false,
|
||||
show_debug: false,
|
||||
show_notifications_panel: true,
|
||||
test_checkbox_selection: Selection::Unselected,
|
||||
debug: Gpui2UiDebug::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_project_panel_open(&self) -> bool {
|
||||
self.show_project_panel
|
||||
}
|
||||
|
||||
pub fn toggle_project_panel(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.show_project_panel = !self.show_project_panel;
|
||||
|
||||
self.show_collab_panel = false;
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn is_collab_panel_open(&self) -> bool {
|
||||
self.show_collab_panel
|
||||
}
|
||||
|
||||
pub fn toggle_collab_panel(&mut self) {
|
||||
self.show_collab_panel = !self.show_collab_panel;
|
||||
|
||||
self.show_project_panel = false;
|
||||
}
|
||||
|
||||
pub fn is_terminal_open(&self) -> bool {
|
||||
self.show_terminal
|
||||
}
|
||||
|
||||
pub fn toggle_terminal(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.show_terminal = !self.show_terminal;
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn is_chat_panel_open(&self) -> bool {
|
||||
self.show_chat_panel
|
||||
}
|
||||
|
||||
pub fn toggle_chat_panel(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.show_chat_panel = !self.show_chat_panel;
|
||||
|
||||
self.show_assistant_panel = false;
|
||||
self.show_notifications_panel = false;
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn is_notifications_panel_open(&self) -> bool {
|
||||
self.show_notifications_panel
|
||||
}
|
||||
|
||||
pub fn toggle_notifications_panel(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.show_notifications_panel = !self.show_notifications_panel;
|
||||
|
||||
self.show_chat_panel = false;
|
||||
self.show_assistant_panel = false;
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn is_assistant_panel_open(&self) -> bool {
|
||||
self.show_assistant_panel
|
||||
}
|
||||
|
||||
pub fn toggle_assistant_panel(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.show_assistant_panel = !self.show_assistant_panel;
|
||||
|
||||
self.show_chat_panel = false;
|
||||
self.show_notifications_panel = false;
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn is_language_selector_open(&self) -> bool {
|
||||
self.show_language_selector
|
||||
}
|
||||
|
||||
pub fn toggle_language_selector(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.show_language_selector = !self.show_language_selector;
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn toggle_debug(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.show_debug = !self.show_debug;
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn debug_toggle_user_settings(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.debug.enable_user_settings = !self.debug.enable_user_settings;
|
||||
|
||||
let mut theme_settings = ThemeSettings::get_global(cx).clone();
|
||||
|
||||
if self.debug.enable_user_settings {
|
||||
theme_settings.ui_font_size = 18.0.into();
|
||||
} else {
|
||||
theme_settings.ui_font_size = 16.0.into();
|
||||
}
|
||||
|
||||
ThemeSettings::override_global(theme_settings.clone(), cx);
|
||||
|
||||
cx.set_rem_size(theme_settings.ui_font_size);
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn debug_toggle_livestream(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.debug.in_livestream = !self.debug.in_livestream;
|
||||
|
||||
self.title_bar = TitleBar::view(
|
||||
cx,
|
||||
Some(static_livestream()).filter(|_| self.debug.in_livestream),
|
||||
);
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn debug_toggle_toast(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.debug.show_toast = !self.debug.show_toast;
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn view(cx: &mut WindowContext) -> View<Self> {
|
||||
cx.build_view(|cx| Self::new(cx))
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Workspace {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Div<Self> {
|
||||
let root_group = PaneGroup::new_panes(
|
||||
vec![Pane::new(
|
||||
"pane-0",
|
||||
Size {
|
||||
width: relative(1.).into(),
|
||||
height: relative(1.).into(),
|
||||
},
|
||||
)
|
||||
.child(self.editor_1.clone())],
|
||||
SplitDirection::Horizontal,
|
||||
);
|
||||
let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
|
||||
|
||||
div()
|
||||
.relative()
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.font(ui_font)
|
||||
.gap_0()
|
||||
.justify_start()
|
||||
.items_start()
|
||||
.text_color(cx.theme().colors().text)
|
||||
.bg(cx.theme().colors().background)
|
||||
.child(self.title_bar.clone())
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_12()
|
||||
.left_12()
|
||||
.z_index(99)
|
||||
.bg(cx.theme().colors().background)
|
||||
.child(
|
||||
Checkbox::new("test_checkbox", self.test_checkbox_selection).on_click(
|
||||
|selection, workspace: &mut Workspace, cx| {
|
||||
workspace.test_checkbox_selection = selection;
|
||||
|
||||
cx.notify();
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.overflow_hidden()
|
||||
.border_t()
|
||||
.border_b()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.children(
|
||||
Some(
|
||||
Panel::new("project-panel-outer", cx)
|
||||
.side(PanelSide::Left)
|
||||
.child(ProjectPanel::new("project-panel-inner")),
|
||||
)
|
||||
.filter(|_| self.is_project_panel_open()),
|
||||
)
|
||||
.children(
|
||||
Some(
|
||||
Panel::new("collab-panel-outer", cx)
|
||||
.child(CollabPanel::new("collab-panel-inner"))
|
||||
.side(PanelSide::Left),
|
||||
)
|
||||
.filter(|_| self.is_collab_panel_open()),
|
||||
)
|
||||
// .child(NotificationToast::new(
|
||||
// "maxbrunsfeld has requested to add you as a contact.".into(),
|
||||
// ))
|
||||
.child(
|
||||
v_stack()
|
||||
.flex_1()
|
||||
.h_full()
|
||||
.child(div().flex().flex_1().child(root_group))
|
||||
.children(
|
||||
Some(
|
||||
Panel::new("terminal-panel", cx)
|
||||
.child(Terminal::new())
|
||||
.allowed_sides(PanelAllowedSides::BottomOnly)
|
||||
.side(PanelSide::Bottom),
|
||||
)
|
||||
.filter(|_| self.is_terminal_open()),
|
||||
),
|
||||
)
|
||||
.children(
|
||||
Some(
|
||||
Panel::new("chat-panel-outer", cx)
|
||||
.side(PanelSide::Right)
|
||||
.child(ChatPanel::new("chat-panel-inner").messages(vec![
|
||||
ChatMessage::new(
|
||||
"osiewicz".to_string(),
|
||||
"is this thing on?".to_string(),
|
||||
DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z")
|
||||
.unwrap()
|
||||
.naive_local(),
|
||||
),
|
||||
ChatMessage::new(
|
||||
"maxdeviant".to_string(),
|
||||
"Reading you loud and clear!".to_string(),
|
||||
DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z")
|
||||
.unwrap()
|
||||
.naive_local(),
|
||||
),
|
||||
])),
|
||||
)
|
||||
.filter(|_| self.is_chat_panel_open()),
|
||||
)
|
||||
.children(
|
||||
Some(
|
||||
Panel::new("notifications-panel-outer", cx)
|
||||
.side(PanelSide::Right)
|
||||
.child(NotificationsPanel::new("notifications-panel-inner")),
|
||||
)
|
||||
.filter(|_| self.is_notifications_panel_open()),
|
||||
)
|
||||
.children(
|
||||
Some(
|
||||
Panel::new("assistant-panel-outer", cx)
|
||||
.child(AssistantPanel::new("assistant-panel-inner")),
|
||||
)
|
||||
.filter(|_| self.is_assistant_panel_open()),
|
||||
),
|
||||
)
|
||||
.child(StatusBar::new())
|
||||
.when(self.debug.show_toast, |this| {
|
||||
this.child(Toast::new(ToastOrigin::Bottom).child(Label::new("A toast")))
|
||||
})
|
||||
.children(
|
||||
Some(
|
||||
div()
|
||||
.absolute()
|
||||
.top(px(50.))
|
||||
.left(px(640.))
|
||||
.z_index(8)
|
||||
.child(LanguageSelector::new("language-selector")),
|
||||
)
|
||||
.filter(|_| self.is_language_selector_open()),
|
||||
)
|
||||
.z_index(8)
|
||||
// Debug
|
||||
.child(
|
||||
v_stack()
|
||||
.z_index(9)
|
||||
.absolute()
|
||||
.top_20()
|
||||
.left_1_4()
|
||||
.w_40()
|
||||
.gap_2()
|
||||
.when(self.show_debug, |this| {
|
||||
this.child(Button::<Workspace>::new("Toggle User Settings").on_click(
|
||||
Arc::new(|workspace, cx| workspace.debug_toggle_user_settings(cx)),
|
||||
))
|
||||
.child(
|
||||
Button::<Workspace>::new("Toggle Toasts").on_click(Arc::new(
|
||||
|workspace, cx| workspace.debug_toggle_toast(cx),
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
Button::<Workspace>::new("Toggle Livestream").on_click(Arc::new(
|
||||
|workspace, cx| workspace.debug_toggle_livestream(cx),
|
||||
)),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
Button::<Workspace>::new("Toggle Debug")
|
||||
.on_click(Arc::new(|workspace, cx| workspace.toggle_debug(cx))),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use super::*;
|
||||
use gpui::VisualContext;
|
||||
|
||||
pub struct WorkspaceStory {
|
||||
workspace: View<Workspace>,
|
||||
}
|
||||
|
||||
impl WorkspaceStory {
|
||||
pub fn view(cx: &mut WindowContext) -> View<Self> {
|
||||
cx.build_view(|cx| Self {
|
||||
workspace: Workspace::view(cx),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for WorkspaceStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
div().child(self.workspace.clone())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,36 +15,12 @@
|
|||
#![allow(dead_code, unused_variables)]
|
||||
|
||||
mod components;
|
||||
mod elevation;
|
||||
pub mod prelude;
|
||||
pub mod settings;
|
||||
mod static_data;
|
||||
mod styled_ext;
|
||||
mod to_extract;
|
||||
mod styles;
|
||||
pub mod utils;
|
||||
|
||||
pub use components::*;
|
||||
use gpui::actions;
|
||||
pub use prelude::*;
|
||||
pub use static_data::*;
|
||||
pub use styled_ext::*;
|
||||
pub use to_extract::*;
|
||||
|
||||
// This needs to be fully qualified with `crate::` otherwise we get a panic
|
||||
// at:
|
||||
// thread '<unnamed>' panicked at crates/gpui2/src/platform/mac/platform.rs:66:81:
|
||||
// called `Option::unwrap()` on a `None` value
|
||||
//
|
||||
// AFAICT this is something to do with conflicting names between crates and modules that
|
||||
// interfaces with declaring the `ClassDecl`.
|
||||
pub use crate::settings::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod story;
|
||||
#[cfg(feature = "stories")]
|
||||
pub use story::*;
|
||||
actions!(NoAction);
|
||||
|
||||
pub fn binding(key: &str) -> gpui::KeyBinding {
|
||||
gpui::KeyBinding::new(key, NoAction {}, None)
|
||||
}
|
||||
pub use styles::*;
|
Loading…
Add table
Add a link
Reference in a new issue