123 lines
4.5 KiB
Dart
123 lines
4.5 KiB
Dart
// 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;
|
|
}
|
|
}
|