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,
|
||||
button::Button,
|
||||
checkbox::Checkbox,
|
||||
group_box::GroupBox,
|
||||
group_box::{GroupBox, GroupBoxVariants as _},
|
||||
h_flex,
|
||||
slider::{Slider, SliderState},
|
||||
v_flex,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use gpui::{
|
|||
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::tag::Tag;
|
||||
use gpui_component::{ActiveTheme, IconName, StyledExt, h_flex};
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
use gpui::{
|
||||
relative, App, AppContext, Context, Entity, Focusable, IntoElement, ParentElement, Render,
|
||||
StyleRefinement, Styled, Window,
|
||||
App, AppContext, Context, Entity, Focusable, IntoElement, ParentElement, Render,
|
||||
StyleRefinement, Styled, Window, relative,
|
||||
};
|
||||
|
||||
use gpui_component::{
|
||||
ActiveTheme as _, StyledExt,
|
||||
button::{Button, ButtonVariants},
|
||||
checkbox::Checkbox,
|
||||
group_box::GroupBox,
|
||||
group_box::{GroupBox, GroupBoxVariants as _},
|
||||
h_flex,
|
||||
radio::{Radio, RadioGroup},
|
||||
switch::Switch,
|
||||
text::TextView,
|
||||
v_flex, ActiveTheme as _, StyledExt,
|
||||
v_flex,
|
||||
};
|
||||
|
||||
use crate::section;
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ mod radio_story;
|
|||
mod resizable_story;
|
||||
mod scrollable_story;
|
||||
mod select_story;
|
||||
mod settings_story;
|
||||
mod sheet_story;
|
||||
mod sidebar_story;
|
||||
mod skeleton_story;
|
||||
|
|
@ -93,6 +94,7 @@ pub use resizable_story::ResizableStory;
|
|||
pub use scrollable_story::ScrollableStory;
|
||||
pub use select_story::SelectStory;
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub use settings_story::SettingsStory;
|
||||
pub use sheet_story::SheetStory;
|
||||
pub use sidebar_story::SidebarStory;
|
||||
pub use skeleton_story::SkeletonStory;
|
||||
|
|
@ -115,7 +117,7 @@ use gpui_component::{
|
|||
ActiveTheme, IconName, Root, TitleBar, WindowExt,
|
||||
button::Button,
|
||||
dock::{Panel, PanelControl, PanelEvent, PanelInfo, PanelState, TitleStyle, register_panel},
|
||||
group_box::GroupBox,
|
||||
group_box::{GroupBox, GroupBoxVariants as _},
|
||||
h_flex,
|
||||
menu::PopupMenu,
|
||||
notification::Notification,
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ impl Gallery {
|
|||
StoryContainer::panel::<ResizableStory>(window, cx),
|
||||
StoryContainer::panel::<ScrollableStory>(window, cx),
|
||||
StoryContainer::panel::<SelectStory>(window, cx),
|
||||
StoryContainer::panel::<SettingsStory>(window, cx),
|
||||
StoryContainer::panel::<SheetStory>(window, cx),
|
||||
StoryContainer::panel::<SidebarStory>(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
|
||||
zh-CN: 显示代码操作
|
||||
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,
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// an optional title to groups related content together.
|
||||
#[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.
|
||||
pub fn id(mut self, id: impl Into<ElementId>) -> Self {
|
||||
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 {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
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,
|
||||
PanelRightOpen,
|
||||
Plus,
|
||||
Redo,
|
||||
Redo2,
|
||||
Replace,
|
||||
ResizeCorner,
|
||||
Search,
|
||||
|
|
@ -100,6 +102,8 @@ pub enum IconName {
|
|||
ThumbsDown,
|
||||
ThumbsUp,
|
||||
TriangleAlert,
|
||||
Undo,
|
||||
Undo2,
|
||||
User,
|
||||
WindowClose,
|
||||
WindowMaximize,
|
||||
|
|
@ -180,6 +184,8 @@ impl IconNamed for IconName {
|
|||
Self::PanelRightClose => "icons/panel-right-close.svg",
|
||||
Self::PanelRightOpen => "icons/panel-right-open.svg",
|
||||
Self::Plus => "icons/plus.svg",
|
||||
Self::Redo => "icons/redo.svg",
|
||||
Self::Redo2 => "icons/redo-2.svg",
|
||||
Self::Replace => "icons/replace.svg",
|
||||
Self::ResizeCorner => "icons/resize-corner.svg",
|
||||
Self::Search => "icons/search.svg",
|
||||
|
|
@ -194,6 +200,8 @@ impl IconNamed for IconName {
|
|||
Self::ThumbsDown => "icons/thumbs-down.svg",
|
||||
Self::ThumbsUp => "icons/thumbs-up.svg",
|
||||
Self::TriangleAlert => "icons/triangle-alert.svg",
|
||||
Self::Undo => "icons/undo.svg",
|
||||
Self::Undo2 => "icons/undo-2.svg",
|
||||
Self::User => "icons/user.svg",
|
||||
Self::WindowClose => "icons/window-close.svg",
|
||||
Self::WindowMaximize => "icons/window-maximize.svg",
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ pub mod radio;
|
|||
pub mod resizable;
|
||||
pub mod scroll;
|
||||
pub mod select;
|
||||
pub mod setting;
|
||||
pub mod sheet;
|
||||
pub mod sidebar;
|
||||
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::{
|
||||
div, percentage, prelude::FluentBuilder as _, AnyElement, App, ClickEvent, ElementId,
|
||||
InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, SharedString,
|
||||
StatefulInteractiveElement as _, Styled as _, Window,
|
||||
StatefulInteractiveElement as _, StyleRefinement, Styled, Window,
|
||||
};
|
||||
use std::rc::Rc;
|
||||
|
||||
/// Menu for the [`super::Sidebar`]
|
||||
#[derive(IntoElement)]
|
||||
pub struct SidebarMenu {
|
||||
style: StyleRefinement,
|
||||
collapsed: bool,
|
||||
items: Vec<SidebarMenuItem>,
|
||||
}
|
||||
|
|
@ -20,6 +21,7 @@ impl SidebarMenu {
|
|||
/// Create a new SidebarMenu
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
style: StyleRefinement::default(),
|
||||
items: Vec::new(),
|
||||
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 {
|
||||
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
|
||||
.into_iter()
|
||||
.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.
|
||||
#[inline]
|
||||
pub fn table_row_height(&self) -> Pixels {
|
||||
|
|
@ -762,4 +791,31 @@ mod tests {
|
|||
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 {
|
||||
id: ElementId,
|
||||
init_state: Option<InitState>,
|
||||
raw: SharedString,
|
||||
state: Entity<TextViewState>,
|
||||
style: StyleRefinement,
|
||||
selectable: bool,
|
||||
|
|
@ -343,6 +344,14 @@ impl Text {
|
|||
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 {
|
||||
|
|
@ -403,11 +412,12 @@ impl TextView {
|
|||
cx,
|
||||
);
|
||||
if let Some(tx) = &state.read(cx).tx {
|
||||
let _ = tx.try_send(Update::Text(markdown));
|
||||
let _ = tx.try_send(Update::Text(markdown.clone()));
|
||||
}
|
||||
Self {
|
||||
id,
|
||||
init_state: Some(init_state),
|
||||
raw: markdown.clone(),
|
||||
style: StyleRefinement::default(),
|
||||
state,
|
||||
selectable: false,
|
||||
|
|
@ -432,13 +442,14 @@ impl TextView {
|
|||
let init_state =
|
||||
Self::create_init_state(TextViewType::Html, &html, &highlight_theme, &state, cx);
|
||||
if let Some(tx) = &state.read(cx).tx {
|
||||
let _ = tx.try_send(Update::Text(html));
|
||||
let _ = tx.try_send(Update::Text(html.clone()));
|
||||
}
|
||||
Self {
|
||||
id,
|
||||
init_state: Some(init_state),
|
||||
style: StyleRefinement::default(),
|
||||
state,
|
||||
raw: html,
|
||||
selectable: false,
|
||||
scrollable: false,
|
||||
}
|
||||
|
|
@ -446,15 +457,16 @@ impl TextView {
|
|||
|
||||
/// Set the source text of the text view.
|
||||
pub fn text(mut self, raw: impl Into<SharedString>) -> Self {
|
||||
let raw: SharedString = raw.into();
|
||||
if let Some(init_state) = &mut self.init_state {
|
||||
match init_state {
|
||||
InitState::Initializing { text, .. } => *text = raw.into(),
|
||||
InitState::Initializing { text, .. } => *text = raw.clone(),
|
||||
InitState::Initialized { tx } => {
|
||||
let _ = tx.try_send(Update::Text(raw.into()));
|
||||
let _ = tx.try_send(Update::Text(raw.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.raw = raw;
|
||||
self
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -139,8 +139,8 @@ impl TreeItem {
|
|||
}
|
||||
|
||||
/// Add multiple child items to this tree item.
|
||||
pub fn children(mut self, children: impl Into<Vec<TreeItem>>) -> Self {
|
||||
self.children.extend(children.into());
|
||||
pub fn children(mut self, children: impl IntoIterator<Item = TreeItem>) -> Self {
|
||||
self.children.extend(children);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ The GroupBox component is a versatile container that groups related content toge
|
|||
## Import
|
||||
|
||||
```rust
|
||||
use gpui_component::group_box::{GroupBox, GroupBoxVariant};
|
||||
use gpui_component::group_box::{GroupBox, GroupBoxVariant, GroupBoxVariants as _};
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
### Form Section
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ collapsed: false
|
|||
- [Chart](chart) - Data visualization charts (Line, Bar, Area, Pie)
|
||||
- [List](list) - List display with items
|
||||
- [Menu](menu) - Menu and context menu and dropdown menu.
|
||||
- [Settings](settings) - Settings UI
|
||||
- [Table](table) - High-performance data tables
|
||||
- [Tabs](tabs) - Tabbed interface
|
||||
- [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