From c33efe8cd0714b28834df206740aa9d2b4d7e04e Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com> Date: Mon, 19 Feb 2024 13:37:52 +0100 Subject: [PATCH] recent projects: cleanup ui (#7528) As the ui for the file finder was recently changed in #7364, I think it makes sense to also update the ui of the recent projects overlay. Before: ![image](https://github.com/zed-industries/zed/assets/53836821/8a0f5bef-9b37-40f3-a974-9dfd7833cc71) After: ![image](https://github.com/zed-industries/zed/assets/53836821/7e9f934a-1ac3-4716-b7b6-67a7435f3bde) Release Notes: - Improved UI of recent project overlay --- .../src/collab_panel/channel_modal.rs | 2 +- .../src/collab_panel/contact_finder.rs | 2 +- crates/command_palette/src/command_palette.rs | 2 +- crates/file_finder/src/file_finder.rs | 2 +- crates/gpui/src/elements/list.rs | 397 ++++++++++++------ crates/gpui/src/elements/uniform_list.rs | 2 +- .../src/language_selector.rs | 2 +- crates/outline/src/outline.rs | 2 +- crates/picker/src/picker.rs | 162 ++++--- crates/project_symbols/src/project_symbols.rs | 4 +- .../src/highlighted_workspace_location.rs | 3 +- crates/recent_projects/src/recent_projects.rs | 54 ++- crates/storybook/src/stories/picker.rs | 2 +- crates/theme_selector/src/theme_selector.rs | 2 +- crates/vcs_menu/src/lib.rs | 2 +- crates/welcome/src/base_keymap_picker.rs | 2 +- 16 files changed, 430 insertions(+), 212 deletions(-) diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index f3f77a28f6..501524501e 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -43,7 +43,7 @@ impl ChannelModal { cx.observe(&channel_store, |_, _, cx| cx.notify()).detach(); let channel_modal = cx.view().downgrade(); let picker = cx.new_view(|cx| { - Picker::new( + Picker::uniform_list( ChannelModalDelegate { channel_modal, matching_users: Vec::new(), diff --git a/crates/collab_ui/src/collab_panel/contact_finder.rs b/crates/collab_ui/src/collab_panel/contact_finder.rs index 2c59df2eb5..6a1932a843 100644 --- a/crates/collab_ui/src/collab_panel/contact_finder.rs +++ b/crates/collab_ui/src/collab_panel/contact_finder.rs @@ -22,7 +22,7 @@ impl ContactFinder { potential_contacts: Arc::from([]), selected_index: 0, }; - let picker = cx.new_view(|cx| Picker::new(delegate, cx).modal(false)); + let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx).modal(false)); Self { picker } } diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index c47614743e..2a7a94b544 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -80,7 +80,7 @@ impl CommandPalette { previous_focus_handle, ); - let picker = cx.new_view(|cx| Picker::new(delegate, cx)); + let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx)); Self { picker } } } diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 521682e6b3..fc8e5d1d99 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -93,7 +93,7 @@ impl FileFinder { fn new(delegate: FileFinderDelegate, cx: &mut ViewContext) -> Self { Self { - picker: cx.new_view(|cx| Picker::new(delegate, cx)), + picker: cx.new_view(|cx| Picker::uniform_list(delegate, cx)), } } } diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index c16fb32a52..7961c0df84 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -7,20 +7,22 @@ //! If all of your elements are the same height, see [`UniformList`] for a simpler API use crate::{ - point, px, AnyElement, AvailableSpace, Bounds, ContentMask, DispatchPhase, Element, - IntoElement, Pixels, Point, ScrollWheelEvent, Size, Style, StyleRefinement, Styled, - WindowContext, + point, px, size, AnyElement, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges, + Element, ElementContext, IntoElement, Pixels, Point, ScrollWheelEvent, Size, Style, + StyleRefinement, Styled, WindowContext, }; use collections::VecDeque; use refineable::Refineable as _; use std::{cell::RefCell, ops::Range, rc::Rc}; use sum_tree::{Bias, SumTree}; +use taffy::style::Overflow; /// Construct a new list element pub fn list(state: ListState) -> List { List { state, style: StyleRefinement::default(), + sizing_behavior: ListSizingBehavior::default(), } } @@ -28,6 +30,15 @@ pub fn list(state: ListState) -> List { pub struct List { state: ListState, style: StyleRefinement, + sizing_behavior: ListSizingBehavior, +} + +impl List { + /// Set the sizing behavior for the list. + pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self { + self.sizing_behavior = behavior; + self + } } /// The list state that views must hold on behalf of the list element. @@ -36,6 +47,7 @@ pub struct ListState(Rc>); struct StateInner { last_layout_bounds: Option>, + last_padding: Option>, render_item: Box AnyElement>, items: SumTree, logical_scroll_top: Option, @@ -67,10 +79,27 @@ pub struct ListScrollEvent { pub is_scrolled: bool, } +/// The sizing behavior to apply during layout. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub enum ListSizingBehavior { + /// The list should calculate its size based on the size of its items. + Infer, + /// The list should not calculate a fixed size. + #[default] + Auto, +} + +struct LayoutItemsResponse { + max_item_width: Pixels, + scroll_top: ListOffset, + available_item_space: Size, + item_elements: VecDeque, +} + #[derive(Clone)] enum ListItem { Unrendered, - Rendered { height: Pixels }, + Rendered { size: Size }, } #[derive(Clone, Debug, Default, PartialEq)] @@ -112,6 +141,7 @@ impl ListState { items.extend((0..element_count).map(|_| ListItem::Unrendered), &()); Self(Rc::new(RefCell::new(StateInner { last_layout_bounds: None, + last_padding: None, render_item: Box::new(render_item), items, logical_scroll_top: None, @@ -202,6 +232,7 @@ impl ListState { let height = state .last_layout_bounds .map_or(px(0.), |bounds| bounds.size.height); + let padding = state.last_padding.unwrap_or_default(); if ix <= scroll_top.item_ix { scroll_top.item_ix = ix; @@ -209,7 +240,7 @@ impl ListState { } else { let mut cursor = state.items.cursor::(); cursor.seek(&Count(ix + 1), Bias::Right, &()); - let bottom = cursor.start().height; + let bottom = cursor.start().height + padding.top; let goal_top = px(0.).max(bottom - height); cursor.seek(&Height(goal_top), Bias::Left, &()); @@ -242,13 +273,13 @@ impl ListState { let scroll_top = cursor.start().1 .0 + scroll_top.offset_in_item; cursor.seek_forward(&Count(ix), Bias::Right, &()); - if let Some(&ListItem::Rendered { height }) = cursor.item() { + if let Some(&ListItem::Rendered { size }) = cursor.item() { let &(Count(count), Height(top)) = cursor.start(); if count == ix { let top = bounds.top() + top - scroll_top; return Some(Bounds::from_corners( point(bounds.left(), top), - point(bounds.right(), top + height), + point(bounds.right(), top + size.height), )); } } @@ -271,6 +302,7 @@ impl StateInner { height: Pixels, delta: Point, cx: &mut WindowContext, + padding: Edges, ) { // Drop scroll events after a reset, since we can't calculate // the new logical scroll top without the item heights @@ -278,7 +310,8 @@ impl StateInner { return; } - let scroll_max = (self.items.summary().height - height).max(px(0.)); + let scroll_max = + (self.items.summary().height + padding.top + padding.bottom - height).max(px(0.)); let new_scroll_top = (self.scroll_top(scroll_top) - delta.y) .max(px(0.)) .min(scroll_max); @@ -330,15 +363,144 @@ impl StateInner { cursor.seek(&Count(logical_scroll_top.item_ix), Bias::Right, &()); cursor.start().height + logical_scroll_top.offset_in_item } + + fn layout_items( + &mut self, + available_width: Option, + available_height: Pixels, + padding: &Edges, + cx: &mut ElementContext, + ) -> LayoutItemsResponse { + let old_items = self.items.clone(); + let mut measured_items = VecDeque::new(); + let mut item_elements = VecDeque::new(); + let mut rendered_height = padding.top; + let mut max_item_width = px(0.); + let mut scroll_top = self.logical_scroll_top(); + + let available_item_space = size( + available_width.map_or(AvailableSpace::MinContent, |width| { + AvailableSpace::Definite(width) + }), + AvailableSpace::MinContent, + ); + + let mut cursor = old_items.cursor::(); + + // Render items after the scroll top, including those in the trailing overdraw + cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &()); + for (ix, item) in cursor.by_ref().enumerate() { + let visible_height = rendered_height - scroll_top.offset_in_item; + if visible_height >= available_height + self.overdraw { + break; + } + + // Use the previously cached height if available + let mut size = if let ListItem::Rendered { size } = item { + Some(*size) + } else { + None + }; + + // If we're within the visible area or the height wasn't cached, render and measure the item's element + if visible_height < available_height || size.is_none() { + let mut element = (self.render_item)(scroll_top.item_ix + ix, cx); + let element_size = element.measure(available_item_space, cx); + size = Some(element_size); + if visible_height < available_height { + item_elements.push_back(element); + } + } + + let size = size.unwrap(); + rendered_height += size.height; + max_item_width = max_item_width.max(size.width); + measured_items.push_back(ListItem::Rendered { size }); + } + rendered_height += padding.bottom; + + // Prepare to start walking upward from the item at the scroll top. + cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &()); + + // If the rendered items do not fill the visible region, then adjust + // the scroll top upward. + if rendered_height - scroll_top.offset_in_item < available_height { + while rendered_height < available_height { + cursor.prev(&()); + if cursor.item().is_some() { + let mut element = (self.render_item)(cursor.start().0, cx); + let element_size = element.measure(available_item_space, cx); + + rendered_height += element_size.height; + measured_items.push_front(ListItem::Rendered { size: element_size }); + item_elements.push_front(element) + } else { + break; + } + } + + scroll_top = ListOffset { + item_ix: cursor.start().0, + offset_in_item: rendered_height - available_height, + }; + + match self.alignment { + ListAlignment::Top => { + scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.)); + self.logical_scroll_top = Some(scroll_top); + } + ListAlignment::Bottom => { + scroll_top = ListOffset { + item_ix: cursor.start().0, + offset_in_item: rendered_height - available_height, + }; + self.logical_scroll_top = None; + } + }; + } + + // Measure items in the leading overdraw + let mut leading_overdraw = scroll_top.offset_in_item; + while leading_overdraw < self.overdraw { + cursor.prev(&()); + if let Some(item) = cursor.item() { + let size = if let ListItem::Rendered { size } = item { + *size + } else { + let mut element = (self.render_item)(cursor.start().0, cx); + element.measure(available_item_space, cx) + }; + + leading_overdraw += size.height; + measured_items.push_front(ListItem::Rendered { size }); + } else { + break; + } + } + + let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len()); + let mut cursor = old_items.cursor::(); + let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right, &()); + new_items.extend(measured_items, &()); + cursor.seek(&Count(measured_range.end), Bias::Right, &()); + new_items.append(cursor.suffix(&()), &()); + + self.items = new_items; + + LayoutItemsResponse { + max_item_width, + scroll_top, + available_item_space, + item_elements, + } + } } impl std::fmt::Debug for ListItem { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Unrendered => write!(f, "Unrendered"), - Self::Rendered { height, .. } => { - f.debug_struct("Rendered").field("height", height).finish() - } + Self::Rendered { size, .. } => f.debug_struct("Rendered").field("size", size).finish(), } } } @@ -361,11 +523,67 @@ impl Element for List { _state: Option, cx: &mut crate::ElementContext, ) -> (crate::LayoutId, Self::State) { - let mut style = Style::default(); - style.refine(&self.style); - let layout_id = cx.with_text_style(style.text_style().cloned(), |cx| { - cx.request_layout(&style, None) - }); + let layout_id = match self.sizing_behavior { + ListSizingBehavior::Infer => { + let mut style = Style::default(); + style.overflow.y = Overflow::Scroll; + style.refine(&self.style); + cx.with_text_style(style.text_style().cloned(), |cx| { + let state = &mut *self.state.0.borrow_mut(); + + let available_height = if let Some(last_bounds) = state.last_layout_bounds { + last_bounds.size.height + } else { + // If we don't have the last layout bounds (first render), + // we might just use the overdraw value as the available height to layout enough items. + state.overdraw + }; + let padding = style.padding.to_pixels( + state.last_layout_bounds.unwrap_or_default().size.into(), + cx.rem_size(), + ); + + let layout_response = state.layout_items(None, available_height, &padding, cx); + let max_element_width = layout_response.max_item_width; + + let summary = state.items.summary(); + let total_height = summary.height; + let all_rendered = summary.unrendered_count == 0; + + if all_rendered { + cx.request_measured_layout( + style, + move |known_dimensions, available_space, _cx| { + let width = known_dimensions.width.unwrap_or(match available_space + .width + { + AvailableSpace::Definite(x) => x, + AvailableSpace::MinContent | AvailableSpace::MaxContent => { + max_element_width + } + }); + let height = match available_space.height { + AvailableSpace::Definite(height) => total_height.min(height), + AvailableSpace::MinContent | AvailableSpace::MaxContent => { + total_height + } + }; + size(width, height) + }, + ) + } else { + cx.request_layout(&style, None) + } + }) + } + ListSizingBehavior::Auto => { + let mut style = Style::default(); + style.refine(&self.style); + cx.with_text_style(style.text_style().cloned(), |cx| { + cx.request_layout(&style, None) + }) + } + }; (layout_id, ()) } @@ -376,9 +594,11 @@ impl Element for List { cx: &mut crate::ElementContext, ) { let state = &mut *self.state.0.borrow_mut(); - state.reset = false; + let mut style = Style::default(); + style.refine(&self.style); + // If the width of the list has changed, invalidate all cached item heights if state.last_layout_bounds.map_or(true, |last_bounds| { last_bounds.size.width != bounds.size.width @@ -389,130 +609,28 @@ impl Element for List { ) } - let old_items = state.items.clone(); - let mut measured_items = VecDeque::new(); - let mut item_elements = VecDeque::new(); - let mut rendered_height = px(0.); - let mut scroll_top = state.logical_scroll_top(); + let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size()); + let mut layout_response = + state.layout_items(Some(bounds.size.width), bounds.size.height, &padding, cx); - let available_item_space = Size { - width: AvailableSpace::Definite(bounds.size.width), - height: AvailableSpace::MinContent, - }; - - let mut cursor = old_items.cursor::(); - - // Render items after the scroll top, including those in the trailing overdraw - cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &()); - for (ix, item) in cursor.by_ref().enumerate() { - let visible_height = rendered_height - scroll_top.offset_in_item; - if visible_height >= bounds.size.height + state.overdraw { - break; - } - - // Use the previously cached height if available - let mut height = if let ListItem::Rendered { height } = item { - Some(*height) - } else { - None - }; - - // If we're within the visible area or the height wasn't cached, render and measure the item's element - if visible_height < bounds.size.height || height.is_none() { - let mut element = (state.render_item)(scroll_top.item_ix + ix, cx); - let element_size = element.measure(available_item_space, cx); - height = Some(element_size.height); - if visible_height < bounds.size.height { - item_elements.push_back(element); + // Only paint the visible items, if there is actually any space for them (taking padding into account) + if bounds.size.height > padding.top + padding.bottom { + // Paint the visible items + cx.with_content_mask(Some(ContentMask { bounds }), |cx| { + let mut item_origin = bounds.origin + Point::new(px(0.), padding.top); + item_origin.y -= layout_response.scroll_top.offset_in_item; + for item_element in &mut layout_response.item_elements { + let item_height = item_element + .measure(layout_response.available_item_space, cx) + .height; + item_element.draw(item_origin, layout_response.available_item_space, cx); + item_origin.y += item_height; } - } - - let height = height.unwrap(); - rendered_height += height; - measured_items.push_back(ListItem::Rendered { height }); + }); } - // Prepare to start walking upward from the item at the scroll top. - cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &()); - - // If the rendered items do not fill the visible region, then adjust - // the scroll top upward. - if rendered_height - scroll_top.offset_in_item < bounds.size.height { - while rendered_height < bounds.size.height { - cursor.prev(&()); - if cursor.item().is_some() { - let mut element = (state.render_item)(cursor.start().0, cx); - let element_size = element.measure(available_item_space, cx); - - rendered_height += element_size.height; - measured_items.push_front(ListItem::Rendered { - height: element_size.height, - }); - item_elements.push_front(element) - } else { - break; - } - } - - scroll_top = ListOffset { - item_ix: cursor.start().0, - offset_in_item: rendered_height - bounds.size.height, - }; - - match state.alignment { - ListAlignment::Top => { - scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.)); - state.logical_scroll_top = Some(scroll_top); - } - ListAlignment::Bottom => { - scroll_top = ListOffset { - item_ix: cursor.start().0, - offset_in_item: rendered_height - bounds.size.height, - }; - state.logical_scroll_top = None; - } - }; - } - - // Measure items in the leading overdraw - let mut leading_overdraw = scroll_top.offset_in_item; - while leading_overdraw < state.overdraw { - cursor.prev(&()); - if let Some(item) = cursor.item() { - let height = if let ListItem::Rendered { height } = item { - *height - } else { - let mut element = (state.render_item)(cursor.start().0, cx); - element.measure(available_item_space, cx).height - }; - - leading_overdraw += height; - measured_items.push_front(ListItem::Rendered { height }); - } else { - break; - } - } - - let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len()); - let mut cursor = old_items.cursor::(); - let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right, &()); - new_items.extend(measured_items, &()); - cursor.seek(&Count(measured_range.end), Bias::Right, &()); - new_items.append(cursor.suffix(&()), &()); - - // Paint the visible items - cx.with_content_mask(Some(ContentMask { bounds }), |cx| { - let mut item_origin = bounds.origin; - item_origin.y -= scroll_top.offset_in_item; - for item_element in &mut item_elements { - let item_height = item_element.measure(available_item_space, cx).height; - item_element.draw(item_origin, available_item_space, cx); - item_origin.y += item_height; - } - }); - - state.items = new_items; state.last_layout_bounds = Some(bounds); + state.last_padding = Some(padding); let list_state = self.state.clone(); let height = bounds.size.height; @@ -523,10 +641,11 @@ impl Element for List { && cx.was_top_layer(&event.position, cx.stacking_order()) { list_state.0.borrow_mut().scroll( - &scroll_top, + &layout_response.scroll_top, height, event.delta.pixel_delta(px(20.)), cx, + padding, ) } }); @@ -562,11 +681,11 @@ impl sum_tree::Item for ListItem { unrendered_count: 1, height: px(0.), }, - ListItem::Rendered { height } => ListItemSummary { + ListItem::Rendered { size } => ListItemSummary { count: 1, rendered_count: 1, unrendered_count: 0, - height: *height, + height: size.height, }, } } diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index ce32b993a5..8a6651524b 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -218,7 +218,7 @@ impl Element for UniformList { if let Some(ix) = shared_scroll_to_item { let list_height = padded_bounds.size.height; let mut updated_scroll_offset = shared_scroll_offset.borrow_mut(); - let item_top = item_height * ix; + let item_top = item_height * ix + padding.top; let item_bottom = item_top + item_height; let scroll_top = -updated_scroll_offset.y; if item_top < scroll_top { diff --git a/crates/language_selector/src/language_selector.rs b/crates/language_selector/src/language_selector.rs index 00ff809fc4..0a3faffbee 100644 --- a/crates/language_selector/src/language_selector.rs +++ b/crates/language_selector/src/language_selector.rs @@ -61,7 +61,7 @@ impl LanguageSelector { language_registry, ); - let picker = cx.new_view(|cx| Picker::new(delegate, cx)); + let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx)); Self { picker } } } diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 29682262be..a078ad6a7c 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -86,7 +86,7 @@ impl OutlineView { cx: &mut ViewContext, ) -> OutlineView { let delegate = OutlineViewDelegate::new(cx.view().downgrade(), outline, editor, cx); - let picker = cx.new_view(|cx| Picker::new(delegate, cx).max_height(vh(0.75, cx))); + let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx).max_height(vh(0.75, cx))); OutlineView { picker } } } diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 3db2f135c2..09019a8434 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -1,16 +1,21 @@ use editor::Editor; use gpui::{ - div, prelude::*, uniform_list, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, - FocusableView, Length, MouseButton, MouseDownEvent, Render, Task, UniformListScrollHandle, - View, ViewContext, WindowContext, + div, list, prelude::*, uniform_list, AnyElement, AppContext, DismissEvent, EventEmitter, + FocusHandle, FocusableView, Length, ListState, MouseButton, MouseDownEvent, Render, Task, + UniformListScrollHandle, View, ViewContext, WindowContext, }; use std::sync::Arc; use ui::{prelude::*, v_flex, Color, Divider, Label, ListItem, ListItemSpacing}; use workspace::ModalView; +enum ElementContainer { + List(ListState), + UniformList(UniformListScrollHandle), +} + pub struct Picker { pub delegate: D, - scroll_handle: UniformListScrollHandle, + element_container: ElementContainer, editor: View, pending_update_matches: Option>, confirm_on_update: Option, @@ -65,14 +70,27 @@ fn create_editor(placeholder: Arc, cx: &mut WindowContext<'_>) -> View Picker { - pub fn new(delegate: D, cx: &mut ViewContext) -> Self { + /// A picker, which displays its matches using `gpui::uniform_list`, all matches should have the same height. + /// If `PickerDelegate::render_match` can return items with different heights, use `Picker::list`. + pub fn uniform_list(delegate: D, cx: &mut ViewContext) -> Self { + Self::new(delegate, cx, true) + } + + /// A picker, which displays its matches using `gpui::list`, matches can have different heights. + /// If `PickerDelegate::render_match` only returns items with the same height, use `Picker::uniform_list` as its implementation is optimized for that. + pub fn list(delegate: D, cx: &mut ViewContext) -> Self { + Self::new(delegate, cx, false) + } + + fn new(delegate: D, cx: &mut ViewContext, is_uniform: bool) -> Self { let editor = create_editor(delegate.placeholder_text(), cx); cx.subscribe(&editor, Self::on_input_editor_event).detach(); let mut this = Self { delegate, editor, - scroll_handle: UniformListScrollHandle::new(), + element_container: Self::crate_element_container(is_uniform, cx), pending_update_matches: None, confirm_on_update: None, width: None, @@ -83,6 +101,28 @@ impl Picker { this } + fn crate_element_container(is_uniform: bool, cx: &mut ViewContext) -> ElementContainer { + if is_uniform { + ElementContainer::UniformList(UniformListScrollHandle::new()) + } else { + let view = cx.view().downgrade(); + ElementContainer::List(ListState::new( + 0, + gpui::ListAlignment::Top, + px(1000.), + move |ix, cx| { + view.upgrade() + .map(|view| { + view.update(cx, |this, cx| { + this.render_element(cx, ix).into_any_element() + }) + }) + .unwrap_or_else(|| div().into_any_element()) + }, + )) + } + } + pub fn width(mut self, width: impl Into) -> Self { self.width = Some(width.into()); self @@ -108,7 +148,7 @@ impl Picker { let index = self.delegate.selected_index(); let ix = if index == count - 1 { 0 } else { index + 1 }; self.delegate.set_selected_index(ix, cx); - self.scroll_handle.scroll_to_item(ix); + self.scroll_to_item_index(ix); cx.notify(); } } @@ -119,7 +159,7 @@ impl Picker { let index = self.delegate.selected_index(); let ix = if index == 0 { count - 1 } else { index - 1 }; self.delegate.set_selected_index(ix, cx); - self.scroll_handle.scroll_to_item(ix); + self.scroll_to_item_index(ix); cx.notify(); } } @@ -128,7 +168,7 @@ impl Picker { let count = self.delegate.match_count(); if count > 0 { self.delegate.set_selected_index(0, cx); - self.scroll_handle.scroll_to_item(0); + self.scroll_to_item_index(0); cx.notify(); } } @@ -137,7 +177,7 @@ impl Picker { let count = self.delegate.match_count(); if count > 0 { self.delegate.set_selected_index(count - 1, cx); - self.scroll_handle.scroll_to_item(count - 1); + self.scroll_to_item_index(count - 1); cx.notify(); } } @@ -147,7 +187,7 @@ impl Picker { let index = self.delegate.selected_index(); let new_index = if index + 1 == count { 0 } else { index + 1 }; self.delegate.set_selected_index(new_index, cx); - self.scroll_handle.scroll_to_item(new_index); + self.scroll_to_item_index(new_index); cx.notify(); } @@ -215,8 +255,12 @@ impl Picker { } fn matches_updated(&mut self, cx: &mut ViewContext) { + if let ElementContainer::List(state) = &mut self.element_container { + state.reset(self.delegate.match_count()); + } + let index = self.delegate.selected_index(); - self.scroll_handle.scroll_to_item(index); + self.scroll_to_item_index(index); self.pending_update_matches = None; if let Some(secondary) = self.confirm_on_update.take() { self.delegate.confirm(secondary, cx); @@ -232,6 +276,58 @@ impl Picker { self.editor .update(cx, |editor, cx| editor.set_text(query, cx)); } + + fn scroll_to_item_index(&mut self, ix: usize) { + match &mut self.element_container { + ElementContainer::List(state) => state.scroll_to_reveal_item(ix), + ElementContainer::UniformList(scroll_handle) => scroll_handle.scroll_to_item(ix), + } + } + + fn render_element(&self, cx: &mut ViewContext, ix: usize) -> impl IntoElement { + div() + .on_mouse_down( + MouseButton::Left, + cx.listener(move |this, event: &MouseDownEvent, cx| { + this.handle_click(ix, event.modifiers.command, cx) + }), + ) + .children( + self.delegate + .render_match(ix, ix == self.delegate.selected_index(), cx), + ) + .when( + self.delegate.separators_after_indices().contains(&ix), + |picker| { + picker + .border_color(cx.theme().colors().border_variant) + .border_b_1() + .pb(px(-1.0)) + }, + ) + } + + fn render_element_container(&self, cx: &mut ViewContext) -> impl IntoElement { + match &self.element_container { + ElementContainer::UniformList(scroll_handle) => uniform_list( + cx.view().clone(), + "candidates", + self.delegate.match_count(), + move |picker, visible_range, cx| { + visible_range + .map(|ix| picker.render_element(cx, ix)) + .collect() + }, + ) + .py_2() + .track_scroll(scroll_handle.clone()) + .into_any_element(), + ElementContainer::List(state) => list(state.clone()) + .with_sizing_behavior(gpui::ListSizingBehavior::Infer) + .py_2() + .into_any_element(), + } + } } impl EventEmitter for Picker {} @@ -269,50 +365,10 @@ impl Render for Picker { el.child( v_flex() .flex_grow() - .py_2() .max_h(self.max_height.unwrap_or(rems(18.).into())) .overflow_hidden() .children(self.delegate.render_header(cx)) - .child( - uniform_list( - cx.view().clone(), - "candidates", - self.delegate.match_count(), - { - let separators_after_indices = self.delegate.separators_after_indices(); - let selected_index = self.delegate.selected_index(); - move |picker, visible_range, cx| { - visible_range - .map(|ix| { - div() - .on_mouse_down( - MouseButton::Left, - cx.listener(move |this, event: &MouseDownEvent, cx| { - this.handle_click( - ix, - event.modifiers.command, - cx, - ) - }), - ) - .children(picker.delegate.render_match( - ix, - ix == selected_index, - cx, - )).when(separators_after_indices.contains(&ix), |picker| { - picker - .border_color(cx.theme().colors().border_variant) - .border_b_1() - .pb(px(-1.0)) - }) - }) - .collect() - } - }, - ) - .track_scroll(self.scroll_handle.clone()) - ) - + .child(self.render_element_container(cx)), ) }) .when(self.delegate.match_count() == 0, |el| { diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index bb09741f07..bd5b2e64c4 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -25,7 +25,7 @@ pub fn init(cx: &mut AppContext) { let handle = cx.view().downgrade(); workspace.toggle_modal(cx, move |cx| { let delegate = ProjectSymbolsDelegate::new(handle, project); - Picker::new(delegate, cx).width(rems(34.)) + Picker::uniform_list(delegate, cx).width(rems(34.)) }) }); }, @@ -344,7 +344,7 @@ mod tests { // Create the project symbols view. let symbols = cx.new_view(|cx| { - Picker::new( + Picker::uniform_list( ProjectSymbolsDelegate::new(workspace.downgrade(), project.clone()), cx, ) diff --git a/crates/recent_projects/src/highlighted_workspace_location.rs b/crates/recent_projects/src/highlighted_workspace_location.rs index b31dabe8bd..436bafb062 100644 --- a/crates/recent_projects/src/highlighted_workspace_location.rs +++ b/crates/recent_projects/src/highlighted_workspace_location.rs @@ -5,7 +5,7 @@ use ui::{prelude::*, HighlightedLabel}; use util::paths::PathExt; use workspace::WorkspaceLocation; -#[derive(IntoElement)] +#[derive(Clone, IntoElement)] pub struct HighlightedText { pub text: String, pub highlight_positions: Vec, @@ -48,6 +48,7 @@ impl RenderOnce for HighlightedText { } } +#[derive(Clone)] pub struct HighlightedWorkspaceLocation { pub names: HighlightedText, pub paths: Vec, diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 1d8ddefcbc..0fd2552902 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -10,7 +10,7 @@ use highlighted_workspace_location::HighlightedWorkspaceLocation; use ordered_float::OrderedFloat; use picker::{Picker, PickerDelegate}; use std::sync::Arc; -use ui::{prelude::*, ListItem, ListItemSpacing}; +use ui::{prelude::*, tooltip_container, HighlightedLabel, ListItem, ListItemSpacing}; use util::paths::PathExt; use workspace::{ModalView, Workspace, WorkspaceLocation, WORKSPACE_DB}; @@ -30,7 +30,14 @@ impl ModalView for RecentProjects {} impl RecentProjects { fn new(delegate: RecentProjectsDelegate, rem_width: f32, cx: &mut ViewContext) -> Self { - let picker = cx.new_view(|cx| Picker::new(delegate, cx)); + let picker = cx.new_view(|cx| { + // We want to use a list when we render paths, because the items can have different heights (multiple paths). + if delegate.render_paths { + Picker::list(delegate, cx) + } else { + Picker::uniform_list(delegate, cx) + } + }); let _subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent)); // We do not want to block the UI on a potentially lengthy call to DB, so we're gonna swap // out workspace locations once the future runs to completion. @@ -82,7 +89,7 @@ impl RecentProjects { workspace.toggle_modal(cx, |cx| { let delegate = RecentProjectsDelegate::new(weak_workspace, true); - let modal = RecentProjects::new(delegate, 34., cx); + let modal = Self::new(delegate, 34., cx); modal }); })?; @@ -139,7 +146,7 @@ impl PickerDelegate for RecentProjectsDelegate { type ListItem = ListItem; fn placeholder_text(&self) -> Arc { - "Recent Projects...".into() + "Search recent projects...".into() } fn match_count(&self) -> usize { @@ -230,6 +237,8 @@ impl PickerDelegate for RecentProjectsDelegate { &self.workspace_locations[r#match.candidate_id], ); + let tooltip_highlighted_location = highlighted_location.clone(); + Some( ListItem::new(ix) .inset(true) @@ -239,9 +248,42 @@ impl PickerDelegate for RecentProjectsDelegate { v_flex() .child(highlighted_location.names) .when(self.render_paths, |this| { - this.children(highlighted_location.paths) + this.children(highlighted_location.paths.into_iter().map(|path| { + HighlightedLabel::new(path.text, path.highlight_positions) + .size(LabelSize::Small) + .color(Color::Muted) + })) }), - ), + ) + .tooltip(move |cx| { + let tooltip_highlighted_location = tooltip_highlighted_location.clone(); + cx.new_view(move |_| MatchTooltip { + highlighted_location: tooltip_highlighted_location, + }) + .into() + }), ) } } + +struct MatchTooltip { + highlighted_location: HighlightedWorkspaceLocation, +} + +impl Render for MatchTooltip { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + tooltip_container(cx, |div, _| { + div.children( + self.highlighted_location + .paths + .clone() + .into_iter() + .map(|path| { + HighlightedLabel::new(path.text, path.highlight_positions) + .size(LabelSize::Small) + .color(Color::Muted) + }), + ) + }) + } +} diff --git a/crates/storybook/src/stories/picker.rs b/crates/storybook/src/stories/picker.rs index 515c967ae5..cdc0a0907e 100644 --- a/crates/storybook/src/stories/picker.rs +++ b/crates/storybook/src/stories/picker.rs @@ -190,7 +190,7 @@ impl PickerStory { ]); delegate.update_matches("".into(), cx).detach(); - let picker = Picker::new(delegate, cx); + let picker = Picker::uniform_list(delegate, cx); picker.focus(cx); picker }), diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index f82a0c5ac7..2ad1085f66 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -60,7 +60,7 @@ impl Render for ThemeSelector { impl ThemeSelector { pub fn new(delegate: ThemeSelectorDelegate, cx: &mut ViewContext) -> Self { - let picker = cx.new_view(|cx| Picker::new(delegate, cx)); + let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx)); Self { picker } } } diff --git a/crates/vcs_menu/src/lib.rs b/crates/vcs_menu/src/lib.rs index 44564ce878..645c3e7128 100644 --- a/crates/vcs_menu/src/lib.rs +++ b/crates/vcs_menu/src/lib.rs @@ -34,7 +34,7 @@ pub struct BranchList { impl BranchList { fn new(delegate: BranchListDelegate, rem_width: f32, cx: &mut ViewContext) -> Self { - let picker = cx.new_view(|cx| Picker::new(delegate, cx)); + let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx)); let _subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent)); Self { picker, diff --git a/crates/welcome/src/base_keymap_picker.rs b/crates/welcome/src/base_keymap_picker.rs index 7913e4df37..c6acd95a96 100644 --- a/crates/welcome/src/base_keymap_picker.rs +++ b/crates/welcome/src/base_keymap_picker.rs @@ -55,7 +55,7 @@ impl BaseKeymapSelector { delegate: BaseKeymapSelectorDelegate, cx: &mut ViewContext, ) -> Self { - let picker = cx.new_view(|cx| Picker::new(delegate, cx)); + let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx)); Self { picker } } }