File: SkiaTextLayout.cs
Web Access
Project: src\src\Graphics\src\Graphics.Skia\Graphics.Skia.csproj (Microsoft.Maui.Graphics.Skia)
using System;
using System.Collections.Generic;
using SkiaSharp;
 
namespace Microsoft.Maui.Graphics.Skia
{
	public class SkiaTextLayout : IDisposable
	{
		private readonly LayoutLine _callback;
		private readonly RectF _rect;
		private readonly ITextAttributes _textAttributes;
		private readonly string _value;
		private readonly TextFlow _textFlow;
		private readonly SKPaint _paint;
		private readonly bool _disposePaint;
		private readonly float _lineHeight;
		private readonly float _descent;
 
		public bool WordWrap { get; set; } = true;
 
		public SkiaTextLayout(
			string value,
			RectF rect,
			ITextAttributes textAttributes,
			LayoutLine callback,
			TextFlow textFlow = TextFlow.ClipBounds,
			SKPaint paint = null)
		{
			_value = value;
			_textAttributes = textAttributes;
			_rect = rect;
			_callback = callback;
			_textFlow = textFlow;
			_paint = paint;
 
			if (paint == null)
			{
				_paint = new SKPaint()
				{
					Typeface = _textAttributes?.Font?.ToSKTypeface() ?? SKTypeface.Default,
					TextSize = _textAttributes.FontSize
				};
 
				_disposePaint = true;
			}
 
			var metrics = _paint.FontMetrics;
			_descent = metrics.Descent;
			_lineHeight = _paint.FontSpacing;
		}
 
		public void LayoutText()
		{
			if (string.IsNullOrEmpty(_value))
				return;
 
			var x = _rect.X;
			var y = _rect.Y;
			var width = _rect.Width;
			var height = _rect.Height;
 
			x += _textAttributes.Margin;
			y += _textAttributes.Margin;
			width -= (_textAttributes.Margin * 2);
			height -= (_textAttributes.Margin * 2);
 
			var top = y;
			var bottom = y + height;
 
			if (_textAttributes.HorizontalAlignment == HorizontalAlignment.Right)
				_paint.TextAlign = SKTextAlign.Right;
			else if (_textAttributes.HorizontalAlignment == HorizontalAlignment.Center)
				_paint.TextAlign = SKTextAlign.Center;
 
			var lines = CreateLines(y, bottom, width);
			switch (_textAttributes.VerticalAlignment)
			{
				case VerticalAlignment.Center:
					LayoutCenterAligned(lines, x, width, top, height);
					break;
				case VerticalAlignment.Bottom:
					LayoutBottomAligned(lines, x, width, bottom, top);
					break;
				default:
					LayoutTopAligned(lines, x, y, width);
					break;
			}
 
			_paint.TextAlign = SKTextAlign.Left;
		}
 
		private void LayoutCenterAligned(
			List<TextLine> lines,
			float x,
			float width,
			float top,
			float height)
		{
			var linesToDraw = lines.Count;
 
			if (_textFlow == TextFlow.ClipBounds)
			{
				var maxLines = Math.Floor(height / _lineHeight);
				linesToDraw = (int)Math.Min(maxLines, lines.Count);
			}
 
			// Figure out the vertical center of the rect
			var y = top + height / 2;
 
			// Figure out the center index of the list, and the center point to start drawing from.
			var startIndex = (lines.Count / 2);
			if (linesToDraw % 2 != 0)
				y -= _lineHeight / 2;
 
			// Figure out which index to draw first (of the range) and the point of the first line.
			for (var i = 0; i < linesToDraw / 2; i++)
			{
				y -= _lineHeight;
				startIndex--;
			}
 
			y -= _descent;
 
			// Draw each line.
			for (var i = 0; i < linesToDraw; i++)
			{
				y += _lineHeight;
				var line = lines[i + startIndex];
 
				var point = new PointF(x, y);
				switch (_textAttributes.HorizontalAlignment)
				{
					case HorizontalAlignment.Center:
						point.X = x + width / 2;
						break;
					case HorizontalAlignment.Right:
						point.X = x + width;
						break;
				}
 
				_callback(point, _textAttributes, line.Value, 0, 0, 0);
			}
		}
 
		private void LayoutBottomAligned(
			List<TextLine> lines,
			float x,
			float width,
			float bottom,
			float top)
		{
			var y = bottom - _descent;
 
			for (int i = lines.Count - 1; i >= 0; i--)
			{
				var line = lines[i];
 
				if (_textFlow == TextFlow.ClipBounds && y - _lineHeight < top)
					return;
 
				var point = new PointF(x, y);
				switch (_textAttributes.HorizontalAlignment)
				{
					case HorizontalAlignment.Center:
						point.X = x + width / 2;
						break;
					case HorizontalAlignment.Right:
						point.X = x + width;
						break;
				}
 
				_callback(point, _textAttributes, line.Value, 0, 0, 0);
 
				y -= _lineHeight;
			}
		}
 
		private void LayoutTopAligned(
			List<TextLine> lines,
			float x,
			float y,
			float width)
		{
			y -= _descent;
 
			foreach (var line in lines)
			{
				y += _lineHeight;
 
				var point = new PointF(x, y);
				switch (_textAttributes.HorizontalAlignment)
				{
					case HorizontalAlignment.Center:
						point.X = x + width / 2;
						break;
					case HorizontalAlignment.Right:
						point.X = x + width;
						break;
				}
 
				_callback(point, _textAttributes, line.Value, 0, 0, 0);
			}
		}
 
		private List<TextLine> CreateLines(float y, float bottom, float width)
		{
			var lines = new List<TextLine>();
 
			var index = 0;
			var length = _value.Length;
			while (index < length)
			{
				y += _lineHeight;
 
				if (_textFlow == TextFlow.ClipBounds && _textAttributes.VerticalAlignment == VerticalAlignment.Top && y > bottom)
					return lines;
 
				var count = (int)_paint.BreakText(_value.Substring(index), width, out var textWidth);
 
				var found = false;
				if (WordWrap && index + count < length)
				{
					for (var i = index + count - 1; i >= index && !found; i--)
					{
						if (char.IsWhiteSpace(_value[i]))
						{
							count = i - index + 1;
							found = true;
						}
					}
				}
 
				var line = _value.Substring(index, count);
				if (found)
					textWidth = _paint.MeasureText(line);
				lines.Add(new TextLine(line, textWidth));
 
				index += count;
			}
 
			return lines;
		}
 
		public void Dispose()
		{
			if (_disposePaint)
				_paint?.Dispose();
		}
	}
 
	public class TextLine
	{
		public string Value { get; }
		public float Width { get; }
 
		public TextLine(
			string value,
			float width)
		{
			Value = value;
			Width = width;
		}
	}
}