Repository: microsoft/terminal (language: cpp)

Issue:
Atlas Engine: Way to get back spaces between lines?
### Windows Terminal version

1.16.220921001-preview

### Windows build number

10.0.19044.0

### Other Software

[helix-editor](https://github.com/helix-editor/helix)

### Steps to reproduce

Font: [Overpass Mono](https://github.com/RedHatOfficial/Overpass)

### Expected Behavior

![without atlas terminal](https://user-images.githubusercontent.com/96306150/192085377-e0366cda-2557-4b1c-98f0-78d74eeb5863.jpg)

![2022-09-24-000125](https://user-images.githubusercontent.com/96306150/192086078-ebbdbbbc-a21d-464b-8ec8-8d822ec272c5.jpg)

This is without Atlas Engine in the current terminal preview. In the helix editor there are some space around the 1.

### Actual Behavior

![with atlas engine](https://user-images.githubusercontent.com/96306150/192085388-b985e6e0-ea85-4e84-9680-ae2edc2d138b.jpg)

![2022-09-24-000126](https://user-images.githubusercontent.com/96306150/192086098-3a2b9768-7e93-4289-ab2e-98de0e50f644.jpg)


This is with Atlas Engine, latest terminal preview.

A small query -
Is there a way to get back the space above the 1? I understand that it is intentional that line-gaps are intentionally ignored as per the release notes. Or is there any advantage over not having the space around the one?


Discussion / hints:
Please close the issue if I misunderstood this feature.
It's somewhat ridiculous how annoying it can be to deal with fonts... 😅
So some fonts incorrectly specify a line gap, even though they don't want one, and some fonts do specify one without which there's no proper line spacing. Additionally I assumed that terminals technically don't have any gaps between character cells anyways (https://github.com/microsoft/terminal/pull/14014). So what should we do now?

I'll try to download your font and try to find a solution that works for both, "Terminus TTF" and "Overpass Mono" simultaneously, but I suspect either of the two fonts will need to be broken, depending on whether we want to support line gaps or not.
The same occurs with "Comic Code" ($0 "demo-purchase" available from https://www.myfonts.com/products/demo-regular-comic-code-474333 - actually to the point of cutting of the top 1 or 2 rows of pixels on high characters (like "f" and "8")

Yes, I understand that "technically" characters fill an entire "cell", but IMO:
* the release-version works just fine (i.e as the font says)
* there is no need to ignore it due to "visual considerations" like empty space between blocks (the user chooses the font)
* if you really want it the add an option to enable/disable the extra line-height
* or just add a custom "line-height" setting to allow users to have extra spacing between lines (some fonts are really tight, and some people have hard time reading -> "Accessability-feature")

So, please don't cut this extra line-height

Fix patch:
diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json
index 36b20927a6b..bac66b8066e 100644
--- a/doc/cascadia/profiles.schema.json
+++ b/doc/cascadia/profiles.schema.json
@@ -24,6 +24,13 @@
       "pattern": "^(-?\\d+)?(,\\s?(-?\\d+)?)?$",
       "type": "string"
     },
+    "CSSLengthPercentage": {
+      "pattern": "^[+-]?\\d+(?:\\.\\d+)?(?:%|ch|pt|px)?$",
+      "type": [
+        "string",
+        "null"
+      ]
+    },
     "DynamicProfileSource": {
       "enum": [
         "Windows.Terminal.Wsl",
@@ -314,6 +321,14 @@
             }
           },
           "additionalProperties": false
+        },
+        "cellWidth": {
+          "$ref": "#/$defs/CSSLengthPercentage",
+          "description": "Override the width of the terminal's cells. The override works similar to CSS' letter-spacing. It defaults to the natural glyph advance width of the primary font rounded to the nearest pixel."
+        },
+        "cellHeight": {
+          "$ref": "#/$defs/CSSLengthPercentage",
+          "description": "Override the height of the terminal's cells. The override works similar to CSS' line-height. Defaults to the sum of the natural glyph ascend, descend and line-gap of the primary font rounded to the nearest pixel. The default is usually quite close to setting this to 1.2."
         }
       },
       "type": "object"
diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp
index 5bf00b07982..b82709af5fd 100644
--- a/src/cascadia/TerminalControl/ControlCore.cpp
+++ b/src/cascadia/TerminalControl/ControlCore.cpp
@@ -728,6 +728,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
 
         auto lock = _terminal->LockForWriting();
 
+        _cellWidth = CSSLengthPercentage::FromString(_settings->CellWidth().c_str());
+        _cellHeight = CSSLengthPercentage::FromString(_settings->CellHeight().c_str());
         _runtimeOpacity = std::nullopt;
 
         // Manually turn off acrylic if they turn off transparency.
@@ -880,6 +882,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         _actualFont = { fontFace, 0, fontWeight.Weight, _desiredFont.GetEngineSize(), CP_UTF8, false };
         _actualFontFaceName = { fontFace };
 
+        _desiredFont.SetCellSize(_cellWidth, _cellHeight);
+
         const auto before = _actualFont.GetSize();
         _updateFont();
         const auto after = _actualFont.GetSize();
diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h
index 890e9ec3d51..f9c7792f6ae 100644
--- a/src/cascadia/TerminalControl/ControlCore.h
+++ b/src/cascadia/TerminalControl/ControlCore.h
@@ -258,6 +258,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         FontInfoDesired _desiredFont;
         FontInfo _actualFont;
         winrt::hstring _actualFontFaceName;
+        CSSLengthPercentage _cellWidth;
+        CSSLengthPercentage _cellHeight;
 
         // storage location for the leading surrogate of a utf-16 surrogate pair
         std::optional<wchar_t> _leadingSurrogate{ std::nullopt };
diff --git a/src/cascadia/TerminalControl/IControlSettings.idl b/src/cascadia/TerminalControl/IControlSettings.idl
index d014b8ac9be..a47af4cc262 100644
--- a/src/cascadia/TerminalControl/IControlSettings.idl
+++ b/src/cascadia/TerminalControl/IControlSettings.idl
@@ -42,6 +42,8 @@ namespace Microsoft.Terminal.Control
         String Padding { get; };
         Windows.Foundation.Collections.IMap<String, UInt32> FontFeatures { get; };
         Windows.Foundation.Collections.IMap<String, Single> FontAxes { get; };
+        String CellWidth { get; };
+        String CellHeight { get; };
 
         Microsoft.Terminal.Control.IKeyBindings KeyBindings { get; };
 
diff --git a/src/cascadia/TerminalSettingsEditor/Appearances.cpp b/src/cascadia/TerminalSettingsEditor/Appearances.cpp
index 724faa3b0de..5a8608e7dca 100644
--- a/src/cascadia/TerminalSettingsEditor/Appearances.cpp
+++ b/src/cascadia/TerminalSettingsEditor/Appearances.cpp
@@ -49,6 +49,78 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
         }
     }
 
+    double AppearanceViewModel::LineHeight() const noexcept
+    {
+        const auto fontInfo = _appearance.SourceProfile().FontInfo();
+        const auto cellHeight = fontInfo.CellHeight();
+        const auto str = cellHeight.c_str();
+
+        auto& errnoRef = errno; // Nonzero cost, pay it once.
+        errnoRef = 0;
+
+        wchar_t* end;
+        const auto value = std::wcstod(str, &end);
+
+        return str == end || errnoRef == ERANGE ? NAN : value;
+    }
+
+    void AppearanceViewModel::LineHeight(const double value)
+    {
+        std::wstring str;
+
+        if (value >= 0.1 && value <= 10.0)
+        {
+            str = fmt::format(FMT_STRING(L"{:.6g}"), value);
+        }
+
+        const auto fontInfo = _appearance.SourceProfile().FontInfo();
+
+        if (fontInfo.CellHeight() != str)
+        {
+            if (str.empty())
+            {
+                fontInfo.ClearCellHeight();
+            }
+            else
+            {
+                fontInfo.CellHeight(str);
+            }
+            _NotifyChanges(L"HasLineHeight", L"LineHeight");
+        }
+    }
+
+    bool AppearanceViewModel::HasLineHeight() const
+    {
+        const auto fontInfo = _appearance.SourceProfile().FontInfo();
+        return fontInfo.HasCellHeight();
+    }
+
+    void AppearanceViewModel::ClearLineHeight()
+    {
+        LineHeight(NAN);
+    }
+
+    Model::FontConfig AppearanceViewModel::LineHeightOverrideSource() const
+    {
+        const auto fontInfo = _appearance.SourceProfile().FontInfo();
+        return fontInfo.CellHeightOverrideSource();
+    }
+
+    void AppearanceViewModel::SetFontWeightFromDouble(double fontWeight)
+    {
+        FontWeight(Converters::DoubleToFontWeight(fontWeight));
+    }
+
+    void AppearanceViewModel::SetBackgroundImageOpacityFromPercentageValue(double percentageValue)
+    {
+        BackgroundImageOpacity(Converters::PercentageValueToPercentage(percentageValue));
+    }
+
+    void AppearanceViewModel::SetBackgroundImagePath(winrt::hstring path)
+    {
+        BackgroundImagePath(path);
+    }
+
     bool AppearanceViewModel::UseDesktopBGImage()
     {
         return BackgroundImagePath() == L"desktopWallpaper";
@@ -123,10 +195,14 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
             // > .NET rounds to 12 significant digits when displaying doubles, so we will [...]
             // ...obviously not do that, because this is an UI element for humans. This prevents
             // issues when displaying 32-bit floats, because WinUI is unaware about their existence.
-            SignificantDigitsNumberRounder rounder;
-            rounder.SignificantDigits(6);
-            // BODGY: Depends on WinUI internals.
-            _fontSizeBox().NumberFormatter().as<DecimalFormatter>().NumberRounder(rounder);
+            IncrementNumberRounder rounder;
+            rounder.Increment(1e-6);
+
+            for (const auto& box : { _fontSizeBox(), _lineHeightBox() })
+            {
+                // BODGY: Depends on WinUI internals.
+                box.NumberFormatter().as<DecimalFormatter>().NumberRounder(rounder);
+            }
         }
 
         INITIALIZE_BINDABLE_ENUM_SETTING(CursorShape, CursorStyle, winrt::Microsoft::Terminal::Core::CursorStyle, L"Profile_CursorShape", L"Content");
diff --git a/src/cascadia/TerminalSettingsEditor/Appearances.h b/src/cascadia/TerminalSettingsEditor/Appearances.h
index 1be2b1ea2ce..949b2628567 100644
--- a/src/cascadia/TerminalSettingsEditor/Appearances.h
+++ b/src/cascadia/TerminalSettingsEditor/Appearances.h
@@ -51,18 +51,14 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
     public:
         AppearanceViewModel(const Model::AppearanceConfig& appearance);
 
-        void SetFontWeightFromDouble(double fontWeight)
-        {
-            FontWeight(winrt::Microsoft::Terminal::Settings::Editor::Converters::DoubleToFontWeight(fontWeight));
-        }
-        void SetBackgroundImageOpacityFromPercentageValue(double percentageValue)
-        {
-            BackgroundImageOpacity(winrt::Microsoft::Terminal::Settings::Editor::Converters::PercentageValueToPercentage(percentageValue));
-        }
-        void SetBackgroundImagePath(winrt::hstring path)
-        {
-            BackgroundImagePath(path);
-        }
+        double LineHeight() const noexcept;
+        void LineHeight(const double value);
+        bool HasLineHeight() const;
+        void ClearLineHeight();
+        Model::FontConfig LineHeightOverrideSource() const;
+        void SetFontWeightFromDouble(double fontWeight);
+        void SetBackgroundImageOpacityFromPercentageValue(double percentageValue);
+        void SetBackgroundImagePath(winrt::hstring path);
 
         // background image
         bool UseDesktopBGImage();
@@ -101,6 +97,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
     private:
         Model::AppearanceConfig _appearance;
         winrt::hstring _lastBgImagePath;
+        float _cachedLineHeight = 0;
     };
 
     struct Appearances : AppearancesT<Appearances>
diff --git a/src/cascadia/TerminalSettingsEditor/Appearances.idl b/src/cascadia/TerminalSettingsEditor/Appearances.idl
index 23bdfdecb6c..c89c51adf62 100644
--- a/src/cascadia/TerminalSettingsEditor/Appearances.idl
+++ b/src/cascadia/TerminalSettingsEditor/Appearances.idl
@@ -39,6 +39,7 @@ namespace Microsoft.Terminal.Settings.Editor
 
         OBSERVABLE_PROJECTED_APPEARANCE_SETTING(String, FontFace);
         OBSERVABLE_PROJECTED_APPEARANCE_SETTING(Single, FontSize);
+        OBSERVABLE_PROJECTED_APPEARANCE_SETTING(Double, LineHeight);
         OBSERVABLE_PROJECTED_APPEARANCE_SETTING(Windows.UI.Text.FontWeight, FontWeight);
 
         OBSERVABLE_PROJECTED_APPEARANCE_SETTING(String, DarkColorSchemeName);
diff --git a/src/cascadia/TerminalSettingsEditor/Appearances.xaml b/src/cascadia/TerminalSettingsEditor/Appearances.xaml
index 076522ff877..16b031d3fe1 100644
--- a/src/cascadia/TerminalSettingsEditor/Appearances.xaml
+++ b/src/cascadia/TerminalSettingsEditor/Appearances.xaml
@@ -219,7 +219,6 @@
                                     Visibility="{x:Bind Appearance.IsDefault, Mode=OneWay}">
                 <muxc:NumberBox x:Name="_fontSizeBox"
                                 x:Uid="Profile_FontSizeBox"
-                                AcceptsExpression="False"
                                 LargeChange="10"
                                 Maximum="128"
                                 Minimum="1"
@@ -228,6 +227,22 @@
                                 Value="{x:Bind Appearance.FontSize, Mode=TwoWay}" />
             </local:SettingContainer>
 
+            <!--  Line Height  -->
+            <local:SettingContainer x:Uid="Profile_LineHeight"
+                                    ClearSettingValue="{x:Bind Appearance.ClearLineHeight}"
+                                    HasSettingValue="{x:Bind Appearance.HasLineHeight, Mode=OneWay}"
+                                    SettingOverrideSource="{x:Bind Appearance.LineHeightOverrideSource, Mode=OneWay}"
+                                    Visibility="{x:Bind Appearance.IsDefault, Mode=OneWay}">
+                <muxc:NumberBox x:Name="_lineHeightBox"
+                                x:Uid="Profile_LineHeightBox"
+                                LargeChange="0.1"
+                                Maximum="10"
+                                Minimum="0.1"
+                                SmallChange="0.1"
+                                Style="{StaticResource NumberBoxSettingStyle}"
+                                Value="{x:Bind Appearance.LineHeight, Mode=TwoWay}" />
+            </local:SettingContainer>
+
             <!--  Font Weight  -->
             <local:SettingContainer x:Name="FontWeightContainer"
                                     x:Uid="Profile_FontWeight"
diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw
index 85e46d0d730..bb02b7917cb 100644
--- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw
+++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw
@@ -874,6 +874,22 @@
     <value>Size of the font in points.</value>
     <comment>A description for what the "font size" setting does. Presented near "Profile_FontSize".</comment>
   </data>
+  <data name="Profile_LineHeight.Header" xml:space="preserve">
+    <value>Line height</value>
+    <comment>Header for a control that sets the text line height.</comment>
+  </data>
+  <data name="Profile_LineHeightBox.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
+    <value>Line height</value>
+    <comment>Header for a control that sets the text line height.</comment>
+  </data>
+  <data name="Profile_LineHeight.HelpText" xml:space="preserve">
+    <value>Sets the height of each line in the terminal as a multiple of the font size. The default depends on your font and is usually around 1.2.</value>
+    <comment>A description for what the "line height" setting does. Presented near "Profile_LineHeight".</comment>
+  </data>
+  <data name="Profile_LineHeightBox.PlaceholderText" xml:space="preserve">
+    <value>1.2</value>
+    <comment>"1.2" is a decimal number.</comment>
+  </data>
   <data name="Profile_FontWeightComboBox.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
     <value>Font weight</value>
     <comment>Name for a control to select the weight (i.e. bold, thin, etc.) of the text in the app.</comment>
diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp
index 42ec1d28725..4a4fd5a057f 100644
--- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp
+++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp
@@ -295,7 +295,7 @@ Model::Profile CascadiaSettings::DuplicateProfile(const Model::Profile& source)
     MTSM_PROFILE_SETTINGS(DUPLICATE_PROFILE_SETTINGS)
 #undef DUPLICATE_PROFILE_SETTINGS
 
-    // These two aren't in MTSM_PROFILE_SETTINGS because they're special
+    // These aren't in MTSM_PROFILE_SETTINGS because they're special
     DUPLICATE_SETTING_MACRO(TabColor);
     DUPLICATE_SETTING_MACRO(Padding);
 
diff --git a/src/cascadia/TerminalSettingsModel/FontConfig.cpp b/src/cascadia/TerminalSettingsModel/FontConfig.cpp
index 62ebc5aacec..fafdf0976bc 100644
--- a/src/cascadia/TerminalSettingsModel/FontConfig.cpp
+++ b/src/cascadia/TerminalSettingsModel/FontConfig.cpp
@@ -78,11 +78,6 @@ void FontConfig::LayerJson(const Json::Value& json)
     }
 }
 
