setting: Add Settings component. (#1632)

https://github.com/user-attachments/assets/3c6315d9-1183-419c-bb17-a8f7ac0cf25c
This commit is contained in:
Jason Lee 2025-11-20 21:51:44 +08:00 committed by GitHub
parent 91de551ab0
commit c8652f9010
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 2517 additions and 60 deletions

View file

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-redo2-icon lucide-redo-2"
><path d="m15 14 5-5-5-5" /><path
d="M20 9H9.5A5.5 5.5 0 0 0 4 14.5A5.5 5.5 0 0 0 9.5 20H13"
/></svg>

After

Width:  |  Height:  |  Size: 382 B

View file

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-redo-icon lucide-redo"
><path d="M21 7v6h-6" /><path
d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3l3 2.7"
/></svg>

After

Width:  |  Height:  |  Size: 362 B

View file

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-undo2-icon lucide-undo-2"
><path d="M9 14 4 9l5-5" /><path
d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11"
/></svg>

After

Width:  |  Height:  |  Size: 383 B

View file

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-undo-icon lucide-undo"
><path d="M3 7v6h6" /><path
d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"
/></svg>

After

Width:  |  Height:  |  Size: 360 B

View file

@ -5,7 +5,7 @@ use gpui_component::{
ActiveTheme, Colorize as _, IconName, Sizable, ActiveTheme, Colorize as _, IconName, Sizable,
button::Button, button::Button,
checkbox::Checkbox, checkbox::Checkbox,
group_box::GroupBox, group_box::{GroupBox, GroupBoxVariants as _},
h_flex, h_flex,
slider::{Slider, SliderState}, slider::{Slider, SliderState},
v_flex, v_flex,

View file

@ -4,7 +4,7 @@ use gpui::{
Styled, Window, prelude::FluentBuilder as _, Styled, Window, prelude::FluentBuilder as _,
}; };
use gpui_component::group_box::GroupBox; use gpui_component::group_box::{GroupBox, GroupBoxVariants as _};
use gpui_component::label::Label; use gpui_component::label::Label;
use gpui_component::tag::Tag; use gpui_component::tag::Tag;
use gpui_component::{ActiveTheme, IconName, StyledExt, h_flex}; use gpui_component::{ActiveTheme, IconName, StyledExt, h_flex};

View file

@ -1,17 +1,18 @@
use gpui::{ use gpui::{
relative, App, AppContext, Context, Entity, Focusable, IntoElement, ParentElement, Render, App, AppContext, Context, Entity, Focusable, IntoElement, ParentElement, Render,
StyleRefinement, Styled, Window, StyleRefinement, Styled, Window, relative,
}; };
use gpui_component::{ use gpui_component::{
ActiveTheme as _, StyledExt,
button::{Button, ButtonVariants}, button::{Button, ButtonVariants},
checkbox::Checkbox, checkbox::Checkbox,
group_box::GroupBox, group_box::{GroupBox, GroupBoxVariants as _},
h_flex, h_flex,
radio::{Radio, RadioGroup}, radio::{Radio, RadioGroup},
switch::Switch, switch::Switch,
text::TextView, text::TextView,
v_flex, ActiveTheme as _, StyledExt, v_flex,
}; };
use crate::section; use crate::section;

View file

@ -32,6 +32,7 @@ mod radio_story;
mod resizable_story; mod resizable_story;
mod scrollable_story; mod scrollable_story;
mod select_story; mod select_story;
mod settings_story;
mod sheet_story; mod sheet_story;
mod sidebar_story; mod sidebar_story;
mod skeleton_story; mod skeleton_story;
@ -93,6 +94,7 @@ pub use resizable_story::ResizableStory;
pub use scrollable_story::ScrollableStory; pub use scrollable_story::ScrollableStory;
pub use select_story::SelectStory; pub use select_story::SelectStory;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub use settings_story::SettingsStory;
pub use sheet_story::SheetStory; pub use sheet_story::SheetStory;
pub use sidebar_story::SidebarStory; pub use sidebar_story::SidebarStory;
pub use skeleton_story::SkeletonStory; pub use skeleton_story::SkeletonStory;
@ -115,7 +117,7 @@ use gpui_component::{
ActiveTheme, IconName, Root, TitleBar, WindowExt, ActiveTheme, IconName, Root, TitleBar, WindowExt,
button::Button, button::Button,
dock::{Panel, PanelControl, PanelEvent, PanelInfo, PanelState, TitleStyle, register_panel}, dock::{Panel, PanelControl, PanelEvent, PanelInfo, PanelState, TitleStyle, register_panel},
group_box::GroupBox, group_box::{GroupBox, GroupBoxVariants as _},
h_flex, h_flex,
menu::PopupMenu, menu::PopupMenu,
notification::Notification, notification::Notification,

View file

@ -70,6 +70,7 @@ impl Gallery {
StoryContainer::panel::<ResizableStory>(window, cx), StoryContainer::panel::<ResizableStory>(window, cx),
StoryContainer::panel::<ScrollableStory>(window, cx), StoryContainer::panel::<ScrollableStory>(window, cx),
StoryContainer::panel::<SelectStory>(window, cx), StoryContainer::panel::<SelectStory>(window, cx),
StoryContainer::panel::<SettingsStory>(window, cx),
StoryContainer::panel::<SheetStory>(window, cx), StoryContainer::panel::<SheetStory>(window, cx),
StoryContainer::panel::<SidebarStory>(window, cx), StoryContainer::panel::<SidebarStory>(window, cx),
StoryContainer::panel::<SkeletonStory>(window, cx), StoryContainer::panel::<SkeletonStory>(window, cx),

View file

@ -0,0 +1,386 @@
use gpui::{
App, AppContext, Axis, Context, Element, Entity, FocusHandle, Focusable, Global, IntoElement,
ParentElement as _, Render, SharedString, Styled, Window,
};
use gpui_component::{
ActiveTheme, Icon, IconName, Sizable, Size, Theme, ThemeMode,
button::Button,
group_box::GroupBoxVariant,
h_flex,
label::Label,
setting::{NumberFieldOptions, SettingField, SettingGroup, SettingItem, SettingPage, Settings},
text::TextView,
v_flex,
};
struct AppSettings {
auto_switch_theme: bool,
cli_path: SharedString,
font_family: SharedString,
font_size: f64,
notifications_enabled: bool,
auto_update: bool,
resettable: bool,
}
impl Default for AppSettings {
fn default() -> Self {
Self {
auto_switch_theme: false,
cli_path: "/usr/local/bin/bash".into(),
font_family: "Arial".into(),
font_size: 14.0,
notifications_enabled: true,
auto_update: true,
resettable: true,
}
}
}
impl Global for AppSettings {}
impl AppSettings {
fn global(cx: &App) -> &AppSettings {
cx.global::<AppSettings>()
}
pub fn global_mut(cx: &mut App) -> &mut AppSettings {
cx.global_mut::<AppSettings>()
}
}
pub struct SettingsStory {
focus_handle: FocusHandle,
group_variant: GroupBoxVariant,
size: Size,
}
impl super::Story for SettingsStory {
fn title() -> &'static str {
"Settings"
}
fn description() -> &'static str {
"A collection of settings groups and items for the application."
}
fn new_view(window: &mut Window, cx: &mut App) -> Entity<impl Render> {
Self::view(window, cx)
}
}
impl SettingsStory {
pub fn view(window: &mut Window, cx: &mut App) -> Entity<Self> {
cx.new(|cx| Self::new(window, cx))
}
fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
cx.set_global::<AppSettings>(AppSettings::default());
Self {
focus_handle: cx.focus_handle(),
group_variant: GroupBoxVariant::Outline,
size: Size::default(),
}
}
fn setting_pages(&self, window: &mut Window, cx: &mut Context<Self>) -> Vec<SettingPage> {
let view = cx.entity();
let default_settings = AppSettings::default();
let resettable = AppSettings::global(cx).resettable;
vec![
SettingPage::new("General")
.resettable(resettable)
.default_open(true)
.groups(vec![
SettingGroup::new().title("Appearance").items(vec![
SettingItem::new(
"Dark Mode",
SettingField::switch(
|cx: &App| cx.theme().mode.is_dark(),
|val: bool, cx: &mut App| {
let mode = if val {
ThemeMode::Dark
} else {
ThemeMode::Light
};
Theme::global_mut(cx).mode = mode;
Theme::change(mode, None, cx);
},
)
.default_value(false),
)
.description("Switch between light and dark themes."),
SettingItem::new(
"Auto Switch Theme",
SettingField::checkbox(
|cx: &App| AppSettings::global(cx).auto_switch_theme,
|val: bool, cx: &mut App| {
AppSettings::global_mut(cx).auto_switch_theme = val;
},
)
.default_value(default_settings.auto_switch_theme),
)
.description("Automatically switch theme based on system settings."),
SettingItem::new(
"resettable",
SettingField::switch(
|cx: &App| AppSettings::global(cx).resettable,
|checked: bool, cx: &mut App| {
AppSettings::global_mut(cx).resettable = checked
},
),
)
.description("Enable/Disable reset button for settings."),
SettingItem::new(
"Group Variant",
SettingField::dropdown(
vec![
(GroupBoxVariant::Normal.as_str().into(), "Normal".into()),
(GroupBoxVariant::Outline.as_str().into(), "Outline".into()),
(GroupBoxVariant::Fill.as_str().into(), "Fill".into()),
],
{
let view = view.clone();
move |cx: &App| {
SharedString::from(
view.read(cx).group_variant.as_str().to_string(),
)
}
},
{
let view = view.clone();
move |val: SharedString, cx: &mut App| {
view.update(cx, |view, cx| {
view.group_variant =
GroupBoxVariant::from_str(val.as_str());
cx.notify();
});
}
},
)
.default_value(GroupBoxVariant::Outline.as_str().to_string()),
)
.description("Select the variant for setting groups."),
SettingItem::new(
"Group Size",
SettingField::dropdown(
vec![
(Size::Medium.as_str().into(), "Medium".into()),
(Size::Small.as_str().into(), "Small".into()),
(Size::XSmall.as_str().into(), "XSmall".into()),
],
{
let view = view.clone();
move |cx: &App| {
SharedString::from(view.read(cx).size.as_str().to_string())
}
},
{
let view = view.clone();
move |val: SharedString, cx: &mut App| {
view.update(cx, |view, cx| {
view.size = Size::from_str(val.as_str());
cx.notify();
});
}
},
)
.default_value(Size::default().as_str().to_string()),
)
.description("Select the size for the setting group."),
]),
SettingGroup::new()
.title("Font")
.item(
SettingItem::new(
"Font Family",
SettingField::dropdown(
vec![
("Arial".into(), "Arial".into()),
("Helvetica".into(), "Helvetica".into()),
("Times New Roman".into(), "Times New Roman".into()),
("Courier New".into(), "Courier New".into()),
],
|cx: &App| AppSettings::global(cx).font_family.clone(),
|val: SharedString, cx: &mut App| {
AppSettings::global_mut(cx).font_family = val;
},
)
.default_value(default_settings.font_family),
)
.description("Select the font family for the story."),
)
.item(
SettingItem::new(
"Font Size",
SettingField::number_input(
NumberFieldOptions {
min: 8.0,
max: 72.0,
..Default::default()
},
|cx: &App| AppSettings::global(cx).font_size,
|val: f64, cx: &mut App| {
AppSettings::global_mut(cx).font_size = val;
},
)
.default_value(default_settings.font_size),
)
.description("Adjust the font size for better readability."),
),
SettingGroup::new().title("Other").items(vec![
SettingItem::element(|options, _, _| {
h_flex()
.w_full()
.justify_between()
.flex_wrap()
.gap_3()
.child("This is a custom element item by use SettingItem::element.")
.child(
Button::new("action")
.icon(IconName::Globe)
.label("Repository...")
.outline()
.with_size(options.size)
.on_click(|_, _, cx| {
cx.open_url(
"https://github.com/longbridge/gpui-component",
);
}),
)
.into_any_element()
}),
SettingItem::new(
"CLI Path",
SettingField::input(
|cx: &App| AppSettings::global(cx).cli_path.clone(),
|val: SharedString, cx: &mut App| {
println!("cli-path set value: {}", val);
AppSettings::global_mut(cx).cli_path = val;
},
)
.default_value(default_settings.cli_path),
)
.layout(Axis::Vertical)
.description(
"Path to the CLI executable. \n\
This item uses Vertical layout. The title,\
description, and field are all aligned vertically with width 100%.",
),
]),
]),
SettingPage::new("Software Update")
.resettable(resettable)
.groups(vec![SettingGroup::new().title("Updates").items(vec![
SettingItem::new(
"Enable Notifications",
SettingField::switch(
|cx: &App| AppSettings::global(cx).notifications_enabled,
|val: bool, cx: &mut App| {
AppSettings::global_mut(cx).notifications_enabled = val;
},
)
.default_value(default_settings.notifications_enabled),
)
.description("Receive notifications about updates and news."),
SettingItem::new(
"Auto Update",
SettingField::switch(
|cx: &App| AppSettings::global(cx).auto_update,
|val: bool, cx: &mut App| {
AppSettings::global_mut(cx).auto_update = val;
},
)
.default_value(default_settings.auto_update),
)
.description("Automatically download and install updates."),
])]),
SettingPage::new("About")
.resettable(resettable)
.group(
SettingGroup::new().item(SettingItem::element(|_options, _, cx| {
v_flex()
.gap_3()
.w_full()
.items_center()
.justify_center()
.child(Icon::new(IconName::GalleryVerticalEnd).size_16())
.child("GPUI Component")
.child(
Label::new(
"Rust GUI components for building fantastic cross-platform \
desktop application by using GPUI.",
)
.text_sm()
.text_color(cx.theme().muted_foreground),
)
.into_any()
})),
)
.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."),
])),
]
}
}
impl Focusable for SettingsStory {
fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl Render for SettingsStory {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
Settings::new("app-settings")
.with_size(self.size)
.with_group_variant(self.group_variant)
.pages(self.setting_pages(window, cx))
}
}

