From 44829a05e4477432de01091a1659062984c47e2f Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Fri, 21 Nov 2025 18:48:29 +0800 Subject: [PATCH] setting: Add SettingFieldElement trait. (#1657) Co-authored-by: Sunli --- crates/story/src/settings_story.rs | 117 ++++++++++++++---------- crates/ui/src/setting/fields/element.rs | 77 ++++++++++++++-- crates/ui/src/setting/fields/mod.rs | 37 +++++--- crates/ui/src/setting/group.rs | 2 +- crates/ui/src/setting/item.rs | 10 +- docs/docs/components/settings.md | 56 +++++++++++- 6 files changed, 220 insertions(+), 79 deletions(-) diff --git a/crates/story/src/settings_story.rs b/crates/story/src/settings_story.rs index a55257cb..c84058ac 100644 --- a/crates/story/src/settings_story.rs +++ b/crates/story/src/settings_story.rs @@ -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, url: impl Into) -> 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."), + ])), ] } } diff --git a/crates/ui/src/setting/fields/element.rs b/crates/ui/src/setting/fields/element.rs index 5df18e1d..77359d6e 100644 --- a/crates/ui/src/setting/fields/element.rs +++ b/crates/ui/src/setting/fields/element.rs @@ -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 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(pub(crate) T); +impl SettingFieldElement for AnySettingFieldElement +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> { + 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 AnyElement>, + element_render: Rc>, } impl ElementField { - pub(crate) fn new( - element_render: Rc AnyElement + 'static>, - ) -> Self { - Self { element_render } + pub(crate) fn new(element_render: E) -> Self + where + E: SettingFieldElement + '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) } } diff --git a/crates/ui/src/setting/fields/mod.rs b/crates/ui/src/setting/fields/mod.rs index 4852a371..ea56cb1b 100644 --- a/crates/ui/src/setting/fields/mod.rs +++ b/crates/ui/src/setting/fields/mod.rs @@ -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 AnyElement>, + element: Rc>, }, } @@ -108,11 +109,9 @@ impl SettingFieldType { } #[inline] - pub(super) fn element_render( - &self, - ) -> Rc AnyElement + 'static> { + pub(super) fn element(&self) -> Rc> { 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 { Self::new(SettingFieldType::Dropdown { options }, value, set_value) } - /// Create a new setting field with the given element render function. - pub fn element(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(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(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 { diff --git a/crates/ui/src/setting/group.rs b/crates/ui/src/setting/group.rs index 33fdc634..a2511058 100644 --- a/crates/ui/src/setting/group.rs +++ b/crates/ui/src/setting/group.rs @@ -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 } diff --git a/crates/ui/src/setting/item.rs b/crates/ui/src/setting/item.rs index a977f569..5623aa77 100644 --- a/crates/ui/src/setting/item.rs +++ b/crates/ui/src/setting/item.rs @@ -44,8 +44,8 @@ impl SettingItem { } } - /// Create a new custom element setting item. - pub fn element(render: R) -> Self + /// Create a new custom element setting item with a render closure. + pub fn render(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::() && field_type.is_dropdown() => { Box::new(DropdownField::::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, diff --git a/docs/docs/components/settings.md b/docs/docs/components/settings.md index 67d3c912..d8279d91 100644 --- a/docs/docs/components/settings.md +++ b/docs/docs/components/settings.md @@ -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 +````