Add Text::styled() and use it in command palette

Conrad Irwin created

Prevents jumping while typing

Change summary

crates/gpui2/src/elements/text.rs  | 29 +++++++++++++++++++++++++++--
crates/gpui2/src/text_system.rs    |  1 +
crates/ui2/src/components/label.rs | 27 +++++++++++++--------------
3 files changed, 41 insertions(+), 16 deletions(-)

Detailed changes

crates/gpui2/src/elements/text.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
     AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, Line, Pixels, SharedString,
-    Size, ViewContext,
+    Size, TextRun, ViewContext,
 };
 use parking_lot::Mutex;
 use smallvec::SmallVec;
@@ -11,6 +11,7 @@ impl<V: 'static> Component<V> for SharedString {
     fn render(self) -> AnyElement<V> {
         Text {
             text: self,
+            runs: None,
             state_type: PhantomData,
         }
         .render()
@@ -21,6 +22,7 @@ impl<V: 'static> Component<V> for &'static str {
     fn render(self) -> AnyElement<V> {
         Text {
             text: self.into(),
+            runs: None,
             state_type: PhantomData,
         }
         .render()
@@ -33,6 +35,7 @@ impl<V: 'static> Component<V> for String {
     fn render(self) -> AnyElement<V> {
         Text {
             text: self.into(),
+            runs: None,
             state_type: PhantomData,
         }
         .render()
@@ -41,9 +44,25 @@ impl<V: 'static> Component<V> for String {
 
 pub struct Text<V> {
     text: SharedString,
+    runs: Option<Vec<TextRun>>,
     state_type: PhantomData<V>,
 }
 
+impl<V: 'static> Text<V> {
+    /// styled renders text that has different runs of different styles.
+    /// callers are responsible for setting the correct style for each run.
+    ////
+    /// For uniform text you can usually just pass a string as a child, and
+    /// cx.text_style() will be used automatically.
+    pub fn styled(text: SharedString, runs: Vec<TextRun>) -> Self {
+        Text {
+            text,
+            runs: Some(runs),
+            state_type: Default::default(),
+        }
+    }
+}
+
 impl<V: 'static> Component<V> for Text<V> {
     fn render(self) -> AnyElement<V> {
         AnyElement::new(self)
@@ -82,6 +101,12 @@ impl<V: 'static> Element<V> for Text<V> {
 
         let rem_size = cx.rem_size();
 
+        let runs = if let Some(runs) = self.runs.take() {
+            runs
+        } else {
+            vec![text_style.to_run(text.len())]
+        };
+
         let layout_id = cx.request_measured_layout(Default::default(), rem_size, {
             let element_state = element_state.clone();
             move |known_dimensions, _| {
@@ -89,7 +114,7 @@ impl<V: 'static> Element<V> for Text<V> {
                     .layout_text(
                         &text,
                         font_size,
-                        &[text_style.to_run(text.len())],
+                        &runs[..],
                         known_dimensions.width, // Wrap if we know the width.
                     )
                     .log_err()

crates/gpui2/src/text_system.rs 🔗

@@ -368,6 +368,7 @@ impl Display for FontStyle {
 
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub struct TextRun {
+    // number of utf8 bytes
     pub len: usize,
     pub font: Font,
     pub color: Hsla,

crates/ui2/src/components/label.rs 🔗

@@ -1,5 +1,4 @@
-use gpui::{relative, Hsla, WindowContext};
-use smallvec::SmallVec;
+use gpui::{relative, Hsla, Text, TextRun, WindowContext};
 
 use crate::prelude::*;
 use crate::styled_ext::StyledExt;
@@ -105,6 +104,8 @@ pub struct HighlightedLabel {
 }
 
 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(),
@@ -126,10 +127,11 @@ impl HighlightedLabel {
 
     fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let highlight_color = cx.theme().colors().text_accent;
+        let mut text_style = cx.text_style().clone();
 
         let mut highlight_indices = self.highlight_indices.iter().copied().peekable();
 
-        let mut runs: SmallVec<[Run; 8]> = SmallVec::new();
+        let mut runs: Vec<TextRun> = Vec::new();
 
         for (char_ix, char) in self.label.char_indices() {
             let mut color = self.color.hsla(cx);
@@ -137,16 +139,14 @@ impl HighlightedLabel {
             if let Some(highlight_ix) = highlight_indices.peek() {
                 if char_ix == *highlight_ix {
                     color = highlight_color;
-
                     highlight_indices.next();
                 }
             }
 
             let last_run = runs.last_mut();
-
             let start_new_run = if let Some(last_run) = last_run {
                 if color == last_run.color {
-                    last_run.text.push(char);
+                    last_run.len += char.len_utf8();
                     false
                 } else {
                     true
@@ -156,10 +156,8 @@ impl HighlightedLabel {
             };
 
             if start_new_run {
-                runs.push(Run {
-                    text: char.to_string(),
-                    color,
-                });
+                text_style.color = color;
+                runs.push(text_style.to_run(char.len_utf8()))
             }
         }
 
@@ -176,10 +174,7 @@ impl HighlightedLabel {
                         .bg(LabelColor::Hidden.hsla(cx)),
                 )
             })
-            .children(
-                runs.into_iter()
-                    .map(|run| div().text_color(run.color).child(run.text)),
-            )
+            .child(Text::styled(self.label, runs))
     }
 }
 
@@ -213,6 +208,10 @@ mod stories {
                     "Hello, world!",
                     vec![0, 1, 2, 7, 8, 12],
                 ))
+                .child(HighlightedLabel::new(
+                    "Héllo, world!",
+                    vec![0, 1, 3, 8, 9, 13],
+                ))
         }
     }
 }