diff --git a/crates/feedback2/src/deploy_feedback_button.rs b/crates/feedback2/src/deploy_feedback_button.rs index e5884cf9b1..3b28c64dc3 100644 --- a/crates/feedback2/src/deploy_feedback_button.rs +++ b/crates/feedback2/src/deploy_feedback_button.rs @@ -32,7 +32,7 @@ impl Render for DeployFeedbackButton { IconButton::new("give-feedback", Icon::Envelope) .style(ui::ButtonStyle::Subtle) .selected(is_open) - .tooltip(|cx| Tooltip::text("Give Feedback", cx)) + .tooltip(|cx| Tooltip::text("Share Feedback", cx)) .on_click(|_, cx| { cx.dispatch_action(Box::new(GiveFeedback)); }) diff --git a/crates/feedback2/src/feedback_modal.rs b/crates/feedback2/src/feedback_modal.rs index 583d31ce5d..23d8c38626 100644 --- a/crates/feedback2/src/feedback_modal.rs +++ b/crates/feedback2/src/feedback_modal.rs @@ -6,15 +6,15 @@ use db::kvp::KEY_VALUE_STORE; use editor::{Editor, EditorEvent}; use futures::AsyncReadExt; use gpui::{ - div, red, rems, serde_json, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, - FocusableView, Model, PromptLevel, Render, Task, View, ViewContext, + div, rems, serde_json, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, + Model, PromptLevel, Render, Task, View, ViewContext, }; use isahc::Request; use language::Buffer; use project::Project; use regex::Regex; use serde_derive::Serialize; -use ui::{prelude::*, Button, ButtonStyle, Label, Tooltip}; +use ui::{prelude::*, Button, ButtonStyle, IconPosition, Tooltip}; use util::ResultExt; use workspace::Workspace; @@ -104,6 +104,11 @@ impl FeedbackModal { let feedback_editor = cx.build_view(|cx| { let mut editor = Editor::for_buffer(buffer, Some(project.clone()), cx); + editor.set_placeholder_text( + "You can use markdown to add links or organize feedback.", + cx, + ); + // editor.set_show_gutter(false, cx); editor.set_vertical_scroll_margin(5, cx); editor }); @@ -251,12 +256,6 @@ impl Render for FeedbackModal { }; let valid_character_count = FEEDBACK_CHAR_LIMIT.contains(&self.character_count); - let characters_remaining = - if valid_character_count || self.character_count > *FEEDBACK_CHAR_LIMIT.end() { - *FEEDBACK_CHAR_LIMIT.end() as i32 - self.character_count as i32 - } else { - self.character_count as i32 - *FEEDBACK_CHAR_LIMIT.start() as i32 - }; let allow_submission = valid_character_count && valid_email_address && !self.pending_submission; @@ -264,9 +263,9 @@ impl Render for FeedbackModal { let has_feedback = self.feedback_editor.read(cx).text_option(cx).is_some(); let submit_button_text = if self.pending_submission { - "Sending..." + "Submitting..." } else { - "Send Feedback" + "Submit" }; let dismiss = cx.listener(|_, _, cx| { cx.emit(DismissEvent); @@ -285,102 +284,121 @@ impl Render for FeedbackModal { let open_community_repo = cx.listener(|_, _, cx| cx.dispatch_action(Box::new(OpenZedCommunityRepo))); - // TODO: Nate UI pass + // Moved this here because providing it inline breaks rustfmt + let provide_an_email_address = + "Provide an email address if you want us to be able to reply."; + v_stack() .elevation_3(cx) .key_context("GiveFeedback") .on_action(cx.listener(Self::cancel)) .min_w(rems(40.)) .max_w(rems(96.)) - .border() - .border_color(red()) - .h(rems(40.)) - .p_2() - .gap_2() + .h(rems(32.)) + .p_4() + .gap_4() + .child(v_stack().child( + // TODO: Add Headline component to `ui2` + div().text_xl().child("Share Feedback"), + )) .child( - v_stack().child( - div() - .size_full() - .child(Label::new("Give Feedback").color(Color::Default)) - .child(Label::new("This editor supports markdown").color(Color::Muted)), - ), + Label::new(if self.character_count < *FEEDBACK_CHAR_LIMIT.start() { + format!( + "Feedback must be at least {} characters.", + FEEDBACK_CHAR_LIMIT.start() + ) + } else if self.character_count > *FEEDBACK_CHAR_LIMIT.end() { + format!( + "Feedback must be less than {} characters.", + FEEDBACK_CHAR_LIMIT.end() + ) + } else { + format!( + "Characters: {}", + *FEEDBACK_CHAR_LIMIT.end() - self.character_count + ) + }) + .color(if valid_character_count { + Color::Success + } else { + Color::Error + }), ) .child( div() .flex_1() .bg(cx.theme().colors().editor_background) + .p_2() .border() + .rounded_md() .border_color(cx.theme().colors().border) .child(self.feedback_editor.clone()), ) - .child( - div().child( - Label::new(format!( - "Characters: {}", - characters_remaining - )) - .color( - if valid_character_count { - Color::Success - } else { - Color::Error - } - ) - ), - ) .child( div() - .bg(cx.theme().colors().editor_background) - .border() - .border_color(cx.theme().colors().border) - .child(self.email_address_editor.clone()) - ) - .child( - h_stack() - .justify_between() - .gap_1() - .child(Button::new("community_repo", "Community Repo") - .style(ButtonStyle::Filled) - .color(Color::Muted) - .on_click(open_community_repo) + .child( + h_stack() + .bg(cx.theme().colors().editor_background) + .p_2() + .border() + .rounded_md() + .border_color(cx.theme().colors().border) + .child(self.email_address_editor.clone()), ) - .child(h_stack().justify_between().gap_1() - .child( - Button::new("cancel_feedback", "Cancel") - .style(ButtonStyle::Subtle) - .color(Color::Muted) - // TODO: replicate this logic when clicking outside the modal - // TODO: Will require somehow overriding the modal dismal default behavior - .map(|this| { - if has_feedback { - this.on_click(dismiss_prompt) - } else { - this.on_click(dismiss) - } - }) - ) - .child( - Button::new("send_feedback", submit_button_text) - .color(Color::Accent) - .style(ButtonStyle::Filled) - // TODO: Ensure that while submitting, "Sending..." is shown and disable the button - // TODO: If submit errors: show popup with error, don't close modal, set text back to "Send Feedback", and re-enable button - // TODO: If submit is successful, close the modal - .on_click(cx.listener(|this, _, cx| { - let _ = this.submit(cx); - })) - .tooltip(|cx| { - Tooltip::with_meta( - "Submit feedback to the Zed team.", - None, - "Provide an email address if you want us to be able to reply.", - cx, + .child( + h_stack() + .justify_between() + .gap_1() + .child( + Button::new("community_repo", "Community Repo") + .style(ButtonStyle::Transparent) + .icon(Icon::ExternalLink) + .icon_position(IconPosition::End) + .icon_size(IconSize::Small) + .on_click(open_community_repo), + ) + .child( + h_stack() + .gap_1() + .child( + Button::new("cancel_feedback", "Cancel") + .style(ButtonStyle::Subtle) + .color(Color::Muted) + // TODO: replicate this logic when clicking outside the modal + // TODO: Will require somehow overriding the modal dismal default behavior + .map(|this| { + if has_feedback { + this.on_click(dismiss_prompt) + } else { + this.on_click(dismiss) + } + }), ) - }) - .when(!allow_submission, |this| this.disabled(true)) - ), - ) - + .child( + Button::new("send_feedback", submit_button_text) + .color(Color::Accent) + .style(ButtonStyle::Filled) + // TODO: Ensure that while submitting, "Sending..." is shown and disable the button + // TODO: If submit errors: show popup with error, don't close modal, set text back to "Submit", and re-enable button + // TODO: If submit is successful, close the modal + .on_click(cx.listener(|this, _, cx| { + let _ = this.submit(cx); + })) + .tooltip(move |cx| { + Tooltip::with_meta( + "Submit feedback to the Zed team.", + None, + provide_an_email_address, + cx, + ) + }) + .when(!allow_submission, |this| this.disabled(true)), + ), + ), + ), ) } } + +// TODO: Add compilation flags to enable debug mode, where we can simulate sending feedback that both succeeds and fails, so we can test the UI +// TODO: Maybe store email address whenever the modal is closed, versus just on submit, so users can remove it if they want without submitting diff --git a/crates/gpui2/src/styled.rs b/crates/gpui2/src/styled.rs index 346c1a760d..209169a9a6 100644 --- a/crates/gpui2/src/styled.rs +++ b/crates/gpui2/src/styled.rs @@ -245,6 +245,13 @@ pub trait Styled: Sized { self } + /// Sets the flex direction of the element to `column-reverse`. + /// [Docs](https://tailwindcss.com/docs/flex-direction#column-reverse) + fn flex_col_reverse(mut self) -> Self { + self.style().flex_direction = Some(FlexDirection::ColumnReverse); + self + } + /// Sets the flex direction of the element to `row`. /// [Docs](https://tailwindcss.com/docs/flex-direction#row) fn flex_row(mut self) -> Self { @@ -252,6 +259,13 @@ pub trait Styled: Sized { self } + /// Sets the flex direction of the element to `row-reverse`. + /// [Docs](https://tailwindcss.com/docs/flex-direction#row-reverse) + fn flex_row_reverse(mut self) -> Self { + self.style().flex_direction = Some(FlexDirection::RowReverse); + self + } + /// Sets the element to allow a flex item to grow and shrink as needed, ignoring its initial size. /// [Docs](https://tailwindcss.com/docs/flex#flex-1) fn flex_1(mut self) -> Self { diff --git a/crates/ui2/src/components/button/button.rs b/crates/ui2/src/components/button/button.rs index c1262321ce..fc7ca2c128 100644 --- a/crates/ui2/src/components/button/button.rs +++ b/crates/ui2/src/components/button/button.rs @@ -1,6 +1,6 @@ use gpui::{AnyView, DefiniteLength}; -use crate::prelude::*; +use crate::{prelude::*, IconPosition}; use crate::{ ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, IconSize, Label, LineHeightStyle, }; @@ -14,6 +14,7 @@ pub struct Button { label_color: Option, selected_label: Option, icon: Option, + icon_position: Option, icon_size: Option, icon_color: Option, selected_icon: Option, @@ -27,6 +28,7 @@ impl Button { label_color: None, selected_label: None, icon: None, + icon_position: None, icon_size: None, icon_color: None, selected_icon: None, @@ -48,6 +50,11 @@ impl Button { self } + pub fn icon_position(mut self, icon_position: impl Into>) -> Self { + self.icon_position = icon_position.into(); + self + } + pub fn icon_size(mut self, icon_size: impl Into>) -> Self { self.icon_size = icon_size.into(); self @@ -141,19 +148,29 @@ impl RenderOnce for Button { self.label_color.unwrap_or_default() }; - self.base - .children(self.icon.map(|icon| { - ButtonIcon::new(icon) - .disabled(is_disabled) - .selected(is_selected) - .selected_icon(self.selected_icon) - .size(self.icon_size) - .color(self.icon_color) - })) - .child( - Label::new(label) - .color(label_color) - .line_height_style(LineHeightStyle::UILabel), - ) + self.base.child( + h_stack() + .gap_1() + .map(|this| { + if self.icon_position == Some(IconPosition::End) { + this.flex_row_reverse() + } else { + this + } + }) + .child( + Label::new(label) + .color(label_color) + .line_height_style(LineHeightStyle::UILabel), + ) + .children(self.icon.map(|icon| { + ButtonIcon::new(icon) + .disabled(is_disabled) + .selected(is_selected) + .selected_icon(self.selected_icon) + .size(self.icon_size) + .color(self.icon_color) + })), + ) } } diff --git a/crates/ui2/src/components/button/button_like.rs b/crates/ui2/src/components/button/button_like.rs index 74dfdc45d8..7203b3494f 100644 --- a/crates/ui2/src/components/button/button_like.rs +++ b/crates/ui2/src/components/button/button_like.rs @@ -30,6 +30,13 @@ pub trait ButtonCommon: Clickable + Disableable { fn tooltip(self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self; } +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] +pub enum IconPosition { + #[default] + Start, + End, +} + #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] pub enum ButtonStyle { /// A filled button with a solid background color. Provides emphasis versus @@ -347,6 +354,7 @@ impl RenderOnce for ButtonLike { ButtonSize::None => this, }) .bg(self.style.enabled(cx).background) + .when(self.disabled, |this| this.cursor_not_allowed()) .when(!self.disabled, |this| { this.cursor_pointer() .hover(|hover| hover.bg(self.style.hovered(cx).background)) diff --git a/crates/ui2/src/components/icon.rs b/crates/ui2/src/components/icon.rs index f534e04b68..8cea775aed 100644 --- a/crates/ui2/src/components/icon.rs +++ b/crates/ui2/src/components/icon.rs @@ -51,6 +51,7 @@ pub enum Icon { CopilotDisabled, Dash, Envelope, + ExternalLink, ExclamationTriangle, Exit, File, @@ -123,13 +124,13 @@ impl Icon { Icon::Close => "icons/x.svg", Icon::Collab => "icons/user_group_16.svg", Icon::Copilot => "icons/copilot.svg", - Icon::CopilotInit => "icons/copilot_init.svg", Icon::CopilotError => "icons/copilot_error.svg", Icon::CopilotDisabled => "icons/copilot_disabled.svg", Icon::Dash => "icons/dash.svg", Icon::Envelope => "icons/feedback.svg", Icon::ExclamationTriangle => "icons/warning.svg", + Icon::ExternalLink => "icons/external_link.svg", Icon::Exit => "icons/exit.svg", Icon::File => "icons/file.svg", Icon::FileDoc => "icons/file_icons/book.svg", diff --git a/crates/ui2/src/prelude.rs b/crates/ui2/src/prelude.rs index 42fb44ed4d..076d34644c 100644 --- a/crates/ui2/src/prelude.rs +++ b/crates/ui2/src/prelude.rs @@ -12,6 +12,6 @@ pub use crate::selectable::*; pub use crate::{h_stack, v_stack}; pub use crate::{Button, ButtonSize, ButtonStyle, IconButton}; pub use crate::{ButtonCommon, Color, StyledExt}; -pub use crate::{Icon, IconElement, IconSize}; +pub use crate::{Icon, IconElement, IconPosition, IconSize}; pub use crate::{Label, LabelCommon, LabelSize, LineHeightStyle}; pub use theme::ActiveTheme; diff --git a/crates/workspace2/src/status_bar.rs b/crates/workspace2/src/status_bar.rs index 07c48293b5..d198b0485e 100644 --- a/crates/workspace2/src/status_bar.rs +++ b/crates/workspace2/src/status_bar.rs @@ -5,8 +5,8 @@ use gpui::{ div, AnyView, Div, IntoElement, ParentElement, Render, Styled, Subscription, View, ViewContext, WindowContext, }; +use ui::h_stack; use ui::prelude::*; -use ui::{h_stack, Icon, IconButton}; use util::ResultExt; pub trait StatusItemView: Render { @@ -48,30 +48,7 @@ impl Render for StatusBar { .h_8() .bg(cx.theme().colors().status_bar_background) .child(h_stack().gap_1().child(self.render_left_tools(cx))) - .child( - h_stack() - .gap_4() - .child( - h_stack().gap_1().child( - // Feedback Tool - div() - .border() - .border_color(gpui::red()) - .child(IconButton::new("status-feedback", Icon::Envelope)), - ), - ) - .child( - // Right Dock - h_stack().gap_1().child( - // Terminal - div() - .border() - .border_color(gpui::red()) - .child(IconButton::new("status-chat", Icon::MessageBubbles)), - ), - ) - .child(self.render_right_tools(cx)), - ) + .child(h_stack().gap_4().child(self.render_right_tools(cx))) } }