Calendar improvements cycle 2

This commit is contained in:
Burak Kaan Köse
2026-03-25 15:49:14 +01:00
parent 8c492bb094
commit e3c3b341e5
16 changed files with 332 additions and 62 deletions
@@ -6,14 +6,14 @@ namespace Wino.Calendar.Controls;
public sealed class CalendarEmptySlotTappedEventArgs : EventArgs
{
public CalendarEmptySlotTappedEventArgs(DateTime clickedDate, Point positionerPoint, Size cellSize)
public CalendarEmptySlotTappedEventArgs(DateTime clickedDate, Point anchorPoint, Size cellSize)
{
ClickedDate = clickedDate;
PositionerPoint = positionerPoint;
AnchorPoint = anchorPoint;
CellSize = cellSize;
}
public DateTime ClickedDate { get; }
public Point PositionerPoint { get; }
public Point AnchorPoint { get; }
public Size CellSize { get; }
}
@@ -9,7 +9,6 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:skia="using:SkiaSharp.Views.Windows"
xmlns:viewModels="using:Wino.Calendar.ViewModels.Data"
x:Name="Root"
SizeChanged="ControlSizeChanged"
mc:Ignorable="d">
@@ -22,10 +21,7 @@
</DataTemplate>
<DataTemplate x:Key="MonthEventTemplate" x:DataType="viewModels:CalendarItemViewModel">
<local:CalendarItemControl
Margin="0,2,0,0"
CalendarItem="{x:Bind}"
IsCustomEventArea="True" />
<local:CalendarItemControl CalendarItem="{x:Bind}" IsCustomEventArea="True" />
</DataTemplate>
<DataTemplate x:Key="TimedHeaderTemplate" x:DataType="local:HeaderTextLayout">
@@ -77,8 +73,8 @@
x:Name="TimedHeaderHost"
Grid.Row="0"
Grid.ColumnSpan="2"
Margin="64,0,0,0"
Height="44"
Margin="64,0,0,0"
Background="{ThemeResource LayerFillColorDefaultBrush}">
<skia:SKXamlCanvas x:Name="TimedHeaderCanvas" PaintSurface="TimedHeaderCanvasPaintSurface" />
<ItemsControl
@@ -159,7 +155,13 @@
Background="Transparent"
Tapped="MonthInteractionLayerTapped" />
<Canvas x:Name="MonthCellLabelsCanvas" IsHitTestVisible="False" />
<Canvas x:Name="MonthItemsCanvas" />
<Canvas x:Name="MonthItemsCanvas">
<Canvas.Transitions>
<TransitionCollection>
<AddDeleteThemeTransition />
</TransitionCollection>
</Canvas.Transitions>
</Canvas>
</Grid>
</Grid>
</Grid>
@@ -35,6 +35,7 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
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),
@@ -78,6 +79,12 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
[GeneratedDependencyProperty]
public partial Brush? WorkHourBackground { get; set; }
[GeneratedDependencyProperty]
public partial Brush? SelectedSlotBackground { get; set; }
[GeneratedDependencyProperty]
public partial DateTime? SelectedDateTime { get; set; }
public CalendarPeriodControl()
{
InitializeComponent();
@@ -153,6 +160,8 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
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 OnSelectedDateTimeChanged(DateTime? newValue) => InvalidateStructureCanvases();
partial void OnCalendarItemsChanged(IReadOnlyList<CalendarItemViewModel>? newValue)
{
@@ -231,6 +240,12 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
QueueRefresh();
}
private void InvalidateStructureCanvases()
{
TimedStructureCanvas.Invalidate();
MonthStructureCanvas.Invalidate();
}
private void Refresh()
{
if (!_refreshPending || !IsLoaded || ActualWidth <= 0 || VisibleRange is null || CalendarSettings is null)
@@ -461,6 +476,7 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
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));
var canvas = e.Surface.Canvas;
canvas.Clear(SKColors.Transparent);
@@ -499,6 +515,12 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
}
}
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;
@@ -522,6 +544,7 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
Color = new SKColor(0, 120, 215, 26),
IsAntialias = true
};
using var selectedPaint = CreateFillPaint(SelectedSlotBackground ?? new SolidColorBrush(Colors.Transparent));
var canvas = e.Surface.Canvas;
canvas.Clear(SKColors.Transparent);
@@ -545,6 +568,12 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
canvas.DrawRect((float)cell.Bounds.X, (float)cell.Bounds.Y, (float)cell.Bounds.Width, (float)cell.Bounds.Height, todayPaint);
}
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;
@@ -604,7 +633,7 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
{
var presenter = new ContentPresenter
{
Width = item.Bounds.Width,
Width = Math.Max(0d, item.Bounds.Width - TimedItemRightSpacing),
Height = item.Bounds.Height,
Content = item.Item,
ContentTemplate = item.Template
@@ -669,12 +698,13 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
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,
new Point(dayIndex * _timedLayout.DayWidth, slotIndex * intervalHeight),
anchorPoint,
new Size(_timedLayout.DayWidth, intervalHeight)));
}
@@ -690,15 +720,75 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
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),
new Point(cell.Bounds.X, cell.Bounds.Y),
anchorPoint,
new Size(cell.Bounds.Width, cell.Bounds.Height)));
}
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? 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 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)
@@ -66,7 +66,8 @@ internal static class MonthCalendarLayoutCalculator
private const double CellPadding = 4d;
private const double DayLabelHeight = 20d;
private const double ItemHeight = 18d;
private const double RegularItemHeight = 18d;
private const double ExpandedItemHeight = 30d;
private const double ItemGap = 2d;
public static MonthCalendarLayoutResult Calculate(VisibleDateRange range, IEnumerable<CalendarItemViewModel> items, double availableWidth, double availableHeight)
@@ -92,12 +93,13 @@ internal static class MonthCalendarLayoutCalculator
foreach (var cell in cells)
{
var cellItems = GetCellItems(items, cell.Date).ToList();
var nextItemY = cell.Bounds.Y + DayLabelHeight + CellPadding;
for (var index = 0; index < cellItems.Count; index++)
{
var y = cell.Bounds.Y + DayLabelHeight + CellPadding + (index * (ItemHeight + ItemGap));
var itemHeight = GetItemHeight(cellItems[index]);
if (y + ItemHeight > cell.Bounds.Y + cell.Bounds.Height - CellPadding)
if (nextItemY + itemHeight > cell.Bounds.Y + cell.Bounds.Height - CellPadding)
{
break;
}
@@ -108,9 +110,11 @@ internal static class MonthCalendarLayoutCalculator
cell.Date,
new LayoutRect(
cell.Bounds.X + CellPadding,
y,
nextItemY,
Math.Max(0, cell.Bounds.Width - (CellPadding * 2)),
ItemHeight)));
itemHeight)));
nextItemY += itemHeight + ItemGap;
}
}
@@ -143,4 +147,9 @@ internal static class MonthCalendarLayoutCalculator
}
}
}
private static double GetItemHeight(CalendarItemViewModel item)
=> item.IsAllDayEvent || item.IsMultiDayEvent
? ExpandedItemHeight
: RegularItemHeight;
}