list: Update ListItem and DropdownListItem style. (#1095)
## Break Change - The `Selectable` trait has been removed `element_id` method, this is necessary. - The `item` of `List` now must impl `Selectable` trait. ## List > NOTE: The default ListItem is no rounded and paddings. <img width="1144" height="762" alt="image" src="https://github.com/user-attachments/assets/32348682-f89a-487a-af9f-5ed379a7f2d8" /> ## Dropdown <img width="537" height="387" alt="image" src="https://github.com/user-attachments/assets/3cc99d9f-3714-4d10-b467-6c91917bcbf0" />
This commit is contained in:
parent
6d0443998c
commit
93b578294c
15 changed files with 304 additions and 174 deletions
|
|
@ -71,15 +71,11 @@ impl ListDelegate for ListItemDeletegate {
|
|||
_: &mut Context<List<Self>>,
|
||||
) -> Option<Self::Item> {
|
||||
let confirmed = Some(ix) == self.confirmed_index;
|
||||
let selected = Some(ix) == self.selected_index;
|
||||
|
||||
if let Some(item) = self.matches.get(ix) {
|
||||
let list_item = ListItem::new(("item", ix))
|
||||
.check_icon(IconName::Check)
|
||||
.confirmed(confirmed)
|
||||
.selected(selected)
|
||||
.py_1()
|
||||
.px_3()
|
||||
.child(
|
||||
h_flex()
|
||||
.items_center()
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ use std::time::Duration;
|
|||
|
||||
use fake::Fake;
|
||||
use gpui::{
|
||||
actions, div, px, App, AppContext, Context, ElementId, Entity, FocusHandle, Focusable,
|
||||
InteractiveElement, IntoElement, ParentElement, Render, RenderOnce, SharedString, Styled,
|
||||
Subscription, Task, Timer, Window,
|
||||
actions, div, prelude::FluentBuilder as _, px, App, AppContext, Context, Edges, ElementId,
|
||||
Entity, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render,
|
||||
RenderOnce, SharedString, Styled, Subscription, Task, Timer, Window,
|
||||
};
|
||||
|
||||
use gpui_component::{
|
||||
|
|
@ -13,7 +13,7 @@ use gpui_component::{
|
|||
h_flex,
|
||||
label::Label,
|
||||
list::{List, ListDelegate, ListEvent, ListItem},
|
||||
v_flex, ActiveTheme, Sizable,
|
||||
v_flex, ActiveTheme, Selectable, Sizable,
|
||||
};
|
||||
|
||||
actions!(list_story, [SelectedCompany]);
|
||||
|
|
@ -65,6 +65,17 @@ impl CompanyListItem {
|
|||
}
|
||||
}
|
||||
|
||||
impl Selectable for CompanyListItem {
|
||||
fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
fn is_selected(&self) -> bool {
|
||||
self.selected
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for CompanyListItem {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let text_color = if self.selected {
|
||||
|
|
@ -88,10 +99,16 @@ impl RenderOnce for CompanyListItem {
|
|||
};
|
||||
|
||||
self.base
|
||||
.px_3()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.overflow_x_hidden()
|
||||
.bg(bg_color)
|
||||
.border_1()
|
||||
.border_color(bg_color)
|
||||
.when(self.selected, |this| {
|
||||
this.border_color(cx.theme().list_active_border)
|
||||
})
|
||||
.rounded(cx.theme().radius)
|
||||
.child(
|
||||
h_flex()
|
||||
.items_center()
|
||||
|
|
@ -283,10 +300,9 @@ impl ListStory {
|
|||
eof: false,
|
||||
};
|
||||
|
||||
let company_list = cx.new(|cx| List::new(delegate, window, cx));
|
||||
// company_list.update(cx, |list, cx| {
|
||||
// list.set_selected_index(Some(3), cx);
|
||||
// });
|
||||
let company_list =
|
||||
cx.new(|cx| List::new(delegate, window, cx).paddings(Edges::all(px(8.))));
|
||||
|
||||
let _subscriptions =
|
||||
vec![
|
||||
cx.subscribe(&company_list, |_, _, ev: &ListEvent, _| match ev {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use crate::{
|
|||
use gpui::{
|
||||
div, prelude::FluentBuilder as _, relative, Action, AnyElement, App, ClickEvent, Corners, Div,
|
||||
Edges, ElementId, Hsla, InteractiveElement, Interactivity, IntoElement, MouseButton,
|
||||
ParentElement, Pixels, RenderOnce, SharedString, StatefulInteractiveElement as _,
|
||||
ParentElement, Pixels, RenderOnce, SharedString, Stateful, StatefulInteractiveElement as _,
|
||||
StyleRefinement, Styled, Window,
|
||||
};
|
||||
|
||||
|
|
@ -181,9 +181,8 @@ impl ButtonVariant {
|
|||
/// A Button element.
|
||||
#[derive(IntoElement)]
|
||||
pub struct Button {
|
||||
base: Div,
|
||||
base: Stateful<Div>,
|
||||
style: StyleRefinement,
|
||||
id: ElementId,
|
||||
icon: Option<Icon>,
|
||||
label: Option<SharedString>,
|
||||
children: Vec<AnyElement>,
|
||||
|
|
@ -215,9 +214,8 @@ impl From<Button> for AnyElement {
|
|||
impl Button {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
base: div().flex_shrink_0(),
|
||||
base: div().id(id.into()).flex_shrink_0(),
|
||||
style: StyleRefinement::default(),
|
||||
id: id.into(),
|
||||
icon: None,
|
||||
label: None,
|
||||
disabled: false,
|
||||
|
|
@ -335,10 +333,6 @@ impl Disableable for Button {
|
|||
}
|
||||
|
||||
impl Selectable for Button {
|
||||
fn element_id(&self) -> &ElementId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
|
|
@ -391,7 +385,6 @@ impl RenderOnce for Button {
|
|||
};
|
||||
|
||||
self.base
|
||||
.id(self.id)
|
||||
.flex_shrink_0()
|
||||
.cursor_default()
|
||||
.flex()
|
||||
|
|
|
|||
|
|
@ -93,10 +93,6 @@ impl ButtonVariants for DropdownButton {
|
|||
}
|
||||
|
||||
impl Selectable for DropdownButton {
|
||||
fn element_id(&self) -> &ElementId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
|
|
|
|||
|
|
@ -76,10 +76,6 @@ impl Disableable for Checkbox {
|
|||
}
|
||||
|
||||
impl Selectable for Checkbox {
|
||||
fn element_id(&self) -> &ElementId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn selected(self, selected: bool) -> Self {
|
||||
self.checked(selected)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use gpui::{
|
||||
anchored, canvas, deferred, div, prelude::FluentBuilder, px, rems, AnyElement, App, AppContext,
|
||||
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,
|
||||
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,
|
||||
};
|
||||
use rust_i18n::t;
|
||||
|
||||
|
|
@ -11,8 +11,9 @@ use crate::{
|
|||
actions::{Cancel, Confirm, SelectNext, SelectPrev},
|
||||
h_flex,
|
||||
input::clear_button,
|
||||
list::{List, ListDelegate, ListItem},
|
||||
v_flex, ActiveTheme, Disableable as _, Icon, IconName, Sizable, Size, StyleSized, StyledExt,
|
||||
list::{List, ListDelegate},
|
||||
v_flex, ActiveTheme, Disableable, Icon, IconName, Selectable, Sizable, Size, StyleSized,
|
||||
StyledExt,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
@ -135,7 +136,7 @@ impl<D> ListDelegate for DropdownListDelegate<D>
|
|||
where
|
||||
D: DropdownDelegate + 'static,
|
||||
{
|
||||
type Item = ListItem;
|
||||
type Item = DropdownListItem;
|
||||
|
||||
fn items_count(&self, _: &App) -> usize {
|
||||
self.delegate.len()
|
||||
|
|
@ -156,11 +157,9 @@ where
|
|||
.map_or(Size::Medium, |dropdown| dropdown.read(cx).size);
|
||||
|
||||
if let Some(item) = self.delegate.get(ix) {
|
||||
let list_item = ListItem::new(("list-item", ix))
|
||||
.check_icon(IconName::Check)
|
||||
let list_item = DropdownListItem::new(ix)
|
||||
.selected(selected)
|
||||
.input_text_size(size)
|
||||
.list_size(size)
|
||||
.with_size(size)
|
||||
.child(div().whitespace_nowrap().child(item.title().to_string()));
|
||||
Some(list_item)
|
||||
} else {
|
||||
|
|
@ -354,6 +353,7 @@ where
|
|||
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);
|
||||
if !searchable {
|
||||
list = list.no_query();
|
||||
|
|
@ -835,3 +835,100 @@ where
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct DropdownListItem {
|
||||
id: ElementId,
|
||||
size: Size,
|
||||
style: StyleRefinement,
|
||||
selected: bool,
|
||||
disabled: bool,
|
||||
children: Vec<AnyElement>,
|
||||
}
|
||||
|
||||
impl DropdownListItem {
|
||||
pub fn new(ix: usize) -> Self {
|
||||
Self {
|
||||
id: ("dropdown-item", ix).into(),
|
||||
size: Size::default(),
|
||||
style: StyleRefinement::default(),
|
||||
selected: false,
|
||||
disabled: false,
|
||||
children: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for DropdownListItem {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements);
|
||||
}
|
||||
}
|
||||
|
||||
impl Disableable for DropdownListItem {
|
||||
fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Selectable for DropdownListItem {
|
||||
fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
fn is_selected(&self) -> bool {
|
||||
self.selected
|
||||
}
|
||||
}
|
||||
|
||||
impl Sizable for DropdownListItem {
|
||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
||||
self.size = size.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for DropdownListItem {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for DropdownListItem {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
h_flex()
|
||||
.id(self.id)
|
||||
.relative()
|
||||
.gap_x_1()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.rounded(cx.theme().radius)
|
||||
.text_base()
|
||||
.text_color(cx.theme().foreground)
|
||||
.relative()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.input_text_size(self.size)
|
||||
.list_size(self.size)
|
||||
.refine_style(&self.style)
|
||||
.when(!self.disabled, |this| {
|
||||
this.when(!self.selected, |this| {
|
||||
this.hover(|this| this.bg(cx.theme().accent.alpha(0.7)))
|
||||
})
|
||||
})
|
||||
.when(self.selected, |this| this.bg(cx.theme().accent))
|
||||
.when(self.disabled, |this| {
|
||||
this.text_color(cx.theme().muted_foreground)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.gap_x_1()
|
||||
.child(div().w_full().children(self.children)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::time::Duration;
|
|||
|
||||
use crate::actions::{Cancel, Confirm, SelectNext, SelectPrev};
|
||||
use crate::input::InputState;
|
||||
use crate::{h_flex, Icon, Sizable as _};
|
||||
use crate::{h_flex, Icon, Selectable, Sizable as _, StyledExt};
|
||||
use crate::{
|
||||
input::{InputEvent, TextInput},
|
||||
scroll::{Scrollbar, ScrollbarState},
|
||||
|
|
@ -14,7 +14,9 @@ use gpui::{
|
|||
Focusable, InteractiveElement, IntoElement, KeyBinding, Length, ListSizingBehavior,
|
||||
MouseButton, ParentElement, Render, Styled, Task, UniformListScrollHandle, Window,
|
||||
};
|
||||
use gpui::{px, App, Context, EventEmitter, MouseDownEvent, ScrollStrategy, Subscription};
|
||||
use gpui::{
|
||||
App, Context, Edges, EventEmitter, MouseDownEvent, Pixels, ScrollStrategy, Subscription,
|
||||
};
|
||||
use rust_i18n::t;
|
||||
use smol::Timer;
|
||||
|
||||
|
|
@ -44,7 +46,7 @@ pub enum ListEvent {
|
|||
/// A delegate for the List.
|
||||
#[allow(unused)]
|
||||
pub trait ListDelegate: Sized + 'static {
|
||||
type Item: IntoElement;
|
||||
type Item: Selectable + IntoElement;
|
||||
|
||||
/// When Query Input change, this method will be called.
|
||||
/// You can perform search here.
|
||||
|
|
@ -82,7 +84,8 @@ pub trait ListDelegate: Sized + 'static {
|
|||
|
||||
/// Returns Some(AnyElement) to render the initial state of the list.
|
||||
///
|
||||
/// This can be used to show a view for the list before the user has interacted with it.
|
||||
/// This can be used to show a view for the list before the user has
|
||||
/// interacted with it.
|
||||
///
|
||||
/// For example: The last search results, or the last selected item.
|
||||
///
|
||||
|
|
@ -100,7 +103,8 @@ pub trait ListDelegate: Sized + 'static {
|
|||
false
|
||||
}
|
||||
|
||||
/// Returns a Element to show when loading, default is built-in Skeleton loading view.
|
||||
/// Returns a Element to show when loading, default is built-in Skeleton
|
||||
/// loading view.
|
||||
fn render_loading(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
|
|
@ -117,7 +121,8 @@ pub trait ListDelegate: Sized + 'static {
|
|||
cx: &mut Context<List<Self>>,
|
||||
);
|
||||
|
||||
/// Set the confirm and give the selected index, this is means user have clicked the item or pressed Enter.
|
||||
/// Set the confirm and give the selected index,
|
||||
/// this is means user have clicked the item or pressed Enter.
|
||||
///
|
||||
/// This will always to `set_selected_index` before confirm.
|
||||
fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<List<Self>>) {}
|
||||
|
|
@ -132,8 +137,10 @@ pub trait ListDelegate: Sized + 'static {
|
|||
true
|
||||
}
|
||||
|
||||
/// Returns a threshold value (n rows), of course, when scrolling to the bottom,
|
||||
/// the remaining number of rows triggers `load_more`.
|
||||
/// Returns a threshold value (n rows), of course,
|
||||
/// when scrolling to the bottom, the remaining number of rows
|
||||
/// triggers `load_more`.
|
||||
///
|
||||
/// This should smaller than the total number of first load rows.
|
||||
///
|
||||
/// Default: 20 rows
|
||||
|
|
@ -146,7 +153,8 @@ pub trait ListDelegate: Sized + 'static {
|
|||
/// This will performed in a background task.
|
||||
///
|
||||
/// This is always called when the table is near the bottom,
|
||||
/// so you must check if there is more data to load or lock the loading state.
|
||||
/// so you must check if there is more data to load or lock
|
||||
/// the loading state.
|
||||
fn load_more(&mut self, window: &mut Window, cx: &mut Context<List<Self>>) {}
|
||||
}
|
||||
|
||||
|
|
@ -154,6 +162,7 @@ pub struct List<D: ListDelegate> {
|
|||
focus_handle: FocusHandle,
|
||||
delegate: D,
|
||||
max_height: Option<Length>,
|
||||
paddings: Edges<Pixels>,
|
||||
query_input: Option<Entity<InputState>>,
|
||||
last_query: Option<String>,
|
||||
selectable: bool,
|
||||
|
|
@ -163,7 +172,7 @@ pub struct List<D: ListDelegate> {
|
|||
scroll_state: ScrollbarState,
|
||||
pub(crate) size: Size,
|
||||
selected_index: Option<usize>,
|
||||
right_clicked_index: Option<usize>,
|
||||
mouse_right_clicked_index: Option<usize>,
|
||||
reset_on_cancel: bool,
|
||||
_search_task: Task<()>,
|
||||
_load_more_task: Task<()>,
|
||||
|
|
@ -175,11 +184,8 @@ where
|
|||
D: ListDelegate,
|
||||
{
|
||||
pub fn new(delegate: D, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let query_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
// .prefix(|_, cx| Icon::new(IconName::Search).text_color(cx.theme().muted_foreground))
|
||||
.placeholder(t!("List.search_placeholder"))
|
||||
});
|
||||
let query_input =
|
||||
cx.new(|cx| InputState::new(window, cx).placeholder(t!("List.search_placeholder")));
|
||||
|
||||
let _query_input_subscription =
|
||||
cx.subscribe_in(&query_input, window, Self::on_query_input_event);
|
||||
|
|
@ -190,7 +196,7 @@ where
|
|||
query_input: Some(query_input),
|
||||
last_query: None,
|
||||
selected_index: None,
|
||||
right_clicked_index: None,
|
||||
mouse_right_clicked_index: None,
|
||||
vertical_scroll_handle: UniformListScrollHandle::new(),
|
||||
scroll_state: ScrollbarState::default(),
|
||||
max_height: None,
|
||||
|
|
@ -199,6 +205,7 @@ where
|
|||
querying: false,
|
||||
size: Size::default(),
|
||||
reset_on_cancel: true,
|
||||
paddings: Edges::default(),
|
||||
_search_task: Task::ready(()),
|
||||
_load_more_task: Task::ready(()),
|
||||
_query_input_subscription,
|
||||
|
|
@ -260,7 +267,8 @@ where
|
|||
self.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
/// Set the selected index of the list, this will also scroll to the selected item.
|
||||
/// Set the selected index of the list,
|
||||
/// this will also scroll to the selected item.
|
||||
fn _set_selected_index(
|
||||
&mut self,
|
||||
ix: Option<usize>,
|
||||
|
|
@ -272,7 +280,8 @@ where
|
|||
self.scroll_to_selected_item(window, cx);
|
||||
}
|
||||
|
||||
/// Set the selected index of the list, this method will not scroll to the selected item.
|
||||
/// Set the selected index of the list,
|
||||
/// this method will not scroll to the selected item.
|
||||
pub fn set_selected_index(
|
||||
&mut self,
|
||||
ix: Option<usize>,
|
||||
|
|
@ -317,6 +326,12 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// Set paddings for the list.
|
||||
pub fn paddings(mut self, paddings: Edges<Pixels>) -> Self {
|
||||
self.paddings = paddings;
|
||||
self
|
||||
}
|
||||
|
||||
fn on_query_input_event(
|
||||
&mut self,
|
||||
_: &Entity<InputState>,
|
||||
|
|
@ -375,7 +390,8 @@ where
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
/// Dispatch delegate's `load_more` method when the visible range is near the end.
|
||||
/// Dispatch delegate's `load_more` method when the
|
||||
/// visible range is near the end.
|
||||
fn load_more_if_need(
|
||||
&mut self,
|
||||
items_count: usize,
|
||||
|
|
@ -384,7 +400,8 @@ where
|
|||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let threshold = self.delegate.load_more_threshold();
|
||||
// Securely handle subtract logic to prevent attempt to subtract with overflow
|
||||
// Securely handle subtract logic to prevent attempt
|
||||
// to subtract with overflow
|
||||
if visible_end >= items_count.saturating_sub(threshold) {
|
||||
if !self.delegate.can_load_more(cx) {
|
||||
return;
|
||||
|
|
@ -500,31 +517,21 @@ where
|
|||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let selected = self.selected_index == Some(ix);
|
||||
let right_clicked = self.right_clicked_index == Some(ix);
|
||||
let mouse_right_clicked = self.mouse_right_clicked_index == Some(ix);
|
||||
|
||||
div()
|
||||
.id("list-item")
|
||||
.w_full()
|
||||
.relative()
|
||||
.children(self.delegate.render_item(ix, window, cx))
|
||||
.children(self.delegate.render_item(ix, window, cx).map(|item| {
|
||||
item.selected(selected)
|
||||
.secondary_selected(mouse_right_clicked)
|
||||
}))
|
||||
.when(self.selectable, |this| {
|
||||
this.when(selected || right_clicked, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top(px(0.))
|
||||
.left(px(0.))
|
||||
.right(px(0.))
|
||||
.bottom(px(0.))
|
||||
.when(selected, |this| this.bg(cx.theme().list_active))
|
||||
.border_1()
|
||||
.border_color(cx.theme().list_active_border),
|
||||
)
|
||||
})
|
||||
.on_mouse_down(
|
||||
this.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
cx.listener(move |this, ev: &MouseDownEvent, window, cx| {
|
||||
this.right_clicked_index = None;
|
||||
this.mouse_right_clicked_index = None;
|
||||
this.selected_index = Some(ix);
|
||||
this.on_action_confirm(
|
||||
&Confirm {
|
||||
|
|
@ -538,12 +545,55 @@ where
|
|||
.on_mouse_down(
|
||||
MouseButton::Right,
|
||||
cx.listener(move |this, _, _, cx| {
|
||||
this.right_clicked_index = Some(ix);
|
||||
this.mouse_right_clicked_index = Some(ix);
|
||||
cx.notify();
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn render_items(
|
||||
&mut self,
|
||||
items_count: usize,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let sizing_behavior = if self.max_height.is_some() {
|
||||
ListSizingBehavior::Infer
|
||||
} else {
|
||||
ListSizingBehavior::Auto
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.flex_grow()
|
||||
.relative()
|
||||
.when_some(self.max_height, |this, h| this.max_h(h))
|
||||
.overflow_hidden()
|
||||
.when(items_count == 0, |this| {
|
||||
this.child(self.delegate().render_empty(window, cx))
|
||||
})
|
||||
.when(items_count > 0, |this| {
|
||||
this.child(
|
||||
uniform_list(
|
||||
"uniform-list",
|
||||
items_count,
|
||||
cx.processor(move |list, visible_range: Range<usize>, window, cx| {
|
||||
list.load_more_if_need(items_count, visible_range.end, window, cx);
|
||||
|
||||
visible_range
|
||||
.map(|ix| list.render_list_item(ix, window, cx))
|
||||
.collect::<Vec<_>>()
|
||||
}),
|
||||
)
|
||||
.flex_grow()
|
||||
.paddings(self.paddings)
|
||||
.with_sizing_behavior(sizing_behavior)
|
||||
.track_scroll(self.vertical_scroll_handle.clone())
|
||||
.into_any_element(),
|
||||
)
|
||||
})
|
||||
.children(self.render_scrollbar(window, cx))
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Focusable for List<D>
|
||||
|
|
@ -564,14 +614,8 @@ where
|
|||
D: ListDelegate,
|
||||
{
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let vertical_scroll_handle = self.vertical_scroll_handle.clone();
|
||||
let items_count = self.delegate.items_count(cx);
|
||||
let loading = self.delegate.loading(cx);
|
||||
let sizing_behavior = if self.max_height.is_some() {
|
||||
ListSizingBehavior::Infer
|
||||
} else {
|
||||
ListSizingBehavior::Auto
|
||||
};
|
||||
|
||||
let initial_view = if let Some(input) = &self.query_input {
|
||||
if input.read(cx).value().is_empty() {
|
||||
|
|
@ -624,53 +668,13 @@ where
|
|||
if let Some(view) = initial_view {
|
||||
this.child(view)
|
||||
} else {
|
||||
this.child(
|
||||
v_flex()
|
||||
.flex_grow()
|
||||
.relative()
|
||||
.when_some(self.max_height, |this, h| this.max_h(h))
|
||||
.overflow_hidden()
|
||||
.when(items_count == 0, |this| {
|
||||
this.child(self.delegate().render_empty(window, cx))
|
||||
})
|
||||
.when(items_count > 0, |this| {
|
||||
this.child(
|
||||
uniform_list(
|
||||
"uniform-list",
|
||||
items_count,
|
||||
cx.processor(
|
||||
move |list, visible_range: Range<usize>, window, cx| {
|
||||
list.load_more_if_need(
|
||||
items_count,
|
||||
visible_range.end,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
visible_range
|
||||
.map(|ix| {
|
||||
list.render_list_item(
|
||||
ix, window, cx,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
},
|
||||
),
|
||||
)
|
||||
.flex_grow()
|
||||
.with_sizing_behavior(sizing_behavior)
|
||||
.track_scroll(vertical_scroll_handle)
|
||||
.into_any_element(),
|
||||
)
|
||||
})
|
||||
.children(self.render_scrollbar(window, cx)),
|
||||
)
|
||||
this.child(self.render_items(items_count, window, cx))
|
||||
}
|
||||
})
|
||||
// Click out to cancel right clicked row
|
||||
.when(self.right_clicked_index.is_some(), |this| {
|
||||
.when(self.mouse_right_clicked_index.is_some(), |this| {
|
||||
this.on_mouse_down_out(cx.listener(|this, _, _, cx| {
|
||||
this.right_clicked_index = None;
|
||||
this.mouse_right_clicked_index = None;
|
||||
cx.notify();
|
||||
}))
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
use crate::{h_flex, ActiveTheme, Disableable, Icon, Selectable, Sizable as _};
|
||||
use crate::{h_flex, ActiveTheme, Disableable, Icon, Selectable, Sizable as _, StyledExt};
|
||||
use gpui::{
|
||||
div, prelude::FluentBuilder as _, AnyElement, App, ClickEvent, Div, ElementId,
|
||||
InteractiveElement, IntoElement, MouseButton, MouseMoveEvent, ParentElement, RenderOnce,
|
||||
Stateful, StatefulInteractiveElement as _, Styled, Window,
|
||||
Stateful, StatefulInteractiveElement as _, StyleRefinement, Styled, Window,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ListItem {
|
||||
id: ElementId,
|
||||
base: Stateful<Div>,
|
||||
style: StyleRefinement,
|
||||
disabled: bool,
|
||||
selected: bool,
|
||||
secondary_selected: bool,
|
||||
confirmed: bool,
|
||||
check_icon: Option<Icon>,
|
||||
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
|
||||
|
|
@ -24,10 +25,11 @@ impl ListItem {
|
|||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
let id: ElementId = id.into();
|
||||
Self {
|
||||
id: id.clone(),
|
||||
base: h_flex().id(id).gap_x_1().py_1().px_2().text_base(),
|
||||
base: h_flex().id(id),
|
||||
style: StyleRefinement::default(),
|
||||
disabled: false,
|
||||
selected: false,
|
||||
secondary_selected: false,
|
||||
confirmed: false,
|
||||
on_click: None,
|
||||
on_mouse_enter: None,
|
||||
|
|
@ -97,10 +99,6 @@ impl Disableable for ListItem {
|
|||
}
|
||||
|
||||
impl Selectable for ListItem {
|
||||
fn element_id(&self) -> &ElementId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
|
|
@ -109,11 +107,16 @@ impl Selectable for ListItem {
|
|||
fn is_selected(&self) -> bool {
|
||||
self.selected
|
||||
}
|
||||
|
||||
fn secondary_selected(mut self, selected: bool) -> Self {
|
||||
self.secondary_selected = selected;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for ListItem {
|
||||
fn style(&mut self) -> &mut gpui::StyleRefinement {
|
||||
self.base.style()
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -127,11 +130,22 @@ impl RenderOnce for ListItem {
|
|||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let is_active = self.confirmed || self.selected;
|
||||
|
||||
let corner_radii = self.style.corner_radii.clone();
|
||||
|
||||
let mut selected_style = StyleRefinement::default();
|
||||
selected_style.corner_radii = corner_radii;
|
||||
|
||||
self.base
|
||||
.relative()
|
||||
.gap_x_1()
|
||||
.py_1()
|
||||
.px_3()
|
||||
.text_base()
|
||||
.text_color(cx.theme().foreground)
|
||||
.relative()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.refine_style(&self.style)
|
||||
.when(!self.disabled, |this| {
|
||||
this.when_some(self.on_click, |this, on_click| {
|
||||
this.on_mouse_down(MouseButton::Left, move |_, _, cx| {
|
||||
|
|
@ -168,5 +182,25 @@ impl RenderOnce for ListItem {
|
|||
}),
|
||||
)
|
||||
.when_some(self.suffix, |this, suffix| this.child(suffix(window, cx)))
|
||||
.map(|this| {
|
||||
if self.selected || self.secondary_selected {
|
||||
this.bg(cx.theme().accent).child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.right_0()
|
||||
.bottom_0()
|
||||
.when(!self.secondary_selected, |this| {
|
||||
this.bg(cx.theme().list_active)
|
||||
})
|
||||
.border_1()
|
||||
.border_color(cx.theme().list_active_border)
|
||||
.refine_style(&selected_style),
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,10 +67,6 @@ impl Disableable for MenuItem {
|
|||
}
|
||||
|
||||
impl Selectable for MenuItem {
|
||||
fn element_id(&self) -> &ElementId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ pub fn init(cx: &mut App) {
|
|||
]);
|
||||
}
|
||||
|
||||
pub trait PopupMenuExt: Styled + Selectable + IntoElement + 'static {
|
||||
pub trait PopupMenuExt: Styled + Selectable + InteractiveElement + IntoElement + 'static {
|
||||
/// Create a popup menu with the given items, anchored to the TopLeft corner
|
||||
fn popup_menu(
|
||||
self,
|
||||
|
|
@ -44,9 +44,9 @@ pub trait PopupMenuExt: Styled + Selectable + IntoElement + 'static {
|
|||
f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
|
||||
) -> Popover<PopupMenu> {
|
||||
let style = self.style().clone();
|
||||
let element_id = self.element_id();
|
||||
let id = self.interactivity().element_id.clone();
|
||||
|
||||
Popover::new(SharedString::from(format!("popup-menu:{:?}", element_id)))
|
||||
Popover::new(SharedString::from(format!("popup-menu:{:?}", id)))
|
||||
.no_style()
|
||||
.trigger(self)
|
||||
.trigger_style(style)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ impl PopoverContent {
|
|||
let focus_handle = cx.focus_handle();
|
||||
|
||||
Self {
|
||||
style: StyleRefinement::default().p_2(),
|
||||
style: StyleRefinement::default(),
|
||||
focus_handle,
|
||||
content: Rc::new(content),
|
||||
}
|
||||
|
|
@ -52,6 +52,7 @@ impl Styled for PopoverContent {
|
|||
impl Render for PopoverContent {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.p_2()
|
||||
.refine_style(&self.style)
|
||||
.track_focus(&self.focus_handle)
|
||||
.key_context(CONTEXT)
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
use gpui::{
|
||||
prelude::FluentBuilder as _, Div, ElementId, InteractiveElement, IntoElement, ParentElement,
|
||||
RenderOnce, SharedString, Styled,
|
||||
prelude::FluentBuilder as _, Div, InteractiveElement, IntoElement, ParentElement, RenderOnce,
|
||||
Styled,
|
||||
};
|
||||
|
||||
use crate::{h_flex, popup_menu::PopupMenuExt, ActiveTheme as _, Collapsible, Selectable};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct SidebarFooter {
|
||||
id: ElementId,
|
||||
base: Div,
|
||||
selected: bool,
|
||||
collapsed: bool,
|
||||
|
|
@ -16,7 +15,6 @@ pub struct SidebarFooter {
|
|||
impl SidebarFooter {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: SharedString::from("sidebar-footer").into(),
|
||||
base: h_flex().gap_2().w_full(),
|
||||
selected: false,
|
||||
collapsed: false,
|
||||
|
|
@ -29,14 +27,11 @@ impl Selectable for SidebarFooter {
|
|||
self
|
||||
}
|
||||
|
||||
fn element_id(&self) -> &gpui::ElementId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn is_selected(&self) -> bool {
|
||||
self.selected
|
||||
}
|
||||
}
|
||||
|
||||
impl Collapsible for SidebarFooter {
|
||||
fn is_collapsed(&self) -> bool {
|
||||
self.collapsed
|
||||
|
|
@ -57,11 +52,18 @@ impl Styled for SidebarFooter {
|
|||
self.base.style()
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractiveElement for SidebarFooter {
|
||||
fn interactivity(&mut self) -> &mut gpui::Interactivity {
|
||||
self.base.interactivity()
|
||||
}
|
||||
}
|
||||
|
||||
impl PopupMenuExt for SidebarFooter {}
|
||||
impl RenderOnce for SidebarFooter {
|
||||
fn render(self, _: &mut gpui::Window, cx: &mut gpui::App) -> impl gpui::IntoElement {
|
||||
h_flex()
|
||||
.id(self.id)
|
||||
.id("sidebar-footer")
|
||||
.gap_2()
|
||||
.p_2()
|
||||
.w_full()
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
use gpui::{
|
||||
prelude::FluentBuilder as _, Div, ElementId, InteractiveElement, IntoElement, ParentElement,
|
||||
RenderOnce, SharedString, Styled,
|
||||
prelude::FluentBuilder as _, Div, InteractiveElement, IntoElement, ParentElement, RenderOnce,
|
||||
Styled,
|
||||
};
|
||||
|
||||
use crate::{h_flex, popup_menu::PopupMenuExt, ActiveTheme as _, Collapsible, Selectable};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct SidebarHeader {
|
||||
id: ElementId,
|
||||
base: Div,
|
||||
selected: bool,
|
||||
collapsed: bool,
|
||||
|
|
@ -16,7 +15,6 @@ pub struct SidebarHeader {
|
|||
impl SidebarHeader {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: SharedString::from("sidebar-header").into(),
|
||||
base: h_flex().gap_2().w_full(),
|
||||
selected: false,
|
||||
collapsed: false,
|
||||
|
|
@ -29,10 +27,6 @@ impl Selectable for SidebarHeader {
|
|||
self
|
||||
}
|
||||
|
||||
fn element_id(&self) -> &gpui::ElementId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn is_selected(&self) -> bool {
|
||||
self.selected
|
||||
}
|
||||
|
|
@ -58,11 +52,18 @@ impl Styled for SidebarHeader {
|
|||
self.base.style()
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractiveElement for SidebarHeader {
|
||||
fn interactivity(&mut self) -> &mut gpui::Interactivity {
|
||||
self.base.interactivity()
|
||||
}
|
||||
}
|
||||
|
||||
impl PopupMenuExt for SidebarHeader {}
|
||||
impl RenderOnce for SidebarHeader {
|
||||
fn render(self, _: &mut gpui::Window, cx: &mut gpui::App) -> impl gpui::IntoElement {
|
||||
h_flex()
|
||||
.id(self.id)
|
||||
.id("sidebar-header")
|
||||
.gap_2()
|
||||
.p_2()
|
||||
.w_full()
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ use crate::{
|
|||
ActiveTheme,
|
||||
};
|
||||
use gpui::{
|
||||
div, point, px, App, Axis, BoxShadow, DefiniteLength, Div, Edges, Element, ElementId,
|
||||
FocusHandle, Hsla, Pixels, Refineable, StyleRefinement, Styled, Window,
|
||||
div, point, px, App, Axis, BoxShadow, DefiniteLength, Div, Edges, Element, FocusHandle, Hsla,
|
||||
Pixels, Refineable, StyleRefinement, Styled, Window,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
|
@ -338,14 +338,16 @@ impl From<Pixels> for Size {
|
|||
|
||||
/// A trait for defining element that can be selected.
|
||||
pub trait Selectable: Sized {
|
||||
/// Returns the element id of the element.
|
||||
fn element_id(&self) -> &ElementId;
|
||||
|
||||
/// Set the selected state of the element.
|
||||
fn selected(self, selected: bool) -> Self;
|
||||
|
||||
/// Returns true if the element is selected.
|
||||
fn is_selected(&self) -> bool;
|
||||
|
||||
/// Set is the element mouse right clicked, default do nothing.
|
||||
fn secondary_selected(self, _: bool) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for defining element that can be disabled.
|
||||
|
|
|
|||
|
|
@ -533,10 +533,6 @@ impl ParentElement for Tab {
|
|||
}
|
||||
|
||||
impl Selectable for Tab {
|
||||
fn element_id(&self) -> &ElementId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
|
|
|
|||
Loading…
Reference in a new issue