Calendar rendering improvements.

This commit is contained in:
Burak Kaan Köse
2026-03-25 13:39:27 +01:00
parent 0056f372b9
commit 8c492bb094
20 changed files with 212 additions and 108 deletions
@@ -9,12 +9,14 @@
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"
Loaded="ControlLoaded"
SizeChanged="ControlSizeChanged"
mc:Ignorable="d">
<UserControl.Resources>
<DataTemplate x:Key="CalendarEventTemplate" x:DataType="viewModels:CalendarItemViewModel">
<local:CalendarItemControl CalendarItem="{x:Bind}" />
</DataTemplate>
@@ -3,12 +3,15 @@ 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;
@@ -27,6 +30,8 @@ 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;
@@ -48,9 +53,12 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
private double _monthCellWidth;
private double _monthCellHeight;
private bool _hasPresentedState;
private bool _refreshPending = true;
private bool _refreshScheduled;
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; }
@@ -64,7 +72,21 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
[GeneratedDependencyProperty]
public partial string? TimedHeaderDateFormat { get; set; }
public CalendarPeriodControl() => InitializeComponent();
[GeneratedDependencyProperty]
public partial Brush? DefaultHourBackground { get; set; }
[GeneratedDependencyProperty]
public partial Brush? WorkHourBackground { 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;
@@ -128,33 +150,51 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
public double TimelineHeight => TimedCalendarLayoutCalculator.GetTimelineHeight(GetHourHeight());
partial void OnVisibleRangeChanged(VisibleDateRange? newValue) => Refresh();
partial void OnCalendarSettingsChanged(CalendarSettings? newValue) => Refresh();
partial void OnTimedHeaderDateFormatChanged(string? newValue) => Refresh();
partial void OnVisibleRangeChanged(VisibleDateRange? newValue) => RequestRefresh();
partial void OnCalendarSettingsChanged(CalendarSettings? newValue) => RequestRefresh();
partial void OnTimedHeaderDateFormatChanged(string? newValue) => RequestRefresh();
partial void OnCalendarItemsChanged(IReadOnlyList<CalendarItemViewModel>? newValue)
{
DetachCurrentItemsSource();
AttachItemsSource(newValue);
Refresh();
RequestRefresh();
}
private void ControlLoaded(object sender, RoutedEventArgs e)
private void ControlSizeChanged(object sender, SizeChangedEventArgs e)
{
AttachItemsSource(CalendarItems);
Refresh();
}
if (!HasMeaningfulSizeChange(e))
{
return;
}
private void ControlSizeChanged(object sender, SizeChangedEventArgs e) => Refresh();
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)
{
_observableItemsSource = itemsSource as INotifyCollectionChanged;
if (_observableItemsSource is not null)
if (itemsSource is INotifyCollectionChanged observableItemsSource)
{
_observableItemsSource = observableItemsSource;
_observableItemsSource.CollectionChanged += ItemsSourceCollectionChanged;
}
}
@@ -183,11 +223,17 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
}
}
private void ItemsSourceCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) => Refresh();
private void ItemsSourceCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) => RequestRefresh();
private void RequestRefresh()
{
_refreshPending = true;
QueueRefresh();
}
private void Refresh()
{
if (!IsLoaded || ActualWidth <= 0 || VisibleRange is null || CalendarSettings is null)
if (!_refreshPending || !IsLoaded || ActualWidth <= 0 || VisibleRange is null || CalendarSettings is null)
{
return;
}
@@ -206,9 +252,37 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
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 SizeRefreshTimerTick(DispatcherQueueTimer sender, object args)
{
sender.Stop();
QueueRefresh();
}
private void RefreshTimedView()
@@ -385,8 +459,8 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
{
using var linePaint = CreateLinePaint();
using var minorLinePaint = CreateMinorLinePaint();
using var defaultFillPaint = CreateFillPaint(GetDefaultHourBackground());
using var workFillPaint = CreateFillPaint(GetWorkHourBackground());
using var defaultFillPaint = CreateFillPaint(DefaultHourBackground ?? new SolidColorBrush(Colors.Transparent));
using var workFillPaint = CreateFillPaint(WorkHourBackground ?? new SolidColorBrush(Colors.Transparent));
var canvas = e.Surface.Canvas;
canvas.Clear(SKColors.Transparent);
@@ -937,26 +1011,6 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
: SKColors.Transparent;
}
private Brush GetDefaultHourBackground()
{
if (Application.Current.Resources.TryGetValue("LayerFillColorDefaultBrush", out var resource) && resource is Brush brush)
{
return brush;
}
return new SolidColorBrush(Color.FromArgb(255, 28, 34, 42));
}
private Brush GetWorkHourBackground()
{
if (Application.Current.Resources.TryGetValue("SolidBackgroundFillColorBaseBrush", out var resource) && resource is SolidColorBrush solidBrush)
{
return new SolidColorBrush(Color.FromArgb(64, solidBrush.Color.R, solidBrush.Color.G, solidBrush.Color.B));
}
return new SolidColorBrush(Color.FromArgb(255, 34, 40, 52));
}
private static double GetTimedGridIntervalHeight(double hourHeight) => hourHeight * (TimedGridIntervalMinutes / 60d);
private double GetTimedGridIntervalHeight() => GetTimedGridIntervalHeight(GetHourHeight());
@@ -14,55 +14,59 @@
BasedOn="{StaticResource DefaultButtonStyle}"
TargetType="Button">
<Setter Property="Margin" Value="0" />
<Setter Property="Width" Value="44" />
<Setter Property="Height" Value="36" />
<Setter Property="Padding" Value="10,6" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Height" Value="32" />
<Setter Property="Padding" Value="0" />
<Setter Property="Foreground" Value="{ThemeResource SystemColorControlAccentBrush}" />
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
</Style>
</UserControl.Resources>
<Grid Margin="4,0,0,0" Background="Transparent">
<Grid Background="Transparent" ColumnSpacing="16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="64" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid VerticalAlignment="Center" ColumnSpacing="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Click="PreviousDateClicked" Style="{StaticResource CalendarNavigationButtonStyle}">
<Viewbox Width="12">
<PathIcon Data="F1 M 8.72 18.599998 C 8.879999 18.733334 9.059999 18.799999 9.26 18.799999 C 9.459999 18.799999 9.633332 18.719999 9.78 18.559999 C 9.926666 18.4 10 18.219999 10 18.019999 C 10 17.82 9.92 17.653332 9.76 17.52 L 4.52 12.559999 L 17.24 12.559999 C 17.453333 12.559999 17.633331 12.486667 17.779999 12.339999 C 17.926666 12.193334 18 12.013333 18 11.799999 C 18 11.586666 17.926666 11.406667 17.779999 11.259999 C 17.633331 11.113333 17.453333 11.039999 17.24 11.039999 L 4.52 11.039999 L 9.76 6.08 C 9.973333 5.893333 10.046666 5.653332 9.98 5.359999 C 9.913333 5.066666 9.74 4.880001 9.46 4.799999 C 9.179999 4.720001 8.933332 4.786667 8.72 5 L 2.32 11.08 C 2.16 11.24 2.053333 11.426666 2 11.639999 C 1.973333 11.746666 1.973333 11.853333 2 11.959999 C 2.053333 12.173333 2.16 12.360001 2.32 12.52 Z " />
</Viewbox>
</Button>
<Button
Grid.Column="1"
Click="NextDateClicked"
Style="{StaticResource CalendarNavigationButtonStyle}">
<Viewbox Width="12">
<PathIcon Data="F1 M 11.28 5 C 11.12 4.866667 10.94 4.806667 10.74 4.82 C 10.539999 4.833334 10.366666 4.913334 10.219999 5.059999 C 10.073333 5.206665 10 5.379999 10 5.58 C 10 5.779999 10.08 5.946667 10.24 6.08 L 15.48 11.039999 L 2.76 11.039999 C 2.546667 11.039999 2.366667 11.113333 2.22 11.259999 C 2.073333 11.406667 2 11.586666 2 11.799999 C 2 12.013333 2.073333 12.193334 2.22 12.339999 C 2.366667 12.486667 2.546667 12.559999 2.76 12.559999 L 15.48 12.559999 L 10.24 17.52 C 10.026667 17.706665 9.953333 17.946667 10.02 18.24 C 10.086666 18.533333 10.259999 18.719999 10.54 18.799999 C 10.82 18.879999 11.066667 18.813334 11.28 18.599998 L 17.68 12.52 C 17.84 12.360001 17.946667 12.173333 18 11.959999 C 18 11.853333 18 11.746666 18 11.639999 C 17.946667 11.426666 17.84 11.24 17.68 11.08 Z " />
</Viewbox>
</Button>
</Grid>
<TextBlock
x:Name="VisibleDateRangeTextBlock"
Grid.Column="1"
MaxHeight="30"
Margin="0,4,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="18"
IsHitTestVisible="False"
Style="{StaticResource BodyTextBlockStyle}"
TextAlignment="Center" />
<StackPanel
<calendarControls:WinoCalendarTypeSelectorControl
x:Name="CalendarTypeSelector"
Grid.Column="2"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Orientation="Horizontal"
Spacing="8">
<StackPanel
VerticalAlignment="Center"
Orientation="Horizontal"
Spacing="4">
<Button Click="PreviousDateClicked" Style="{StaticResource CalendarNavigationButtonStyle}">
<PathIcon Data="F1 M 8.72 18.599998 C 8.879999 18.733334 9.059999 18.799999 9.26 18.799999 C 9.459999 18.799999 9.633332 18.719999 9.78 18.559999 C 9.926666 18.4 10 18.219999 10 18.019999 C 10 17.82 9.92 17.653332 9.76 17.52 L 4.52 12.559999 L 17.24 12.559999 C 17.453333 12.559999 17.633331 12.486667 17.779999 12.339999 C 17.926666 12.193334 18 12.013333 18 11.799999 C 18 11.586666 17.926666 11.406667 17.779999 11.259999 C 17.633331 11.113333 17.453333 11.039999 17.24 11.039999 L 4.52 11.039999 L 9.76 6.08 C 9.973333 5.893333 10.046666 5.653332 9.98 5.359999 C 9.913333 5.066666 9.74 4.880001 9.46 4.799999 C 9.179999 4.720001 8.933332 4.786667 8.72 5 L 2.32 11.08 C 2.16 11.24 2.053333 11.426666 2 11.639999 C 1.973333 11.746666 1.973333 11.853333 2 11.959999 C 2.053333 12.173333 2.16 12.360001 2.32 12.52 Z " />
</Button>
<Button Click="NextDateClicked" Style="{StaticResource CalendarNavigationButtonStyle}">
<PathIcon Data="F1 M 11.28 5 C 11.12 4.866667 10.94 4.806667 10.74 4.82 C 10.539999 4.833334 10.366666 4.913334 10.219999 5.059999 C 10.073333 5.206665 10 5.379999 10 5.58 C 10 5.779999 10.08 5.946667 10.24 6.08 L 15.48 11.039999 L 2.76 11.039999 C 2.546667 11.039999 2.366667 11.113333 2.22 11.259999 C 2.073333 11.406667 2 11.586666 2 11.799999 C 2 12.013333 2.073333 12.193334 2.22 12.339999 C 2.366667 12.486667 2.546667 12.559999 2.76 12.559999 L 15.48 12.559999 L 10.24 17.52 C 10.026667 17.706665 9.953333 17.946667 10.02 18.24 C 10.086666 18.533333 10.259999 18.719999 10.54 18.799999 C 10.82 18.879999 11.066667 18.813334 11.28 18.599998 L 17.68 12.52 C 17.84 12.360001 17.946667 12.173333 18 11.959999 C 18 11.853333 18 11.746666 18 11.639999 C 17.946667 11.426666 17.84 11.24 17.68 11.08 Z " />
</Button>
</StackPanel>
<calendarControls:WinoCalendarTypeSelectorControl x:Name="CalendarTypeSelector" VerticalAlignment="Center" />
</StackPanel>
VerticalAlignment="Center" />
</Grid>
</Grid>
</UserControl>