diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_TextBlock.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_TextBlock.cs index ee8c055adf42..8bd99a258b44 100644 --- a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_TextBlock.cs +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_TextBlock.cs @@ -14,6 +14,7 @@ using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Media.Imaging; using static Private.Infrastructure.TestServices; +using System.Collections.Generic; namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Controls { @@ -367,5 +368,63 @@ public async Task When_Empty_TextBlocks_Stacked() previousOrigin = textBlockOrigin; } } + +#if !__MACOS__ + [TestMethod] + [RunsOnUIThread] + public async Task When_TextTrimming() + { + var sut = new TextBlock + { + Text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + TextTrimming = TextTrimming.Clip, + }; + var container = new Border + { + BorderThickness = new Thickness(1), + BorderBrush = new SolidColorBrush(Colors.Red), + Width = 100, + Child = sut, + }; + + var states = new List(); + sut.IsTextTrimmedChanged += (s, e) => states.Add(sut.IsTextTrimmed); + + WindowHelper.WindowContent = container; + await WindowHelper.WaitForLoaded(container); + await WindowHelper.WaitForIdle(); // necessary on ios, since the container finished loading before the text is drawn + + Assert.IsTrue(sut.IsTextTrimmed, "IsTextTrimmed should be trimmed."); + Assert.IsTrue(states.Count == 1 && states[0] == true, $"IsTextTrimmedChanged should only proc once for IsTextTrimmed=true. states: {(string.Join(", ", states) is string { Length: > 0 } tmp ? tmp : "(-empty-)")}"); + } + + [TestMethod] + [RunsOnUIThread] + public async Task When_TextTrimmingNone() + { + var sut = new TextBlock + { + Text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + TextTrimming = TextTrimming.None, + }; + var container = new Border + { + BorderThickness = new Thickness(1), + BorderBrush = new SolidColorBrush(Colors.Red), + Width = 100, + Child = sut, + }; + + var states = new List(); + sut.IsTextTrimmedChanged += (s, e) => states.Add(sut.IsTextTrimmed); + + WindowHelper.WindowContent = container; + await WindowHelper.WaitForLoaded(container); + await WindowHelper.WaitForIdle(); // necessary on ios, since the container finished loading before the text is drawn + + Assert.IsFalse(sut.IsTextTrimmed, "IsTextTrimmed should not be trimmed."); + Assert.IsTrue(states.Count == 0, $"IsTextTrimmedChanged should not proc at all. states: {(string.Join(", ", states) is string { Length: > 0 } tmp ? tmp : "(-empty-)")}"); + } +#endif } } diff --git a/src/Uno.UI/Generated/3.0.0.0/Windows.UI.Xaml.Controls/TextBlock.cs b/src/Uno.UI/Generated/3.0.0.0/Windows.UI.Xaml.Controls/TextBlock.cs index 8415726bfa60..6af0c303476f 100644 --- a/src/Uno.UI/Generated/3.0.0.0/Windows.UI.Xaml.Controls/TextBlock.cs +++ b/src/Uno.UI/Generated/3.0.0.0/Windows.UI.Xaml.Controls/TextBlock.cs @@ -184,16 +184,7 @@ public bool IsTextScaleFactorEnabled #endif // Skipping already declared property TextDecorations // Skipping already declared property HorizontalTextAlignment -#if __ANDROID__ || __IOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__ - [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] - public bool IsTextTrimmed - { - get - { - return (bool)this.GetValue(IsTextTrimmedProperty); - } - } -#endif + // Skipping already declared property IsTextTrimmed #if __ANDROID__ || __IOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] public global::System.Collections.Generic.IList TextHighlighters @@ -299,14 +290,7 @@ public bool IsTextTrimmed #endif // Skipping already declared property TextDecorationsProperty // Skipping already declared property HorizontalTextAlignmentProperty -#if __ANDROID__ || __IOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__ - [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] - public static global::Windows.UI.Xaml.DependencyProperty IsTextTrimmedProperty { get; } = - Windows.UI.Xaml.DependencyProperty.Register( - nameof(IsTextTrimmed), typeof(bool), - typeof(global::Windows.UI.Xaml.Controls.TextBlock), - new Windows.UI.Xaml.FrameworkPropertyMetadata(default(bool))); -#endif + // Skipping already declared property IsTextTrimmed #if __ANDROID__ || __IOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] public static global::Windows.UI.Xaml.DependencyProperty SelectionFlyoutProperty { get; } = @@ -470,21 +454,6 @@ public void CopySelectionToClipboard() } } #endif -#if __ANDROID__ || __IOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__ - [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] - public event global::Windows.Foundation.TypedEventHandler IsTextTrimmedChanged - { - [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] - add - { - global::Windows.Foundation.Metadata.ApiInformation.TryRaiseNotImplemented("Windows.UI.Xaml.Controls.TextBlock", "event TypedEventHandler TextBlock.IsTextTrimmedChanged"); - } - [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] - remove - { - global::Windows.Foundation.Metadata.ApiInformation.TryRaiseNotImplemented("Windows.UI.Xaml.Controls.TextBlock", "event TypedEventHandler TextBlock.IsTextTrimmedChanged"); - } - } -#endif + // Skipping already declared event Microsoft.UI.Xaml.Controls.TextBlock.IsTextTrimmedChanged } } diff --git a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.Android.cs b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.Android.cs index 7fed36f3c3e9..02556f3732be 100644 --- a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.Android.cs +++ b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.Android.cs @@ -388,6 +388,8 @@ protected override Size ArrangeOverride(Size finalSize) UpdateNativeTextBlockLayout(); } + UpdateIsTextTrimmed(); + return finalSize; } } @@ -460,6 +462,14 @@ private Size UpdateLayout(ref LayoutBuilder layout, Size availableSize, bool exa return layout.MeasuredSize; } + partial void UpdateIsTextTrimmed() + { + IsTextTrimmed = IsTextTrimmable && ( + _measureLayout.MeasuredSize.Width > _arrangeLayout.MeasuredSize.Width || + _measureLayout.MeasuredSize.Height > _arrangeLayout.MeasuredSize.Height + ); + } + /// /// A layout builder, used to perform change tracking and avoid creating the same layout multiple times. /// diff --git a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.cs b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.cs index 8ac0c9c3a9db..d357b1ab3cfe 100644 --- a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.cs +++ b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Uno.Disposables; using Uno.Extensions; @@ -672,6 +673,40 @@ private void OnTextDecorationsChanged() #endregion + #region DependencyProperty: IsTextTrimmed +#if false || false || IS_UNIT_TESTS || false || false || __NETSTD_REFERENCE__ || __MACOS__ + [NotImplemented("IS_UNIT_TESTS", "__NETSTD_REFERENCE__", "__MACOS__")] +#endif + public event TypedEventHandler IsTextTrimmedChanged; + +#if false || false || IS_UNIT_TESTS || false || false || __NETSTD_REFERENCE__ || __MACOS__ + [NotImplemented("IS_UNIT_TESTS", "__NETSTD_REFERENCE__", "__MACOS__")] +#endif + public static DependencyProperty IsTextTrimmedProperty { get; } = DependencyProperty.Register( + nameof(IsTextTrimmed), + typeof(bool), + typeof(TextBlock), + new FrameworkPropertyMetadata(false, propertyChangedCallback: (s, e) => ((TextBlock)s).OnIsTextTrimmedChanged())); + +#if false || false || IS_UNIT_TESTS || false || false || __NETSTD_REFERENCE__ || __MACOS__ + [NotImplemented("IS_UNIT_TESTS", "__NETSTD_REFERENCE__", "__MACOS__")] +#endif + public bool IsTextTrimmed + { + get => (bool)GetValue(IsTextTrimmedProperty); + private set => SetValue(IsTextTrimmedProperty, value); + } + + private void OnIsTextTrimmedChanged() + { + OnIsTextTrimmedChangedPartial(); + IsTextTrimmedChanged?.Invoke(this, new()); + } + + partial void OnIsTextTrimmedChangedPartial(); + + #endregion + /// /// Gets whether the TextBlock is using the fast path in which Inlines /// have not been initialized and don't need to be synchronized. @@ -1080,5 +1115,13 @@ internal override void UpdateThemeBindings(Data.ResourceUpdateReason updateReaso IsVisible() && /*IsEnabled() &&*/ (IsTextSelectionEnabled || IsTabStop) && AreAllAncestorsVisible(); + + [SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "Used only by some platforms")] + private bool IsTextTrimmable => + TextTrimming != TextTrimming.None || + MaxLines != 0; + + [SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "Used only by some platforms")] + partial void UpdateIsTextTrimmed(); } } diff --git a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.iOS.cs b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.iOS.cs index b48a7ec9b249..3507702d090d 100644 --- a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.iOS.cs +++ b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.iOS.cs @@ -68,6 +68,8 @@ public override void Draw(CGRect rect) { _attributedString?.DrawString(_drawRect, NSStringDrawingOptions.UsesLineFragmentOrigin, null); } + + UpdateIsTextTrimmed(); } /// @@ -344,5 +346,13 @@ private int GetCharacterIndexAtPoint(Point point) return characterIndex; } + + partial void UpdateIsTextTrimmed() + { + IsTextTrimmed = IsTextTrimmable && ( + _attributedString.Size.Width > _drawRect.Size.Width || + _attributedString.Size.Height > _drawRect.Size.Height + ); + } } } diff --git a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.skia.cs b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.skia.cs index 80d8c0bd7fc7..1e0ef30e812a 100644 --- a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.skia.cs +++ b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.skia.cs @@ -88,7 +88,11 @@ protected override Size ArrangeOverride(Size finalSize) _textVisual.Size = new Vector2((float)arrangedSizeWithoutPadding.Width, (float)arrangedSizeWithoutPadding.Height); _textVisual.Offset = new Vector3((float)padding.Left, (float)padding.Top, 0); ApplyFlowDirection((float)finalSize.Width); - return base.ArrangeOverride(finalSize); + + var result = base.ArrangeOverride(finalSize); + UpdateIsTextTrimmed(); + + return result; } /// @@ -169,5 +173,13 @@ partial void OnLineStackingStrategyChangedPartial() { Inlines.InvalidateMeasure(); } + + partial void UpdateIsTextTrimmed() + { + IsTextTrimmed = IsTextTrimmable && ( + (_textVisual.Size.X + Padding.Left + Padding.Right) > ActualWidth || + (_textVisual.Size.Y + Padding.Top + Padding.Bottom) > ActualHeight + ); + } } } diff --git a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.wasm.cs b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.wasm.cs index 6313122d5189..5c379085beaf 100644 --- a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.wasm.cs +++ b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.wasm.cs @@ -10,6 +10,7 @@ using Windows.UI.Text; using Windows.UI.Xaml.Media; using Uno.UI; +using Uno.UI.Xaml; namespace Windows.UI.Xaml.Controls { @@ -150,6 +151,13 @@ protected override Size ArrangeOverride(Size finalSize) return base.ArrangeOverride(arrangeSize); } + internal override void OnLayoutUpdated() + { + base.OnLayoutUpdated(); + + UpdateIsTextTrimmed(); + } + partial void OnFontStyleChangedPartial() => _fontStyleChanged = true; partial void OnFontWeightChangedPartial() => _fontWeightChanged = true; @@ -196,5 +204,12 @@ partial void OnTextChangedPartial() partial void OnTextWrappingChangedPartial() => _textWrappingChanged = true; partial void OnPaddingChangedPartial() => _paddingChangedChanged = true; + + partial void UpdateIsTextTrimmed() + { + IsTextTrimmed = + IsTextTrimmable && + WindowManagerInterop.GetIsOverflowing(HtmlId); + } } } diff --git a/src/Uno.UI/UI/Xaml/WindowManagerInterop.wasm.cs b/src/Uno.UI/UI/Xaml/WindowManagerInterop.wasm.cs index 1348f6dbf71b..c2c273911e3d 100644 --- a/src/Uno.UI/UI/Xaml/WindowManagerInterop.wasm.cs +++ b/src/Uno.UI/UI/Xaml/WindowManagerInterop.wasm.cs @@ -1056,6 +1056,9 @@ internal static void WindowActivate() NativeMethods.WindowActivate(); } + internal static bool GetIsOverflowing(IntPtr htmlId) + => NativeMethods.GetIsOverflowing(htmlId); + #region Pointers [Flags] internal enum HtmlPointerButtonsState @@ -1082,6 +1085,7 @@ internal enum HtmlPointerButtonUpdate Eraser = 5 } #endregion + internal static partial class NativeMethods { [JSImport("globalThis.Uno.UI.WindowManager.current.arrangeElementNativeFast")] @@ -1177,6 +1181,9 @@ internal static partial void ArrangeElement( [JSImport("globalThis.Uno.UI.WindowManager.current.activate")] internal static partial void WindowActivate(); + + [JSImport("globalThis.Uno.UI.WindowManager.current.getIsOverflowing")] + internal static partial bool GetIsOverflowing(IntPtr htmlId); } } } diff --git a/src/Uno.UI/ts/WindowManager.ts b/src/Uno.UI/ts/WindowManager.ts index 9d38af103930..e6444c9f5b13 100644 --- a/src/Uno.UI/ts/WindowManager.ts +++ b/src/Uno.UI/ts/WindowManager.ts @@ -1780,6 +1780,11 @@ namespace Uno.UI { (this.getView(elementId) as HTMLInputElement).setSelectionRange(start, start + length); } + public getIsOverflowing(elementId: number): boolean { + const element = this.getView(elementId) as HTMLElement; + + return element.clientWidth < element.scrollWidth || element.clientHeight < element.scrollHeight; + } } if (typeof define === "function") {