gpui-component/crates/story/src/form_story.rs
Jason Lee 9a040eaaac
list: Refactor List, Dropdown delegate API to support section. (#1107)
Ref UITableView API:

https://developer.apple.com/documentation/uikit/uitableviewdatasource

## Changes

- Added some section related API to `ListDelegeate` and
`DropdownDelegate` with default implement, so if you don't need section
that you can just keep the default.
- Added `sections_count` method to get the number of sections, default
is 1.
- Added `render_section_header` for special the section header by if
needed, default return None.
- Added `render_section_footer` for special the section footer by if
needed, default return None.

## Break Changes

- The `DropdownState` have change new method to use IndexPath type:

  ```diff
  - DropdownState::new(vec![], Some(1), window, cx);
  + DropdownState::new(vec![], Some(IndexPath::new(1)), window, cx);
  ```

- The `ListDelegeate`, `DropdownDelegate` has changed API:
  - The `ix` are change from `usize` to `IndexPath`.

  ```diff
- fn render_item(&self, ix: usize, window: &mut Window, cx: &mut
Context<List<Self>>) -> Option<Self::Item>
+ fn render_item(&self, ix: IndexPath, window: &mut Window, cx: &mut
Context<List<Self>>) -> Option<Self::Item>

- fn set_selected_index(&mut self, ix: Option<usize>, window: &mut
Window, cx: &mut Context<List<Self>>)
+ fn set_selected_index(&mut self, ix: Option<IndexPath>, window: &mut
Window, cx: &mut Context<List<Self>>)
  ```

- The `items_count` method have added `section` argument to support list
section.
  
  ```diff
  - fn items_count(&self, cx: &App) -> usize
  + fn items_count(&self, section: usize, cx: &App) -> usize
  ```

- The `can_load_more` method has renamed to `is_eof` in `ListDelegate`
and `TableDelegate`.

  ```diff
  - fn can_load_more(&self, cx: &App) -> bool
  + fn is_eof(&self, cx: &App) -> bool
  ```

- The `can_search` method has renamed to `searchable` in `ListDelegate`.

  ```diff
  - fn can_search(&self) -> bool
  + fn searchable(&self) -> bool
  ```

## Showcase

<img width="1196" height="925" alt="image"
src="https://github.com/user-attachments/assets/22750abe-cc3f-427e-903b-51758bd4e027"
/>

<img width="1214" height="934" alt="image"
src="https://github.com/user-attachments/assets/52d42546-e6e7-4448-9c0f-43cd659fb630"
/>

---------

Co-authored-by: Floyd Wang <gassnake999@gmail.com>
2025-08-05 19:23:32 +08:00

257 lines
9.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use gpui::{
div, prelude::FluentBuilder as _, px, App, AppContext, Axis, Context, Entity, Focusable,
InteractiveElement, IntoElement, ParentElement as _, Render, Styled, Window,
};
use gpui_component::{
button::{Button, ButtonGroup},
checkbox::Checkbox,
color_picker::{ColorPicker, ColorPickerState},
date_picker::{DatePicker, DatePickerState},
divider::Divider,
dropdown::{Dropdown, DropdownState},
form::{form_field, v_form},
h_flex,
input::{InputState, TextInput},
switch::Switch,
v_flex, ActiveTheme, AxisExt, FocusableCycle, IndexPath, Selectable, Sizable, Size,
};
pub struct FormStory {
name_prefix_state: Entity<DropdownState<Vec<String>>>,
name_input: Entity<InputState>,
email_input: Entity<InputState>,
bio_input: Entity<InputState>,
color_state: Entity<ColorPickerState>,
subscribe_email: bool,
date: Entity<DatePickerState>,
layout: Axis,
size: Size,
}
impl super::Story for FormStory {
fn title() -> &'static str {
"Form"
}
fn description() -> &'static str {
"Form to collect multiple inputs."
}
fn closable() -> bool {
false
}
fn new_view(window: &mut Window, cx: &mut App) -> Entity<impl Render + Focusable> {
Self::view(window, cx)
}
}
impl FormStory {
pub fn view(window: &mut Window, cx: &mut App) -> Entity<Self> {
cx.new(|cx| Self::new(window, cx))
}
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let name_prefix_state = cx.new(|cx| {
DropdownState::new(
vec![
"Mr.".to_string(),
"Mrs.".to_string(),
"Ms.".to_string(),
"Dr.".to_string(),
],
Some(IndexPath::default()),
window,
cx,
)
});
let name_input = cx.new(|cx| InputState::new(window, cx).default_value("Jason Lee"));
let color_state = cx.new(|cx| ColorPickerState::new(window, cx));
let email_input =
cx.new(|cx| InputState::new(window, cx).placeholder("Enter text here..."));
let bio_input = cx.new(|cx| {
InputState::new(window, cx)
.auto_grow(5, 20)
.placeholder("Enter text here...")
.default_value("Hello 世界this is GPUI component.")
});
let date = cx.new(|cx| DatePickerState::new(window, cx));
Self {
name_prefix_state,
name_input,
email_input,
bio_input,
date,
color_state,
subscribe_email: false,
layout: Axis::Vertical,
size: Size::default(),
}
}
}
impl FocusableCycle for FormStory {
fn cycle_focus_handles(&self, _: &mut Window, cx: &mut App) -> Vec<gpui::FocusHandle>
where
Self: Sized,
{
vec![
self.name_input.focus_handle(cx),
self.email_input.focus_handle(cx),
self.bio_input.focus_handle(cx),
]
}
}
impl Focusable for FormStory {
fn focus_handle(&self, cx: &gpui::App) -> gpui::FocusHandle {
self.name_input.focus_handle(cx)
}
}
impl Render for FormStory {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.id("form-story")
.size_full()
.p_4()
.justify_start()
.gap_3()
.child(
h_flex()
.gap_3()
.flex_wrap()
.justify_between()
.child(
Switch::new("layout")
.checked(self.layout.is_horizontal())
.label("Horizontal")
.on_click(cx.listener(|this, checked: &bool, _, cx| {
if *checked {
this.layout = Axis::Horizontal;
} else {
this.layout = Axis::Vertical;
}
cx.notify();
})),
)
.child(
ButtonGroup::new("size")
.outline()
.small()
.child(
Button::new("large")
.selected(self.size == Size::Large)
.child("Large"),
)
.child(
Button::new("medium")
.child("Medium")
.selected(self.size == Size::Medium),
)
.child(
Button::new("small")
.child("Small")
.selected(self.size == Size::Small),
)
.on_click(cx.listener(|this, selecteds: &Vec<usize>, _, cx| {
if selecteds.contains(&0) {
this.size = Size::Large;
} else if selecteds.contains(&1) {
this.size = Size::Medium;
} else if selecteds.contains(&2) {
this.size = Size::Small;
}
cx.notify();
})),
),
)
.child(Divider::horizontal())
.child(
v_form()
.layout(self.layout)
.with_size(self.size)
.child(
form_field().label_fn(|_, _| "Name").child(
h_flex()
.gap_2()
.border_1()
.border_color(cx.theme().border)
.rounded(cx.theme().radius)
.child(
div().w(px(90.)).child(
Dropdown::new(&self.name_prefix_state)
.pr_0()
.appearance(false),
),
)
.child(div().flex_1().child(
TextInput::new(&self.name_input).pl_0().appearance(false),
)),
),
)
.child(
form_field()
.label("Email")
.child(TextInput::new(&self.email_input))
.required(true),
)
.child(
form_field()
.label("Bio")
.when(self.layout.is_vertical(), |this| this.items_start())
.child(TextInput::new(&self.bio_input))
.description_fn(|_, _| {
div().child("Use at most 100 words to describe yourself.")
}),
)
.child(
form_field()
.no_label_indent()
.child("This is a full width form field."),
)
.child(
form_field()
.label("Please select your birthday")
.child(DatePicker::new(&self.date))
.description("Select your birthday, we will send you a gift."),
)
.child(
form_field().child(
Switch::new("subscribe-newsletter")
.label("Subscribe our newsletter")
.checked(self.subscribe_email)
.on_click(cx.listener(|this, checked: &bool, _, cx| {
this.subscribe_email = *checked;
cx.notify();
})),
),
)
.child(
form_field().child(
ColorPicker::new(&self.color_state)
.small()
.label("Theme color"),
),
)
.child(
form_field().child(
Checkbox::new("use-vertical-layout")
.label("Vertical layout")
.checked(self.layout.is_vertical())
.on_click(cx.listener(|this, checked: &bool, _, cx| {
this.layout = if *checked {
Axis::Vertical
} else {
Axis::Horizontal
};
cx.notify();
})),
),
),
)
}
}