command 2 2 (#3317)

Conrad Irwin created

- Update command matches faster
- Fix action dispatching...
- Add Text::styled() and use it in command palette
- Fix SingleLine editor font size
- Fix elevation on go_to_line2
- Allow clicking on commands in the command palette

Release Notes:

- N/A

Change summary

crates/command_palette2/src/command_palette.rs | 11 ---
crates/editor2/src/editor.rs                   |  2 
crates/go_to_line2/src/go_to_line.rs           |  5 
crates/gpui2/src/elements/text.rs              | 29 ++++++++
crates/gpui2/src/interactive.rs                | 40 ++++++++++-
crates/gpui2/src/text_system.rs                |  1 
crates/gpui2/src/window.rs                     |  7 ++
crates/picker2/src/picker2.rs                  | 66 +++++++++++++++----
crates/ui2/src/components/keybinding.rs        |  2 
crates/ui2/src/components/label.rs             | 27 +++----
crates/workspace2/src/modal_layer.rs           | 25 ++++---
11 files changed, 157 insertions(+), 58 deletions(-)

Detailed changes

crates/command_palette2/src/command_palette.rs 🔗

@@ -130,16 +130,7 @@ impl CommandPaletteDelegate {
     ) -> Self {
         Self {
             command_palette,
-            matches: commands
-                .iter()
-                .enumerate()
-                .map(|(i, command)| StringMatch {
-                    candidate_id: i,
-                    string: command.name.clone(),
-                    positions: Vec::new(),
-                    score: 0.0,
-                })
-                .collect(),
+            matches: vec![],
             commands,
             selected_ix: 0,
             previous_focus_handle,

crates/editor2/src/editor.rs 🔗

@@ -9383,7 +9383,7 @@ impl Render for Editor {
                     color: cx.theme().colors().text,
                     font_family: "Zed Sans".into(), // todo!()
                     font_features: FontFeatures::default(),
-                    font_size: rems(1.0).into(),
+                    font_size: rems(0.875).into(),
                     font_weight: FontWeight::NORMAL,
                     font_style: FontStyle::Normal,
                     line_height: relative(1.3).into(), // TODO relative(settings.buffer_line_height.value()),

crates/go_to_line2/src/go_to_line.rs 🔗

@@ -5,7 +5,7 @@ use gpui::{
 };
 use text::{Bias, Point};
 use theme::ActiveTheme;
-use ui::{h_stack, modal, v_stack, Label, LabelColor};
+use ui::{h_stack, v_stack, Label, LabelColor, StyledExt};
 use util::paths::FILE_ROW_COLUMN_DELIMITER;
 use workspace::{Modal, ModalEvent, Workspace};
 
@@ -148,7 +148,8 @@ impl Render for GoToLine {
     type Element = Div<Self>;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-        modal(cx)
+        div()
+            .elevation_2(cx)
             .context("GoToLine")
             .on_action(Self::cancel)
             .on_action(Self::confirm)

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/interactive.rs 🔗

@@ -71,6 +71,40 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
         self
     }
 
+    fn on_any_mouse_down(
+        mut self,
+        handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.stateless_interactivity()
+            .mouse_down_listeners
+            .push(Box::new(move |view, event, bounds, phase, cx| {
+                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                    handler(view, event, cx)
+                }
+            }));
+        self
+    }
+
+    fn on_any_mouse_up(
+        mut self,
+        handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.stateless_interactivity()
+            .mouse_up_listeners
+            .push(Box::new(move |view, event, bounds, phase, cx| {
+                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                    handler(view, event, cx)
+                }
+            }));
+        self
+    }
+
     fn on_mouse_up(
         mut self,
         button: MouseButton,
@@ -111,7 +145,6 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
 
     fn on_mouse_up_out(
         mut self,
-        button: MouseButton,
         handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext<V>) + 'static,
     ) -> Self
     where
@@ -120,10 +153,7 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
         self.stateless_interactivity()
             .mouse_up_listeners
             .push(Box::new(move |view, event, bounds, phase, cx| {
-                if phase == DispatchPhase::Capture
-                    && event.button == button
-                    && !bounds.contains_point(&event.position)
-                {
+                if phase == DispatchPhase::Capture && !bounds.contains_point(&event.position) {
                     handler(view, event, cx);
                 }
             }));

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/gpui2/src/window.rs 🔗

@@ -101,6 +101,12 @@ pub struct FocusHandle {
     handles: Arc<RwLock<SlotMap<FocusId, AtomicUsize>>>,
 }
 
+impl std::fmt::Debug for FocusHandle {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_fmt(format_args!("FocusHandle({:?})", self.id))
+    }
+}
+
 impl FocusHandle {
     pub(crate) fn new(handles: &Arc<RwLock<SlotMap<FocusId, AtomicUsize>>>) -> Self {
         let id = handles.write().insert(AtomicUsize::new(1));
@@ -424,6 +430,7 @@ impl<'a> WindowContext<'a> {
                     .dispatch_tree
                     .focusable_node_id(focus_handle.id)
                 {
+                    cx.propagate_event = true;
                     cx.dispatch_action_on_node(node_id, action);
                 }
             })

crates/picker2/src/picker2.rs 🔗

@@ -1,7 +1,7 @@
 use editor::Editor;
 use gpui::{
-    div, uniform_list, Component, Div, ParentElement, Render, StatelessInteractive, Styled, Task,
-    UniformListScrollHandle, View, ViewContext, VisualContext, WindowContext,
+    div, uniform_list, Component, Div, MouseButton, ParentElement, Render, StatelessInteractive,
+    Styled, Task, UniformListScrollHandle, View, ViewContext, VisualContext, WindowContext,
 };
 use std::{cmp, sync::Arc};
 use ui::{prelude::*, v_stack, Divider, Label, LabelColor};
@@ -10,7 +10,8 @@ pub struct Picker<D: PickerDelegate> {
     pub delegate: D,
     scroll_handle: UniformListScrollHandle,
     editor: View<Editor>,
-    pending_update_matches: Option<Task<Option<()>>>,
+    pending_update_matches: Option<Task<()>>,
+    confirm_on_update: Option<bool>,
 }
 
 pub trait PickerDelegate: Sized + 'static {
@@ -42,12 +43,15 @@ impl<D: PickerDelegate> Picker<D> {
             editor
         });
         cx.subscribe(&editor, Self::on_input_editor_event).detach();
-        Self {
+        let mut this = Self {
             delegate,
+            editor,
             scroll_handle: UniformListScrollHandle::new(),
             pending_update_matches: None,
-            editor,
-        }
+            confirm_on_update: None,
+        };
+        this.update_matches("".to_string(), cx);
+        this
     }
 
     pub fn focus(&self, cx: &mut WindowContext) {
@@ -99,11 +103,26 @@ impl<D: PickerDelegate> Picker<D> {
     }
 
     fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
-        self.delegate.confirm(false, cx);
+        if self.pending_update_matches.is_some() {
+            self.confirm_on_update = Some(false)
+        } else {
+            self.delegate.confirm(false, cx);
+        }
     }
 
     fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
-        self.delegate.confirm(true, cx);
+        if self.pending_update_matches.is_some() {
+            self.confirm_on_update = Some(true)
+        } else {
+            self.delegate.confirm(true, cx);
+        }
+    }
+
+    fn handle_click(&mut self, ix: usize, secondary: bool, cx: &mut ViewContext<Self>) {
+        cx.stop_propagation();
+        cx.prevent_default();
+        self.delegate.set_selected_index(ix, cx);
+        self.delegate.confirm(secondary, cx);
     }
 
     fn on_input_editor_event(
@@ -126,7 +145,7 @@ impl<D: PickerDelegate> Picker<D> {
             this.update(&mut cx, |this, cx| {
                 this.matches_updated(cx);
             })
-            .ok()
+            .ok();
         }));
     }
 
