Detailed changes
@@ -593,7 +593,6 @@ impl PickerDelegate for ConfigOptionPickerDelegate {
ui::DocumentationAside::new(
ui::DocumentationSide::Left,
- ui::DocumentationEdge::Top,
Rc::new(move |_| {
v_flex()
.gap_1()
@@ -604,6 +603,10 @@ impl PickerDelegate for ConfigOptionPickerDelegate {
)
})
}
+
+ fn documentation_aside_index(&self) -> Option<usize> {
+ self.selected_description.as_ref().map(|(ix, _, _)| *ix)
+ }
}
fn extract_options(
@@ -7,8 +7,8 @@ use gpui::{Context, Entity, FocusHandle, WeakEntity, Window, prelude::*};
use settings::Settings as _;
use std::{rc::Rc, sync::Arc};
use ui::{
- Button, ContextMenu, ContextMenuEntry, DocumentationEdge, DocumentationSide, KeyBinding,
- PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*,
+ Button, ContextMenu, ContextMenuEntry, DocumentationSide, KeyBinding, PopoverMenu,
+ PopoverMenuHandle, Tooltip, prelude::*,
};
use crate::{CycleModeSelector, ToggleProfileSelector, ui::HoldForDefault};
@@ -105,7 +105,7 @@ impl ModeSelector {
.toggleable(IconPosition::End, is_selected);
let entry = if let Some(description) = &mode.description {
- entry.documentation_aside(side, DocumentationEdge::Bottom, {
+ entry.documentation_aside(side, {
let description = description.clone();
move |_| {
@@ -16,7 +16,7 @@ use itertools::Itertools;
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
use settings::SettingsStore;
-use ui::{DocumentationAside, DocumentationEdge, DocumentationSide, IntoElement, prelude::*};
+use ui::{DocumentationAside, DocumentationSide, IntoElement, prelude::*};
use util::ResultExt;
use zed_actions::agent::OpenSettings;
@@ -388,7 +388,6 @@ impl PickerDelegate for AcpModelPickerDelegate {
DocumentationAside::new(
DocumentationSide::Left,
- DocumentationEdge::Top,
Rc::new(move |_| {
v_flex()
.gap_1()
@@ -400,6 +399,10 @@ impl PickerDelegate for AcpModelPickerDelegate {
})
}
+ fn documentation_aside_index(&self) -> Option<usize> {
+ self.selected_description.as_ref().map(|(ix, _, _)| *ix)
+ }
+
fn render_footer(
&self,
_window: &mut Window,
@@ -15,8 +15,8 @@ use std::{
sync::{Arc, atomic::AtomicBool},
};
use ui::{
- DocumentationAside, DocumentationEdge, DocumentationSide, HighlightedLabel, KeyBinding,
- LabelSize, ListItem, ListItemSpacing, PopoverMenuHandle, TintColor, Tooltip, prelude::*,
+ DocumentationAside, DocumentationSide, HighlightedLabel, KeyBinding, LabelSize, ListItem,
+ ListItemSpacing, PopoverMenuHandle, TintColor, Tooltip, prelude::*,
};
/// Trait for types that can provide and manage agent profiles
@@ -244,6 +244,7 @@ pub(crate) struct ProfilePickerDelegate {
string_candidates: Arc<Vec<StringMatchCandidate>>,
filtered_entries: Vec<ProfilePickerEntry>,
selected_index: usize,
+ hovered_index: Option<usize>,
query: String,
cancel: Option<Arc<AtomicBool>>,
focus_handle: FocusHandle,
@@ -270,6 +271,7 @@ impl ProfilePickerDelegate {
string_candidates,
filtered_entries,
selected_index: 0,
+ hovered_index: None,
query: String::new(),
cancel: None,
focus_handle,
@@ -578,23 +580,38 @@ impl PickerDelegate for ProfilePickerDelegate {
let candidate = self.candidates.get(entry.candidate_index)?;
let active_id = self.provider.profile_id(cx);
let is_active = active_id == candidate.id;
+ let has_documentation = Self::documentation(candidate).is_some();
Some(
- ListItem::new(candidate.id.0.clone())
- .inset(true)
- .spacing(ListItemSpacing::Sparse)
- .toggle_state(selected)
- .child(HighlightedLabel::new(
- candidate.name.clone(),
- entry.positions.clone(),
- ))
- .when(is_active, |this| {
- this.end_slot(
- div()
- .pr_2()
- .child(Icon::new(IconName::Check).color(Color::Accent)),
- )
+ div()
+ .id(("profile-picker-item", ix))
+ .when(has_documentation, |this| {
+ this.on_hover(cx.listener(move |picker, hovered, _, cx| {
+ if *hovered {
+ picker.delegate.hovered_index = Some(ix);
+ } else if picker.delegate.hovered_index == Some(ix) {
+ picker.delegate.hovered_index = None;
+ }
+ cx.notify();
+ }))
})
+ .child(
+ ListItem::new(candidate.id.0.clone())
+ .inset(true)
+ .spacing(ListItemSpacing::Sparse)
+ .toggle_state(selected)
+ .child(HighlightedLabel::new(
+ candidate.name.clone(),
+ entry.positions.clone(),
+ ))
+ .when(is_active, |this| {
+ this.end_slot(
+ div()
+ .pr_2()
+ .child(Icon::new(IconName::Check).color(Color::Accent)),
+ )
+ }),
+ )
.into_any_element(),
)
}
@@ -608,7 +625,8 @@ impl PickerDelegate for ProfilePickerDelegate {
) -> Option<DocumentationAside> {
use std::rc::Rc;
- let entry = match self.filtered_entries.get(self.selected_index)? {
+ let hovered_index = self.hovered_index?;
+ let entry = match self.filtered_entries.get(hovered_index)? {
ProfilePickerEntry::Profile(entry) => entry,
ProfilePickerEntry::Header(_) => return None,
};
@@ -626,11 +644,14 @@ impl PickerDelegate for ProfilePickerDelegate {
Some(DocumentationAside {
side,
- edge: DocumentationEdge::Top,
render: Rc::new(move |_| Label::new(docs_aside.clone()).into_any_element()),
})
}
+ fn documentation_aside_index(&self) -> Option<usize> {
+ self.hovered_index
+ }
+
fn render_footer(
&self,
_: &mut Window,
@@ -718,6 +739,7 @@ mod tests {
string_candidates: Arc::new(Vec::new()),
filtered_entries: Vec::new(),
selected_index: 0,
+ hovered_index: None,
query: String::new(),
cancel: None,
focus_handle,
@@ -752,6 +774,7 @@ mod tests {
background: cx.background_executor().clone(),
candidates,
string_candidates: Arc::new(Vec::new()),
+ hovered_index: None,
filtered_entries: vec![
ProfilePickerEntry::Profile(ProfileMatchEntry {
candidate_index: 0,
@@ -36,8 +36,8 @@ use std::{
};
use supermaven::{AccountStatus, Supermaven};
use ui::{
- Clickable, ContextMenu, ContextMenuEntry, DocumentationEdge, DocumentationSide, IconButton,
- IconButtonShape, Indicator, PopoverMenu, PopoverMenuHandle, ProgressBar, Tooltip, prelude::*,
+ Clickable, ContextMenu, ContextMenuEntry, DocumentationSide, IconButton, IconButtonShape,
+ Indicator, PopoverMenu, PopoverMenuHandle, ProgressBar, Tooltip, prelude::*,
};
use util::ResultExt as _;
use workspace::{
@@ -680,7 +680,7 @@ impl EditPredictionButton {
menu = menu.item(
entry
.disabled(true)
- .documentation_aside(DocumentationSide::Left, DocumentationEdge::Top, move |_cx| {
+ .documentation_aside(DocumentationSide::Left, move |_cx| {
Label::new(format!("Edit predictions cannot be toggled for this buffer because they are disabled for {}", language.name()))
.into_any_element()
})
@@ -726,7 +726,7 @@ impl EditPredictionButton {
.item(
ContextMenuEntry::new("Eager")
.toggleable(IconPosition::Start, eager_mode)
- .documentation_aside(DocumentationSide::Left, DocumentationEdge::Top, move |_| {
+ .documentation_aside(DocumentationSide::Left, move |_| {
Label::new("Display predictions inline when there are no language server completions available.").into_any_element()
})
.handler({
@@ -739,7 +739,7 @@ impl EditPredictionButton {
.item(
ContextMenuEntry::new("Subtle")
.toggleable(IconPosition::Start, subtle_mode)
- .documentation_aside(DocumentationSide::Left, DocumentationEdge::Top, move |_| {
+ .documentation_aside(DocumentationSide::Left, move |_| {
Label::new("Display predictions inline only when holding a modifier key (alt by default).").into_any_element()
})
.handler({
@@ -778,7 +778,7 @@ impl EditPredictionButton {
.toggleable(IconPosition::Start, data_collection.is_enabled())
.icon(icon_name)
.icon_color(icon_color)
- .documentation_aside(DocumentationSide::Left, DocumentationEdge::Top, move |cx| {
+ .documentation_aside(DocumentationSide::Left, move |cx| {
let (msg, label_color, icon_name, icon_color) = match (is_open_source, is_collecting) {
(true, true) => (
"Project identified as open source, and you're sharing data.",
@@ -862,7 +862,7 @@ impl EditPredictionButton {
ContextMenuEntry::new("Configure Excluded Files")
.icon(IconName::LockOutlined)
.icon_color(Color::Muted)
- .documentation_aside(DocumentationSide::Left, DocumentationEdge::Top, |_| {
+ .documentation_aside(DocumentationSide::Left, |_| {
Label::new(indoc!{"
Open your settings to add sensitive paths for which Zed will never predict edits."}).into_any_element()
})
@@ -17,8 +17,8 @@ use project::{
};
use settings::{Settings as _, SettingsStore};
use ui::{
- Context, ContextMenu, ContextMenuEntry, ContextMenuItem, DocumentationAside, DocumentationEdge,
- DocumentationSide, Indicator, PopoverMenu, PopoverMenuHandle, Tooltip, Window, prelude::*,
+ Context, ContextMenu, ContextMenuEntry, ContextMenuItem, DocumentationAside, DocumentationSide,
+ Indicator, PopoverMenu, PopoverMenuHandle, Tooltip, Window, prelude::*,
};
use util::{ResultExt, rel_path::RelPath};
@@ -384,7 +384,6 @@ impl LanguageServerState {
tooltip_text.map(|tooltip_text| {
DocumentationAside::new(
DocumentationSide::Right,
- DocumentationEdge::Top,
Rc::new(move |_| Label::new(tooltip_text.clone()).into_any_element()),
)
}),
@@ -9,19 +9,21 @@ use editor::{
scroll::Autoscroll,
};
use gpui::{
- Action, AnyElement, App, ClickEvent, Context, DismissEvent, Entity, EventEmitter, FocusHandle,
- Focusable, Length, ListSizingBehavior, ListState, MouseButton, MouseUpEvent, Render,
- ScrollStrategy, Task, UniformListScrollHandle, Window, actions, div, list, prelude::*,
- uniform_list,
+ Action, AnyElement, App, Bounds, ClickEvent, Context, DismissEvent, Entity, EventEmitter,
+ FocusHandle, Focusable, Length, ListSizingBehavior, ListState, MouseButton, MouseUpEvent,
+ Pixels, Render, ScrollStrategy, Task, UniformListScrollHandle, Window, actions, canvas, div,
+ list, prelude::*, uniform_list,
};
use head::Head;
use schemars::JsonSchema;
use serde::Deserialize;
-use std::{ops::Range, sync::Arc, time::Duration};
+use std::{
+ cell::Cell, cell::RefCell, collections::HashMap, ops::Range, rc::Rc, sync::Arc, time::Duration,
+};
use theme::ThemeSettings;
use ui::{
- Color, Divider, DocumentationAside, DocumentationEdge, DocumentationSide, Label, ListItem,
- ListItemSpacing, ScrollAxes, Scrollbars, WithScrollbar, prelude::*, utils::WithRemSize, v_flex,
+ Color, Divider, DocumentationAside, DocumentationSide, Label, ListItem, ListItemSpacing,
+ ScrollAxes, Scrollbars, WithScrollbar, prelude::*, utils::WithRemSize, v_flex,
};
use workspace::{ModalView, item::Settings};
@@ -72,6 +74,10 @@ pub struct Picker<D: PickerDelegate> {
///
/// Set this to `false` when rendering the `Picker` as part of a larger modal.
is_modal: bool,
+ /// Bounds tracking for the picker container (for aside positioning)
+ picker_bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
+ /// Bounds tracking for items (for aside positioning) - maps item index to bounds
+ item_bounds: Rc<RefCell<HashMap<usize, Bounds<Pixels>>>>,
}
#[derive(Debug, Default, Clone, Copy, PartialEq)]
@@ -243,6 +249,13 @@ pub trait PickerDelegate: Sized + 'static {
) -> Option<DocumentationAside> {
None
}
+
+ /// Returns the index of the item whose documentation aside should be shown.
+ /// This is used to position the aside relative to that item.
+ /// Typically this is the hovered item, not necessarily the selected item.
+ fn documentation_aside_index(&self) -> Option<usize> {
+ None
+ }
}
impl<D: PickerDelegate> Focusable for Picker<D> {
@@ -329,6 +342,8 @@ impl<D: PickerDelegate> Picker<D> {
max_height: Some(rems(24.).into()),
show_scrollbar: false,
is_modal: true,
+ picker_bounds: Rc::new(Cell::new(None)),
+ item_bounds: Rc::new(RefCell::new(HashMap::default())),
};
this.update_matches("".to_string(), window, cx);
// give the delegate 4ms to render the first set of suggestions.
@@ -750,9 +765,23 @@ impl<D: PickerDelegate> Picker<D> {
cx: &mut Context<Self>,
ix: usize,
) -> impl IntoElement + use<D> {
+ let item_bounds = self.item_bounds.clone();
+
div()
.id(("item", ix))
.cursor_pointer()
+ .child(
+ canvas(
+ move |bounds, _window, _cx| {
+ item_bounds.borrow_mut().insert(ix, bounds);
+ },
+ |_bounds, _state, _window, _cx| {},
+ )
+ .size_full()
+ .absolute()
+ .top_0()
+ .left_0(),
+ )
.on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
this.handle_click(ix, event.modifiers().secondary(), window, cx)
}))
@@ -847,11 +876,24 @@ impl<D: PickerDelegate> Render for Picker<D> {
let aside = self.delegate.documentation_aside(window, cx);
let editor_position = self.delegate.editor_position();
+ let picker_bounds = self.picker_bounds.clone();
let menu = v_flex()
.key_context("Picker")
.size_full()
.when_some(self.width, |el, width| el.w(width))
.overflow_hidden()
+ .child(
+ canvas(
+ move |bounds, _window, _cx| {
+ picker_bounds.set(Some(bounds));
+ },
+ |_bounds, _state, _window, _cx| {},
+ )
+ .size_full()
+ .absolute()
+ .top_0()
+ .left_0(),
+ )
// This is a bit of a hack to remove the modal styling when we're rendering the `Picker`
// as a part of a modal rather than the entire modal.
//
@@ -949,21 +991,39 @@ impl<D: PickerDelegate> Render for Picker<D> {
};
if is_wide_window {
- div().relative().child(menu).child(
- h_flex()
- .absolute()
- .when(aside.side == DocumentationSide::Left, |this| {
- this.right_full().mr_1()
- })
- .when(aside.side == DocumentationSide::Right, |this| {
- this.left_full().ml_1()
- })
- .when(aside.edge == DocumentationEdge::Top, |this| this.top_0())
- .when(aside.edge == DocumentationEdge::Bottom, |this| {
- this.bottom_0()
- })
- .child(render_aside(aside, cx)),
- )
+ let aside_index = self.delegate.documentation_aside_index();
+ let picker_bounds = self.picker_bounds.get();
+ let item_bounds =
+ aside_index.and_then(|ix| self.item_bounds.borrow().get(&ix).copied());
+
+ let item_position = match (picker_bounds, item_bounds) {
+ (Some(picker_bounds), Some(item_bounds)) => {
+ let relative_top = item_bounds.origin.y - picker_bounds.origin.y;
+ let height = item_bounds.size.height;
+ Some((relative_top, height))
+ }
+ _ => None,
+ };
+
+ div()
+ .relative()
+ .child(menu)
+ // Only render the aside once we have bounds to avoid flicker
+ .when_some(item_position, |this, (top, height)| {
+ this.child(
+ h_flex()
+ .absolute()
+ .when(aside.side == DocumentationSide::Left, |el| {
+ el.right_full().mr_1()
+ })
+ .when(aside.side == DocumentationSide::Right, |el| {
+ el.left_full().ml_1()
+ })
+ .top(top)
+ .h(height)
+ .child(render_aside(aside, cx)),
+ )
+ })
} else {
v_flex()
.w_full()
@@ -10,7 +10,8 @@ use gpui::{
use menu::{SelectChild, SelectFirst, SelectLast, SelectNext, SelectParent, SelectPrevious};
use settings::Settings;
use std::{
- cell::Cell,
+ cell::{Cell, RefCell},
+ collections::HashMap,
rc::Rc,
time::{Duration, Instant},
};
@@ -182,12 +183,10 @@ impl ContextMenuEntry {
pub fn documentation_aside(
mut self,
side: DocumentationSide,
- edge: DocumentationEdge,
render: impl Fn(&mut App) -> AnyElement + 'static,
) -> Self {
self.documentation_aside = Some(DocumentationAside {
side,
- edge,
render: Rc::new(render),
});
@@ -215,14 +214,16 @@ pub struct ContextMenu {
key_context: SharedString,
_on_blur_subscription: Subscription,
keep_open_on_confirm: bool,
- documentation_aside: Option<(usize, DocumentationAside)>,
fixed_width: Option<DefiniteLength>,
+ main_menu: Option<Entity<ContextMenu>>,
+ main_menu_observed_bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
+ // Docs aide-related fields
+ documentation_aside: Option<(usize, DocumentationAside)>,
+ aside_trigger_bounds: Rc<RefCell<HashMap<usize, Bounds<Pixels>>>>,
// Submenu-related fields
- parent_menu: Option<Entity<ContextMenu>>,
submenu_state: SubmenuState,
hover_target: HoverTarget,
submenu_safety_threshold_x: Option<Pixels>,
- submenu_observed_bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
submenu_trigger_bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
submenu_trigger_mouse_down: bool,
ignore_blur_until: Option<Instant>,
@@ -234,27 +235,15 @@ pub enum DocumentationSide {
Right,
}
-#[derive(Copy, Default, Clone, PartialEq, Eq)]
-pub enum DocumentationEdge {
- #[default]
- Top,
- Bottom,
-}
-
#[derive(Clone)]
pub struct DocumentationAside {
pub side: DocumentationSide,
- pub edge: DocumentationEdge,
pub render: Rc<dyn Fn(&mut App) -> AnyElement>,
}
impl DocumentationAside {
- pub fn new(
- side: DocumentationSide,
- edge: DocumentationEdge,
- render: Rc<dyn Fn(&mut App) -> AnyElement>,
- ) -> Self {
- Self { side, edge, render }
+ pub fn new(side: DocumentationSide, render: Rc<dyn Fn(&mut App) -> AnyElement>) -> Self {
+ Self { side, render }
}
}
@@ -287,7 +276,7 @@ impl ContextMenu {
}
}
- if this.parent_menu.is_none() {
+ if this.main_menu.is_none() {
if let SubmenuState::Open(open_submenu) = &this.submenu_state {
let submenu_focus = open_submenu.entity.read(cx).focus_handle.clone();
if submenu_focus.contains_focused(window, cx) {
@@ -314,13 +303,14 @@ impl ContextMenu {
key_context: "menu".into(),
_on_blur_subscription,
keep_open_on_confirm: false,
- documentation_aside: None,
fixed_width: None,
- parent_menu: None,
+ main_menu: None,
+ main_menu_observed_bounds: Rc::new(Cell::new(None)),
+ documentation_aside: None,
+ aside_trigger_bounds: Rc::new(RefCell::new(HashMap::default())),
submenu_state: SubmenuState::Closed,
hover_target: HoverTarget::MainMenu,
submenu_safety_threshold_x: None,
- submenu_observed_bounds: Rc::new(Cell::new(None)),
submenu_trigger_bounds: Rc::new(Cell::new(None)),
submenu_trigger_mouse_down: false,
ignore_blur_until: None,
@@ -363,7 +353,7 @@ impl ContextMenu {
}
}
- if this.parent_menu.is_none() {
+ if this.main_menu.is_none() {
if let SubmenuState::Open(open_submenu) = &this.submenu_state {
let submenu_focus = open_submenu.entity.read(cx).focus_handle.clone();
if submenu_focus.contains_focused(window, cx) {
@@ -390,13 +380,14 @@ impl ContextMenu {
key_context: "menu".into(),
_on_blur_subscription,
keep_open_on_confirm: true,
- documentation_aside: None,
fixed_width: None,
- parent_menu: None,
+ main_menu: None,
+ main_menu_observed_bounds: Rc::new(Cell::new(None)),
+ documentation_aside: None,
+ aside_trigger_bounds: Rc::new(RefCell::new(HashMap::default())),
submenu_state: SubmenuState::Closed,
hover_target: HoverTarget::MainMenu,
submenu_safety_threshold_x: None,
- submenu_observed_bounds: Rc::new(Cell::new(None)),
submenu_trigger_bounds: Rc::new(Cell::new(None)),
submenu_trigger_mouse_down: false,
ignore_blur_until: None,
@@ -444,7 +435,7 @@ impl ContextMenu {
}
}
- if this.parent_menu.is_none() {
+ if this.main_menu.is_none() {
if let SubmenuState::Open(open_submenu) = &this.submenu_state {
let submenu_focus =
open_submenu.entity.read(cx).focus_handle.clone();
@@ -458,13 +449,14 @@ impl ContextMenu {
},
),
keep_open_on_confirm: false,
- documentation_aside: None,
fixed_width: None,
- parent_menu: None,
+ main_menu: None,
+ main_menu_observed_bounds: Rc::new(Cell::new(None)),
+ documentation_aside: None,
+ aside_trigger_bounds: Rc::new(RefCell::new(HashMap::default())),
submenu_state: SubmenuState::Closed,
hover_target: HoverTarget::MainMenu,
submenu_safety_threshold_x: None,
- submenu_observed_bounds: Rc::new(Cell::new(None)),
submenu_trigger_bounds: Rc::new(Cell::new(None)),
submenu_trigger_mouse_down: false,
ignore_blur_until: None,
@@ -837,7 +829,7 @@ impl ContextMenu {
(handler)(context, window, cx)
}
- if self.parent_menu.is_some() && !self.keep_open_on_confirm {
+ if self.main_menu.is_some() && !self.keep_open_on_confirm {
self.clicked = true;
}
@@ -849,11 +841,11 @@ impl ContextMenu {
}
pub fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
- if self.parent_menu.is_some() {
+ if self.main_menu.is_some() {
cx.emit(DismissEvent);
// Restore keyboard focus to the parent menu so arrow keys / Escape / Enter work again.
- if let Some(parent) = &self.parent_menu {
+ if let Some(parent) = &self.main_menu {
let parent_focus = parent.read(cx).focus_handle.clone();
parent.update(cx, |parent, _cx| {
@@ -986,11 +978,11 @@ impl ContextMenu {
window: &mut Window,
cx: &mut Context<Self>,
) {
- if self.parent_menu.is_none() {
+ if self.main_menu.is_none() {
return;
}
- if let Some(parent) = &self.parent_menu {
+ if let Some(parent) = &self.main_menu {
let parent_clone = parent.clone();
let parent_focus = parent.read(cx).focus_handle.clone();
@@ -1090,13 +1082,14 @@ impl ContextMenu {
key_context: "menu".into(),
_on_blur_subscription,
keep_open_on_confirm: false,
- documentation_aside: None,
fixed_width: None,
- parent_menu: Some(parent_entity),
+ documentation_aside: None,
+ aside_trigger_bounds: Rc::new(RefCell::new(HashMap::default())),
+ main_menu: Some(parent_entity),
+ main_menu_observed_bounds: Rc::new(Cell::new(None)),
submenu_state: SubmenuState::Closed,
hover_target: HoverTarget::MainMenu,
submenu_safety_threshold_x: None,
- submenu_observed_bounds: Rc::new(Cell::new(None)),
submenu_trigger_bounds: Rc::new(Cell::new(None)),
submenu_trigger_mouse_down: false,
ignore_blur_until: None,
@@ -1111,7 +1104,7 @@ impl ContextMenu {
self.submenu_state = SubmenuState::Closed;
self.hover_target = HoverTarget::MainMenu;
self.submenu_safety_threshold_x = None;
- self.submenu_observed_bounds.set(None);
+ self.main_menu_observed_bounds.set(None);
self.submenu_trigger_bounds.set(None);
if clear_selection {
@@ -1142,7 +1135,7 @@ impl ContextMenu {
// If we're switching from one submenu item to another, throw away any previously-captured
// offset so we don't reuse a stale position.
- self.submenu_observed_bounds.set(None);
+ self.main_menu_observed_bounds.set(None);
self.submenu_trigger_bounds.set(None);
self.submenu_safety_threshold_x = None;
@@ -1265,6 +1258,7 @@ impl ContextMenu {
let handler = handler.clone();
let menu = cx.entity().downgrade();
let selectable = *selectable;
+ let aside_trigger_bounds = self.aside_trigger_bounds.clone();
div()
.id(("context-menu-child", ix))
@@ -1280,6 +1274,23 @@ impl ContextMenu {
cx.notify();
}))
})
+ .when(documentation_aside.is_some(), |this| {
+ this.child(
+ canvas(
+ {
+ let aside_trigger_bounds = aside_trigger_bounds.clone();
+ move |bounds, _window, _cx| {
+ aside_trigger_bounds.borrow_mut().insert(ix, bounds);
+ }
+ },
+ |_bounds, _state, _window, _cx| {},
+ )
+ .size_full()
+ .absolute()
+ .top_0()
+ .left_0(),
+ )
+ })
.child(
ListItem::new(ix)
.inset(true)
@@ -1453,7 +1464,7 @@ impl ContextMenu {
}
fn padded_submenu_bounds(&self) -> Option<Bounds<Pixels>> {
- let bounds = self.submenu_observed_bounds.get()?;
+ let bounds = self.main_menu_observed_bounds.get()?;
Some(Bounds {
origin: Point {
x: bounds.origin.x - px(50.0),
@@ -1473,7 +1484,7 @@ impl ContextMenu {
offset: Pixels,
cx: &mut Context<Self>,
) -> impl IntoElement {
- let bounds_cell = self.submenu_observed_bounds.clone();
+ let bounds_cell = self.main_menu_observed_bounds.clone();
let canvas = canvas(
{
move |bounds, _window, _cx| {
@@ -1618,6 +1629,8 @@ impl ContextMenu {
.into_any_element()
};
+ let aside_trigger_bounds = self.aside_trigger_bounds.clone();
+
div()
.id(("context-menu-child", ix))
.when_some(documentation_aside.clone(), |this, documentation_aside| {
@@ -1631,13 +1644,30 @@ impl ContextMenu {
cx.notify();
}))
})
+ .when(documentation_aside.is_some(), |this| {
+ this.child(
+ canvas(
+ {
+ let aside_trigger_bounds = aside_trigger_bounds.clone();
+ move |bounds, _window, _cx| {
+ aside_trigger_bounds.borrow_mut().insert(ix, bounds);
+ }
+ },
+ |_bounds, _state, _window, _cx| {},
+ )
+ .size_full()
+ .absolute()
+ .top_0()
+ .left_0(),
+ )
+ })
.child(
ListItem::new(ix)
.group_name("label_container")
.inset(true)
.disabled(*disabled)
.toggle_state(Some(ix) == self.selected_index)
- .when(self.parent_menu.is_none() && !*disabled, |item| {
+ .when(self.main_menu.is_none() && !*disabled, |item| {
item.on_hover(cx.listener(move |this, hovered, window, cx| {
if *hovered {
this.clear_selected();
@@ -1652,7 +1682,7 @@ impl ContextMenu {
}
}))
})
- .when(self.parent_menu.is_some(), |item| {
+ .when(self.main_menu.is_some(), |item| {
item.on_click(cx.listener(move |this, _, window, cx| {
if matches!(
&this.submenu_state,
@@ -1680,7 +1710,7 @@ impl ContextMenu {
cx.notify();
}
- if let Some(parent) = &this.parent_menu {
+ if let Some(parent) = &this.main_menu {
let mouse_pos = window.mouse_position();
let parent_clone = parent.clone();
@@ -1857,7 +1887,7 @@ impl Render for ContextMenu {
let is_initializing = open_submenu.offset.is_none();
let computed_offset = if is_initializing {
- let menu_bounds = self.submenu_observed_bounds.get();
+ let menu_bounds = self.main_menu_observed_bounds.get();
let trigger_bounds = open_submenu
.trigger_bounds
.or_else(|| self.submenu_trigger_bounds.get());
@@ -1900,7 +1930,7 @@ impl Render for ContextMenu {
};
let render_menu = |cx: &mut Context<Self>, window: &mut Window| {
- let bounds_cell = self.submenu_observed_bounds.clone();
+ let bounds_cell = self.main_menu_observed_bounds.clone();
let menu_bounds_measure = canvas(
{
move |bounds, _window, _cx| {
@@ -1958,7 +1988,7 @@ impl Render for ContextMenu {
}
}
- if let Some(parent) = &this.parent_menu {
+ if let Some(parent) = &this.main_menu {
let overridden_by_parent_trigger = parent
.read(cx)
.submenu_trigger_bounds
@@ -2007,24 +2037,40 @@ impl Render for ContextMenu {
}
if is_wide_window {
+ let menu_bounds = self.main_menu_observed_bounds.get();
+ let trigger_bounds = self
+ .documentation_aside
+ .as_ref()
+ .and_then(|(ix, _)| self.aside_trigger_bounds.borrow().get(ix).copied());
+
+ let trigger_position = match (menu_bounds, trigger_bounds) {
+ (Some(menu_bounds), Some(trigger_bounds)) => {
+ let relative_top = trigger_bounds.origin.y - menu_bounds.origin.y;
+ let height = trigger_bounds.size.height;
+ Some((relative_top, height))
+ }
+ _ => None,
+ };
+
div()
.relative()
.child(render_menu(cx, window))
- .children(aside.map(|(_item_index, aside)| {
- h_flex()
- .absolute()
- .when(aside.side == DocumentationSide::Left, |this| {
- this.right_full().mr_1()
- })
- .when(aside.side == DocumentationSide::Right, |this| {
- this.left_full().ml_1()
- })
- .when(aside.edge == DocumentationEdge::Top, |this| this.top_0())
- .when(aside.edge == DocumentationEdge::Bottom, |this| {
- this.bottom_0()
- })
- .child(render_aside(aside, cx))
- }))
+ // Only render the aside once we have trigger bounds to avoid flicker.
+ .when_some(trigger_position, |this, (top, height)| {
+ this.children(aside.map(|(_, aside)| {
+ h_flex()
+ .absolute()
+ .when(aside.side == DocumentationSide::Left, |el| {
+ el.right_full().mr_1()
+ })
+ .when(aside.side == DocumentationSide::Right, |el| {
+ el.left_full().ml_1()
+ })
+ .top(top)
+ .h(height)
+ .child(render_aside(aside, cx))
+ }))
+ })
.when_some(submenu_container, |this, (ix, submenu, offset)| {
this.child(self.render_submenu_container(ix, submenu, offset, cx))
})
@@ -19,8 +19,8 @@ use project::{DisableAiSettings, project_settings::DiagnosticSeverity};
use search::{BufferSearchBar, buffer_search};
use settings::{Settings, SettingsStore};
use ui::{
- ButtonStyle, ContextMenu, ContextMenuEntry, DocumentationEdge, DocumentationSide, IconButton,
- IconName, IconSize, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*,
+ ButtonStyle, ContextMenu, ContextMenuEntry, DocumentationSide, IconButton, IconName, IconSize,
+ PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*,
};
use vim_mode_setting::{HelixModeSetting, VimModeSetting};
use workspace::item::ItemBufferKind;
@@ -416,7 +416,7 @@ impl Render for QuickActionBar {
}
});
if !edit_predictions_enabled_at_cursor {
- edit_prediction_entry = edit_prediction_entry.documentation_aside(DocumentationSide::Left, DocumentationEdge::Top, |_| {
+ edit_prediction_entry = edit_prediction_entry.documentation_aside(DocumentationSide::Left, |_| {
Label::new("You can't toggle edit predictions for this file as it is within the excluded files list.").into_any_element()
});
}
@@ -467,7 +467,7 @@ impl Render for QuickActionBar {
}
});
if !diagnostics_enabled {
- inline_diagnostics_item = inline_diagnostics_item.disabled(true).documentation_aside(DocumentationSide::Left, DocumentationEdge::Top, |_| Label::new("Inline diagnostics are not available until regular diagnostics are enabled.").into_any_element());
+ inline_diagnostics_item = inline_diagnostics_item.disabled(true).documentation_aside(DocumentationSide::Left, |_| Label::new("Inline diagnostics are not available until regular diagnostics are enabled.").into_any_element());
}
menu = menu.item(inline_diagnostics_item)
}