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 VisibleDates, double DayWidth, IReadOnlyList 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 items, double availableWidth, double hourHeight) { var visibleDates = range.Dates; var dayWidth = visibleDates.Count == 0 ? 0d : availableWidth / visibleDates.Count; var layouts = new List(); 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 BuildDaySegments(IEnumerable items, DateOnly date) { var dayStart = date.ToDateTime(TimeOnly.MinValue); var dayEnd = dayStart.AddDays(1); var segments = new List(); 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 CalculateAllDayItems(VisibleDateRange range, IEnumerable items, double availableWidth) { var visibleDates = range.Dates; var dayWidth = visibleDates.Count == 0 ? 0d : availableWidth / visibleDates.Count; var layouts = new List(); 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 visibleDates, IEnumerable items) { var laneCount = 0; foreach (var date in visibleDates) { laneCount = Math.Max(laneCount, BuildAllDayItems(items, date).Count); } return laneCount; } private static IEnumerable> BuildClusters(List segments) { if (segments.Count == 0) { yield break; } var cluster = new List { 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 BuildAllDayItems(IEnumerable items, DateOnly date) { var dayStart = date.ToDateTime(TimeOnly.MinValue); var dayEnd = dayStart.AddDays(1); var allDayItems = new List(); 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 segments) { var columnEnds = new List(); 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; } } }