-bool FontConfig::HasAnyOptionSet() const
-{
-    return HasFontFace() || HasFontSize() || HasFontWeight();
-}
-
 winrt::Microsoft::Terminal::Settings::Model::Profile FontConfig::SourceProfile()
 {
     return _sourceProfile.get();
diff --git a/src/cascadia/TerminalSettingsModel/FontConfig.h b/src/cascadia/TerminalSettingsModel/FontConfig.h
index 7b9b4663d21..8a789278eb7 100644
--- a/src/cascadia/TerminalSettingsModel/FontConfig.h
+++ b/src/cascadia/TerminalSettingsModel/FontConfig.h
@@ -35,7 +35,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
         static winrt::com_ptr<FontConfig> CopyFontInfo(const FontConfig* source, winrt::weak_ref<Profile> sourceProfile);
         Json::Value ToJson() const;
         void LayerJson(const Json::Value& json);
-        bool HasAnyOptionSet() const;
 
         Model::Profile SourceProfile();
 
diff --git a/src/cascadia/TerminalSettingsModel/FontConfig.idl b/src/cascadia/TerminalSettingsModel/FontConfig.idl
index 174a6ff74b0..249bb94f8d1 100644
--- a/src/cascadia/TerminalSettingsModel/FontConfig.idl
+++ b/src/cascadia/TerminalSettingsModel/FontConfig.idl
@@ -18,8 +18,9 @@ namespace Microsoft.Terminal.Settings.Model
         INHERITABLE_FONT_SETTING(String, FontFace);
         INHERITABLE_FONT_SETTING(Single, FontSize);
         INHERITABLE_FONT_SETTING(Windows.UI.Text.FontWeight, FontWeight);
-
         INHERITABLE_FONT_SETTING(Windows.Foundation.Collections.IMap<String COMMA UInt32>, FontFeatures);
         INHERITABLE_FONT_SETTING(Windows.Foundation.Collections.IMap<String COMMA Single>, FontAxes);
+        INHERITABLE_FONT_SETTING(String, CellWidth);
+        INHERITABLE_FONT_SETTING(String, CellHeight);
     }
 }
