Improve completion, action and shared project popovers' layout (#4226)

Kirill Bulatov created

Before:

* cutoff completion labels and docs
<img width="801" alt="image"
src="https://github.com/zed-industries/zed/assets/2690773/12fe39b2-a8a7-42d8-b697-e2a4fbd836b7">

* too long action labels start to introduce newlines and break the
layout
<img width="716" alt="image"
src="https://github.com/zed-industries/zed/assets/2690773/a0530f1e-4200-4cc4-8e0f-9a63d04e4d5c">

* too long completion proposals are cut off
<img width="1321" alt="image"
src="https://github.com/zed-industries/zed/assets/2690773/1ec154a1-9de5-4e47-9e9f-632b3e33f42a">



After:

* the docs are shifted, but both completions and docs are rendered
entirely now
<img width="1351" alt="image"
src="https://github.com/zed-industries/zed/assets/2690773/ddb0baa4-88b6-45e9-b3e1-496c0d6a0d0f">

* 
<img width="750" alt="image"
src="https://github.com/zed-industries/zed/assets/2690773/102ed77c-d7d0-41d2-a4a0-a8c73285ea8a">

* completions are not jumping in sized anymore, with all elements
aligned to occupy max elements' width and height
<img width="1459" alt="image"
src="https://github.com/zed-industries/zed/assets/2690773/483e3534-b00a-423c-a848-3c4ca1199510">


Release Notes:

- Improved completion and action label layouts

Change summary

crates/collab_ui/src/notifications/incoming_call_notification.rs  |  2 
crates/collab_ui/src/notifications/project_shared_notification.rs |  2 
crates/editor/src/editor.rs                                       | 17 
crates/gpui/src/elements/uniform_list.rs                          | 33 
crates/ui/src/components/list/list_item.rs                        |  1 
5 files changed, 44 insertions(+), 11 deletions(-)

Detailed changes

crates/collab_ui/src/notifications/incoming_call_notification.rs 🔗

@@ -125,7 +125,7 @@ impl Render for IncomingCallNotification {
 
         cx.set_rem_size(ui_font_size);
 
-        div().size_full().font(ui_font).child(
+        h_flex().flex_grow().size_full().font(ui_font).child(
             CollabNotification::new(
                 self.state.call.calling_user.avatar_uri.clone(),
                 Button::new("accept", "Accept").on_click({

crates/collab_ui/src/notifications/project_shared_notification.rs 🔗

@@ -129,7 +129,7 @@ impl Render for ProjectSharedNotification {
 
         cx.set_rem_size(ui_font_size);
 
-        div().size_full().font(ui_font).child(
+        h_flex().flex_grow().size_full().font(ui_font).child(
             CollabNotification::new(
                 self.owner.avatar_uri.clone(),
                 Button::new("open", "Open").on_click(cx.listener(move |this, _event, cx| {

crates/editor/src/editor.rs 🔗

@@ -848,7 +848,7 @@ impl CompletionsMenu {
                     .flex_1()
                     .px_1p5()
                     .py_1()
-                    .min_w(px(260.))
+                    .min_w(px(160.))
                     .max_w(px(640.))
                     .w(px(500.))
                     .overflow_y_scroll()
@@ -910,7 +910,7 @@ impl CompletionsMenu {
                                 None
                             };
 
-                        div().min_w(px(220.)).max_w(px(540.)).child(
+                        h_flex().flex_grow().w_full().min_w(px(120.)).child(
                             ListItem::new(mat.candidate_id)
                                 .inset(true)
                                 .selected(item_ix == selected_item)
@@ -925,7 +925,7 @@ impl CompletionsMenu {
                                         )
                                         .map(|task| task.detach_and_log_err(cx));
                                 }))
-                                .child(h_flex().overflow_hidden().child(completion_label))
+                                .child(h_flex().flex_grow().child(completion_label))
                                 .end_slot::<Div>(documentation_label),
                         )
                     })
@@ -934,7 +934,9 @@ impl CompletionsMenu {
         )
         .max_h(max_height)
         .track_scroll(self.scroll_handle.clone())
-        .with_width_from_item(widest_completion_ix);
+        .with_width_from_item(widest_completion_ix)
+        .use_max_height()
+        .use_max_width();
 
         Popover::new()
             .child(list)
@@ -1076,8 +1078,11 @@ impl CodeActionsMenu {
                         let item_ix = range.start + ix;
                         let selected = selected_item == item_ix;
                         let colors = cx.theme().colors();
-                        div()
+                        h_flex()
+                            .flex_grow()
+                            .w_full()
                             .px_2()
+                            .min_w(px(120.))
                             .text_color(colors.text)
                             .when(selected, |style| {
                                 style
@@ -1121,6 +1126,8 @@ impl CodeActionsMenu {
                 .max_by_key(|(_, action)| action.lsp_action.title.chars().count())
                 .map(|(ix, _)| ix),
         )
+        .use_max_width()
+        .use_max_height()
         .into_any_element();
 
         if self.deployed_from_indicator {

crates/gpui/src/elements/uniform_list.rs 🔗

@@ -56,6 +56,8 @@ where
             ..Default::default()
         },
         scroll_handle: None,
+        use_max_width: false,
+        use_max_height: false,
     }
 }
 
@@ -64,6 +66,8 @@ pub struct UniformList {
     id: ElementId,
     item_count: usize,
     item_to_measure_index: usize,
+    use_max_width: bool,
+    use_max_height: bool,
     render_items:
         Box<dyn for<'a> Fn(Range<usize>, &'a mut WindowContext) -> SmallVec<[AnyElement; 64]>>,
     interactivity: Interactivity,
@@ -280,19 +284,40 @@ impl UniformList {
         self
     }
 
+    /// Forces the list to use the `AvailableSpace::MaxContent` for its items width during laying out.
+    pub fn use_max_width(mut self) -> Self {
+        self.use_max_width = true;
+        self
+    }
+
+    /// Forces the list to use the `AvailableSpace::MaxContent` for its items' height during laying out.
+    pub fn use_max_height(mut self) -> Self {
+        self.use_max_height = true;
+        self
+    }
+
     fn measure_item(&self, list_width: Option<Pixels>, cx: &mut ElementContext) -> Size<Pixels> {
         if self.item_count == 0 {
             return Size::default();
         }
 
+        let width_default = if self.use_max_width {
+            AvailableSpace::MaxContent
+        } else {
+            AvailableSpace::MinContent
+        };
+        let height_default = if self.use_max_height {
+            AvailableSpace::MaxContent
+        } else {
+            AvailableSpace::MinContent
+        };
+
         let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1);
         let mut items = (self.render_items)(item_ix..item_ix + 1, cx);
         let mut item_to_measure = items.pop().unwrap();
         let available_space = size(
-            list_width.map_or(AvailableSpace::MinContent, |width| {
-                AvailableSpace::Definite(width)
-            }),
-            AvailableSpace::MinContent,
+            list_width.map_or(width_default, AvailableSpace::Definite),
+            height_default,
         );
         item_to_measure.measure(available_space, cx)
     }

crates/ui/src/components/list/list_item.rs 🔗

@@ -149,6 +149,7 @@ impl ParentElement for ListItem {
 impl RenderOnce for ListItem {
     fn render(self, cx: &mut WindowContext) -> impl IntoElement {
         h_flex()
+            .flex_grow()
             .id(self.id)
             .w_full()
             .relative()