Initial commit: .NET MAUI Linux Platform
Complete Linux platform implementation for .NET MAUI with:
- 35+ Skia-rendered controls (Button, Label, Entry, CarouselView, etc.)
- Platform services (Clipboard, FilePicker, Notifications, DragDrop, etc.)
- Accessibility support (AT-SPI2, High Contrast)
- HiDPI and Input Method support
- 216 unit tests
- CI/CD workflows
- Project templates
- Documentation
🤖 Generated with Claude Code
This commit is contained in:
467
Views/SkiaDatePicker.cs
Normal file
467
Views/SkiaDatePicker.cs
Normal file
@@ -0,0 +1,467 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform;
|
||||
|
||||
/// <summary>
|
||||
/// Skia-rendered date picker control with calendar popup.
|
||||
/// </summary>
|
||||
public class SkiaDatePicker : SkiaView
|
||||
{
|
||||
private DateTime _date = DateTime.Today;
|
||||
private DateTime _minimumDate = new DateTime(1900, 1, 1);
|
||||
private DateTime _maximumDate = new DateTime(2100, 12, 31);
|
||||
private DateTime _displayMonth;
|
||||
private bool _isOpen;
|
||||
private string _format = "d";
|
||||
|
||||
// Styling
|
||||
public SKColor TextColor { get; set; } = SKColors.Black;
|
||||
public SKColor BorderColor { get; set; } = new SKColor(0xBD, 0xBD, 0xBD);
|
||||
public SKColor CalendarBackgroundColor { get; set; } = SKColors.White;
|
||||
public SKColor SelectedDayColor { get; set; } = new SKColor(0x21, 0x96, 0xF3);
|
||||
public SKColor TodayColor { get; set; } = new SKColor(0x21, 0x96, 0xF3, 0x40);
|
||||
public SKColor HeaderColor { get; set; } = new SKColor(0x21, 0x96, 0xF3);
|
||||
public SKColor DisabledDayColor { get; set; } = new SKColor(0xBD, 0xBD, 0xBD);
|
||||
public float FontSize { get; set; } = 14;
|
||||
public float CornerRadius { get; set; } = 4;
|
||||
|
||||
private const float CalendarWidth = 280;
|
||||
private const float CalendarHeight = 320;
|
||||
private const float DayCellSize = 36;
|
||||
private const float HeaderHeight = 48;
|
||||
|
||||
public DateTime Date
|
||||
{
|
||||
get => _date;
|
||||
set
|
||||
{
|
||||
var clamped = ClampDate(value);
|
||||
if (_date != clamped)
|
||||
{
|
||||
_date = clamped;
|
||||
_displayMonth = new DateTime(_date.Year, _date.Month, 1);
|
||||
DateSelected?.Invoke(this, EventArgs.Empty);
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DateTime MinimumDate
|
||||
{
|
||||
get => _minimumDate;
|
||||
set { _minimumDate = value; Invalidate(); }
|
||||
}
|
||||
|
||||
public DateTime MaximumDate
|
||||
{
|
||||
get => _maximumDate;
|
||||
set { _maximumDate = value; Invalidate(); }
|
||||
}
|
||||
|
||||
public string Format
|
||||
{
|
||||
get => _format;
|
||||
set { _format = value; Invalidate(); }
|
||||
}
|
||||
|
||||
public bool IsOpen
|
||||
{
|
||||
get => _isOpen;
|
||||
set { _isOpen = value; Invalidate(); }
|
||||
}
|
||||
|
||||
public event EventHandler? DateSelected;
|
||||
|
||||
public SkiaDatePicker()
|
||||
{
|
||||
IsFocusable = true;
|
||||
_displayMonth = new DateTime(_date.Year, _date.Month, 1);
|
||||
}
|
||||
|
||||
private DateTime ClampDate(DateTime date)
|
||||
{
|
||||
if (date < _minimumDate) return _minimumDate;
|
||||
if (date > _maximumDate) return _maximumDate;
|
||||
return date;
|
||||
}
|
||||
|
||||
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
|
||||
{
|
||||
DrawPickerButton(canvas, bounds);
|
||||
|
||||
if (_isOpen)
|
||||
{
|
||||
DrawCalendar(canvas, bounds);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawPickerButton(SKCanvas canvas, SKRect bounds)
|
||||
{
|
||||
// Draw background
|
||||
using var bgPaint = new SKPaint
|
||||
{
|
||||
Color = IsEnabled ? BackgroundColor : new SKColor(0xF5, 0xF5, 0xF5),
|
||||
Style = SKPaintStyle.Fill,
|
||||
IsAntialias = true
|
||||
};
|
||||
canvas.DrawRoundRect(new SKRoundRect(bounds, CornerRadius), bgPaint);
|
||||
|
||||
// Draw border
|
||||
using var borderPaint = new SKPaint
|
||||
{
|
||||
Color = IsFocused ? SelectedDayColor : BorderColor,
|
||||
Style = SKPaintStyle.Stroke,
|
||||
StrokeWidth = IsFocused ? 2 : 1,
|
||||
IsAntialias = true
|
||||
};
|
||||
canvas.DrawRoundRect(new SKRoundRect(bounds, CornerRadius), borderPaint);
|
||||
|
||||
// Draw date text
|
||||
using var font = new SKFont(SKTypeface.Default, FontSize);
|
||||
using var textPaint = new SKPaint(font)
|
||||
{
|
||||
Color = IsEnabled ? TextColor : TextColor.WithAlpha(128),
|
||||
IsAntialias = true
|
||||
};
|
||||
|
||||
var dateText = _date.ToString(_format);
|
||||
var textBounds = new SKRect();
|
||||
textPaint.MeasureText(dateText, ref textBounds);
|
||||
|
||||
var textX = bounds.Left + 12;
|
||||
var textY = bounds.MidY - textBounds.MidY;
|
||||
canvas.DrawText(dateText, textX, textY, textPaint);
|
||||
|
||||
// Draw calendar icon
|
||||
DrawCalendarIcon(canvas, new SKRect(bounds.Right - 36, bounds.MidY - 10, bounds.Right - 12, bounds.MidY + 10));
|
||||
}
|
||||
|
||||
private void DrawCalendarIcon(SKCanvas canvas, SKRect bounds)
|
||||
{
|
||||
using var paint = new SKPaint
|
||||
{
|
||||
Color = IsEnabled ? TextColor : TextColor.WithAlpha(128),
|
||||
Style = SKPaintStyle.Stroke,
|
||||
StrokeWidth = 1.5f,
|
||||
IsAntialias = true
|
||||
};
|
||||
|
||||
// Calendar outline
|
||||
var calRect = new SKRect(bounds.Left, bounds.Top + 3, bounds.Right, bounds.Bottom);
|
||||
canvas.DrawRoundRect(new SKRoundRect(calRect, 2), paint);
|
||||
|
||||
// Top tabs
|
||||
canvas.DrawLine(bounds.Left + 5, bounds.Top, bounds.Left + 5, bounds.Top + 5, paint);
|
||||
canvas.DrawLine(bounds.Right - 5, bounds.Top, bounds.Right - 5, bounds.Top + 5, paint);
|
||||
|
||||
// Header line
|
||||
canvas.DrawLine(bounds.Left, bounds.Top + 8, bounds.Right, bounds.Top + 8, paint);
|
||||
|
||||
// Dots for days
|
||||
paint.Style = SKPaintStyle.Fill;
|
||||
paint.StrokeWidth = 0;
|
||||
for (int row = 0; row < 2; row++)
|
||||
{
|
||||
for (int col = 0; col < 3; col++)
|
||||
{
|
||||
var dotX = bounds.Left + 4 + col * 6;
|
||||
var dotY = bounds.Top + 12 + row * 4;
|
||||
canvas.DrawCircle(dotX, dotY, 1, paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawCalendar(SKCanvas canvas, SKRect bounds)
|
||||
{
|
||||
var calendarRect = new SKRect(
|
||||
bounds.Left,
|
||||
bounds.Bottom + 4,
|
||||
bounds.Left + CalendarWidth,
|
||||
bounds.Bottom + 4 + CalendarHeight);
|
||||
|
||||
// Draw shadow
|
||||
using var shadowPaint = new SKPaint
|
||||
{
|
||||
Color = new SKColor(0, 0, 0, 40),
|
||||
MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 4),
|
||||
Style = SKPaintStyle.Fill
|
||||
};
|
||||
canvas.DrawRoundRect(new SKRoundRect(new SKRect(calendarRect.Left + 2, calendarRect.Top + 2, calendarRect.Right + 2, calendarRect.Bottom + 2), CornerRadius), shadowPaint);
|
||||
|
||||
// Draw background
|
||||
using var bgPaint = new SKPaint
|
||||
{
|
||||
Color = CalendarBackgroundColor,
|
||||
Style = SKPaintStyle.Fill,
|
||||
IsAntialias = true
|
||||
};
|
||||
canvas.DrawRoundRect(new SKRoundRect(calendarRect, CornerRadius), bgPaint);
|
||||
|
||||
// Draw border
|
||||
using var borderPaint = new SKPaint
|
||||
{
|
||||
Color = BorderColor,
|
||||
Style = SKPaintStyle.Stroke,
|
||||
StrokeWidth = 1,
|
||||
IsAntialias = true
|
||||
};
|
||||
canvas.DrawRoundRect(new SKRoundRect(calendarRect, CornerRadius), borderPaint);
|
||||
|
||||
// Draw header
|
||||
DrawCalendarHeader(canvas, new SKRect(calendarRect.Left, calendarRect.Top, calendarRect.Right, calendarRect.Top + HeaderHeight));
|
||||
|
||||
// Draw weekday headers
|
||||
DrawWeekdayHeaders(canvas, new SKRect(calendarRect.Left, calendarRect.Top + HeaderHeight, calendarRect.Right, calendarRect.Top + HeaderHeight + 30));
|
||||
|
||||
// Draw days
|
||||
DrawDays(canvas, new SKRect(calendarRect.Left, calendarRect.Top + HeaderHeight + 30, calendarRect.Right, calendarRect.Bottom));
|
||||
}
|
||||
|
||||
private void DrawCalendarHeader(SKCanvas canvas, SKRect bounds)
|
||||
{
|
||||
// Draw header background
|
||||
using var headerPaint = new SKPaint
|
||||
{
|
||||
Color = HeaderColor,
|
||||
Style = SKPaintStyle.Fill
|
||||
};
|
||||
|
||||
var headerRect = new SKRect(bounds.Left, bounds.Top, bounds.Right, bounds.Bottom);
|
||||
canvas.Save();
|
||||
canvas.ClipRoundRect(new SKRoundRect(new SKRect(bounds.Left, bounds.Top, bounds.Right, bounds.Top + CornerRadius * 2), CornerRadius));
|
||||
canvas.DrawRect(headerRect, headerPaint);
|
||||
canvas.Restore();
|
||||
canvas.DrawRect(new SKRect(bounds.Left, bounds.Top + CornerRadius, bounds.Right, bounds.Bottom), headerPaint);
|
||||
|
||||
// Draw month/year text
|
||||
using var font = new SKFont(SKTypeface.Default, 16);
|
||||
using var textPaint = new SKPaint(font)
|
||||
{
|
||||
Color = SKColors.White,
|
||||
IsAntialias = true
|
||||
};
|
||||
|
||||
var monthYear = _displayMonth.ToString("MMMM yyyy");
|
||||
var textBounds = new SKRect();
|
||||
textPaint.MeasureText(monthYear, ref textBounds);
|
||||
canvas.DrawText(monthYear, bounds.MidX - textBounds.MidX, bounds.MidY - textBounds.MidY, textPaint);
|
||||
|
||||
// Draw navigation arrows
|
||||
using var arrowPaint = new SKPaint
|
||||
{
|
||||
Color = SKColors.White,
|
||||
Style = SKPaintStyle.Stroke,
|
||||
StrokeWidth = 2,
|
||||
IsAntialias = true,
|
||||
StrokeCap = SKStrokeCap.Round
|
||||
};
|
||||
|
||||
// Left arrow
|
||||
var leftArrowX = bounds.Left + 20;
|
||||
using var leftPath = new SKPath();
|
||||
leftPath.MoveTo(leftArrowX + 6, bounds.MidY - 6);
|
||||
leftPath.LineTo(leftArrowX, bounds.MidY);
|
||||
leftPath.LineTo(leftArrowX + 6, bounds.MidY + 6);
|
||||
canvas.DrawPath(leftPath, arrowPaint);
|
||||
|
||||
// Right arrow
|
||||
var rightArrowX = bounds.Right - 20;
|
||||
using var rightPath = new SKPath();
|
||||
rightPath.MoveTo(rightArrowX - 6, bounds.MidY - 6);
|
||||
rightPath.LineTo(rightArrowX, bounds.MidY);
|
||||
rightPath.LineTo(rightArrowX - 6, bounds.MidY + 6);
|
||||
canvas.DrawPath(rightPath, arrowPaint);
|
||||
}
|
||||
|
||||
private void DrawWeekdayHeaders(SKCanvas canvas, SKRect bounds)
|
||||
{
|
||||
var dayNames = new[] { "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa" };
|
||||
var cellWidth = bounds.Width / 7;
|
||||
|
||||
using var font = new SKFont(SKTypeface.Default, 12);
|
||||
using var paint = new SKPaint(font)
|
||||
{
|
||||
Color = new SKColor(0x80, 0x80, 0x80),
|
||||
IsAntialias = true
|
||||
};
|
||||
|
||||
for (int i = 0; i < 7; i++)
|
||||
{
|
||||
var textBounds = new SKRect();
|
||||
paint.MeasureText(dayNames[i], ref textBounds);
|
||||
var x = bounds.Left + i * cellWidth + cellWidth / 2 - textBounds.MidX;
|
||||
var y = bounds.MidY - textBounds.MidY;
|
||||
canvas.DrawText(dayNames[i], x, y, paint);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawDays(SKCanvas canvas, SKRect bounds)
|
||||
{
|
||||
var firstDay = new DateTime(_displayMonth.Year, _displayMonth.Month, 1);
|
||||
var daysInMonth = DateTime.DaysInMonth(_displayMonth.Year, _displayMonth.Month);
|
||||
var startDayOfWeek = (int)firstDay.DayOfWeek;
|
||||
|
||||
var cellWidth = bounds.Width / 7;
|
||||
var cellHeight = (bounds.Height - 10) / 6;
|
||||
|
||||
using var font = new SKFont(SKTypeface.Default, 14);
|
||||
using var textPaint = new SKPaint(font) { IsAntialias = true };
|
||||
using var bgPaint = new SKPaint { Style = SKPaintStyle.Fill, IsAntialias = true };
|
||||
|
||||
var today = DateTime.Today;
|
||||
|
||||
for (int day = 1; day <= daysInMonth; day++)
|
||||
{
|
||||
var dayDate = new DateTime(_displayMonth.Year, _displayMonth.Month, day);
|
||||
var cellIndex = startDayOfWeek + day - 1;
|
||||
var row = cellIndex / 7;
|
||||
var col = cellIndex % 7;
|
||||
|
||||
var cellX = bounds.Left + col * cellWidth;
|
||||
var cellY = bounds.Top + row * cellHeight;
|
||||
var cellRect = new SKRect(cellX + 2, cellY + 2, cellX + cellWidth - 2, cellY + cellHeight - 2);
|
||||
|
||||
var isSelected = dayDate.Date == _date.Date;
|
||||
var isToday = dayDate.Date == today;
|
||||
var isDisabled = dayDate < _minimumDate || dayDate > _maximumDate;
|
||||
|
||||
// Draw day background
|
||||
if (isSelected)
|
||||
{
|
||||
bgPaint.Color = SelectedDayColor;
|
||||
canvas.DrawCircle(cellRect.MidX, cellRect.MidY, Math.Min(cellRect.Width, cellRect.Height) / 2 - 2, bgPaint);
|
||||
}
|
||||
else if (isToday)
|
||||
{
|
||||
bgPaint.Color = TodayColor;
|
||||
canvas.DrawCircle(cellRect.MidX, cellRect.MidY, Math.Min(cellRect.Width, cellRect.Height) / 2 - 2, bgPaint);
|
||||
}
|
||||
|
||||
// Draw day text
|
||||
textPaint.Color = isSelected ? SKColors.White : isDisabled ? DisabledDayColor : TextColor;
|
||||
var dayText = day.ToString();
|
||||
var textBounds = new SKRect();
|
||||
textPaint.MeasureText(dayText, ref textBounds);
|
||||
canvas.DrawText(dayText, cellRect.MidX - textBounds.MidX, cellRect.MidY - textBounds.MidY, textPaint);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnPointerPressed(PointerEventArgs e)
|
||||
{
|
||||
if (!IsEnabled) return;
|
||||
|
||||
if (_isOpen)
|
||||
{
|
||||
var calendarTop = Bounds.Bottom + 4;
|
||||
|
||||
// Check header navigation
|
||||
if (e.Y >= calendarTop && e.Y < calendarTop + HeaderHeight)
|
||||
{
|
||||
if (e.X < Bounds.Left + 40)
|
||||
{
|
||||
// Previous month
|
||||
_displayMonth = _displayMonth.AddMonths(-1);
|
||||
Invalidate();
|
||||
return;
|
||||
}
|
||||
else if (e.X > Bounds.Left + CalendarWidth - 40)
|
||||
{
|
||||
// Next month
|
||||
_displayMonth = _displayMonth.AddMonths(1);
|
||||
Invalidate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check day selection
|
||||
var daysTop = calendarTop + HeaderHeight + 30;
|
||||
if (e.Y >= daysTop && e.Y < calendarTop + CalendarHeight)
|
||||
{
|
||||
var cellWidth = CalendarWidth / 7;
|
||||
var cellHeight = (CalendarHeight - HeaderHeight - 40) / 6;
|
||||
|
||||
var col = (int)((e.X - Bounds.Left) / cellWidth);
|
||||
var row = (int)((e.Y - daysTop) / cellHeight);
|
||||
|
||||
var firstDay = new DateTime(_displayMonth.Year, _displayMonth.Month, 1);
|
||||
var startDayOfWeek = (int)firstDay.DayOfWeek;
|
||||
var dayIndex = row * 7 + col - startDayOfWeek + 1;
|
||||
var daysInMonth = DateTime.DaysInMonth(_displayMonth.Year, _displayMonth.Month);
|
||||
|
||||
if (dayIndex >= 1 && dayIndex <= daysInMonth)
|
||||
{
|
||||
var selectedDate = new DateTime(_displayMonth.Year, _displayMonth.Month, dayIndex);
|
||||
if (selectedDate >= _minimumDate && selectedDate <= _maximumDate)
|
||||
{
|
||||
Date = selectedDate;
|
||||
_isOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (e.Y < calendarTop)
|
||||
{
|
||||
_isOpen = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_isOpen = true;
|
||||
}
|
||||
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
public override void OnKeyDown(KeyEventArgs e)
|
||||
{
|
||||
if (!IsEnabled) return;
|
||||
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.Enter:
|
||||
case Key.Space:
|
||||
_isOpen = !_isOpen;
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Escape:
|
||||
if (_isOpen)
|
||||
{
|
||||
_isOpen = false;
|
||||
e.Handled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case Key.Left:
|
||||
Date = _date.AddDays(-1);
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Right:
|
||||
Date = _date.AddDays(1);
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Up:
|
||||
Date = _date.AddDays(-7);
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Down:
|
||||
Date = _date.AddDays(7);
|
||||
e.Handled = true;
|
||||
break;
|
||||
}
|
||||
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
protected override SKSize MeasureOverride(SKSize availableSize)
|
||||
{
|
||||
return new SKSize(
|
||||
availableSize.Width < float.MaxValue ? Math.Min(availableSize.Width, 200) : 200,
|
||||
40);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user