diff --git a/src/cascadia/TerminalSettingsModel/MTSMSettings.h b/src/cascadia/TerminalSettingsModel/MTSMSettings.h
index cb0ed03e3f1..73d7cd80009 100644
--- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h
+++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h
@@ -100,7 +100,9 @@ Author(s):
     X(float, FontSize, "size", DEFAULT_FONT_SIZE)                                      \
     X(winrt::Windows::UI::Text::FontWeight, FontWeight, "weight", DEFAULT_FONT_WEIGHT) \
     X(IFontAxesMap, FontAxes, "axes")                                                  \
-    X(IFontFeatureMap, FontFeatures, "features")
+    X(IFontFeatureMap, FontFeatures, "features")                                       \
+    X(winrt::hstring, CellWidth, "cellWidth")                                          \
+    X(winrt::hstring, CellHeight, "cellHeight")
 
 #define MTSM_APPEARANCE_SETTINGS(X)                                                                                                                                \
     X(Core::CursorStyle, CursorShape, "cursorShape", Core::CursorStyle::Bar)                                                                                       \
diff --git a/src/cascadia/TerminalSettingsModel/Profile.cpp b/src/cascadia/TerminalSettingsModel/Profile.cpp
index 9db6741703e..b660337e971 100644
--- a/src/cascadia/TerminalSettingsModel/Profile.cpp
+++ b/src/cascadia/TerminalSettingsModel/Profile.cpp
@@ -324,11 +324,9 @@ Json::Value Profile::ToJson() const
     MTSM_PROFILE_SETTINGS(PROFILE_SETTINGS_TO_JSON)
 #undef PROFILE_SETTINGS_TO_JSON
 
