Skip to content

Commit 5f96344

Browse files
authored
Add ProgressRing component for FWE (#12674)
Adds a material3 style progress ring, for use in FWE. <img width="271" height="149" alt="Bildschirmfoto 2025-11-12 um 15 37 30" src="https://github.com/user-attachments/assets/b73a96ed-d8a1-46e2-bedf-bf56c6335646" />
1 parent 0654f5e commit 5f96344

File tree

6 files changed

+163
-1
lines changed

6 files changed

+163
-1
lines changed

site/lib/_sass/components/_misc.scss

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,18 @@
7777
}
7878
}
7979
}
80+
81+
.progress-ring {
82+
circle {
83+
fill: none;
84+
stroke-linecap: round;
85+
}
86+
87+
.ring-inactive {
88+
stroke: var(--site-inset-borderColor);
89+
}
90+
91+
.ring-active {
92+
stroke: var(--site-primary-color);
93+
}
94+
}

site/lib/main.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import 'src/components/pages/archive_table.dart';
1919
import 'src/components/pages/devtools_release_notes_index.dart';
2020
import 'src/components/pages/expansion_list.dart';
2121
import 'src/components/pages/learning_resource_index.dart';
22+
import 'src/components/tutorial/progress_ring.dart';
2223
import 'src/components/tutorial/quiz.dart';
2324
import 'src/extensions/registry.dart';
2425
import 'src/layouts/catalog_page_layout.dart';
@@ -98,6 +99,7 @@ List<CustomComponent> get _embeddableComponents => [
9899
const YoutubeEmbed(),
99100
const FileTree(),
100101
const Quiz(),
102+
const ProgressRing(),
101103
CustomComponent(
102104
pattern: RegExp('OSSelector', caseSensitive: false),
103105
builder: (_, _, _) => const OsSelector(),
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright 2025 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:math';
6+
7+
import 'package:jaspr/jaspr.dart';
8+
9+
class InteractiveProgressRing extends StatelessComponent {
10+
const InteractiveProgressRing({required this.progress, required this.size})
11+
: assert(progress >= 0.0 && progress <= 1.0);
12+
13+
final double progress;
14+
final double size;
15+
16+
@override
17+
Component build(BuildContext context) {
18+
// The radius of the ring.
19+
const r = 24;
20+
// The stroke width of the ring.
21+
const strokeWidth = 4;
22+
// The full circumference of the ring.
23+
const full = pi * r * 2;
24+
// Offset to start drawing at 12 o'clock, since default strokes
25+
// start at 3 o'clock.
26+
const quarter = full / 4;
27+
// The absolute gap between active and inactive tracks.
28+
const gap = strokeWidth * 2;
29+
// The relative gap as a factor of the full circumference.
30+
const rGap = gap / full;
31+
32+
// Adjust progress to precicely align with each quarter of the ring
33+
// while accounting for the visual gap (reversing the stroke offset logic)
34+
// and keeping a continuous curve to each limit (0 and 1).
35+
final adjustedProgress =
36+
progress -
37+
switch (progress) {
38+
// (== 0 for prog -> 0; == gap for prog -> 0.25)
39+
< 0.25 => (progress * 4 * rGap),
40+
// (== gap for prog -> 0.75; == 0 for prog -> 1)
41+
> 0.75 => (1 - progress) * 4 * rGap,
42+
_ => rGap,
43+
};
44+
45+
// Absolute lengths of the active and inactive portions of the ring.
46+
final inactiveLength = (1 - adjustedProgress) * full - gap * 2;
47+
final activeLength = adjustedProgress * full;
48+
49+
return svg(
50+
classes: 'progress-ring',
51+
width: size.px,
52+
height: size.px,
53+
viewBox: '0 0 ${r * 2 + strokeWidth} ${r * 2 + strokeWidth}',
54+
[
55+
// For values close to 1, inactiveLength can become <=0 due to gap.
56+
if (inactiveLength > 0)
57+
// Inactive portion, drawn from (progress)° to (0)°
58+
circle(
59+
classes: 'ring-inactive',
60+
cx: '${r + strokeWidth / 2}',
61+
cy: '${r + strokeWidth / 2}',
62+
r: '$r',
63+
strokeWidth: strokeWidth.toString(),
64+
attributes: {
65+
'stroke-dasharray': '$inactiveLength ${full - inactiveLength}',
66+
'stroke-dashoffset': '${quarter - activeLength - gap * 1.5}',
67+
},
68+
[],
69+
),
70+
71+
// Active portion, drawn from 0° to (progress)°
72+
circle(
73+
classes: 'ring-active',
74+
cx: '${r + strokeWidth / 2}',
75+
cy: '${r + strokeWidth / 2}',
76+
r: '$r',
77+
strokeWidth: strokeWidth.toString(),
78+
attributes: {
79+
'stroke-dasharray': '$activeLength ${full - activeLength}',
80+
'stroke-dashoffset': '${quarter - gap / 2}',
81+
},
82+
[],
83+
),
84+
],
85+
);
86+
}
87+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright 2025 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:jaspr/jaspr.dart';
6+
import 'package:jaspr_content/jaspr_content.dart';
7+
8+
import 'client/progress_ring.dart';
9+
10+
class ProgressRing extends CustomComponentBase {
11+
const ProgressRing();
12+
13+
@override
14+
Pattern get pattern => RegExp('ProgressRing', caseSensitive: false);
15+
16+
@override
17+
Component apply(
18+
String name,
19+
Map<String, String> attributes,
20+
Component? child,
21+
) {
22+
final progress = double.tryParse(attributes['progress'] ?? '') ?? 0.0;
23+
assert(
24+
progress >= 0.0 && progress <= 1.0,
25+
'ProgressRing progress must be between 0.0 and 1.0',
26+
);
27+
28+
final small = attributes['small'] != null;
29+
final large = attributes['large'] != null;
30+
31+
return InteractiveProgressRing(
32+
progress: progress,
33+
size: small
34+
? 24
35+
: large
36+
? 48
37+
: 32,
38+
);
39+
}
40+
}

site/lib/src/pages/custom_pages.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ description: This is a test page for experimenting with First Week Experience (F
7878
sitemap: false
7979
---
8080
81+
## Quiz
82+
8183
<Quiz title="Flutter and Dart Basics Quiz">
8284
- question: What is the Effective Dart guideline for the first sentence of a documentation comment?
8385
options:
@@ -108,5 +110,21 @@ sitemap: false
108110
correct: false
109111
explanation: Stack is used for overlapping widgets, not for scrollable lists.
110112
</Quiz>
113+
114+
## Progress Ring
115+
116+
<ProgressRing progress="0.0" />
117+
<ProgressRing progress="0.25" />
118+
<ProgressRing progress="0.5" />
119+
<ProgressRing progress="0.75" />
120+
<ProgressRing progress="1.0" />
121+
122+
---
123+
124+
<ProgressRing progress="0.1" small />
125+
<ProgressRing progress="0.6" small />
126+
<ProgressRing progress="0.3" large />
127+
<ProgressRing progress="0.95" large />
128+
111129
''',
112130
);

site/lib/src/style_hash.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
// dart format off
33

44
/// The generated hash of the `main.css` file.
5-
const generatedStylesHash = 'URzUaI467vwY';
5+
const generatedStylesHash = 'm+c3m5q1zKUq';

0 commit comments

Comments
 (0)