Merge branch 'main' into collab-panel2

This commit is contained in:
Conrad Irwin 2023-11-29 16:32:25 -07:00
commit 4c27f4453c
25 changed files with 338 additions and 688 deletions

View file

@ -178,6 +178,7 @@ use gpui::{
use project::{Fs, Project}; use project::{Fs, Project};
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use settings::{Settings, SettingsStore}; use settings::{Settings, SettingsStore};
use ui::prelude::*;
use ui::{ use ui::{
h_stack, v_stack, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconElement, IconSize, h_stack, v_stack, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconElement, IconSize,
Label, List, ListHeader, ListItem, Tooltip, Label, List, ListHeader, ListItem, Tooltip,
@ -2333,7 +2334,8 @@ impl CollabPanel {
} }
fn render_signed_out(&mut self, cx: &mut ViewContext<Self>) -> Div { fn render_signed_out(&mut self, cx: &mut ViewContext<Self>) -> Div {
v_stack().child(Button::new("Sign in to collaborate").on_click(cx.listener( v_stack().child(
Button::new("sign_in", "Sign in to collaborate").on_click(cx.listener(
|this, _, cx| { |this, _, cx| {
let client = this.client.clone(); let client = this.client.clone();
cx.spawn(|_, mut cx| async move { cx.spawn(|_, mut cx| async move {
@ -2344,7 +2346,8 @@ impl CollabPanel {
}) })
.detach() .detach()
}, },
))) )),
)
} }
fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> List { fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> List {
@ -2559,7 +2562,7 @@ impl CollabPanel {
.group_hover("", |style| style.visible()) .group_hover("", |style| style.visible())
.child( .child(
IconButton::new("remove_contact", Icon::Close) IconButton::new("remove_contact", Icon::Close)
.color(Color::Muted) .icon_color(Color::Muted)
.tooltip(|cx| Tooltip::text("Remove Contact", cx)) .tooltip(|cx| Tooltip::text("Remove Contact", cx))
.on_click(cx.listener({ .on_click(cx.listener({
let github_login = github_login.clone(); let github_login = github_login.clone();
@ -2623,13 +2626,13 @@ impl CollabPanel {
.on_click(cx.listener(move |this, _, cx| { .on_click(cx.listener(move |this, _, cx| {
this.respond_to_contact_request(user_id, false, cx); this.respond_to_contact_request(user_id, false, cx);
})) }))
.color(color) .icon_color(color)
.tooltip(|cx| Tooltip::text("Decline invite", cx)), .tooltip(|cx| Tooltip::text("Decline invite", cx)),
IconButton::new("remove_contact", Icon::Check) IconButton::new("remove_contact", Icon::Check)
.on_click(cx.listener(move |this, _, cx| { .on_click(cx.listener(move |this, _, cx| {
this.respond_to_contact_request(user_id, true, cx); this.respond_to_contact_request(user_id, true, cx);
})) }))
.color(color) .icon_color(color)
.tooltip(|cx| Tooltip::text("Accept invite", cx)), .tooltip(|cx| Tooltip::text("Accept invite", cx)),
] ]
} else { } else {
@ -2638,7 +2641,7 @@ impl CollabPanel {
.on_click(cx.listener(move |this, _, cx| { .on_click(cx.listener(move |this, _, cx| {
this.remove_contact(user_id, &github_login, cx); this.remove_contact(user_id, &github_login, cx);
})) }))
.color(color) .icon_color(color)
.tooltip(|cx| Tooltip::text("Cancel invite", cx))] .tooltip(|cx| Tooltip::text("Cancel invite", cx))]
}; };
@ -2780,7 +2783,7 @@ impl CollabPanel {
"channel_chat", "channel_chat",
Icon::MessageBubbles, Icon::MessageBubbles,
) )
.color(if has_messages_notification { .icon_color(if has_messages_notification {
Color::Default Color::Default
} else { } else {
Color::Muted Color::Muted
@ -2795,7 +2798,7 @@ impl CollabPanel {
.group_hover("", |style| style.visible()) .group_hover("", |style| style.visible())
.child( .child(
IconButton::new("channel_notes", Icon::File) IconButton::new("channel_notes", Icon::File)
.color(if has_notes_notification { .icon_color(if has_notes_notification {
Color::Default Color::Default
} else { } else {
Color::Muted Color::Muted

View file

@ -37,10 +37,7 @@ use gpui::{
}; };
use project::Project; use project::Project;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::{ use ui::{h_stack, prelude::*, Avatar, Button, ButtonStyle2, IconButton, KeyBinding, Tooltip};
h_stack, Avatar, Button, ButtonCommon, ButtonLike, ButtonVariant, Clickable, Color, IconButton,
IconElement, IconSize, KeyBinding, Tooltip,
};
use util::ResultExt; use util::ResultExt;
use workspace::{notifications::NotifyResultExt, Workspace}; use workspace::{notifications::NotifyResultExt, Workspace};
@ -156,8 +153,8 @@ impl Render for CollabTitlebarItem {
.border_color(gpui::red()) .border_color(gpui::red())
.id("project_owner_indicator") .id("project_owner_indicator")
.child( .child(
Button::new("player") Button::new("player", "player")
.variant(ButtonVariant::Ghost) .style(ButtonStyle2::Subtle)
.color(Some(Color::Player(0))), .color(Some(Color::Player(0))),
) )
.tooltip(move |cx| Tooltip::text("Toggle following", cx)), .tooltip(move |cx| Tooltip::text("Toggle following", cx)),
@ -168,7 +165,10 @@ impl Render for CollabTitlebarItem {
.border() .border()
.border_color(gpui::red()) .border_color(gpui::red())
.id("titlebar_project_menu_button") .id("titlebar_project_menu_button")
.child(Button::new("project_name").variant(ButtonVariant::Ghost)) .child(
Button::new("project_name", "project_name")
.style(ButtonStyle2::Subtle),
)
.tooltip(move |cx| Tooltip::text("Recent Projects", cx)), .tooltip(move |cx| Tooltip::text("Recent Projects", cx)),
) )
// TODO - Add git menu // TODO - Add git menu
@ -178,8 +178,8 @@ impl Render for CollabTitlebarItem {
.border_color(gpui::red()) .border_color(gpui::red())
.id("titlebar_git_menu_button") .id("titlebar_git_menu_button")
.child( .child(
Button::new("branch_name") Button::new("branch_name", "branch_name")
.variant(ButtonVariant::Ghost) .style(ButtonStyle2::Subtle)
.color(Some(Color::Muted)), .color(Some(Color::Muted)),
) )
.tooltip(move |cx| { .tooltip(move |cx| {
@ -238,7 +238,10 @@ impl Render for CollabTitlebarItem {
h_stack() h_stack()
.child( .child(
h_stack() h_stack()
.child(Button::new(if is_shared { "Unshare" } else { "Share" })) .child(Button::new(
"toggle_sharing",
if is_shared { "Unshare" } else { "Share" },
))
.child(IconButton::new("leave-call", ui::Icon::Exit).on_click({ .child(IconButton::new("leave-call", ui::Icon::Exit).on_click({
let workspace = workspace.clone(); let workspace = workspace.clone();
move |_, cx| { move |_, cx| {
@ -291,7 +294,7 @@ impl Render for CollabTitlebarItem {
this.child(ui::Avatar::data(avatar)) this.child(ui::Avatar::data(avatar))
}) })
} else { } else {
this.child(Button::new("Sign in").on_click(move |_, cx| { this.child(Button::new("sign_in", "Sign in").on_click(move |_, cx| {
let client = client.clone(); let client = client.clone();
cx.spawn(move |mut cx| async move { cx.spawn(move |mut cx| async move {
client client
@ -301,27 +304,6 @@ impl Render for CollabTitlebarItem {
}) })
.detach(); .detach();
})) }))
// Temporary, will be removed when the last part of button2 is merged
.child(
div().border().border_color(gpui::blue()).child(
ButtonLike::new("test-button")
.children([
Avatar::uri(
"https://avatars.githubusercontent.com/u/1714999?v=4",
)
.into_element()
.into_any(),
IconElement::new(ui::Icon::ChevronDown)
.size(IconSize::Small)
.into_element()
.into_any(),
])
.on_click(move |event, _cx| {
dbg!(format!("clicked: {:?}", event.down.position));
})
.tooltip(|cx| Tooltip::text("Test tooltip", cx)),
),
)
} }
}) })
} }

View file

@ -2,10 +2,11 @@ use crate::notification_window_options;
use call::{ActiveCall, IncomingCall}; use call::{ActiveCall, IncomingCall};
use futures::StreamExt; use futures::StreamExt;
use gpui::{ use gpui::{
div, green, px, red, AppContext, Div, Element, ParentElement, Render, RenderOnce, div, px, red, AppContext, Div, Element, ParentElement, Render, RenderOnce, Styled, ViewContext,
StatefulInteractiveElement, Styled, ViewContext, VisualContext as _, WindowHandle, VisualContext as _, WindowHandle,
}; };
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
use ui::prelude::*;
use ui::{h_stack, v_stack, Avatar, Button, Label}; use ui::{h_stack, v_stack, Avatar, Button, Label};
use util::ResultExt; use util::ResultExt;
use workspace::AppState; use workspace::AppState;
@ -199,14 +200,24 @@ impl IncomingCallNotification {
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> impl Element { fn render_buttons(&self, cx: &mut ViewContext<Self>) -> impl Element {
h_stack() h_stack()
.child(Button::new("Accept").render(cx).bg(green()).on_click({ .child(
Button::new("accept", "Accept")
.render(cx)
// .bg(green())
.on_click({
let state = self.state.clone(); let state = self.state.clone();
move |_, cx| state.respond(true, cx) move |_, cx| state.respond(true, cx)
})) }),
.child(Button::new("Decline").render(cx).bg(red()).on_click({ )
.child(
Button::new("decline", "Decline")
.render(cx)
// .bg(red())
.on_click({
let state = self.state.clone(); let state = self.state.clone();
move |_, cx| state.respond(false, cx) move |_, cx| state.respond(false, cx)
})) }),
)
// enum Accept {} // enum Accept {}
// enum Decline {} // enum Decline {}

View file

@ -1,5 +1,6 @@
use crate::ProjectDiagnosticsEditor; use crate::ProjectDiagnosticsEditor;
use gpui::{div, Div, EventEmitter, ParentElement, Render, ViewContext, WeakView}; use gpui::{div, Div, EventEmitter, ParentElement, Render, ViewContext, WeakView};
use ui::prelude::*;
use ui::{Icon, IconButton, Tooltip}; use ui::{Icon, IconButton, Tooltip};
use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};

View file

@ -99,7 +99,8 @@ use text::{OffsetUtf16, Rope};
use theme::{ use theme::{
ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings, ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings,
}; };
use ui::{h_stack, v_stack, HighlightedLabel, IconButton, Popover, StyledExt, Tooltip}; use ui::prelude::*;
use ui::{h_stack, v_stack, HighlightedLabel, IconButton, Popover, Tooltip};
use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
use workspace::{ use workspace::{
item::{ItemEvent, ItemHandle}, item::{ItemEvent, ItemHandle},
@ -4391,7 +4392,7 @@ impl Editor {
editor.fold_at(&FoldAt { buffer_row }, cx); editor.fold_at(&FoldAt { buffer_row }, cx);
} }
})) }))
.color(ui::Color::Muted) .icon_color(ui::Color::Muted)
}) })
}) })
.flatten() .flatten()

View file

@ -48,6 +48,7 @@ use std::{
}; };
use sum_tree::Bias; use sum_tree::Bias;
use theme::{ActiveTheme, PlayerColor}; use theme::{ActiveTheme, PlayerColor};
use ui::prelude::*;
use ui::{h_stack, IconButton, Tooltip}; use ui::{h_stack, IconButton, Tooltip};
use util::ResultExt; use util::ResultExt;
use workspace::item::Item; use workspace::item::Item;

View file

@ -18,7 +18,7 @@ use project::search::SearchQuery;
use serde::Deserialize; use serde::Deserialize;
use std::{any::Any, sync::Arc}; use std::{any::Any, sync::Arc};
use ui::{h_stack, ButtonGroup, Icon, IconButton, IconElement}; use ui::{h_stack, Icon, IconButton, IconElement};
use util::ResultExt; use util::ResultExt;
use workspace::{ use workspace::{
item::ItemHandle, item::ItemHandle,
@ -214,10 +214,11 @@ impl Render for BufferSearchBar {
.child( .child(
h_stack() h_stack()
.flex_none() .flex_none()
.child(ButtonGroup::new(vec![ .child(
search_button_for_mode(SearchMode::Text), h_stack()
search_button_for_mode(SearchMode::Regex), .child(search_button_for_mode(SearchMode::Text))
])) .child(search_button_for_mode(SearchMode::Regex)),
)
.when(supported_options.replacement, |this| { .when(supported_options.replacement, |this| {
this.child(super::toggle_replace_button(self.replace_enabled)) this.child(super::toggle_replace_button(self.replace_enabled))
}), }),
@ -586,8 +587,7 @@ impl BufferSearchBar {
// let style = theme.search.action_button.clone(); // let style = theme.search.action_button.clone();
IconButton::new(0, ui::Icon::SelectAll) IconButton::new(0, ui::Icon::SelectAll).action(Box::new(SelectAllMatches))
.on_click(|_, cx| cx.dispatch_action(Box::new(SelectAllMatches)))
} }
pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) { pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {

View file

@ -3,7 +3,8 @@ pub use buffer_search::BufferSearchBar;
use gpui::{actions, Action, AppContext, IntoElement}; use gpui::{actions, Action, AppContext, IntoElement};
pub use mode::SearchMode; pub use mode::SearchMode;
use project::search::SearchQuery; use project::search::SearchQuery;
use ui::ButtonVariant; use ui::prelude::*;
use ui::{ButtonStyle2, Icon, IconButton};
//pub use project_search::{ProjectSearchBar, ProjectSearchView}; //pub use project_search::{ProjectSearchBar, ProjectSearchView};
// use theme::components::{ // use theme::components::{
// action_button::Button, svg::Svg, ComponentExt, IconButtonStyle, ToggleIconButtonStyle, // action_button::Button, svg::Svg, ComponentExt, IconButtonStyle, ToggleIconButtonStyle,
@ -83,35 +84,35 @@ impl SearchOptions {
} }
pub fn as_button(&self, active: bool) -> impl IntoElement { pub fn as_button(&self, active: bool) -> impl IntoElement {
ui::IconButton::new(0, self.icon()) IconButton::new(0, self.icon())
.on_click({ .on_click({
let action = self.to_toggle_action(); let action = self.to_toggle_action();
move |_, cx| { move |_, cx| {
cx.dispatch_action(action.boxed_clone()); cx.dispatch_action(action.boxed_clone());
} }
}) })
.variant(ui::ButtonVariant::Ghost) .style(ButtonStyle2::Subtle)
.when(active, |button| button.variant(ButtonVariant::Filled)) .when(active, |button| button.style(ButtonStyle2::Filled))
} }
} }
fn toggle_replace_button(active: bool) -> impl IntoElement { fn toggle_replace_button(active: bool) -> impl IntoElement {
// todo: add toggle_replace button // todo: add toggle_replace button
ui::IconButton::new(0, ui::Icon::Replace) IconButton::new(0, Icon::Replace)
.on_click(|_, cx| { .on_click(|_, cx| {
cx.dispatch_action(Box::new(ToggleReplace)); cx.dispatch_action(Box::new(ToggleReplace));
cx.notify(); cx.notify();
}) })
.variant(ui::ButtonVariant::Ghost) .style(ButtonStyle2::Subtle)
.when(active, |button| button.variant(ButtonVariant::Filled)) .when(active, |button| button.style(ButtonStyle2::Filled))
} }
fn render_replace_button( fn render_replace_button(
action: impl Action + 'static + Send + Sync, action: impl Action + 'static + Send + Sync,
icon: ui::Icon, icon: Icon,
) -> impl IntoElement { ) -> impl IntoElement {
// todo: add tooltip // todo: add tooltip
ui::IconButton::new(0, icon).on_click(move |_, cx| { IconButton::new(0, icon).on_click(move |_, cx| {
cx.dispatch_action(action.boxed_clone()); cx.dispatch_action(action.boxed_clone());
}) })
} }

View file

@ -1,5 +1,6 @@
use gpui::{ClickEvent, IntoElement, WindowContext}; use gpui::{ClickEvent, IntoElement, WindowContext};
use ui::{Button, ButtonVariant, IconButton}; use ui::prelude::*;
use ui::{Button, IconButton};
use crate::mode::SearchMode; use crate::mode::SearchMode;
@ -23,13 +24,7 @@ pub(crate) fn render_search_mode_button(
is_active: bool, is_active: bool,
on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static, on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
) -> Button { ) -> Button {
let button_variant = if is_active { Button::new(mode.label(), mode.label())
ButtonVariant::Filled .selected(is_active)
} else {
ButtonVariant::Ghost
};
Button::new(mode.label())
.on_click(on_click) .on_click(on_click)
.variant(button_variant)
} }

View file

@ -1,12 +1,10 @@
mod avatar; mod avatar;
mod button; mod button;
mod button2;
mod checkbox; mod checkbox;
mod context_menu; mod context_menu;
mod disclosure; mod disclosure;
mod divider; mod divider;
mod icon; mod icon;
mod icon_button;
mod keybinding; mod keybinding;
mod label; mod label;
mod list; mod list;
@ -19,13 +17,11 @@ mod stories;
pub use avatar::*; pub use avatar::*;
pub use button::*; pub use button::*;
pub use button2::*;
pub use checkbox::*; pub use checkbox::*;
pub use context_menu::*; pub use context_menu::*;
pub use disclosure::*; pub use disclosure::*;
pub use divider::*; pub use divider::*;
pub use icon::*; pub use icon::*;
pub use icon_button::*;
pub use keybinding::*; pub use keybinding::*;
pub use label::*; pub use label::*;
pub use list::*; pub use list::*;

View file

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

View file

@ -0,0 +1,91 @@
use gpui::AnyView;
use crate::prelude::*;
use crate::{ButtonCommon, ButtonLike, ButtonSize2, ButtonStyle2, Label, LineHeightStyle};
#[derive(IntoElement)]
pub struct Button {
base: ButtonLike,
label: SharedString,
label_color: Option<Color>,
selected: bool,
}
impl Button {
pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
Self {
base: ButtonLike::new(id),
label: label.into(),
label_color: None,
selected: false,
}
}
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
pub fn color(mut self, label_color: impl Into<Option<Color>>) -> Self {
self.label_color = label_color.into();
self
}
}
impl Clickable for Button {
fn on_click(
mut self,
handler: impl Fn(&gpui::ClickEvent, &mut WindowContext) + 'static,
) -> Self {
self.base = self.base.on_click(handler);
self
}
}
impl Disableable for Button {
fn disabled(mut self, disabled: bool) -> Self {
self.base = self.base.disabled(disabled);
self
}
}
impl ButtonCommon for Button {
fn id(&self) -> &ElementId {
self.base.id()
}
fn style(mut self, style: ButtonStyle2) -> Self {
self.base = self.base.style(style);
self
}
fn size(mut self, size: ButtonSize2) -> Self {
self.base = self.base.size(size);
self
}
fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
self.base = self.base.tooltip(tooltip);
self
}
}
impl RenderOnce for Button {
type Rendered = ButtonLike;
fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
let label_color = if self.base.disabled {
Color::Disabled
} else if self.selected {
Color::Selected
} else {
Color::Default
};
self.base.child(
Label::new(self.label)
.color(label_color)
.line_height_style(LineHeightStyle::UILabel),
)
}
}

View file

@ -1,31 +1,17 @@
use gpui::{ use gpui::{rems, AnyElement, AnyView, ClickEvent, Div, Hsla, Rems, Stateful};
rems, AnyElement, AnyView, ClickEvent, Div, Hsla, IntoElement, Rems, Stateful,
StatefulInteractiveElement, WindowContext,
};
use smallvec::SmallVec; use smallvec::SmallVec;
use crate::{h_stack, prelude::*}; use crate::h_stack;
use crate::prelude::*;
// 🚧 Heavily WIP 🚧 pub trait ButtonCommon: Clickable + Disableable {
fn id(&self) -> &ElementId;
// #[derive(Default, PartialEq, Clone, Copy)] fn style(self, style: ButtonStyle2) -> Self;
// pub enum ButtonType2 { fn size(self, size: ButtonSize2) -> Self;
// #[default] fn tooltip(self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self;
// DefaultButton,
// IconButton,
// ButtonLike,
// SplitButton,
// ToggleButton,
// }
#[derive(Default, PartialEq, Clone, Copy)]
pub enum IconPosition2 {
#[default]
Before,
After,
} }
#[derive(Default, PartialEq, Clone, Copy)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
pub enum ButtonStyle2 { pub enum ButtonStyle2 {
#[default] #[default]
Filled, Filled,
@ -34,7 +20,7 @@ pub enum ButtonStyle2 {
Transparent, Transparent,
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone)]
pub struct ButtonStyle { pub struct ButtonStyle {
pub background: Hsla, pub background: Hsla,
pub border_color: Hsla, pub border_color: Hsla,
@ -181,82 +167,11 @@ impl ButtonSize2 {
} }
} }
// pub struct Button {
// id: ElementId,
// icon: Option<Icon>,
// icon_color: Option<Color>,
// icon_position: Option<IconPosition2>,
// label: Option<Label>,
// label_color: Option<Color>,
// appearance: ButtonAppearance2,
// state: InteractionState,
// selected: bool,
// disabled: bool,
// tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
// width: Option<DefiniteLength>,
// action: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
// secondary_action: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
// /// Used to pass down some content to the button
// /// to enable creating custom buttons.
// children: SmallVec<[AnyElement; 2]>,
// }
pub trait ButtonCommon: Clickable + Disableable {
fn id(&self) -> &ElementId;
fn style(self, style: ButtonStyle2) -> Self;
fn size(self, size: ButtonSize2) -> Self;
fn tooltip(self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self;
}
// pub struct LabelButton {
// // Base properties...
// id: ElementId,
// appearance: ButtonAppearance,
// state: InteractionState,
// disabled: bool,
// size: ButtonSize,
// tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
// width: Option<DefiniteLength>,
// // Button-specific properties...
// label: Option<SharedString>,
// label_color: Option<Color>,
// icon: Option<Icon>,
// icon_color: Option<Color>,
// icon_position: Option<IconPosition>,
// // Define more fields for additional properties as needed
// }
// impl ButtonCommon for LabelButton {
// fn id(&self) -> &ElementId {
// &self.id
// }
// fn appearance(&mut self, appearance: ButtonAppearance) -> &mut Self {
// self.style= style;
// self
// }
// // implement methods from ButtonCommon trait...
// }
// impl LabelButton {
// pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
// Self {
// id: id.into(),
// label: Some(label.into()),
// // initialize other fields with default values...
// }
// }
// // ... Define other builder methods specific to Button type...
// }
// TODO: Icon Button
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct ButtonLike { pub struct ButtonLike {
id: ElementId, id: ElementId,
style: ButtonStyle2, pub(super) style: ButtonStyle2,
disabled: bool, pub(super) disabled: bool,
size: ButtonSize2, size: ButtonSize2,
tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>, tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>, on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
@ -325,6 +240,12 @@ impl ButtonCommon for ButtonLike {
} }
} }
impl ParentElement for ButtonLike {
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
&mut self.children
}
}
impl RenderOnce for ButtonLike { impl RenderOnce for ButtonLike {
type Rendered = Stateful<Div>; type Rendered = Stateful<Div>;
@ -349,57 +270,3 @@ impl RenderOnce for ButtonLike {
.children(self.children) .children(self.children)
} }
} }
impl ParentElement for ButtonLike {
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
&mut self.children
}
}
// pub struct ToggleButton {
// // based on either IconButton2 or Button, with additional 'selected: bool' property
// }
// impl ButtonCommon for ToggleButton {
// fn id(&self) -> &ElementId {
// &self.id
// }
// // ... Implement other methods from ButtonCommon trait with builder patterns...
// }
// impl ToggleButton {
// pub fn new() -> Self {
// // Initialize with default values
// Self {
// // ... initialize fields, possibly with defaults or required parameters...
// }
// }
// // ... Define other builder methods specific to ToggleButton type...
// }
// pub struct SplitButton {
// // Base properties...
// id: ElementId,
// // Button-specific properties, possibly including a DefaultButton
// secondary_action: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext)>>,
// // More fields as necessary...
// }
// impl ButtonCommon for SplitButton {
// fn id(&self) -> &ElementId {
// &self.id
// }
// // ... Implement other methods from ButtonCommon trait with builder patterns...
// }
// impl SplitButton {
// pub fn new(id: impl Into<ElementId>) -> Self {
// Self {
// id: id.into(),
// // ... initialize other fields with default values...
// }
// }
// // ... Define other builder methods specific to SplitButton type...
// }

View file

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

View file

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

View file

@ -39,8 +39,8 @@ impl RenderOnce for Disclosure {
false => Icon::ChevronRight, false => Icon::ChevronRight,
}, },
) )
.color(Color::Muted) .icon_color(Color::Muted)
.size(IconSize::Small) .icon_size(IconSize::Small)
.when_some(self.on_toggle, move |this, on_toggle| { .when_some(self.on_toggle, move |this, on_toggle| {
this.on_click(move |event, cx| on_toggle(event, cx)) this.on_click(move |event, cx| on_toggle(event, cx))
}) })

View file

@ -1,135 +0,0 @@
use crate::{h_stack, prelude::*, Icon, IconElement, IconSize};
use gpui::{prelude::*, Action, AnyView, ClickEvent, Div, Stateful};
#[derive(IntoElement)]
pub struct IconButton {
id: ElementId,
icon: Icon,
color: Color,
size: IconSize,
variant: ButtonVariant,
disabled: bool,
selected: bool,
tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>>,
on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
}
impl RenderOnce for IconButton {
type Rendered = Stateful<Div>;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
let icon_color = match (self.disabled, self.selected, self.color) {
(true, _, _) => Color::Disabled,
(false, true, _) => Color::Selected,
_ => self.color,
};
let (mut bg_color, bg_active_color) = match self.variant {
ButtonVariant::Filled => (
cx.theme().colors().element_background,
cx.theme().colors().element_active,
),
ButtonVariant::Ghost => (
cx.theme().colors().ghost_element_background,
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)
.size(self.size)
.color(icon_color),
);
if let Some(click_handler) = self.on_click {
button = button.on_click(move |event, cx| {
cx.stop_propagation();
click_handler(event, cx);
})
}
if let Some(tooltip) = self.tooltip {
if !self.selected {
button = button.tooltip(move |cx| tooltip(cx))
}
}
button
}
}
impl IconButton {
pub fn new(id: impl Into<ElementId>, icon: Icon) -> Self {
Self {
id: id.into(),
icon,
color: Color::default(),
size: Default::default(),
variant: ButtonVariant::default(),
selected: false,
disabled: false,
tooltip: None,
on_click: None,
}
}
pub fn icon(mut self, icon: Icon) -> Self {
self.icon = icon;
self
}
pub fn color(mut self, color: Color) -> Self {
self.color = color;
self
}
pub fn size(mut self, size: IconSize) -> Self {
self.size = size;
self
}
pub fn variant(mut self, variant: ButtonVariant) -> Self {
self.variant = variant;
self
}
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
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(&ClickEvent, &mut WindowContext)) -> Self {
self.on_click = Some(Box::new(handler));
self
}
pub fn action(self, action: Box<dyn Action>) -> Self {
self.on_click(move |_event, cx| cx.dispatch_action(action.boxed_clone()))
}
}

View file

@ -78,7 +78,7 @@ impl RenderOnce for ListHeader {
h_stack() h_stack()
.gap_2() .gap_2()
.items_center() .items_center()
.children(icons.into_iter().map(|i| i.color(Color::Muted))), .children(icons.into_iter().map(|i| i.icon_color(Color::Muted))),
), ),
Some(ListHeaderMeta::Button(label)) => div().child(label), Some(ListHeaderMeta::Button(label)) => div().child(label),
Some(ListHeaderMeta::Text(label)) => div().child(label), Some(ListHeaderMeta::Text(label)) => div().child(label),

View file

@ -2,7 +2,7 @@ use gpui::{Div, Render};
use story::Story; use story::Story;
use crate::prelude::*; use crate::prelude::*;
use crate::{h_stack, Button, Icon, IconPosition}; use crate::{Button, ButtonStyle2};
pub struct ButtonStory; pub struct ButtonStory;
@ -12,66 +12,11 @@ impl Render for ButtonStory {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element { fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
Story::container() Story::container()
.child(Story::title_for::<Button>()) .child(Story::title_for::<Button>())
.child( .child(Story::label("Default"))
div() .child(Button::new("default_filled", "Click me"))
.flex() .child(Story::label("Default (Subtle)"))
.gap_8() .child(Button::new("default_subtle", "Click me").style(ButtonStyle2::Subtle))
.child( .child(Story::label("Default (Transparent)"))
div().child(Story::label("Ghost (Default)")).child( .child(Button::new("default_transparent", "Click me").style(ButtonStyle2::Transparent))
h_stack()
.gap_2()
.child(Button::new("Label").variant(ButtonVariant::Ghost)),
),
)
.child(Story::label("Ghost Left Icon"))
.child(
h_stack().gap_2().child(
Button::new("Label")
.variant(ButtonVariant::Ghost)
.icon(Icon::Plus)
.icon_position(IconPosition::Left),
),
),
)
.child(Story::label("Ghost Right Icon"))
.child(
h_stack().gap_2().child(
Button::new("Label")
.variant(ButtonVariant::Ghost)
.icon(Icon::Plus)
.icon_position(IconPosition::Right),
),
)
.child(
div().child(Story::label("Filled")).child(
h_stack()
.gap_2()
.child(Button::new("Label").variant(ButtonVariant::Filled)),
),
)
.child(Story::label("Filled Left Button"))
.child(
h_stack().gap_2().child(
Button::new("Label")
.variant(ButtonVariant::Filled)
.icon(Icon::Plus)
.icon_position(IconPosition::Left),
),
)
.child(Story::label("Filled Right Button"))
.child(
h_stack().gap_2().child(
Button::new("Label")
.variant(ButtonVariant::Filled)
.icon(Icon::Plus)
.icon_position(IconPosition::Right),
),
)
.child(Story::label("Button with `on_click`"))
.child(
Button::new("Label")
.variant(ButtonVariant::Ghost)
.on_click(|_, _cx| println!("Button clicked.")),
)
} }
} }