View file

@ -188,3 +188,14 @@ Input:
en: Show Code Actions en: Show Code Actions
zh-CN: 显示代码操作 zh-CN: 显示代码操作
zh-HK: 顯示代碼操作 zh-HK: 顯示代碼操作
Settings:
search_placeholder:
en: Search...
zh-CN: 搜索...
zh-HK: 搜索...
it: Ricerca...
Reset All:
en: Reset All
zh-CN: 重置全部
zh-HK: 重置全部
it: Resetta Tutto

View file

@ -15,6 +15,47 @@ pub enum GroupBoxVariant {
Outline, Outline,
} }
/// Trait to add GroupBox variant methods to elements.
pub trait GroupBoxVariants: Sized {
/// Set the variant of the [`GroupBox`].
fn with_variant(self, variant: GroupBoxVariant) -> Self;
/// Set to use [`GroupBoxVariant::Normal`] to GroupBox.
fn normal(mut self) -> Self {
self = self.with_variant(GroupBoxVariant::Normal);
self
}
/// Set to use [`GroupBoxVariant::Fill`] to GroupBox.
fn fill(mut self) -> Self {
self = self.with_variant(GroupBoxVariant::Fill);
self
}
/// Set to use [`GroupBoxVariant::Outline`] to GroupBox.
fn outline(mut self) -> Self {
self = self.with_variant(GroupBoxVariant::Outline);
self
}
}
impl GroupBoxVariant {
/// Create a GroupBoxVariant from a string.
pub fn from_str(s: &str) -> Self {
match s.to_lowercase().as_str() {
"fill" => GroupBoxVariant::Fill,
"outline" => GroupBoxVariant::Outline,
_ => GroupBoxVariant::Normal,
}
}
/// Convert the GroupBoxVariant to a string.
pub fn as_str(&self) -> &str {
match self {
GroupBoxVariant::Normal => "normal",
GroupBoxVariant::Fill => "fill",
GroupBoxVariant::Outline => "outline",
}
}
}
/// GroupBox is a styled container element that with /// GroupBox is a styled container element that with
/// an optional title to groups related content together. /// an optional title to groups related content together.
#[derive(IntoElement)] #[derive(IntoElement)]
@ -42,26 +83,6 @@ impl GroupBox {
} }
} }
/// Set the variant of the group box.
pub fn with_variant(mut self, variant: GroupBoxVariant) -> Self {
self.variant = variant;
self
}
/// Set to use Fill variant.
pub fn fill(mut self) -> Self {
self.variant = GroupBoxVariant::Fill;
self
}
/// Set use outline style of the group box.
///
/// If true, the group box will have a border around it, and no background color.
pub fn outline(mut self) -> Self {
self.variant = GroupBoxVariant::Outline;
self
}
/// Set the id of the group box, default is None. /// Set the id of the group box, default is None.
pub fn id(mut self, id: impl Into<ElementId>) -> Self { pub fn id(mut self, id: impl Into<ElementId>) -> Self {
self.id = Some(id.into()); self.id = Some(id.into());
@ -99,6 +120,13 @@ impl Styled for GroupBox {
} }
} }
impl GroupBoxVariants for GroupBox {
fn with_variant(mut self, variant: GroupBoxVariant) -> Self {
self.variant = variant;
self
}
}
impl RenderOnce for GroupBox { impl RenderOnce for GroupBox {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let (bg, border, has_paddings) = match self.variant { let (bg, border, has_paddings) = match self.variant {
@ -135,3 +163,29 @@ impl RenderOnce for GroupBox {
) )
} }
} }
#[cfg(test)]
mod test {
#[test]
fn test_group_variant_from_str() {
use super::GroupBoxVariant;
assert_eq!(GroupBoxVariant::from_str("normal"), GroupBoxVariant::Normal);
assert_eq!(GroupBoxVariant::from_str("fill"), GroupBoxVariant::Fill);
assert_eq!(
GroupBoxVariant::from_str("outline"),
GroupBoxVariant::Outline
);
assert_eq!(GroupBoxVariant::from_str("other"), GroupBoxVariant::Normal);
assert_eq!(GroupBoxVariant::from_str("FILL"), GroupBoxVariant::Fill);
assert_eq!(
GroupBoxVariant::from_str("OutLine"),
GroupBoxVariant::Outline
);
assert_eq!(GroupBoxVariant::Normal.as_str(), "normal");
assert_eq!(GroupBoxVariant::Fill.as_str(), "fill");
assert_eq!(GroupBoxVariant::Outline.as_str(), "outline");
}
}

