Position prediction popover adjacent to edit if possible (#24750)

Max Brunsfeld , Danilo Leal , rtfeldman , and Agus Zubiaga created

Also, place accept key binding indicator on right edge of popover to
avoid obscuring content above.

Release Notes:

- N/A

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: rtfeldman <oss@rtfeldman.com>
Co-authored-by: Agus Zubiaga <hi@aguz.me>

Change summary

crates/editor/src/element.rs | 102 +++++++++++++++++--------------------
1 file changed, 48 insertions(+), 54 deletions(-)

Detailed changes

crates/editor/src/element.rs 🔗

@@ -3808,39 +3808,38 @@ impl EditorElement {
                 );
 
                 let styled_text = highlighted_edits.to_styled_text(&style.text);
+                let line_count = highlighted_edits.text.lines().count();
 
-                const ACCEPT_INDICATOR_HEIGHT: Pixels = px(24.);
+                const BORDER_WIDTH: Pixels = px(1.);
 
-                let mut element = v_flex()
-                    .items_end()
+                let mut element = h_flex()
+                    .items_start()
+                    .child(
+                        div()
+                            .bg(cx.theme().colors().editor_background)
+                            .border(BORDER_WIDTH)
+                            .shadow_sm()
+                            .border_color(cx.theme().colors().border)
+                            .rounded_l_lg()
+                            .when(line_count > 1, |el| el.rounded_br_lg())
+                            .pr_1()
+                            .child(styled_text),
+                    )
                     .child(
                         h_flex()
-                            .h(ACCEPT_INDICATOR_HEIGHT)
-                            .mb(px(-1.))
+                            .h(line_height + BORDER_WIDTH * px(2.))
                             .px_1p5()
                             .gap_1()
                             .shadow_sm()
                             .bg(Editor::edit_prediction_line_popover_bg_color(cx))
-                            .border_1()
-                            .border_b_0()
+                            .border(BORDER_WIDTH)
+                            .border_l_0()
                             .border_color(cx.theme().colors().border)
-                            .rounded_t_lg()
+                            .rounded_r_lg()
                             .children(editor.render_edit_prediction_accept_keybind(window, cx)),
                     )
-                    .child(
-                        div()
-                            .bg(cx.theme().colors().editor_background)
-                            .border_1()
-                            .shadow_sm()
-                            .border_color(cx.theme().colors().border)
-                            .rounded_lg()
-                            .rounded_tr(Pixels::ZERO)
-                            .child(styled_text),
-                    )
                     .into_any();
 
-                let line_count = highlighted_edits.text.lines().count();
-
                 let longest_row =
                     editor_snapshot.longest_row_in_range(edit_start.row()..edit_end.row() + 1);
                 let longest_line_width = if visible_row_range.contains(&longest_row) {
@@ -3871,55 +3870,50 @@ impl EditorElement {
 
                 // Fully visible if it can be displayed within the window (allow overlapping other
                 // panes). However, this is only allowed if the popover starts within text_bounds.
-                let is_fully_visible = x_after_longest < text_bounds.right()
+                let can_position_to_the_right = x_after_longest < text_bounds.right()
                     && x_after_longest + element_bounds.width < viewport_bounds.right();
 
-                let mut origin = if is_fully_visible {
+                let mut origin = if can_position_to_the_right {
                     point(
                         x_after_longest,
                         text_bounds.origin.y + edit_start.row().as_f32() * line_height
                             - scroll_pixel_position.y,
                     )
                 } else {
-                    // Avoid overlapping both the edited rows and the user's cursor.
-                    let target_above = DisplayRow(
-                        edit_start
-                            .row()
-                            .0
-                            .min(
-                                newest_selection_head
-                                    .map_or(u32::MAX, |cursor_row| cursor_row.row().0),
-                            )
-                            .saturating_sub(line_count as u32),
-                    );
-                    let mut row_target;
-                    if visible_row_range.contains(&DisplayRow(target_above.0.saturating_sub(1))) {
-                        row_target = target_above;
-                    } else {
-                        row_target = DisplayRow(
-                            edit_end.row().0.max(
-                                newest_selection_head.map_or(0, |cursor_row| cursor_row.row().0),
-                            ) + 1,
-                        );
-                        if !visible_row_range.contains(&row_target) {
-                            // Not visible, so fallback on displaying immediately below the cursor.
-                            if let Some(cursor) = newest_selection_head {
-                                row_target = DisplayRow(cursor.row().0 + 1);
-                            } else {
-                                // Not visible and no cursor visible, so fallback on displaying at the top of the editor.
-                                row_target = DisplayRow(0);
-                            }
-                        }
-                    };
+                    let cursor_row = newest_selection_head.map(|head| head.row());
+                    let above_edit = edit_start
+                        .row()
+                        .0
+                        .checked_sub(line_count as u32)
+                        .map(DisplayRow);
+                    let below_edit = Some(edit_end.row() + 1);
+                    let above_cursor = cursor_row
+                        .and_then(|row| row.0.checked_sub(line_count as u32).map(DisplayRow));
+                    let below_cursor = cursor_row.map(|cursor_row| cursor_row + 1);
+
+                    // Place the edit popover adjacent to the edit if there is a location
+                    // available that is onscreen and does not obscure the cursor. Otherwise,
+                    // place it adjacent to the cursor.
+                    let row_target = [above_edit, below_edit, above_cursor, below_cursor]
+                        .into_iter()
+                        .flatten()
+                        .find(|&start_row| {
+                            let end_row = start_row + line_count as u32;
+                            visible_row_range.contains(&start_row)
+                                && visible_row_range.contains(&end_row)
+                                && cursor_row.map_or(true, |cursor_row| {
+                                    !((start_row..end_row).contains(&cursor_row))
+                                })
+                        })?;
 
-                    text_bounds.origin
+                    content_origin
                         + point(
                             -scroll_pixel_position.x,
                             row_target.as_f32() * line_height - scroll_pixel_position.y,
                         )
                 };
 
-                origin.y -= ACCEPT_INDICATOR_HEIGHT;
+                origin.x -= BORDER_WIDTH;
 
                 window.defer_draw(element, origin, 1);