Browse Source

Added TextScaling static class with configuration properties

Added ITextScaler to support custom text scaling
Calculate a font scale factor and use this to adjust height and spacing values
Fixed TextBox not updating when LineHeight changes and MaxLines or MinLines are active
pull/20292/head
Tom Edwards 3 months ago
parent
commit
e5babc8a6f
  1. 2
      samples/ControlCatalog/MainView.xaml
  2. 16
      src/Avalonia.Base/ITextScaler.cs
  3. 2
      src/Avalonia.Base/Platform/DefaultPlatformSettings.cs
  4. 12
      src/Avalonia.Base/Platform/IPlatformSettings.cs
  5. 2
      src/Avalonia.Controls/Documents/IInlineHost.cs
  6. 2
      src/Avalonia.Controls/Documents/Inline.cs
  7. 32
      src/Avalonia.Controls/Documents/TextElement.cs
  8. 17
      src/Avalonia.Controls/IPlatformTextScaleable.cs
  9. 13
      src/Avalonia.Controls/ITextScaleable.cs
  10. 57
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  11. 6
      src/Avalonia.Controls/Primitives/TextSelectionCanvas.cs
  12. 9
      src/Avalonia.Controls/SelectableTextBlock.cs
  13. 46
      src/Avalonia.Controls/TextBlock.cs
  14. 29
      src/Avalonia.Controls/TextBox.cs
  15. 139
      src/Avalonia.Controls/TextScaling.cs
  16. 9
      src/Avalonia.Controls/TopLevel.cs
  17. 4
      src/Windows/Avalonia.Win32/Win32PlatformSettings.cs
  18. 2
      src/iOS/Avalonia.iOS/PlatformSettings.cs

2
samples/ControlCatalog/MainView.xaml

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

16
src/Avalonia.Base/ITextScaler.cs

@ -0,0 +1,16 @@
using System;
namespace Avalonia;
/// <summary>
/// Represents an object which can algoritmically scale text.
/// </summary>
public interface ITextScaler
{
double GetScaledFontSize(Visual target, double baseFontSize);
/// <summary>
/// Raised when the text scaling algorithm has changed. Indicates that all text should be rescaled.
/// </summary>
event EventHandler<EventArgs>? TextScalingChanged;
}

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

@ -57,6 +57,6 @@ namespace Avalonia.Platform
protected virtual void OnTextScaleChanged() => Dispatcher.UIThread.Send(_ => TextScalingChanged?.Invoke(this, EventArgs.Empty));
public virtual double GetScaledFontSize(double baseFontSize) => baseFontSize;
public virtual double GetScaledFontSize(Visual target, double baseFontSize) => baseFontSize;
}
}

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

@ -10,7 +10,7 @@ namespace Avalonia.Platform
/// Some of these settings might be changed by used globally in the OS in runtime.
/// </summary>
[NotClientImplementable]
public interface IPlatformSettings
public interface IPlatformSettings : ITextScaler
{
/// <summary>
/// The size of the rectangle around the location of a pointer down that a pointer up
@ -52,15 +52,5 @@ 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, IPlatformTextScaleable
internal interface IInlineHost : ILogical, ITextScaleable
{
void Invalidate();

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

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

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

@ -14,15 +14,6 @@ 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>
@ -111,17 +102,6 @@ namespace Avalonia.Controls.Documents
set => SetValue(BackgroundProperty, value);
}
/// <summary>
/// Gets or sets whether <see cref="FontSize"/>, <see cref="LetterSpacing"/>, <see cref="TextBlock.LineHeight"/>, and
/// <see cref="TextBlock.LineSpacing"/> should be scaled according to platform text scaling rules when measuring and rendering this control.
/// </summary>
/// <remarks>Text scaling is typically not uniform. Smaller text scales up faster than larger text.</remarks>
public bool IsPlatformTextScalingEnabled
{
get => GetValue(IsPlatformTextScalingEnabledProperty);
set => SetValue(IsPlatformTextScalingEnabledProperty, value);
}
/// <summary>
/// Gets or sets the font family.
/// </summary>
@ -194,9 +174,6 @@ 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>
@ -377,17 +354,22 @@ namespace Avalonia.Controls.Documents
{
base.OnPropertyChanged(change);
if (change.Property.OwnerType == typeof(TextScaling))
{
InlineHost?.Invalidate();
return;
}
switch (change.Property.Name)
{
case nameof(Background):
case nameof(FontFamily):
case nameof(FontSize):
case nameof(IsPlatformTextScalingEnabled):
case nameof(FontStyle):
case nameof(FontWeight):
case nameof(FontStretch):
case nameof(Foreground):
InlineHost?.Invalidate();
InlineHost?.Invalidate();
break;
}
}

17
src/Avalonia.Controls/IPlatformTextScaleable.cs

@ -1,17 +0,0 @@
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);
}

13
src/Avalonia.Controls/ITextScaleable.cs

@ -0,0 +1,13 @@
namespace Avalonia.Controls;
/// <summary>
/// Represents an object which particpates in <see cref="TextScaling"/>.
/// </summary>
/// <seealso cref="ITextScaler"/>
public interface ITextScaleable
{
/// <summary>
/// Called when the active text scaling algorithm for this object changes.
/// </summary>
void OnTextScalingChanged();
}

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

@ -1,9 +1,7 @@
using System;
using System.Collections.Generic;
using System.Data;
using Avalonia.Controls.Documents;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Media.Immutable;
@ -15,7 +13,7 @@ using Avalonia.VisualTree;
namespace Avalonia.Controls.Presenters
{
public class TextPresenter : Control, IPlatformTextScaleable
public class TextPresenter : Control, ITextScaleable
{
public static readonly StyledProperty<bool> ShowSelectionHighlightProperty =
AvaloniaProperty.Register<TextPresenter, bool>(nameof(ShowSelectionHighlight), defaultValue: true);
@ -47,12 +45,6 @@ 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>
@ -64,7 +56,7 @@ namespace Avalonia.Controls.Presenters
/// </summary>
public static readonly StyledProperty<string?> PreeditTextProperty =
AvaloniaProperty.Register<TextPresenter, string?>(nameof(PreeditText));
/// <summary>
/// Defines the <see cref="PreeditText"/> property.
/// </summary>
@ -130,13 +122,6 @@ 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>
@ -161,7 +146,7 @@ namespace Avalonia.Controls.Presenters
get => GetValue(PreeditTextProperty);
set => SetValue(PreeditTextProperty, value);
}
public int? PreeditTextCursorPosition
{
get => GetValue(PreeditTextCursorPositionProperty);
@ -194,12 +179,6 @@ namespace Avalonia.Controls.Presenters
get => TextElement.GetFontSize(this);
set => TextElement.SetFontSize(this, value);
}
/// <inheritdoc cref="IPlatformTextScaleable.GetScaledFontSize(double)"/>
protected double GetScaledFontSize(double baseFontSize) => !double.IsNaN(baseFontSize) && IsPlatformTextScalingEnabled &&
TopLevel.GetTopLevel(this) is { PlatformSettings: { } platformSettings } ? platformSettings.GetScaledFontSize(baseFontSize) : baseFontSize;
double IPlatformTextScaleable.GetScaledFontSize(double baseFontSize) => GetScaledFontSize(baseFontSize);
/// <summary>
/// Gets or sets the font style.
@ -354,13 +333,7 @@ namespace Avalonia.Controls.Presenters
internal TextSelectionHandleCanvas? TextSelectionHandleCanvas { get; set; }
void IPlatformTextScaleable.OnPlatformTextScalingChanged()
{
if (IsPlatformTextScalingEnabled)
{
InvalidateMeasure();
}
}
void ITextScaleable.OnTextScalingChanged() => InvalidateMeasure();
/// <summary>
/// Creates the <see cref="TextLayout"/> used to render the text.
@ -377,9 +350,12 @@ 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, GetScaledFontSize(FontSize), foreground, TextAlignment,
var effectiveFontSize = TextScaling.GetScaledFontSize(this, FontSize);
var fontScaleFactor = effectiveFontSize / FontSize;
var textLayout = new TextLayout(text, typeface, FontFeatures, effectiveFontSize, foreground, TextAlignment,
TextWrapping, maxWidth: maxWidth, maxHeight: maxHeight, textStyleOverrides: textStyleOverrides,
flowDirection: FlowDirection, lineHeight: GetScaledFontSize(LineHeight), letterSpacing: GetScaledFontSize(LetterSpacing));
flowDirection: FlowDirection, lineHeight: LineHeight * fontScaleFactor, letterSpacing: LetterSpacing * fontScaleFactor);
return textLayout;
}
@ -568,7 +544,7 @@ namespace Avalonia.Controls.Presenters
var preeditText = PreeditText;
var text = GetCombinedText(Text, caretIndex, preeditText);
var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
var effectiveFontSize = GetScaledFontSize(FontSize);
var effectiveFontSize = TextScaling.GetScaledFontSize(this, FontSize);
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
var start = Math.Min(selectionStart, selectionEnd);
@ -775,7 +751,7 @@ namespace Avalonia.Controls.Presenters
CaretChanged();
}
private void EnsureCaretTimer()
{
if (_caretTimer == null)
@ -801,7 +777,7 @@ namespace Avalonia.Controls.Presenters
_caretTimer = null;
}
if (CaretBlinkInterval.TotalMilliseconds > 0)
if (CaretBlinkInterval.TotalMilliseconds > 0)
{
_caretTimer = new DispatcherTimer { Interval = CaretBlinkInterval };
_caretTimer.Tick += CaretTimerTick;
@ -998,7 +974,7 @@ namespace Avalonia.Controls.Presenters
_caretTimer.Tick -= CaretTimerTick;
}
}
private void OnPreeditChanged(string? preeditText, int? cursorPosition)
{
if (string.IsNullOrEmpty(preeditText))
@ -1029,7 +1005,7 @@ namespace Avalonia.Controls.Presenters
{
OnPreeditChanged(change.NewValue as string, PreeditTextCursorPosition);
}
if(change.Property == PreeditTextCursorPositionProperty)
{
OnPreeditChanged(PreeditText, PreeditTextCursorPosition);
@ -1056,6 +1032,11 @@ namespace Avalonia.Controls.Presenters
ResetCaretTimer();
}
if (change.Property.OwnerType == typeof(TextScaling))
{
InvalidateTextLayout();
}
switch (change.Property.Name)
{
case nameof(PreeditText):

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

@ -154,7 +154,7 @@ namespace Avalonia.Controls.Primitives
}
var point = ToPresenter(handle.IndicatorPosition);
point = point.WithY(point.Y - ((IPlatformTextScaleable)_presenter).GetScaledFontSize(_presenter.FontSize) / 2);
point = point.WithY(point.Y - TextScaling.GetScaledFontSize(_presenter, _presenter.FontSize) / 2);
var hit = _presenter.TextLayout.HitTestPoint(point);
var position = hit.CharacterHit.FirstCharacterIndex + hit.CharacterHit.TrailingLength;
@ -327,9 +327,6 @@ namespace Avalonia.Controls.Primitives
{
if (_textBox.ContextFlyout is PopupFlyoutBase flyout)
{
var verticalOffset = ((IPlatformTextScaleable)_textBox).GetScaledFontSize(double.IsNaN(_textBox.LineHeight) ? _textBox.FontSize : _textBox.LineHeight) +
ContextMenuPadding;
TextSelectionHandle? handle = null;
if (_textBox.SelectionStart != _textBox.SelectionEnd)
@ -349,6 +346,7 @@ namespace Avalonia.Controls.Primitives
if (handle != null)
{
var verticalOffset = (_textBox.GetLineHeight() ?? 0) + ContextMenuPadding;
var topLeft = ToTextBox(handle.GetTopLeft());
flyout.VerticalOffset = topLeft.Y - verticalOffset;
flyout.HorizontalOffset = topLeft.X;

9
src/Avalonia.Controls/SelectableTextBlock.cs

@ -183,8 +183,9 @@ namespace Avalonia.Controls
protected override TextLayout CreateTextLayout(string? text)
{
var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
var effectiveFontSize = GetScaledFontSize(FontSize);
var effectiveFontSize = TextScaling.GetScaledFontSize(this, FontSize);
var fontScaleFactor = effectiveFontSize / FontSize;
var defaultProperties = new GenericTextRunProperties(
typeface,
FontFeatures,
@ -193,9 +194,9 @@ namespace Avalonia.Controls
Foreground);
var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false,
defaultProperties, TextWrapping, GetScaledFontSize(LineHeight), 0, GetScaledFontSize(LetterSpacing))
defaultProperties, TextWrapping, LineHeight * fontScaleFactor, 0, LetterSpacing * fontScaleFactor)
{
LineSpacing = GetScaledFontSize(LineSpacing),
LineSpacing = LineSpacing * fontScaleFactor,
};
List<ValueSpan<TextRunProperties>>? textStyleOverrides = null;

46
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, IPlatformTextScaleable
public class TextBlock : Control, IInlineHost, ITextScaleable
{
/// <summary>
/// Defines the <see cref="Background"/> property.
@ -23,12 +23,6 @@ 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>
@ -198,13 +192,6 @@ 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>
@ -250,12 +237,6 @@ namespace Avalonia.Controls
set => SetValue(FontSizeProperty, value);
}
/// <inheritdoc cref="IPlatformTextScaleable.GetScaledFontSize(double)"/>
protected double GetScaledFontSize(double baseFontSize) => !double.IsNaN(baseFontSize) && IsPlatformTextScalingEnabled &&
TopLevel.GetTopLevel(this) is { PlatformSettings: { } platformSettings } ? platformSettings.GetScaledFontSize(baseFontSize) : baseFontSize;
double IPlatformTextScaleable.GetScaledFontSize(double baseFontSize) => GetScaledFontSize(baseFontSize);
/// <summary>
/// Gets or sets the font style used to draw the control's text.
/// </summary>
@ -671,13 +652,7 @@ namespace Avalonia.Controls
}
}
void IPlatformTextScaleable.OnPlatformTextScalingChanged()
{
if (IsPlatformTextScalingEnabled)
{
InvalidateMeasure();
}
}
void ITextScaleable.OnTextScalingChanged() => InvalidateMeasure();
/// <summary>
/// Creates the <see cref="TextLayout"/> used to render the text.
@ -690,14 +665,16 @@ namespace Avalonia.Controls
var defaultProperties = new GenericTextRunProperties(
typeface,
FontFeatures,
GetScaledFontSize(FontSize),
TextScaling.GetScaledFontSize(this, FontSize),
TextDecorations,
Foreground);
var fontScaleFactor = defaultProperties.FontRenderingEmSize / FontSize;
var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, IsMeasureValid ? TextAlignment : TextAlignment.Left, true, false,
defaultProperties, TextWrapping, GetScaledFontSize(LineHeight), 0, GetScaledFontSize(LetterSpacing))
defaultProperties, TextWrapping, LineHeight * fontScaleFactor, 0, LetterSpacing * fontScaleFactor)
{
LineSpacing = GetScaledFontSize(LineSpacing),
LineSpacing = LineSpacing * fontScaleFactor,
};
ITextSource textSource;
@ -762,7 +739,7 @@ namespace Avalonia.Controls
//Force arrange so text will be properly aligned.
InvalidateArrange();
}
var inlines = Inlines;
if (HasComplexContent)
@ -862,10 +839,15 @@ namespace Avalonia.Controls
}
}
if (change.Property.OwnerType == typeof(TextScaling))
{
InvalidateTextLayout();
return;
}
switch (change.Property.Name)
{
case nameof(FontSize):
case nameof(IsPlatformTextScalingEnabled):
case nameof(FontWeight):
case nameof(FontStyle):
case nameof(FontFamily):

29
src/Avalonia.Controls/TextBox.cs

@ -824,6 +824,20 @@ namespace Avalonia.Controls
return this._presenter?.TextLayout.TextLines.Count ?? -1;
}
/// <summary>
/// Gets the height of each line in the <see cref="TextBox"/>, or null if no layout information is available.
/// </summary>
/// <returns></returns>
public double? GetLineHeight()
{
if (_presenter == null)
return null;
var scaledFontSize = TextScaling.GetScaledFontSize(_presenter, _presenter.FontSize);
return double.IsNaN(LineHeight) ? scaledFontSize : LineHeight * (scaledFontSize / _presenter.FontSize);
}
/// <summary>
/// Raised when content is being copied to the clipboard
/// </summary>
@ -985,6 +999,10 @@ namespace Avalonia.Controls
{
InvalidateMeasure();
}
else if (change.Property == LineHeightProperty)
{
InvalidateMeasure();
}
else if (change.Property == UndoLimitProperty)
{
OnUndoLimitChanged(change.GetNewValue<int>());
@ -2392,9 +2410,14 @@ namespace Avalonia.Controls
/// <returns>Height of <paramref name="numLines"/> lines of text in device-independent pixels.</returns>
private double CalculateLineDIPHeight(int numLines)
{
var textScaler = _presenter as IPlatformTextScaleable;
var effectiveFontSize = textScaler?.GetScaledFontSize(FontSize) ?? FontSize;
var effectiveLineHeight = textScaler?.GetScaledFontSize(LineHeight) ?? LineHeight;
var effectiveFontSize = FontSize;
var effectiveLineHeight = LineHeight;
if (_presenter != null)
{
effectiveFontSize = TextScaling.GetScaledFontSize(_presenter, effectiveFontSize);
effectiveLineHeight = effectiveFontSize / _presenter.FontSize * LineHeight;
}
var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
var paragraphProperties = TextLayout.CreateTextParagraphProperties(typeface, effectiveFontSize, null, default, default, null, default, effectiveLineHeight, default, FontFeatures);

139
src/Avalonia.Controls/TextScaling.cs

@ -0,0 +1,139 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Avalonia.Controls.Documents;
using Avalonia.Threading;
namespace Avalonia.Controls;
/// <summary>
/// Configures and computes text scaling. This is an accessibility feature which allows the user to request that text be
/// drawn at a different size than normal, without altering other UI elements. The default scaling algorithm is determined
/// by the platform and may make text smaller as well as larger.
/// </summary>
/// <remarks>
/// Text scaling is applied when text is measured. It does not modify the value of <see cref="TextElement.FontSize"/>.
/// </remarks>
/// <seealso cref="ITextScaleable"/>
/// <seealso cref="ITextScaler"/>
public static class TextScaling
{
private static readonly ConditionalWeakTable<ITextScaler, HashSet<Visual>> s_customTextScalerSubscribers = [];
/// <summary>
/// Determines whether <see cref="TextElement.FontSize"/> (along with <see cref="TextElement.LetterSpacing"/>, <see cref="TextBlock.LineHeight"/>, and
/// <see cref="TextBlock.LineSpacing"/>) should be scalled by calling <see cref="GetScaledFontSize"/> when measuring text content. The value is inherited.
/// </summary>
/// <remarks>Text scaling is typically not uniform. Smaller text scales up faster than larger text.</remarks>
public static readonly AttachedProperty<bool> IsEnabledProperty =
AvaloniaProperty.RegisterAttached<Visual, bool>("IsEnabled", typeof(TextScaling), inherits: true);
/// <summary>
/// Determines the minimum size (in em units) to which text will be scaled by <see cref="GetScaledFontSize"/>. The value is inherited.
/// </summary>
/// <remarks>This value is used only when <see cref="IsEnabledProperty"/> is true.</remarks>
public static readonly AttachedProperty<double> MinFontSizeProperty =
AvaloniaProperty.RegisterAttached<Visual, double>("MinFontSize", typeof(TextScaling), inherits: true, validate: size => size >= 0);
/// <summary>
/// Determines the maximum size (in em units) to which text will be scaled by <see cref="GetScaledFontSize"/>. The value is inherited.
/// </summary>
/// <remarks>This value is used only when <see cref="IsEnabledProperty"/> is true.</remarks>
public static readonly AttachedProperty<double> MaxFontSizeProperty =
AvaloniaProperty.RegisterAttached<Visual, double>("MaxFontSize", typeof(TextScaling), defaultValue: double.PositiveInfinity, inherits: true,
validate: size => size > 0);
/// <summary>
/// Determines a user-defined text scaling algorithm, which overrides platform text scaling in <see cref="GetScaledFontSize"/>. The value is inherited.
/// </summary>
/// <remarks>This value is used only when <see cref="IsEnabledProperty"/> is true.</remarks>
public static readonly AttachedProperty<ITextScaler?> CustomTextScalerProperty =
AvaloniaProperty.RegisterAttached<Visual, ITextScaler?>("CustomTextScaler", typeof(TextScaling), inherits: true);
/// <inheritdoc cref="IsEnabledProperty"/> <see cref="IsEnabledProperty"/>
public static bool GetIsEnabled(Visual visual) => visual.GetValue(IsEnabledProperty);
/// <inheritdoc cref="IsEnabledProperty"/> <see cref="IsEnabledProperty"/>
public static void SetIsEnabled(Visual visual, bool value) => visual.SetValue(IsEnabledProperty, value);
/// <inheritdoc cref="MinFontSizeProperty"/> <see cref="MinFontSizeProperty"/>
public static double GetMinFontSize(Visual visual) => visual.GetValue(MinFontSizeProperty);
/// <inheritdoc cref="MinFontSizeProperty"/> <see cref="MinFontSizeProperty"/>
public static void SetMinFontSize(Visual visual, double value) => visual.SetValue(MinFontSizeProperty, value);
/// <inheritdoc cref="MaxFontSizeProperty"/> <see cref="MaxFontSizeProperty"/>
public static double GetMaxFontSize(Visual visual) => visual.GetValue(MaxFontSizeProperty);
/// <inheritdoc cref="MaxFontSizeProperty"/> <see cref="MaxFontSizeProperty"/>
public static void SetMaxFontSize(Visual visual, double value) => visual.SetValue(MaxFontSizeProperty, value);
/// <inheritdoc cref="CustomTextScalerProperty"/> <see cref="CustomTextScalerProperty"/>
public static ITextScaler? GetCustomTextScaler(Visual visual) => visual.GetValue(CustomTextScalerProperty);
/// <inheritdoc cref="CustomTextScalerProperty"/> <see cref="CustomTextScalerProperty"/>
public static void SetCustomTextScaler(Visual visual, ITextScaler? value) => visual.SetValue(CustomTextScalerProperty, value);
static TextScaling()
{
CustomTextScalerProperty.Changed.AddClassHandler<Visual>(OnCustomTextScalerChanged);
}
private static void OnCustomTextScalerChanged(Visual visual, AvaloniaPropertyChangedEventArgs args)
{
if (visual is not ITextScaleable)
{
return;
}
var (oldScaler, newScaler) = args.GetOldAndNewValue<ITextScaler?>();
if (oldScaler != null && s_customTextScalerSubscribers.TryGetValue(oldScaler, out var oldSubscribers))
{
oldSubscribers.Remove(visual);
}
if (newScaler != null)
{
if (s_customTextScalerSubscribers.TryGetValue(newScaler, out var newSubscribers))
{
newSubscribers.Add(visual);
}
else
{
s_customTextScalerSubscribers.Add(newScaler, [visual]);
newScaler.TextScalingChanged += OnCustomTextScalingChanged;
}
}
}
private static void OnCustomTextScalingChanged(object? sender, EventArgs e)
{
Dispatcher.UIThread.VerifyAccess();
if (sender is ITextScaler scaler && s_customTextScalerSubscribers.TryGetValue(scaler, out var subscribers))
{
foreach (var visual in subscribers)
{
if (GetIsEnabled(visual))
{
((ITextScaleable)visual).OnTextScalingChanged();
}
}
}
}
/// <summary>
/// Scales <paramref name="baseFontSize"/> according to either the current platform text scaling rules, or the rules
/// defined by the object assigned to <see cref="CustomTextScalerProperty"/> for <paramref name="visual"/>.
/// </summary>
/// <remarks>The values of <see cref="IsEnabledProperty"/>, <see cref="MinFontSizeProperty"/>, and <see cref="MaxFontSizeProperty"/>
/// are enforced, even when <see cref="CustomTextScalerProperty"/> is in use.</remarks>
public static double GetScaledFontSize(Visual visual, double baseFontSize)
{
if (double.IsNaN(baseFontSize) || baseFontSize <= 0 || !GetIsEnabled(visual) ||
(GetCustomTextScaler(visual) ?? TopLevel.GetTopLevel(visual)?.PlatformSettings) is not { } scaler)
{
return baseFontSize;
}
// Extend min/max to encompass the base size. Conveniently, this means that min > max is not possible.
var min = Math.Min(GetMinFontSize(visual), baseFontSize);
var max = Math.Max(GetMaxFontSize(visual), baseFontSize);
return Math.Clamp(scaler.GetScaledFontSize(visual, baseFontSize), min, max);
}
}

9
src/Avalonia.Controls/TopLevel.cs

@ -347,9 +347,14 @@ namespace Avalonia.Controls
private void OnPlatformTextScalingChanged(object? sender, EventArgs e)
{
foreach (var scaleable in this.GetVisualDescendants().OfType<IPlatformTextScaleable>())
foreach (var visual in this.GetVisualDescendants())
{
scaleable.OnPlatformTextScalingChanged();
if (visual is not ITextScaleable scaleable || !TextScaling.GetIsEnabled(visual) || TextScaling.GetCustomTextScaler(visual) != null)
{
continue;
}
scaleable.OnTextScalingChanged();
}
}

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

@ -110,9 +110,9 @@ internal class Win32PlatformSettings : DefaultPlatformSettings
/// <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)
public override double GetScaledFontSize(Visual target, double baseFontSize)
{
if (baseFontSize <= 0)
if (baseFontSize <= 0 || _textScaleFactor == 1)
{
return baseFontSize;
}

2
src/iOS/Avalonia.iOS/PlatformSettings.cs

@ -83,7 +83,7 @@ internal class PlatformSettings : DefaultPlatformSettings
}
}
public override double GetScaledFontSize(double baseFontSize)
public override double GetScaledFontSize(Visual target, double baseFontSize)
{
if (baseFontSize <= 0)
{

Loading…
Cancel
Save