-    // Font settings
-    const auto fontInfoImpl = winrt::get_self<FontConfig>(_FontInfo);
-    if (fontInfoImpl->HasAnyOptionSet())
+    if (auto fontJSON = winrt::get_self<FontConfig>(_FontInfo)->ToJson(); !fontJSON.empty())
     {
-        json[JsonKey(FontInfoKey)] = winrt::get_self<FontConfig>(_FontInfo)->ToJson();
+        json[JsonKey(FontInfoKey)] = std::move(fontJSON);
     }
 
     if (_UnfocusedAppearance)
diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp
index ce281f5e2c2..fe0d29dd614 100644
--- a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp
+++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp
@@ -271,11 +271,14 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
         _ProfileSource = profile.Source();
         _UseAcrylic = profile.UseAcrylic();
 
-        _FontFace = profile.FontInfo().FontFace();
-        _FontSize = profile.FontInfo().FontSize();
-        _FontWeight = profile.FontInfo().FontWeight();
-        _FontFeatures = profile.FontInfo().FontFeatures();
-        _FontAxes = profile.FontInfo().FontAxes();
+        const auto fontInfo = profile.FontInfo();
+        _FontFace = fontInfo.FontFace();
+        _FontSize = fontInfo.FontSize();
+        _FontWeight = fontInfo.FontWeight();
+        _FontFeatures = fontInfo.FontFeatures();
+        _FontAxes = fontInfo.FontAxes();
+        _CellWidth = fontInfo.CellWidth();
+        _CellHeight = fontInfo.CellHeight();
         _Padding = profile.Padding();
 
         _Commandline = profile.Commandline();
diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.h b/src/cascadia/TerminalSettingsModel/TerminalSettings.h
index e92ce3aeed9..0e1e9d08497 100644
--- a/src/cascadia/TerminalSettingsModel/TerminalSettings.h
+++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.h
@@ -126,6 +126,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
         INHERITABLE_SETTING(Model::TerminalSettings, winrt::Windows::UI::Text::FontWeight, FontWeight);
         INHERITABLE_SETTING(Model::TerminalSettings, IFontAxesMap, FontAxes);
         INHERITABLE_SETTING(Model::TerminalSettings, IFontFeatureMap, FontFeatures);
+        INHERITABLE_SETTING(Model::TerminalSettings, hstring, CellWidth);
+        INHERITABLE_SETTING(Model::TerminalSettings, hstring, CellHeight);
 
         INHERITABLE_SETTING(Model::TerminalSettings, Model::ColorScheme, AppliedColorScheme);
         INHERITABLE_SETTING(Model::TerminalSettings, hstring, BackgroundImage);
diff --git a/src/cascadia/inc/ControlProperties.h b/src/cascadia/inc/ControlProperties.h
index 8132d579e59..38e6da14bdc 100644
--- a/src/cascadia/inc/ControlProperties.h
+++ b/src/cascadia/inc/ControlProperties.h
@@ -61,6 +61,8 @@
     X(winrt::Windows::UI::Text::FontWeight, FontWeight)                                                                                                  \
     X(IFontFeatureMap, FontFeatures)                                                                                                                     \
     X(IFontAxesMap, FontAxes)                                                                                                                            \
+    X(winrt::hstring, CellWidth)                                                                                                                         \
+    X(winrt::hstring, CellHeight)                                                                                                                        \
     X(winrt::Microsoft::Terminal::Control::IKeyBindings, KeyBindings, nullptr)                                                                           \
     X(winrt::hstring, Commandline)                                                                                                                       \
     X(winrt::hstring, StartingDirectory)                                                                                                                 \
diff --git a/src/renderer/atlas/AtlasEngine.api.cpp b/src/renderer/atlas/AtlasEngine.api.cpp
index d9871debc6c..df9859a0cf3 100644
--- a/src/renderer/atlas/AtlasEngine.api.cpp
+++ b/src/renderer/atlas/AtlasEngine.api.cpp
@@ -627,20 +627,23 @@ void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const Fo
     const auto designUnitsPerPx = fontSizeInPx / static_cast<float>(metrics.designUnitsPerEm);
     const auto ascent = static_cast<float>(metrics.ascent) * designUnitsPerPx;
     const auto descent = static_cast<float>(metrics.descent) * designUnitsPerPx;
+    const auto lineGap = static_cast<float>(metrics.lineGap) * designUnitsPerPx;
     const auto underlinePosition = static_cast<float>(-metrics.underlinePosition) * designUnitsPerPx;
     const auto underlineThickness = static_cast<float>(metrics.underlineThickness) * designUnitsPerPx;
     const auto strikethroughPosition = static_cast<float>(-metrics.strikethroughPosition) * designUnitsPerPx;
     const auto strikethroughThickness = static_cast<float>(metrics.strikethroughThickness) * designUnitsPerPx;
-
     const auto advanceWidth = static_cast<float>(glyphMetrics.advanceWidth) * designUnitsPerPx;
+    const auto advanceHeight = ascent + descent + lineGap;
+
+    auto adjustedWidth = std::roundf(fontInfoDesired.GetCellWidth().Resolve(advanceWidth, _api.dpi, fontSizeInPx, advanceWidth));
+    auto adjustedHeight = std::roundf(fontInfoDesired.GetCellHeight().Resolve(advanceHeight, _api.dpi, fontSizeInPx, advanceWidth));
+
+    // Protection against bad user values in GetCellWidth/Y.
+    // AtlasEngine fails hard with 0 cell sizes.
+    adjustedWidth = std::max(1.0f, adjustedWidth);
+    adjustedHeight = std::max(1.0f, adjustedHeight);
 
-    // NOTE: Line-gaps shouldn't be taken into account for lineHeight calculations.
-    // Terminals don't really have "gaps" between lines and instead the expectation
-    // is that two full block characters above each other don't leave any gaps
-    // between the lines. "Terminus TTF" for instance sets a line-gap of 90 units
-    // even though its font bitmap only covers the ascend/descend height.
-    const auto baseline = std::roundf(ascent);
-    const auto lineHeight = std::roundf(baseline + descent);
+    const auto baseline = std::roundf(ascent + (lineGap + adjustedHeight - advanceHeight) / 2.0f);
     const auto underlinePos = std::roundf(baseline + underlinePosition);
     const auto underlineWidth = std::max(1.0f, std::roundf(underlineThickness));
     const auto strikethroughPos = std::roundf(baseline + strikethroughPosition);
@@ -667,10 +670,10 @@ void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const Fo
     const auto doubleUnderlineGap = std::max(1.0f, std::roundf(1.2f / 72.0f * _api.dpi));
     doubleUnderlinePosBottom = std::max(doubleUnderlinePosBottom, doubleUnderlinePosTop + doubleUnderlineGap + thinLineWidth);
     // Our cells can't overlap each other so we additionally clamp the bottom line to be inside the cell boundaries.
-    doubleUnderlinePosBottom = std::min(doubleUnderlinePosBottom, lineHeight - thinLineWidth);
+    doubleUnderlinePosBottom = std::min(doubleUnderlinePosBottom, adjustedHeight - thinLineWidth);
 