View file

@ -86,6 +86,8 @@ pub enum IconName {
PanelRightClose, PanelRightClose,
PanelRightOpen, PanelRightOpen,
Plus, Plus,
Redo,
Redo2,
Replace, Replace,
ResizeCorner, ResizeCorner,
Search, Search,
@ -100,6 +102,8 @@ pub enum IconName {
ThumbsDown, ThumbsDown,
ThumbsUp, ThumbsUp,
TriangleAlert, TriangleAlert,
Undo,
Undo2,
User, User,
WindowClose, WindowClose,
WindowMaximize, WindowMaximize,
@ -180,6 +184,8 @@ impl IconNamed for IconName {
Self::PanelRightClose => "icons/panel-right-close.svg", Self::PanelRightClose => "icons/panel-right-close.svg",
Self::PanelRightOpen => "icons/panel-right-open.svg", Self::PanelRightOpen => "icons/panel-right-open.svg",
Self::Plus => "icons/plus.svg", Self::Plus => "icons/plus.svg",
Self::Redo => "icons/redo.svg",
Self::Redo2 => "icons/redo-2.svg",
Self::Replace => "icons/replace.svg", Self::Replace => "icons/replace.svg",
Self::ResizeCorner => "icons/resize-corner.svg", Self::ResizeCorner => "icons/resize-corner.svg",
Self::Search => "icons/search.svg", Self::Search => "icons/search.svg",
@ -194,6 +200,8 @@ impl IconNamed for IconName {
Self::ThumbsDown => "icons/thumbs-down.svg", Self::ThumbsDown => "icons/thumbs-down.svg",
Self::ThumbsUp => "icons/thumbs-up.svg", Self::ThumbsUp => "icons/thumbs-up.svg",
Self::TriangleAlert => "icons/triangle-alert.svg", Self::TriangleAlert => "icons/triangle-alert.svg",
Self::Undo => "icons/undo.svg",
Self::Undo2 => "icons/undo-2.svg",
Self::User => "icons/user.svg", Self::User => "icons/user.svg",
Self::WindowClose => "icons/window-close.svg", Self::WindowClose => "icons/window-close.svg",
Self::WindowMaximize => "icons/window-maximize.svg", Self::WindowMaximize => "icons/window-maximize.svg",

View file

@ -50,6 +50,7 @@ pub mod radio;
pub mod resizable; pub mod resizable;
pub mod scroll; pub mod scroll;
pub mod select; pub mod select;
pub mod setting;
pub mod sheet; pub mod sheet;
pub mod sidebar; pub mod sidebar;
pub mod skeleton; pub mod skeleton;

View file

@ -0,0 +1,57 @@
use std::rc::Rc;
use crate::{
checkbox::Checkbox,
setting::{
fields::{get_value, set_value, SettingFieldRender},
AnySettingField, RenderOptions,
},
switch::Switch,
Sizable, StyledExt,
};
use gpui::{div, AnyElement, App, IntoElement, ParentElement as _, StyleRefinement, Window};
pub(crate) struct BoolField {
use_switch: bool,
}
impl BoolField {
pub(crate) fn new(use_switch: bool) -> Self {
Self { use_switch }
}
}
impl SettingFieldRender for BoolField {
fn render(
&self,
field: Rc<dyn AnySettingField>,
options: &RenderOptions,
style: &StyleRefinement,
_: &mut Window,
cx: &mut App,
) -> AnyElement {
let checked = get_value::<bool>(&field, cx);
let set_value = set_value::<bool>(&field, cx);
div()
.refine_style(style)
.child(if self.use_switch {
Switch::new("check")
.checked(checked)
.with_size(options.size)
.on_click(move |checked: &bool, _, cx: &mut App| {
set_value(*checked, cx);
})
.into_any_element()
} else {
Checkbox::new("check")
.checked(checked)
.with_size(options.size)
.on_click(move |checked: &bool, _, cx: &mut App| {
set_value(*checked, cx);
})
.into_any_element()
})
.into_any_element()
}
}

View file

@ -0,0 +1,83 @@
use std::rc::Rc;
use gpui::{
prelude::FluentBuilder as _, AnyElement, App, Corner, IntoElement, SharedString,
StyleRefinement, Styled, Window,
};
use crate::{
button::Button,
menu::{DropdownMenu, PopupMenuItem},
setting::{
fields::{get_value, set_value, SettingFieldRender},
AnySettingField, RenderOptions,
},
AxisExt, Sizable, StyledExt,
};
pub(crate) struct DropdownField<T> {
options: Vec<(SharedString, SharedString)>,
_marker: std::marker::PhantomData<T>,
}
impl<T> DropdownField<T> {
pub(crate) fn new(options: Option<&Vec<(SharedString, SharedString)>>) -> Self {
Self {
options: options.cloned().unwrap_or(vec![]),
_marker: std::marker::PhantomData,
}
}
}
impl<T> SettingFieldRender for DropdownField<T>
where
T: Into<SharedString> + From<SharedString> + Clone + 'static,
{
fn render(
&self,
field: Rc<dyn AnySettingField>,
options: &RenderOptions,
style: &StyleRefinement,
_: &mut Window,
cx: &mut App,
) -> AnyElement {
let old_value = get_value::<T>(&field, cx);
let set_value = set_value::<T>(&field, cx);
let dropdown_options = self.options.clone();
let old_label = dropdown_options
.iter()
.find(|(value, _)| *value == old_value.clone().into())
.map(|(_, label)| label.clone())
.unwrap_or_else(|| old_value.clone().into());
Button::new("btn")
.when(options.layout.is_vertical(), |this| this.w_full())
.label(old_label)
.dropdown_caret(true)
.outline()
.with_size(options.size)
.refine_style(style)
.dropdown_menu_with_anchor(Corner::TopRight, move |menu, _, _| {
let set_value = set_value.clone();
let menu = dropdown_options.iter().fold(menu, |menu, (value, label)| {
let old_value: SharedString = old_value.clone().into();
let checked = &old_value == value;
menu.item(
PopupMenuItem::new(label.clone())
.checked(checked)
.on_click({
let value = value.clone();
let set_value = set_value.clone();
move |_, _, cx| {
set_value(T::from(value.clone()), cx);
}
}),
)
});
menu
})
.into_any_element()
}
}

View file

@ -0,0 +1,29 @@
use gpui::{AnyElement, App, StyleRefinement, Window};
use std::rc::Rc;
use crate::setting::{fields::SettingFieldRender, AnySettingField, RenderOptions};
pub(crate) struct ElementField {
element_render: Rc<dyn Fn(&RenderOptions, &mut Window, &mut App) -> AnyElement>,
}
impl ElementField {
pub(crate) fn new(
element_render: Rc<dyn Fn(&RenderOptions, &mut Window, &mut App) -> AnyElement + 'static>,
) -> Self {
Self { element_render }
}
}
impl SettingFieldRender for ElementField {
fn render(
&self,
_: Rc<dyn AnySettingField>,
options: &RenderOptions,
_style: &StyleRefinement,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
(self.element_render)(options, window, cx)
}
}

View file

@ -0,0 +1,283 @@
mod bool;
mod dropdown;
mod element;
mod number;
mod string;
pub(crate) use bool::*;
pub(crate) use dropdown::*;
pub(crate) use element::*;
pub(crate) use number::*;
pub(crate) use string::*;
pub use number::NumberFieldOptions;
use gpui::{AnyElement, App, IntoElement, SharedString, StyleRefinement, Styled, Window};
use std::{any::Any, rc::Rc};
use crate::setting::RenderOptions;
pub(crate) trait SettingFieldRender {
#[allow(clippy::too_many_arguments)]
fn render(
&self,
field: Rc<dyn AnySettingField>,
options: &RenderOptions,
style: &StyleRefinement,
window: &mut Window,
cx: &mut App,
) -> AnyElement;
}
pub(crate) fn get_value<T: Clone + 'static>(field: &Rc<dyn AnySettingField>, cx: &mut App) -> T {
let setting_field = field
.as_any()
.downcast_ref::<SettingField<T>>()
.expect("Failed to downcast setting field");
(setting_field.value)(cx)
}
pub(crate) fn set_value<T: Clone + 'static>(
field: &Rc<dyn AnySettingField>,
_cx: &mut App,
) -> Rc<dyn Fn(T, &mut App)> {
let setting_field = field
.as_any()
.downcast_ref::<SettingField<T>>()
.expect("Failed to downcast setting field");
setting_field.set_value.clone()
}
/// The type of setting field to render.
#[derive(Clone)]
pub enum SettingFieldType {
Switch,
Checkbox,
NumberInput {
options: NumberFieldOptions,
},
Input,
Dropdown {
options: Vec<(SharedString, SharedString)>,
},
Element {
element_render: Rc<dyn Fn(&RenderOptions, &mut Window, &mut App) -> AnyElement>,
},
}
impl SettingFieldType {
#[inline]
pub(crate) fn is_switch(&self) -> bool {
matches!(self, SettingFieldType::Switch)
}
#[inline]
pub(crate) fn is_number_input(&self) -> bool {
matches!(self, SettingFieldType::NumberInput { .. })
}
#[inline]
pub(crate) fn is_input(&self) -> bool {
matches!(self, SettingFieldType::Input)
}
#[inline]
pub(crate) fn is_dropdown(&self) -> bool {
matches!(self, SettingFieldType::Dropdown { .. })
}
#[inline]
pub(crate) fn is_element(&self) -> bool {
matches!(self, SettingFieldType::Element { .. })
}
#[inline]
pub(super) fn dropdown_options(&self) -> Option<&Vec<(SharedString, SharedString)>> {
match self {
SettingFieldType::Dropdown { options } => Some(options),
_ => None,
}
}
#[inline]
pub(super) fn number_input_options(&self) -> Option<&NumberFieldOptions> {
match self {
SettingFieldType::NumberInput { options } => Some(options),
_ => None,
}
}
#[inline]
pub(super) fn element_render(
&self,
) -> Rc<dyn Fn(&RenderOptions, &mut Window, &mut App) -> AnyElement + 'static> {
match self {
SettingFieldType::Element { element_render } => element_render.clone(),
_ => unreachable!("element_render called on non-element field"),
}
}
}
/// A setting field that can get and set a value of type T in the App.
pub struct SettingField<T> {
pub(crate) field_type: SettingFieldType,
pub(crate) style: StyleRefinement,
/// Function to get the value for this field.
pub(crate) value: Rc<dyn Fn(&App) -> T>,
/// Function to set the value for this field.
pub(crate) set_value: Rc<dyn Fn(T, &mut App)>,
pub(crate) default_value: Option<T>,
}
impl SettingField<bool> {
/// Create a new Switch field.
pub fn switch<V, S>(value: V, set_value: S) -> Self
where
V: Fn(&App) -> bool + 'static,
S: Fn(bool, &mut App) + 'static,
{
Self::new(SettingFieldType::Switch, value, set_value)
}
/// Create a new Checkbox field.
pub fn checkbox<V, S>(value: V, set_value: S) -> Self
where
V: Fn(&App) -> bool + 'static,
S: Fn(bool, &mut App) + 'static,
{
Self::new(SettingFieldType::Checkbox, value, set_value)
}
}
impl SettingField<SharedString> {
/// Create a new Input field.
pub fn input<V, S>(value: V, set_value: S) -> Self
where
V: Fn(&App) -> SharedString + 'static,
S: Fn(SharedString, &mut App) + 'static,
{
Self::new(SettingFieldType::Input, value, set_value)
}
/// Create a new Dropdown field with the given options.
pub fn dropdown<V, S>(
options: Vec<(SharedString, SharedString)>,
value: V,
set_value: S,
) -> Self
where
V: Fn(&App) -> SharedString + 'static,
S: Fn(SharedString, &mut App) + 'static,
{
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
where
E: IntoElement,
R: Fn(&RenderOptions, &mut Window, &mut App) -> E + 'static,
{
Self::new(
SettingFieldType::Element {
element_render: Rc::new(move |options, window, cx| {
element_render(options, window, cx).into_any_element()
}),
},
|_| SharedString::default(),
|_, _| {},
)
}
}
impl SettingField<f64> {
/// Create a new Number Input field with the given options.
pub fn number_input<V, S>(options: NumberFieldOptions, value: V, set_value: S) -> Self
where
V: Fn(&App) -> f64 + 'static,
S: Fn(f64, &mut App) + 'static,
{
Self::new(SettingFieldType::NumberInput { options }, value, set_value)
}
}
impl<T> SettingField<T> {
/// Create a new setting field with the given get and set functions.
fn new<V, S>(field_type: SettingFieldType, value: V, set_value: S) -> Self
where
V: Fn(&App) -> T + 'static,
S: Fn(T, &mut App) + 'static,
{
Self {
field_type,
style: StyleRefinement::default(),
value: Rc::new(value),
set_value: Rc::new(set_value),
default_value: None,
}
}
/// Set the default value for this setting field, default is None.
///
/// If set, this value can be used to reset the setting to its default state.
/// If not set, the setting cannot be reset.
pub fn default_value(mut self, default_value: impl Into<T>) -> Self {
self.default_value = Some(default_value.into());
self
}
}
impl<T> Styled for SettingField<T> {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
/// A trait for setting fields that allows for dynamic typing.
pub trait AnySettingField {
fn as_any(&self) -> &dyn std::any::Any;
fn type_name(&self) -> &'static str;
fn type_id(&self) -> std::any::TypeId;
fn field_type(&self) -> &SettingFieldType;
fn style(&self) -> &StyleRefinement;
fn is_resettable(&self, cx: &App) -> bool;
fn reset(&self, window: &mut Window, cx: &mut App);
}
impl<T: Clone + PartialEq + Send + Sync + 'static> AnySettingField for SettingField<T> {
fn as_any(&self) -> &dyn Any {
self
}
fn type_name(&self) -> &'static str {
std::any::type_name::<T>()
}
fn type_id(&self) -> std::any::TypeId {
std::any::TypeId::of::<T>()
}
fn field_type(&self) -> &SettingFieldType {
&self.field_type
}
fn style(&self) -> &StyleRefinement {
&self.style
}
fn is_resettable(&self, cx: &App) -> bool {
let Some(default_value) = self.default_value.as_ref() else {
return false;
};
&(self.value)(cx) != default_value
}
fn reset(&self, _: &mut Window, cx: &mut App) {
let Some(default_value) = self.default_value.as_ref() else {
return;
};
(self.set_value)(default_value.clone(), cx)
}
}

View file

@ -0,0 +1,111 @@
use std::rc::Rc;
use gpui::{
prelude::FluentBuilder as _, AnyElement, App, AppContext as _, Entity, IntoElement,
SharedString, StyleRefinement, Styled, Window,
};
use crate::{
input::{InputState, NumberInput, NumberInputEvent},
setting::{
fields::{get_value, set_value, SettingFieldRender},
AnySettingField, RenderOptions,
},
AxisExt, Sizable, StyledExt,
};
#[derive(Clone, Debug)]
pub struct NumberFieldOptions {
/// The minimum value for the number input, default is `f64::MIN`.
pub min: f64,
/// The maximum value for the number input, default is `f64::MAX`.
pub max: f64,
/// The step value for the number input, default is `1.0`.
pub step: f64,
}
impl Default for NumberFieldOptions {
fn default() -> Self {
Self {
min: f64::MIN,
max: f64::MAX,
step: 1.0,
}
}
}
pub(crate) struct NumberField {
options: NumberFieldOptions,
}
impl NumberField {
pub(crate) fn new(options: Option<&NumberFieldOptions>) -> Self {
Self {
options: options.cloned().unwrap_or_default(),
}
}
}
struct State {
input: Entity<InputState>,
_subscription: gpui::Subscription,
}
impl SettingFieldRender for NumberField {
fn render(
&self,
field: Rc<dyn AnySettingField>,
options: &RenderOptions,
style: &StyleRefinement,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
let value = get_value::<f64>(&field, cx);
let set_value = set_value::<f64>(&field, cx);
let num_options = self.options.clone();
let state = window
.use_keyed_state("number-state", cx, |window, cx| {
let input =
cx.new(|cx| InputState::new(window, cx).default_value(value.to_string()));
let _subscription = cx.subscribe_in(&input, window, {
move |_, input, event: &NumberInputEvent, window, cx| match event {
NumberInputEvent::Step(action) => input.update(cx, |input, cx| {
let value = input.value();
if let Ok(value) = value.parse::<f64>() {
let new_value = if *action == crate::input::StepAction::Increment {
(value + num_options.step).min(num_options.max)
} else {
(value - num_options.step).max(num_options.min)
};
set_value(new_value, cx);
input.set_value(
SharedString::from(new_value.to_string()),
window,
cx,
);
}
}),
}
});
State {
input,
_subscription,
}
})
.read(cx);
NumberInput::new(&state.input)
.with_size(options.size)
.map(|this| {
if options.layout.is_horizontal() {
this.w_32()
} else {
this.w_full()
}
})
.refine_style(style)
.into_any_element()
}
}

View file

@ -0,0 +1,81 @@
use std::rc::Rc;
use gpui::{
prelude::FluentBuilder as _, AnyElement, App, AppContext as _, Entity, IntoElement,
SharedString, StyleRefinement, Styled, Window,
};
use crate::{
input::{Input, InputEvent, InputState},
setting::{
fields::{get_value, set_value, SettingFieldRender},
AnySettingField, RenderOptions,
},
AxisExt as _, Sizable, StyledExt,
};
pub(crate) struct StringField<T> {
_marker: std::marker::PhantomData<T>,
}
impl<T> StringField<T> {
pub(crate) fn new() -> Self {
Self {
_marker: std::marker::PhantomData,
}
}
}
struct State {
input: Entity<InputState>,
_subscription: gpui::Subscription,
}
impl<T> SettingFieldRender for StringField<T>
where
T: Into<SharedString> + From<SharedString> + Clone + 'static,
{
fn render(
&self,
field: Rc<dyn AnySettingField>,
options: &RenderOptions,
style: &StyleRefinement,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
let value = get_value::<T>(&field, cx);
let set_value = set_value::<T>(&field, cx);
let state = window
.use_keyed_state("string-state", cx, |window, cx| {
let input = cx.new(|cx| InputState::new(window, cx).default_value(value));
let _subscription = cx.subscribe(&input, {
move |_, input, event: &InputEvent, cx| match event {
InputEvent::Change => {
let value = input.read(cx).value();
set_value(value.into(), cx);
}
_ => {}
}
});
State {
input,
_subscription,
}
})
.read(cx);
Input::new(&state.input)
.with_size(options.size)
.map(|this| {
if options.layout.is_horizontal() {
this.w_64()
} else {
this.w_full()
}
})
.refine_style(style)
.into_any_element()
}
}

View file

@ -0,0 +1,115 @@
use gpui::{
prelude::FluentBuilder as _, App, IntoElement, ParentElement as _, SharedString,
StyleRefinement, Styled, Window,
};
use crate::{
group_box::{GroupBox, GroupBoxVariants},
label::Label,
setting::{RenderOptions, SettingItem},
v_flex, ActiveTheme, StyledExt,
};
/// A setting group that can contain multiple setting items.
#[derive(Clone)]
pub struct SettingGroup {
style: StyleRefinement,
pub(super) title: Option<SharedString>,
pub(super) description: Option<SharedString>,
pub(super) items: Vec<SettingItem>,
}
impl Styled for SettingGroup {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl SettingGroup {
/// Create a new setting group.
pub fn new() -> Self {
Self {
style: StyleRefinement::default(),
title: None,
description: None,
items: Vec::new(),
}
}
/// Set the label of the setting group, default is None.
pub fn title(mut self, title: impl Into<SharedString>) -> Self {
self.title = Some(title.into());
self
}
/// Set the description of the setting group, default is None.
pub fn description(mut self, description: impl Into<SharedString>) -> Self {
self.description = Some(description.into());
self
}
/// Add a setting item to the group.
pub fn item(mut self, item: SettingItem) -> Self {
self.items.push(item);
self
}
/// Add multiple setting items to the group.
pub fn items<I>(mut self, items: I) -> Self
where
I: IntoIterator<Item = SettingItem>,
{
self.items.extend(items);
self
}
/// Return true if any of the setting items in the group match the given query.
pub(super) fn is_match(&self, query: &str) -> bool {
self.items.iter().any(|item| item.is_match(query))
}
pub(super) fn is_resettable(&self, cx: &App) -> bool {
self.items.iter().any(|item| item.is_resettable(cx))
}
pub(crate) fn render(
self,
group_ix: usize,
query: &str,
options: &RenderOptions,
window: &mut Window,
cx: &mut App,
) -> impl IntoElement {
GroupBox::new()
.id(SharedString::from(format!("group-{}", group_ix)))
.with_variant(options.group_variant)
.when_some(self.title.clone(), |this, title| {
this.title(v_flex().gap_1().child(title).when_some(
self.description.clone(),
|this, description| {
this.child(
Label::new(description)
.text_sm()
.text_color(cx.theme().muted_foreground),
)
},
))
})
.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))
} else {
None
}
}))
.refine_style(&self.style)
}
pub(crate) fn reset(&self, window: &mut Window, cx: &mut App) {
for item in &self.items {
item.reset(window, cx);
}
}
}

