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, group_box::GroupBoxVariant,
h_flex, h_flex,
label::Label, label::Label,
setting::{NumberFieldOptions, SettingField, SettingGroup, SettingItem, SettingPage, Settings}, setting::{
NumberFieldOptions, RenderOptions, SettingField, SettingFieldElement, SettingGroup,
SettingItem, SettingPage, Settings,
},
text::TextView, text::TextView,
v_flex, v_flex,
}; };
@ -56,6 +59,34 @@ pub struct SettingsStory {
size: Size, 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 { impl super::Story for SettingsStory {
fn title() -> &'static str { fn title() -> &'static str {
"Settings" "Settings"
@ -232,7 +263,7 @@ impl SettingsStory {
.description("Adjust the font size for better readability."), .description("Adjust the font size for better readability."),
), ),
SettingGroup::new().title("Other").items(vec![ SettingGroup::new().title("Other").items(vec![
SettingItem::element(|options, _, _| { SettingItem::render(|options, _, _| {
h_flex() h_flex()
.w_full() .w_full()
.justify_between() .justify_between()
@ -301,7 +332,7 @@ impl SettingsStory {
SettingPage::new("About") SettingPage::new("About")
.resettable(resettable) .resettable(resettable)
.group( .group(
SettingGroup::new().item(SettingItem::element(|_options, _, cx| { SettingGroup::new().item(SettingItem::render(|_options, _, cx| {
v_flex() v_flex()
.gap_3() .gap_3()
.w_full() .w_full()
@ -323,28 +354,18 @@ impl SettingsStory {
.group(SettingGroup::new().title("Links").items(vec![ .group(SettingGroup::new().title("Links").items(vec![
SettingItem::new( SettingItem::new(
"GitHub Repository", "GitHub Repository",
SettingField::element(|options, _window, _cx| { SettingField::element(OpenURLSettingField::new(
Button::new("open-url") "Repository...",
.outline() "https://github.com/longbridge/gpui-component",
.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."), .description("Open the GitHub repository in your default browser."),
SettingItem::new( SettingItem::new(
"Documentation", "Documentation",
SettingField::element(|options, _window, _cx| { SettingField::element(OpenURLSettingField::new(
Button::new("open-url") "Rust Docs...",
.outline() "https://docs.rs/gpui-component"
.label("Rust Docs...") )),
.with_size(options.size)
.on_click(|_, _window, cx| {
cx.open_url("https://docs.rs/gpui-component");
})
}),
) )
.description(TextView::markdown( .description(TextView::markdown(
"desc", "desc",
@ -354,7 +375,7 @@ impl SettingsStory {
)), )),
SettingItem::new( SettingItem::new(
"Website", "Website",
SettingField::element(|options, _window, _cx| { SettingField::render(|options, _window, _cx| {
Button::new("open-url") Button::new("open-url")
.outline() .outline()
.label("Website...") .label("Website...")

View file

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

View file

@ -99,7 +99,7 @@ impl SettingGroup {
.gap_4() .gap_4()
.children(self.items.iter().enumerate().filter_map(|(item_ix, item)| { .children(self.items.iter().enumerate().filter_map(|(item_ix, item)| {
if item.is_match(&query) { 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 { } else {
None None
} }

View file

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

View file

@ -174,12 +174,12 @@ SettingItem::new("Title", SettingField::switch(...))
.description("Description text") .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 ```rust
SettingItem::element(|options, _, _| { SettingItem::render(|options, _, _| {
h_flex() h_flex()
.w_full() .w_full()
.justify_between() .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 ```rust
SettingItem::new( SettingItem::new(
"GitHub Repository", "GitHub Repository",
SettingField::element(|options, _window, _cx| { SettingField::render(|options, _window, _cx| {
Button::new("open-url") Button::new("open-url")
.outline() .outline()
.label("Repository...") .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 ## API Reference
- [Settings] - [Settings]
@ -453,6 +497,8 @@ Settings::new("app-settings")
[SettingGroup]: https://docs.rs/gpui-component/latest/gpui_component/setting/struct.SettingGroup.html [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 [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 [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 [NumberFieldOptions]: https://docs.rs/gpui-component/latest/gpui_component/setting/struct.NumberFieldOptions.html
[GroupBox]: ./group-box.md [GroupBox]: ./group-box.md
[Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html
````