266 lines
8.3 KiB
C#
266 lines
8.3 KiB
C#
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
|
|
{
|
|
private const double AllDayItemHeight = 32d;
|
|
private const double AllDayItemGap = 4d;
|
|
private const double AllDaySectionPadding = 6d;
|
|
|
|
public static double GetTimelineHeight(double hourHeight) => hourHeight * 24d;
|
|
|
|
public static double GetAllDayHeight(int laneCount)
|
|
{
|
|
if (laneCount <= 0)
|
|
{
|
|
return 0d;
|
|
}
|
|
|
|
return (AllDaySectionPadding * 2d) +
|
|
(laneCount * AllDayItemHeight) +
|
|
((laneCount - 1) * AllDayItemGap);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if (item.IsAllDayEvent)
|
|
{
|
|
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;
|
|
}
|
|
|
|
public static IReadOnlyList<TimedItemLayout> CalculateAllDayItems(VisibleDateRange range, IEnumerable<CalendarItemViewModel> items, double availableWidth)
|
|
{
|
|
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 dayItems = BuildAllDayItems(items, date)
|
|
.OrderBy(item => item.StartDate)
|
|
.ThenBy(item => item.EndDate)
|
|
.ThenBy(item => item.Title)
|
|
.ToList();
|
|
|
|
for (var rowIndex = 0; rowIndex < dayItems.Count; rowIndex++)
|
|
{
|
|
var y = AllDaySectionPadding + (rowIndex * (AllDayItemHeight + AllDayItemGap));
|
|
var x = (dayIndex * dayWidth) + 2d;
|
|
var width = Math.Max(0d, dayWidth - 4d);
|
|
|
|
layouts.Add(new TimedItemLayout(
|
|
dayItems[rowIndex],
|
|
dayIndex,
|
|
date,
|
|
new LayoutRect(x, y, width, AllDayItemHeight)));
|
|
}
|
|
}
|
|
|
|
return layouts;
|
|
}
|
|
|
|
public static int GetAllDayLaneCount(IReadOnlyList<DateOnly> visibleDates, IEnumerable<CalendarItemViewModel> items)
|
|
{
|
|
var laneCount = 0;
|
|
|
|
foreach (var date in visibleDates)
|
|
{
|
|
laneCount = Math.Max(laneCount, BuildAllDayItems(items, date).Count);
|
|
}
|
|
|
|
return laneCount;
|
|
}
|
|
|
|
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 List<CalendarItemViewModel> BuildAllDayItems(IEnumerable<CalendarItemViewModel> items, DateOnly date)
|
|
{
|
|
var dayStart = date.ToDateTime(TimeOnly.MinValue);
|
|
var dayEnd = dayStart.AddDays(1);
|
|
var allDayItems = new List<CalendarItemViewModel>();
|
|
|
|
foreach (var item in items)
|
|
{
|
|
if (!item.IsAllDayEvent)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!CalendarItemAccessor.TryGetTimeRange(item, out var start, out var end))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var localStart = start.LocalDateTime;
|
|
var localEnd = end.LocalDateTime;
|
|
|
|
if (localEnd <= localStart)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (localStart < dayEnd && localEnd > dayStart)
|
|
{
|
|
allDayItems.Add(item);
|
|
}
|
|
}
|
|
|
|
return allDayItems;
|
|
}
|
|
|
|
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; }
|
|
}
|
|
}
|