Browse Source

Added support for platform text scaling, disabled by default

Only TextBlock, Inline, and TextPresenter scale text by themselves
No change to low-level text rendering or FontSize values
Implemented for iOS and Windows
Windows scaling is currently uniform regardless of base font size, which is incorrect
pull/20292/head
Tom Edwards 3 months ago
parent
commit
bf2a577f83
  1. 3
      samples/ControlCatalog/MainView.xaml
  2. 10
      src/Avalonia.Base/Platform/DefaultPlatformSettings.cs
  3. 12
      src/Avalonia.Base/Platform/IPlatformSettings.cs
  4. 2
      src/Avalonia.Controls/Documents/IInlineHost.cs
  5. 2
      src/Avalonia.Controls/Documents/Inline.cs
  6. 23
      src/Avalonia.Controls/Documents/TextElement.cs
  7. 17
      src/Avalonia.Controls/IPlatformTextScaleable.cs
  8. 36
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  9. 2
      src/Avalonia.Controls/Primitives/TextSelectionCanvas.cs
  10. 6
      src/Avalonia.Controls/SelectableTextBlock.cs
  11. 35
      src/Avalonia.Controls/TextBlock.cs
  12. 12
      src/Avalonia.Controls/TopLevel.cs
  13. 47
      src/Windows/Avalonia.Win32/Win32PlatformSettings.cs
  14. 9
      src/Windows/Avalonia.Win32/WinRT/winrt.idl
  15. 20
      src/iOS/Avalonia.iOS/PlatformSettings.cs

3
samples/ControlCatalog/MainView.xaml

@ -6,7 +6,8 @@
xmlns:models="using:ControlCatalog.Models"
xmlns:pages="using:ControlCatalog.Pages"
xmlns:viewModels="using:ControlCatalog.ViewModels"
x:DataType="viewModels:MainWindowViewModel">
x:DataType="viewModels:MainWindowViewModel"
TextElement.IsPlatformTextScalingEnabled="True">
<Grid>
<Grid.Styles>
<Style Selector="TextBlock.h2">

10
src/Avalonia.Base/Platform/DefaultPlatformSettings.cs

@ -1,10 +1,8 @@
using System;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.Threading;
using Avalonia.VisualTree;
namespace Avalonia.Platform
{
@ -49,10 +47,16 @@ namespace Avalonia.Platform
public virtual event EventHandler<PlatformColorValues>? ColorValuesChanged;
protected void OnColorValuesChanged(PlatformColorValues colorValues)
protected virtual void OnColorValuesChanged(PlatformColorValues colorValues)
{
Dispatcher.UIThread.Send(
_ => ColorValuesChanged?.Invoke(this, colorValues));
}
public event EventHandler<EventArgs>? TextScalingChanged;
protected virtual void OnTextScaleChanged() => Dispatcher.UIThread.Send(_ => TextScalingChanged?.Invoke(this, EventArgs.Empty));
public virtual double GetScaledFontSize(double baseFontSize) => baseFontSize;
}
}

12
src/Avalonia.Base/Platform/IPlatformSettings.cs

@ -42,7 +42,7 @@ namespace Avalonia.Platform
/// Get a configuration for platform-specific hotkeys in an Avalonia application.
/// </summary>
PlatformHotkeyConfiguration HotkeyConfiguration { get; }
/// <summary>
/// Gets current system color values including dark mode and accent colors.
/// </summary>
@ -52,5 +52,15 @@ namespace Avalonia.Platform
/// Raises when current system color values are changed. Including changing of a dark mode and accent colors.
/// </summary>
event EventHandler<PlatformColorValues>? ColorValuesChanged;
/// <summary>
/// Raised when the system text scaling changes.
/// </summary>
event EventHandler<EventArgs>? TextScalingChanged;
/// <summary>
/// Scales a font size according to the current system text scaling rules.
/// </summary>
double GetScaledFontSize(double baseFontSize);
}
}

