Skip to main content

rgpui_component/list/
list_item.rs

1use crate::{ActiveTheme, Disableable, Icon, Selectable, Sizable as _, StyledExt, h_flex};
2use rgpui::{
3    AnyElement, App, ClickEvent, Div, ElementId, InteractiveElement, IntoElement, MouseButton,
4    MouseDownEvent, MouseMoveEvent, ParentElement, RenderOnce, Stateful,
5    StatefulInteractiveElement as _, StyleRefinement, Styled, Window, div,
6    prelude::FluentBuilder as _,
7};
8use smallvec::SmallVec;
9use std::collections::HashMap;
10
11/// 列表项的模式
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13enum ListItemMode {
14    /// 普通条目
15    #[default]
16    Entry,
17    /// 分隔线
18    Separator,
19}
20
21impl ListItemMode {
22    /// 检查是否为分隔线
23    #[inline]
24    fn is_separator(&self) -> bool {
25        matches!(self, ListItemMode::Separator)
26    }
27}
28
29/// 列表项组件
30#[derive(IntoElement)]
31pub struct ListItem {
32    /// 基础 Stateful Div
33    base: Stateful<Div>,
34    /// 列表项模式
35    mode: ListItemMode,
36    /// 样式引用
37    style: StyleRefinement,
38    /// 是否禁用
39    disabled: bool,
40    /// 是否选中
41    selected: bool,
42    /// 是否次要选中(如右键菜单)
43    secondary_selected: bool,
44    /// 是否确认状态
45    confirmed: bool,
46    /// 选中标记图标
47    check_icon: Option<Icon>,
48    /// 点击回调
49    on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
50    /// 鼠标按下回调
51    on_mouse_down:
52        HashMap<MouseButton, Box<dyn Fn(&MouseDownEvent, &mut Window, &mut App) + 'static>>,
53    /// 鼠标进入回调
54    on_mouse_enter: Option<Box<dyn Fn(&MouseMoveEvent, &mut Window, &mut App) + 'static>>,
55    /// 后缀元素
56    suffix: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>>,
57    /// 子元素
58    children: SmallVec<[AnyElement; 2]>,
59}
60
61impl ListItem {
62    /// 创建新的列表项
63    pub fn new(id: impl Into<ElementId>) -> Self {
64        let id: ElementId = id.into();
65        Self {
66            mode: ListItemMode::Entry,
67            base: h_flex().id(id),
68            style: StyleRefinement::default(),
69            disabled: false,
70            selected: false,
71            secondary_selected: false,
72            confirmed: false,
73            on_click: None,
74            on_mouse_down: HashMap::new(),
75            on_mouse_enter: None,
76            check_icon: None,
77            suffix: None,
78            children: SmallVec::new(),
79        }
80    }
81
82    /// 将此列表项设置为分隔线,不可选中
83    pub fn separator(mut self) -> Self {
84        self.mode = ListItemMode::Separator;
85        self
86    }
87
88    /// 设置显示选中标记图标,默认为 None
89    pub fn check_icon(mut self, icon: impl Into<Icon>) -> Self {
90        self.check_icon = Some(icon.into());
91        self
92    }
93
94    /// 设置列表项为选中样式
95    pub fn selected(mut self, selected: bool) -> Self {
96        self.selected = selected;
97        self
98    }
99
100    /// 设置列表项为确认样式,将显示勾选图标
101    pub fn confirmed(mut self, confirmed: bool) -> Self {
102        self.confirmed = confirmed;
103        self
104    }
105
106    /// 设置禁用状态
107    pub fn disabled(mut self, disabled: bool) -> Self {
108        self.disabled = disabled;
109        self
110    }
111
112    /// 设置输入字段的后缀元素,例如清除按钮
113    pub fn suffix<F, E>(mut self, builder: F) -> Self
114    where
115        F: Fn(&mut Window, &mut App) -> E + 'static,
116        E: IntoElement,
117    {
118        self.suffix = Some(Box::new(move |window, cx| {
119            builder(window, cx).into_any_element()
120        }));
121        self
122    }
123
124    /// 添加点击事件处理
125    pub fn on_click(
126        mut self,
127        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
128    ) -> Self {
129        self.on_click = Some(Box::new(handler));
130        self
131    }
132
133    /// 添加鼠标按下事件处理
134    pub fn on_mouse_down(
135        mut self,
136        button: MouseButton,
137        handler: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static,
138    ) -> Self {
139        self.on_mouse_down.insert(button, Box::new(handler));
140        self
141    }
142
143    /// 添加鼠标进入事件处理
144    pub fn on_mouse_enter(
145        mut self,
146        handler: impl Fn(&MouseMoveEvent, &mut Window, &mut App) + 'static,
147    ) -> Self {
148        self.on_mouse_enter = Some(Box::new(handler));
149        self
150    }
151}
152
153impl Disableable for ListItem {
154    fn disabled(mut self, disabled: bool) -> Self {
155        self.disabled = disabled;
156        self
157    }
158}
159
160impl Selectable for ListItem {
161    fn selected(mut self, selected: bool) -> Self {
162        self.selected = selected;
163        self
164    }
165
166    fn is_selected(&self) -> bool {
167        self.selected
168    }
169
170    fn secondary_selected(mut self, selected: bool) -> Self {
171        self.secondary_selected = selected;
172        self
173    }
174}
175
176impl Styled for ListItem {
177    fn style(&mut self) -> &mut rgpui::StyleRefinement {
178        &mut self.style
179    }
180}
181
182impl ParentElement for ListItem {
183    fn extend(&mut self, elements: impl IntoIterator<Item = rgpui::AnyElement>) {
184        self.children.extend(elements);
185    }
186}
187
188impl RenderOnce for ListItem {
189    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
190        let is_active = self.confirmed || self.selected || self.secondary_selected;
191
192        let corner_radii = self.style.corner_radii.clone();
193
194        let mut selected_style = StyleRefinement::default();
195        selected_style.corner_radii = corner_radii;
196
197        let is_selectable = !(self.disabled || self.mode.is_separator());
198
199        self.base
200            .relative()
201            .gap_x_1()
202            .py_1()
203            .px_3()
204            .text_base()
205            .text_color(cx.theme().foreground)
206            .relative()
207            .items_center()
208            .justify_between()
209            .refine_style(&self.style)
210            .when(is_selectable, |this| {
211                this.when_some(self.on_click, |this, on_click| this.on_click(on_click))
212                    .when_some(self.on_mouse_enter, |this, on_mouse_enter| {
213                        this.on_mouse_move(move |ev, window, cx| (on_mouse_enter)(ev, window, cx))
214                    })
215                    .map(|this| {
216                        self.on_mouse_down
217                            .into_iter()
218                            .fold(this, |this, (button, handler)| {
219                                this.on_mouse_down(button, move |ev, window, cx| {
220                                    handler(ev, window, cx)
221                                })
222                            })
223                    })
224                    .when(!is_active, |this| {
225                        this.hover(|this| this.bg(cx.theme().list_hover))
226                    })
227            })
228            .when(!is_selectable, |this| {
229                this.text_color(cx.theme().muted_foreground)
230            })
231            .child(
232                h_flex()
233                    .w_full()
234                    .items_center()
235                    .justify_between()
236                    .gap_x_1()
237                    .child(div().w_full().children(self.children))
238                    .when_some(self.check_icon, |this, icon| {
239                        this.child(
240                            div().w_5().items_center().justify_center().when(
241                                self.confirmed,
242                                |this| {
243                                    this.child(icon.small().text_color(cx.theme().muted_foreground))
244                                },
245                            ),
246                        )
247                    }),
248            )
249            .when_some(self.suffix, |this, suffix| this.child(suffix(window, cx)))
250            .map(|this| {
251                if is_selectable && (self.selected || self.secondary_selected) {
252                    let bg = if self.selected && cx.theme().list.active_highlight {
253                        cx.theme().list_active
254                    } else {
255                        cx.theme().accent
256                    };
257
258                    this.when(!self.secondary_selected, |this| this.bg(bg))
259                        .when(cx.theme().list.active_highlight, |this| {
260                            this.child(
261                                div()
262                                    .absolute()
263                                    .top_0()
264                                    .left_0()
265                                    .right_0()
266                                    .bottom_0()
267                                    .border_1()
268                                    .border_color(cx.theme().list_active_border)
269                                    .refine_style(&selected_style),
270                            )
271                        })
272                } else {
273                    this
274                }
275            })
276    }
277}