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:
logikonline
2025-12-21 13:26:56 -05:00
parent f945d2a537
commit 1d55ac672a
142 changed files with 38925 additions and 4201 deletions

View File

@@ -0,0 +1,259 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.ComponentModel;
using System.Globalization;
using SkiaSharp;
using Microsoft.Maui.Graphics;
namespace Microsoft.Maui.Platform.Linux.Converters;
/// <summary>
/// Type converter for converting between MAUI Color and SKColor.
/// Enables XAML styling with Color values that get applied to Skia controls.
/// </summary>
public class SKColorTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(string) ||
sourceType == typeof(Color) ||
base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
{
return destinationType == typeof(string) ||
destinationType == typeof(Color) ||
base.CanConvertTo(context, destinationType);
}
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
if (value is Color mauiColor)
{
return ToSKColor(mauiColor);
}
if (value is string str)
{
return ParseColor(str);
}
return base.ConvertFrom(context, culture, value);
}
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
{
if (value is SKColor skColor)
{
if (destinationType == typeof(string))
{
return $"#{skColor.Alpha:X2}{skColor.Red:X2}{skColor.Green:X2}{skColor.Blue:X2}";
}
if (destinationType == typeof(Color))
{
return ToMauiColor(skColor);
}
}
return base.ConvertTo(context, culture, value, destinationType);
}
/// <summary>
/// Converts a MAUI Color to an SKColor.
/// </summary>
public static SKColor ToSKColor(Color mauiColor)
{
return new SKColor(
(byte)(mauiColor.Red * 255),
(byte)(mauiColor.Green * 255),
(byte)(mauiColor.Blue * 255),
(byte)(mauiColor.Alpha * 255));
}
/// <summary>
/// Converts an SKColor to a MAUI Color.
/// </summary>
public static Color ToMauiColor(SKColor skColor)
{
return new Color(
skColor.Red / 255f,
skColor.Green / 255f,
skColor.Blue / 255f,
skColor.Alpha / 255f);
}
/// <summary>
/// Parses a color string (hex, named, or rgb format).
/// </summary>
private static SKColor ParseColor(string colorString)
{
if (string.IsNullOrWhiteSpace(colorString))
return SKColors.Black;
colorString = colorString.Trim();
// Try hex format
if (colorString.StartsWith("#"))
{
return SKColor.Parse(colorString);
}
// Try named colors
var namedColor = GetNamedColor(colorString.ToLowerInvariant());
if (namedColor.HasValue)
return namedColor.Value;
// Try rgb/rgba format
if (colorString.StartsWith("rgb", StringComparison.OrdinalIgnoreCase))
{
return ParseRgbColor(colorString);
}
// Fallback to SKColor.Parse
if (SKColor.TryParse(colorString, out var parsed))
return parsed;
return SKColors.Black;
}
private static SKColor? GetNamedColor(string name) => name switch
{
"transparent" => SKColors.Transparent,
"black" => SKColors.Black,
"white" => SKColors.White,
"red" => SKColors.Red,
"green" => SKColors.Green,
"blue" => SKColors.Blue,
"yellow" => SKColors.Yellow,
"cyan" => SKColors.Cyan,
"magenta" => SKColors.Magenta,
"gray" or "grey" => SKColors.Gray,
"darkgray" or "darkgrey" => SKColors.DarkGray,
"lightgray" or "lightgrey" => SKColors.LightGray,
"orange" => new SKColor(0xFF, 0xA5, 0x00),
"pink" => new SKColor(0xFF, 0xC0, 0xCB),
"purple" => new SKColor(0x80, 0x00, 0x80),
"brown" => new SKColor(0xA5, 0x2A, 0x2A),
"navy" => new SKColor(0x00, 0x00, 0x80),
"teal" => new SKColor(0x00, 0x80, 0x80),
"olive" => new SKColor(0x80, 0x80, 0x00),
"silver" => new SKColor(0xC0, 0xC0, 0xC0),
"maroon" => new SKColor(0x80, 0x00, 0x00),
"lime" => new SKColor(0x00, 0xFF, 0x00),
"aqua" => new SKColor(0x00, 0xFF, 0xFF),
"fuchsia" => new SKColor(0xFF, 0x00, 0xFF),
"gold" => new SKColor(0xFF, 0xD7, 0x00),
"coral" => new SKColor(0xFF, 0x7F, 0x50),
"salmon" => new SKColor(0xFA, 0x80, 0x72),
"crimson" => new SKColor(0xDC, 0x14, 0x3C),
"indigo" => new SKColor(0x4B, 0x00, 0x82),
"violet" => new SKColor(0xEE, 0x82, 0xEE),
"turquoise" => new SKColor(0x40, 0xE0, 0xD0),
"tan" => new SKColor(0xD2, 0xB4, 0x8C),
"chocolate" => new SKColor(0xD2, 0x69, 0x1E),
"tomato" => new SKColor(0xFF, 0x63, 0x47),
"steelblue" => new SKColor(0x46, 0x82, 0xB4),
"skyblue" => new SKColor(0x87, 0xCE, 0xEB),
"slategray" or "slategrey" => new SKColor(0x70, 0x80, 0x90),
"seagreen" => new SKColor(0x2E, 0x8B, 0x57),
"royalblue" => new SKColor(0x41, 0x69, 0xE1),
"plum" => new SKColor(0xDD, 0xA0, 0xDD),
"peru" => new SKColor(0xCD, 0x85, 0x3F),
"orchid" => new SKColor(0xDA, 0x70, 0xD6),
"orangered" => new SKColor(0xFF, 0x45, 0x00),
"olivedrab" => new SKColor(0x6B, 0x8E, 0x23),
"midnightblue" => new SKColor(0x19, 0x19, 0x70),
"mediumblue" => new SKColor(0x00, 0x00, 0xCD),
"limegreen" => new SKColor(0x32, 0xCD, 0x32),
"hotpink" => new SKColor(0xFF, 0x69, 0xB4),
"honeydew" => new SKColor(0xF0, 0xFF, 0xF0),
"greenyellow" => new SKColor(0xAD, 0xFF, 0x2F),
"forestgreen" => new SKColor(0x22, 0x8B, 0x22),
"firebrick" => new SKColor(0xB2, 0x22, 0x22),
"dodgerblue" => new SKColor(0x1E, 0x90, 0xFF),
"deeppink" => new SKColor(0xFF, 0x14, 0x93),
"deepskyblue" => new SKColor(0x00, 0xBF, 0xFF),
"darkviolet" => new SKColor(0x94, 0x00, 0xD3),
"darkturquoise" => new SKColor(0x00, 0xCE, 0xD1),
"darkslategray" or "darkslategrey" => new SKColor(0x2F, 0x4F, 0x4F),
"darkred" => new SKColor(0x8B, 0x00, 0x00),
"darkorange" => new SKColor(0xFF, 0x8C, 0x00),
"darkolivegreen" => new SKColor(0x55, 0x6B, 0x2F),
"darkmagenta" => new SKColor(0x8B, 0x00, 0x8B),
"darkkhaki" => new SKColor(0xBD, 0xB7, 0x6B),
"darkgreen" => new SKColor(0x00, 0x64, 0x00),
"darkgoldenrod" => new SKColor(0xB8, 0x86, 0x0B),
"darkcyan" => new SKColor(0x00, 0x8B, 0x8B),
"darkblue" => new SKColor(0x00, 0x00, 0x8B),
"cornflowerblue" => new SKColor(0x64, 0x95, 0xED),
"cadetblue" => new SKColor(0x5F, 0x9E, 0xA0),
"blueviolet" => new SKColor(0x8A, 0x2B, 0xE2),
"azure" => new SKColor(0xF0, 0xFF, 0xFF),
"aquamarine" => new SKColor(0x7F, 0xFF, 0xD4),
"aliceblue" => new SKColor(0xF0, 0xF8, 0xFF),
_ => null
};
private static SKColor ParseRgbColor(string colorString)
{
try
{
var isRgba = colorString.StartsWith("rgba", StringComparison.OrdinalIgnoreCase);
var startIndex = colorString.IndexOf('(');
var endIndex = colorString.IndexOf(')');
if (startIndex == -1 || endIndex == -1)
return SKColors.Black;
var values = colorString.Substring(startIndex + 1, endIndex - startIndex - 1)
.Split(',')
.Select(v => v.Trim())
.ToArray();
if (values.Length < 3)
return SKColors.Black;
var r = byte.Parse(values[0]);
var g = byte.Parse(values[1]);
var b = byte.Parse(values[2]);
byte a = 255;
if (isRgba && values.Length >= 4)
{
var alphaValue = float.Parse(values[3], CultureInfo.InvariantCulture);
a = (byte)(alphaValue <= 1 ? alphaValue * 255 : alphaValue);
}
return new SKColor(r, g, b, a);
}
catch
{
return SKColors.Black;
}
}
}
/// <summary>
/// Extension methods for color conversion.
/// </summary>
public static class ColorExtensions
{
/// <summary>
/// Converts a MAUI Color to an SKColor.
/// </summary>
public static SKColor ToSKColor(this Color color)
{
return SKColorTypeConverter.ToSKColor(color);
}
/// <summary>
/// Converts an SKColor to a MAUI Color.
/// </summary>
public static Color ToMauiColor(this SKColor color)
{
return SKColorTypeConverter.ToMauiColor(color);
}
}

