Calendar rendering implementation.
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Wino.Calendar.Controls;
|
||||
|
||||
public sealed class CalendarEmptySlotTappedEventArgs : EventArgs
|
||||
{
|
||||
public CalendarEmptySlotTappedEventArgs(DateTime clickedDate, Point positionerPoint, Size cellSize)
|
||||
{
|
||||
ClickedDate = clickedDate;
|
||||
PositionerPoint = positionerPoint;
|
||||
CellSize = cellSize;
|
||||
}
|
||||
|
||||
public DateTime ClickedDate { get; }
|
||||
public Point PositionerPoint { get; }
|
||||
public Size CellSize { get; }
|
||||
}
|
||||
@@ -9,8 +9,6 @@ using Microsoft.UI.Xaml.Media;
|
||||
using Wino.Calendar.ViewModels.Data;
|
||||
using Wino.Calendar.ViewModels.Messages;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
namespace Wino.Calendar.Controls;
|
||||
|
||||
public sealed partial class CalendarItemControl : UserControl
|
||||
@@ -96,7 +94,7 @@ public sealed partial class CalendarItemControl : UserControl
|
||||
|
||||
if (isSingleTap && CalendarItem != null)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send(new CalendarItemTappedMessage(CalendarItem, null));
|
||||
WeakReferenceMessenger.Default.Send(new CalendarItemTappedMessage(CalendarItem));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:helpers="using:Wino.Helpers"
|
||||
xmlns:local="using:Wino.Calendar.Controls"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:skia="using:SkiaSharp.Views.Windows"
|
||||
@@ -53,14 +54,34 @@
|
||||
<Grid>
|
||||
<Grid x:Name="TimedRoot" Visibility="Collapsed">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="44" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="64" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid x:Name="TimedHeaderHost">
|
||||
<Border
|
||||
x:Name="TimedHourHeaderHost"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Height="44"
|
||||
Background="{ThemeResource LayerFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="0,0,1,1" />
|
||||
|
||||
<Grid
|
||||
x:Name="TimedHeaderHost"
|
||||
Grid.Row="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="64,0,0,0"
|
||||
Height="44"
|
||||
Background="{ThemeResource LayerFillColorDefaultBrush}">
|
||||
<skia:SKXamlCanvas x:Name="TimedHeaderCanvas" PaintSurface="TimedHeaderCanvasPaintSurface" />
|
||||
<ItemsControl
|
||||
x:Name="TimedHeadersItemsControl"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
ItemTemplate="{StaticResource TimedHeaderTemplate}"
|
||||
ItemsSource="{x:Bind TimedHeaderTexts, Mode=OneWay}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
@@ -69,17 +90,42 @@
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
|
||||
<ScrollViewer
|
||||
x:Name="TimedScrollViewer"
|
||||
<Grid
|
||||
Grid.Row="1"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
VerticalScrollMode="Enabled">
|
||||
<Grid x:Name="TimedViewport" Height="{x:Bind TimelineHeight, Mode=OneWay}">
|
||||
<skia:SKXamlCanvas x:Name="TimedStructureCanvas" PaintSurface="TimedStructureCanvasPaintSurface" />
|
||||
<Canvas x:Name="TimedItemsCanvas" />
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
Grid.ColumnSpan="2"
|
||||
Background="Transparent">
|
||||
<ScrollViewer
|
||||
x:Name="TimedScrollViewer"
|
||||
Background="Transparent"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
VerticalScrollMode="Enabled">
|
||||
<Grid x:Name="TimedScrollContentGrid" Height="{x:Bind TimelineHeight, Mode=OneWay}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="64" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Canvas
|
||||
x:Name="HourLabelsCanvas"
|
||||
Width="64"
|
||||
IsHitTestVisible="False" />
|
||||
|
||||
<Grid
|
||||
x:Name="TimedViewport"
|
||||
Grid.Column="1"
|
||||
Height="{x:Bind TimelineHeight, Mode=OneWay}">
|
||||
<skia:SKXamlCanvas x:Name="TimedStructureCanvas" PaintSurface="TimedStructureCanvasPaintSurface" />
|
||||
<Border
|
||||
x:Name="TimedInteractionLayer"
|
||||
Background="Transparent"
|
||||
Tapped="TimedInteractionLayerTapped" />
|
||||
<Canvas x:Name="TimedItemsCanvas" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
|
||||
<Grid x:Name="MonthRoot" Visibility="Collapsed">
|
||||
@@ -106,7 +152,11 @@
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<skia:SKXamlCanvas x:Name="MonthStructureCanvas" PaintSurface="MonthStructureCanvasPaintSurface" />
|
||||
<Canvas x:Name="MonthCellLabelsCanvas" />
|
||||
<Border
|
||||
x:Name="MonthInteractionLayer"
|
||||
Background="Transparent"
|
||||
Tapped="MonthInteractionLayerTapped" />
|
||||
<Canvas x:Name="MonthCellLabelsCanvas" IsHitTestVisible="False" />
|
||||
<Canvas x:Name="MonthItemsCanvas" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -12,9 +12,11 @@ using Microsoft.UI.Composition;
|
||||
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.Foundation;
|
||||
using Windows.UI;
|
||||
using Wino.Calendar.ViewModels.Data;
|
||||
using Wino.Core.Domain.Enums;
|
||||
@@ -25,6 +27,9 @@ namespace Wino.Calendar.Controls;
|
||||
|
||||
public sealed partial class CalendarPeriodControl : UserControl, INotifyPropertyChanged
|
||||
{
|
||||
private const double TimedHourColumnWidth = 64d;
|
||||
private const double TimedGridIntervalMinutes = 30d;
|
||||
private const double TimedSelectionIntervalMinutes = 30d;
|
||||
private VisibleDateRange _currentRange = new(
|
||||
CalendarDisplayType.Month,
|
||||
DateOnly.FromDateTime(DateTime.Today),
|
||||
@@ -62,6 +67,7 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
public CalendarPeriodControl() => InitializeComponent();
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
public event EventHandler<CalendarEmptySlotTappedEventArgs>? EmptySlotTapped;
|
||||
|
||||
private ObservableCollection<HeaderTextLayout> TimedHeaderTextsCollection { get; } = [];
|
||||
private ObservableCollection<HeaderTextLayout> MonthHeaderTextsCollection { get; } = [];
|
||||
@@ -209,20 +215,22 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
{
|
||||
TimedRoot.Visibility = Visibility.Visible;
|
||||
MonthRoot.Visibility = Visibility.Collapsed;
|
||||
ResetTimedVisualState();
|
||||
|
||||
TimedDayWidth = _currentRange.Dates.Count == 0 ? 0d : ActualWidth / _currentRange.Dates.Count;
|
||||
TimedViewport.Width = ActualWidth;
|
||||
var timedSurfaceWidth = GetTimedSurfaceWidth();
|
||||
|
||||
TimedDayWidth = _currentRange.Dates.Count == 0 ? 0d : timedSurfaceWidth / _currentRange.Dates.Count;
|
||||
TimedScrollContentGrid.Width = ActualWidth;
|
||||
TimedViewport.Width = timedSurfaceWidth;
|
||||
TimedViewport.Height = TimelineHeight;
|
||||
|
||||
_timedLayout = TimedCalendarLayoutCalculator.Calculate(_currentRange, CurrentItems, ActualWidth, GetHourHeight());
|
||||
_timedLayout = TimedCalendarLayoutCalculator.Calculate(_currentRange, CurrentItems, timedSurfaceWidth, GetHourHeight());
|
||||
|
||||
ReplaceCollection(
|
||||
TimedHeaderTextsCollection,
|
||||
_timedLayout.VisibleDates.Select(date =>
|
||||
new HeaderTextLayout(
|
||||
date.ToDateTime(TimeOnly.MinValue).ToString(
|
||||
string.IsNullOrWhiteSpace(TimedHeaderDateFormat) ? "ddd dd" : TimedHeaderDateFormat,
|
||||
CalendarSettings!.CultureInfo),
|
||||
GetTimedHeaderText(date),
|
||||
TimedDayWidth)));
|
||||
|
||||
var eventTemplate = (DataTemplate)Resources["CalendarEventTemplate"];
|
||||
@@ -232,6 +240,7 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
item.Template = eventTemplate;
|
||||
return item;
|
||||
}));
|
||||
RenderHourLabels();
|
||||
RenderTimedItems();
|
||||
|
||||
TimedHeaderCanvas.Invalidate();
|
||||
@@ -352,12 +361,14 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
var canvas = e.Surface.Canvas;
|
||||
canvas.Clear(SKColors.Transparent);
|
||||
|
||||
if (_timedLayout.VisibleDates.Count == 0 || ActualWidth <= 0)
|
||||
var timedSurfaceWidth = GetTimedSurfaceWidth();
|
||||
|
||||
if (_timedLayout.VisibleDates.Count == 0 || timedSurfaceWidth <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var scaleX = (float)(e.Info.Width / ActualWidth);
|
||||
var scaleX = (float)(e.Info.Width / timedSurfaceWidth);
|
||||
var height = e.Info.Height;
|
||||
var dayWidth = (float)(_timedLayout.DayWidth * scaleX);
|
||||
|
||||
@@ -373,41 +384,50 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
private void TimedStructureCanvasPaintSurface(object? sender, SKPaintSurfaceEventArgs e)
|
||||
{
|
||||
using var linePaint = CreateLinePaint();
|
||||
using var minorLinePaint = CreateMinorLinePaint();
|
||||
using var defaultFillPaint = CreateFillPaint(GetDefaultHourBackground());
|
||||
using var workFillPaint = CreateFillPaint(GetWorkHourBackground());
|
||||
var canvas = e.Surface.Canvas;
|
||||
canvas.Clear(SKColors.Transparent);
|
||||
|
||||
if (_timedLayout.VisibleDates.Count == 0 || ActualWidth <= 0)
|
||||
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 / ActualWidth);
|
||||
var scaleX = (float)(e.Info.Width / timedSurfaceWidth);
|
||||
var scaleY = (float)(e.Info.Height / timelineHeight);
|
||||
var dayWidth = (float)(_timedLayout.DayWidth * scaleX);
|
||||
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 = CalendarSettings?.WorkingDays.Contains(_timedLayout.VisibleDates[dayIndex].DayOfWeek) == true;
|
||||
|
||||
for (var hour = 0; hour < 24; hour++)
|
||||
for (var intervalIndex = 0; intervalIndex < intervalCount; intervalIndex++)
|
||||
{
|
||||
var y = (float)(hour * hourHeight * scaleY);
|
||||
var scaledHourHeight = (float)(hourHeight * scaleY);
|
||||
var fillPaint = hour >= workDayStartHour && hour < workDayEndHour ? workFillPaint : defaultFillPaint;
|
||||
canvas.DrawRect(x, y, dayWidth, scaledHourHeight, fillPaint);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
for (var hour = 0; hour <= 24; hour++)
|
||||
for (var intervalIndex = 0; intervalIndex <= intervalCount; intervalIndex++)
|
||||
{
|
||||
var y = (float)(hour * hourHeight * scaleY);
|
||||
canvas.DrawLine(0, y, e.Info.Width, y, linePaint);
|
||||
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++)
|
||||
@@ -472,6 +492,34 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -532,6 +580,70 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
EmptySlotTapped?.Invoke(
|
||||
this,
|
||||
new CalendarEmptySlotTappedEventArgs(
|
||||
clickedDate,
|
||||
new Point(dayIndex * _timedLayout.DayWidth, slotIndex * intervalHeight),
|
||||
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];
|
||||
|
||||
EmptySlotTapped?.Invoke(
|
||||
this,
|
||||
new CalendarEmptySlotTappedEventArgs(
|
||||
cell.Date.ToDateTime(TimeOnly.MinValue),
|
||||
new Point(cell.Bounds.X, cell.Bounds.Y),
|
||||
new Size(cell.Bounds.Width, cell.Bounds.Height)));
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -597,11 +709,9 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
|
||||
private void RunTimedTransition(CalendarTransitionInfo transition)
|
||||
{
|
||||
var headerVisual = ElementCompositionPreview.GetElementVisual(TimedHeaderHost);
|
||||
var contentVisual = ElementCompositionPreview.GetElementVisual(TimedScrollViewer);
|
||||
var compositor = headerVisual.Compositor;
|
||||
var compositor = contentVisual.Compositor;
|
||||
|
||||
PrepareAnimatedVisual(headerVisual, TimedHeaderHost);
|
||||
PrepareAnimatedVisual(contentVisual, TimedScrollViewer);
|
||||
|
||||
switch (transition.Kind)
|
||||
@@ -618,6 +728,11 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetTimedVisualState()
|
||||
{
|
||||
ResetAnimatedElement(TimedScrollViewer);
|
||||
}
|
||||
|
||||
private static void StartNavigationTransition(Compositor compositor, Visual visual, int direction, double width)
|
||||
{
|
||||
var travel = (float)Math.Max(48d, Math.Min(160d, width * 0.08d));
|
||||
@@ -644,8 +759,7 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
var signedTravel = direction >= 0 ? travel : -travel;
|
||||
var clipInset = (float)Math.Max(18d, Math.Min(64d, width * 0.05d));
|
||||
|
||||
StartTimedElementTransition(compositor, TimedHeaderHost, signedTravel * 0.45f, 0f, 0.78f, TimeSpan.FromMilliseconds(180), direction >= 0 ? 0f : clipInset, direction >= 0 ? clipInset : 0f);
|
||||
StartTimedElementTransition(compositor, TimedScrollViewer, signedTravel, 0f, 0.68f, TimeSpan.FromMilliseconds(240), direction >= 0 ? 0f : clipInset, direction >= 0 ? clipInset : 0f);
|
||||
StartTimedElementTransition(compositor, TimedScrollViewer, 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)
|
||||
@@ -672,8 +786,7 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
|
||||
private void StartTimedModeTransition(Compositor compositor)
|
||||
{
|
||||
StartTimedElementTransition(compositor, TimedHeaderHost, 0f, 10f, 0f, TimeSpan.FromMilliseconds(180), 0f, 0f);
|
||||
StartTimedElementTransition(compositor, TimedScrollViewer, 0f, 18f, 0f, TimeSpan.FromMilliseconds(240), 0f, 0f);
|
||||
StartTimedElementTransition(compositor, TimedScrollViewer, 0f, 18f, 0f, TimeSpan.FromMilliseconds(240), 0f, 0f, animateScale: false);
|
||||
}
|
||||
|
||||
private static void StartRefreshTransition(Compositor compositor, Visual visual)
|
||||
@@ -688,7 +801,6 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
|
||||
private void StartTimedRefreshTransition(Compositor compositor)
|
||||
{
|
||||
StartOpacityTransition(compositor, ElementCompositionPreview.GetElementVisual(TimedHeaderHost), 0.86f, TimeSpan.FromMilliseconds(140));
|
||||
StartOpacityTransition(compositor, ElementCompositionPreview.GetElementVisual(TimedScrollViewer), 0.8f, TimeSpan.FromMilliseconds(160));
|
||||
}
|
||||
|
||||
@@ -700,7 +812,25 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
visual.StopAnimation(nameof(visual.Scale));
|
||||
}
|
||||
|
||||
private static void StartTimedElementTransition(Compositor compositor, UIElement target, float offsetX, float offsetY, float startingOpacity, TimeSpan duration, float leftInset, float rightInset)
|
||||
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);
|
||||
@@ -723,11 +853,6 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
opacityAnimation.InsertKeyFrame(1f, 1f, fadeEasing);
|
||||
opacityAnimation.Duration = duration;
|
||||
|
||||
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;
|
||||
|
||||
var leftInsetAnimation = compositor.CreateScalarKeyFrameAnimation();
|
||||
leftInsetAnimation.InsertKeyFrame(1f, 0f, easing);
|
||||
leftInsetAnimation.Duration = duration;
|
||||
@@ -738,7 +863,20 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
|
||||
visual.StartAnimation(nameof(visual.Offset), offsetAnimation);
|
||||
visual.StartAnimation(nameof(visual.Opacity), opacityAnimation);
|
||||
visual.StartAnimation(nameof(visual.Scale), scaleAnimation);
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -768,6 +906,18 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
@@ -805,6 +955,12 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
return new SolidColorBrush(Color.FromArgb(255, 34, 40, 52));
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
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 DayColumnControl : Control
|
||||
{
|
||||
private const string PART_HeaderDateDayText = nameof(PART_HeaderDateDayText);
|
||||
private const string PART_IsTodayBorder = nameof(PART_IsTodayBorder);
|
||||
private const string PART_ColumnHeaderText = nameof(PART_ColumnHeaderText);
|
||||
private const string PART_AllDayItemsControl = nameof(PART_AllDayItemsControl);
|
||||
|
||||
private const string TodayState = nameof(TodayState);
|
||||
private const string NotTodayState = nameof(NotTodayState);
|
||||
|
||||
private TextBlock? HeaderDateDayText;
|
||||
private TextBlock? ColumnHeaderText;
|
||||
private Border? IsTodayBorder;
|
||||
private ItemsControl? AllDayItemsControl;
|
||||
private CalendarEventCollection? _boundEventsCollection;
|
||||
|
||||
public CalendarDayModel DayModel
|
||||
{
|
||||
get { return (CalendarDayModel)GetValue(DayModelProperty); }
|
||||
set { SetValue(DayModelProperty, value); }
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty DayModelProperty = DependencyProperty.Register(
|
||||
nameof(DayModel),
|
||||
typeof(CalendarDayModel),
|
||||
typeof(DayColumnControl),
|
||||
new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
|
||||
|
||||
public DayColumnControl()
|
||||
{
|
||||
DefaultStyleKey = typeof(DayColumnControl);
|
||||
Unloaded += OnUnloaded;
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate()
|
||||
{
|
||||
base.OnApplyTemplate();
|
||||
|
||||
HeaderDateDayText = GetTemplateChild(PART_HeaderDateDayText) as TextBlock;
|
||||
ColumnHeaderText = GetTemplateChild(PART_ColumnHeaderText) as TextBlock;
|
||||
IsTodayBorder = GetTemplateChild(PART_IsTodayBorder) as Border;
|
||||
AllDayItemsControl = GetTemplateChild(PART_AllDayItemsControl) as ItemsControl;
|
||||
|
||||
RegisterEventsCollectionHandlers();
|
||||
UpdateValues();
|
||||
}
|
||||
|
||||
private static void OnRenderingPropertiesChanged(DependencyObject control, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (control is DayColumnControl columnControl)
|
||||
{
|
||||
columnControl.RegisterEventsCollectionHandlers();
|
||||
columnControl.UpdateValues();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUnloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DeregisterEventsCollectionHandlers();
|
||||
}
|
||||
|
||||
private bool IsMonthlyTemplate() => ColumnHeaderText == null;
|
||||
|
||||
private void RegisterEventsCollectionHandlers()
|
||||
{
|
||||
var nextCollection = DayModel?.EventsCollection;
|
||||
if (ReferenceEquals(_boundEventsCollection, nextCollection))
|
||||
return;
|
||||
|
||||
DeregisterEventsCollectionHandlers();
|
||||
|
||||
_boundEventsCollection = nextCollection;
|
||||
if (_boundEventsCollection == null)
|
||||
return;
|
||||
|
||||
((INotifyCollectionChanged)_boundEventsCollection.AllDayEvents).CollectionChanged += EventsCollectionChanged;
|
||||
((INotifyCollectionChanged)_boundEventsCollection.RegularEvents).CollectionChanged += EventsCollectionChanged;
|
||||
}
|
||||
|
||||
private void DeregisterEventsCollectionHandlers()
|
||||
{
|
||||
if (_boundEventsCollection == null)
|
||||
return;
|
||||
|
||||
((INotifyCollectionChanged)_boundEventsCollection.AllDayEvents).CollectionChanged -= EventsCollectionChanged;
|
||||
((INotifyCollectionChanged)_boundEventsCollection.RegularEvents).CollectionChanged -= EventsCollectionChanged;
|
||||
_boundEventsCollection = null;
|
||||
}
|
||||
|
||||
private void EventsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
UpdateEventItemsSource();
|
||||
}
|
||||
|
||||
private void UpdateEventItemsSource()
|
||||
{
|
||||
if (AllDayItemsControl == null || DayModel == null) return;
|
||||
|
||||
if (IsMonthlyTemplate())
|
||||
{
|
||||
// Month cells should show all events for the day, not only all-day/multi-day.
|
||||
var monthlyItems = DayModel.EventsCollection.AllDayEvents
|
||||
.Concat(DayModel.EventsCollection.RegularEvents)
|
||||
.GroupBy(a => a.Id)
|
||||
.Select(g => g.First())
|
||||
.OrderBy(a => a.StartDate)
|
||||
.ToList();
|
||||
|
||||
AllDayItemsControl.ItemsSource = monthlyItems;
|
||||
return;
|
||||
}
|
||||
|
||||
AllDayItemsControl.ItemsSource = DayModel.EventsCollection.AllDayEvents;
|
||||
}
|
||||
|
||||
private void UpdateValues()
|
||||
{
|
||||
if (DayModel == null) return;
|
||||
|
||||
if (HeaderDateDayText != null)
|
||||
{
|
||||
HeaderDateDayText.Text = DayModel.RepresentingDate.Day.ToString();
|
||||
}
|
||||
|
||||
// Monthly template does not use it.
|
||||
if (ColumnHeaderText != null)
|
||||
{
|
||||
ColumnHeaderText.Text = DayModel.RepresentingDate.ToString("dddd", DayModel.CalendarRenderOptions.CalendarSettings.CultureInfo);
|
||||
}
|
||||
|
||||
UpdateEventItemsSource();
|
||||
|
||||
if (IsTodayBorder == null) return;
|
||||
bool isToday = DayModel.RepresentingDate.Date == DateTime.Now.Date;
|
||||
|
||||
VisualStateManager.GoToState(this, isToday ? TodayState : NotTodayState, false);
|
||||
|
||||
UpdateLayout();
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Calendar.Controls;
|
||||
|
||||
public partial class DayHeaderControl : Control
|
||||
{
|
||||
private const string PART_DayHeaderTextBlock = nameof(PART_DayHeaderTextBlock);
|
||||
private TextBlock? HeaderTextblock;
|
||||
|
||||
public DayHeaderDisplayType DisplayType
|
||||
{
|
||||
get { return (DayHeaderDisplayType)GetValue(DisplayTypeProperty); }
|
||||
set { SetValue(DisplayTypeProperty, value); }
|
||||
}
|
||||
|
||||
public DateTime Date
|
||||
{
|
||||
get { return (DateTime)GetValue(DateProperty); }
|
||||
set { SetValue(DateProperty, value); }
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty DateProperty = DependencyProperty.Register(nameof(Date), typeof(DateTime), typeof(DayHeaderControl), new PropertyMetadata(default(DateTime), new PropertyChangedCallback(OnHeaderPropertyChanged)));
|
||||
public static readonly DependencyProperty DisplayTypeProperty = DependencyProperty.Register(nameof(DisplayType), typeof(DayHeaderDisplayType), typeof(DayHeaderControl), new PropertyMetadata(DayHeaderDisplayType.TwentyFourHour, new PropertyChangedCallback(OnHeaderPropertyChanged)));
|
||||
|
||||
public DayHeaderControl()
|
||||
{
|
||||
DefaultStyleKey = typeof(DayHeaderControl);
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate()
|
||||
{
|
||||
base.OnApplyTemplate();
|
||||
|
||||
HeaderTextblock = GetTemplateChild(PART_DayHeaderTextBlock) as TextBlock;
|
||||
UpdateHeaderText();
|
||||
}
|
||||
|
||||
private static void OnHeaderPropertyChanged(DependencyObject control, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (control is DayHeaderControl headerControl)
|
||||
{
|
||||
headerControl.UpdateHeaderText();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateHeaderText()
|
||||
{
|
||||
if (HeaderTextblock != null)
|
||||
{
|
||||
HeaderTextblock.Text = DisplayType == DayHeaderDisplayType.TwelveHour ? Date.ToString("h tt") : Date.ToString("HH:mm");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using CommunityToolkit.WinUI;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Wino.Calendar.Controls;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
namespace Wino.Mail.WinUI.Controls.Calendar;
|
||||
|
||||
/// <summary>
|
||||
/// AOT-Safe ItemsControl for use in UniformGrid panels.
|
||||
/// </summary>
|
||||
///
|
||||
public partial class UniformItemsControl : Grid
|
||||
{
|
||||
[GeneratedDependencyProperty]
|
||||
public partial DayRangeRenderModel? RenderModel { get; set; }
|
||||
|
||||
[GeneratedDependencyProperty]
|
||||
public partial List<CalendarDayModel>? ItemsSource { get; set; }
|
||||
|
||||
partial void OnRenderModelChanged(DayRangeRenderModel? newValue)
|
||||
{
|
||||
if (newValue == null || ItemsSource == null) return;
|
||||
|
||||
AdjustColumns();
|
||||
}
|
||||
|
||||
partial void OnItemsSourceChanged(List<CalendarDayModel>? newValue)
|
||||
{
|
||||
if (newValue == null || ItemsSource == null) return;
|
||||
|
||||
AdjustColumns();
|
||||
}
|
||||
|
||||
private void AdjustColumns()
|
||||
{
|
||||
if (RenderModel == null || ItemsSource == null) return;
|
||||
|
||||
Children.Clear();
|
||||
ColumnDefinitions.Clear();
|
||||
|
||||
var columns = RenderModel.TotalDays;
|
||||
|
||||
// First divide.
|
||||
for (int i = 0; i < columns; i++)
|
||||
{
|
||||
ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
}
|
||||
|
||||
// Then add items.
|
||||
for (int i = 0; i < columns; i++)
|
||||
{
|
||||
var item = ItemsSource[i];
|
||||
|
||||
var control = new DayColumnControl()
|
||||
{
|
||||
DayModel = item
|
||||
};
|
||||
|
||||
SetColumn(control, i);
|
||||
Children.Add(control);
|
||||
}
|
||||
}
|
||||
}
|
||||
//public partial class UniformItemsControl : ItemsControl
|
||||
//{
|
||||
// private const string ControlUniformGridName = "PART_UniformGrid";
|
||||
|
||||
// [GeneratedDependencyProperty]
|
||||
// public partial DayRangeRenderModel? RenderModel { get; set; }
|
||||
|
||||
// partial void OnRenderModelChanged(DayRangeRenderModel? newValue)
|
||||
// {
|
||||
// if (newValue == null) return;
|
||||
|
||||
// // Adjust the ItemsPanel based on the RenderModel's columns.
|
||||
// var uniGrid = WinoVisualTreeHelper.FindDescendants<UniformGrid>(this);
|
||||
|
||||
// //if (uniGrid != null)
|
||||
// //{
|
||||
// // uniGrid.Columns = newValue.TotalDays;
|
||||
// //}
|
||||
// }
|
||||
//}
|
||||
@@ -1,293 +0,0 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.WinUI;
|
||||
using Itenso.TimePeriod;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.Foundation;
|
||||
using Wino.Calendar.Models;
|
||||
using Wino.Calendar.ViewModels.Data;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Calendar.Controls;
|
||||
|
||||
public partial class WinoCalendarPanel : Panel
|
||||
{
|
||||
private const double LastItemRightExtraMargin = 12d;
|
||||
|
||||
// Store each ICalendarItem measurements by their Id.
|
||||
private readonly Dictionary<ICalendarItem, CalendarItemMeasurement> _measurements = new Dictionary<ICalendarItem, CalendarItemMeasurement>();
|
||||
|
||||
public static readonly DependencyProperty EventItemMarginProperty = DependencyProperty.Register(nameof(EventItemMargin), typeof(Thickness), typeof(WinoCalendarPanel), new PropertyMetadata(new Thickness(0, 0, 0, 0)));
|
||||
public static readonly DependencyProperty HourHeightProperty = DependencyProperty.Register(nameof(HourHeight), typeof(double), typeof(WinoCalendarPanel), new PropertyMetadata(0d));
|
||||
public static readonly DependencyProperty PeriodProperty = DependencyProperty.Register(nameof(Period), typeof(ITimePeriod), typeof(WinoCalendarPanel), new PropertyMetadata(null));
|
||||
|
||||
public ITimePeriod Period
|
||||
{
|
||||
get { return (ITimePeriod)GetValue(PeriodProperty); }
|
||||
set { SetValue(PeriodProperty, value); }
|
||||
}
|
||||
|
||||
public double HourHeight
|
||||
{
|
||||
get { return (double)GetValue(HourHeightProperty); }
|
||||
set { SetValue(HourHeightProperty, value); }
|
||||
}
|
||||
|
||||
public Thickness EventItemMargin
|
||||
{
|
||||
get { return (Thickness)GetValue(EventItemMarginProperty); }
|
||||
set { SetValue(EventItemMarginProperty, value); }
|
||||
}
|
||||
|
||||
private void ResetMeasurements() => _measurements.Clear();
|
||||
|
||||
private double GetChildTopMargin(ICalendarItem calendarItemViewModel, double availableHeight)
|
||||
{
|
||||
var childStart = calendarItemViewModel.StartDate;
|
||||
|
||||
if (childStart <= Period.Start)
|
||||
{
|
||||
// Event started before or exactly at the periods tart. This might be a multi-day event.
|
||||
// We can simply consider event must not have a top margin.
|
||||
|
||||
return 0d;
|
||||
}
|
||||
|
||||
double minutesFromStart = (childStart - Period.Start).TotalMinutes;
|
||||
return (minutesFromStart / 1440) * availableHeight;
|
||||
}
|
||||
|
||||
private double GetChildWidth(CalendarItemMeasurement calendarItemMeasurement, double availableWidth)
|
||||
{
|
||||
return (calendarItemMeasurement.Right - calendarItemMeasurement.Left) * availableWidth;
|
||||
}
|
||||
|
||||
private double GetChildLeftMargin(CalendarItemMeasurement calendarItemMeasurement, double availableWidth)
|
||||
=> availableWidth * calendarItemMeasurement.Left;
|
||||
|
||||
private double GetChildHeight(ICalendarItem child)
|
||||
{
|
||||
// All day events are not measured.
|
||||
if (child.IsAllDayEvent) return 0;
|
||||
|
||||
double childDurationInMinutes = 0d;
|
||||
double availableHeight = HourHeight * 24;
|
||||
|
||||
var periodRelation = child.Period.GetRelation(Period);
|
||||
|
||||
// Debug.WriteLine($"Render relation of {child.Title} ({child.Period.Start} - {child.Period.End}) is {periodRelation} with {Period.Start.Day}");
|
||||
|
||||
if (!child.IsMultiDayEvent)
|
||||
{
|
||||
childDurationInMinutes = child.Period.Duration.TotalMinutes;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Multi-day event.
|
||||
// Check how many of the event falls into the current period.
|
||||
childDurationInMinutes = (child.Period.End - Period.Start).TotalMinutes;
|
||||
}
|
||||
|
||||
return (childDurationInMinutes / 1440) * availableHeight;
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
ResetMeasurements();
|
||||
return base.MeasureOverride(availableSize);
|
||||
}
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
if (Period == null || HourHeight == 0d) return finalSize;
|
||||
|
||||
// Measure/arrange each child height and width.
|
||||
// This is a vertical calendar. Therefore the height of each child is the duration of the event.
|
||||
// Children weights for left and right will be saved if they don't exist.
|
||||
// This is important because we don't want to measure the weights again.
|
||||
// They don't change until new event is added or removed.
|
||||
// Width of the each child may depend on the rectangle packing algorithm.
|
||||
// Children are first categorized into columns. Then each column is shifted to the left until
|
||||
// no overlap occurs. The width of each child is calculated based on the number of columns it spans.
|
||||
|
||||
double availableHeight = finalSize.Height;
|
||||
double availableWidth = finalSize.Width;
|
||||
|
||||
var calendarControls = Children.Cast<ContentPresenter>();
|
||||
|
||||
if (!calendarControls.Any()) return base.ArrangeOverride(finalSize);
|
||||
|
||||
var events = calendarControls.Select(a => a.Content as CalendarItemViewModel).OfType<ICalendarItem>();
|
||||
|
||||
LayoutEvents(events);
|
||||
|
||||
foreach (var control in calendarControls)
|
||||
{
|
||||
// We can't arrange this child.
|
||||
if (!(control.Content is ICalendarItem child)) continue;
|
||||
|
||||
bool isHorizontallyLastItem = false;
|
||||
|
||||
double childWidth = 0,
|
||||
childHeight = Math.Max(0, GetChildHeight(child)),
|
||||
childTop = Math.Max(0, GetChildTopMargin(child, availableHeight)),
|
||||
childLeft = 0;
|
||||
|
||||
// No need to measure anything here.
|
||||
if (childHeight == 0) continue;
|
||||
|
||||
if (!_measurements.ContainsKey(child))
|
||||
{
|
||||
// Multi-day event.
|
||||
|
||||
childLeft = 0;
|
||||
childWidth = availableWidth;
|
||||
}
|
||||
else
|
||||
{
|
||||
var childMeasurement = _measurements[child];
|
||||
|
||||
childWidth = Math.Max(0, GetChildWidth(childMeasurement, finalSize.Width));
|
||||
childLeft = Math.Max(0, GetChildLeftMargin(childMeasurement, availableWidth));
|
||||
|
||||
isHorizontallyLastItem = childMeasurement.Right == 1;
|
||||
}
|
||||
|
||||
// Add additional right margin to items that falls on the right edge of the panel.
|
||||
double extraRightMargin = 0;
|
||||
|
||||
// Multi-day events don't have any margin and their hit test is disabled.
|
||||
if (!child.IsMultiDayEvent)
|
||||
{
|
||||
// Max of 5% of the width or 20px max.
|
||||
extraRightMargin = isHorizontallyLastItem ? Math.Max(LastItemRightExtraMargin, finalSize.Width * 5 / 100) : 0;
|
||||
}
|
||||
|
||||
if (childWidth < 0) childWidth = 1;
|
||||
|
||||
// Regular events must have 2px margin
|
||||
if (!child.IsMultiDayEvent && !child.IsAllDayEvent)
|
||||
{
|
||||
childLeft += 2;
|
||||
childTop += 2;
|
||||
childHeight -= 2;
|
||||
childWidth -= 2;
|
||||
}
|
||||
|
||||
var arrangementRect = new Rect(childLeft + EventItemMargin.Left, childTop + EventItemMargin.Top, Math.Max(childWidth - extraRightMargin, 1), childHeight);
|
||||
|
||||
// Make sure measured size will fit in the arranged box.
|
||||
var measureSize = arrangementRect.ToSize();
|
||||
control.Measure(measureSize);
|
||||
control.Arrange(arrangementRect);
|
||||
|
||||
//Debug.WriteLine($"{child.Title}, Measured: {measureSize}, Arranged: {arrangementRect}");
|
||||
}
|
||||
|
||||
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
#region ColumSpanning and Packing Algorithm
|
||||
|
||||
private void AddOrUpdateMeasurement(ICalendarItem calendarItem, CalendarItemMeasurement measurement)
|
||||
{
|
||||
if (_measurements.ContainsKey(calendarItem))
|
||||
{
|
||||
_measurements[calendarItem] = measurement;
|
||||
}
|
||||
else
|
||||
{
|
||||
_measurements.Add(calendarItem, measurement);
|
||||
}
|
||||
}
|
||||
|
||||
// Pick the left and right positions of each event, such that there are no overlap.
|
||||
private void LayoutEvents(IEnumerable<ICalendarItem> events)
|
||||
{
|
||||
var columns = new List<List<ICalendarItem>>();
|
||||
DateTime? lastEventEnding = null;
|
||||
|
||||
foreach (var ev in events.OrderBy(ev => ev.StartDate).ThenBy(ev => ev.EndDate))
|
||||
{
|
||||
// Multi-day events are not measured.
|
||||
if (ev.IsMultiDayEvent) continue;
|
||||
|
||||
if (ev.Period.Start >= lastEventEnding)
|
||||
{
|
||||
PackEvents(columns);
|
||||
columns.Clear();
|
||||
lastEventEnding = null;
|
||||
}
|
||||
|
||||
bool placed = false;
|
||||
|
||||
foreach (var col in columns)
|
||||
{
|
||||
if (!col.Last().Period.OverlapsWith(ev.Period))
|
||||
{
|
||||
col.Add(ev);
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!placed)
|
||||
{
|
||||
columns.Add(new List<ICalendarItem> { ev });
|
||||
}
|
||||
if (lastEventEnding == null || ev.Period.End > lastEventEnding.Value)
|
||||
{
|
||||
lastEventEnding = ev.Period.End;
|
||||
}
|
||||
}
|
||||
if (columns.Count > 0)
|
||||
{
|
||||
PackEvents(columns);
|
||||
}
|
||||
}
|
||||
|
||||
// Set the left and right positions for each event in the connected group.
|
||||
private void PackEvents(List<List<ICalendarItem>> columns)
|
||||
{
|
||||
float numColumns = columns.Count;
|
||||
int iColumn = 0;
|
||||
|
||||
foreach (var col in columns)
|
||||
{
|
||||
foreach (var ev in col)
|
||||
{
|
||||
int colSpan = ExpandEvent(ev, iColumn, columns);
|
||||
|
||||
var leftWeight = iColumn / numColumns;
|
||||
var rightWeight = (iColumn + colSpan) / numColumns;
|
||||
|
||||
AddOrUpdateMeasurement(ev, new CalendarItemMeasurement(leftWeight, rightWeight));
|
||||
}
|
||||
|
||||
iColumn++;
|
||||
}
|
||||
}
|
||||
|
||||
// Checks how many columns the event can expand into, without colliding with other events.
|
||||
private int ExpandEvent(ICalendarItem ev, int iColumn, List<List<ICalendarItem>> columns)
|
||||
{
|
||||
int colSpan = 1;
|
||||
|
||||
foreach (var col in columns.Skip(iColumn + 1))
|
||||
{
|
||||
foreach (var ev1 in col)
|
||||
{
|
||||
if (ev1.Period.OverlapsWith(ev.Period)) return colSpan;
|
||||
}
|
||||
|
||||
colSpan++;
|
||||
}
|
||||
|
||||
return colSpan;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Input;
|
||||
using CommunityToolkit.Diagnostics;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Mail.WinUI.Controls;
|
||||
|
||||
namespace Wino.Calendar.Controls;
|
||||
|
||||
@@ -11,6 +12,7 @@ public partial class WinoCalendarTypeSelectorControl : Control
|
||||
private const string PART_TodayButton = nameof(PART_TodayButton);
|
||||
private const string PART_DayToggle = nameof(PART_DayToggle);
|
||||
private const string PART_WeekToggle = nameof(PART_WeekToggle);
|
||||
private const string PART_WorkWeekToggle = nameof(PART_WorkWeekToggle);
|
||||
private const string PART_MonthToggle = nameof(PART_MonthToggle);
|
||||
|
||||
public static readonly DependencyProperty SelectedTypeProperty = DependencyProperty.Register(
|
||||
@@ -42,6 +44,7 @@ public partial class WinoCalendarTypeSelectorControl : Control
|
||||
private AppBarButton? _todayButton;
|
||||
private AppBarToggleButton? _dayToggle;
|
||||
private AppBarToggleButton? _weekToggle;
|
||||
private AppBarToggleButton? _workWeekToggle;
|
||||
private AppBarToggleButton? _monthToggle;
|
||||
|
||||
public WinoCalendarTypeSelectorControl()
|
||||
@@ -58,24 +61,34 @@ public partial class WinoCalendarTypeSelectorControl : Control
|
||||
_todayButton = GetTemplateChild(PART_TodayButton) as AppBarButton;
|
||||
_dayToggle = GetTemplateChild(PART_DayToggle) as AppBarToggleButton;
|
||||
_weekToggle = GetTemplateChild(PART_WeekToggle) as AppBarToggleButton;
|
||||
_workWeekToggle = GetTemplateChild(PART_WorkWeekToggle) as AppBarToggleButton;
|
||||
_monthToggle = GetTemplateChild(PART_MonthToggle) as AppBarToggleButton;
|
||||
|
||||
Guard.IsNotNull(_todayButton, nameof(_todayButton));
|
||||
Guard.IsNotNull(_dayToggle, nameof(_dayToggle));
|
||||
Guard.IsNotNull(_weekToggle, nameof(_weekToggle));
|
||||
Guard.IsNotNull(_workWeekToggle, nameof(_workWeekToggle));
|
||||
Guard.IsNotNull(_monthToggle, nameof(_monthToggle));
|
||||
|
||||
_todayButton!.Click += TodayClicked;
|
||||
|
||||
_dayToggle!.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Day); };
|
||||
_weekToggle!.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Week); };
|
||||
_monthToggle!.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Month); };
|
||||
_dayToggle!.Click += DayToggleClicked;
|
||||
_weekToggle!.Click += WeekToggleClicked;
|
||||
_workWeekToggle!.Click += WorkWeekToggleClicked;
|
||||
_monthToggle!.Click += MonthToggleClicked;
|
||||
|
||||
UpdateToggleButtonStates();
|
||||
}
|
||||
|
||||
private void TodayClicked(object? sender, RoutedEventArgs e) => TodayClickedCommand?.Execute(null);
|
||||
|
||||
private void DayToggleClicked(object sender, RoutedEventArgs e) => SetSelectedType(CalendarDisplayType.Day);
|
||||
|
||||
private void WeekToggleClicked(object sender, RoutedEventArgs e) => SetSelectedType(CalendarDisplayType.Week);
|
||||
|
||||
private void WorkWeekToggleClicked(object sender, RoutedEventArgs e) => SetSelectedType(CalendarDisplayType.WorkWeek);
|
||||
|
||||
private void MonthToggleClicked(object sender, RoutedEventArgs e) => SetSelectedType(CalendarDisplayType.Month);
|
||||
|
||||
private void SetSelectedType(CalendarDisplayType type)
|
||||
{
|
||||
SelectedType = type;
|
||||
@@ -84,8 +97,10 @@ public partial class WinoCalendarTypeSelectorControl : Control
|
||||
|
||||
private static void OnSelectedTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var control = d as WinoCalendarTypeSelectorControl;
|
||||
control?.UpdateToggleButtonStates();
|
||||
if (d is WinoCalendarTypeSelectorControl control)
|
||||
{
|
||||
control.UpdateToggleButtonStates();
|
||||
}
|
||||
}
|
||||
|
||||
private void UnregisterHandlers()
|
||||
@@ -94,17 +109,38 @@ public partial class WinoCalendarTypeSelectorControl : Control
|
||||
{
|
||||
_todayButton.Click -= TodayClicked;
|
||||
}
|
||||
|
||||
if (_dayToggle != null)
|
||||
{
|
||||
_dayToggle.Click -= DayToggleClicked;
|
||||
}
|
||||
|
||||
if (_weekToggle != null)
|
||||
{
|
||||
_weekToggle.Click -= WeekToggleClicked;
|
||||
}
|
||||
|
||||
if (_workWeekToggle != null)
|
||||
{
|
||||
_workWeekToggle.Click -= WorkWeekToggleClicked;
|
||||
}
|
||||
|
||||
if (_monthToggle != null)
|
||||
{
|
||||
_monthToggle.Click -= MonthToggleClicked;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateToggleButtonStates()
|
||||
{
|
||||
if (_dayToggle == null || _weekToggle == null || _monthToggle == null)
|
||||
if (_dayToggle == null || _weekToggle == null || _workWeekToggle == null || _monthToggle == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_dayToggle.IsChecked = SelectedType == CalendarDisplayType.Day;
|
||||
_weekToggle.IsChecked = SelectedType == CalendarDisplayType.Week;
|
||||
_workWeekToggle.IsChecked = SelectedType == CalendarDisplayType.WorkWeek;
|
||||
_monthToggle.IsChecked = SelectedType == CalendarDisplayType.Month;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ public static class ControlConstants
|
||||
{ WinoIconGlyph.CalendarToday, "\uE911" },
|
||||
{ WinoIconGlyph.CalendarDay, "\uE913" },
|
||||
{ WinoIconGlyph.CalendarWeek, "\uE914" },
|
||||
{ WinoIconGlyph.CalendarWorkWeek, "\uE914" },
|
||||
{ WinoIconGlyph.CalendarMonth, "\uE91c" },
|
||||
{ WinoIconGlyph.CalendarYear, "\uE917" },
|
||||
{ WinoIconGlyph.WeatherBlow, "\uE907" },
|
||||
|
||||
Reference in New Issue
Block a user