T13a Lists: ListItem, List, SelectableList, VirtualList, InfiniteList, GroupedList, SortableList, SwipeableListItem (review pending: codex 401)
This commit is contained in:
23 files changed
+1820
-14
No files matched your search
@@ -0,0 +1,189 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
AnyElement, App, ClickEvent, ElementId, InteractiveElement, IntoElement, MouseButton,
|
||||
ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div,
|
||||
prelude::*, relative,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{
|
||||
theme::{ActiveTheme, ControlSize, Radius, TextSize},
|
||||
typography::LEADING,
|
||||
};
|
||||
|
||||
type OnClick = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>;
|
||||
|
||||
/// One row of a list: something to lead with, a title and a line under it, and something at the end. It lights on hover when it can be pressed, and stays lit when selected.
|
||||
#[derive(IntoElement)]
|
||||
pub struct ListItem {
|
||||
id: ElementId,
|
||||
title: SharedString,
|
||||
description: Option<SharedString>,
|
||||
leading: Option<AnyElement>,
|
||||
trailing: Option<AnyElement>,
|
||||
selected: bool,
|
||||
current: bool,
|
||||
disabled: bool,
|
||||
on_click: Option<OnClick>,
|
||||
}
|
||||
|
||||
impl ListItem {
|
||||
pub fn new(id: impl Into<ElementId>, title: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
title: title.into(),
|
||||
description: None,
|
||||
leading: None,
|
||||
trailing: None,
|
||||
selected: false,
|
||||
current: false,
|
||||
disabled: false,
|
||||
on_click: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A second line, quieter than the title.
|
||||
pub fn description(mut self, text: impl Into<SharedString>) -> Self {
|
||||
self.description = Some(text.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// An icon or an avatar before the text.
|
||||
pub fn leading(mut self, element: impl IntoElement) -> Self {
|
||||
self.leading = Some(element.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
/// A time, a count or an action after the text.
|
||||
pub fn trailing(mut self, element: impl IntoElement) -> Self {
|
||||
self.trailing = Some(element.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
/// Rings the row the keyboard is on, in a list that has focus.
|
||||
pub fn current(mut self, current: bool) -> Self {
|
||||
self.current = current;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Makes the row pressable; the press keeps focus where it is.
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_click = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ListItem {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let colors = &theme.colors;
|
||||
let line = |text: SharedString| div().truncate().line_height(relative(LEADING)).child(text);
|
||||
let pressable = self.on_click.is_some() && !self.disabled;
|
||||
div()
|
||||
.id(self.id)
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.px_3()
|
||||
.py_1p5()
|
||||
.min_h(theme.control_height(ControlSize::Lg))
|
||||
.rounded(theme.radius(Radius::Md))
|
||||
.border_1()
|
||||
.border_color(if self.current {
|
||||
colors.focus
|
||||
} else {
|
||||
gpui::transparent_black()
|
||||
})
|
||||
.when(self.selected, |row| row.bg(colors.active))
|
||||
.when(self.disabled, |row| row.opacity(0.5))
|
||||
.when_some(self.on_click.filter(|_| pressable), |row, on_click| {
|
||||
row.cursor_pointer()
|
||||
.when(!self.selected, |row| {
|
||||
row.hover(|style| style.bg(colors.hover))
|
||||
})
|
||||
.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
|
||||
.on_click(move |event, window, cx| on_click(event, window, cx))
|
||||
})
|
||||
.children(self.leading.map(|leading| div().flex_none().child(leading)))
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(
|
||||
line(self.title)
|
||||
.text_size(theme.text_size(TextSize::Base))
|
||||
.text_color(colors.fg),
|
||||
)
|
||||
.children(self.description.map(|text| {
|
||||
line(text)
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.text_color(colors.fg_muted)
|
||||
})),
|
||||
)
|
||||
.children(self.trailing.map(|trailing| {
|
||||
div()
|
||||
.flex_none()
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.text_color(colors.fg_subtle)
|
||||
.child(trailing)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// Rows in a column; `divided` draws a hairline between them.
|
||||
#[derive(IntoElement, Default)]
|
||||
pub struct List {
|
||||
rows: SmallVec<[AnyElement; 8]>,
|
||||
divided: bool,
|
||||
}
|
||||
|
||||
impl List {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn divided(mut self) -> Self {
|
||||
self.divided = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for List {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.rows.extend(elements);
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for List {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let border = cx.theme().colors.border;
|
||||
let divided = self.divided;
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.when(!divided, |list| list.gap_0p5())
|
||||
.children(self.rows.into_iter().enumerate().map(move |(ix, row)| {
|
||||
div()
|
||||
.when(divided && ix > 0, |row| {
|
||||
row.border_t_1().border_color(border)
|
||||
})
|
||||
.child(row)
|
||||
}))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user