// SPDX-FileCopyrightText: 2026 FocusFlow contributors // SPDX-License-Identifier: AGPL-3.0-only /// Renders Difficulty Bars pieces for the FocusFlow Flutter timeline. library; import 'package:flutter/material.dart'; import 'package:scheduler_core/scheduler_core.dart'; import '../../theme/focus_flow_tokens.dart'; /// Five-bar effort indicator for a timeline task. class DifficultyBars extends StatelessWidget { /// Creates difficulty bars for a scheduler difficulty token. const DifficultyBars({ required this.difficulty, this.color = FocusFlowTokens.restPurple, this.size = const Size(54, 34), super.key, }); /// Difficulty token that determines how many bars are filled. final TimelineDifficultyIconToken difficulty; /// Color used for filled bars. final Color color; /// Fixed paint size for the indicator. final Size size; /// Converts a timeline difficulty token to a filled bar count. static int fillCount(TimelineDifficultyIconToken difficulty) { return switch (difficulty) { TimelineDifficultyIconToken.notSet => 0, TimelineDifficultyIconToken.veryEasy => 1, TimelineDifficultyIconToken.easy => 2, TimelineDifficultyIconToken.medium => 3, TimelineDifficultyIconToken.hard => 4, TimelineDifficultyIconToken.veryHard => 5, }; } /// Converts a domain difficulty level to a filled bar count. static int fillCountForLevel(DifficultyLevel difficulty) { return switch (difficulty) { DifficultyLevel.notSet => 0, DifficultyLevel.veryEasy => 1, DifficultyLevel.easy => 2, DifficultyLevel.medium => 3, DifficultyLevel.hard => 4, DifficultyLevel.veryHard => 5, }; } /// Builds the widget subtree for this component. /// The method translates the current widget configuration and state into Flutter UI without mutating scheduler domain data. @override Widget build(BuildContext context) { return CustomPaint( size: size, painter: _DifficultyBarsPainter( filledCount: fillCount(difficulty), color: color, ), ); } } /// Private implementation type for `_DifficultyBarsPainter` in this library. /// It keeps supporting state and behavior scoped to the owning file so the public API stays focused on the scheduler concepts that callers use directly. class _DifficultyBarsPainter extends CustomPainter { /// Creates a `_DifficultyBarsPainter` instance with the values required by its invariants. /// Callers should pass already-normalized scheduler data because validation and defaulting belong at the model or service boundary, not in arbitrary UI code. const _DifficultyBarsPainter({ required this.filledCount, required this.color, }); /// Stores the `filledCount` value for this object. /// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior. final int filledCount; /// Stores the `color` value for this object. /// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior. final Color color; /// Performs the `paint` 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 void paint(Canvas canvas, Size size) { final gap = size.width * 0.08; final barWidth = (size.width - gap * 4) / 5; final strokePaint = Paint() ..color = FocusFlowTokens.textFaint ..style = PaintingStyle.stroke ..strokeWidth = 1.3; final fillPaint = Paint() ..color = color ..style = PaintingStyle.fill; for (var index = 0; index < 5; index += 1) { final left = index * (barWidth + gap); final height = size.height * (0.34 + index * 0.13); final top = size.height - height; final rect = RRect.fromRectAndRadius( Rect.fromLTWH(left, top, barWidth, height), Radius.circular(barWidth / 2), ); if (index < filledCount) { canvas.drawRRect(rect, fillPaint); } else { canvas.drawRRect(rect, strokePaint); } } } /// Performs the `shouldRepaint` 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 shouldRepaint(covariant _DifficultyBarsPainter oldDelegate) { return oldDelegate.filledCount != filledCount || oldDelegate.color != color; } }