focus-flow/apps/focus_flow_flutter/lib/widgets/status_circle_button.dart

125 lines
3.6 KiB
Dart

// SPDX-FileCopyrightText: 2026 FocusFlow contributors
// SPDX-License-Identifier: AGPL-3.0-only
/// Shared status-circle affordance for timeline and modal task controls.
library;
import 'package:flutter/material.dart';
/// Circular completion control with shared hover feedback.
class StatusCircleButton extends StatefulWidget {
/// Creates a status circle button.
const StatusCircleButton({
required this.color,
required this.checkColor,
required this.isCompleted,
required this.size,
required this.borderWidth,
required this.iconSize,
required this.semanticLabel,
this.onPressed,
this.circleKey,
this.hoverBlurRadius = 10,
super.key,
});
/// Key assigned to the animated circle surface.
final Key? circleKey;
/// Accent color used for the border, fill, and hover glow.
final Color color;
/// Color used for the completed check icon.
final Color checkColor;
/// Whether the circle is in its completed state.
final bool isCompleted;
/// Square size of the rendered circle.
final double size;
/// Border width for the circle outline.
final double borderWidth;
/// Size of the completed check icon.
final double iconSize;
/// Semantic label used when the control is interactive.
final String semanticLabel;
/// Callback invoked when the circle is tapped.
final VoidCallback? onPressed;
/// Blur radius used for the hover shadow.
final double hoverBlurRadius;
/// Creates the mutable state object used by this stateful widget.
/// Flutter calls this once for each mounted widget instance before lifecycle callbacks and builds begin.
@override
State<StatusCircleButton> createState() => _StatusCircleButtonState();
}
/// Mutable state for [StatusCircleButton].
class _StatusCircleButtonState extends State<StatusCircleButton> {
/// Whether the pointer is hovering this status circle.
bool _isHovered = false;
/// 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) {
final ring = AnimatedContainer(
key: widget.circleKey,
width: widget.size,
height: widget.size,
duration: const Duration(milliseconds: 120),
curve: Curves.easeOut,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: widget.isCompleted
? widget.color
: _isHovered
? widget.color.withValues(alpha: 0.12)
: Colors.transparent,
border: Border.all(color: widget.color, width: widget.borderWidth),
boxShadow: _isHovered && widget.onPressed != null
? [
BoxShadow(
color: widget.color.withValues(alpha: 0.38),
blurRadius: widget.hoverBlurRadius,
spreadRadius: 1,
),
]
: const [],
),
child: widget.isCompleted
? Icon(Icons.check, color: widget.checkColor, size: widget.iconSize)
: null,
);
if (widget.onPressed == null) {
return ring;
}
return Semantics(
button: true,
label: widget.semanticLabel,
child: MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (_) {
setState(() {
_isHovered = true;
});
},
onExit: (_) {
setState(() {
_isHovered = false;
});
},
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: widget.onPressed,
child: ring,
),
),
);
}
}