feat: revamp the chat panel ui (#7)
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m40s
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m40s
Reviewed-on: #7
This commit was merged in pull request #7.
This commit is contained in:
221
crates/ui/src/list/cache.rs
Normal file
221
crates/ui/src/list/cache.rs
Normal file
@@ -0,0 +1,221 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{App, Pixels, Size};
|
||||
|
||||
use crate::IndexPath;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum RowEntry {
|
||||
Entry(IndexPath),
|
||||
SectionHeader(usize),
|
||||
SectionFooter(usize),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub(crate) struct MeasuredEntrySize {
|
||||
pub(crate) item_size: Size<Pixels>,
|
||||
pub(crate) section_header_size: Size<Pixels>,
|
||||
pub(crate) section_footer_size: Size<Pixels>,
|
||||
}
|
||||
|
||||
impl RowEntry {
|
||||
#[inline]
|
||||
#[allow(unused)]
|
||||
pub(crate) fn is_section_header(&self) -> bool {
|
||||
matches!(self, RowEntry::SectionHeader(_))
|
||||
}
|
||||
|
||||
pub(crate) fn eq_index_path(&self, path: &IndexPath) -> bool {
|
||||
match self {
|
||||
RowEntry::Entry(index_path) => index_path == path,
|
||||
RowEntry::SectionHeader(_) | RowEntry::SectionFooter(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) fn index(&self) -> IndexPath {
|
||||
match self {
|
||||
RowEntry::Entry(index_path) => *index_path,
|
||||
RowEntry::SectionHeader(ix) => IndexPath::default().section(*ix),
|
||||
RowEntry::SectionFooter(ix) => IndexPath::default().section(*ix),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[allow(unused)]
|
||||
pub(crate) fn is_section_footer(&self) -> bool {
|
||||
matches!(self, RowEntry::SectionFooter(_))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn is_entry(&self) -> bool {
|
||||
matches!(self, RowEntry::Entry(_))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[allow(unused)]
|
||||
pub(crate) fn section_ix(&self) -> Option<usize> {
|
||||
match self {
|
||||
RowEntry::SectionHeader(ix) | RowEntry::SectionFooter(ix) => Some(*ix),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub(crate) struct RowsCache {
|
||||
/// Only have section's that have rows.
|
||||
pub(crate) entities: Rc<Vec<RowEntry>>,
|
||||
pub(crate) items_count: usize,
|
||||
/// The sections, the item is number of rows in each section.
|
||||
pub(crate) sections: Rc<Vec<usize>>,
|
||||
pub(crate) entries_sizes: Rc<Vec<Size<Pixels>>>,
|
||||
measured_size: MeasuredEntrySize,
|
||||
}
|
||||
|
||||
impl RowsCache {
|
||||
pub(crate) fn get(&self, flatten_ix: usize) -> Option<RowEntry> {
|
||||
self.entities.get(flatten_ix).cloned()
|
||||
}
|
||||
|
||||
/// Returns the number of flattened rows (Includes header, item, footer).
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.entities.len()
|
||||
}
|
||||
|
||||
/// Return the number of items in the cache.
|
||||
pub(crate) fn items_count(&self) -> usize {
|
||||
self.items_count
|
||||
}
|
||||
|
||||
/// Returns the index of the Entry with given path in the flattened rows.
|
||||
pub(crate) fn position_of(&self, path: &IndexPath) -> Option<usize> {
|
||||
self.entities
|
||||
.iter()
|
||||
.position(|p| p.is_entry() && p.eq_index_path(path))
|
||||
}
|
||||
|
||||
/// Return prev row, if the row is the first in the first section, goes to the last row.
|
||||
///
|
||||
/// Empty rows section are skipped.
|
||||
pub(crate) fn prev(&self, path: Option<IndexPath>) -> IndexPath {
|
||||
let path = path.unwrap_or_default();
|
||||
let Some(pos) = self.position_of(&path) else {
|
||||
return self
|
||||
.entities
|
||||
.iter()
|
||||
.rfind(|entry| entry.is_entry())
|
||||
.map(|entry| entry.index())
|
||||
.unwrap_or_default();
|
||||
};
|
||||
|
||||
if let Some(path) = self
|
||||
.entities
|
||||
.iter()
|
||||
.take(pos)
|
||||
.rev()
|
||||
.find(|entry| entry.is_entry())
|
||||
.map(|entry| entry.index())
|
||||
{
|
||||
path
|
||||
} else {
|
||||
self.entities
|
||||
.iter()
|
||||
.rfind(|entry| entry.is_entry())
|
||||
.map(|entry| entry.index())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the next row, if the row is the last in the last section, goes to the first row.
|
||||
///
|
||||
/// Empty rows section are skipped.
|
||||
pub(crate) fn next(&self, path: Option<IndexPath>) -> IndexPath {
|
||||
let Some(mut path) = path else {
|
||||
return IndexPath::default();
|
||||
};
|
||||
|
||||
let Some(pos) = self.position_of(&path) else {
|
||||
return self
|
||||
.entities
|
||||
.iter()
|
||||
.find(|entry| entry.is_entry())
|
||||
.map(|entry| entry.index())
|
||||
.unwrap_or_default();
|
||||
};
|
||||
|
||||
if let Some(next_path) = self
|
||||
.entities
|
||||
.iter()
|
||||
.skip(pos + 1)
|
||||
.find(|entry| entry.is_entry())
|
||||
.map(|entry| entry.index())
|
||||
{
|
||||
path = next_path;
|
||||
} else {
|
||||
path = self
|
||||
.entities
|
||||
.iter()
|
||||
.find(|entry| entry.is_entry())
|
||||
.map(|entry| entry.index())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
pub(crate) fn prepare_if_needed<F>(
|
||||
&mut self,
|
||||
sections_count: usize,
|
||||
measured_size: MeasuredEntrySize,
|
||||
cx: &App,
|
||||
rows_count_f: F,
|
||||
) where
|
||||
F: Fn(usize, &App) -> usize,
|
||||
{
|
||||
let mut new_sections = vec![];
|
||||
for section_ix in 0..sections_count {
|
||||
new_sections.push(rows_count_f(section_ix, cx));
|
||||
}
|
||||
|
||||
let need_update = new_sections != *self.sections || self.measured_size != measured_size;
|
||||
|
||||
if !need_update {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut entries_sizes = vec![];
|
||||
let mut total_items_count = 0;
|
||||
self.measured_size = measured_size;
|
||||
self.sections = Rc::new(new_sections);
|
||||
self.entities = Rc::new(
|
||||
self.sections
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(section, items_count)| {
|
||||
total_items_count += items_count;
|
||||
let mut children = vec![];
|
||||
if *items_count == 0 {
|
||||
return children;
|
||||
}
|
||||
|
||||
children.push(RowEntry::SectionHeader(section));
|
||||
entries_sizes.push(measured_size.section_header_size);
|
||||
for row in 0..*items_count {
|
||||
children.push(RowEntry::Entry(IndexPath {
|
||||
section,
|
||||
row,
|
||||
..Default::default()
|
||||
}));
|
||||
entries_sizes.push(measured_size.item_size);
|
||||
}
|
||||
children.push(RowEntry::SectionFooter(section));
|
||||
entries_sizes.push(measured_size.section_footer_size);
|
||||
children
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
self.entries_sizes = Rc::new(entries_sizes);
|
||||
self.items_count = total_items_count;
|
||||
}
|
||||
}
|
||||
171
crates/ui/src/list/delegate.rs
Normal file
171
crates/ui/src/list/delegate.rs
Normal file
@@ -0,0 +1,171 @@
|
||||
use gpui::{AnyElement, App, Context, IntoElement, ParentElement as _, Styled as _, Task, Window};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::list::loading::Loading;
|
||||
use crate::list::ListState;
|
||||
use crate::{h_flex, Icon, IconName, IndexPath, Selectable};
|
||||
|
||||
/// A delegate for the List.
|
||||
#[allow(unused)]
|
||||
pub trait ListDelegate: Sized + 'static {
|
||||
type Item: Selectable + IntoElement;
|
||||
|
||||
/// When Query Input change, this method will be called.
|
||||
/// You can perform search here.
|
||||
fn perform_search(
|
||||
&mut self,
|
||||
query: &str,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<ListState<Self>>,
|
||||
) -> Task<()> {
|
||||
Task::ready(())
|
||||
}
|
||||
|
||||
/// Return the number of sections in the list, default is 1.
|
||||
///
|
||||
/// Min value is 1.
|
||||
fn sections_count(&self, cx: &App) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
/// Return the number of items in the section at the given index.
|
||||
///
|
||||
/// NOTE: Only the sections with items_count > 0 will be rendered. If the section has 0 items,
|
||||
/// the section header and footer will also be skipped.
|
||||
fn items_count(&self, section: usize, cx: &App) -> usize;
|
||||
|
||||
/// Render the item at the given index.
|
||||
///
|
||||
/// Return None will skip the item.
|
||||
///
|
||||
/// NOTE: Every item should have same height.
|
||||
fn render_item(
|
||||
&mut self,
|
||||
ix: IndexPath,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<ListState<Self>>,
|
||||
) -> Option<Self::Item>;
|
||||
|
||||
/// Render the section header at the given index, default is None.
|
||||
///
|
||||
/// NOTE: Every header should have same height.
|
||||
fn render_section_header(
|
||||
&mut self,
|
||||
section: usize,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<ListState<Self>>,
|
||||
) -> Option<impl IntoElement> {
|
||||
None::<AnyElement>
|
||||
}
|
||||
|
||||
/// Render the section footer at the given index, default is None.
|
||||
///
|
||||
/// NOTE: Every footer should have same height.
|
||||
fn render_section_footer(
|
||||
&mut self,
|
||||
section: usize,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<ListState<Self>>,
|
||||
) -> Option<impl IntoElement> {
|
||||
None::<AnyElement>
|
||||
}
|
||||
|
||||
/// Return a Element to show when list is empty.
|
||||
fn render_empty(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<ListState<Self>>,
|
||||
) -> impl IntoElement {
|
||||
h_flex()
|
||||
.size_full()
|
||||
.justify_center()
|
||||
.text_color(cx.theme().text_muted.opacity(0.6))
|
||||
.child(Icon::new(IconName::Inbox).size_12())
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// For example: The last search results, or the last selected item.
|
||||
///
|
||||
/// Default is None, that means no initial state.
|
||||
fn render_initial(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<ListState<Self>>,
|
||||
) -> Option<AnyElement> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the loading state to show the loading view.
|
||||
fn loading(&self, cx: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Returns a Element to show when loading, default is built-in Skeleton
|
||||
/// loading view.
|
||||
fn render_loading(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<ListState<Self>>,
|
||||
) -> impl IntoElement {
|
||||
Loading
|
||||
}
|
||||
|
||||
/// Set the selected index, just store the ix, don't confirm.
|
||||
fn set_selected_index(
|
||||
&mut self,
|
||||
ix: Option<IndexPath>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<ListState<Self>>,
|
||||
);
|
||||
|
||||
/// Set the index of the item that has been right clicked.
|
||||
fn set_right_clicked_index(
|
||||
&mut self,
|
||||
ix: Option<IndexPath>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<ListState<Self>>,
|
||||
) {
|
||||
}
|
||||
|
||||
/// 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<ListState<Self>>) {
|
||||
}
|
||||
|
||||
/// Cancel the selection, e.g.: Pressed ESC.
|
||||
fn cancel(&mut self, window: &mut Window, cx: &mut Context<ListState<Self>>) {}
|
||||
|
||||
/// Return true to enable load more data when scrolling to the bottom.
|
||||
///
|
||||
/// Default: false
|
||||
fn has_more(&self, cx: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Returns a threshold value (n entities), 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 entities (section header, footer and row)
|
||||
fn load_more_threshold(&self) -> usize {
|
||||
20
|
||||
}
|
||||
|
||||
/// Load more data when the table is scrolled to the bottom.
|
||||
///
|
||||
/// 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.
|
||||
fn load_more(&mut self, window: &mut Window, cx: &mut Context<ListState<Self>>) {}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,39 +1,57 @@
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
div, AnyElement, App, ClickEvent, Div, ElementId, InteractiveElement, IntoElement, MouseButton,
|
||||
MouseMoveEvent, ParentElement, RenderOnce, Stateful, StatefulInteractiveElement as _, Styled,
|
||||
Window,
|
||||
div, AnyElement, App, ClickEvent, Div, ElementId, InteractiveElement, IntoElement,
|
||||
MouseMoveEvent, ParentElement, RenderOnce, Stateful, StatefulInteractiveElement as _,
|
||||
StyleRefinement, Styled, Window,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::{h_flex, Disableable, Icon, IconName, Selectable, Sizable as _};
|
||||
use crate::{h_flex, Disableable, Icon, Selectable, Sizable as _, StyledExt};
|
||||
|
||||
type OnClick = Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>;
|
||||
type OnMouseEnter = Option<Box<dyn Fn(&MouseMoveEvent, &mut Window, &mut App) + 'static>>;
|
||||
type Suffix = Option<Box<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>>;
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
enum ListItemMode {
|
||||
#[default]
|
||||
Entry,
|
||||
Separator,
|
||||
}
|
||||
|
||||
impl ListItemMode {
|
||||
#[inline]
|
||||
fn is_separator(&self) -> bool {
|
||||
matches!(self, ListItemMode::Separator)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ListItem {
|
||||
base: Stateful<Div>,
|
||||
mode: ListItemMode,
|
||||
style: StyleRefinement,
|
||||
disabled: bool,
|
||||
selected: bool,
|
||||
secondary_selected: bool,
|
||||
confirmed: bool,
|
||||
check_icon: Option<Icon>,
|
||||
on_click: OnClick,
|
||||
on_mouse_enter: OnMouseEnter,
|
||||
suffix: Suffix,
|
||||
#[allow(clippy::type_complexity)]
|
||||
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
on_mouse_enter: Option<Box<dyn Fn(&MouseMoveEvent, &mut Window, &mut App) + 'static>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
suffix: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>>,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl ListItem {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
let id: ElementId = id.into();
|
||||
|
||||
Self {
|
||||
base: h_flex().id(id).gap_x_1().py_1().px_2().text_base(),
|
||||
mode: ListItemMode::Entry,
|
||||
base: h_flex().id(id),
|
||||
style: StyleRefinement::default(),
|
||||
disabled: false,
|
||||
selected: false,
|
||||
secondary_selected: false,
|
||||
confirmed: false,
|
||||
on_click: None,
|
||||
on_mouse_enter: None,
|
||||
@@ -43,9 +61,15 @@ impl ListItem {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set this list item to as a separator, it not able to be selected.
|
||||
pub fn separator(mut self) -> Self {
|
||||
self.mode = ListItemMode::Separator;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set to show check icon, default is None.
|
||||
pub fn check_icon(mut self, icon: IconName) -> Self {
|
||||
self.check_icon = Some(Icon::new(icon));
|
||||
pub fn check_icon(mut self, icon: impl Into<Icon>) -> Self {
|
||||
self.check_icon = Some(icon.into());
|
||||
self
|
||||
}
|
||||
|
||||
@@ -111,11 +135,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,35 +156,39 @@ impl ParentElement for ListItem {
|
||||
|
||||
impl RenderOnce for ListItem {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let is_active = self.selected || self.confirmed;
|
||||
let is_active = self.confirmed || self.selected;
|
||||
|
||||
let corner_radii = self.style.corner_radii.clone();
|
||||
|
||||
let _selected_style = StyleRefinement {
|
||||
corner_radii,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let is_selectable = !(self.disabled || self.mode.is_separator());
|
||||
|
||||
self.base
|
||||
.relative()
|
||||
.gap_x_1()
|
||||
.py_1()
|
||||
.px_3()
|
||||
.text_base()
|
||||
.text_color(cx.theme().text)
|
||||
.relative()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.when_some(self.on_click, |this, on_click| {
|
||||
if !self.disabled {
|
||||
this.cursor_pointer()
|
||||
.on_mouse_down(MouseButton::Left, move |_, _window, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_click(on_click)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
.refine_style(&self.style)
|
||||
.when(is_selectable, |this| {
|
||||
this.when_some(self.on_click, |this, on_click| this.on_click(on_click))
|
||||
.when_some(self.on_mouse_enter, |this, on_mouse_enter| {
|
||||
this.on_mouse_move(move |ev, window, cx| (on_mouse_enter)(ev, window, cx))
|
||||
})
|
||||
.when(!is_active, |this| {
|
||||
this.hover(|this| this.bg(cx.theme().ghost_element_hover))
|
||||
})
|
||||
})
|
||||
.when(is_active, |this| this.bg(cx.theme().element_active))
|
||||
.when(!is_active && !self.disabled, |this| {
|
||||
this.hover(|this| this.bg(cx.theme().elevated_surface_background))
|
||||
})
|
||||
// Mouse enter
|
||||
.when_some(self.on_mouse_enter, |this, on_mouse_enter| {
|
||||
if !self.disabled {
|
||||
this.on_mouse_move(move |ev, window, cx| (on_mouse_enter)(ev, window, cx))
|
||||
} else {
|
||||
this
|
||||
}
|
||||
.when(!is_selectable, |this| {
|
||||
this.text_color(cx.theme().text_muted)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -177,5 +210,17 @@ impl RenderOnce for ListItem {
|
||||
}),
|
||||
)
|
||||
.when_some(self.suffix, |this, suffix| this.child(suffix(window, cx)))
|
||||
.map(|this| {
|
||||
if is_selectable && (self.selected || self.secondary_selected) {
|
||||
let bg = if self.selected {
|
||||
cx.theme().ghost_element_active
|
||||
} else {
|
||||
cx.theme().ghost_element_background
|
||||
};
|
||||
this.bg(bg)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ impl RenderOnce for LoadingItem {
|
||||
.gap_1p5()
|
||||
.overflow_hidden()
|
||||
.child(Skeleton::new().h_5().w_48().max_w_full())
|
||||
.child(Skeleton::new().secondary(true).h_3().w_64().max_w_full()),
|
||||
.child(Skeleton::new().secondary().h_3().w_64().max_w_full()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
pub(crate) mod cache;
|
||||
mod delegate;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod list;
|
||||
mod list_item;
|
||||
mod loading;
|
||||
mod separator_item;
|
||||
|
||||
pub use delegate::*;
|
||||
pub use list::*;
|
||||
pub use list_item::*;
|
||||
pub use separator_item::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Settings for List.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ListSettings {
|
||||
/// Whether to use active highlight style on ListItem, default
|
||||
pub active_highlight: bool,
|
||||
}
|
||||
|
||||
impl Default for ListSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
active_highlight: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
50
crates/ui/src/list/separator_item.rs
Normal file
50
crates/ui/src/list/separator_item.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use gpui::{AnyElement, ParentElement, RenderOnce, StyleRefinement};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::list::ListItem;
|
||||
use crate::{Selectable, StyledExt};
|
||||
|
||||
pub struct ListSeparatorItem {
|
||||
style: StyleRefinement,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl ListSeparatorItem {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
style: StyleRefinement::default(),
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ListSeparatorItem {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for ListSeparatorItem {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements);
|
||||
}
|
||||
}
|
||||
|
||||
impl Selectable for ListSeparatorItem {
|
||||
fn selected(self, _: bool) -> Self {
|
||||
self
|
||||
}
|
||||
|
||||
fn is_selected(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ListSeparatorItem {
|
||||
fn render(self, _: &mut gpui::Window, _: &mut gpui::App) -> impl gpui::IntoElement {
|
||||
ListItem::new("separator")
|
||||
.refine_style(&self.style)
|
||||
.children(self.children)
|
||||
.disabled(true)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user