// 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 Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform.Linux.Rendering;
using SkiaSharp;
namespace Microsoft.Maui.Platform;
public partial class SkiaEntry
{
protected override void DrawBackground(SKCanvas canvas, SKRect bounds)
{
// Skip base background drawing if Entry is transparent
// (transparent Entry is likely inside a Border that handles appearance)
var bgColor = ToSKColor(EntryBackgroundColor);
var baseBgColor = GetEffectiveBackgroundColor();
if (bgColor.Alpha < 10 && baseBgColor.Alpha < 10)
return;
// Otherwise let base class draw
base.DrawBackground(canvas, bounds);
}
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
{
var bgColor = ToSKColor(EntryBackgroundColor);
var isTransparent = bgColor.Alpha < 10; // Consider nearly transparent as transparent
if (!isTransparent)
{
// Draw background
using var bgPaint = new SKPaint
{
Color = bgColor,
IsAntialias = true,
Style = SKPaintStyle.Fill
};
var rect = new SKRoundRect(bounds, (float)CornerRadius);
canvas.DrawRoundRect(rect, bgPaint);
// Draw border
var borderColor = IsFocused ? ToSKColor(FocusedBorderColor) : ToSKColor(BorderColor);
var borderWidth = IsFocused ? (float)BorderWidth + 1 : (float)BorderWidth;
using var borderPaint = new SKPaint
{
Color = borderColor,
IsAntialias = true,
Style = SKPaintStyle.Stroke,
StrokeWidth = borderWidth
};
canvas.DrawRoundRect(rect, borderPaint);
}
// Calculate content bounds
var contentBounds = new SKRect(
bounds.Left + (float)Padding.Left,
bounds.Top + (float)Padding.Top,
bounds.Right - (float)Padding.Right,
bounds.Bottom - (float)Padding.Bottom);
// Reserve space for clear button if shown
var clearButtonSize = 20f;
var clearButtonMargin = 8f;
var showClear = ShouldShowClearButton();
if (showClear)
{
contentBounds.Right -= clearButtonSize + clearButtonMargin;
}
// Set up clipping for text area
canvas.Save();
canvas.ClipRect(contentBounds);
var fontStyle = GetFontStyle();
var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(GetEffectiveFontFamily(), fontStyle)
?? SKTypeface.Default;
using var font = new SKFont(typeface, (float)FontSize);
using var paint = new SKPaint(font) { IsAntialias = true };
var displayText = GetDisplayText();
// Append pre-edit text at cursor position for IME composition display
var preEditInsertPos = Math.Min(_cursorPosition, displayText.Length);
var displayTextWithPreEdit = string.IsNullOrEmpty(_preEditText)
? displayText
: displayText.Insert(preEditInsertPos, _preEditText);
var hasText = !string.IsNullOrEmpty(displayTextWithPreEdit);
if (hasText)
{
paint.Color = GetEffectiveTextColor();
// Measure text to cursor position for scrolling
var textToCursor = displayText.Substring(0, Math.Min(_cursorPosition, displayText.Length));
var cursorX = paint.MeasureText(textToCursor);
// Auto-scroll to keep cursor visible
if (cursorX - _scrollOffset > contentBounds.Width - 10)
{
_scrollOffset = cursorX - contentBounds.Width + 10;
}
else if (cursorX - _scrollOffset < 0)
{
_scrollOffset = cursorX;
}
// Draw selection (check != 0 to handle both forward and backward selection)
if (IsFocused && _selectionLength != 0)
{
DrawSelection(canvas, paint, displayText, contentBounds);
}
// Calculate text position based on vertical alignment
var textBounds = new SKRect();
paint.MeasureText(displayText, ref textBounds);
float x = contentBounds.Left - _scrollOffset;
float y = VerticalTextAlignment switch
{
TextAlignment.Start => contentBounds.Top - textBounds.Top,
TextAlignment.End => contentBounds.Bottom - textBounds.Bottom,
_ => contentBounds.MidY - textBounds.MidY // Center
};
// Draw the text with font fallback for emoji/CJK support
DrawTextWithFallback(canvas, displayTextWithPreEdit, x, y, paint, typeface);
// Draw underline for pre-edit (composition) text
if (!string.IsNullOrEmpty(_preEditText))
{
DrawPreEditUnderline(canvas, paint, displayText, x, y, contentBounds);
}
// Draw cursor
if (IsFocused && !IsReadOnly && _cursorVisible)
{
DrawCursor(canvas, paint, displayText, contentBounds);
}
}
else if (!string.IsNullOrEmpty(Placeholder))
{
// Draw placeholder
paint.Color = GetEffectivePlaceholderColor();
var textBounds = new SKRect();
paint.MeasureText(Placeholder, ref textBounds);
float x = contentBounds.Left;
float y = contentBounds.MidY - textBounds.MidY;
canvas.DrawText(Placeholder, x, y, paint);
}
else if (IsFocused && !IsReadOnly && _cursorVisible)
{
// Draw cursor even with no text
DrawCursor(canvas, paint, "", contentBounds);
}
canvas.Restore();
// Draw clear button if applicable
if (showClear)
{
DrawClearButton(canvas, bounds, clearButtonSize, clearButtonMargin);
}
}
private bool ShouldShowClearButton()
{
if (string.IsNullOrEmpty(Text)) return false;
// Check both legacy ShowClearButton and MAUI ClearButtonVisibility
if (ShowClearButton && IsFocused) return true;
return ClearButtonVisibility switch
{
ClearButtonVisibility.WhileEditing => IsFocused,
ClearButtonVisibility.Never => false,
_ => false
};
}
private void DrawClearButton(SKCanvas canvas, SKRect bounds, float size, float margin)
{
var centerX = bounds.Right - margin - size / 2;
var centerY = bounds.MidY;
// Draw circle background
using var circlePaint = new SKPaint
{
Color = SkiaTheme.Gray400SK,
IsAntialias = true,
Style = SKPaintStyle.Fill
};
canvas.DrawCircle(centerX, centerY, size / 2 - 2, circlePaint);
// Draw X
using var xPaint = new SKPaint
{
Color = SkiaTheme.BackgroundWhiteSK,
IsAntialias = true,
Style = SKPaintStyle.Stroke,
StrokeWidth = 2,
StrokeCap = SKStrokeCap.Round
};
var offset = size / 4 - 1;
canvas.DrawLine(centerX - offset, centerY - offset, centerX + offset, centerY + offset, xPaint);
canvas.DrawLine(centerX - offset, centerY + offset, centerX + offset, centerY - offset, xPaint);
}
private void DrawSelection(SKCanvas canvas, SKPaint paint, string displayText, SKRect bounds)
{
var selStart = Math.Min(_selectionStart, _selectionStart + _selectionLength);
var selEnd = Math.Max(_selectionStart, _selectionStart + _selectionLength);
var textToStart = displayText.Substring(0, selStart);
var textToEnd = displayText.Substring(0, selEnd);
var startX = bounds.Left - _scrollOffset + paint.MeasureText(textToStart);
var endX = bounds.Left - _scrollOffset + paint.MeasureText(textToEnd);
using var selPaint = new SKPaint
{
Color = ToSKColor(SelectionColor),
Style = SKPaintStyle.Fill
};
canvas.DrawRect(startX, bounds.Top, endX - startX, bounds.Height, selPaint);
}
private void DrawCursor(SKCanvas canvas, SKPaint paint, string displayText, SKRect bounds)
{
var textToCursor = displayText.Substring(0, Math.Min(_cursorPosition, displayText.Length));
var cursorX = bounds.Left - _scrollOffset + paint.MeasureText(textToCursor);
using var cursorPaint = new SKPaint
{
Color = ToSKColor(CursorColor),
StrokeWidth = 2,
IsAntialias = true
};
canvas.DrawLine(cursorX, bounds.Top + 2, cursorX, bounds.Bottom - 2, cursorPaint);
}
///
/// 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);
private void ResetCursorBlink()
{
_cursorBlinkTime = DateTime.UtcNow;
_cursorVisible = true;
}
///
/// Updates cursor blink animation.
///
public void UpdateCursorBlink()
{
if (!IsFocused) return;
var elapsed = (DateTime.UtcNow - _cursorBlinkTime).TotalMilliseconds;
var newVisible = ((int)(elapsed / 500) % 2) == 0;
if (newVisible != _cursorVisible)
{
_cursorVisible = newVisible;
Invalidate();
}
}
protected override Size MeasureOverride(Size availableSize)
{
var fontStyle = GetFontStyle();
var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(GetEffectiveFontFamily(), fontStyle)
?? SKTypeface.Default;
using var font = new SKFont(typeface, (float)FontSize);
// Use font metrics for consistent height regardless of text content
// This prevents size changes when placeholder disappears or text changes
var metrics = font.Metrics;
var textHeight = metrics.Descent - metrics.Ascent + metrics.Leading;
return new Size(
200, // Default width, will be overridden by layout
textHeight + Padding.Top + Padding.Bottom + BorderWidth * 2);
}
}