diff --git a/CHANGELOG.md b/CHANGELOG.md index ccd8e93..42bd43f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,41 @@ ## Unreleased +### Motion + +- `MotionModel` on every node (`BaseNode.motion`): offset, scale, opacity, + rotation, 3D tilt with perspective and origin, an animated transition + (duration and curve), continuous spin, `loop` (restart or back and forth), + `enter`, `enterFrom`, `delayMs` and a bindable `replayKey`. Values bind as + `motion.` variables. + +### Triggers + +- `timer` (`TriggerModel.intervalMs`) and `keyPress` (`TriggerModel.key`) on + canvases; `swipeLeft/Right/Up/Down` and `doubleTap` on frames, rectangles, + buttons, icons and text, beside `cursorEnter` and `cursorExit`. Trigger + JSON naming `hoverEnter` or `hoverExit` reads as `cursorEnter` and + `cursorExit` (`TriggerType.aliases`). + +### Lists and sliders + +- `ListViewProperties.stacked` (items drawn on top of each other) and `wrap` + with `wrapSpacing`, `wrapRunSpacing` and `wrapAlignment`. +- Sliders: `orientation` (vertical), `inverted`, thumb stroke, and a + `roundedRect` thumb shape with width, height and corners. +- Row/column JSON with `itemSpacing` is read as `spacing`. + +### Variables and JSON + +- JSON with a separate `formula` for a computed variable is read into its + value. +- Bindable `effectsVisible`, `strokeWeight`, `cornerRadius`, `fontWeight` + and a slider's `min`/`max` as property variables. +- `variableSyntaxIdentifierPattern` matches a `${= ...}` formula whole, + object literals included. +- `EdgeInsetsModel.fromJson` reads `{left, top, right, bottom}` and + `{horizontal, vertical}`; placeholders and lists read JSON without their + editor-only keys. + ### New - `SignatureNode` (`signature`), a draw-to-sign pad. Its value is the list diff --git a/lib/src/api/mixins.dart b/lib/src/api/mixins.dart index 7c00015..e448d45 100644 --- a/lib/src/api/mixins.dart +++ b/lib/src/api/mixins.dart @@ -562,6 +562,22 @@ enum SliderTrackShapeEnum { } } +/// Represents the shape of a slider's thumb. +enum SliderThumbShapeEnum { + /// A circle of the slider's thumb radius. + circle, + + /// A rectangle of the slider's thumb width and height with rounded + /// corners, like a fader cap. + roundedRect; + + /// Display-able string representation of the enum. + String get prettify => switch (this) { + SliderThumbShapeEnum.circle => 'Circle', + SliderThumbShapeEnum.roundedRect => 'Rectangle', + }; +} + /// When a slider shows its value indicator. Mirrors Flutter's /// `ShowValueIndicator`. enum ShowValueIndicatorC { @@ -1045,8 +1061,9 @@ mixin PropertyVariableMixin on BaseNode { /// Properties of the node that can be used in actions or can be changed by /// an action. /// - /// Style properties (fill, stroke, opacity, width, height; a text's fill is - /// its color) can be set by + /// Style properties (fill, stroke, opacity, width, height, effectsVisible, + /// strokeWeight, cornerRadius; a text's fill is its color, and its + /// fontWeight a number from 100 to 900) can be set by /// conditions, bound to variables or formulas, or changed by a set value /// action, on any node that has them. late final List propertyVariables = [ @@ -1056,13 +1073,30 @@ mixin PropertyVariableMixin on BaseNode { PaintValue(name: 'fill', value: geometry.fills.firstOrNull), PaintValue(name: 'stroke', value: geometry.strokes.firstOrNull), ], - if (this case final TextNode text) + if (this case final TextNode text) ...[ PaintValue( name: 'fill', value: text.textMixedProps.firstOrNull?.fills.firstOrNull, ), - if (this case final BlendMixin blend) + IntValue( + name: 'fontWeight', + value: + ((text.textMixedProps.firstOrNull?.fontName.weight?.index ?? 3) + + 1) * + 100, + ), + ], + if (this case final BlendMixin blend) ...[ DoubleValue(name: 'opacity', value: blend.opacity), + BoolValue( + name: 'effectsVisible', + value: blend.effects.every((effect) => effect.visible), + ), + ], + if (this case final GeometryMixin geometry) + DoubleValue(name: 'strokeWeight', value: geometry.strokeWeight), + if (this case final CornerMixin corner) + DoubleValue(name: 'cornerRadius', value: corner.cornerRadius.tl.x), DoubleValue(name: 'width', value: basicBoxLocal.width), DoubleValue(name: 'height', value: basicBoxLocal.height), ]; diff --git a/lib/src/api/models/edge_insets.dart b/lib/src/api/models/edge_insets.dart index ebb5dc1..a920d7e 100644 --- a/lib/src/api/models/edge_insets.dart +++ b/lib/src/api/models/edge_insets.dart @@ -188,8 +188,24 @@ class EdgeInsetsModel with Equatable, DynamicSerializableMixin { @override List get props => [l, t, r, b]; + static const Set _longSideKeys = { + 'left', + 'top', + 'right', + 'bottom', + 'horizontal', + 'vertical', + }; + + static double _side(Map json, String side, String axis) => + ((json[side] ?? json[axis] ?? 0) as num).toDouble(); + /// Factory constructor for creating a new [EdgeInsetsModel] instance /// from JSON data. + /// + /// Reads the stored forms (a number for all sides, `[horizontal, + /// vertical]`, `[l, t, r, b]`, `{l, t, r, b}`) and the spelled-out + /// `{left, top, right, bottom}` / `{horizontal, vertical}`. factory EdgeInsetsModel.fromJson(dynamic json) { return switch (json) { num all => EdgeInsetsModel.all(all.toDouble()), @@ -203,6 +219,14 @@ class EdgeInsetsModel with Equatable, DynamicSerializableMixin { right: right.toDouble(), bottom: bottom.toDouble(), ), + // Spelled-out sides, as Flutter names them; missing sides are 0. + Map sides when sides.keys.any(_longSideKeys.contains) => + EdgeInsetsModel.only( + left: _side(sides, 'left', 'horizontal'), + top: _side(sides, 'top', 'vertical'), + right: _side(sides, 'right', 'horizontal'), + bottom: _side(sides, 'bottom', 'vertical'), + ), _ => _$EdgeInsetsModelFromJson(json), }; } diff --git a/lib/src/api/models/models.dart b/lib/src/api/models/models.dart index 30e1607..51cd044 100644 --- a/lib/src/api/models/models.dart +++ b/lib/src/api/models/models.dart @@ -52,6 +52,7 @@ export 'input_border.dart'; export 'input_decoration.dart'; export 'letter_spacing.dart'; export 'line_height.dart'; +export 'motion.dart'; export 'list_tile_control_affinity.dart'; export 'loop_animation.dart'; export 'navigation_bar_item.dart'; diff --git a/lib/src/api/models/motion.dart b/lib/src/api/models/motion.dart new file mode 100644 index 0000000..e0fc384 --- /dev/null +++ b/lib/src/api/models/motion.dart @@ -0,0 +1,332 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import '../../../codelessly_api.dart'; + +part 'motion.g.dart'; + +/// The easing a [MotionModel] transition follows. +enum MotionCurve { + /// Constant speed. + linear, + + /// Starts slow, ends slow. + easeInOut, + + /// Starts fast, ends slow. + easeOut, + + /// Starts slow, ends fast. + easeIn, + + /// Overshoots a little and settles, like a spring. + easeOutBack, + + /// A fast cubic deceleration. + easeOutCubic; + + /// Displayable name. + String get prettify => switch (this) { + MotionCurve.linear => 'Linear', + MotionCurve.easeInOut => 'Ease in out', + MotionCurve.easeOut => 'Ease out', + MotionCurve.easeIn => 'Ease in', + MotionCurve.easeOutBack => 'Overshoot', + MotionCurve.easeOutCubic => 'Decelerate', + }; +} + +/// Whether a [MotionModel] repeats on its own. +enum MotionLoop { + /// Moves only when its values change. + none, + + /// From rest to its values, then jumps back to rest and starts again. + restart, + + /// From rest to its values and back, over and over (a pulse). + reverse; + + /// Displayable name. + String get prettify => switch (this) { + MotionLoop.none => 'No loop', + MotionLoop.restart => 'Loop', + MotionLoop.reverse => 'Back and forth', + }; +} + +/// How a node moves: a transform drawn on top of its layout (it never changes +/// the space the node takes up), an animated transition whenever a value +/// changes, and an optional continuous spin. +/// +/// Every value can be bound to a variable or formula through +/// [BaseNode.variables] using the property names in [MotionModel.properties] +/// (for example `variables['motion.rotateX'] = '${= front - index}'`), so a +/// motion can follow app state: a sleeve tips forward when it is flipped, a +/// tonearm swings in as a side plays, a platter spins while it is playing. +@JsonSerializable() +class MotionModel with Equatable, SerializableMixin { + /// Horizontal offset in logical pixels. + final double translateX; + + /// Vertical offset in logical pixels. + final double translateY; + + /// Uniform scale; 1 is the node's own size. + final double scale; + + /// Opacity from 0 (invisible) to 1, animated like the other values, on + /// top of the node's own opacity. + final double opacity; + + /// Rotation in the plane, in degrees. Unlike [BaseNode.rotationDegrees] + /// this doesn't change the node's layout box, accepts fractions and can be + /// animated. + final double rotate; + + /// Tilt around the horizontal axis, in degrees. Positive tips the top + /// toward the viewer. + final double rotateX; + + /// Tilt around the vertical axis, in degrees. Positive turns the right + /// edge toward the viewer. + final double rotateY; + + /// Depth of the 3D tilt: the larger, the stronger the perspective. + /// 0 draws tilts flat (orthographic). + final double perspective; + + /// The point everything scales, rotates and tilts around, in the node's + /// box: (-1, -1) is the top left, (0, 0) the centre, (1, 1) the bottom + /// right. + final double originX; + + /// See [originX]. + final double originY; + + /// Duration of the animation when any value changes, in milliseconds. + /// 0 jumps straight to the new values. + final int durationMs; + + /// Easing of the transition. + final MotionCurve curve; + + /// Continuous rotation in turns per second (0.555 is 33⅓ rpm). 0 stops. + /// Changes in speed ease in and out over [spinRampMs], like a motor + /// coming up to speed. + final double spin; + + /// How long the spin takes to reach a new speed, in milliseconds. + final int spinRampMs; + + /// Repeats the motion on its own, between the node at rest (no + /// transform) and these values, each way over [durationMs]: a skeleton's + /// pulse, a blinking cursor. + final MotionLoop loop; + + /// Animates from rest (or from [enterFrom]) to these values when the node + /// first appears, over [durationMs], instead of starting there: a record + /// sliding out of its sleeve when the screen opens. + final bool enter; + + /// Where the entrance starts instead of rest: its offset, scale, opacity, + /// rotation and tilts (its other settings are ignored). Setting it plays + /// an entrance even when [enter] is false: a record fading and settling in + /// from `{"scale": 1.15, "opacity": 0}`. + final MotionModel? enterFrom; + + /// Plays the entrance again, from [enterFrom] or rest, whenever this + /// changes. Meant to be bound through `motion.replayKey` in + /// [BaseNode.variables] to any value, for example `${playingId}`, so + /// loading another record replays its entrance. + final String? replayKey; + + /// Waits this long, in milliseconds, before a transition, entrance or + /// loop starts, so motions can be staggered. + final int delayMs; + + /// Creates a [MotionModel]. + const MotionModel({ + this.translateX = 0, + this.translateY = 0, + this.scale = 1, + this.opacity = 1, + this.rotate = 0, + this.rotateX = 0, + this.rotateY = 0, + this.perspective = 1, + this.originX = 0, + this.originY = 0, + this.durationMs = 0, + this.curve = MotionCurve.easeInOut, + this.spin = 0, + this.spinRampMs = 800, + this.loop = MotionLoop.none, + this.enter = false, + this.enterFrom, + this.replayKey, + this.delayMs = 0, + }); + + /// No motion at all. + static const MotionModel none = MotionModel(); + + /// The names of the values that can be bound to variables, as + /// `motion.` keys of [BaseNode.variables]. + static const List properties = [ + 'translateX', + 'translateY', + 'scale', + 'opacity', + 'rotate', + 'rotateX', + 'rotateY', + 'spin', + ]; + + /// The name of the binding that replays the entrance when its value + /// changes; see [replayKey]. + static const String replayKeyProperty = 'replayKey'; + + /// The key in [BaseNode.variables] that binds [property]. + static String variableKey(String property) => 'motion.$property'; + + /// Whether this changes how the node is drawn when nothing is bound. + bool get hasTransform => + translateX != 0 || + translateY != 0 || + scale != 1 || + opacity != 1 || + rotate != 0 || + rotateX != 0 || + rotateY != 0; + + /// Whether the node animates in when it first appears. + bool get hasEntrance => enter || enterFrom != null; + + /// Where the entrance starts: these settings with the transform of + /// [enterFrom], or at rest when there is none. + MotionModel get entranceStart => switch (enterFrom) { + null => atRest, + MotionModel from => copyWith( + translateX: from.translateX, + translateY: from.translateY, + scale: from.scale, + opacity: from.opacity, + rotate: from.rotate, + rotateX: from.rotateX, + rotateY: from.rotateY, + ), + }; + + /// The value of [property] in this model. + double valueOf(String property) => switch (property) { + 'translateX' => translateX, + 'translateY' => translateY, + 'scale' => scale, + 'opacity' => opacity, + 'rotate' => rotate, + 'rotateX' => rotateX, + 'rotateY' => rotateY, + 'spin' => spin, + _ => throw ArgumentError.value(property, 'property'), + }; + + /// Returns a copy with the given values replaced. + MotionModel copyWith({ + double? translateX, + double? translateY, + double? scale, + double? opacity, + double? rotate, + double? rotateX, + double? rotateY, + double? perspective, + double? originX, + double? originY, + int? durationMs, + MotionCurve? curve, + double? spin, + int? spinRampMs, + MotionLoop? loop, + bool? enter, + MotionModel? enterFrom, + bool clearEnterFrom = false, + String? replayKey, + bool clearReplayKey = false, + int? delayMs, + }) => MotionModel( + translateX: translateX ?? this.translateX, + translateY: translateY ?? this.translateY, + scale: scale ?? this.scale, + opacity: opacity ?? this.opacity, + rotate: rotate ?? this.rotate, + rotateX: rotateX ?? this.rotateX, + rotateY: rotateY ?? this.rotateY, + perspective: perspective ?? this.perspective, + originX: originX ?? this.originX, + originY: originY ?? this.originY, + durationMs: durationMs ?? this.durationMs, + curve: curve ?? this.curve, + spin: spin ?? this.spin, + spinRampMs: spinRampMs ?? this.spinRampMs, + loop: loop ?? this.loop, + enter: enter ?? this.enter, + enterFrom: clearEnterFrom ? null : enterFrom ?? this.enterFrom, + replayKey: clearReplayKey ? null : replayKey ?? this.replayKey, + delayMs: delayMs ?? this.delayMs, + ); + + /// These settings with every value at rest: no transform at all. + MotionModel get atRest => copyWith( + translateX: 0, + translateY: 0, + scale: 1, + opacity: 1, + rotate: 0, + rotateX: 0, + rotateY: 0, + ); + + /// Returns a copy with [property] set to [value]. + MotionModel withValue(String property, double value) => switch (property) { + 'translateX' => copyWith(translateX: value), + 'translateY' => copyWith(translateY: value), + 'scale' => copyWith(scale: value), + 'opacity' => copyWith(opacity: value.clamp(0, 1).toDouble()), + 'rotate' => copyWith(rotate: value), + 'rotateX' => copyWith(rotateX: value), + 'rotateY' => copyWith(rotateY: value), + 'spin' => copyWith(spin: value), + _ => throw ArgumentError.value(property, 'property'), + }; + + @override + List get props => [ + translateX, + translateY, + scale, + opacity, + rotate, + rotateX, + rotateY, + perspective, + originX, + originY, + durationMs, + curve, + spin, + spinRampMs, + loop, + enter, + enterFrom, + replayKey, + delayMs, + ]; + + /// Creates a [MotionModel] from JSON. + factory MotionModel.fromJson(Map json) => _$MotionModelFromJson(json); + + @override + Map toJson() => _$MotionModelOmitDefaults(this, _$MotionModelToJson(this)); +} diff --git a/lib/src/api/models/motion.g.dart b/lib/src/api/models/motion.g.dart new file mode 100644 index 0000000..83b196e --- /dev/null +++ b/lib/src/api/models/motion.g.dart @@ -0,0 +1,101 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'motion.dart'; + +// ************************************************************************** +// JsonDefaultsGenerator +// ************************************************************************** + +Map _$MotionModelOmitDefaults( + MotionModel instance, + Map json, +) { + if (isJsonDefault(instance.translateX, 0)) json.remove('translateX'); + if (isJsonDefault(instance.translateY, 0)) json.remove('translateY'); + if (isJsonDefault(instance.scale, 1)) json.remove('scale'); + if (isJsonDefault(instance.opacity, 1)) json.remove('opacity'); + if (isJsonDefault(instance.rotate, 0)) json.remove('rotate'); + if (isJsonDefault(instance.rotateX, 0)) json.remove('rotateX'); + if (isJsonDefault(instance.rotateY, 0)) json.remove('rotateY'); + if (isJsonDefault(instance.perspective, 1)) json.remove('perspective'); + if (isJsonDefault(instance.originX, 0)) json.remove('originX'); + if (isJsonDefault(instance.originY, 0)) json.remove('originY'); + if (isJsonDefault(instance.durationMs, 0)) json.remove('durationMs'); + if (isJsonDefault(instance.curve, MotionCurve.easeInOut)) + json.remove('curve'); + if (isJsonDefault(instance.spin, 0)) json.remove('spin'); + if (isJsonDefault(instance.spinRampMs, 800)) json.remove('spinRampMs'); + if (isJsonDefault(instance.loop, MotionLoop.none)) json.remove('loop'); + if (isJsonDefault(instance.enter, false)) json.remove('enter'); + if (isJsonDefault(instance.delayMs, 0)) json.remove('delayMs'); + return json; +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +MotionModel _$MotionModelFromJson(Map json) => MotionModel( + translateX: (json['translateX'] as num?)?.toDouble() ?? 0, + translateY: (json['translateY'] as num?)?.toDouble() ?? 0, + scale: (json['scale'] as num?)?.toDouble() ?? 1, + opacity: (json['opacity'] as num?)?.toDouble() ?? 1, + rotate: (json['rotate'] as num?)?.toDouble() ?? 0, + rotateX: (json['rotateX'] as num?)?.toDouble() ?? 0, + rotateY: (json['rotateY'] as num?)?.toDouble() ?? 0, + perspective: (json['perspective'] as num?)?.toDouble() ?? 1, + originX: (json['originX'] as num?)?.toDouble() ?? 0, + originY: (json['originY'] as num?)?.toDouble() ?? 0, + durationMs: (json['durationMs'] as num?)?.toInt() ?? 0, + curve: + $enumDecodeNullable(_$MotionCurveEnumMap, json['curve']) ?? + MotionCurve.easeInOut, + spin: (json['spin'] as num?)?.toDouble() ?? 0, + spinRampMs: (json['spinRampMs'] as num?)?.toInt() ?? 800, + loop: + $enumDecodeNullable(_$MotionLoopEnumMap, json['loop']) ?? MotionLoop.none, + enter: json['enter'] as bool? ?? false, + enterFrom: json['enterFrom'] == null + ? null + : MotionModel.fromJson(json['enterFrom'] as Map), + replayKey: json['replayKey'] as String?, + delayMs: (json['delayMs'] as num?)?.toInt() ?? 0, +); + +Map _$MotionModelToJson(MotionModel instance) => + { + 'translateX': instance.translateX, + 'translateY': instance.translateY, + 'scale': instance.scale, + 'opacity': instance.opacity, + 'rotate': instance.rotate, + 'rotateX': instance.rotateX, + 'rotateY': instance.rotateY, + 'perspective': instance.perspective, + 'originX': instance.originX, + 'originY': instance.originY, + 'durationMs': instance.durationMs, + 'curve': _$MotionCurveEnumMap[instance.curve]!, + 'spin': instance.spin, + 'spinRampMs': instance.spinRampMs, + 'loop': _$MotionLoopEnumMap[instance.loop]!, + 'enter': instance.enter, + 'enterFrom': ?instance.enterFrom?.toJson(), + 'replayKey': ?instance.replayKey, + 'delayMs': instance.delayMs, + }; + +const _$MotionCurveEnumMap = { + MotionCurve.linear: 'linear', + MotionCurve.easeInOut: 'easeInOut', + MotionCurve.easeOut: 'easeOut', + MotionCurve.easeIn: 'easeIn', + MotionCurve.easeOutBack: 'easeOutBack', + MotionCurve.easeOutCubic: 'easeOutCubic', +}; + +const _$MotionLoopEnumMap = { + MotionLoop.none: 'none', + MotionLoop.restart: 'restart', + MotionLoop.reverse: 'reverse', +}; diff --git a/lib/src/api/models/trigger.dart b/lib/src/api/models/trigger.dart index ad24821..e082d89 100644 --- a/lib/src/api/models/trigger.dart +++ b/lib/src/api/models/trigger.dart @@ -51,7 +51,53 @@ enum TriggerType { /// Triggered when the variable named by [TriggerModel.variable] changes, /// while the screen it is set on shows. - valueChanged; + valueChanged, + + /// Triggered over and over while the canvas is shown, every + /// [TriggerModel.intervalMs]. + timer, + + /// Triggered when a key is pressed while the canvas is shown. The key is + /// [TriggerModel.key], a logical key label such as `Arrow Right`, `Enter` + /// or `A`. + keyPress, + + /// Triggered when the user swipes the target to the left. + swipeLeft, + + /// Triggered when the user swipes the target to the right. + swipeRight, + + /// Triggered when the user swipes the target up. + swipeUp, + + /// Triggered when the user swipes the target down. + swipeDown, + + /// Triggered when the user taps the target twice in quick succession. + /// A node with a double tap reaction delays its single taps ([click]) by + /// the double tap timeout (300ms), the way Flutter tells the two apart. + doubleTap; + + /// The triggers for the mouse cursor entering and leaving the target. + static const List hovers = [ + TriggerType.cursorEnter, + TriggerType.cursorExit, + ]; + + /// Earlier names of trigger types, read as the types they became. + static const Map aliases = { + 'hoverEnter': 'cursorEnter', + 'hoverExit': 'cursorExit', + }; + + /// The swipe triggers. + static const List swipes = [ + TriggerType.swipeLeft, + TriggerType.swipeRight, + TriggerType.swipeUp, + TriggerType.swipeDown, + ]; /// Displayable string representation of the trigger type. String get prettify { @@ -82,6 +128,20 @@ enum TriggerType { return 'Load'; case TriggerType.valueChanged: return 'Value Changed'; + case TriggerType.timer: + return 'Timer'; + case TriggerType.keyPress: + return 'Key Press'; + case TriggerType.swipeLeft: + return 'Swipe Left'; + case TriggerType.swipeRight: + return 'Swipe Right'; + case TriggerType.swipeUp: + return 'Swipe Up'; + case TriggerType.swipeDown: + return 'Swipe Down'; + case TriggerType.doubleTap: + return 'Double Tap'; } } } @@ -101,26 +161,53 @@ class TriggerModel with Equatable, SerializableMixin { /// For [TriggerType.valueChanged], the name of the variable to watch. final String? variable; + /// How often a [TriggerType.timer] fires, in milliseconds. + final int? intervalMs; + + /// The key a [TriggerType.keyPress] listens for, as a logical key label + /// (`Arrow Right`, `Enter`, `Space`, `A`). + final String? key; + /// Creates a [TriggerModel] with given data. - TriggerModel({required this.type, this.debounceMs = 0, this.variable}); + TriggerModel({ + required this.type, + this.debounceMs = 0, + this.variable, + this.intervalMs, + this.key, + }); + + /// The interval of a timer trigger; one second when unset. + int get effectiveIntervalMs => (intervalMs ?? 1000).clamp(16, 86400000); @override - List get props => [type, debounceMs, variable]; + List get props => [type, debounceMs, variable, intervalMs, key]; /// Duplicate this [TriggerModel] with given data overrides. TriggerModel copyWith({ TriggerType? type, int? debounceMs, String? variable, + int? intervalMs, + String? key, }) => TriggerModel( type: type ?? this.type, debounceMs: debounceMs ?? this.debounceMs, variable: variable ?? this.variable, + intervalMs: intervalMs ?? this.intervalMs, + key: key ?? this.key, ); /// Factory constructor for creating a new [TriggerModel] instance from /// JSON data. - factory TriggerModel.fromJson(Map json) => _$TriggerModelFromJson(json); + /// + /// A type named by an earlier name ([TriggerType.aliases]) reads as the + /// type it became. + factory TriggerModel.fromJson(Map json) => + _$TriggerModelFromJson(switch (TriggerType.aliases[json['type']]) { + final String type => {...json, 'type': type}, + null => json, + }); @override Map toJson() => _$TriggerModelOmitDefaults(this, _$TriggerModelToJson(this)); diff --git a/lib/src/api/models/trigger.g.dart b/lib/src/api/models/trigger.g.dart index d522dee..3faa98c 100644 --- a/lib/src/api/models/trigger.g.dart +++ b/lib/src/api/models/trigger.g.dart @@ -22,6 +22,8 @@ TriggerModel _$TriggerModelFromJson(Map json) => TriggerModel( type: $enumDecode(_$TriggerTypeEnumMap, json['type']), debounceMs: (json['debounceMs'] as num?)?.toInt() ?? 0, variable: json['variable'] as String?, + intervalMs: (json['intervalMs'] as num?)?.toInt(), + key: json['key'] as String?, ); Map _$TriggerModelToJson(TriggerModel instance) => @@ -29,6 +31,8 @@ Map _$TriggerModelToJson(TriggerModel instance) => 'type': _$TriggerTypeEnumMap[instance.type]!, 'debounceMs': instance.debounceMs, 'variable': ?instance.variable, + 'intervalMs': ?instance.intervalMs, + 'key': ?instance.key, }; const _$TriggerTypeEnumMap = { @@ -45,4 +49,11 @@ const _$TriggerTypeEnumMap = { TriggerType.pointerUp: 'pointerUp', TriggerType.load: 'load', TriggerType.valueChanged: 'valueChanged', + TriggerType.timer: 'timer', + TriggerType.keyPress: 'keyPress', + TriggerType.swipeLeft: 'swipeLeft', + TriggerType.swipeRight: 'swipeRight', + TriggerType.swipeUp: 'swipeUp', + TriggerType.swipeDown: 'swipeDown', + TriggerType.doubleTap: 'doubleTap', }; diff --git a/lib/src/api/models/variables_model.dart b/lib/src/api/models/variables_model.dart index 7e703dc..b301a7e 100644 --- a/lib/src/api/models/variables_model.dart +++ b/lib/src/api/models/variables_model.dart @@ -147,6 +147,15 @@ class VariableData /// metadata or any other information that is not part of the core variable. final Map extra; + /// Whether this is a computed variable: its [value] is a `${= ...}` + /// formula over other variables, e.g. + /// `${= filter(records, r => r.crate == crateId)}`, worked out whenever it + /// is read. + bool get isComputed { + final String text = value.trim(); + return text.startsWith(r'${=') && text.endsWith('}'); + } + /// Creates a new [VariableData]. VariableData({ required this.id, @@ -187,7 +196,7 @@ class VariableData /// Creates a new [VariableData] from a JSON map. factory VariableData.fromJson(Map json) => - _$VariableDataFromJson(json); + _$VariableDataFromJson(withFormulaAsValue(json)); @override Map toJson() => @@ -201,6 +210,7 @@ class VariableData name: name, value: value, type: type, + extra: extra, ); /// Returns the value converted to the appropriate type according to [type]. @@ -219,6 +229,14 @@ class VariableData }; } +/// [json] with a `formula` (how computed variables were stored for a while) +/// moved into `value`, where a computed variable keeps its formula. +Map withFormulaAsValue(Map json) { + final Object? formula = json['formula']; + if (formula is! String || formula.trim().isEmpty) return json; + return {...json, 'value': formula}..remove('formula'); +} + /// Returns the value converted to the appropriate type according to [type]. /// Supported color values: /// - ColorRGBA @@ -312,7 +330,7 @@ class CanvasVariableData extends VariableData { /// Creates a new [CanvasVariableData] from a JSON map. factory CanvasVariableData.fromJson(Map json) => - _$CanvasVariableDataFromJson(json); + _$CanvasVariableDataFromJson(withFormulaAsValue(json)); @override Map toJson() => diff --git a/lib/src/api/nodes/accordion_node.g.dart b/lib/src/api/nodes/accordion_node.g.dart index b54c03c..f339ef6 100644 --- a/lib/src/api/nodes/accordion_node.g.dart +++ b/lib/src/api/nodes/accordion_node.g.dart @@ -187,6 +187,9 @@ AccordionNode _$AccordionNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..spacing = (json['spacing'] as num?)?.toDouble() ?? 0.0 ..wrap = json['wrap'] as bool? ?? false ..runSpacing = (json['runSpacing'] as num?)?.toDouble() ?? 0.0 @@ -227,6 +230,7 @@ Map _$AccordionNodeToJson( 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'children': instance.children, 'rowColumnType': _$RowColumnTypeEnumMap[instance.rowColumnType]!, 'mainAxisAlignment': _$MainAxisAlignmentCEnumMap[instance.mainAxisAlignment]!, diff --git a/lib/src/api/nodes/app_bar_node.g.dart b/lib/src/api/nodes/app_bar_node.g.dart index 62664dd..c8a56d1 100644 --- a/lib/src/api/nodes/app_bar_node.g.dart +++ b/lib/src/api/nodes/app_bar_node.g.dart @@ -167,6 +167,9 @@ AppBarNode _$AppBarNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$AppBarNodeToJson(AppBarNode instance) => @@ -203,6 +206,7 @@ Map _$AppBarNodeToJson(AppBarNode instance) => 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'properties': instance.properties.toJson(), }; diff --git a/lib/src/api/nodes/auto_placeholder_node.g.dart b/lib/src/api/nodes/auto_placeholder_node.g.dart index 72a513d..fbf6e0c 100644 --- a/lib/src/api/nodes/auto_placeholder_node.g.dart +++ b/lib/src/api/nodes/auto_placeholder_node.g.dart @@ -239,6 +239,9 @@ AutoPlaceholderNode _$AutoPlaceholderNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..inkWell = json['inkWell'] == null ? null : InkWellModel.fromJson( @@ -252,7 +255,6 @@ AutoPlaceholderNode _$AutoPlaceholderNodeFromJson(Map json) => Map _$AutoPlaceholderNodeToJson( AutoPlaceholderNode instance, ) => { - 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'variables': instance.variables, 'multipleVariables': instance.multipleVariables, 'componentId': ?instance.componentId, @@ -270,6 +272,7 @@ Map _$AutoPlaceholderNodeToJson( 'flex': instance.flex, 'squeezable': instance.squeezable, 'aspectRatioLock': instance.aspectRatioLock, + 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'alignment': instance.alignment.toJson(), 'basicBoxLocal': instance.basicBoxLocal.toJson(), 'margin': instance.margin.toJson(), @@ -284,6 +287,7 @@ Map _$AutoPlaceholderNodeToJson( 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'children': instance.children, 'opacity': instance.opacity, 'blendMode': _$BlendModeCEnumMap[instance.blendMode]!, diff --git a/lib/src/api/nodes/badge_node.g.dart b/lib/src/api/nodes/badge_node.g.dart index 80933a6..1521b92 100644 --- a/lib/src/api/nodes/badge_node.g.dart +++ b/lib/src/api/nodes/badge_node.g.dart @@ -169,6 +169,9 @@ BadgeNode _$BadgeNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$BadgeNodeToJson(BadgeNode instance) => { @@ -204,6 +207,7 @@ Map _$BadgeNodeToJson(BadgeNode instance) => { 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'properties': instance.properties.toJson(), 'childID': ?instance.childID, diff --git a/lib/src/api/nodes/base_node.dart b/lib/src/api/nodes/base_node.dart index 009759e..9e6b05d 100644 --- a/lib/src/api/nodes/base_node.dart +++ b/lib/src/api/nodes/base_node.dart @@ -412,6 +412,10 @@ abstract class BaseNode /// should be included and accounted for or not. bool enabled; + /// How the node moves: a transform drawn over its layout, transitions and + /// spin. Null for a node that doesn't move. See [MotionModel]. + MotionModel? motion; + /// Whether the node is sized in fraction of the parent's size. bool get isFractionallySized => widthFactor != null || heightFactor != null; diff --git a/lib/src/api/nodes/button_node.dart b/lib/src/api/nodes/button_node.dart index 14967e1..bb8dab8 100644 --- a/lib/src/api/nodes/button_node.dart +++ b/lib/src/api/nodes/button_node.dart @@ -163,6 +163,7 @@ class ButtonNode extends SceneNode with CustomPropertiesMixin { List get triggerTypes => [ TriggerType.click, TriggerType.longPress, + TriggerType.doubleTap, TriggerType.cursorEnter, TriggerType.cursorExit, ]; diff --git a/lib/src/api/nodes/button_node.g.dart b/lib/src/api/nodes/button_node.g.dart index ffca1fa..4a2fdf3 100644 --- a/lib/src/api/nodes/button_node.g.dart +++ b/lib/src/api/nodes/button_node.g.dart @@ -178,6 +178,9 @@ ButtonNode _$ButtonNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$ButtonNodeToJson(ButtonNode instance) => @@ -214,6 +217,7 @@ Map _$ButtonNodeToJson(ButtonNode instance) => 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'properties': instance.properties.toJson(), }; diff --git a/lib/src/api/nodes/canvas_node.dart b/lib/src/api/nodes/canvas_node.dart index d2fc9bf..fe5482f 100644 --- a/lib/src/api/nodes/canvas_node.dart +++ b/lib/src/api/nodes/canvas_node.dart @@ -46,6 +46,8 @@ class CanvasNode extends ParentNode List get triggerTypes => [ TriggerType.load, TriggerType.valueChanged, + TriggerType.timer, + TriggerType.keyPress, ]; @override diff --git a/lib/src/api/nodes/canvas_node.g.dart b/lib/src/api/nodes/canvas_node.g.dart index d39ebdc..071273a 100644 --- a/lib/src/api/nodes/canvas_node.g.dart +++ b/lib/src/api/nodes/canvas_node.g.dart @@ -293,6 +293,9 @@ CanvasNode _$CanvasNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..inkWell = json['inkWell'] == null ? null : InkWellModel.fromJson( @@ -338,6 +341,7 @@ Map _$CanvasNodeToJson( 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'children': instance.children, 'opacity': instance.opacity, 'blendMode': _$BlendModeCEnumMap[instance.blendMode]!, diff --git a/lib/src/api/nodes/checkbox_node.g.dart b/lib/src/api/nodes/checkbox_node.g.dart index 63fd267..532b91d 100644 --- a/lib/src/api/nodes/checkbox_node.g.dart +++ b/lib/src/api/nodes/checkbox_node.g.dart @@ -173,6 +173,9 @@ CheckboxNode _$CheckboxNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$CheckboxNodeToJson(CheckboxNode instance) => @@ -209,6 +212,7 @@ Map _$CheckboxNodeToJson(CheckboxNode instance) => 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'properties': instance.properties.toJson(), 'value': ?instance.value, diff --git a/lib/src/api/nodes/chip_node.g.dart b/lib/src/api/nodes/chip_node.g.dart index 99e001b..fa2988a 100644 --- a/lib/src/api/nodes/chip_node.g.dart +++ b/lib/src/api/nodes/chip_node.g.dart @@ -167,6 +167,9 @@ ChipNode _$ChipNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$ChipNodeToJson(ChipNode instance) => { @@ -202,6 +205,7 @@ Map _$ChipNodeToJson(ChipNode instance) => { 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'properties': instance.properties.toJson(), }; diff --git a/lib/src/api/nodes/divider_node.g.dart b/lib/src/api/nodes/divider_node.g.dart index f646445..929d8c2 100644 --- a/lib/src/api/nodes/divider_node.g.dart +++ b/lib/src/api/nodes/divider_node.g.dart @@ -156,10 +156,14 @@ DividerNode _$DividerNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$DividerNodeToJson(DividerNode instance) => { + 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'variables': instance.variables, 'multipleVariables': instance.multipleVariables, 'componentId': ?instance.componentId, @@ -177,7 +181,6 @@ Map _$DividerNodeToJson(DividerNode instance) => 'flex': instance.flex, 'squeezable': instance.squeezable, 'aspectRatioLock': instance.aspectRatioLock, - 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'alignment': instance.alignment.toJson(), 'basicBoxLocal': instance.basicBoxLocal.toJson(), 'margin': instance.margin.toJson(), @@ -192,6 +195,7 @@ Map _$DividerNodeToJson(DividerNode instance) => 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'properties': instance.properties.toJson(), }; diff --git a/lib/src/api/nodes/dropdown_node.g.dart b/lib/src/api/nodes/dropdown_node.g.dart index 1920668..3936ece 100644 --- a/lib/src/api/nodes/dropdown_node.g.dart +++ b/lib/src/api/nodes/dropdown_node.g.dart @@ -188,6 +188,9 @@ DropdownNode _$DropdownNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$DropdownNodeToJson(DropdownNode instance) => @@ -224,6 +227,7 @@ Map _$DropdownNodeToJson(DropdownNode instance) => 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'properties': instance.properties.toJson(), 'value': ?instance.value, diff --git a/lib/src/api/nodes/embedded_video_node.g.dart b/lib/src/api/nodes/embedded_video_node.g.dart index 0134088..3b0bc1f 100644 --- a/lib/src/api/nodes/embedded_video_node.g.dart +++ b/lib/src/api/nodes/embedded_video_node.g.dart @@ -193,11 +193,13 @@ EmbeddedVideoNode _$EmbeddedVideoNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$EmbeddedVideoNodeToJson(EmbeddedVideoNode instance) => { - 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'variables': instance.variables, 'multipleVariables': instance.multipleVariables, 'componentId': ?instance.componentId, @@ -215,6 +217,7 @@ Map _$EmbeddedVideoNodeToJson(EmbeddedVideoNode instance) => 'flex': instance.flex, 'squeezable': instance.squeezable, 'aspectRatioLock': instance.aspectRatioLock, + 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'alignment': instance.alignment.toJson(), 'basicBoxLocal': instance.basicBoxLocal.toJson(), 'margin': instance.margin.toJson(), @@ -229,6 +232,7 @@ Map _$EmbeddedVideoNodeToJson(EmbeddedVideoNode instance) => 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'properties': instance.properties.toJson(), }; diff --git a/lib/src/api/nodes/expansion_tile_node.g.dart b/lib/src/api/nodes/expansion_tile_node.g.dart index a01635c..186067b 100644 --- a/lib/src/api/nodes/expansion_tile_node.g.dart +++ b/lib/src/api/nodes/expansion_tile_node.g.dart @@ -201,6 +201,9 @@ ExpansionTileNode _$ExpansionTileNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..rowColumnType = $enumDecode( _$RowColumnTypeEnumMap, json['rowColumnType'], @@ -253,6 +256,7 @@ Map _$ExpansionTileNodeToJson( 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'children': instance.children, 'rowColumnType': _$RowColumnTypeEnumMap[instance.rowColumnType]!, 'mainAxisAlignment': _$MainAxisAlignmentCEnumMap[instance.mainAxisAlignment]!, diff --git a/lib/src/api/nodes/external_component_node.g.dart b/lib/src/api/nodes/external_component_node.g.dart index 280a808..cf0f0ab 100644 --- a/lib/src/api/nodes/external_component_node.g.dart +++ b/lib/src/api/nodes/external_component_node.g.dart @@ -155,11 +155,15 @@ ExternalComponentNode _$ExternalComponentNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$ExternalComponentNodeToJson( ExternalComponentNode instance, ) => { + 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'variables': instance.variables, 'multipleVariables': instance.multipleVariables, 'componentId': ?instance.componentId, @@ -177,7 +181,6 @@ Map _$ExternalComponentNodeToJson( 'flex': instance.flex, 'squeezable': instance.squeezable, 'aspectRatioLock': instance.aspectRatioLock, - 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'alignment': instance.alignment.toJson(), 'basicBoxLocal': instance.basicBoxLocal.toJson(), 'margin': instance.margin.toJson(), @@ -192,6 +195,7 @@ Map _$ExternalComponentNodeToJson( 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'properties': instance.properties.toJson(), }; diff --git a/lib/src/api/nodes/floating_action_button_node.g.dart b/lib/src/api/nodes/floating_action_button_node.g.dart index 4d374a7..8f6799d 100644 --- a/lib/src/api/nodes/floating_action_button_node.g.dart +++ b/lib/src/api/nodes/floating_action_button_node.g.dart @@ -176,6 +176,9 @@ FloatingActionButtonNode _$FloatingActionButtonNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$FloatingActionButtonNodeToJson( @@ -213,6 +216,7 @@ Map _$FloatingActionButtonNodeToJson( 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'properties': instance.properties.toJson(), }; diff --git a/lib/src/api/nodes/frame_node.dart b/lib/src/api/nodes/frame_node.dart index 2723aff..aae0ed7 100644 --- a/lib/src/api/nodes/frame_node.dart +++ b/lib/src/api/nodes/frame_node.dart @@ -120,6 +120,8 @@ class FrameNode extends DefaultShapeNode List get triggerTypes => [ TriggerType.click, TriggerType.longPress, + TriggerType.doubleTap, + ...TriggerType.swipes, TriggerType.cursorEnter, TriggerType.cursorExit, ]; diff --git a/lib/src/api/nodes/frame_node.g.dart b/lib/src/api/nodes/frame_node.g.dart index 9169d00..691803b 100644 --- a/lib/src/api/nodes/frame_node.g.dart +++ b/lib/src/api/nodes/frame_node.g.dart @@ -268,6 +268,9 @@ FrameNode _$FrameNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..inkWell = json['inkWell'] == null ? null : InkWellModel.fromJson( @@ -308,6 +311,7 @@ Map _$FrameNodeToJson(FrameNode instance) => { 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'children': instance.children, 'opacity': instance.opacity, 'blendMode': _$BlendModeCEnumMap[instance.blendMode]!, diff --git a/lib/src/api/nodes/freeform_placeholder_node.g.dart b/lib/src/api/nodes/freeform_placeholder_node.g.dart index 51bc2ec..f7bdbae 100644 --- a/lib/src/api/nodes/freeform_placeholder_node.g.dart +++ b/lib/src/api/nodes/freeform_placeholder_node.g.dart @@ -167,7 +167,6 @@ FreeformPlaceholderNode _$FreeformPlaceholderNodeFromJson(Map json) => $enumDecodeNullable(_$StrokeSideEnumMap, json['strokeSide']) ?? StrokeSide.all, ) - ..squeezable = json['squeezable'] as bool? ?? false ..variables = (json['variables'] as Map?)?.map( (k, e) => MapEntry(k as String, e as String), @@ -192,6 +191,7 @@ FreeformPlaceholderNode _$FreeformPlaceholderNodeFromJson(Map json) => (k, e) => MapEntry(k as String, e), ) ?? {} + ..squeezable = json['squeezable'] as bool? ?? false ..widthFactor = (json['widthFactor'] as num?)?.toDouble() ..heightFactor = (json['heightFactor'] as num?)?.toDouble() ..interactionStates = @@ -210,6 +210,9 @@ FreeformPlaceholderNode _$FreeformPlaceholderNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..inkWell = json['inkWell'] == null ? null : InkWellModel.fromJson( @@ -220,6 +223,13 @@ FreeformPlaceholderNode _$FreeformPlaceholderNodeFromJson(Map json) => Map _$FreeformPlaceholderNodeToJson( FreeformPlaceholderNode instance, ) => { + 'reactions': instance.reactions.map((e) => e.toJson()).toList(), + 'variables': instance.variables, + 'multipleVariables': instance.multipleVariables, + 'componentId': ?instance.componentId, + 'markerType': ?_$ComponentMarkerTypeEnumMap[instance.markerType], + 'componentVersion': instance.componentVersion, + 'componentSchema': instance.componentSchema, 'id': instance.id, 'name': instance.name, 'visible': instance.visible, @@ -231,13 +241,6 @@ Map _$FreeformPlaceholderNodeToJson( 'flex': instance.flex, 'squeezable': instance.squeezable, 'aspectRatioLock': instance.aspectRatioLock, - 'reactions': instance.reactions.map((e) => e.toJson()).toList(), - 'variables': instance.variables, - 'multipleVariables': instance.multipleVariables, - 'componentId': ?instance.componentId, - 'markerType': ?_$ComponentMarkerTypeEnumMap[instance.markerType], - 'componentVersion': instance.componentVersion, - 'componentSchema': instance.componentSchema, 'alignment': instance.alignment.toJson(), 'basicBoxLocal': instance.basicBoxLocal.toJson(), 'margin': instance.margin.toJson(), @@ -252,6 +255,7 @@ Map _$FreeformPlaceholderNodeToJson( 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'children': instance.children, 'opacity': instance.opacity, 'blendMode': _$BlendModeCEnumMap[instance.blendMode]!, diff --git a/lib/src/api/nodes/grid_view_node.g.dart b/lib/src/api/nodes/grid_view_node.g.dart index 38c061e..5febad6 100644 --- a/lib/src/api/nodes/grid_view_node.g.dart +++ b/lib/src/api/nodes/grid_view_node.g.dart @@ -256,6 +256,9 @@ GridViewNode _$GridViewNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..deniedTypes = (json['deniedTypes'] as List) .map((e) => e as String) .toList() @@ -278,6 +281,7 @@ GridViewNode _$GridViewNodeFromJson(Map json) => Map _$GridViewNodeToJson(GridViewNode instance) => { + 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'variables': instance.variables, 'multipleVariables': instance.multipleVariables, 'componentId': ?instance.componentId, @@ -295,7 +299,6 @@ Map _$GridViewNodeToJson(GridViewNode instance) => 'flex': instance.flex, 'squeezable': instance.squeezable, 'aspectRatioLock': instance.aspectRatioLock, - 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'alignment': instance.alignment.toJson(), 'basicBoxLocal': instance.basicBoxLocal.toJson(), 'margin': instance.margin.toJson(), @@ -310,6 +313,7 @@ Map _$GridViewNodeToJson(GridViewNode instance) => 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'children': instance.children, 'deniedTypes': instance.deniedTypes, 'maxAllowedSize': ?instance.maxAllowedSize?.toJson(), diff --git a/lib/src/api/nodes/icon_node.dart b/lib/src/api/nodes/icon_node.dart index da38009..b33e735 100644 --- a/lib/src/api/nodes/icon_node.dart +++ b/lib/src/api/nodes/icon_node.dart @@ -104,6 +104,7 @@ class IconNode extends SceneNode List get triggerTypes => [ TriggerType.click, TriggerType.longPress, + TriggerType.doubleTap, TriggerType.cursorEnter, TriggerType.cursorExit, ]; diff --git a/lib/src/api/nodes/icon_node.g.dart b/lib/src/api/nodes/icon_node.g.dart index 46bbc0a..8b4186e 100644 --- a/lib/src/api/nodes/icon_node.g.dart +++ b/lib/src/api/nodes/icon_node.g.dart @@ -177,6 +177,9 @@ IconNode _$IconNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$IconNodeToJson(IconNode instance) => { @@ -212,6 +215,7 @@ Map _$IconNodeToJson(IconNode instance) => { 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'opacity': instance.opacity, 'blendMode': _$BlendModeCEnumMap[instance.blendMode]!, 'isMask': instance.isMask, diff --git a/lib/src/api/nodes/line_chart_node.g.dart b/lib/src/api/nodes/line_chart_node.g.dart index 39272b9..40f65eb 100644 --- a/lib/src/api/nodes/line_chart_node.g.dart +++ b/lib/src/api/nodes/line_chart_node.g.dart @@ -196,10 +196,14 @@ LineChartNode _$LineChartNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$LineChartNodeToJson(LineChartNode instance) => { + 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'variables': instance.variables, 'multipleVariables': instance.multipleVariables, 'componentId': ?instance.componentId, @@ -217,7 +221,6 @@ Map _$LineChartNodeToJson(LineChartNode instance) => 'flex': instance.flex, 'squeezable': instance.squeezable, 'aspectRatioLock': instance.aspectRatioLock, - 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'alignment': instance.alignment.toJson(), 'basicBoxLocal': instance.basicBoxLocal.toJson(), 'margin': instance.margin.toJson(), @@ -232,6 +235,7 @@ Map _$LineChartNodeToJson(LineChartNode instance) => 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'properties': instance.properties.toJson(), }; diff --git a/lib/src/api/nodes/list_tile_node.g.dart b/lib/src/api/nodes/list_tile_node.g.dart index 2a2a8bc..85c98cc 100644 --- a/lib/src/api/nodes/list_tile_node.g.dart +++ b/lib/src/api/nodes/list_tile_node.g.dart @@ -183,6 +183,9 @@ ListTileNode _$ListTileNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$ListTileNodeToJson(ListTileNode instance) => @@ -219,6 +222,7 @@ Map _$ListTileNodeToJson(ListTileNode instance) => 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'children': instance.children, 'type': instance.type, 'leading': ?instance.leading, diff --git a/lib/src/api/nodes/list_view_node.dart b/lib/src/api/nodes/list_view_node.dart index 3dcac28..3feca3a 100644 --- a/lib/src/api/nodes/list_view_node.dart +++ b/lib/src/api/nodes/list_view_node.dart @@ -120,8 +120,10 @@ class ListViewNode extends SinglePlaceholderNode } /// Creates a [ListViewNode] from a JSON object. + /// + /// A list always scrolls with a Flutter list view, so JSON written by hand + /// can leave those flags out. factory ListViewNode.fromJson(Map json) => _$ListViewNodeFromJson({ - // Node JSON may leave out scrolling fields; read the constructor's. 'isScrollable': true, 'useFlutterListView': true, 'allowedTypes': [], @@ -194,6 +196,40 @@ class ListViewProperties extends CustomProperties with FieldsHolder { /// items, if enabled. DividerProperties dividerProperties; + /// Whether the items are stacked on top of each other, like a deck of + /// cards or records in a crate, instead of laid out one after another. + /// The first item is drawn on top. A stacked list doesn't scroll; items + /// are placed at the top center of the list and are usually moved apart + /// with motion bound to their index (see [MotionModel]). + bool stacked; + + /// Whether the items flow onto further lines (runs) when they don't fit, + /// like Flutter's [Wrap], instead of scrolling in one line. The scroll + /// direction is the wrap direction: a horizontal list fills a line from + /// left to right, then starts a new line below; a vertical list fills a + /// column top to bottom, then starts a new column to the right. Items keep + /// their own size (they are not stretched to the list's cross extent). + /// Separators are not drawn; use [wrapSpacing] and [wrapRunSpacing]. + /// Ignored when [stacked] is true. + /// + /// A wrapping list shows every item, so it needs an item count or a data + /// source. It scrolls along the cross axis when its runs don't fit its box; + /// give it a shrink-wrap fit on the cross axis to grow with its runs. + bool wrap; + + /// The gap between two items of the same line when [wrap] is true. + double wrapSpacing; + + /// The gap between two lines (runs) when [wrap] is true. + double wrapRunSpacing; + + /// How the items of a line are placed along the wrap direction when [wrap] + /// is true. Maps to Flutter's [WrapAlignment] by name. + MainAxisAlignmentC wrapAlignment; + + /// Whether this list wraps its items: [wrap] is on and [stacked] is off. + bool get isWrapping => wrap && !stacked; + /// Creates a new [ListViewProperties] instance. ListViewProperties({ this.itemCount, @@ -202,6 +238,11 @@ class ListViewProperties extends CustomProperties with FieldsHolder { this.separator = ListItemSeparator.space, this.separatorSpacing = 0, this.hasSeparator = false, + this.stacked = false, + this.wrap = false, + this.wrapSpacing = 0, + this.wrapRunSpacing = 0, + this.wrapAlignment = MainAxisAlignmentC.start, DividerProperties? dividerProperties, }) : dividerProperties = dividerProperties ?? DividerProperties(); @@ -221,9 +262,19 @@ class ListViewProperties extends CustomProperties with FieldsHolder { ColorRGBA? dividerColor, double? dividerSpacing, bool? hasSeparator, + bool? stacked, + bool? wrap, + double? wrapSpacing, + double? wrapRunSpacing, + MainAxisAlignmentC? wrapAlignment, DividerProperties? dividerProperties, }) { return ListViewProperties( + stacked: stacked ?? this.stacked, + wrap: wrap ?? this.wrap, + wrapSpacing: wrapSpacing ?? this.wrapSpacing, + wrapRunSpacing: wrapRunSpacing ?? this.wrapRunSpacing, + wrapAlignment: wrapAlignment ?? this.wrapAlignment, itemCount: itemCount ?? this.itemCount, itemExtent: itemExtent ?? this.itemExtent, cacheExtent: cacheExtent ?? this.cacheExtent, @@ -237,6 +288,45 @@ class ListViewProperties extends CustomProperties with FieldsHolder { @override FieldsMap generateFields() => { ...super.generateFields(), + 'stacked': BoolFieldAccess( + 'Stacked', + 'Draw the items on top of each other, the first on top, instead of ' + 'one after another.', + () => stacked, + (value) => stacked = value, + requiresLayout: true, + ), + 'wrap': BoolFieldAccess( + 'Wrap', + 'Flow the items onto further lines when they do not fit, instead of ' + 'scrolling in one line. Ignored when Stacked is on.', + () => wrap, + (value) => wrap = value, + requiresLayout: true, + ), + 'wrapSpacing': NumFieldAccess( + 'Wrap Spacing', + 'The gap between two items of the same line of a wrapping list.', + () => wrapSpacing, + (value) => wrapSpacing = value, + requiresLayout: true, + ), + 'wrapRunSpacing': NumFieldAccess( + 'Wrap Run Spacing', + 'The gap between two lines of a wrapping list.', + () => wrapRunSpacing, + (value) => wrapRunSpacing = value, + requiresLayout: true, + ), + 'wrapAlignment': EnumFieldAccess( + 'Wrap Alignment', + 'How the items of a line of a wrapping list are placed along it.', + () => wrapAlignment, + (value) => wrapAlignment = value, + defaultValue: () => MainAxisAlignmentC.start, + options: () => MainAxisAlignmentC.values, + requiresLayout: true, + ), 'hasSeparator': BoolFieldAccess( 'Has Separator', 'Whether to use a separator between list view items.', @@ -285,6 +375,11 @@ class ListViewProperties extends CustomProperties with FieldsHolder { separator, separatorSpacing, hasSeparator, + stacked, + wrap, + wrapSpacing, + wrapRunSpacing, + wrapAlignment, dividerProperties, ]; } diff --git a/lib/src/api/nodes/list_view_node.g.dart b/lib/src/api/nodes/list_view_node.g.dart index 5f43ffd..bb25f58 100644 --- a/lib/src/api/nodes/list_view_node.g.dart +++ b/lib/src/api/nodes/list_view_node.g.dart @@ -81,6 +81,12 @@ Map _$ListViewPropertiesOmitDefaults( if (isJsonDefault(instance.separator, ListItemSeparator.space)) json.remove('separator'); if (isJsonDefault(instance.hasSeparator, false)) json.remove('hasSeparator'); + if (isJsonDefault(instance.stacked, false)) json.remove('stacked'); + if (isJsonDefault(instance.wrap, false)) json.remove('wrap'); + if (isJsonDefault(instance.wrapSpacing, 0)) json.remove('wrapSpacing'); + if (isJsonDefault(instance.wrapRunSpacing, 0)) json.remove('wrapRunSpacing'); + if (isJsonDefault(instance.wrapAlignment, MainAxisAlignmentC.start)) + json.remove('wrapAlignment'); return json; } @@ -226,6 +232,9 @@ ListViewNode _$ListViewNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..deniedTypes = (json['deniedTypes'] as List) .map((e) => e as String) .toList() @@ -250,6 +259,7 @@ ListViewNode _$ListViewNodeFromJson(Map json) => Map _$ListViewNodeToJson(ListViewNode instance) => { + 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'variables': instance.variables, 'multipleVariables': instance.multipleVariables, 'componentId': ?instance.componentId, @@ -267,7 +277,6 @@ Map _$ListViewNodeToJson(ListViewNode instance) => 'flex': instance.flex, 'squeezable': instance.squeezable, 'aspectRatioLock': instance.aspectRatioLock, - 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'alignment': instance.alignment.toJson(), 'basicBoxLocal': instance.basicBoxLocal.toJson(), 'margin': instance.margin.toJson(), @@ -282,6 +291,7 @@ Map _$ListViewNodeToJson(ListViewNode instance) => 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'children': instance.children, 'deniedTypes': instance.deniedTypes, 'maxAllowedSize': ?instance.maxAllowedSize?.toJson(), @@ -368,6 +378,13 @@ ListViewProperties _$ListViewPropertiesFromJson(Map json) => ListViewProperties( ListItemSeparator.space, separatorSpacing: (json['separatorSpacing'] as num?)?.toDouble() ?? 0, hasSeparator: json['hasSeparator'] as bool? ?? false, + stacked: json['stacked'] as bool? ?? false, + wrap: json['wrap'] as bool? ?? false, + wrapSpacing: (json['wrapSpacing'] as num?)?.toDouble() ?? 0, + wrapRunSpacing: (json['wrapRunSpacing'] as num?)?.toDouble() ?? 0, + wrapAlignment: + $enumDecodeNullable(_$MainAxisAlignmentCEnumMap, json['wrapAlignment']) ?? + MainAxisAlignmentC.start, dividerProperties: json['dividerProperties'] == null ? null : DividerProperties.fromJson(json['dividerProperties'] as Map), @@ -382,9 +399,23 @@ Map _$ListViewPropertiesToJson(ListViewProperties instance) => 'separator': _$ListItemSeparatorEnumMap[instance.separator]!, 'hasSeparator': instance.hasSeparator, 'dividerProperties': instance.dividerProperties.toJson(), + 'stacked': instance.stacked, + 'wrap': instance.wrap, + 'wrapSpacing': instance.wrapSpacing, + 'wrapRunSpacing': instance.wrapRunSpacing, + 'wrapAlignment': _$MainAxisAlignmentCEnumMap[instance.wrapAlignment]!, }; const _$ListItemSeparatorEnumMap = { ListItemSeparator.divider: 'divider', ListItemSeparator.space: 'space', }; + +const _$MainAxisAlignmentCEnumMap = { + MainAxisAlignmentC.start: 'start', + MainAxisAlignmentC.center: 'center', + MainAxisAlignmentC.end: 'end', + MainAxisAlignmentC.spaceBetween: 'spaceBetween', + MainAxisAlignmentC.spaceAround: 'spaceAround', + MainAxisAlignmentC.spaceEvenly: 'spaceEvenly', +}; diff --git a/lib/src/api/nodes/loading_indicator_node.g.dart b/lib/src/api/nodes/loading_indicator_node.g.dart index 18c097f..976f79e 100644 --- a/lib/src/api/nodes/loading_indicator_node.g.dart +++ b/lib/src/api/nodes/loading_indicator_node.g.dart @@ -166,12 +166,14 @@ LoadingIndicatorNode _$LoadingIndicatorNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$LoadingIndicatorNodeToJson( LoadingIndicatorNode instance, ) => { - 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'variables': instance.variables, 'multipleVariables': instance.multipleVariables, 'componentId': ?instance.componentId, @@ -189,6 +191,7 @@ Map _$LoadingIndicatorNodeToJson( 'flex': instance.flex, 'squeezable': instance.squeezable, 'aspectRatioLock': instance.aspectRatioLock, + 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'alignment': instance.alignment.toJson(), 'basicBoxLocal': instance.basicBoxLocal.toJson(), 'margin': instance.margin.toJson(), @@ -203,6 +206,7 @@ Map _$LoadingIndicatorNodeToJson( 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'properties': instance.properties.toJson(), }; diff --git a/lib/src/api/nodes/navigation_bar_node.g.dart b/lib/src/api/nodes/navigation_bar_node.g.dart index 6781362..4ec0256 100644 --- a/lib/src/api/nodes/navigation_bar_node.g.dart +++ b/lib/src/api/nodes/navigation_bar_node.g.dart @@ -191,10 +191,14 @@ NavigationBarNode _$NavigationBarNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$NavigationBarNodeToJson(NavigationBarNode instance) => { + 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'variables': instance.variables, 'multipleVariables': instance.multipleVariables, 'componentId': ?instance.componentId, @@ -212,7 +216,6 @@ Map _$NavigationBarNodeToJson(NavigationBarNode instance) => 'flex': instance.flex, 'squeezable': instance.squeezable, 'aspectRatioLock': instance.aspectRatioLock, - 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'alignment': instance.alignment.toJson(), 'basicBoxLocal': instance.basicBoxLocal.toJson(), 'margin': instance.margin.toJson(), @@ -227,6 +230,7 @@ Map _$NavigationBarNodeToJson(NavigationBarNode instance) => 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'currentIndex': instance.currentIndex, 'properties': instance.properties.toJson(), diff --git a/lib/src/api/nodes/page_view_node.g.dart b/lib/src/api/nodes/page_view_node.g.dart index ea1bbf9..541dfd3 100644 --- a/lib/src/api/nodes/page_view_node.g.dart +++ b/lib/src/api/nodes/page_view_node.g.dart @@ -200,6 +200,9 @@ PageViewNode _$PageViewNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..ephemeral = json['ephemeral'] as bool? ?? false ..allowedTypes = (json['allowedTypes'] as List) .map((e) => e as String) @@ -248,6 +251,7 @@ Map _$PageViewNodeToJson(PageViewNode instance) => 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'children': instance.children, 'clipsContent': instance.clipsContent, 'ephemeral': instance.ephemeral, diff --git a/lib/src/api/nodes/placeholder_node.g.dart b/lib/src/api/nodes/placeholder_node.g.dart index a99c387..f7f0ff6 100644 --- a/lib/src/api/nodes/placeholder_node.g.dart +++ b/lib/src/api/nodes/placeholder_node.g.dart @@ -145,10 +145,14 @@ PlaceholderNode _$PlaceholderNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$PlaceholderNodeToJson(PlaceholderNode instance) => { + 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'variables': instance.variables, 'multipleVariables': instance.multipleVariables, 'componentId': ?instance.componentId, @@ -166,7 +170,6 @@ Map _$PlaceholderNodeToJson(PlaceholderNode instance) => 'flex': instance.flex, 'squeezable': instance.squeezable, 'aspectRatioLock': instance.aspectRatioLock, - 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'alignment': instance.alignment.toJson(), 'basicBoxLocal': instance.basicBoxLocal.toJson(), 'margin': instance.margin.toJson(), @@ -181,6 +184,7 @@ Map _$PlaceholderNodeToJson(PlaceholderNode instance) => 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'leadingAsset': ?instance.leadingAsset, 'text': ?instance.text, diff --git a/lib/src/api/nodes/progressbar_node.g.dart b/lib/src/api/nodes/progressbar_node.g.dart index 4d25169..fcda11a 100644 --- a/lib/src/api/nodes/progressbar_node.g.dart +++ b/lib/src/api/nodes/progressbar_node.g.dart @@ -179,6 +179,9 @@ ProgressBarNode _$ProgressBarNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$ProgressBarNodeToJson(ProgressBarNode instance) => @@ -215,6 +218,7 @@ Map _$ProgressBarNodeToJson(ProgressBarNode instance) => 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'properties': instance.properties.toJson(), 'currentValue': instance.currentValue, diff --git a/lib/src/api/nodes/radio_node.g.dart b/lib/src/api/nodes/radio_node.g.dart index eedcc3f..4280132 100644 --- a/lib/src/api/nodes/radio_node.g.dart +++ b/lib/src/api/nodes/radio_node.g.dart @@ -166,6 +166,9 @@ RadioNode _$RadioNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$RadioNodeToJson(RadioNode instance) => { @@ -201,6 +204,7 @@ Map _$RadioNodeToJson(RadioNode instance) => { 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'properties': instance.properties.toJson(), 'value': instance.value, diff --git a/lib/src/api/nodes/rectangle_node.dart b/lib/src/api/nodes/rectangle_node.dart index 32330af..d0d1fba 100644 --- a/lib/src/api/nodes/rectangle_node.dart +++ b/lib/src/api/nodes/rectangle_node.dart @@ -69,6 +69,8 @@ class RectangleNode extends DefaultShapeNode with CornerMixin, ClipMixin { List get triggerTypes => [ TriggerType.click, TriggerType.longPress, + TriggerType.doubleTap, + ...TriggerType.swipes, TriggerType.cursorEnter, TriggerType.cursorExit, ]; diff --git a/lib/src/api/nodes/rectangle_node.g.dart b/lib/src/api/nodes/rectangle_node.g.dart index b7d7535..5df1531 100644 --- a/lib/src/api/nodes/rectangle_node.g.dart +++ b/lib/src/api/nodes/rectangle_node.g.dart @@ -214,6 +214,9 @@ RectangleNode _$RectangleNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..inkWell = json['inkWell'] == null ? null : InkWellModel.fromJson( @@ -255,6 +258,7 @@ Map _$RectangleNodeToJson(RectangleNode instance) => 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'opacity': instance.opacity, 'blendMode': _$BlendModeCEnumMap[instance.blendMode]!, 'isMask': instance.isMask, diff --git a/lib/src/api/nodes/row_column_node.dart b/lib/src/api/nodes/row_column_node.dart index c7d527d..94756f9 100644 --- a/lib/src/api/nodes/row_column_node.dart +++ b/lib/src/api/nodes/row_column_node.dart @@ -144,7 +144,14 @@ class RowColumnNode extends ParentNode }; /// Creates a [RowColumnNode] from a JSON data. - factory RowColumnNode.fromJson(Map json) => _$RowColumnNodeFromJson(json); + /// + /// `itemSpacing`, an earlier name for [RowColumnMixin.spacing], is read as + /// it. + factory RowColumnNode.fromJson(Map json) => _$RowColumnNodeFromJson( + json.containsKey('itemSpacing') && !json.containsKey('spacing') + ? {...json, 'spacing': json['itemSpacing']} + : json, + ); @override Map toJson() => diff --git a/lib/src/api/nodes/row_column_node.g.dart b/lib/src/api/nodes/row_column_node.g.dart index de761ba..1ac8e86 100644 --- a/lib/src/api/nodes/row_column_node.g.dart +++ b/lib/src/api/nodes/row_column_node.g.dart @@ -278,6 +278,9 @@ RowColumnNode _$RowColumnNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..inkWell = json['inkWell'] == null ? null : InkWellModel.fromJson( @@ -320,6 +323,7 @@ Map _$RowColumnNodeToJson( 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'children': instance.children, 'opacity': instance.opacity, 'blendMode': _$BlendModeCEnumMap[instance.blendMode]!, diff --git a/lib/src/api/nodes/segmented_button_node.g.dart b/lib/src/api/nodes/segmented_button_node.g.dart index 5e31132..3f71ea4 100644 --- a/lib/src/api/nodes/segmented_button_node.g.dart +++ b/lib/src/api/nodes/segmented_button_node.g.dart @@ -182,6 +182,9 @@ SegmentedButtonNode _$SegmentedButtonNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$SegmentedButtonNodeToJson( @@ -219,6 +222,7 @@ Map _$SegmentedButtonNodeToJson( 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'properties': instance.properties.toJson(), }; diff --git a/lib/src/api/nodes/signature_node.g.dart b/lib/src/api/nodes/signature_node.g.dart index 5c6f119..7e94a61 100644 --- a/lib/src/api/nodes/signature_node.g.dart +++ b/lib/src/api/nodes/signature_node.g.dart @@ -181,6 +181,9 @@ SignatureNode _$SignatureNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$SignatureNodeToJson(SignatureNode instance) => @@ -217,6 +220,7 @@ Map _$SignatureNodeToJson(SignatureNode instance) => 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'properties': instance.properties.toJson(), 'value': instance.value, diff --git a/lib/src/api/nodes/single_placeholder_node.g.dart b/lib/src/api/nodes/single_placeholder_node.g.dart index 70b2859..72d475b 100644 --- a/lib/src/api/nodes/single_placeholder_node.g.dart +++ b/lib/src/api/nodes/single_placeholder_node.g.dart @@ -160,6 +160,9 @@ SinglePlaceholderNode _$SinglePlaceholderNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$SinglePlaceholderNodeToJson( @@ -197,6 +200,7 @@ Map _$SinglePlaceholderNodeToJson( 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'children': instance.children, 'type': instance.type, 'ephemeral': instance.ephemeral, diff --git a/lib/src/api/nodes/slider_node.dart b/lib/src/api/nodes/slider_node.dart index 8963620..265c785 100644 --- a/lib/src/api/nodes/slider_node.dart +++ b/lib/src/api/nodes/slider_node.dart @@ -22,6 +22,12 @@ const double kSliderDefaultTickMarkRadius = 1; /// Default radius of slider's thumb. const double kSliderDefaultThumbRadius = 10; +/// Default width and height of a slider's rectangular thumb. +const double kSliderDefaultThumbSize = 20; + +/// Default corner radius of a slider's rectangular thumb. +const double kSliderDefaultThumbCornerRadius = 4; + /// Default radius of slider's overlay. const double kSliderDefaultOverlayRadius = 24; @@ -114,12 +120,17 @@ class SliderNode extends SceneNode with CustomPropertiesMixin { }; @override - List get triggerTypes => [TriggerType.changed]; + List get triggerTypes => [ + TriggerType.changed, + TriggerType.doubleTap, + ]; @override late final List propertyVariables = [ ...super.propertyVariables, DoubleValue(name: 'value', value: value), + DoubleValue(name: 'min', value: properties.min), + DoubleValue(name: 'max', value: properties.max), ]; /// Creates a [SliderNode] instance from a JSON object. @@ -186,9 +197,26 @@ class SliderProperties extends CustomProperties with FieldsHolder { /// Whether to allow non-whole numbers as min or max values of slider. bool allowFractionalPoints; - /// Radius of slider's thumb. + /// Radius of slider's thumb when [thumbShape] is + /// [SliderThumbShapeEnum.circle]. double thumbRadius; + /// Shape of slider's thumb: a circle of [thumbRadius], or a rounded + /// rectangle of [thumbWidth] by [thumbHeight] with [thumbCornerRadius] + /// corners. + SliderThumbShapeEnum thumbShape; + + /// Width of a rectangular thumb as drawn on screen, across a vertical + /// slider's track and along a horizontal one's. + double thumbWidth; + + /// Height of a rectangular thumb as drawn on screen, along a vertical + /// slider's track and across a horizontal one's. + double thumbHeight; + + /// Corner radius of a rectangular thumb. + double thumbCornerRadius; + /// Whether to show slider's thumb. bool showThumb; @@ -224,6 +252,22 @@ class SliderProperties extends CustomProperties with FieldsHolder { /// room for the thumb's overlay. Mirrors `Slider.padding`. EdgeInsetsModel? padding; + /// Direction the slider runs in. A vertical slider runs from [min] at the + /// bottom to [max] at the top (top to bottom when [inverted]); its node box + /// is its full hit area. + AxisC orientation; + + /// Whether the slider runs the other way: right to left when horizontal, + /// top to bottom when vertical. + bool inverted; + + /// Color of the stroke around the slider's thumb. No stroke when null or + /// when [thumbStrokeWidth] is 0. + ColorRGBA? thumbStrokeColor; + + /// Width of the stroke around the slider's thumb, drawn inside the thumb. + double thumbStrokeWidth; + /// Creates a [SliderProperties] instance with the given data. SliderProperties({ this.activeTrackColor = ColorRGBA.black, @@ -245,6 +289,10 @@ class SliderProperties extends CustomProperties with FieldsHolder { this.valueIndicatorTextColor = ColorRGBA.white, this.valueIndicatorFontSize = 14, this.thumbRadius = kSliderDefaultThumbRadius, + this.thumbShape = SliderThumbShapeEnum.circle, + this.thumbWidth = kSliderDefaultThumbSize, + this.thumbHeight = kSliderDefaultThumbSize, + this.thumbCornerRadius = kSliderDefaultThumbCornerRadius, this.showThumb = true, this.trackShape = SliderTrackShapeEnum.roundedRectangle, this.tickMarkRadius = kSliderDefaultTickMarkRadius, @@ -255,6 +303,10 @@ class SliderProperties extends CustomProperties with FieldsHolder { this.secondaryTrackValue, this.secondaryActiveTrackColor, this.padding, + this.orientation = AxisC.horizontal, + this.inverted = false, + this.thumbStrokeColor, + this.thumbStrokeWidth = 0, }); /// Duplicates this [SliderProperties] instance with the given data @@ -279,6 +331,10 @@ class SliderProperties extends CustomProperties with FieldsHolder { ColorRGBA? valueIndicatorTextColor, double? valueIndicatorFontSize, double? thumbRadius, + SliderThumbShapeEnum? thumbShape, + double? thumbWidth, + double? thumbHeight, + double? thumbCornerRadius, bool? showThumb, SliderTrackShapeEnum? trackShape, double? tickMarkRadius, @@ -289,6 +345,10 @@ class SliderProperties extends CustomProperties with FieldsHolder { double? secondaryTrackValue, ColorRGBA? secondaryActiveTrackColor, EdgeInsetsModel? padding, + AxisC? orientation, + bool? inverted, + ColorRGBA? thumbStrokeColor, + double? thumbStrokeWidth, }) { return SliderProperties( activeTrackColor: activeTrackColor ?? this.activeTrackColor, @@ -314,6 +374,10 @@ class SliderProperties extends CustomProperties with FieldsHolder { valueIndicatorFontSize: valueIndicatorFontSize ?? this.valueIndicatorFontSize, thumbRadius: thumbRadius ?? this.thumbRadius, + thumbShape: thumbShape ?? this.thumbShape, + thumbWidth: thumbWidth ?? this.thumbWidth, + thumbHeight: thumbHeight ?? this.thumbHeight, + thumbCornerRadius: thumbCornerRadius ?? this.thumbCornerRadius, showThumb: showThumb ?? this.showThumb, trackShape: trackShape ?? this.trackShape, tickMarkRadius: tickMarkRadius ?? this.tickMarkRadius, @@ -326,6 +390,10 @@ class SliderProperties extends CustomProperties with FieldsHolder { secondaryActiveTrackColor: secondaryActiveTrackColor ?? this.secondaryActiveTrackColor, padding: padding ?? this.padding, + orientation: orientation ?? this.orientation, + inverted: inverted ?? this.inverted, + thumbStrokeColor: thumbStrokeColor ?? this.thumbStrokeColor, + thumbStrokeWidth: thumbStrokeWidth ?? this.thumbStrokeWidth, ); } @@ -528,6 +596,59 @@ class SliderProperties extends CustomProperties with FieldsHolder { () => secondaryActiveTrackColor, (value) => secondaryActiveTrackColor = value, ), + 'orientation': EnumFieldAccess( + 'Orientation', + 'Direction the slider runs in.', + () => orientation, + (AxisC value) => orientation = value, + defaultValue: () => AxisC.horizontal, + options: () => AxisC.values, + requiresLayout: true, + ), + 'inverted': BoolFieldAccess( + 'Inverted', + 'Whether the slider runs right to left, or top to bottom.', + () => inverted, + (bool value) => inverted = value, + ), + 'thumbStrokeColor': ColorFieldAccess( + 'Thumb Stroke Color', + 'Color of the stroke around the slider\'s thumb.', + () => thumbStrokeColor, + (value) => thumbStrokeColor = value, + ), + 'thumbStrokeWidth': NumFieldAccess( + 'Thumb Stroke Width', + 'Width of the stroke around the slider\'s thumb.', + () => thumbStrokeWidth, + (double value) => thumbStrokeWidth = value, + ), + 'thumbShape': EnumFieldAccess( + 'Thumb Shape', + 'Shape of slider\'s thumb: a circle or a rounded rectangle.', + () => thumbShape, + (SliderThumbShapeEnum value) => thumbShape = value, + defaultValue: () => SliderThumbShapeEnum.circle, + options: () => SliderThumbShapeEnum.values, + ), + 'thumbWidth': NumFieldAccess( + 'Thumb Width', + 'Width of slider\'s rectangular thumb.', + () => thumbWidth, + (double value) => thumbWidth = value, + ), + 'thumbHeight': NumFieldAccess( + 'Thumb Height', + 'Height of slider\'s rectangular thumb.', + () => thumbHeight, + (double value) => thumbHeight = value, + ), + 'thumbCornerRadius': NumFieldAccess( + 'Thumb Corner Radius', + 'Corner radius of slider\'s rectangular thumb.', + () => thumbCornerRadius, + (double value) => thumbCornerRadius = value, + ), }; @override @@ -551,6 +672,10 @@ class SliderProperties extends CustomProperties with FieldsHolder { valueIndicatorTextColor, valueIndicatorFontSize, thumbRadius, + thumbShape, + thumbWidth, + thumbHeight, + thumbCornerRadius, showThumb, trackShape, tickMarkRadius, @@ -561,6 +686,10 @@ class SliderProperties extends CustomProperties with FieldsHolder { secondaryTrackValue, secondaryActiveTrackColor, padding, + orientation, + inverted, + thumbStrokeColor, + thumbStrokeWidth, ]; /// Creates a [SliderProperties] instance from a JSON object. diff --git a/lib/src/api/nodes/slider_node.g.dart b/lib/src/api/nodes/slider_node.g.dart index c1afa8b..3f3fa0f 100644 --- a/lib/src/api/nodes/slider_node.g.dart +++ b/lib/src/api/nodes/slider_node.g.dart @@ -89,6 +89,17 @@ Map _$SliderPropertiesOmitDefaults( json.remove('allowFractionalPoints'); if (isJsonDefault(instance.thumbRadius, kSliderDefaultThumbRadius)) json.remove('thumbRadius'); + if (isJsonDefault(instance.thumbShape, SliderThumbShapeEnum.circle)) + json.remove('thumbShape'); + if (isJsonDefault(instance.thumbWidth, kSliderDefaultThumbSize)) + json.remove('thumbWidth'); + if (isJsonDefault(instance.thumbHeight, kSliderDefaultThumbSize)) + json.remove('thumbHeight'); + if (isJsonDefault( + instance.thumbCornerRadius, + kSliderDefaultThumbCornerRadius, + )) + json.remove('thumbCornerRadius'); if (isJsonDefault(instance.showThumb, true)) json.remove('showThumb'); if (isJsonDefault(instance.trackShape, SliderTrackShapeEnum.roundedRectangle)) json.remove('trackShape'); @@ -103,6 +114,11 @@ Map _$SliderPropertiesOmitDefaults( json.remove('overlayRadius'); if (isJsonDefault(instance.deriveOverlayColorFromThumb, true)) json.remove('deriveOverlayColorFromThumb'); + if (isJsonDefault(instance.orientation, AxisC.horizontal)) + json.remove('orientation'); + if (isJsonDefault(instance.inverted, false)) json.remove('inverted'); + if (isJsonDefault(instance.thumbStrokeWidth, 0)) + json.remove('thumbStrokeWidth'); return json; } @@ -202,6 +218,9 @@ SliderNode _$SliderNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$SliderNodeToJson(SliderNode instance) => @@ -238,6 +257,7 @@ Map _$SliderNodeToJson(SliderNode instance) => 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'value': instance.value, 'properties': instance.properties.toJson(), @@ -308,6 +328,16 @@ SliderProperties _$SliderPropertiesFromJson(Map json) => SliderProperties( (json['valueIndicatorFontSize'] as num?)?.toDouble() ?? 14, thumbRadius: (json['thumbRadius'] as num?)?.toDouble() ?? kSliderDefaultThumbRadius, + thumbShape: + $enumDecodeNullable(_$SliderThumbShapeEnumEnumMap, json['thumbShape']) ?? + SliderThumbShapeEnum.circle, + thumbWidth: + (json['thumbWidth'] as num?)?.toDouble() ?? kSliderDefaultThumbSize, + thumbHeight: + (json['thumbHeight'] as num?)?.toDouble() ?? kSliderDefaultThumbSize, + thumbCornerRadius: + (json['thumbCornerRadius'] as num?)?.toDouble() ?? + kSliderDefaultThumbCornerRadius, showThumb: json['showThumb'] as bool? ?? true, trackShape: $enumDecodeNullable(_$SliderTrackShapeEnumEnumMap, json['trackShape']) ?? @@ -337,6 +367,14 @@ SliderProperties _$SliderPropertiesFromJson(Map json) => SliderProperties( padding: json['padding'] == null ? null : EdgeInsetsModel.fromJson(json['padding']), + orientation: + $enumDecodeNullable(_$AxisCEnumMap, json['orientation']) ?? + AxisC.horizontal, + inverted: json['inverted'] as bool? ?? false, + thumbStrokeColor: json['thumbStrokeColor'] == null + ? null + : ColorRGBA.fromJson(json['thumbStrokeColor']), + thumbStrokeWidth: (json['thumbStrokeWidth'] as num?)?.toDouble() ?? 0, ); Map _$SliderPropertiesToJson( @@ -361,6 +399,10 @@ Map _$SliderPropertiesToJson( 'valueIndicatorFontSize': instance.valueIndicatorFontSize, 'allowFractionalPoints': instance.allowFractionalPoints, 'thumbRadius': instance.thumbRadius, + 'thumbShape': _$SliderThumbShapeEnumEnumMap[instance.thumbShape]!, + 'thumbWidth': instance.thumbWidth, + 'thumbHeight': instance.thumbHeight, + 'thumbCornerRadius': instance.thumbCornerRadius, 'showThumb': instance.showThumb, 'trackShape': _$SliderTrackShapeEnumEnumMap[instance.trackShape]!, 'tickMarkRadius': instance.tickMarkRadius, @@ -373,6 +415,15 @@ Map _$SliderPropertiesToJson( 'secondaryTrackValue': ?instance.secondaryTrackValue, 'secondaryActiveTrackColor': ?instance.secondaryActiveTrackColor?.toJson(), 'padding': ?instance.padding?.toJson(), + 'orientation': _$AxisCEnumMap[instance.orientation]!, + 'inverted': instance.inverted, + 'thumbStrokeColor': ?instance.thumbStrokeColor?.toJson(), + 'thumbStrokeWidth': instance.thumbStrokeWidth, +}; + +const _$SliderThumbShapeEnumEnumMap = { + SliderThumbShapeEnum.circle: 'circle', + SliderThumbShapeEnum.roundedRect: 'roundedRect', }; const _$SliderTrackShapeEnumEnumMap = { @@ -395,3 +446,8 @@ const _$ShowValueIndicatorCEnumMap = { ShowValueIndicatorC.alwaysVisible: 'alwaysVisible', ShowValueIndicatorC.never: 'never', }; + +const _$AxisCEnumMap = { + AxisC.horizontal: 'horizontal', + AxisC.vertical: 'vertical', +}; diff --git a/lib/src/api/nodes/spacer_node.g.dart b/lib/src/api/nodes/spacer_node.g.dart index 9459359..1ecf7a1 100644 --- a/lib/src/api/nodes/spacer_node.g.dart +++ b/lib/src/api/nodes/spacer_node.g.dart @@ -124,10 +124,14 @@ SpacerNode _$SpacerNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$SpacerNodeToJson(SpacerNode instance) => { + 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'variables': instance.variables, 'multipleVariables': instance.multipleVariables, 'componentId': ?instance.componentId, @@ -145,7 +149,6 @@ Map _$SpacerNodeToJson(SpacerNode instance) => 'flex': instance.flex, 'squeezable': instance.squeezable, 'aspectRatioLock': instance.aspectRatioLock, - 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'basicBoxLocal': instance.basicBoxLocal.toJson(), 'widthFactor': ?instance.widthFactor, 'heightFactor': ?instance.heightFactor, @@ -156,6 +159,7 @@ Map _$SpacerNodeToJson(SpacerNode instance) => 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, }; diff --git a/lib/src/api/nodes/switch_node.g.dart b/lib/src/api/nodes/switch_node.g.dart index 02e0a7a..793f566 100644 --- a/lib/src/api/nodes/switch_node.g.dart +++ b/lib/src/api/nodes/switch_node.g.dart @@ -175,6 +175,9 @@ SwitchNode _$SwitchNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$SwitchNodeToJson(SwitchNode instance) => @@ -211,6 +214,7 @@ Map _$SwitchNodeToJson(SwitchNode instance) => 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'value': instance.value, 'properties': instance.properties.toJson(), diff --git a/lib/src/api/nodes/tab_bar_node.g.dart b/lib/src/api/nodes/tab_bar_node.g.dart index 5129879..b2eff4e 100644 --- a/lib/src/api/nodes/tab_bar_node.g.dart +++ b/lib/src/api/nodes/tab_bar_node.g.dart @@ -210,6 +210,9 @@ TabBarNode _$TabBarNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..scrollDirection = $enumDecode(_$AxisCEnumMap, json['scrollDirection']) ..reverse = json['reverse'] as bool ..primary = json['primary'] as bool @@ -254,6 +257,7 @@ Map _$TabBarNodeToJson(TabBarNode instance) => 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'isScrollable': instance.isScrollable, 'scrollDirection': _$AxisCEnumMap[instance.scrollDirection]!, 'reverse': instance.reverse, diff --git a/lib/src/api/nodes/text_field_node.g.dart b/lib/src/api/nodes/text_field_node.g.dart index a5d119f..cf1b249 100644 --- a/lib/src/api/nodes/text_field_node.g.dart +++ b/lib/src/api/nodes/text_field_node.g.dart @@ -199,6 +199,9 @@ TextFieldNode _$TextFieldNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$TextFieldNodeToJson(TextFieldNode instance) => @@ -235,6 +238,7 @@ Map _$TextFieldNodeToJson(TextFieldNode instance) => 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'properties': instance.properties.toJson(), 'initialText': instance.initialText, diff --git a/lib/src/api/nodes/text_node.dart b/lib/src/api/nodes/text_node.dart index fb8d6db..3ed9104 100644 --- a/lib/src/api/nodes/text_node.dart +++ b/lib/src/api/nodes/text_node.dart @@ -148,6 +148,7 @@ class TextNode extends SceneNode with BlendMixin, TextMixin, FontMixin { List get triggerTypes => [ TriggerType.click, TriggerType.longPress, + TriggerType.doubleTap, TriggerType.cursorEnter, TriggerType.cursorExit, ]; diff --git a/lib/src/api/nodes/text_node.g.dart b/lib/src/api/nodes/text_node.g.dart index cb168b4..d1232ac 100644 --- a/lib/src/api/nodes/text_node.g.dart +++ b/lib/src/api/nodes/text_node.g.dart @@ -221,18 +221,14 @@ TextNode _$TextNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$TextNodeToJson(TextNode instance) => { 'characters': instance.characters, - 'textAlignHorizontal': - _$TextAlignHorizontalEnumEnumMap[instance.textAlignHorizontal]!, - 'textAlignVertical': - _$TextAlignVerticalEnumEnumMap[instance.textAlignVertical]!, - 'paragraphIndent': instance.paragraphIndent, - 'paragraphSpacing': instance.paragraphSpacing, - 'reactions': instance.reactions.map((e) => e.toJson()).toList(), - 'maxLines': ?instance.maxLines, + 'textMixedProps': instance.textMixedProps.map((e) => e.toJson()).toList(), 'variables': instance.variables, 'multipleVariables': instance.multipleVariables, 'componentId': ?instance.componentId, @@ -250,7 +246,14 @@ Map _$TextNodeToJson(TextNode instance) => { 'flex': instance.flex, 'squeezable': instance.squeezable, 'aspectRatioLock': instance.aspectRatioLock, - 'textMixedProps': instance.textMixedProps.map((e) => e.toJson()).toList(), + 'textAlignHorizontal': + _$TextAlignHorizontalEnumEnumMap[instance.textAlignHorizontal]!, + 'textAlignVertical': + _$TextAlignVerticalEnumEnumMap[instance.textAlignVertical]!, + 'paragraphIndent': instance.paragraphIndent, + 'reactions': instance.reactions.map((e) => e.toJson()).toList(), + 'paragraphSpacing': instance.paragraphSpacing, + 'maxLines': ?instance.maxLines, 'alignment': instance.alignment.toJson(), 'overflow': _$TextOverflowCEnumMap[instance.overflow]!, 'softWrap': instance.softWrap, @@ -268,6 +271,7 @@ Map _$TextNodeToJson(TextNode instance) => { 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'opacity': instance.opacity, 'blendMode': _$BlendModeCEnumMap[instance.blendMode]!, 'isMask': instance.isMask, diff --git a/lib/src/api/nodes/variance_node.g.dart b/lib/src/api/nodes/variance_node.g.dart index b0c8323..fcfd033 100644 --- a/lib/src/api/nodes/variance_node.g.dart +++ b/lib/src/api/nodes/variance_node.g.dart @@ -173,6 +173,9 @@ VarianceNode _$VarianceNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..ephemeral = json['ephemeral'] as bool? ?? false ..allowedTypes = (json['allowedTypes'] as List) .map((e) => e as String) @@ -216,6 +219,7 @@ Map _$VarianceNodeToJson(VarianceNode instance) => 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'ephemeral': instance.ephemeral, 'allowedTypes': instance.allowedTypes, 'deniedTypes': instance.deniedTypes, diff --git a/lib/src/api/nodes/web_view_node.g.dart b/lib/src/api/nodes/web_view_node.g.dart index 8e246d4..769ecf2 100644 --- a/lib/src/api/nodes/web_view_node.g.dart +++ b/lib/src/api/nodes/web_view_node.g.dart @@ -156,11 +156,13 @@ WebViewNode _$WebViewNodeFromJson(Map json) => ..loopAnimation = json['loopAnimation'] == null ? null : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$WebViewNodeToJson(WebViewNode instance) => { - 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'variables': instance.variables, 'multipleVariables': instance.multipleVariables, 'componentId': ?instance.componentId, @@ -178,6 +180,7 @@ Map _$WebViewNodeToJson(WebViewNode instance) => 'flex': instance.flex, 'squeezable': instance.squeezable, 'aspectRatioLock': instance.aspectRatioLock, + 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'alignment': instance.alignment.toJson(), 'basicBoxLocal': instance.basicBoxLocal.toJson(), 'margin': instance.margin.toJson(), @@ -192,6 +195,7 @@ Map _$WebViewNodeToJson(WebViewNode instance) => 'zoomable': instance.zoomable, 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'properties': instance.properties.toJson(), }; diff --git a/lib/src/api/regexes.dart b/lib/src/api/regexes.dart index 5e35f2d..66a07b4 100644 --- a/lib/src/api/regexes.dart +++ b/lib/src/api/regexes.dart @@ -128,9 +128,24 @@ const String variablePathComposingPattern = /// Regex for [variablePathComposingPattern]. final RegExp variablePathComposingRegex = RegExp(variablePathComposingPattern); +const String _formulaAtom = r'''[^{}'"]|'[^']*'|"[^"]*"'''; +const String _formulaBody0 = '(?:$_formulaAtom)*'; +const String _formulaBody1 = '(?:$_formulaAtom|\\{$_formulaBody0\\})*'; +const String _formulaBody2 = '(?:$_formulaAtom|\\{$_formulaBody1\\})*'; + +/// The expression of a `${= ...}` formula: text and quoted strings, with +/// braces only in balanced pairs nested up to three deep, such as the object +/// literal in `${= merge(r, {plays: r.plays + 1})}`. Braces inside quoted +/// strings don't count. +const String formulaExpressionPattern = + '(?:$_formulaAtom|\\{$_formulaBody2\\})*'; + /// A regex that matches any variable path identifier in a string that is -/// wrapped in ${} curly braces. -const String variableSyntaxIdentifierPattern = r'(?[^}]+)\}'; +/// wrapped in ${} curly braces. A formula (`${= ...}`) matches whole, braces +/// of its object literals included, so `${= merge(r, {a: 1})}` is one match +/// rather than ending at the first `}`. +const String variableSyntaxIdentifierPattern = + '(?=$formulaExpressionPattern|[^}]+)\\}'; /// A regex that can match empty ${} syntax with partial match for curly braces. const String emptyVariableSyntaxIdentifierPattern = r'^(? r.crate == crateId)}', + ); + expect(computed.isComputed, isTrue); + expect(computed.value, r'${= filter(records, r => r.crate == crateId)}'); + expect( + VariableData(id: 'w', name: 'plain', value: 'jazz').isComputed, + isFalse, + ); + }); + + test('a formula stored in its own field is read into the value', () { + final VariableData variable = VariableData.fromJson({ + 'id': 'v', + 'name': 'frontRec', + 'type': 'map', + 'formula': r'${= visibleRecords[front]}', + }); + expect(variable.value, r'${= visibleRecords[front]}'); + expect(variable.isComputed, isTrue); + expect(variable.toJson().containsKey('formula'), isFalse); + + final CanvasVariableData canvasVariable = CanvasVariableData.fromJson( + { + 'id': 'v', + 'canvasId': 'c', + 'name': 'count', + 'type': 'integer', + 'formula': r'${= length(records)}', + }, + ); + expect(canvasVariable.value, r'${= length(records)}'); + }); +} diff --git a/test/double_tap_hover_slider_test.dart b/test/double_tap_hover_slider_test.dart new file mode 100644 index 0000000..cd0f717 --- /dev/null +++ b/test/double_tap_hover_slider_test.dart @@ -0,0 +1,102 @@ +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:test/test.dart'; + +void main() { + test('Double tap and hover triggers survive JSON', () { + for (final type in [ + TriggerType.doubleTap, + TriggerType.cursorEnter, + TriggerType.cursorExit, + ]) { + final reaction = Reaction( + trigger: TriggerModel(type: type), + action: CallFunctionAction(name: 'f'), + ); + final json = reaction.toJson(); + expect(json['trigger'], {'type': type.name}); + expect(Reaction.fromJson(json).trigger.type, type); + } + expect(TriggerType.doubleTap.prettify, 'Double Tap'); + }); + + test( + 'Trigger JSON with the earlier hover names reads as cursor triggers', + () { + expect( + TriggerModel.fromJson({'type': 'hoverEnter'}).type, + TriggerType.cursorEnter, + ); + expect( + TriggerModel.fromJson({'type': 'hoverExit'}).type, + TriggerType.cursorExit, + ); + expect(TriggerType.hovers, [ + TriggerType.cursorEnter, + TriggerType.cursorExit, + ]); + }, + ); + + test('Shapes, buttons, icons and sliders offer the new triggers', () { + final box = NodeBox(0, 0, 10, 10); + final frame = FrameNode( + id: 'f', + name: 'f', + basicBoxLocal: box, + children: const [], + ); + final rect = RectangleNode(id: 'r', name: 'r', basicBoxLocal: box); + final slider = SliderNode( + id: 's', + name: 's', + basicBoxLocal: box, + value: 0, + properties: SliderProperties(), + ); + for (final ReactionMixin node in [frame, rect]) { + expect( + node.triggerTypes, + containsAll([ + TriggerType.click, + TriggerType.doubleTap, + ...TriggerType.hovers, + ]), + ); + } + expect(slider.triggerTypes.first, TriggerType.changed); + expect(slider.triggerTypes, contains(TriggerType.doubleTap)); + expect( + slider.propertyVariables.map((v) => v.name), + containsAll(['value', 'min', 'max']), + ); + }); + + test('Slider orientation, inversion and thumb stroke survive JSON', () { + final props = SliderProperties( + min: -8, + max: 8, + orientation: AxisC.vertical, + inverted: true, + thumbStrokeColor: ColorRGBA.white, + thumbStrokeWidth: 2, + ); + final json = props.toJson(); + expect(json['orientation'], 'vertical'); + expect(json['inverted'], true); + expect(json['thumbStrokeWidth'], 2); + expect(json['thumbStrokeColor'], isNotNull); + final back = SliderProperties.fromJson(json); + expect(back, props); + + final defaults = SliderProperties().toJson(); + for (final key in [ + 'orientation', + 'inverted', + 'thumbStrokeWidth', + 'thumbStrokeColor', + ]) { + expect(defaults.containsKey(key), isFalse, reason: key); + } + expect(SliderProperties.fromJson({}).orientation, AxisC.horizontal); + }); +} diff --git a/test/edge_insets_json_test.dart b/test/edge_insets_json_test.dart new file mode 100644 index 0000000..0a9c96b --- /dev/null +++ b/test/edge_insets_json_test.dart @@ -0,0 +1,31 @@ +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:test/test.dart'; + +void main() { + test('padding reads spelled-out sides', () { + expect( + EdgeInsetsModel.fromJson({'left': 1, 'top': 2, 'right': 3, 'bottom': 4}), + const EdgeInsetsModel.only(left: 1, top: 2, right: 3, bottom: 4), + ); + expect( + EdgeInsetsModel.fromJson({'horizontal': 16, 'vertical': 8}), + const EdgeInsetsModel.symmetric(horizontal: 16, vertical: 8), + ); + expect( + EdgeInsetsModel.fromJson({'top': 12}), + const EdgeInsetsModel.only(top: 12), + ); + }); + + test('the stored forms still read', () { + expect(EdgeInsetsModel.fromJson(8), const EdgeInsetsModel.all(8)); + expect( + EdgeInsetsModel.fromJson([1, 2, 3, 4]), + const EdgeInsetsModel.only(left: 1, top: 2, right: 3, bottom: 4), + ); + expect( + EdgeInsetsModel.fromJson({'l': 1, 't': 2, 'r': 3, 'b': 4}), + const EdgeInsetsModel.only(left: 1, top: 2, right: 3, bottom: 4), + ); + }); +} diff --git a/test/list_wrap_test.dart b/test/list_wrap_test.dart new file mode 100644 index 0000000..52b5262 --- /dev/null +++ b/test/list_wrap_test.dart @@ -0,0 +1,51 @@ +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:test/test.dart'; + +void main() { + test('wrap settings survive JSON and are omitted at their defaults', () { + expect( + ListViewProperties().toJson().keys, + isNot( + anyOf( + contains('wrap'), + contains('wrapSpacing'), + contains('wrapRunSpacing'), + contains('wrapAlignment'), + ), + ), + ); + + final ListViewProperties props = ListViewProperties( + wrap: true, + wrapSpacing: 8, + wrapRunSpacing: 6, + wrapAlignment: MainAxisAlignmentC.center, + ); + final Map json = props.toJson(); + expect(json['wrap'], isTrue); + expect(json['wrapSpacing'], 8); + expect(json['wrapRunSpacing'], 6); + expect(json['wrapAlignment'], 'center'); + + final ListViewProperties copy = ListViewProperties.fromJson(json); + expect(copy, props); + expect(copy.isWrapping, isTrue); + expect(props.copyWith(), props); + expect(props.copyWith(stacked: true).isWrapping, isFalse); + }); + + test('a wrapping list node round-trips', () { + final ListViewNode node = ListViewNode( + id: 'list', + name: 'Crates', + basicBoxLocal: NodeBox(0, 0, 300, 40), + properties: ListViewProperties(wrap: true, wrapSpacing: 4), + children: const [], + scrollDirection: AxisC.horizontal, + ); + final ListViewNode copy = ListViewNode.fromJson(node.toJson()); + expect(copy.properties.wrap, isTrue); + expect(copy.properties.wrapSpacing, 4); + expect(copy.scrollDirection, AxisC.horizontal); + }); +} diff --git a/test/nodes/row_column_item_spacing_test.dart b/test/nodes/row_column_item_spacing_test.dart new file mode 100644 index 0000000..9eeba69 --- /dev/null +++ b/test/nodes/row_column_item_spacing_test.dart @@ -0,0 +1,30 @@ +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:test/test.dart'; + +Map _json(Map extra) => { + 'id': 'r', + 'name': 'Row', + 'type': 'rowColumn', + 'rowColumnType': 'row', + 'basicBoxLocal': [0, 0, 100, 40], + 'children': [], + ...extra, +}; + +void main() { + test('itemSpacing, an earlier name for spacing, is read as spacing', () { + final RowColumnNode node = RowColumnNode.fromJson( + _json({'itemSpacing': 12}), + ); + expect(node.spacing, 12); + expect(node.toJson()['spacing'], 12); + expect(node.toJson().containsKey('itemSpacing'), isFalse); + }); + + test('spacing wins over itemSpacing', () { + final RowColumnNode node = RowColumnNode.fromJson( + _json({'itemSpacing': 12, 'spacing': 4}), + ); + expect(node.spacing, 4); + }); +} diff --git a/test/placeholder_json_defaults_test.dart b/test/placeholder_json_defaults_test.dart new file mode 100644 index 0000000..46596e9 --- /dev/null +++ b/test/placeholder_json_defaults_test.dart @@ -0,0 +1,22 @@ +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:test/test.dart'; + +void main() { + test('A list parses from hand-written JSON with only what it needs', () { + for (final type in ['listView']) { + final node = NodeJsonConverter().fromJson({ + 'type': type, + 'id': 'x', + 'name': 'n', + 'basicBoxLocal': [0, 0, 100, 100], + 'children': ['item'], + 'properties': {}, + }) as SinglePlaceholderNode; + expect(node.allowedTypes, isEmpty, reason: type); + // A list can't hold another list directly. + expect(node.deniedTypes, ['listView'], reason: type); + expect((node as ListViewNode).isScrollable, isTrue); + expect(node.useFlutterListView, isTrue); + } + }); +} diff --git a/test/slider_thumb_motion_entrance_test.dart b/test/slider_thumb_motion_entrance_test.dart new file mode 100644 index 0000000..fa860f2 --- /dev/null +++ b/test/slider_thumb_motion_entrance_test.dart @@ -0,0 +1,108 @@ +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:test/test.dart'; + +void main() { + group('Slider thumb shape', () { + test('defaults to a circle and omits the rectangle settings', () { + final SliderProperties properties = SliderProperties(); + expect(properties.thumbShape, SliderThumbShapeEnum.circle); + expect(properties.thumbWidth, kSliderDefaultThumbSize); + expect(properties.thumbHeight, kSliderDefaultThumbSize); + expect(properties.thumbCornerRadius, kSliderDefaultThumbCornerRadius); + final Map json = properties.toJson(); + for (final String key in [ + 'thumbShape', + 'thumbWidth', + 'thumbHeight', + 'thumbCornerRadius', + ]) { + expect(json.containsKey(key), isFalse, reason: key); + } + }); + + test('a rounded rectangle thumb round-trips', () { + final SliderProperties properties = SliderProperties.fromJson({ + 'thumbShape': 'roundedRect', + 'thumbWidth': 44, + 'thumbHeight': 14, + 'thumbCornerRadius': 2, + }); + expect(properties.thumbShape, SliderThumbShapeEnum.roundedRect); + expect(properties.toJson(), { + 'thumbShape': 'roundedRect', + 'thumbWidth': 44.0, + 'thumbHeight': 14.0, + 'thumbCornerRadius': 2.0, + }); + expect(SliderProperties.fromJson(properties.toJson()), properties); + expect( + properties.copyWith(thumbShape: SliderThumbShapeEnum.circle).thumbWidth, + 44, + ); + }); + + test('padding 0 is kept; no padding stays null', () { + expect(SliderProperties.fromJson({}).padding, isNull); + final SliderProperties zero = SliderProperties.fromJson({'padding': 0}); + expect(zero.padding, EdgeInsetsModel.zero); + expect(zero.toJson(), {'padding': 0}); + }); + }); + + group('Motion entrance', () { + test('enterFrom implies an entrance that starts from its values', () { + const MotionModel motion = MotionModel( + opacity: 0.8, + durationMs: 300, + enterFrom: MotionModel(scale: 1.15, opacity: 0), + ); + expect(motion.hasEntrance, isTrue); + expect(MotionModel.none.hasEntrance, isFalse); + expect(const MotionModel(enter: true).hasEntrance, isTrue); + final MotionModel start = motion.entranceStart; + expect(start.scale, 1.15); + expect(start.opacity, 0); + expect(start.durationMs, 300); + expect(const MotionModel(scale: 2).entranceStart.scale, 1); + }); + + test('enterFrom and replayKey round-trip and clear', () { + const MotionModel motion = MotionModel( + enterFrom: MotionModel(translateY: 12), + replayKey: 'side-a', + ); + expect(motion.toJson(), { + 'enterFrom': {'translateY': 12.0}, + 'replayKey': 'side-a', + }); + expect(MotionModel.fromJson(motion.toJson()), motion); + expect( + motion.copyWith(clearEnterFrom: true, clearReplayKey: true), + MotionModel.none, + ); + expect( + MotionModel.variableKey(MotionModel.replayKeyProperty), + 'motion.replayKey', + ); + }); + }); + + test('variable syntax matches keep a formula with object literals whole', () { + const String formula = + r'${= map(records, r => r.id == x ? merge(r, {plays: r.plays + 1}) : r)}'; + expect( + [ + for (final m in variableSyntaxIdentifierRegex.allMatches( + 'a $formula b \${c.d} \${= {k: {v: 1}}.k.v}', + )) + m[0], + ], + [formula, r'${c.d}', r'${= {k: {v: 1}}.k.v}'], + ); + // Renaming a variable leaves formulas as they are and renames paths. + expect( + 'x $formula \${records}'.replaceVariableName('records', 'crate'), + 'x $formula \${crate}', + ); + }); +} diff --git a/test/style_property_variables_test.dart b/test/style_property_variables_test.dart new file mode 100644 index 0000000..5a1972d --- /dev/null +++ b/test/style_property_variables_test.dart @@ -0,0 +1,55 @@ +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:test/test.dart'; + +void main() { + test('A shape offers its effects, stroke weight and corner radius to set ' + 'value actions', () { + final RectangleNode node = RectangleNode( + id: 'chip', + name: 'Chip', + basicBoxLocal: NodeBox(0, 0, 80, 32), + strokeWeight: 2, + cornerRadius: const CornerRadius.all(RadiusModel.circular(6)), + effects: [ + Effect.dropShadow( + id: 'shadow', + visible: false, + radius: 0, + color: ColorRGBA.black, + offset: const Vec(2, 2), + spread: 0, + ), + ], + ); + final Map values = { + for (final ValueModel value in node.propertyVariables) + value.name: value.value, + }; + expect(values['effectsVisible'], isFalse); + expect(values['strokeWeight'], 2); + expect(values['cornerRadius'], 6); + }); + + test('A text offers its font weight as a number', () { + final TextNode node = TextNode( + id: 'text', + name: 'Text', + basicBoxLocal: NodeBox(0, 0, 80, 20), + characters: 'Oat', + textMixedProps: [ + StartEndProp( + start: 0, + end: 3, + fontName: const FontName( + family: 'Roboto', + weight: FontWeightNumeric.w700, + ), + ), + ], + ); + final ValueModel weight = node.propertyVariables.firstWhere( + (value) => value.name == 'fontWeight', + ); + expect(weight.value, 700); + }); +}