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

@@ -7,8 +7,9 @@ namespace Microsoft.Maui.Platform;
/// <summary>
/// Base class for all Skia-rendered views on Linux.
/// Inherits from BindableObject to enable XAML styling, data binding, and Visual State Manager.
/// </summary>
public abstract class SkiaView : IDisposable
public abstract class SkiaView : BindableObject, IDisposable
{
// Popup overlay system for dropdowns, calendars, etc.
private static readonly List<(SkiaView Owner, Action<SKCanvas> Draw)> _popupOverlays = new();
@@ -32,7 +33,7 @@ public abstract class SkiaView : IDisposable
{
canvas.Restore();
}
foreach (var (_, draw) in _popupOverlays)
{
canvas.Save();
@@ -41,6 +42,189 @@ public abstract class SkiaView : IDisposable
}
}
/// <summary>
/// Gets the popup owner that should receive pointer events at the given coordinates.
/// This allows popups to receive events even outside their normal bounds.
/// </summary>
public static SkiaView? GetPopupOwnerAt(float x, float y)
{
// Check in reverse order (topmost popup first)
for (int i = _popupOverlays.Count - 1; i >= 0; i--)
{
var owner = _popupOverlays[i].Owner;
if (owner.HitTestPopupArea(x, y))
{
return owner;
}
}
return null;
}
/// <summary>
/// Checks if there are any active popup overlays.
/// </summary>
public static bool HasActivePopup => _popupOverlays.Count > 0;
/// <summary>
/// Override this to define the popup area for hit testing.
/// </summary>
protected virtual bool HitTestPopupArea(float x, float y)
{
// Default: no popup area beyond normal bounds
return Bounds.Contains(x, y);
}
#region BindableProperties
/// <summary>
/// Bindable property for IsVisible.
/// </summary>
public static readonly BindableProperty IsVisibleProperty =
BindableProperty.Create(
nameof(IsVisible),
typeof(bool),
typeof(SkiaView),
true,
propertyChanged: (b, o, n) => ((SkiaView)b).OnVisibilityChanged());
/// <summary>
/// Bindable property for IsEnabled.
/// </summary>
public static readonly BindableProperty IsEnabledProperty =
BindableProperty.Create(
nameof(IsEnabled),
typeof(bool),
typeof(SkiaView),
true,
propertyChanged: (b, o, n) => ((SkiaView)b).OnEnabledChanged());
/// <summary>
/// Bindable property for Opacity.
/// </summary>
public static readonly BindableProperty OpacityProperty =
BindableProperty.Create(
nameof(Opacity),
typeof(float),
typeof(SkiaView),
1.0f,
coerceValue: (b, v) => Math.Clamp((float)v, 0f, 1f),
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
/// <summary>
/// Bindable property for BackgroundColor.
/// </summary>
public static readonly BindableProperty BackgroundColorProperty =
BindableProperty.Create(
nameof(BackgroundColor),
typeof(SKColor),
typeof(SkiaView),
SKColors.Transparent,
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
/// <summary>
/// Bindable property for WidthRequest.
/// </summary>
public static readonly BindableProperty WidthRequestProperty =
BindableProperty.Create(
nameof(WidthRequest),
typeof(double),
typeof(SkiaView),
-1.0,
propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure());
/// <summary>
/// Bindable property for HeightRequest.
/// </summary>
public static readonly BindableProperty HeightRequestProperty =
BindableProperty.Create(
nameof(HeightRequest),
typeof(double),
typeof(SkiaView),
-1.0,
propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure());
/// <summary>
/// Bindable property for MinimumWidthRequest.
/// </summary>
public static readonly BindableProperty MinimumWidthRequestProperty =
BindableProperty.Create(
nameof(MinimumWidthRequest),
typeof(double),
typeof(SkiaView),
0.0,
propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure());
/// <summary>
/// Bindable property for MinimumHeightRequest.
/// </summary>
public static readonly BindableProperty MinimumHeightRequestProperty =
BindableProperty.Create(
nameof(MinimumHeightRequest),
typeof(double),
typeof(SkiaView),
0.0,
propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure());
/// <summary>
/// Bindable property for IsFocusable.
/// </summary>
public static readonly BindableProperty IsFocusableProperty =
BindableProperty.Create(
nameof(IsFocusable),
typeof(bool),
typeof(SkiaView),
false);
/// <summary>
/// Bindable property for Margin.
/// </summary>
public static readonly BindableProperty MarginProperty =
BindableProperty.Create(
nameof(Margin),
typeof(Thickness),
typeof(SkiaView),
default(Thickness),
propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure());
/// <summary>
/// Bindable property for HorizontalOptions.
/// </summary>
public static readonly BindableProperty HorizontalOptionsProperty =
BindableProperty.Create(
nameof(HorizontalOptions),
typeof(LayoutOptions),
typeof(SkiaView),
LayoutOptions.Fill,
propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure());
/// <summary>
/// Bindable property for VerticalOptions.
/// </summary>
public static readonly BindableProperty VerticalOptionsProperty =
BindableProperty.Create(
nameof(VerticalOptions),
typeof(LayoutOptions),
typeof(SkiaView),
LayoutOptions.Fill,
propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure());
/// <summary>
/// Bindable property for Name (used for template child lookup).
/// </summary>
public static readonly BindableProperty NameProperty =
BindableProperty.Create(
nameof(Name),
typeof(string),
typeof(SkiaView),
string.Empty);
#endregion
private bool _disposed;
private SKRect _bounds;
private SkiaView? _parent;
private readonly List<SkiaView> _children = new();
/// <summary>
/// Gets the absolute bounds of this view in screen coordinates.
/// </summary>
@@ -64,15 +248,6 @@ public abstract class SkiaView : IDisposable
return bounds;
}
private bool _disposed;
private SKRect _bounds;
private bool _isVisible = true;
private bool _isEnabled = true;
private float _opacity = 1.0f;
private SKColor _backgroundColor = SKColors.Transparent;
private SkiaView? _parent;
private readonly List<SkiaView> _children = new();
/// <summary>
/// Gets or sets the bounds of this view in parent coordinates.
/// </summary>
@@ -94,15 +269,8 @@ public abstract class SkiaView : IDisposable
/// </summary>
public bool IsVisible
{
get => _isVisible;
set
{
if (_isVisible != value)
{
_isVisible = value;
Invalidate();
}
}
get => (bool)GetValue(IsVisibleProperty);
set => SetValue(IsVisibleProperty, value);
}
/// <summary>
@@ -110,15 +278,8 @@ public abstract class SkiaView : IDisposable
/// </summary>
public bool IsEnabled
{
get => _isEnabled;
set
{
if (_isEnabled != value)
{
_isEnabled = value;
Invalidate();
}
}
get => (bool)GetValue(IsEnabledProperty);
set => SetValue(IsEnabledProperty, value);
}
/// <summary>
@@ -126,21 +287,14 @@ public abstract class SkiaView : IDisposable
/// </summary>
public float Opacity
{
get => _opacity;
set
{
var clamped = Math.Clamp(value, 0f, 1f);
if (_opacity != clamped)
{
_opacity = clamped;
Invalidate();
}
}
get => (float)GetValue(OpacityProperty);
set => SetValue(OpacityProperty, value);
}
/// <summary>
/// Gets or sets the background color.
/// </summary>
private SKColor _backgroundColor = SKColors.Transparent;
public SKColor BackgroundColor
{
get => _backgroundColor;
@@ -149,6 +303,7 @@ public abstract class SkiaView : IDisposable
if (_backgroundColor != value)
{
_backgroundColor = value;
SetValue(BackgroundColorProperty, value); // Keep BindableProperty in sync for bindings
Invalidate();
}
}
@@ -157,17 +312,101 @@ public abstract class SkiaView : IDisposable
/// <summary>
/// Gets or sets the requested width.
/// </summary>
public double RequestedWidth { get; set; } = -1;
public double WidthRequest
{
get => (double)GetValue(WidthRequestProperty);
set => SetValue(WidthRequestProperty, value);
}
/// <summary>
/// Gets or sets the requested height.
/// </summary>
public double RequestedHeight { get; set; } = -1;
public double HeightRequest
{
get => (double)GetValue(HeightRequestProperty);
set => SetValue(HeightRequestProperty, value);
}
/// <summary>
/// Gets or sets the minimum width request.
/// </summary>
public double MinimumWidthRequest
{
get => (double)GetValue(MinimumWidthRequestProperty);
set => SetValue(MinimumWidthRequestProperty, value);
}
/// <summary>
/// Gets or sets the minimum height request.
/// </summary>
public double MinimumHeightRequest
{
get => (double)GetValue(MinimumHeightRequestProperty);
set => SetValue(MinimumHeightRequestProperty, value);
}
/// <summary>
/// Gets or sets the requested width (backwards compatibility alias).
/// </summary>
public double RequestedWidth
{
get => WidthRequest;
set => WidthRequest = value;
}
/// <summary>
/// Gets or sets the requested height (backwards compatibility alias).
/// </summary>
public double RequestedHeight
{
get => HeightRequest;
set => HeightRequest = value;
}
/// <summary>
/// Gets or sets whether this view can receive keyboard focus.
/// </summary>
public bool IsFocusable { get; set; }
public bool IsFocusable
{
get => (bool)GetValue(IsFocusableProperty);
set => SetValue(IsFocusableProperty, value);
}
/// <summary>
/// Gets or sets the margin around this view.
/// </summary>
public Thickness Margin
{
get => (Thickness)GetValue(MarginProperty);
set => SetValue(MarginProperty, value);
}
/// <summary>
/// Gets or sets the horizontal layout options.
/// </summary>
public LayoutOptions HorizontalOptions
{
get => (LayoutOptions)GetValue(HorizontalOptionsProperty);
set => SetValue(HorizontalOptionsProperty, value);
}
/// <summary>
/// Gets or sets the vertical layout options.
/// </summary>
public LayoutOptions VerticalOptions
{
get => (LayoutOptions)GetValue(VerticalOptionsProperty);
set => SetValue(VerticalOptionsProperty, value);
}
/// <summary>
/// Gets or sets the name of this view (used for template child lookup).
/// </summary>
public string Name
{
get => (string)GetValue(NameProperty);
set => SetValue(NameProperty, value);
}
/// <summary>
/// Gets or sets whether this view currently has keyboard focus.
@@ -183,6 +422,34 @@ public abstract class SkiaView : IDisposable
internal set => _parent = value;
}
/// <summary>
/// Gets the bounds of this view in screen coordinates (accounting for scroll offsets).
/// </summary>
public SKRect ScreenBounds
{
get
{
var bounds = Bounds;
var parent = _parent;
// Walk up the tree and adjust for scroll offsets
while (parent != null)
{
if (parent is SkiaScrollView scrollView)
{
bounds = new SKRect(
bounds.Left - scrollView.ScrollX,
bounds.Top - scrollView.ScrollY,
bounds.Right - scrollView.ScrollX,
bounds.Bottom - scrollView.ScrollY);
}
parent = parent.Parent;
}
return bounds;
}
}
/// <summary>
/// Gets the desired size calculated during measure.
/// </summary>
@@ -198,6 +465,36 @@ public abstract class SkiaView : IDisposable
/// </summary>
public event EventHandler? Invalidated;
/// <summary>
/// Called when visibility changes.
/// </summary>
protected virtual void OnVisibilityChanged()
{
Invalidate();
}
/// <summary>
/// Called when enabled state changes.
/// </summary>
protected virtual void OnEnabledChanged()
{
Invalidate();
}
/// <summary>
/// Called when binding context changes. Propagates to children.
/// </summary>
protected override void OnBindingContextChanged()
{
base.OnBindingContextChanged();
// Propagate binding context to children
foreach (var child in _children)
{
SetInheritedBindingContext(child, BindingContext);
}
}
/// <summary>
/// Adds a child view.
/// </summary>
@@ -208,6 +505,13 @@ public abstract class SkiaView : IDisposable
child._parent = this;
_children.Add(child);
// Propagate binding context to new child
if (BindingContext != null)
{
SetInheritedBindingContext(child, BindingContext);
}
Invalidate();
}
@@ -234,6 +538,13 @@ public abstract class SkiaView : IDisposable
child._parent = this;
_children.Insert(index, child);
// Propagate binding context to new child
if (BindingContext != null)
{
SetInheritedBindingContext(child, BindingContext);
}
Invalidate();
}
@@ -275,7 +586,9 @@ public abstract class SkiaView : IDisposable
public void Draw(SKCanvas canvas)
{
if (!IsVisible || Opacity <= 0)
{
return;
}
canvas.Save();
@@ -338,8 +651,8 @@ public abstract class SkiaView : IDisposable
/// </summary>
protected virtual SKSize MeasureOverride(SKSize availableSize)
{
var width = RequestedWidth >= 0 ? (float)RequestedWidth : 0;
var height = RequestedHeight >= 0 ? (float)RequestedHeight : 0;
var width = WidthRequest >= 0 ? (float)WidthRequest : 0;
var height = HeightRequest >= 0 ? (float)HeightRequest : 0;
return new SKSize(width, height);
}
@@ -369,6 +682,7 @@ public abstract class SkiaView : IDisposable
/// <summary>
/// Performs hit testing to find the view at the given coordinates.
/// Coordinates are in absolute window space, matching how Bounds are stored.
/// </summary>
public virtual SkiaView? HitTest(float x, float y)
{
@@ -379,11 +693,10 @@ public abstract class SkiaView : IDisposable
return null;
// Check children in reverse order (top-most first)
var localX = x - Bounds.Left;
var localY = y - Bounds.Top;
// Coordinates stay in absolute space since children have absolute Bounds
for (int i = _children.Count - 1; i >= 0; i--)
{
var hit = _children[i].HitTest(localX, localY);
var hit = _children[i].HitTest(x, y);
if (hit != null)
return hit;
}