125 lines
3.6 KiB
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,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|