Calendar rendering.
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Wino.Calendar.ViewModels.Data;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
namespace Wino.Calendar.Controls;
|
||||
|
||||
public sealed class TimedItemLayout
|
||||
{
|
||||
public TimedItemLayout(CalendarItemViewModel item, int dayIndex, DateOnly date, LayoutRect bounds, DataTemplate? template = null)
|
||||
{
|
||||
Item = item;
|
||||
DayIndex = dayIndex;
|
||||
Date = date;
|
||||
Bounds = bounds;
|
||||
Template = template;
|
||||
}
|
||||
|
||||
public CalendarItemViewModel Item { get; set; }
|
||||
public int DayIndex { get; set; }
|
||||
public DateOnly Date { get; set; }
|
||||
public LayoutRect Bounds { get; set; }
|
||||
public DataTemplate? Template { get; set; }
|
||||
}
|
||||
|
||||
internal sealed record TimedCalendarLayoutResult(IReadOnlyList<DateOnly> VisibleDates, double DayWidth, IReadOnlyList<TimedItemLayout> Items);
|
||||
|
||||
internal static class TimedCalendarLayoutCalculator
|
||||
{
|
||||
public static double GetTimelineHeight(double hourHeight) => hourHeight * 24d;
|
||||
|
||||
public static TimedCalendarLayoutResult Calculate(VisibleDateRange range, IEnumerable<CalendarItemViewModel> items, double availableWidth, double hourHeight)
|
||||
{
|
||||
var visibleDates = range.Dates;
|
||||
var dayWidth = visibleDates.Count == 0 ? 0d : availableWidth / visibleDates.Count;
|
||||
var layouts = new List<TimedItemLayout>();
|
||||
|
||||
for (var dayIndex = 0; dayIndex < visibleDates.Count; dayIndex++)
|
||||
{
|
||||
var date = visibleDates[dayIndex];
|
||||
var daySegments = BuildDaySegments(items, date)
|
||||
.OrderBy(segment => segment.StartMinute)
|
||||
.ThenBy(segment => segment.EndMinute)
|
||||
.ToList();
|
||||
|
||||
foreach (var cluster in BuildClusters(daySegments))
|
||||
{
|
||||
AssignColumns(cluster);
|
||||
var columnCount = cluster.Max(segment => segment.ColumnIndex) + 1;
|
||||
var subColumnWidth = columnCount == 0 ? dayWidth : dayWidth / columnCount;
|
||||
|
||||
foreach (var segment in cluster)
|
||||
{
|
||||
var x = (dayIndex * dayWidth) + (segment.ColumnIndex * subColumnWidth) + 2;
|
||||
var width = Math.Max(0, subColumnWidth - 4);
|
||||
var y = (segment.StartMinute / 60d) * hourHeight;
|
||||
var height = Math.Max(1, ((segment.EndMinute - segment.StartMinute) / 60d) * hourHeight);
|
||||
|
||||
layouts.Add(new TimedItemLayout(segment.Item, dayIndex, date, new LayoutRect(x, y, width, height)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new TimedCalendarLayoutResult(visibleDates, dayWidth, layouts);
|
||||
}
|
||||
|
||||
private static List<Segment> BuildDaySegments(IEnumerable<CalendarItemViewModel> items, DateOnly date)
|
||||
{
|
||||
var dayStart = date.ToDateTime(TimeOnly.MinValue);
|
||||
var dayEnd = dayStart.AddDays(1);
|
||||
var segments = new List<Segment>();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (!CalendarItemAccessor.TryGetTimeRange(item, out var start, out var end))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var localStart = start.LocalDateTime;
|
||||
var localEnd = end.LocalDateTime;
|
||||
|
||||
if (localEnd <= localStart)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var segmentStart = localStart > dayStart ? localStart : dayStart;
|
||||
var segmentEnd = localEnd < dayEnd ? localEnd : dayEnd;
|
||||
|
||||
if (segmentEnd <= segmentStart)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
segments.Add(new Segment(item, (segmentStart - dayStart).TotalMinutes, (segmentEnd - dayStart).TotalMinutes));
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
private static IEnumerable<List<Segment>> BuildClusters(List<Segment> segments)
|
||||
{
|
||||
if (segments.Count == 0)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var cluster = new List<Segment> { segments[0] };
|
||||
var clusterEnd = segments[0].EndMinute;
|
||||
|
||||
for (var index = 1; index < segments.Count; index++)
|
||||
{
|
||||
var segment = segments[index];
|
||||
|
||||
if (segment.StartMinute < clusterEnd)
|
||||
{
|
||||
cluster.Add(segment);
|
||||
clusterEnd = Math.Max(clusterEnd, segment.EndMinute);
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return cluster;
|
||||
cluster = [segment];
|
||||
clusterEnd = segment.EndMinute;
|
||||
}
|
||||
|
||||
yield return cluster;
|
||||
}
|
||||
|
||||
private static void AssignColumns(List<Segment> segments)
|
||||
{
|
||||
var columnEnds = new List<double>();
|
||||
|
||||
foreach (var segment in segments)
|
||||
{
|
||||
var assignedColumn = -1;
|
||||
|
||||
for (var columnIndex = 0; columnIndex < columnEnds.Count; columnIndex++)
|
||||
{
|
||||
if (columnEnds[columnIndex] <= segment.StartMinute)
|
||||
{
|
||||
assignedColumn = columnIndex;
|
||||
columnEnds[columnIndex] = segment.EndMinute;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (assignedColumn < 0)
|
||||
{
|
||||
assignedColumn = columnEnds.Count;
|
||||
columnEnds.Add(segment.EndMinute);
|
||||
}
|
||||
|
||||
segment.ColumnIndex = assignedColumn;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record Segment(CalendarItemViewModel Item, double StartMinute, double EndMinute)
|
||||
{
|
||||
public int ColumnIndex { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user