From 2da13499e5a174ed9998648b4881613e9e097229 Mon Sep 17 00:00:00 2001 From: ihavecoke Date: Sat, 21 Sep 2024 01:53:32 +0800 Subject: [PATCH] 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 --- crates/story/src/list_story.rs | 40 +++++++-- crates/ui/src/list/list.rs | 143 ++++++++++++++++++++------------- 2 files changed, 122 insertions(+), 61 deletions(-) diff --git a/crates/story/src/list_story.rs b/crates/story/src/list_story.rs index d4347444..5ba966c6 100644 --- a/crates/story/src/list_story.rs +++ b/crates/story/src/list_story.rs @@ -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>) -> Option { + 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::>(); + + + 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, cx: &mut ViewContext>) { if let Some(ix) = ix { self.selected_index = ix; diff --git a/crates/ui/src/list/list.rs b/crates/ui/src/list/list.rs index 3006e893..d8787ba2 100644 --- a/crates/ui/src/list/list.rs +++ b/crates/ui/src/list/list.rs @@ -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>) -> Option { + None + } + /// Return the confirmed index of the selected item. fn confirmed_index(&self) -> Option { 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::>() - } - }) + .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::>() + } + }) + .flex_grow() + .with_sizing_behavior(sizing_behavior) + .track_scroll(vertical_scroll_handle) + .into_any_element(), + ) + }) + .children(self.render_scrollbar(cx)), + ) + } + }) } }