View file

@ -14,6 +14,12 @@ impl Render for IconButtonStory {
.child(Story::title_for::<IconButton>()) .child(Story::title_for::<IconButton>())
.child(Story::label("Default")) .child(Story::label("Default"))
.child(div().w_8().child(IconButton::new("icon_a", Icon::Hash))) .child(div().w_8().child(IconButton::new("icon_a", Icon::Hash)))
.child(Story::label("Selected"))
.child(
div()
.w_8()
.child(IconButton::new("icon_a", Icon::Hash).selected(true)),
)
.child(Story::label("With `on_click`")) .child(Story::label("With `on_click`"))
.child( .child(
div() div()

View file

@ -8,6 +8,5 @@ pub use crate::clickable::*;
pub use crate::disableable::*; pub use crate::disableable::*;
pub use crate::fixed::*; pub use crate::fixed::*;
pub use crate::selectable::*; pub use crate::selectable::*;
pub use crate::StyledExt; pub use crate::{ButtonCommon, Color, StyledExt};
pub use crate::{ButtonVariant, Color};
pub use theme::ActiveTheme; pub use theme::ActiveTheme;

View file

@ -701,11 +701,6 @@ impl Render for PanelButtons {
(action, name.into()) (action, name.into())
}; };
let button = IconButton::new(name, icon)
.selected(is_active_button)
.action(action.boxed_clone())
.tooltip(move |cx| Tooltip::for_action(tooltip.clone(), &*action, cx));
Some( Some(
menu_handle(name) menu_handle(name)
.menu(move |cx| { .menu(move |cx| {
@ -731,7 +726,14 @@ impl Render for PanelButtons {
}) })
.anchor(menu_anchor) .anchor(menu_anchor)
.attach(menu_attach) .attach(menu_attach)
.child(|is_open| button.selected(is_open)), .child(move |_is_open| {
IconButton::new(name, icon)
.selected(is_active_button)
.action(action.boxed_clone())
.tooltip(move |cx| {
Tooltip::for_action(tooltip.clone(), &*action, cx)
})
}),
) )
}); });

