Detailed changes
@@ -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! {
- settings::SettingsUiItem::None
- },
- (Some(_), None, Data::Struct(_)) => quote! {
- settings::SettingsUiItem::None
- },
- (None, Some(_), Data::Struct(_)) => quote! {
+ (None, _, 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)) => {