Browse Source

Merge 0fcf8acea7 into 3068850405

pull/20292/merge
Tom Edwards 3 days ago
committed by GitHub
parent
commit
675aa3cbe4
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 3
      samples/ControlCatalog/MainView.xaml
  2. 16
      src/Avalonia.Base/ITextScaler.cs
  3. 10
      src/Avalonia.Base/Platform/DefaultPlatformSettings.cs
  4. 4
      src/Avalonia.Base/Platform/IPlatformSettings.cs
  5. 2
      src/Avalonia.Controls/Documents/IInlineHost.cs
  6. 2
      src/Avalonia.Controls/Documents/Inline.cs
  7. 8
      src/Avalonia.Controls/Documents/TextElement.cs
  8. 13
      src/Avalonia.Controls/ITextScaleable.cs
  9. 37
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  10. 6
      src/Avalonia.Controls/Primitives/TextSelectionCanvas.cs
  11. 12
      src/Avalonia.Controls/SelectableTextBlock.cs
  12. 21
      src/Avalonia.Controls/TextBlock.cs
  13. 66
      src/Avalonia.Controls/TextBox.cs
  14. 139
      src/Avalonia.Controls/TextScaling.cs
  15. 17
      src/Avalonia.Controls/TopLevel.cs
  16. 47
      src/Windows/Avalonia.Win32/Win32PlatformSettings.cs
  17. 9
      src/Windows/Avalonia.Win32/WinRT/winrt.idl
  18. 37
      src/iOS/Avalonia.iOS/PlatformSettings.cs

3
samples/ControlCatalog/MainView.xaml

@ -6,7 +6,8 @@
xmlns:models="using:ControlCatalog.Models" xmlns:models="using:ControlCatalog.Models"
xmlns:pages="using:ControlCatalog.Pages" xmlns:pages="using:ControlCatalog.Pages"
xmlns:viewModels="using:ControlCatalog.ViewModels" xmlns:viewModels="using:ControlCatalog.ViewModels"
x:DataType="viewModels:MainWindowViewModel"> x:DataType="viewModels:MainWindowViewModel"
TextScaling.IsEnabled="True">
<Grid> <Grid>
<Grid.Styles> <Grid.Styles>
<Style Selector="TextBlock.h2"> <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;
}

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

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

4
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. /// Some of these settings might be changed by used globally in the OS in runtime.
/// </summary> /// </summary>
[NotClientImplementable] [NotClientImplementable]
public interface IPlatformSettings public interface IPlatformSettings : ITextScaler
{ {
/// <summary> /// <summary>
/// The size of the rectangle around the location of a pointer down that a pointer up /// The size of the rectangle around the location of a pointer down that a pointer up
@ -42,7 +42,7 @@ namespace Avalonia.Platform
/// Get a configuration for platform-specific hotkeys in an Avalonia application. /// Get a configuration for platform-specific hotkeys in an Avalonia application.
/// </summary> /// </summary>
PlatformHotkeyConfiguration HotkeyConfiguration { get; } PlatformHotkeyConfiguration HotkeyConfiguration { get; }
/// <summary> /// <summary>
/// Gets current system color values including dark mode and accent colors. /// Gets current system color values including dark mode and accent colors.
/// </summary> /// </summary>

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

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

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

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

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

@ -354,6 +354,12 @@ namespace Avalonia.Controls.Documents
{ {
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
if (change.Property.OwnerType == typeof(TextScaling))
{
InlineHost?.Invalidate();
return;
}
switch (change.Property.Name) switch (change.Property.Name)
{ {
case nameof(Background): case nameof(Background):
@ -363,7 +369,7 @@ namespace Avalonia.Controls.Documents
case nameof(FontWeight): case nameof(FontWeight):
case nameof(FontStretch): case nameof(FontStretch):
case nameof(Foreground): case nameof(Foreground):
InlineHost?.Invalidate(); InlineHost?.Invalidate();
break; break;
} }
} }

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();
}

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

@ -1,9 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data;
using Avalonia.Controls.Documents; using Avalonia.Controls.Documents;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
@ -15,7 +13,7 @@ using Avalonia.VisualTree;
namespace Avalonia.Controls.Presenters namespace Avalonia.Controls.Presenters
{ {
public class TextPresenter : Control public class TextPresenter : Control, ITextScaleable
{ {
public static readonly StyledProperty<bool> ShowSelectionHighlightProperty = public static readonly StyledProperty<bool> ShowSelectionHighlightProperty =
AvaloniaProperty.Register<TextPresenter, bool>(nameof(ShowSelectionHighlight), defaultValue: true); AvaloniaProperty.Register<TextPresenter, bool>(nameof(ShowSelectionHighlight), defaultValue: true);
@ -58,7 +56,7 @@ namespace Avalonia.Controls.Presenters
/// </summary> /// </summary>
public static readonly StyledProperty<string?> PreeditTextProperty = public static readonly StyledProperty<string?> PreeditTextProperty =
AvaloniaProperty.Register<TextPresenter, string?>(nameof(PreeditText)); AvaloniaProperty.Register<TextPresenter, string?>(nameof(PreeditText));
/// <summary> /// <summary>
/// Defines the <see cref="PreeditText"/> property. /// Defines the <see cref="PreeditText"/> property.
/// </summary> /// </summary>
@ -148,7 +146,7 @@ namespace Avalonia.Controls.Presenters
get => GetValue(PreeditTextProperty); get => GetValue(PreeditTextProperty);
set => SetValue(PreeditTextProperty, value); set => SetValue(PreeditTextProperty, value);
} }
public int? PreeditTextCursorPosition public int? PreeditTextCursorPosition
{ {
get => GetValue(PreeditTextCursorPositionProperty); get => GetValue(PreeditTextCursorPositionProperty);
@ -335,6 +333,8 @@ namespace Avalonia.Controls.Presenters
internal TextSelectionHandleCanvas? TextSelectionHandleCanvas { get; set; } internal TextSelectionHandleCanvas? TextSelectionHandleCanvas { get; set; }
void ITextScaleable.OnTextScalingChanged() => InvalidateMeasure();
/// <summary> /// <summary>
/// Creates the <see cref="TextLayout"/> used to render the text. /// Creates the <see cref="TextLayout"/> used to render the text.
/// </summary> /// </summary>
@ -349,10 +349,13 @@ namespace Avalonia.Controls.Presenters
var maxWidth = MathUtilities.IsZero(constraint.Width) ? double.PositiveInfinity : constraint.Width; var maxWidth = MathUtilities.IsZero(constraint.Width) ? double.PositiveInfinity : constraint.Width;
var maxHeight = MathUtilities.IsZero(constraint.Height) ? double.PositiveInfinity : constraint.Height; var maxHeight = MathUtilities.IsZero(constraint.Height) ? double.PositiveInfinity : constraint.Height;
var effectiveFontSize = TextScaling.GetScaledFontSize(this, FontSize);
var fontScaleFactor = effectiveFontSize / FontSize;
var textLayout = new TextLayout( var textLayout = new TextLayout(
text, text,
typeface, typeface,
FontSize, effectiveFontSize,
Foreground, Foreground,
TextAlignment, TextAlignment,
TextWrapping, TextWrapping,
@ -361,8 +364,8 @@ namespace Avalonia.Controls.Presenters
FlowDirection, FlowDirection,
maxWidth, maxWidth,
maxHeight, maxHeight,
LineHeight, LineHeight * fontScaleFactor,
LetterSpacing, LetterSpacing * fontScaleFactor,
0, 0,
FontFeatures, FontFeatures,
textStyleOverrides); textStyleOverrides);
@ -554,6 +557,7 @@ namespace Avalonia.Controls.Presenters
var preeditText = PreeditText; var preeditText = PreeditText;
var text = GetCombinedText(Text, caretIndex, preeditText); var text = GetCombinedText(Text, caretIndex, preeditText);
var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch); var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
var effectiveFontSize = TextScaling.GetScaledFontSize(this, FontSize);
var selectionStart = SelectionStart; var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd; var selectionEnd = SelectionEnd;
var start = Math.Min(selectionStart, selectionEnd); var start = Math.Min(selectionStart, selectionEnd);
@ -568,7 +572,7 @@ namespace Avalonia.Controls.Presenters
var preeditHighlight = new ValueSpan<TextRunProperties>(caretIndex, preeditText.Length, var preeditHighlight = new ValueSpan<TextRunProperties>(caretIndex, preeditText.Length,
new GenericTextRunProperties( new GenericTextRunProperties(
typeface, typeface,
FontSize, effectiveFontSize,
TextDecorations.Underline, TextDecorations.Underline,
foreground, foreground,
fontFeatures: FontFeatures)); fontFeatures: FontFeatures));
@ -587,7 +591,7 @@ namespace Avalonia.Controls.Presenters
new ValueSpan<TextRunProperties>(start, length, new ValueSpan<TextRunProperties>(start, length,
new GenericTextRunProperties( new GenericTextRunProperties(
typeface, typeface,
FontSize, effectiveFontSize,
foregroundBrush: SelectionForegroundBrush, foregroundBrush: SelectionForegroundBrush,
fontFeatures: FontFeatures)) fontFeatures: FontFeatures))
}; };
@ -766,7 +770,7 @@ namespace Avalonia.Controls.Presenters
CaretChanged(); CaretChanged();
} }
private void EnsureCaretTimer() private void EnsureCaretTimer()
{ {
if (_caretTimer == null) if (_caretTimer == null)
@ -792,7 +796,7 @@ namespace Avalonia.Controls.Presenters
_caretTimer = null; _caretTimer = null;
} }
if (CaretBlinkInterval.TotalMilliseconds > 0) if (CaretBlinkInterval.TotalMilliseconds > 0)
{ {
_caretTimer = new DispatcherTimer { Interval = CaretBlinkInterval }; _caretTimer = new DispatcherTimer { Interval = CaretBlinkInterval };
_caretTimer.Tick += CaretTimerTick; _caretTimer.Tick += CaretTimerTick;
@ -989,7 +993,7 @@ namespace Avalonia.Controls.Presenters
_caretTimer.Tick -= CaretTimerTick; _caretTimer.Tick -= CaretTimerTick;
} }
} }
private void OnPreeditChanged(string? preeditText, int? cursorPosition) private void OnPreeditChanged(string? preeditText, int? cursorPosition)
{ {
if (string.IsNullOrEmpty(preeditText)) if (string.IsNullOrEmpty(preeditText))
@ -1020,7 +1024,7 @@ namespace Avalonia.Controls.Presenters
{ {
OnPreeditChanged(change.NewValue as string, PreeditTextCursorPosition); OnPreeditChanged(change.NewValue as string, PreeditTextCursorPosition);
} }
if(change.Property == PreeditTextCursorPositionProperty) if(change.Property == PreeditTextCursorPositionProperty)
{ {
OnPreeditChanged(PreeditText, PreeditTextCursorPosition); OnPreeditChanged(PreeditText, PreeditTextCursorPosition);
@ -1047,6 +1051,11 @@ namespace Avalonia.Controls.Presenters
ResetCaretTimer(); ResetCaretTimer();
} }
if (change.Property.OwnerType == typeof(TextScaling))
{
InvalidateTextLayout();
}
switch (change.Property.Name) switch (change.Property.Name)
{ {
case nameof(PreeditText): case nameof(PreeditText):

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

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

12
src/Avalonia.Controls/SelectableTextBlock.cs

@ -183,18 +183,20 @@ namespace Avalonia.Controls
protected override TextLayout CreateTextLayout(string? text) protected override TextLayout CreateTextLayout(string? text)
{ {
var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch); var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
var effectiveFontSize = TextScaling.GetScaledFontSize(this, FontSize);
var fontScaleFactor = effectiveFontSize / FontSize;
var defaultProperties = new GenericTextRunProperties( var defaultProperties = new GenericTextRunProperties(
typeface, typeface,
FontSize, effectiveFontSize,
TextDecorations, TextDecorations,
Foreground, Foreground,
fontFeatures: FontFeatures); fontFeatures: FontFeatures);
var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false, var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false,
defaultProperties, TextWrapping, LineHeight, 0, LetterSpacing) defaultProperties, TextWrapping, LineHeight * fontScaleFactor, 0, LetterSpacing * fontScaleFactor)
{ {
LineSpacing = LineSpacing LineSpacing = LineSpacing * fontScaleFactor,
}; };
List<ValueSpan<TextRunProperties>>? textStyleOverrides = null; List<ValueSpan<TextRunProperties>>? textStyleOverrides = null;
@ -235,7 +237,7 @@ namespace Avalonia.Controls
overlapLength, overlapLength,
new GenericTextRunProperties( new GenericTextRunProperties(
textRun.Properties?.Typeface ?? typeface, textRun.Properties?.Typeface ?? typeface,
FontSize, effectiveFontSize,
foregroundBrush: SelectionForegroundBrush, foregroundBrush: SelectionForegroundBrush,
fontFeatures: textRun.Properties?.FontFeatures ?? FontFeatures))); fontFeatures: textRun.Properties?.FontFeatures ?? FontFeatures)));
@ -249,7 +251,7 @@ namespace Avalonia.Controls
new ValueSpan<TextRunProperties>(start, length, new ValueSpan<TextRunProperties>(start, length,
new GenericTextRunProperties( new GenericTextRunProperties(
typeface, typeface,
FontSize, effectiveFontSize,
foregroundBrush: SelectionForegroundBrush, foregroundBrush: SelectionForegroundBrush,
fontFeatures: FontFeatures)) fontFeatures: FontFeatures))
]; ];

21
src/Avalonia.Controls/TextBlock.cs

@ -15,7 +15,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// A control that displays a block of text. /// A control that displays a block of text.
/// </summary> /// </summary>
public class TextBlock : Control, IInlineHost public class TextBlock : Control, IInlineHost, ITextScaleable
{ {
/// <summary> /// <summary>
/// Defines the <see cref="Background"/> property. /// Defines the <see cref="Background"/> property.
@ -650,6 +650,8 @@ namespace Avalonia.Controls
} }
} }
void ITextScaleable.OnTextScalingChanged() => InvalidateMeasure();
/// <summary> /// <summary>
/// Creates the <see cref="TextLayout"/> used to render the text. /// Creates the <see cref="TextLayout"/> used to render the text.
/// </summary> /// </summary>
@ -660,15 +662,17 @@ namespace Avalonia.Controls
var defaultProperties = new GenericTextRunProperties( var defaultProperties = new GenericTextRunProperties(
typeface, typeface,
FontSize, TextScaling.GetScaledFontSize(this, FontSize),
TextDecorations, TextDecorations,
Foreground, Foreground,
fontFeatures: FontFeatures); fontFeatures: FontFeatures);
var fontScaleFactor = defaultProperties.FontRenderingEmSize / FontSize;
var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, IsMeasureValid ? TextAlignment : TextAlignment.Left, true, false, var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, IsMeasureValid ? TextAlignment : TextAlignment.Left, true, false,
defaultProperties, TextWrapping, LineHeight, 0, LetterSpacing) defaultProperties, TextWrapping, LineHeight * fontScaleFactor, 0, LetterSpacing * fontScaleFactor)
{ {
LineSpacing = LineSpacing LineSpacing = LineSpacing * fontScaleFactor,
}; };
ITextSource textSource; ITextSource textSource;
@ -733,7 +737,7 @@ namespace Avalonia.Controls
//Force arrange so text will be properly aligned. //Force arrange so text will be properly aligned.
InvalidateArrange(); InvalidateArrange();
} }
var inlines = Inlines; var inlines = Inlines;
if (HasComplexContent) if (HasComplexContent)
@ -833,6 +837,12 @@ namespace Avalonia.Controls
} }
} }
if (change.Property.OwnerType == typeof(TextScaling))
{
InvalidateTextLayout();
return;
}
switch (change.Property.Name) switch (change.Property.Name)
{ {
case nameof(FontSize): case nameof(FontSize):
@ -849,6 +859,7 @@ namespace Avalonia.Controls
case nameof(Padding): case nameof(Padding):
case nameof(LineHeight): case nameof(LineHeight):
case nameof(LineSpacing):
case nameof(LetterSpacing): case nameof(LetterSpacing):
case nameof(MaxLines): case nameof(MaxLines):

66
src/Avalonia.Controls/TextBox.cs

@ -901,6 +901,20 @@ namespace Avalonia.Controls
return this._presenter?.TextLayout.TextLines.Count ?? -1; 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> /// <summary>
/// Raised when content is being copied to the clipboard /// Raised when content is being copied to the clipboard
/// </summary> /// </summary>
@ -1062,6 +1076,10 @@ namespace Avalonia.Controls
{ {
InvalidateMeasure(); InvalidateMeasure();
} }
else if (change.Property == LineHeightProperty)
{
InvalidateMeasure();
}
else if (change.Property == UndoLimitProperty) else if (change.Property == UndoLimitProperty)
{ {
OnUndoLimitChanged(change.GetNewValue<int>()); OnUndoLimitChanged(change.GetNewValue<int>());
@ -2459,39 +2477,31 @@ namespace Avalonia.Controls
{ {
if (_scrollViewer != null) if (_scrollViewer != null)
{ {
var maxHeight = double.PositiveInfinity; _scrollViewer.SetCurrentValue(MaxHeightProperty, MaxLines > 0 && double.IsNaN(Height) ? CalculateLineDIPHeight(MaxLines) : double.PositiveInfinity);
_scrollViewer.SetCurrentValue(MinHeightProperty, MinLines > 0 && double.IsNaN(Height) ? CalculateLineDIPHeight(MinLines) : 0);
if (MaxLines > 0 && double.IsNaN(Height)) }
{
var fontSize = FontSize;
var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
var paragraphProperties = TextLayout.CreateTextParagraphProperties(typeface, fontSize, null, default, default, null, default, LineHeight, default, FontFeatures);
var textLayout = new TextLayout(new LineTextSource(MaxLines), paragraphProperties);
var verticalSpace = GetVerticalSpaceBetweenScrollViewerAndPresenter();
maxHeight = Math.Ceiling(textLayout.Height + verticalSpace);
}
_scrollViewer.SetCurrentValue(MaxHeightProperty, maxHeight);
var minHeight = 0.0;
if (MinLines > 0 && double.IsNaN(Height)) return base.MeasureOverride(availableSize);
{ }
var fontSize = FontSize;
var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
var paragraphProperties = TextLayout.CreateTextParagraphProperties(typeface, fontSize, null, default, default, null, default, LineHeight, default, FontFeatures);
var textLayout = new TextLayout(new LineTextSource(MinLines), paragraphProperties);
var verticalSpace = GetVerticalSpaceBetweenScrollViewerAndPresenter();
minHeight = Math.Ceiling(textLayout.Height + verticalSpace); /// <returns>Height of <paramref name="numLines"/> lines of text in device-independent pixels.</returns>
} private double CalculateLineDIPHeight(int numLines)
{
var effectiveFontSize = FontSize;
var effectiveLineHeight = LineHeight;
_scrollViewer.SetCurrentValue(MinHeightProperty, minHeight); if (_presenter != null)
{
effectiveFontSize = TextScaling.GetScaledFontSize(_presenter, effectiveFontSize);
effectiveLineHeight = effectiveFontSize / _presenter.FontSize * LineHeight;
} }
return base.MeasureOverride(availableSize); var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
var paragraphProperties = TextLayout.CreateTextParagraphProperties(typeface, effectiveFontSize, null, default, default, null, default, effectiveLineHeight, default, FontFeatures);
var textLayout = new TextLayout(new LineTextSource(numLines), paragraphProperties);
var verticalSpace = GetVerticalSpaceBetweenScrollViewerAndPresenter();
return Math.Ceiling(textLayout.Height + verticalSpace);
} }
private class LineTextSource : ITextSource private class LineTextSource : ITextSource

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);
}
}

17
src/Avalonia.Controls/TopLevel.cs

@ -26,6 +26,7 @@ using System.Threading.Tasks;
using Avalonia.Diagnostics; using Avalonia.Diagnostics;
using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.VisualTree;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -244,6 +245,8 @@ namespace Avalonia.Controls
PlatformImpl?.SetFrameThemeVariant((PlatformThemeVariant?)variant ?? PlatformThemeVariant.Light); PlatformImpl?.SetFrameThemeVariant((PlatformThemeVariant?)variant ?? PlatformThemeVariant.Light);
}); });
PlatformSettings?.TextScalingChanged += OnPlatformTextScalingChanged;
_keyboardNavigationHandler?.SetOwner(this); _keyboardNavigationHandler?.SetOwner(this);
_accessKeyHandler?.SetOwner(this); _accessKeyHandler?.SetOwner(this);
@ -323,6 +326,20 @@ namespace Avalonia.Controls
} }
}); });
} }
private void OnPlatformTextScalingChanged(object? sender, EventArgs e)
{
foreach (var visual in this.GetVisualDescendants())
{
if (visual is not ITextScaleable scaleable || !TextScaling.GetIsEnabled(visual) || TextScaling.GetCustomTextScaler(visual) != null)
{
continue;
}
scaleable.OnTextScalingChanged();
}
}
/// <summary> /// <summary>
/// Fired when the window is opened. /// Fired when the window is opened.
/// </summary> /// </summary>

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

@ -8,11 +8,22 @@ namespace Avalonia.Win32;
internal class Win32PlatformSettings : DefaultPlatformSettings 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 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) 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 TimeSpan GetDoubleTapTime(PointerType type) => TimeSpan.FromMilliseconds(GetDoubleClickTime());
public override PlatformColorValues GetColorValues() public override PlatformColorValues GetColorValues()
{ {
if (!s_uiSettingsSupported.Value) if (!s_uiSettingsSupported)
{ {
return base.GetColorValues(); return base.GetColorValues();
} }
@ -74,10 +85,10 @@ internal class Win32PlatformSettings : DefaultPlatformSettings
PlatformThemeVariant.Light, PlatformThemeVariant.Light,
ContrastPreference = ColorContrastPreference.NoPreference, ContrastPreference = ColorContrastPreference.NoPreference,
AccentColor1 = accent AccentColor1 = accent
}; };
} }
} }
internal void OnColorValuesChanged() internal void OnColorValuesChanged()
{ {
var oldColorValues = _lastColorValues; var oldColorValues = _lastColorValues;
@ -87,5 +98,25 @@ internal class Win32PlatformSettings : DefaultPlatformSettings
{ {
OnColorValuesChanged(colorValues); 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(Visual target, double baseFontSize)
{
if (baseFontSize <= 0 || _textScaleFactor == 1)
{
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 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)] [uuid(03021BE4-5254-4781-8194-5168F7D06D7B)]
interface IUISettings3 : IInspectable interface IUISettings3 : IInspectable
{ {

37
src/iOS/Avalonia.iOS/PlatformSettings.cs

@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Generic;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using Foundation;
using UIKit; using UIKit;
namespace Avalonia.iOS; namespace Avalonia.iOS;
@ -8,8 +10,27 @@ namespace Avalonia.iOS;
// TODO: ideally should be created per view/activity. // TODO: ideally should be created per view/activity.
internal class PlatformSettings : DefaultPlatformSettings internal class PlatformSettings : DefaultPlatformSettings
{ {
private readonly NSObject _contentSizeChangedToken;
private readonly Dictionary<double, double> _fontScaleCache = [];
private PlatformColorValues? _lastColorValues; private PlatformColorValues? _lastColorValues;
public PlatformSettings()
{
_contentSizeChangedToken = UIApplication.Notifications.ObserveContentSizeCategoryChanged(OnContentSizeCategoryChanged);
}
~PlatformSettings()
{
_contentSizeChangedToken.Dispose();
}
private void OnContentSizeCategoryChanged(object? sender, UIContentSizeCategoryChangedEventArgs e)
{
_fontScaleCache.Clear();
OnTextScaleChanged();
}
public override PlatformColorValues GetColorValues() public override PlatformColorValues GetColorValues()
{ {
var themeVariant = UITraitCollection.CurrentTraitCollection.UserInterfaceStyle == UIUserInterfaceStyle.Dark ? var themeVariant = UITraitCollection.CurrentTraitCollection.UserInterfaceStyle == UIUserInterfaceStyle.Dark ?
@ -61,4 +82,20 @@ internal class PlatformSettings : DefaultPlatformSettings
OnColorValuesChanged(colorValues); OnColorValuesChanged(colorValues);
} }
} }
public override double GetScaledFontSize(Visual target, double baseFontSize)
{
if (baseFontSize <= 0)
{
return baseFontSize;
}
if (!_fontScaleCache.TryGetValue(baseFontSize, out var scaledSize))
{
var font = UIFont.SystemFontOfSize((nfloat)baseFontSize);
scaledSize = _fontScaleCache[baseFontSize] = UIFontMetrics.DefaultMetrics.GetScaledFont(font).PointSize;
}
return scaledSize;
}
} }

Loading…
Cancel
Save