setting: Add Settings component. (#1632)
https://github.com/user-attachments/assets/3c6315d9-1183-419c-bb17-a8f7ac0cf25c
This commit is contained in:
parent
91de551ab0
commit
c8652f9010
32 changed files with 2517 additions and 60 deletions
14
crates/assets/assets/icons/redo-2.svg
Normal file
14
crates/assets/assets/icons/redo-2.svg
Normal 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 |
14
crates/assets/assets/icons/redo.svg
Normal file
14
crates/assets/assets/icons/redo.svg
Normal 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 |
14
crates/assets/assets/icons/undo-2.svg
Normal file
14
crates/assets/assets/icons/undo-2.svg
Normal 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 |
14
crates/assets/assets/icons/undo.svg
Normal file
14
crates/assets/assets/icons/undo.svg
Normal 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 |
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
386
crates/story/src/settings_story.rs
Normal file
386
crates/story/src/settings_story.rs
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
57
crates/ui/src/setting/fields/bool.rs
Normal file
57
crates/ui/src/setting/fields/bool.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
83
crates/ui/src/setting/fields/dropdown.rs
Normal file
83
crates/ui/src/setting/fields/dropdown.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
29
crates/ui/src/setting/fields/element.rs
Normal file
29
crates/ui/src/setting/fields/element.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
283
crates/ui/src/setting/fields/mod.rs
Normal file
283
crates/ui/src/setting/fields/mod.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
111
crates/ui/src/setting/fields/number.rs
Normal file
111
crates/ui/src/setting/fields/number.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
81
crates/ui/src/setting/fields/string.rs
Normal file
81
crates/ui/src/setting/fields/string.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
115
crates/ui/src/setting/group.rs
Normal file
115
crates/ui/src/setting/group.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
212
crates/ui/src/setting/item.rs
Normal file
212
crates/ui/src/setting/item.rs
Normal 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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
11
crates/ui/src/setting/mod.rs
Normal file
11
crates/ui/src/setting/mod.rs
Normal 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::*;
|
||||||
174
crates/ui/src/setting/page.rs
Normal file
174
crates/ui/src/setting/page.rs
Normal 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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
269
crates/ui/src/setting/settings.rs
Normal file
269
crates/ui/src/setting/settings.rs
Normal 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,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
458
docs/docs/components/settings.md
Normal file
458
docs/docs/components/settings.md
Normal 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
|
||||||
Loading…
Reference in a new issue