command 2 2 (#3317)

- 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
This commit is contained in:
Conrad Irwin 2023-11-13 23:12:03 -07:00 committed by GitHub
commit 58f9ef99f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 157 additions and 58 deletions

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
use crate::{ use crate::{
AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, Line, Pixels, SharedString, AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, Line, Pixels, SharedString,
Size, ViewContext, Size, TextRun, ViewContext,
}; };
use parking_lot::Mutex; use parking_lot::Mutex;
use smallvec::SmallVec; use smallvec::SmallVec;
@ -11,6 +11,7 @@ impl<V: 'static> Component<V> for SharedString {
fn render(self) -> AnyElement<V> { fn render(self) -> AnyElement<V> {
Text { Text {
text: self, text: self,
runs: None,
state_type: PhantomData, state_type: PhantomData,
} }
.render() .render()
@ -21,6 +22,7 @@ impl<V: 'static> Component<V> for &'static str {
fn render(self) -> AnyElement<V> { fn render(self) -> AnyElement<V> {
Text { Text {
text: self.into(), text: self.into(),
runs: None,
state_type: PhantomData, state_type: PhantomData,
} }
.render() .render()
@ -33,6 +35,7 @@ impl<V: 'static> Component<V> for String {
fn render(self) -> AnyElement<V> { fn render(self) -> AnyElement<V> {
Text { Text {
text: self.into(), text: self.into(),
runs: None,
state_type: PhantomData, state_type: PhantomData,
} }
.render() .render()
@ -41,9 +44,25 @@ impl<V: 'static> Component<V> for String {
pub struct Text<V> { pub struct Text<V> {
text: SharedString, text: SharedString,
runs: Option<Vec<TextRun>>,
state_type: PhantomData<V>, 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> { impl<V: 'static> Component<V> for Text<V> {
fn render(self) -> AnyElement<V> { fn render(self) -> AnyElement<V> {
AnyElement::new(self) AnyElement::new(self)
@ -82,6 +101,12 @@ impl<V: 'static> Element<V> for Text<V> {
let rem_size = cx.rem_size(); 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 layout_id = cx.request_measured_layout(Default::default(), rem_size, {
let element_state = element_state.clone(); let element_state = element_state.clone();
move |known_dimensions, _| { move |known_dimensions, _| {
@ -89,7 +114,7 @@ impl<V: 'static> Element<V> for Text<V> {
.layout_text( .layout_text(
&text, &text,
font_size, font_size,
&[text_style.to_run(text.len())], &runs[..],
known_dimensions.width, // Wrap if we know the width. known_dimensions.width, // Wrap if we know the width.
) )
.log_err() .log_err()

View file

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

View file

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

View file

@ -101,6 +101,12 @@ pub struct FocusHandle {
handles: Arc<RwLock<SlotMap<FocusId, AtomicUsize>>>, 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 { impl FocusHandle {
pub(crate) fn new(handles: &Arc<RwLock<SlotMap<FocusId, AtomicUsize>>>) -> Self { pub(crate) fn new(handles: &Arc<RwLock<SlotMap<FocusId, AtomicUsize>>>) -> Self {
let id = handles.write().insert(AtomicUsize::new(1)); let id = handles.write().insert(AtomicUsize::new(1));
@ -424,6 +430,7 @@ impl<'a> WindowContext<'a> {
.dispatch_tree .dispatch_tree
.focusable_node_id(focus_handle.id) .focusable_node_id(focus_handle.id)
{ {
cx.propagate_event = true;
cx.dispatch_action_on_node(node_id, action); cx.dispatch_action_on_node(node_id, action);
} }
}) })

View file

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

View file

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

View file

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

View file

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