@@ -134,6 +153,9 @@ impl<D: PickerDelegate> Picker<D> {
         let index = self.delegate.selected_index();
         self.scroll_handle.scroll_to_item(index);
         self.pending_update_matches = None;
+        if let Some(secondary) = self.confirm_on_update.take() {
+            self.delegate.confirm(secondary, cx);
+        }
         cx.notify();
     }
 }
@@ -171,7 +193,22 @@ impl<D: PickerDelegate> Render for Picker<D> {
                                     let selected_ix = this.delegate.selected_index();
                                     visible_range
                                         .map(|ix| {
-                                            this.delegate.render_match(ix, ix == selected_ix, cx)
+                                            div()
+                                                .on_mouse_down(
+                                                    MouseButton::Left,
+                                                    move |this: &mut Self, event, cx| {
+                                                        this.handle_click(
+                                                            ix,
+                                                            event.modifiers.command,
+                                                            cx,
+                                                        )
+                                                    },
+                                                )
+                                                .child(this.delegate.render_match(
+                                                    ix,
+                                                    ix == selected_ix,
+                                                    cx,
+                                                ))
                                         })
                                         .collect()
                                 }
@@ -184,10 +221,11 @@ impl<D: PickerDelegate> Render for Picker<D> {
             })
             .when(self.delegate.match_count() == 0, |el| {
                 el.child(
-                    v_stack()
-                        .p_1()
-                        .grow()
-                        .child(Label::new("No matches").color(LabelColor::Muted)),
+                    v_stack().p_1().grow().child(
+                        div()
+                            .px_1()
+                            .child(Label::new("No matches").color(LabelColor::Muted)),
+                    ),
                 )
             })
     }

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