2
src/Avalonia.Controls/Documents/IInlineHost.cs

@ -3,7 +3,7 @@ using Avalonia.LogicalTree;
namespace Avalonia.Controls.Documents
{
internal interface IInlineHost : ILogical
internal interface IInlineHost : ILogical, IPlatformTextScaleable
{
void Invalidate();

2
src/Avalonia.Controls/Documents/Inline.cs

@ -83,7 +83,7 @@ namespace Avalonia.Controls.Documents
return new GenericTextRunProperties(
typeface,
FontFeatures,
FontSize,
InlineHost?.GetScaledFontSize(FontSize) ?? FontSize,
TextDecorations,
Foreground,
parentOrSelfBackground,

23
src/Avalonia.Controls/Documents/TextElement.cs

@ -14,6 +14,15 @@ namespace Avalonia.Controls.Documents
public static readonly StyledProperty<IBrush?> BackgroundProperty =
Border.BackgroundProperty.AddOwner<TextElement>();
/// <summary>
/// Defines the <see cref="IsPlatformTextScalingEnabled"/> property.
/// </summary>
public static readonly AttachedProperty<bool> IsPlatformTextScalingEnabledProperty =
AvaloniaProperty.RegisterAttached<TextElement, Control, bool>(
nameof(IsPlatformTextScalingEnabled),
defaultValue: false,
inherits: true);
/// <summary>
/// Defines the <see cref="FontFamily"/> property.
/// </summary>
@ -102,6 +111,16 @@ namespace Avalonia.Controls.Documents
set => SetValue(BackgroundProperty, value);
}
/// <summary>
/// Gets or sets whether the <see cref="FontSize"/> should be scaled according to platform text scaling
/// rules when measuring and rendering this control.
/// </summary>
public bool IsPlatformTextScalingEnabled
{
get => GetValue(IsPlatformTextScalingEnabledProperty);
set => SetValue(IsPlatformTextScalingEnabledProperty, value);
}
/// <summary>
/// Gets or sets the font family.
/// </summary>
@ -174,6 +193,9 @@ namespace Avalonia.Controls.Documents
set => SetValue(LetterSpacingProperty, value);
}
public static bool GetIsPlatformTextScalingEnabled(Control control) => control.GetValue(IsPlatformTextScalingEnabledProperty);
public static void SetIsPlatformTextScalingEnabled(Control control, bool value) => control.SetValue(IsPlatformTextScalingEnabledProperty, value);
/// <summary>
/// Gets the value of the attached <see cref="FontFamilyProperty"/> on a control.
/// </summary>
@ -359,6 +381,7 @@ namespace Avalonia.Controls.Documents
case nameof(Background):
case nameof(FontFamily):
case nameof(FontSize):
case nameof(IsPlatformTextScalingEnabled):
case nameof(FontStyle):
case nameof(FontWeight):
case nameof(FontStretch):

17
src/Avalonia.Controls/IPlatformTextScaleable.cs

@ -0,0 +1,17 @@
namespace Avalonia.Controls;
/// <summary>
/// Represents an object which particpates in platform text scaling. This is an accessibility feature
/// which allows the user to request that text be drawn larger (or on some platforms smaller) than normal,
/// without altering other UI elements.
/// </summary>
public interface IPlatformTextScaleable
{
bool IsPlatformTextScalingEnabled { get; }
void OnPlatformTextScalingChanged();
/// <summary>
/// Scales a font size according to the current system text scaling rules and the value of <see cref="IsPlatformTextScalingEnabled"/>.
/// </summary>
double GetScaledFontSize(double baseFontSize);
}

36
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -15,7 +15,7 @@ using Avalonia.VisualTree;
namespace Avalonia.Controls.Presenters
{
public class TextPresenter : Control
public class TextPresenter : Control, IPlatformTextScaleable
{
public static readonly StyledProperty<bool> ShowSelectionHighlightProperty =
AvaloniaProperty.Register<TextPresenter, bool>(nameof(ShowSelectionHighlight), defaultValue: true);
@ -47,6 +47,12 @@ namespace Avalonia.Controls.Presenters
public static readonly StyledProperty<int> SelectionEndProperty =
TextBox.SelectionEndProperty.AddOwner<TextPresenter>(new(coerce: TextBox.CoerceCaretIndex));
/// <summary>
/// Defines the <see cref="IsPlatformTextScalingEnabled"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsPlatformTextScalingEnabledProperty =
TextElement.IsPlatformTextScalingEnabledProperty.AddOwner<TextPresenter>();
/// <summary>
/// Defines the <see cref="Text"/> property.
/// </summary>
@ -124,6 +130,13 @@ namespace Avalonia.Controls.Presenters
set => SetValue(BackgroundProperty, value);
}
/// <inheritdoc cref="TextElement.IsPlatformTextScalingEnabled"/>
public bool IsPlatformTextScalingEnabled
{
get => GetValue(IsPlatformTextScalingEnabledProperty);
set => SetValue(IsPlatformTextScalingEnabledProperty, value);
}
/// <summary>
/// Gets or sets a value that determines whether the TextPresenter shows a selection highlight.
/// </summary>
@ -181,6 +194,13 @@ namespace Avalonia.Controls.Presenters
get => TextElement.GetFontSize(this);
set => TextElement.SetFontSize(this, value);
}
double IPlatformTextScaleable.GetScaledFontSize(double baseFontSize) => IsPlatformTextScalingEnabled && TopLevel.GetTopLevel(this) is { PlatformSettings: { } platformSettings } ? platformSettings.GetScaledFontSize(baseFontSize) : baseFontSize;
/// <summary>
/// Gets <see cref="FontSize"/> scaled according to the platform's current text scaling rules.
/// </summary>
protected double EffectiveFontSize => ((IPlatformTextScaleable)this).GetScaledFontSize(FontSize);
/// <summary>
/// Gets or sets the font style.
@ -335,6 +355,14 @@ namespace Avalonia.Controls.Presenters
internal TextSelectionHandleCanvas? TextSelectionHandleCanvas { get; set; }
void IPlatformTextScaleable.OnPlatformTextScalingChanged()
{
if (IsPlatformTextScalingEnabled)
{
InvalidateMeasure();
}
}
/// <summary>
/// Creates the <see cref="TextLayout"/> used to render the text.
/// </summary>
@ -350,7 +378,7 @@ namespace Avalonia.Controls.Presenters
var maxWidth = MathUtilities.IsZero(constraint.Width) ? double.PositiveInfinity : constraint.Width;
var maxHeight = MathUtilities.IsZero(constraint.Height) ? double.PositiveInfinity : constraint.Height;
var textLayout = new TextLayout(text, typeface, FontFeatures, FontSize, foreground, TextAlignment,
var textLayout = new TextLayout(text, typeface, FontFeatures, EffectiveFontSize, foreground, TextAlignment,
TextWrapping, maxWidth: maxWidth, maxHeight: maxHeight, textStyleOverrides: textStyleOverrides,
flowDirection: FlowDirection, lineHeight: LineHeight, letterSpacing: LetterSpacing);
@ -553,7 +581,7 @@ namespace Avalonia.Controls.Presenters
if (!string.IsNullOrEmpty(preeditText))
{
var preeditHighlight = new ValueSpan<TextRunProperties>(caretIndex, preeditText.Length,
new GenericTextRunProperties(typeface, FontFeatures, FontSize,
new GenericTextRunProperties(typeface, FontFeatures, EffectiveFontSize,
foregroundBrush: foreground,
textDecorations: TextDecorations.Underline));
@ -569,7 +597,7 @@ namespace Avalonia.Controls.Presenters
textStyleOverrides = new[]
{
new ValueSpan<TextRunProperties>(start, length,
new GenericTextRunProperties(typeface, FontFeatures, FontSize,
new GenericTextRunProperties(typeface, FontFeatures, EffectiveFontSize,
foregroundBrush: SelectionForegroundBrush))
};
}

2
src/Avalonia.Controls/Primitives/TextSelectionCanvas.cs

@ -154,7 +154,7 @@ namespace Avalonia.Controls.Primitives
}
var point = ToPresenter(handle.IndicatorPosition);
point = point.WithY(point.Y - _presenter.FontSize / 2);
point = point.WithY(point.Y - ((IPlatformTextScaleable)_presenter).GetScaledFontSize(_presenter.FontSize) / 2);
var hit = _presenter.TextLayout.HitTestPoint(point);
var position = hit.CharacterHit.FirstCharacterIndex + hit.CharacterHit.TrailingLength;

6
src/Avalonia.Controls/SelectableTextBlock.cs

@ -187,7 +187,7 @@ namespace Avalonia.Controls
var defaultProperties = new GenericTextRunProperties(
typeface,
FontFeatures,
FontSize,
EffectiveFontSize,
TextDecorations,
Foreground);
@ -236,7 +236,7 @@ namespace Avalonia.Controls
new GenericTextRunProperties(
textRun.Properties?.Typeface ?? typeface,
textRun.Properties?.FontFeatures ?? FontFeatures,
FontSize,
EffectiveFontSize,
foregroundBrush: SelectionForegroundBrush)));
accumulatedLength += runLength;
@ -247,7 +247,7 @@ namespace Avalonia.Controls
textStyleOverrides =
[
new ValueSpan<TextRunProperties>(start, length,
new GenericTextRunProperties(typeface, FontFeatures, FontSize,
new GenericTextRunProperties(typeface, FontFeatures, EffectiveFontSize,
foregroundBrush: SelectionForegroundBrush))
];
}

35
src/Avalonia.Controls/TextBlock.cs

@ -15,7 +15,7 @@ namespace Avalonia.Controls
/// <summary>
/// A control that displays a block of text.
/// </summary>
public class TextBlock : Control, IInlineHost
public class TextBlock : Control, IInlineHost, IPlatformTextScaleable
{
/// <summary>
/// Defines the <see cref="Background"/> property.
@ -23,6 +23,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<IBrush?> BackgroundProperty =
Border.BackgroundProperty.AddOwner<TextBlock>();
/// <summary>
/// Defines the <see cref="IsPlatformTextScalingEnabled"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsPlatformTextScalingEnabledProperty =
TextElement.IsPlatformTextScalingEnabledProperty.AddOwner<TextBlock>();
/// <summary>
/// Defines the <see cref="Padding"/> property.
/// </summary>
@ -98,7 +104,7 @@ namespace Avalonia.Controls
/// Defines the <see cref="LetterSpacing"/> property.
/// </summary>
/// <remarks>
/// This property uses <see cref="AvaloniaProperty.AddOwner{TOwner}(AvaloniaProperty)"/> to share the same
/// This property uses <see cref="StyledProperty{TValue}.AddOwner{TOwner}(StyledPropertyMetadata{TValue}?)"/> to share the same
/// definition as <see cref="TextElement.LetterSpacingProperty"/>, ensuring consistent behavior across text
/// elements and templated controls.
/// </remarks>
@ -192,6 +198,13 @@ namespace Avalonia.Controls
/// </summary>
public TextLayout TextLayout => _textLayout ??= CreateTextLayout(Text);
/// <inheritdoc cref="TextElement.IsPlatformTextScalingEnabled"/>
public bool IsPlatformTextScalingEnabled
{
get => GetValue(IsPlatformTextScalingEnabledProperty);
set => SetValue(IsPlatformTextScalingEnabledProperty, value);
}
/// <summary>
/// Gets or sets the padding to place around the <see cref="Text"/>.
/// </summary>
@ -237,6 +250,13 @@ namespace Avalonia.Controls
set => SetValue(FontSizeProperty, value);
}
double IPlatformTextScaleable.GetScaledFontSize(double baseFontSize) => IsPlatformTextScalingEnabled && TopLevel.GetTopLevel(this) is { PlatformSettings: { } platformSettings } ? platformSettings.GetScaledFontSize(baseFontSize) : baseFontSize;
/// <summary>
/// Gets <see cref="FontSize"/> scaled according to the platform's current text scaling rules.
/// </summary>
protected double EffectiveFontSize => ((IPlatformTextScaleable)this).GetScaledFontSize(FontSize);
/// <summary>
/// Gets or sets the font style used to draw the control's text.
/// </summary>
@ -652,6 +672,14 @@ namespace Avalonia.Controls
}
}
void IPlatformTextScaleable.OnPlatformTextScalingChanged()
{
if (IsPlatformTextScalingEnabled)
{
InvalidateMeasure();
}
}
/// <summary>
/// Creates the <see cref="TextLayout"/> used to render the text.
/// </summary>
@ -663,7 +691,7 @@ namespace Avalonia.Controls
var defaultProperties = new GenericTextRunProperties(
typeface,
FontFeatures,
FontSize,
EffectiveFontSize,
TextDecorations,
Foreground);
@ -838,6 +866,7 @@ namespace Avalonia.Controls
switch (change.Property.Name)
{
case nameof(FontSize):
case nameof(IsPlatformTextScalingEnabled):
case nameof(FontWeight):
case nameof(FontStyle):
case nameof(FontFamily):

12
src/Avalonia.Controls/TopLevel.cs

@ -26,6 +26,7 @@ using System.Threading.Tasks;
using Avalonia.Diagnostics;
using Avalonia.Rendering.Composition;
using Avalonia.Threading;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
@ -252,6 +253,8 @@ namespace Avalonia.Controls
PlatformImpl?.SetFrameThemeVariant((PlatformThemeVariant?)variant ?? PlatformThemeVariant.Light);
});
PlatformSettings?.TextScalingChanged += OnPlatformTextScalingChanged;
_keyboardNavigationHandler?.SetOwner(this);
_accessKeyHandler?.SetOwner(this);
@ -341,6 +344,15 @@ namespace Avalonia.Controls
}
});
}
private void OnPlatformTextScalingChanged(object? sender, EventArgs e)
{
foreach (var scaleable in this.GetVisualDescendants().OfType<IPlatformTextScaleable>())
{
scaleable.OnPlatformTextScalingChanged();
}
}
/// <summary>
/// Fired when the window is opened.
/// </summary>