View file

@ -0,0 +1,212 @@
use gpui::{
div, prelude::FluentBuilder as _, AnyElement, App, Axis, InteractiveElement as _, IntoElement,
ParentElement, SharedString, Styled, Window,
};
use std::{any::TypeId, ops::Deref, rc::Rc};
use crate::{
label::Label,
setting::{
fields::{BoolField, DropdownField, NumberField, SettingFieldRender, StringField},
AnySettingField, ElementField, RenderOptions,
},
text::Text,
v_flex, ActiveTheme as _, AxisExt, StyledExt as _,
};
/// Setting item.
#[derive(Clone)]
pub enum SettingItem {
/// A normal setting item with a title, description, and field.
Item {
title: SharedString,
description: Option<Text>,
layout: Axis,
field: Rc<dyn AnySettingField>,
},
/// A full custom element to render.
Element {
render: Rc<dyn Fn(&RenderOptions, &mut Window, &mut App) -> AnyElement + 'static>,
},
}
impl SettingItem {
/// Create a new setting item.
pub fn new<F>(title: impl Into<SharedString>, field: F) -> Self
where
F: AnySettingField + 'static,
{
SettingItem::Item {
title: title.into(),
description: None,
layout: Axis::Horizontal,
field: Rc::new(field),
}
}
/// Create a new custom element setting item.
pub fn element<R, E>(render: R) -> Self
where
E: IntoElement,
R: Fn(&RenderOptions, &mut Window, &mut App) -> E + 'static,
{
SettingItem::Element {
render: Rc::new(move |options, window, cx| {
render(options, window, cx).into_any_element()
}),
}
}
/// Set the description of the setting item.
///
/// Only applies to [`SettingItem::Item`].
pub fn description(mut self, description: impl Into<Text>) -> Self {
match &mut self {
SettingItem::Item { description: d, .. } => {
*d = Some(description.into());
}
SettingItem::Element { .. } => {}
}
self
}
/// Set the layout of the setting item.
///
/// Only applies to [`SettingItem::Item`].
pub fn layout(mut self, layout: Axis) -> Self {
match &mut self {
SettingItem::Item { layout: l, .. } => {
*l = layout;
}
SettingItem::Element { .. } => {}
}
self
}
pub(crate) fn is_match(&self, query: &str) -> bool {
match self {
SettingItem::Item {
title, description, ..
} => {
title.to_lowercase().contains(&query.to_lowercase())
|| description.as_ref().map_or(false, |d| {
d.as_str().to_lowercase().contains(&query.to_lowercase())
})
}
// We need to show all custom elements when not searching.
SettingItem::Element { .. } => query.is_empty(),
}
}
pub(crate) fn is_resettable(&self, cx: &App) -> bool {
match self {
SettingItem::Item { field, .. } => field.is_resettable(cx),
SettingItem::Element { .. } => false,
}
}
pub(crate) fn reset(&self, window: &mut Window, cx: &mut App) {
match self {
SettingItem::Item { field, .. } => field.reset(window, cx),
SettingItem::Element { .. } => {}
}
}
fn render_field(
field: Rc<dyn AnySettingField>,
options: RenderOptions,
window: &mut Window,
cx: &mut App,
) -> impl IntoElement {
let field_type = field.field_type();
let style = field.style().clone();
let type_id = field.deref().type_id();
let renderer: Box<dyn SettingFieldRender> = match type_id {
t if t == std::any::TypeId::of::<bool>() => {
Box::new(BoolField::new(field_type.is_switch()))
}
t if t == TypeId::of::<f64>() && field_type.is_number_input() => {
Box::new(NumberField::new(field_type.number_input_options()))
}
t if t == TypeId::of::<SharedString>() && field_type.is_input() => {
Box::new(StringField::<SharedString>::new())
}
t if t == TypeId::of::<String>() && field_type.is_input() => {
Box::new(StringField::<String>::new())
}
t if t == TypeId::of::<SharedString>() && field_type.is_dropdown() => Box::new(
DropdownField::<SharedString>::new(field_type.dropdown_options()),
),
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()))
}
_ => unimplemented!("Unsupported setting type: {}", field.deref().type_name()),
};
renderer.render(field, &options, &style, window, cx)
}
pub(super) fn render(
self,
ix: usize,
options: &RenderOptions,
window: &mut Window,
cx: &mut App,
) -> impl IntoElement {
div()
.id(SharedString::from(format!("item-{}", ix)))
.w_full()
.child(match self {
SettingItem::Item {
title,
description,
layout,
field,
} => div()
.w_full()
.overflow_hidden()
.map(|this| {
if layout.is_horizontal() {
this.h_flex().justify_between().items_start()
} else {
this.v_flex()
}
})
.gap_3()
.child(
v_flex()
.map(|this| {
if layout.is_horizontal() {
this.flex_1().max_w_3_5()
} else {
this.w_full()
}
})
.gap_1()
.child(Label::new(title.clone()).text_sm())
.when_some(description.clone(), |this, description| {
this.child(
div()
.size_full()
.text_sm()
.text_color(cx.theme().muted_foreground)
.child(description),
)
}),
)
.child(div().id("field").child(Self::render_field(
field,
RenderOptions { layout, ..*options },
window,
cx,
)))
.into_any_element(),
SettingItem::Element { render } => {
(render)(&options, window, cx).into_any_element()
}
})
}
}

