From 0b3208efcb08fc220b6196926a9ea8dc17e481cd Mon Sep 17 00:00:00 2001 From: solid-maksymtielnyi Date: Fri, 26 Jun 2026 16:35:28 +0300 Subject: [PATCH 1/4] Add horizontalScrollControllerGroup, verticalScrollControllerGroup and onLayoutMetrics parameters to CategoryOverflowCalendarDayView --- lib/calendar_day_view.dart | 9 +- .../category_overflow_calendar_day_view.dart | 90 ++++++++++++++++++- 2 files changed, 93 insertions(+), 6 deletions(-) diff --git a/lib/calendar_day_view.dart b/lib/calendar_day_view.dart index 2383901..3feb5ea 100644 --- a/lib/calendar_day_view.dart +++ b/lib/calendar_day_view.dart @@ -6,12 +6,15 @@ /// in the app that need better day view library calendar_day_view; +export 'package:linked_scroll_controller/linked_scroll_controller.dart' + show LinkedScrollControllerGroup; + +export 'src/day_views/calendar_day_view_base.dart'; +export 'src/day_views/category/category_overflow_calendar_day_view.dart'; export 'src/day_views/event_calendar_day_view.dart'; export 'src/day_views/in_row_calendar_day_view.dart'; export 'src/day_views/overflow/overflow_calendar_day_view.dart'; -export 'src/day_views/category/category_overflow_calendar_day_view.dart'; -export 'src/models/day_event.dart'; export 'src/models/categorized_day_event.dart'; +export 'src/models/day_event.dart'; export 'src/models/event_category.dart'; export 'src/models/typedef.dart'; -export 'src/day_views/calendar_day_view_base.dart'; diff --git a/lib/src/day_views/category/category_overflow_calendar_day_view.dart b/lib/src/day_views/category/category_overflow_calendar_day_view.dart index 51ff710..e36d11e 100644 --- a/lib/src/day_views/category/category_overflow_calendar_day_view.dart +++ b/lib/src/day_views/category/category_overflow_calendar_day_view.dart @@ -4,7 +4,6 @@ import 'package:calendar_day_view/src/extensions/list_extensions.dart'; import 'package:calendar_day_view/src/extensions/time_of_day_extension.dart'; import 'package:calendar_day_view/src/widgets/timed_rebuilder.dart'; import 'package:flutter/material.dart'; -import 'package:linked_scroll_controller/linked_scroll_controller.dart'; import 'package:timezone/timezone.dart'; import '../../../calendar_day_view.dart'; @@ -21,6 +20,48 @@ typedef TitleRowBuilder = Widget Function({ Widget? logo, }); +typedef CategoryDayViewLayoutMetricsCallback = void Function( + CategoryDayViewLayoutMetrics metrics, +); + +@immutable +class CategoryDayViewLayoutMetrics { + final bool isHorizontallyOverflowing; + final bool isVerticallyOverflowing; + final double contentWidth; + final double contentHeight; + + @override + int get hashCode => Object.hash( + isHorizontallyOverflowing, + isVerticallyOverflowing, + contentWidth, + contentHeight, + ); + + const CategoryDayViewLayoutMetrics({ + required this.isHorizontallyOverflowing, + required this.isVerticallyOverflowing, + required this.contentWidth, + required this.contentHeight, + }); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is CategoryDayViewLayoutMetrics && + other.isHorizontallyOverflowing == isHorizontallyOverflowing && + other.isVerticallyOverflowing == isVerticallyOverflowing && + other.contentWidth == contentWidth && + other.contentHeight == contentHeight; + + @override + String toString() => 'CategoryDayViewLayoutMetrics(' + 'isHorizontallyOverflowing: $isHorizontallyOverflowing, ' + 'isVerticallyOverflowing: $isVerticallyOverflowing, ' + 'contentWidth: $contentWidth, contentHeight: $contentHeight)'; +} + abstract class GroupingStrategy { ({List> grouped, List> nonGrouped}) groupEvents(List> events); @@ -28,6 +69,7 @@ abstract class GroupingStrategy { class NoGroupingStrategy implements GroupingStrategy { const NoGroupingStrategy(); + @override ({List> grouped, List> nonGrouped}) groupEvents(List> events) { @@ -49,6 +91,7 @@ class EventGroup extends CategorizedDayEvent { abstract class GroupLayoutStrategy { const GroupLayoutStrategy(); + bool canLayout(EventGroup group) => true; Widget layout( @@ -93,6 +136,9 @@ class CategoryOverflowCalendarDayView extends StatefulWidget required this.minColumnWidth, required this.timeLabelsFormatter, required this.currentTimeFormatter, + this.horizontalScrollControllerGroup, + this.verticalScrollControllerGroup, + this.onLayoutMetrics, ValueGetter? clock, }) : clock = clock ?? DateTime.now, super(key: key); @@ -100,6 +146,9 @@ class CategoryOverflowCalendarDayView extends StatefulWidget final Border? tableBodyBorder; final Border? timeColumnBorder; final double minColumnWidth; + final LinkedScrollControllerGroup? horizontalScrollControllerGroup; + final LinkedScrollControllerGroup? verticalScrollControllerGroup; + final CategoryDayViewLayoutMetricsCallback? onLayoutMetrics; final ValueGetter clock; final GroupingStrategy? groupingStrategy; final GroupLayoutStrategy? groupLayoutStrategy; @@ -176,10 +225,12 @@ class CategoryOverflowCalendarDayView extends StatefulWidget class _CategoryOverflowCalendarDayViewState extends State> { - final _horizontalScrollLink = LinkedScrollControllerGroup(); + late final _horizontalScrollLink = + widget.horizontalScrollControllerGroup ?? LinkedScrollControllerGroup(); late final _headerScrollController = _horizontalScrollLink.addAndGet(); late final _horizScrollController = _horizontalScrollLink.addAndGet(); - final _verticalScrollLink = LinkedScrollControllerGroup(); + late final _verticalScrollLink = + widget.verticalScrollControllerGroup ?? LinkedScrollControllerGroup(); late final _timeScrollController = _verticalScrollLink.addAndGet(); late final _vertScrollController = _verticalScrollLink.addAndGet(); @@ -207,6 +258,30 @@ class _CategoryOverflowCalendarDayViewState final rowLength = totalWidth - widget.timeColumnWidth; final tileWidth = rowLength / categoriesCount; + final onLayoutMetrics = widget.onLayoutMetrics; + + if (onLayoutMetrics != null) { + final contentWidth = rowLength; + final contentHeight = rowHeight * timeList.length; + + WidgetsBinding.instance.addPostFrameCallback((_) { + final hPosition = _horizScrollController.hasClients + ? _horizScrollController.position + : null; + final vPosition = _vertScrollController.hasClients + ? _vertScrollController.position + : null; + onLayoutMetrics(CategoryDayViewLayoutMetrics( + isHorizontallyOverflowing: + hPosition != null && hPosition.maxScrollExtent > 0, + isVerticallyOverflowing: + vPosition != null && vPosition.maxScrollExtent > 0, + contentWidth: contentWidth, + contentHeight: contentHeight, + )); + }); + } + return Column( mainAxisSize: MainAxisSize.min, children: [ @@ -314,6 +389,15 @@ class _CategoryOverflowCalendarDayViewState ), ); } + + @override + void dispose() { + _headerScrollController.dispose(); + _horizScrollController.dispose(); + _timeScrollController.dispose(); + _vertScrollController.dispose(); + super.dispose(); + } } class VerticalClipper extends CustomClipper { From db31f5f581d3d2dd5bdc08b69e20316d503b5cb9 Mon Sep 17 00:00:00 2001 From: solid-maksymtielnyi Date: Sat, 27 Jun 2026 00:59:12 +0300 Subject: [PATCH 2/4] Add bodyViewport to CategoryDayViewLayoutMetrics --- .../category_overflow_calendar_day_view.dart | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/lib/src/day_views/category/category_overflow_calendar_day_view.dart b/lib/src/day_views/category/category_overflow_calendar_day_view.dart index e36d11e..3124e9c 100644 --- a/lib/src/day_views/category/category_overflow_calendar_day_view.dart +++ b/lib/src/day_views/category/category_overflow_calendar_day_view.dart @@ -31,12 +31,25 @@ class CategoryDayViewLayoutMetrics { final double contentWidth; final double contentHeight; + /// The scrollable body's viewport, relative to the day view's top-left. + /// + /// [Rect.left] is where the body starts horizontally (after the time + /// column), [Rect.top] is where it starts vertically (below the header), and + /// the size is the visible viewport — as opposed to the full scrollable + /// content size, which is [contentWidth] x [contentHeight]. + /// + /// Useful for overlaying content (indicators, minimaps, custom gesture + /// layers) that must align with the table body. Defaults to [Rect.zero] + /// until the first frame has been laid out and measured. + final Rect bodyViewport; + @override int get hashCode => Object.hash( isHorizontallyOverflowing, isVerticallyOverflowing, contentWidth, contentHeight, + bodyViewport, ); const CategoryDayViewLayoutMetrics({ @@ -44,6 +57,7 @@ class CategoryDayViewLayoutMetrics { required this.isVerticallyOverflowing, required this.contentWidth, required this.contentHeight, + this.bodyViewport = Rect.zero, }); @override @@ -53,13 +67,15 @@ class CategoryDayViewLayoutMetrics { other.isHorizontallyOverflowing == isHorizontallyOverflowing && other.isVerticallyOverflowing == isVerticallyOverflowing && other.contentWidth == contentWidth && - other.contentHeight == contentHeight; + other.contentHeight == contentHeight && + other.bodyViewport == bodyViewport; @override String toString() => 'CategoryDayViewLayoutMetrics(' 'isHorizontallyOverflowing: $isHorizontallyOverflowing, ' 'isVerticallyOverflowing: $isVerticallyOverflowing, ' - 'contentWidth: $contentWidth, contentHeight: $contentHeight)'; + 'contentWidth: $contentWidth, contentHeight: $contentHeight, ' + 'bodyViewport: $bodyViewport)'; } abstract class GroupingStrategy { @@ -234,6 +250,24 @@ class _CategoryOverflowCalendarDayViewState late final _timeScrollController = _verticalScrollLink.addAndGet(); late final _vertScrollController = _verticalScrollLink.addAndGet(); + /// Identifies the scrollable body so its viewport rect can be measured. + final _bodyKey = GlobalKey(); + + /// Measures the scrollable body's viewport relative to the day view's + /// top-left, so the result accounts for the header and any safe-area inset. + Rect _measureBodyViewport() { + final root = context.findRenderObject(); + final body = _bodyKey.currentContext?.findRenderObject(); + if (root is! RenderBox || + body is! RenderBox || + !root.hasSize || + !body.hasSize) { + return Rect.zero; + } + + return root.globalToLocal(body.localToGlobal(Offset.zero)) & body.size; + } + @override Widget build(BuildContext context) { final timeStart = widget.currentDate.copyTimeAndMinClean(widget.startOfDay); @@ -265,6 +299,7 @@ class _CategoryOverflowCalendarDayViewState final contentHeight = rowHeight * timeList.length; WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; final hPosition = _horizScrollController.hasClients ? _horizScrollController.position : null; @@ -278,6 +313,7 @@ class _CategoryOverflowCalendarDayViewState vPosition != null && vPosition.maxScrollExtent > 0, contentWidth: contentWidth, contentHeight: contentHeight, + bodyViewport: _measureBodyViewport(), )); }); } @@ -336,6 +372,7 @@ class _CategoryOverflowCalendarDayViewState ), Expanded( child: ClipPath( + key: _bodyKey, clipper: VerticalClipper(), child: DecoratedBox( decoration: BoxDecoration( From 86795c87552d6ddbe5671c73547217527574d0fb Mon Sep 17 00:00:00 2001 From: solid-maksymtielnyi Date: Sat, 27 Jun 2026 01:44:11 +0300 Subject: [PATCH 3/4] refactor: Add bodyContentSize instead of separate dimensions --- .../category_overflow_calendar_day_view.dart | 78 +++++++------------ 1 file changed, 26 insertions(+), 52 deletions(-) diff --git a/lib/src/day_views/category/category_overflow_calendar_day_view.dart b/lib/src/day_views/category/category_overflow_calendar_day_view.dart index 3124e9c..ca0430e 100644 --- a/lib/src/day_views/category/category_overflow_calendar_day_view.dart +++ b/lib/src/day_views/category/category_overflow_calendar_day_view.dart @@ -26,56 +26,33 @@ typedef CategoryDayViewLayoutMetricsCallback = void Function( @immutable class CategoryDayViewLayoutMetrics { - final bool isHorizontallyOverflowing; - final bool isVerticallyOverflowing; - final double contentWidth; - final double contentHeight; - - /// The scrollable body's viewport, relative to the day view's top-left. - /// - /// [Rect.left] is where the body starts horizontally (after the time - /// column), [Rect.top] is where it starts vertically (below the header), and - /// the size is the visible viewport — as opposed to the full scrollable - /// content size, which is [contentWidth] x [contentHeight]. - /// - /// Useful for overlaying content (indicators, minimaps, custom gesture - /// layers) that must align with the table body. Defaults to [Rect.zero] - /// until the first frame has been laid out and measured. + final Size bodyContentSize; final Rect bodyViewport; + bool get isHorizontallyOverflowing => + bodyContentSize.width > bodyViewport.width; + + bool get isVerticallyOverflowing => + bodyContentSize.height > bodyViewport.height; + @override - int get hashCode => Object.hash( - isHorizontallyOverflowing, - isVerticallyOverflowing, - contentWidth, - contentHeight, - bodyViewport, - ); + int get hashCode => Object.hash(bodyContentSize, bodyViewport); const CategoryDayViewLayoutMetrics({ - required this.isHorizontallyOverflowing, - required this.isVerticallyOverflowing, - required this.contentWidth, - required this.contentHeight, - this.bodyViewport = Rect.zero, + required this.bodyContentSize, + required this.bodyViewport, }); @override bool operator ==(Object other) => identical(this, other) || other is CategoryDayViewLayoutMetrics && - other.isHorizontallyOverflowing == isHorizontallyOverflowing && - other.isVerticallyOverflowing == isVerticallyOverflowing && - other.contentWidth == contentWidth && - other.contentHeight == contentHeight && + other.bodyContentSize == bodyContentSize && other.bodyViewport == bodyViewport; @override String toString() => 'CategoryDayViewLayoutMetrics(' - 'isHorizontallyOverflowing: $isHorizontallyOverflowing, ' - 'isVerticallyOverflowing: $isVerticallyOverflowing, ' - 'contentWidth: $contentWidth, contentHeight: $contentHeight, ' - 'bodyViewport: $bodyViewport)'; + 'bodyContentSize: $bodyContentSize, bodyViewport: $bodyViewport)'; } abstract class GroupingStrategy { @@ -255,14 +232,15 @@ class _CategoryOverflowCalendarDayViewState /// Measures the scrollable body's viewport relative to the day view's /// top-left, so the result accounts for the header and any safe-area inset. - Rect _measureBodyViewport() { + /// Returns null while the render tree is not ready to be measured. + Rect? _measureBodyViewport() { final root = context.findRenderObject(); final body = _bodyKey.currentContext?.findRenderObject(); if (root is! RenderBox || body is! RenderBox || !root.hasSize || !body.hasSize) { - return Rect.zero; + return null; } return root.globalToLocal(body.localToGlobal(Offset.zero)) & body.size; @@ -295,25 +273,21 @@ class _CategoryOverflowCalendarDayViewState final onLayoutMetrics = widget.onLayoutMetrics; if (onLayoutMetrics != null) { - final contentWidth = rowLength; - final contentHeight = rowHeight * timeList.length; + final bodyContentSize = Size( + rowLength, + rowHeight * timeList.length, + ); WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; - final hPosition = _horizScrollController.hasClients - ? _horizScrollController.position - : null; - final vPosition = _vertScrollController.hasClients - ? _vertScrollController.position - : null; + + final bodyViewport = _measureBodyViewport(); + + if (bodyViewport == null) return; + onLayoutMetrics(CategoryDayViewLayoutMetrics( - isHorizontallyOverflowing: - hPosition != null && hPosition.maxScrollExtent > 0, - isVerticallyOverflowing: - vPosition != null && vPosition.maxScrollExtent > 0, - contentWidth: contentWidth, - contentHeight: contentHeight, - bodyViewport: _measureBodyViewport(), + bodyContentSize: bodyContentSize, + bodyViewport: bodyViewport, )); }); } From 62c2bf31aabc1591328ade7c9e8a65e5e85e2f01 Mon Sep 17 00:00:00 2001 From: solid-maksymtielnyi Date: Wed, 1 Jul 2026 00:31:03 +0300 Subject: [PATCH 4/4] Improve _measureBodyViewport implementation --- .../day_views/category/category_overflow_calendar_day_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/day_views/category/category_overflow_calendar_day_view.dart b/lib/src/day_views/category/category_overflow_calendar_day_view.dart index ca0430e..9c3b603 100644 --- a/lib/src/day_views/category/category_overflow_calendar_day_view.dart +++ b/lib/src/day_views/category/category_overflow_calendar_day_view.dart @@ -243,7 +243,7 @@ class _CategoryOverflowCalendarDayViewState return null; } - return root.globalToLocal(body.localToGlobal(Offset.zero)) & body.size; + return body.localToGlobal(Offset.zero, ancestor: root) & body.size; } @override