47
src/Windows/Avalonia.Win32/Win32PlatformSettings.cs

@ -8,11 +8,22 @@ namespace Avalonia.Win32;
internal class Win32PlatformSettings : DefaultPlatformSettings
{
private static readonly Lazy<bool> s_uiSettingsSupported = new(() =>
WinRTApiInformation.IsTypePresent("Windows.UI.ViewManagement.UISettings")
&& WinRTApiInformation.IsTypePresent("Windows.UI.ViewManagement.AccessibilitySettings"));
private PlatformColorValues? _lastColorValues;
private double _textScaleFactor = s_uiSettings2?.TextScaleFactor ?? 1;
private static readonly IUISettings2? s_uiSettings2;
private static readonly bool s_uiSettingsSupported;
static Win32PlatformSettings()
{
s_uiSettingsSupported = WinRTApiInformation.IsTypePresent("Windows.UI.ViewManagement.UISettings")
&& WinRTApiInformation.IsTypePresent("Windows.UI.ViewManagement.AccessibilitySettings");
if (s_uiSettingsSupported)
{
s_uiSettings2 = NativeWinRTMethods.CreateInstance<IUISettings2>("Windows.UI.ViewManagement.UISettings");
}
}
public override Size GetTapSize(PointerType type)
{
@ -33,10 +44,10 @@ internal class Win32PlatformSettings : DefaultPlatformSettings
}
public override TimeSpan GetDoubleTapTime(PointerType type) => TimeSpan.FromMilliseconds(GetDoubleClickTime());
public override PlatformColorValues GetColorValues()
{
if (!s_uiSettingsSupported.Value)
if (!s_uiSettingsSupported)
{
return base.GetColorValues();
}
@ -74,10 +85,10 @@ internal class Win32PlatformSettings : DefaultPlatformSettings
PlatformThemeVariant.Light,
ContrastPreference = ColorContrastPreference.NoPreference,
AccentColor1 = accent
};
};
}
}
internal void OnColorValuesChanged()
{
var oldColorValues = _lastColorValues;
@ -87,5 +98,25 @@ internal class Win32PlatformSettings : DefaultPlatformSettings
{
OnColorValuesChanged(colorValues);
}
var newTextScaleFactor = s_uiSettings2?.TextScaleFactor ?? 1;
if (newTextScaleFactor != _textScaleFactor)
{
_textScaleFactor = newTextScaleFactor;
OnTextScaleChanged();
}
}
/// <summary>
/// The algorithm used is undocumented, but <see href="https://github.com/microsoft/microsoft-ui-xaml/blob/5788dee271452753d5b2c70179f976c3e96a45c7/src/dxaml/xcp/core/text/common/TextFormatting.cpp#L181-L204">defined in Microsoft's source code</see>.
/// </summary>
public override double GetScaledFontSize(double baseFontSize)
{
if (baseFontSize <= 0)
{
return baseFontSize;
}
return Math.Max(1, baseFontSize) + Math.Max(-Math.Exp(1) * Math.Log(baseFontSize) + 18, 0.0) * (_textScaleFactor - 1);
}
}