View file

@ -181,6 +181,7 @@ pub mod simple_message_notification {
}; };
use serde::Deserialize; use serde::Deserialize;
use std::{borrow::Cow, sync::Arc}; use std::{borrow::Cow, sync::Arc};
use ui::prelude::*;
use ui::{h_stack, v_stack, Button, Icon, IconElement, Label, StyledExt}; use ui::{h_stack, v_stack, Button, Icon, IconElement, Label, StyledExt};
#[derive(Clone, Default, Deserialize, PartialEq)] #[derive(Clone, Default, Deserialize, PartialEq)]
@ -287,12 +288,14 @@ pub mod simple_message_notification {
), ),
) )
.children(self.click_message.iter().map(|message| { .children(self.click_message.iter().map(|message| {
Button::new(message.clone()).on_click(cx.listener(|this, _, cx| { Button::new(message.clone(), message.clone()).on_click(cx.listener(
|this, _, cx| {
if let Some(on_click) = this.on_click.as_ref() { if let Some(on_click) = this.on_click.as_ref() {
(on_click)(cx) (on_click)(cx)
}; };
this.dismiss(cx) this.dismiss(cx)
})) },
))
})) }))
} }
} }

View file

@ -71,14 +71,14 @@ impl Render for StatusBar {
div() div()
.border() .border()
.border_color(gpui::red()) .border_color(gpui::red())
.child(Button::new("15:22")), .child(Button::new("status_line_column_numbers", "15:22")),
) )
.child( .child(
// TODO: Language picker // TODO: Language picker
div() div()
.border() .border()
.border_color(gpui::red()) .border_color(gpui::red())
.child(Button::new("Rust")), .child(Button::new("status_buffer_language", "Rust")),
), ),
) )
.child( .child(

View file

@ -94,9 +94,9 @@ impl Render for Toolbar {
.border() .border()
.border_color(gpui::red()) .border_color(gpui::red())
.p_1() .p_1()
.child(Button::new("crates")) .child(Button::new("breadcrumb_crates", "crates"))
.child(Label::new("/").color(Color::Muted)) .child(Label::new("/").color(Color::Muted))
.child(Button::new("workspace2")), .child(Button::new("breadcrumb_workspace2", "workspace2")),
) )
// Toolbar right side // Toolbar right side
.child( .child(