-    const auto cellWidth = gsl::narrow<u16>(std::lroundf(advanceWidth));
-    const auto cellHeight = gsl::narrow<u16>(lineHeight);
+    const auto cellWidth = gsl::narrow<u16>(std::lroundf(adjustedWidth));
+    const auto cellHeight = gsl::narrow<u16>(std::lroundf(adjustedHeight));
 
     {
         til::size coordSize;
@@ -708,7 +711,7 @@ void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const Fo
         fontMetrics->fontName = std::move(fontName);
         fontMetrics->fontSizeInDIP = fontSizeInDIP;
         fontMetrics->baselineInDIP = baseline / static_cast<float>(_api.dpi) * 96.0f;
-        fontMetrics->advanceScale = cellWidth / advanceWidth;
+        fontMetrics->advanceScale = cellWidth / adjustedWidth;
         fontMetrics->cellSize = { cellWidth, cellHeight };
         fontMetrics->fontWeight = fontWeightU16;
         fontMetrics->underlinePos = underlinePosU16;
diff --git a/src/renderer/atlas/AtlasEngine.cpp b/src/renderer/atlas/AtlasEngine.cpp
index c9ff2327d2b..4bdf26e9dbd 100644
--- a/src/renderer/atlas/AtlasEngine.cpp
+++ b/src/renderer/atlas/AtlasEngine.cpp
@@ -1148,6 +1148,14 @@ void AtlasEngine::_recreateFontDependentResources()
                 // fonts making them look fairly unslightly. With no option to easily disable this
                 // feature in Windows Terminal, it's better left disabled by default.
 
+                const DWRITE_LINE_SPACING lineSpacing{
+                    .method = DWRITE_LINE_SPACING_METHOD_UNIFORM,
+                    .height = _r.cellSizeDIP.y,
+                    .baseline = _api.fontMetrics.baselineInDIP,
+                    .fontLineGapUsage = DWRITE_FONT_LINE_GAP_USAGE_ENABLED,
+                };
+                THROW_IF_FAILED(textFormat.query<IDWriteTextFormat2>()->SetLineSpacing(&lineSpacing));
+
                 if (!_api.fontAxisValues.empty())
                 {
                     if (const auto textFormat3 = textFormat.try_query<IDWriteTextFormat3>())
diff --git a/src/renderer/atlas/AtlasEngine.h b/src/renderer/atlas/AtlasEngine.h
index f463a000575..07701271a38 100644
--- a/src/renderer/atlas/AtlasEngine.h
+++ b/src/renderer/atlas/AtlasEngine.h
@@ -412,8 +412,8 @@ namespace Microsoft::Console::Render
             wil::com_ptr<IDWriteFontCollection> fontCollection;
             wil::com_ptr<IDWriteFontFamily> fontFamily;
             std::wstring fontName;
-            float baselineInDIP = 0.0f;
-            float fontSizeInDIP = 0.0f;
+            f32 baselineInDIP = 0.0f;
+            f32 fontSizeInDIP = 0.0f;
             f32 advanceScale = 0;
             u16x2 cellSize;
             u16 fontWeight = 0;
diff --git a/src/renderer/base/CSSLengthPercentage.cpp b/src/renderer/base/CSSLengthPercentage.cpp
new file mode 100644
index 00000000000..f9ac48f94c9
--- /dev/null
+++ b/src/renderer/base/CSSLengthPercentage.cpp
@@ -0,0 +1,78 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "precomp.h"
+
+#include "../inc/CSSLengthPercentage.h"
+
+CSSLengthPercentage CSSLengthPercentage::FromString(const wchar_t* str)
+{
+    auto& errnoRef = errno; // Nonzero cost, pay it once.
+    errnoRef = 0;
+
+    wchar_t* end;
+    auto value = std::wcstof(str, &end);
+
+    if (str == end || errnoRef == ERANGE)
+    {
+        return {};
+    }
+
+    auto referenceFrame = ReferenceFrame::FontSize;
+
+    if (*end)
+    {
+        if (wcscmp(end, L"%") == 0)
+        {
+            value /= 100.0f;
+        }
+        else if (wcscmp(end, L"px") == 0)
+        {
+            referenceFrame = ReferenceFrame::Absolute;
+            value /= 96.0f;
+        }
+        else if (wcscmp(end, L"pt") == 0)
+        {
+            referenceFrame = ReferenceFrame::Absolute;
+            value /= 72.0f;
+        }
+        else if (wcscmp(end, L"ch") == 0)
+        {
+            referenceFrame = ReferenceFrame::AdvanceWidth;
+        }
+        else
+        {
+            return {};
+        }
+    }
+
+    CSSLengthPercentage obj;
+    obj._value = value;
+    obj._referenceFrame = referenceFrame;
+    return obj;
+}
+
+CSSLengthPercentage::ReferenceFrame CSSLengthPercentage::GetReferenceFrame() const noexcept
+{
+    return _referenceFrame;
+}
+
+float CSSLengthPercentage::Resolve(float factor) const noexcept
+{
+    return _value * factor;
+}
+
+float CSSLengthPercentage::Resolve(float fallback, float dpi, float fontSize, float advanceWidth) const noexcept
+{
+    switch (_referenceFrame)
+    {
+    case ReferenceFrame::Absolute:
+        return _value * dpi;
+    case ReferenceFrame::FontSize:
+        return _value * fontSize;
+    case ReferenceFrame::AdvanceWidth:
+        return _value * advanceWidth;
+    default:
+        return fallback;
+    }
+}
diff --git a/src/renderer/base/FontInfoDesired.cpp b/src/renderer/base/FontInfoDesired.cpp
index 43f9cf37480..742883e7c14 100644
--- a/src/renderer/base/FontInfoDesired.cpp
+++ b/src/renderer/base/FontInfoDesired.cpp
@@ -23,6 +23,22 @@ FontInfoDesired::FontInfoDesired(const FontInfo& fiFont) noexcept :
 {
 }
 
+void FontInfoDesired::SetCellSize(const CSSLengthPercentage& cellWidth, const CSSLengthPercentage& cellHeight) noexcept
+{
+    _cellWidth = cellWidth;
+    _cellHeight = cellHeight;
+}
+
+const CSSLengthPercentage& FontInfoDesired::GetCellWidth() const noexcept
+{
+    return _cellWidth;
+}
+
+const CSSLengthPercentage& FontInfoDesired::GetCellHeight() const noexcept
+{
+    return _cellHeight;
+}
+
 float FontInfoDesired::GetFontSize() const noexcept
 {
     return _fontSize;
diff --git a/src/renderer/base/lib/base.vcxproj b/src/renderer/base/lib/base.vcxproj
index 697934e8551..3c177d955e2 100644
--- a/src/renderer/base/lib/base.vcxproj
+++ b/src/renderer/base/lib/base.vcxproj
@@ -11,6 +11,7 @@
   <Import Project="$(SolutionDir)src\common.build.pre.props" />
   <Import Project="$(SolutionDir)src\common.nugetversions.props" />
   <ItemGroup>
+    <ClCompile Include="..\CSSLengthPercentage.cpp" />
     <ClCompile Include="..\FontInfo.cpp" />
     <ClCompile Include="..\FontInfoBase.cpp" />
     <ClCompile Include="..\FontInfoDesired.cpp" />
@@ -25,6 +26,7 @@
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="..\..\inc\Cluster.hpp" />
+    <ClInclude Include="..\..\inc\CSSLengthPercentage.h" />
     <ClInclude Include="..\..\inc\FontInfo.hpp" />
     <ClInclude Include="..\..\inc\FontInfoBase.hpp" />
     <ClInclude Include="..\..\inc\FontInfoDesired.hpp" />
diff --git a/src/renderer/base/lib/base.vcxproj.filters b/src/renderer/base/lib/base.vcxproj.filters
index a678fb27cce..8887f1c610f 100644
--- a/src/renderer/base/lib/base.vcxproj.filters
+++ b/src/renderer/base/lib/base.vcxproj.filters
@@ -45,6 +45,9 @@
     <ClCompile Include="..\RenderSettings.cpp">
       <Filter>Source Files</Filter>
     </ClCompile>
+    <ClCompile Include="..\CSSLengthPercentage.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="..\precomp.h">
@@ -89,8 +92,11 @@
     <ClInclude Include="..\FontCache.h">
       <Filter>Header Files</Filter>
     </ClInclude>
+    <ClInclude Include="..\..\inc\CSSLengthPercentage.h">
+      <Filter>Header Files\inc</Filter>
+    </ClInclude>
   </ItemGroup>
   <ItemGroup>
     <Natvis Include="$(SolutionDir)tools\ConsoleTypes.natvis" />
   </ItemGroup>
-</Project>
\ No newline at end of file
+</Project>
diff --git a/src/renderer/dx/DxFontRenderData.cpp b/src/renderer/dx/DxFontRenderData.cpp
index b2444bb8251..559ebe6d85b 100644
--- a/src/renderer/dx/DxFontRenderData.cpp
+++ b/src/renderer/dx/DxFontRenderData.cpp
@@ -706,6 +706,7 @@ std::vector<DWRITE_FONT_AXIS_VALUE> DxFontRenderData::GetAxisVector(const DWRITE
 // - None
 void DxFontRenderData::_BuildFontRenderData(const FontInfoDesired& desired, FontInfo& actual, const int dpi)
 {
+    const auto dpiF = static_cast<float>(dpi);
     auto fontLocaleName = UserLocaleName();
     // This is the first attempt to resolve font face after `UpdateFont`.
     // Note that the following line may cause property changes _inside_ `_defaultFontInfo` because the desired font may not exist.
@@ -737,27 +738,20 @@ void DxFontRenderData::_BuildFontRenderData(const FontInfoDesired& desired, Font
     // - 12 ppi font * (96 dpi / 96 dpi) * (96 dpi / 72 points per inch) = 16 pixels tall font for 100% display (96 dpi is 100%)
     // - 12 ppi font * (144 dpi / 96 dpi) * (96 dpi / 72 points per inch) = 24 pixels tall font for 150% display (144 dpi is 150%)
     // - 12 ppi font * (192 dpi / 96 dpi) * (96 dpi / 72 points per inch) = 32 pixels tall font for 200% display (192 dpi is 200%)
-    auto heightDesired = desired.GetEngineSize().height * USER_DEFAULT_SCREEN_DPI / POINTS_PER_INCH;
+    const auto heightDesired = desired.GetEngineSize().height / POINTS_PER_INCH * dpiF;
 
     // The advance is the number of pixels left-to-right (X dimension) for the given font.
     // We're finding a proportional factor here with the design units in "ems", not an actual pixel measurement.
-
-    // Now we play trickery with the font size. Scale by the DPI to get the height we expect.
-    heightDesired *= (static_cast<float>(dpi) / static_cast<float>(USER_DEFAULT_SCREEN_DPI));
-
     const auto widthAdvance = static_cast<float>(advanceInDesignUnits) / fontMetrics.designUnitsPerEm;
 
     // Use the real pixel height desired by the "em" factor for the width to get the number of pixels
     // we will need per character in width. This will almost certainly result in fractional X-dimension pixels.
-    const auto widthApprox = heightDesired * widthAdvance;
-
-    // Since we can't deal with columns of the presentation grid being fractional pixels in width, round to the nearest whole pixel.
-    const auto widthExact = round(widthApprox);
+    const auto widthAdvanceInPx = heightDesired * widthAdvance;
 
     // Now reverse the "em" factor from above to turn the exact pixel width into a (probably) fractional
     // height in pixels of each character. It's easier for us to pad out height and align vertically
     // than it is horizontally.
-    const auto fontSize = widthExact / widthAdvance;
+    const auto fontSize = roundf(widthAdvanceInPx) / widthAdvance;
     _fontSize = fontSize;
 
     // Now figure out the basic properties of the character height which include ascent and descent
@@ -809,8 +803,12 @@ void DxFontRenderData::_BuildFontRenderData(const FontInfoDesired& desired, Font
 
     const auto fullPixelAscent = ceil(ascent + halfGap);
     const auto fullPixelDescent = ceil(descent + halfGap);
-    lineSpacing.height = fullPixelAscent + fullPixelDescent;
-    lineSpacing.baseline = fullPixelAscent;
+    const auto defaultHeight = fullPixelAscent + fullPixelDescent;
+    const auto lineHeight = desired.GetCellHeight().Resolve(defaultHeight, dpiF, heightDesired, widthAdvanceInPx);
+    const auto baseline = fullPixelAscent + (lineHeight - defaultHeight) / 2.0f;
+
+    lineSpacing.height = roundf(lineHeight);
+    lineSpacing.baseline = roundf(baseline);
 
     // According to MSDN (https://docs.microsoft.com/en-us/windows/win32/api/dwrite_3/ne-dwrite_3-dwrite_font_line_gap_usage)
     // Setting "ENABLED" means we've included the line gapping in the spacing numbers given.
@@ -818,6 +816,9 @@ void DxFontRenderData::_BuildFontRenderData(const FontInfoDesired& desired, Font
 
     _lineSpacing = lineSpacing;
 
+    const auto widthApprox = desired.GetCellWidth().Resolve(widthAdvanceInPx, dpiF, heightDesired, widthAdvanceInPx);
+    const auto widthExact = roundf(widthApprox);
+
     // The scaled size needs to represent the pixel box that each character will fit within for the purposes
     // of hit testing math and other such multiplication/division.
     til::size coordSize;
@@ -861,8 +862,8 @@ void DxFontRenderData::_BuildFontRenderData(const FontInfoDesired& desired, Font
 
     // Offsets are relative to the base line of the font, so we subtract
     // from the ascent to get an offset relative to the top of the cell.
-    lineMetrics.underlineOffset = fullPixelAscent - lineMetrics.underlineOffset;
-    lineMetrics.strikethroughOffset = fullPixelAscent - lineMetrics.strikethroughOffset;
+    lineMetrics.underlineOffset = lineSpacing.baseline - lineMetrics.underlineOffset;
+    lineMetrics.strikethroughOffset = lineSpacing.baseline - lineMetrics.strikethroughOffset;
 
     // For double underlines we need a second offset, just below the first,
     // but with a bit of a gap (about double the grid line width).
diff --git a/src/renderer/inc/CSSLengthPercentage.h b/src/renderer/inc/CSSLengthPercentage.h
new file mode 100644
index 00000000000..289904ceb93
--- /dev/null
+++ b/src/renderer/inc/CSSLengthPercentage.h
@@ -0,0 +1,33 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#pragma once
+
+struct CSSLengthPercentage
+{
+    enum class ReferenceFrame : uint8_t
+    {
+        // This indicates the object is empty/unset.
+        // No need to call Resolve().
+        None,
+        // Call Resolve() with factor set to the target DPI (e.g. 96 for DIP).
+        // Returns an absolute length value scaled by that DPI.
+        Absolute,
+        // Call Resolve() with factor set to the font size
+        // in an arbitrary DPI. Returns a value relative to it.
+        FontSize,
+        // Call Resolve() with factor set to the "0" glyph advance width
+        // in an arbitrary DPI. Returns a value relative to it.
+        AdvanceWidth,
+    };
+
+    static CSSLengthPercentage FromString(const wchar_t* str);
+
+    ReferenceFrame GetReferenceFrame() const noexcept;
+    float Resolve(float factor) const noexcept;
+    float Resolve(float fallback, float dpi, float fontSize, float advanceWidth) const noexcept;
+
+private:
+    float _value = 0;
+    ReferenceFrame _referenceFrame = ReferenceFrame::None;
+};
diff --git a/src/renderer/inc/FontInfoDesired.hpp b/src/renderer/inc/FontInfoDesired.hpp
index 5e77e466f9c..84643283183 100644
--- a/src/renderer/inc/FontInfoDesired.hpp
+++ b/src/renderer/inc/FontInfoDesired.hpp
@@ -20,6 +20,7 @@ Author(s):
 
 #include "FontInfoBase.hpp"
 #include "FontInfo.hpp"
+#include "CSSLengthPercentage.h"
 
 class FontInfoDesired : public FontInfoBase
 {
@@ -33,6 +34,10 @@ class FontInfoDesired : public FontInfoBase
 
     bool operator==(const FontInfoDesired& other) = delete;
 
+    void SetCellSize(const CSSLengthPercentage& cellWidth, const CSSLengthPercentage& cellHeight) noexcept;
+
+    const CSSLengthPercentage& GetCellWidth() const noexcept;
+    const CSSLengthPercentage& GetCellHeight() const noexcept;
     float GetFontSize() const noexcept;
     til::size GetEngineSize() const noexcept;
     bool IsDefaultRasterFont() const noexcept;
@@ -40,4 +45,6 @@ class FontInfoDesired : public FontInfoBase
 private:
     til::size _coordSizeDesired;
     float _fontSize;
+    CSSLengthPercentage _cellWidth;
+    CSSLengthPercentage _cellHeight;
 };


Test patch: