Add placeholder text as a feature of Editor, use it in chat panel

Max Brunsfeld created

Change summary

gpui/src/fonts.rs             |  2 
zed/assets/themes/_base.toml  |  1 
zed/src/chat_panel.rs         |  9 +++++++
zed/src/editor.rs             | 41 +++++++++++++++++++++++++++++++++++++
zed/src/editor/display_map.rs |  4 +++
zed/src/theme.rs              |  8 +++++++
6 files changed, 63 insertions(+), 2 deletions(-)

Detailed changes

gpui/src/fonts.rs 🔗

@@ -26,7 +26,7 @@ pub struct TextStyle {
     pub font_properties: Properties,
 }
 
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, Default)]
 pub struct HighlightStyle {
     pub color: Color,
     pub font_properties: Properties,

zed/assets/themes/_base.toml 🔗

@@ -67,6 +67,7 @@ background = "$surface.0"
 
 [chat_panel.input_editor]
 text = "$text.1.color"
+placeholder_text = "$text.2.color"
 background = "$surface.1"
 selection = "$selection.host"
 

zed/src/chat_panel.rs 🔗

@@ -156,7 +156,14 @@ impl ChatPanel {
 
     fn set_active_channel(&mut self, channel: ModelHandle<Channel>, cx: &mut ViewContext<Self>) {
         if self.active_channel.as_ref().map(|e| &e.0) != Some(&channel) {
-            self.message_list.reset(channel.read(cx).message_count());
+            {
+                let channel = channel.read(cx);
+                self.message_list.reset(channel.message_count());
+                let placeholder = format!("Message #{}", channel.name());
+                self.input_editor.update(cx, move |editor, cx| {
+                    editor.set_placeholder_text(placeholder, cx);
+                });
+            }
             let subscription = cx.subscribe(&channel, Self::channel_did_change);
             self.active_channel = Some((channel, subscription));
         }

zed/src/editor.rs 🔗

@@ -295,15 +295,18 @@ pub struct Editor {
     blink_epoch: usize,
     blinking_paused: bool,
     mode: EditorMode,
+    placeholder_text: Option<Arc<str>>,
 }
 
 pub struct Snapshot {
     pub display_snapshot: DisplayMapSnapshot,
+    pub placeholder_text: Option<Arc<str>>,
     pub gutter_visible: bool,
     pub auto_height: bool,
     pub theme: Arc<Theme>,
     pub font_family: FamilyId,
     pub font_size: f32,
+    is_focused: bool,
     scroll_position: Vector2F,
     scroll_top_anchor: Anchor,
 }
@@ -378,6 +381,7 @@ impl Editor {
             blink_epoch: 0,
             blinking_paused: false,
             mode: EditorMode::Full,
+            placeholder_text: None,
         }
     }
 
@@ -407,11 +411,25 @@ impl Editor {
             scroll_position: self.scroll_position,
             scroll_top_anchor: self.scroll_top_anchor.clone(),
             theme: settings.theme.clone(),
+            placeholder_text: self.placeholder_text.clone(),
             font_family: settings.buffer_font_family,
             font_size: settings.buffer_font_size,
+            is_focused: self
+                .handle
+                .upgrade(cx)
+                .map_or(false, |handle| handle.is_focused(cx)),
         }
     }
 
+    pub fn set_placeholder_text(
+        &mut self,
+        placeholder_text: impl Into<Arc<str>>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.placeholder_text = Some(placeholder_text.into());
+        cx.notify();
+    }
+
     fn set_scroll_position(&mut self, mut scroll_position: Vector2F, cx: &mut ViewContext<Self>) {
         let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let scroll_top_buffer_offset =
@@ -2406,6 +2424,29 @@ impl Snapshot {
             return Ok(Vec::new());
         }
 
+        // When the editor is empty and unfocused, then show the placeholder.
+        if self.display_snapshot.is_empty() && !self.is_focused {
+            let placeholder_lines = self
+                .placeholder_text
+                .as_ref()
+                .map_or("", AsRef::as_ref)
+                .split('\n')
+                .skip(rows.start as usize)
+                .take(rows.len());
+            let font_id = font_cache
+                .select_font(self.font_family, &style.placeholder_text.font_properties)?;
+            return Ok(placeholder_lines
+                .into_iter()
+                .map(|line| {
+                    layout_cache.layout_str(
+                        line,
+                        self.font_size,
+                        &[(line.len(), font_id, style.placeholder_text.color)],
+                    )
+                })
+                .collect());
+        }
+
         let mut prev_font_properties = FontProperties::new();
         let mut prev_font_id = font_cache
             .select_font(self.font_family, &prev_font_properties)

zed/src/editor/display_map.rs 🔗

@@ -108,6 +108,10 @@ impl DisplayMapSnapshot {
         self.folds_snapshot.fold_count()
     }
 
+    pub fn is_empty(&self) -> bool {
+        self.buffer_snapshot.len() == 0
+    }
+
     pub fn buffer_rows(&self, start_row: u32) -> BufferRows {
         self.wraps_snapshot.buffer_rows(start_row)
     }

zed/src/theme.rs 🔗

@@ -131,6 +131,8 @@ pub struct ContainedLabel {
 #[derive(Clone, Deserialize)]
 pub struct EditorStyle {
     pub text: HighlightStyle,
+    #[serde(default)]
+    pub placeholder_text: HighlightStyle,
     pub background: Color,
     pub selection: SelectionStyle,
     pub gutter_background: Color,
@@ -143,6 +145,7 @@ pub struct EditorStyle {
 #[derive(Clone, Deserialize)]
 pub struct InputEditorStyle {
     pub text: HighlightStyle,
+    pub placeholder_text: HighlightStyle,
     pub background: Color,
     pub selection: SelectionStyle,
 }
@@ -177,6 +180,10 @@ impl Default for EditorStyle {
                 color: Color::from_u32(0xff0000ff),
                 font_properties: Default::default(),
             },
+            placeholder_text: HighlightStyle {
+                color: Color::from_u32(0x00ff00ff),
+                font_properties: Default::default(),
+            },
             background: Default::default(),
             gutter_background: Default::default(),
             active_line_background: Default::default(),
@@ -192,6 +199,7 @@ impl InputEditorStyle {
     pub fn as_editor(&self) -> EditorStyle {
         EditorStyle {
             text: self.text.clone(),
+            placeholder_text: self.placeholder_text.clone(),
             background: self.background,
             selection: self.selection,
             ..Default::default()