View File

@@ -0,0 +1,328 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.ComponentModel;
using System.Globalization;
using SkiaSharp;
using Microsoft.Maui;
namespace Microsoft.Maui.Platform.Linux.Converters;
/// <summary>
/// Type converter for converting between MAUI Thickness and SKRect (for padding/margin).
/// Enables XAML styling with Thickness values that get applied to Skia controls.
/// </summary>
public class SKRectTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(string) ||
sourceType == typeof(Thickness) ||
sourceType == typeof(double) ||
sourceType == typeof(float) ||
base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
{
return destinationType == typeof(string) ||
destinationType == typeof(Thickness) ||
base.CanConvertTo(context, destinationType);
}
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
if (value is Thickness thickness)
{
return ThicknessToSKRect(thickness);
}
if (value is double d)
{
return new SKRect((float)d, (float)d, (float)d, (float)d);
}
if (value is float f)
{
return new SKRect(f, f, f, f);
}
if (value is string str)
{
return ParseRect(str);
}
return base.ConvertFrom(context, culture, value);
}
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
{
if (value is SKRect rect)
{
if (destinationType == typeof(string))
{
return $"{rect.Left},{rect.Top},{rect.Right},{rect.Bottom}";
}
if (destinationType == typeof(Thickness))
{
return SKRectToThickness(rect);
}
}
return base.ConvertTo(context, culture, value, destinationType);
}
/// <summary>
/// Converts a MAUI Thickness to an SKRect (used as padding storage).
/// </summary>
public static SKRect ThicknessToSKRect(Thickness thickness)
{
return new SKRect(
(float)thickness.Left,
(float)thickness.Top,
(float)thickness.Right,
(float)thickness.Bottom);
}
/// <summary>
/// Converts an SKRect (used as padding storage) to a MAUI Thickness.
/// </summary>
public static Thickness SKRectToThickness(SKRect rect)
{
return new Thickness(rect.Left, rect.Top, rect.Right, rect.Bottom);
}
/// <summary>
/// Parses a string into an SKRect for padding/margin.
/// Supports formats: "uniform", "horizontal,vertical", "left,top,right,bottom"
/// </summary>
private static SKRect ParseRect(string str)
{
if (string.IsNullOrWhiteSpace(str))
return SKRect.Empty;
str = str.Trim();
var parts = str.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 1)
{
// Uniform padding
if (float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var uniform))
{
return new SKRect(uniform, uniform, uniform, uniform);
}
}
else if (parts.Length == 2)
{
// Horizontal, Vertical
if (float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var horizontal) &&
float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var vertical))
{
return new SKRect(horizontal, vertical, horizontal, vertical);
}
}
else if (parts.Length == 4)
{
// Left, Top, Right, Bottom
if (float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var left) &&
float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var top) &&
float.TryParse(parts[2], NumberStyles.Float, CultureInfo.InvariantCulture, out var right) &&
float.TryParse(parts[3], NumberStyles.Float, CultureInfo.InvariantCulture, out var bottom))
{
return new SKRect(left, top, right, bottom);
}
}
return SKRect.Empty;
}
}
/// <summary>
/// Type converter for SKSize.
/// </summary>
public class SKSizeTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(string) ||
sourceType == typeof(Size) ||
base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
{
return destinationType == typeof(string) ||
destinationType == typeof(Size) ||
base.CanConvertTo(context, destinationType);
}
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
if (value is Size size)
{
return new SKSize((float)size.Width, (float)size.Height);
}
if (value is string str)
{
return ParseSize(str);
}
return base.ConvertFrom(context, culture, value);
}
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
{
if (value is SKSize size)
{
if (destinationType == typeof(string))
{
return $"{size.Width},{size.Height}";
}
if (destinationType == typeof(Size))
{
return new Size(size.Width, size.Height);
}
}
return base.ConvertTo(context, culture, value, destinationType);
}
private static SKSize ParseSize(string str)
{
if (string.IsNullOrWhiteSpace(str))
return SKSize.Empty;
str = str.Trim();
var parts = str.Split(new[] { ',', ' ', 'x', 'X' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 1)
{
if (float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var uniform))
{
return new SKSize(uniform, uniform);
}
}
else if (parts.Length == 2)
{
if (float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var width) &&
float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var height))
{
return new SKSize(width, height);
}
}
return SKSize.Empty;
}
}
/// <summary>
/// Type converter for SKPoint.
/// </summary>
public class SKPointTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(string) ||
sourceType == typeof(Point) ||
base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
{
return destinationType == typeof(string) ||
destinationType == typeof(Point) ||
base.CanConvertTo(context, destinationType);
}
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
if (value is Point point)
{
return new SKPoint((float)point.X, (float)point.Y);
}
if (value is string str)
{
return ParsePoint(str);
}
return base.ConvertFrom(context, culture, value);
}
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
{
if (value is SKPoint point)
{
if (destinationType == typeof(string))
{
return $"{point.X},{point.Y}";
}
if (destinationType == typeof(Point))
{
return new Point(point.X, point.Y);
}
}
return base.ConvertTo(context, culture, value, destinationType);
}
private static SKPoint ParsePoint(string str)
{
if (string.IsNullOrWhiteSpace(str))
return SKPoint.Empty;
str = str.Trim();
var parts = str.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 2)
{
if (float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var x) &&
float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var y))
{
return new SKPoint(x, y);
}
}
return SKPoint.Empty;
}
}
/// <summary>
/// Extension methods for SkiaSharp type conversions.
/// </summary>
public static class SKTypeExtensions
{
public static SKRect ToSKRect(this Thickness thickness)
{
return SKRectTypeConverter.ThicknessToSKRect(thickness);
}
public static Thickness ToThickness(this SKRect rect)
{
return SKRectTypeConverter.SKRectToThickness(rect);
}
public static SKSize ToSKSize(this Size size)
{
return new SKSize((float)size.Width, (float)size.Height);
}
public static Size ToSize(this SKSize size)
{
return new Size(size.Width, size.Height);
}
public static SKPoint ToSKPoint(this Point point)
{
return new SKPoint((float)point.X, (float)point.Y);
}
public static Point ToPoint(this SKPoint point)
{
return new Point(point.X, point.Y);
}
}