From 08c45f8f3cc3be13400cc2f570ea01cacb33cd88 Mon Sep 17 00:00:00 2001 From: Sahil-simform Date: Mon, 28 Jul 2025 19:51:04 +0530 Subject: [PATCH 1/3] improvement --- .../event_arrangers/side_event_arranger.dart | 536 ++++++++++-------- 1 file changed, 314 insertions(+), 222 deletions(-) diff --git a/lib/src/event_arrangers/side_event_arranger.dart b/lib/src/event_arrangers/side_event_arranger.dart index e960421b..04871c74 100644 --- a/lib/src/event_arrangers/side_event_arranger.dart +++ b/lib/src/event_arrangers/side_event_arranger.dart @@ -40,258 +40,350 @@ class SideEventArranger extends EventArranger { required int startHour, required DateTime calendarViewDate, }) { - final totalWidth = width; + if (events.isEmpty) return []; + final startHourInMinutes = startHour * 60; - List<_SideEventConfigs> _categorizedColumnedEvents( - List> events) { - final merged = MergeEventArranger(includeEdges: includeEdges).arrange( - events: events, - height: height, - width: width, - heightPerMinute: heightPerMinute, - startHour: startHour, - calendarViewDate: calendarViewDate, - ); - - final arranged = <_SideEventConfigs>[]; - - for (final event in merged) { - if (event.events.isEmpty) { - // NOTE(parth): This is safety condition. - // This condition should never be true. - // If by chance this becomes true, there is something wrong with - // logic. And that need to be fixed ASAP. - - continue; - } + // Step 1: Normalize all events for the current calendar view date + final normalizedEvents = + _normalizeEventsForDate(events, calendarViewDate, startHourInMinutes); - if (event.events.length > 1) { - // NOTE: This means all the events are overlapping with each other. - // So, we will extract all the events that can be fit in - // Single column without overlapping and run the function - // again for the rest of the events. - - final columnedEvents = _extractSingleColumnEvents( - event.events, - event.endDuration.getTotalMinutes, - ); - - final sided = _categorizedColumnedEvents( - event.events.where((e) => !columnedEvents.contains(e)).toList(), - ); - - var maxColumns = 1; - - for (final event in sided) { - if (event.columns > maxColumns) { - maxColumns = event.columns; - } - } - - arranged.add(_SideEventConfigs( - columns: maxColumns + 1, - event: columnedEvents, - sideEvents: sided, - )); - } else { - // If this block gets executed that means we have only one event. - // Return the event as is. + if (normalizedEvents.isEmpty) return []; + + // Step 2: Sort events by start time for optimal processing + normalizedEvents + .sort((a, b) => a.effectiveStartTime.compareTo(b.effectiveStartTime)); + + // Step 3: Create columns using an efficient sweep line algorithm + final columns = _createOptimalColumns(normalizedEvents); - arranged.add(_SideEventConfigs(columns: 1, event: event.events)); + // Step 4: Convert columns to organized calendar event data + return _convertColumnsToOrganizedData(columns, width, height, + heightPerMinute, startHourInMinutes, calendarViewDate); + } + + /// Normalizes events for the specific calendar view date + List<_NormalizedEvent> _normalizeEventsForDate( + List> events, + DateTime calendarViewDate, + int startHourInMinutes, + ) { + final normalizedEvents = <_NormalizedEvent>[]; + + for (final event in events) { + if (event.startTime == null || event.endTime == null) continue; + + int effectiveStartTime; + int effectiveEndTime; + + if (event.isRangingEvent) { + // Handle multi-day events based on which day we're viewing + final isStartDate = + calendarViewDate.isAtSameMomentAs(event.date.withoutTime); + final isEndDate = + calendarViewDate.isAtSameMomentAs(event.endDate.withoutTime); + + if (isStartDate && isEndDate) { + // Event starts and ends on the same day (single day spanning event) + effectiveStartTime = event.startTime!.getTotalMinutes; + effectiveEndTime = event.endTime!.getTotalMinutes; + } else if (isStartDate) { + // First day of multi-day event - start at event time, end at day end + effectiveStartTime = event.startTime!.getTotalMinutes; + effectiveEndTime = Constants.minutesADay; + } else if (isEndDate) { + // Last day of multi-day event - start at day start, end at event time + effectiveStartTime = 0; + effectiveEndTime = event.endTime!.getTotalMinutes; + } else { + // Middle day of multi-day event - full day + effectiveStartTime = 0; + effectiveEndTime = Constants.minutesADay; } + } else { + // Regular single-day event + effectiveStartTime = event.startTime!.getTotalMinutes; + effectiveEndTime = event.endTime!.getTotalMinutes; } - return arranged; + // Skip events that don't appear in the visible time range + final visibleStart = startHourInMinutes; + final visibleEnd = Constants.minutesADay; + + if (effectiveEndTime <= visibleStart || + effectiveStartTime >= visibleEnd) { + continue; + } + + normalizedEvents.add(_NormalizedEvent( + originalEvent: event, + effectiveStartTime: effectiveStartTime, + effectiveEndTime: effectiveEndTime, + isMultiDay: event.isRangingEvent, + isFullDay: event.isFullDayEvent, + )); } - List> _arrangeEvents( - List<_SideEventConfigs> events, double width, double offset) { - final arranged = >[]; - - for (final event in events) { - final slotWidth = - math.min(width / event.columns, maxWidth ?? double.maxFinite); - - if (event.event.isNotEmpty) { - // TODO(parth): Arrange events and add it in arranged. - - arranged.addAll(event.event.map((e) { - final startTime = e.startTime!; - final endTime = e.endTime!; - - int eventStart; - int eventEnd; - - if (e.isRangingEvent) { - // Handle multi-day events differently based on which day is currently being viewed - final isStartDate = - calendarViewDate.isAtSameMomentAs(e.date.withoutTime); - final isEndDate = - calendarViewDate.isAtSameMomentAs(e.endDate.withoutTime); - - if (isStartDate && isEndDate) { - // Single day event with start and end time - eventStart = startTime.getTotalMinutes - (startHourInMinutes); - eventEnd = endTime.getTotalMinutes - (startHourInMinutes) <= 0 - ? Constants.minutesADay - (startHourInMinutes) - : endTime.getTotalMinutes - (startHourInMinutes); - } else if (isStartDate) { - // First day - show from start time to end of day - eventStart = startTime.getTotalMinutes - (startHourInMinutes); - eventEnd = Constants.minutesADay - (startHourInMinutes); - } else if (isEndDate) { - // Last day - show from start of day to end time - eventStart = 0; - eventEnd = endTime.getTotalMinutes - (startHourInMinutes) <= 0 - ? Constants.minutesADay - (startHourInMinutes) - : endTime.getTotalMinutes - (startHourInMinutes); - } else { - // Middle days - show full day - eventStart = 0; - eventEnd = Constants.minutesADay - (startHourInMinutes); - } - } else { - // Single day event - use normal start/end times - eventStart = startTime.getTotalMinutes - (startHourInMinutes); - eventEnd = endTime.getTotalMinutes - (startHourInMinutes) <= 0 - ? Constants.minutesADay - (startHourInMinutes) - : endTime.getTotalMinutes - (startHourInMinutes); - } - - // Ensure values are within valid range - eventStart = math.max(0, eventStart); - eventEnd = math.min( - Constants.minutesADay - (startHourInMinutes), - eventEnd, - ); - - final top = eventStart * heightPerMinute; - - // Calculate visibleMinutes (the total minutes displayed in the view) - final visibleMinutes = Constants.minutesADay - (startHourInMinutes); - - // Check if event ends at or beyond the visible area - final bottom = eventEnd >= visibleMinutes - ? 0.0 // Event extends to bottom of view - : height - eventEnd * heightPerMinute; - - return OrganizedCalendarEventData( - left: offset, - right: totalWidth - (offset + slotWidth), - top: top, - bottom: bottom, - startDuration: startTime.copyFromMinutes(eventStart), - endDuration: endTime.copyFromMinutes(eventEnd), - events: [e], - calendarViewDate: calendarViewDate, - ); - })); - } + return normalizedEvents; + } + + /// Creates optimal columns using O(n²) algorithm that prevents overlapping + List>> _createOptimalColumns( + List<_NormalizedEvent> events) { + if (events.isEmpty) return []; + + final columns = >>[]; - if (event.sideEvents.isNotEmpty) { - arranged.addAll(_arrangeEvents( - event.sideEvents, - math.max(0, width - slotWidth), - slotWidth + offset, - )); + for (final event in events) { + int targetColumn = -1; + + // Check each existing column to see if this event can fit + for (int i = 0; i < columns.length; i++) { + if (_canEventFitInColumn(event, columns[i])) { + targetColumn = i; + break; } } - return arranged; + // If no available column found, create a new one + if (targetColumn == -1) { + columns.add([event]); + } else { + columns[targetColumn].add(event); + } + } + + return columns; + } + + /// Checks if an event can fit in a specific column without overlapping + bool _canEventFitInColumn( + _NormalizedEvent event, List<_NormalizedEvent> column) { + for (final existing in column) { + if (_eventsOverlap(event, existing)) { + return false; + } + } + return true; + } + + /// Checks if two events overlap based on includeEdges setting + bool _eventsOverlap(_NormalizedEvent event1, _NormalizedEvent event2) { + if (includeEdges) { + // Events overlap if they have any overlapping time (including touching edges) + return event1.effectiveStartTime <= event2.effectiveEndTime && + event2.effectiveStartTime <= event1.effectiveEndTime; + } else { + // Events overlap only if they have actual time overlap (not just touching) + return event1.effectiveStartTime < event2.effectiveEndTime && + event2.effectiveStartTime < event1.effectiveEndTime; } + } - // By default the offset will be 0. + /// Converts columns to organized calendar event data with proper width allocation + List> _convertColumnsToOrganizedData( + List>> columns, + double totalWidth, + double height, + double heightPerMinute, + int startHourInMinutes, + DateTime calendarViewDate, + ) { + final result = >[]; + final columnCount = columns.length; + + if (columnCount == 0) return result; + + // Two different strategies based on whether maxWidth is specified + if (maxWidth != null) { + // Strategy 1: Fixed width columns when maxWidth is specified + _processWithFixedMaxWidth(columns, result, totalWidth, height, + heightPerMinute, startHourInMinutes, calendarViewDate); + } else { + // Strategy 2: Dynamic width allocation when maxWidth is NOT specified + _processWithDynamicWidth(columns, result, totalWidth, height, + heightPerMinute, startHourInMinutes, calendarViewDate); + } - final columned = _categorizedColumnedEvents(events); - final arranged = _arrangeEvents(columned, totalWidth, 0); - return arranged; + return result; } - List> _extractSingleColumnEvents( - List> events, int end) { - // Find the longest event from the list. - final longestEvent = events.fold>( - events.first, - (e1, e2) => e1.duration > e2.duration ? e1 : e2, - ); - - // Create a new list from events and remove the longest one from it. - final searchEvents = [...events]..remove(longestEvent); - - // Create a new list for events in single column. - // Right now it has longest event, - // By the end of the function, this will have the list of the events, - // that are not intersecting with each other. - // and this will be returned from the function. - final columnedEvents = [longestEvent]; - - // Calculate effective end minute from latest columned event. - var endMinutes = longestEvent.endTime!.getTotalMinutes; - - // Run the loop while effective end minute of columned events are - // less than end. - while (endMinutes < end && searchEvents.isNotEmpty) { - // Maps the event with it's duration. - final mappings = >{}; - - // Create a new list from searchEvents. - for (final event in [...searchEvents]) { - // Need to add logic to include edges... - final start = event.startTime!.getTotalMinutes; - - // TODO(parth): Need to improve this. - // This does not handle the case where there is a event before the - // longest event which is not intersecting. - // - if (start < endMinutes || (includeEdges && start == endMinutes)) { - // Remove search event from list so, we do not iterate through it - // again. - searchEvents.remove(event); - } else { - // Add the event in mappings. - final diff = event.startTime!.getTotalMinutes - endMinutes; + /// Process events with fixed maxWidth for each column + /// Process events with fixed maxWidth for each column + void _processWithFixedMaxWidth( + List>> columns, + List> result, + double totalWidth, + double height, + double heightPerMinute, + int startHourInMinutes, + DateTime calendarViewDate, + ) { + final columnCount = columns.length; + final baseSlotWidth = totalWidth / columnCount; + + // Calculate actual widths for all events first to avoid gaps + final eventWidths = {}; + for (int columnIndex = 0; columnIndex < columnCount; columnIndex++) { + final column = columns[columnIndex]; + for (final normalizedEvent in column) { + // Calculate dynamic width available to the right + final dynamicWidth = _calculateAvailableWidthToRight( + normalizedEvent, columns, columnIndex, totalWidth); + + final maxWidthPixels = maxWidth ?? double.maxFinite; + eventWidths[normalizedEvent.hashCode] = + math.min(dynamicWidth, maxWidthPixels); + } + } - mappings.addAll({ - diff: event, - }); - } + // Position events by column + for (int columnIndex = 0; columnIndex < columnCount; columnIndex++) { + final column = columns[columnIndex]; + final leftPosition = columnIndex * maxWidth!; + + for (final normalizedEvent in column) { + final event = normalizedEvent.originalEvent; + + final displayStartTime = math.max( + 0, normalizedEvent.effectiveStartTime - startHourInMinutes); + final displayEndTime = math.min( + Constants.minutesADay - startHourInMinutes, + normalizedEvent.effectiveEndTime - startHourInMinutes); + + // Use the individual event width rather than column width + final actualWidth = eventWidths[normalizedEvent.hashCode]!; + + final top = displayStartTime * heightPerMinute; + final visibleMinutes = Constants.minutesADay - startHourInMinutes; + final bottom = displayEndTime >= visibleMinutes + ? 0.0 + : height - displayEndTime * heightPerMinute; + + result.add(OrganizedCalendarEventData( + left: leftPosition, + right: math.max(0.0, totalWidth - leftPosition - actualWidth), + top: top, + bottom: bottom, + startDuration: event.startTime!.copyFromMinutes(displayStartTime), + endDuration: event.endTime!.copyFromMinutes(displayEndTime), + events: [event], + calendarViewDate: calendarViewDate, + )); } + } + } - // This can be any integer larger than 1440 as one day has 1440 minutes. - // so, different of 2 events end and start time will never be greater than - // 1440. - var min = 4000; + /// Process events with dynamic width allocation (stretch to available space) + void _processWithDynamicWidth( + List>> columns, + List> result, + double totalWidth, + double height, + double heightPerMinute, + int startHourInMinutes, + DateTime calendarViewDate, + ) { + final columnCount = columns.length; + + for (int columnIndex = 0; columnIndex < columnCount; columnIndex++) { + final column = columns[columnIndex]; + + for (final normalizedEvent in column) { + final event = normalizedEvent.originalEvent; + + final displayStartTime = math.max( + 0, normalizedEvent.effectiveStartTime - startHourInMinutes); + final displayEndTime = math.min( + Constants.minutesADay - startHourInMinutes, + normalizedEvent.effectiveEndTime - startHourInMinutes); + + // Calculate available width by checking what's to the right + final availableWidth = _calculateAvailableWidthToRight( + normalizedEvent, columns, columnIndex, totalWidth); + + // Use standard column positioning for left offset + final baseSlotWidth = totalWidth / columnCount; + final leftOffset = columnIndex * baseSlotWidth; + + // Use the available width (can extend beyond original column) + final actualWidth = availableWidth; + + final top = displayStartTime * heightPerMinute; + final visibleMinutes = Constants.minutesADay - startHourInMinutes; + final bottom = displayEndTime >= visibleMinutes + ? 0.0 + : height - displayEndTime * heightPerMinute; + + result.add(OrganizedCalendarEventData( + left: leftOffset, + right: math.max(0.0, totalWidth - leftOffset - actualWidth), + top: top, + bottom: bottom, + startDuration: event.startTime!.copyFromMinutes(displayStartTime), + endDuration: event.endTime!.copyFromMinutes(displayEndTime), + events: [event], + calendarViewDate: calendarViewDate, + )); + } + } + } - for (final mapping in mappings.entries) { - if (mapping.key < min) { - min = mapping.key; + /// Calculates how much width is available to the right of an event + double _calculateAvailableWidthToRight( + _NormalizedEvent targetEvent, + List>> allColumns, + int targetColumnIndex, + double totalWidth, + ) { + final columnCount = allColumns.length; + final baseSlotWidth = totalWidth / columnCount; + final startPosition = targetColumnIndex * baseSlotWidth; + + // Start with remaining space from current position to end + double availableWidth = totalWidth - startPosition; + + // Check each column to the right to see if any events overlap with our target event + for (int rightColumnIndex = targetColumnIndex + 1; + rightColumnIndex < columnCount; + rightColumnIndex++) { + final rightColumn = allColumns[rightColumnIndex]; + bool hasBlockingEvent = false; + + // Check if any event in this right column overlaps with our target event + for (final rightEvent in rightColumn) { + if (_eventsOverlap(targetEvent, rightEvent)) { + hasBlockingEvent = true; + break; } } - if (mappings[min] != null) { - // If mapping had min event, add it in columnedEvents, - // and remove it from searchEvents so, we do not iterate through it - // again. - columnedEvents.add(mappings[min]!); - searchEvents.remove(mappings[min]); - - endMinutes = mappings[min]!.endTime!.getTotalMinutes; + if (hasBlockingEvent) { + // This column blocks further expansion, limit width to this position + final blockingPosition = rightColumnIndex * baseSlotWidth; + availableWidth = blockingPosition - startPosition; + break; } } - return columnedEvents; + // Ensure minimum width of at least one column + return math.max(baseSlotWidth, availableWidth); } } -class _SideEventConfigs { - final int columns; - final List> event; - final List<_SideEventConfigs> sideEvents; - - const _SideEventConfigs({ - this.event = const [], - required this.columns, - this.sideEvents = const [], +/// Internal class to normalize event data for efficient processing +class _NormalizedEvent { + final CalendarEventData originalEvent; + final int effectiveStartTime; + final int effectiveEndTime; + final bool isMultiDay; + final bool isFullDay; + + const _NormalizedEvent({ + required this.originalEvent, + required this.effectiveStartTime, + required this.effectiveEndTime, + required this.isMultiDay, + required this.isFullDay, }); } From 6108c1c04222ca3c90ce8c489829c97bde1093c1 Mon Sep 17 00:00:00 2001 From: Sahil-simform Date: Tue, 29 Jul 2025 11:17:46 +0530 Subject: [PATCH 2/3] improvement --- example/lib/main.dart | 96 +++- example/lib/widgets/add_event_form.dart | 3 +- .../event_arrangers/side_event_arranger.dart | 530 ++++++++++++++++-- 3 files changed, 593 insertions(+), 36 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 4909fb8b..d15e5dc8 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -22,6 +22,7 @@ class _MyAppState extends State { bool isDarkMode = false; // This widget is the root of your application. + final _controller = EventController()..addAll(_events); @override Widget build(BuildContext context) { return CalendarThemeProvider( @@ -39,7 +40,7 @@ class _MyAppState extends State { : MultiDayViewThemeData.light(), ), child: CalendarControllerProvider( - controller: EventController()..addAll(_events), + controller: _controller, child: MaterialApp( title: 'Flutter Calendar Page Demo', debugShowCheckedModeBanner: false, @@ -149,4 +150,97 @@ List _events = [ title: "Chemistry Viva", description: "Today is Joe's birthday.", ), + CalendarEventData( + date: _now.add(Duration(days: 5)), + startTime: DateTime( + _now.add(Duration(days: 5)).year, + _now.add(Duration(days: 5)).month, + _now.add(Duration(days: 5)).day, + 12, + 13), + endTime: DateTime( + _now.add(Duration(days: 5)).year, + _now.add(Duration(days: 5)).month, + _now.add(Duration(days: 5)).day, + 16, + 13), + endDate: _now.add(Duration(days: 5)), + title: "Team Brainstorming", + description: "Quarterly planning session", + color: Color(0xFF9C27B0), + // 4294127905 + recurrenceSettings: null, + ), + CalendarEventData( + date: _now.add(Duration(days: 2)), + startTime: DateTime( + _now.add(Duration(days: 2)).year, + _now.add(Duration(days: 2)).month, + _now.add(Duration(days: 2)).day, + 18, + 14), + endTime: DateTime( + _now.add(Duration(days: 2)).year, + _now.add(Duration(days: 2)).month, + _now.add(Duration(days: 2)).day, + 20, + 00), + endDate: _now.add(Duration(days: 2)), + title: "Project Review", + description: "Review sprint progress with stakeholders", + color: Color(0xFF4CAF35), + // 4280415061 + recurrenceSettings: null, + ), + CalendarEventData( + date: _now.add(Duration(days: 7)), + startTime: DateTime( + _now.add(Duration(days: 7)).year, + _now.add(Duration(days: 7)).month, + _now.add(Duration(days: 7)).day, + 18, + 14), + endTime: DateTime( + _now.add(Duration(days: 7)).year, + _now.add(Duration(days: 7)).month, + _now.add(Duration(days: 7)).day, + 22, + 14), + endDate: _now.add(Duration(days: 7)), + title: "Design Workshop", + description: "UX/UI design planning for new features", + color: Color(0xFF4CAF2F), + // 4280415055 + recurrenceSettings: null, + ), + CalendarEventData( + date: _now, + startTime: DateTime(_now.year, _now.month, _now.day, 15, 0), + endTime: DateTime(_now.year, _now.month, _now.day, 16, 0), + title: "col1 row1", + description: "", + color: AppColors.red), + CalendarEventData( + date: _now, + startTime: DateTime(_now.year, _now.month, _now.day, 16, 0), + endTime: DateTime(_now.year, _now.month, _now.day, 17, 0), + title: "col1 row2", + description: "", + color: Colors.pink), + CalendarEventData( + date: _now, + startTime: DateTime(_now.year, _now.month, _now.day, 15, 30), + endTime: DateTime(_now.year, _now.month, _now.day, 16, 30), + title: "col2 row1", + description: "col2 row1", + color: Colors.yellow, + ), + CalendarEventData( + date: _now, + startTime: DateTime(_now.year, _now.month, _now.day, 16, 30), + endTime: DateTime(_now.year, _now.month, _now.day, 17, 30), + title: "col2 row2", + description: "col2 row2", + color: AppColors.black, + ), ]; diff --git a/example/lib/widgets/add_event_form.dart b/example/lib/widgets/add_event_form.dart index 934a6bb1..bcda6483 100644 --- a/example/lib/widgets/add_event_form.dart +++ b/example/lib/widgets/add_event_form.dart @@ -103,7 +103,7 @@ class _AddOrEditEventFormState extends State { Text( 'Recurring Event', style: TextStyle( - color: color.surface, + color: color.onSurface, fontSize: 16, ), ), @@ -128,7 +128,6 @@ class _AddOrEditEventFormState extends State { } }); }, - activeColor: color.surface, ), ], ), diff --git a/lib/src/event_arrangers/side_event_arranger.dart b/lib/src/event_arrangers/side_event_arranger.dart index 04871c74..7e7d1d88 100644 --- a/lib/src/event_arrangers/side_event_arranger.dart +++ b/lib/src/event_arrangers/side_event_arranger.dart @@ -4,6 +4,40 @@ part of 'event_arrangers.dart'; +/// **SideEventArranger: Smart Event Layout Manager** +/// +/// This class arranges overlapping calendar events side by side in an optimal way, +/// ensuring no visual overlaps while maximizing space utilization. +/// +/// ## **Core Functionality:** +/// - Arranges overlapping events in separate columns +/// - Calculates optimal widths for each event based on available space +/// - Supports both fixed maxWidth constraints and dynamic width allocation +/// - Handles multi-day events, full-day events, and edge cases +/// +/// ## **Algorithm Overview:** +/// 1. **Event Normalization**: Converts events to a standardized format for the current view date +/// 2. **Column Creation**: Uses O(n²) algorithm to group non-overlapping events into columns +/// 3. **Width Calculation**: Determines optimal width for each event based on available space +/// 4. **Positioning**: Places events with proper left/right positioning to eliminate gaps +/// +/// ## **Key Features:** +/// - **Gap Elimination**: Events expand to fill available space when possible +/// - **Edge Case Handling**: Properly handles touching events, multi-day events, and time boundaries +/// - **Flexible Width Control**: Supports both fixed maxWidth and dynamic width allocation +/// - **Performance Optimized**: Efficient algorithms for real-world calendar scenarios +/// +/// ## **Usage Examples:** +/// ```dart +/// // Basic usage with dynamic width +/// final arranger = SideEventArranger(); +/// +/// // With maximum width constraint (100px) +/// final arranger = SideEventArranger(maxWidth: 100.0); +/// +/// // Include touching events as overlapping +/// final arranger = SideEventArranger(includeEdges: true); +/// ``` class SideEventArranger extends EventArranger { /// This class will provide method that will arrange /// all the events side by side. @@ -12,25 +46,84 @@ class SideEventArranger extends EventArranger { this.includeEdges = false, }); - /// Decides whether events that are overlapping on edge - /// (ex, event1 has the same end-time as the start-time of event 2) - /// should be offset or not. - /// - /// If includeEdges is true, it will offset the events else it will not. - /// + /// **Edge Case Handling Configuration** + /// + /// Determines whether events that are touching at their edges should be + /// considered as overlapping and placed in separate columns. + /// + /// **Use Cases:** + /// - `false` (default): Events like 2:00-3:00 PM and 3:00-4:00 PM can share the same column + /// - `true`: Touching events are forced into separate columns for visual clarity + /// + /// **Example:** + /// ```dart + /// // Events: [14:00-15:00], [15:00-16:00] + /// // includeEdges = false: Both events in same column (touching is OK) + /// // includeEdges = true: Events in separate columns (touching treated as overlap) + /// ``` final bool includeEdges; - /// If enough space is available, the event slot will - /// use the specified max width. - /// Otherwise, it will reduce to fit all events in the cell. - /// If max width is not specified, slots will expand to fill the cell. + /// **Maximum Width Constraint (Optional)** + /// + /// When specified, limits the maximum width any event can take, regardless + /// of available space. This ensures consistent visual appearance. + /// + /// **Behavior:** + /// - `null` (default): Events expand to fill all available space + /// - `double value`: Events are capped at this width in pixels + /// + /// **Use Cases:** + /// - UI consistency: Prevent events from becoming too wide + /// - Mobile optimization: Ensure readable text on narrow screens + /// - Design constraints: Match specific layout requirements + /// + /// **Example:** + /// ```dart + /// // Without maxWidth: Event might expand to 400px if space available + /// // With maxWidth: 150.0: Event width capped at 150px + /// ``` final double? maxWidth; - /// {@macro event_arranger_arrange_method_doc} - /// - /// Make sure that all the events that are passed in [events], must be in - /// ascending order of start time. - + /// **Main Event Arrangement Algorithm** + /// + /// This is the primary entry point that orchestrates the entire event arrangement process. + /// It handles all the complex logic for positioning overlapping events optimally. + /// + /// ## **Algorithm Steps:** + /// 1. **Input Validation**: Return empty if no events provided + /// 2. **Event Normalization**: Convert events to internal format for current date + /// 3. **Time-based Sorting**: Order events by start time for optimal processing + /// 4. **Column Creation**: Group non-overlapping events into columns + /// 5. **Width Calculation**: Determine optimal positioning and sizing + /// + /// ## **Input Parameters:** + /// - `events`: List of calendar events to arrange (must be sorted by start time) + /// - `width`: Total available width for the calendar view + /// - `height`: Total available height for the calendar view + /// - `heightPerMinute`: Vertical pixels per minute (for time-based positioning) + /// - `startHour`: First visible hour (e.g., 8 for 8:00 AM) + /// - `calendarViewDate`: The specific date being displayed + /// + /// ## **Output:** + /// List of `OrganizedCalendarEventData` with calculated positions: + /// - `left`: Distance from left edge in pixels + /// - `right`: Distance from right edge in pixels + /// - `top`: Distance from top edge in pixels + /// - `bottom`: Distance from bottom edge in pixels + /// + /// ## **Edge Cases Handled:** + /// - Empty event list → Returns empty result + /// - Events outside visible time range → Automatically filtered out + /// - Multi-day events → Properly clipped to current day view + /// - Zero-duration events → Handled with minimum height + /// - Events spanning midnight → Correctly processed for day boundaries + /// + /// ## **Performance Considerations:** + /// - Time Complexity: O(n²) for column creation, O(n) for positioning + /// - Space Complexity: O(n) for internal data structures + /// - Optimized for typical calendar scenarios (few overlapping events) + /// + /// **Prerequisite**: Events must be sorted by start time for optimal results. @override List> arrange({ required List> events, @@ -54,7 +147,7 @@ class SideEventArranger extends EventArranger { normalizedEvents .sort((a, b) => a.effectiveStartTime.compareTo(b.effectiveStartTime)); - // Step 3: Create columns using an efficient sweep line algorithm + // Step 3: Create columns using simple algorithm that prevents overlapping final columns = _createOptimalColumns(normalizedEvents); // Step 4: Convert columns to organized calendar event data @@ -62,7 +155,38 @@ class SideEventArranger extends EventArranger { heightPerMinute, startHourInMinutes, calendarViewDate); } - /// Normalizes events for the specific calendar view date + /// **Event Normalization for Date-Specific View** + /// + /// Converts raw calendar events into a standardized internal format optimized + /// for the specific date being displayed. This handles complex multi-day event logic. + /// + /// ## **Key Responsibilities:** + /// 1. **Multi-day Event Handling**: Clips events to the current day's boundaries + /// 2. **Time Range Validation**: Filters out events outside visible hours + /// 3. **Data Standardization**: Converts to internal `_NormalizedEvent` format + /// + /// ## **Multi-day Event Cases:** + /// - **Same Day**: Event starts and ends on the same day → Use original times + /// - **Start Day**: Event starts today, ends later → Start at event time, end at day end + /// - **End Day**: Event started earlier, ends today → Start at day start, end at event time + /// - **Middle Day**: Event spans multiple days → Full day (start=0, end=1440 minutes) + /// + /// ## **Edge Cases Handled:** + /// - `null` start/end times → Skipped completely + /// - Events outside visible time range → Filtered out + /// - Zero-duration events → Processed normally + /// - Events crossing midnight → Properly clipped to day boundaries + /// + /// ## **Example Scenarios:** + /// ```dart + /// // Viewing: Jan 2, 2024 + /// // Event: Jan 1 10:00 PM - Jan 3 2:00 PM + /// // Result: Jan 2 0:00 AM - Jan 2 11:59 PM (full day for middle day) + /// + /// // Viewing: Jan 15, 2024 + /// // Event: Jan 15 2:00 PM - Jan 15 4:00 PM + /// // Result: Jan 15 2:00 PM - Jan 15 4:00 PM (same day, unchanged) + /// ``` List<_NormalizedEvent> _normalizeEventsForDate( List> events, DateTime calendarViewDate, @@ -127,7 +251,43 @@ class SideEventArranger extends EventArranger { return normalizedEvents; } - /// Creates optimal columns using O(n²) algorithm that prevents overlapping + /// **Column Creation Algorithm - Event Grouping Strategy** + /// + /// Groups events into columns where no two events in the same column overlap. + /// This is the core algorithm that determines the layout structure. + /// + /// ## **Algorithm Logic:** + /// 1. **Sequential Processing**: Process events one by one in chronological order + /// 2. **Column Search**: For each event, find the first available column where it fits + /// 3. **Collision Detection**: Use `_canEventFitInColumn` to check for overlaps + /// 4. **New Column Creation**: If no existing column works, create a new one + /// + /// ## **Time Complexity:** + /// - **Best Case**: O(n) - All events sequential, no overlaps + /// - **Worst Case**: O(n²) - All events overlap, each needs new column + /// - **Typical Case**: O(n*k) where k is average number of columns (~3-5) + /// + /// ## **Column Selection Strategy:** + /// Uses **First-Fit** approach: Always tries to place event in the leftmost available column. + /// This minimizes the total number of columns needed and creates compact layouts. + /// + /// ## **Example Walkthrough:** + /// ```dart + /// Events: [9:00-10:00], [9:30-11:00], [10:30-12:00], [11:30-13:00] + /// + /// Step 1: [9:00-10:00] → Column 0 (first event) + /// Step 2: [9:30-11:00] → Column 1 (overlaps with Column 0) + /// Step 3: [10:30-12:00] → Column 0 (fits after first event ends) + /// Step 4: [11:30-13:00] → Column 1 (fits after second event ends) + /// + /// Result: 2 columns, optimal layout + /// ``` + /// + /// ## **Edge Cases:** + /// - **Empty Input**: Returns empty column list + /// - **Single Event**: Creates one column with one event + /// - **No Overlaps**: All events in single column (most compact) + /// - **All Events Overlap**: Each event gets its own column List>> _createOptimalColumns( List<_NormalizedEvent> events) { if (events.isEmpty) return []; @@ -156,7 +316,26 @@ class SideEventArranger extends EventArranger { return columns; } - /// Checks if an event can fit in a specific column without overlapping + /// **Column Fit Testing - Overlap Detection** + /// + /// Determines whether a new event can be placed in an existing column + /// without overlapping with any events already in that column. + /// + /// ## **Algorithm:** + /// 1. **Iterate through existing events** in the target column + /// 2. **Check overlap** with each existing event using `_eventsOverlap` + /// 3. **Return false** immediately if any overlap is found (early termination) + /// 4. **Return true** only if no overlaps detected + /// + /// ## **Performance Optimization:** + /// - **Early Exit**: Stops checking as soon as first overlap is found + /// - **Time Complexity**: O(k) where k is events in column (typically 1-3) + /// - **Space Complexity**: O(1) - no additional storage needed + /// + /// ## **Use Cases:** + /// - Column selection during event placement + /// - Validation of layout correctness + /// - Debugging overlap detection issues bool _canEventFitInColumn( _NormalizedEvent event, List<_NormalizedEvent> column) { for (final existing in column) { @@ -167,7 +346,47 @@ class SideEventArranger extends EventArranger { return true; } - /// Checks if two events overlap based on includeEdges setting + /// **Temporal Overlap Detection - Core Logic** + /// + /// Determines whether two events overlap in time, considering the `includeEdges` setting. + /// This is the fundamental building block for all layout decisions. + /// + /// ## **Overlap Logic:** + /// + /// ### **When includeEdges = false (default):** + /// Events that touch at edges are NOT considered overlapping. + /// ```dart + /// Event A: 14:00-15:00, Event B: 15:00-16:00 → No overlap (touching is OK) + /// Event A: 14:00-15:30, Event B: 15:00-16:00 → Overlap (actual time conflict) + /// ``` + /// + /// ### **When includeEdges = true:** + /// Events that touch at edges ARE considered overlapping. + /// ```dart + /// Event A: 14:00-15:00, Event B: 15:00-16:00 → Overlap (touching treated as conflict) + /// Event A: 14:00-15:30, Event B: 15:00-16:00 → Overlap (actual time conflict) + /// ``` + /// + /// ## **Mathematical Formulation:** + /// For events with times (start1, end1) and (start2, end2): + /// + /// **includeEdges = false:** + /// - Overlap if: `start1 < end2 AND start2 < end1` + /// - No overlap if events just touch: `end1 == start2` + /// + /// **includeEdges = true:** + /// - Overlap if: `start1 <= end2 AND start2 <= end1` + /// - Overlap even if events just touch: `end1 == start2` + /// + /// ## **Edge Cases:** + /// - **Zero Duration Events**: Point events (start == end) handled correctly + /// - **Same Start/End Times**: Identical events always overlap + /// - **Reversed Time Order**: Algorithm works regardless of parameter order + /// + /// ## **Performance:** + /// - **Time Complexity**: O(1) - Simple arithmetic comparisons + /// - **Space Complexity**: O(1) - No additional storage + /// - **Highly Optimized**: Used in tight loops, marked for inlining bool _eventsOverlap(_NormalizedEvent event1, _NormalizedEvent event2) { if (includeEdges) { // Events overlap if they have any overlapping time (including touching edges) @@ -180,7 +399,41 @@ class SideEventArranger extends EventArranger { } } - /// Converts columns to organized calendar event data with proper width allocation + /// **Layout Strategy Coordinator - Width Allocation Decision** + /// + /// Orchestrates the final positioning phase by choosing between two different + /// width allocation strategies based on the `maxWidth` configuration. + /// + /// ## **Strategy Selection:** + /// + /// ### **Fixed MaxWidth Strategy** (`maxWidth != null`): + /// - **Use Case**: Consistent visual appearance, mobile-friendly layouts + /// - **Behavior**: Events respect the maxWidth constraint + /// - **Benefits**: Predictable sizing, prevents overly wide events + /// - **Method**: `_processWithFixedMaxWidth()` + /// + /// ### **Dynamic Width Strategy** (`maxWidth == null`): + /// - **Use Case**: Maximum space utilization, desktop layouts + /// - **Behavior**: Events expand to fill all available space + /// - **Benefits**: No wasted space, optimal readability + /// - **Method**: `_processWithDynamicWidth()` + /// + /// ## **Common Processing Steps:** + /// 1. **Time Calculations**: Convert event times to pixel positions + /// 2. **Width Calculations**: Determine optimal width for each event + /// 3. **Position Calculations**: Calculate left/right positioning + /// 4. **Result Generation**: Create `OrganizedCalendarEventData` objects + /// + /// ## **Output Format:** + /// Each event becomes an `OrganizedCalendarEventData` with: + /// - **Spatial Data**: left, right, top, bottom positions in pixels + /// - **Temporal Data**: startDuration, endDuration for time references + /// - **Event Data**: Reference to original event and metadata + /// + /// ## **Error Handling:** + /// - **Empty Columns**: Returns empty result gracefully + /// - **Invalid Dimensions**: Protected by bounds checking + /// - **Calculation Errors**: Math.max/min prevent negative values List> _convertColumnsToOrganizedData( List>> columns, double totalWidth, @@ -208,8 +461,40 @@ class SideEventArranger extends EventArranger { return result; } - /// Process events with fixed maxWidth for each column - /// Process events with fixed maxWidth for each column + /// **Fixed MaxWidth Processing - Constrained Layout Strategy** + /// + /// Handles event positioning when a maximum width constraint is specified. + /// This strategy balances space utilization with visual consistency. + /// + /// ## **Core Algorithm:** + /// 1. **Width Pre-calculation**: Calculate optimal width for each event + /// 2. **MaxWidth Application**: Cap event widths at the specified maximum + /// 3. **Gap Elimination**: Position events to minimize empty space + /// 4. **Column-based Positioning**: Use actual column widths for positioning + /// + /// ## **Width Calculation Process:** + /// ```dart + /// For each event: + /// 1. Calculate available space to the right + /// 2. Apply maxWidth constraint: min(available, maxWidth) + /// 3. Store in eventWidths map for positioning phase + /// ``` + /// + /// ## **Positioning Strategy:** + /// - **Column Width**: Use the widest event in each column to determine column width + /// - **Left Position**: Accumulate column widths to eliminate gaps + /// - **Right Position**: Calculate remaining space after event width + /// + /// ## **Benefits:** + /// - **Consistent Appearance**: No event exceeds maxWidth limit + /// - **Optimal Space Usage**: Events expand up to the constraint + /// - **Gap-Free Layout**: Columns positioned adjacently + /// - **Mobile Friendly**: Prevents overly wide events on small screens + /// + /// ## **Edge Cases:** + /// - **MaxWidth > Available Space**: Event uses available space (no artificial padding) + /// - **Multiple Events in Column**: All respect the same maxWidth constraint + /// - **Very Small MaxWidth**: Events maintain minimum readability void _processWithFixedMaxWidth( List>> columns, List> result, @@ -220,7 +505,7 @@ class SideEventArranger extends EventArranger { DateTime calendarViewDate, ) { final columnCount = columns.length; - final baseSlotWidth = totalWidth / columnCount; + if (columnCount == 0) return; // Calculate actual widths for all events first to avoid gaps final eventWidths = {}; @@ -231,16 +516,25 @@ class SideEventArranger extends EventArranger { final dynamicWidth = _calculateAvailableWidthToRight( normalizedEvent, columns, columnIndex, totalWidth); - final maxWidthPixels = maxWidth ?? double.maxFinite; + // Take minimum of dynamic width and maxWidth (maxWidth is absolute pixels) + final maxWidthPixels = maxWidth!; eventWidths[normalizedEvent.hashCode] = math.min(dynamicWidth, maxWidthPixels); } } - // Position events by column + // Position events to eliminate gaps by tracking actual positions + double currentLeftPosition = 0.0; + for (int columnIndex = 0; columnIndex < columnCount; columnIndex++) { final column = columns[columnIndex]; - final leftPosition = columnIndex * maxWidth!; + + // Find the maximum width needed for this column + double maxColumnWidth = double.maxFinite; + for (final normalizedEvent in column) { + final eventWidth = eventWidths[normalizedEvent.hashCode]!; + maxColumnWidth = math.min(maxColumnWidth, eventWidth); + } for (final normalizedEvent in column) { final event = normalizedEvent.originalEvent; @@ -251,7 +545,6 @@ class SideEventArranger extends EventArranger { Constants.minutesADay - startHourInMinutes, normalizedEvent.effectiveEndTime - startHourInMinutes); - // Use the individual event width rather than column width final actualWidth = eventWidths[normalizedEvent.hashCode]!; final top = displayStartTime * heightPerMinute; @@ -261,8 +554,8 @@ class SideEventArranger extends EventArranger { : height - displayEndTime * heightPerMinute; result.add(OrganizedCalendarEventData( - left: leftPosition, - right: math.max(0.0, totalWidth - leftPosition - actualWidth), + left: currentLeftPosition, + right: math.max(0.0, totalWidth - currentLeftPosition - actualWidth), top: top, bottom: bottom, startDuration: event.startTime!.copyFromMinutes(displayStartTime), @@ -271,10 +564,55 @@ class SideEventArranger extends EventArranger { calendarViewDate: calendarViewDate, )); } + + // Move to next column position based on actual width used + currentLeftPosition += maxColumnWidth; } } - /// Process events with dynamic width allocation (stretch to available space) + /// **Dynamic Width Processing - Maximum Space Utilization Strategy** + /// + /// Handles event positioning when no maximum width constraint is specified. + /// Events expand to use all available space for optimal readability. + /// + /// ## **Core Philosophy:** + /// Maximize the use of available horizontal space while maintaining proper + /// visual separation between overlapping events. + /// + /// ## **Algorithm Steps:** + /// 1. **Equal Column Distribution**: Start with equal-width columns as baseline + /// 2. **Available Space Calculation**: Determine how much each event can expand + /// 3. **Rightward Expansion**: Events extend rightward until blocked by overlapping events + /// 4. **Standard Positioning**: Use column-based left positioning for consistency + /// + /// ## **Width Expansion Logic:** + /// ```dart + /// For each event: + /// 1. Start at column left boundary + /// 2. Expand rightward until hitting overlapping event or boundary + /// 3. Use calculated available width for the event + /// ``` + /// + /// ## **Column Positioning:** + /// - **Left Position**: Based on equal column distribution (`columnIndex * baseWidth`) + /// - **Event Width**: Uses `_calculateAvailableWidthToRight()` for optimal expansion + /// - **Right Position**: Calculated as `totalWidth - left - eventWidth` + /// + /// ## **Benefits:** + /// - **Maximum Readability**: Events use all available space + /// - **No Wasted Space**: Horizontal area fully utilized + /// - **Smart Expansion**: Events stop at logical boundaries + /// - **Desktop Optimized**: Ideal for larger screens + /// + /// ## **Use Cases:** + /// - **Desktop Calendars**: Large screens with plenty of horizontal space + /// - **Print Layouts**: Maximum information density required + /// - **Detail Views**: When event content needs maximum width + /// + /// ## **Edge Cases:** + /// - **Single Column**: Event expands to full width + /// - **Many Overlaps**: Events get narrow but fair space allocation + /// - **Variable Content**: Width adapts to optimal display needs void _processWithDynamicWidth( List>> columns, List> result, @@ -329,7 +667,67 @@ class SideEventArranger extends EventArranger { } } - /// Calculates how much width is available to the right of an event + /// **Available Width Calculation - Smart Space Detection** + /// + /// Calculates how much horizontal space an event can use by analyzing + /// the layout to the right and detecting blocking overlapping events. + /// + /// ## **Core Algorithm:** + /// 1. **Baseline Calculation**: Start with remaining space from current position to edge + /// 2. **Right-side Scanning**: Check each column to the right for blocking events + /// 3. **Overlap Detection**: Use `_eventsOverlap()` to find temporal conflicts + /// 4. **Width Limiting**: Stop expansion at first blocking event found + /// 5. **Minimum Guarantee**: Ensure at least one column width is available + /// + /// ## **Scanning Strategy:** + /// ```dart + /// For each column to the right: + /// For each event in that column: + /// If event overlaps with target: + /// Limit width to that column's position + /// Break (stop scanning further right) + /// ``` + /// + /// ## **Width Calculation Logic:** + /// - **Initial Width**: `totalWidth - startPosition` (all remaining space) + /// - **Blocking Position**: `rightColumnIndex * baseSlotWidth` + /// - **Final Width**: `blockingPosition - startPosition` + /// - **Minimum Width**: `max(baseSlotWidth, calculatedWidth)` + /// + /// ## **Example Scenarios:** + /// + /// ### **Scenario 1: No Blocking Events** + /// ```dart + /// Event: 9:00-10:00 AM in Column 0 + /// Right columns: No overlapping events + /// Result: Expands to full remaining width + /// ``` + /// + /// ### **Scenario 2: Blocked by Column 2** + /// ```dart + /// Event: 9:00-11:00 AM in Column 0 + /// Column 2: Has event 9:30-10:30 AM (overlaps!) + /// Result: Width limited to Column 2's left boundary + /// ``` + /// + /// ### **Scenario 3: Multiple Potential Blocks** + /// ```dart + /// Event: 9:00-12:00 PM in Column 0 + /// Column 1: Event 10:00-11:00 AM (overlaps) + /// Column 3: Event 11:00-12:00 PM (also overlaps) + /// Result: Width limited to Column 1 (first blocking column) + /// ``` + /// + /// ## **Performance Optimizations:** + /// - **Early Termination**: Stops scanning at first blocking event + /// - **Column-based Scanning**: Only checks relevant columns + /// - **Efficient Overlap Detection**: Uses optimized `_eventsOverlap()` + /// + /// ## **Edge Cases:** + /// - **Rightmost Column**: Gets all remaining space + /// - **All Columns Blocked**: Falls back to minimum column width + /// - **No Right Columns**: Expands to total width boundary + /// - **Zero Available Space**: Guaranteed minimum width prevents layout break double _calculateAvailableWidthToRight( _NormalizedEvent targetEvent, List>> allColumns, @@ -371,12 +769,78 @@ class SideEventArranger extends EventArranger { } } -/// Internal class to normalize event data for efficient processing +/// **Internal Event Data Structure - Normalized Representation** +/// +/// Standardized internal format for events optimized for layout calculations. +/// Converts complex calendar event data into a simplified, computation-friendly format. +/// +/// ## **Design Purpose:** +/// - **Performance**: Optimized data structure for layout algorithms +/// - **Consistency**: Standardized time representation (minutes since midnight) +/// - **Simplicity**: Removes complexity of original event data during processing +/// - **Multi-day Support**: Handles events spanning multiple days uniformly +/// +/// ## **Key Fields:** +/// +/// ### **originalEvent**: Reference to source data +/// - **Type**: `CalendarEventData` +/// - **Purpose**: Maintains link to original event for final output +/// - **Usage**: Accessed when creating final organized event data +/// +/// ### **effectiveStartTime**: Normalized start time in minutes +/// - **Format**: Minutes since midnight (0-1439) +/// - **Multi-day Logic**: Adjusted for current view date +/// - **Example**: 2:30 PM = 870 minutes (14.5 * 60) +/// +/// ### **effectiveEndTime**: Normalized end time in minutes +/// - **Format**: Minutes since midnight (0-1440) +/// - **Multi-day Logic**: Adjusted for current view date +/// - **Boundary**: Can be 1440 (next day) for events ending at midnight +/// +/// ### **isMultiDay**: Multi-day event indicator +/// - **Purpose**: Tracks whether event spans multiple calendar days +/// - **Usage**: Affects rendering and time calculations +/// - **Source**: Derived from `CalendarEventData.isRangingEvent` +/// +/// ### **isFullDay**: Full-day event indicator +/// - **Purpose**: Identifies all-day events for special handling +/// - **Usage**: May affect positioning and rendering logic +/// - **Source**: Derived from `CalendarEventData.isFullDayEvent` +/// +/// ## **Normalization Benefits:** +/// - **Uniform Time Format**: All events use same time representation +/// - **Simplified Comparisons**: Easy overlap detection with integer arithmetic +/// - **Multi-day Handling**: Complex spanning logic resolved once +/// - **Performance**: Reduced object complexity for hot path operations +/// +/// ## **Usage Pattern:** +/// ```dart +/// // Created during normalization phase +/// final normalized = _NormalizedEvent( +/// originalEvent: calendarEvent, +/// effectiveStartTime: 540, // 9:00 AM +/// effectiveEndTime: 660, // 11:00 AM +/// isMultiDay: false, +/// isFullDay: false, +/// ); +/// +/// // Used in overlap detection +/// if (event1.effectiveEndTime > event2.effectiveStartTime) { ... } +/// ``` class _NormalizedEvent { + /// Reference to the original calendar event data final CalendarEventData originalEvent; + + /// Event start time in minutes since midnight (0-1439) final int effectiveStartTime; + + /// Event end time in minutes since midnight (0-1440) final int effectiveEndTime; + + /// Whether this event spans multiple calendar days final bool isMultiDay; + + /// Whether this is a full-day event final bool isFullDay; const _NormalizedEvent({ From 51b571b206d664351db3826c1e7b31694f7bc529 Mon Sep 17 00:00:00 2001 From: "Ami.B" Date: Tue, 12 Aug 2025 17:26:04 +0530 Subject: [PATCH 3/3] :memo: update and simplify file comments --- .../event_arrangers/side_event_arranger.dart | 560 +++--------------- 1 file changed, 89 insertions(+), 471 deletions(-) diff --git a/lib/src/event_arrangers/side_event_arranger.dart b/lib/src/event_arrangers/side_event_arranger.dart index 7e7d1d88..79958c57 100644 --- a/lib/src/event_arrangers/side_event_arranger.dart +++ b/lib/src/event_arrangers/side_event_arranger.dart @@ -4,40 +4,11 @@ part of 'event_arrangers.dart'; -/// **SideEventArranger: Smart Event Layout Manager** -/// -/// This class arranges overlapping calendar events side by side in an optimal way, -/// ensuring no visual overlaps while maximizing space utilization. -/// -/// ## **Core Functionality:** -/// - Arranges overlapping events in separate columns -/// - Calculates optimal widths for each event based on available space -/// - Supports both fixed maxWidth constraints and dynamic width allocation -/// - Handles multi-day events, full-day events, and edge cases -/// -/// ## **Algorithm Overview:** -/// 1. **Event Normalization**: Converts events to a standardized format for the current view date -/// 2. **Column Creation**: Uses O(n²) algorithm to group non-overlapping events into columns -/// 3. **Width Calculation**: Determines optimal width for each event based on available space -/// 4. **Positioning**: Places events with proper left/right positioning to eliminate gaps -/// -/// ## **Key Features:** -/// - **Gap Elimination**: Events expand to fill available space when possible -/// - **Edge Case Handling**: Properly handles touching events, multi-day events, and time boundaries -/// - **Flexible Width Control**: Supports both fixed maxWidth and dynamic width allocation -/// - **Performance Optimized**: Efficient algorithms for real-world calendar scenarios -/// -/// ## **Usage Examples:** -/// ```dart -/// // Basic usage with dynamic width -/// final arranger = SideEventArranger(); -/// -/// // With maximum width constraint (100px) -/// final arranger = SideEventArranger(maxWidth: 100.0); -/// -/// // Include touching events as overlapping -/// final arranger = SideEventArranger(includeEdges: true); -/// ``` +/// Arranges overlapping calendar events side by side, ensuring no visual overlap and optimal use of space. +/// Supports multi-day and full-day events, fixed or dynamic width allocation, and configurable edge overlap handling. +/// +/// Events are grouped into columns to prevent overlaps, and each event is positioned and sized for best readability. +/// Use [maxWidth] to constrain event width, and [includeEdges] to control whether touching events are treated as overlapping. class SideEventArranger extends EventArranger { /// This class will provide method that will arrange /// all the events side by side. @@ -46,84 +17,28 @@ class SideEventArranger extends EventArranger { this.includeEdges = false, }); - /// **Edge Case Handling Configuration** - /// - /// Determines whether events that are touching at their edges should be - /// considered as overlapping and placed in separate columns. - /// - /// **Use Cases:** - /// - `false` (default): Events like 2:00-3:00 PM and 3:00-4:00 PM can share the same column - /// - `true`: Touching events are forced into separate columns for visual clarity - /// - /// **Example:** - /// ```dart - /// // Events: [14:00-15:00], [15:00-16:00] - /// // includeEdges = false: Both events in same column (touching is OK) - /// // includeEdges = true: Events in separate columns (touching treated as overlap) - /// ``` + /// Decides whether events that are overlapping on edge + /// (ex, event1 has the same end-time as the start-time of event 2) + /// should be offset or not. + /// + /// If includeEdges is true, it will offset the events else it will not. + /// Defaults to false. final bool includeEdges; - /// **Maximum Width Constraint (Optional)** - /// - /// When specified, limits the maximum width any event can take, regardless - /// of available space. This ensures consistent visual appearance. - /// - /// **Behavior:** - /// - `null` (default): Events expand to fill all available space - /// - `double value`: Events are capped at this width in pixels - /// - /// **Use Cases:** - /// - UI consistency: Prevent events from becoming too wide - /// - Mobile optimization: Ensure readable text on narrow screens - /// - Design constraints: Match specific layout requirements - /// - /// **Example:** - /// ```dart - /// // Without maxWidth: Event might expand to 400px if space available - /// // With maxWidth: 150.0: Event width capped at 150px - /// ``` + /// If enough space is available, the event slot will + /// use the specified max width. + /// Otherwise, it will reduce to fit all events in the cell. + /// If max width is not specified, slots will expand to fill the cell. final double? maxWidth; - /// **Main Event Arrangement Algorithm** - /// - /// This is the primary entry point that orchestrates the entire event arrangement process. - /// It handles all the complex logic for positioning overlapping events optimally. - /// - /// ## **Algorithm Steps:** - /// 1. **Input Validation**: Return empty if no events provided - /// 2. **Event Normalization**: Convert events to internal format for current date - /// 3. **Time-based Sorting**: Order events by start time for optimal processing - /// 4. **Column Creation**: Group non-overlapping events into columns - /// 5. **Width Calculation**: Determine optimal positioning and sizing - /// - /// ## **Input Parameters:** - /// - `events`: List of calendar events to arrange (must be sorted by start time) - /// - `width`: Total available width for the calendar view - /// - `height`: Total available height for the calendar view - /// - `heightPerMinute`: Vertical pixels per minute (for time-based positioning) - /// - `startHour`: First visible hour (e.g., 8 for 8:00 AM) - /// - `calendarViewDate`: The specific date being displayed - /// - /// ## **Output:** - /// List of `OrganizedCalendarEventData` with calculated positions: - /// - `left`: Distance from left edge in pixels - /// - `right`: Distance from right edge in pixels - /// - `top`: Distance from top edge in pixels - /// - `bottom`: Distance from bottom edge in pixels - /// - /// ## **Edge Cases Handled:** - /// - Empty event list → Returns empty result - /// - Events outside visible time range → Automatically filtered out - /// - Multi-day events → Properly clipped to current day view - /// - Zero-duration events → Handled with minimum height - /// - Events spanning midnight → Correctly processed for day boundaries - /// - /// ## **Performance Considerations:** - /// - Time Complexity: O(n²) for column creation, O(n) for positioning - /// - Space Complexity: O(n) for internal data structures - /// - Optimized for typical calendar scenarios (few overlapping events) - /// - /// **Prerequisite**: Events must be sorted by start time for optimal results. + /// Arranges events for a calendar day view. + /// + /// Normalizes, sorts, and groups events into columns, then calculates their positions and sizes for rendering. + /// Returns a list of [OrganizedCalendarEventData] with pixel positions and event references. + /// + /// Handles edge cases like empty event lists, events outside the visible range, multi-day events, zero-duration events, and events spanning midnight. + /// + /// Optimized for typical calendar scenarios. Events should be sorted by start time for @override List> arrange({ required List> events, @@ -155,38 +70,22 @@ class SideEventArranger extends EventArranger { heightPerMinute, startHourInMinutes, calendarViewDate); } - /// **Event Normalization for Date-Specific View** - /// - /// Converts raw calendar events into a standardized internal format optimized - /// for the specific date being displayed. This handles complex multi-day event logic. - /// - /// ## **Key Responsibilities:** - /// 1. **Multi-day Event Handling**: Clips events to the current day's boundaries - /// 2. **Time Range Validation**: Filters out events outside visible hours - /// 3. **Data Standardization**: Converts to internal `_NormalizedEvent` format - /// - /// ## **Multi-day Event Cases:** - /// - **Same Day**: Event starts and ends on the same day → Use original times - /// - **Start Day**: Event starts today, ends later → Start at event time, end at day end - /// - **End Day**: Event started earlier, ends today → Start at day start, end at event time - /// - **Middle Day**: Event spans multiple days → Full day (start=0, end=1440 minutes) - /// - /// ## **Edge Cases Handled:** - /// - `null` start/end times → Skipped completely - /// - Events outside visible time range → Filtered out - /// - Zero-duration events → Processed normally - /// - Events crossing midnight → Properly clipped to day boundaries - /// - /// ## **Example Scenarios:** - /// ```dart - /// // Viewing: Jan 2, 2024 - /// // Event: Jan 1 10:00 PM - Jan 3 2:00 PM - /// // Result: Jan 2 0:00 AM - Jan 2 11:59 PM (full day for middle day) - /// - /// // Viewing: Jan 15, 2024 - /// // Event: Jan 15 2:00 PM - Jan 15 4:00 PM - /// // Result: Jan 15 2:00 PM - Jan 15 4:00 PM (same day, unchanged) - /// ``` + /// Normalizes events for the given date. + /// + /// Clips multi-day events to the current day's boundaries, filters out events outside visible hours, + /// and converts each event to a standardized internal format for layout. + /// + /// Multi-day event cases: + /// - Same day: uses original start/end times. + /// - Start day: starts at event time, ends at day end. + /// - End day: starts at day start, ends at event time. + /// - Middle day: uses full day (0–1440 minutes). + /// + /// Skips events with null times or outside the visible range. + /// + /// Example: + /// Viewing Jan 2, 2024 for event Jan 1 10:00 PM – Jan 3 2:00 PM + /// → Result: Jan 2 0:00 AM – Jan 2 11:59 PM (full day for middle day) List<_NormalizedEvent> _normalizeEventsForDate( List> events, DateTime calendarViewDate, @@ -251,43 +150,12 @@ class SideEventArranger extends EventArranger { return normalizedEvents; } - /// **Column Creation Algorithm - Event Grouping Strategy** - /// - /// Groups events into columns where no two events in the same column overlap. - /// This is the core algorithm that determines the layout structure. - /// - /// ## **Algorithm Logic:** - /// 1. **Sequential Processing**: Process events one by one in chronological order - /// 2. **Column Search**: For each event, find the first available column where it fits - /// 3. **Collision Detection**: Use `_canEventFitInColumn` to check for overlaps - /// 4. **New Column Creation**: If no existing column works, create a new one - /// - /// ## **Time Complexity:** - /// - **Best Case**: O(n) - All events sequential, no overlaps - /// - **Worst Case**: O(n²) - All events overlap, each needs new column - /// - **Typical Case**: O(n*k) where k is average number of columns (~3-5) - /// - /// ## **Column Selection Strategy:** - /// Uses **First-Fit** approach: Always tries to place event in the leftmost available column. - /// This minimizes the total number of columns needed and creates compact layouts. - /// - /// ## **Example Walkthrough:** - /// ```dart - /// Events: [9:00-10:00], [9:30-11:00], [10:30-12:00], [11:30-13:00] - /// - /// Step 1: [9:00-10:00] → Column 0 (first event) - /// Step 2: [9:30-11:00] → Column 1 (overlaps with Column 0) - /// Step 3: [10:30-12:00] → Column 0 (fits after first event ends) - /// Step 4: [11:30-13:00] → Column 1 (fits after second event ends) - /// - /// Result: 2 columns, optimal layout - /// ``` - /// - /// ## **Edge Cases:** - /// - **Empty Input**: Returns empty column list - /// - **Single Event**: Creates one column with one event - /// - **No Overlaps**: All events in single column (most compact) - /// - **All Events Overlap**: Each event gets its own column + /// Groups events into columns so that no two events in the same column overlap. + /// + /// Uses a first-fit approach: each event is placed in the leftmost column where it does not overlap with existing events. + /// This minimizes the number of columns and creates a compact layout. + /// + /// Handles edge cases like empty input, single events, no overlaps, and all events overlapping. List>> _createOptimalColumns( List<_NormalizedEvent> events) { if (events.isEmpty) return []; @@ -316,26 +184,12 @@ class SideEventArranger extends EventArranger { return columns; } - /// **Column Fit Testing - Overlap Detection** - /// - /// Determines whether a new event can be placed in an existing column - /// without overlapping with any events already in that column. - /// - /// ## **Algorithm:** - /// 1. **Iterate through existing events** in the target column - /// 2. **Check overlap** with each existing event using `_eventsOverlap` - /// 3. **Return false** immediately if any overlap is found (early termination) - /// 4. **Return true** only if no overlaps detected - /// - /// ## **Performance Optimization:** - /// - **Early Exit**: Stops checking as soon as first overlap is found - /// - **Time Complexity**: O(k) where k is events in column (typically 1-3) - /// - **Space Complexity**: O(1) - no additional storage needed - /// - /// ## **Use Cases:** - /// - Column selection during event placement - /// - Validation of layout correctness - /// - Debugging overlap detection issues + /// Returns true if a new event can be placed in a column without overlapping any existing events. + /// + /// Iterates through events in the target column and returns false immediately if any overlap is found. + /// Optimized for early exit and minimal memory usage. + /// + /// Used for column selection and layout validation. bool _canEventFitInColumn( _NormalizedEvent event, List<_NormalizedEvent> column) { for (final existing in column) { @@ -346,47 +200,13 @@ class SideEventArranger extends EventArranger { return true; } - /// **Temporal Overlap Detection - Core Logic** - /// - /// Determines whether two events overlap in time, considering the `includeEdges` setting. - /// This is the fundamental building block for all layout decisions. - /// - /// ## **Overlap Logic:** - /// - /// ### **When includeEdges = false (default):** - /// Events that touch at edges are NOT considered overlapping. - /// ```dart - /// Event A: 14:00-15:00, Event B: 15:00-16:00 → No overlap (touching is OK) - /// Event A: 14:00-15:30, Event B: 15:00-16:00 → Overlap (actual time conflict) - /// ``` - /// - /// ### **When includeEdges = true:** - /// Events that touch at edges ARE considered overlapping. - /// ```dart - /// Event A: 14:00-15:00, Event B: 15:00-16:00 → Overlap (touching treated as conflict) - /// Event A: 14:00-15:30, Event B: 15:00-16:00 → Overlap (actual time conflict) - /// ``` - /// - /// ## **Mathematical Formulation:** - /// For events with times (start1, end1) and (start2, end2): - /// - /// **includeEdges = false:** - /// - Overlap if: `start1 < end2 AND start2 < end1` - /// - No overlap if events just touch: `end1 == start2` - /// - /// **includeEdges = true:** - /// - Overlap if: `start1 <= end2 AND start2 <= end1` - /// - Overlap even if events just touch: `end1 == start2` - /// - /// ## **Edge Cases:** - /// - **Zero Duration Events**: Point events (start == end) handled correctly - /// - **Same Start/End Times**: Identical events always overlap - /// - **Reversed Time Order**: Algorithm works regardless of parameter order - /// - /// ## **Performance:** - /// - **Time Complexity**: O(1) - Simple arithmetic comparisons - /// - **Space Complexity**: O(1) - No additional storage - /// - **Highly Optimized**: Used in tight loops, marked for inlining + /// Returns true if two events overlap in time, considering [includeEdges]. + /// + /// If [includeEdges] is false, events that only touch at edges are not considered overlapping. + /// If true, touching events are treated as overlapping. + /// + /// Handles edge cases like zero-duration events and identical start/end times. + /// Optimized for fast arithmetic comparison. bool _eventsOverlap(_NormalizedEvent event1, _NormalizedEvent event2) { if (includeEdges) { // Events overlap if they have any overlapping time (including touching edges) @@ -399,41 +219,13 @@ class SideEventArranger extends EventArranger { } } - /// **Layout Strategy Coordinator - Width Allocation Decision** - /// - /// Orchestrates the final positioning phase by choosing between two different - /// width allocation strategies based on the `maxWidth` configuration. - /// - /// ## **Strategy Selection:** - /// - /// ### **Fixed MaxWidth Strategy** (`maxWidth != null`): - /// - **Use Case**: Consistent visual appearance, mobile-friendly layouts - /// - **Behavior**: Events respect the maxWidth constraint - /// - **Benefits**: Predictable sizing, prevents overly wide events - /// - **Method**: `_processWithFixedMaxWidth()` - /// - /// ### **Dynamic Width Strategy** (`maxWidth == null`): - /// - **Use Case**: Maximum space utilization, desktop layouts - /// - **Behavior**: Events expand to fill all available space - /// - **Benefits**: No wasted space, optimal readability - /// - **Method**: `_processWithDynamicWidth()` - /// - /// ## **Common Processing Steps:** - /// 1. **Time Calculations**: Convert event times to pixel positions - /// 2. **Width Calculations**: Determine optimal width for each event - /// 3. **Position Calculations**: Calculate left/right positioning - /// 4. **Result Generation**: Create `OrganizedCalendarEventData` objects - /// - /// ## **Output Format:** - /// Each event becomes an `OrganizedCalendarEventData` with: - /// - **Spatial Data**: left, right, top, bottom positions in pixels - /// - **Temporal Data**: startDuration, endDuration for time references - /// - **Event Data**: Reference to original event and metadata - /// - /// ## **Error Handling:** - /// - **Empty Columns**: Returns empty result gracefully - /// - **Invalid Dimensions**: Protected by bounds checking - /// - **Calculation Errors**: Math.max/min prevent negative values + /// Converts columns to organized event data and calculates positions and sizes for rendering. + /// + /// Uses either fixed maxWidth or dynamic width allocation based on [maxWidth]. + /// Handles time and width calculations, positioning, and result generation. + /// + /// Returns a list of [OrganizedCalendarEventData] with pixel positions and event references. + /// Handles empty columns and invalid dimensions gracefully. List> _convertColumnsToOrganizedData( List>> columns, double totalWidth, @@ -461,40 +253,12 @@ class SideEventArranger extends EventArranger { return result; } - /// **Fixed MaxWidth Processing - Constrained Layout Strategy** - /// /// Handles event positioning when a maximum width constraint is specified. - /// This strategy balances space utilization with visual consistency. - /// - /// ## **Core Algorithm:** - /// 1. **Width Pre-calculation**: Calculate optimal width for each event - /// 2. **MaxWidth Application**: Cap event widths at the specified maximum - /// 3. **Gap Elimination**: Position events to minimize empty space - /// 4. **Column-based Positioning**: Use actual column widths for positioning - /// - /// ## **Width Calculation Process:** - /// ```dart - /// For each event: - /// 1. Calculate available space to the right - /// 2. Apply maxWidth constraint: min(available, maxWidth) - /// 3. Store in eventWidths map for positioning phase - /// ``` - /// - /// ## **Positioning Strategy:** - /// - **Column Width**: Use the widest event in each column to determine column width - /// - **Left Position**: Accumulate column widths to eliminate gaps - /// - **Right Position**: Calculate remaining space after event width - /// - /// ## **Benefits:** - /// - **Consistent Appearance**: No event exceeds maxWidth limit - /// - **Optimal Space Usage**: Events expand up to the constraint - /// - **Gap-Free Layout**: Columns positioned adjacently - /// - **Mobile Friendly**: Prevents overly wide events on small screens - /// - /// ## **Edge Cases:** - /// - **MaxWidth > Available Space**: Event uses available space (no artificial padding) - /// - **Multiple Events in Column**: All respect the same maxWidth constraint - /// - **Very Small MaxWidth**: Events maintain minimum readability + /// + /// Calculates optimal width for each event, applies [maxWidth], and positions events to minimize gaps. + /// Uses actual column widths for positioning and ensures consistent appearance. + /// + /// Handles edge cases like maxWidth exceeding available space, multiple events per column, and very small maxWidth values. void _processWithFixedMaxWidth( List>> columns, List> result, @@ -570,49 +334,12 @@ class SideEventArranger extends EventArranger { } } - /// **Dynamic Width Processing - Maximum Space Utilization Strategy** - /// /// Handles event positioning when no maximum width constraint is specified. - /// Events expand to use all available space for optimal readability. - /// - /// ## **Core Philosophy:** - /// Maximize the use of available horizontal space while maintaining proper - /// visual separation between overlapping events. - /// - /// ## **Algorithm Steps:** - /// 1. **Equal Column Distribution**: Start with equal-width columns as baseline - /// 2. **Available Space Calculation**: Determine how much each event can expand - /// 3. **Rightward Expansion**: Events extend rightward until blocked by overlapping events - /// 4. **Standard Positioning**: Use column-based left positioning for consistency - /// - /// ## **Width Expansion Logic:** - /// ```dart - /// For each event: - /// 1. Start at column left boundary - /// 2. Expand rightward until hitting overlapping event or boundary - /// 3. Use calculated available width for the event - /// ``` - /// - /// ## **Column Positioning:** - /// - **Left Position**: Based on equal column distribution (`columnIndex * baseWidth`) - /// - **Event Width**: Uses `_calculateAvailableWidthToRight()` for optimal expansion - /// - **Right Position**: Calculated as `totalWidth - left - eventWidth` - /// - /// ## **Benefits:** - /// - **Maximum Readability**: Events use all available space - /// - **No Wasted Space**: Horizontal area fully utilized - /// - **Smart Expansion**: Events stop at logical boundaries - /// - **Desktop Optimized**: Ideal for larger screens - /// - /// ## **Use Cases:** - /// - **Desktop Calendars**: Large screens with plenty of horizontal space - /// - **Print Layouts**: Maximum information density required - /// - **Detail Views**: When event content needs maximum width - /// - /// ## **Edge Cases:** - /// - **Single Column**: Event expands to full width - /// - **Many Overlaps**: Events get narrow but fair space allocation - /// - **Variable Content**: Width adapts to optimal display needs + /// + /// Events expand to use all available horizontal space for optimal readability, stopping at overlapping events in columns to the right. + /// Uses equal column distribution for left positioning and calculates available width for each event. + /// + /// Ensures maximum readability, no wasted space, and adapts to variable content and overlap scenarios. void _processWithDynamicWidth( List>> columns, List> result, @@ -667,67 +394,12 @@ class SideEventArranger extends EventArranger { } } - /// **Available Width Calculation - Smart Space Detection** - /// - /// Calculates how much horizontal space an event can use by analyzing - /// the layout to the right and detecting blocking overlapping events. - /// - /// ## **Core Algorithm:** - /// 1. **Baseline Calculation**: Start with remaining space from current position to edge - /// 2. **Right-side Scanning**: Check each column to the right for blocking events - /// 3. **Overlap Detection**: Use `_eventsOverlap()` to find temporal conflicts - /// 4. **Width Limiting**: Stop expansion at first blocking event found - /// 5. **Minimum Guarantee**: Ensure at least one column width is available - /// - /// ## **Scanning Strategy:** - /// ```dart - /// For each column to the right: - /// For each event in that column: - /// If event overlaps with target: - /// Limit width to that column's position - /// Break (stop scanning further right) - /// ``` - /// - /// ## **Width Calculation Logic:** - /// - **Initial Width**: `totalWidth - startPosition` (all remaining space) - /// - **Blocking Position**: `rightColumnIndex * baseSlotWidth` - /// - **Final Width**: `blockingPosition - startPosition` - /// - **Minimum Width**: `max(baseSlotWidth, calculatedWidth)` - /// - /// ## **Example Scenarios:** - /// - /// ### **Scenario 1: No Blocking Events** - /// ```dart - /// Event: 9:00-10:00 AM in Column 0 - /// Right columns: No overlapping events - /// Result: Expands to full remaining width - /// ``` - /// - /// ### **Scenario 2: Blocked by Column 2** - /// ```dart - /// Event: 9:00-11:00 AM in Column 0 - /// Column 2: Has event 9:30-10:30 AM (overlaps!) - /// Result: Width limited to Column 2's left boundary - /// ``` - /// - /// ### **Scenario 3: Multiple Potential Blocks** - /// ```dart - /// Event: 9:00-12:00 PM in Column 0 - /// Column 1: Event 10:00-11:00 AM (overlaps) - /// Column 3: Event 11:00-12:00 PM (also overlaps) - /// Result: Width limited to Column 1 (first blocking column) - /// ``` - /// - /// ## **Performance Optimizations:** - /// - **Early Termination**: Stops scanning at first blocking event - /// - **Column-based Scanning**: Only checks relevant columns - /// - **Efficient Overlap Detection**: Uses optimized `_eventsOverlap()` - /// - /// ## **Edge Cases:** - /// - **Rightmost Column**: Gets all remaining space - /// - **All Columns Blocked**: Falls back to minimum column width - /// - **No Right Columns**: Expands to total width boundary - /// - **Zero Available Space**: Guaranteed minimum width prevents layout break + /// Calculates how much horizontal space an event can use by checking for blocking events in columns to the right. + /// + /// Scans columns to the right and limits width at the first blocking event, guaranteeing a minimum width. + /// Optimized for early exit and efficient overlap detection. + /// + /// Handles edge cases like rightmost columns, all columns blocked, and zero available space. double _calculateAvailableWidthToRight( _NormalizedEvent targetEvent, List>> allColumns, @@ -769,77 +441,23 @@ class SideEventArranger extends EventArranger { } } -/// **Internal Event Data Structure - Normalized Representation** -/// -/// Standardized internal format for events optimized for layout calculations. -/// Converts complex calendar event data into a simplified, computation-friendly format. -/// -/// ## **Design Purpose:** -/// - **Performance**: Optimized data structure for layout algorithms -/// - **Consistency**: Standardized time representation (minutes since midnight) -/// - **Simplicity**: Removes complexity of original event data during processing -/// - **Multi-day Support**: Handles events spanning multiple days uniformly -/// -/// ## **Key Fields:** -/// -/// ### **originalEvent**: Reference to source data -/// - **Type**: `CalendarEventData` -/// - **Purpose**: Maintains link to original event for final output -/// - **Usage**: Accessed when creating final organized event data -/// -/// ### **effectiveStartTime**: Normalized start time in minutes -/// - **Format**: Minutes since midnight (0-1439) -/// - **Multi-day Logic**: Adjusted for current view date -/// - **Example**: 2:30 PM = 870 minutes (14.5 * 60) -/// -/// ### **effectiveEndTime**: Normalized end time in minutes -/// - **Format**: Minutes since midnight (0-1440) -/// - **Multi-day Logic**: Adjusted for current view date -/// - **Boundary**: Can be 1440 (next day) for events ending at midnight -/// -/// ### **isMultiDay**: Multi-day event indicator -/// - **Purpose**: Tracks whether event spans multiple calendar days -/// - **Usage**: Affects rendering and time calculations -/// - **Source**: Derived from `CalendarEventData.isRangingEvent` -/// -/// ### **isFullDay**: Full-day event indicator -/// - **Purpose**: Identifies all-day events for special handling -/// - **Usage**: May affect positioning and rendering logic -/// - **Source**: Derived from `CalendarEventData.isFullDayEvent` -/// -/// ## **Normalization Benefits:** -/// - **Uniform Time Format**: All events use same time representation -/// - **Simplified Comparisons**: Easy overlap detection with integer arithmetic -/// - **Multi-day Handling**: Complex spanning logic resolved once -/// - **Performance**: Reduced object complexity for hot path operations -/// -/// ## **Usage Pattern:** -/// ```dart -/// // Created during normalization phase -/// final normalized = _NormalizedEvent( -/// originalEvent: calendarEvent, -/// effectiveStartTime: 540, // 9:00 AM -/// effectiveEndTime: 660, // 11:00 AM -/// isMultiDay: false, -/// isFullDay: false, -/// ); -/// -/// // Used in overlap detection -/// if (event1.effectiveEndTime > event2.effectiveStartTime) { ... } -/// ``` +/// Internal event data structure for calendar layout. +/// +/// Stores normalized start/end times (minutes since midnight), multi-day/full-day flags, and a reference to the original event. +/// Used for efficient overlap detection, layout calculations, and rendering in calendar views. class _NormalizedEvent { /// Reference to the original calendar event data final CalendarEventData originalEvent; - + /// Event start time in minutes since midnight (0-1439) final int effectiveStartTime; - + /// Event end time in minutes since midnight (0-1440) final int effectiveEndTime; - + /// Whether this event spans multiple calendar days final bool isMultiDay; - + /// Whether this is a full-day event final bool isFullDay;