diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
index 0697831987..4ccb3f6a37 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
@@ -60,14 +60,10 @@ namespace Avalonia.Media.TextFormatting
_textTrimming = textTrimming ?? TextTrimming.None;
- LineHeight = lineHeight;
-
MaxWidth = maxWidth;
MaxHeight = maxHeight;
- LetterSpacing = letterSpacing;
-
MaxLines = maxLines;
_textLines = CreateTextLines();
@@ -81,8 +77,6 @@ namespace Avalonia.Media.TextFormatting
/// The text trimming.
/// The maximum width.
/// The maximum height.
- /// The height of each line of text.
- /// The letter spacing that is applied to rendered glyphs.
/// The maximum number of text lines.
public TextLayout(
ITextSource textSource,
@@ -90,8 +84,6 @@ namespace Avalonia.Media.TextFormatting
TextTrimming? textTrimming = null,
double maxWidth = double.PositiveInfinity,
double maxHeight = double.PositiveInfinity,
- double lineHeight = double.NaN,
- double letterSpacing = 0,
int maxLines = 0)
{
_textSource = textSource;
@@ -100,14 +92,10 @@ namespace Avalonia.Media.TextFormatting
_textTrimming = textTrimming ?? TextTrimming.None;
- LineHeight = lineHeight;
-
MaxWidth = maxWidth;
MaxHeight = maxHeight;
- LetterSpacing = letterSpacing;
-
MaxLines = maxLines;
_textLines = CreateTextLines();
@@ -120,7 +108,7 @@ namespace Avalonia.Media.TextFormatting
/// A value of NaN (equivalent to an attribute value of "Auto") indicates that the line height
/// is determined automatically from the current font characteristics. The default is NaN.
///
- public double LineHeight { get; }
+ public double LineHeight => _paragraphProperties.LineHeight;
///
/// Gets the maximum width.
@@ -140,7 +128,7 @@ namespace Avalonia.Media.TextFormatting
///
/// Gets the text spacing.
///
- public double LetterSpacing { get; }
+ public double LetterSpacing => _paragraphProperties.LetterSpacing;
///
/// Gets the text lines.
@@ -495,7 +483,7 @@ namespace Avalonia.Media.TextFormatting
/// The height of each line of text.
/// The letter spacing that is applied to rendered glyphs.
///
- private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize,
+ internal static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize,
IBrush? foreground, TextAlignment textAlignment, TextWrapping textWrapping,
TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight,
double letterSpacing)
diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs
index b725326855..4518289335 100644
--- a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs
+++ b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs
@@ -208,7 +208,7 @@ namespace Avalonia.PropertyStore
IsOverridenCurrentValue = isOverriddenCurrentValue;
IsCoercedDefaultValue = isCoercedDefaultValue;
- if (_uncommon?._coerce is { } coerce)
+ if (!isCoercedDefaultValue && _uncommon?._coerce is { } coerce)
v = coerce(owner.Owner, value);
if (priority <= Priority)
@@ -262,7 +262,8 @@ namespace Avalonia.PropertyStore
if (_uncommon?._coerce is { } coerce)
{
v = coerce(owner.Owner, value);
- bv = coerce(owner.Owner, baseValue);
+ if (priority != basePriority)
+ bv = coerce(owner.Owner, baseValue);
}
if (!EqualityComparer.Default.Equals(Value, v))
diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs
index 6144136882..57d709ba94 100644
--- a/src/Avalonia.Controls/TextBlock.cs
+++ b/src/Avalonia.Controls/TextBlock.cs
@@ -639,8 +639,7 @@ namespace Avalonia.Controls
TextTrimming,
_constraint.Width,
_constraint.Height,
- maxLines: MaxLines,
- lineHeight: LineHeight);
+ MaxLines);
}
///
diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs
index 51a7fe9b7f..7bc26bf3b0 100644
--- a/src/Avalonia.Controls/TextBox.cs
+++ b/src/Avalonia.Controls/TextBox.cs
@@ -18,9 +18,6 @@ using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Automation.Peers;
using Avalonia.Threading;
-using Avalonia.Platform;
-using System.Reflection;
-using static System.Net.Mime.MediaTypeNames;
namespace Avalonia.Controls
{
@@ -28,6 +25,7 @@ namespace Avalonia.Controls
/// Represents a control that can be used to display or edit unformatted text.
///
[TemplatePart("PART_TextPresenter", typeof(TextPresenter))]
+ [TemplatePart("PART_ScrollViewer", typeof(ScrollViewer))]
[PseudoClasses(":empty")]
public class TextBox : TemplatedControl, UndoRedoHelper.IUndoRedoHost
{
@@ -158,7 +156,7 @@ namespace Avalonia.Controls
/// Defines see property.
///
public static readonly StyledProperty LineHeightProperty =
- TextBlock.LineHeightProperty.AddOwner();
+ TextBlock.LineHeightProperty.AddOwner(new(defaultValue: double.NaN));
///
/// Defines see property.
@@ -310,6 +308,7 @@ namespace Avalonia.Controls
}
private TextPresenter? _presenter;
+ private ScrollViewer? _scrollViewer;
private readonly TextBoxTextInputMethodClient _imClient = new();
private readonly UndoRedoHelper _undoRedoHelper;
private bool _isUndoingRedoing;
@@ -490,7 +489,7 @@ namespace Avalonia.Controls
}
///
- /// Gets or sets the maximum character length of the TextBox
+ /// Gets or sets the maximum number of visible lines.
///
public int MaxLength
{
@@ -803,6 +802,8 @@ namespace Avalonia.Controls
{
_presenter = e.NameScope.Get("PART_TextPresenter");
+ _scrollViewer = e.NameScope.Find("PART_ScrollViewer");
+
_imClient.SetPresenter(_presenter, this);
if (IsFocused)
@@ -855,6 +856,10 @@ namespace Avalonia.Controls
{
OnSelectionEndChanged(change);
}
+ else if (change.Property == MaxLinesProperty)
+ {
+ InvalidateMeasure();
+ }
else if (change.Property == UndoLimitProperty)
{
OnUndoLimitChanged(change.GetNewValue());
@@ -942,40 +947,10 @@ namespace Avalonia.Controls
{
return;
}
+
_selectedTextChangesMadeSinceLastUndoSnapshot++;
SnapshotUndoRedo(ignoreChangeCount: false);
- if (_presenter != null && MaxLines > 0)
- {
- var lineCount = _presenter.TextLayout.TextLines.Count;
-
- var length = 0;
-
- var graphemeEnumerator = new GraphemeEnumerator(input.AsSpan());
-
- while (graphemeEnumerator.MoveNext(out var grapheme))
- {
- if (grapheme.FirstCodepoint.IsBreakChar)
- {
- if (lineCount + 1 > MaxLines)
- {
- break;
- }
- else
- {
- lineCount++;
- }
- }
-
- length += grapheme.Length;
- }
-
- if (length < input.Length)
- {
- input = input.Remove(Math.Max(0, length));
- }
- }
-
var currentText = Text ?? string.Empty;
var selectionLength = Math.Abs(SelectionStart - SelectionEnd);
var newLength = input.Length + currentText.Length - selectionLength;
@@ -1518,7 +1493,7 @@ namespace Avalonia.Controls
_presenter.MoveCaretToPoint(point);
var caretIndex = _presenter.CaretIndex;
-
+
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
@@ -1976,5 +1951,47 @@ namespace Avalonia.Controls
{
CanRedo = _undoRedoHelper.CanRedo;
}
+
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ if(_scrollViewer != null)
+ {
+ var maxHeight = double.PositiveInfinity;
+
+ 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);
+ var textLayout = new TextLayout(new MaxLinesTextSource(MaxLines), paragraphProperties);
+
+ maxHeight = Math.Ceiling(textLayout.Height);
+ }
+
+ _scrollViewer.SetCurrentValue(MaxHeightProperty, maxHeight);
+ }
+
+ return base.MeasureOverride(availableSize);
+ }
+
+ private class MaxLinesTextSource : ITextSource
+ {
+ private readonly int _maxLines;
+
+ public MaxLinesTextSource(int maxLines)
+ {
+ _maxLines = maxLines;
+ }
+
+ public TextRun? GetTextRun(int textSourceIndex)
+ {
+ if (textSourceIndex >= _maxLines)
+ {
+ return null;
+ }
+
+ return new TextEndOfLine(1);
+ }
+ }
}
}
diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs
index 43ae48341c..9abf4f97a7 100644
--- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs
+++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs
@@ -4,7 +4,6 @@ using System.Threading.Tasks;
using Avalonia.Controls.Platform;
using Avalonia.Logging;
using Avalonia.Platform;
-using Avalonia.Threading;
using Tmds.DBus.Protocol;
using Tmds.DBus.SourceGenerator;
@@ -57,15 +56,6 @@ namespace Avalonia.FreeDesktop
WatchAsync();
}
- private void InitializeSNWService()
- {
- if (_connection is null || _isDisposed)
- return;
-
- _statusNotifierWatcher = new OrgKdeStatusNotifierWatcher(_connection, "org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher");
- _serviceConnected = true;
- }
-
private async void WatchAsync()
{
try
@@ -84,13 +74,13 @@ namespace Avalonia.FreeDesktop
private void OnNameChange(string? newOwner)
{
- if (_isDisposed)
+ if (_isDisposed || _connection is null)
return;
if (!_serviceConnected & newOwner is not null)
{
_serviceConnected = true;
- InitializeSNWService();
+ _statusNotifierWatcher = new OrgKdeStatusNotifierWatcher(_connection, "org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher");
DestroyTrayIcon();
@@ -182,8 +172,11 @@ namespace Avalonia.FreeDesktop
public void SetIsVisible(bool visible)
{
- if (_isDisposed)
+ if (_isDisposed || !_serviceConnected)
+ {
+ _isVisible = visible;
return;
+ }
switch (visible)
{
diff --git a/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml b/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml
index b3453569a4..8519099a27 100644
--- a/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml
@@ -179,8 +179,8 @@
-
-
+
+
@@ -189,7 +189,6 @@
-
@@ -197,6 +196,7 @@
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
@@ -239,16 +238,17 @@
+
-
-
-
-
+
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml b/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
index 9449fcfb72..40abac8e86 100644
--- a/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
@@ -133,7 +133,8 @@
IsVisible="False"
Text="{TemplateBinding Watermark}"
DockPanel.Dock="Top" />
-
-
+
-
-
+
+
@@ -136,7 +136,6 @@
DockPanel.Dock="Top">
-
@@ -144,19 +143,45 @@
+
-
-
-
-
+
+
-
-
+
+
+
-
-
+
+
+
+
+
-
@@ -177,19 +201,20 @@
+
-
-
-
-
+
diff --git a/src/Avalonia.Themes.Simple/Controls/TextBox.xaml b/src/Avalonia.Themes.Simple/Controls/TextBox.xaml
index b42d52a920..b835bf10aa 100644
--- a/src/Avalonia.Themes.Simple/Controls/TextBox.xaml
+++ b/src/Avalonia.Themes.Simple/Controls/TextBox.xaml
@@ -123,7 +123,8 @@
- CoerceFooInvocations { get; } = new();
public List CoreChanges { get; } = new();
public static int CoerceFoo(AvaloniaObject instance, int value)
{
+ (instance as Class1)?.CoerceFooInvocations.Add(value);
return instance is Class1 o ?
Math.Clamp(value, o.MinFoo, o.MaxFoo) :
Math.Clamp(value, 0, 100);
diff --git a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs
index 9a7d51d86d..94e24e484f 100644
--- a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs
@@ -440,7 +440,7 @@ namespace Avalonia.Controls.UnitTests
throw new InvalidOperationException("Could not get the point in root coordinates.");
}
- private Control CreateTemplate(ScrollViewer control, INameScope scope)
+ internal static Control CreateTemplate(ScrollViewer control, INameScope scope)
{
return new Grid
{
@@ -480,7 +480,7 @@ namespace Avalonia.Controls.UnitTests
};
}
- private Control CreateScrollBarTemplate(ScrollBar scrollBar, INameScope scope)
+ private static Control CreateScrollBarTemplate(ScrollBar scrollBar, INameScope scope)
{
return new Border
{
diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
index 0d559557fe..109c233a56 100644
--- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
@@ -884,7 +884,7 @@ namespace Avalonia.Controls.UnitTests
Template = CreateTemplate(),
Text = "ABC",
MaxLines = 1,
- AcceptsReturn= true
+ AcceptsReturn = true
};
var impl = CreateMockTopLevelImpl();
@@ -896,8 +896,11 @@ namespace Avalonia.Controls.UnitTests
topLevel.ApplyTemplate();
topLevel.LayoutManager.ExecuteInitialLayoutPass();
+ target.ApplyTemplate();
target.Measure(Size.Infinity);
+ var initialHeight = target.DesiredSize.Height;
+
topLevel.Clipboard?.SetTextAsync(Environment.NewLine).GetAwaiter().GetResult();
RaiseKeyEvent(target, Key.V, KeyModifiers.Control);
@@ -905,7 +908,10 @@ namespace Avalonia.Controls.UnitTests
RaiseTextEvent(target, Environment.NewLine);
- Assert.Equal("ABC", target.Text);
+ target.InvalidateMeasure();
+ target.Measure(Size.Infinity);
+
+ Assert.Equal(initialHeight, target.DesiredSize.Height);
}
}
@@ -1116,7 +1122,11 @@ namespace Avalonia.Controls.UnitTests
private IControlTemplate CreateTemplate()
{
return new FuncControlTemplate((control, scope) =>
- new TextPresenter
+ new ScrollViewer
+ {
+ Name = "Part_ScrollViewer",
+ Template = new FuncControlTemplate(ScrollViewerTests.CreateTemplate),
+ Content = new TextPresenter
{
Name = "PART_TextPresenter",
[!!TextPresenter.TextProperty] = new Binding
@@ -1133,7 +1143,8 @@ namespace Avalonia.Controls.UnitTests
Priority = BindingPriority.Template,
RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
}
- }.RegisterInNameScope(scope));
+ }.RegisterInNameScope(scope)
+ }.RegisterInNameScope(scope));
}
private static void RaiseKeyEvent(TextBox textBox, Key key, KeyModifiers inputModifiers)