From b6e6dd861d165afb7f8bc9fe031a89ab615a3efb Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 14 Feb 2023 22:42:14 +0100 Subject: [PATCH] Implement cell size customizations (#14255) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Does what it says in the title. After this commit you can customize the height and width of the terminal's cells. This commit supports parts of CSS' `` data type: Font-size relative sizes as multiples (`1.2`), percentage (`120%`), or advance-width relative (`1.2ch`), as well as absolute sizes in CSS pixels (`px`) or points (`pt`). This PR is neither bug free in DxEngine, nor in AtlasEngine. The former fails to implement glyph advance corrections (for instance #9381), as well as disallowing glyphs to overlap rows. The latter has the same overlap issue, but more severely as it tries to shrink glyphs to fit in. Closes #3498 Closes #14068 ## Validation Steps Performed * Setting `height` to `1` creates 12pt tall rows ✅ * Setting `height` to `1ch` creates square cells ✅ * Setting `width` to `1` creates square cells ✅ * Setting `width` or `height` to `Npx` or `Npt` works ✅ * Trailing zeroes are trimmed properly during serialization ✅ * Patching the PR to allow >100 line heights and entering "100.123456" displays 6 fractional digits ✅ --- doc/cascadia/profiles.schema.json | 15 ++++ src/cascadia/TerminalControl/ControlCore.cpp | 4 + src/cascadia/TerminalControl/ControlCore.h | 2 + .../TerminalControl/IControlSettings.idl | 2 + .../TerminalSettingsEditor/Appearances.cpp | 84 ++++++++++++++++++- .../TerminalSettingsEditor/Appearances.h | 21 ++--- .../TerminalSettingsEditor/Appearances.idl | 1 + .../TerminalSettingsEditor/Appearances.xaml | 17 +++- .../Resources/en-US/Resources.resw | 16 ++++ .../CascadiaSettings.cpp | 2 +- .../TerminalSettingsModel/FontConfig.cpp | 5 -- .../TerminalSettingsModel/FontConfig.h | 1 - .../TerminalSettingsModel/FontConfig.idl | 3 +- .../TerminalSettingsModel/MTSMSettings.h | 4 +- .../TerminalSettingsModel/Profile.cpp | 6 +- .../TerminalSettings.cpp | 13 +-- .../TerminalSettingsModel/TerminalSettings.h | 2 + src/cascadia/inc/ControlProperties.h | 2 + src/renderer/atlas/AtlasEngine.api.cpp | 27 +++--- src/renderer/atlas/AtlasEngine.cpp | 8 ++ src/renderer/atlas/AtlasEngine.h | 4 +- src/renderer/base/CSSLengthPercentage.cpp | 78 +++++++++++++++++ src/renderer/base/FontInfoDesired.cpp | 16 ++++ src/renderer/base/lib/base.vcxproj | 2 + src/renderer/base/lib/base.vcxproj.filters | 8 +- src/renderer/dx/DxFontRenderData.cpp | 29 +++---- src/renderer/inc/CSSLengthPercentage.h | 33 ++++++++ src/renderer/inc/FontInfoDesired.hpp | 7 ++ 28 files changed, 348 insertions(+), 64 deletions(-) create mode 100644 src/renderer/base/CSSLengthPercentage.cpp create mode 100644 src/renderer/inc/CSSLengthPercentage.h 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 _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 FontFeatures { get; }; Windows.Foundation.Collections.IMap 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 8cf0079138a..c995a13ea7d 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().NumberRounder(rounder); + IncrementNumberRounder rounder; + rounder.Increment(1e-6); + + for (const auto& box : { _fontSizeBox(), _lineHeightBox() }) + { + // BODGY: Depends on WinUI internals. + box.NumberFormatter().as().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 13f4719a30d..06b9e64d0ce 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(); @@ -100,6 +96,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation private: Model::AppearanceConfig _appearance; winrt::hstring _lastBgImagePath; + float _cachedLineHeight = 0; }; struct Appearances : AppearancesT diff --git a/src/cascadia/TerminalSettingsEditor/Appearances.idl b/src/cascadia/TerminalSettingsEditor/Appearances.idl index 2a83e44459d..4029cbdd168 100644 --- a/src/cascadia/TerminalSettingsEditor/Appearances.idl +++ b/src/cascadia/TerminalSettingsEditor/Appearances.idl @@ -38,6 +38,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}"> + + + + + Size of the font in points. A description for what the "font size" setting does. Presented near "Profile_FontSize". + + Line height + Header for a control that sets the text line height. + + + Line height + Header for a control that sets the text line height. + + + 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. + A description for what the "line height" setting does. Presented near "Profile_LineHeight". + + + 1.2 + "1.2" is a decimal number. + Font weight Name for a control to select the weight (i.e. bold, thin, etc.) of the text in the app. 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 CopyFontInfo(const FontConfig* source, winrt::weak_ref 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, FontFeatures); INHERITABLE_FONT_SETTING(Windows.Foundation.Collections.IMap, 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 ee33e57daea..3aa896f50da 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(_FontInfo); - if (fontInfoImpl->HasAnyOptionSet()) + if (auto fontJSON = winrt::get_self(_FontInfo)->ToJson(); !fontJSON.empty()) { - json[JsonKey(FontInfoKey)] = winrt::get_self(_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 ce6076e7cea..f64a9eee655 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 08ea820fd2e..dafdf6bf787 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 777802e1117..6f4a2d02b75 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(metrics.designUnitsPerEm); const auto ascent = static_cast(metrics.ascent) * designUnitsPerPx; const auto descent = static_cast(metrics.descent) * designUnitsPerPx; + const auto lineGap = static_cast(metrics.lineGap) * designUnitsPerPx; const auto underlinePosition = static_cast(-metrics.underlinePosition) * designUnitsPerPx; const auto underlineThickness = static_cast(metrics.underlineThickness) * designUnitsPerPx; const auto strikethroughPosition = static_cast(-metrics.strikethroughPosition) * designUnitsPerPx; const auto strikethroughThickness = static_cast(metrics.strikethroughThickness) * designUnitsPerPx; - const auto advanceWidth = static_cast(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(std::lroundf(advanceWidth)); - const auto cellHeight = gsl::narrow(lineHeight); + const auto cellWidth = gsl::narrow(std::lroundf(adjustedWidth)); + const auto cellHeight = gsl::narrow(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(_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 dd26ac928b2..d6f1562cead 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()->SetLineSpacing(&lineSpacing)); + if (!_api.fontAxisValues.empty()) { if (const auto textFormat3 = textFormat.try_query()) diff --git a/src/renderer/atlas/AtlasEngine.h b/src/renderer/atlas/AtlasEngine.h index fd8523e9373..146d8053d1d 100644 --- a/src/renderer/atlas/AtlasEngine.h +++ b/src/renderer/atlas/AtlasEngine.h @@ -412,8 +412,8 @@ namespace Microsoft::Console::Render wil::com_ptr fontCollection; wil::com_ptr 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 @@ + @@ -25,6 +26,7 @@ + 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 @@ Source Files + + Source Files + @@ -89,8 +92,11 @@ Header Files + + Header Files\inc + - \ No newline at end of file + diff --git a/src/renderer/dx/DxFontRenderData.cpp b/src/renderer/dx/DxFontRenderData.cpp index d33f5f25d02..605dfcdf882 100644 --- a/src/renderer/dx/DxFontRenderData.cpp +++ b/src/renderer/dx/DxFontRenderData.cpp @@ -706,6 +706,7 @@ std::vector DxFontRenderData::GetAxisVector(const DWRITE // - None void DxFontRenderData::_BuildFontRenderData(const FontInfoDesired& desired, FontInfo& actual, const int dpi) { + const auto dpiF = static_cast(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(dpi) / static_cast(USER_DEFAULT_SCREEN_DPI)); - const auto widthAdvance = static_cast(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; };