2025-12-19 09:30:16 +00:00
|
|
|
// Licensed to the .NET Foundation under one or more agreements.
|
|
|
|
|
// The .NET Foundation licenses this file to you under the MIT license.
|
|
|
|
|
|
2026-01-16 05:36:36 +00:00
|
|
|
using Microsoft.Maui.Controls;
|
2026-01-16 05:14:14 +00:00
|
|
|
using Microsoft.Maui.Graphics;
|
2025-12-21 13:26:56 -05:00
|
|
|
using Microsoft.Maui.Platform.Linux;
|
2026-01-17 03:36:37 +00:00
|
|
|
using Microsoft.Maui.Platform.Linux.Converters;
|
2026-01-16 05:14:14 +00:00
|
|
|
using SkiaSharp;
|
2025-12-19 09:30:16 +00:00
|
|
|
|
|
|
|
|
namespace Microsoft.Maui.Platform;
|
|
|
|
|
|
2026-01-16 05:14:14 +00:00
|
|
|
/// <summary>
|
|
|
|
|
/// Event args for time picker time changes.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public class TimeChangedEventArgs : EventArgs
|
|
|
|
|
{
|
|
|
|
|
public TimeSpan OldTime { get; }
|
|
|
|
|
public TimeSpan NewTime { get; }
|
|
|
|
|
|
|
|
|
|
public TimeChangedEventArgs(TimeSpan oldTime, TimeSpan newTime)
|
|
|
|
|
{
|
|
|
|
|
OldTime = oldTime;
|
|
|
|
|
NewTime = newTime;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 09:30:16 +00:00
|
|
|
/// <summary>
|
|
|
|
|
/// Skia-rendered time picker control with clock popup.
|
2026-01-16 05:14:14 +00:00
|
|
|
/// Implements MAUI ITimePicker interface patterns.
|
2025-12-19 09:30:16 +00:00
|
|
|
/// </summary>
|
|
|
|
|
public class SkiaTimePicker : SkiaView
|
|
|
|
|
{
|
2025-12-21 13:26:56 -05:00
|
|
|
#region BindableProperties
|
2025-12-19 09:30:16 +00:00
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
public static readonly BindableProperty TimeProperty =
|
2026-01-16 05:14:14 +00:00
|
|
|
BindableProperty.Create(nameof(Time), typeof(TimeSpan), typeof(SkiaTimePicker), DateTime.Now.TimeOfDay, BindingMode.TwoWay,
|
|
|
|
|
propertyChanged: (b, o, n) => ((SkiaTimePicker)b).OnTimePropertyChanged((TimeSpan)o, (TimeSpan)n));
|
2025-12-19 09:30:16 +00:00
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
public static readonly BindableProperty FormatProperty =
|
Verify Views files against decompiled, extract embedded types
Fixed files:
- SkiaImageButton.cs: Added SVG support with multi-path search
- SkiaNavigationPage.cs: Added LinuxApplication.IsGtkMode check
- SkiaRefreshView.cs: Added ICommand support (Command, CommandParameter)
- SkiaTemplatedView.cs: Added missing using statements
Extracted embedded types to separate files (matching decompiled pattern):
- From SkiaMenuBar.cs: MenuBarItem, MenuItem, SkiaMenuFlyout, MenuItemClickedEventArgs
- From SkiaNavigationPage.cs: NavigationEventArgs
- From SkiaTabbedPage.cs: TabItem
- From SkiaVisualStateManager.cs: SkiaVisualStateGroupList, SkiaVisualStateGroup, SkiaVisualState, SkiaVisualStateSetter
- From SkiaSwipeView.cs: SwipeItem, SwipeStartedEventArgs, SwipeEndedEventArgs
- From SkiaFlyoutPage.cs: FlyoutLayoutBehavior (already separate)
- From SkiaIndicatorView.cs: IndicatorShape (already separate)
- From SkiaBorder.cs: SkiaFrame
- From SkiaCarouselView.cs: PositionChangedEventArgs
- From SkiaCollectionView.cs: SkiaSelectionMode, ItemsLayoutOrientation
- From SkiaContentPresenter.cs: LayoutAlignment
Verified matching decompiled:
- SkiaContextMenu.cs, SkiaFlexLayout.cs, SkiaGraphicsView.cs
Build: 0 errors
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 17:02:39 -05:00
|
|
|
BindableProperty.Create(nameof(Format), typeof(string), typeof(SkiaTimePicker), "t", BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate());
|
|
|
|
|
|
|
|
|
|
public static readonly BindableProperty TextColorProperty =
|
2026-01-16 05:14:14 +00:00
|
|
|
BindableProperty.Create(nameof(TextColor), typeof(Color), typeof(SkiaTimePicker), Colors.Black, BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate());
|
|
|
|
|
|
|
|
|
|
public static readonly BindableProperty BorderColorProperty =
|
2026-01-16 05:14:14 +00:00
|
|
|
BindableProperty.Create(nameof(BorderColor), typeof(Color), typeof(SkiaTimePicker), Color.FromRgb(189, 189, 189), BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate());
|
|
|
|
|
|
|
|
|
|
public static readonly BindableProperty ClockBackgroundColorProperty =
|
2026-01-16 05:14:14 +00:00
|
|
|
BindableProperty.Create(nameof(ClockBackgroundColor), typeof(Color), typeof(SkiaTimePicker), Colors.White, BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate());
|
|
|
|
|
|
|
|
|
|
public static readonly BindableProperty ClockFaceColorProperty =
|
2026-01-16 05:14:14 +00:00
|
|
|
BindableProperty.Create(nameof(ClockFaceColor), typeof(Color), typeof(SkiaTimePicker), Color.FromRgb(245, 245, 245), BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate());
|
|
|
|
|
|
|
|
|
|
public static readonly BindableProperty SelectedColorProperty =
|
2026-01-16 05:14:14 +00:00
|
|
|
BindableProperty.Create(nameof(SelectedColor), typeof(Color), typeof(SkiaTimePicker), Color.FromRgb(33, 150, 243), BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate());
|
|
|
|
|
|
|
|
|
|
public static readonly BindableProperty HeaderColorProperty =
|
2026-01-16 05:14:14 +00:00
|
|
|
BindableProperty.Create(nameof(HeaderColor), typeof(Color), typeof(SkiaTimePicker), Color.FromRgb(33, 150, 243), BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate());
|
|
|
|
|
|
2026-01-16 05:36:36 +00:00
|
|
|
public static readonly BindableProperty FontFamilyProperty =
|
|
|
|
|
BindableProperty.Create(nameof(FontFamily), typeof(string), typeof(SkiaTimePicker), string.Empty, BindingMode.TwoWay,
|
|
|
|
|
propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate());
|
|
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
public static readonly BindableProperty FontSizeProperty =
|
2026-01-16 05:14:14 +00:00
|
|
|
BindableProperty.Create(nameof(FontSize), typeof(double), typeof(SkiaTimePicker), 14.0, BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaTimePicker)b).InvalidateMeasure());
|
|
|
|
|
|
2026-01-16 05:36:36 +00:00
|
|
|
public static readonly BindableProperty FontAttributesProperty =
|
|
|
|
|
BindableProperty.Create(nameof(FontAttributes), typeof(FontAttributes), typeof(SkiaTimePicker), FontAttributes.None, BindingMode.TwoWay,
|
|
|
|
|
propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate());
|
|
|
|
|
|
|
|
|
|
public static readonly BindableProperty CharacterSpacingProperty =
|
|
|
|
|
BindableProperty.Create(nameof(CharacterSpacing), typeof(double), typeof(SkiaTimePicker), 0.0, BindingMode.TwoWay,
|
|
|
|
|
propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate());
|
|
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
public static readonly BindableProperty CornerRadiusProperty =
|
2026-01-16 05:14:14 +00:00
|
|
|
BindableProperty.Create(nameof(CornerRadius), typeof(double), typeof(SkiaTimePicker), 4.0, BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate());
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Properties
|
2025-12-19 09:30:16 +00:00
|
|
|
|
|
|
|
|
public TimeSpan Time
|
|
|
|
|
{
|
2025-12-21 13:26:56 -05:00
|
|
|
get => (TimeSpan)GetValue(TimeProperty);
|
|
|
|
|
set => SetValue(TimeProperty, value);
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public string Format
|
|
|
|
|
{
|
2025-12-21 13:26:56 -05:00
|
|
|
get => (string)GetValue(FormatProperty);
|
|
|
|
|
set => SetValue(FormatProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:14:14 +00:00
|
|
|
public Color TextColor
|
2025-12-21 13:26:56 -05:00
|
|
|
{
|
2026-01-16 05:14:14 +00:00
|
|
|
get => (Color)GetValue(TextColorProperty);
|
2025-12-21 13:26:56 -05:00
|
|
|
set => SetValue(TextColorProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:14:14 +00:00
|
|
|
public Color BorderColor
|
2025-12-21 13:26:56 -05:00
|
|
|
{
|
2026-01-16 05:14:14 +00:00
|
|
|
get => (Color)GetValue(BorderColorProperty);
|
2025-12-21 13:26:56 -05:00
|
|
|
set => SetValue(BorderColorProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:14:14 +00:00
|
|
|
public Color ClockBackgroundColor
|
2025-12-21 13:26:56 -05:00
|
|
|
{
|
2026-01-16 05:14:14 +00:00
|
|
|
get => (Color)GetValue(ClockBackgroundColorProperty);
|
2025-12-21 13:26:56 -05:00
|
|
|
set => SetValue(ClockBackgroundColorProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:14:14 +00:00
|
|
|
public Color ClockFaceColor
|
2025-12-21 13:26:56 -05:00
|
|
|
{
|
2026-01-16 05:14:14 +00:00
|
|
|
get => (Color)GetValue(ClockFaceColorProperty);
|
2025-12-21 13:26:56 -05:00
|
|
|
set => SetValue(ClockFaceColorProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:14:14 +00:00
|
|
|
public Color SelectedColor
|
2025-12-21 13:26:56 -05:00
|
|
|
{
|
2026-01-16 05:14:14 +00:00
|
|
|
get => (Color)GetValue(SelectedColorProperty);
|
2025-12-21 13:26:56 -05:00
|
|
|
set => SetValue(SelectedColorProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:14:14 +00:00
|
|
|
public Color HeaderColor
|
2025-12-21 13:26:56 -05:00
|
|
|
{
|
2026-01-16 05:14:14 +00:00
|
|
|
get => (Color)GetValue(HeaderColorProperty);
|
2025-12-21 13:26:56 -05:00
|
|
|
set => SetValue(HeaderColorProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:36:36 +00:00
|
|
|
public string FontFamily
|
|
|
|
|
{
|
|
|
|
|
get => (string)GetValue(FontFamilyProperty);
|
|
|
|
|
set => SetValue(FontFamilyProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:14:14 +00:00
|
|
|
public double FontSize
|
2025-12-21 13:26:56 -05:00
|
|
|
{
|
2026-01-16 05:14:14 +00:00
|
|
|
get => (double)GetValue(FontSizeProperty);
|
2025-12-21 13:26:56 -05:00
|
|
|
set => SetValue(FontSizeProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:36:36 +00:00
|
|
|
public FontAttributes FontAttributes
|
|
|
|
|
{
|
|
|
|
|
get => (FontAttributes)GetValue(FontAttributesProperty);
|
|
|
|
|
set => SetValue(FontAttributesProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public double CharacterSpacing
|
|
|
|
|
{
|
|
|
|
|
get => (double)GetValue(CharacterSpacingProperty);
|
|
|
|
|
set => SetValue(CharacterSpacingProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:14:14 +00:00
|
|
|
public double CornerRadius
|
2025-12-21 13:26:56 -05:00
|
|
|
{
|
2026-01-16 05:14:14 +00:00
|
|
|
get => (double)GetValue(CornerRadiusProperty);
|
2025-12-21 13:26:56 -05:00
|
|
|
set => SetValue(CornerRadiusProperty, value);
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public bool IsOpen
|
|
|
|
|
{
|
|
|
|
|
get => _isOpen;
|
2025-12-21 13:26:56 -05:00
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
if (_isOpen != value)
|
|
|
|
|
{
|
|
|
|
|
_isOpen = value;
|
|
|
|
|
if (_isOpen)
|
|
|
|
|
RegisterPopupOverlay(this, DrawClockOverlay);
|
|
|
|
|
else
|
|
|
|
|
UnregisterPopupOverlay(this);
|
|
|
|
|
Invalidate();
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
#endregion
|
|
|
|
|
|
2026-01-16 05:14:14 +00:00
|
|
|
#region Fields
|
|
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
private bool _isOpen;
|
|
|
|
|
private int _selectedHour;
|
|
|
|
|
private int _selectedMinute;
|
|
|
|
|
private bool _isSelectingHours = true;
|
|
|
|
|
|
|
|
|
|
private const float ClockSize = 280;
|
|
|
|
|
private const float ClockRadius = 100;
|
|
|
|
|
private const float HeaderHeight = 80;
|
|
|
|
|
private const float PopupHeight = ClockSize + HeaderHeight;
|
|
|
|
|
|
2026-01-16 05:14:14 +00:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Events
|
|
|
|
|
|
|
|
|
|
public event EventHandler<TimeChangedEventArgs>? TimeSelected;
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Constructor
|
|
|
|
|
|
|
|
|
|
public SkiaTimePicker()
|
|
|
|
|
{
|
|
|
|
|
IsFocusable = true;
|
|
|
|
|
_selectedHour = DateTime.Now.Hour;
|
|
|
|
|
_selectedMinute = DateTime.Now.Minute;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Helper Methods
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Converts a MAUI Color to SkiaSharp SKColor.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private static SKColor ToSKColor(Color? color)
|
|
|
|
|
{
|
|
|
|
|
if (color == null) return SKColors.Transparent;
|
2026-01-17 03:36:37 +00:00
|
|
|
return color.ToSKColor();
|
2026-01-16 05:14:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Converts a MAUI Color to SKColor with modified alpha.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private static SKColor ToSKColorWithAlpha(Color? color, byte alpha)
|
|
|
|
|
{
|
|
|
|
|
if (color == null) return SKColors.Transparent;
|
2026-01-17 03:36:37 +00:00
|
|
|
return color.ToSKColor().WithAlpha(alpha);
|
2026-01-16 05:14:14 +00:00
|
|
|
}
|
2025-12-19 09:30:16 +00:00
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
/// <summary>
|
|
|
|
|
/// Gets the clock popup rectangle with edge detection applied.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private SKRect GetPopupRect(SKRect pickerBounds)
|
|
|
|
|
{
|
2026-01-16 05:14:14 +00:00
|
|
|
float cornerRadius = (float)CornerRadius;
|
2025-12-21 13:26:56 -05:00
|
|
|
var windowWidth = LinuxApplication.Current?.MainWindow?.Width ?? 800;
|
|
|
|
|
var windowHeight = LinuxApplication.Current?.MainWindow?.Height ?? 600;
|
|
|
|
|
|
|
|
|
|
var popupLeft = pickerBounds.Left;
|
|
|
|
|
var popupTop = pickerBounds.Bottom + 4;
|
|
|
|
|
|
|
|
|
|
if (popupLeft + ClockSize > windowWidth)
|
|
|
|
|
{
|
|
|
|
|
popupLeft = windowWidth - ClockSize - 4;
|
|
|
|
|
}
|
|
|
|
|
if (popupLeft < 0) popupLeft = 4;
|
|
|
|
|
|
|
|
|
|
if (popupTop + PopupHeight > windowHeight)
|
|
|
|
|
{
|
|
|
|
|
popupTop = pickerBounds.Top - PopupHeight - 4;
|
|
|
|
|
}
|
|
|
|
|
if (popupTop < 0) popupTop = 4;
|
|
|
|
|
|
|
|
|
|
return new SKRect(popupLeft, popupTop, popupLeft + ClockSize, popupTop + PopupHeight);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:14:14 +00:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Private Methods
|
2025-12-21 13:26:56 -05:00
|
|
|
|
2026-01-16 05:14:14 +00:00
|
|
|
private void OnTimePropertyChanged(TimeSpan oldValue, TimeSpan newValue)
|
2025-12-21 13:26:56 -05:00
|
|
|
{
|
2026-01-16 05:14:14 +00:00
|
|
|
_selectedHour = newValue.Hours;
|
|
|
|
|
_selectedMinute = newValue.Minutes;
|
|
|
|
|
TimeSelected?.Invoke(this, new TimeChangedEventArgs(oldValue, newValue));
|
2025-12-21 13:26:56 -05:00
|
|
|
Invalidate();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void DrawClockOverlay(SKCanvas canvas)
|
|
|
|
|
{
|
|
|
|
|
if (!_isOpen) return;
|
2026-01-17 05:22:37 +00:00
|
|
|
var sb = ScreenBounds;
|
|
|
|
|
DrawClockPopup(canvas, new SKRect((float)sb.Left, (float)sb.Top, (float)sb.Right, (float)sb.Bottom));
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void DrawPickerButton(SKCanvas canvas, SKRect bounds)
|
|
|
|
|
{
|
2026-01-16 05:14:14 +00:00
|
|
|
float cornerRadius = (float)CornerRadius;
|
|
|
|
|
float fontSize = (float)FontSize;
|
|
|
|
|
SKColor textColor = ToSKColor(TextColor);
|
|
|
|
|
SKColor borderColor = ToSKColor(BorderColor);
|
|
|
|
|
SKColor selectedColor = ToSKColor(SelectedColor);
|
|
|
|
|
|
2025-12-19 09:30:16 +00:00
|
|
|
using var bgPaint = new SKPaint
|
|
|
|
|
{
|
2026-01-17 03:36:37 +00:00
|
|
|
Color = IsEnabled ? GetEffectiveBackgroundColor() : SkiaTheme.Gray100SK,
|
2025-12-19 09:30:16 +00:00
|
|
|
Style = SKPaintStyle.Fill,
|
|
|
|
|
IsAntialias = true
|
|
|
|
|
};
|
2026-01-16 05:14:14 +00:00
|
|
|
canvas.DrawRoundRect(new SKRoundRect(bounds, cornerRadius), bgPaint);
|
2025-12-19 09:30:16 +00:00
|
|
|
|
|
|
|
|
using var borderPaint = new SKPaint
|
|
|
|
|
{
|
2026-01-16 05:14:14 +00:00
|
|
|
Color = IsFocused ? selectedColor : borderColor,
|
2025-12-19 09:30:16 +00:00
|
|
|
Style = SKPaintStyle.Stroke,
|
|
|
|
|
StrokeWidth = IsFocused ? 2 : 1,
|
|
|
|
|
IsAntialias = true
|
|
|
|
|
};
|
2026-01-16 05:14:14 +00:00
|
|
|
canvas.DrawRoundRect(new SKRoundRect(bounds, cornerRadius), borderPaint);
|
2025-12-19 09:30:16 +00:00
|
|
|
|
2026-01-16 05:36:36 +00:00
|
|
|
// Get typeface based on FontFamily and FontAttributes
|
|
|
|
|
SKTypeface typeface = SKTypeface.Default;
|
|
|
|
|
if (!string.IsNullOrEmpty(FontFamily))
|
|
|
|
|
{
|
|
|
|
|
var style = FontAttributes switch
|
|
|
|
|
{
|
|
|
|
|
FontAttributes.Bold => SKFontStyle.Bold,
|
|
|
|
|
FontAttributes.Italic => SKFontStyle.Italic,
|
|
|
|
|
FontAttributes.Bold | FontAttributes.Italic => SKFontStyle.BoldItalic,
|
|
|
|
|
_ => SKFontStyle.Normal
|
|
|
|
|
};
|
|
|
|
|
typeface = SKTypeface.FromFamilyName(FontFamily, style) ?? SKTypeface.Default;
|
|
|
|
|
}
|
|
|
|
|
else if (FontAttributes != FontAttributes.None)
|
|
|
|
|
{
|
|
|
|
|
var style = FontAttributes switch
|
|
|
|
|
{
|
|
|
|
|
FontAttributes.Bold => SKFontStyle.Bold,
|
|
|
|
|
FontAttributes.Italic => SKFontStyle.Italic,
|
|
|
|
|
FontAttributes.Bold | FontAttributes.Italic => SKFontStyle.BoldItalic,
|
|
|
|
|
_ => SKFontStyle.Normal
|
|
|
|
|
};
|
|
|
|
|
typeface = SKTypeface.FromFamilyName(null, style) ?? SKTypeface.Default;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
using var font = new SKFont(typeface, fontSize);
|
2025-12-19 09:30:16 +00:00
|
|
|
using var textPaint = new SKPaint(font)
|
|
|
|
|
{
|
2026-01-16 05:14:14 +00:00
|
|
|
Color = IsEnabled ? textColor : textColor.WithAlpha(128),
|
2025-12-19 09:30:16 +00:00
|
|
|
IsAntialias = true
|
|
|
|
|
};
|
2025-12-21 13:26:56 -05:00
|
|
|
var timeText = DateTime.Today.Add(Time).ToString(Format);
|
2025-12-19 09:30:16 +00:00
|
|
|
var textBounds = new SKRect();
|
|
|
|
|
textPaint.MeasureText(timeText, ref textBounds);
|
2025-12-21 13:26:56 -05:00
|
|
|
canvas.DrawText(timeText, bounds.Left + 12, bounds.MidY - textBounds.MidY, textPaint);
|
2025-12-19 09:30:16 +00:00
|
|
|
|
|
|
|
|
DrawClockIcon(canvas, new SKRect(bounds.Right - 36, bounds.MidY - 10, bounds.Right - 12, bounds.MidY + 10));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void DrawClockIcon(SKCanvas canvas, SKRect bounds)
|
|
|
|
|
{
|
2026-01-16 05:14:14 +00:00
|
|
|
SKColor textColor = ToSKColor(TextColor);
|
2025-12-19 09:30:16 +00:00
|
|
|
using var paint = new SKPaint
|
|
|
|
|
{
|
2026-01-16 05:14:14 +00:00
|
|
|
Color = IsEnabled ? textColor : textColor.WithAlpha(128),
|
2025-12-19 09:30:16 +00:00
|
|
|
Style = SKPaintStyle.Stroke,
|
|
|
|
|
StrokeWidth = 1.5f,
|
|
|
|
|
IsAntialias = true
|
|
|
|
|
};
|
|
|
|
|
var radius = Math.Min(bounds.Width, bounds.Height) / 2 - 2;
|
2025-12-21 13:26:56 -05:00
|
|
|
canvas.DrawCircle(bounds.MidX, bounds.MidY, radius, paint);
|
|
|
|
|
canvas.DrawLine(bounds.MidX, bounds.MidY, bounds.MidX, bounds.MidY - radius * 0.5f, paint);
|
|
|
|
|
canvas.DrawLine(bounds.MidX, bounds.MidY, bounds.MidX + radius * 0.4f, bounds.MidY, paint);
|
2025-12-19 09:30:16 +00:00
|
|
|
paint.Style = SKPaintStyle.Fill;
|
2025-12-21 13:26:56 -05:00
|
|
|
canvas.DrawCircle(bounds.MidX, bounds.MidY, 1.5f, paint);
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void DrawClockPopup(SKCanvas canvas, SKRect bounds)
|
|
|
|
|
{
|
2026-01-16 05:14:14 +00:00
|
|
|
float cornerRadius = (float)CornerRadius;
|
2025-12-21 13:26:56 -05:00
|
|
|
var popupRect = GetPopupRect(bounds);
|
|
|
|
|
|
2026-01-17 03:36:37 +00:00
|
|
|
using var shadowPaint = new SKPaint { Color = SkiaTheme.Shadow25SK, MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 4), Style = SKPaintStyle.Fill };
|
2026-01-16 05:14:14 +00:00
|
|
|
canvas.DrawRoundRect(new SKRoundRect(new SKRect(popupRect.Left + 2, popupRect.Top + 2, popupRect.Right + 2, popupRect.Bottom + 2), cornerRadius), shadowPaint);
|
2025-12-19 09:30:16 +00:00
|
|
|
|
2026-01-16 05:14:14 +00:00
|
|
|
using var bgPaint = new SKPaint { Color = ToSKColor(ClockBackgroundColor), Style = SKPaintStyle.Fill, IsAntialias = true };
|
|
|
|
|
canvas.DrawRoundRect(new SKRoundRect(popupRect, cornerRadius), bgPaint);
|
2025-12-19 09:30:16 +00:00
|
|
|
|
2026-01-16 05:14:14 +00:00
|
|
|
using var borderPaint = new SKPaint { Color = ToSKColor(BorderColor), Style = SKPaintStyle.Stroke, StrokeWidth = 1, IsAntialias = true };
|
|
|
|
|
canvas.DrawRoundRect(new SKRoundRect(popupRect, cornerRadius), borderPaint);
|
2025-12-19 09:30:16 +00:00
|
|
|
|
|
|
|
|
DrawTimeHeader(canvas, new SKRect(popupRect.Left, popupRect.Top, popupRect.Right, popupRect.Top + HeaderHeight));
|
|
|
|
|
DrawClockFace(canvas, new SKRect(popupRect.Left, popupRect.Top + HeaderHeight, popupRect.Right, popupRect.Bottom));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void DrawTimeHeader(SKCanvas canvas, SKRect bounds)
|
|
|
|
|
{
|
2026-01-16 05:14:14 +00:00
|
|
|
float cornerRadius = (float)CornerRadius;
|
|
|
|
|
using var headerPaint = new SKPaint { Color = ToSKColor(HeaderColor), Style = SKPaintStyle.Fill };
|
2025-12-19 09:30:16 +00:00
|
|
|
canvas.Save();
|
2026-01-16 05:14:14 +00:00
|
|
|
canvas.ClipRoundRect(new SKRoundRect(new SKRect(bounds.Left, bounds.Top, bounds.Right, bounds.Top + cornerRadius * 2), cornerRadius));
|
2025-12-19 09:30:16 +00:00
|
|
|
canvas.DrawRect(bounds, headerPaint);
|
|
|
|
|
canvas.Restore();
|
2026-01-16 05:14:14 +00:00
|
|
|
canvas.DrawRect(new SKRect(bounds.Left, bounds.Top + cornerRadius, bounds.Right, bounds.Bottom), headerPaint);
|
2025-12-19 09:30:16 +00:00
|
|
|
|
|
|
|
|
using var font = new SKFont(SKTypeface.Default, 32);
|
2026-01-17 03:36:37 +00:00
|
|
|
using var selectedPaint = new SKPaint(font) { Color = SkiaTheme.BackgroundWhiteSK, IsAntialias = true };
|
|
|
|
|
using var unselectedPaint = new SKPaint(font) { Color = SkiaTheme.WhiteSemiTransparentSK, IsAntialias = true };
|
2025-12-19 09:30:16 +00:00
|
|
|
|
|
|
|
|
var hourText = _selectedHour.ToString("D2");
|
|
|
|
|
var minuteText = _selectedMinute.ToString("D2");
|
|
|
|
|
var hourPaint = _isSelectingHours ? selectedPaint : unselectedPaint;
|
|
|
|
|
var minutePaint = _isSelectingHours ? unselectedPaint : selectedPaint;
|
|
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
var hourBounds = new SKRect(); var colonBounds = new SKRect(); var minuteBounds = new SKRect();
|
2025-12-19 09:30:16 +00:00
|
|
|
hourPaint.MeasureText(hourText, ref hourBounds);
|
2025-12-21 13:26:56 -05:00
|
|
|
selectedPaint.MeasureText(":", ref colonBounds);
|
2025-12-19 09:30:16 +00:00
|
|
|
minutePaint.MeasureText(minuteText, ref minuteBounds);
|
|
|
|
|
|
|
|
|
|
var totalWidth = hourBounds.Width + colonBounds.Width + minuteBounds.Width + 8;
|
|
|
|
|
var startX = bounds.MidX - totalWidth / 2;
|
|
|
|
|
var centerY = bounds.MidY - hourBounds.MidY;
|
|
|
|
|
|
|
|
|
|
canvas.DrawText(hourText, startX, centerY, hourPaint);
|
2025-12-21 13:26:56 -05:00
|
|
|
canvas.DrawText(":", startX + hourBounds.Width + 4, centerY, selectedPaint);
|
2025-12-19 09:30:16 +00:00
|
|
|
canvas.DrawText(minuteText, startX + hourBounds.Width + colonBounds.Width + 8, centerY, minutePaint);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void DrawClockFace(SKCanvas canvas, SKRect bounds)
|
|
|
|
|
{
|
|
|
|
|
var centerX = bounds.MidX;
|
|
|
|
|
var centerY = bounds.MidY;
|
|
|
|
|
|
2026-01-16 05:14:14 +00:00
|
|
|
SKColor textColor = ToSKColor(TextColor);
|
|
|
|
|
SKColor clockFaceColor = ToSKColor(ClockFaceColor);
|
|
|
|
|
SKColor selectedColor = ToSKColor(SelectedColor);
|
|
|
|
|
|
|
|
|
|
using var facePaint = new SKPaint { Color = clockFaceColor, Style = SKPaintStyle.Fill, IsAntialias = true };
|
2025-12-19 09:30:16 +00:00
|
|
|
canvas.DrawCircle(centerX, centerY, ClockRadius + 20, facePaint);
|
|
|
|
|
|
|
|
|
|
using var font = new SKFont(SKTypeface.Default, 14);
|
2026-01-16 05:14:14 +00:00
|
|
|
using var textPaint = new SKPaint(font) { Color = textColor, IsAntialias = true };
|
2025-12-19 09:30:16 +00:00
|
|
|
|
|
|
|
|
if (_isSelectingHours)
|
|
|
|
|
{
|
|
|
|
|
for (int i = 1; i <= 12; i++)
|
|
|
|
|
{
|
|
|
|
|
var angle = (i * 30 - 90) * Math.PI / 180;
|
|
|
|
|
var x = centerX + (float)(ClockRadius * Math.Cos(angle));
|
|
|
|
|
var y = centerY + (float)(ClockRadius * Math.Sin(angle));
|
|
|
|
|
var isSelected = (_selectedHour % 12 == i % 12);
|
|
|
|
|
if (isSelected)
|
|
|
|
|
{
|
2026-01-16 05:14:14 +00:00
|
|
|
using var selBgPaint = new SKPaint { Color = selectedColor, Style = SKPaintStyle.Fill, IsAntialias = true };
|
2025-12-21 13:26:56 -05:00
|
|
|
canvas.DrawCircle(x, y, 18, selBgPaint);
|
2026-01-17 03:36:37 +00:00
|
|
|
textPaint.Color = SkiaTheme.BackgroundWhiteSK;
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
2026-01-16 05:14:14 +00:00
|
|
|
else textPaint.Color = textColor;
|
|
|
|
|
var tBounds = new SKRect();
|
|
|
|
|
textPaint.MeasureText(i.ToString(), ref tBounds);
|
|
|
|
|
canvas.DrawText(i.ToString(), x - tBounds.MidX, y - tBounds.MidY, textPaint);
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
2026-01-16 05:14:14 +00:00
|
|
|
DrawClockHand(canvas, centerX, centerY, (_selectedHour % 12) * 30 - 90, ClockRadius - 18, selectedColor);
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
for (int i = 0; i < 12; i++)
|
|
|
|
|
{
|
|
|
|
|
var minute = i * 5;
|
|
|
|
|
var angle = (minute * 6 - 90) * Math.PI / 180;
|
|
|
|
|
var x = centerX + (float)(ClockRadius * Math.Cos(angle));
|
|
|
|
|
var y = centerY + (float)(ClockRadius * Math.Sin(angle));
|
|
|
|
|
var isSelected = (_selectedMinute / 5 == i);
|
|
|
|
|
if (isSelected)
|
|
|
|
|
{
|
2026-01-16 05:14:14 +00:00
|
|
|
using var selBgPaint = new SKPaint { Color = selectedColor, Style = SKPaintStyle.Fill, IsAntialias = true };
|
2025-12-21 13:26:56 -05:00
|
|
|
canvas.DrawCircle(x, y, 18, selBgPaint);
|
2026-01-17 03:36:37 +00:00
|
|
|
textPaint.Color = SkiaTheme.BackgroundWhiteSK;
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
2026-01-16 05:14:14 +00:00
|
|
|
else textPaint.Color = textColor;
|
|
|
|
|
var tBounds = new SKRect();
|
|
|
|
|
textPaint.MeasureText(minute.ToString("D2"), ref tBounds);
|
|
|
|
|
canvas.DrawText(minute.ToString("D2"), x - tBounds.MidX, y - tBounds.MidY, textPaint);
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
2026-01-16 05:14:14 +00:00
|
|
|
DrawClockHand(canvas, centerX, centerY, _selectedMinute * 6 - 90, ClockRadius - 18, selectedColor);
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:14:14 +00:00
|
|
|
private void DrawClockHand(SKCanvas canvas, float centerX, float centerY, float angleDegrees, float length, SKColor color)
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
|
|
|
|
var angle = angleDegrees * Math.PI / 180;
|
2026-01-16 05:14:14 +00:00
|
|
|
using var handPaint = new SKPaint { Color = color, Style = SKPaintStyle.Stroke, StrokeWidth = 2, IsAntialias = true };
|
2025-12-21 13:26:56 -05:00
|
|
|
canvas.DrawLine(centerX, centerY, centerX + (float)(length * Math.Cos(angle)), centerY + (float)(length * Math.Sin(angle)), handPaint);
|
2025-12-19 09:30:16 +00:00
|
|
|
handPaint.Style = SKPaintStyle.Fill;
|
|
|
|
|
canvas.DrawCircle(centerX, centerY, 6, handPaint);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:14:14 +00:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Overrides
|
|
|
|
|
|
|
|
|
|
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
|
|
|
|
|
{
|
|
|
|
|
DrawPickerButton(canvas, bounds);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 09:30:16 +00:00
|
|
|
public override void OnPointerPressed(PointerEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (!IsEnabled) return;
|
|
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
if (IsOpen)
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
2025-12-21 13:26:56 -05:00
|
|
|
var screenBounds = ScreenBounds;
|
2026-01-17 05:22:37 +00:00
|
|
|
var popupRect = GetPopupRect(new SKRect((float)screenBounds.Left, (float)screenBounds.Top, (float)screenBounds.Right, (float)screenBounds.Bottom));
|
2025-12-19 09:30:16 +00:00
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
var headerRect = new SKRect(popupRect.Left, popupRect.Top, popupRect.Right, popupRect.Top + HeaderHeight);
|
|
|
|
|
if (headerRect.Contains(e.X, e.Y))
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
2025-12-21 13:26:56 -05:00
|
|
|
_isSelectingHours = e.X < popupRect.Left + ClockSize / 2;
|
2025-12-19 09:30:16 +00:00
|
|
|
Invalidate();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
var clockCenterX = popupRect.Left + ClockSize / 2;
|
|
|
|
|
var clockCenterY = popupRect.Top + HeaderHeight + ClockSize / 2;
|
2025-12-19 09:30:16 +00:00
|
|
|
var dx = e.X - clockCenterX;
|
|
|
|
|
var dy = e.Y - clockCenterY;
|
|
|
|
|
var distance = Math.Sqrt(dx * dx + dy * dy);
|
|
|
|
|
|
|
|
|
|
if (distance <= ClockRadius + 20)
|
|
|
|
|
{
|
|
|
|
|
var angle = Math.Atan2(dy, dx) * 180 / Math.PI + 90;
|
|
|
|
|
if (angle < 0) angle += 360;
|
|
|
|
|
|
|
|
|
|
if (_isSelectingHours)
|
|
|
|
|
{
|
|
|
|
|
_selectedHour = ((int)Math.Round(angle / 30) % 12);
|
|
|
|
|
if (_selectedHour == 0) _selectedHour = 12;
|
2025-12-21 13:26:56 -05:00
|
|
|
if (Time.Hours >= 12 && _selectedHour != 12) _selectedHour += 12;
|
|
|
|
|
else if (Time.Hours < 12 && _selectedHour == 12) _selectedHour = 0;
|
|
|
|
|
_isSelectingHours = false;
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
_selectedMinute = ((int)Math.Round(angle / 6) % 60);
|
|
|
|
|
Time = new TimeSpan(_selectedHour, _selectedMinute, 0);
|
2025-12-21 13:26:56 -05:00
|
|
|
IsOpen = false;
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
Invalidate();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
if (screenBounds.Contains(e.X, e.Y))
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
2025-12-21 13:26:56 -05:00
|
|
|
IsOpen = false;
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2025-12-21 13:26:56 -05:00
|
|
|
IsOpen = true;
|
2025-12-19 09:30:16 +00:00
|
|
|
_isSelectingHours = true;
|
|
|
|
|
}
|
|
|
|
|
Invalidate();
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
public override void OnFocusLost()
|
|
|
|
|
{
|
|
|
|
|
base.OnFocusLost();
|
|
|
|
|
if (IsOpen)
|
|
|
|
|
{
|
|
|
|
|
IsOpen = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 09:30:16 +00:00
|
|
|
public override void OnKeyDown(KeyEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (!IsEnabled) return;
|
|
|
|
|
|
|
|
|
|
switch (e.Key)
|
|
|
|
|
{
|
2026-01-16 05:14:14 +00:00
|
|
|
case Key.Enter:
|
|
|
|
|
case Key.Space:
|
|
|
|
|
if (IsOpen)
|
|
|
|
|
{
|
|
|
|
|
if (_isSelectingHours) _isSelectingHours = false;
|
|
|
|
|
else { Time = new TimeSpan(_selectedHour, _selectedMinute, 0); IsOpen = false; }
|
|
|
|
|
}
|
2025-12-21 13:26:56 -05:00
|
|
|
else { IsOpen = true; _isSelectingHours = true; }
|
2026-01-16 05:14:14 +00:00
|
|
|
e.Handled = true;
|
|
|
|
|
break;
|
|
|
|
|
case Key.Escape:
|
|
|
|
|
if (IsOpen) { IsOpen = false; e.Handled = true; }
|
|
|
|
|
break;
|
|
|
|
|
case Key.Up:
|
|
|
|
|
if (_isSelectingHours) _selectedHour = (_selectedHour + 1) % 24;
|
|
|
|
|
else _selectedMinute = (_selectedMinute + 1) % 60;
|
|
|
|
|
e.Handled = true;
|
|
|
|
|
break;
|
|
|
|
|
case Key.Down:
|
|
|
|
|
if (_isSelectingHours) _selectedHour = (_selectedHour - 1 + 24) % 24;
|
|
|
|
|
else _selectedMinute = (_selectedMinute - 1 + 60) % 60;
|
|
|
|
|
e.Handled = true;
|
|
|
|
|
break;
|
|
|
|
|
case Key.Left:
|
|
|
|
|
case Key.Right:
|
|
|
|
|
_isSelectingHours = !_isSelectingHours;
|
|
|
|
|
e.Handled = true;
|
|
|
|
|
break;
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
Invalidate();
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-17 05:22:37 +00:00
|
|
|
protected override Size MeasureOverride(Size availableSize)
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
2026-01-17 05:22:37 +00:00
|
|
|
return new Size(availableSize.Width < double.MaxValue ? Math.Min(availableSize.Width, 200) : 200, 40);
|
2025-12-21 13:26:56 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override bool HitTestPopupArea(float x, float y)
|
|
|
|
|
{
|
|
|
|
|
var screenBounds = ScreenBounds;
|
|
|
|
|
if (screenBounds.Contains(x, y))
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
if (_isOpen)
|
|
|
|
|
{
|
2026-01-17 05:22:37 +00:00
|
|
|
var popupRect = GetPopupRect(new SKRect((float)screenBounds.Left, (float)screenBounds.Top, (float)screenBounds.Right, (float)screenBounds.Bottom));
|
2025-12-21 13:26:56 -05:00
|
|
|
return popupRect.Contains(x, y);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
2026-01-16 05:14:14 +00:00
|
|
|
|
|
|
|
|
#endregion
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|