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,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);