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

@@ -6,99 +6,192 @@ using SkiaSharp;
namespace Microsoft.Maui.Platform;
/// <summary>
/// Skia-rendered border/frame container control.
/// Skia-rendered border/frame container control with full XAML styling support.
/// </summary>
public class SkiaBorder : SkiaLayoutView
{
private float _strokeThickness = 1;
private float _cornerRadius = 0;
private SKColor _stroke = SKColors.Black;
private float _paddingLeft = 0;
private float _paddingTop = 0;
private float _paddingRight = 0;
private float _paddingBottom = 0;
private bool _hasShadow;
#region BindableProperties
public static readonly BindableProperty StrokeThicknessProperty =
BindableProperty.Create(nameof(StrokeThickness), typeof(float), typeof(SkiaBorder), 1f,
propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate());
public static readonly BindableProperty CornerRadiusProperty =
BindableProperty.Create(nameof(CornerRadius), typeof(float), typeof(SkiaBorder), 0f,
propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate());
public static readonly BindableProperty StrokeProperty =
BindableProperty.Create(nameof(Stroke), typeof(SKColor), typeof(SkiaBorder), SKColors.Black,
propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate());
public static readonly BindableProperty PaddingLeftProperty =
BindableProperty.Create(nameof(PaddingLeft), typeof(float), typeof(SkiaBorder), 0f,
propertyChanged: (b, o, n) => ((SkiaBorder)b).InvalidateMeasure());
public static readonly BindableProperty PaddingTopProperty =
BindableProperty.Create(nameof(PaddingTop), typeof(float), typeof(SkiaBorder), 0f,
propertyChanged: (b, o, n) => ((SkiaBorder)b).InvalidateMeasure());
public static readonly BindableProperty PaddingRightProperty =
BindableProperty.Create(nameof(PaddingRight), typeof(float), typeof(SkiaBorder), 0f,
propertyChanged: (b, o, n) => ((SkiaBorder)b).InvalidateMeasure());
public static readonly BindableProperty PaddingBottomProperty =
BindableProperty.Create(nameof(PaddingBottom), typeof(float), typeof(SkiaBorder), 0f,
propertyChanged: (b, o, n) => ((SkiaBorder)b).InvalidateMeasure());
public static readonly BindableProperty HasShadowProperty =
BindableProperty.Create(nameof(HasShadow), typeof(bool), typeof(SkiaBorder), false,
propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate());
public static readonly BindableProperty ShadowColorProperty =
BindableProperty.Create(nameof(ShadowColor), typeof(SKColor), typeof(SkiaBorder), new SKColor(0, 0, 0, 40),
propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate());
public static readonly BindableProperty ShadowBlurRadiusProperty =
BindableProperty.Create(nameof(ShadowBlurRadius), typeof(float), typeof(SkiaBorder), 4f,
propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate());
public static readonly BindableProperty ShadowOffsetXProperty =
BindableProperty.Create(nameof(ShadowOffsetX), typeof(float), typeof(SkiaBorder), 2f,
propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate());
public static readonly BindableProperty ShadowOffsetYProperty =
BindableProperty.Create(nameof(ShadowOffsetY), typeof(float), typeof(SkiaBorder), 2f,
propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate());
#endregion
#region Properties
public float StrokeThickness
{
get => _strokeThickness;
set { _strokeThickness = value; Invalidate(); }
get => (float)GetValue(StrokeThicknessProperty);
set => SetValue(StrokeThicknessProperty, value);
}
public float CornerRadius
{
get => _cornerRadius;
set { _cornerRadius = value; Invalidate(); }
get => (float)GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
public SKColor Stroke
{
get => _stroke;
set { _stroke = value; Invalidate(); }
get => (SKColor)GetValue(StrokeProperty);
set => SetValue(StrokeProperty, value);
}
public float PaddingLeft
{
get => _paddingLeft;
set { _paddingLeft = value; InvalidateMeasure(); }
get => (float)GetValue(PaddingLeftProperty);
set => SetValue(PaddingLeftProperty, value);
}
public float PaddingTop
{
get => _paddingTop;
set { _paddingTop = value; InvalidateMeasure(); }
get => (float)GetValue(PaddingTopProperty);
set => SetValue(PaddingTopProperty, value);
}
public float PaddingRight
{
get => _paddingRight;
set { _paddingRight = value; InvalidateMeasure(); }
get => (float)GetValue(PaddingRightProperty);
set => SetValue(PaddingRightProperty, value);
}
public float PaddingBottom
{
get => _paddingBottom;
set { _paddingBottom = value; InvalidateMeasure(); }
get => (float)GetValue(PaddingBottomProperty);
set => SetValue(PaddingBottomProperty, value);
}
public bool HasShadow
{
get => _hasShadow;
set { _hasShadow = value; Invalidate(); }
get => (bool)GetValue(HasShadowProperty);
set => SetValue(HasShadowProperty, value);
}
public SKColor ShadowColor
{
get => (SKColor)GetValue(ShadowColorProperty);
set => SetValue(ShadowColorProperty, value);
}
public float ShadowBlurRadius
{
get => (float)GetValue(ShadowBlurRadiusProperty);
set => SetValue(ShadowBlurRadiusProperty, value);
}
public float ShadowOffsetX
{
get => (float)GetValue(ShadowOffsetXProperty);
set => SetValue(ShadowOffsetXProperty, value);
}
public float ShadowOffsetY
{
get => (float)GetValue(ShadowOffsetYProperty);
set => SetValue(ShadowOffsetYProperty, value);
}
#endregion
/// <summary>
/// Sets uniform padding on all sides.
/// </summary>
public void SetPadding(float all)
{
_paddingLeft = _paddingTop = _paddingRight = _paddingBottom = all;
InvalidateMeasure();
PaddingLeft = PaddingTop = PaddingRight = PaddingBottom = all;
}
/// <summary>
/// Sets padding with horizontal and vertical values.
/// </summary>
public void SetPadding(float horizontal, float vertical)
{
_paddingLeft = _paddingRight = horizontal;
_paddingTop = _paddingBottom = vertical;
InvalidateMeasure();
PaddingLeft = PaddingRight = horizontal;
PaddingTop = PaddingBottom = vertical;
}
/// <summary>
/// Sets padding with individual values for each side.
/// </summary>
public void SetPadding(float left, float top, float right, float bottom)
{
PaddingLeft = left;
PaddingTop = top;
PaddingRight = right;
PaddingBottom = bottom;
}
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
{
var strokeThickness = StrokeThickness;
var cornerRadius = CornerRadius;
var borderRect = new SKRect(
bounds.Left + _strokeThickness / 2,
bounds.Top + _strokeThickness / 2,
bounds.Right - _strokeThickness / 2,
bounds.Bottom - _strokeThickness / 2);
bounds.Left + strokeThickness / 2,
bounds.Top + strokeThickness / 2,
bounds.Right - strokeThickness / 2,
bounds.Bottom - strokeThickness / 2);
// Draw shadow if enabled
if (_hasShadow)
if (HasShadow)
{
using var shadowPaint = new SKPaint
{
Color = new SKColor(0, 0, 0, 40),
MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 4),
Color = ShadowColor,
MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, ShadowBlurRadius),
Style = SKPaintStyle.Fill
};
var shadowRect = new SKRect(borderRect.Left + 2, borderRect.Top + 2, borderRect.Right + 2, borderRect.Bottom + 2);
canvas.DrawRoundRect(new SKRoundRect(shadowRect, _cornerRadius), shadowPaint);
var shadowRect = new SKRect(
borderRect.Left + ShadowOffsetX,
borderRect.Top + ShadowOffsetY,
borderRect.Right + ShadowOffsetX,
borderRect.Bottom + ShadowOffsetY);
canvas.DrawRoundRect(new SKRoundRect(shadowRect, cornerRadius), shadowPaint);
}
// Draw background
@@ -108,22 +201,22 @@ public class SkiaBorder : SkiaLayoutView
Style = SKPaintStyle.Fill,
IsAntialias = true
};
canvas.DrawRoundRect(new SKRoundRect(borderRect, _cornerRadius), bgPaint);
canvas.DrawRoundRect(new SKRoundRect(borderRect, cornerRadius), bgPaint);
// Draw border
if (_strokeThickness > 0)
if (strokeThickness > 0)
{
using var borderPaint = new SKPaint
{
Color = _stroke,
Color = Stroke,
Style = SKPaintStyle.Stroke,
StrokeWidth = _strokeThickness,
StrokeWidth = strokeThickness,
IsAntialias = true
};
canvas.DrawRoundRect(new SKRoundRect(borderRect, _cornerRadius), borderPaint);
canvas.DrawRoundRect(new SKRoundRect(borderRect, cornerRadius), borderPaint);
}
// Draw children (call base which draws children)
// Draw children
foreach (var child in Children)
{
if (child.IsVisible)
@@ -140,21 +233,27 @@ public class SkiaBorder : SkiaLayoutView
protected new SKRect GetContentBounds(SKRect bounds)
{
var strokeThickness = StrokeThickness;
return new SKRect(
bounds.Left + _paddingLeft + _strokeThickness,
bounds.Top + _paddingTop + _strokeThickness,
bounds.Right - _paddingRight - _strokeThickness,
bounds.Bottom - _paddingBottom - _strokeThickness);
bounds.Left + PaddingLeft + strokeThickness,
bounds.Top + PaddingTop + strokeThickness,
bounds.Right - PaddingRight - strokeThickness,
bounds.Bottom - PaddingBottom - strokeThickness);
}
protected override SKSize MeasureOverride(SKSize availableSize)
{
var paddingWidth = _paddingLeft + _paddingRight + _strokeThickness * 2;
var paddingHeight = _paddingTop + _paddingBottom + _strokeThickness * 2;
var strokeThickness = StrokeThickness;
var paddingWidth = PaddingLeft + PaddingRight + strokeThickness * 2;
var paddingHeight = PaddingTop + PaddingBottom + strokeThickness * 2;
// Respect explicit size requests
var requestedWidth = WidthRequest >= 0 ? (float)WidthRequest : availableSize.Width;
var requestedHeight = HeightRequest >= 0 ? (float)HeightRequest : availableSize.Height;
var childAvailable = new SKSize(
availableSize.Width - paddingWidth,
availableSize.Height - paddingHeight);
Math.Max(0, requestedWidth - paddingWidth),
Math.Max(0, requestedHeight - paddingHeight));
var maxChildSize = SKSize.Empty;
@@ -166,19 +265,27 @@ public class SkiaBorder : SkiaLayoutView
Math.Max(maxChildSize.Height, childSize.Height));
}
return new SKSize(
maxChildSize.Width + paddingWidth,
maxChildSize.Height + paddingHeight);
// Use requested size if set, otherwise use child size + padding
var width = WidthRequest >= 0 ? (float)WidthRequest : maxChildSize.Width + paddingWidth;
var height = HeightRequest >= 0 ? (float)HeightRequest : maxChildSize.Height + paddingHeight;
return new SKSize(width, height);
}
protected override SKRect ArrangeOverride(SKRect bounds)
{
var contentBounds = GetContentBounds(bounds);
foreach (var child in Children)
{
child.Arrange(contentBounds);
// Apply child's margin
var margin = child.Margin;
var marginedBounds = new SKRect(
contentBounds.Left + (float)margin.Left,
contentBounds.Top + (float)margin.Top,
contentBounds.Right - (float)margin.Right,
contentBounds.Bottom - (float)margin.Bottom);
child.Arrange(marginedBounds);
}
return bounds;
@@ -186,7 +293,8 @@ public class SkiaBorder : SkiaLayoutView
}
/// <summary>
/// Frame control (alias for Border with shadow enabled).
/// Frame control - a Border with shadow enabled by default.
/// Mimics the MAUI Frame control appearance.
/// </summary>
public class SkiaFrame : SkiaBorder
{
@@ -196,5 +304,7 @@ public class SkiaFrame : SkiaBorder
CornerRadius = 4;
SetPadding(10);
BackgroundColor = SKColors.White;
Stroke = SKColors.Transparent;
StrokeThickness = 0;
}
}