settings_ui: Get editor settings working (#37330)
Closes #ISSUE This PR includes the necessary work to get `EditorSettings` showing up in the settings UI. Including making the `path` field on `SettingsUiItem`'s optional so that top level items such as `EditorSettings` which have `Settings::KEY = None` (i.e. are treated like `serde(flatten)`) have their paths computed correctly for JSON reading/updating. It includes the first examples of a pattern I expect to continue with the `SettingsUi` work with respect to settings reorganization, that being adding missing defaults, and adding explicit values (or aliases) to settings which previously relied on `null` being a value for optional fields. Release Notes: - N/A *or* Added/Fixed/Improved ...
This commit is contained in:
@@ -188,8 +188,8 @@
|
||||
// 4. A box drawn around the following character
|
||||
// "hollow"
|
||||
//
|
||||
// Default: not set, defaults to "bar"
|
||||
"cursor_shape": null,
|
||||
// Default: "bar"
|
||||
"cursor_shape": "bar",
|
||||
// Determines when the mouse cursor should be hidden in an editor or input box.
|
||||
//
|
||||
// 1. Never hide the mouse cursor:
|
||||
@@ -282,8 +282,8 @@
|
||||
// - "warning"
|
||||
// - "info"
|
||||
// - "hint"
|
||||
// - null — allow all diagnostics (default)
|
||||
"diagnostics_max_severity": null,
|
||||
// - "all" — allow all diagnostics (default)
|
||||
"diagnostics_max_severity": "all",
|
||||
// Whether to show wrap guides (vertical rulers) in the editor.
|
||||
// Setting this to true will show a guide at the 'preferred_line_length' value
|
||||
// if 'soft_wrap' is set to 'preferred_line_length', and will show any
|
||||
|
||||
@@ -61,7 +61,9 @@ pub struct EditorSettings {
|
||||
}
|
||||
|
||||
/// How to render LSP `textDocument/documentColor` colors in the editor.
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DocumentColorsRenderMode {
|
||||
/// Do not query and render document colors.
|
||||
@@ -75,7 +77,7 @@ pub enum DocumentColorsRenderMode {
|
||||
Background,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CurrentLineHighlight {
|
||||
// Don't highlight the current line.
|
||||
@@ -89,7 +91,7 @@ pub enum CurrentLineHighlight {
|
||||
}
|
||||
|
||||
/// When to populate a new search's query based on the text under the cursor.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SeedQuerySetting {
|
||||
/// Always populate the search query with the word under the cursor.
|
||||
@@ -101,7 +103,9 @@ pub enum SeedQuerySetting {
|
||||
}
|
||||
|
||||
/// What to do when multibuffer is double clicked in some of its excerpts (parts of singleton buffers).
|
||||
#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[derive(
|
||||
Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DoubleClickInMultibuffer {
|
||||
/// Behave as a regular buffer and select the whole word.
|
||||
@@ -120,7 +124,9 @@ pub struct Jupyter {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[derive(
|
||||
Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct JupyterContent {
|
||||
/// Whether the Jupyter feature is enabled.
|
||||
@@ -292,7 +298,9 @@ pub struct ScrollbarAxes {
|
||||
}
|
||||
|
||||
/// Whether to allow drag and drop text selection in buffer.
|
||||
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[derive(
|
||||
Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi,
|
||||
)]
|
||||
pub struct DragAndDropSelection {
|
||||
/// When true, enables drag and drop text selection in buffer.
|
||||
///
|
||||
@@ -332,7 +340,7 @@ pub enum ScrollbarDiagnostics {
|
||||
/// The key to use for adding multiple cursors
|
||||
///
|
||||
/// Default: alt
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum MultiCursorModifier {
|
||||
Alt,
|
||||
@@ -343,7 +351,7 @@ pub enum MultiCursorModifier {
|
||||
/// Whether the editor will scroll beyond the last line.
|
||||
///
|
||||
/// Default: one_page
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ScrollBeyondLastLine {
|
||||
/// The editor will not scroll beyond the last line.
|
||||
@@ -357,7 +365,9 @@ pub enum ScrollBeyondLastLine {
|
||||
}
|
||||
|
||||
/// Default options for buffer and project search items.
|
||||
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[derive(
|
||||
Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi,
|
||||
)]
|
||||
pub struct SearchSettings {
|
||||
/// Whether to show the project search button in the status bar.
|
||||
#[serde(default = "default_true")]
|
||||
@@ -373,7 +383,9 @@ pub struct SearchSettings {
|
||||
}
|
||||
|
||||
/// What to do when go to definition yields no results.
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GoToDefinitionFallback {
|
||||
/// Disables the fallback.
|
||||
@@ -386,7 +398,9 @@ pub enum GoToDefinitionFallback {
|
||||
/// Determines when the mouse cursor should be hidden in an editor or input box.
|
||||
///
|
||||
/// Default: on_typing_and_movement
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum HideMouseMode {
|
||||
/// Never hide the mouse cursor
|
||||
@@ -401,7 +415,9 @@ pub enum HideMouseMode {
|
||||
/// Determines how snippets are sorted relative to other completion items.
|
||||
///
|
||||
/// Default: inline
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SnippetSortOrder {
|
||||
/// Place snippets at the top of the completion list
|
||||
@@ -416,6 +432,7 @@ pub enum SnippetSortOrder {
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
|
||||
#[settings_ui(group = "Editor")]
|
||||
pub struct EditorSettingsContent {
|
||||
/// Whether the cursor blinks in the editor.
|
||||
///
|
||||
@@ -424,7 +441,7 @@ pub struct EditorSettingsContent {
|
||||
/// Cursor shape for the default editor.
|
||||
/// Can be "bar", "block", "underline", or "hollow".
|
||||
///
|
||||
/// Default: None
|
||||
/// Default: bar
|
||||
pub cursor_shape: Option<CursorShape>,
|
||||
/// Determines when the mouse cursor should be hidden in an editor or input box.
|
||||
///
|
||||
@@ -601,7 +618,7 @@ pub struct EditorSettingsContent {
|
||||
}
|
||||
|
||||
// Status bar related settings
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)]
|
||||
pub struct StatusBarContent {
|
||||
/// Whether to display the active language button in the status bar.
|
||||
///
|
||||
@@ -614,7 +631,7 @@ pub struct StatusBarContent {
|
||||
}
|
||||
|
||||
// Toolbar related settings
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)]
|
||||
pub struct ToolbarContent {
|
||||
/// Whether to display breadcrumbs in the editor toolbar.
|
||||
///
|
||||
@@ -640,7 +657,9 @@ pub struct ToolbarContent {
|
||||
}
|
||||
|
||||
/// Scrollbar related settings
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default, SettingsUi,
|
||||
)]
|
||||
pub struct ScrollbarContent {
|
||||
/// When to show the scrollbar in the editor.
|
||||
///
|
||||
@@ -675,7 +694,9 @@ pub struct ScrollbarContent {
|
||||
}
|
||||
|
||||
/// Minimap related settings
|
||||
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[derive(
|
||||
Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, SettingsUi,
|
||||
)]
|
||||
pub struct MinimapContent {
|
||||
/// When to show the minimap in the editor.
|
||||
///
|
||||
@@ -723,7 +744,9 @@ pub struct ScrollbarAxesContent {
|
||||
}
|
||||
|
||||
/// Gutter related settings
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi,
|
||||
)]
|
||||
pub struct GutterContent {
|
||||
/// Whether to show line numbers in the gutter.
|
||||
///
|
||||
|
||||
@@ -32,7 +32,7 @@ use parking_lot::Mutex;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use settings::WorktreeId;
|
||||
use settings::{SettingsUi, WorktreeId};
|
||||
use smallvec::SmallVec;
|
||||
use smol::future::yield_now;
|
||||
use std::{
|
||||
@@ -173,7 +173,9 @@ pub enum IndentKind {
|
||||
}
|
||||
|
||||
/// The shape of a selection cursor.
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CursorShape {
|
||||
/// A vertical bar
|
||||
|
||||
@@ -281,7 +281,17 @@ impl Default for GlobalLspSettings {
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, JsonSchema,
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Ord,
|
||||
PartialOrd,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
JsonSchema,
|
||||
SettingsUi,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DiagnosticSeverity {
|
||||
@@ -290,6 +300,7 @@ pub enum DiagnosticSeverity {
|
||||
Error,
|
||||
Warning,
|
||||
Info,
|
||||
#[serde(alias = "all")]
|
||||
Hint,
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ mod keymap_file;
|
||||
mod settings_file;
|
||||
mod settings_json;
|
||||
mod settings_store;
|
||||
mod settings_ui;
|
||||
mod settings_ui_core;
|
||||
mod vscode_import;
|
||||
|
||||
use gpui::{App, Global};
|
||||
@@ -24,7 +24,7 @@ pub use settings_store::{
|
||||
InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsSources,
|
||||
SettingsStore,
|
||||
};
|
||||
pub use settings_ui::*;
|
||||
pub use settings_ui_core::*;
|
||||
// Re-export the derive macro
|
||||
pub use settings_ui_macros::SettingsUi;
|
||||
pub use vscode_import::{VsCodeSettings, VsCodeSettingsSource};
|
||||
|
||||
@@ -33,7 +33,7 @@ pub type EditorconfigProperties = ec4rs::Properties;
|
||||
use crate::{
|
||||
ActiveSettingsProfileName, ParameterizedJsonSchema, SettingsJsonSchemaParams, SettingsUiEntry,
|
||||
VsCodeSettings, WorktreeId, parse_json_with_comments, replace_value_in_json_text,
|
||||
settings_ui::SettingsUi, update_value_in_json_text,
|
||||
settings_ui_core::SettingsUi, update_value_in_json_text,
|
||||
};
|
||||
|
||||
/// A value that can be defined as a user setting.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::any::TypeId;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use fs::Fs;
|
||||
use gpui::{AnyElement, App, AppContext as _, ReadGlobal as _, Window};
|
||||
@@ -14,40 +16,26 @@ pub trait SettingsUi {
|
||||
|
||||
fn settings_ui_entry() -> SettingsUiEntry {
|
||||
SettingsUiEntry {
|
||||
item: SettingsUiEntryVariant::None,
|
||||
path: None,
|
||||
title: "None entry",
|
||||
item: SettingsUiItem::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SettingsUiEntry {
|
||||
// todo(settings_ui): move this back here once there isn't a None variant
|
||||
// pub path: &'static str,
|
||||
// pub title: &'static str,
|
||||
pub item: SettingsUiEntryVariant,
|
||||
}
|
||||
|
||||
pub enum SettingsUiEntryVariant {
|
||||
Group {
|
||||
path: &'static str,
|
||||
title: &'static str,
|
||||
items: Vec<SettingsUiEntry>,
|
||||
},
|
||||
Item {
|
||||
path: &'static str,
|
||||
item: SettingsUiItemSingle,
|
||||
},
|
||||
Dynamic {
|
||||
path: &'static str,
|
||||
options: Vec<SettingsUiEntry>,
|
||||
determine_option: fn(&serde_json::Value, &mut App) -> usize,
|
||||
},
|
||||
// todo(settings_ui): remove
|
||||
None,
|
||||
/// The path in the settings JSON file for this setting. Relative to parent
|
||||
/// None implies `#[serde(flatten)]` or `Settings::KEY.is_none()` for top level settings
|
||||
pub path: Option<&'static str>,
|
||||
pub title: &'static str,
|
||||
pub item: SettingsUiItem,
|
||||
}
|
||||
|
||||
pub enum SettingsUiItemSingle {
|
||||
SwitchField,
|
||||
NumericStepper,
|
||||
/// A numeric stepper for a specific type of number
|
||||
NumericStepper(NumType),
|
||||
ToggleGroup(&'static [&'static str]),
|
||||
/// This should be used when toggle group size > 6
|
||||
DropDown(&'static [&'static str]),
|
||||
@@ -96,16 +84,19 @@ impl<T: serde::Serialize> SettingsValue<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SettingsUiItemDynamic {
|
||||
pub options: Vec<SettingsUiEntry>,
|
||||
pub determine_option: fn(&serde_json::Value, &mut App) -> usize,
|
||||
}
|
||||
|
||||
pub struct SettingsUiItemGroup {
|
||||
pub items: Vec<SettingsUiEntry>,
|
||||
}
|
||||
|
||||
pub enum SettingsUiItem {
|
||||
Group {
|
||||
title: &'static str,
|
||||
items: Vec<SettingsUiEntry>,
|
||||
},
|
||||
Group(SettingsUiItemGroup),
|
||||
Single(SettingsUiItemSingle),
|
||||
Dynamic {
|
||||
options: Vec<SettingsUiEntry>,
|
||||
determine_option: fn(&serde_json::Value, &mut App) -> usize,
|
||||
},
|
||||
Dynamic(SettingsUiItemDynamic),
|
||||
None,
|
||||
}
|
||||
|
||||
@@ -121,8 +112,56 @@ impl SettingsUi for Option<bool> {
|
||||
}
|
||||
}
|
||||
|
||||
impl SettingsUi for u64 {
|
||||
fn settings_ui_item() -> SettingsUiItem {
|
||||
SettingsUiItem::Single(SettingsUiItemSingle::NumericStepper)
|
||||
#[repr(u8)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum NumType {
|
||||
U64 = 0,
|
||||
U32 = 1,
|
||||
F32 = 2,
|
||||
}
|
||||
pub static NUM_TYPE_NAMES: std::sync::LazyLock<[&'static str; NumType::COUNT]> =
|
||||
std::sync::LazyLock::new(|| NumType::ALL.map(NumType::type_name));
|
||||
pub static NUM_TYPE_IDS: std::sync::LazyLock<[TypeId; NumType::COUNT]> =
|
||||
std::sync::LazyLock::new(|| NumType::ALL.map(NumType::type_id));
|
||||
|
||||
impl NumType {
|
||||
const COUNT: usize = 3;
|
||||
const ALL: [NumType; Self::COUNT] = [NumType::U64, NumType::U32, NumType::F32];
|
||||
|
||||
pub fn type_id(self) -> TypeId {
|
||||
match self {
|
||||
NumType::U64 => TypeId::of::<u64>(),
|
||||
NumType::U32 => TypeId::of::<u32>(),
|
||||
NumType::F32 => TypeId::of::<f32>(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn type_name(self) -> &'static str {
|
||||
match self {
|
||||
NumType::U64 => std::any::type_name::<u64>(),
|
||||
NumType::U32 => std::any::type_name::<u32>(),
|
||||
NumType::F32 => std::any::type_name::<f32>(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! numeric_stepper_for_num_type {
|
||||
($type:ty, $num_type:ident) => {
|
||||
impl SettingsUi for $type {
|
||||
fn settings_ui_item() -> SettingsUiItem {
|
||||
SettingsUiItem::Single(SettingsUiItemSingle::NumericStepper(NumType::$num_type))
|
||||
}
|
||||
}
|
||||
|
||||
impl SettingsUi for Option<$type> {
|
||||
fn settings_ui_item() -> SettingsUiItem {
|
||||
SettingsUiItem::Single(SettingsUiItemSingle::NumericStepper(NumType::$num_type))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
numeric_stepper_for_num_type!(u64, U64);
|
||||
numeric_stepper_for_num_type!(u32, U32);
|
||||
// todo(settings_ui) is there a better ui for f32?
|
||||
numeric_stepper_for_num_type!(f32, F32);
|
||||
@@ -9,7 +9,10 @@ use command_palette_hooks::CommandPaletteFilter;
|
||||
use editor::EditorSettingsControls;
|
||||
use feature_flags::{FeatureFlag, FeatureFlagViewExt};
|
||||
use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, ReadGlobal, actions};
|
||||
use settings::{SettingsStore, SettingsUiEntryVariant, SettingsUiItemSingle, SettingsValue};
|
||||
use settings::{
|
||||
NumType, SettingsStore, SettingsUiEntry, SettingsUiItem, SettingsUiItemDynamic,
|
||||
SettingsUiItemGroup, SettingsUiItemSingle, SettingsValue,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use ui::{NumericStepper, SwitchField, ToggleButtonGroup, ToggleButtonSimple, prelude::*};
|
||||
use workspace::{
|
||||
@@ -134,7 +137,7 @@ impl Item for SettingsPage {
|
||||
|
||||
struct UiEntry {
|
||||
title: &'static str,
|
||||
path: &'static str,
|
||||
path: Option<&'static str>,
|
||||
_depth: usize,
|
||||
// a
|
||||
// b < a descendant range < a total descendant range
|
||||
@@ -182,14 +185,14 @@ struct SettingsUiTree {
|
||||
|
||||
fn build_tree_item(
|
||||
tree: &mut Vec<UiEntry>,
|
||||
entry: SettingsUiEntryVariant,
|
||||
entry: SettingsUiEntry,
|
||||
depth: usize,
|
||||
prev_index: Option<usize>,
|
||||
) {
|
||||
let index = tree.len();
|
||||
tree.push(UiEntry {
|
||||
title: "",
|
||||
path: "",
|
||||
title: entry.title,
|
||||
path: entry.path,
|
||||
_depth: depth,
|
||||
descendant_range: index + 1..index + 1,
|
||||
total_descendant_range: index + 1..index + 1,
|
||||
@@ -200,14 +203,8 @@ fn build_tree_item(
|
||||
if let Some(prev_index) = prev_index {
|
||||
tree[prev_index].next_sibling = Some(index);
|
||||
}
|
||||
match entry {
|
||||
SettingsUiEntryVariant::Group {
|
||||
path,
|
||||
title,
|
||||
items: group_items,
|
||||
} => {
|
||||
tree[index].path = path;
|
||||
tree[index].title = title;
|
||||
match entry.item {
|
||||
SettingsUiItem::Group(SettingsUiItemGroup { items: group_items }) => {
|
||||
for group_item in group_items {
|
||||
let prev_index = tree[index]
|
||||
.descendant_range
|
||||
@@ -215,22 +212,17 @@ fn build_tree_item(
|
||||
.not()
|
||||
.then_some(tree[index].descendant_range.end - 1);
|
||||
tree[index].descendant_range.end = tree.len() + 1;
|
||||
build_tree_item(tree, group_item.item, depth + 1, prev_index);
|
||||
build_tree_item(tree, group_item, depth + 1, prev_index);
|
||||
tree[index].total_descendant_range.end = tree.len();
|
||||
}
|
||||
}
|
||||
SettingsUiEntryVariant::Item { path, item } => {
|
||||
tree[index].path = path;
|
||||
// todo(settings_ui) create title from path in macro, and use here
|
||||
tree[index].title = path;
|
||||
SettingsUiItem::Single(item) => {
|
||||
tree[index].render = Some(item);
|
||||
}
|
||||
SettingsUiEntryVariant::Dynamic {
|
||||
path,
|
||||
SettingsUiItem::Dynamic(SettingsUiItemDynamic {
|
||||
options,
|
||||
determine_option,
|
||||
} => {
|
||||
tree[index].path = path;
|
||||
}) => {
|
||||
tree[index].select_descendant = Some(determine_option);
|
||||
for option in options {
|
||||
let prev_index = tree[index]
|
||||
@@ -239,11 +231,11 @@ fn build_tree_item(
|
||||
.not()
|
||||
.then_some(tree[index].descendant_range.end - 1);
|
||||
tree[index].descendant_range.end = tree.len() + 1;
|
||||
build_tree_item(tree, option.item, depth + 1, prev_index);
|
||||
build_tree_item(tree, option, depth + 1, prev_index);
|
||||
tree[index].total_descendant_range.end = tree.len();
|
||||
}
|
||||
}
|
||||
SettingsUiEntryVariant::None => {
|
||||
SettingsUiItem::None => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -255,21 +247,17 @@ impl SettingsUiTree {
|
||||
let mut tree = vec![];
|
||||
let mut root_entry_indices = vec![];
|
||||
for item in settings_store.settings_ui_items() {
|
||||
if matches!(item.item, SettingsUiEntryVariant::None) {
|
||||
if matches!(item.item, SettingsUiItem::None)
|
||||
// todo(settings_ui): How to handle top level single items? BaseKeymap is in this category. Probably need a way to
|
||||
// link them to other groups
|
||||
|| matches!(item.item, SettingsUiItem::Single(_))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
assert!(
|
||||
matches!(item.item, SettingsUiEntryVariant::Group { .. }),
|
||||
"top level items must be groups: {:?}",
|
||||
match item.item {
|
||||
SettingsUiEntryVariant::Item { path, .. } => path,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
);
|
||||
let prev_root_entry_index = root_entry_indices.last().copied();
|
||||
root_entry_indices.push(tree.len());
|
||||
build_tree_item(&mut tree, item.item, 0, prev_root_entry_index);
|
||||
build_tree_item(&mut tree, item, 0, prev_root_entry_index);
|
||||
}
|
||||
|
||||
root_entry_indices.sort_by_key(|i| tree[*i].title);
|
||||
@@ -314,9 +302,12 @@ fn render_content(
|
||||
.size_full()
|
||||
.child(Label::new(SharedString::new_static("No settings found")).color(Color::Error));
|
||||
};
|
||||
let mut content = v_flex().size_full().gap_4();
|
||||
let mut content = v_flex().size_full().gap_4().overflow_hidden();
|
||||
|
||||
let mut path = smallvec::smallvec![active_entry.path];
|
||||
let mut path = smallvec::smallvec![];
|
||||
if let Some(active_entry_path) = active_entry.path {
|
||||
path.push(active_entry_path);
|
||||
}
|
||||
let mut entry_index_queue = VecDeque::new();
|
||||
|
||||
if let Some(child_index) = active_entry.first_descendant_index() {
|
||||
@@ -331,7 +322,11 @@ fn render_content(
|
||||
while let Some(index) = entry_index_queue.pop_front() {
|
||||
// todo(settings_ui): subgroups?
|
||||
let child = &tree.entries[index];
|
||||
path.push(child.path);
|
||||
let mut pushed_path = false;
|
||||
if let Some(child_path) = child.path {
|
||||
path.push(child_path);
|
||||
pushed_path = true;
|
||||
}
|
||||
let settings_value = settings_value_from_settings_and_path(
|
||||
path.clone(),
|
||||
// PERF: how to structure this better? There feels like there's a way to avoid the clone
|
||||
@@ -347,7 +342,9 @@ fn render_content(
|
||||
entry_index_queue.push_front(descendant_index);
|
||||
}
|
||||
}
|
||||
path.pop();
|
||||
if pushed_path {
|
||||
path.pop();
|
||||
}
|
||||
let Some(child_render) = child.render.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
@@ -433,8 +430,8 @@ fn render_item_single(
|
||||
SettingsUiItemSingle::SwitchField => {
|
||||
render_any_item(settings_value, render_switch_field, window, cx)
|
||||
}
|
||||
SettingsUiItemSingle::NumericStepper => {
|
||||
render_any_item(settings_value, render_numeric_stepper, window, cx)
|
||||
SettingsUiItemSingle::NumericStepper(num_type) => {
|
||||
render_any_numeric_stepper(settings_value, *num_type, window, cx)
|
||||
}
|
||||
SettingsUiItemSingle::ToggleGroup(variants) => {
|
||||
render_toggle_button_group(settings_value, variants, window, cx)
|
||||
@@ -468,6 +465,7 @@ fn downcast_any_item<T: serde::de::DeserializeOwned>(
|
||||
.map(|value| serde_json::from_value::<T>(value).expect("value is not a T"));
|
||||
// todo(settings_ui) Create test that constructs UI tree, and asserts that all elements have default values
|
||||
let default_value = serde_json::from_value::<T>(settings_value.default_value)
|
||||
.with_context(|| format!("path: {:?}", settings_value.path.join(".")))
|
||||
.expect("default value is not an Option<T>");
|
||||
let deserialized_setting_value = SettingsValue {
|
||||
title: settings_value.title,
|
||||
@@ -488,14 +486,62 @@ fn render_any_item<T: serde::de::DeserializeOwned>(
|
||||
render_fn(deserialized_setting_value, window, cx)
|
||||
}
|
||||
|
||||
fn render_numeric_stepper(
|
||||
value: SettingsValue<u64>,
|
||||
fn render_any_numeric_stepper(
|
||||
settings_value: SettingsValue<serde_json::Value>,
|
||||
num_type: NumType,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
match num_type {
|
||||
NumType::U64 => render_numeric_stepper::<u64>(
|
||||
downcast_any_item(settings_value),
|
||||
u64::saturating_sub,
|
||||
u64::saturating_add,
|
||||
|n| {
|
||||
serde_json::Number::try_from(n)
|
||||
.context("Failed to convert u64 to serde_json::Number")
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
NumType::U32 => render_numeric_stepper::<u32>(
|
||||
downcast_any_item(settings_value),
|
||||
u32::saturating_sub,
|
||||
u32::saturating_add,
|
||||
|n| {
|
||||
serde_json::Number::try_from(n)
|
||||
.context("Failed to convert u32 to serde_json::Number")
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
NumType::F32 => render_numeric_stepper::<f32>(
|
||||
downcast_any_item(settings_value),
|
||||
|a, b| a - b,
|
||||
|a, b| a + b,
|
||||
|n| {
|
||||
serde_json::Number::from_f64(n as f64)
|
||||
.context("Failed to convert f32 to serde_json::Number")
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_numeric_stepper<
|
||||
T: serde::de::DeserializeOwned + std::fmt::Display + Copy + From<u8> + 'static,
|
||||
>(
|
||||
value: SettingsValue<T>,
|
||||
saturating_sub: fn(T, T) -> T,
|
||||
saturating_add: fn(T, T) -> T,
|
||||
to_serde_number: fn(T) -> anyhow::Result<serde_json::Number>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) -> AnyElement {
|
||||
let id = element_id_from_path(&value.path);
|
||||
let path = value.path.clone();
|
||||
let num = value.value.unwrap_or_else(|| value.default_value);
|
||||
let num = *value.read();
|
||||
|
||||
NumericStepper::new(
|
||||
id,
|
||||
@@ -503,8 +549,7 @@ fn render_numeric_stepper(
|
||||
{
|
||||
let path = value.path.clone();
|
||||
move |_, _, cx| {
|
||||
let Some(number) = serde_json::Number::from_u128(num.saturating_sub(1) as u128)
|
||||
else {
|
||||
let Some(number) = to_serde_number(saturating_sub(num, 1.into())).ok() else {
|
||||
return;
|
||||
};
|
||||
let new_value = serde_json::Value::Number(number);
|
||||
@@ -512,7 +557,7 @@ fn render_numeric_stepper(
|
||||
}
|
||||
},
|
||||
move |_, _, cx| {
|
||||
let Some(number) = serde_json::Number::from_u128(num.saturating_add(1) as u128) else {
|
||||
let Some(number) = to_serde_number(saturating_add(num, 1.into())).ok() else {
|
||||
return;
|
||||
};
|
||||
|
||||
|
||||
@@ -57,30 +57,22 @@ pub fn derive_settings_ui(input: proc_macro::TokenStream) -> proc_macro::TokenSt
|
||||
}
|
||||
}
|
||||
|
||||
if path_name.is_none() && group_name.is_some() {
|
||||
// todo(settings_ui) derive path from settings
|
||||
panic!("path is required when group is specified");
|
||||
}
|
||||
let ui_item_fn_body = generate_ui_item_body(group_name.as_ref(), path_name.as_ref(), &input);
|
||||
|
||||
let ui_render_fn_body = generate_ui_item_body(group_name.as_ref(), path_name.as_ref(), &input);
|
||||
// todo(settings_ui): Reformat title to be title case with spaces if group name not present,
|
||||
// and make group name optional, repurpose group as tag indicating item is group
|
||||
let title = group_name.unwrap_or(input.ident.to_string());
|
||||
|
||||
let settings_ui_item_fn_body = path_name
|
||||
.as_ref()
|
||||
.map(|path_name| map_ui_item_to_render(path_name, quote! { Self }))
|
||||
.unwrap_or(quote! {
|
||||
settings::SettingsUiEntry {
|
||||
item: settings::SettingsUiEntryVariant::None
|
||||
}
|
||||
});
|
||||
let ui_entry_fn_body = map_ui_item_to_entry(path_name.as_deref(), &title, quote! { Self });
|
||||
|
||||
let expanded = quote! {
|
||||
impl #impl_generics settings::SettingsUi for #name #ty_generics #where_clause {
|
||||
fn settings_ui_item() -> settings::SettingsUiItem {
|
||||
#ui_render_fn_body
|
||||
#ui_item_fn_body
|
||||
}
|
||||
|
||||
fn settings_ui_entry() -> settings::SettingsUiEntry {
|
||||
#settings_ui_item_fn_body
|
||||
#ui_entry_fn_body
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -114,27 +106,14 @@ fn option_inner_type(ty: TokenStream) -> Option<TokenStream> {
|
||||
return Some(ty.to_token_stream());
|
||||
}
|
||||
|
||||
fn map_ui_item_to_render(path: &str, ty: TokenStream) -> TokenStream {
|
||||
fn map_ui_item_to_entry(path: Option<&str>, title: &str, ty: TokenStream) -> TokenStream {
|
||||
let ty = extract_type_from_option(ty);
|
||||
let path = path.map_or_else(|| quote! {None}, |path| quote! {Some(#path)});
|
||||
quote! {
|
||||
settings::SettingsUiEntry {
|
||||
item: match #ty::settings_ui_item() {
|
||||
settings::SettingsUiItem::Group{title, items} => settings::SettingsUiEntryVariant::Group {
|
||||
title,
|
||||
path: #path,
|
||||
items,
|
||||
},
|
||||
settings::SettingsUiItem::Single(item) => settings::SettingsUiEntryVariant::Item {
|
||||
path: #path,
|
||||
item,
|
||||
},
|
||||
settings::SettingsUiItem::Dynamic{ options, determine_option } => settings::SettingsUiEntryVariant::Dynamic {
|
||||
path: #path,
|
||||
options,
|
||||
determine_option,
|
||||
},
|
||||
settings::SettingsUiItem::None => settings::SettingsUiEntryVariant::None,
|
||||
}
|
||||
title: #title,
|
||||
path: #path,
|
||||
item: #ty::settings_ui_item(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,16 +125,10 @@ fn generate_ui_item_body(
|
||||
) -> TokenStream {
|
||||
match (group_name, path_name, &input.data) {
|
||||
(_, _, Data::Union(_)) => unimplemented!("Derive SettingsUi for Unions"),
|
||||
(None, None, Data::Struct(_)) => quote! {
|
||||
(None, _, Data::Struct(_)) => quote! {
|
||||
settings::SettingsUiItem::None
|
||||
},
|
||||
(Some(_), None, Data::Struct(_)) => quote! {
|
||||
settings::SettingsUiItem::None
|
||||
},
|
||||
(None, Some(_), Data::Struct(_)) => quote! {
|
||||
settings::SettingsUiItem::None
|
||||
},
|
||||
(Some(group_name), _, Data::Struct(data_struct)) => {
|
||||
(Some(_), _, Data::Struct(data_struct)) => {
|
||||
let fields = data_struct
|
||||
.fields
|
||||
.iter()
|
||||
@@ -180,10 +153,11 @@ fn generate_ui_item_body(
|
||||
field.ty.to_token_stream(),
|
||||
)
|
||||
})
|
||||
.map(|(name, ty)| map_ui_item_to_render(&name, ty));
|
||||
// todo(settings_ui): Re-format field name as nice title, and support setting different title with attr
|
||||
.map(|(name, ty)| map_ui_item_to_entry(Some(&name), &name, ty));
|
||||
|
||||
quote! {
|
||||
settings::SettingsUiItem::Group{ title: #group_name, items: vec![#(#fields),*] }
|
||||
settings::SettingsUiItem::Group(settings::SettingsUiItemGroup{ items: vec![#(#fields),*] })
|
||||
}
|
||||
}
|
||||
(None, _, Data::Enum(data_enum)) => {
|
||||
|
||||
Reference in New Issue
Block a user