Merge pull request #2133 from zed-industries/feedback-submit-button

Joseph T. Lyons created

Implement a button for submitting feedback

Change summary

crates/feedback/src/feedback_editor.rs | 98 +++++++++++++++++++++++----
crates/theme/src/theme.rs              |  7 ++
crates/theme/src/theme_registry.rs     |  6 
crates/zed/src/zed.rs                  |  5 +
styles/src/styleTree/app.ts            |  2 
styles/src/styleTree/feedback.ts       | 37 ++++++++++
6 files changed, 135 insertions(+), 20 deletions(-)

Detailed changes

crates/feedback/src/feedback_editor.rs 🔗

@@ -25,7 +25,7 @@ use settings::Settings;
 use workspace::{
     item::{Item, ItemHandle},
     searchable::{SearchableItem, SearchableItemHandle},
-    AppState, StatusItemView, Workspace,
+    AppState, StatusItemView, ToolbarItemLocation, ToolbarItemView, Workspace,
 };
 
 use crate::system_specs::SystemSpecs;
@@ -35,7 +35,7 @@ const FEEDBACK_PLACEHOLDER_TEXT: &str = "Save to submit feedback as Markdown.";
 const FEEDBACK_SUBMISSION_ERROR_TEXT: &str =
     "Feedback failed to submit, see error log for details.";
 
-actions!(feedback, [SubmitFeedback, GiveFeedback, DeployFeedback]);
+actions!(feedback, [GiveFeedback, SubmitFeedback]);
 
 pub fn init(system_specs: SystemSpecs, app_state: Arc<AppState>, cx: &mut MutableAppContext) {
     cx.add_action({
@@ -43,17 +43,27 @@ pub fn init(system_specs: SystemSpecs, app_state: Arc<AppState>, cx: &mut Mutabl
             FeedbackEditor::deploy(system_specs.clone(), workspace, app_state.clone(), cx);
         }
     });
+
+    cx.add_async_action(
+        |submit_feedback_button: &mut SubmitFeedbackButton, _: &SubmitFeedback, cx| {
+            if let Some(active_item) = submit_feedback_button.active_item.as_ref() {
+                Some(active_item.update(cx, |feedback_editor, cx| feedback_editor.handle_save(cx)))
+            } else {
+                None
+            }
+        },
+    );
 }
 
-pub struct FeedbackButton;
+pub struct DeployFeedbackButton;
 
