Files
maui-linux/Views/SkiaEditor.Drawing.cs
logikonline 077abc2feb refactor: split large files into partial classes by concern
Split LinuxApplication into Input and Lifecycle partials. Extract SkiaView into Accessibility, Drawing, and Input partials. Split SkiaEntry and SkiaEditor into Drawing and Input partials. Extract TextRenderingHelper from SkiaRenderingEngine. Create dedicated files for SkiaAbsoluteLayout, SkiaGrid, and SkiaStackLayout. This reduces file sizes from 40K+ lines to manageable units organized by responsibility.
2026-03-06 22:36:23 -05:00

256 lines
10 KiB
C#

// 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);
}
/// <summary>
/// Draws text with font fallback for emoji, CJK, and other scripts.
/// </summary>
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);
/// <summary>
/// Draws underline for IME pre-edit (composition) text.
/// </summary>
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);
}
}