Range thing.
This commit is contained in:
@@ -1,381 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.WinUI;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Wino.Calendar.Args;
|
||||
using Wino.Calendar.ViewModels.Data;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
using Wino.Helpers;
|
||||
|
||||
namespace Wino.Calendar.Controls;
|
||||
|
||||
public partial class WinoCalendarControl : Control, IDisposable
|
||||
{
|
||||
private const string PART_WinoFlipView = nameof(PART_WinoFlipView);
|
||||
private const string PART_IdleGrid = nameof(PART_IdleGrid);
|
||||
|
||||
public event EventHandler<TimelineCellSelectedArgs>? TimelineCellSelected;
|
||||
public event EventHandler<TimelineCellUnselectedArgs>? TimelineCellUnselected;
|
||||
|
||||
public event EventHandler? ScrollPositionChanging;
|
||||
|
||||
#region Dependency Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the collection of day ranges to render.
|
||||
/// Each day range usually represents a week, but it may support other ranges.
|
||||
/// </summary>
|
||||
[GeneratedDependencyProperty]
|
||||
public partial ObservableCollection<DayRangeRenderModel>? DayRanges { get; set; }
|
||||
|
||||
[GeneratedDependencyProperty(DefaultValue = -1)]
|
||||
public partial int SelectedFlipViewIndex { get; set; }
|
||||
|
||||
[GeneratedDependencyProperty]
|
||||
public partial DayRangeRenderModel? SelectedFlipViewDayRange { get; set; }
|
||||
|
||||
[GeneratedDependencyProperty]
|
||||
public partial WinoDayTimelineCanvas? ActiveCanvas { get; set; }
|
||||
|
||||
[GeneratedDependencyProperty(DefaultValue = true)]
|
||||
public partial bool IsFlipIdle { get; set; }
|
||||
|
||||
[GeneratedDependencyProperty]
|
||||
public partial ScrollViewer? ActiveScrollViewer { get; set; }
|
||||
|
||||
[GeneratedDependencyProperty]
|
||||
public partial ItemsPanelTemplate? VerticalItemsPanelTemplate { get; set; }
|
||||
|
||||
[GeneratedDependencyProperty]
|
||||
public partial ItemsPanelTemplate? HorizontalItemsPanelTemplate { get; set; }
|
||||
|
||||
[GeneratedDependencyProperty(DefaultValue = CalendarOrientation.Horizontal)]
|
||||
public partial CalendarOrientation Orientation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the day-week-month-year display type.
|
||||
/// Orientation is not determined by this property, but Orientation property.
|
||||
/// This property is used to determine the template to use for the calendar.
|
||||
/// </summary>
|
||||
[GeneratedDependencyProperty(DefaultValue = CalendarDisplayType.Day)]
|
||||
public partial CalendarDisplayType DisplayType { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
private WinoCalendarFlipView? InternalFlipView;
|
||||
private Grid? IdleGrid;
|
||||
|
||||
private ScrollViewer? _previousScrollViewer;
|
||||
private WinoDayTimelineCanvas? _previousCanvas;
|
||||
|
||||
public WinoCalendarControl()
|
||||
{
|
||||
DefaultStyleKey = typeof(WinoCalendarControl);
|
||||
SizeChanged += CalendarSizeChanged;
|
||||
}
|
||||
|
||||
partial void OnVerticalItemsPanelTemplateChanged(ItemsPanelTemplate? newValue)
|
||||
=> ManageCalendarOrientation();
|
||||
|
||||
partial void OnHorizontalItemsPanelTemplateChanged(ItemsPanelTemplate? newValue)
|
||||
=> ManageCalendarOrientation();
|
||||
|
||||
partial void OnOrientationChanged(CalendarOrientation newValue)
|
||||
=> ManageCalendarOrientation();
|
||||
|
||||
partial void OnDisplayTypeChanged(CalendarDisplayType newValue)
|
||||
=> ManageDisplayType();
|
||||
|
||||
partial void OnIsFlipIdleChanged(bool newValue)
|
||||
=> UpdateIdleState();
|
||||
|
||||
partial void OnDayRangesChanged(ObservableCollection<DayRangeRenderModel>? newValue)
|
||||
=> EnsureStableSelection();
|
||||
|
||||
partial void OnSelectedFlipViewDayRangeChanged(DayRangeRenderModel? newValue)
|
||||
=> EnsureStableSelection();
|
||||
|
||||
partial void OnActiveScrollViewerPropertyChanged(DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var newValue = e.NewValue as ScrollViewer;
|
||||
if (_previousScrollViewer != null)
|
||||
{
|
||||
DeregisterScrollChanges(_previousScrollViewer);
|
||||
}
|
||||
|
||||
if (newValue != null)
|
||||
{
|
||||
RegisterScrollChanges(newValue);
|
||||
}
|
||||
|
||||
_previousScrollViewer = newValue;
|
||||
ManageHighlightedDateRange();
|
||||
}
|
||||
|
||||
partial void OnActiveCanvasPropertyChanged(DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var newValue = e.NewValue as WinoDayTimelineCanvas;
|
||||
if (_previousCanvas != null)
|
||||
{
|
||||
DeregisterCanvas(_previousCanvas);
|
||||
}
|
||||
|
||||
if (newValue != null)
|
||||
{
|
||||
RegisterCanvas(newValue);
|
||||
}
|
||||
|
||||
_previousCanvas = newValue;
|
||||
ManageHighlightedDateRange();
|
||||
}
|
||||
|
||||
private void ManageCalendarOrientation()
|
||||
{
|
||||
if (InternalFlipView == null || HorizontalItemsPanelTemplate == null || VerticalItemsPanelTemplate == null) return;
|
||||
|
||||
InternalFlipView.ItemsPanel = Orientation == CalendarOrientation.Horizontal ? HorizontalItemsPanelTemplate : VerticalItemsPanelTemplate;
|
||||
}
|
||||
|
||||
private void ManageDisplayType()
|
||||
{
|
||||
if (InternalFlipView == null) return;
|
||||
|
||||
InternalFlipView.DisplayType = DisplayType;
|
||||
}
|
||||
|
||||
private void ManageHighlightedDateRange()
|
||||
{
|
||||
if (InternalFlipView?.IsProgrammaticNavigationInProgress == true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SelectedFlipViewDayRange = InternalFlipView?.SelectedItem as DayRangeRenderModel;
|
||||
}
|
||||
|
||||
private void DeregisterCanvas(WinoDayTimelineCanvas canvas)
|
||||
{
|
||||
if (canvas == null) return;
|
||||
|
||||
canvas.SelectedDateTime = null;
|
||||
canvas.TimelineCellSelected -= ActiveTimelineCellSelected;
|
||||
canvas.TimelineCellUnselected -= ActiveTimelineCellUnselected;
|
||||
}
|
||||
|
||||
private void RegisterCanvas(WinoDayTimelineCanvas canvas)
|
||||
{
|
||||
if (canvas == null) return;
|
||||
|
||||
canvas.SelectedDateTime = null;
|
||||
canvas.TimelineCellSelected += ActiveTimelineCellSelected;
|
||||
canvas.TimelineCellUnselected += ActiveTimelineCellUnselected;
|
||||
}
|
||||
|
||||
private void RegisterScrollChanges(ScrollViewer scrollViewer)
|
||||
{
|
||||
if (scrollViewer == null) return;
|
||||
|
||||
scrollViewer.ViewChanging += ScrollViewChanging;
|
||||
}
|
||||
|
||||
private void DeregisterScrollChanges(ScrollViewer scrollViewer)
|
||||
{
|
||||
if (scrollViewer == null) return;
|
||||
|
||||
scrollViewer.ViewChanging -= ScrollViewChanging;
|
||||
}
|
||||
|
||||
private void ScrollViewChanging(object? sender, ScrollViewerViewChangingEventArgs e)
|
||||
=> ScrollPositionChanging?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
private void CalendarSizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
if (ActiveCanvas == null) return;
|
||||
|
||||
ActiveCanvas.SelectedDateTime = null;
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate()
|
||||
{
|
||||
base.OnApplyTemplate();
|
||||
|
||||
if (InternalFlipView != null)
|
||||
{
|
||||
InternalFlipView.ProgrammaticNavigationCompleted -= InternalFlipViewProgrammaticNavigationCompleted;
|
||||
|
||||
if (InternalFlipView is IDisposable disposableFlipView)
|
||||
{
|
||||
disposableFlipView.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
if (_previousScrollViewer != null)
|
||||
{
|
||||
DeregisterScrollChanges(_previousScrollViewer);
|
||||
_previousScrollViewer = null;
|
||||
}
|
||||
|
||||
if (_previousCanvas != null)
|
||||
{
|
||||
DeregisterCanvas(_previousCanvas);
|
||||
_previousCanvas = null;
|
||||
}
|
||||
|
||||
InternalFlipView = GetTemplateChild(PART_WinoFlipView) as WinoCalendarFlipView;
|
||||
IdleGrid = GetTemplateChild(PART_IdleGrid) as Grid;
|
||||
|
||||
if (InternalFlipView != null)
|
||||
{
|
||||
InternalFlipView.ProgrammaticNavigationCompleted += InternalFlipViewProgrammaticNavigationCompleted;
|
||||
}
|
||||
|
||||
UpdateIdleState();
|
||||
ManageCalendarOrientation();
|
||||
ManageDisplayType();
|
||||
EnsureStableSelection();
|
||||
}
|
||||
|
||||
private void InternalFlipViewProgrammaticNavigationCompleted(object? sender, ProgrammaticNavigationCompletedEventArgs e)
|
||||
{
|
||||
SelectedFlipViewDayRange = e.DayRange;
|
||||
}
|
||||
|
||||
private void UpdateIdleState()
|
||||
{
|
||||
if (InternalFlipView != null)
|
||||
{
|
||||
InternalFlipView.Opacity = IsFlipIdle ? 0 : 1;
|
||||
}
|
||||
|
||||
if (IdleGrid != null)
|
||||
{
|
||||
IdleGrid.Visibility = IsFlipIdle ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureStableSelection()
|
||||
{
|
||||
if (InternalFlipView == null || DayRanges == null || DayRanges.Count == 0)
|
||||
return;
|
||||
|
||||
var targetIndex = SelectedFlipViewIndex;
|
||||
|
||||
if (SelectedFlipViewDayRange != null)
|
||||
{
|
||||
var selectedRangeIndex = DayRanges.IndexOf(SelectedFlipViewDayRange);
|
||||
if (selectedRangeIndex >= 0)
|
||||
{
|
||||
targetIndex = selectedRangeIndex;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetIndex < 0 || targetIndex >= DayRanges.Count)
|
||||
{
|
||||
targetIndex = 0;
|
||||
}
|
||||
|
||||
if (InternalFlipView.SelectedIndex != targetIndex)
|
||||
{
|
||||
InternalFlipView.SelectedIndex = targetIndex;
|
||||
}
|
||||
}
|
||||
|
||||
private void ActiveTimelineCellUnselected(object? sender, TimelineCellUnselectedArgs e)
|
||||
=> TimelineCellUnselected?.Invoke(this, e);
|
||||
|
||||
private void ActiveTimelineCellSelected(object? sender, TimelineCellSelectedArgs e)
|
||||
=> TimelineCellSelected?.Invoke(this, e);
|
||||
|
||||
public void NavigateToDay(DateTime dateTime) => InternalFlipView?.NavigateToDay(dateTime);
|
||||
|
||||
public async void NavigateToHour(TimeSpan timeSpan)
|
||||
{
|
||||
if (ActiveScrollViewer == null) return;
|
||||
|
||||
// Total height of the FlipViewItem is the same as vertical ScrollViewer to position day headers.
|
||||
|
||||
await Task.Yield();
|
||||
await DispatcherQueue.EnqueueAsync(() =>
|
||||
{
|
||||
if (ActiveScrollViewer == null) return;
|
||||
|
||||
double hourHeght = 60;
|
||||
double totalHeight = ActiveScrollViewer.ScrollableHeight;
|
||||
double scrollPosition = timeSpan.TotalHours * hourHeght;
|
||||
|
||||
ActiveScrollViewer.ChangeView(null, scrollPosition, null, disableAnimation: false);
|
||||
});
|
||||
}
|
||||
public void ResetTimelineSelection()
|
||||
{
|
||||
if (ActiveCanvas == null) return;
|
||||
|
||||
ActiveCanvas.SelectedDateTime = null;
|
||||
}
|
||||
|
||||
public void GoNextRange()
|
||||
{
|
||||
if (InternalFlipView == null) return;
|
||||
|
||||
InternalFlipView.GoNextFlip();
|
||||
}
|
||||
|
||||
public void GoPreviousRange()
|
||||
{
|
||||
if (InternalFlipView == null) return;
|
||||
|
||||
InternalFlipView.GoPreviousFlip();
|
||||
}
|
||||
|
||||
public void UnselectActiveTimelineCell()
|
||||
{
|
||||
if (ActiveCanvas == null) return;
|
||||
|
||||
ActiveCanvas.SelectedDateTime = null;
|
||||
}
|
||||
|
||||
public CalendarItemControl GetCalendarItemControl(CalendarItemViewModel calendarItemViewModel)
|
||||
{
|
||||
return this.FindDescendants<CalendarItemControl>().FirstOrDefault(a => a.CalendarItem == calendarItemViewModel)!;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
SizeChanged -= CalendarSizeChanged;
|
||||
|
||||
if (_previousScrollViewer != null)
|
||||
{
|
||||
DeregisterScrollChanges(_previousScrollViewer);
|
||||
_previousScrollViewer = null;
|
||||
}
|
||||
|
||||
if (_previousCanvas != null)
|
||||
{
|
||||
DeregisterCanvas(_previousCanvas);
|
||||
_previousCanvas = null;
|
||||
}
|
||||
|
||||
if (InternalFlipView != null)
|
||||
{
|
||||
InternalFlipView.ProgrammaticNavigationCompleted -= InternalFlipViewProgrammaticNavigationCompleted;
|
||||
|
||||
if (InternalFlipView is IDisposable disposableFlipView)
|
||||
{
|
||||
disposableFlipView.Dispose();
|
||||
}
|
||||
|
||||
InternalFlipView = null;
|
||||
}
|
||||
|
||||
IdleGrid = null;
|
||||
ActiveCanvas = null;
|
||||
ActiveScrollViewer = null;
|
||||
TimelineCellSelected = null;
|
||||
TimelineCellUnselected = null;
|
||||
ScrollPositionChanging = null;
|
||||
}
|
||||
}
|
||||
@@ -1,321 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.WinUI;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Wino.Core.Domain.Collections;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
namespace Wino.Calendar.Controls;
|
||||
|
||||
public partial class WinoCalendarFlipView : CustomCalendarFlipView, IDisposable
|
||||
{
|
||||
public static readonly DependencyProperty IsIdleProperty = DependencyProperty.Register(nameof(IsIdle), typeof(bool), typeof(WinoCalendarFlipView), new PropertyMetadata(true));
|
||||
public static readonly DependencyProperty ActiveCanvasProperty = DependencyProperty.Register(nameof(ActiveCanvas), typeof(WinoDayTimelineCanvas), typeof(WinoCalendarFlipView), new PropertyMetadata(null));
|
||||
public static readonly DependencyProperty ActiveVerticalScrollViewerProperty = DependencyProperty.Register(nameof(ActiveVerticalScrollViewer), typeof(ScrollViewer), typeof(WinoCalendarFlipView), new PropertyMetadata(null));
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the active canvas that is currently displayed in the flip view.
|
||||
/// Each day-range of flip view item has a canvas that displays the day timeline.
|
||||
/// </summary>
|
||||
public WinoDayTimelineCanvas? ActiveCanvas
|
||||
{
|
||||
get { return (WinoDayTimelineCanvas?)GetValue(ActiveCanvasProperty); }
|
||||
set { SetValue(ActiveCanvasProperty, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the scroll viewer that is currently active in the flip view.
|
||||
/// It's the vertical scroll that scrolls the timeline only, not the header part that belongs
|
||||
/// to parent FlipView control.
|
||||
/// </summary>
|
||||
public ScrollViewer? ActiveVerticalScrollViewer
|
||||
{
|
||||
get { return (ScrollViewer?)GetValue(ActiveVerticalScrollViewerProperty); }
|
||||
set { SetValue(ActiveVerticalScrollViewerProperty, value); }
|
||||
}
|
||||
|
||||
public bool IsIdle
|
||||
{
|
||||
get { return (bool)GetValue(IsIdleProperty); }
|
||||
set { SetValue(IsIdleProperty, value); }
|
||||
}
|
||||
|
||||
internal bool IsProgrammaticNavigationInProgress { get; private set; }
|
||||
|
||||
internal int? PendingTargetIndex { get; private set; }
|
||||
|
||||
internal event EventHandler<ProgrammaticNavigationCompletedEventArgs>? ProgrammaticNavigationCompleted;
|
||||
|
||||
private INotifyCollectionChanged? _trackedItemsSource;
|
||||
private readonly long _itemsSourceCallbackToken;
|
||||
private FrameworkElement? _pendingActiveElementContainer;
|
||||
private bool _isActiveElementResolutionPending;
|
||||
|
||||
public WinoCalendarFlipView()
|
||||
{
|
||||
_itemsSourceCallbackToken = RegisterPropertyChangedCallback(ItemsSourceProperty, new DependencyPropertyChangedCallback(OnItemsSourceChanged));
|
||||
}
|
||||
|
||||
private static void OnItemsSourceChanged(DependencyObject d, DependencyProperty e)
|
||||
{
|
||||
if (d is WinoCalendarFlipView flipView)
|
||||
{
|
||||
flipView.RegisterItemsSourceChange();
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterItemsSourceChange()
|
||||
{
|
||||
if (_trackedItemsSource != null)
|
||||
{
|
||||
_trackedItemsSource.CollectionChanged -= ItemsSourceUpdated;
|
||||
}
|
||||
|
||||
_trackedItemsSource = GetItemsSource();
|
||||
|
||||
if (_trackedItemsSource != null)
|
||||
{
|
||||
_trackedItemsSource.CollectionChanged += ItemsSourceUpdated;
|
||||
}
|
||||
|
||||
UpdateIdleState();
|
||||
}
|
||||
|
||||
protected override void OnSelectedItemChanged(object? oldValue, object? newValue)
|
||||
{
|
||||
base.OnSelectedItemChanged(oldValue, newValue);
|
||||
|
||||
UpdateActiveElements();
|
||||
}
|
||||
|
||||
protected override void OnContainerPrepared(DependencyObject element, object item)
|
||||
{
|
||||
base.OnContainerPrepared(element, item);
|
||||
|
||||
// Check if this is the currently selected item's container
|
||||
var index = IndexFromContainer(element);
|
||||
if (index >= 0 && index == SelectedIndex)
|
||||
{
|
||||
// Container for selected item is now ready, update active elements
|
||||
UpdateActiveElements();
|
||||
}
|
||||
}
|
||||
|
||||
private void ItemsSourceUpdated(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
UpdateIdleState();
|
||||
}
|
||||
|
||||
private void UpdateIdleState()
|
||||
{
|
||||
var itemsSource = GetItemsSource();
|
||||
IsIdle = itemsSource == null || itemsSource.Count == 0;
|
||||
}
|
||||
|
||||
private void UpdateActiveElements()
|
||||
{
|
||||
var itemsSource = GetItemsSource();
|
||||
|
||||
if (SelectedIndex < 0)
|
||||
{
|
||||
CancelPendingActiveElementResolution();
|
||||
|
||||
if (itemsSource == null || itemsSource.Count == 0)
|
||||
{
|
||||
ActiveCanvas = null;
|
||||
ActiveVerticalScrollViewer = null;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryResolveActiveElements())
|
||||
{
|
||||
CancelPendingActiveElementResolution();
|
||||
}
|
||||
else
|
||||
{
|
||||
ScheduleActiveElementResolution();
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryResolveActiveElements()
|
||||
{
|
||||
if (SelectedIndex < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ContainerFromIndex(SelectedIndex) is not FlipViewItem container)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var activeCanvas = container.FindDescendant<WinoDayTimelineCanvas>();
|
||||
var activeVerticalScrollViewer = container.FindDescendant<ScrollViewer>();
|
||||
|
||||
if (activeCanvas == null && activeVerticalScrollViewer == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ActiveCanvas = activeCanvas;
|
||||
ActiveVerticalScrollViewer = activeVerticalScrollViewer;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ScheduleActiveElementResolution()
|
||||
{
|
||||
if (ContainerFromIndex(SelectedIndex) is FrameworkElement container &&
|
||||
!ReferenceEquals(_pendingActiveElementContainer, container))
|
||||
{
|
||||
if (_pendingActiveElementContainer != null)
|
||||
{
|
||||
_pendingActiveElementContainer.Loaded -= PendingActiveElementContainerLoaded;
|
||||
}
|
||||
|
||||
_pendingActiveElementContainer = container;
|
||||
_pendingActiveElementContainer.Loaded += PendingActiveElementContainerLoaded;
|
||||
}
|
||||
|
||||
if (_isActiveElementResolutionPending)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isActiveElementResolutionPending = true;
|
||||
LayoutUpdated += FlipViewLayoutUpdated;
|
||||
|
||||
DispatcherQueue.TryEnqueue(RetryActiveElementResolution);
|
||||
}
|
||||
|
||||
private void RetryActiveElementResolution()
|
||||
{
|
||||
if (TryResolveActiveElements())
|
||||
{
|
||||
CancelPendingActiveElementResolution();
|
||||
}
|
||||
}
|
||||
|
||||
private void PendingActiveElementContainerLoaded(object sender, RoutedEventArgs e)
|
||||
=> RetryActiveElementResolution();
|
||||
|
||||
private void FlipViewLayoutUpdated(object? sender, object e)
|
||||
=> RetryActiveElementResolution();
|
||||
|
||||
private void CancelPendingActiveElementResolution()
|
||||
{
|
||||
if (_pendingActiveElementContainer != null)
|
||||
{
|
||||
_pendingActiveElementContainer.Loaded -= PendingActiveElementContainerLoaded;
|
||||
_pendingActiveElementContainer = null;
|
||||
}
|
||||
|
||||
if (_isActiveElementResolutionPending)
|
||||
{
|
||||
LayoutUpdated -= FlipViewLayoutUpdated;
|
||||
_isActiveElementResolutionPending = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Navigates to the specified date in the calendar.
|
||||
/// </summary>
|
||||
/// <param name="dateTime">Date to navigate.</param>
|
||||
public async void NavigateToDay(DateTime dateTime)
|
||||
{
|
||||
await Task.Yield();
|
||||
|
||||
await DispatcherQueue.EnqueueAsync(() =>
|
||||
{
|
||||
// Find the day range that contains the date.
|
||||
var dayRanges = GetItemsSource();
|
||||
var dayRange = dayRanges?.FirstOrDefault(a => a.CalendarDays.Any(b => b.RepresentingDate.Date == dateTime.Date));
|
||||
|
||||
if (dayRange != null && dayRanges != null)
|
||||
{
|
||||
var navigationItemIndex = dayRanges.IndexOf(dayRange);
|
||||
var hasNavigationWork = navigationItemIndex != SelectedIndex;
|
||||
|
||||
IsProgrammaticNavigationInProgress = hasNavigationWork;
|
||||
PendingTargetIndex = navigationItemIndex;
|
||||
|
||||
if (!hasNavigationWork)
|
||||
{
|
||||
PendingTargetIndex = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (Math.Abs(navigationItemIndex - SelectedIndex) > 4)
|
||||
{
|
||||
// Difference between dates are high.
|
||||
// No need to animate this much, just go without animating.
|
||||
|
||||
SelectedIndex = navigationItemIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Until we reach the day in the flip, simulate next-prev button clicks.
|
||||
// This will make sure the FlipView animations are triggered.
|
||||
// Setting SelectedIndex directly doesn't trigger the animations.
|
||||
|
||||
while (SelectedIndex != navigationItemIndex)
|
||||
{
|
||||
if (SelectedIndex > navigationItemIndex)
|
||||
{
|
||||
GoPreviousFlip();
|
||||
}
|
||||
else
|
||||
{
|
||||
GoNextFlip();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (SelectedIndex == PendingTargetIndex)
|
||||
{
|
||||
ProgrammaticNavigationCompleted?.Invoke(this, new ProgrammaticNavigationCompletedEventArgs(SelectedItem as DayRangeRenderModel ?? dayRange));
|
||||
}
|
||||
|
||||
IsProgrammaticNavigationInProgress = false;
|
||||
PendingTargetIndex = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private ObservableRangeCollection<DayRangeRenderModel>? GetItemsSource()
|
||||
=> ItemsSource as ObservableRangeCollection<DayRangeRenderModel>;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
CancelPendingActiveElementResolution();
|
||||
|
||||
if (_trackedItemsSource != null)
|
||||
{
|
||||
_trackedItemsSource.CollectionChanged -= ItemsSourceUpdated;
|
||||
_trackedItemsSource = null;
|
||||
}
|
||||
|
||||
UnregisterPropertyChangedCallback(ItemsSourceProperty, _itemsSourceCallbackToken);
|
||||
ProgrammaticNavigationCompleted = null;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ProgrammaticNavigationCompletedEventArgs : EventArgs
|
||||
{
|
||||
public ProgrammaticNavigationCompletedEventArgs(DayRangeRenderModel dayRange)
|
||||
{
|
||||
DayRange = dayRange;
|
||||
}
|
||||
|
||||
public DayRangeRenderModel DayRange { get; }
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
using System;
|
||||
using System.Windows.Input;
|
||||
using CommunityToolkit.Diagnostics;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
using Wino.Helpers;
|
||||
|
||||
namespace Wino.Calendar.Controls;
|
||||
|
||||
public partial class WinoCalendarView : Control, IDisposable
|
||||
{
|
||||
private const string PART_DayViewItemBorder = nameof(PART_DayViewItemBorder);
|
||||
private const string PART_CalendarView = nameof(PART_CalendarView);
|
||||
|
||||
public static readonly DependencyProperty HighlightedDateRangeProperty = DependencyProperty.Register(nameof(HighlightedDateRange), typeof(DateRange), typeof(WinoCalendarView), new PropertyMetadata(null, new PropertyChangedCallback(OnHighlightedDateRangeChanged)));
|
||||
public static readonly DependencyProperty VisibleDateBackgroundProperty = DependencyProperty.Register(nameof(VisibleDateBackground), typeof(Brush), typeof(WinoCalendarView), new PropertyMetadata(null, new PropertyChangedCallback(OnPropertiesChanged)));
|
||||
public static readonly DependencyProperty DateClickedCommandProperty = DependencyProperty.Register(nameof(DateClickedCommand), typeof(ICommand), typeof(WinoCalendarView), new PropertyMetadata(null));
|
||||
public static readonly DependencyProperty TodayBackgroundColorProperty = DependencyProperty.Register(nameof(TodayBackgroundColor), typeof(Color), typeof(WinoCalendarView), new PropertyMetadata(new Color()));
|
||||
|
||||
public Color TodayBackgroundColor
|
||||
{
|
||||
get { return (Color)GetValue(TodayBackgroundColorProperty); }
|
||||
set { SetValue(TodayBackgroundColorProperty, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the command to execute when a date is picked.
|
||||
/// Unused.
|
||||
/// </summary>
|
||||
public ICommand? DateClickedCommand
|
||||
{
|
||||
get { return (ICommand?)GetValue(DateClickedCommandProperty); }
|
||||
set { SetValue(DateClickedCommandProperty, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the highlighted range of dates.
|
||||
/// </summary>
|
||||
public DateRange? HighlightedDateRange
|
||||
{
|
||||
get { return (DateRange?)GetValue(HighlightedDateRangeProperty); }
|
||||
set { SetValue(HighlightedDateRangeProperty, value); }
|
||||
}
|
||||
|
||||
public Brush? VisibleDateBackground
|
||||
{
|
||||
get { return (Brush?)GetValue(VisibleDateBackgroundProperty); }
|
||||
set { SetValue(VisibleDateBackgroundProperty, value); }
|
||||
}
|
||||
|
||||
|
||||
|
||||
private CalendarView? CalendarView;
|
||||
private long _displayModeCallbackToken = -1;
|
||||
|
||||
public WinoCalendarView()
|
||||
{
|
||||
DefaultStyleKey = typeof(WinoCalendarView);
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate()
|
||||
{
|
||||
base.OnApplyTemplate();
|
||||
|
||||
if (CalendarView != null)
|
||||
{
|
||||
CalendarView.SelectedDatesChanged -= InternalCalendarViewSelectionChanged;
|
||||
|
||||
if (_displayModeCallbackToken != -1)
|
||||
{
|
||||
CalendarView.UnregisterPropertyChangedCallback(CalendarView.DisplayModeProperty, _displayModeCallbackToken);
|
||||
_displayModeCallbackToken = -1;
|
||||
}
|
||||
}
|
||||
|
||||
CalendarView = GetTemplateChild(PART_CalendarView) as CalendarView;
|
||||
|
||||
Guard.IsNotNull(CalendarView, nameof(CalendarView));
|
||||
if (CalendarView == null) return;
|
||||
|
||||
CalendarView.SelectedDatesChanged -= InternalCalendarViewSelectionChanged;
|
||||
CalendarView.SelectedDatesChanged += InternalCalendarViewSelectionChanged;
|
||||
|
||||
// TODO: Should come from settings.
|
||||
CalendarView.FirstDayOfWeek = Windows.Globalization.DayOfWeek.Monday;
|
||||
|
||||
// Everytime display mode changes, update the visible date range backgrounds.
|
||||
// If users go back from year -> month -> day, we need to update the visible date range backgrounds.
|
||||
|
||||
_displayModeCallbackToken = CalendarView.RegisterPropertyChangedCallback(CalendarView.DisplayModeProperty, (s, e) => UpdateVisibleDateRangeBackgrounds());
|
||||
}
|
||||
|
||||
private void InternalCalendarViewSelectionChanged(CalendarView sender, CalendarViewSelectedDatesChangedEventArgs args)
|
||||
{
|
||||
if (args.AddedDates?.Count > 0)
|
||||
{
|
||||
var clickedDate = args.AddedDates[0].Date;
|
||||
SetInnerDisplayDate(clickedDate);
|
||||
|
||||
var clickArgs = new CalendarViewDayClickedEventArgs(clickedDate);
|
||||
DateClickedCommand?.Execute(clickArgs);
|
||||
}
|
||||
|
||||
// Reset selection, we don't show selected dates but react to them.
|
||||
CalendarView?.SelectedDates.Clear();
|
||||
}
|
||||
|
||||
private static void OnPropertiesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is WinoCalendarView control)
|
||||
{
|
||||
control.UpdateVisibleDateRangeBackgrounds();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetInnerDisplayDate(DateTime dateTime) => CalendarView?.SetDisplayDate(dateTime);
|
||||
|
||||
// Changing selected dates will trigger the selection changed event.
|
||||
// It will behave like user clicked the date.
|
||||
public void GoToDay(DateTime dateTime) => CalendarView?.SelectedDates.Add(dateTime);
|
||||
|
||||
private static void OnHighlightedDateRangeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is WinoCalendarView control)
|
||||
{
|
||||
if (control.HighlightedDateRange == null) return;
|
||||
|
||||
control.SetInnerDisplayDate(control.HighlightedDateRange.StartDate);
|
||||
control.UpdateVisibleDateRangeBackgrounds();
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateVisibleDateRangeBackgrounds()
|
||||
{
|
||||
if (HighlightedDateRange == null || VisibleDateBackground == null || CalendarView == null) return;
|
||||
|
||||
var markDateCalendarDayItems = WinoVisualTreeHelper.FindDescendants<CalendarViewDayItem>(CalendarView);
|
||||
|
||||
foreach (var calendarDayItem in markDateCalendarDayItems)
|
||||
{
|
||||
var border = WinoVisualTreeHelper.GetChildObject<Border>(calendarDayItem, PART_DayViewItemBorder);
|
||||
|
||||
if (border == null) return;
|
||||
|
||||
if (calendarDayItem.Date.Date == DateTime.Today.Date)
|
||||
{
|
||||
border.Background = new SolidColorBrush(TodayBackgroundColor);
|
||||
}
|
||||
else if (calendarDayItem.Date.Date >= HighlightedDateRange.StartDate.Date && calendarDayItem.Date.Date < HighlightedDateRange.EndDate.Date)
|
||||
{
|
||||
border.Background = VisibleDateBackground;
|
||||
}
|
||||
else
|
||||
{
|
||||
border.Background = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (CalendarView == null)
|
||||
return;
|
||||
|
||||
CalendarView.SelectedDatesChanged -= InternalCalendarViewSelectionChanged;
|
||||
|
||||
if (_displayModeCallbackToken != -1)
|
||||
{
|
||||
CalendarView.UnregisterPropertyChangedCallback(CalendarView.DisplayModeProperty, _displayModeCallbackToken);
|
||||
_displayModeCallbackToken = -1;
|
||||
}
|
||||
|
||||
CalendarView = null;
|
||||
}
|
||||
}
|
||||
@@ -1,332 +0,0 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.WinUI;
|
||||
using Microsoft.UI.Input;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using SkiaSharp;
|
||||
using SkiaSharp.Views.Windows;
|
||||
using Windows.Foundation;
|
||||
using Wino.Calendar.Args;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
namespace Wino.Calendar.Controls;
|
||||
|
||||
public partial class WinoDayTimelineCanvas : Control, IDisposable
|
||||
{
|
||||
public event EventHandler<TimelineCellSelectedArgs>? TimelineCellSelected;
|
||||
public event EventHandler<TimelineCellUnselectedArgs>? TimelineCellUnselected;
|
||||
|
||||
private const string PART_InternalCanvas = nameof(PART_InternalCanvas);
|
||||
private SKXamlCanvas? Canvas;
|
||||
|
||||
public static readonly DependencyProperty RenderOptionsProperty = DependencyProperty.Register(nameof(RenderOptions), typeof(CalendarRenderOptions), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
|
||||
public static readonly DependencyProperty SeperatorColorProperty = DependencyProperty.Register(nameof(SeperatorColor), typeof(SolidColorBrush), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
|
||||
public static readonly DependencyProperty HalfHourSeperatorColorProperty = DependencyProperty.Register(nameof(HalfHourSeperatorColor), typeof(SolidColorBrush), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
|
||||
public static readonly DependencyProperty SelectedCellBackgroundBrushProperty = DependencyProperty.Register(nameof(SelectedCellBackgroundBrush), typeof(SolidColorBrush), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
|
||||
public static readonly DependencyProperty WorkingHourCellBackgroundColorProperty = DependencyProperty.Register(nameof(WorkingHourCellBackgroundColor), typeof(SolidColorBrush), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
|
||||
public static readonly DependencyProperty SelectedDateTimeProperty = DependencyProperty.Register(nameof(SelectedDateTime), typeof(DateTime?), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnSelectedDateTimeChanged)));
|
||||
public static readonly DependencyProperty PositionerUIElementProperty = DependencyProperty.Register(nameof(PositionerUIElement), typeof(UIElement), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null));
|
||||
|
||||
public UIElement? PositionerUIElement
|
||||
{
|
||||
get { return (UIElement?)GetValue(PositionerUIElementProperty); }
|
||||
set { SetValue(PositionerUIElementProperty, value); }
|
||||
}
|
||||
|
||||
public CalendarRenderOptions? RenderOptions
|
||||
{
|
||||
get { return (CalendarRenderOptions?)GetValue(RenderOptionsProperty); }
|
||||
set { SetValue(RenderOptionsProperty, value); }
|
||||
}
|
||||
|
||||
public SolidColorBrush? HalfHourSeperatorColor
|
||||
{
|
||||
get { return (SolidColorBrush?)GetValue(HalfHourSeperatorColorProperty); }
|
||||
set { SetValue(HalfHourSeperatorColorProperty, value); }
|
||||
}
|
||||
|
||||
public SolidColorBrush? SeperatorColor
|
||||
{
|
||||
get { return (SolidColorBrush?)GetValue(SeperatorColorProperty); }
|
||||
set { SetValue(SeperatorColorProperty, value); }
|
||||
}
|
||||
|
||||
public SolidColorBrush? WorkingHourCellBackgroundColor
|
||||
{
|
||||
get { return (SolidColorBrush?)GetValue(WorkingHourCellBackgroundColorProperty); }
|
||||
set { SetValue(WorkingHourCellBackgroundColorProperty, value); }
|
||||
}
|
||||
|
||||
public SolidColorBrush? SelectedCellBackgroundBrush
|
||||
{
|
||||
get { return (SolidColorBrush?)GetValue(SelectedCellBackgroundBrushProperty); }
|
||||
set { SetValue(SelectedCellBackgroundBrushProperty, value); }
|
||||
}
|
||||
|
||||
public DateTime? SelectedDateTime
|
||||
{
|
||||
get { return (DateTime?)GetValue(SelectedDateTimeProperty); }
|
||||
set { SetValue(SelectedDateTimeProperty, value); }
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate()
|
||||
{
|
||||
base.OnApplyTemplate();
|
||||
|
||||
Canvas = GetTemplateChild(PART_InternalCanvas) as SKXamlCanvas;
|
||||
|
||||
if (Canvas != null)
|
||||
{
|
||||
Canvas.PaintSurface += OnCanvasPaintSurface;
|
||||
Canvas.PointerPressed += OnCanvasPointerPressed;
|
||||
}
|
||||
|
||||
ForceDraw();
|
||||
}
|
||||
|
||||
private static void OnSelectedDateTimeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is WinoDayTimelineCanvas control)
|
||||
{
|
||||
if (e.OldValue != null && e.NewValue == null)
|
||||
{
|
||||
control.RaiseCellUnselected();
|
||||
}
|
||||
|
||||
control.ForceDraw();
|
||||
}
|
||||
}
|
||||
|
||||
private void RaiseCellUnselected()
|
||||
{
|
||||
TimelineCellUnselected?.Invoke(this, new TimelineCellUnselectedArgs());
|
||||
}
|
||||
|
||||
private void OnCanvasPointerPressed(object? sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
if (RenderOptions == null) return;
|
||||
var canvas = Canvas;
|
||||
if (canvas == null) return;
|
||||
|
||||
var hourHeight = RenderOptions.CalendarSettings.HourHeight;
|
||||
|
||||
// When users click to cell we need to find the day, hour and minutes (first 30 minutes or second 30 minutes) that it represents on the timeline.
|
||||
|
||||
if (PositionerUIElement == null)
|
||||
{
|
||||
PositionerUIElement = this.FindParents().LastOrDefault(a => a is Grid);
|
||||
}
|
||||
|
||||
if (PositionerUIElement == null)
|
||||
return;
|
||||
|
||||
PointerPoint positionerRootPoint = e.GetCurrentPoint(PositionerUIElement);
|
||||
PointerPoint canvasPointerPoint = e.GetCurrentPoint(canvas);
|
||||
|
||||
Point touchPoint = canvasPointerPoint.Position;
|
||||
|
||||
var singleDayWidth = (canvas.ActualWidth / RenderOptions.TotalDayCount);
|
||||
|
||||
int day = (int)(touchPoint.X / singleDayWidth);
|
||||
int hour = (int)(touchPoint.Y / hourHeight);
|
||||
|
||||
bool isSecondHalf = touchPoint.Y % hourHeight > (hourHeight / 2);
|
||||
|
||||
var diffX = positionerRootPoint.Position.X - touchPoint.X;
|
||||
var diffY = positionerRootPoint.Position.Y - touchPoint.Y;
|
||||
|
||||
var cellStartRelativePositionX = diffX + (day * singleDayWidth);
|
||||
var cellEndRelativePositionX = cellStartRelativePositionX + singleDayWidth;
|
||||
|
||||
var cellStartRelativePositionY = diffY + (hour * hourHeight) + (isSecondHalf ? hourHeight / 2 : 0);
|
||||
var cellEndRelativePositionY = cellStartRelativePositionY + (isSecondHalf ? (hourHeight / 2) : hourHeight);
|
||||
|
||||
var cellSize = new Size(cellEndRelativePositionX - cellStartRelativePositionX, hourHeight / 2);
|
||||
var positionerPoint = new Point(cellStartRelativePositionX, cellStartRelativePositionY);
|
||||
|
||||
var clickedDateTime = RenderOptions.DateRange.StartDate.AddDays(day).AddHours(hour).AddMinutes(isSecondHalf ? 30 : 0);
|
||||
|
||||
// If there is already a selected date, in order to mimic the popup behavior, we need to dismiss the previous selection first.
|
||||
// Next click will be a new selection.
|
||||
|
||||
// Raise the events directly here instead of DP to not lose pointer position.
|
||||
if (clickedDateTime == SelectedDateTime || SelectedDateTime != null)
|
||||
{
|
||||
SelectedDateTime = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedDateTime = clickedDateTime;
|
||||
TimelineCellSelected?.Invoke(this, new TimelineCellSelectedArgs(clickedDateTime, touchPoint, positionerPoint, cellSize));
|
||||
}
|
||||
|
||||
Debug.WriteLine($"Clicked: {clickedDateTime}");
|
||||
}
|
||||
|
||||
public WinoDayTimelineCanvas()
|
||||
{
|
||||
DefaultStyleKey = typeof(WinoDayTimelineCanvas);
|
||||
}
|
||||
|
||||
private static void OnRenderingPropertiesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is WinoDayTimelineCanvas control)
|
||||
{
|
||||
control.ForceDraw();
|
||||
}
|
||||
}
|
||||
|
||||
private void ForceDraw() => Canvas?.Invalidate();
|
||||
|
||||
private bool CanDrawTimeline()
|
||||
{
|
||||
return RenderOptions != null
|
||||
&& Canvas != null
|
||||
&& WorkingHourCellBackgroundColor != null
|
||||
&& SeperatorColor != null
|
||||
&& HalfHourSeperatorColor != null
|
||||
&& SelectedCellBackgroundBrush != null;
|
||||
}
|
||||
|
||||
private void OnCanvasPaintSurface(object? sender, SKPaintSurfaceEventArgs e)
|
||||
{
|
||||
if (!CanDrawTimeline()) return;
|
||||
var renderOptions = RenderOptions!;
|
||||
var workingHourCellBackgroundColor = WorkingHourCellBackgroundColor!;
|
||||
var seperatorColor = SeperatorColor!;
|
||||
var halfHourSeperatorColor = HalfHourSeperatorColor!;
|
||||
var selectedCellBackgroundBrush = SelectedCellBackgroundBrush!;
|
||||
|
||||
var canvas = e.Surface.Canvas;
|
||||
canvas.Clear(SKColors.Transparent);
|
||||
|
||||
int hours = 24;
|
||||
|
||||
double canvasWidth = e.Info.Width;
|
||||
double canvasHeight = e.Info.Height;
|
||||
|
||||
if (canvasWidth == 0 || canvasHeight == 0) return;
|
||||
|
||||
// Calculate the width of each rectangle (1 day column)
|
||||
// Equal distribution of the whole width.
|
||||
double rectWidth = canvasWidth / renderOptions.TotalDayCount;
|
||||
|
||||
// Calculate the height of each rectangle (1 hour row)
|
||||
double rectHeight = renderOptions.CalendarSettings.HourHeight;
|
||||
|
||||
// Define stroke and fill colors
|
||||
var strokeColor = ToSKColor(seperatorColor.Color);
|
||||
float strokeThickness = 0.5f;
|
||||
|
||||
// Create paints for drawing
|
||||
using var strokePaint = new SKPaint
|
||||
{
|
||||
Color = strokeColor,
|
||||
StrokeWidth = strokeThickness,
|
||||
Style = SKPaintStyle.Stroke,
|
||||
IsAntialias = true
|
||||
};
|
||||
|
||||
using var fillPaint = new SKPaint
|
||||
{
|
||||
Style = SKPaintStyle.Fill,
|
||||
IsAntialias = true
|
||||
};
|
||||
|
||||
using var dashedPaint = new SKPaint
|
||||
{
|
||||
Color = ToSKColor(halfHourSeperatorColor.Color),
|
||||
StrokeWidth = strokeThickness,
|
||||
Style = SKPaintStyle.Stroke,
|
||||
PathEffect = SKPathEffect.CreateDash([2f, 2f], 0),
|
||||
IsAntialias = true
|
||||
};
|
||||
|
||||
for (int day = 0; day < renderOptions.TotalDayCount; day++)
|
||||
{
|
||||
var currentDay = renderOptions.DateRange.StartDate.AddDays(day);
|
||||
|
||||
bool isWorkingDay = renderOptions.CalendarSettings.WorkingDays.Contains(currentDay.DayOfWeek);
|
||||
|
||||
// Loop through each hour (rows)
|
||||
for (int hour = 0; hour < hours; hour++)
|
||||
{
|
||||
var renderTime = TimeSpan.FromHours(hour);
|
||||
|
||||
var representingDateTime = currentDay.AddHours(hour);
|
||||
|
||||
// Calculate the position and size of the rectangle
|
||||
float x = (float)(day * rectWidth);
|
||||
float y = (float)(hour * rectHeight);
|
||||
float width = (float)rectWidth;
|
||||
float height = (float)rectHeight;
|
||||
|
||||
var rectangle = new SKRect(x, y, x + width, y + height);
|
||||
|
||||
// Draw the rectangle border.
|
||||
// This is the main rectangle.
|
||||
canvas.DrawRect(rectangle, strokePaint);
|
||||
|
||||
// Fill another rectangle with the working hour background color
|
||||
// This rectangle must be placed with -1 margin to prevent invisible borders of the main rectangle.
|
||||
if (isWorkingDay && renderTime >= renderOptions.CalendarSettings.WorkingHourStart && renderTime <= renderOptions.CalendarSettings.WorkingHourEnd)
|
||||
{
|
||||
var backgroundRectangle = new SKRect(x + 1, y + 1, x + width - 1, y + height - 1);
|
||||
|
||||
canvas.DrawRect(backgroundRectangle, strokePaint);
|
||||
fillPaint.Color = ToSKColor(workingHourCellBackgroundColor.Color);
|
||||
canvas.DrawRect(backgroundRectangle, fillPaint);
|
||||
}
|
||||
|
||||
// Draw a line in the center of the rectangle for representing half hours.
|
||||
float lineY = y + height / 2;
|
||||
canvas.DrawLine(x, lineY, x + width, lineY, dashedPaint);
|
||||
}
|
||||
|
||||
// Draw selected item background color for the date if possible.
|
||||
if (SelectedDateTime != null)
|
||||
{
|
||||
var selectedDateTime = SelectedDateTime.Value;
|
||||
if (selectedDateTime.Date == currentDay.Date)
|
||||
{
|
||||
var selectionRectHeight = rectHeight / 2;
|
||||
var selectedY = selectedDateTime.Hour * rectHeight + (selectedDateTime.Minute / 60) * rectHeight;
|
||||
|
||||
// Second half of the hour is selected.
|
||||
if (selectedDateTime.TimeOfDay.Minutes == 30)
|
||||
{
|
||||
selectedY += rectHeight / 2;
|
||||
}
|
||||
|
||||
var selectedRectangle = new SKRect(
|
||||
(float)(day * rectWidth),
|
||||
(float)selectedY,
|
||||
(float)(day * rectWidth + rectWidth),
|
||||
(float)(selectedY + selectionRectHeight));
|
||||
|
||||
fillPaint.Color = ToSKColor(selectedCellBackgroundBrush.Color);
|
||||
canvas.DrawRect(selectedRectangle, fillPaint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static SKColor ToSKColor(Windows.UI.Color color)
|
||||
{
|
||||
return new SKColor(color.R, color.G, color.B, color.A);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Canvas == null) return;
|
||||
|
||||
Canvas.PaintSurface -= OnCanvasPaintSurface;
|
||||
Canvas.PointerPressed -= OnCanvasPointerPressed;
|
||||
|
||||
Canvas = null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user