// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using Microsoft.Maui.Controls; using Microsoft.Maui.Graphics; using Microsoft.Maui.Platform.Linux.Rendering; using SkiaSharp; namespace Microsoft.Maui.Platform; public partial class SkiaEditor { protected override void OnDraw(SKCanvas canvas, SKRect bounds) { var paddingLeft = (float)Padding.Left; var paddingTop = (float)Padding.Top; var paddingRight = (float)Padding.Right; var paddingBottom = (float)Padding.Bottom; var fontSize = (float)FontSize; var lineHeight = (float)LineHeight; var cornerRadius = (float)CornerRadius; // Update wrap width if bounds changed and re-wrap text var newWrapWidth = bounds.Width - paddingLeft - paddingRight; if (Math.Abs(newWrapWidth - _wrapWidth) > 1) { _wrapWidth = newWrapWidth; UpdateLines(); } // Draw background var bgColor = EditorBackgroundColor != null ? ToSKColor(EditorBackgroundColor) : (IsEnabled ? SkiaTheme.BackgroundWhiteSK : SkiaTheme.Gray100SK); using var bgPaint = new SKPaint { Color = bgColor, Style = SKPaintStyle.Fill, IsAntialias = true }; canvas.DrawRoundRect(new SKRoundRect(bounds, cornerRadius), bgPaint); // Draw border only if BorderColor is not transparent if (BorderColor != null && BorderColor != Colors.Transparent && BorderColor.Alpha > 0) { using var borderPaint = new SKPaint { Color = IsFocused ? ToSKColor(CursorColor) : ToSKColor(BorderColor), Style = SKPaintStyle.Stroke, StrokeWidth = IsFocused ? 2 : 1, IsAntialias = true }; canvas.DrawRoundRect(new SKRoundRect(bounds, cornerRadius), borderPaint); } // Setup text rendering using var font = new SKFont(SKTypeface.Default, fontSize); var lineSpacing = fontSize * lineHeight; // Clip to content area var contentRect = new SKRect( bounds.Left + paddingLeft, bounds.Top + paddingTop, bounds.Right - paddingRight, bounds.Bottom - paddingBottom); canvas.Save(); canvas.ClipRect(contentRect); // Don't translate - let the text draw at absolute positions // canvas.Translate(0, -_scrollOffsetY); if (string.IsNullOrEmpty(Text) && !string.IsNullOrEmpty(Placeholder)) { using var placeholderPaint = new SKPaint(font) { Color = GetEffectivePlaceholderColor(), IsAntialias = true }; // Handle multiline placeholder text by splitting on newlines var placeholderLines = Placeholder.Split('\n'); var y = contentRect.Top + fontSize; foreach (var line in placeholderLines) { canvas.DrawText(line, contentRect.Left, y, placeholderPaint); y += lineSpacing; } } else { var textColor = GetEffectiveTextColor(); using var textPaint = new SKPaint(font) { Color = IsEnabled ? textColor : textColor.WithAlpha(128), IsAntialias = true }; using var selectionPaint = new SKPaint { Color = ToSKColor(SelectionColor), Style = SKPaintStyle.Fill }; var y = contentRect.Top + fontSize; var charIndex = 0; for (int lineIndex = 0; lineIndex < _lines.Count; lineIndex++) { var line = _lines[lineIndex]; var x = contentRect.Left; // Draw selection for this line if applicable if (_selectionStart >= 0 && _selectionLength != 0) { // Handle both positive and negative selection lengths var selStart = _selectionLength > 0 ? _selectionStart : _selectionStart + _selectionLength; var selEnd = _selectionLength > 0 ? _selectionStart + _selectionLength : _selectionStart; var lineStart = charIndex; var lineEnd = charIndex + line.Length; if (selEnd > lineStart && selStart < lineEnd) { var selStartInLine = Math.Max(0, selStart - lineStart); var selEndInLine = Math.Min(line.Length, selEnd - lineStart); var startX = x + MeasureText(line.Substring(0, selStartInLine), font); var endX = x + MeasureText(line.Substring(0, selEndInLine), font); canvas.DrawRect(new SKRect(startX, y - fontSize, endX, y + lineSpacing - fontSize), selectionPaint); } } // Determine if pre-edit text should be displayed on this line var (cursorLine, cursorCol) = GetLineColumn(_cursorPosition); var displayLine = line; var hasPreEditOnThisLine = !string.IsNullOrEmpty(_preEditText) && cursorLine == lineIndex; if (hasPreEditOnThisLine) { // Insert pre-edit text at cursor position within this line var insertPos = Math.Min(cursorCol, line.Length); displayLine = line.Insert(insertPos, _preEditText); } // Draw the text with font fallback for emoji/CJK support DrawTextWithFallback(canvas, displayLine, x, y, textPaint, SKTypeface.Default); // Draw underline for pre-edit (composition) text if (hasPreEditOnThisLine) { DrawPreEditUnderline(canvas, textPaint, line, x, y, contentRect); } // Draw cursor if on this line if (IsFocused && _cursorVisible) { if (cursorLine == lineIndex) { // Account for pre-edit text when calculating cursor position var textToCursor = line.Substring(0, Math.Min(cursorCol, line.Length)); var cursorX = x + MeasureText(textToCursor, font); // If there's pre-edit text, cursor goes after it if (hasPreEditOnThisLine && _preEditText.Length > 0) { cursorX += MeasureText(_preEditText, font); } using var cursorPaint = new SKPaint { Color = ToSKColor(CursorColor), Style = SKPaintStyle.Stroke, StrokeWidth = 2, IsAntialias = true }; canvas.DrawLine(cursorX, y - fontSize + 2, cursorX, y + 2, cursorPaint); } } y += lineSpacing; charIndex += line.Length + 1; } } canvas.Restore(); // Draw scrollbar if needed var totalHeight = _lines.Count * fontSize * lineHeight; if (totalHeight > contentRect.Height) { DrawScrollbar(canvas, bounds, contentRect.Height, totalHeight); } } private float MeasureText(string text, SKFont font) { if (string.IsNullOrEmpty(text)) return 0; using var paint = new SKPaint(font); return paint.MeasureText(text); } private void DrawScrollbar(SKCanvas canvas, SKRect bounds, float viewHeight, float contentHeight) { var scrollbarWidth = 6f; var scrollbarMargin = 2f; var paddingTop = (float)Padding.Top; var scrollbarHeight = Math.Max(20, viewHeight * (viewHeight / contentHeight)); var scrollbarY = bounds.Top + paddingTop + (_scrollOffsetY / contentHeight) * (viewHeight - scrollbarHeight); using var paint = new SKPaint { Color = SkiaTheme.Shadow25SK, Style = SKPaintStyle.Fill, IsAntialias = true }; canvas.DrawRoundRect(new SKRoundRect( new SKRect( bounds.Right - scrollbarWidth - scrollbarMargin, scrollbarY, bounds.Right - scrollbarMargin, scrollbarY + scrollbarHeight), scrollbarWidth / 2), paint); } /// /// Draws text with font fallback for emoji, CJK, and other scripts. /// private void DrawTextWithFallback(SKCanvas canvas, string text, float x, float y, SKPaint paint, SKTypeface preferredTypeface) => TextRenderingHelper.DrawTextWithFallback(canvas, text, x, y, paint, preferredTypeface, (float)FontSize); /// /// Draws underline for IME pre-edit (composition) text. /// private void DrawPreEditUnderline(SKCanvas canvas, SKPaint paint, string displayText, float x, float y, SKRect bounds) => TextRenderingHelper.DrawPreEditUnderline(canvas, paint, displayText, _cursorPosition, _preEditText, x, y); protected override Size MeasureOverride(Size availableSize) { if (AutoSize) { var fontSize = (float)FontSize; var lineHeight = (float)LineHeight; var lineSpacing = fontSize * lineHeight; var verticalPadding = Padding.Top + Padding.Bottom; var height = Math.Max(lineSpacing + verticalPadding, _lines.Count * lineSpacing + verticalPadding); return new Size( availableSize.Width < double.MaxValue ? availableSize.Width : 200, Math.Min(height, availableSize.Height < double.MaxValue ? availableSize.Height : 200)); } return new Size( availableSize.Width < double.MaxValue ? Math.Min(availableSize.Width, 200) : 200, availableSize.Height < double.MaxValue ? Math.Min(availableSize.Height, 150) : 150); } }