Preview 3: Complete control implementation with XAML data binding
Major milestone adding full control functionality: Controls Enhanced: - Entry/Editor: Full keyboard input, cursor navigation, selection, clipboard - CollectionView: Data binding, selection highlighting, scrolling - CheckBox/Switch/Slider: Interactive state management - Picker/DatePicker/TimePicker: Dropdown selection with popup overlays - ProgressBar/ActivityIndicator: Animated progress display - Button: Press/release visual states - Border/Frame: Rounded corners, stroke styling - Label: Text wrapping, alignment, decorations - Grid/StackLayout: Margin and padding support Features Added: - DisplayAlert dialogs with button actions - NavigationPage with toolbar and back navigation - Shell with flyout menu navigation - XAML value converters for data binding - Margin support in all layout containers - Popup overlay system for pickers New Samples: - TodoApp: Full CRUD task manager with NavigationPage - ShellDemo: Comprehensive control showcase Removed: - ControlGallery (replaced by ShellDemo) - LinuxDemo (replaced by TodoApp) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,24 +7,319 @@ using Microsoft.Maui.Platform.Linux.Rendering;
|
||||
namespace Microsoft.Maui.Platform;
|
||||
|
||||
/// <summary>
|
||||
/// Skia-rendered label control for displaying text.
|
||||
/// Skia-rendered label control for displaying text with full XAML styling support.
|
||||
/// </summary>
|
||||
public class SkiaLabel : SkiaView
|
||||
{
|
||||
public string Text { get; set; } = "";
|
||||
public SKColor TextColor { get; set; } = SKColors.Black;
|
||||
public string FontFamily { get; set; } = "Sans";
|
||||
public float FontSize { get; set; } = 14;
|
||||
public bool IsBold { get; set; }
|
||||
public bool IsItalic { get; set; }
|
||||
public bool IsUnderline { get; set; }
|
||||
public bool IsStrikethrough { get; set; }
|
||||
public TextAlignment HorizontalTextAlignment { get; set; } = TextAlignment.Start;
|
||||
public TextAlignment VerticalTextAlignment { get; set; } = TextAlignment.Center;
|
||||
public LineBreakMode LineBreakMode { get; set; } = LineBreakMode.TailTruncation;
|
||||
public int MaxLines { get; set; } = 0; // 0 = unlimited
|
||||
public float LineHeight { get; set; } = 1.2f;
|
||||
public float CharacterSpacing { get; set; }
|
||||
#region BindableProperties
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for Text.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty TextProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(Text),
|
||||
typeof(string),
|
||||
typeof(SkiaLabel),
|
||||
"",
|
||||
propertyChanged: (b, o, n) => ((SkiaLabel)b).OnTextChanged());
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for TextColor.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty TextColorProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(TextColor),
|
||||
typeof(SKColor),
|
||||
typeof(SkiaLabel),
|
||||
SKColors.Black,
|
||||
propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate());
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for FontFamily.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty FontFamilyProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(FontFamily),
|
||||
typeof(string),
|
||||
typeof(SkiaLabel),
|
||||
"Sans",
|
||||
propertyChanged: (b, o, n) => ((SkiaLabel)b).OnFontChanged());
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for FontSize.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty FontSizeProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(FontSize),
|
||||
typeof(float),
|
||||
typeof(SkiaLabel),
|
||||
14f,
|
||||
propertyChanged: (b, o, n) => ((SkiaLabel)b).OnFontChanged());
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for IsBold.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty IsBoldProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(IsBold),
|
||||
typeof(bool),
|
||||
typeof(SkiaLabel),
|
||||
false,
|
||||
propertyChanged: (b, o, n) => ((SkiaLabel)b).OnFontChanged());
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for IsItalic.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty IsItalicProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(IsItalic),
|
||||
typeof(bool),
|
||||
typeof(SkiaLabel),
|
||||
false,
|
||||
propertyChanged: (b, o, n) => ((SkiaLabel)b).OnFontChanged());
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for IsUnderline.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty IsUnderlineProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(IsUnderline),
|
||||
typeof(bool),
|
||||
typeof(SkiaLabel),
|
||||
false,
|
||||
propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate());
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for IsStrikethrough.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty IsStrikethroughProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(IsStrikethrough),
|
||||
typeof(bool),
|
||||
typeof(SkiaLabel),
|
||||
false,
|
||||
propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate());
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for HorizontalTextAlignment.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty HorizontalTextAlignmentProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(HorizontalTextAlignment),
|
||||
typeof(TextAlignment),
|
||||
typeof(SkiaLabel),
|
||||
TextAlignment.Start,
|
||||
propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate());
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for VerticalTextAlignment.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty VerticalTextAlignmentProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(VerticalTextAlignment),
|
||||
typeof(TextAlignment),
|
||||
typeof(SkiaLabel),
|
||||
TextAlignment.Center,
|
||||
propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate());
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for LineBreakMode.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty LineBreakModeProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(LineBreakMode),
|
||||
typeof(LineBreakMode),
|
||||
typeof(SkiaLabel),
|
||||
LineBreakMode.TailTruncation,
|
||||
propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate());
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for MaxLines.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty MaxLinesProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(MaxLines),
|
||||
typeof(int),
|
||||
typeof(SkiaLabel),
|
||||
0,
|
||||
propertyChanged: (b, o, n) => ((SkiaLabel)b).OnTextChanged());
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for LineHeight.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty LineHeightProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(LineHeight),
|
||||
typeof(float),
|
||||
typeof(SkiaLabel),
|
||||
1.2f,
|
||||
propertyChanged: (b, o, n) => ((SkiaLabel)b).OnTextChanged());
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for CharacterSpacing.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty CharacterSpacingProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(CharacterSpacing),
|
||||
typeof(float),
|
||||
typeof(SkiaLabel),
|
||||
0f,
|
||||
propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate());
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for Padding.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty PaddingProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(Padding),
|
||||
typeof(SKRect),
|
||||
typeof(SkiaLabel),
|
||||
SKRect.Empty,
|
||||
propertyChanged: (b, o, n) => ((SkiaLabel)b).OnTextChanged());
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the text content.
|
||||
/// </summary>
|
||||
public string Text
|
||||
{
|
||||
get => (string)GetValue(TextProperty);
|
||||
set => SetValue(TextProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the text color.
|
||||
/// </summary>
|
||||
public SKColor TextColor
|
||||
{
|
||||
get => (SKColor)GetValue(TextColorProperty);
|
||||
set => SetValue(TextColorProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the font family.
|
||||
/// </summary>
|
||||
public string FontFamily
|
||||
{
|
||||
get => (string)GetValue(FontFamilyProperty);
|
||||
set => SetValue(FontFamilyProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the font size.
|
||||
/// </summary>
|
||||
public float FontSize
|
||||
{
|
||||
get => (float)GetValue(FontSizeProperty);
|
||||
set => SetValue(FontSizeProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the text is bold.
|
||||
/// </summary>
|
||||
public bool IsBold
|
||||
{
|
||||
get => (bool)GetValue(IsBoldProperty);
|
||||
set => SetValue(IsBoldProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the text is italic.
|
||||
/// </summary>
|
||||
public bool IsItalic
|
||||
{
|
||||
get => (bool)GetValue(IsItalicProperty);
|
||||
set => SetValue(IsItalicProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the text has underline.
|
||||
/// </summary>
|
||||
public bool IsUnderline
|
||||
{
|
||||
get => (bool)GetValue(IsUnderlineProperty);
|
||||
set => SetValue(IsUnderlineProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the text has strikethrough.
|
||||
/// </summary>
|
||||
public bool IsStrikethrough
|
||||
{
|
||||
get => (bool)GetValue(IsStrikethroughProperty);
|
||||
set => SetValue(IsStrikethroughProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the horizontal text alignment.
|
||||
/// </summary>
|
||||
public TextAlignment HorizontalTextAlignment
|
||||
{
|
||||
get => (TextAlignment)GetValue(HorizontalTextAlignmentProperty);
|
||||
set => SetValue(HorizontalTextAlignmentProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the vertical text alignment.
|
||||
/// </summary>
|
||||
public TextAlignment VerticalTextAlignment
|
||||
{
|
||||
get => (TextAlignment)GetValue(VerticalTextAlignmentProperty);
|
||||
set => SetValue(VerticalTextAlignmentProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the line break mode.
|
||||
/// </summary>
|
||||
public LineBreakMode LineBreakMode
|
||||
{
|
||||
get => (LineBreakMode)GetValue(LineBreakModeProperty);
|
||||
set => SetValue(LineBreakModeProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of lines. 0 = unlimited.
|
||||
/// </summary>
|
||||
public int MaxLines
|
||||
{
|
||||
get => (int)GetValue(MaxLinesProperty);
|
||||
set => SetValue(MaxLinesProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the line height multiplier.
|
||||
/// </summary>
|
||||
public float LineHeight
|
||||
{
|
||||
get => (float)GetValue(LineHeightProperty);
|
||||
set => SetValue(LineHeightProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the character spacing.
|
||||
/// </summary>
|
||||
public float CharacterSpacing
|
||||
{
|
||||
get => (float)GetValue(CharacterSpacingProperty);
|
||||
set => SetValue(CharacterSpacingProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the padding.
|
||||
/// </summary>
|
||||
public SKRect Padding
|
||||
{
|
||||
get => (SKRect)GetValue(PaddingProperty);
|
||||
set => SetValue(PaddingProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the horizontal alignment (compatibility property).
|
||||
/// </summary>
|
||||
public SkiaTextAlignment HorizontalAlignment
|
||||
{
|
||||
get => HorizontalTextAlignment switch
|
||||
@@ -42,6 +337,10 @@ public class SkiaLabel : SkiaView
|
||||
_ => TextAlignment.Start
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the vertical alignment (compatibility property).
|
||||
/// </summary>
|
||||
public SkiaVerticalAlignment VerticalAlignment
|
||||
{
|
||||
get => VerticalTextAlignment switch
|
||||
@@ -59,7 +358,45 @@ public class SkiaLabel : SkiaView
|
||||
_ => TextAlignment.Start
|
||||
};
|
||||
}
|
||||
public SKRect Padding { get; set; } = new SKRect(0, 0, 0, 0);
|
||||
|
||||
#endregion
|
||||
|
||||
private static SKTypeface? _cachedTypeface;
|
||||
|
||||
private void OnTextChanged()
|
||||
{
|
||||
InvalidateMeasure();
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
private void OnFontChanged()
|
||||
{
|
||||
InvalidateMeasure();
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
private static SKTypeface GetLinuxTypeface()
|
||||
{
|
||||
if (_cachedTypeface != null) return _cachedTypeface;
|
||||
|
||||
// Try common Linux font paths
|
||||
string[] fontPaths = {
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/truetype/ubuntu/Ubuntu-R.ttf",
|
||||
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
|
||||
"/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf"
|
||||
};
|
||||
|
||||
foreach (var path in fontPaths)
|
||||
{
|
||||
if (System.IO.File.Exists(path))
|
||||
{
|
||||
_cachedTypeface = SKTypeface.FromFile(path);
|
||||
if (_cachedTypeface != null) return _cachedTypeface;
|
||||
}
|
||||
}
|
||||
return SKTypeface.Default;
|
||||
}
|
||||
|
||||
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
|
||||
{
|
||||
@@ -71,8 +408,11 @@ public class SkiaLabel : SkiaView
|
||||
SKFontStyleWidth.Normal,
|
||||
IsItalic ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright);
|
||||
|
||||
var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, fontStyle)
|
||||
?? SKTypeface.Default;
|
||||
var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, fontStyle);
|
||||
if (typeface == null || typeface == SKTypeface.Default)
|
||||
{
|
||||
typeface = GetLinuxTypeface();
|
||||
}
|
||||
|
||||
using var font = new SKFont(typeface, FontSize);
|
||||
using var paint = new SKPaint(font)
|
||||
@@ -89,13 +429,16 @@ public class SkiaLabel : SkiaView
|
||||
bounds.Bottom - Padding.Bottom);
|
||||
|
||||
// Handle single line vs multiline
|
||||
if (MaxLines == 1 || !Text.Contains('\n'))
|
||||
// Use DrawSingleLine for normal labels (MaxLines <= 1 or unlimited) without newlines
|
||||
// Use DrawMultiLineWithWrapping only when MaxLines > 1 (word wrap needed) or text has newlines
|
||||
bool needsMultiLine = MaxLines > 1 || Text.Contains('\n');
|
||||
if (needsMultiLine)
|
||||
{
|
||||
DrawSingleLine(canvas, paint, font, contentBounds);
|
||||
DrawMultiLineWithWrapping(canvas, paint, font, contentBounds);
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawMultiLine(canvas, paint, font, contentBounds);
|
||||
DrawSingleLine(canvas, paint, font, contentBounds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,10 +503,140 @@ public class SkiaLabel : SkiaView
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawMultiLineWithWrapping(SKCanvas canvas, SKPaint paint, SKFont font, SKRect bounds)
|
||||
{
|
||||
// Handle inverted or zero-height/width bounds
|
||||
var effectiveBounds = bounds;
|
||||
|
||||
// Fix invalid height
|
||||
if (bounds.Height <= 0)
|
||||
{
|
||||
var effectiveLH = LineHeight <= 0 ? 1.2f : LineHeight;
|
||||
var estimatedHeight = MaxLines > 0 ? MaxLines * FontSize * effectiveLH : FontSize * effectiveLH * 10;
|
||||
effectiveBounds = new SKRect(bounds.Left, bounds.Top, bounds.Right, bounds.Top + estimatedHeight);
|
||||
}
|
||||
|
||||
// Fix invalid width - use a reasonable default if width is invalid or extremely large
|
||||
float effectiveWidth = effectiveBounds.Width;
|
||||
if (effectiveWidth <= 0)
|
||||
{
|
||||
// Use a default width based on canvas
|
||||
effectiveWidth = 400; // Reasonable default
|
||||
}
|
||||
|
||||
// Note: Previously had width capping logic here that reduced effective width
|
||||
// to 60% for multiline labels. Removed - the layout system should now provide
|
||||
// correct widths, and artificially capping causes text to wrap too early.
|
||||
|
||||
// First, word-wrap the text to fit within bounds
|
||||
var wrappedLines = WrapText(paint, Text, effectiveWidth);
|
||||
|
||||
// LineHeight of -1 or <= 0 means "use default" - use 1.2 as default multiplier
|
||||
var effectiveLineHeight = LineHeight <= 0 ? 1.2f : LineHeight;
|
||||
var lineSpacing = FontSize * effectiveLineHeight;
|
||||
var maxLinesToDraw = MaxLines > 0 ? Math.Min(MaxLines, wrappedLines.Count) : wrappedLines.Count;
|
||||
|
||||
// Calculate total height
|
||||
var totalHeight = maxLinesToDraw * lineSpacing;
|
||||
|
||||
// Calculate starting Y based on vertical alignment
|
||||
float startY = VerticalTextAlignment switch
|
||||
{
|
||||
TextAlignment.Start => effectiveBounds.Top + FontSize,
|
||||
TextAlignment.Center => effectiveBounds.MidY - totalHeight / 2 + FontSize,
|
||||
TextAlignment.End => effectiveBounds.Bottom - totalHeight + FontSize,
|
||||
_ => effectiveBounds.Top + FontSize
|
||||
};
|
||||
|
||||
for (int i = 0; i < maxLinesToDraw; i++)
|
||||
{
|
||||
var line = wrappedLines[i];
|
||||
|
||||
// Add ellipsis if this is the last line and there are more lines
|
||||
bool isLastLine = i == maxLinesToDraw - 1;
|
||||
bool hasMoreContent = maxLinesToDraw < wrappedLines.Count;
|
||||
if (isLastLine && hasMoreContent && LineBreakMode == LineBreakMode.TailTruncation)
|
||||
{
|
||||
line = TruncateTextWithEllipsis(paint, line, effectiveWidth);
|
||||
}
|
||||
|
||||
var textBounds = new SKRect();
|
||||
paint.MeasureText(line, ref textBounds);
|
||||
|
||||
float x = HorizontalTextAlignment switch
|
||||
{
|
||||
TextAlignment.Start => effectiveBounds.Left,
|
||||
TextAlignment.Center => effectiveBounds.MidX - textBounds.Width / 2,
|
||||
TextAlignment.End => effectiveBounds.Right - textBounds.Width,
|
||||
_ => effectiveBounds.Left
|
||||
};
|
||||
|
||||
float y = startY + i * lineSpacing;
|
||||
|
||||
// Don't break early for inverted bounds - just draw
|
||||
if (effectiveBounds.Height > 0 && y > effectiveBounds.Bottom)
|
||||
break;
|
||||
|
||||
canvas.DrawText(line, x, y, paint);
|
||||
}
|
||||
}
|
||||
|
||||
private List<string> WrapText(SKPaint paint, string text, float maxWidth)
|
||||
{
|
||||
var result = new List<string>();
|
||||
|
||||
// Split by newlines first
|
||||
var paragraphs = text.Split('\n');
|
||||
|
||||
foreach (var paragraph in paragraphs)
|
||||
{
|
||||
if (string.IsNullOrEmpty(paragraph))
|
||||
{
|
||||
result.Add("");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if paragraph fits in one line
|
||||
if (paint.MeasureText(paragraph) <= maxWidth)
|
||||
{
|
||||
result.Add(paragraph);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Word wrap this paragraph
|
||||
var words = paragraph.Split(' ');
|
||||
var currentLine = "";
|
||||
|
||||
foreach (var word in words)
|
||||
{
|
||||
var testLine = string.IsNullOrEmpty(currentLine) ? word : currentLine + " " + word;
|
||||
var lineWidth = paint.MeasureText(testLine);
|
||||
|
||||
if (lineWidth > maxWidth && !string.IsNullOrEmpty(currentLine))
|
||||
{
|
||||
result.Add(currentLine);
|
||||
currentLine = word;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentLine = testLine;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(currentLine))
|
||||
{
|
||||
result.Add(currentLine);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void DrawMultiLine(SKCanvas canvas, SKPaint paint, SKFont font, SKRect bounds)
|
||||
{
|
||||
var lines = Text.Split('\n');
|
||||
var lineSpacing = FontSize * LineHeight;
|
||||
var effectiveLineHeight = LineHeight <= 0 ? 1.2f : LineHeight;
|
||||
var lineSpacing = FontSize * effectiveLineHeight;
|
||||
var maxLinesToDraw = MaxLines > 0 ? Math.Min(MaxLines, lines.Length) : lines.Length;
|
||||
|
||||
// Calculate total height
|
||||
@@ -208,6 +681,42 @@ public class SkiaLabel : SkiaView
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Truncates text and ALWAYS adds ellipsis (used when there's more content to indicate).
|
||||
/// </summary>
|
||||
private string TruncateTextWithEllipsis(SKPaint paint, string text, float maxWidth)
|
||||
{
|
||||
const string ellipsis = "...";
|
||||
var ellipsisWidth = paint.MeasureText(ellipsis);
|
||||
var textWidth = paint.MeasureText(text);
|
||||
|
||||
// If text + ellipsis fits, just append ellipsis
|
||||
if (textWidth + ellipsisWidth <= maxWidth)
|
||||
return text + ellipsis;
|
||||
|
||||
// Otherwise, truncate to make room for ellipsis
|
||||
var availableWidth = maxWidth - ellipsisWidth;
|
||||
if (availableWidth <= 0)
|
||||
return ellipsis;
|
||||
|
||||
// Binary search for the right length
|
||||
int low = 0;
|
||||
int high = text.Length;
|
||||
|
||||
while (low < high)
|
||||
{
|
||||
int mid = (low + high + 1) / 2;
|
||||
var substring = text.Substring(0, mid);
|
||||
|
||||
if (paint.MeasureText(substring) <= availableWidth)
|
||||
low = mid;
|
||||
else
|
||||
high = mid - 1;
|
||||
}
|
||||
|
||||
return text.Substring(0, low) + ellipsis;
|
||||
}
|
||||
|
||||
private string TruncateText(SKPaint paint, string text, float maxWidth)
|
||||
{
|
||||
const string ellipsis = "...";
|
||||
@@ -252,33 +761,51 @@ public class SkiaLabel : SkiaView
|
||||
SKFontStyleWidth.Normal,
|
||||
IsItalic ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright);
|
||||
|
||||
var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, fontStyle)
|
||||
?? SKTypeface.Default;
|
||||
// Use same typeface logic as OnDraw to ensure consistent measurement
|
||||
var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, fontStyle);
|
||||
if (typeface == null || typeface == SKTypeface.Default)
|
||||
{
|
||||
typeface = GetLinuxTypeface();
|
||||
}
|
||||
|
||||
using var font = new SKFont(typeface, FontSize);
|
||||
using var paint = new SKPaint(font);
|
||||
|
||||
if (MaxLines == 1 || !Text.Contains('\n'))
|
||||
// Use same logic as OnDraw: multiline only when MaxLines > 1 or text has newlines
|
||||
bool needsMultiLine = MaxLines > 1 || Text.Contains('\n');
|
||||
if (!needsMultiLine)
|
||||
{
|
||||
var textBounds = new SKRect();
|
||||
paint.MeasureText(Text, ref textBounds);
|
||||
|
||||
// Add small buffer for font rendering tolerance
|
||||
const float widthBuffer = 4f;
|
||||
|
||||
return new SKSize(
|
||||
textBounds.Width + Padding.Left + Padding.Right,
|
||||
textBounds.Width + Padding.Left + Padding.Right + widthBuffer,
|
||||
textBounds.Height + Padding.Top + Padding.Bottom);
|
||||
}
|
||||
else
|
||||
{
|
||||
var lines = Text.Split('\n');
|
||||
var maxLinesToMeasure = MaxLines > 0 ? Math.Min(MaxLines, lines.Length) : lines.Length;
|
||||
// Use available width for word wrapping measurement
|
||||
var wrapWidth = availableSize.Width - Padding.Left - Padding.Right;
|
||||
if (wrapWidth <= 0)
|
||||
{
|
||||
wrapWidth = float.MaxValue; // No wrapping if no width constraint
|
||||
}
|
||||
|
||||
// Wrap text to get actual line count
|
||||
var wrappedLines = WrapText(paint, Text, wrapWidth);
|
||||
var maxLinesToMeasure = MaxLines > 0 ? Math.Min(MaxLines, wrappedLines.Count) : wrappedLines.Count;
|
||||
|
||||
float maxWidth = 0;
|
||||
foreach (var line in lines.Take(maxLinesToMeasure))
|
||||
foreach (var line in wrappedLines.Take(maxLinesToMeasure))
|
||||
{
|
||||
maxWidth = Math.Max(maxWidth, paint.MeasureText(line));
|
||||
}
|
||||
|
||||
var totalHeight = maxLinesToMeasure * FontSize * LineHeight;
|
||||
var effectiveLineHeight = LineHeight <= 0 ? 1.2f : LineHeight;
|
||||
var totalHeight = maxLinesToMeasure * FontSize * effectiveLineHeight;
|
||||
|
||||
return new SKSize(
|
||||
maxWidth + Padding.Left + Padding.Right,
|
||||
|
||||
Reference in New Issue
Block a user