Skip to content

Commit

Permalink
Add calendar controllers
Browse files Browse the repository at this point in the history
  • Loading branch information
Pante committed Jul 13, 2024
1 parent c0dbbdf commit 6827db0
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 14 deletions.
4 changes: 2 additions & 2 deletions forui/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ class Testing extends StatelessWidget {
const Testing({super.key});

@override
Widget build(BuildContext context) => FCalendar.raw(
Widget build(BuildContext context) => FCalendar(
start: DateTime(1900, 1, 8),
end: DateTime(2024, 7, 10),
selected: _selected.contains,
controller: FCalendarMultiValueController(),
);
}
31 changes: 19 additions & 12 deletions forui/lib/src/widgets/calendar/calendar.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:forui/forui.dart';
Expand All @@ -10,6 +11,7 @@ import 'package:sugar/sugar.dart';
export 'day/day_picker.dart' show FCalendarDayPickerStyle, FCalendarDayStyle;
export 'shared/entry.dart' show FCalendarEntryStyle;
export 'shared/header.dart' show FCalendarHeaderStyle, FCalendarPickerType;
export 'calendar_controller.dart';
export 'year_month_picker.dart' show FCalendarYearMonthPickerStyle;

/// A calendar.
Expand All @@ -23,6 +25,9 @@ class FCalendar extends StatelessWidget {
/// The style. Defaults to [FThemeData.calendarStyle].
final FCalendarStyle? style;

/// A controller that determines if a date is selected.
final FCalendarController controller;

/// The start date. It is truncated to the nearest date.
///
/// ## Contract:
Expand All @@ -43,9 +48,6 @@ class FCalendar extends StatelessWidget {
/// Defaults to returning true for all dates.
final Predicate<DateTime> enabled;

/// A predicate that determines if a date is selected. It may be called more than once for a single date.
final Predicate<DateTime> selected;

/// A callback for when the displayed month changes.
final ValueChanged<DateTime>? onMonthChange;

Expand All @@ -57,13 +59,13 @@ class FCalendar extends StatelessWidget {
final ValueNotifier<FCalendarPickerType> _type;
final ValueNotifier<LocalDate> _month;

/// Creates a [FCalendar] with custom date selection.
/// Creates a [FCalendar].
///
/// [initialDate] defaults to [today]. It is truncated to the nearest date.
FCalendar.raw({
FCalendar({
required this.controller,
required this.start,
required this.end,
required this.selected,
this.style,
this.enabled = _true,
this.onMonthChange,
Expand Down Expand Up @@ -102,21 +104,28 @@ class FCalendar extends StatelessWidget {
ValueListenableBuilder(
valueListenable: _type,
builder: (context, value, child) => switch (value) {
FCalendarPickerType.day => PagedDayPicker(
FCalendarPickerType.day => ValueListenableBuilder(
valueListenable: controller,
builder: (context, _, __) => PagedDayPicker(
style: style,
start: start.toLocalDate(),
end: end.toLocalDate(),
today: today.toLocalDate(),
initial: _month.value,
enabled: (date) => enabled(date.toNative()),
selected: (date) => selected(date.toNative()),
selected: (date) => controller.contains(date.toNative()),
onMonthChange: (date) {
_month.value = date;
onMonthChange?.call(date.toNative());
},
onPress: (date) => onPress?.call(date.toNative()),
onPress: (date) {
final native = date.toNative();
controller.onPress(native);
onPress?.call(native);
},
onLongPress: (date) => onLongPress?.call(date.toNative()),
),
),
FCalendarPickerType.yearMonth => YearMonthPicker(
style: style,
start: start.toLocalDate(),
Expand All @@ -141,13 +150,11 @@ class FCalendar extends StatelessWidget {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty('style', style))
..add(DiagnosticsProperty('controller', controller))
..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));
Expand Down
105 changes: 105 additions & 0 deletions forui/lib/src/widgets/calendar/calendar_controller.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import 'package:flutter/widgets.dart';
import 'package:forui/forui.dart';
import 'package:sugar/sugar.dart';

/// A controller that controls date selection in a calendar.
abstract class FCalendarController<T> extends ValueNotifier<T> {
/// Creates a [FCalendarController] with the given initial [value].
FCalendarController(super._value);

/// Called when the given [date] in a [FCalendarPickerType.day] picker is pressed.
///
/// [date] is always in UTC timezone and truncated to the nearest date.
void onPress(DateTime date);

/// Returns true if the given [date] is selected.
bool contains(DateTime date);
}

/// A date selection controller that allows only a single date to be selected.
///
/// The selected date is always in UTC timezone and truncated to the nearest date.
final class FCalendarSingleValueController extends FCalendarController<DateTime?> {
/// Creates a [FCalendarSingleValueController] with the given initial [value].
///
/// ## Contract:
/// Throws an [AssertionError] if the given [value] is not in UTC timezone.
FCalendarSingleValueController([super.value]) : assert(value?.isUtc ?? true, 'value must be in UTC timezone');

@override
bool contains(DateTime date) => value?.toLocalDate() == date.toLocalDate();

@override
void onPress(DateTime date) {
if (value?.toLocalDate() == date.toLocalDate()) {
value = null;
} else {
value = date;
}
}
}

/// A date selection controller that allows multiple dates to be selected.
///
/// The selected dates are always in UTC timezone and truncated to the nearest date.
final class FCalendarMultiValueController extends FCalendarController<Set<DateTime>> {
/// Creates a [FCalendarMultiValueController] with the given initial [value].
///
/// ## Contract:
/// Throws an [AssertionError] if the given dates in [value] is not in UTC timezone.
FCalendarMultiValueController([super.value = const {}]) : assert(value.every((d) => d.isUtc), 'dates must be in UTC timezone');

@override
bool contains(DateTime date) => value.contains(date);

@override
void onPress(DateTime date) {
final copy = { ...value };
value = copy..toggle(date);
}
}

/// A date selection controller that allows a single range to be selected.
///
/// Both the start and end dates of the range is inclusive. The selected dates are always in UTC timezone and truncated
/// to the nearest date.
final class FCalendarSingleRangeController extends FCalendarController<(DateTime, DateTime)?> {
/// Creates a [FCalendarSingleRangeController] with the given initial [value].
///
/// ## Contract:
/// Throws an [AssertionError] if the given [value] is not in UTC timezone.
FCalendarSingleRangeController([super.value])
: assert(value == null || (value.$1.isUtc && value.$2.isUtc), 'value must be in UTC timezone');

@override
bool contains(DateTime date) {
if (value case (final first, final last)) {
final current = date.toLocalDate();
return first.toLocalDate() <= current && current <= last.toLocalDate();
}

return false;
}

@override
void onPress(DateTime date) {
if (value == null) {
value = (date, date);
return;
}

final (first, last) = value!;
final pressed = date.toLocalDate();

switch ((first.toLocalDate(), last.toLocalDate())) {
case (final first, final last) when pressed == first || pressed == last:
value = null;

case (final first, final last) when pressed < first:
value = (pressed.toNative(), last.toNative());

case (final first, _):
value = (first.toNative(), pressed.toNative());
}
}
}

0 comments on commit 6827db0

Please sign in to comment.