From 6cf0549ba578e9fdbf8672884d1ab0e6dec1112b Mon Sep 17 00:00:00 2001 From: Matthias Ngeo Date: Fri, 12 Jul 2024 23:12:56 +0800 Subject: [PATCH] Refactor calendar --- forui/example/lib/main.dart | 8 +- forui/lib/src/widgets/calendar/calendar.dart | 229 ++++++++++++ .../day/day_picker.dart | 201 +++++++---- .../day/paged_day_picker.dart | 74 ++-- .../month/month_picker.dart | 42 ++- .../month/paged_month_picker.dart | 55 +-- .../src/widgets/calendar/shared/entry.dart | 283 +++++++++++++++ .../src/widgets/calendar/shared/header.dart | 256 +++++++++++++ .../shared}/paged_picker.dart | 118 +++--- .../year/paged_year_picker.dart | 47 ++- .../year/year_picker.dart | 41 ++- .../widgets/calendar/year_month_picker.dart | 138 +++++++ .../src/widgets/old_calendar/calendar.dart | 169 --------- .../src/widgets/old_calendar/controls.dart | 143 -------- .../lib/src/widgets/old_calendar/day/day.dart | 338 ------------------ .../lib/src/widgets/old_calendar/toggle.dart | 82 ----- .../old_calendar/year_month/year_month.dart | 182 ---------- .../year_month/year_month_picker.dart | 91 ----- forui/pubspec.yaml | 1 + forui/test/src/foundation/inkwell_test.dart | 2 - 20 files changed, 1230 insertions(+), 1270 deletions(-) create mode 100644 forui/lib/src/widgets/calendar/calendar.dart rename forui/lib/src/widgets/{old_calendar => calendar}/day/day_picker.dart (65%) rename forui/lib/src/widgets/{old_calendar => calendar}/day/paged_day_picker.dart (72%) rename forui/lib/src/widgets/{old_calendar/year_month => calendar}/month/month_picker.dart (71%) rename forui/lib/src/widgets/{old_calendar/year_month => calendar}/month/paged_month_picker.dart (57%) create mode 100644 forui/lib/src/widgets/calendar/shared/entry.dart create mode 100644 forui/lib/src/widgets/calendar/shared/header.dart rename forui/lib/src/widgets/{old_calendar => calendar/shared}/paged_picker.dart (66%) rename forui/lib/src/widgets/{old_calendar/year_month => calendar}/year/paged_year_picker.dart (69%) rename forui/lib/src/widgets/{old_calendar/year_month => calendar}/year/year_picker.dart (69%) create mode 100644 forui/lib/src/widgets/calendar/year_month_picker.dart delete mode 100644 forui/lib/src/widgets/old_calendar/calendar.dart delete mode 100644 forui/lib/src/widgets/old_calendar/controls.dart delete mode 100644 forui/lib/src/widgets/old_calendar/day/day.dart delete mode 100644 forui/lib/src/widgets/old_calendar/toggle.dart delete mode 100644 forui/lib/src/widgets/old_calendar/year_month/year_month.dart delete mode 100644 forui/lib/src/widgets/old_calendar/year_month/year_month_picker.dart diff --git a/forui/example/lib/main.dart b/forui/example/lib/main.dart index 964568f6b..d664140ce 100644 --- a/forui/example/lib/main.dart +++ b/forui/example/lib/main.dart @@ -53,11 +53,11 @@ class Testing extends StatelessWidget { style: FCalendarStyle.inherit(colorScheme: context.theme.colorScheme, typography: context.theme.typography, style: context.theme.style), start: LocalDate(1900, 1, 8), end: LocalDate(2024, 7, 10), - today: LocalDate.now(), + current: LocalDate.now(), initialMonth: LocalDate(2023, 7), - enabledPredicate: (_) => true, - selectedPredicate: _selected.contains, - onMonthChange: print, + enabled: (_) => true, + selected: _selected.contains, + date: print, onPress: print, onLongPress: print, ); diff --git a/forui/lib/src/widgets/calendar/calendar.dart b/forui/lib/src/widgets/calendar/calendar.dart new file mode 100644 index 000000000..ed929410f --- /dev/null +++ b/forui/lib/src/widgets/calendar/calendar.dart @@ -0,0 +1,229 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:forui/forui.dart'; +import 'package:forui/src/widgets/calendar/day/day_picker.dart'; +import 'package:forui/src/widgets/calendar/day/paged_day_picker.dart'; +import 'package:forui/src/widgets/calendar/shared/header.dart'; +import 'package:forui/src/widgets/calendar/year_month_picker.dart'; +import 'package:sugar/sugar.dart'; + +class FCalendar extends StatelessWidget { + + @override + Widget build(BuildContext context) { + // TODO: implement build + throw UnimplementedError(); + } + +} + +class _Calendar extends StatelessWidget { + final FCalendarStyle style; + final LocalDate start; + final LocalDate end; + final LocalDate today; + final ValueNotifier type; + final ValueNotifier month; + final Predicate enabled; + final Predicate selected; + final ValueChanged onMonthChange; + final ValueChanged onPress; + final ValueChanged onLongPress; + + const _Calendar({ + required this.style, + required this.start, + required this.end, + required this.today, + required this.type, + required this.month, + required this.enabled, + required this.selected, + required this.onMonthChange, + required this.onPress, + required this.onLongPress, + }); + + @override + Widget build(BuildContext context) => DecoratedBox( + decoration: style.decoration, + child: Padding( + padding: style.padding, + child: SizedBox( + height: (DayPicker.maxRows * DayPicker.tileDimension) + Header.height + 5, + width: DateTime.daysPerWeek * DayPicker.tileDimension, + child: Stack( + alignment: Alignment.topCenter, + children: [ + ValueListenableBuilder( + valueListenable: month, + builder: (context, month, child) => Header( + style: style.headerStyle, + type: type, + month: month, + ), + ), + ValueListenableBuilder( + valueListenable: type, + builder: (context, value, child) => switch (value) { + FCalendarPickerType.day => PagedDayPicker( + style: style, + start: start, + end: end, + today: today, + initial: month.value.truncate(to: DateUnit.months), + enabled: enabled, + selected: selected, + onMonthChange: (date) { + month.value = date; + onMonthChange(date); + }, + onPress: onPress, + onLongPress: onLongPress, + ), + FCalendarPickerType.yearMonth => YearMonthPicker( + style: style, + start: start, + end: end, + today: today, + onChange: (date) { + month.value = date; + type.value = FCalendarPickerType.day; + }, + ), + }, + ), + ], + ), + ), + ), + ); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('style', style)) + ..add(DiagnosticsProperty('start', start)) + ..add(DiagnosticsProperty('end', end)) + ..add(DiagnosticsProperty('today', today)) + ..add(DiagnosticsProperty('type', type)) + ..add(DiagnosticsProperty('month', month)) + ..add(DiagnosticsProperty('enabled', enabled)) + ..add(DiagnosticsProperty('selected', selected)) + ..add(DiagnosticsProperty('onMonthChange', onMonthChange)) + ..add(DiagnosticsProperty('onPress', onPress)) + ..add(DiagnosticsProperty('onLongPress', onLongPress)); + } +} + +/// The calendar's style. +final class FCalendarStyle with Diagnosticable { + /// The header's style. + final FCalendarHeaderStyle headerStyle; + + /// The day picker's style. + final FCalendarDayPickerStyle dayPickerStyle; + + /// The year/month picker's style. + final FCalendarYearMonthPickerStyle yearMonthPickerStyle; + + /// The decoration surrounding the header & picker. + final BoxDecoration decoration; + + /// The padding surrounding the header & picker. Defaults to `const EdgeInsets.symmetric(horizontal: 12, vertical: 16)`. + final EdgeInsets padding; + + /// The duration of the page switch animation. Defaults to 200 milliseconds. + final Duration pageAnimationDuration; + + /// Creates a new [FCalendarStyle]. + FCalendarStyle({ + required this.headerStyle, + required this.dayPickerStyle, + required this.yearMonthPickerStyle, + required this.decoration, + this.padding = const EdgeInsets.symmetric(horizontal: 12, vertical: 16), + this.pageAnimationDuration = const Duration(milliseconds: 200), + }); + + /// Creates a [FCalendarStyle] that inherits the color scheme and typography. + FCalendarStyle.inherit({ + required FColorScheme colorScheme, + required FTypography typography, + required FStyle style, + }) : this( + headerStyle: FCalendarHeaderStyle.inherit(colorScheme: colorScheme, typography: typography), + dayPickerStyle: FCalendarDayPickerStyle.inherit(colorScheme: colorScheme, typography: typography), + yearMonthPickerStyle: FCalendarYearMonthPickerStyle.inherit(colorScheme: colorScheme, typography: typography), + decoration: BoxDecoration( + borderRadius: style.borderRadius, + border: Border.all(color: colorScheme.border), + color: colorScheme.background, + ), + ); + + /// Returns a copy of this [FCalendarStyle] but with the given fields replaced with the new values. + /// + /// ```dart + /// final style = FCalendarStyle( + /// headerStyle: ..., + /// dayPickerStyle: ..., + /// // Other arguments omitted for brevity. + /// ); + /// + /// final copy = style.copyWith(dayPickerStyle: ...); + /// + /// print(style.headerStyle == copy.headerStyle); // true + /// print(style.dayPickerStyle == copy.dayPickerStyle); // false + /// ``` + FCalendarStyle copyWith({ + FCalendarHeaderStyle? headerStyle, + FCalendarDayPickerStyle? dayPickerStyle, + FCalendarYearMonthPickerStyle? yearMonthPickerStyle, + BoxDecoration? decoration, + EdgeInsets? padding, + Duration? pageAnimationDuration, + }) => + FCalendarStyle( + headerStyle: headerStyle ?? this.headerStyle, + dayPickerStyle: dayPickerStyle ?? this.dayPickerStyle, + yearMonthPickerStyle: yearMonthPickerStyle ?? this.yearMonthPickerStyle, + decoration: decoration ?? this.decoration, + padding: padding ?? this.padding, + pageAnimationDuration: pageAnimationDuration ?? this.pageAnimationDuration, + ); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('headerStyle', headerStyle)) + ..add(DiagnosticsProperty('dayPickerStyle', dayPickerStyle)) + ..add(DiagnosticsProperty('yearMonthPickerStyle', yearMonthPickerStyle)) + ..add(DiagnosticsProperty('decoration', decoration)) + ..add(DiagnosticsProperty('padding', padding)) + ..add(DiagnosticsProperty('pageAnimationDuration', pageAnimationDuration)); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is FCalendarStyle && + runtimeType == other.runtimeType && + headerStyle == other.headerStyle && + dayPickerStyle == other.dayPickerStyle && + yearMonthPickerStyle == other.yearMonthPickerStyle && + decoration == other.decoration && + padding == other.padding && + pageAnimationDuration == other.pageAnimationDuration; + + @override + int get hashCode => + headerStyle.hashCode ^ + dayPickerStyle.hashCode ^ + yearMonthPickerStyle.hashCode ^ + decoration.hashCode ^ + padding.hashCode ^ + pageAnimationDuration.hashCode; +} diff --git a/forui/lib/src/widgets/old_calendar/day/day_picker.dart b/forui/lib/src/widgets/calendar/day/day_picker.dart similarity index 65% rename from forui/lib/src/widgets/old_calendar/day/day_picker.dart rename to forui/lib/src/widgets/calendar/day/day_picker.dart index 008afc05a..268734b0b 100644 --- a/forui/lib/src/widgets/old_calendar/day/day_picker.dart +++ b/forui/lib/src/widgets/calendar/day/day_picker.dart @@ -1,21 +1,24 @@ -part of '../calendar.dart'; +import 'dart:collection'; -/// The maximum number of rows in a month. In this case, a 31 day month that starts on Saturday. -@internal -const maxDayPickerGridRows = 7; - -/// The height & width of a day in a [DayPicker]. -@internal -const maxDayPickerTileDimension = 42.0; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:forui/forui.dart'; +import 'package:forui/src/widgets/calendar/shared/entry.dart'; +import 'package:meta/meta.dart'; +import 'package:sugar/sugar.dart'; @internal class DayPicker extends StatefulWidget { + static const maxRows = 7; + static const tileDimension = 42.0; + final FCalendarDayPickerStyle style; final LocalDate month; final LocalDate today; final LocalDate? focused; - final bool Function(LocalDate day) enabledPredicate; - final bool Function(LocalDate day) selectedPredicate; + final Predicate enabled; + final Predicate selected; final ValueChanged onPress; final ValueChanged onLongPress; @@ -24,8 +27,8 @@ class DayPicker extends StatefulWidget { required this.month, required this.today, required this.focused, - required this.enabledPredicate, - required this.selectedPredicate, + required this.enabled, + required this.selected, required this.onPress, required this.onLongPress, super.key, @@ -42,10 +45,10 @@ class DayPicker extends StatefulWidget { ..add(DiagnosticsProperty('month', month)) ..add(DiagnosticsProperty('today', today)) ..add(DiagnosticsProperty('focused', focused)) - ..add(ObjectFlagProperty('enabledPredicate', enabledPredicate, ifNull: 'null')) - ..add(ObjectFlagProperty('selectedPredicate', selectedPredicate, ifNull: 'null')) - ..add(ObjectFlagProperty('onPress', onPress, ifNull: 'null')) - ..add(ObjectFlagProperty('onLongPress', onLongPress, ifNull: 'null')); + ..add(DiagnosticsProperty('enabledPredicate', enabled, ifNull: 'all enabled')) + ..add(DiagnosticsProperty('selectedPredicate', selected, ifNull: 'none selected')) + ..add(DiagnosticsProperty('onPress', onPress)) + ..add(DiagnosticsProperty('onLongPress', onLongPress)); } } @@ -56,6 +59,17 @@ class _DayPickerState extends State { void initState() { super.initState(); + final (first, last) = _range; + for (var date = first; date <= last; date = date.tomorrow) { + _days[date] = FocusNode(skipTraversal: true, debugLabel: '$date'); + } + + if (_days[widget.focused] case final focusNode?) { + focusNode.requestFocus(); + } + } + + (LocalDate, LocalDate) get _range { final firstDayOfWeek = widget.style.startDayOfWeek ?? DateTime.sunday; // TODO: Localization final firstDayOfMonth = widget.month.firstDayOfMonth; var difference = firstDayOfMonth.weekday - firstDayOfWeek; @@ -73,44 +87,39 @@ class _DayPickerState extends State { } final last = lastDayOfMonth.plus(days: difference); - for (var date = first; date <= last; date = date.tomorrow) { - _days[date] = FocusNode(skipTraversal: true, debugLabel: '$date'); - } - if (_days[widget.focused] case final focusNode?) { - focusNode.requestFocus(); - } + return (first, last); } + @override Widget build(BuildContext context) => SizedBox( - width: DateTime.daysPerWeek * maxDayPickerTileDimension, - child: GridView.custom( - padding: EdgeInsets.zero, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - gridDelegate: const _GridDelegate(), - childrenDelegate: SliverChildListDelegate( - [ - ..._headers(context), - for (final MapEntry(key: date, value: focusNode) in _days.entries) - day( - widget.style, - date, - focusNode, - widget.selectedPredicate, - widget.onPress, - widget.onLongPress, - enabled: widget.enabledPredicate(date), - current: date.month == widget.month.month, - today: date == widget.today, - selected: widget.selectedPredicate(date), - ), - ], - addRepaintBoundaries: false, - ), + width: DateTime.daysPerWeek * DayPicker.tileDimension, + child: GridView.custom( + padding: EdgeInsets.zero, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const _GridDelegate(), + childrenDelegate: SliverChildListDelegate( + addRepaintBoundaries: false, + [ + ..._headers(context), + for (final MapEntry(key: date, value: focusNode) in _days.entries) + Entry.day( + style: widget.style, + date: date, + focusNode: focusNode, + current: date.month == widget.month.month, + today: date == widget.today, + enabled: widget.enabled, + selected: widget.selected, + onPress: widget.onPress, + onLongPress: widget.onLongPress, + ), + ], ), - ); + ), + ); List _headers(BuildContext context) { final firstDayOfWeek = widget.style.startDayOfWeek ?? DateTime.sunday; // TODO: Localization @@ -127,7 +136,7 @@ class _DayPickerState extends State { @override void didUpdateWidget(DayPicker old) { super.didUpdateWidget(old); - assert(old.month == widget.month, 'We assumed that a new DayPicker is created each time we navigate to a new month.'); + assert(old.month == widget.month, 'current month must not change.'); if (_days[widget.focused] case final focusNode? when old.focused != widget.focused) { focusNode.requestFocus(); @@ -149,18 +158,19 @@ class _GridDelegate extends SliverGridDelegate { @override SliverGridLayout getLayout(SliverConstraints constraints) => SliverGridRegularTileLayout( - childCrossAxisExtent: maxDayPickerTileDimension, - childMainAxisExtent: maxDayPickerTileDimension, - crossAxisCount: DateTime.daysPerWeek, - crossAxisStride: maxDayPickerTileDimension, - mainAxisStride: maxDayPickerTileDimension, - reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection), - ); + childCrossAxisExtent: DayPicker.tileDimension, + childMainAxisExtent: DayPicker.tileDimension, + crossAxisCount: DateTime.daysPerWeek, + crossAxisStride: DayPicker.tileDimension, + mainAxisStride: DayPicker.tileDimension, + reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection), + ); @override bool shouldRelayout(_GridDelegate oldDelegate) => false; } + /// A day picker's style. final class FCalendarDayPickerStyle with Diagnosticable { /// The text style for the day of th week headers. @@ -200,11 +210,11 @@ final class FCalendarDayPickerStyle with Diagnosticable { typography.sm.copyWith(color: colorScheme.mutedForeground.withOpacity(0.5), fontWeight: FontWeight.w500); final disabled = FCalendarDayStyle( - selectedStyle: FCalendarDayStateStyle.inherit( + selectedStyle: FCalendarEntryStyle( backgroundColor: colorScheme.primaryForeground, textStyle: mutedTextStyle, ), - unselectedStyle: FCalendarDayStateStyle.inherit( + unselectedStyle: FCalendarEntryStyle( backgroundColor: colorScheme.background, textStyle: mutedTextStyle, ), @@ -214,32 +224,29 @@ final class FCalendarDayPickerStyle with Diagnosticable { headerTextStyle: typography.xs.copyWith(color: colorScheme.mutedForeground), enabledStyles: ( current: FCalendarDayStyle( - selectedStyle: FCalendarDayStateStyle.inherit( + selectedStyle: FCalendarEntryStyle( backgroundColor: colorScheme.foreground, textStyle: typography.sm.copyWith(color: colorScheme.background, fontWeight: FontWeight.w500), ), - unselectedStyle: FCalendarDayStateStyle.inherit( + unselectedStyle: FCalendarEntryStyle( backgroundColor: colorScheme.background, textStyle: textStyle, focusedBackgroundColor: colorScheme.secondary, ), ), enclosing: FCalendarDayStyle( - selectedStyle: FCalendarDayStateStyle.inherit( + selectedStyle: FCalendarEntryStyle( backgroundColor: colorScheme.primaryForeground, textStyle: mutedTextStyle, ), - unselectedStyle: FCalendarDayStateStyle.inherit( + unselectedStyle: FCalendarEntryStyle( backgroundColor: colorScheme.background, textStyle: mutedTextStyle, focusedBackgroundColor: colorScheme.primaryForeground, ), ), ), - disabledStyles: ( - current: disabled, - enclosing: disabled, - ), + disabledStyles: (current: disabled, enclosing: disabled), ); } @@ -303,5 +310,65 @@ final class FCalendarDayPickerStyle with Diagnosticable { startDayOfWeek == other.startDayOfWeek; @override - int get hashCode => headerTextStyle.hashCode ^ enabledStyles.hashCode ^ disabledStyles.hashCode ^ startDayOfWeek.hashCode; + int get hashCode => + headerTextStyle.hashCode ^ enabledStyles.hashCode ^ disabledStyles.hashCode ^ startDayOfWeek.hashCode; +} + +/// A calender day's style. +final class FCalendarDayStyle with Diagnosticable { + /// The selected dates' style. + final FCalendarEntryStyle selectedStyle; + + /// The unselected dates' style. + final FCalendarEntryStyle unselectedStyle; + + /// Creates a [FCalendarDayStyle]. + const FCalendarDayStyle({ + required this.unselectedStyle, + required this.selectedStyle, + }); + + /// Returns a copy of this [FCalendarDayStyle] but with the given fields replaced with the new values. + /// + /// ```dart + /// final style = FCalendarDayStyle( + /// selectedStyle: ..., + /// unselectedStyle: ..., + /// // Other arguments omitted for brevity + /// ); + /// + /// final copy = style.copyWith( + /// unselectedStyle: ..., + /// ); + /// + /// print(style.selectedStyle == copy.selectedStyle); // true + /// print(style.unselectedStyle == copy.unselectedStyle); // false + /// ``` + FCalendarDayStyle copyWith({ + FCalendarEntryStyle? selectedStyle, + FCalendarEntryStyle? unselectedStyle, + }) => + FCalendarDayStyle( + selectedStyle: selectedStyle ?? this.selectedStyle, + unselectedStyle: unselectedStyle ?? this.unselectedStyle, + ); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('selectedStyle', selectedStyle)) + ..add(DiagnosticsProperty('unselectedStyle', unselectedStyle)); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is FCalendarDayStyle && + runtimeType == other.runtimeType && + unselectedStyle == other.unselectedStyle && + selectedStyle == other.selectedStyle; + + @override + int get hashCode => unselectedStyle.hashCode ^ selectedStyle.hashCode; } diff --git a/forui/lib/src/widgets/old_calendar/day/paged_day_picker.dart b/forui/lib/src/widgets/calendar/day/paged_day_picker.dart similarity index 72% rename from forui/lib/src/widgets/old_calendar/day/paged_day_picker.dart rename to forui/lib/src/widgets/calendar/day/paged_day_picker.dart index c5e552f72..9ec611d66 100644 --- a/forui/lib/src/widgets/old_calendar/day/paged_day_picker.dart +++ b/forui/lib/src/widgets/calendar/day/paged_day_picker.dart @@ -1,14 +1,20 @@ -part of '../calendar.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/semantics.dart'; +import 'package:flutter/widgets.dart'; +import 'package:forui/src/widgets/calendar/day/day_picker.dart'; +import 'package:forui/src/widgets/calendar/shared/paged_picker.dart'; +import 'package:meta/meta.dart'; +import 'package:sugar/sugar.dart'; @internal class PagedDayPicker extends PagedPicker { - final bool Function(LocalDate day) selectedPredicate; - final ValueChanged? onMonthChange; + final Predicate selected; + final ValueChanged? onMonthChange; final ValueChanged onPress; final ValueChanged onLongPress; PagedDayPicker({ - required this.selectedPredicate, + required this.selected, required this.onMonthChange, required this.onPress, required this.onLongPress, @@ -17,37 +23,33 @@ class PagedDayPicker extends PagedPicker { required super.end, required super.today, required super.initial, - required bool Function(LocalDate) enabledPredicate, + required super.enabled, super.key, - }) : super( - enabledPredicate: (date) => start <= date && date <= end && enabledPredicate(date), - ); + }); @override - State createState() => _PageDayPickerState(); + State createState() => _PagedDayPickerState(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties - ..add(DiagnosticsProperty('selectedPredicate', selectedPredicate)) + ..add(DiagnosticsProperty('selectedPredicate', selected)) ..add(DiagnosticsProperty('onMonthChange', onMonthChange)) ..add(DiagnosticsProperty('onPress', onPress)) ..add(DiagnosticsProperty('onLongPress', onLongPress)); } } -class _PageDayPickerState extends PagedPickerState { - late TextDirection _textDirection; - +class _PagedDayPickerState extends PagedPickerState { @override Widget buildItem(BuildContext context, int page) => DayPicker( - focused: focusedDate, style: widget.style.dayPickerStyle, month: widget.start.truncate(to: DateUnit.months).plus(months: page), today: widget.today, - enabledPredicate: widget.enabledPredicate, - selectedPredicate: widget.selectedPredicate, + focused: focusedDate, + enabled: widget.enabled, + selected: widget.selected, onPress: (date) { setState(() => focusedDate = date); widget.onPress(date); @@ -59,24 +61,7 @@ class _PageDayPickerState extends PagedPickerState { ); @override - void didChangeDependencies() { - super.didChangeDependencies(); - _textDirection = Directionality.of(context); - } - - /// Handler for when the overall day grid obtains or loses focus. - @override - void handleGridFocusChange(bool focused) { - setState(() { - if (focused && focusedDate == null) { - final preferred = widget.today.truncate(to: DateUnit.months) == current ? widget.today.day : 1; - focusedDate = _focusableDayForMonth(current, preferred); - } - }); - } - - @override - void handlePageChange(int page) { + void onPageChange(int page) { setState(() { final changed = widget.start.truncate(to: DateUnit.months).plus(months: page); if (current == changed) { @@ -84,7 +69,7 @@ class _PageDayPickerState extends PagedPickerState { } current = changed; - widget.onMonthChange?.call(current.toNative()); + widget.onMonthChange?.call(current); if (focusedDate case final focused? when focused.truncate(to: DateUnit.months) == current) { // We have navigated to a new month with the grid focused, but the // focused day is not in this month. Choose a new one trying to keep @@ -92,10 +77,17 @@ class _PageDayPickerState extends PagedPickerState { focusedDate = _focusableDayForMonth(current, focusedDate!.day); } - SemanticsService.announce( - current.toString(), // TODO: localization - _textDirection, - ); + SemanticsService.announce(current.toString(), textDirection); // TODO: localization + }); + } + + @override + void onGridFocusChange(bool focused) { + setState(() { + if (focused && focusedDate == null) { + final preferred = widget.today.truncate(to: DateUnit.months) == current ? widget.today.day : 1; + focusedDate = _focusableDayForMonth(current, preferred); + } }); } @@ -108,14 +100,14 @@ class _PageDayPickerState extends PagedPickerState { // Can we use the preferred day in this month? if (preferredDay <= month.daysInMonth) { final newFocus = month.copyWith(day: preferredDay); - if (widget.enabledPredicate(newFocus)) { + if (widget.enabled(newFocus)) { return newFocus; } } // Start at the 1st and take the first enabled date. for (var newFocus = month; newFocus.month == month.month; newFocus = newFocus.tomorrow) { - if (widget.enabledPredicate(newFocus)) { + if (widget.enabled(newFocus)) { return newFocus; } } diff --git a/forui/lib/src/widgets/old_calendar/year_month/month/month_picker.dart b/forui/lib/src/widgets/calendar/month/month_picker.dart similarity index 71% rename from forui/lib/src/widgets/old_calendar/year_month/month/month_picker.dart rename to forui/lib/src/widgets/calendar/month/month_picker.dart index 346b0a97e..cad228eb8 100644 --- a/forui/lib/src/widgets/old_calendar/year_month/month/month_picker.dart +++ b/forui/lib/src/widgets/calendar/month/month_picker.dart @@ -1,7 +1,17 @@ -part of '../../calendar.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:forui/src/widgets/calendar/shared/entry.dart'; +import 'package:forui/src/widgets/calendar/year_month_picker.dart'; +import 'package:intl/intl.dart'; +import 'package:meta/meta.dart'; +import 'package:sugar/sugar.dart'; + +final _yMMMM = DateFormat.yMMMM(); @internal class MonthPicker extends StatefulWidget { + static const columns = 3; + final FCalendarYearMonthPickerStyle style; final LocalDate currentYear; final LocalDate start; @@ -10,7 +20,7 @@ class MonthPicker extends StatefulWidget { final LocalDate? focused; final ValueChanged onPress; - const MonthPicker({ + MonthPicker({ required this.style, required this.currentYear, required this.start, @@ -19,7 +29,7 @@ class MonthPicker extends StatefulWidget { required this.focused, required this.onPress, super.key, - }); + }): assert(currentYear == currentYear.truncate(to: DateUnit.years), 'currentYear must be truncated to years'); @override State createState() => _MonthPickerState(); @@ -45,28 +55,27 @@ class _MonthPickerState extends State { super.initState(); _months = List.generate(12, (i) => FocusNode(skipTraversal: true, debugLabel: '$i')); - final focused = widget.focused; - if (focused != null) { - _months[focused.month - 1].requestFocus(); + if (widget.focused != null) { + _months[widget.focused!.month - 1].requestFocus(); } } @override Widget build(BuildContext context) => GridView( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: yearMonthPickerColumns, + crossAxisCount: MonthPicker.columns, childAspectRatio: 1.618, ), children: [ for (var month = widget.currentYear, i = 0; i < 12; month = month.plus(months: 1), i++) - yearMonth( - widget.style, - month, - _months[i], - widget.onPress, - (date) => '${date.month}', // TODO: localize - enabled: widget.start <= month && month <= widget.end, + Entry.yearMonth( + style: widget.style, + date: month, + focusNode: _months[i], current: widget.today.truncate(to: DateUnit.months) == month, + enabled: widget.start <= month && month <= widget.end, + format: (date) => _yMMMM.format(date.toNative()), // TODO: localize + onPress: widget.onPress, ), ], ); @@ -74,10 +83,7 @@ class _MonthPickerState extends State { @override void didUpdateWidget(MonthPicker old) { super.didUpdateWidget(old); - assert( - old.currentYear == widget.currentYear, - 'We assumed that a new YearPicker is created each time we navigate to a years page.', - ); + assert(old.currentYear == widget.currentYear, 'currentYear must not change.'); final focused = widget.focused; if (focused == null || focused < widget.currentYear || widget.currentYear.plus(years: 1) <= focused) { diff --git a/forui/lib/src/widgets/old_calendar/year_month/month/paged_month_picker.dart b/forui/lib/src/widgets/calendar/month/paged_month_picker.dart similarity index 57% rename from forui/lib/src/widgets/old_calendar/year_month/month/paged_month_picker.dart rename to forui/lib/src/widgets/calendar/month/paged_month_picker.dart index 7a451ec25..118be0be2 100644 --- a/forui/lib/src/widgets/old_calendar/year_month/month/paged_month_picker.dart +++ b/forui/lib/src/widgets/calendar/month/paged_month_picker.dart @@ -1,4 +1,9 @@ -part of '../../calendar.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:forui/src/widgets/calendar/month/month_picker.dart'; +import 'package:forui/src/widgets/calendar/shared/paged_picker.dart'; +import 'package:meta/meta.dart'; +import 'package:sugar/sugar.dart'; @internal class PagedMonthPicker extends PagedPicker { @@ -12,15 +17,19 @@ class PagedMonthPicker extends PagedPicker { required super.today, required super.initial, super.key, - }) : super(enabledPredicate: (date) => start <= date && date <= end); + }); @override State createState() => _PagedMonthPickerState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('onPress', onPress)); + } } class _PagedMonthPickerState extends PagedPickerState { - late TextDirection _textDirection; - @override Widget buildItem(BuildContext context, int page) => MonthPicker( style: widget.style.yearMonthPickerStyle, @@ -33,13 +42,10 @@ class _PagedMonthPickerState extends PagedPickerState { ); @override - void didChangeDependencies() { - super.didChangeDependencies(); - _textDirection = Directionality.of(context); - } + void onPageChange(int page) {} // Months will only appear on a single page. @override - void handleGridFocusChange(bool focused) { + void onGridFocusChange(bool focused) { setState(() { if (focused && focusedDate == null) { final currentMonth = widget.today.truncate(to: DateUnit.months); @@ -48,29 +54,6 @@ class _PagedMonthPickerState extends PagedPickerState { }); } - @override - void handlePageChange(int page) { - setState(() { - print('handlePageChange'); - // final changed = widget.start.truncate(to: DateUnit.years).plus(years: page * yearMonthPickerItems); - // if (current == changed) { - // return; - // } - // - // current = changed; - // if (focusedDate case final focused? when focused.truncate(to: DateUnit.years) == current) { - // // We have navigated to a new page with the grid focused, but the - // // focused year is not in this page. Choose a new one. - // focusedDate = _focusableMonth(current, focusedDate!); - // } - - SemanticsService.announce( - current.toString(), // TODO: localization - _textDirection, - ); - }); - } - LocalDate? _focusableMonth(LocalDate preferredMonth) { final end = widget.initial.plus(years: 1); if (widget.initial <= preferredMonth && preferredMonth < end) { @@ -78,7 +61,7 @@ class _PagedMonthPickerState extends PagedPickerState { } for (var newFocus = widget.initial; newFocus < end; newFocus = newFocus.plus(months: 1)) { - if (widget.enabledPredicate(newFocus)) { + if (widget.enabled(newFocus)) { return newFocus; } } @@ -91,9 +74,9 @@ class _PagedMonthPickerState extends PagedPickerState { @override Map get directionOffset => const { - TraversalDirection.up: Period(months: -yearMonthPickerColumns), + TraversalDirection.up: Period(months: -MonthPicker.columns), TraversalDirection.right: Period(months: 1), - TraversalDirection.down: Period(months: yearMonthPickerColumns), + TraversalDirection.down: Period(months: MonthPicker.columns), TraversalDirection.left: Period(months: -1), }; -} \ No newline at end of file +} diff --git a/forui/lib/src/widgets/calendar/shared/entry.dart b/forui/lib/src/widgets/calendar/shared/entry.dart new file mode 100644 index 000000000..708e5248a --- /dev/null +++ b/forui/lib/src/widgets/calendar/shared/entry.dart @@ -0,0 +1,283 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:forui/src/foundation/inkwell.dart'; +import 'package:forui/src/widgets/calendar/year_month_picker.dart'; +import 'package:intl/intl.dart'; +import 'package:meta/meta.dart'; +import 'package:sugar/sugar.dart'; + +import 'package:forui/src/widgets/calendar/day/day_picker.dart'; + +final _yMMMMd = DateFormat.yMMMMd(); + +@internal +abstract class Entry extends StatelessWidget { + final FCalendarEntryStyle style; + final ValueWidgetBuilder builder; + + factory Entry.day({ + required FCalendarDayPickerStyle style, + required LocalDate date, + required FocusNode focusNode, + required bool current, + required bool today, + required Predicate enabled, + required Predicate selected, + required ValueChanged onPress, + required ValueChanged onLongPress, + }) { + final enable = enabled(date); + final select = selected(date); + + final styles = enable ? style.enabledStyles : style.disabledStyles; + final dayStyle = current ? styles.current : styles.enclosing; + final entryStyle = select ? dayStyle.selectedStyle : dayStyle.unselectedStyle; + + // ignore: avoid_positional_boolean_parameters + Widget builder(BuildContext context, bool focused, Widget? child) => _Content( + style: entryStyle, + borderRadius: BorderRadius.horizontal( + left: selected(date.yesterday) ? Radius.zero : const Radius.circular(4), + right: selected(date.tomorrow) ? Radius.zero : const Radius.circular(4), + ), + text: '${date.day}', // TODO: localization + focused: focused, + current: today, + ); + + if (enabled(date)) { + return _EnabledEntry( + focusNode: focusNode, + date: date, + semanticLabel: '${_yMMMMd.format(date.toNative())}${today ? ', Today' : ''}', + selected: selected(date), + onPress: onPress, + onLongPress: onLongPress, + style: entryStyle, + builder: builder, + ); + } else { + return _DisabledEntry(style: entryStyle, builder: builder); + } + } + + factory Entry.yearMonth({ + required FCalendarYearMonthPickerStyle style, + required LocalDate date, + required FocusNode focusNode, + required bool current, + required bool enabled, + required ValueChanged onPress, + required String Function(LocalDate) format, + }) { + final entryStyle = enabled ? style.enabledStyle : style.disabledStyle; + + // ignore: avoid_positional_boolean_parameters + Widget builder(BuildContext context, bool focused, Widget? child) => _Content( + style: entryStyle, + borderRadius: BorderRadius.circular(8), + text: format(date), + focused: focused, + current: current, + ); + + if (enabled) { + return _EnabledEntry( + focusNode: focusNode, + date: date, + semanticLabel: format(date), + onPress: onPress, + style: entryStyle, + builder: builder, + ); + } else { + return _DisabledEntry(style: entryStyle, builder: builder); + } + } + + const Entry._({ + required this.style, + required this.builder, + }); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('style', style)) + ..add(DiagnosticsProperty('builder', builder)); + } +} + +class _EnabledEntry extends Entry { + final FocusNode focusNode; + final LocalDate date; + final String semanticLabel; + final bool selected; + final ValueChanged onPress; + final ValueChanged? onLongPress; + + const _EnabledEntry({ + required this.focusNode, + required this.date, + required this.semanticLabel, + required this.onPress, + required super.style, + required super.builder, + this.selected = false, + this.onLongPress, + }) : super._(); + + @override + Widget build(BuildContext context) => FInkWell( + focusNode: focusNode, + semanticLabel: semanticLabel, + selected: selected, + onPress: () => onPress(date), + onLongPress: () => onLongPress?.call(date), + builder: builder, + ); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('focusNode', focusNode)) + ..add(DiagnosticsProperty('date', date)) + ..add(StringProperty('semanticLabel', semanticLabel)) + ..add(FlagProperty('selected', value: selected, ifTrue: 'selected')) + ..add(DiagnosticsProperty('onPress', onPress)) + ..add(DiagnosticsProperty('onLongPress', onLongPress)); + } +} + +class _DisabledEntry extends Entry { + const _DisabledEntry({ + required super.style, + required super.builder, + }) : super._(); + + @override + Widget build(BuildContext context) => ExcludeSemantics(child: builder(context, false, null)); +} + +class _Content extends StatelessWidget { + final FCalendarEntryStyle style; + final BorderRadius borderRadius; + final String text; + final bool focused; + final bool current; + + const _Content({ + required this.style, + required this.borderRadius, + required this.text, + required this.focused, + required this.current, + }); + + @override + Widget build(BuildContext context) { + var textStyle = focused ? style.focusedTextStyle : style.textStyle; + if (current) { + textStyle = textStyle.copyWith(decoration: TextDecoration.underline); + } + + return DecoratedBox( + decoration: BoxDecoration( + borderRadius: borderRadius, + color: focused ? style.focusedBackgroundColor : style.backgroundColor, + ), + child: Center( + child: Text(text, style: textStyle), + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('style', style)) + ..add(DiagnosticsProperty('borderRadius', borderRadius)) + ..add(StringProperty('text', text)) + ..add(FlagProperty('focused', value: focused, ifTrue: 'focused')) + ..add(FlagProperty('current', value: current, ifTrue: 'current')); + } +} + +/// A calendar entry's style. +final class FCalendarEntryStyle with Diagnosticable { + /// The unfocused day's background color. + final Color backgroundColor; + + /// The unfocused day's text style. + final TextStyle textStyle; + + /// The focused day's background color. Defaults to [backgroundColor]. + final Color focusedBackgroundColor; + + /// The focused day's text style. Defaults to [textStyle]. + final TextStyle focusedTextStyle; + + /// Creates a [FCalendarEntryStyle]. + FCalendarEntryStyle({ + required this.backgroundColor, + required this.textStyle, + Color? focusedBackgroundColor, + TextStyle? focusedTextStyle, + }) : focusedBackgroundColor = focusedBackgroundColor ?? backgroundColor, + focusedTextStyle = focusedTextStyle ?? textStyle; + + /// Returns a copy of this [FCalendarEntryStyle] but with the given fields replaced with the new values. + /// + /// ```dart + /// final style = FCalendarEntryStyle( + /// backgroundColor: ..., + /// textStyle: ..., + /// ); + /// + /// final copy = style.copyWith( + /// textStyle: ..., + /// ); + /// + /// print(style.backgroundColor == copy.backgroundColor); // true + /// print(style.textStyle == copy.textStyle); // false + /// ``` + FCalendarEntryStyle copyWith({ + Color? backgroundColor, + TextStyle? textStyle, + Color? focusedBackgroundColor, + TextStyle? focusedTextStyle, + }) => + FCalendarEntryStyle( + backgroundColor: backgroundColor ?? this.backgroundColor, + textStyle: textStyle ?? this.textStyle, + focusedBackgroundColor: focusedBackgroundColor ?? this.focusedBackgroundColor, + focusedTextStyle: focusedTextStyle ?? this.focusedTextStyle, + ); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(ColorProperty('backgroundColor', backgroundColor)) + ..add(DiagnosticsProperty('textStyle', textStyle)) + ..add(ColorProperty('focusedBackgroundColor', focusedBackgroundColor)) + ..add(DiagnosticsProperty('focusedTextStyle', focusedTextStyle)); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is FCalendarEntryStyle && + runtimeType == other.runtimeType && + backgroundColor == other.backgroundColor && + textStyle == other.textStyle && + focusedBackgroundColor == other.focusedBackgroundColor && + focusedTextStyle == other.focusedTextStyle; + + @override + int get hashCode => + backgroundColor.hashCode ^ textStyle.hashCode ^ focusedBackgroundColor.hashCode ^ focusedTextStyle.hashCode; +} diff --git a/forui/lib/src/widgets/calendar/shared/header.dart b/forui/lib/src/widgets/calendar/shared/header.dart new file mode 100644 index 000000000..facb8e82a --- /dev/null +++ b/forui/lib/src/widgets/calendar/shared/header.dart @@ -0,0 +1,256 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:forui/forui.dart'; +import 'package:forui/src/foundation/inkwell.dart'; +import 'package:intl/intl.dart'; +import 'package:meta/meta.dart'; +import 'package:sugar/sugar.dart'; + +/// The current picker type. +enum FCalendarPickerType { + /// The day picker. + day, + + /// The year-month picker. + yearMonth, +} + +final _yMMMM = DateFormat.yMMMM(); + +@internal +class Header extends StatefulWidget { + static const height = 31.0; + + final FCalendarHeaderStyle style; + final ValueNotifier type; + final LocalDate month; + + const Header({ + required this.style, + required this.type, + required this.month, + super.key, + }); + + @override + State
createState() => _HeaderState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('style', style)) + ..add(DiagnosticsProperty('type', type)) + ..add(DiagnosticsProperty('month', month)); + } +} + +class _HeaderState extends State
with SingleTickerProviderStateMixin { + late AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController(vsync: this, duration: widget.style.animationDuration); + widget.type.addListener(_animate); + } + + @override + Widget build(BuildContext context) => SizedBox( + height: Header.height, + child: FInkWell( + onPress: () => widget.type.value = switch (widget.type.value) { + FCalendarPickerType.day => FCalendarPickerType.day, + FCalendarPickerType.yearMonth => FCalendarPickerType.yearMonth, + }, + builder: (context, _, child) => child!, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(_yMMMM.format(widget.month.toNative()), style: widget.style.headerTextStyle), // TODO: Localization + RotationTransition( + turns: Tween(begin: 0.0, end: 0.25).animate(_controller), + child: Padding( + padding: const EdgeInsets.all(2.0), + child: FAssets.icons.chevronRight( + height: 15, + colorFilter: ColorFilter.mode(context.theme.colorScheme.primary, BlendMode.srcIn), + ), + ), + ), + ], + ), + ), + ); + + @override + void didUpdateWidget(Header old) { + super.didUpdateWidget(old); + old.type.removeListener(_animate); + widget.type.addListener(_animate); + } + + @override + void dispose() { + widget.type.removeListener(_animate); + _controller.dispose(); + super.dispose(); + } + + void _animate() { + if (_controller.isCompleted) { + _controller.reverse(); + } else { + _controller.forward(); + } + } +} + +@internal +class Navigation extends StatelessWidget { + final FCalendarHeaderStyle style; + final VoidCallback? onPrevious; + final VoidCallback? onNext; + + const Navigation({ + required this.style, + required this.onPrevious, + required this.onNext, + super.key, + }); + + @override + Widget build(BuildContext context) { + final buttonStyle = context.theme.buttonStyles.outline; + final effectiveButtonStyle = buttonStyle.copyWith( + enabledBoxDecoration: buttonStyle.enabledBoxDecoration.copyWith(borderRadius: BorderRadius.circular(4)), + disabledBoxDecoration: buttonStyle.disabledBoxDecoration.copyWith(borderRadius: BorderRadius.circular(4)), + ); + + return Padding( + padding: const EdgeInsets.only(bottom: 5), + child: SizedBox( + height: Header.height, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 7), + child: FButton.raw( + // TODO: Replace with FButton.icon. + style: effectiveButtonStyle, + onPress: onPrevious, + child: Padding( + padding: const EdgeInsets.all(7), + child: FAssets.icons.chevronLeft( + height: 17, + colorFilter: ColorFilter.mode(style.iconColor, BlendMode.srcIn), + ), + ), + ), + ), + const Expanded(child: SizedBox()), + Padding( + padding: const EdgeInsets.only(right: 7), + child: FButton.raw( + // TODO: Replace with FButton.icon. + style: effectiveButtonStyle, + onPress: onNext, + child: Padding( + padding: const EdgeInsets.all(7), + child: FAssets.icons.chevronRight( + height: 17, + colorFilter: ColorFilter.mode(style.iconColor, BlendMode.srcIn), + ), + ), + ), + ), + ], + ), + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('style', style)) + ..add(DiagnosticsProperty('onPrevious', onPrevious)) + ..add(DiagnosticsProperty('onNext', onNext)); + } +} + +/// The calendar header's style. +final class FCalendarHeaderStyle with Diagnosticable { + /// The header's text style. + final TextStyle headerTextStyle; + + /// The header icons' color. + final Color iconColor; + + /// The arrow turn animation's duration. Defaults to `Duration(milliseconds: 200)`. + final Duration animationDuration; + + /// Creates a [FCalendarHeaderStyle]. + FCalendarHeaderStyle({ + required this.headerTextStyle, + required this.iconColor, + this.animationDuration = const Duration(milliseconds: 200), + }); + + /// Creates a [FCalendarHeaderStyle] that inherits its values from the given [colorScheme] and [typography]. + FCalendarHeaderStyle.inherit({required FColorScheme colorScheme, required FTypography typography}) + : this( + headerTextStyle: typography.sm.copyWith(color: colorScheme.primary, fontWeight: FontWeight.w600), + iconColor: colorScheme.mutedForeground, + ); + + /// Creates a copy of this but with the given fields replaced with the new values. + /// + /// ```dart + /// final style = FCalendarHeaderStyle( + /// headerTextStyle: ..., + /// iconColor:..., + /// // Other arguments omitted for brevity. + /// ); + /// + /// final copy = style.copyWith( + /// iconColor: ..., + /// ); + /// + /// print(style.headerTextStyle == copy.headerTextStyle); // true + /// print(style.iconColor == copy.iconColor); // false + /// ``` + FCalendarHeaderStyle copyWith({ + TextStyle? headerTextStyle, + Color? iconColor, + Duration? animationDuration, + }) => + FCalendarHeaderStyle( + headerTextStyle: headerTextStyle ?? this.headerTextStyle, + iconColor: iconColor ?? this.iconColor, + animationDuration: animationDuration ?? this.animationDuration, + ); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('headerTextStyle', headerTextStyle)) + ..add(ColorProperty('iconColor', iconColor)) + ..add(DiagnosticsProperty('animationDuration', animationDuration)); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is FCalendarHeaderStyle && + runtimeType == other.runtimeType && + headerTextStyle == other.headerTextStyle && + iconColor == other.iconColor && + animationDuration == other.animationDuration; + + @override + int get hashCode => headerTextStyle.hashCode ^ iconColor.hashCode ^ animationDuration.hashCode; +} diff --git a/forui/lib/src/widgets/old_calendar/paged_picker.dart b/forui/lib/src/widgets/calendar/shared/paged_picker.dart similarity index 66% rename from forui/lib/src/widgets/old_calendar/paged_picker.dart rename to forui/lib/src/widgets/calendar/shared/paged_picker.dart index 88848f1be..239ff8b64 100644 --- a/forui/lib/src/widgets/old_calendar/paged_picker.dart +++ b/forui/lib/src/widgets/calendar/shared/paged_picker.dart @@ -1,4 +1,10 @@ -part of 'calendar.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:forui/src/widgets/calendar/calendar.dart'; +import 'package:forui/src/widgets/calendar/shared/header.dart'; +import 'package:meta/meta.dart'; +import 'package:sugar/sugar.dart'; @internal abstract class PagedPicker extends StatefulWidget { @@ -7,17 +13,18 @@ abstract class PagedPicker extends StatefulWidget { final LocalDate end; final LocalDate today; final LocalDate initial; - final bool Function(LocalDate day) enabledPredicate; + final Predicate enabled; - const PagedPicker({ + PagedPicker({ required this.style, required this.start, required this.end, required this.today, required this.initial, - required this.enabledPredicate, + Predicate? enabled, super.key, - }); + }): + enabled = ((date) => start <= date && date <= end && (enabled?.call(date) ?? true)); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { @@ -28,26 +35,27 @@ abstract class PagedPicker extends StatefulWidget { ..add(DiagnosticsProperty('end', end)) ..add(DiagnosticsProperty('today', today)) ..add(DiagnosticsProperty('initial', initial)) - ..add(DiagnosticsProperty('enabledPredicate', enabledPredicate)); + ..add(DiagnosticsProperty('enabledPredicate', enabled)); } } // Most of the traversal logic is copied from Material's _MonthPickerState. @internal abstract class PagedPickerState extends State { - static const _shortcuts = { + static const _shortcuts = { SingleActivator(LogicalKeyboardKey.arrowLeft): DirectionalFocusIntent(TraversalDirection.left), SingleActivator(LogicalKeyboardKey.arrowRight): DirectionalFocusIntent(TraversalDirection.right), SingleActivator(LogicalKeyboardKey.arrowDown): DirectionalFocusIntent(TraversalDirection.down), SingleActivator(LogicalKeyboardKey.arrowUp): DirectionalFocusIntent(TraversalDirection.up), }; - late LocalDate current; LocalDate? focusedDate; + late LocalDate current; + late TextDirection textDirection; final GlobalKey _pageViewKey = GlobalKey(); late PageController _controller; late Map> _actions; - late FocusNode _gridFocusNode; + late FocusNode _focusNode; @override void initState() { @@ -55,33 +63,33 @@ abstract class PagedPickerState extends State { current = widget.initial; _controller = PageController(initialPage: delta(widget.start, widget.initial)); _actions = { - NextFocusIntent: CallbackAction(onInvoke: _handleGridNextFocus), - PreviousFocusIntent: CallbackAction(onInvoke: _handleGridPreviousFocus), - DirectionalFocusIntent: CallbackAction(onInvoke: _handleDirectionFocus), + NextFocusIntent: CallbackAction(onInvoke: _onGridNextFocus), + PreviousFocusIntent: CallbackAction(onInvoke: _onGridPreviousFocus), + DirectionalFocusIntent: CallbackAction(onInvoke: _onDirectionFocus), }; - _gridFocusNode = FocusNode(debugLabel: 'Grid'); + _focusNode = FocusNode(); } @override Widget build(BuildContext context) => Column( children: [ - Controls( + Navigation( style: widget.style.headerStyle, - onPrevious: _first ? null : _handlePrevious, - onNext: _last ? null : _handleNext, + onPrevious: _first ? null : _onPrevious, + onNext: _last ? null : _onNext, ), Expanded( child: FocusableActionDetector( shortcuts: _shortcuts, actions: _actions, - focusNode: _gridFocusNode, - onFocusChange: handleGridFocusChange, + focusNode: _focusNode, + onFocusChange: onGridFocusChange, child: PageView.builder( key: _pageViewKey, controller: _controller, itemBuilder: buildItem, itemCount: delta(widget.start, widget.end) + 1, - onPageChanged: handlePageChange, + onPageChanged: onPageChange, ), ), ), @@ -90,10 +98,16 @@ abstract class PagedPickerState extends State { Widget buildItem(BuildContext context, int page); + @override + void didChangeDependencies() { + super.didChangeDependencies(); + textDirection = Directionality.of(context); + } + @override void dispose() { _controller.dispose(); - _gridFocusNode.dispose(); + _focusNode.dispose(); super.dispose(); } @@ -103,16 +117,17 @@ abstract class PagedPickerState extends State { properties ..add(DiagnosticsProperty('current', current)) ..add(DiagnosticsProperty('focusedDate', focusedDate)) + ..add(DiagnosticsProperty('textDirection', textDirection)) ..add(DiagnosticsProperty('directionOffset', directionOffset)); } - void _handleNext() { + void _onNext() { if (!_last) { _controller.nextPage(duration: widget.style.pageAnimationDuration, curve: Curves.ease); } } - void _handlePrevious() { + void _onPrevious() { if (!_first) { _controller.previousPage(duration: widget.style.pageAnimationDuration, curve: Curves.ease); } @@ -122,34 +137,30 @@ abstract class PagedPickerState extends State { bool get _last => delta(widget.start, current) == delta(widget.start, widget.end); - /// Navigate to the given month. - void _showPage(LocalDate date, {bool jump = false}) { + void _showPage(LocalDate date) { final page = delta(widget.start, date); - if (jump) { - _controller.jumpToPage(page); - } else { - _controller.animateToPage( - page, - duration: widget.style.pageAnimationDuration, - curve: Curves.ease, - ); - } + _controller.animateToPage( + page, + duration: widget.style.pageAnimationDuration, + curve: Curves.ease, + ); } - void handlePageChange(int page); + void onPageChange(int page); - void handleGridFocusChange(bool focused); + // ignore: avoid_positional_boolean_parameters + void onGridFocusChange(bool focused); /// Move focus to the next element after the day grid. - void _handleGridNextFocus(NextFocusIntent intent) { - _gridFocusNode + void _onGridNextFocus(NextFocusIntent intent) { + _focusNode ..requestFocus() ..nextFocus(); } /// Move focus to the previous element before the day grid. - void _handleGridPreviousFocus(PreviousFocusIntent intent) { - _gridFocusNode + void _onGridPreviousFocus(PreviousFocusIntent intent) { + _focusNode ..requestFocus() ..previousFocus(); } @@ -163,13 +174,11 @@ abstract class PagedPickerState extends State { /// For horizontal directions, it will move forward or backward a day (depending /// on the current [TextDirection]). For vertical directions it will move up and /// down a week at a time. - void _handleDirectionFocus(DirectionalFocusIntent intent) { + void _onDirectionFocus(DirectionalFocusIntent intent) { assert(focusedDate != null, 'Cannot move focus without a focused day.'); setState(() { - final nextDate = _nextDateInDirection(focusedDate!, intent.direction); - if (nextDate != null) { - focusedDate = nextDate; - print(focusedDate); + if (_nextDate(focusedDate!, intent.direction) case final next?) { + focusedDate = next; if (delta(widget.start, focusedDate!) != delta(widget.start, current)) { _showPage(focusedDate!); } @@ -177,24 +186,21 @@ abstract class PagedPickerState extends State { }); } - // Swap left and right if the text direction if RTL - Period _adjustDirectionOffset(TraversalDirection traversalDirection, TextDirection textDirection) => - directionOffset[switch ((traversalDirection, textDirection)) { - (TraversalDirection.left, TextDirection.rtl) => TraversalDirection.right, - (TraversalDirection.right, TextDirection.rtl) => TraversalDirection.left, - _ => traversalDirection, - }]!; - - LocalDate? _nextDateInDirection(LocalDate date, TraversalDirection direction) { + LocalDate? _nextDate(LocalDate date, TraversalDirection direction) { final textDirection = Directionality.of(context); + final offset = directionOffset[switch ((direction, textDirection)) { + (TraversalDirection.left, TextDirection.rtl) => TraversalDirection.right, + (TraversalDirection.right, TextDirection.rtl) => TraversalDirection.left, + _ => direction, + }]!; - var next = date + _adjustDirectionOffset(direction, textDirection); + var next = date + offset; while (widget.start <= next && next <= widget.end) { - if (widget.enabledPredicate(next)) { + if (widget.enabled(next)) { return next; } - next = date + _adjustDirectionOffset(direction, textDirection); + next = date + offset; } return null; diff --git a/forui/lib/src/widgets/old_calendar/year_month/year/paged_year_picker.dart b/forui/lib/src/widgets/calendar/year/paged_year_picker.dart similarity index 69% rename from forui/lib/src/widgets/old_calendar/year_month/year/paged_year_picker.dart rename to forui/lib/src/widgets/calendar/year/paged_year_picker.dart index 54a29b202..34773142e 100644 --- a/forui/lib/src/widgets/old_calendar/year_month/year/paged_year_picker.dart +++ b/forui/lib/src/widgets/calendar/year/paged_year_picker.dart @@ -1,4 +1,9 @@ -part of '../../calendar.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; +import 'package:forui/src/widgets/calendar/shared/paged_picker.dart'; +import 'package:forui/src/widgets/calendar/year/year_picker.dart'; +import 'package:meta/meta.dart'; +import 'package:sugar/sugar.dart'; @internal class PagedYearPicker extends PagedPicker { @@ -12,19 +17,23 @@ class PagedYearPicker extends PagedPicker { required super.today, required super.initial, super.key, - }) : super(enabledPredicate: (date) => start <= date && date <= end); + }); @override State createState() => _PagedYearPickerState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('onPress', onPress)); + } } class _PagedYearPickerState extends PagedPickerState { - late TextDirection _textDirection; - @override Widget buildItem(BuildContext context, int page) => YearPicker( style: widget.style.yearMonthPickerStyle, - startYear: widget.start.truncate(to: DateUnit.years).plus(years: page * yearMonthPickerItems), + startYear: widget.start.truncate(to: DateUnit.years).plus(years: page * YearPicker.items), start: widget.start, end: widget.end, today: widget.today, @@ -33,13 +42,7 @@ class _PagedYearPickerState extends PagedPickerState { ); @override - void didChangeDependencies() { - super.didChangeDependencies(); - _textDirection = Directionality.of(context); - } - - @override - void handleGridFocusChange(bool focused) { + void onGridFocusChange(bool focused) { setState(() { if (focused && focusedDate == null) { final currentYear = widget.today.truncate(to: DateUnit.years); @@ -49,9 +52,9 @@ class _PagedYearPickerState extends PagedPickerState { } @override - void handlePageChange(int page) { + void onPageChange(int page) { setState(() { - final changed = widget.start.truncate(to: DateUnit.years).plus(years: page * yearMonthPickerItems); + final changed = widget.start.truncate(to: DateUnit.years).plus(years: page * YearPicker.items); if (current == changed) { return; } @@ -63,22 +66,18 @@ class _PagedYearPickerState extends PagedPickerState { focusedDate = _focusableYear(current, focusedDate!); } - SemanticsService.announce( - current.toString(), // TODO: localization - _textDirection, - ); + SemanticsService.announce(current.toString(), textDirection); }); } LocalDate? _focusableYear(LocalDate startYear, LocalDate preferredYear) { - final endYear = startYear.plus(years: yearMonthPickerItems); + final endYear = startYear.plus(years: YearPicker.items); if (startYear <= preferredYear && preferredYear < endYear) { return preferredYear; } - // Start at the 1st and take the first enabled date. for (var newFocus = startYear; newFocus < endYear; newFocus = newFocus.plus(years: 1)) { - if (widget.enabledPredicate(newFocus)) { + if (widget.enabled(newFocus)) { return newFocus; } } @@ -87,13 +86,13 @@ class _PagedYearPickerState extends PagedPickerState { } @override - int delta(LocalDate start, LocalDate end) => ((end.year - start.year) / yearMonthPickerItems).floor(); + int delta(LocalDate start, LocalDate end) => ((end.year - start.year) / YearPicker.items).floor(); @override Map get directionOffset => const { - TraversalDirection.up: Period(years: -yearMonthPickerColumns), + TraversalDirection.up: Period(years: -YearPicker.columns), TraversalDirection.right: Period(years: 1), - TraversalDirection.down: Period(years: yearMonthPickerColumns), + TraversalDirection.down: Period(years: YearPicker.columns), TraversalDirection.left: Period(years: -1), }; } diff --git a/forui/lib/src/widgets/old_calendar/year_month/year/year_picker.dart b/forui/lib/src/widgets/calendar/year/year_picker.dart similarity index 69% rename from forui/lib/src/widgets/old_calendar/year_month/year/year_picker.dart rename to forui/lib/src/widgets/calendar/year/year_picker.dart index 786037591..799f2aed9 100644 --- a/forui/lib/src/widgets/old_calendar/year_month/year/year_picker.dart +++ b/forui/lib/src/widgets/calendar/year/year_picker.dart @@ -1,7 +1,16 @@ -part of '../../calendar.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:forui/src/widgets/calendar/shared/entry.dart'; +import 'package:forui/src/widgets/calendar/year_month_picker.dart'; +import 'package:meta/meta.dart'; +import 'package:sugar/sugar.dart'; @internal class YearPicker extends StatefulWidget { + static const columns = 3; + static const rows = 4; + static const items = columns * rows; + final FCalendarYearMonthPickerStyle style; final LocalDate startYear; final LocalDate start; @@ -23,6 +32,7 @@ class YearPicker extends StatefulWidget { @override State createState() => _YearPickerState(); + @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); @@ -43,10 +53,10 @@ class _YearPickerState extends State { @override void initState() { super.initState(); - _years = List.generate(yearMonthPickerItems, (i) => FocusNode(skipTraversal: true, debugLabel: '$i')); + _years = List.generate(YearPicker.items, (i) => FocusNode(skipTraversal: true, debugLabel: '$i')); final focused = widget.focused; - if (focused == null || focused < widget.startYear || widget.startYear.plus(years: yearMonthPickerItems) <= focused) { + if (focused == null || focused < widget.startYear || widget.startYear.plus(years: YearPicker.items) <= focused) { return; } @@ -56,19 +66,19 @@ class _YearPickerState extends State { @override Widget build(BuildContext context) => GridView( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: yearMonthPickerColumns, + crossAxisCount: YearPicker.columns, childAspectRatio: 1.618, ), children: [ - for (var year = widget.startYear, i = 0; i < yearMonthPickerItems; year = year.plus(years: 1), i++) - yearMonth( - widget.style, - year, - _years[i], - widget.onPress, - (date) => '${date.year}', // TODO: localize - enabled: widget.start <= year && year <= widget.end, + for (var year = widget.startYear, i = 0; i < YearPicker.items; year = year.plus(years: 1), i++) + Entry.yearMonth( + style: widget.style, + date: year, + focusNode: _years[i], current: widget.today.year == year.year, + enabled: widget.start <= year && year <= widget.end, + format: (date) => '${date.year}', // TODO: localization + onPress: widget.onPress, ), ], ); @@ -76,13 +86,10 @@ class _YearPickerState extends State { @override void didUpdateWidget(YearPicker old) { super.didUpdateWidget(old); - assert( - old.startYear == widget.startYear, - 'We assumed that a new YearPicker is created each time we navigate to a years page.', - ); + assert(old.startYear == widget.startYear, 'startYear must noe change.'); final focused = widget.focused; - if (focused == null || focused < widget.startYear || widget.startYear.plus(years: yearMonthPickerItems) <= focused) { + if (focused == null || focused < widget.startYear || widget.startYear.plus(years: YearPicker.items) <= focused) { return; } diff --git a/forui/lib/src/widgets/calendar/year_month_picker.dart b/forui/lib/src/widgets/calendar/year_month_picker.dart new file mode 100644 index 000000000..33e8e8a93 --- /dev/null +++ b/forui/lib/src/widgets/calendar/year_month_picker.dart @@ -0,0 +1,138 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:forui/forui.dart'; +import 'package:forui/src/widgets/calendar/calendar.dart'; +import 'package:forui/src/widgets/calendar/month/paged_month_picker.dart'; +import 'package:forui/src/widgets/calendar/shared/entry.dart'; +import 'package:forui/src/widgets/calendar/year/paged_year_picker.dart'; +import 'package:meta/meta.dart'; +import 'package:sugar/sugar.dart'; + +@internal +class YearMonthPicker extends StatefulWidget { + final FCalendarStyle style; + final LocalDate start; + final LocalDate end; + final LocalDate today; + final ValueChanged onChange; + + const YearMonthPicker({ + required this.style, + required this.start, + required this.end, + required this.today, + required this.onChange, + super.key, + }); + + @override + State createState() => _YearMonthPickerState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('style', style)) + ..add(DiagnosticsProperty('start', start)) + ..add(DiagnosticsProperty('end', end)) + ..add(DiagnosticsProperty('today', today)) + ..add(DiagnosticsProperty('onChange', onChange)); + } +} + +class _YearMonthPickerState extends State { + LocalDate? _date; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + if (_date == null) { + return PagedYearPicker( + style: widget.style, + start: widget.start, + end: widget.end, + today: widget.today, + initial: widget.today.truncate(to: DateUnit.years), + onPress: (year) => setState(() => _date = year), + ); + } else { + return PagedMonthPicker( + style: widget.style, + start: widget.start, + end: widget.end, + today: widget.today, + initial: _date!, + onPress: widget.onChange, + ); + } + } +} + + +/// The year/month picker's style. +final class FCalendarYearMonthPickerStyle with Diagnosticable { + /// The enabled years/months' styles. + final FCalendarEntryStyle enabledStyle; + /// The disabled years/months' styles. + final FCalendarEntryStyle disabledStyle; + + /// Creates a new year/month picker style. + FCalendarYearMonthPickerStyle({required this.enabledStyle, required this.disabledStyle}); + + /// Creates a new year/month picker style that inherits the color scheme and typography. + FCalendarYearMonthPickerStyle.inherit({required FColorScheme colorScheme, required FTypography typography}) + : this( + enabledStyle: FCalendarEntryStyle( + backgroundColor: colorScheme.background, + textStyle: typography.sm.copyWith(color: colorScheme.foreground, fontWeight: FontWeight.w500), + focusedBackgroundColor: colorScheme.secondary, + ), + disabledStyle: FCalendarEntryStyle( + backgroundColor: colorScheme.background, + textStyle: typography.sm + .copyWith(color: colorScheme.mutedForeground.withOpacity(0.5), fontWeight: FontWeight.w500), + ), + ); + + /// Returns a copy of this [FCalendarYearMonthPickerStyle] but with the given fields replaced with the new values. + /// + /// ```dart + /// final style = FCalendarYearMonthPickerStyle( + /// enabledStyle: ..., + /// disabledStyle: ..., + /// ); + /// + /// final copy = style.copyWith(disabledStyle: ...); + /// + /// print(style.enabledStyle == copy.enabledStyle); // true + /// print(style.disabledStyle == copy.disabledStyle); // false + /// ``` + FCalendarYearMonthPickerStyle copyWith({ + FCalendarEntryStyle? enabledStyle, + FCalendarEntryStyle? disabledStyle, + }) => FCalendarYearMonthPickerStyle( + enabledStyle: enabledStyle ?? this.enabledStyle, + disabledStyle: disabledStyle ?? this.disabledStyle, + ); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('enabledStyle', enabledStyle)) + ..add(DiagnosticsProperty('disabledStyle', disabledStyle)); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is FCalendarYearMonthPickerStyle && runtimeType == other.runtimeType && + enabledStyle == other.enabledStyle && disabledStyle == other.disabledStyle; + + @override + int get hashCode => enabledStyle.hashCode ^ disabledStyle.hashCode; +} diff --git a/forui/lib/src/widgets/old_calendar/calendar.dart b/forui/lib/src/widgets/old_calendar/calendar.dart deleted file mode 100644 index cd5439302..000000000 --- a/forui/lib/src/widgets/old_calendar/calendar.dart +++ /dev/null @@ -1,169 +0,0 @@ -import 'dart:collection'; -import 'dart:ffi'; -import 'dart:math'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; -import 'package:forui/forui.dart'; -import 'package:meta/meta.dart'; -import 'package:sugar/sugar.dart' hide Offset; - -part 'day/day.dart'; -part 'day/day_picker.dart'; -part 'day/paged_day_picker.dart'; - -part 'year_month/year_month.dart'; -part 'year_month/year_month_picker.dart'; -part 'year_month/month/month_picker.dart'; -part 'year_month/month/paged_month_picker.dart'; -part 'year_month/year/paged_year_picker.dart'; -part 'year_month/year/year_picker.dart'; - -part 'controls.dart'; -part 'paged_picker.dart'; -part 'toggle.dart'; - -enum FCalendarPickerMode { - day, - yearMonth, -} - -@internal -class Calendar extends StatefulWidget { - final FCalendarStyle style; // TODO: Make this nullable. - final FCalendarPickerMode initialMode; - final LocalDate start; - final LocalDate end; - final LocalDate today; - final LocalDate initialMonth; - final bool Function(LocalDate day) enabledPredicate; - final bool Function(LocalDate day) selectedPredicate; - final ValueChanged? onMonthChange; - final ValueChanged onPress; - final ValueChanged onLongPress; - - const Calendar({ - required this.style, - required this.start, - required this.end, - required this.today, - required this.initialMonth, - required this.enabledPredicate, - required this.selectedPredicate, - this.onMonthChange, - required this.onPress, - required this.onLongPress, - this.initialMode = FCalendarPickerMode.day, - super.key, - }); - - @override - State createState() => _CalendarState(); -} - -class _CalendarState extends State with TickerProviderStateMixin { - late ValueNotifier mode; - late LocalDate month; - late AnimationController _yearMonthPickerController; - - @override - void initState() { - super.initState(); - mode = ValueNotifier(widget.initialMode); - month = widget.initialMonth; - _yearMonthPickerController = AnimationController(vsync: this, duration: const Duration(milliseconds: 200)); - } - - @override - Widget build(BuildContext context) => DecoratedBox( - decoration: widget.style.decoration, - child: Padding( - padding: widget.style.padding, - child: SizedBox( - height: (maxDayPickerTileDimension * maxDayPickerGridRows) + toggleHeight + 5, - width: maxDayPickerTileDimension * DateTime.daysPerWeek, - child: Stack( - alignment: Alignment.topCenter, - children: [ - ValueListenableBuilder( - valueListenable: mode, - builder: (context, value, child) => switch (value) { - FCalendarPickerMode.day => PagedDayPicker( - style: widget.style, - start: widget.start, - end: widget.end, - today: widget.today, - initial: month.truncate(to: DateUnit.months), - enabledPredicate: widget.enabledPredicate, - selectedPredicate: widget.selectedPredicate, - onMonthChange: (month) { - print('called'); - setState(() { - this.month = month.toLocalDate(); - }); - widget.onMonthChange?.call(month); - }, - onPress: widget.onPress, - onLongPress: widget.onLongPress, - ), - FCalendarPickerMode.yearMonth => YearMonthPicker( - style: widget.style, - start: widget.start, - end: widget.end, - today: widget.today, - onMonthChange: (date) => setState(() { - mode.value = FCalendarPickerMode.day; - month = date; - }), - ), - }, - ), - Toggle( - style: widget.style, - month: month, - mode: mode, - yearMonthPickerController: _yearMonthPickerController, - ), - ], - ), - ), - ), - ); -} - -final class FCalendarStyle with Diagnosticable { - final FCalendarHeaderStyle headerStyle; - final FCalendarDayPickerStyle dayPickerStyle; - final FCalendarYearMonthPickerStyle yearMonthPickerStyle; - final BoxDecoration decoration; - final EdgeInsets padding; - final Duration pageAnimationDuration; - - FCalendarStyle({ - required this.headerStyle, - required this.dayPickerStyle, - required this.yearMonthPickerStyle, - required this.decoration, - this.padding = const EdgeInsets.symmetric(horizontal: 12, vertical: 16), - this.pageAnimationDuration = const Duration(milliseconds: 200), - }); - - FCalendarStyle.inherit({ - required FColorScheme colorScheme, - required FTypography typography, - required FStyle style, - }) : this( - headerStyle: FCalendarHeaderStyle.inherit(colorScheme: colorScheme, typography: typography), - dayPickerStyle: FCalendarDayPickerStyle.inherit(colorScheme: colorScheme, typography: typography), - yearMonthPickerStyle: FCalendarYearMonthPickerStyle.inherit(colorScheme: colorScheme, typography: typography), - decoration: BoxDecoration( - borderRadius: style.borderRadius, - border: Border.all( - color: colorScheme.border, - ), - color: colorScheme.background, - ), - ); -} diff --git a/forui/lib/src/widgets/old_calendar/controls.dart b/forui/lib/src/widgets/old_calendar/controls.dart deleted file mode 100644 index 231d45287..000000000 --- a/forui/lib/src/widgets/old_calendar/controls.dart +++ /dev/null @@ -1,143 +0,0 @@ -part of 'calendar.dart'; - -@internal -class Controls extends StatelessWidget { - final FCalendarHeaderStyle style; - final VoidCallback? onPrevious; - final VoidCallback? onNext; - - const Controls({ - required this.style, - required this.onPrevious, - required this.onNext, - super.key, - }); - - @override - Widget build(BuildContext context) { - final buttonStyle = context.theme.buttonStyles.outline; - final effectiveButtonStyle = buttonStyle.copyWith( - enabledBoxDecoration: buttonStyle.enabledBoxDecoration.copyWith(borderRadius: BorderRadius.circular(4)), - disabledBoxDecoration: buttonStyle.disabledBoxDecoration.copyWith(borderRadius: BorderRadius.circular(4)), - ); - - return Padding( - padding: const EdgeInsets.only(bottom: 5), - child: SizedBox( - height: toggleHeight, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only(left: 7), - child: FButton.raw( - // TODO: Replace with FButton.icon. - style: effectiveButtonStyle, - onPress: onPrevious, - child: Padding( - padding: const EdgeInsets.all(7), - child: FAssets.icons.chevronLeft( - height: 17, - colorFilter: ColorFilter.mode( - style.iconColor, - BlendMode.srcIn, - ), - ), - ), - ), - ), - const Expanded(child: SizedBox()), - Padding( - padding: const EdgeInsets.only(right: 7), - child: FButton.raw( - // TODO: Replace with FButton.icon. - style: effectiveButtonStyle, - onPress: onNext, - child: Padding( - padding: const EdgeInsets.all(7), - child: FAssets.icons.chevronRight( - height: 17, - colorFilter: ColorFilter.mode( - style.iconColor, - BlendMode.srcIn, - ), - ), - ), - ), - ), - ], - ), - ), - ); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty('style', style)) - ..add(DiagnosticsProperty('onPrevious', onPrevious)) - ..add(DiagnosticsProperty('onNext', onNext)); - } -} - -/// The calendar header's style. -final class FCalendarHeaderStyle { - /// The header's text style. - final TextStyle headerTextStyle; - - /// The unfocused header icons' color. - final Color iconColor; - - /// Creates a [FCalendarHeaderStyle]. - FCalendarHeaderStyle({ - required this.headerTextStyle, - required this.iconColor, - }); - - /// Creates a [FCalendarHeaderStyle] that inherits its values from the given [colorScheme] and [typography]. - FCalendarHeaderStyle.inherit({required FColorScheme colorScheme, required FTypography typography}) - : this( - headerTextStyle: typography.sm.copyWith( - color: colorScheme.primary, - fontWeight: FontWeight.w600, - ), - iconColor: colorScheme.mutedForeground, - ); - - /// Creates a copy of this but with the given fields replaced with the new values. - /// - /// ```dart - /// final style = FCalendarHeaderStyle( - /// headerTextStyle: ..., - /// iconColor:..., - /// // Other arguments omitted for brevity. - /// ); - /// - /// final copy = style.copyWith( - /// iconColor: ..., - /// ); - /// - /// print(style.headerTextStyle == copy.headerTextStyle); // true - /// print(style.iconColor == copy.iconColor); // false - /// ``` - FCalendarHeaderStyle copyWith({ - TextStyle? headerTextStyle, - Color? iconColor, - }) => - FCalendarHeaderStyle( - headerTextStyle: headerTextStyle ?? this.headerTextStyle, - iconColor: iconColor ?? this.iconColor, - ); - - @override - bool operator ==(Object other) => - identical(this, other) || - other is FCalendarHeaderStyle && - runtimeType == other.runtimeType && - headerTextStyle == other.headerTextStyle && - iconColor == other.iconColor; - - @override - int get hashCode => headerTextStyle.hashCode ^ iconColor.hashCode; -} diff --git a/forui/lib/src/widgets/old_calendar/day/day.dart b/forui/lib/src/widgets/old_calendar/day/day.dart deleted file mode 100644 index e0eb5cada..000000000 --- a/forui/lib/src/widgets/old_calendar/day/day.dart +++ /dev/null @@ -1,338 +0,0 @@ -part of '../calendar.dart'; - -/// Returns the [date]. -@internal -Widget day( - FCalendarDayPickerStyle pickerStyle, - LocalDate date, - FocusNode focusNode, - bool Function(LocalDate day) selectedPredicate, - ValueChanged onPress, - ValueChanged onLongPress, { - required bool enabled, - required bool current, - required bool today, - required bool selected, -}) { - final styles = enabled ? pickerStyle.enabledStyles : pickerStyle.disabledStyles; - final dayStyle = current ? styles.current : styles.enclosing; - final style = selected ? dayStyle.selectedStyle : dayStyle.unselectedStyle; - - if (enabled) { - return EnabledDay( - style: style, - date: date, - focusNode: focusNode, - selectedPredicate: selectedPredicate, - onPress: onPress, - onLongPress: onLongPress, - today: today, - selected: selected, - ); - } else { - return DisabledDay(style: style, date: date, today: today, selectedPredicate: selectedPredicate); - } -} - -@internal -class EnabledDay extends StatefulWidget { - final FCalendarDayStateStyle style; - final LocalDate date; - final FocusNode focusNode; - final bool Function(LocalDate day) selectedPredicate; - final ValueChanged onPress; - final ValueChanged onLongPress; - final bool today; - final bool selected; - - const EnabledDay({ - required this.style, - required this.date, - required this.focusNode, - required this.selectedPredicate, - required this.onPress, - required this.onLongPress, - required this.today, - required this.selected, - super.key, - }); - - @override - State createState() => _EnabledDayState(); - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty('style', style, level: DiagnosticLevel.debug)) - ..add(DiagnosticsProperty('date', date, level: DiagnosticLevel.debug)) - ..add(DiagnosticsProperty('focusNode', focusNode, level: DiagnosticLevel.debug)) - ..add(DiagnosticsProperty('selectedPredicate', selectedPredicate, level: DiagnosticLevel.debug)) - ..add(DiagnosticsProperty('onPress', onPress, level: DiagnosticLevel.debug)) - ..add(DiagnosticsProperty('onLongPress', onLongPress, level: DiagnosticLevel.debug)) - ..add(FlagProperty('today', value: today, ifTrue: 'today', level: DiagnosticLevel.debug)) - ..add(FlagProperty('selected', value: selected, ifTrue: 'selected', level: DiagnosticLevel.debug)); - } -} - -class _EnabledDayState extends State { - bool _focused = false; - bool _hovered = false; - - @override - void initState() { - super.initState(); - widget.focusNode.addListener(_updateFocused); - } - - @override - void didUpdateWidget(EnabledDay old) { - super.didUpdateWidget(old); - old.focusNode.removeListener(_updateFocused); - widget.focusNode.addListener(_updateFocused); - } - - @override - Widget build(BuildContext context) { - var textStyle = _focused || _hovered ? widget.style.focusedTextStyle : widget.style.textStyle; - if (widget.today) { - textStyle = textStyle.copyWith(decoration: TextDecoration.underline); - } - - return Focus( - focusNode: widget.focusNode, - child: MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: (_) => setState(() => _hovered = true), - onExit: (_) => setState(() => _hovered = false), - child: Semantics( - label: '${widget.date}${widget.today ? ', Today' : ''}', // TODO: localization - button: true, - selected: widget.selected, - excludeSemantics: true, - child: GestureDetector( - onTap: () => widget.onPress(widget.date), - onLongPress: () => widget.onLongPress(widget.date), - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.horizontal( - left: widget.selectedPredicate(widget.date.yesterday) ? Radius.zero : const Radius.circular(4), - right: widget.selectedPredicate(widget.date.tomorrow) ? Radius.zero : const Radius.circular(4), - ), - color: _focused || _hovered ? widget.style.focusedBackgroundColor : widget.style.backgroundColor, - ), - child: Center( - child: Text('${widget.date.day}', style: textStyle), // TODO: localization - ), - ), - ), - ), - ), - ); - } - - @override - void dispose() { - widget.focusNode.removeListener(_updateFocused); - super.dispose(); - } - - void _updateFocused() => setState(() => _focused = widget.focusNode.hasFocus); -} - -@internal -class DisabledDay extends StatelessWidget { - final FCalendarDayStateStyle style; - final LocalDate date; - final bool today; - final bool Function(LocalDate day) selectedPredicate; - - const DisabledDay({ - required this.style, - required this.date, - required this.today, - required this.selectedPredicate, - super.key, - }); - - @override - Widget build(BuildContext context) { - var textStyle = style.textStyle; - if (today) { - textStyle = textStyle.copyWith(decoration: TextDecoration.underline); - } - - return ExcludeSemantics( - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.horizontal( - left: selectedPredicate(date.yesterday) ? Radius.zero : const Radius.circular(4), - right: selectedPredicate(date.tomorrow) ? Radius.zero : const Radius.circular(4), - ), - color: style.backgroundColor, - ), - child: Center( - child: Text('${date.day}', style: textStyle), // TODO: localization - ), - ), - ); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty('style', style, level: DiagnosticLevel.debug)) - ..add(DiagnosticsProperty('date', date, level: DiagnosticLevel.debug)) - ..add(DiagnosticsProperty('today', today)) - ..add(DiagnosticsProperty('selectedPredicate', selectedPredicate)); - } -} - -/// A calender day's style. -final class FCalendarDayStyle with Diagnosticable { - /// The selected dates' style. - final FCalendarDayStateStyle selectedStyle; - - /// The unselected dates' style. - final FCalendarDayStateStyle unselectedStyle; - - /// Creates a [FCalendarDayStyle]. - const FCalendarDayStyle({ - required this.unselectedStyle, - required this.selectedStyle, - }); - - /// Returns a copy of this [FCalendarDayStyle] but with the given fields replaced with the new values. - /// - /// ```dart - /// final style = FDayStyle( - /// selectedStyle: ..., - /// unselectedStyle: ..., - /// // Other arguments omitted for brevity - /// ); - /// - /// final copy = style.copyWith( - /// unselectedStyle: ..., - /// ); - /// - /// print(style.selectedStyle == copy.selectedStyle); // true - /// print(style.unselectedStyle == copy.unselectedStyle); // false - /// ``` - FCalendarDayStyle copyWith({ - FCalendarDayStateStyle? selectedStyle, - FCalendarDayStateStyle? unselectedStyle, - }) => - FCalendarDayStyle( - selectedStyle: selectedStyle ?? this.selectedStyle, - unselectedStyle: unselectedStyle ?? this.unselectedStyle, - ); - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty('selectedStyle', selectedStyle)) - ..add(DiagnosticsProperty('unselectedStyle', unselectedStyle)); - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is FCalendarDayStyle && - runtimeType == other.runtimeType && - unselectedStyle == other.unselectedStyle && - selectedStyle == other.selectedStyle; - - @override - int get hashCode => unselectedStyle.hashCode ^ selectedStyle.hashCode; -} - -/// A calendar day state's style. -final class FCalendarDayStateStyle with Diagnosticable { - /// The unfocused day's background color. - final Color backgroundColor; - - /// The unfocused day's text style. - final TextStyle textStyle; - - /// The focused day's background color. Defaults to [backgroundColor]. - final Color focusedBackgroundColor; - - /// The focused day's text style. Defaults to [textStyle]. - final TextStyle focusedTextStyle; - - /// Creates a [FCalendarDayStateStyle]. - FCalendarDayStateStyle({ - required this.backgroundColor, - required this.textStyle, - Color? focusedBackgroundColor, - TextStyle? focusedTextStyle, - }) : focusedBackgroundColor = focusedBackgroundColor ?? backgroundColor, - focusedTextStyle = focusedTextStyle ?? textStyle; - - /// Creates a [FCalendarDayStateStyle] that inherits the given colors. - FCalendarDayStateStyle.inherit({ - required Color backgroundColor, - required TextStyle textStyle, - Color? focusedBackgroundColor, - TextStyle? focusedTextStyle, - }) : this( - backgroundColor: backgroundColor, - textStyle: textStyle, - focusedBackgroundColor: focusedBackgroundColor ?? backgroundColor, - focusedTextStyle: focusedTextStyle ?? textStyle, - ); - - /// Returns a copy of this [FCalendarDayStateStyle] but with the given fields replaced with the new values. - /// - /// ```dart - /// final style = FDayStateStyle( - /// backgroundColor: ..., - /// textStyle: ..., - /// ); - /// - /// final copy = style.copyWith( - /// textStyle: ..., - /// ); - /// - /// print(style.backgroundColor == copy.backgroundColor); // true - /// print(style.textStyle == copy.textStyle); // false - /// ``` - FCalendarDayStateStyle copyWith({ - Color? backgroundColor, - TextStyle? textStyle, - Color? focusedBackgroundColor, - TextStyle? focusedTextStyle, - }) => - FCalendarDayStateStyle( - backgroundColor: backgroundColor ?? this.backgroundColor, - textStyle: textStyle ?? this.textStyle, - focusedBackgroundColor: focusedBackgroundColor ?? this.focusedBackgroundColor, - focusedTextStyle: focusedTextStyle ?? this.focusedTextStyle, - ); - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty('backgroundColor', backgroundColor)) - ..add(DiagnosticsProperty('textStyle', textStyle)) - ..add(DiagnosticsProperty('focusedBackgroundColor', focusedBackgroundColor)) - ..add(DiagnosticsProperty('focusedTextStyle', focusedTextStyle)); - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is FCalendarDayStateStyle && - runtimeType == other.runtimeType && - backgroundColor == other.backgroundColor && - textStyle == other.textStyle && - focusedBackgroundColor == other.focusedBackgroundColor && - focusedTextStyle == other.focusedTextStyle; - - @override - int get hashCode => - backgroundColor.hashCode ^ textStyle.hashCode ^ focusedBackgroundColor.hashCode ^ focusedTextStyle.hashCode; -} diff --git a/forui/lib/src/widgets/old_calendar/toggle.dart b/forui/lib/src/widgets/old_calendar/toggle.dart deleted file mode 100644 index 4295b45af..000000000 --- a/forui/lib/src/widgets/old_calendar/toggle.dart +++ /dev/null @@ -1,82 +0,0 @@ -part of 'calendar.dart'; - -/// The toggle's height. This is a workaround to the pickers being in a stack alongside the toggle. -@internal -const toggleHeight = 31.0; - -class Toggle extends StatefulWidget { - final FCalendarStyle style; - final LocalDate month; - final ValueNotifier mode; - final AnimationController yearMonthPickerController; - - const Toggle({ - super.key, - required this.style, - required this.month, - required this.mode, - required this.yearMonthPickerController, - }); - - @override - State createState() => _ToggleState(); -} - -class _ToggleState extends State with SingleTickerProviderStateMixin { - late AnimationController _controller; - - @override - void initState() { - super.initState(); - _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 100)); - widget.mode.addListener(() { - if (_controller.isCompleted) { - _controller.reverse(); - } else { - widget.yearMonthPickerController.forward(); - _controller.forward(); - } - }); - } - - @override - Widget build(BuildContext context) => - SizedBox( - height: toggleHeight, - child: GestureDetector( - onTap: () { - final value = widget.mode.value; - if (value == FCalendarPickerMode.day) { - widget.mode.value = FCalendarPickerMode.yearMonth; - } else { - widget.mode.value = FCalendarPickerMode.day; - } - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('${widget.month.month} ${widget.month.year}', style: widget.style.headerStyle.headerTextStyle), // TODO: Localization - RotationTransition( - turns: Tween(begin: 0.0, end: 0.25).animate(_controller), - child: Padding( - padding: const EdgeInsets.all(2.0), - child: FAssets.icons.chevronRight( - height: 15, - colorFilter: ColorFilter.mode( - context.theme.colorScheme.primary, - BlendMode.srcIn, - ), - ), - ), - ), - ], - ), - ), - ); - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } -} diff --git a/forui/lib/src/widgets/old_calendar/year_month/year_month.dart b/forui/lib/src/widgets/old_calendar/year_month/year_month.dart deleted file mode 100644 index 9873ed116..000000000 --- a/forui/lib/src/widgets/old_calendar/year_month/year_month.dart +++ /dev/null @@ -1,182 +0,0 @@ -part of '../calendar.dart'; - -/// Returns the current year/month. -@internal -Widget yearMonth( - FCalendarYearMonthPickerStyle pickerStyle, - LocalDate date, - FocusNode focusNode, - ValueChanged onPress, - String Function(LocalDate) localize, { - required bool enabled, - required bool current, -}) { - final style = enabled ? pickerStyle.enabledStyle : pickerStyle.disabledStyle; - if (enabled) { - return EnabledYearMonth( - style: style, - date: date, - focusNode: focusNode, - onPress: onPress, - localize: localize, - current: current, - ); - } else { - return DisabledYearMonth( - style: style, - date: date, - localize: localize, - current: current, - ); - } -} - -@internal -class EnabledYearMonth extends StatefulWidget { - final FCalendarYearMonthPickerStateStyle style; - final LocalDate date; - final FocusNode focusNode; - final bool current; - final ValueChanged onPress; - final String Function(LocalDate) localize; - - const EnabledYearMonth({ - required this.style, - required this.date, - required this.focusNode, - required this.current, - required this.onPress, - required this.localize, - super.key, - }); - - @override - State createState() => _EnabledYearMonthState(); - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty('style', style, level: DiagnosticLevel.debug)) - ..add(DiagnosticsProperty('date', date, level: DiagnosticLevel.debug)) - ..add(DiagnosticsProperty('focusNode', focusNode, level: DiagnosticLevel.debug)) - ..add(FlagProperty('current', value: current, ifTrue: 'current', level: DiagnosticLevel.debug)) - ..add(DiagnosticsProperty('onPress', onPress, level: DiagnosticLevel.debug)) - ..add(DiagnosticsProperty('localize', localize, level: DiagnosticLevel.debug)); - } -} - -class _EnabledYearMonthState extends State { - bool _focused = false; - bool _hovered = false; - - @override - void initState() { - super.initState(); - widget.focusNode.addListener(_updateFocused); - } - - @override - void didUpdateWidget(EnabledYearMonth old) { - super.didUpdateWidget(old); - old.focusNode.removeListener(_updateFocused); - widget.focusNode.addListener(_updateFocused); - } - - @override - Widget build(BuildContext context) { - var textStyle = _focused || _hovered ? widget.style.focusedTextStyle : widget.style.textStyle; - if (widget.current) { - textStyle = textStyle.copyWith(decoration: TextDecoration.underline); - } - - return Focus( - focusNode: widget.focusNode, - child: MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: (_) => setState(() => _hovered = true), - onExit: (_) => setState(() => _hovered = false), - child: Semantics( - label: widget.localize(widget.date), - button: true, - excludeSemantics: true, - child: GestureDetector( - onTap: () => widget.onPress(widget.date), - child: DecoratedBox( - decoration: _focused || _hovered ? widget.style.focusedDecoration : widget.style.decoration, - child: Center( - child: Text(widget.localize(widget.date), style: textStyle), - ), - ), - ), - ), - ), - ); - } - - @override - void dispose() { - widget.focusNode.removeListener(_updateFocused); - super.dispose(); - } - - void _updateFocused() => setState(() => _focused = widget.focusNode.hasFocus); -} - -@internal -class DisabledYearMonth extends StatelessWidget { - final FCalendarYearMonthPickerStateStyle style; - final LocalDate date; - final bool current; - final String Function(LocalDate) localize; - - const DisabledYearMonth({ - required this.style, - required this.date, - required this.current, - required this.localize, - super.key, - }); - - @override - Widget build(BuildContext context) { - var textStyle = style.textStyle; - if (current) { - textStyle = textStyle.copyWith(decoration: TextDecoration.underline); - } - - return ExcludeSemantics( - child: DecoratedBox( - decoration: style.decoration, - child: Center( - child: Text(localize(date), style: textStyle), - ), - ), - ); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty('style', style, level: DiagnosticLevel.debug)) - ..add(DiagnosticsProperty('date', date, level: DiagnosticLevel.debug)) - ..add(DiagnosticsProperty('current', current)) - ..add(DiagnosticsProperty('localize', localize)); - } -} - -final class FCalendarYearMonthPickerStateStyle with Diagnosticable { - final BoxDecoration decoration; - final TextStyle textStyle; - final BoxDecoration focusedDecoration; - final TextStyle focusedTextStyle; - - FCalendarYearMonthPickerStateStyle({ - required this.decoration, - required this.textStyle, - BoxDecoration? focusedDecoration, - TextStyle? focusedTextStyle, - }) : focusedDecoration = focusedDecoration ?? decoration, - focusedTextStyle = focusedTextStyle ?? textStyle; -} diff --git a/forui/lib/src/widgets/old_calendar/year_month/year_month_picker.dart b/forui/lib/src/widgets/old_calendar/year_month/year_month_picker.dart deleted file mode 100644 index d30be3ca4..000000000 --- a/forui/lib/src/widgets/old_calendar/year_month/year_month_picker.dart +++ /dev/null @@ -1,91 +0,0 @@ -part of '../calendar.dart'; - -/// The number of columns in a year & month picker. -@internal -const yearMonthPickerColumns = 3; - -/// The number of rows in a year & month picker. -@internal -const yearMonthPickerRows = 4; - -/// The total number of items in a year & month picker. -@internal -const yearMonthPickerItems = yearMonthPickerColumns * yearMonthPickerRows; - -class YearMonthPicker extends StatefulWidget { - final FCalendarStyle style; - final LocalDate start; - final LocalDate end; - final LocalDate today; - final ValueChanged onMonthChange; - - const YearMonthPicker({ - required this.style, - required this.start, - required this.end, - required this.today, - required this.onMonthChange, - super.key, - }); - - @override - State createState() => _YearMonthPickerState(); -} - -class _YearMonthPickerState extends State { - LocalDate? _date; - - @override - void initState() { - super.initState(); - } - - @override - Widget build(BuildContext context) { - if (_date == null) { - return PagedYearPicker( - onPress: (year) => setState(() { - _date = year; - }), - style: widget.style, - start: widget.start, - end: widget.end, - today: widget.today, - initial: widget.today.truncate(to: DateUnit.years), - ); - } else { - return PagedMonthPicker( - onPress: widget.onMonthChange, - style: widget.style, - start: widget.start, - end: widget.end, - today: widget.today, - initial: _date!, - ); - } - } -} - -final class FCalendarYearMonthPickerStyle with Diagnosticable { - final FCalendarYearMonthPickerStateStyle enabledStyle; - final FCalendarYearMonthPickerStateStyle disabledStyle; - - FCalendarYearMonthPickerStyle({required this.enabledStyle, required this.disabledStyle}); - - FCalendarYearMonthPickerStyle.inherit({required FColorScheme colorScheme, required FTypography typography}) - : this( - enabledStyle: FCalendarYearMonthPickerStateStyle( - decoration: const BoxDecoration(), - textStyle: typography.sm.copyWith(color: colorScheme.foreground, fontWeight: FontWeight.w500), - focusedDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: colorScheme.secondary, - ), - ), - disabledStyle: FCalendarYearMonthPickerStateStyle( - decoration: const BoxDecoration(), - textStyle: typography.sm - .copyWith(color: colorScheme.mutedForeground.withOpacity(0.5), fontWeight: FontWeight.w500), - ), - ); -} diff --git a/forui/pubspec.yaml b/forui/pubspec.yaml index 87d78832d..9ce013bd0 100644 --- a/forui/pubspec.yaml +++ b/forui/pubspec.yaml @@ -21,6 +21,7 @@ dependencies: flutter_svg: ^2.0.10+1 forui_assets: ^0.2.0 google_fonts: ^6.2.0 + intl: any meta: ^1.11.0 nitrogen_flutter_svg: ^0.3.0+1 nitrogen_types: ^0.3.0+1 diff --git a/forui/test/src/foundation/inkwell_test.dart b/forui/test/src/foundation/inkwell_test.dart index 59e045b4c..2dc309a01 100644 --- a/forui/test/src/foundation/inkwell_test.dart +++ b/forui/test/src/foundation/inkwell_test.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart';