You've already forked gpui-component
mirror of
https://github.com/librekeys/gpui-component.git
synced 2026-04-14 08:46:29 -07:00
Draft List, Dropdown.
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
use gpui::{
|
||||
anchored, canvas, deferred, div, prelude::FluentBuilder, px, rems, AnyElement, App, AppContext,
|
||||
Bounds, ClickEvent, Context, DismissEvent, Edges, ElementId, Empty, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, InteractiveElement, IntoElement, KeyBinding, Length, ParentElement,
|
||||
Pixels, Render, RenderOnce, SharedString, StatefulInteractiveElement, StyleRefinement, Styled,
|
||||
Subscription, Task, WeakEntity, Window,
|
||||
Bounds, ClickEvent, Context, DismissEvent, ElementId, Empty, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, InteractiveElement, IntoElement, KeyBinding, Length, ParentElement, Pixels, Render,
|
||||
RenderOnce, SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Subscription,
|
||||
Task, WeakEntity, Window,
|
||||
};
|
||||
use rust_i18n::t;
|
||||
|
||||
@@ -11,9 +11,9 @@ use crate::{
|
||||
actions::{Cancel, Confirm, SelectDown, SelectUp},
|
||||
h_flex,
|
||||
input::clear_button,
|
||||
list::{List, ListDelegate},
|
||||
v_flex, ActiveTheme, Disableable, Icon, IconName, IndexPath, Selectable, Sizable, Size,
|
||||
StyleSized, StyledExt,
|
||||
list::{List, ListState},
|
||||
v_flex, ActiveTheme, Disableable, Icon, IconName, IndexPath, ListItem, Selectable, Sizable,
|
||||
Size, StyleSized, StyledExt,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -138,144 +138,6 @@ impl<T: DropdownItem> DropdownDelegate for Vec<T> {
|
||||
}
|
||||
}
|
||||
|
||||
struct DropdownListDelegate<D: DropdownDelegate + 'static> {
|
||||
delegate: D,
|
||||
dropdown: WeakEntity<DropdownState<D>>,
|
||||
selected_index: Option<IndexPath>,
|
||||
}
|
||||
|
||||
impl<D> ListDelegate for DropdownListDelegate<D>
|
||||
where
|
||||
D: DropdownDelegate + 'static,
|
||||
{
|
||||
type Item = DropdownListItem;
|
||||
|
||||
fn sections_count(&self, cx: &App) -> usize {
|
||||
self.delegate.sections_count(cx)
|
||||
}
|
||||
|
||||
fn items_count(&self, section: usize, _: &App) -> usize {
|
||||
self.delegate.items_count(section)
|
||||
}
|
||||
|
||||
fn render_section_header(
|
||||
&self,
|
||||
section: usize,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<List<Self>>,
|
||||
) -> Option<impl IntoElement> {
|
||||
let dropdown = self.dropdown.upgrade()?.read(cx);
|
||||
let Some(item) = self.delegate.section(section) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
return Some(
|
||||
div()
|
||||
.py_0p5()
|
||||
.px_2()
|
||||
.list_size(dropdown.size)
|
||||
.text_sm()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(item),
|
||||
);
|
||||
}
|
||||
|
||||
fn render_item(
|
||||
&self,
|
||||
ix: IndexPath,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<List<Self>>,
|
||||
) -> Option<Self::Item> {
|
||||
let selected = self
|
||||
.selected_index
|
||||
.map_or(false, |selected_index| selected_index == ix);
|
||||
let size = self
|
||||
.dropdown
|
||||
.upgrade()
|
||||
.map_or(Size::Medium, |dropdown| dropdown.read(cx).size);
|
||||
|
||||
if let Some(item) = self.delegate.item(ix) {
|
||||
let content = item.display_title().unwrap_or_else(|| {
|
||||
div()
|
||||
.whitespace_nowrap()
|
||||
.child(item.title().to_string())
|
||||
.into_any_element()
|
||||
});
|
||||
let list_item = DropdownListItem::new(ix.row)
|
||||
.selected(selected)
|
||||
.with_size(size)
|
||||
.child(content);
|
||||
Some(list_item)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel(&mut self, window: &mut Window, cx: &mut Context<List<Self>>) {
|
||||
let dropdown = self.dropdown.clone();
|
||||
cx.defer_in(window, move |_, window, cx| {
|
||||
_ = dropdown.update(cx, |this, cx| {
|
||||
this.open = false;
|
||||
this.focus(window, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<List<Self>>) {
|
||||
let selected_value = self
|
||||
.selected_index
|
||||
.and_then(|ix| self.delegate.item(ix))
|
||||
.map(|item| item.value().clone());
|
||||
let dropdown = self.dropdown.clone();
|
||||
|
||||
cx.defer_in(window, move |_, window, cx| {
|
||||
_ = dropdown.update(cx, |this, cx| {
|
||||
cx.emit(DropdownEvent::Confirm(selected_value.clone()));
|
||||
this.selected_value = selected_value;
|
||||
this.open = false;
|
||||
this.focus(window, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn perform_search(
|
||||
&mut self,
|
||||
query: &str,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<List<Self>>,
|
||||
) -> Task<()> {
|
||||
self.dropdown.upgrade().map_or(Task::ready(()), |dropdown| {
|
||||
dropdown.update(cx, |_, cx| self.delegate.perform_search(query, window, cx))
|
||||
})
|
||||
}
|
||||
|
||||
fn set_selected_index(
|
||||
&mut self,
|
||||
ix: Option<IndexPath>,
|
||||
_: &mut Window,
|
||||
_: &mut Context<List<Self>>,
|
||||
) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn render_empty(&self, window: &mut Window, cx: &mut Context<List<Self>>) -> impl IntoElement {
|
||||
if let Some(empty) = self
|
||||
.dropdown
|
||||
.upgrade()
|
||||
.and_then(|dropdown| dropdown.read(cx).empty.as_ref())
|
||||
{
|
||||
empty(window, cx).into_any_element()
|
||||
} else {
|
||||
h_flex()
|
||||
.justify_center()
|
||||
.py_6()
|
||||
.text_color(cx.theme().muted_foreground.opacity(0.6))
|
||||
.child(Icon::new(IconName::Inbox).size(px(28.)))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum DropdownEvent<D: DropdownDelegate + 'static> {
|
||||
Confirm(Option<<D::Item as DropdownItem>::Value>),
|
||||
}
|
||||
@@ -283,7 +145,9 @@ pub enum DropdownEvent<D: DropdownDelegate + 'static> {
|
||||
/// State of the [`Dropdown`].
|
||||
pub struct DropdownState<D: DropdownDelegate + 'static> {
|
||||
focus_handle: FocusHandle,
|
||||
list: Entity<List<DropdownListDelegate<D>>>,
|
||||
list: Entity<ListState>,
|
||||
delegate: D,
|
||||
selected_index: Option<IndexPath>,
|
||||
size: Size,
|
||||
empty: Option<Box<dyn Fn(&Window, &App) -> AnyElement>>,
|
||||
/// Store the bounds of the input
|
||||
@@ -508,19 +372,15 @@ where
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
let delegate = DropdownListDelegate {
|
||||
delegate,
|
||||
dropdown: cx.entity().downgrade(),
|
||||
selected_index,
|
||||
};
|
||||
|
||||
let searchable = delegate.delegate.searchable();
|
||||
let searchable = delegate.searchable();
|
||||
let section_items_count = (0..delegate.sections_count(cx))
|
||||
.map(|section| delegate.items_count(section))
|
||||
.collect();
|
||||
|
||||
let list = cx.new(|cx| {
|
||||
let mut list = List::new(delegate, window, cx)
|
||||
.max_h(rems(20.))
|
||||
.paddings(Edges::all(px(4.)))
|
||||
.reset_on_cancel(false);
|
||||
let mut list =
|
||||
ListState::sections(section_items_count, window, cx).reset_on_cancel(false);
|
||||
if !searchable {
|
||||
list = list.no_query();
|
||||
}
|
||||
@@ -534,6 +394,8 @@ where
|
||||
|
||||
let mut this = Self {
|
||||
focus_handle,
|
||||
delegate,
|
||||
selected_index,
|
||||
list,
|
||||
size: Size::Medium,
|
||||
selected_value: None,
|
||||
@@ -575,8 +437,7 @@ where
|
||||
) where
|
||||
<<D as DropdownDelegate>::Item as DropdownItem>::Value: PartialEq,
|
||||
{
|
||||
let delegate = self.list.read(cx).delegate();
|
||||
let selected_index = delegate.delegate.position(selected_value);
|
||||
let selected_index = self.delegate.position(selected_value);
|
||||
self.set_selected_index(selected_index, window, cx);
|
||||
}
|
||||
|
||||
@@ -587,7 +448,7 @@ where
|
||||
fn update_selected_value(&mut self, _: &Window, cx: &App) {
|
||||
self.selected_value = self
|
||||
.selected_index(cx)
|
||||
.and_then(|ix| self.list.read(cx).delegate().delegate.item(ix))
|
||||
.and_then(|ix| self.delegate.item(ix))
|
||||
.map(|item| item.value().clone());
|
||||
}
|
||||
|
||||
@@ -664,12 +525,16 @@ where
|
||||
}
|
||||
|
||||
/// Set the items for the dropdown.
|
||||
pub fn set_items(&mut self, items: D, _: &mut Window, cx: &mut Context<Self>)
|
||||
pub fn set_items(&mut self, items: D, window: &mut Window, cx: &mut Context<Self>)
|
||||
where
|
||||
D: DropdownDelegate + 'static,
|
||||
{
|
||||
self.list.update(cx, |list, _| {
|
||||
list.delegate_mut().delegate = items;
|
||||
let section_items_count = (0..items.sections_count(cx))
|
||||
.map(|section| items.items_count(section))
|
||||
.collect();
|
||||
|
||||
self.list.update(cx, |list, cx| {
|
||||
list.reset(section_items_count, window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -775,9 +640,6 @@ where
|
||||
let Some(title) = self
|
||||
.state
|
||||
.read(cx)
|
||||
.list
|
||||
.read(cx)
|
||||
.delegate()
|
||||
.delegate
|
||||
.item(*selected_index)
|
||||
.map(|item| {
|
||||
@@ -801,6 +663,69 @@ where
|
||||
})
|
||||
.child(title)
|
||||
}
|
||||
|
||||
fn render_item(&self, ix: IndexPath, _: &mut Window, cx: &mut App) -> Option<ListItem> {
|
||||
let selected = self.state.read(cx).selected_index == Some(ix);
|
||||
let Some(item) = self.state.read(cx).delegate.item(ix) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let size = self.size;
|
||||
|
||||
let content = item.display_title().unwrap_or_else(|| {
|
||||
div()
|
||||
.whitespace_nowrap()
|
||||
.child(item.title().to_string())
|
||||
.into_any_element()
|
||||
});
|
||||
let list_item = DropdownListItem::new(ix.row)
|
||||
.selected(selected)
|
||||
.with_size(size)
|
||||
.child(content);
|
||||
Some(list_item)
|
||||
}
|
||||
|
||||
fn cancel(state: &Entity<DropdownState<D>>, window: &mut Window, cx: &mut App) {
|
||||
state.update(cx, |state, cx| {
|
||||
state.open = false;
|
||||
state.focus(window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(
|
||||
state: &Entity<DropdownState<D>>,
|
||||
_secondary: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let selected_value = state
|
||||
.read(cx)
|
||||
.selected_index
|
||||
.and_then(|ix| state.read(cx).delegate.item(ix))
|
||||
.map(|item| item.value().clone());
|
||||
|
||||
state.update(cx, |state, cx| {
|
||||
cx.emit(DropdownEvent::Confirm(selected_value.clone()));
|
||||
state.selected_value = selected_value;
|
||||
state.open = false;
|
||||
state.focus(window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn perform_search(&self, query: &str, window: &mut Window, cx: &mut App) -> Task<()> {
|
||||
self.state.update(cx, |state, cx| {
|
||||
state.delegate.perform_search(query, window, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn render_empty(_: &mut Window, cx: &mut App) -> AnyElement {
|
||||
h_flex()
|
||||
.justify_center()
|
||||
.py_6()
|
||||
.text_color(cx.theme().muted_foreground.opacity(0.6))
|
||||
.child(Icon::new(IconName::Inbox).size(px(28.)))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Sizable for Dropdown<D>
|
||||
@@ -865,10 +790,13 @@ where
|
||||
}
|
||||
|
||||
let state = self.state.read(cx);
|
||||
let list_state = state.list.clone();
|
||||
let is_open = state.open;
|
||||
let size = self.size;
|
||||
let show_clean = self.cleanable && state.selected_index(cx).is_some();
|
||||
let bounds = state.bounds;
|
||||
let allow_open = !(state.open || self.disabled);
|
||||
let outline_visible = state.open || is_focused && !self.disabled;
|
||||
let allow_open = !(is_open || self.disabled);
|
||||
let outline_visible = is_open || is_focused && !self.disabled;
|
||||
let popup_radius = cx.theme().radius.min(px(8.));
|
||||
|
||||
div()
|
||||
@@ -944,7 +872,7 @@ where
|
||||
let icon = match self.icon.clone() {
|
||||
Some(icon) => icon,
|
||||
None => {
|
||||
if state.open {
|
||||
if is_open {
|
||||
Icon::new(IconName::ChevronUp)
|
||||
} else {
|
||||
Icon::new(IconName::ChevronDown)
|
||||
@@ -970,7 +898,7 @@ where
|
||||
.size_full(),
|
||||
),
|
||||
)
|
||||
.when(state.open, |this| {
|
||||
.when(is_open, |this| {
|
||||
this.child(
|
||||
deferred(
|
||||
anchored().snap_to_window_with_margin(px(8.)).child(
|
||||
@@ -989,7 +917,63 @@ where
|
||||
.border_color(cx.theme().border)
|
||||
.rounded(popup_radius)
|
||||
.shadow_md()
|
||||
.child(state.list.clone()),
|
||||
.child(
|
||||
List::new(&list_state)
|
||||
.max_h(rems(20.))
|
||||
.p(px(4.))
|
||||
// .item(|ix, window, cx| {
|
||||
// // self.render_item(ix, window, cx)
|
||||
// })
|
||||
.section_header({
|
||||
let state = self.state.clone();
|
||||
move |section, _, cx| {
|
||||
let Some(item) = state
|
||||
.read(cx)
|
||||
.delegate
|
||||
.section(section)
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(
|
||||
div()
|
||||
.py_0p5()
|
||||
.px_2()
|
||||
.list_size(size)
|
||||
.text_sm()
|
||||
.text_color(
|
||||
cx.theme().muted_foreground,
|
||||
)
|
||||
.child(item)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
})
|
||||
.empty(Self::render_empty)
|
||||
.on_select({
|
||||
let state = self.state.clone();
|
||||
move |ix, window, cx| {
|
||||
state.update(cx, |state, cx| {
|
||||
state
|
||||
.set_selected_index(ix, window, cx);
|
||||
})
|
||||
}
|
||||
})
|
||||
.on_confirm({
|
||||
let state = self.state.clone();
|
||||
move |secondary, window, cx| {
|
||||
Self::confirm(&state, secondary, window, cx)
|
||||
}
|
||||
})
|
||||
.on_cancel({
|
||||
let state = self.state.clone();
|
||||
move |window, cx| {
|
||||
Self::cancel(&state, window, cx);
|
||||
}
|
||||
}), // .on_search(|query, window, cx| {
|
||||
// self.perform_search(query, window, cx)
|
||||
// }),
|
||||
),
|
||||
)
|
||||
.on_mouse_down_out(window.listener_for(
|
||||
&self.state,
|
||||
|
||||
@@ -2,9 +2,9 @@ use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
canvas, deferred, div, prelude::FluentBuilder, px, relative, Action, AnyElement, App,
|
||||
AppContext, Bounds, Context, DismissEvent, Empty, Entity, EventEmitter,
|
||||
InteractiveElement as _, IntoElement, ParentElement, Pixels, Point, Render, RenderOnce,
|
||||
SharedString, Styled, StyledText, Subscription, Window,
|
||||
AppContext, Bounds, Context, Empty, Entity, InteractiveElement as _, IntoElement,
|
||||
ParentElement, Pixels, Point, Render, RenderOnce, SharedString, Styled, StyledText,
|
||||
Subscription, Window,
|
||||
};
|
||||
use lsp_types::CodeAction;
|
||||
|
||||
@@ -14,7 +14,7 @@ const MAX_MENU_HEIGHT: Pixels = px(480.);
|
||||
use crate::{
|
||||
actions, h_flex,
|
||||
input::{self, popovers::editor_popover, InputState},
|
||||
list::{List, ListDelegate, ListEvent},
|
||||
list::{List, ListEvent, ListState},
|
||||
ActiveTheme, IndexPath, Selectable,
|
||||
};
|
||||
|
||||
@@ -25,23 +25,6 @@ pub(crate) struct CodeActionItem {
|
||||
pub(crate) action: CodeAction,
|
||||
}
|
||||
|
||||
struct MenuDelegate {
|
||||
menu: Entity<CodeActionMenu>,
|
||||
items: Vec<Rc<CodeActionItem>>,
|
||||
selected_ix: usize,
|
||||
}
|
||||
|
||||
impl MenuDelegate {
|
||||
fn set_items(&mut self, items: Vec<CodeActionItem>) {
|
||||
self.items = items.into_iter().map(Rc::new).collect();
|
||||
self.selected_ix = 0;
|
||||
}
|
||||
|
||||
fn selected_item(&self) -> Option<&Rc<CodeActionItem>> {
|
||||
self.items.get(self.selected_ix)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct MenuItem {
|
||||
ix: usize,
|
||||
@@ -101,51 +84,14 @@ impl RenderOnce for MenuItem {
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for MenuDelegate {}
|
||||
|
||||
impl ListDelegate for MenuDelegate {
|
||||
type Item = MenuItem;
|
||||
|
||||
fn items_count(&self, _: usize, _: &gpui::App) -> usize {
|
||||
self.items.len()
|
||||
}
|
||||
|
||||
fn render_item(
|
||||
&self,
|
||||
ix: crate::IndexPath,
|
||||
_: &mut Window,
|
||||
_: &mut Context<List<Self>>,
|
||||
) -> Option<Self::Item> {
|
||||
let item = self.items.get(ix.row)?;
|
||||
Some(MenuItem::new(ix.row, item.clone()))
|
||||
}
|
||||
|
||||
fn set_selected_index(
|
||||
&mut self,
|
||||
ix: Option<crate::IndexPath>,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<List<Self>>,
|
||||
) {
|
||||
self.selected_ix = ix.map(|i| i.row).unwrap_or(0);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<List<Self>>) {
|
||||
let Some(item) = self.selected_item() else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.menu.update(cx, |this, cx| {
|
||||
this.select_item(&item, window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// A context menu for code completions and code actions.
|
||||
pub struct CodeActionMenu {
|
||||
offset: usize,
|
||||
state: Entity<InputState>,
|
||||
list: Entity<List<MenuDelegate>>,
|
||||
list: Entity<ListState>,
|
||||
items: Vec<Rc<CodeActionItem>>,
|
||||
selected_ix: usize,
|
||||
|
||||
open: bool,
|
||||
bounds: Bounds<Pixels>,
|
||||
|
||||
@@ -162,18 +108,7 @@ impl CodeActionMenu {
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
cx.new(|cx| {
|
||||
let view = cx.entity();
|
||||
let menu = MenuDelegate {
|
||||
menu: view,
|
||||
items: vec![],
|
||||
selected_ix: 0,
|
||||
};
|
||||
|
||||
let list = cx.new(|cx| {
|
||||
List::new(menu, window, cx)
|
||||
.no_query()
|
||||
.max_h(MAX_MENU_HEIGHT)
|
||||
});
|
||||
let list = cx.new(|cx| ListState::new(0, window, cx).no_query());
|
||||
|
||||
let _subscriptions =
|
||||
vec![
|
||||
@@ -192,6 +127,8 @@ impl CodeActionMenu {
|
||||
offset: 0,
|
||||
state,
|
||||
list,
|
||||
items: vec![],
|
||||
selected_ix: 0,
|
||||
open: false,
|
||||
bounds: Bounds::default(),
|
||||
_subscriptions,
|
||||
@@ -199,6 +136,23 @@ impl CodeActionMenu {
|
||||
})
|
||||
}
|
||||
|
||||
fn set_items(
|
||||
&mut self,
|
||||
items: Vec<CodeActionItem>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.items = items.into_iter().map(Rc::new).collect();
|
||||
self.list.update(cx, |this, cx| {
|
||||
this.reset(vec![self.items.len()], window, cx);
|
||||
});
|
||||
self.selected_ix = 0;
|
||||
}
|
||||
|
||||
fn selected_item(&self) -> Option<&Rc<CodeActionItem>> {
|
||||
self.items.get(self.selected_ix)
|
||||
}
|
||||
|
||||
fn select_item(&mut self, item: &CodeActionItem, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let state = self.state.clone();
|
||||
let item = item.clone();
|
||||
@@ -308,6 +262,36 @@ impl CodeActionMenu {
|
||||
+ Point::new(-px(4.), last_layout.line_height + px(4.)),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_item(
|
||||
&self,
|
||||
ix: crate::IndexPath,
|
||||
_: &mut Window,
|
||||
_: &mut Context<List<Self>>,
|
||||
) -> Option<Self::Item> {
|
||||
let item = self.items.get(ix.row)?;
|
||||
Some(MenuItem::new(ix.row, item.clone()))
|
||||
}
|
||||
|
||||
fn set_selected_index(
|
||||
&mut self,
|
||||
ix: Option<crate::IndexPath>,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<List<Self>>,
|
||||
) {
|
||||
self.selected_ix = ix.map(|i| i.row).unwrap_or(0);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<List<Self>>) {
|
||||
let Some(item) = self.selected_item() else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.menu.update(cx, |this, cx| {
|
||||
this.select_item(&item, window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for CodeActionMenu {
|
||||
@@ -336,7 +320,18 @@ impl Render for CodeActionMenu {
|
||||
.top(pos.y)
|
||||
.max_w(max_width)
|
||||
.min_w(px(120.))
|
||||
.child(self.list.clone())
|
||||
.child(
|
||||
List::new(&self.list)
|
||||
.max_h(MAX_MENU_HEIGHT)
|
||||
.on_select(|ix, window, cx| {
|
||||
self.selected_ix = ix;
|
||||
cx.notify(view.entity_id());
|
||||
})
|
||||
.on_confirm(|secondary, window, cx| {
|
||||
self.confirm(secondary, window, cx);
|
||||
})
|
||||
.item(|ix, window, cx| self.render_item(ix, window, cx)),
|
||||
)
|
||||
.child(
|
||||
canvas(
|
||||
move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds),
|
||||
|
||||
@@ -20,28 +20,10 @@ use crate::{
|
||||
InputState, RopeExt,
|
||||
},
|
||||
label::Label,
|
||||
list::{List, ListDelegate, ListEvent},
|
||||
ActiveTheme, IndexPath, Selectable,
|
||||
list::{List, ListEvent, ListState},
|
||||
ActiveTheme, IndexPath, ListItem, Selectable,
|
||||
};
|
||||
|
||||
struct ContextMenuDelegate {
|
||||
query: SharedString,
|
||||
menu: Entity<CompletionMenu>,
|
||||
items: Vec<Rc<CompletionItem>>,
|
||||
selected_ix: usize,
|
||||
}
|
||||
|
||||
impl ContextMenuDelegate {
|
||||
fn set_items(&mut self, items: Vec<CompletionItem>) {
|
||||
self.items = items.into_iter().map(Rc::new).collect();
|
||||
self.selected_ix = 0;
|
||||
}
|
||||
|
||||
fn selected_item(&self) -> Option<&Rc<CompletionItem>> {
|
||||
self.items.get(self.selected_ix)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct CompletionMenuItem {
|
||||
ix: usize,
|
||||
@@ -128,57 +110,20 @@ impl RenderOnce for CompletionMenuItem {
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for ContextMenuDelegate {}
|
||||
|
||||
impl ListDelegate for ContextMenuDelegate {
|
||||
type Item = CompletionMenuItem;
|
||||
|
||||
fn items_count(&self, _: usize, _: &gpui::App) -> usize {
|
||||
self.items.len()
|
||||
}
|
||||
|
||||
fn render_item(
|
||||
&self,
|
||||
ix: crate::IndexPath,
|
||||
_: &mut Window,
|
||||
_: &mut Context<List<Self>>,
|
||||
) -> Option<Self::Item> {
|
||||
let item = self.items.get(ix.row)?;
|
||||
Some(CompletionMenuItem::new(ix.row, item.clone()).highlight_prefix(self.query.clone()))
|
||||
}
|
||||
|
||||
fn set_selected_index(
|
||||
&mut self,
|
||||
ix: Option<crate::IndexPath>,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<List<Self>>,
|
||||
) {
|
||||
self.selected_ix = ix.map(|i| i.row).unwrap_or(0);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<List<Self>>) {
|
||||
let Some(item) = self.selected_item() else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.menu.update(cx, |this, cx| {
|
||||
this.select_item(&item, window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// A context menu for code completions and code actions.
|
||||
pub struct CompletionMenu {
|
||||
offset: usize,
|
||||
editor: Entity<InputState>,
|
||||
list: Entity<List<ContextMenuDelegate>>,
|
||||
list: Entity<ListState>,
|
||||
items: Vec<Rc<CompletionItem>>,
|
||||
|
||||
selected_ix: usize,
|
||||
query: SharedString,
|
||||
open: bool,
|
||||
bounds: Bounds<Pixels>,
|
||||
|
||||
/// The offset of the first character that triggered the completion.
|
||||
pub(crate) trigger_start_offset: Option<usize>,
|
||||
query: SharedString,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -192,19 +137,7 @@ impl CompletionMenu {
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
cx.new(|cx| {
|
||||
let view = cx.entity();
|
||||
let menu = ContextMenuDelegate {
|
||||
query: SharedString::default(),
|
||||
menu: view,
|
||||
items: vec![],
|
||||
selected_ix: 0,
|
||||
};
|
||||
|
||||
let list = cx.new(|cx| {
|
||||
List::new(menu, window, cx)
|
||||
.no_query()
|
||||
.max_h(MAX_MENU_HEIGHT)
|
||||
});
|
||||
let list = cx.new(|cx| ListState::new(0, window, cx).no_query());
|
||||
|
||||
let _subscriptions =
|
||||
vec![
|
||||
@@ -223,15 +156,26 @@ impl CompletionMenu {
|
||||
offset: 0,
|
||||
editor,
|
||||
list,
|
||||
items: vec![],
|
||||
selected_ix: 0,
|
||||
query: SharedString::default(),
|
||||
open: false,
|
||||
trigger_start_offset: None,
|
||||
query: SharedString::default(),
|
||||
bounds: Bounds::default(),
|
||||
_subscriptions,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn set_items(&mut self, items: Vec<CompletionItem>) {
|
||||
self.items = items.into_iter().map(Rc::new).collect();
|
||||
self.selected_ix = 0;
|
||||
}
|
||||
|
||||
fn selected_item(&self) -> Option<&Rc<CompletionItem>> {
|
||||
self.items.get(self.selected_ix)
|
||||
}
|
||||
|
||||
fn select_item(&mut self, item: &CompletionItem, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let offset = self.offset;
|
||||
let item = item.clone();
|
||||
@@ -305,7 +249,7 @@ impl CompletionMenu {
|
||||
}
|
||||
|
||||
fn on_action_enter(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(item) = self.list.read(cx).delegate().selected_item().cloned() else {
|
||||
let Some(item) = self.selected_item().cloned() else {
|
||||
return;
|
||||
};
|
||||
self.select_item(&item, window, cx);
|
||||
@@ -356,6 +300,9 @@ impl CompletionMenu {
|
||||
let items = items.into();
|
||||
self.offset = offset;
|
||||
self.open = true;
|
||||
self.query = self.query.clone();
|
||||
self.set_items(items.clone());
|
||||
|
||||
self.list.update(cx, |this, cx| {
|
||||
let longest_ix = items
|
||||
.iter()
|
||||
@@ -366,8 +313,6 @@ impl CompletionMenu {
|
||||
.map(|(ix, _)| ix)
|
||||
.unwrap_or(0);
|
||||
|
||||
this.delegate_mut().query = self.query.clone();
|
||||
this.delegate_mut().set_items(items);
|
||||
this.set_selected_index(Some(IndexPath::new(0)), window, cx);
|
||||
this.set_item_to_measure_index(IndexPath::new(longest_ix), window, cx);
|
||||
});
|
||||
@@ -391,6 +336,24 @@ impl CompletionMenu {
|
||||
+ Point::new(-px(4.), last_layout.line_height + px(4.)),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_item(
|
||||
&self,
|
||||
ix: crate::IndexPath,
|
||||
_: &mut Window,
|
||||
_: &mut App,
|
||||
) -> Option<CompletionMenuItem> {
|
||||
let item = self.items.get(ix.row)?;
|
||||
Some(CompletionMenuItem::new(ix.row, item.clone()).highlight_prefix(self.query.clone()))
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(item) = self.selected_item().cloned() else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.select_item(&item, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for CompletionMenu {
|
||||
@@ -399,7 +362,7 @@ impl Render for CompletionMenu {
|
||||
return Empty.into_any_element();
|
||||
}
|
||||
|
||||
if self.list.read(cx).delegate().items.is_empty() {
|
||||
if self.items.is_empty() {
|
||||
self.open = false;
|
||||
return Empty.into_any_element();
|
||||
}
|
||||
@@ -411,9 +374,6 @@ impl Render for CompletionMenu {
|
||||
};
|
||||
|
||||
let selected_documentation = self
|
||||
.list
|
||||
.read(cx)
|
||||
.delegate()
|
||||
.selected_item()
|
||||
.and_then(|item| item.documentation.clone());
|
||||
|
||||
@@ -437,7 +397,20 @@ impl Render for CompletionMenu {
|
||||
editor_popover("completion-menu", cx)
|
||||
.max_w(max_width)
|
||||
.min_w(px(120.))
|
||||
.child(self.list.clone())
|
||||
.child(
|
||||
List::new(&self.list)
|
||||
.max_h(MAX_MENU_HEIGHT)
|
||||
.on_select(cx.processor(
|
||||
move |this, ix: Option<IndexPath>, _, cx| {
|
||||
this.selected_ix = ix.map(|i| i.row).unwrap_or(0);
|
||||
cx.notify();
|
||||
},
|
||||
))
|
||||
.on_confirm(cx.processor(|this, secondary, window, cx| {
|
||||
this.confirm(secondary, window, cx);
|
||||
}))
|
||||
.item(|ix, window, cx| self.render_item(ix, window, cx)),
|
||||
)
|
||||
.child(
|
||||
canvas(
|
||||
move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,9 @@
|
||||
pub(crate) mod cache;
|
||||
mod delegate;
|
||||
mod list;
|
||||
mod list_item;
|
||||
mod loading;
|
||||
mod separator_item;
|
||||
|
||||
pub use delegate::*;
|
||||
pub use list::*;
|
||||
pub use list_item::*;
|
||||
pub use separator_item::*;
|
||||
|
||||
Reference in New Issue
Block a user