1560 lines
56 KiB
C#
1560 lines
56 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Collections.Specialized;
|
|
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Numerics;
|
|
using System.Runtime.CompilerServices;
|
|
using CommunityToolkit.WinUI;
|
|
using Itenso.TimePeriod;
|
|
using Microsoft.UI;
|
|
using Microsoft.UI.Composition;
|
|
using Microsoft.UI.Dispatching;
|
|
using Microsoft.UI.Xaml;
|
|
using Microsoft.UI.Xaml.Controls;
|
|
using Microsoft.UI.Xaml.Hosting;
|
|
using Microsoft.UI.Xaml.Input;
|
|
using Microsoft.UI.Xaml.Media;
|
|
using SkiaSharp;
|
|
using SkiaSharp.Views.Windows;
|
|
using Windows.ApplicationModel.DataTransfer;
|
|
using Windows.Foundation;
|
|
using Windows.UI;
|
|
using Wino.Calendar.ViewModels.Data;
|
|
using Wino.Core.Domain.Enums;
|
|
using Wino.Core.Domain.Interfaces;
|
|
using Wino.Core.Domain.Models.Calendar;
|
|
|
|
namespace Wino.Calendar.Controls;
|
|
|
|
public sealed partial class CalendarPeriodControl : UserControl, INotifyPropertyChanged
|
|
{
|
|
private static readonly TimeSpan SizeRefreshDebounceInterval = TimeSpan.FromMilliseconds(75);
|
|
private const double SizeChangeThreshold = 0.5d;
|
|
private const double TimedHourColumnWidth = 64d;
|
|
private const double TimedGridIntervalMinutes = 30d;
|
|
private const double TimedSelectionIntervalMinutes = 30d;
|
|
private const double TimedItemRightSpacing = 10d;
|
|
private VisibleDateRange _currentRange = new(
|
|
CalendarDisplayType.Month,
|
|
DateOnly.FromDateTime(DateTime.Today),
|
|
DateOnly.FromDateTime(DateTime.Today),
|
|
DateOnly.FromDateTime(DateTime.Today),
|
|
DateOnly.FromDateTime(DateTime.Today),
|
|
1,
|
|
true,
|
|
true,
|
|
[DateOnly.FromDateTime(DateTime.Today)]);
|
|
|
|
private TimedCalendarLayoutResult _timedLayout = new([], 0, []);
|
|
private MonthCalendarLayoutResult _monthLayout = new(0, 0, [], []);
|
|
private INotifyCollectionChanged? _observableItemsSource;
|
|
private double _timedDayWidth;
|
|
private double _timedAllDayHeight;
|
|
private double _monthCellWidth;
|
|
private double _monthCellHeight;
|
|
private bool _hasPresentedState;
|
|
private bool _refreshPending = true;
|
|
private bool _refreshScheduled;
|
|
private CalendarDropTargetInfo? _hoverTarget;
|
|
private CalendarDragPackage? _activeDragPackage;
|
|
private CalendarDisplayType _lastDisplayMode = CalendarDisplayType.Month;
|
|
private DateOnly _lastDisplayDate = DateOnly.FromDateTime(DateTime.Today);
|
|
private DayOfWeek _lastFirstDayOfWeek = DayOfWeek.Monday;
|
|
private readonly DispatcherQueueTimer _sizeRefreshTimer;
|
|
|
|
[GeneratedDependencyProperty]
|
|
public partial VisibleDateRange? VisibleRange { get; set; }
|
|
|
|
[GeneratedDependencyProperty]
|
|
public partial CalendarSettings? CalendarSettings { get; set; }
|
|
|
|
[GeneratedDependencyProperty]
|
|
public partial IReadOnlyList<CalendarItemViewModel>? CalendarItems { get; set; }
|
|
|
|
[GeneratedDependencyProperty]
|
|
public partial string? TimedHeaderDateFormat { get; set; }
|
|
|
|
[GeneratedDependencyProperty]
|
|
public partial Brush? DefaultHourBackground { get; set; }
|
|
|
|
[GeneratedDependencyProperty]
|
|
public partial Brush? WorkHourBackground { get; set; }
|
|
|
|
[GeneratedDependencyProperty]
|
|
public partial Brush? SelectedSlotBackground { get; set; }
|
|
|
|
[GeneratedDependencyProperty]
|
|
public partial Brush? HoverSlotBackground { get; set; }
|
|
|
|
[GeneratedDependencyProperty]
|
|
public partial DateTime? SelectedDateTime { get; set; }
|
|
|
|
public CalendarPeriodControl()
|
|
{
|
|
InitializeComponent();
|
|
|
|
_sizeRefreshTimer = DispatcherQueue.CreateTimer();
|
|
_sizeRefreshTimer.Interval = SizeRefreshDebounceInterval;
|
|
_sizeRefreshTimer.IsRepeating = false;
|
|
_sizeRefreshTimer.Tick += SizeRefreshTimerTick;
|
|
}
|
|
|
|
public event PropertyChangedEventHandler? PropertyChanged;
|
|
public event EventHandler<CalendarEmptySlotTappedEventArgs>? EmptySlotTapped;
|
|
public event EventHandler<CalendarItemDroppedEventArgs>? CalendarItemDropped;
|
|
|
|
private ObservableCollection<HeaderTextLayout> TimedHeaderTextsCollection { get; } = [];
|
|
private ObservableCollection<HeaderTextLayout> MonthHeaderTextsCollection { get; } = [];
|
|
private ObservableCollection<TimedItemLayout> TimedItemsCollection { get; } = [];
|
|
private ObservableCollection<TimedItemLayout> TimedAllDayItemsCollection { get; } = [];
|
|
private ObservableCollection<MonthCellLabelLayout> MonthCellLabelsCollection { get; } = [];
|
|
private ObservableCollection<MonthItemLayout> MonthItemsCollection { get; } = [];
|
|
|
|
public IEnumerable<HeaderTextLayout> TimedHeaderTexts => TimedHeaderTextsCollection;
|
|
public IEnumerable<HeaderTextLayout> MonthHeaderTexts => MonthHeaderTextsCollection;
|
|
public IEnumerable<TimedItemLayout> TimedItems => TimedItemsCollection;
|
|
public IEnumerable<TimedItemLayout> TimedAllDayItems => TimedAllDayItemsCollection;
|
|
public IEnumerable<MonthCellLabelLayout> MonthCellLabels => MonthCellLabelsCollection;
|
|
public IEnumerable<MonthItemLayout> MonthItems => MonthItemsCollection;
|
|
|
|
public double TimedDayWidth
|
|
{
|
|
get => _timedDayWidth;
|
|
private set
|
|
{
|
|
if (_timedDayWidth == value)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_timedDayWidth = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
public double MonthCellWidth
|
|
{
|
|
get => _monthCellWidth;
|
|
private set
|
|
{
|
|
if (_monthCellWidth == value)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_monthCellWidth = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
public double MonthCellHeight
|
|
{
|
|
get => _monthCellHeight;
|
|
private set
|
|
{
|
|
if (_monthCellHeight == value)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_monthCellHeight = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
public double TimedAllDayHeight
|
|
{
|
|
get => _timedAllDayHeight;
|
|
private set
|
|
{
|
|
if (_timedAllDayHeight == value)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_timedAllDayHeight = value;
|
|
OnPropertyChanged();
|
|
OnPropertyChanged(nameof(HasTimedAllDayItems));
|
|
}
|
|
}
|
|
|
|
public bool HasTimedAllDayItems => TimedAllDayHeight > 0d;
|
|
|
|
public double TimelineHeight => TimedCalendarLayoutCalculator.GetTimelineHeight(GetHourHeight());
|
|
|
|
partial void OnVisibleRangeChanged(VisibleDateRange? newValue) => RequestRefresh();
|
|
partial void OnCalendarSettingsChanged(CalendarSettings? newValue) => RequestRefresh();
|
|
partial void OnTimedHeaderDateFormatChanged(string? newValue) => RequestRefresh();
|
|
partial void OnSelectedSlotBackgroundChanged(Brush? newValue) => InvalidateStructureCanvases();
|
|
partial void OnHoverSlotBackgroundChanged(Brush? newValue) => InvalidateStructureCanvases();
|
|
partial void OnSelectedDateTimeChanged(DateTime? newValue) => InvalidateStructureCanvases();
|
|
|
|
partial void OnCalendarItemsChanged(IReadOnlyList<CalendarItemViewModel>? newValue)
|
|
{
|
|
DetachCurrentItemsSource();
|
|
AttachItemsSource(newValue);
|
|
RequestRefresh();
|
|
}
|
|
|
|
private void ControlSizeChanged(object sender, SizeChangedEventArgs e)
|
|
{
|
|
if (!HasMeaningfulSizeChange(e))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var isLiveResize = _hasPresentedState &&
|
|
e.PreviousSize.Width > 0 &&
|
|
e.PreviousSize.Height > 0;
|
|
|
|
if (isLiveResize)
|
|
{
|
|
_refreshPending = true;
|
|
_sizeRefreshTimer.Stop();
|
|
_sizeRefreshTimer.Start();
|
|
return;
|
|
}
|
|
|
|
if (!_refreshPending)
|
|
{
|
|
return;
|
|
}
|
|
|
|
QueueRefresh();
|
|
}
|
|
|
|
private IEnumerable<CalendarItemViewModel> CurrentItems => CalendarItems ?? [];
|
|
|
|
private void AttachItemsSource(IReadOnlyList<CalendarItemViewModel>? itemsSource)
|
|
{
|
|
if (itemsSource is INotifyCollectionChanged observableItemsSource)
|
|
{
|
|
_observableItemsSource = observableItemsSource;
|
|
_observableItemsSource.CollectionChanged += ItemsSourceCollectionChanged;
|
|
}
|
|
}
|
|
|
|
private void DetachItemsSource(IReadOnlyList<CalendarItemViewModel>? itemsSource)
|
|
{
|
|
var observableItemsSource = itemsSource as INotifyCollectionChanged;
|
|
|
|
if (observableItemsSource is not null)
|
|
{
|
|
observableItemsSource.CollectionChanged -= ItemsSourceCollectionChanged;
|
|
}
|
|
|
|
if (ReferenceEquals(_observableItemsSource, observableItemsSource))
|
|
{
|
|
_observableItemsSource = null;
|
|
}
|
|
}
|
|
|
|
private void DetachCurrentItemsSource()
|
|
{
|
|
if (_observableItemsSource is not null)
|
|
{
|
|
_observableItemsSource.CollectionChanged -= ItemsSourceCollectionChanged;
|
|
_observableItemsSource = null;
|
|
}
|
|
}
|
|
|
|
private void ItemsSourceCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) => RequestRefresh();
|
|
|
|
private void RequestRefresh()
|
|
{
|
|
_refreshPending = true;
|
|
QueueRefresh();
|
|
}
|
|
|
|
private void InvalidateStructureCanvases()
|
|
{
|
|
TimedAllDayCanvas.Invalidate();
|
|
TimedStructureCanvas.Invalidate();
|
|
MonthStructureCanvas.Invalidate();
|
|
}
|
|
|
|
private void Refresh()
|
|
{
|
|
if (!_refreshPending || !IsLoaded || ActualWidth <= 0 || VisibleRange is null || CalendarSettings is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var transition = GetTransitionInfo();
|
|
_currentRange = CreateLayoutRange(VisibleRange, CalendarSettings);
|
|
|
|
if (VisibleRange.DisplayType == CalendarDisplayType.Month)
|
|
{
|
|
RefreshMonthView();
|
|
}
|
|
else
|
|
{
|
|
RefreshTimedView();
|
|
}
|
|
|
|
RunTransition(transition);
|
|
_hasPresentedState = true;
|
|
_refreshPending = false;
|
|
_lastDisplayMode = VisibleRange.DisplayType;
|
|
_lastDisplayDate = VisibleRange.AnchorDate;
|
|
_lastFirstDayOfWeek = CalendarSettings.FirstDayOfWeek;
|
|
|
|
Debug.WriteLine($"Refreshed control.");
|
|
}
|
|
|
|
private static bool HasMeaningfulSizeChange(SizeChangedEventArgs e)
|
|
=> Math.Abs(e.NewSize.Width - e.PreviousSize.Width) > SizeChangeThreshold ||
|
|
Math.Abs(e.NewSize.Height - e.PreviousSize.Height) > SizeChangeThreshold;
|
|
|
|
private void QueueRefresh()
|
|
{
|
|
if (_refreshScheduled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_refreshScheduled = true;
|
|
DispatcherQueue.TryEnqueue(() =>
|
|
{
|
|
_refreshScheduled = false;
|
|
Refresh();
|
|
});
|
|
}
|
|
|
|
private void ControlUnloaded(object sender, RoutedEventArgs e)
|
|
{
|
|
DetachCurrentItemsSource();
|
|
_sizeRefreshTimer.Stop();
|
|
_sizeRefreshTimer.Tick -= SizeRefreshTimerTick;
|
|
}
|
|
|
|
private void SizeRefreshTimerTick(DispatcherQueueTimer sender, object args)
|
|
{
|
|
sender.Stop();
|
|
QueueRefresh();
|
|
}
|
|
|
|
private void RefreshTimedView()
|
|
{
|
|
TimedRoot.Visibility = Visibility.Visible;
|
|
MonthRoot.Visibility = Visibility.Collapsed;
|
|
ResetTimedVisualState();
|
|
|
|
var timedSurfaceWidth = GetTimedSurfaceWidth();
|
|
|
|
TimedDayWidth = _currentRange.Dates.Count == 0 ? 0d : timedSurfaceWidth / _currentRange.Dates.Count;
|
|
TimedAllDayHeight = TimedCalendarLayoutCalculator.GetAllDayHeight(
|
|
TimedCalendarLayoutCalculator.GetAllDayLaneCount(_currentRange.Dates, CurrentItems));
|
|
TimedScrollContentGrid.Width = ActualWidth;
|
|
TimedViewport.Width = timedSurfaceWidth;
|
|
TimedViewport.Height = TimelineHeight;
|
|
TimedAllDayHost.Width = timedSurfaceWidth;
|
|
TimedAllDayItemsCanvas.Width = timedSurfaceWidth;
|
|
TimedAllDayItemsCanvas.Height = TimedAllDayHeight;
|
|
|
|
_timedLayout = TimedCalendarLayoutCalculator.Calculate(_currentRange, CurrentItems, timedSurfaceWidth, GetHourHeight());
|
|
|
|
ReplaceCollection(
|
|
TimedHeaderTextsCollection,
|
|
_timedLayout.VisibleDates.Select(date =>
|
|
new HeaderTextLayout(
|
|
GetTimedHeaderText(date),
|
|
TimedDayWidth)));
|
|
|
|
var eventTemplate = (DataTemplate)Resources["CalendarEventTemplate"];
|
|
|
|
ReplaceCollection(TimedAllDayItemsCollection, TimedCalendarLayoutCalculator.CalculateAllDayItems(_currentRange, CurrentItems, timedSurfaceWidth).Select(item =>
|
|
{
|
|
PrepareDisplayMetadata(item.Item, item.Date);
|
|
item.Template = eventTemplate;
|
|
return item;
|
|
}));
|
|
|
|
ReplaceCollection(TimedItemsCollection, _timedLayout.Items.Select(item =>
|
|
{
|
|
PrepareDisplayMetadata(item.Item, item.Date);
|
|
item.Template = eventTemplate;
|
|
return item;
|
|
}));
|
|
RenderHourLabels();
|
|
RenderTimedAllDayItems();
|
|
RenderTimedItems();
|
|
|
|
TimedHeaderCanvas.Invalidate();
|
|
TimedStructureCanvas.Invalidate();
|
|
TimedAllDayCanvas.Invalidate();
|
|
|
|
TimedAllDayHost.InvalidateArrange();
|
|
}
|
|
|
|
private void RefreshMonthView()
|
|
{
|
|
TimedRoot.Visibility = Visibility.Collapsed;
|
|
MonthRoot.Visibility = Visibility.Visible;
|
|
|
|
var availableMonthHeight = Math.Max(0d, ActualHeight - MonthHeadersItemsControl.ActualHeight);
|
|
_monthLayout = MonthCalendarLayoutCalculator.Calculate(_currentRange, CurrentItems, ActualWidth, availableMonthHeight);
|
|
|
|
MonthCellWidth = _monthLayout.CellWidth;
|
|
MonthCellHeight = _monthLayout.CellHeight;
|
|
|
|
MonthViewport.Width = ActualWidth;
|
|
MonthViewport.Height = availableMonthHeight;
|
|
|
|
ReplaceCollection(
|
|
MonthHeaderTextsCollection,
|
|
Enumerable.Range(0, MonthCalendarLayoutCalculator.ColumnCount)
|
|
.Select(index =>
|
|
{
|
|
var day = (DayOfWeek)(((int)CalendarSettings!.FirstDayOfWeek + index) % 7);
|
|
return new HeaderTextLayout(
|
|
CalendarSettings.CultureInfo.DateTimeFormat.AbbreviatedDayNames[(int)day],
|
|
MonthCellWidth);
|
|
}));
|
|
|
|
ReplaceCollection(
|
|
MonthCellLabelsCollection,
|
|
_monthLayout.Cells.Select(cell =>
|
|
new MonthCellLabelLayout(
|
|
cell.Date.Day.ToString(CalendarSettings!.CultureInfo),
|
|
cell.Date.Month == VisibleRange!.AnchorDate.Month && cell.Date.Year == VisibleRange.AnchorDate.Year ? 1d : 0.55d,
|
|
new LayoutRect(cell.Bounds.X + 4, cell.Bounds.Y + 2, cell.Bounds.Width, cell.Bounds.Height))));
|
|
|
|
var monthEventTemplate = (DataTemplate)Resources["MonthEventTemplate"];
|
|
ReplaceCollection(MonthItemsCollection, _monthLayout.Items.Select(item =>
|
|
{
|
|
PrepareDisplayMetadata(item.Item, item.Date);
|
|
item.Template = monthEventTemplate;
|
|
return item;
|
|
}));
|
|
RenderMonthCellLabels();
|
|
RenderMonthItems();
|
|
|
|
MonthStructureCanvas.Invalidate();
|
|
}
|
|
|
|
private void PrepareDisplayMetadata(CalendarItemViewModel item, DateOnly date)
|
|
{
|
|
if (CalendarSettings is null || item is not ICalendarItemViewModel calendarItemViewModel)
|
|
{
|
|
return;
|
|
}
|
|
|
|
DispatcherQueue.TryEnqueue(() =>
|
|
{
|
|
calendarItemViewModel.DisplayingPeriod = new TimeRange(
|
|
date.ToDateTime(TimeOnly.MinValue),
|
|
date.AddDays(1).ToDateTime(TimeOnly.MinValue));
|
|
calendarItemViewModel.CalendarSettings = CalendarSettings;
|
|
});
|
|
}
|
|
|
|
private static VisibleDateRange CreateLayoutRange(VisibleDateRange visibleRange, CalendarSettings calendarSettings)
|
|
{
|
|
if (visibleRange.DisplayType != CalendarDisplayType.Month)
|
|
{
|
|
return visibleRange;
|
|
}
|
|
|
|
var start = AlignToWeekStart(visibleRange.StartDate, calendarSettings.FirstDayOfWeek);
|
|
var end = AlignToWeekEnd(visibleRange.EndDate, calendarSettings.FirstDayOfWeek);
|
|
var totalDays = end.DayNumber - start.DayNumber + 1;
|
|
|
|
if (totalDays <= 35)
|
|
{
|
|
end = start.AddDays(34);
|
|
}
|
|
else if (totalDays < 42)
|
|
{
|
|
end = start.AddDays(41);
|
|
}
|
|
|
|
var dates = Enumerable.Range(0, end.DayNumber - start.DayNumber + 1)
|
|
.Select(offset => start.AddDays(offset))
|
|
.ToArray();
|
|
|
|
return new VisibleDateRange(
|
|
visibleRange.DisplayType,
|
|
visibleRange.AnchorDate,
|
|
start,
|
|
end,
|
|
visibleRange.PrimaryDate,
|
|
dates.Length,
|
|
dates.Contains(DateOnly.FromDateTime(DateTime.Today)),
|
|
start.Year == end.Year && start.Month == end.Month,
|
|
dates);
|
|
}
|
|
|
|
private static DateOnly AlignToWeekStart(DateOnly date, DayOfWeek firstDayOfWeek)
|
|
{
|
|
var offset = ((int)date.DayOfWeek - (int)firstDayOfWeek + 7) % 7;
|
|
return date.AddDays(-offset);
|
|
}
|
|
|
|
private static DateOnly AlignToWeekEnd(DateOnly date, DayOfWeek firstDayOfWeek)
|
|
{
|
|
var lastDayOfWeek = (DayOfWeek)(((int)firstDayOfWeek + 6) % 7);
|
|
var offset = ((int)lastDayOfWeek - (int)date.DayOfWeek + 7) % 7;
|
|
return date.AddDays(offset);
|
|
}
|
|
|
|
private void TimedHeaderCanvasPaintSurface(object? sender, SKPaintSurfaceEventArgs e)
|
|
{
|
|
using var borderPaint = CreateLinePaint();
|
|
var canvas = e.Surface.Canvas;
|
|
canvas.Clear(SKColors.Transparent);
|
|
|
|
var timedSurfaceWidth = GetTimedSurfaceWidth();
|
|
|
|
if (_timedLayout.VisibleDates.Count == 0 || timedSurfaceWidth <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var scaleX = (float)(e.Info.Width / timedSurfaceWidth);
|
|
var height = e.Info.Height;
|
|
var dayWidth = (float)(_timedLayout.DayWidth * scaleX);
|
|
|
|
for (var index = 1; index < _timedLayout.VisibleDates.Count; index++)
|
|
{
|
|
var x = dayWidth * index;
|
|
canvas.DrawLine(x, 0, x, height, borderPaint);
|
|
}
|
|
|
|
canvas.DrawLine(0, height - 1, e.Info.Width, height - 1, borderPaint);
|
|
}
|
|
|
|
private void TimedAllDayCanvasPaintSurface(object? sender, SKPaintSurfaceEventArgs e)
|
|
{
|
|
using var borderPaint = CreateLinePaint();
|
|
using var hoverFillPaint = CreateFillPaint(HoverSlotBackground ?? new SolidColorBrush(Colors.Transparent));
|
|
var canvas = e.Surface.Canvas;
|
|
canvas.Clear(SKColors.Transparent);
|
|
|
|
var timedSurfaceWidth = GetTimedSurfaceWidth();
|
|
|
|
if (_timedLayout.VisibleDates.Count == 0 || timedSurfaceWidth <= 0 || TimedAllDayHeight <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var scaleX = (float)(e.Info.Width / timedSurfaceWidth);
|
|
var height = e.Info.Height;
|
|
var dayWidth = (float)(_timedLayout.DayWidth * scaleX);
|
|
|
|
var hoveredTimedAllDayRect = GetHoveredTimedAllDayRect(dayWidth, height);
|
|
if (hoveredTimedAllDayRect.HasValue && hoverFillPaint.Color.Alpha > 0)
|
|
{
|
|
canvas.DrawRect(hoveredTimedAllDayRect.Value, hoverFillPaint);
|
|
}
|
|
|
|
for (var index = 1; index < _timedLayout.VisibleDates.Count; index++)
|
|
{
|
|
var x = dayWidth * index;
|
|
canvas.DrawLine(x, 0, x, height, borderPaint);
|
|
}
|
|
|
|
canvas.DrawLine(0, height - 1, e.Info.Width, height - 1, borderPaint);
|
|
}
|
|
|
|
private void TimedStructureCanvasPaintSurface(object? sender, SKPaintSurfaceEventArgs e)
|
|
{
|
|
using var linePaint = CreateLinePaint();
|
|
using var minorLinePaint = CreateMinorLinePaint();
|
|
using var defaultFillPaint = CreateFillPaint(DefaultHourBackground ?? new SolidColorBrush(Colors.Transparent));
|
|
using var workFillPaint = CreateFillPaint(WorkHourBackground ?? new SolidColorBrush(Colors.Transparent));
|
|
using var selectedFillPaint = CreateFillPaint(SelectedSlotBackground ?? new SolidColorBrush(Colors.Transparent));
|
|
using var hoverFillPaint = CreateFillPaint(HoverSlotBackground ?? new SolidColorBrush(Colors.Transparent));
|
|
var canvas = e.Surface.Canvas;
|
|
canvas.Clear(SKColors.Transparent);
|
|
|
|
var timedSurfaceWidth = GetTimedSurfaceWidth();
|
|
|
|
if (_timedLayout.VisibleDates.Count == 0 || timedSurfaceWidth <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var hourHeight = GetHourHeight();
|
|
var timelineHeight = TimedCalendarLayoutCalculator.GetTimelineHeight(hourHeight);
|
|
var scaleX = (float)(e.Info.Width / timedSurfaceWidth);
|
|
var scaleY = (float)(e.Info.Height / timelineHeight);
|
|
var dayWidth = (float)(_timedLayout.DayWidth * scaleX);
|
|
var isWorkingHoursEnabled = CalendarSettings?.IsWorkingHoursEnabled == true;
|
|
var workDayStartHour = CalendarSettings?.WorkingHourStart.TotalHours ?? 9d;
|
|
var workDayEndHour = CalendarSettings?.WorkingHourEnd.TotalHours ?? 17d;
|
|
var intervalHeight = (float)(GetTimedGridIntervalHeight() * scaleY);
|
|
var intervalCount = (int)(24d * 60d / TimedGridIntervalMinutes);
|
|
|
|
for (var dayIndex = 0; dayIndex < _timedLayout.VisibleDates.Count; dayIndex++)
|
|
{
|
|
var x = dayIndex * dayWidth;
|
|
var isWorkingDay = isWorkingHoursEnabled &&
|
|
CalendarSettings?.WorkingDays.Contains(_timedLayout.VisibleDates[dayIndex].DayOfWeek) == true;
|
|
|
|
for (var intervalIndex = 0; intervalIndex < intervalCount; intervalIndex++)
|
|
{
|
|
var intervalStartHour = (intervalIndex * TimedGridIntervalMinutes) / 60d;
|
|
var y = intervalIndex * intervalHeight;
|
|
var fillPaint = isWorkingDay && intervalStartHour >= workDayStartHour && intervalStartHour < workDayEndHour
|
|
? workFillPaint
|
|
: defaultFillPaint;
|
|
canvas.DrawRect(x, y, dayWidth, intervalHeight, fillPaint);
|
|
}
|
|
}
|
|
|
|
var hoveredTimedSlotRect = GetHoveredTimedSlotRect(dayWidth, intervalHeight, intervalCount);
|
|
if (hoveredTimedSlotRect.HasValue && hoverFillPaint.Color.Alpha > 0)
|
|
{
|
|
canvas.DrawRect(hoveredTimedSlotRect.Value, hoverFillPaint);
|
|
}
|
|
|
|
var selectedTimedSlotRect = GetSelectedTimedSlotRect(dayWidth, intervalHeight, intervalCount);
|
|
if (selectedTimedSlotRect.HasValue && selectedFillPaint.Color.Alpha > 0)
|
|
{
|
|
canvas.DrawRect(selectedTimedSlotRect.Value, selectedFillPaint);
|
|
}
|
|
|
|
for (var intervalIndex = 0; intervalIndex <= intervalCount; intervalIndex++)
|
|
{
|
|
var y = intervalIndex * intervalHeight;
|
|
var paint = intervalIndex % 2 == 0 ? linePaint : minorLinePaint;
|
|
canvas.DrawLine(0, y, e.Info.Width, y, paint);
|
|
}
|
|
|
|
for (var index = 0; index <= _timedLayout.VisibleDates.Count; index++)
|
|
{
|
|
var x = dayWidth * index;
|
|
canvas.DrawLine(x, 0, x, e.Info.Height, linePaint);
|
|
}
|
|
}
|
|
|
|
private void MonthStructureCanvasPaintSurface(object? sender, SKPaintSurfaceEventArgs e)
|
|
{
|
|
using var linePaint = CreateLinePaint();
|
|
using var todayPaint = new SKPaint
|
|
{
|
|
Style = SKPaintStyle.Fill,
|
|
Color = new SKColor(0, 120, 215, 26),
|
|
IsAntialias = true
|
|
};
|
|
using var selectedPaint = CreateFillPaint(SelectedSlotBackground ?? new SolidColorBrush(Colors.Transparent));
|
|
using var hoverPaint = CreateFillPaint(HoverSlotBackground ?? new SolidColorBrush(Colors.Transparent));
|
|
|
|
var canvas = e.Surface.Canvas;
|
|
canvas.Clear(SKColors.Transparent);
|
|
|
|
if (_monthLayout.CellWidth <= 0 || _monthLayout.CellHeight <= 0 || MonthViewport.ActualWidth <= 0 || MonthViewport.ActualHeight <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var cellWidth = (float)(e.Info.Width / MonthCalendarLayoutCalculator.ColumnCount);
|
|
var cellHeight = (float)(e.Info.Height / MonthCalendarLayoutCalculator.RowCount);
|
|
var today = DateOnly.FromDateTime(DateTime.Now.Date);
|
|
|
|
foreach (var cell in _monthLayout.Cells)
|
|
{
|
|
if (cell.Date != today)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
canvas.DrawRect((float)cell.Bounds.X, (float)cell.Bounds.Y, (float)cell.Bounds.Width, (float)cell.Bounds.Height, todayPaint);
|
|
}
|
|
|
|
var hoveredMonthCellRect = GetHoveredMonthCellRect();
|
|
if (hoveredMonthCellRect.HasValue && hoverPaint.Color.Alpha > 0)
|
|
{
|
|
canvas.DrawRect(hoveredMonthCellRect.Value, hoverPaint);
|
|
}
|
|
|
|
var selectedMonthCellRect = GetSelectedMonthCellRect();
|
|
if (selectedMonthCellRect.HasValue && selectedPaint.Color.Alpha > 0)
|
|
{
|
|
canvas.DrawRect(selectedMonthCellRect.Value, selectedPaint);
|
|
}
|
|
|
|
for (var row = 0; row <= MonthCalendarLayoutCalculator.RowCount; row++)
|
|
{
|
|
var y = row * cellHeight;
|
|
canvas.DrawLine(0, y, e.Info.Width, y, linePaint);
|
|
}
|
|
|
|
for (var column = 0; column <= MonthCalendarLayoutCalculator.ColumnCount; column++)
|
|
{
|
|
var x = column * cellWidth;
|
|
canvas.DrawLine(x, 0, x, e.Info.Height, linePaint);
|
|
}
|
|
}
|
|
|
|
private static void ReplaceCollection<T>(ObservableCollection<T> target, IEnumerable<T> items)
|
|
{
|
|
target.Clear();
|
|
|
|
foreach (var item in items)
|
|
{
|
|
target.Add(item);
|
|
}
|
|
}
|
|
|
|
private void RenderHourLabels()
|
|
{
|
|
HourLabelsCanvas.Children.Clear();
|
|
HourLabelsCanvas.Height = TimelineHeight;
|
|
|
|
var hourHeight = GetHourHeight();
|
|
var labelWidth = Math.Max(0d, TimedHourColumnWidth - 10d);
|
|
|
|
for (var hour = 0; hour <= 24; hour++)
|
|
{
|
|
var textBlock = new TextBlock
|
|
{
|
|
Width = labelWidth,
|
|
Text = GetTimedHourLabelText(hour),
|
|
TextAlignment = TextAlignment.Right,
|
|
Opacity = 0.72
|
|
};
|
|
|
|
var y = hour == 24
|
|
? Math.Max(0d, TimelineHeight - 20d)
|
|
: Math.Max(0d, (hour * hourHeight) - 10d);
|
|
|
|
Canvas.SetLeft(textBlock, 0d);
|
|
Canvas.SetTop(textBlock, y);
|
|
HourLabelsCanvas.Children.Add(textBlock);
|
|
}
|
|
}
|
|
|
|
private void RenderTimedItems()
|
|
{
|
|
TimedItemsCanvas.Children.Clear();
|
|
|
|
foreach (var item in TimedItemsCollection)
|
|
{
|
|
var presenter = new ContentPresenter
|
|
{
|
|
Width = Math.Max(0d, item.Bounds.Width - TimedItemRightSpacing),
|
|
Height = item.Bounds.Height,
|
|
Content = item.Item,
|
|
ContentTemplate = item.Template
|
|
};
|
|
|
|
Canvas.SetLeft(presenter, item.Bounds.X);
|
|
Canvas.SetTop(presenter, item.Bounds.Y);
|
|
TimedItemsCanvas.Children.Add(presenter);
|
|
}
|
|
}
|
|
|
|
private void RenderTimedAllDayItems()
|
|
{
|
|
TimedAllDayItemsCanvas.Children.Clear();
|
|
|
|
foreach (var item in TimedAllDayItemsCollection)
|
|
{
|
|
var presenter = new ContentPresenter
|
|
{
|
|
Width = item.Bounds.Width,
|
|
Height = item.Bounds.Height,
|
|
Content = item.Item,
|
|
ContentTemplate = item.Template
|
|
};
|
|
|
|
Canvas.SetLeft(presenter, item.Bounds.X);
|
|
Canvas.SetTop(presenter, item.Bounds.Y);
|
|
TimedAllDayItemsCanvas.Children.Add(presenter);
|
|
}
|
|
}
|
|
|
|
private void RenderMonthCellLabels()
|
|
{
|
|
MonthCellLabelsCanvas.Children.Clear();
|
|
|
|
foreach (var label in MonthCellLabelsCollection)
|
|
{
|
|
var textBlock = new TextBlock
|
|
{
|
|
Width = label.Bounds.Width,
|
|
Height = label.Bounds.Height,
|
|
Opacity = label.LabelOpacity,
|
|
Text = label.DayText
|
|
};
|
|
|
|
Canvas.SetLeft(textBlock, label.Bounds.X);
|
|
Canvas.SetTop(textBlock, label.Bounds.Y);
|
|
MonthCellLabelsCanvas.Children.Add(textBlock);
|
|
}
|
|
}
|
|
|
|
private void RenderMonthItems()
|
|
{
|
|
MonthItemsCanvas.Children.Clear();
|
|
|
|
foreach (var item in MonthItemsCollection)
|
|
{
|
|
var presenter = new ContentPresenter
|
|
{
|
|
Width = item.Bounds.Width,
|
|
Height = item.Bounds.Height,
|
|
Content = item.Item,
|
|
ContentTemplate = item.Template
|
|
};
|
|
|
|
Canvas.SetLeft(presenter, item.Bounds.X);
|
|
Canvas.SetTop(presenter, item.Bounds.Y);
|
|
MonthItemsCanvas.Children.Add(presenter);
|
|
}
|
|
}
|
|
|
|
private void TimedInteractionLayerTapped(object sender, TappedRoutedEventArgs e)
|
|
{
|
|
if (_timedLayout.VisibleDates.Count == 0 || _timedLayout.DayWidth <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var position = e.GetPosition(TimedViewport);
|
|
var dayIndex = Math.Clamp((int)(position.X / _timedLayout.DayWidth), 0, _timedLayout.VisibleDates.Count - 1);
|
|
var intervalHeight = GetTimedSelectionIntervalHeight();
|
|
var slotIndex = Math.Clamp((int)(position.Y / intervalHeight), 0, (int)((24d * 60d / TimedSelectionIntervalMinutes) - 1));
|
|
var slotStart = TimeSpan.FromMinutes(slotIndex * TimedSelectionIntervalMinutes);
|
|
var clickedDate = _timedLayout.VisibleDates[dayIndex].ToDateTime(TimeOnly.MinValue).Add(slotStart);
|
|
var anchorPoint = TimedViewport.TransformToVisual(Root).TransformPoint(position);
|
|
|
|
EmptySlotTapped?.Invoke(
|
|
this,
|
|
new CalendarEmptySlotTappedEventArgs(
|
|
clickedDate,
|
|
anchorPoint,
|
|
new Size(_timedLayout.DayWidth, intervalHeight)));
|
|
}
|
|
|
|
private void MonthInteractionLayerTapped(object sender, TappedRoutedEventArgs e)
|
|
{
|
|
if (_monthLayout.Cells.Count == 0 || _monthLayout.CellWidth <= 0 || _monthLayout.CellHeight <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var position = e.GetPosition(MonthViewport);
|
|
var column = Math.Clamp((int)(position.X / _monthLayout.CellWidth), 0, MonthCalendarLayoutCalculator.ColumnCount - 1);
|
|
var row = Math.Clamp((int)(position.Y / _monthLayout.CellHeight), 0, MonthCalendarLayoutCalculator.RowCount - 1);
|
|
var cellIndex = Math.Clamp((row * MonthCalendarLayoutCalculator.ColumnCount) + column, 0, _monthLayout.Cells.Count - 1);
|
|
var cell = _monthLayout.Cells[cellIndex];
|
|
var anchorPoint = MonthViewport.TransformToVisual(Root).TransformPoint(position);
|
|
|
|
EmptySlotTapped?.Invoke(
|
|
this,
|
|
new CalendarEmptySlotTappedEventArgs(
|
|
cell.Date.ToDateTime(TimeOnly.MinValue),
|
|
anchorPoint,
|
|
new Size(cell.Bounds.Width, cell.Bounds.Height)));
|
|
}
|
|
|
|
private void TimedViewportPointerMoved(object sender, PointerRoutedEventArgs e)
|
|
=> UpdateHoverTargetForActiveDrag(() => ResolveTimedDropTarget(e.GetCurrentPoint(TimedViewport).Position, _activeDragPackage?.CalendarItemViewModel));
|
|
|
|
private void TimedAllDayHostPointerMoved(object sender, PointerRoutedEventArgs e)
|
|
=> UpdateHoverTargetForActiveDrag(() => ResolveTimedAllDayDropTarget(e.GetCurrentPoint(TimedAllDayHost).Position, _activeDragPackage?.CalendarItemViewModel));
|
|
|
|
private void MonthViewportPointerMoved(object sender, PointerRoutedEventArgs e)
|
|
=> UpdateHoverTargetForActiveDrag(() => ResolveMonthDropTarget(e.GetCurrentPoint(MonthViewport).Position, _activeDragPackage?.CalendarItemViewModel));
|
|
|
|
private void CalendarDropTargetPointerExited(object sender, PointerRoutedEventArgs e)
|
|
{
|
|
if (_activeDragPackage == null)
|
|
{
|
|
SetHoverTarget(null);
|
|
}
|
|
}
|
|
|
|
private void UpdateHoverTargetForActiveDrag(Func<CalendarDropTargetInfo?> resolveHoverTarget)
|
|
{
|
|
if (_activeDragPackage == null)
|
|
{
|
|
SetHoverTarget(null);
|
|
return;
|
|
}
|
|
|
|
SetHoverTarget(resolveHoverTarget());
|
|
}
|
|
|
|
private void TimedViewportDragOver(object sender, DragEventArgs e)
|
|
{
|
|
if (!TryGetDragPackage(e, out var dragPackage))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var hoverTarget = ResolveTimedDropTarget(e.GetPosition(TimedViewport), dragPackage.CalendarItemViewModel);
|
|
|
|
UpdateDragOverState(e, dragPackage, hoverTarget);
|
|
}
|
|
|
|
private void TimedAllDayHostDragOver(object sender, DragEventArgs e)
|
|
{
|
|
if (!TryGetDragPackage(e, out var dragPackage))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var hoverTarget = ResolveTimedAllDayDropTarget(e.GetPosition(TimedAllDayHost), dragPackage.CalendarItemViewModel);
|
|
|
|
UpdateDragOverState(e, dragPackage, hoverTarget);
|
|
}
|
|
|
|
private void MonthViewportDragOver(object sender, DragEventArgs e)
|
|
{
|
|
if (!TryGetDragPackage(e, out var dragPackage))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var hoverTarget = ResolveMonthDropTarget(e.GetPosition(MonthViewport), dragPackage.CalendarItemViewModel);
|
|
|
|
UpdateDragOverState(e, dragPackage, hoverTarget);
|
|
}
|
|
|
|
private void TimedViewportDrop(object sender, DragEventArgs e)
|
|
=> HandleDrop(e, ResolveTimedDropTarget(e.GetPosition(TimedViewport), _activeDragPackage?.CalendarItemViewModel));
|
|
|
|
private void TimedAllDayHostDrop(object sender, DragEventArgs e)
|
|
=> HandleDrop(e, ResolveTimedAllDayDropTarget(e.GetPosition(TimedAllDayHost), _activeDragPackage?.CalendarItemViewModel));
|
|
|
|
private void MonthViewportDrop(object sender, DragEventArgs e)
|
|
=> HandleDrop(e, ResolveMonthDropTarget(e.GetPosition(MonthViewport), _activeDragPackage?.CalendarItemViewModel));
|
|
|
|
private void CalendarDropTargetDragLeave(object sender, DragEventArgs e)
|
|
{
|
|
_activeDragPackage = null;
|
|
SetHoverTarget(null);
|
|
}
|
|
|
|
private bool TryGetDragPackage(DragEventArgs e, out CalendarDragPackage dragPackage)
|
|
{
|
|
dragPackage = null;
|
|
|
|
if (!e.DataView.Properties.ContainsKey(nameof(CalendarDragPackage)))
|
|
{
|
|
e.AcceptedOperation = DataPackageOperation.None;
|
|
_activeDragPackage = null;
|
|
SetHoverTarget(null);
|
|
return false;
|
|
}
|
|
|
|
dragPackage = e.DataView.Properties[nameof(CalendarDragPackage)] as CalendarDragPackage;
|
|
|
|
if (dragPackage?.CalendarItemViewModel?.CanDragDrop != true)
|
|
{
|
|
e.AcceptedOperation = DataPackageOperation.None;
|
|
_activeDragPackage = null;
|
|
SetHoverTarget(null);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private void UpdateDragOverState(DragEventArgs e, CalendarDragPackage dragPackage, CalendarDropTargetInfo? hoverTarget)
|
|
{
|
|
_activeDragPackage = dragPackage;
|
|
SetHoverTarget(hoverTarget);
|
|
|
|
if (hoverTarget.HasValue)
|
|
{
|
|
e.AcceptedOperation = DataPackageOperation.Move;
|
|
}
|
|
else
|
|
{
|
|
e.AcceptedOperation = DataPackageOperation.None;
|
|
}
|
|
}
|
|
|
|
private void HandleDrop(DragEventArgs e, CalendarDropTargetInfo? hoverTarget)
|
|
{
|
|
try
|
|
{
|
|
if (_activeDragPackage?.CalendarItemViewModel?.CanDragDrop != true || !hoverTarget.HasValue)
|
|
{
|
|
e.AcceptedOperation = DataPackageOperation.None;
|
|
return;
|
|
}
|
|
|
|
e.AcceptedOperation = DataPackageOperation.Move;
|
|
CalendarItemDropped?.Invoke(
|
|
this,
|
|
new CalendarItemDroppedEventArgs(
|
|
_activeDragPackage.CalendarItemViewModel,
|
|
hoverTarget.Value.TargetStart,
|
|
hoverTarget.Value.Kind));
|
|
}
|
|
finally
|
|
{
|
|
_activeDragPackage = null;
|
|
SetHoverTarget(null);
|
|
}
|
|
}
|
|
|
|
private CalendarDropTargetInfo? ResolveTimedDropTarget(Point position, CalendarItemViewModel? draggedItem)
|
|
{
|
|
if (draggedItem?.IsAllDayEvent == true ||
|
|
_timedLayout.VisibleDates.Count == 0 ||
|
|
_timedLayout.DayWidth <= 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var dayIndex = Math.Clamp((int)(position.X / _timedLayout.DayWidth), 0, _timedLayout.VisibleDates.Count - 1);
|
|
var intervalHeight = GetTimedSelectionIntervalHeight();
|
|
var slotIndex = Math.Clamp((int)(position.Y / intervalHeight), 0, (int)((24d * 60d / TimedSelectionIntervalMinutes) - 1));
|
|
var date = _timedLayout.VisibleDates[dayIndex];
|
|
var slotStart = TimeSpan.FromMinutes(slotIndex * TimedSelectionIntervalMinutes);
|
|
|
|
return new CalendarDropTargetInfo(
|
|
CalendarDropTargetKind.TimedSlot,
|
|
date,
|
|
dayIndex,
|
|
slotIndex,
|
|
date.ToDateTime(TimeOnly.MinValue).Add(slotStart));
|
|
}
|
|
|
|
private CalendarDropTargetInfo? ResolveTimedAllDayDropTarget(Point position, CalendarItemViewModel? draggedItem)
|
|
{
|
|
if (draggedItem is { IsAllDayEvent: false } ||
|
|
_timedLayout.VisibleDates.Count == 0 ||
|
|
_timedLayout.DayWidth <= 0 ||
|
|
TimedAllDayHeight <= 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var dayIndex = Math.Clamp((int)(position.X / _timedLayout.DayWidth), 0, _timedLayout.VisibleDates.Count - 1);
|
|
var date = _timedLayout.VisibleDates[dayIndex];
|
|
|
|
return new CalendarDropTargetInfo(
|
|
CalendarDropTargetKind.TimedAllDay,
|
|
date,
|
|
dayIndex,
|
|
-1,
|
|
date.ToDateTime(TimeOnly.MinValue));
|
|
}
|
|
|
|
private CalendarDropTargetInfo? ResolveMonthDropTarget(Point position, CalendarItemViewModel? draggedItem)
|
|
{
|
|
if (_monthLayout.Cells.Count == 0 || _monthLayout.CellWidth <= 0 || _monthLayout.CellHeight <= 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var column = Math.Clamp((int)(position.X / _monthLayout.CellWidth), 0, MonthCalendarLayoutCalculator.ColumnCount - 1);
|
|
var row = Math.Clamp((int)(position.Y / _monthLayout.CellHeight), 0, MonthCalendarLayoutCalculator.RowCount - 1);
|
|
var cellIndex = Math.Clamp((row * MonthCalendarLayoutCalculator.ColumnCount) + column, 0, _monthLayout.Cells.Count - 1);
|
|
var cell = _monthLayout.Cells[cellIndex];
|
|
var targetStart = cell.Date.ToDateTime(TimeOnly.MinValue);
|
|
|
|
if (draggedItem is { IsAllDayEvent: false })
|
|
{
|
|
targetStart = targetStart.Add(draggedItem.StartDate.TimeOfDay);
|
|
}
|
|
|
|
return new CalendarDropTargetInfo(
|
|
CalendarDropTargetKind.MonthCell,
|
|
cell.Date,
|
|
-1,
|
|
-1,
|
|
targetStart);
|
|
}
|
|
|
|
private void SetHoverTarget(CalendarDropTargetInfo? hoverTarget)
|
|
{
|
|
if (_hoverTarget == hoverTarget)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_hoverTarget = hoverTarget;
|
|
InvalidateStructureCanvases();
|
|
}
|
|
|
|
private SKRect? GetSelectedTimedSlotRect(float dayWidth, float intervalHeight, int intervalCount)
|
|
{
|
|
if (SelectedDateTime is not DateTime selectedDateTime || _timedLayout.VisibleDates.Count == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var dayIndex = FindVisibleDateIndex(DateOnly.FromDateTime(selectedDateTime));
|
|
if (dayIndex < 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var slotIndex = (int)Math.Floor(selectedDateTime.TimeOfDay.TotalMinutes / TimedSelectionIntervalMinutes);
|
|
slotIndex = Math.Clamp(slotIndex, 0, intervalCount - 1);
|
|
|
|
var x = dayIndex * dayWidth;
|
|
var y = slotIndex * intervalHeight;
|
|
return new SKRect(x, y, x + dayWidth, y + intervalHeight);
|
|
}
|
|
|
|
private SKRect? GetHoveredTimedSlotRect(float dayWidth, float intervalHeight, int intervalCount)
|
|
{
|
|
if (_hoverTarget is not { Kind: CalendarDropTargetKind.TimedSlot } hoverTarget)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var slotIndex = Math.Clamp(hoverTarget.SlotIndex, 0, intervalCount - 1);
|
|
var x = hoverTarget.DayIndex * dayWidth;
|
|
var y = slotIndex * intervalHeight;
|
|
return new SKRect(x, y, x + dayWidth, y + intervalHeight);
|
|
}
|
|
|
|
private SKRect? GetHoveredTimedAllDayRect(float dayWidth, float height)
|
|
{
|
|
if (_hoverTarget is not { Kind: CalendarDropTargetKind.TimedAllDay } hoverTarget)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var x = hoverTarget.DayIndex * dayWidth;
|
|
return new SKRect(x, 0, x + dayWidth, height);
|
|
}
|
|
|
|
private SKRect? GetSelectedMonthCellRect()
|
|
{
|
|
if (SelectedDateTime is not DateTime selectedDateTime)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var selectedDate = DateOnly.FromDateTime(selectedDateTime);
|
|
foreach (var cell in _monthLayout.Cells)
|
|
{
|
|
if (cell.Date != selectedDate)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
return new SKRect(
|
|
(float)cell.Bounds.X,
|
|
(float)cell.Bounds.Y,
|
|
(float)(cell.Bounds.X + cell.Bounds.Width),
|
|
(float)(cell.Bounds.Y + cell.Bounds.Height));
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private SKRect? GetHoveredMonthCellRect()
|
|
{
|
|
if (_hoverTarget is not { Kind: CalendarDropTargetKind.MonthCell } hoverTarget)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
foreach (var cell in _monthLayout.Cells)
|
|
{
|
|
if (cell.Date != hoverTarget.Date)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
return new SKRect(
|
|
(float)cell.Bounds.X,
|
|
(float)cell.Bounds.Y,
|
|
(float)(cell.Bounds.X + cell.Bounds.Width),
|
|
(float)(cell.Bounds.Y + cell.Bounds.Height));
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private int FindVisibleDateIndex(DateOnly date)
|
|
{
|
|
for (var index = 0; index < _timedLayout.VisibleDates.Count; index++)
|
|
{
|
|
if (_timedLayout.VisibleDates[index] == date)
|
|
{
|
|
return index;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
private double GetTimedSurfaceWidth() => Math.Max(0d, ActualWidth - TimedHourColumnWidth);
|
|
|
|
private string GetTimedHeaderText(DateOnly date)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(TimedHeaderDateFormat) && CalendarSettings is not null)
|
|
{
|
|
try
|
|
{
|
|
return date.ToDateTime(TimeOnly.MinValue).ToString(TimedHeaderDateFormat, CalendarSettings.CultureInfo);
|
|
}
|
|
catch (FormatException)
|
|
{
|
|
}
|
|
}
|
|
|
|
return CalendarSettings?.GetTimedDayHeaderText(date) ?? date.ToDateTime(TimeOnly.MinValue).ToString("ddd dd");
|
|
}
|
|
|
|
private string GetTimedHourLabelText(int hour)
|
|
=> CalendarSettings?.GetTimedHourLabelText(hour) ?? $"{hour:00}:00";
|
|
|
|
private CalendarTransitionInfo GetTransitionInfo()
|
|
{
|
|
if (!_hasPresentedState || VisibleRange is null || CalendarSettings is null)
|
|
{
|
|
return new CalendarTransitionInfo(CalendarTransitionKind.None, 0);
|
|
}
|
|
|
|
if (_lastDisplayMode != VisibleRange.DisplayType)
|
|
{
|
|
return new CalendarTransitionInfo(CalendarTransitionKind.ModeChange, 0);
|
|
}
|
|
|
|
if (_lastDisplayDate != VisibleRange.AnchorDate)
|
|
{
|
|
return new CalendarTransitionInfo(
|
|
CalendarTransitionKind.Navigation,
|
|
VisibleRange.AnchorDate.CompareTo(_lastDisplayDate));
|
|
}
|
|
|
|
if (_lastFirstDayOfWeek != CalendarSettings.FirstDayOfWeek)
|
|
{
|
|
return new CalendarTransitionInfo(CalendarTransitionKind.Refresh, 0);
|
|
}
|
|
|
|
return new CalendarTransitionInfo(CalendarTransitionKind.None, 0);
|
|
}
|
|
|
|
private void RunTransition(CalendarTransitionInfo transition)
|
|
{
|
|
if (transition.Kind == CalendarTransitionKind.None || VisibleRange is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (VisibleRange.DisplayType != CalendarDisplayType.Month)
|
|
{
|
|
RunTimedTransition(transition);
|
|
return;
|
|
}
|
|
|
|
var target = (UIElement)MonthRoot;
|
|
var visual = ElementCompositionPreview.GetElementVisual(target);
|
|
var compositor = visual.Compositor;
|
|
|
|
visual.CenterPoint = new Vector3((float)(target.RenderSize.Width * 0.5), (float)(target.RenderSize.Height * 0.5), 0f);
|
|
visual.StopAnimation(nameof(visual.Offset));
|
|
visual.StopAnimation(nameof(visual.Opacity));
|
|
visual.StopAnimation(nameof(visual.Scale));
|
|
|
|
switch (transition.Kind)
|
|
{
|
|
case CalendarTransitionKind.Navigation:
|
|
StartNavigationTransition(compositor, visual, transition.Direction, target.RenderSize.Width);
|
|
break;
|
|
case CalendarTransitionKind.ModeChange:
|
|
StartModeTransition(compositor, visual);
|
|
break;
|
|
case CalendarTransitionKind.Refresh:
|
|
StartRefreshTransition(compositor, visual);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void RunTimedTransition(CalendarTransitionInfo transition)
|
|
{
|
|
var contentVisual = ElementCompositionPreview.GetElementVisual(TimedScrollViewer);
|
|
var compositor = contentVisual.Compositor;
|
|
|
|
PrepareAnimatedVisual(contentVisual, TimedScrollViewer);
|
|
|
|
switch (transition.Kind)
|
|
{
|
|
case CalendarTransitionKind.Navigation:
|
|
StartTimedNavigationTransition(compositor, transition.Direction);
|
|
break;
|
|
case CalendarTransitionKind.ModeChange:
|
|
StartTimedModeTransition(compositor);
|
|
break;
|
|
case CalendarTransitionKind.Refresh:
|
|
StartTimedRefreshTransition(compositor);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void ResetTimedVisualState()
|
|
{
|
|
ResetAnimatedElement(TimedScrollViewer);
|
|
|
|
// TODO: This is buggy.
|
|
// ResetAnimatedElement(TimedAllDayHost);
|
|
}
|
|
|
|
private static void StartNavigationTransition(Compositor compositor, Visual visual, int direction, double width)
|
|
{
|
|
var travel = (float)Math.Max(48d, Math.Min(160d, width * 0.08d));
|
|
var startX = direction >= 0 ? travel : -travel;
|
|
|
|
var offsetAnimation = compositor.CreateVector3KeyFrameAnimation();
|
|
offsetAnimation.InsertKeyFrame(0f, new Vector3(startX, 0f, 0f));
|
|
offsetAnimation.InsertKeyFrame(1f, Vector3.Zero);
|
|
offsetAnimation.Duration = TimeSpan.FromMilliseconds(220);
|
|
|
|
var opacityAnimation = compositor.CreateScalarKeyFrameAnimation();
|
|
opacityAnimation.InsertKeyFrame(0f, 0.72f);
|
|
opacityAnimation.InsertKeyFrame(1f, 1f);
|
|
opacityAnimation.Duration = offsetAnimation.Duration;
|
|
|
|
visual.StartAnimation(nameof(visual.Offset), offsetAnimation);
|
|
visual.StartAnimation(nameof(visual.Opacity), opacityAnimation);
|
|
}
|
|
|
|
private void StartTimedNavigationTransition(Compositor compositor, int direction)
|
|
{
|
|
var width = Math.Max(TimedRoot.RenderSize.Width, ActualWidth);
|
|
var travel = (float)Math.Max(56d, Math.Min(184d, width * 0.09d));
|
|
var signedTravel = direction >= 0 ? travel : -travel;
|
|
var clipInset = (float)Math.Max(18d, Math.Min(64d, width * 0.05d));
|
|
|
|
StartTimedElementTransition(compositor, TimedScrollViewer, signedTravel, 0f, 0.68f, TimeSpan.FromMilliseconds(240), direction >= 0 ? 0f : clipInset, direction >= 0 ? clipInset : 0f, animateScale: false);
|
|
if (HasTimedAllDayItems)
|
|
{
|
|
// TODO: This is buggy.
|
|
// StartTimedElementTransition(compositor, TimedAllDayHost, signedTravel, 0f, 0.68f, TimeSpan.FromMilliseconds(240), direction >= 0 ? 0f : clipInset, direction >= 0 ? clipInset : 0f, animateScale: false);
|
|
}
|
|
}
|
|
|
|
private static void StartModeTransition(Compositor compositor, Visual visual)
|
|
{
|
|
var offsetAnimation = compositor.CreateVector3KeyFrameAnimation();
|
|
offsetAnimation.InsertKeyFrame(0f, new Vector3(0f, 18f, 0f));
|
|
offsetAnimation.InsertKeyFrame(1f, Vector3.Zero);
|
|
offsetAnimation.Duration = TimeSpan.FromMilliseconds(260);
|
|
|
|
var opacityAnimation = compositor.CreateScalarKeyFrameAnimation();
|
|
opacityAnimation.InsertKeyFrame(0f, 0f);
|
|
opacityAnimation.InsertKeyFrame(1f, 1f);
|
|
opacityAnimation.Duration = offsetAnimation.Duration;
|
|
|
|
var scaleAnimation = compositor.CreateVector3KeyFrameAnimation();
|
|
scaleAnimation.InsertKeyFrame(0f, new Vector3(0.985f, 0.985f, 1f));
|
|
scaleAnimation.InsertKeyFrame(1f, new Vector3(1f, 1f, 1f));
|
|
scaleAnimation.Duration = offsetAnimation.Duration;
|
|
|
|
visual.StartAnimation(nameof(visual.Offset), offsetAnimation);
|
|
visual.StartAnimation(nameof(visual.Opacity), opacityAnimation);
|
|
visual.StartAnimation(nameof(visual.Scale), scaleAnimation);
|
|
}
|
|
|
|
private void StartTimedModeTransition(Compositor compositor)
|
|
{
|
|
StartTimedElementTransition(compositor, TimedScrollViewer, 0f, 18f, 0f, TimeSpan.FromMilliseconds(240), 0f, 0f, animateScale: false);
|
|
if (HasTimedAllDayItems)
|
|
{
|
|
// TODO: This is buggy.
|
|
// StartTimedElementTransition(compositor, TimedAllDayHost, 0f, 18f, 0f, TimeSpan.FromMilliseconds(240), 0f, 0f, animateScale: false);
|
|
}
|
|
}
|
|
|
|
private static void StartRefreshTransition(Compositor compositor, Visual visual)
|
|
{
|
|
var opacityAnimation = compositor.CreateScalarKeyFrameAnimation();
|
|
opacityAnimation.InsertKeyFrame(0f, 0.82f);
|
|
opacityAnimation.InsertKeyFrame(1f, 1f);
|
|
opacityAnimation.Duration = TimeSpan.FromMilliseconds(160);
|
|
|
|
visual.StartAnimation(nameof(visual.Opacity), opacityAnimation);
|
|
}
|
|
|
|
private void StartTimedRefreshTransition(Compositor compositor)
|
|
{
|
|
StartOpacityTransition(compositor, ElementCompositionPreview.GetElementVisual(TimedScrollViewer), 0.8f, TimeSpan.FromMilliseconds(160));
|
|
if (HasTimedAllDayItems)
|
|
{
|
|
// TODO: This is buggy.
|
|
// StartOpacityTransition(compositor, ElementCompositionPreview.GetElementVisual(TimedAllDayHost), 0.8f, TimeSpan.FromMilliseconds(160));
|
|
}
|
|
}
|
|
|
|
private static void PrepareAnimatedVisual(Visual visual, UIElement target)
|
|
{
|
|
visual.CenterPoint = new Vector3((float)(target.RenderSize.Width * 0.5), (float)(target.RenderSize.Height * 0.5), 0f);
|
|
visual.StopAnimation(nameof(visual.Offset));
|
|
visual.StopAnimation(nameof(visual.Opacity));
|
|
visual.StopAnimation(nameof(visual.Scale));
|
|
}
|
|
|
|
private static void ResetAnimatedElement(UIElement target)
|
|
{
|
|
var visual = ElementCompositionPreview.GetElementVisual(target);
|
|
PrepareAnimatedVisual(visual, target);
|
|
|
|
visual.Offset = Vector3.Zero;
|
|
visual.Opacity = 1f;
|
|
visual.Scale = new Vector3(1f, 1f, 1f);
|
|
|
|
if (visual.Clip is InsetClip clip)
|
|
{
|
|
clip.StopAnimation(nameof(clip.LeftInset));
|
|
clip.StopAnimation(nameof(clip.RightInset));
|
|
clip.LeftInset = 0f;
|
|
clip.RightInset = 0f;
|
|
}
|
|
}
|
|
|
|
private static void StartTimedElementTransition(Compositor compositor, UIElement target, float offsetX, float offsetY, float startingOpacity, TimeSpan duration, float leftInset, float rightInset, bool animateScale = true)
|
|
{
|
|
var visual = ElementCompositionPreview.GetElementVisual(target);
|
|
PrepareAnimatedVisual(visual, target);
|
|
|
|
var easing = compositor.CreateCubicBezierEasingFunction(new Vector2(0.16f, 1f), new Vector2(0.3f, 1f));
|
|
var fadeEasing = compositor.CreateCubicBezierEasingFunction(new Vector2(0.2f, 0f), new Vector2(0f, 1f));
|
|
var clip = visual.Clip as InsetClip ?? compositor.CreateInsetClip();
|
|
|
|
clip.LeftInset = leftInset;
|
|
clip.RightInset = rightInset;
|
|
visual.Clip = clip;
|
|
|
|
var offsetAnimation = compositor.CreateVector3KeyFrameAnimation();
|
|
offsetAnimation.InsertKeyFrame(0f, new Vector3(offsetX, offsetY, 0f));
|
|
offsetAnimation.InsertKeyFrame(1f, Vector3.Zero, easing);
|
|
offsetAnimation.Duration = duration;
|
|
|
|
var opacityAnimation = compositor.CreateScalarKeyFrameAnimation();
|
|
opacityAnimation.InsertKeyFrame(0f, startingOpacity);
|
|
opacityAnimation.InsertKeyFrame(1f, 1f, fadeEasing);
|
|
opacityAnimation.Duration = duration;
|
|
|
|
var leftInsetAnimation = compositor.CreateScalarKeyFrameAnimation();
|
|
leftInsetAnimation.InsertKeyFrame(1f, 0f, easing);
|
|
leftInsetAnimation.Duration = duration;
|
|
|
|
var rightInsetAnimation = compositor.CreateScalarKeyFrameAnimation();
|
|
rightInsetAnimation.InsertKeyFrame(1f, 0f, easing);
|
|
rightInsetAnimation.Duration = duration;
|
|
|
|
visual.StartAnimation(nameof(visual.Offset), offsetAnimation);
|
|
visual.StartAnimation(nameof(visual.Opacity), opacityAnimation);
|
|
|
|
if (animateScale)
|
|
{
|
|
var scaleAnimation = compositor.CreateVector3KeyFrameAnimation();
|
|
scaleAnimation.InsertKeyFrame(0f, new Vector3(0.996f, 0.996f, 1f));
|
|
scaleAnimation.InsertKeyFrame(1f, new Vector3(1f, 1f, 1f), easing);
|
|
scaleAnimation.Duration = duration;
|
|
visual.StartAnimation(nameof(visual.Scale), scaleAnimation);
|
|
}
|
|
else
|
|
{
|
|
visual.Scale = new Vector3(1f, 1f, 1f);
|
|
}
|
|
|
|
clip.StartAnimation(nameof(clip.LeftInset), leftInsetAnimation);
|
|
clip.StartAnimation(nameof(clip.RightInset), rightInsetAnimation);
|
|
}
|
|
|
|
private static void StartOpacityTransition(Compositor compositor, Visual visual, float startingOpacity, TimeSpan duration)
|
|
{
|
|
visual.StopAnimation(nameof(visual.Opacity));
|
|
|
|
var easing = compositor.CreateCubicBezierEasingFunction(new Vector2(0.2f, 0f), new Vector2(0f, 1f));
|
|
var opacityAnimation = compositor.CreateScalarKeyFrameAnimation();
|
|
opacityAnimation.InsertKeyFrame(0f, startingOpacity);
|
|
opacityAnimation.InsertKeyFrame(1f, 1f, easing);
|
|
opacityAnimation.Duration = duration;
|
|
|
|
visual.StartAnimation(nameof(visual.Opacity), opacityAnimation);
|
|
}
|
|
|
|
private static SKPaint CreateLinePaint()
|
|
{
|
|
var strokeColor = GetStrokeColor();
|
|
|
|
return new SKPaint
|
|
{
|
|
Color = new SKColor(strokeColor.R, strokeColor.G, strokeColor.B, (byte)Math.Max(40, strokeColor.A / 2)),
|
|
IsAntialias = false,
|
|
StrokeWidth = 1
|
|
};
|
|
}
|
|
|
|
private static SKPaint CreateMinorLinePaint()
|
|
{
|
|
var strokeColor = GetStrokeColor();
|
|
|
|
return new SKPaint
|
|
{
|
|
Color = new SKColor(strokeColor.R, strokeColor.G, strokeColor.B, (byte)Math.Max(20, strokeColor.A / 4)),
|
|
IsAntialias = false,
|
|
StrokeWidth = 1
|
|
};
|
|
}
|
|
|
|
private static SKPaint CreateFillPaint(Brush brush)
|
|
{
|
|
return new SKPaint
|
|
{
|
|
Color = ToSkColor(brush),
|
|
Style = SKPaintStyle.Fill,
|
|
IsAntialias = false
|
|
};
|
|
}
|
|
|
|
private static SKColor ToSkColor(Brush brush)
|
|
{
|
|
return brush is SolidColorBrush solidColorBrush
|
|
? new SKColor(solidColorBrush.Color.R, solidColorBrush.Color.G, solidColorBrush.Color.B, solidColorBrush.Color.A)
|
|
: SKColors.Transparent;
|
|
}
|
|
|
|
private static double GetTimedGridIntervalHeight(double hourHeight) => hourHeight * (TimedGridIntervalMinutes / 60d);
|
|
|
|
private double GetTimedGridIntervalHeight() => GetTimedGridIntervalHeight(GetHourHeight());
|
|
|
|
private double GetTimedSelectionIntervalHeight() => GetHourHeight() * (TimedSelectionIntervalMinutes / 60d);
|
|
|
|
private double GetHourHeight() => CalendarSettings?.HourHeight ?? 60d;
|
|
|
|
private static Color GetStrokeColor()
|
|
{
|
|
if (Application.Current.Resources.TryGetValue("CardStrokeColorDefaultBrush", out var resource) &&
|
|
resource is SolidColorBrush solidBrush)
|
|
{
|
|
return solidBrush.Color;
|
|
}
|
|
|
|
return Color.FromArgb(96, 210, 210, 210);
|
|
}
|
|
|
|
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
|
{
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
|
}
|
|
|
|
private readonly record struct CalendarTransitionInfo(CalendarTransitionKind Kind, int Direction);
|
|
|
|
private readonly record struct CalendarDropTargetInfo(
|
|
CalendarDropTargetKind Kind,
|
|
DateOnly Date,
|
|
int DayIndex,
|
|
int SlotIndex,
|
|
DateTime TargetStart);
|
|
|
|
private enum CalendarTransitionKind
|
|
{
|
|
None,
|
|
Navigation,
|
|
ModeChange,
|
|
Refresh
|
|
}
|
|
}
|