-impl Entity for FeedbackButton {
+impl Entity for DeployFeedbackButton {
     type Event = ();
 }
 
-impl View for FeedbackButton {
+impl View for DeployFeedbackButton {
     fn ui_name() -> &'static str {
-        "FeedbackButton"
+        "DeployFeedbackButton"
     }
 
     fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
@@ -77,7 +87,7 @@ impl View for FeedbackButton {
     }
 }
 
-impl StatusItemView for FeedbackButton {
+impl StatusItemView for DeployFeedbackButton {
     fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
 }
 
@@ -120,11 +130,7 @@ impl FeedbackEditor {
         }
     }
 
-    fn handle_save(
-        &mut self,
-        _: ModelHandle<Project>,
-        cx: &mut ViewContext<Self>,
-    ) -> Task<anyhow::Result<()>> {
+    fn handle_save(&mut self, cx: &mut ViewContext<Self>) -> Task<anyhow::Result<()>> {
         let feedback_text = self.editor.read(cx).text(cx);
         let feedback_char_count = feedback_text.chars().count();
         let feedback_text = feedback_text.trim().to_string();
@@ -304,19 +310,19 @@ impl Item for FeedbackEditor {
 
     fn save(
         &mut self,
-        project: ModelHandle<Project>,
+        _: ModelHandle<Project>,
         cx: &mut ViewContext<Self>,
     ) -> Task<anyhow::Result<()>> {
-        self.handle_save(project, cx)
+        self.handle_save(cx)
     }
 
     fn save_as(
         &mut self,
-        project: ModelHandle<Project>,
+        _: ModelHandle<Project>,
         _: std::path::PathBuf,
         cx: &mut ViewContext<Self>,
     ) -> Task<anyhow::Result<()>> {
-        self.handle_save(project, cx)
+        self.handle_save(cx)
     }
 
     fn reload(
@@ -435,3 +441,63 @@ impl SearchableItem for FeedbackEditor {
             .update(cx, |editor, cx| editor.active_match_index(matches, cx))
     }
 }
+
+pub struct SubmitFeedbackButton {
+    active_item: Option<ViewHandle<FeedbackEditor>>,
+}
+
+impl SubmitFeedbackButton {
+    pub fn new() -> Self {
+        Self {
+            active_item: Default::default(),
+        }
+    }
+}
+
+impl Entity for SubmitFeedbackButton {
+    type Event = ();
+}
+
+impl View for SubmitFeedbackButton {
+    fn ui_name() -> &'static str {
+        "SubmitFeedbackButton"
+    }
+
+    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+        let theme = cx.global::<Settings>().theme.clone();
+        enum SubmitFeedbackButton {}
+        MouseEventHandler::<SubmitFeedbackButton>::new(0, cx, |state, _| {
+            let style = theme.feedback.submit_button.style_for(state, false);
+            Label::new("Submit as Markdown".into(), style.text.clone())
+                .contained()
+                .with_style(style.container)
+                .boxed()
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .on_click(MouseButton::Left, |_, cx| {
+            cx.dispatch_action(SubmitFeedback)
+        })
+        .aligned()
+        .contained()
+        .with_margin_left(theme.feedback.button_margin)
+        .boxed()
+    }
+}
+
+impl ToolbarItemView for SubmitFeedbackButton {
+    fn set_active_pane_item(
+        &mut self,
+        active_pane_item: Option<&dyn ItemHandle>,
+        cx: &mut ViewContext<Self>,
+    ) -> workspace::ToolbarItemLocation {
+        cx.notify();
+        if let Some(feedback_editor) = active_pane_item.and_then(|i| i.downcast::<FeedbackEditor>())
+        {
+            self.active_item = Some(feedback_editor);
+            ToolbarItemLocation::PrimaryRight { flex: None }
+        } else {
+            self.active_item = None;
+            ToolbarItemLocation::Hidden
+        }
+    }
+}

crates/theme/src/theme.rs 🔗

@@ -36,6 +36,7 @@ pub struct Theme {
     pub incoming_call_notification: IncomingCallNotification,
     pub tooltip: TooltipStyle,
     pub terminal: TerminalStyle,
+    pub feedback: FeedbackStyle,
     pub color_scheme: ColorScheme,
 }
 
@@ -806,6 +807,12 @@ pub struct TerminalStyle {
     pub dim_foreground: Color,
 }
 
+#[derive(Clone, Deserialize, Default)]
+pub struct FeedbackStyle {
+    pub submit_button: Interactive<ContainedText>,
+    pub button_margin: f32,
+}
+
 #[derive(Clone, Deserialize, Default)]
 pub struct ColorScheme {
     pub name: String,

crates/theme/src/theme_registry.rs 🔗

@@ -55,13 +55,13 @@ impl ThemeRegistry {
             .load(&asset_path)
             .with_context(|| format!("failed to load theme file {}", asset_path))?;
 
-        let mut theme: Theme = fonts::with_font_cache(self.font_cache.clone(), || {
+        // Allocate into the heap directly, the Theme struct is too large to fit in the stack.
+        let mut theme: Arc<Theme> = fonts::with_font_cache(self.font_cache.clone(), || {
             serde_path_to_error::deserialize(&mut serde_json::Deserializer::from_slice(&theme_json))
         })?;
 
         // Reset name to be the file path, so that we can use it to access the stored themes
-        theme.meta.name = name.into();
-        let theme = Arc::new(theme);
+        Arc::get_mut(&mut theme).unwrap().meta.name = name.into();
         self.themes.lock().insert(name.to_string(), theme.clone());
         Ok(theme)
     }

crates/zed/src/zed.rs 🔗

@@ -11,6 +11,7 @@ use collections::VecDeque;
 pub use editor;
 use editor::{Editor, MultiBuffer};
 
+use feedback::feedback_editor::SubmitFeedbackButton;
 use futures::StreamExt;
 use gpui::{
     actions,
@@ -287,6 +288,8 @@ pub fn initialize_workspace(
                         toolbar.add_item(buffer_search_bar, cx);
                         let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
                         toolbar.add_item(project_search_bar, cx);
+                        let submit_feedback_button = cx.add_view(|_| SubmitFeedbackButton::new());
+                        toolbar.add_item(submit_feedback_button, cx);
                     })
                 });
             }
@@ -344,7 +347,7 @@ pub fn initialize_workspace(
     let activity_indicator =
         activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx);
     let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
-    let feedback_button = cx.add_view(|_| feedback::feedback_editor::FeedbackButton {});
+    let feedback_button = cx.add_view(|_| feedback::feedback_editor::DeployFeedbackButton {});
     workspace.status_bar().update(cx, |status_bar, cx| {
         status_bar.add_left_item(diagnostic_summary, cx);
         status_bar.add_left_item(activity_indicator, cx);

styles/src/styleTree/app.ts 🔗

@@ -19,6 +19,7 @@ import terminal from "./terminal";
 import contactList from "./contactList";
 import incomingCallNotification from "./incomingCallNotification";
 import { ColorScheme } from "../themes/common/colorScheme";
+import feedback from "./feedback";
 
 export default function app(colorScheme: ColorScheme): Object {
   return {
@@ -51,6 +52,7 @@ export default function app(colorScheme: ColorScheme): Object {
     simpleMessageNotification: simpleMessageNotification(colorScheme),
     tooltip: tooltip(colorScheme),
     terminal: terminal(colorScheme),
+    feedback: feedback(colorScheme),
     colorScheme: {
       ...colorScheme,
       players: Object.values(colorScheme.players),

styles/src/styleTree/feedback.ts 🔗

@@ -0,0 +1,37 @@
+
+import { ColorScheme } from "../themes/common/colorScheme";
+import { background, border, text } from "./components";
+
+export default function feedback(colorScheme: ColorScheme) {
+  let layer = colorScheme.highest;
+
+  // Currently feedback only needs style for the submit feedback button
+  return {
+    submit_button: {
+      ...text(layer, "mono", "on"),
+      background: background(layer, "on"),
+      cornerRadius: 6,
+      border: border(layer, "on"),
+      margin: {
+        right: 4,
+      },
+      padding: {
+        bottom: 2,
+        left: 10,
+        right: 10,
+        top: 2,
+      },
+      clicked: {
+        ...text(layer, "mono", "on", "pressed"),
+        background: background(layer, "on", "pressed"),
+        border: border(layer, "on", "pressed"),
+      },
+      hover: {
+        ...text(layer, "mono", "on", "hovered"),
+        background: background(layer, "on", "hovered"),
+        border: border(layer, "on", "hovered"),
+      },
+    },
+    button_margin: 8
+  };
+}