// SPDX-FileCopyrightText: 2026 FocusFlow contributors // SPDX-License-Identifier: AGPL-3.0-only /// 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 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, ), ]; } /// Performs the `operator ==` behavior for this scheduler component. /// The method provides a named entry point for this operation so callers do not need to know the lower-level state or persistence steps involved. @override bool operator ==(Object other) { return identical(this, other) || other is TimelineGeometry && other.startMinutes == startMinutes && other.endMinutes == endMinutes && other.pixelsPerMinute == pixelsPerMinute && other.minCardHeight == minCardHeight; } /// Returns the derived `hashCode` value for this object. /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. @override int get hashCode { return Object.hash( startMinutes, endMinutes, pixelsPerMinute, minCardHeight, ); } /// Runs the `_labelFor` helper used inside this library. /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read. 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; }