settings_ui: Correct stepper increment and enforce max value for Centered Layout Padding (#40751)

Delvin created

Closes #40748

This PR improves the Centered Layout Padding in settings ui by limiting the numeric stepper to be within valid values and adding a custom schema generated to improve the JSON LSP completions and warnings when editing the setting field manually.

Release Notes:

- settings ui: limit stepper increment for centered padding between 0 and 0.4 and increment by 0.05 now

---------

Co-authored by: Anthony Eid <anthony@zed.dev>

Change summary

crates/settings/src/settings_content/editor.rs    | 62 +++++++++++++++++
crates/settings/src/settings_content/workspace.rs | 12 +-
crates/settings_ui/src/settings_ui.rs             |  1 
crates/ui_input/src/number_field.rs               | 10 ++
crates/workspace/src/workspace.rs                 | 20 +++--
5 files changed, 89 insertions(+), 16 deletions(-)

Detailed changes

crates/settings/src/settings_content/editor.rs 🔗

@@ -850,3 +850,65 @@ impl From<f32> for InactiveOpacity {
         Self(x)
     }
 }
+
+/// Centered layout related setting (left/right).
+///
+/// Valid range: 0.0 to 0.4
+/// Default: 2.0
+#[derive(
+    Clone,
+    Copy,
+    Debug,
+    Serialize,
+    Deserialize,
+    MergeFrom,
+    PartialEq,
+    PartialOrd,
+    derive_more::FromStr,
+)]
+#[serde(transparent)]
+pub struct CenteredPaddingSettings(
+    #[serde(serialize_with = "serialize_f32_with_two_decimal_places")] pub f32,
+);
+
+impl CenteredPaddingSettings {
+    pub const MIN_PADDING: f32 = 0.0;
+    // This is an f64 so serde_json can give a type hint without random numbers in the back
+    pub const DEFAULT_PADDING: f64 = 0.2;
+    pub const MAX_PADDING: f32 = 0.4;
+}
+
+impl Display for CenteredPaddingSettings {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{:.2}", self.0)
+    }
+}
+
+impl From<f32> for CenteredPaddingSettings {
+    fn from(x: f32) -> Self {
+        Self(x)
+    }
+}
+
+impl Default for CenteredPaddingSettings {
+    fn default() -> Self {
+        Self(Self::DEFAULT_PADDING as f32)
+    }
+}
+
+impl schemars::JsonSchema for CenteredPaddingSettings {
+    fn schema_name() -> std::borrow::Cow<'static, str> {
+        "CenteredPaddingSettings".into()
+    }
+
+    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
+        use schemars::json_schema;
+        json_schema!({
+            "type": "number",
+            "minimum": Self::MIN_PADDING,
+            "maximum": Self::MAX_PADDING,
+            "default": Self::DEFAULT_PADDING,
+            "description": "Centered layout related setting (left/right)."
+        })
+    }
+}

crates/settings/src/settings_content/workspace.rs 🔗

@@ -7,8 +7,8 @@ use serde_with::skip_serializing_none;
 use settings_macros::MergeFrom;
 
 use crate::{
-    DelayMs, DockPosition, DockSide, InactiveOpacity, ScrollbarSettingsContent, ShowIndentGuides,
-    serialize_optional_f32_with_two_decimal_places,
+    CenteredPaddingSettings, DelayMs, DockPosition, DockSide, InactiveOpacity,
+    ScrollbarSettingsContent, ShowIndentGuides, serialize_optional_f32_with_two_decimal_places,
 };
 
 #[skip_serializing_none]
@@ -463,22 +463,20 @@ pub enum PaneSplitDirectionVertical {
     Right,
 }
 
-#[skip_serializing_none]
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Default)]
 #[serde(rename_all = "snake_case")]
+#[skip_serializing_none]
 pub struct CenteredLayoutSettings {
     /// The relative width of the left padding of the central pane from the
     /// workspace when the centered layout is used.
     ///
     /// Default: 0.2
-    #[serde(serialize_with = "serialize_optional_f32_with_two_decimal_places")]
-    pub left_padding: Option<f32>,
+    pub left_padding: Option<CenteredPaddingSettings>,
     // The relative width of the right padding of the central pane from the
     // workspace when the centered layout is used.
     ///
     /// Default: 0.2
-    #[serde(serialize_with = "serialize_optional_f32_with_two_decimal_places")]
-    pub right_padding: Option<f32>,
+    pub right_padding: Option<CenteredPaddingSettings>,
 }
 
 #[derive(

