focus-flow/apps/focus_flow_flutter/lib/widgets/timeline/timeline_geometry.dart

117 lines
3.2 KiB
Dart

/// Renders Timeline Geometry pieces for the FocusFlow Flutter timeline.
library;
/// Converts timeline clock values into vertical pixel positions.
class TimelineGeometry {
/// Creates geometry for a visible time range.
const TimelineGeometry({
required this.startMinutes,
required this.endMinutes,
required this.pixelsPerMinute,
this.minCardHeight = 0,
}) : assert(startMinutes >= 0),
assert(endMinutes <= 24 * 60),
assert(endMinutes > startMinutes),
assert(minCardHeight >= 0);
/// First visible minute from midnight.
final int startMinutes;
/// Last visible minute from midnight.
final int endMinutes;
/// Vertical scale used to convert minutes into pixels.
final double pixelsPerMinute;
/// Minimum rendered height for a timeline card.
///
/// Defaults to zero so scheduled cards match their exact duration on the
/// timeline.
final double minCardHeight;
/// Total vertical height of the visible timeline.
double get totalHeight => (endMinutes - startMinutes) * pixelsPerMinute;
/// Returns the vertical offset for [minutes] since midnight.
double yForMinutesSinceMidnight(int minutes) {
return (minutes - startMinutes) * pixelsPerMinute;
}
/// Returns the rendered height for a duration in minutes.
double heightForDuration(int minutes) {
final height = minutes * pixelsPerMinute;
return height < minCardHeight ? minCardHeight : height;
}
/// Generates timeline ticks at [stepMinutes] intervals.
List<TimelineTick> ticks({int stepMinutes = 15}) {
return [
for (
var minute = startMinutes;
minute <= endMinutes;
minute += stepMinutes
)
TimelineTick(
minutesSinceMidnight: minute,
y: yForMinutesSinceMidnight(minute),
label: _labelFor(minute),
major: minute % 60 == 0,
),
];
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
other is TimelineGeometry &&
other.startMinutes == startMinutes &&
other.endMinutes == endMinutes &&
other.pixelsPerMinute == pixelsPerMinute &&
other.minCardHeight == minCardHeight;
}
@override
int get hashCode {
return Object.hash(
startMinutes,
endMinutes,
pixelsPerMinute,
minCardHeight,
);
}
static String _labelFor(int minutes) {
final hour24 = (minutes ~/ 60) % 24;
final minute = minutes % 60;
final period = hour24 >= 12 ? 'PM' : 'AM';
final rawHour = hour24 % 12;
final hour = rawHour == 0 ? 12 : rawHour;
if (minute == 0) {
return '$hour:00 $period';
}
return '$hour:${minute.toString().padLeft(2, '0')}';
}
}
/// A labeled tick on the compact timeline axis.
class TimelineTick {
/// Creates a timeline tick.
const TimelineTick({
required this.minutesSinceMidnight,
required this.y,
required this.label,
required this.major,
});
/// Minute from midnight represented by this tick.
final int minutesSinceMidnight;
/// Vertical pixel offset for this tick.
final double y;
/// Display label for this tick.
final String label;
/// Whether this tick represents a major interval.
final bool major;
}