setting: Add SettingFieldElement trait. (#1657)

Co-authored-by: Sunli <scott_s829@163.com>
This commit is contained in:
Jason Lee 2025-11-21 18:48:29 +08:00 committed by GitHub
parent ffeeee5d12
commit 44829a05e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 220 additions and 79 deletions

View file

@ -9,7 +9,10 @@ use gpui_component::{
group_box::GroupBoxVariant,
h_flex,
label::Label,
setting::{NumberFieldOptions, SettingField, SettingGroup, SettingItem, SettingPage, Settings},
setting::{
NumberFieldOptions, RenderOptions, SettingField, SettingFieldElement, SettingGroup,
SettingItem, SettingPage, Settings,
},
text::TextView,
v_flex,
};
@ -56,6 +59,34 @@ pub struct SettingsStory {
size: Size,
}
struct OpenURLSettingField {
label: SharedString,
url: SharedString,
}
impl OpenURLSettingField {
fn new(label: impl Into<SharedString>, url: impl Into<SharedString>) -> Self {
Self {
label: label.into(),
url: url.into(),
}
}
}
impl SettingFieldElement for OpenURLSettingField {
type Element = Button;
fn render_field(&self, options: &RenderOptions, _: &mut Window, _: &mut App) -> Self::Element {
let url = self.url.clone();
Button::new("open-url")
.outline()
.label(self.label.clone())
.with_size(options.size)
.on_click(move |_, _window, cx| {
cx.open_url(url.as_str());
})
}
}
impl super::Story for SettingsStory {
fn title() -> &'static str {
"Settings"
@ -232,7 +263,7 @@ impl SettingsStory {
.description("Adjust the font size for better readability."),
),
SettingGroup::new().title("Other").items(vec![
SettingItem::element(|options, _, _| {
SettingItem::render(|options, _, _| {
h_flex()
.w_full()
.justify_between()
@ -301,7 +332,7 @@ impl SettingsStory {
SettingPage::new("About")
.resettable(resettable)
.group(
SettingGroup::new().item(SettingItem::element(|_options, _, cx| {
SettingGroup::new().item(SettingItem::render(|_options, _, cx| {
v_flex()
.gap_3()
.w_full()
@ -321,51 +352,41 @@ impl SettingsStory {
})),
)
.group(SettingGroup::new().title("Links").items(vec![
SettingItem::new(
"GitHub Repository",
SettingField::element(|options, _window, _cx| {
Button::new("open-url")
.outline()
.label("Repository...")
.with_size(options.size)
.on_click(|_, _window, cx| {
cx.open_url("https://github.com/longbridge/gpui-component");
})
}),
)
.description("Open the GitHub repository in your default browser."),
SettingItem::new(
"Documentation",
SettingField::element(|options, _window, _cx| {
Button::new("open-url")
.outline()
.label("Rust Docs...")
.with_size(options.size)
.on_click(|_, _window, cx| {
cx.open_url("https://docs.rs/gpui-component");
})
}),
)
.description(TextView::markdown(
"desc",
"Rust doc for the `gpui-component` crate.",
window,
cx,
)),
SettingItem::new(
"Website",
SettingField::element(|options, _window, _cx| {
Button::new("open-url")
.outline()
.label("Website...")
.with_size(options.size)
.on_click(|_, _window, cx| {
cx.open_url("https://longbridge.github.io/gpui-component/");
})
}),
)
.description("Official website and documentation for the GPUI Component."),
])),
SettingItem::new(
"GitHub Repository",
SettingField::element(OpenURLSettingField::new(
"Repository...",
"https://github.com/longbridge/gpui-component",
)),
)
.description("Open the GitHub repository in your default browser."),
SettingItem::new(
"Documentation",
SettingField::element(OpenURLSettingField::new(
"Rust Docs...",
"https://docs.rs/gpui-component"
)),
)
.description(TextView::markdown(
"desc",
"Rust doc for the `gpui-component` crate.",
window,
cx,
)),
SettingItem::new(
"Website",
SettingField::render(|options, _window, _cx| {
Button::new("open-url")
.outline()
.label("Website...")
.with_size(options.size)
.on_click(|_, _window, cx| {
cx.open_url("https://longbridge.github.io/gpui-component/");
})
}),
)
.description("Official website and documentation for the GPUI Component."),
])),
]
}
}

View file

@ -1,17 +1,80 @@
use gpui::{AnyElement, App, StyleRefinement, Window};
use gpui::{AnyElement, App, IntoElement, StyleRefinement, Window};
use std::rc::Rc;
use crate::setting::{fields::SettingFieldRender, AnySettingField, RenderOptions};
/// A trait for rendering custom setting field elements.
///
/// For [`crate::setting::SettingField::element`] method.
pub trait SettingFieldElement {
type Element: IntoElement + 'static;
fn render_field(
&self,
options: &RenderOptions,
window: &mut Window,
cx: &mut App,
) -> Self::Element;
}
impl<F, E> SettingFieldElement for F
where
E: IntoElement + 'static,
F: Fn(&RenderOptions, &mut Window, &mut App) -> E,
{
type Element = E;
fn render_field(
&self,
options: &RenderOptions,
window: &mut Window,
cx: &mut App,
) -> Self::Element {
(self)(options, window, cx)
}
}
pub(crate) struct AnySettingFieldElement<T>(pub(crate) T);
impl<T> SettingFieldElement for AnySettingFieldElement<T>
where
T: SettingFieldElement,
{
type Element = AnyElement;
fn render_field(
&self,
options: &RenderOptions,
window: &mut Window,
cx: &mut App,
) -> Self::Element {
self.0.render_field(options, window, cx).into_any_element()
}
}
impl SettingFieldElement for Rc<dyn SettingFieldElement<Element = AnyElement>> {
type Element = AnyElement;
fn render_field(
&self,
options: &RenderOptions,
window: &mut Window,
cx: &mut App,
) -> Self::Element {
self.as_ref().render_field(options, window, cx)
}
}
pub(crate) struct ElementField {
element_render: Rc<dyn Fn(&RenderOptions, &mut Window, &mut App) -> AnyElement>,
element_render: Rc<dyn SettingFieldElement<Element = AnyElement>>,
}
impl ElementField {
pub(crate) fn new(
element_render: Rc<dyn Fn(&RenderOptions, &mut Window, &mut App) -> AnyElement + 'static>,
) -> Self {
Self { element_render }
pub(crate) fn new<E>(element_render: E) -> Self
where
E: SettingFieldElement<Element = AnyElement> + 'static,
{
Self {
element_render: Rc::new(element_render),
}
}
}
@ -24,6 +87,6 @@ impl SettingFieldRender for ElementField {
window: &mut Window,
cx: &mut App,
) -> AnyElement {
(self.element_render)(options, window, cx)
(self.element_render).render_field(options, window, cx)
}
}

View file

@ -10,6 +10,7 @@ pub(crate) use element::*;
pub(crate) use number::*;
pub(crate) use string::*;
pub use element::SettingFieldElement;
pub use number::NumberFieldOptions;
use gpui::{AnyElement, App, IntoElement, SharedString, StyleRefinement, Styled, Window};
@ -61,7 +62,7 @@ pub enum SettingFieldType {
options: Vec<(SharedString, SharedString)>,
},
Element {
element_render: Rc<dyn Fn(&RenderOptions, &mut Window, &mut App) -> AnyElement>,
element: Rc<dyn SettingFieldElement<Element = AnyElement>>,
},
}
@ -108,11 +109,9 @@ impl SettingFieldType {
}
#[inline]
pub(super) fn element_render(
&self,
) -> Rc<dyn Fn(&RenderOptions, &mut Window, &mut App) -> AnyElement + 'static> {
pub(super) fn element(&self) -> Rc<dyn SettingFieldElement<Element = AnyElement>> {
match self {
SettingFieldType::Element { element_render } => element_render.clone(),
SettingFieldType::Element { element } => element.clone(),
_ => unreachable!("element_render called on non-element field"),
}
}
@ -172,22 +171,36 @@ impl SettingField<SharedString> {
Self::new(SettingFieldType::Dropdown { options }, value, set_value)
}
/// Create a new setting field with the given element render function.
pub fn element<R, E>(element_render: R) -> Self
/// Create a new setting field with the given custom element that implements [`SettingFieldElement`] trait.
///
/// See also [`SettingField::render`] for simply building with a render closure.
pub fn element<E>(element: E) -> Self
where
E: IntoElement,
R: Fn(&RenderOptions, &mut Window, &mut App) -> E + 'static,
E: SettingFieldElement + 'static,
{
Self::new(
SettingFieldType::Element {
element_render: Rc::new(move |options, window, cx| {
element_render(options, window, cx).into_any_element()
}),
element: Rc::new(AnySettingFieldElement(element)),
},
|_| SharedString::default(),
|_, _| {},
)
}
/// Create a new setting field with the given element render closure.
///
/// See also [`SettingField::element`] for building with a custom field for more complex scenarios.
pub fn render<E, R>(element_render: R) -> Self
where
E: IntoElement + 'static,
R: Fn(&RenderOptions, &mut Window, &mut App) -> E + 'static,
{
Self::element(
move |options: &RenderOptions, window: &mut Window, cx: &mut App| {
(element_render)(options, window, cx).into_any_element()
},
)
}
}
impl SettingField<f64> {

View file

@ -99,7 +99,7 @@ impl SettingGroup {
.gap_4()
.children(self.items.iter().enumerate().filter_map(|(item_ix, item)| {
if item.is_match(&query) {
Some(item.clone().render(item_ix, options, window, cx))
Some(item.clone().render_item(item_ix, options, window, cx))
} else {
None
}

View file

@ -44,8 +44,8 @@ impl SettingItem {
}
}
/// Create a new custom element setting item.
pub fn element<R, E>(render: R) -> Self
/// Create a new custom element setting item with a render closure.
pub fn render<R, E>(render: R) -> Self
where
E: IntoElement,
R: Fn(&RenderOptions, &mut Window, &mut App) -> E + 'static,
@ -140,16 +140,14 @@ impl SettingItem {
t if t == TypeId::of::<String>() && field_type.is_dropdown() => {
Box::new(DropdownField::<String>::new(field_type.dropdown_options()))
}
_ if field_type.is_element() => {
Box::new(ElementField::new(field_type.element_render()))
}
_ if field_type.is_element() => Box::new(ElementField::new(field_type.element())),
_ => unimplemented!("Unsupported setting type: {}", field.deref().type_name()),
};
renderer.render(field, &options, &style, window, cx)
}
pub(super) fn render(
pub(super) fn render_item(
self,
ix: usize,
options: &RenderOptions,

View file

@ -174,12 +174,12 @@ SettingItem::new("Title", SettingField::switch(...))
.description("Description text")
```
### Custom Element Item
### Custom Item with a render closure
You can create a fully custom setting item using `SettingItem::element`:
You can create a fully custom setting item using `SettingItem::render`:
```rust
SettingItem::element(|options, _, _| {
SettingItem::render(|options, _, _| {
h_flex()
.w_full()
.justify_between()
@ -324,12 +324,14 @@ SettingItem::new(
)
```
### Custom Element Field
### Custom Field by Render Closure
The `SettingField::render` method allows you to create a custom field using a closure that returns an element.
```rust
SettingItem::new(
"GitHub Repository",
SettingField::element(|options, _window, _cx| {
SettingField::render(|options, _window, _cx| {
Button::new("open-url")
.outline()
.label("Repository...")
@ -341,6 +343,48 @@ SettingItem::new(
)
```
### Custom Field Element
You may have a complex field that you want to reuse, you may want split the element into a separate struct to do the complex logic.
In this case, the [SettingFieldElement] trait can help you to create a custom field element.
````rust
use gpui_component::setting::{SettingFieldElement, RenderOptions};
struct OpenURLSettingField {
label: SharedString,
url: SharedString,
}
impl SettingFieldElement for OpenURLSettingField {
type Element = Button;
fn render_field(&self, options: &RenderOptions, _: &mut Window, _: &mut App) -> Self::Element {
let url = self.url.clone();
Button::new("open-url")
.outline()
.label(self.label.clone())
.with_size(options.size)
.on_click(move |_, _window, cx| {
cx.open_url(url.as_str());
})
}
}
```
Then use it in the setting item:
```rust
SettingItem::new(
"GitHub Repository",
SettingField::element(OpenURLSettingField {
label: "Repository...".into(),
url: "https://github.com/longbridge/gpui-component".into(),
})
)
```
## API Reference
- [Settings]
@ -453,6 +497,8 @@ Settings::new("app-settings")
[SettingGroup]: https://docs.rs/gpui-component/latest/gpui_component/setting/struct.SettingGroup.html
[SettingItem]: https://docs.rs/gpui-component/latest/gpui_component/setting/struct.SettingItem.html
[SettingField]: https://docs.rs/gpui-component/latest/gpui_component/setting/enum.SettingField.html
[SettingFieldElement]: https://docs.rs/gpui-component/latest/gpui_component/setting/trait.SettingFieldElement.html
[NumberFieldOptions]: https://docs.rs/gpui-component/latest/gpui_component/setting/struct.NumberFieldOptions.html
[GroupBox]: ./group-box.md
[Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html
````