When completions menu is displayed above cursor, reverse order (#23446)
Considered doing this when previously working on completions menu layout, as it brings the default selection position next to the cursor position, and is generally more symmetrical. With #23445 there is now a more compelling reason, as the "translucent, cropped bottom" display doesn't make sense when displayed above. Release Notes: - N/A
This commit is contained in:
parent
1769bc957b
commit
2f1af2ab69
5 changed files with 163 additions and 56 deletions
|
@ -125,11 +125,16 @@ impl CodeContextMenu {
|
|||
&self,
|
||||
style: &EditorStyle,
|
||||
max_height_in_lines: u32,
|
||||
y_flipped: bool,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> AnyElement {
|
||||
match self {
|
||||
CodeContextMenu::Completions(menu) => menu.render(style, max_height_in_lines, cx),
|
||||
CodeContextMenu::CodeActions(menu) => menu.render(style, max_height_in_lines, cx),
|
||||
CodeContextMenu::Completions(menu) => {
|
||||
menu.render(style, max_height_in_lines, y_flipped, cx)
|
||||
}
|
||||
CodeContextMenu::CodeActions(menu) => {
|
||||
menu.render(style, max_height_in_lines, y_flipped, cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -268,23 +273,12 @@ impl CompletionsMenu {
|
|||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
self.update_selection_index(0, provider, cx);
|
||||
}
|
||||
|
||||
fn select_prev(
|
||||
&mut self,
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
self.update_selection_index(self.prev_match_index(), provider, cx);
|
||||
}
|
||||
|
||||
fn select_next(
|
||||
&mut self,
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
self.update_selection_index(self.next_match_index(), provider, cx);
|
||||
let index = if self.scroll_handle.y_flipped() {
|
||||
self.entries.borrow().len() - 1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
self.update_selection_index(index, provider, cx);
|
||||
}
|
||||
|
||||
fn select_last(
|
||||
|
@ -292,7 +286,37 @@ impl CompletionsMenu {
|
|||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
let index = self.entries.borrow().len() - 1;
|
||||
let index = if self.scroll_handle.y_flipped() {
|
||||
0
|
||||
} else {
|
||||
self.entries.borrow().len() - 1
|
||||
};
|
||||
self.update_selection_index(index, provider, cx);
|
||||
}
|
||||
|
||||
fn select_prev(
|
||||
&mut self,
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
let index = if self.scroll_handle.y_flipped() {
|
||||
self.next_match_index()
|
||||
} else {
|
||||
self.prev_match_index()
|
||||
};
|
||||
self.update_selection_index(index, provider, cx);
|
||||
}
|
||||
|
||||
fn select_next(
|
||||
&mut self,
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
let index = if self.scroll_handle.y_flipped() {
|
||||
self.prev_match_index()
|
||||
} else {
|
||||
self.next_match_index()
|
||||
};
|
||||
self.update_selection_index(index, provider, cx);
|
||||
}
|
||||
|
||||
|
@ -336,6 +360,11 @@ impl CompletionsMenu {
|
|||
}
|
||||
_ => {
|
||||
entries.insert(0, hint);
|
||||
// When `y_flipped`, need to scroll to bring it into view.
|
||||
if self.selected_item == 0 {
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -439,6 +468,7 @@ impl CompletionsMenu {
|
|||
&self,
|
||||
style: &EditorStyle,
|
||||
max_height_in_lines: u32,
|
||||
y_flipped: bool,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> AnyElement {
|
||||
let completions = self.completions.borrow_mut();
|
||||
|
@ -664,6 +694,7 @@ impl CompletionsMenu {
|
|||
.occlude()
|
||||
.max_h(max_height_in_lines as f32 * cx.line_height())
|
||||
.track_scroll(self.scroll_handle.clone())
|
||||
.y_flipped(y_flipped)
|
||||
.with_width_from_item(widest_completion_ix)
|
||||
.with_sizing_behavior(ListSizingBehavior::Infer);
|
||||
|
||||
|
@ -981,39 +1012,63 @@ pub struct CodeActionsMenu {
|
|||
|
||||
impl CodeActionsMenu {
|
||||
fn select_first(&mut self, cx: &mut ViewContext<Editor>) {
|
||||
self.selected_item = 0;
|
||||
self.selected_item = if self.scroll_handle.y_flipped() {
|
||||
self.actions.len() - 1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
fn select_last(&mut self, cx: &mut ViewContext<Editor>) {
|
||||
self.selected_item = if self.scroll_handle.y_flipped() {
|
||||
0
|
||||
} else {
|
||||
self.actions.len() - 1
|
||||
};
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
|
||||
if self.selected_item > 0 {
|
||||
self.selected_item -= 1;
|
||||
self.selected_item = if self.scroll_handle.y_flipped() {
|
||||
self.next_match_index()
|
||||
} else {
|
||||
self.selected_item = self.actions.len() - 1;
|
||||
}
|
||||
self.prev_match_index()
|
||||
};
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_next(&mut self, cx: &mut ViewContext<Editor>) {
|
||||
if self.selected_item + 1 < self.actions.len() {
|
||||
self.selected_item += 1;
|
||||
self.selected_item = if self.scroll_handle.y_flipped() {
|
||||
self.prev_match_index()
|
||||
} else {
|
||||
self.selected_item = 0;
|
||||
}
|
||||
self.next_match_index()
|
||||
};
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_last(&mut self, cx: &mut ViewContext<Editor>) {
|
||||
self.selected_item = self.actions.len() - 1;
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
cx.notify()
|
||||
fn prev_match_index(&self) -> usize {
|
||||
if self.selected_item > 0 {
|
||||
self.selected_item - 1
|
||||
} else {
|
||||
self.actions.len() - 1
|
||||
}
|
||||
}
|
||||
|
||||
fn next_match_index(&self) -> usize {
|
||||
if self.selected_item + 1 < self.actions.len() {
|
||||
self.selected_item + 1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
fn visible(&self) -> bool {
|
||||
|
@ -1032,6 +1087,7 @@ impl CodeActionsMenu {
|
|||
&self,
|
||||
_style: &EditorStyle,
|
||||
max_height_in_lines: u32,
|
||||
y_flipped: bool,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> AnyElement {
|
||||
let actions = self.actions.clone();
|
||||
|
@ -1107,6 +1163,7 @@ impl CodeActionsMenu {
|
|||
.occlude()
|
||||
.max_h(max_height_in_lines as f32 * cx.line_height())
|
||||
.track_scroll(self.scroll_handle.clone())
|
||||
.y_flipped(y_flipped)
|
||||
.with_width_from_item(
|
||||
self.actions
|
||||
.iter()
|
||||
|
|
|
@ -5246,11 +5246,12 @@ impl Editor {
|
|||
&self,
|
||||
style: &EditorStyle,
|
||||
max_height_in_lines: u32,
|
||||
y_flipped: bool,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Option<AnyElement> {
|
||||
self.context_menu.borrow().as_ref().and_then(|menu| {
|
||||
if menu.visible() {
|
||||
Some(menu.render(style, max_height_in_lines, cx))
|
||||
Some(menu.render(style, max_height_in_lines, y_flipped, cx))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
|
|
@ -3136,10 +3136,10 @@ impl EditorElement {
|
|||
let available_above = bottom_y_when_flipped - text_hitbox.top();
|
||||
let available_below = text_hitbox.bottom() - target_position.y;
|
||||
let y_overflows_below = unconstrained_max_height > available_below;
|
||||
let mut y_is_flipped = y_overflows_below && available_above > available_below;
|
||||
let mut y_flipped = y_overflows_below && available_above > available_below;
|
||||
let mut height = cmp::min(
|
||||
unconstrained_max_height,
|
||||
if y_is_flipped {
|
||||
if y_flipped {
|
||||
available_above
|
||||
} else {
|
||||
available_below
|
||||
|
@ -3151,16 +3151,16 @@ impl EditorElement {
|
|||
let available_above = bottom_y_when_flipped;
|
||||
let available_below = viewport_bounds.bottom() - target_position.y;
|
||||
if available_below > 3. * line_height {
|
||||
y_is_flipped = false;
|
||||
y_flipped = false;
|
||||
height = min_height;
|
||||
} else if available_above > 3. * line_height {
|
||||
y_is_flipped = true;
|
||||
y_flipped = true;
|
||||
height = min_height;
|
||||
} else if available_above > available_below {
|
||||
y_is_flipped = true;
|
||||
y_flipped = true;
|
||||
height = available_above;
|
||||
} else {
|
||||
y_is_flipped = false;
|
||||
y_flipped = false;
|
||||
height = available_below;
|
||||
}
|
||||
}
|
||||
|
@ -3169,7 +3169,7 @@ impl EditorElement {
|
|||
|
||||
// TODO(mgsloan): use viewport_bounds.width as a max width when rendering menu.
|
||||
let Some(mut menu_element) = self.editor.update(cx, |editor, cx| {
|
||||
editor.render_context_menu(&self.style, max_height_in_lines, cx)
|
||||
editor.render_context_menu(&self.style, max_height_in_lines, y_flipped, cx)
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
@ -3181,7 +3181,7 @@ impl EditorElement {
|
|||
x: target_position
|
||||
.x
|
||||
.min((viewport_bounds.right() - menu_size.width).max(Pixels::ZERO)),
|
||||
y: if y_is_flipped {
|
||||
y: if y_flipped {
|
||||
bottom_y_when_flipped - menu_size.height
|
||||
} else {
|
||||
target_position.y
|
||||
|
@ -3192,7 +3192,7 @@ impl EditorElement {
|
|||
// Layout documentation aside
|
||||
let menu_bounds = Bounds::new(menu_position, menu_size);
|
||||
let max_menu_size = size(menu_size.width, unconstrained_max_height);
|
||||
let max_menu_bounds = if y_is_flipped {
|
||||
let max_menu_bounds = if y_flipped {
|
||||
Bounds::new(
|
||||
point(
|
||||
menu_position.x,
|
||||
|
@ -3205,7 +3205,7 @@ impl EditorElement {
|
|||
};
|
||||
self.layout_context_menu_aside(
|
||||
text_hitbox,
|
||||
y_is_flipped,
|
||||
y_flipped,
|
||||
menu_position,
|
||||
menu_bounds,
|
||||
max_menu_bounds,
|
||||
|
@ -3220,7 +3220,7 @@ impl EditorElement {
|
|||
fn layout_context_menu_aside(
|
||||
&self,
|
||||
text_hitbox: &Hitbox,
|
||||
y_is_flipped: bool,
|
||||
y_flipped: bool,
|
||||
menu_position: gpui::Point<Pixels>,
|
||||
menu_bounds: Bounds<Pixels>,
|
||||
max_menu_bounds: Bounds<Pixels>,
|
||||
|
@ -3231,7 +3231,7 @@ impl EditorElement {
|
|||
) {
|
||||
let mut extend_amount = Edges::all(MENU_GAP);
|
||||
// Extend to include the cursored line to avoid overlapping it.
|
||||
if y_is_flipped {
|
||||
if y_flipped {
|
||||
extend_amount.bottom = line_height;
|
||||
} else {
|
||||
extend_amount.top = line_height;
|
||||
|
@ -3280,13 +3280,13 @@ impl EditorElement {
|
|||
let fit_within = |available: Edges<Pixels>, wanted: Size<Pixels>| {
|
||||
// Prefer to fit on the same side of the line as the menu, then on the other side of
|
||||
// the line.
|
||||
if !y_is_flipped && wanted.height < available.bottom {
|
||||
if !y_flipped && wanted.height < available.bottom {
|
||||
Some(bottom_position)
|
||||
} else if !y_is_flipped && wanted.height < available.top {
|
||||
} else if !y_flipped && wanted.height < available.top {
|
||||
Some(top_position)
|
||||
} else if y_is_flipped && wanted.height < available.top {
|
||||
} else if y_flipped && wanted.height < available.top {
|
||||
Some(top_position)
|
||||
} else if y_is_flipped && wanted.height < available.bottom {
|
||||
} else if y_flipped && wanted.height < available.bottom {
|
||||
Some(bottom_position)
|
||||
} else {
|
||||
None
|
||||
|
|
|
@ -106,6 +106,8 @@ pub struct UniformListScrollState {
|
|||
pub deferred_scroll_to_item: Option<(usize, ScrollStrategy)>,
|
||||
/// Size of the item, captured during last layout.
|
||||
pub last_item_size: Option<ItemSize>,
|
||||
/// Whether the list was vertically flipped during last layout.
|
||||
pub y_flipped: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
|
@ -125,6 +127,7 @@ impl UniformListScrollHandle {
|
|||
base_handle: ScrollHandle::new(),
|
||||
deferred_scroll_to_item: None,
|
||||
last_item_size: None,
|
||||
y_flipped: false,
|
||||
})))
|
||||
}
|
||||
|
||||
|
@ -133,6 +136,11 @@ impl UniformListScrollHandle {
|
|||
self.0.borrow_mut().deferred_scroll_to_item = Some((ix, strategy));
|
||||
}
|
||||
|
||||
/// Check if the list is flipped vertically.
|
||||
pub fn y_flipped(&self) -> bool {
|
||||
self.0.borrow().y_flipped
|
||||
}
|
||||
|
||||
/// Get the index of the topmost visible child.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn logical_scroll_top_index(&self) -> usize {
|
||||
|
@ -264,9 +272,13 @@ impl Element for UniformList {
|
|||
bounds.bottom_right() - point(border.right + padding.right, border.bottom),
|
||||
);
|
||||
|
||||
if let Some(handle) = self.scroll_handle.as_mut() {
|
||||
handle.0.borrow_mut().base_handle.set_bounds(bounds);
|
||||
}
|
||||
let y_flipped = if let Some(scroll_handle) = self.scroll_handle.as_mut() {
|
||||
let mut scroll_state = scroll_handle.0.borrow_mut();
|
||||
scroll_state.base_handle.set_bounds(bounds);
|
||||
scroll_state.y_flipped
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if self.item_count > 0 {
|
||||
let content_height =
|
||||
|
@ -286,7 +298,10 @@ impl Element for UniformList {
|
|||
scroll_offset.x = Pixels::ZERO;
|
||||
}
|
||||
|
||||
if let Some((ix, scroll_strategy)) = shared_scroll_to_item {
|
||||
if let Some((mut ix, scroll_strategy)) = shared_scroll_to_item {
|
||||
if y_flipped {
|
||||
ix = self.item_count.saturating_sub(ix + 1);
|
||||
}
|
||||
let list_height = padded_bounds.size.height;
|
||||
let mut updated_scroll_offset = shared_scroll_offset.borrow_mut();
|
||||
let item_top = item_height * ix + padding.top;
|
||||
|
@ -330,7 +345,15 @@ impl Element for UniformList {
|
|||
let visible_range = first_visible_element_ix
|
||||
..cmp::min(last_visible_element_ix, self.item_count);
|
||||
|
||||
let mut items = (self.render_items)(visible_range.clone(), cx);
|
||||
let items = if y_flipped {
|
||||
let flipped_range = self.item_count.saturating_sub(visible_range.end)
|
||||
..self.item_count.saturating_sub(visible_range.start);
|
||||
let mut items = (self.render_items)(flipped_range, cx);
|
||||
items.reverse();
|
||||
items
|
||||
} else {
|
||||
(self.render_items)(visible_range.clone(), cx)
|
||||
};
|
||||
|
||||
let content_mask = ContentMask { bounds };
|
||||
cx.with_content_mask(Some(content_mask), |cx| {
|
||||
|
@ -500,6 +523,30 @@ impl UniformList {
|
|||
self.scroll_handle = Some(handle);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets whether the list is flipped vertically, such that item 0 appears at the bottom.
|
||||
pub fn y_flipped(mut self, y_flipped: bool) -> Self {
|
||||
if let Some(ref scroll_handle) = self.scroll_handle {
|
||||
let mut scroll_state = scroll_handle.0.borrow_mut();
|
||||
let mut base_handle = &scroll_state.base_handle;
|
||||
let offset = base_handle.offset();
|
||||
match scroll_state.last_item_size {
|
||||
Some(last_size) if scroll_state.y_flipped != y_flipped => {
|
||||
let new_y_offset =
|
||||
-(offset.y + last_size.contents.height - last_size.item.height);
|
||||
base_handle.set_offset(point(offset.x, new_y_offset));
|
||||
scroll_state.y_flipped = y_flipped;
|
||||
}
|
||||
// Handle case where list is initially flipped.
|
||||
None if y_flipped => {
|
||||
base_handle.set_offset(point(offset.x, Pixels::MIN));
|
||||
scroll_state.y_flipped = y_flipped;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractiveElement for UniformList {
|
||||
|
|
|
@ -2500,6 +2500,8 @@ impl Pixels {
|
|||
pub const ZERO: Pixels = Pixels(0.0);
|
||||
/// The maximum value that can be represented by `Pixels`.
|
||||
pub const MAX: Pixels = Pixels(f32::MAX);
|
||||
/// The minimum value that can be represented by `Pixels`.
|
||||
pub const MIN: Pixels = Pixels(f32::MIN);
|
||||
|
||||
/// Floors the `Pixels` value to the nearest whole number.
|
||||
///
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue