Browse Source

Preserve run formatting when applying SelectionForegroundBrush in SelectableTextBlock (#20110)

* Add failing test for preserving run formatting during selection

* Fix selection styling to preserve original text formatting in SelectableTextBlock

* fix: apply old logic when `_textRuns` is null
pull/20137/head
Artemis Li 2 months ago
committed by GitHub
parent
commit
60ee26ae21
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 50
      src/Avalonia.Controls/SelectableTextBlock.cs
  2. 59
      tests/Avalonia.Controls.UnitTests/SelectableTextBlockTests.cs

50
src/Avalonia.Controls/SelectableTextBlock.cs

@ -197,7 +197,7 @@ namespace Avalonia.Controls
LineSpacing = LineSpacing
};
IReadOnlyList<ValueSpan<TextRunProperties>>? textStyleOverrides = null;
List<ValueSpan<TextRunProperties>>? textStyleOverrides = null;
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
var start = Math.Min(selectionStart, selectionEnd);
@ -205,12 +205,52 @@ namespace Avalonia.Controls
if (length > 0 && SelectionForegroundBrush != null)
{
textStyleOverrides = new[]
if (_textRuns != null)
{
// Apply selection foreground color without changing the original text formatting.
// The built-in SelectableTextBlock selection logic recreates TextRunProperties,
// which overwrites run-specific Typeface/FontFeatures/FontSize and breaks bold/italic.
// Here we reuse each run's existing properties and only override the foreground brush.
var accumulatedLength = 0;
foreach (var textRun in _textRuns)
{
var runLength = textRun.Text.Length;
if (accumulatedLength + runLength <= start ||
accumulatedLength >= start + length)
{
accumulatedLength += runLength;
continue;
}
var overlapStart = Math.Max(start, accumulatedLength);
var overlapEnd = Math.Min(start + length, accumulatedLength + runLength);
var overlapLength = overlapEnd - overlapStart;
textStyleOverrides ??= [];
textStyleOverrides.Add(
new ValueSpan<TextRunProperties>(
overlapStart,
overlapLength,
new GenericTextRunProperties(
textRun.Properties?.Typeface ?? typeface,
textRun.Properties?.FontFeatures ?? FontFeatures,
FontSize,
foregroundBrush: SelectionForegroundBrush)));
accumulatedLength += runLength;
}
}
else
{
textStyleOverrides =
[
new ValueSpan<TextRunProperties>(start, length,
new GenericTextRunProperties(typeface, FontFeatures, FontSize,
foregroundBrush: SelectionForegroundBrush))
};
new GenericTextRunProperties(typeface, FontFeatures, FontSize,
foregroundBrush: SelectionForegroundBrush))
];
}
}
ITextSource textSource;

59
tests/Avalonia.Controls.UnitTests/SelectableTextBlockTests.cs

@ -0,0 +1,59 @@
using System.Linq;
using Avalonia.Controls.Documents;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Controls.UnitTests
{
public class SelectableTextBlockTests : ScopedTestBase
{
[Fact]
public void SelectionForeground_Should_Not_Reset_Run_Typeface_And_Style()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new SelectableTextBlock
{
SelectionForegroundBrush = Brushes.Red
};
var run = new Run("Hello")
{
FontWeight = FontWeight.Bold,
FontStyle = FontStyle.Italic,
FontSize = 20
};
target.Inlines.Add(run);
target.Measure(Size.Infinity);
target.SelectionStart = 0;
target.SelectionEnd = run.Text.Length;
target.Measure(Size.Infinity);
var textLayout = target.TextLayout;
Assert.NotNull(textLayout);
var textRuns = textLayout.TextLines
.SelectMany(l => l.TextRuns)
.OfType<ShapedTextRun>()
.ToList();
Assert.NotEmpty(textRuns);
var selectedRun = textRuns[0];
var props = selectedRun.Properties;
Assert.Equal(FontWeight.Bold, props.Typeface.Weight);
Assert.Equal(FontStyle.Italic, props.Typeface.Style);
Assert.Same(target.SelectionForegroundBrush, props.ForegroundBrush);
}
}
}
}
Loading…
Cancel
Save