View file

@ -0,0 +1,11 @@
mod fields;
mod group;
mod item;
mod page;
mod settings;
pub use fields::*;
pub use group::*;
pub use item::*;
pub use page::*;
pub use settings::*;

View file

@ -0,0 +1,174 @@
use gpui::{
list, prelude::FluentBuilder as _, px, App, Entity, InteractiveElement as _, IntoElement,
ParentElement as _, SharedString, StatefulInteractiveElement, Styled, Window,
};
use rust_i18n::t;
use crate::{
button::{Button, ButtonVariants},
divider::Divider,
h_flex,
label::Label,
setting::{settings::SettingsState, RenderOptions, SettingGroup},
v_flex, ActiveTheme, IconName, Sizable,
};
/// A setting page that can contain multiple setting groups.
#[derive(Clone)]
pub struct SettingPage {
resettable: bool,
pub(super) default_open: bool,
pub(super) title: SharedString,
pub(super) description: Option<SharedString>,
pub(super) groups: Vec<SettingGroup>,
}
impl SettingPage {
pub fn new(title: impl Into<SharedString>) -> Self {
Self {
resettable: true,
default_open: false,
title: title.into(),
description: None,
groups: Vec::new(),
}
}
/// Set the title of the setting page.
pub fn title(mut self, title: impl Into<SharedString>) -> Self {
self.title = title.into();
self
}
/// Set the description of the setting page, default is None.
pub fn description(mut self, description: impl Into<SharedString>) -> Self {
self.description = Some(description.into());
self
}
/// Set the default open state of the setting page, default is false.
pub fn default_open(mut self, default_open: bool) -> Self {
self.default_open = default_open;
self
}
/// Set whether the setting page is resettable, default is true.
///
/// If true and the items in this page has changed, the reset button will appear.
pub fn resettable(mut self, resettable: bool) -> Self {
self.resettable = resettable;
self
}
/// Add a setting group to the page.
pub fn group(mut self, group: SettingGroup) -> Self {
self.groups.push(group);
self
}
/// Add multiple setting groups to the page.
pub fn groups(mut self, groups: impl IntoIterator<Item = SettingGroup>) -> Self {
self.groups.extend(groups);
self
}
fn is_resettable(&self, cx: &App) -> bool {
self.resettable && self.groups.iter().any(|group| group.is_resettable(cx))
}
fn reset_all(&self, window: &mut Window, cx: &mut App) {
for group in &self.groups {
group.reset(window, cx);
}
}
pub(super) fn render(
&self,
ix: usize,
state: &Entity<SettingsState>,
options: &RenderOptions,
window: &mut Window,
cx: &mut App,
) -> impl IntoElement {
let search_input = state.read(cx).search_input.clone();
let query = search_input.read(cx).value();
let groups = self
.groups
.iter()
.filter(|group| group.is_match(&query))
.cloned()
.collect::<Vec<_>>();
let groups_count = groups.len();
let list_state = window
.use_keyed_state(
SharedString::from(format!("list-state:{}", ix)),
cx,
|_, _| gpui::ListState::new(groups_count, gpui::ListAlignment::Top, px(0.)),
)
.read(cx)
.clone();
if list_state.item_count() != groups_count {
list_state.reset(groups_count);
}
let deferred_scroll_group_ix = state.read(cx).deferred_scroll_group_ix;
if let Some(ix) = deferred_scroll_group_ix {
state.update(cx, |state, _| {
state.deferred_scroll_group_ix = None;
});
list_state.scroll_to_reveal_item(ix);
}
v_flex()
.id(ix)
.p_4()
.size_full()
.overflow_scroll()
.child(
v_flex()
.gap_3()
.child(h_flex().justify_between().child(self.title.clone()).when(
self.is_resettable(cx),
|this| {
this.child(
Button::new("reset")
.icon(IconName::Undo2)
.ghost()
.small()
.tooltip(t!("Settings.Reset All"))
.on_click({
let page = self.clone();
move |_, window, cx| {
page.reset_all(window, cx);
}
}),
)
},
))
.when_some(self.description.clone(), |this, description| {
this.child(
Label::new(description)
.text_sm()
.text_color(cx.theme().muted_foreground),
)
})
.child(Divider::horizontal()),
)
.child(
list(list_state.clone(), {
let query = query.clone();
let options = *options;
move |ix, window, cx| {
let group = groups[ix].clone();
group
.pt_6()
.render(ix, &query, &options, window, cx)
.into_any_element()
}
})
.size_full(),
)
}
}

