Skip to content

Commit fa4e5fe

Browse files
committed
Motion entrances from values with replay, rectangular slider thumbs, formulas with braces in variable syntax
- MotionModel.enterFrom and a bindable replayKey. - SliderProperties.thumbShape (circle, roundedRect) with size and corners. - variableSyntaxIdentifierPattern matches a ${= ...} formula whole, object literals included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013oMokUGNW6txknJs7xxTik
1 parent fa03985 commit fa4e5fe

7 files changed

Lines changed: 290 additions & 6 deletions

File tree

‎lib/src/api/mixins.dart‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,22 @@ enum SliderTrackShapeEnum {
562562
}
563563
}
564564

565+
/// Represents the shape of a slider's thumb.
566+
enum SliderThumbShapeEnum {
567+
/// A circle of the slider's thumb radius.
568+
circle,
569+
570+
/// A rectangle of the slider's thumb width and height with rounded
571+
/// corners, like a fader cap.
572+
roundedRect;
573+
574+
/// Display-able string representation of the enum.
575+
String get prettify => switch (this) {
576+
SliderThumbShapeEnum.circle => 'Circle',
577+
SliderThumbShapeEnum.roundedRect => 'Rectangle',
578+
};
579+
}
580+
565581
/// When a slider shows its value indicator. Mirrors Flutter's
566582
/// `ShowValueIndicator`.
567583
enum ShowValueIndicatorC {

‎lib/src/api/models/motion.dart‎

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,23 @@ class MotionModel with Equatable, SerializableMixin {
124124
/// pulse, a blinking cursor.
125125
final MotionLoop loop;
126126

127-
/// Animates from rest to these values when the node first appears, over
128-
/// [durationMs], instead of starting there: a record sliding out of its
129-
/// sleeve when the screen opens.
127+
/// Animates from rest (or from [enterFrom]) to these values when the node
128+
/// first appears, over [durationMs], instead of starting there: a record
129+
/// sliding out of its sleeve when the screen opens.
130130
final bool enter;
131131

132+
/// Where the entrance starts instead of rest: its offset, scale, opacity,
133+
/// rotation and tilts (its other settings are ignored). Setting it plays
134+
/// an entrance even when [enter] is false: a record fading and settling in
135+
/// from `{"scale": 1.15, "opacity": 0}`.
136+
final MotionModel? enterFrom;
137+
138+
/// Plays the entrance again, from [enterFrom] or rest, whenever this
139+
/// changes. Meant to be bound through `motion.replayKey` in
140+
/// [BaseNode.variables] to any value, for example `${playingId}`, so
141+
/// loading another record replays its entrance.
142+
final String? replayKey;
143+
132144
/// Waits this long, in milliseconds, before a transition, entrance or
133145
/// loop starts, so motions can be staggered.
134146
final int delayMs;
@@ -151,6 +163,8 @@ class MotionModel with Equatable, SerializableMixin {
151163
this.spinRampMs = 800,
152164
this.loop = MotionLoop.none,
153165
this.enter = false,
166+
this.enterFrom,
167+
this.replayKey,
154168
this.delayMs = 0,
155169
});
156170

@@ -170,6 +184,10 @@ class MotionModel with Equatable, SerializableMixin {
170184
'spin',
171185
];
172186

187+
/// The name of the binding that replays the entrance when its value
188+
/// changes; see [replayKey].
189+
static const String replayKeyProperty = 'replayKey';
190+
173191
/// The key in [BaseNode.variables] that binds [property].
174192
static String variableKey(String property) => 'motion.$property';
175193

@@ -183,6 +201,24 @@ class MotionModel with Equatable, SerializableMixin {
183201
rotateX != 0 ||
184202
rotateY != 0;
185203

204+
/// Whether the node animates in when it first appears.
205+
bool get hasEntrance => enter || enterFrom != null;
206+
207+
/// Where the entrance starts: these settings with the transform of
208+
/// [enterFrom], or at rest when there is none.
209+
MotionModel get entranceStart => switch (enterFrom) {
210+
null => atRest,
211+
MotionModel from => copyWith(
212+
translateX: from.translateX,
213+
translateY: from.translateY,
214+
scale: from.scale,
215+
opacity: from.opacity,
216+
rotate: from.rotate,
217+
rotateX: from.rotateX,
218+
rotateY: from.rotateY,
219+
),
220+
};
221+
186222
/// The value of [property] in this model.
187223
double valueOf(String property) => switch (property) {
188224
'translateX' => translateX,
@@ -214,6 +250,10 @@ class MotionModel with Equatable, SerializableMixin {
214250
int? spinRampMs,
215251
MotionLoop? loop,
216252
bool? enter,
253+
MotionModel? enterFrom,
254+
bool clearEnterFrom = false,
255+
String? replayKey,
256+
bool clearReplayKey = false,
217257
int? delayMs,
218258
}) => MotionModel(
219259
translateX: translateX ?? this.translateX,
@@ -232,6 +272,8 @@ class MotionModel with Equatable, SerializableMixin {
232272
spinRampMs: spinRampMs ?? this.spinRampMs,
233273
loop: loop ?? this.loop,
234274
enter: enter ?? this.enter,
275+
enterFrom: clearEnterFrom ? null : enterFrom ?? this.enterFrom,
276+
replayKey: clearReplayKey ? null : replayKey ?? this.replayKey,
235277
delayMs: delayMs ?? this.delayMs,
236278
);
237279

@@ -277,6 +319,8 @@ class MotionModel with Equatable, SerializableMixin {
277319
spinRampMs,
278320
loop,
279321
enter,
322+
enterFrom,
323+
replayKey,
280324
delayMs,
281325
];
282326

‎lib/src/api/models/motion.g.dart‎

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎lib/src/api/nodes/slider_node.dart‎

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ const double kSliderDefaultTickMarkRadius = 1;
2222
/// Default radius of slider's thumb.
2323
const double kSliderDefaultThumbRadius = 10;
2424

25+
/// Default width and height of a slider's rectangular thumb.
26+
const double kSliderDefaultThumbSize = 20;
27+
28+
/// Default corner radius of a slider's rectangular thumb.
29+
const double kSliderDefaultThumbCornerRadius = 4;
30+
2531
/// Default radius of slider's overlay.
2632
const double kSliderDefaultOverlayRadius = 24;
2733

@@ -191,9 +197,26 @@ class SliderProperties extends CustomProperties with FieldsHolder {
191197
/// Whether to allow non-whole numbers as min or max values of slider.
192198
bool allowFractionalPoints;
193199

194-
/// Radius of slider's thumb.
200+
/// Radius of slider's thumb when [thumbShape] is
201+
/// [SliderThumbShapeEnum.circle].
195202
double thumbRadius;
196203

204+
/// Shape of slider's thumb: a circle of [thumbRadius], or a rounded
205+
/// rectangle of [thumbWidth] by [thumbHeight] with [thumbCornerRadius]
206+
/// corners.
207+
SliderThumbShapeEnum thumbShape;
208+
209+
/// Width of a rectangular thumb as drawn on screen, across a vertical
210+
/// slider's track and along a horizontal one's.
211+
double thumbWidth;
212+
213+
/// Height of a rectangular thumb as drawn on screen, along a vertical
214+
/// slider's track and across a horizontal one's.
215+
double thumbHeight;
216+
217+
/// Corner radius of a rectangular thumb.
218+
double thumbCornerRadius;
219+
197220
/// Whether to show slider's thumb.
198221
bool showThumb;
199222

@@ -266,6 +289,10 @@ class SliderProperties extends CustomProperties with FieldsHolder {
266289
this.valueIndicatorTextColor = ColorRGBA.white,
267290
this.valueIndicatorFontSize = 14,
268291
this.thumbRadius = kSliderDefaultThumbRadius,
292+
this.thumbShape = SliderThumbShapeEnum.circle,
293+
this.thumbWidth = kSliderDefaultThumbSize,
294+
this.thumbHeight = kSliderDefaultThumbSize,
295+
this.thumbCornerRadius = kSliderDefaultThumbCornerRadius,
269296
this.showThumb = true,
270297
this.trackShape = SliderTrackShapeEnum.roundedRectangle,
271298
this.tickMarkRadius = kSliderDefaultTickMarkRadius,
@@ -304,6 +331,10 @@ class SliderProperties extends CustomProperties with FieldsHolder {
304331
ColorRGBA? valueIndicatorTextColor,
305332
double? valueIndicatorFontSize,
306333
double? thumbRadius,
334+
SliderThumbShapeEnum? thumbShape,
335+
double? thumbWidth,
336+
double? thumbHeight,
337+
double? thumbCornerRadius,
307338
bool? showThumb,
308339
SliderTrackShapeEnum? trackShape,
309340
double? tickMarkRadius,
@@ -343,6 +374,10 @@ class SliderProperties extends CustomProperties with FieldsHolder {
343374
valueIndicatorFontSize:
344375
valueIndicatorFontSize ?? this.valueIndicatorFontSize,
345376
thumbRadius: thumbRadius ?? this.thumbRadius,
377+
thumbShape: thumbShape ?? this.thumbShape,
378+
thumbWidth: thumbWidth ?? this.thumbWidth,
379+
thumbHeight: thumbHeight ?? this.thumbHeight,
380+
thumbCornerRadius: thumbCornerRadius ?? this.thumbCornerRadius,
346381
showThumb: showThumb ?? this.showThumb,
347382
trackShape: trackShape ?? this.trackShape,
348383
tickMarkRadius: tickMarkRadius ?? this.tickMarkRadius,
@@ -588,6 +623,32 @@ class SliderProperties extends CustomProperties with FieldsHolder {
588623
() => thumbStrokeWidth,
589624
(double value) => thumbStrokeWidth = value,
590625
),
626+
'thumbShape': EnumFieldAccess<SliderThumbShapeEnum>(
627+
'Thumb Shape',
628+
'Shape of slider\'s thumb: a circle or a rounded rectangle.',
629+
() => thumbShape,
630+
(SliderThumbShapeEnum value) => thumbShape = value,
631+
defaultValue: () => SliderThumbShapeEnum.circle,
632+
options: () => SliderThumbShapeEnum.values,
633+
),
634+
'thumbWidth': NumFieldAccess<double>(
635+
'Thumb Width',
636+
'Width of slider\'s rectangular thumb.',
637+
() => thumbWidth,
638+
(double value) => thumbWidth = value,
639+
),
640+
'thumbHeight': NumFieldAccess<double>(
641+
'Thumb Height',
642+
'Height of slider\'s rectangular thumb.',
643+
() => thumbHeight,
644+
(double value) => thumbHeight = value,
645+
),
646+
'thumbCornerRadius': NumFieldAccess<double>(
647+
'Thumb Corner Radius',
648+
'Corner radius of slider\'s rectangular thumb.',
649+
() => thumbCornerRadius,
650+
(double value) => thumbCornerRadius = value,
651+
),
591652
};
592653

593654
@override
@@ -611,6 +672,10 @@ class SliderProperties extends CustomProperties with FieldsHolder {
611672
valueIndicatorTextColor,
612673
valueIndicatorFontSize,
613674
thumbRadius,
675+
thumbShape,
676+
thumbWidth,
677+
thumbHeight,
678+
thumbCornerRadius,
614679
showThumb,
615680
trackShape,
616681
tickMarkRadius,

‎lib/src/api/nodes/slider_node.g.dart‎

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎lib/src/api/regexes.dart‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,24 @@ const String variablePathComposingPattern =
128128
/// Regex for [variablePathComposingPattern].
129129
final RegExp variablePathComposingRegex = RegExp(variablePathComposingPattern);
130130

131+
const String _formulaAtom = r'''[^{}'"]|'[^']*'|"[^"]*"''';
132+
const String _formulaBody0 = '(?:$_formulaAtom)*';
133+
const String _formulaBody1 = '(?:$_formulaAtom|\\{$_formulaBody0\\})*';
134+
const String _formulaBody2 = '(?:$_formulaAtom|\\{$_formulaBody1\\})*';
135+
136+
/// The expression of a `${= ...}` formula: text and quoted strings, with
137+
/// braces only in balanced pairs nested up to three deep, such as the object
138+
/// literal in `${= merge(r, {plays: r.plays + 1})}`. Braces inside quoted
139+
/// strings don't count.
140+
const String formulaExpressionPattern =
141+
'(?:$_formulaAtom|\\{$_formulaBody2\\})*';
142+
131143
/// A regex that matches any variable path identifier in a string that is
132-
/// wrapped in ${} curly braces.
133-
const String variableSyntaxIdentifierPattern = r'(?<!\\)\$\{(?<value>[^}]+)\}';
144+
/// wrapped in ${} curly braces. A formula (`${= ...}`) matches whole, braces
145+
/// of its object literals included, so `${= merge(r, {a: 1})}` is one match
146+
/// rather than ending at the first `}`.
147+
const String variableSyntaxIdentifierPattern =
148+
'(?<!\\\\)\\\$\\{(?<value>=$formulaExpressionPattern|[^}]+)\\}';
134149

135150
/// A regex that can match empty ${} syntax with partial match for curly braces.
136151
const String emptyVariableSyntaxIdentifierPattern = r'^(?<!\\)\${?}?$';

0 commit comments

Comments
 (0)