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:
Jason Lee 2025-07-28 18:38:39 +08:00 committed by GitHub
parent 6d0443998c
commit 93b578294c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 304 additions and 174 deletions

View file

@ -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()

View file

@ -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 {

View file

@ -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()

View file

@ -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

View file

@ -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)
}

View file

@ -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)),
)
}
}

View file

@ -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();
}))
})

View file

@ -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
}
})
}
}

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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()

View file

@ -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()

View file

@ -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.

View file

@ -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