View file

@ -0,0 +1,269 @@
use crate::{
group_box::GroupBoxVariant,
input::{Input, InputState},
resizable::{h_resizable, resizable_panel},
setting::{SettingGroup, SettingPage},
sidebar::{Sidebar, SidebarMenu, SidebarMenuItem},
IconName, Sizable, Size,
};
use gpui::{
div, prelude::FluentBuilder as _, px, relative, App, AppContext as _, Axis, ElementId, Entity,
IntoElement, ParentElement as _, Pixels, RenderOnce, Styled, Window,
};
use rust_i18n::t;
/// The settings structure containing multiple pages for app settings.
///
/// The hierarchy of settings is as follows:
///
/// ```ignore
/// Settings
/// SettingPage <- The single active page displayed
/// SettingGroup
/// SettingItem
/// Label
/// SettingField (e.g., Switch, Dropdown, Input)
/// ```
#[derive(IntoElement)]
pub struct Settings {
id: ElementId,
pages: Vec<SettingPage>,
group_variant: GroupBoxVariant,
size: Size,
sidebar_width: Pixels,
}
impl Settings {
/// Create a new settings with the given ID.
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
pages: vec![],
group_variant: GroupBoxVariant::default(),
size: Size::default(),
sidebar_width: px(250.0),
}
}
/// Set the width of the sidebar, default is `250px`.
pub fn sidebar_width(mut self, width: impl Into<Pixels>) -> Self {
self.sidebar_width = width.into();
self
}
/// Add a page to the settings.
pub fn page(mut self, page: SettingPage) -> Self {
self.pages.push(page);
self
}
/// Add pages to the settings.
pub fn pages(mut self, pages: impl IntoIterator<Item = SettingPage>) -> Self {
self.pages.extend(pages);
self
}
/// Set the default variant for all setting groups.
///
/// All setting groups will use this variant unless overridden individually.
pub fn with_group_variant(mut self, variant: GroupBoxVariant) -> Self {
self.group_variant = variant;
self
}
fn filtered_pages(&self, query: &str) -> Vec<SettingPage> {
self.pages
.iter()
.filter_map(|page| {
let filtered_groups: Vec<SettingGroup> = page
.groups
.iter()
.filter_map(|group| {
let mut group = group.clone();
group.items = group
.items
.iter()
.filter(|item| item.is_match(&query))
.cloned()
.collect();
if group.items.is_empty() {
None
} else {
Some(group)
}
})
.collect();
let mut page = page.clone();
page.groups = filtered_groups;
if page.groups.is_empty() {
None
} else {
Some(page)
}
})
.collect()
}
fn render_active_page(
&self,
state: &Entity<SettingsState>,
pages: &Vec<SettingPage>,
options: &RenderOptions,
window: &mut Window,
cx: &mut App,
) -> impl IntoElement {
let selected_index = state.read(cx).selected_index;
for (ix, page) in pages.into_iter().enumerate() {
if selected_index.page_ix == ix {
return page
.render(ix, state, &options, window, cx)
.into_any_element();
}
}
return div().into_any_element();
}
fn render_sidebar(
&self,
state: &Entity<SettingsState>,
pages: &Vec<SettingPage>,
_: &mut Window,
cx: &mut App,
) -> impl IntoElement {
let selected_index = state.read(cx).selected_index;
let search_input = state.read(cx).search_input.clone();
Sidebar::left()
.width(relative(1.))
.border_width(px(0.))
.collapsed(false)
.header(
div()
.w_full()
.child(Input::new(&search_input).prefix(IconName::Search)),
)
.child(
SidebarMenu::new()
.p_2()
.children(pages.iter().enumerate().map(|(page_ix, page)| {
let is_page_active =
selected_index.page_ix == page_ix && selected_index.group_ix.is_none();
SidebarMenuItem::new(page.title.clone())
.default_open(page.default_open)
.active(is_page_active)
.on_click({
let state = state.clone();
move |_, _, cx| {
state.update(cx, |state, cx| {
state.selected_index = SelectIndex {
page_ix,
..Default::default()
};
cx.notify();
})
}
})
.when(page.groups.len() > 1, |this| {
this.children(
page.groups
.iter()
.filter(|g| g.title.is_some())
.enumerate()
.map(|(group_ix, group)| {
let is_active = selected_index.page_ix == page_ix
&& selected_index.group_ix == Some(group_ix);
let title = group.title.clone().unwrap_or_default();
SidebarMenuItem::new(title).active(is_active).on_click(
{
let state = state.clone();
move |_, _, cx| {
state.update(cx, |state, cx| {
state.selected_index = SelectIndex {
page_ix,
group_ix: Some(group_ix),
};
state.deferred_scroll_group_ix =
Some(group_ix);
cx.notify();
})
}
},
)
}),
)
})
})),
)
}
}
impl Sizable for Settings {
fn with_size(mut self, size: impl Into<Size>) -> Self {
self.size = size.into();
self
}
}
pub(super) struct SettingsState {
pub(super) selected_index: SelectIndex,
/// If set, defer scrolling to this group index after rendering.
pub(super) deferred_scroll_group_ix: Option<usize>,
pub(super) search_input: Entity<InputState>,
}
/// Options for rendering setting item.
#[derive(Clone, Copy)]
pub struct RenderOptions {
pub size: Size,
pub group_variant: GroupBoxVariant,
pub layout: Axis,
}
#[derive(Clone, Copy, Default)]
pub(super) struct SelectIndex {
page_ix: usize,
group_ix: Option<usize>,
}
impl RenderOnce for Settings {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let state = window.use_keyed_state(self.id.clone(), cx, |window, cx| {
let search_input = cx.new(|cx| {
InputState::new(window, cx)
.placeholder(t!("Settings.search_placeholder"))
.default_value("")
});
SettingsState {
search_input,
selected_index: SelectIndex::default(),
deferred_scroll_group_ix: None,
}
});
let query = state.read(cx).search_input.read(cx).value();
let filtered_pages = self.filtered_pages(&query);
let options = RenderOptions {
size: self.size,
group_variant: self.group_variant,
layout: Axis::Horizontal,
};
h_resizable(self.id.clone())
.child(
resizable_panel()
.size(self.sidebar_width)
.child(self.render_sidebar(&state, &filtered_pages, window, cx)),
)
.child(resizable_panel().child(self.render_active_page(
&state,
&filtered_pages,
&options,
window,
cx,
)))
}
}

View file

@ -5,13 +5,14 @@ use crate::{
use gpui::{ use gpui::{
div, percentage, prelude::FluentBuilder as _, AnyElement, App, ClickEvent, ElementId, div, percentage, prelude::FluentBuilder as _, AnyElement, App, ClickEvent, ElementId,
InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, SharedString, InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, SharedString,
StatefulInteractiveElement as _, Styled as _, Window, StatefulInteractiveElement as _, StyleRefinement, Styled, Window,
}; };
use std::rc::Rc; use std::rc::Rc;
/// Menu for the [`super::Sidebar`] /// Menu for the [`super::Sidebar`]
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct SidebarMenu { pub struct SidebarMenu {
style: StyleRefinement,
collapsed: bool, collapsed: bool,
items: Vec<SidebarMenuItem>, items: Vec<SidebarMenuItem>,
} }
@ -20,6 +21,7 @@ impl SidebarMenu {
/// Create a new SidebarMenu /// Create a new SidebarMenu
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
style: StyleRefinement::default(),
items: Vec::new(), items: Vec::new(),
collapsed: false, collapsed: false,
} }
@ -54,9 +56,15 @@ impl Collapsible for SidebarMenu {
} }
} }
impl Styled for SidebarMenu {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl RenderOnce for SidebarMenu { impl RenderOnce for SidebarMenu {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
v_flex().gap_2().children( v_flex().gap_2().refine_style(&self.style).children(
self.items self.items
.into_iter() .into_iter()
.enumerate() .enumerate()

View file

@ -233,6 +233,35 @@ impl Size {
} }
} }
/// Returns the size as a static string.
pub fn as_str(&self) -> &'static str {
match self {
Size::XSmall => "xs",
Size::Small => "sm",
Size::Medium => "md",
Size::Large => "lg",
Size::Size(_) => "custom",
}
}
/// Create a Size from a static string.
///
/// - "xs" or "xsmall"
/// - "sm" or "small"
/// - "md" or "medium"
/// - "lg" or "large"
///
/// Any other value will return Size::Medium.
pub fn from_str(size: &str) -> Self {
match size.to_lowercase().as_str() {
"xs" | "xsmall" => Size::XSmall,
"sm" | "small" => Size::Small,
"md" | "medium" => Size::Medium,
"lg" | "large" => Size::Large,
_ => Size::Medium,
}
}
/// Returns the height for table row. /// Returns the height for table row.
#[inline] #[inline]
pub fn table_row_height(&self) -> Pixels { pub fn table_row_height(&self) -> Pixels {
@ -762,4 +791,31 @@ mod tests {
Size::Size(px(10.)) Size::Size(px(10.))
); );
} }
#[test]
fn test_size_as_str() {
assert_eq!(Size::XSmall.as_str(), "xs");
assert_eq!(Size::Small.as_str(), "sm");
assert_eq!(Size::Medium.as_str(), "md");
assert_eq!(Size::Large.as_str(), "lg");
assert_eq!(Size::Size(px(15.)).as_str(), "custom");
}
#[test]
fn test_size_from_str() {
assert_eq!(Size::from_str("xs"), Size::XSmall);
assert_eq!(Size::from_str("xsmall"), Size::XSmall);
assert_eq!(Size::from_str("sm"), Size::Small);
assert_eq!(Size::from_str("small"), Size::Small);
assert_eq!(Size::from_str("md"), Size::Medium);
assert_eq!(Size::from_str("medium"), Size::Medium);
assert_eq!(Size::from_str("lg"), Size::Large);
assert_eq!(Size::from_str("large"), Size::Large);
assert_eq!(Size::from_str("unknown"), Size::Medium);
// Case insensitive
assert_eq!(Size::from_str("XS"), Size::XSmall);
assert_eq!(Size::from_str("SMALL"), Size::Small);
assert_eq!(Size::from_str("Md"), Size::Medium);
}
} }

View file

