diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/DrawTextWithinBounds.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/DrawTextWithinBounds.png new file mode 100644 index 000000000000..18c28051e939 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/DrawTextWithinBounds.png differ diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue18679.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue18679.cs new file mode 100644 index 000000000000..f2a245ecd37d --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue18679.cs @@ -0,0 +1,100 @@ +using Font = Microsoft.Maui.Graphics.Font; + +namespace Maui.Controls.Sample.Issues +{ + [Issue(IssueTracker.Github, 18679, "Canvas.GetStringSize() is not consistent with actual string size in GraphicsView", PlatformAffected.iOS | PlatformAffected.macOS)] + public class Issue18679 : TestContentPage + { + const string ShortText = "ShortText"; + const string LongText = "CiaomondorowfdskleCiaomondorowfdsk"; + const string MultiLineText = "HELLO, WORLD!\nCiao mondo row 2\nGuten tag!?àèìòù@"; + + protected override void Init() + { + var layout = new VerticalStackLayout + { + Spacing = 10, + }; + var label = new Label + { + Text = "The drawn text should not overflow the bounding rectangle.", + FontSize = 16, + AutomationId = "18679DescriptionLabel", + }; + layout.Add(label); + + // Test Case 1: Single long line (basic case) + layout.Add(CreateDrawable("Test Case 1: Short text", ShortText)); + + // Test Case 2: Multi-line text + layout.Add(CreateDrawable("Test Case 2: Multi-line Text", MultiLineText)); + + // Test Case 3: Unicode/non-Latin text + layout.Add(CreateDrawable("Test Case 3: Long Text", LongText)); + + Content = layout; + } + + private Border CreateDrawable(string testName, string text) + { + var graphicsView = new GraphicsView + { + HeightRequest = 150, + Drawable = new Issue18679_Drawable(text, testName) + }; + + return new Border + { + Content = graphicsView, + Stroke = Colors.LightGray, + StrokeThickness = 1, + }; + } + } + + public class Issue18679_Drawable : IDrawable + { + readonly string _text; + readonly string _label; + static readonly Font Font = Font.Default; + const int FontSize = 20; + + + public Issue18679_Drawable(string text, string label) + { + _text = text; + _label = label; + } + + public void Draw(ICanvas canvas, RectF dirtyRect) + { + canvas.SaveState(); + + // Draw label + canvas.FontSize = 14; + canvas.Font = Font; + canvas.FontColor = Colors.Blue; + canvas.DrawString(_label, 10, 10, dirtyRect.Width - 20, 20, + HorizontalAlignment.Left, VerticalAlignment.Top); + + // Set up for text measurement + canvas.FontSize = FontSize; + canvas.Font = Font; + + // Get text size and create bounds + var stringSize = canvas.GetStringSize(_text, Font, FontSize); + + // Draw the actual text first + canvas.FontColor = Colors.Black; + canvas.DrawString(_text, 2, 40, dirtyRect.Width, dirtyRect.Height, + HorizontalAlignment.Left, VerticalAlignment.Top); + + // Draw the measured bounds rectangle + canvas.StrokeColor = Colors.Red; + canvas.StrokeSize = 1; + canvas.DrawRectangle(2, 40, stringSize.Width, stringSize.Height); + + canvas.RestoreState(); + } + } +} \ No newline at end of file diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/DrawTextWithinBounds.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/DrawTextWithinBounds.png new file mode 100644 index 000000000000..16bf91d858e4 Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/DrawTextWithinBounds.png differ diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue18679.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue18679.cs new file mode 100644 index 000000000000..16eeb313be5b --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue18679.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue18679 : _IssuesUITest +{ + public Issue18679(TestDevice testDevice) : base(testDevice) + { + } + + public override string Issue => "Canvas.GetStringSize() is not consistent with actual string size in GraphicsView"; + + [Test] + [Category(UITestCategories.GraphicsView)] + public void DrawTextWithinBounds() + { + App.WaitForElement("18679DescriptionLabel"); + VerifyScreenshot(); + } +} \ No newline at end of file diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/DrawTextWithinBounds.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/DrawTextWithinBounds.png new file mode 100644 index 000000000000..ed2e9dded753 Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/DrawTextWithinBounds.png differ diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/GraphicsViewShouldNotWrapText.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/GraphicsViewShouldNotWrapText.png index a8fe312a0351..d1be45ddd901 100644 Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/GraphicsViewShouldNotWrapText.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/GraphicsViewShouldNotWrapText.png differ diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/DrawTextWithinBounds.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/DrawTextWithinBounds.png new file mode 100644 index 000000000000..956f96eb858a Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/DrawTextWithinBounds.png differ diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/GraphicsViewShouldNotWrapText.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/GraphicsViewShouldNotWrapText.png index 1d9545542446..c2a09c604b51 100644 Binary files a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/GraphicsViewShouldNotWrapText.png and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/GraphicsViewShouldNotWrapText.png differ diff --git a/src/Graphics/src/Graphics/Platforms/Windows/PlatformStringSizeService.cs b/src/Graphics/src/Graphics/Platforms/Windows/PlatformStringSizeService.cs index e90aa0f922f1..7bddfdb14cdf 100644 --- a/src/Graphics/src/Graphics/Platforms/Windows/PlatformStringSizeService.cs +++ b/src/Graphics/src/Graphics/Platforms/Windows/PlatformStringSizeService.cs @@ -23,28 +23,44 @@ public SizeF GetStringSize(string value, IFont font, float textSize) public SizeF GetStringSize(string value, IFont font, float textSize, HorizontalAlignment horizontalAlignment, VerticalAlignment verticalAlignment) { - var format = font.ToCanvasTextFormat(textSize); - format.WordWrapping = CanvasWordWrapping.NoWrap; - - var device = CanvasDevice.GetSharedDevice(); - var textLayout = new CanvasTextLayout(device, value, format, 0.0f, 0.0f); - textLayout.VerticalAlignment = verticalAlignment switch - { - VerticalAlignment.Top => CanvasVerticalAlignment.Top, - VerticalAlignment.Center => CanvasVerticalAlignment.Center, - VerticalAlignment.Bottom => CanvasVerticalAlignment.Bottom, - _ => CanvasVerticalAlignment.Top - }; - textLayout.HorizontalAlignment = horizontalAlignment switch - { - HorizontalAlignment.Left => CanvasHorizontalAlignment.Left, - HorizontalAlignment.Center => CanvasHorizontalAlignment.Center, - HorizontalAlignment.Right => CanvasHorizontalAlignment.Right, - HorizontalAlignment.Justified => CanvasHorizontalAlignment.Justified, - _ => CanvasHorizontalAlignment.Left, - }; - - return new SizeF((float)textLayout.DrawBounds.Width, (float)textLayout.DrawBounds.Height); - } + if (string.IsNullOrEmpty(value) || font is null || textSize <= 0) + { + return SizeF.Zero; + } + + var format = font.ToCanvasTextFormat(textSize); + format.WordWrapping = CanvasWordWrapping.NoWrap; + + var device = CanvasDevice.GetSharedDevice(); + + using (var textLayout = new CanvasTextLayout(device, value, format, float.MaxValue, float.MaxValue)) + { + textLayout.VerticalAlignment = verticalAlignment switch + { + VerticalAlignment.Top => CanvasVerticalAlignment.Top, + VerticalAlignment.Center => CanvasVerticalAlignment.Center, + VerticalAlignment.Bottom => CanvasVerticalAlignment.Bottom, + _ => CanvasVerticalAlignment.Top + }; + textLayout.HorizontalAlignment = horizontalAlignment switch + { + HorizontalAlignment.Left => CanvasHorizontalAlignment.Left, + HorizontalAlignment.Center => CanvasHorizontalAlignment.Center, + HorizontalAlignment.Right => CanvasHorizontalAlignment.Right, + HorizontalAlignment.Justified => CanvasHorizontalAlignment.Justified, + _ => CanvasHorizontalAlignment.Left, + }; + + var bounds = textLayout.LayoutBounds; + + // If LayoutBounds is empty, fallback to DrawBounds + if (bounds.Width <= 0 || bounds.Height <= 0) + { + bounds = textLayout.DrawBounds; + } + + return new SizeF((float)bounds.Width, (float)bounds.Height); + } + } } -} +} \ No newline at end of file diff --git a/src/Graphics/src/Graphics/Platforms/iOS/PlatformStringSizeService.cs b/src/Graphics/src/Graphics/Platforms/iOS/PlatformStringSizeService.cs index 9f3133ea997d..841e72339e3a 100644 --- a/src/Graphics/src/Graphics/Platforms/iOS/PlatformStringSizeService.cs +++ b/src/Graphics/src/Graphics/Platforms/iOS/PlatformStringSizeService.cs @@ -1,5 +1,6 @@ using System; using CoreGraphics; +using CoreText; using Foundation; using UIKit; @@ -10,19 +11,26 @@ public partial class PlatformStringSizeService : IStringSizeService public SizeF GetStringSize(string value, IFont font, float fontSize) { if (string.IsNullOrEmpty(value)) + { return new SizeF(); + } - var nsString = new NSString(value); - var uiFont = font?.ToPlatformFont(fontSize) ?? FontExtensions.GetDefaultPlatformFont(); + var attributes = new CTStringAttributes(); + attributes.Font = font?.ToCTFont(fontSize) ?? FontExtensions.GetDefaultCTFont(fontSize); - CGSize size = nsString.GetBoundingRect( - CGSize.Empty, - NSStringDrawingOptions.UsesLineFragmentOrigin, - new UIStringAttributes { Font = uiFont }, - null).Size; + var attributedString = new NSAttributedString(value, attributes); + var framesetter = new CTFramesetter(attributedString); - uiFont.Dispose(); - return new SizeF((float)size.Width, (float)size.Height); + // Get suggested frame size with unlimited constraints + var measuredSize = framesetter.SuggestFrameSize( + new NSRange(0, 0), + null, + new CGSize(float.MaxValue, float.MaxValue), + out _); + + framesetter.Dispose(); + attributedString.Dispose(); + return new SizeF((float)measuredSize.Width, (float)measuredSize.Height); } } -} +} \ No newline at end of file