win2d -> skia, some improvements on rendering.

This commit is contained in:
Burak Kaan Köse
2025-12-30 10:02:24 +01:00
parent 72e43e4b7a
commit 07f3dabff6
15 changed files with 174 additions and 129 deletions
+1
View File
@@ -45,6 +45,7 @@
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageVersion Include="Serilog.Sinks.ApplicationInsights" Version="4.0.0" />
<PackageVersion Include="SkiaSharp" Version="3.119.1" />
<PackageVersion Include="SkiaSharp.Views.WinUI" Version="3.119.1" />
<PackageVersion Include="sqlite-net-pcl" Version="1.10.196-beta" />
<PackageVersion Include="System.Drawing.Common" Version="10.0.1" />
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
@@ -150,7 +150,7 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
private async Task InitializeAccountCalendarsAsync()
{
await Dispatcher.ExecuteOnUIThread(() => AccountCalendarStateService.ClearGroupedAccountCalendar());
await Dispatcher.ExecuteOnUIThread(() => AccountCalendarStateService.ClearGroupedAccountCalendars());
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
@@ -611,8 +611,6 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
}
}
private async Task InitializeCalendarEventsForDayRangeAsync(DayRangeRenderModel dayRangeRenderModel)
{
// Clear all events first for all days.
@@ -626,9 +624,8 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
// Initialization is done for all calendars, regardless whether they are actively selected or not.
// This is because the filtering is cached internally of the calendar items in CalendarEventCollection.
var allCalendars = AccountCalendarStateService.GroupedAccountCalendars.SelectMany(a => a.AccountCalendars);
foreach (var calendarViewModel in allCalendars)
foreach (var calendarViewModel in AccountCalendarStateService.AllCalendars)
{
// Check all the events for the given date range and calendar.
// Then find the day representation for all the events returned, and add to the collection.
@@ -69,10 +69,10 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject
}
[ObservableProperty]
private bool _isExpanded = true;
public partial bool IsExpanded { get; set; } = true;
[ObservableProperty]
private bool? isCheckedState = true;
public partial bool? IsCheckedState { get; set; } = true;
private bool _isExternalPropChangeBlocked = false;
@@ -15,7 +15,7 @@ public interface IAccountCalendarStateService : INotifyPropertyChanged
public void AddGroupedAccountCalendar(GroupedAccountCalendarViewModel groupedAccountCalendar);
public void RemoveGroupedAccountCalendar(GroupedAccountCalendarViewModel groupedAccountCalendar);
public void ClearGroupedAccountCalendar();
public void ClearGroupedAccountCalendars();
public void AddAccountCalendar(AccountCalendarViewModel accountCalendar);
public void RemoveAccountCalendar(AccountCalendarViewModel accountCalendar);
@@ -24,5 +24,5 @@ public interface IAccountCalendarStateService : INotifyPropertyChanged
/// Enumeration of currently selected calendars.
/// </summary>
IEnumerable<AccountCalendarViewModel> ActiveCalendars { get; }
// IEnumerable<IGrouping<MailAccount, AccountCalendarViewModel>> GroupedAccountCalendarsEnumerable { get; }
IEnumerable<AccountCalendarViewModel> AllCalendars { get; }
}
@@ -1,5 +1,8 @@
using Microsoft.UI.Xaml.Automation.Peers;
using System.Linq;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Automation.Peers;
using Microsoft.UI.Xaml.Controls;
using Wino.Mail.WinUI.Controls.CalendarFlipView;
namespace Wino.Calendar.Controls;
@@ -11,21 +14,37 @@ public partial class CustomCalendarFlipView : FlipView
private const string PART_PreviousButton = "PreviousButtonHorizontal";
private const string PART_NextButton = "NextButtonHorizontal";
private Button PreviousButton;
private Button NextButton;
private Button? PreviousButton;
private Button? NextButton;
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
PreviousButton = GetTemplateChild(PART_PreviousButton) as Button;
NextButton = GetTemplateChild(PART_NextButton) as Button;
PreviousButton = (Button)GetTemplateChild(PART_PreviousButton);
NextButton = (Button)GetTemplateChild(PART_NextButton);
// Hide navigation buttons
PreviousButton.Opacity = NextButton.Opacity = 0;
PreviousButton.IsHitTestVisible = NextButton.IsHitTestVisible = false;
this.SelectionChanged += FlipViewSelectionChanged;
}
private void FlipViewSelectionChanged(object sender, SelectionChangedEventArgs e) => OnSelectedItemChanged(e.RemovedItems.FirstOrDefault(), e.AddedItems.FirstOrDefault());
protected virtual void OnSelectedItemChanged(object oldValue, object newValue) { }
protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
base.PrepareContainerForItemOverride(element, item);
OnContainerPrepared(element, item);
}
protected virtual void OnContainerPrepared(DependencyObject element, object item) { }
protected override DependencyObject GetContainerForItemOverride() => new WinoCalendarFlyoutItem();
public void GoPreviousFlip()
{
var backPeer = new ButtonAutomationPeer(PreviousButton);
@@ -187,8 +187,6 @@ public partial class WinoCalendarControl : Control
canvas.SelectedDateTime = null;
canvas.TimelineCellSelected -= ActiveTimelineCellSelected;
canvas.TimelineCellUnselected -= ActiveTimelineCellUnselected;
canvas.Dispose();
}
private void RegisterCanvas(WinoDayTimelineCanvas canvas)
@@ -20,9 +20,9 @@ public partial class WinoCalendarFlipView : CustomCalendarFlipView
/// Gets or sets the active canvas that is currently displayed in the flip view.
/// Each day-range of flip view item has a canvas that displays the day timeline.
/// </summary>
public WinoDayTimelineCanvas ActiveCanvas
public WinoDayTimelineCanvas? ActiveCanvas
{
get { return (WinoDayTimelineCanvas)GetValue(ActiveCanvasProperty); }
get { return (WinoDayTimelineCanvas?)GetValue(ActiveCanvasProperty); }
set { SetValue(ActiveCanvasProperty, value); }
}
@@ -31,9 +31,9 @@ public partial class WinoCalendarFlipView : CustomCalendarFlipView
/// It's the vertical scroll that scrolls the timeline only, not the header part that belongs
/// to parent FlipView control.
/// </summary>
public ScrollViewer ActiveVerticalScrollViewer
public ScrollViewer? ActiveVerticalScrollViewer
{
get { return (ScrollViewer)GetValue(ActiveVerticalScrollViewerProperty); }
get { return (ScrollViewer?)GetValue(ActiveVerticalScrollViewerProperty); }
set { SetValue(ActiveVerticalScrollViewerProperty, value); }
}
@@ -45,7 +45,6 @@ public partial class WinoCalendarFlipView : CustomCalendarFlipView
public WinoCalendarFlipView()
{
RegisterPropertyChangedCallback(SelectedIndexProperty, new DependencyPropertyChangedCallback(OnSelectedIndexUpdated));
RegisterPropertyChangedCallback(ItemsSourceProperty, new DependencyPropertyChangedCallback(OnItemsSourceChanged));
}
@@ -57,15 +56,6 @@ public partial class WinoCalendarFlipView : CustomCalendarFlipView
}
}
private static void OnSelectedIndexUpdated(DependencyObject d, DependencyProperty e)
{
if (d is WinoCalendarFlipView flipView)
{
flipView.UpdateActiveCanvas();
flipView.UpdateActiveScrollViewer();
}
}
private void RegisterItemsSourceChange()
{
if (GetItemsSource() is INotifyCollectionChanged notifyCollectionChanged)
@@ -74,61 +64,51 @@ public partial class WinoCalendarFlipView : CustomCalendarFlipView
}
}
protected override void OnSelectedItemChanged(object oldValue, object newValue)
{
base.OnSelectedItemChanged(oldValue, newValue);
UpdateActiveElements();
}
protected override void OnContainerPrepared(DependencyObject element, object item)
{
base.OnContainerPrepared(element, item);
// Check if this is the currently selected item's container
var index = IndexFromContainer(element);
if (index >= 0 && index == SelectedIndex)
{
// Container for selected item is now ready, update active elements
UpdateActiveElements();
}
}
private void ItemsSourceUpdated(object sender, NotifyCollectionChangedEventArgs e)
{
IsIdle = e.Action == NotifyCollectionChangedAction.Reset || e.Action == NotifyCollectionChangedAction.Replace;
}
private async Task<FlipViewItem> GetCurrentFlipViewItem()
{
// TODO: Refactor this mechanism by listening to PrepareContainerForItemOverride and Loaded events together.
while (ContainerFromIndex(SelectedIndex) == null)
{
await Task.Delay(100);
}
return ContainerFromIndex(SelectedIndex) as FlipViewItem;
}
private void UpdateActiveScrollViewer()
private void UpdateActiveElements()
{
if (SelectedIndex < 0)
ActiveVerticalScrollViewer = null;
else
{
GetCurrentFlipViewItem().ContinueWith(task =>
{
if (task.IsCompletedSuccessfully)
{
var flipViewItem = task.Result;
_ = DispatcherQueue.TryEnqueue(() =>
{
ActiveVerticalScrollViewer = flipViewItem.FindDescendant<ScrollViewer>();
});
}
});
}
}
public void UpdateActiveCanvas()
{
if (SelectedIndex < 0)
ActiveCanvas = null;
ActiveVerticalScrollViewer = null;
return;
}
// Get container from index - respects virtualization
if (ContainerFromIndex(SelectedIndex) is FlipViewItem container)
{
ActiveCanvas = container.FindDescendant<WinoDayTimelineCanvas>();
ActiveVerticalScrollViewer = container.FindDescendant<ScrollViewer>();
}
else
{
GetCurrentFlipViewItem().ContinueWith(task =>
{
if (task.IsCompletedSuccessfully)
{
var flipViewItem = task.Result;
_ = DispatcherQueue.TryEnqueue(() =>
{
ActiveCanvas = flipViewItem.FindDescendant<WinoDayTimelineCanvas>();
});
}
});
// Container not ready yet - will be updated when OnContainerPrepared is called
ActiveCanvas = null;
ActiveVerticalScrollViewer = null;
}
}
@@ -2,13 +2,13 @@
using System.Diagnostics;
using System.Linq;
using CommunityToolkit.WinUI;
using Microsoft.Graphics.Canvas.Geometry;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI.Input;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using SkiaSharp;
using SkiaSharp.Views.Windows;
using Windows.Foundation;
using Wino.Calendar.Args;
using Wino.Core.Domain.Models.Calendar;
@@ -21,7 +21,7 @@ public partial class WinoDayTimelineCanvas : Control, IDisposable
public event EventHandler<TimelineCellUnselectedArgs> TimelineCellUnselected;
private const string PART_InternalCanvas = nameof(PART_InternalCanvas);
private CanvasControl Canvas;
private SKXamlCanvas Canvas;
public static readonly DependencyProperty RenderOptionsProperty = DependencyProperty.Register(nameof(RenderOptions), typeof(CalendarRenderOptions), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
public static readonly DependencyProperty SeperatorColorProperty = DependencyProperty.Register(nameof(SeperatorColor), typeof(SolidColorBrush), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
@@ -77,11 +77,13 @@ public partial class WinoDayTimelineCanvas : Control, IDisposable
{
base.OnApplyTemplate();
Canvas = GetTemplateChild(PART_InternalCanvas) as CanvasControl;
Canvas = GetTemplateChild(PART_InternalCanvas) as SKXamlCanvas;
// TODO: These will leak. Dispose them properly when needed.
Canvas.Draw += OnCanvasDraw;
Canvas.PointerPressed += OnCanvasPointerPressed;
if (Canvas != null)
{
Canvas.PaintSurface += OnCanvasPaintSurface;
Canvas.PointerPressed += OnCanvasPointerPressed;
}
ForceDraw();
}
@@ -179,21 +181,23 @@ public partial class WinoDayTimelineCanvas : Control, IDisposable
{
return RenderOptions != null
&& Canvas != null
&& Canvas.ReadyToDraw
&& WorkingHourCellBackgroundColor != null
&& SeperatorColor != null
&& HalfHourSeperatorColor != null
&& SelectedCellBackgroundBrush != null;
}
private void OnCanvasDraw(CanvasControl sender, CanvasDrawEventArgs args)
private void OnCanvasPaintSurface(object sender, SKPaintSurfaceEventArgs e)
{
if (!CanDrawTimeline()) return;
var canvas = e.Surface.Canvas;
canvas.Clear(SKColors.Transparent);
int hours = 24;
double canvasWidth = Canvas.ActualWidth;
double canvasHeight = Canvas.ActualHeight;
double canvasWidth = e.Info.Width;
double canvasHeight = e.Info.Height;
if (canvasWidth == 0 || canvasHeight == 0) return;
@@ -205,9 +209,33 @@ public partial class WinoDayTimelineCanvas : Control, IDisposable
double rectHeight = RenderOptions.CalendarSettings.HourHeight;
// Define stroke and fill colors
var strokeColor = SeperatorColor.Color;
var strokeColor = ToSKColor(SeperatorColor.Color);
float strokeThickness = 0.5f;
// Create paints for drawing
using var strokePaint = new SKPaint
{
Color = strokeColor,
StrokeWidth = strokeThickness,
Style = SKPaintStyle.Stroke,
IsAntialias = true
};
using var fillPaint = new SKPaint
{
Style = SKPaintStyle.Fill,
IsAntialias = true
};
using var dashedPaint = new SKPaint
{
Color = ToSKColor(HalfHourSeperatorColor.Color),
StrokeWidth = strokeThickness,
Style = SKPaintStyle.Stroke,
PathEffect = SKPathEffect.CreateDash([2f, 2f], 0),
IsAntialias = true
};
for (int day = 0; day < RenderOptions.TotalDayCount; day++)
{
var currentDay = RenderOptions.DateRange.StartDate.AddDays(day);
@@ -222,32 +250,31 @@ public partial class WinoDayTimelineCanvas : Control, IDisposable
var representingDateTime = currentDay.AddHours(hour);
// Calculate the position and size of the rectangle
double x = day * rectWidth;
double y = hour * rectHeight;
float x = (float)(day * rectWidth);
float y = (float)(hour * rectHeight);
float width = (float)rectWidth;
float height = (float)rectHeight;
var rectangle = new Rect(x, y, rectWidth, rectHeight);
var rectangle = new SKRect(x, y, x + width, y + height);
// Draw the rectangle border.
// This is the main rectangle.
args.DrawingSession.DrawRectangle(rectangle, strokeColor, strokeThickness);
canvas.DrawRect(rectangle, strokePaint);
// Fill another rectangle with the working hour background color
// This rectangle must be placed with -1 margin to prevent invisible borders of the main rectangle.
if (isWorkingDay && renderTime >= RenderOptions.CalendarSettings.WorkingHourStart && renderTime <= RenderOptions.CalendarSettings.WorkingHourEnd)
{
var backgroundRectangle = new Rect(x + 1, y + 1, rectWidth - 1, rectHeight - 1);
var backgroundRectangle = new SKRect(x + 1, y + 1, x + width - 1, y + height - 1);
args.DrawingSession.DrawRectangle(backgroundRectangle, strokeColor, strokeThickness);
args.DrawingSession.FillRectangle(backgroundRectangle, WorkingHourCellBackgroundColor.Color);
canvas.DrawRect(backgroundRectangle, strokePaint);
fillPaint.Color = ToSKColor(WorkingHourCellBackgroundColor.Color);
canvas.DrawRect(backgroundRectangle, fillPaint);
}
// Draw a line in the center of the rectangle for representing half hours.
double lineY = y + rectHeight / 2;
args.DrawingSession.DrawLine((float)x, (float)lineY, (float)(x + rectWidth), (float)lineY, HalfHourSeperatorColor.Color, strokeThickness, new CanvasStrokeStyle()
{
DashStyle = CanvasDashStyle.Dot
});
float lineY = y + height / 2;
canvas.DrawLine(x, lineY, x + width, lineY, dashedPaint);
}
// Draw selected item background color for the date if possible.
@@ -265,20 +292,30 @@ public partial class WinoDayTimelineCanvas : Control, IDisposable
selectedY += rectHeight / 2;
}
var selectedRectangle = new Rect(day * rectWidth, selectedY, rectWidth, selectionRectHeight);
args.DrawingSession.FillRectangle(selectedRectangle, SelectedCellBackgroundBrush.Color);
var selectedRectangle = new SKRect(
(float)(day * rectWidth),
(float)selectedY,
(float)(day * rectWidth + rectWidth),
(float)(selectedY + selectionRectHeight));
fillPaint.Color = ToSKColor(SelectedCellBackgroundBrush.Color);
canvas.DrawRect(selectedRectangle, fillPaint);
}
}
}
}
private static SKColor ToSKColor(Windows.UI.Color color)
{
return new SKColor(color.R, color.G, color.B, color.A);
}
public void Dispose()
{
if (Canvas == null) return;
Canvas.Draw -= OnCanvasDraw;
Canvas.PaintSurface -= OnCanvasPaintSurface;
Canvas.PointerPressed -= OnCanvasPointerPressed;
Canvas.RemoveFromVisualTree();
Canvas = null;
}
@@ -0,0 +1,11 @@
using Microsoft.UI.Xaml.Controls;
namespace Wino.Mail.WinUI.Controls.CalendarFlipView;
public partial class WinoCalendarFlyoutItem : FlipViewItem
{
public WinoCalendarFlyoutItem()
{
DefaultStyleKey = typeof(WinoCalendarFlyoutItem);
}
}
@@ -24,6 +24,7 @@ public partial class WinoMailItemViewModelListViewItem : ListViewItem
partial void OnItemPropertyChanged(DependencyPropertyChangedEventArgs e)
{
// TODO: This slows down. Optimize later.
Debug.WriteLine("WinoMailItemViewModelListViewItem item changed");
if (e.OldValue is MailItemViewModel oldMailItemViewModel) UnregisterPropertyChanged(oldMailItemViewModel);
@@ -5,6 +5,7 @@ using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using Wino.Calendar.ViewModels.Data;
using Wino.Calendar.ViewModels.Interfaces;
using Wino.Core.Domain.Entities.Shared;
namespace Wino.Mail.WinUI.Services;
@@ -14,44 +15,43 @@ namespace Wino.Mail.WinUI.Services;
/// </summary>
public partial class AccountCalendarStateService : ObservableObject, IAccountCalendarStateService
{
public event EventHandler<GroupedAccountCalendarViewModel> CollectiveAccountGroupSelectionStateChanged;
public event EventHandler<AccountCalendarViewModel> AccountCalendarSelectionStateChanged;
public event EventHandler<GroupedAccountCalendarViewModel>? CollectiveAccountGroupSelectionStateChanged;
public event EventHandler<AccountCalendarViewModel>? AccountCalendarSelectionStateChanged;
private readonly ObservableCollection<GroupedAccountCalendarViewModel> _internalGroupedAccountCalendars;
[ObservableProperty]
public partial ReadOnlyObservableCollection<GroupedAccountCalendarViewModel> GroupedAccountCalendars { get; set; }
private ObservableCollection<GroupedAccountCalendarViewModel> _internalGroupedAccountCalendars = new ObservableCollection<GroupedAccountCalendarViewModel>();
public IEnumerable<AccountCalendarViewModel> ActiveCalendars
{
get
{
return GroupedAccountCalendars
.SelectMany(a => a.AccountCalendars)
.Where(b => b.IsChecked);
return _internalGroupedAccountCalendars
.SelectMany(a => a.AccountCalendars)
.Where(b => b.IsChecked);
}
}
//public IEnumerable<IGrouping<MailAccount, AccountCalendarViewModel>> GroupedAccountCalendarsEnumerable
//{
// get
// {
// return GroupedAccountCalendars
// .Select(a => a.AccountCalendars)
// .SelectMany(b => b)
// .GroupBy(c => c.Account);
// }
//}
public IEnumerable<AccountCalendarViewModel> AllCalendars
{
get
{
return _internalGroupedAccountCalendars
.SelectMany(a => a.AccountCalendars);
}
}
public AccountCalendarStateService()
{
_internalGroupedAccountCalendars = new ObservableCollection<GroupedAccountCalendarViewModel>();
GroupedAccountCalendars = new ReadOnlyObservableCollection<GroupedAccountCalendarViewModel>(_internalGroupedAccountCalendars);
}
private void SingleGroupCalendarCollectiveStateChanged(object sender, EventArgs e)
=> CollectiveAccountGroupSelectionStateChanged?.Invoke(this, sender as GroupedAccountCalendarViewModel);
private void SingleGroupCalendarCollectiveStateChanged(object? sender, EventArgs e)
=> CollectiveAccountGroupSelectionStateChanged?.Invoke(this, sender as GroupedAccountCalendarViewModel ?? throw new InvalidOperationException("Sender must be GroupedAccountCalendarViewModel"));
private void SingleCalendarSelectionStateChanged(object sender, AccountCalendarViewModel e)
private void SingleCalendarSelectionStateChanged(object? sender, AccountCalendarViewModel e)
=> AccountCalendarSelectionStateChanged?.Invoke(this, e);
public void AddGroupedAccountCalendar(GroupedAccountCalendarViewModel groupedAccountCalendar)
@@ -70,7 +70,7 @@ public partial class AccountCalendarStateService : ObservableObject, IAccountCal
_internalGroupedAccountCalendars.Remove(groupedAccountCalendar);
}
public void ClearGroupedAccountCalendar()
public void ClearGroupedAccountCalendars()
{
while (_internalGroupedAccountCalendars.Any())
{
@@ -1,8 +1,8 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:canvas="using:Microsoft.Graphics.Canvas.UI.Xaml"
xmlns:controls="using:Wino.Calendar.Controls">
xmlns:controls="using:Wino.Calendar.Controls"
xmlns:skia="using:SkiaSharp.Views.Windows">
<!-- Background Timeline Canvas -->
<Style TargetType="controls:WinoDayTimelineCanvas">
@@ -10,7 +10,7 @@
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="controls:WinoDayTimelineCanvas">
<canvas:CanvasControl x:Name="PART_InternalCanvas" />
<skia:SKXamlCanvas x:Name="PART_InternalCanvas" />
</ControlTemplate>
</Setter.Value>
</Setter>
@@ -150,11 +150,11 @@
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<DataTemplate x:DataType="data:GroupedAccountCalendarViewModel">
<TextBlock
FontSize="14"
FontWeight="SemiBold"
Text="{Binding Key.Name}" />
Text="{x:Bind Account.Name}" />
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
+1
View File
@@ -144,6 +144,7 @@
<PackageReference Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" />
<PackageReference Include="Microsoft.Graphics.Win2D" />
<PackageReference Include="SkiaSharp.Views.WinUI" />
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
<PackageReference Include="Microsoft.WindowsAppSDK" />