@@ -32,6 +32,7 @@ impl KeyBinding {
                 div()
                     .flex()
                     .gap_1()
+                    .when(keystroke.modifiers.function, |el| el.child(Key::new("fn")))
                     .when(keystroke.modifiers.control, |el| el.child(Key::new("^")))
                     .when(keystroke.modifiers.alt, |el| el.child(Key::new("⌥")))
                     .when(keystroke.modifiers.command, |el| el.child(Key::new("⌘")))
@@ -136,6 +137,7 @@ mod stories {
                 .child(KeyBinding::new(binding("a z")))
                 .child(Story::label(cx, "Chord with Modifier"))
                 .child(KeyBinding::new(binding("ctrl-a shift-z")))
+                .child(KeyBinding::new(binding("fn-s")))
         }
     }
 }

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],
+                ))
         }
     }
 }

crates/workspace2/src/modal_layer.rs 🔗

@@ -2,7 +2,7 @@ use gpui::{
     div, px, AnyView, Div, EventEmitter, FocusHandle, ParentElement, Render, StatelessInteractive,
     Styled, Subscription, View, ViewContext, VisualContext, WindowContext,
 };
-use ui::v_stack;
+use ui::{h_stack, v_stack};
 
 pub struct ActiveModal {
     modal: AnyView,
@@ -33,8 +33,6 @@ impl ModalLayer {
         V: Modal,
         B: FnOnce(&mut ViewContext<V>) -> V,
     {
-        let previous_focus = cx.focused();
-
         if let Some(active_modal) = &self.active_modal {
             let is_close = active_modal.modal.clone().downcast::<V>().is_ok();
             self.hide_modal(cx);
@@ -85,9 +83,6 @@ impl Render for ModalLayer {
 
         div()
             .absolute()
-            .flex()
-            .flex_col()
-            .items_center()
             .size_full()
             .top_0()
             .left_0()
@@ -96,11 +91,21 @@ impl Render for ModalLayer {
                 v_stack()
                     .h(px(0.0))
                     .top_20()
+                    .flex()
+                    .flex_col()
+                    .items_center()
                     .track_focus(&active_modal.focus_handle)
-                    .on_mouse_down_out(|this: &mut Self, event, cx| {
-                        this.hide_modal(cx);
-                    })
-                    .child(active_modal.modal.clone()),
+                    .child(
+                        h_stack()
+                            // needed to prevent mouse events leaking to the
+                            // UI below. // todo! for gpui3.
+                            .on_any_mouse_down(|_, _, cx| cx.stop_propagation())
+                            .on_any_mouse_up(|_, _, cx| cx.stop_propagation())
+                            .on_mouse_down_out(|this: &mut Self, event, cx| {
+                                this.hide_modal(cx);
+                            })
+                            .child(active_modal.modal.clone()),
+                    ),
             )
     }
 }