use std::rc::Rc; use gpui::{ AnyElement, App, ClickEvent, ElementId, FontWeight, InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div, prelude::*, relative, }; use smallvec::SmallVec; use crate::{ theme::{ActiveTheme, ControlSize, Radius, TextSize}, typography::{Ellipsis, LEADING}, }; type OnClick = Rc; /// 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, detail: Option, leading: Option, trailing: Option, selected: bool, current: bool, disabled: bool, strong: bool, quiet: bool, struck: bool, on_click: Option, } impl ListItem { pub fn new(id: impl Into, title: impl Into) -> Self { Self { id: id.into(), title: title.into(), description: None, detail: None, leading: None, trailing: None, selected: false, current: false, disabled: false, strong: false, quiet: false, struck: false, on_click: None, } } /// Sets the title in the strong weight, as a channel with unread messages has it. pub fn strong(mut self, strong: bool) -> Self { self.strong = strong; self } /// Sets the title in the quiet color, as a muted channel has it. pub fn quiet(mut self, quiet: bool) -> Self { self.quiet = quiet; self } /// Strikes the title through, as a finished task has it. pub fn struck(mut self, struck: bool) -> Self { self.struck = struck; self } /// A second line, quieter than the title. pub fn description(mut self, text: impl Into) -> Self { self.description = Some(text.into()); self } /// A third line, quieter still, such as a message's first words. pub fn detail(mut self, text: impl Into) -> Self { self.detail = 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 } pub(crate) fn is_disabled(&self) -> bool { self.disabled } /// 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() .min_w_0() .line_height(relative(LEADING)) .child(Ellipsis::new(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)) .when(self.strong, |title| title.font_weight(FontWeight::SEMIBOLD)) .when(self.struck, |title| title.line_through()) .text_color(if self.quiet { colors.fg_muted } else { colors.fg }), ) .children(self.description.map(|text| { line(text) .text_size(theme.text_size(TextSize::Sm)) .text_color(colors.fg_muted) })) .children(self.detail.map(|text| { line(text) .text_size(theme.text_size(TextSize::Sm)) .text_color(colors.fg_subtle) })), ) .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) { 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) })) } }