crates/settings_ui/src/settings_ui.rs 🔗

@@ -420,6 +420,7 @@ fn init_renderers(cx: &mut App) {
         .add_basic_renderer::<settings::CodeFade>(render_number_field)
         .add_basic_renderer::<settings::DelayMs>(render_number_field)
         .add_basic_renderer::<gpui::FontWeight>(render_number_field)
+        .add_basic_renderer::<settings::CenteredPaddingSettings>(render_number_field)
         .add_basic_renderer::<settings::InactiveOpacity>(render_number_field)
         .add_basic_renderer::<settings::MinimumContrast>(render_number_field)
         .add_basic_renderer::<settings::ShowScrollbar>(render_dropdown)

crates/ui_input/src/number_field.rs 🔗

@@ -8,7 +8,7 @@ use std::{
 use editor::{Editor, EditorStyle};
 use gpui::{ClickEvent, Entity, FocusHandle, Focusable, FontWeight, Modifiers};
 
-use settings::{CodeFade, DelayMs, InactiveOpacity, MinimumContrast};
+use settings::{CenteredPaddingSettings, CodeFade, DelayMs, InactiveOpacity, MinimumContrast};
 use ui::prelude::*;
 
 #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
@@ -71,6 +71,14 @@ impl_newtype_numeric_stepper!(CodeFade, 0.1, 0.2, 0.05, 0.0, 0.9);
 impl_newtype_numeric_stepper!(InactiveOpacity, 0.1, 0.2, 0.05, 0.0, 1.0);
 impl_newtype_numeric_stepper!(MinimumContrast, 1., 10., 0.5, 0.0, 106.0);
 impl_newtype_numeric_stepper!(DelayMs, 100, 500, 10, 0, 2000);
+impl_newtype_numeric_stepper!(
+    CenteredPaddingSettings,
+    0.05,
+    0.2,
+    0.1,
+    CenteredPaddingSettings::MIN_PADDING,
+    CenteredPaddingSettings::MAX_PADDING
+);
 
 macro_rules! impl_numeric_stepper_int {
     ($type:ident) => {

crates/workspace/src/workspace.rs 🔗

@@ -83,7 +83,7 @@ use remote::{
 use schemars::JsonSchema;
 use serde::Deserialize;
 use session::AppSession;
-use settings::{Settings, SettingsLocation, update_settings_file};
+use settings::{CenteredPaddingSettings, Settings, SettingsLocation, update_settings_file};
 use shared_screen::SharedScreen;
 use sqlez::{
     bindable::{Bind, Column, StaticColumnCount},
@@ -1199,9 +1199,6 @@ struct FollowerView {
 }
 
 impl Workspace {
-    const DEFAULT_PADDING: f32 = 0.2;
-    const MAX_PADDING: f32 = 0.4;
-
     pub fn new(
         workspace_id: Option<WorkspaceId>,
         project: Entity<Project>,
@@ -5938,8 +5935,11 @@ impl Workspace {
 
     fn adjust_padding(padding: Option<f32>) -> f32 {
         padding
-            .unwrap_or(Self::DEFAULT_PADDING)
-            .clamp(0.0, Self::MAX_PADDING)
+            .unwrap_or(CenteredPaddingSettings::default().0)
+            .clamp(
+                CenteredPaddingSettings::MIN_PADDING,
+                CenteredPaddingSettings::MAX_PADDING,
+            )
     }
 
     fn render_dock(
@@ -6423,8 +6423,12 @@ impl Render for Workspace {
         let paddings = if centered_layout {
             let settings = WorkspaceSettings::get_global(cx).centered_layout;
             (
-                render_padding(Self::adjust_padding(settings.left_padding)),
-                render_padding(Self::adjust_padding(settings.right_padding)),
+                render_padding(Self::adjust_padding(
+                    settings.left_padding.map(|padding| padding.0),
+                )),
+                render_padding(Self::adjust_padding(
+                    settings.right_padding.map(|padding| padding.0),
+                )),
             )
         } else {
             (None, None)