list: Add render_initial support to List. (#258)

Add a method to the List component to render the initial state. When
input is empty, you may need to render a special interface to the user.
This method can be called at this point

### Showcase

https://github.com/user-attachments/assets/39c61bfe-4451-43f9-b0d9-8f941ebd94dd
This commit is contained in:
ihavecoke 2024-09-21 01:53:32 +08:00 committed by GitHub
parent f20f573dbc
commit 2da13499e5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 122 additions and 61 deletions

View file

@ -2,16 +2,15 @@ use core::time;
use fake::Fake;
use gpui::{
actions, div, px, ElementId, FocusHandle, FocusableView, InteractiveElement, IntoElement,
ParentElement, Render, RenderOnce, Styled, Task, Timer, View, ViewContext, VisualContext,
WindowContext,
actions, div, px, relative, AnyElement, ElementId, FocusHandle, FocusableView,
InteractiveElement, IntoElement, ParentElement, Render, RenderOnce, Styled, Task, Timer, View,
ViewContext, VisualContext, WindowContext,
};
use ui::{
h_flex,
label::Label,
list::ListItem,
list::{List, ListDelegate},
list::{List, ListDelegate, ListItem},
theme::{hsl, ActiveTheme},
v_flex,
};
@ -167,6 +166,37 @@ impl ListDelegate for CompanyListDelegate {
}
}
fn render_initial(&self, cx: &mut ViewContext<List<Self>>) -> Option<AnyElement> {
let histories = ["BABA", "BIDU", "GOOGL", "LB", "LP", "LBW"];
let input_history = histories
.into_iter()
.map(|name| {
div()
.rounded_xl()
.min_w(px(30.))
.border_1()
.rounded_md()
.border_color(cx.theme().muted_foreground.opacity(0.3))
.line_height(relative(1.))
.p_1()
.child(div().child(name).text_xs())
})
.collect::<Vec<_>>();
let element = v_flex()
.p_4()
.child(
v_flex()
.gap_y_2()
.child("History")
.child(h_flex().gap_x_4().children(input_history)),
)
.into_any_element();
Some(element)
}
fn set_selected_index(&mut self, ix: Option<usize>, cx: &mut ViewContext<List<Self>>) {
if let Some(ix) = ix {
self.selected_index = ix;

View file

@ -1,17 +1,18 @@
use std::time::Duration;
use std::{cell::Cell, rc::Rc};
use crate::input::{InputEvent, TextInput};
use crate::scroll::ScrollbarState;
use crate::theme::ActiveTheme;
use crate::{scroll::Scrollbar, v_flex};
use crate::{IconName, Size};
use gpui::{
actions, div, prelude::FluentBuilder, uniform_list, AppContext, FocusHandle, FocusableView,
InteractiveElement, IntoElement, KeyBinding, Length, ListSizingBehavior, MouseButton,
ParentElement, Render, Styled, Task, UniformListScrollHandle, View, ViewContext, VisualContext,
use crate::{
input::{InputEvent, TextInput},
scroll::{Scrollbar, ScrollbarState},
theme::ActiveTheme,
v_flex, IconName, Size,
};
use gpui::{
actions, div, prelude::FluentBuilder, uniform_list, AnyElement, AppContext, Entity,
FocusHandle, FocusableView, InteractiveElement, IntoElement, KeyBinding, Length,
ListSizingBehavior, MouseButton, ParentElement, Render, SharedString, Styled, Task,
UniformListScrollHandle, View, ViewContext, VisualContext, WindowContext,
};
use gpui::{Entity, SharedString, WindowContext};
use smol::Timer;
actions!(list, [Cancel, Confirm, SelectPrev, SelectNext]);
@ -50,6 +51,17 @@ pub trait ListDelegate: Sized + 'static {
div()
}
/// 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(&self, cx: &mut ViewContext<List<Self>>) -> Option<AnyElement> {
None
}
/// Return the confirmed index of the selected item.
fn confirmed_index(&self) -> Option<usize> {
None
@ -322,6 +334,16 @@ where
let selected_bg = cx.theme().list_active;
let inital_view = if let Some(input) = &self.query_input {
if input.read(cx).text().is_empty() {
self.delegate().render_initial(cx)
} else {
None
}
} else {
None
};
v_flex()
.key_context("List")
.id("list")
@ -345,52 +367,61 @@ where
.child(input),
)
})
.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(cx))
})
.when(items_count > 0, |this| {
this.child(
uniform_list(view, "uniform-list", items_count, {
move |list, visible_range, cx| {
visible_range
.map(|ix| {
div()
.id("list-item")
.w_full()
.children(list.delegate.render_item(ix, cx))
.when_some(
list.selected_index,
|this, selected_index| {
this.when(ix == selected_index, |this| {
this.bg(selected_bg)
})
},
)
.on_mouse_down(
MouseButton::Left,
cx.listener(move |this, _, cx| {
cx.stop_propagation();
this.selected_index = Some(ix);
this.on_action_confirm(&Confirm, cx);
}),
)
})
.collect::<Vec<_>>()
}
})
.map(|this| {
if let Some(view) = inital_view {
this.child(view)
} else {
this.child(
v_flex()
.flex_grow()
.with_sizing_behavior(sizing_behavior)
.track_scroll(vertical_scroll_handle)
.into_any_element(),
)
})
.children(self.render_scrollbar(cx)),
)
.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(cx))
})
.when(items_count > 0, |this| {
this.child(
uniform_list(view, "uniform-list", items_count, {
move |list, visible_range, cx| {
visible_range
.map(|ix| {
div()
.id("list-item")
.w_full()
.children(list.delegate.render_item(ix, cx))
.when_some(
list.selected_index,
|this, selected_index| {
this.when(
ix == selected_index,
|this| this.bg(selected_bg),
)
},
)
.on_mouse_down(
MouseButton::Left,
cx.listener(move |this, _, cx| {
cx.stop_propagation();
this.selected_index = Some(ix);
this.on_action_confirm(
&Confirm, cx,
);
}),
)
})
.collect::<Vec<_>>()
}
})
.flex_grow()
.with_sizing_behavior(sizing_behavior)
.track_scroll(vertical_scroll_handle)
.into_any_element(),
)
})
.children(self.render_scrollbar(cx)),
)
}
})
}
}