9
src/Windows/Avalonia.Win32/WinRT/winrt.idl

@ -859,6 +859,15 @@ enum UIColorType
Complement = 9
}
[uuid(BAD82401-2721-44F9-BB91-2BB228BE442F)]
interface IUISettings2 : IInspectable
{
[propget] HRESULT GetTextScaleFactor([out][retval] DOUBLE* value);
[eventadd] HRESULT AddTextScaleFactorChanged([in] void* handler, [out][retval] int* token);
[eventremove] HRESULT RemoveTextScaleFactorChanged([in] int token);
}
[uuid(03021BE4-5254-4781-8194-5168F7D06D7B)]
interface IUISettings3 : IInspectable
{

20
src/iOS/Avalonia.iOS/PlatformSettings.cs

@ -1,6 +1,7 @@
using System;
using Avalonia.Media;
using Avalonia.Platform;
using Foundation;
using UIKit;
namespace Avalonia.iOS;
@ -8,8 +9,25 @@ namespace Avalonia.iOS;
// TODO: ideally should be created per view/activity.
internal class PlatformSettings : DefaultPlatformSettings
{
private readonly NSObject _contentSizeChangedToken;
private PlatformColorValues? _lastColorValues;
public PlatformSettings()
{
_contentSizeChangedToken = UIApplication.Notifications.ObserveContentSizeCategoryChanged(OnContentSizeCategoryChanged);
}
~PlatformSettings()
{
_contentSizeChangedToken.Dispose();
}
private void OnContentSizeCategoryChanged(object? sender, UIContentSizeCategoryChangedEventArgs e)
{
OnTextScaleChanged();
}
public override PlatformColorValues GetColorValues()
{
var themeVariant = UITraitCollection.CurrentTraitCollection.UserInterfaceStyle == UIUserInterfaceStyle.Dark ?
@ -61,4 +79,6 @@ internal class PlatformSettings : DefaultPlatformSettings
OnColorValuesChanged(colorValues);
}
}
public override double GetScaledFontSize(double baseFontSize) => UIFontMetrics.DefaultMetrics.GetScaledValue((nfloat)baseFontSize);
}

Loading…
Cancel
Save