diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index e403a50cf9..1386186ca4 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/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, diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 20b970d672..fe98dd8679 100644 --- a/crates/editor2/src/editor.rs +++ b/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()), diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index 50592901b5..1d57be6fd0 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/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; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - modal(cx) + div() + .elevation_2(cx) .context("GoToLine") .on_action(Self::cancel) .on_action(Self::confirm) diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index eee0584460..5d91b23bf3 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/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 Component for SharedString { fn render(self) -> AnyElement { Text { text: self, + runs: None, state_type: PhantomData, } .render() @@ -21,6 +22,7 @@ impl Component for &'static str { fn render(self) -> AnyElement { Text { text: self.into(), + runs: None, state_type: PhantomData, } .render() @@ -33,6 +35,7 @@ impl Component for String { fn render(self) -> AnyElement { Text { text: self.into(), + runs: None, state_type: PhantomData, } .render() @@ -41,9 +44,25 @@ impl Component for String { pub struct Text { text: SharedString, + runs: Option>, state_type: PhantomData, } +impl Text { + /// 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) -> Self { + Text { + text, + runs: Some(runs), + state_type: Default::default(), + } + } +} + impl Component for Text { fn render(self) -> AnyElement { AnyElement::new(self) @@ -82,6 +101,12 @@ impl Element for Text { 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 Element for Text { .layout_text( &text, font_size, - &[text_style.to_run(text.len())], + &runs[..], known_dimensions.width, // Wrap if we know the width. ) .log_err() diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index aacaeac01f..312121c954 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -71,6 +71,40 @@ pub trait StatelessInteractive: Element { self } + fn on_any_mouse_down( + mut self, + handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext) + '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) + '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: Element { fn on_mouse_up_out( mut self, - button: MouseButton, handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext) + 'static, ) -> Self where @@ -120,10 +153,7 @@ pub trait StatelessInteractive: Element { 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); } })); diff --git a/crates/gpui2/src/text_system.rs b/crates/gpui2/src/text_system.rs index dd0689396e..e8d6acc5a3 100644 --- a/crates/gpui2/src/text_system.rs +++ b/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, diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 425fb9510f..eb69b451b3 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -101,6 +101,12 @@ pub struct FocusHandle { handles: Arc>>, } +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>>) -> 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); } }) diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index e1979f1b13..0cfe5c8992 100644 --- a/crates/picker2/src/picker2.rs +++ b/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 { pub delegate: D, scroll_handle: UniformListScrollHandle, editor: View, - pending_update_matches: Option>>, + pending_update_matches: Option>, + confirm_on_update: Option, } pub trait PickerDelegate: Sized + 'static { @@ -42,12 +43,15 @@ impl Picker { 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 Picker { } fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { - 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.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) { + 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 Picker { this.update(&mut cx, |this, cx| { this.matches_updated(cx); }) - .ok() + .ok(); })); } @@ -134,6 +153,9 @@ impl Picker { 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 Render for Picker { 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 Render for Picker { }) .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)), + ), ) }) } diff --git a/crates/ui2/src/components/keybinding.rs b/crates/ui2/src/components/keybinding.rs index b6c435c607..a3e5a870a6 100644 --- a/crates/ui2/src/components/keybinding.rs +++ b/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"))) } } } diff --git a/crates/ui2/src/components/label.rs b/crates/ui2/src/components/label.rs index 827ba87918..6b915af1b9 100644 --- a/crates/ui2/src/components/label.rs +++ b/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, highlight_indices: Vec) -> Self { Self { label: label.into(), @@ -126,10 +127,11 @@ impl HighlightedLabel { fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { 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 = 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], + )) } } } diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index 09ffa6c13f..b3a5de8fb2 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/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, { - let previous_focus = cx.focused(); - if let Some(active_modal) = &self.active_modal { let is_close = active_modal.modal.clone().downcast::().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()), + ), ) } }