@ -87,6 +87,7 @@ impl RenderOnce for TextViewElement {
pub struct TextView { pub struct TextView {
id: ElementId, id: ElementId,
init_state: Option<InitState>, init_state: Option<InitState>,
raw: SharedString,
state: Entity<TextViewState>, state: Entity<TextViewState>,
style: StyleRefinement, style: StyleRefinement,
selectable: bool, selectable: bool,
@ -343,6 +344,14 @@ impl Text {
Self::TextView(e) => Self::TextView(Box::new(e.style(style))), Self::TextView(e) => Self::TextView(Box::new(e.style(style))),
} }
} }
/// Get the str
pub fn as_str(&self) -> &str {
match self {
Self::String(s) => s.as_str(),
Self::TextView(view) => view.raw.as_str(),
}
}
} }
impl RenderOnce for Text { impl RenderOnce for Text {
@ -403,11 +412,12 @@ impl TextView {
cx, cx,
); );
if let Some(tx) = &state.read(cx).tx { if let Some(tx) = &state.read(cx).tx {
let _ = tx.try_send(Update::Text(markdown)); let _ = tx.try_send(Update::Text(markdown.clone()));
} }
Self { Self {
id, id,
init_state: Some(init_state), init_state: Some(init_state),
raw: markdown.clone(),
style: StyleRefinement::default(), style: StyleRefinement::default(),
state, state,
selectable: false, selectable: false,
@ -432,13 +442,14 @@ impl TextView {
let init_state = let init_state =
Self::create_init_state(TextViewType::Html, &html, &highlight_theme, &state, cx); Self::create_init_state(TextViewType::Html, &html, &highlight_theme, &state, cx);
if let Some(tx) = &state.read(cx).tx { if let Some(tx) = &state.read(cx).tx {
let _ = tx.try_send(Update::Text(html)); let _ = tx.try_send(Update::Text(html.clone()));
} }
Self { Self {
id, id,
init_state: Some(init_state), init_state: Some(init_state),
style: StyleRefinement::default(), style: StyleRefinement::default(),
state, state,
raw: html,
selectable: false, selectable: false,
scrollable: false, scrollable: false,
} }
@ -446,15 +457,16 @@ impl TextView {
/// Set the source text of the text view. /// Set the source text of the text view.
pub fn text(mut self, raw: impl Into<SharedString>) -> Self { pub fn text(mut self, raw: impl Into<SharedString>) -> Self {
let raw: SharedString = raw.into();
if let Some(init_state) = &mut self.init_state { if let Some(init_state) = &mut self.init_state {
match init_state { match init_state {
InitState::Initializing { text, .. } => *text = raw.into(), InitState::Initializing { text, .. } => *text = raw.clone(),
InitState::Initialized { tx } => { InitState::Initialized { tx } => {
let _ = tx.try_send(Update::Text(raw.into())); let _ = tx.try_send(Update::Text(raw.clone()));
} }
} }
} }
self.raw = raw;
self self
} }

View file

@ -139,8 +139,8 @@ impl TreeItem {
} }
/// Add multiple child items to this tree item. /// Add multiple child items to this tree item.
pub fn children(mut self, children: impl Into<Vec<TreeItem>>) -> Self { pub fn children(mut self, children: impl IntoIterator<Item = TreeItem>) -> Self {
self.children.extend(children.into()); self.children.extend(children);
self self
} }

View file

@ -10,7 +10,7 @@ The GroupBox component is a versatile container that groups related content toge
## Import ## Import
```rust ```rust
use gpui_component::group_box::{GroupBox, GroupBoxVariant}; use gpui_component::group_box::{GroupBox, GroupBoxVariant, GroupBoxVariants as _};
``` ```
## Usage ## Usage
@ -159,29 +159,6 @@ GroupBox::new()
) )
``` ```
## API Reference
### GroupBox
| Method | Description |
| ---------------------- | ------------------------------------------- |
| `new()` | Create a new GroupBox with default settings |
| `variant(variant)` | Set the variant of the group box |
| `fill()` | Set to use Fill variant (with background) |
| `outline()` | Set to use Outline variant (with border) |
| `id(id)` | Set the element ID for the group box |
| `title(title)` | Set the title/header for the group box |
| `title_style(style)` | Customize the styling of the title |
| `content_style(style)` | Customize the styling of the content area |
### GroupBoxVariant
| Variant | Description |
| --------- | ---------------------------------------------- |
| `Normal` | Default variant with no background or border |
| `Fill` | Variant with background color and padding |
| `Outline` | Variant with border and padding, no background |
## Examples ## Examples
### Form Section ### Form Section

View file

@ -59,6 +59,7 @@ collapsed: false
- [Chart](chart) - Data visualization charts (Line, Bar, Area, Pie) - [Chart](chart) - Data visualization charts (Line, Bar, Area, Pie)
- [List](list) - List display with items - [List](list) - List display with items
- [Menu](menu) - Menu and context menu and dropdown menu. - [Menu](menu) - Menu and context menu and dropdown menu.
- [Settings](settings) - Settings UI
- [Table](table) - High-performance data tables - [Table](table) - High-performance data tables
- [Tabs](tabs) - Tabbed interface - [Tabs](tabs) - Tabbed interface
- [Tree](tree) - Hierarchical tree data display - [Tree](tree) - Hierarchical tree data display

View file

@ -0,0 +1,458 @@
---
title: Settings
description: A settings UI with grouped setting items and pages.
---
# Settings
> Since: v0.5.0
The Settings component provides a UI for managing application settings. It includes grouped setting items and pages.
We can search by title and description to filter the settings to display only relevant settings (Like this macOS, iOS Settings).
## Import
```rust
use gpui_component::setting::{Settings, SettingPage, SettingGroup, SettingItem, SettingField};
```
## Usage
### Build a settings
Here we have components that can be used to build a settings page.
- [Settings] - The main settings component that holds multiple setting pages.
- [SettingPage] - A page of related setting groups.
- [SettingGroup] - A group of related setting items based on [GroupBox] style.
- [SettingItem] - A single setting item with title, description, and field.
- [SettingField] - Provide different field types like Input, Dropdown, Switch, etc.
The layout of the settings is like this:
```
Settings
SettingPage
SettingGroup
SettingItem
Title
Description (optional)
SettingField
```
### Basic Settings
```rust
use gpui_component::setting::{Settings, SettingPage, SettingGroup, SettingItem, SettingField};
Settings::new("my-settings")
.pages(vec![
SettingPage::new("General")
.group(
SettingGroup::new()
.title("Basic Options")
.item(
SettingItem::new(
"Enable Feature",
SettingField::switch(
|cx: &App| true,
|val: bool, cx: &mut App| {
println!("Feature enabled: {}", val);
},
)
)
)
)
])
```
### With Multiple Pages
:::info
When you want default expland a page, you can use `default_open(true)` on the [SettingPage].
:::
```rust
Settings::new("app-settings")
.pages(vec![
SettingPage::new("General")
.default_open(true)
.group(SettingGroup::new().title("Appearance").items(vec![...])),
SettingPage::new("Software Update")
.group(SettingGroup::new().title("Updates").items(vec![...])),
SettingPage::new("About")
.group(SettingGroup::new().items(vec![...])),
])
```
### Group Variants
```rust
use gpui_component::group_box::GroupBoxVariant;
Settings::new("my-settings")
.with_group_variant(GroupBoxVariant::Outline)
.pages(vec![...])
Settings::new("my-settings")
.with_group_variant(GroupBoxVariant::Fill)
.pages(vec![...])
```
## Setting Page
### Basic Page
```rust
SettingPage::new("General")
.group(SettingGroup::new().title("Options").items(vec![...]))
```
### Multiple Groups
```rust
SettingPage::new("General")
.groups(vec![
SettingGroup::new().title("Appearance").items(vec![...]),
SettingGroup::new().title("Font").items(vec![...]),
SettingGroup::new().title("Other").items(vec![...]),
])
```
### Default Open
```rust
SettingPage::new("General")
.default_open(true)
.groups(vec![...])
```
### resettable
Enable reset functionality for a page:
```rust
SettingPage::new("General")
.resettable(true)
.groups(vec![...])
```
## Setting Group
### Basic Group
```rust
SettingGroup::new()
.title("Appearance")
.items(vec![
SettingItem::new(...),
SettingItem::new(...),
])
```
### Single Item
```rust
SettingGroup::new()
.title("Font")
.item(SettingItem::new(...))
```
### Without Title
```rust
SettingGroup::new()
.items(vec![...])
```
## Setting Item
### Basic Item
```rust
SettingItem::new("Title", SettingField::switch(...))
.description("Description text")
```
### Custom Element Item
You can create a fully custom setting item using `SettingItem::element`:
```rust
SettingItem::element(|options, _, _| {
h_flex()
.w_full()
.justify_between()
.child("Custom content")
.child(
Button::new("action")
.label("Action")
.with_size(options.size)
)
.into_any_element()
})
```
### Vertical Layout
By default, setting items use horizontal layout. Use `layout(Axis::Vertical)` for vertical layout:
```rust
SettingItem::new(
"CLI Path",
SettingField::input(...)
)
.layout(Axis::Vertical)
.description("This item uses vertical layout.")
```
### With Markdown Description
```rust
use gpui_component::text::TextView;
SettingItem::new(
"Documentation",
SettingField::element(...)
)
.description(TextView::markdown(
"desc",
"Rust doc for the `gpui-component` crate.",
window,
cx,
))
```
## Setting Fields
The [SettingField] enum provides different field types for various input needs.
### Switch
The switch field represents a `boolean` on/off state.
```rust
SettingItem::new(
"Dark Mode",
SettingField::switch(
|cx: &App| cx.theme().mode.is_dark(),
|val: bool, cx: &mut App| {
// Handle value change
},
)
.default_value(false)
)
```
### Checkbox
Like the switch, but uses a checkbox UI.
```rust
SettingItem::new(
"Auto Switch Theme",
SettingField::checkbox(
|cx: &App| AppSettings::global(cx).auto_switch_theme,
|val: bool, cx: &mut App| {
AppSettings::global_mut(cx).auto_switch_theme = val;
},
)
.default_value(false)
)
```
### Input
Display a single line text input.
```rust
SettingItem::new(
"CLI Path",
SettingField::input(
|cx: &App| AppSettings::global(cx).cli_path.clone(),
|val: SharedString, cx: &mut App| {
AppSettings::global_mut(cx).cli_path = val;
},
)
.default_value("/usr/local/bin/bash".into())
)
.layout(Axis::Vertical)
.description("Path to the CLI executable.")
```
### Dropdown
A dropdown with a list of options.
```rust
SettingItem::new(
"Font Family",
SettingField::dropdown(
vec![
("Arial".into(), "Arial".into()),
("Helvetica".into(), "Helvetica".into()),
("Times New Roman".into(), "Times New Roman".into()),
],
|cx: &App| AppSettings::global(cx).font_family.clone(),
|val: SharedString, cx: &mut App| {
AppSettings::global_mut(cx).font_family = val;
},
)
.default_value("Arial".into())
)
```
### NumberInput
```rust
use gpui_component::setting::NumberFieldOptions;
SettingItem::new(
"Font Size",
SettingField::number_input(
NumberFieldOptions {
min: 8.0,
max: 72.0,
..Default::default()
},
|cx: &App| AppSettings::global(cx).font_size,
|val: f64, cx: &mut App| {
AppSettings::global_mut(cx).font_size = val;
},
)
.default_value(14.0)
)
```
### Custom Element Field
```rust
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/example/repo");
})
})
)
```
## API Reference
- [Settings]
- [SettingPage]
- [SettingGroup]
- [SettingItem]
- [SettingField]
- [NumberFieldOptions]
### Sizing
Implements [Sizable] trait:
- `xsmall()` - Extra small size
- `small()` - Small size
- `medium()` - Medium size (default)
- `large()` - Large size
- `with_size(Size)` - Set specific size
## Examples
### Complete Settings Example
```rust
use gpui::{App, SharedString};
use gpui_component::{
Settings, SettingPage, SettingGroup, SettingItem, SettingField,
setting::NumberFieldOptions,
group_box::GroupBoxVariant,
Size,
};
Settings::new("app-settings")
.with_size(Size::Medium)
.with_group_variant(GroupBoxVariant::Outline)
.pages(vec![
SettingPage::new("General")
.resettable(true)
.default_open(true)
.groups(vec![
SettingGroup::new()
.title("Appearance")
.items(vec![
SettingItem::new(
"Dark Mode",
SettingField::switch(
|cx: &App| cx.theme().mode.is_dark(),
|val: bool, cx: &mut App| {
// Handle theme change
},
)
)
.description("Switch between light and dark themes."),
]),
SettingGroup::new()
.title("Font")
.items(vec![
SettingItem::new(
"Font Family",
SettingField::dropdown(
vec![
("Arial".into(), "Arial".into()),
("Helvetica".into(), "Helvetica".into()),
],
|cx: &App| "Arial".into(),
|val: SharedString, cx: &mut App| {
// Handle font change
},
)
),
SettingItem::new(
"Font Size",
SettingField::number_input(
NumberFieldOptions {
min: 8.0,
max: 72.0,
..Default::default()
},
|cx: &App| 14.0,
|val: f64, cx: &mut App| {
// Handle size change
},
)
),
]),
]),
SettingPage::new("Software Update")
.resettable(true)
.group(
SettingGroup::new()
.title("Updates")
.items(vec![
SettingItem::new(
"Auto Update",
SettingField::switch(
|cx: &App| true,
|val: bool, cx: &mut App| {
// Handle auto update
},
)
)
.description("Automatically download and install updates."),
])
),
])
```
[Settings]: https://docs.rs/gpui-component/latest/gpui_component/setting/struct.Settings.html
[SettingPage]: https://docs.rs/gpui-component/latest/gpui_component/setting/struct.SettingPage.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
[SettingField]: https://docs.rs/gpui-component/latest/gpui_component/setting/enum.SettingField.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