diff --git a/CHANGELOG.md b/CHANGELOG.md index 07d5136d..dc60a8a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,44 @@ +## Unreleased + +### New + +- `SignatureNode` (`signature`), a draw-to-sign pad. Its value is the list + of strokes drawn (each a list of `[x, y]` points), bindable through + `variables.value` and settable with a set value action; `changed` fires + when a stroke ends. Properties: ink color and width, background, border + and error border, corner radius, placeholder, sign line, and bindable + `error` and `enabled`. +- `BaseNode.loopAnimation` (`LoopAnimation`: pulse, fade, scale or spin, + with duration, delay, reverse, curve and from/to scale, opacity or + turns), on every node type and left out of the JSON when null. + `loopAnimationEnabled` can be bound to turn it on and off. +- Nodes with a click trigger also offer `cursorEnter` and `cursorExit`; + the trigger docs say what `hover`, `cursorEnter` and `cursorExit` mean. +- `DialogPresentation.sideSheet`, and `ShowDialogAction.sheetWidth`, + `maxHeightFactor` (1: no cap) and `showDragHandle` (false) for sheets, + so saved bottom sheets look as before. + `copyWith` keeps `nonBlocking` and `enabled`. +- `ConfirmAction.presentation`, `sheetWidth`, `maxHeightFactor` and + `showDragHandle`, as on `ShowDialogAction`. +- `ShowToastAction.accentColor`, `accentWidth` (4), `iconColor`, + `fontFamily` and `cornerRadius`. +- `FrameNode.contentFit` (a `Fit`, null for none) and `contentAlignment` + (center). +- `TextFieldNode.propertyVariables` lists `errorText` and `error`. +- All new fields are left out of the JSON at their defaults. +- `GridDelegateProperties.shrinkWrapRows`: each row of a grid is as tall + as its tallest item (like a `Row` in an `IntrinsicHeight`) instead of a + fixed `mainAxisExtent` or `childAspectRatio`. Default false, left out of + the JSON. + +### Fixes + +- `SpacerNode.fromJson` defaults `positioningMode` (align), + `aspectRatioLock` (false) and `reactions` ([]) like every other node, and + `GridViewNode.fromJson` defaults `isScrollable` (true) and + `useFlutterListView` (false). Both still write those keys for older + readers. + ## 6.2.0 ### Layout diff --git a/README.md b/README.md index 3dfa1cb6..e57967d5 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,7 @@ or image texture. - `PaintModel`: Defines the style properties such as the type of paint (`type`), its visibility (`visible`), and its opacity (`opacity`), among others. Depending on the type of paint, it might have properties like color (`color`), gradient transform (`gradientTransform`), gradient stops (`gradientStops`), or image transform (`imageTransform`). + A gradient can repeat past its ends (`tileMode`) over a set length in pixels (`gradientExtent`), which makes stripes. ### Flutter parity diff --git a/lib/src/api/models/action/confirm_action.dart b/lib/src/api/models/action/confirm_action.dart index 7d39e4ae..8734f438 100644 --- a/lib/src/api/models/action/confirm_action.dart +++ b/lib/src/api/models/action/confirm_action.dart @@ -18,6 +18,9 @@ part 'confirm_action.g.dart'; /// back" action whose params hold [resultKey]: `true` confirms, anything else /// cancels. Closing the dialog any other way cancels. The params popped with a /// confirm are the `${result}` the [actions] read. +/// +/// Either dialog can be presented as a centered dialog, a bottom sheet or a +/// side sheet ([presentation]), like [ShowDialogAction.presentation]. @JsonSerializable() class ConfirmAction extends ActionModel with Equatable, SerializableMixin { /// The dialog's title. Supports variables and formulas. @@ -71,6 +74,23 @@ class ConfirmAction extends ActionModel with Equatable, SerializableMixin { /// Corner radius of the built-in dialog. Null follows the app's buttons. final double? cornerRadius; + /// How the dialog is presented: centered, as a bottom sheet or as a side + /// sheet. See [ShowDialogAction.presentation]. + @JsonKey(unknownEnumValue: DialogPresentation.dialog) + final DialogPresentation presentation; + + /// Width of a [DialogPresentation.sideSheet], in logical pixels. Null takes + /// the [dialogId] canvas's width, or 400 for the built-in dialog. + final double? sheetWidth; + + /// The most of the screen's height a [DialogPresentation.bottomSheet] takes, + /// from 0 to 1. + final double maxHeightFactor; + + /// Whether a [DialogPresentation.bottomSheet] shows a grab handle at its + /// top. + final bool showDragHandle; + /// The key of the "Navigate back" params that answers a [dialogId] dialog. static const String resultKey = 'confirmed'; @@ -107,6 +127,10 @@ class ConfirmAction extends ActionModel with Equatable, SerializableMixin { this.textColor, this.confirmColor, this.cornerRadius, + this.presentation = DialogPresentation.dialog, + this.sheetWidth, + this.maxHeightFactor = 1, + this.showDragHandle = false, super.nonBlocking, super.enabled, }) : actions = actions ?? [], @@ -137,6 +161,11 @@ class ConfirmAction extends ActionModel with Equatable, SerializableMixin { bool clearConfirmColor = false, double? cornerRadius, bool clearCornerRadius = false, + DialogPresentation? presentation, + double? sheetWidth, + bool clearSheetWidth = false, + double? maxHeightFactor, + bool? showDragHandle, }) => ConfirmAction( title: title ?? this.title, message: message ?? this.message, @@ -155,6 +184,10 @@ class ConfirmAction extends ActionModel with Equatable, SerializableMixin { textColor: clearTextColor ? null : textColor ?? this.textColor, confirmColor: clearConfirmColor ? null : confirmColor ?? this.confirmColor, cornerRadius: clearCornerRadius ? null : cornerRadius ?? this.cornerRadius, + presentation: presentation ?? this.presentation, + sheetWidth: clearSheetWidth ? null : sheetWidth ?? this.sheetWidth, + maxHeightFactor: maxHeightFactor ?? this.maxHeightFactor, + showDragHandle: showDragHandle ?? this.showDragHandle, nonBlocking: nonBlocking, enabled: enabled, ); @@ -176,6 +209,10 @@ class ConfirmAction extends ActionModel with Equatable, SerializableMixin { textColor, confirmColor, cornerRadius, + presentation, + sheetWidth, + maxHeightFactor, + showDragHandle, ]; /// Creates an instance from a JSON object. diff --git a/lib/src/api/models/action/confirm_action.g.dart b/lib/src/api/models/action/confirm_action.g.dart index 93096cc1..5e1888e0 100644 --- a/lib/src/api/models/action/confirm_action.g.dart +++ b/lib/src/api/models/action/confirm_action.g.dart @@ -19,6 +19,12 @@ Map _$ConfirmActionOmitDefaults( if (isJsonDefault(instance.params, {})) json.remove('params'); if (isJsonDefault(instance.barrierDismissible, true)) json.remove('barrierDismissible'); + if (isJsonDefault(instance.presentation, DialogPresentation.dialog)) + json.remove('presentation'); + if (isJsonDefault(instance.maxHeightFactor, 1)) + json.remove('maxHeightFactor'); + if (isJsonDefault(instance.showDragHandle, false)) + json.remove('showDragHandle'); if (isJsonDefault(instance.nonBlocking, false)) json.remove('nonBlocking'); if (isJsonDefault(instance.enabled, true)) json.remove('enabled'); return json; @@ -56,6 +62,16 @@ ConfirmAction _$ConfirmActionFromJson(Map json) => ConfirmAction( ? null : ColorRGBA.fromJson(json['confirmColor']), cornerRadius: (json['cornerRadius'] as num?)?.toDouble(), + presentation: + $enumDecodeNullable( + _$DialogPresentationEnumMap, + json['presentation'], + unknownValue: DialogPresentation.dialog, + ) ?? + DialogPresentation.dialog, + sheetWidth: (json['sheetWidth'] as num?)?.toDouble(), + maxHeightFactor: (json['maxHeightFactor'] as num?)?.toDouble() ?? 1, + showDragHandle: json['showDragHandle'] as bool? ?? false, nonBlocking: json['nonBlocking'] as bool? ?? false, enabled: json['enabled'] as bool? ?? true, )..type = $enumDecode(_$ActionTypeEnumMap, json['type']); @@ -80,8 +96,19 @@ Map _$ConfirmActionToJson(ConfirmAction instance) => 'textColor': ?instance.textColor?.toJson(), 'confirmColor': ?instance.confirmColor?.toJson(), 'cornerRadius': ?instance.cornerRadius, + 'presentation': _$DialogPresentationEnumMap[instance.presentation]!, + 'sheetWidth': ?instance.sheetWidth, + 'maxHeightFactor': instance.maxHeightFactor, + 'showDragHandle': instance.showDragHandle, }; +const _$DialogPresentationEnumMap = { + DialogPresentation.dialog: 'dialog', + DialogPresentation.bottomSheet: 'bottomSheet', + DialogPresentation.adaptive: 'adaptive', + DialogPresentation.sideSheet: 'sideSheet', +}; + const _$ActionTypeEnumMap = { ActionType.navigation: 'navigation', ActionType.showDialog: 'showDialog', diff --git a/lib/src/api/models/action/show_dialog_action.dart b/lib/src/api/models/action/show_dialog_action.dart index e4cfed84..c73beab0 100644 --- a/lib/src/api/models/action/show_dialog_action.dart +++ b/lib/src/api/models/action/show_dialog_action.dart @@ -6,7 +6,9 @@ import '../models.dart'; part 'show_dialog_action.g.dart'; -/// An action that navigates to a new page. +/// An action that shows a canvas over the current page, as a dialog, a +/// bottom sheet or a side sheet (see [presentation]). A "Navigate back" +/// action in the canvas closes it. @JsonSerializable() class ShowDialogAction extends ActionModel with Equatable, SerializableMixin { /// Destination canvas node's ID. @@ -33,6 +35,17 @@ class ShowDialogAction extends ActionModel with Equatable, SerializableMixin { /// default. Bottom sheets always sit at the bottom. final AlignmentModel alignment; + /// Width of a [DialogPresentation.sideSheet], in logical pixels. Null takes + /// the destination canvas's width. + final double? sheetWidth; + + /// The most of the screen's height a bottom sheet takes, from 0 to 1. A + /// taller canvas scrolls within it. + final double maxHeightFactor; + + /// Whether a bottom sheet shows a grab handle at its top. + final bool showDragHandle; + /// Creates a new [ShowDialogAction]. ShowDialogAction({ required this.destinationId, @@ -42,6 +55,9 @@ class ShowDialogAction extends ActionModel with Equatable, SerializableMixin { this.showCloseButton = false, this.presentation = DialogPresentation.dialog, this.alignment = AlignmentModel.center, + this.sheetWidth, + this.maxHeightFactor = 1, + this.showDragHandle = false, super.nonBlocking, super.enabled, }) : params = {...params ?? {}}, @@ -56,6 +72,9 @@ class ShowDialogAction extends ActionModel with Equatable, SerializableMixin { showCloseButton, presentation, alignment, + sheetWidth, + maxHeightFactor, + showDragHandle, ]; /// Duplicates this [ShowDialogAction] with given data overrides. @@ -67,6 +86,10 @@ class ShowDialogAction extends ActionModel with Equatable, SerializableMixin { bool? showCloseButton, DialogPresentation? presentation, AlignmentModel? alignment, + double? sheetWidth, + bool clearSheetWidth = false, + double? maxHeightFactor, + bool? showDragHandle, }) => ShowDialogAction( destinationId: destinationId ?? this.destinationId, params: {...this.params, ...params ?? {}}, @@ -75,6 +98,11 @@ class ShowDialogAction extends ActionModel with Equatable, SerializableMixin { showCloseButton: showCloseButton ?? this.showCloseButton, presentation: presentation ?? this.presentation, alignment: alignment ?? this.alignment, + sheetWidth: clearSheetWidth ? null : sheetWidth ?? this.sheetWidth, + maxHeightFactor: maxHeightFactor ?? this.maxHeightFactor, + showDragHandle: showDragHandle ?? this.showDragHandle, + nonBlocking: nonBlocking, + enabled: enabled, ); /// Creates a new [ShowDialogAction] instance from a JSON data. @@ -99,4 +127,16 @@ enum DialogPresentation { /// A bottom sheet on phones (narrower than 600 px), a dialog otherwise. adaptive, + + /// Anchored to the right edge, as tall as the screen and + /// [ShowDialogAction.sheetWidth] wide. Slides in from the right. + sideSheet; + + /// Displayable name. + String get prettify => switch (this) { + DialogPresentation.dialog => 'Dialog', + DialogPresentation.bottomSheet => 'Bottom Sheet', + DialogPresentation.adaptive => 'Adaptive', + DialogPresentation.sideSheet => 'Side Sheet', + }; } diff --git a/lib/src/api/models/action/show_dialog_action.g.dart b/lib/src/api/models/action/show_dialog_action.g.dart index 45efe467..365505f5 100644 --- a/lib/src/api/models/action/show_dialog_action.g.dart +++ b/lib/src/api/models/action/show_dialog_action.g.dart @@ -20,6 +20,10 @@ Map _$ShowDialogActionOmitDefaults( json.remove('presentation'); if (isJsonDefault(instance.alignment, AlignmentModel.center)) json.remove('alignment'); + if (isJsonDefault(instance.maxHeightFactor, 1)) + json.remove('maxHeightFactor'); + if (isJsonDefault(instance.showDragHandle, false)) + json.remove('showDragHandle'); if (isJsonDefault(instance.nonBlocking, false)) json.remove('nonBlocking'); if (isJsonDefault(instance.enabled, true)) json.remove('enabled'); return json; @@ -49,6 +53,9 @@ ShowDialogAction _$ShowDialogActionFromJson(Map json) => ShowDialogAction( alignment: json['alignment'] == null ? AlignmentModel.center : AlignmentModel.fromJson(json['alignment'] as Map), + sheetWidth: (json['sheetWidth'] as num?)?.toDouble(), + maxHeightFactor: (json['maxHeightFactor'] as num?)?.toDouble() ?? 1, + showDragHandle: json['showDragHandle'] as bool? ?? false, nonBlocking: json['nonBlocking'] as bool? ?? false, enabled: json['enabled'] as bool? ?? true, )..type = $enumDecode(_$ActionTypeEnumMap, json['type']); @@ -65,12 +72,16 @@ Map _$ShowDialogActionToJson(ShowDialogAction instance) => 'showCloseButton': instance.showCloseButton, 'presentation': _$DialogPresentationEnumMap[instance.presentation]!, 'alignment': instance.alignment.toJson(), + 'sheetWidth': ?instance.sheetWidth, + 'maxHeightFactor': instance.maxHeightFactor, + 'showDragHandle': instance.showDragHandle, }; const _$DialogPresentationEnumMap = { DialogPresentation.dialog: 'dialog', DialogPresentation.bottomSheet: 'bottomSheet', DialogPresentation.adaptive: 'adaptive', + DialogPresentation.sideSheet: 'sideSheet', }; const _$ActionTypeEnumMap = { diff --git a/lib/src/api/models/action/show_toast_action.dart b/lib/src/api/models/action/show_toast_action.dart index a7deaf5a..3143f763 100644 --- a/lib/src/api/models/action/show_toast_action.dart +++ b/lib/src/api/models/action/show_toast_action.dart @@ -53,7 +53,9 @@ enum ToastPosition { /// [actions]. /// /// Its colors come from [style] and the app's theme unless [backgroundColor] -/// or [textColor] are set. Its font is the app's. +/// or [textColor] are set. Its font is the app's unless [fontFamily] is set. +/// An [accentColor] draws a bar down its left edge, and [iconColor] colors +/// its [icon]. @JsonSerializable() class ShowToastAction extends ActionModel with Equatable, SerializableMixin { /// The message. Supports variables and formulas, e.g. @@ -91,6 +93,24 @@ class ShowToastAction extends ActionModel with Equatable, SerializableMixin { /// [ToastPosition.bottom]. final double bottomOffset; + /// Color of a bar down the toast's left edge, e.g. a yellow or red accent. + /// Null draws no bar. + final ColorRGBA? accentColor; + + /// Width of the [accentColor] bar. + final double accentWidth; + + /// Color of the [icon]. Null takes the message color. + final ColorRGBA? iconColor; + + /// Font family of the message and button, e.g. `Barlow`. Null takes the + /// app's font. + final String? fontFamily; + + /// Corner radius of the toast. Null makes a pill, or follows the app's + /// buttons when the toast has a button. + final double? cornerRadius; + /// Creates a new [ShowToastAction]. ShowToastAction({ required this.message, @@ -103,6 +123,11 @@ class ShowToastAction extends ActionModel with Equatable, SerializableMixin { List? actions, this.icon, this.bottomOffset = 104, + this.accentColor, + this.accentWidth = 4, + this.iconColor, + this.fontFamily, + this.cornerRadius, super.nonBlocking, super.enabled, }) : actions = actions ?? [], @@ -123,6 +148,11 @@ class ShowToastAction extends ActionModel with Equatable, SerializableMixin { actions, icon, bottomOffset, + accentColor, + accentWidth, + iconColor, + fontFamily, + cornerRadius, ]; /// Duplicates this [ShowToastAction] with given data overrides. @@ -140,6 +170,15 @@ class ShowToastAction extends ActionModel with Equatable, SerializableMixin { MultiSourceIconModel? icon, bool clearIcon = false, double? bottomOffset, + ColorRGBA? accentColor, + bool clearAccentColor = false, + double? accentWidth, + ColorRGBA? iconColor, + bool clearIconColor = false, + String? fontFamily, + bool clearFontFamily = false, + double? cornerRadius, + bool clearCornerRadius = false, }) => ShowToastAction( message: message ?? this.message, duration: duration ?? this.duration, @@ -153,6 +192,11 @@ class ShowToastAction extends ActionModel with Equatable, SerializableMixin { actions: actions ?? this.actions, icon: clearIcon ? null : icon ?? this.icon, bottomOffset: bottomOffset ?? this.bottomOffset, + accentColor: clearAccentColor ? null : accentColor ?? this.accentColor, + accentWidth: accentWidth ?? this.accentWidth, + iconColor: clearIconColor ? null : iconColor ?? this.iconColor, + fontFamily: clearFontFamily ? null : fontFamily ?? this.fontFamily, + cornerRadius: clearCornerRadius ? null : cornerRadius ?? this.cornerRadius, nonBlocking: nonBlocking, enabled: enabled, ); diff --git a/lib/src/api/models/action/show_toast_action.g.dart b/lib/src/api/models/action/show_toast_action.g.dart index 684de2c2..8e5996c1 100644 --- a/lib/src/api/models/action/show_toast_action.g.dart +++ b/lib/src/api/models/action/show_toast_action.g.dart @@ -17,6 +17,7 @@ Map _$ShowToastActionOmitDefaults( if (isJsonDefault(instance.actionLabel, '')) json.remove('actionLabel'); if (isJsonDefault(instance.actions, [])) json.remove('actions'); if (isJsonDefault(instance.bottomOffset, 104)) json.remove('bottomOffset'); + if (isJsonDefault(instance.accentWidth, 4)) json.remove('accentWidth'); if (isJsonDefault(instance.nonBlocking, false)) json.remove('nonBlocking'); if (isJsonDefault(instance.enabled, true)) json.remove('enabled'); return json; @@ -57,6 +58,15 @@ ShowToastAction _$ShowToastActionFromJson(Map json) => ShowToastAction( ? null : MultiSourceIconModel.fromJson(json['icon'] as Map), bottomOffset: (json['bottomOffset'] as num?)?.toDouble() ?? 104, + accentColor: json['accentColor'] == null + ? null + : ColorRGBA.fromJson(json['accentColor']), + accentWidth: (json['accentWidth'] as num?)?.toDouble() ?? 4, + iconColor: json['iconColor'] == null + ? null + : ColorRGBA.fromJson(json['iconColor']), + fontFamily: json['fontFamily'] as String?, + cornerRadius: (json['cornerRadius'] as num?)?.toDouble(), nonBlocking: json['nonBlocking'] as bool? ?? false, enabled: json['enabled'] as bool? ?? true, )..type = $enumDecode(_$ActionTypeEnumMap, json['type']); @@ -76,6 +86,11 @@ Map _$ShowToastActionToJson(ShowToastAction instance) => 'actions': instance.actions.map((e) => e.toJson()).toList(), 'icon': ?instance.icon?.toJson(), 'bottomOffset': instance.bottomOffset, + 'accentColor': ?instance.accentColor?.toJson(), + 'accentWidth': instance.accentWidth, + 'iconColor': ?instance.iconColor?.toJson(), + 'fontFamily': ?instance.fontFamily, + 'cornerRadius': ?instance.cornerRadius, }; const _$ToastStyleEnumMap = { diff --git a/lib/src/api/models/loop_animation.dart b/lib/src/api/models/loop_animation.dart new file mode 100644 index 00000000..776d7b2f --- /dev/null +++ b/lib/src/api/models/loop_animation.dart @@ -0,0 +1,225 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import '../mixins.dart'; +import 'models.dart'; + +part 'loop_animation.g.dart'; + +/// What a [LoopAnimation] animates. +enum LoopAnimationType { + /// Scales the node from [LoopAnimation.scaleFrom] to + /// [LoopAnimation.scaleTo] while fading it from [LoopAnimation.opacityFrom] + /// to [LoopAnimation.opacityTo]: a halo that grows and fades out. + pulse, + + /// Fades the node from [LoopAnimation.opacityFrom] to + /// [LoopAnimation.opacityTo]: a skeleton bar that breathes. + fade, + + /// Scales the node from [LoopAnimation.scaleFrom] to + /// [LoopAnimation.scaleTo]. + scale, + + /// Rotates the node by [LoopAnimation.turns] full turns each cycle: a + /// spinner. + spin; + + /// Displayable string representation of the type. + String get prettify => switch (this) { + LoopAnimationType.pulse => 'Pulse', + LoopAnimationType.fade => 'Fade', + LoopAnimationType.scale => 'Scale', + LoopAnimationType.spin => 'Spin', + }; + + /// Whether this type animates the node's scale. + bool get scales => this == pulse || this == scale; + + /// Whether this type animates the node's opacity. + bool get fades => this == pulse || this == fade; +} + +/// The easing curve of a [LoopAnimation]. Mirrors Flutter's `Curves`. +enum LoopAnimationCurve { + /// `Curves.linear`. + linear, + + /// `Curves.easeIn`. + easeIn, + + /// `Curves.easeOut`. + easeOut, + + /// `Curves.easeInOut`. + easeInOut, + + /// `Curves.fastOutSlowIn`. + fastOutSlowIn, + + /// `Curves.decelerate`. + decelerate; + + /// Displayable string representation of the curve. + String get prettify => switch (this) { + LoopAnimationCurve.linear => 'Linear', + LoopAnimationCurve.easeIn => 'Ease In', + LoopAnimationCurve.easeOut => 'Ease Out', + LoopAnimationCurve.easeInOut => 'Ease In Out', + LoopAnimationCurve.fastOutSlowIn => 'Fast Out Slow In', + LoopAnimationCurve.decelerate => 'Decelerate', + }; +} + +/// An animation that repeats for as long as its node is shown: a pulsing +/// halo, a breathing skeleton bar, a spinning icon. +/// +/// Each cycle runs from the `from` values to the `to` values in +/// [durationMs]. With [reverse], it then runs back to the `from` values; +/// otherwise it jumps back and starts over. +/// +/// The node's own opacity, rotation and size stay as designed; the animated +/// values multiply them. The design canvas holds the animation still at its +/// `from` values; previews and published apps run it. +/// +/// Bind the node's `loopAnimationEnabled` variable to turn it on and off at +/// runtime. +@JsonSerializable() +class LoopAnimation with Equatable, SerializableMixin { + /// What this animation animates. + @JsonKey(unknownEnumValue: LoopAnimationType.pulse) + final LoopAnimationType type; + + /// Whether the animation runs. A disabled animation leaves the node as + /// designed. + final bool enabled; + + /// How long one cycle takes, in milliseconds. With [reverse], running back + /// takes as long again. + final int durationMs; + + /// How long to wait before the first cycle, in milliseconds. + final int delayMs; + + /// Whether each cycle runs back to the `from` values instead of jumping + /// back to them. + final bool reverse; + + /// The easing curve of each cycle. + @JsonKey(unknownEnumValue: LoopAnimationCurve.linear) + final LoopAnimationCurve curve; + + /// The scale at the start of a cycle, for [LoopAnimationType.pulse] and + /// [LoopAnimationType.scale]. The node scales about its center. + final double scaleFrom; + + /// The scale at the end of a cycle. + final double scaleTo; + + /// The opacity at the start of a cycle, for [LoopAnimationType.pulse] and + /// [LoopAnimationType.fade]. + final double opacityFrom; + + /// The opacity at the end of a cycle. + final double opacityTo; + + /// How many full turns the node rotates each cycle, for + /// [LoopAnimationType.spin]. Negative turns rotate counterclockwise. + final double turns; + + /// Creates a [LoopAnimation]. + const LoopAnimation({ + required this.type, + this.enabled = true, + this.durationMs = 1000, + this.delayMs = 0, + this.reverse = false, + this.curve = LoopAnimationCurve.linear, + this.scaleFrom = 1, + this.scaleTo = 1, + this.opacityFrom = 1, + this.opacityTo = 1, + this.turns = 1, + }); + + /// A [LoopAnimation] of [type] with values that show it off: a halo that + /// grows and fades out, a bar that breathes, a node that spins once a + /// second. + factory LoopAnimation.preset(LoopAnimationType type) => switch (type) { + LoopAnimationType.pulse => const LoopAnimation( + type: LoopAnimationType.pulse, + durationMs: 1400, + curve: LoopAnimationCurve.easeOut, + scaleFrom: 0.65, + scaleTo: 1, + opacityFrom: 0.35, + opacityTo: 0, + ), + LoopAnimationType.fade => const LoopAnimation( + type: LoopAnimationType.fade, + durationMs: 900, + reverse: true, + curve: LoopAnimationCurve.easeInOut, + opacityFrom: 0.45, + opacityTo: 1, + ), + LoopAnimationType.scale => const LoopAnimation( + type: LoopAnimationType.scale, + durationMs: 900, + reverse: true, + curve: LoopAnimationCurve.easeInOut, + scaleFrom: 1, + scaleTo: 1.1, + ), + LoopAnimationType.spin => const LoopAnimation(type: LoopAnimationType.spin), + }; + + /// Duplicates this instance with the given data overrides. + LoopAnimation copyWith({ + LoopAnimationType? type, + bool? enabled, + int? durationMs, + int? delayMs, + bool? reverse, + LoopAnimationCurve? curve, + double? scaleFrom, + double? scaleTo, + double? opacityFrom, + double? opacityTo, + double? turns, + }) => LoopAnimation( + type: type ?? this.type, + enabled: enabled ?? this.enabled, + durationMs: durationMs ?? this.durationMs, + delayMs: delayMs ?? this.delayMs, + reverse: reverse ?? this.reverse, + curve: curve ?? this.curve, + scaleFrom: scaleFrom ?? this.scaleFrom, + scaleTo: scaleTo ?? this.scaleTo, + opacityFrom: opacityFrom ?? this.opacityFrom, + opacityTo: opacityTo ?? this.opacityTo, + turns: turns ?? this.turns, + ); + + @override + List get props => [ + type, + enabled, + durationMs, + delayMs, + reverse, + curve, + scaleFrom, + scaleTo, + opacityFrom, + opacityTo, + turns, + ]; + + /// Creates a [LoopAnimation] from JSON data. + factory LoopAnimation.fromJson(Map json) => _$LoopAnimationFromJson(json); + + @override + Map toJson() => + _$LoopAnimationOmitDefaults(this, _$LoopAnimationToJson(this)); +} diff --git a/lib/src/api/models/loop_animation.g.dart b/lib/src/api/models/loop_animation.g.dart new file mode 100644 index 00000000..8d3fc403 --- /dev/null +++ b/lib/src/api/models/loop_animation.g.dart @@ -0,0 +1,84 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'loop_animation.dart'; + +// ************************************************************************** +// JsonDefaultsGenerator +// ************************************************************************** + +Map _$LoopAnimationOmitDefaults( + LoopAnimation instance, + Map json, +) { + if (isJsonDefault(instance.enabled, true)) json.remove('enabled'); + if (isJsonDefault(instance.durationMs, 1000)) json.remove('durationMs'); + if (isJsonDefault(instance.delayMs, 0)) json.remove('delayMs'); + if (isJsonDefault(instance.reverse, false)) json.remove('reverse'); + if (isJsonDefault(instance.curve, LoopAnimationCurve.linear)) + json.remove('curve'); + if (isJsonDefault(instance.scaleFrom, 1)) json.remove('scaleFrom'); + if (isJsonDefault(instance.scaleTo, 1)) json.remove('scaleTo'); + if (isJsonDefault(instance.opacityFrom, 1)) json.remove('opacityFrom'); + if (isJsonDefault(instance.opacityTo, 1)) json.remove('opacityTo'); + if (isJsonDefault(instance.turns, 1)) json.remove('turns'); + return json; +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +LoopAnimation _$LoopAnimationFromJson(Map json) => LoopAnimation( + type: $enumDecode( + _$LoopAnimationTypeEnumMap, + json['type'], + unknownValue: LoopAnimationType.pulse, + ), + enabled: json['enabled'] as bool? ?? true, + durationMs: (json['durationMs'] as num?)?.toInt() ?? 1000, + delayMs: (json['delayMs'] as num?)?.toInt() ?? 0, + reverse: json['reverse'] as bool? ?? false, + curve: + $enumDecodeNullable( + _$LoopAnimationCurveEnumMap, + json['curve'], + unknownValue: LoopAnimationCurve.linear, + ) ?? + LoopAnimationCurve.linear, + scaleFrom: (json['scaleFrom'] as num?)?.toDouble() ?? 1, + scaleTo: (json['scaleTo'] as num?)?.toDouble() ?? 1, + opacityFrom: (json['opacityFrom'] as num?)?.toDouble() ?? 1, + opacityTo: (json['opacityTo'] as num?)?.toDouble() ?? 1, + turns: (json['turns'] as num?)?.toDouble() ?? 1, +); + +Map _$LoopAnimationToJson(LoopAnimation instance) => + { + 'type': _$LoopAnimationTypeEnumMap[instance.type]!, + 'enabled': instance.enabled, + 'durationMs': instance.durationMs, + 'delayMs': instance.delayMs, + 'reverse': instance.reverse, + 'curve': _$LoopAnimationCurveEnumMap[instance.curve]!, + 'scaleFrom': instance.scaleFrom, + 'scaleTo': instance.scaleTo, + 'opacityFrom': instance.opacityFrom, + 'opacityTo': instance.opacityTo, + 'turns': instance.turns, + }; + +const _$LoopAnimationTypeEnumMap = { + LoopAnimationType.pulse: 'pulse', + LoopAnimationType.fade: 'fade', + LoopAnimationType.scale: 'scale', + LoopAnimationType.spin: 'spin', +}; + +const _$LoopAnimationCurveEnumMap = { + LoopAnimationCurve.linear: 'linear', + LoopAnimationCurve.easeIn: 'easeIn', + LoopAnimationCurve.easeOut: 'easeOut', + LoopAnimationCurve.easeInOut: 'easeInOut', + LoopAnimationCurve.fastOutSlowIn: 'fastOutSlowIn', + LoopAnimationCurve.decelerate: 'decelerate', +}; diff --git a/lib/src/api/models/models.dart b/lib/src/api/models/models.dart index 3b86dfac..7bbdbfd0 100644 --- a/lib/src/api/models/models.dart +++ b/lib/src/api/models/models.dart @@ -52,6 +52,7 @@ export 'input_decoration.dart'; export 'letter_spacing.dart'; export 'line_height.dart'; export 'list_tile_control_affinity.dart'; +export 'loop_animation.dart'; export 'navigation_bar_item.dart'; export 'node_box.dart'; export 'paint.dart'; diff --git a/lib/src/api/models/trigger.dart b/lib/src/api/models/trigger.dart index 8b75dbdd..ad248214 100644 --- a/lib/src/api/models/trigger.dart +++ b/lib/src/api/models/trigger.dart @@ -24,16 +24,20 @@ enum TriggerType { /// Triggered when the user long presses on a target. longPress, - /// Triggered when the user hovers over a target with mouse cursor. + /// Triggered each time the mouse cursor moves over the target, after it + /// entered. Use [cursorEnter] and [cursorExit] to track whether the cursor + /// is over the target: they fire once each. hover, /// Triggered when the user drags the target. drag, - /// Triggered when the cursor enters the target region. + /// Triggered once when the mouse cursor enters the target region. Touch + /// input doesn't fire it. cursorEnter, - /// Triggered when the cursor exits the target region. + /// Triggered once when the mouse cursor exits the target region. Touch + /// input doesn't fire it. cursorExit, /// Triggered when a pointer down event is received on the target. diff --git a/lib/src/api/node_json_converter.dart b/lib/src/api/node_json_converter.dart index 311acd7c..f0c0dfca 100644 --- a/lib/src/api/node_json_converter.dart +++ b/lib/src/api/node_json_converter.dart @@ -74,6 +74,7 @@ class NodeJsonConverter implements JsonConverter { 'badge': BadgeNode.fromJson, 'chip': ChipNode.fromJson, 'segmentedButton': SegmentedButtonNode.fromJson, + 'signature': SignatureNode.fromJson, 'external': ExternalComponentNode.fromJson, }; } diff --git a/lib/src/api/nodes/accordion_node.g.dart b/lib/src/api/nodes/accordion_node.g.dart index f2bc76e5..e4257011 100644 --- a/lib/src/api/nodes/accordion_node.g.dart +++ b/lib/src/api/nodes/accordion_node.g.dart @@ -182,6 +182,9 @@ AccordionNode _$AccordionNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..spacing = (json['spacing'] as num?)?.toDouble() ?? 0.0 ..wrap = json['wrap'] as bool? ?? false ..runSpacing = (json['runSpacing'] as num?)?.toDouble() ?? 0.0 @@ -219,6 +222,7 @@ Map _$AccordionNodeToJson( ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'children': instance.children, 'rowColumnType': _$RowColumnTypeEnumMap[instance.rowColumnType]!, diff --git a/lib/src/api/nodes/app_bar_node.g.dart b/lib/src/api/nodes/app_bar_node.g.dart index de0fa8ba..ef128423 100644 --- a/lib/src/api/nodes/app_bar_node.g.dart +++ b/lib/src/api/nodes/app_bar_node.g.dart @@ -162,6 +162,9 @@ AppBarNode _$AppBarNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$AppBarNodeToJson(AppBarNode instance) => @@ -195,6 +198,7 @@ Map _$AppBarNodeToJson(AppBarNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, '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 68fbcbd2..b5a38d50 100644 --- a/lib/src/api/nodes/auto_placeholder_node.g.dart +++ b/lib/src/api/nodes/auto_placeholder_node.g.dart @@ -234,6 +234,9 @@ AutoPlaceholderNode _$AutoPlaceholderNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..inkWell = json['inkWell'] == null ? null : InkWellModel.fromJson( @@ -247,6 +250,7 @@ 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, @@ -264,7 +268,6 @@ Map _$AutoPlaceholderNodeToJson( 'flex': instance.flex, 'aspectRatioLock': instance.aspectRatioLock, 'alignment': instance.alignment.toJson(), - 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'basicBoxLocal': instance.basicBoxLocal.toJson(), 'margin': instance.margin.toJson(), 'padding': instance.padding.toJson(), @@ -276,6 +279,7 @@ Map _$AutoPlaceholderNodeToJson( ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'children': instance.children, 'opacity': instance.opacity, diff --git a/lib/src/api/nodes/badge_node.dart b/lib/src/api/nodes/badge_node.dart index a72c9e2f..3c053fec 100644 --- a/lib/src/api/nodes/badge_node.dart +++ b/lib/src/api/nodes/badge_node.dart @@ -95,7 +95,11 @@ class BadgeNode extends SceneNode with CustomPropertiesMixin { }; @override - List get triggerTypes => [TriggerType.click]; + List get triggerTypes => [ + TriggerType.click, + TriggerType.cursorEnter, + TriggerType.cursorExit, + ]; @override late final List propertyVariables = [ diff --git a/lib/src/api/nodes/badge_node.g.dart b/lib/src/api/nodes/badge_node.g.dart index 675e8d90..fecc3737 100644 --- a/lib/src/api/nodes/badge_node.g.dart +++ b/lib/src/api/nodes/badge_node.g.dart @@ -164,6 +164,9 @@ BadgeNode _$BadgeNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$BadgeNodeToJson(BadgeNode instance) => { @@ -196,6 +199,7 @@ Map _$BadgeNodeToJson(BadgeNode instance) => { ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'properties': instance.properties.toJson(), diff --git a/lib/src/api/nodes/base_node.dart b/lib/src/api/nodes/base_node.dart index f37c42ec..eb4f3b8b 100644 --- a/lib/src/api/nodes/base_node.dart +++ b/lib/src/api/nodes/base_node.dart @@ -388,6 +388,12 @@ abstract class BaseNode @JsonKey(defaultValue: false) bool zoomable = false; + /// An animation that repeats while this node is shown, e.g. a pulsing + /// halo or a spinning icon. Null when the node doesn't animate. + /// + /// See [LoopAnimation]. + LoopAnimation? loopAnimation; + /// Determines whether the node is enabled or not. When translating the node /// into a widget or code, this value is used to determine whether this node /// should be included and accounted for or not. @@ -780,6 +786,12 @@ abstract class BaseNode () => !enabled, (value) => enabled = !value, ), + 'loopAnimationEnabled': BoolFieldAccess( + 'Animate', + 'Whether the loop animation of this widget runs.', + () => loopAnimation?.enabled ?? false, + (value) => loopAnimation = loopAnimation?.copyWith(enabled: value), + ), }; @override diff --git a/lib/src/api/nodes/button_node.dart b/lib/src/api/nodes/button_node.dart index 76da81c8..14967e1f 100644 --- a/lib/src/api/nodes/button_node.dart +++ b/lib/src/api/nodes/button_node.dart @@ -163,6 +163,8 @@ class ButtonNode extends SceneNode with CustomPropertiesMixin { List get triggerTypes => [ TriggerType.click, TriggerType.longPress, + TriggerType.cursorEnter, + TriggerType.cursorExit, ]; @override diff --git a/lib/src/api/nodes/button_node.g.dart b/lib/src/api/nodes/button_node.g.dart index 443ae349..bdc859bd 100644 --- a/lib/src/api/nodes/button_node.g.dart +++ b/lib/src/api/nodes/button_node.g.dart @@ -173,6 +173,9 @@ ButtonNode _$ButtonNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$ButtonNodeToJson(ButtonNode instance) => @@ -206,6 +209,7 @@ Map _$ButtonNodeToJson(ButtonNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'properties': instance.properties.toJson(), diff --git a/lib/src/api/nodes/canvas_node.g.dart b/lib/src/api/nodes/canvas_node.g.dart index 5f4c9a09..83d39fd0 100644 --- a/lib/src/api/nodes/canvas_node.g.dart +++ b/lib/src/api/nodes/canvas_node.g.dart @@ -288,6 +288,9 @@ CanvasNode _$CanvasNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..inkWell = json['inkWell'] == null ? null : InkWellModel.fromJson( @@ -330,6 +333,7 @@ Map _$CanvasNodeToJson( ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'children': instance.children, 'opacity': instance.opacity, diff --git a/lib/src/api/nodes/checkbox_node.g.dart b/lib/src/api/nodes/checkbox_node.g.dart index 1ba0fa1d..b3cecb9e 100644 --- a/lib/src/api/nodes/checkbox_node.g.dart +++ b/lib/src/api/nodes/checkbox_node.g.dart @@ -168,6 +168,9 @@ CheckboxNode _$CheckboxNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$CheckboxNodeToJson(CheckboxNode instance) => @@ -201,6 +204,7 @@ Map _$CheckboxNodeToJson(CheckboxNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'properties': instance.properties.toJson(), diff --git a/lib/src/api/nodes/chip_node.dart b/lib/src/api/nodes/chip_node.dart index e3570bd3..a73d43f3 100644 --- a/lib/src/api/nodes/chip_node.dart +++ b/lib/src/api/nodes/chip_node.dart @@ -97,6 +97,8 @@ class ChipNode extends SceneNode with CustomPropertiesMixin { List get triggerTypes => [ TriggerType.click, TriggerType.changed, + TriggerType.cursorEnter, + TriggerType.cursorExit, ]; @override diff --git a/lib/src/api/nodes/chip_node.g.dart b/lib/src/api/nodes/chip_node.g.dart index 4b930f59..75498f3c 100644 --- a/lib/src/api/nodes/chip_node.g.dart +++ b/lib/src/api/nodes/chip_node.g.dart @@ -162,6 +162,9 @@ ChipNode _$ChipNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$ChipNodeToJson(ChipNode instance) => { @@ -194,6 +197,7 @@ Map _$ChipNodeToJson(ChipNode instance) => { ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, '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 aa478db5..c7ca4ddd 100644 --- a/lib/src/api/nodes/divider_node.g.dart +++ b/lib/src/api/nodes/divider_node.g.dart @@ -151,6 +151,9 @@ DividerNode _$DividerNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$DividerNodeToJson(DividerNode instance) => @@ -184,6 +187,7 @@ Map _$DividerNodeToJson(DividerNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'properties': instance.properties.toJson(), diff --git a/lib/src/api/nodes/dropdown_node.dart b/lib/src/api/nodes/dropdown_node.dart index 2caef62b..370d1e5e 100644 --- a/lib/src/api/nodes/dropdown_node.dart +++ b/lib/src/api/nodes/dropdown_node.dart @@ -56,6 +56,8 @@ class DropdownNode extends SceneNode with CustomPropertiesMixin, FieldsHolder { List get triggerTypes => [ TriggerType.click, TriggerType.changed, + TriggerType.cursorEnter, + TriggerType.cursorExit, ]; @override diff --git a/lib/src/api/nodes/dropdown_node.g.dart b/lib/src/api/nodes/dropdown_node.g.dart index 8739ea00..48dfbc6f 100644 --- a/lib/src/api/nodes/dropdown_node.g.dart +++ b/lib/src/api/nodes/dropdown_node.g.dart @@ -179,6 +179,9 @@ DropdownNode _$DropdownNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$DropdownNodeToJson(DropdownNode instance) => @@ -212,6 +215,7 @@ Map _$DropdownNodeToJson(DropdownNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'properties': instance.properties.toJson(), diff --git a/lib/src/api/nodes/embedded_video_node.g.dart b/lib/src/api/nodes/embedded_video_node.g.dart index 4b4ef740..2dd9770b 100644 --- a/lib/src/api/nodes/embedded_video_node.g.dart +++ b/lib/src/api/nodes/embedded_video_node.g.dart @@ -188,6 +188,9 @@ EmbeddedVideoNode _$EmbeddedVideoNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$EmbeddedVideoNodeToJson(EmbeddedVideoNode instance) => @@ -221,6 +224,7 @@ Map _$EmbeddedVideoNodeToJson(EmbeddedVideoNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, '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 649183fe..df91c8a2 100644 --- a/lib/src/api/nodes/expansion_tile_node.g.dart +++ b/lib/src/api/nodes/expansion_tile_node.g.dart @@ -196,6 +196,9 @@ ExpansionTileNode _$ExpansionTileNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..rowColumnType = $enumDecode( _$RowColumnTypeEnumMap, json['rowColumnType'], @@ -245,6 +248,7 @@ Map _$ExpansionTileNodeToJson( ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'children': instance.children, 'rowColumnType': _$RowColumnTypeEnumMap[instance.rowColumnType]!, diff --git a/lib/src/api/nodes/external_component_node.dart b/lib/src/api/nodes/external_component_node.dart index 072951a0..20ded28c 100644 --- a/lib/src/api/nodes/external_component_node.dart +++ b/lib/src/api/nodes/external_component_node.dart @@ -56,6 +56,8 @@ class ExternalComponentNode extends SceneNode with CustomPropertiesMixin { List get triggerTypes => [ TriggerType.click, TriggerType.longPress, + TriggerType.cursorEnter, + TriggerType.cursorExit, ]; @override diff --git a/lib/src/api/nodes/external_component_node.g.dart b/lib/src/api/nodes/external_component_node.g.dart index 7eeceb4a..cb608cbf 100644 --- a/lib/src/api/nodes/external_component_node.g.dart +++ b/lib/src/api/nodes/external_component_node.g.dart @@ -150,6 +150,9 @@ ExternalComponentNode _$ExternalComponentNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$ExternalComponentNodeToJson( @@ -184,6 +187,7 @@ Map _$ExternalComponentNodeToJson( ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'properties': instance.properties.toJson(), diff --git a/lib/src/api/nodes/floating_action_button_node.dart b/lib/src/api/nodes/floating_action_button_node.dart index 577f22cb..ddaec9a3 100644 --- a/lib/src/api/nodes/floating_action_button_node.dart +++ b/lib/src/api/nodes/floating_action_button_node.dart @@ -84,7 +84,11 @@ class FloatingActionButtonNode extends SceneNode }); @override - List get triggerTypes => [TriggerType.click]; + List get triggerTypes => [ + TriggerType.click, + TriggerType.cursorEnter, + TriggerType.cursorExit, + ]; @override FieldsMap generateFields() => { 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 d969096e..9155e4d9 100644 --- a/lib/src/api/nodes/floating_action_button_node.g.dart +++ b/lib/src/api/nodes/floating_action_button_node.g.dart @@ -171,6 +171,9 @@ FloatingActionButtonNode _$FloatingActionButtonNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$FloatingActionButtonNodeToJson( @@ -205,6 +208,7 @@ Map _$FloatingActionButtonNodeToJson( ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, '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 6bf1a6f9..2723aff3 100644 --- a/lib/src/api/nodes/frame_node.dart +++ b/lib/src/api/nodes/frame_node.dart @@ -15,6 +15,17 @@ class FrameNode extends DefaultShapeNode @override final bool supportsPadding = true; + /// Scales the frame's content to fit it, like Flutter's [FittedBox]: the + /// children are laid out in the frame's design-size content box and the + /// result is scaled by this fit into the content box the frame gets, e.g. + /// [Fit.contain] to show a fixed drawing as large as it fits. Null (the + /// default) lays the children out in the frame's actual size, unscaled. + Fit? contentFit; + + /// Where scaled content sits in the frame when [contentFit] leaves space + /// on one axis. + AlignmentModel contentAlignment; + /// Frame is a Rectangle with children, which can be clipped. FrameNode({ required super.id, @@ -73,6 +84,8 @@ class FrameNode extends DefaultShapeNode String? pageID, String? layoutID, bool showPortal = false, + this.contentFit, + this.contentAlignment = AlignmentModel.center, }) { setCornerMixin( cornerRadius: cornerRadius, @@ -107,6 +120,8 @@ class FrameNode extends DefaultShapeNode List get triggerTypes => [ TriggerType.click, TriggerType.longPress, + TriggerType.cursorEnter, + TriggerType.cursorExit, ]; /// Creates a [FrameNode] from a JSON data. diff --git a/lib/src/api/nodes/frame_node.g.dart b/lib/src/api/nodes/frame_node.g.dart index 48c894de..7e8f6eb0 100644 --- a/lib/src/api/nodes/frame_node.g.dart +++ b/lib/src/api/nodes/frame_node.g.dart @@ -10,6 +10,8 @@ Map _$FrameNodeOmitDefaults( FrameNode instance, Map json, ) { + if (isJsonDefault(instance.contentAlignment, AlignmentModel.center)) + json.remove('contentAlignment'); if (isJsonDefault(instance.visible, true)) json.remove('visible'); if (excludeConstraintsIf(instance)) json.remove('constraints'); if (isJsonDefault(instance.constraints, const BoxConstraintsModel())) @@ -217,6 +219,10 @@ FrameNode _$FrameNodeFromJson(Map json) => pageID: json['pageID'] as String?, layoutID: json['layoutID'] as String?, showPortal: json['showPortal'] as bool? ?? false, + contentFit: $enumDecodeNullable(_$FitEnumMap, json['contentFit']), + contentAlignment: json['contentAlignment'] == null + ? AlignmentModel.center + : AlignmentModel.fromJson(json['contentAlignment'] as Map), ) ..variables = (json['variables'] as Map?)?.map( @@ -257,6 +263,9 @@ FrameNode _$FrameNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..inkWell = json['inkWell'] == null ? null : InkWellModel.fromJson( @@ -294,6 +303,7 @@ Map _$FrameNodeToJson(FrameNode instance) => { ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'children': instance.children, 'opacity': instance.opacity, @@ -327,6 +337,8 @@ Map _$FrameNodeToJson(FrameNode instance) => { 'layoutID': ?instance.layoutID, 'showPortal': instance.showPortal, 'type': instance.type, + 'contentFit': ?_$FitEnumMap[instance.contentFit], + 'contentAlignment': instance.contentAlignment.toJson(), }; const _$BlendModeCEnumMap = { @@ -412,6 +424,16 @@ const _$ScrollViewKeyboardDismissBehaviorCEnumMap = { ScrollViewKeyboardDismissBehaviorC.onDrag: 'onDrag', }; +const _$FitEnumMap = { + Fit.contain: 'contain', + Fit.cover: 'cover', + Fit.fill: 'fill', + Fit.fitHeight: 'fitHeight', + Fit.fitWidth: 'fitWidth', + Fit.none: 'none', + Fit.scaleDown: 'scaleDown', +}; + const _$ComponentMarkerTypeEnumMap = { ComponentMarkerType.component: 'component', ComponentMarkerType.instance: 'instance', diff --git a/lib/src/api/nodes/freeform_placeholder_node.g.dart b/lib/src/api/nodes/freeform_placeholder_node.g.dart index 1f9d5453..78207e7e 100644 --- a/lib/src/api/nodes/freeform_placeholder_node.g.dart +++ b/lib/src/api/nodes/freeform_placeholder_node.g.dart @@ -205,6 +205,9 @@ FreeformPlaceholderNode _$FreeformPlaceholderNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..inkWell = json['inkWell'] == null ? null : InkWellModel.fromJson( @@ -244,6 +247,7 @@ Map _$FreeformPlaceholderNodeToJson( ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'children': instance.children, 'opacity': instance.opacity, diff --git a/lib/src/api/nodes/grid_view_node.dart b/lib/src/api/nodes/grid_view_node.dart index c9a02eff..057f91a8 100644 --- a/lib/src/api/nodes/grid_view_node.dart +++ b/lib/src/api/nodes/grid_view_node.dart @@ -68,6 +68,11 @@ class GridViewNode extends SinglePlaceholderNode bool clipsContent = true, this.statusVariable, + // A grid always scrolls its items (see [isScrollingEnforced]) and never + // uses a Flutter ListView, so neither has to be in its JSON. + bool isScrollable = true, + bool useFlutterListView = false, + // [CloudDatabaseMixin] properties. bool useCloudDatabase = false, String? collectionPath, @@ -93,14 +98,14 @@ class GridViewNode extends SinglePlaceholderNode setClipMixin(clipsContent: clipsContent); setScrollableMixin( - isScrollable: true, + isScrollable: isScrollable, scrollDirection: scrollDirection, reverse: reverse, physics: physics, primary: primary, shrinkWrap: false, keyboardDismissBehavior: keyboardDismissBehavior, - useFlutterListView: false, + useFlutterListView: useFlutterListView, shouldAlwaysScroll: shouldAlwaysScroll, ); @@ -169,18 +174,20 @@ class GridViewNode extends SinglePlaceholderNode childCrossAxisExtent, ); + // Rows sized to their content leave the item's main axis to its own fit. + final bool shrinkWrapRows = properties.gridDelegate.shrinkWrapRows; return switch (scrollDirection) { AxisC.horizontal => BoxConstraintsModel( - minWidth: childMainAxisExtent, - maxWidth: childMainAxisExtent, + minWidth: shrinkWrapRows ? null : childMainAxisExtent, + maxWidth: shrinkWrapRows ? null : childMainAxisExtent, minHeight: childCrossAxisExtent, maxHeight: childCrossAxisExtent, ), AxisC.vertical => BoxConstraintsModel( minWidth: childCrossAxisExtent, maxWidth: childCrossAxisExtent, - minHeight: childMainAxisExtent, - maxHeight: childMainAxisExtent, + minHeight: shrinkWrapRows ? null : childMainAxisExtent, + maxHeight: shrinkWrapRows ? null : childMainAxisExtent, ), }; } @@ -217,8 +224,21 @@ class GridViewNode extends SinglePlaceholderNode ...json, }); + /// Keys that SDKs up to codelessly_api 6.0.0 require in a grid's JSON. They are written + /// even at their defaults so those SDKs can still read it. + static const List _legacyRequiredKeys = [ + 'isScrollable', + 'useFlutterListView', + ]; + @override - Map toJson() => _$GridViewNodeOmitDefaults(this, _$GridViewNodeToJson(this)); + Map toJson() { + final Map json = _$GridViewNodeToJson(this); + final Map legacy = { + for (final key in _legacyRequiredKeys) key: json[key], + }; + return {..._$GridViewNodeOmitDefaults(this, json), ...legacy}; + } } /// The properties of a [GridViewNode]. @@ -308,6 +328,13 @@ sealed class GridDelegateProperties /// [childAspectRatio] controls the main axis extent of each child. double? mainAxisExtent; + /// Whether each row (each column of a horizontal grid) takes the main axis + /// extent of its largest item instead of a fixed one, like a [Row] of + /// cells in an `IntrinsicHeight`: items keep their own main axis fit and + /// the other cells of the row stretch to match. [mainAxisExtent] and + /// [childAspectRatio] are then ignored. + bool shrinkWrapRows; + /// Creates a new [GridDelegateProperties] instance. GridDelegateProperties( this.type, { @@ -315,6 +342,7 @@ sealed class GridDelegateProperties this.crossAxisSpacing = 0, this.childAspectRatio = 1, this.mainAxisExtent, + this.shrinkWrapRows = false, }); /// Creates a copy of this [GridDelegateProperties] instance with the given @@ -325,6 +353,7 @@ sealed class GridDelegateProperties double? childAspectRatio, double? mainAxisExtent, bool forceMainAxisExtent = false, + bool? shrinkWrapRows, }); factory GridDelegateProperties.fromJson(Map json) { @@ -344,6 +373,7 @@ sealed class GridDelegateProperties crossAxisSpacing, childAspectRatio, mainAxisExtent, + shrinkWrapRows, ]; @override @@ -377,6 +407,13 @@ sealed class GridDelegateProperties (value) => mainAxisExtent = value, requiresLayout: true, ), + 'shrinkWrapRows': BoolFieldAccess( + 'Shrink-wrap Rows', + 'Whether each row is as tall as its tallest item.', + () => shrinkWrapRows, + (value) => shrinkWrapRows = value, + requiresLayout: true, + ), }; } @@ -394,6 +431,7 @@ class FixedCrossAxisCountGridDelegateProperties extends GridDelegateProperties { super.crossAxisSpacing = 0, super.childAspectRatio = 1, super.mainAxisExtent, + super.shrinkWrapRows = false, }) : super(GridDelegateType.fixedCrossAxisCount); @override @@ -416,6 +454,7 @@ class FixedCrossAxisCountGridDelegateProperties extends GridDelegateProperties { double? childAspectRatio, double? mainAxisExtent, bool forceMainAxisExtent = false, + bool? shrinkWrapRows, }) { return FixedCrossAxisCountGridDelegateProperties( crossAxisCount: crossAxisCount ?? this.crossAxisCount, @@ -425,6 +464,7 @@ class FixedCrossAxisCountGridDelegateProperties extends GridDelegateProperties { mainAxisExtent: forceMainAxisExtent ? mainAxisExtent : mainAxisExtent ?? super.mainAxisExtent, + shrinkWrapRows: shrinkWrapRows ?? super.shrinkWrapRows, ); } @@ -458,6 +498,7 @@ class MaxCrossAxisExtentGridDelegateProperties extends GridDelegateProperties { super.crossAxisSpacing = 0, super.childAspectRatio = 1, super.mainAxisExtent, + super.shrinkWrapRows = false, }) : super(GridDelegateType.maxCrossAxisExtent); @override @@ -480,6 +521,7 @@ class MaxCrossAxisExtentGridDelegateProperties extends GridDelegateProperties { double? childAspectRatio, double? mainAxisExtent, bool forceMainAxisExtent = false, + bool? shrinkWrapRows, }) { return MaxCrossAxisExtentGridDelegateProperties( maxCrossAxisExtent: maxCrossAxisExtent ?? this.maxCrossAxisExtent, @@ -489,6 +531,7 @@ class MaxCrossAxisExtentGridDelegateProperties extends GridDelegateProperties { mainAxisExtent: forceMainAxisExtent ? mainAxisExtent : mainAxisExtent ?? super.mainAxisExtent, + shrinkWrapRows: shrinkWrapRows ?? super.shrinkWrapRows, ); } diff --git a/lib/src/api/nodes/grid_view_node.g.dart b/lib/src/api/nodes/grid_view_node.g.dart index eb9c6485..c62c130f 100644 --- a/lib/src/api/nodes/grid_view_node.g.dart +++ b/lib/src/api/nodes/grid_view_node.g.dart @@ -47,6 +47,7 @@ Map _$GridViewNodeOmitDefaults( json.remove('componentSchema'); if (isJsonDefault(instance.reactions, const [])) json.remove('reactions'); if (isJsonDefault(instance.children, [])) json.remove('children'); + if (isJsonDefault(instance.isScrollable, true)) json.remove('isScrollable'); if (isJsonDefault(instance.scrollDirection, AxisC.vertical)) json.remove('scrollDirection'); if (isJsonDefault(instance.reverse, false)) json.remove('reverse'); @@ -58,6 +59,8 @@ Map _$GridViewNodeOmitDefaults( ScrollViewKeyboardDismissBehaviorC.manual, )) json.remove('keyboardDismissBehavior'); + if (isJsonDefault(instance.useFlutterListView, false)) + json.remove('useFlutterListView'); if (isJsonDefault(instance.shouldAlwaysScroll, true)) json.remove('shouldAlwaysScroll'); if (isJsonDefault(instance.clipsContent, true)) json.remove('clipsContent'); @@ -88,6 +91,8 @@ Map _$FixedCrossAxisCountGridDelegatePropertiesOmitDefaults( json.remove('crossAxisSpacing'); if (isJsonDefault(instance.childAspectRatio, 1)) json.remove('childAspectRatio'); + if (isJsonDefault(instance.shrinkWrapRows, false)) + json.remove('shrinkWrapRows'); return json; } @@ -101,6 +106,8 @@ Map _$MaxCrossAxisExtentGridDelegatePropertiesOmitDefaults( json.remove('crossAxisSpacing'); if (isJsonDefault(instance.childAspectRatio, 1)) json.remove('childAspectRatio'); + if (isJsonDefault(instance.shrinkWrapRows, false)) + json.remove('shrinkWrapRows'); return json; } @@ -179,6 +186,8 @@ GridViewNode _$GridViewNodeFromJson(Map json) => : VariableData.fromJson( Map.from(json['statusVariable'] as Map), ), + isScrollable: json['isScrollable'] as bool? ?? true, + useFlutterListView: json['useFlutterListView'] as bool? ?? false, useCloudDatabase: json['useCloudDatabase'] as bool? ?? false, collectionPath: json['collectionPath'] as String?, limit: (json['limit'] as num?)?.toInt() ?? 20, @@ -242,6 +251,9 @@ GridViewNode _$GridViewNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..deniedTypes = (json['deniedTypes'] as List) .map((e) => e as String) .toList() @@ -252,8 +264,6 @@ GridViewNode _$GridViewNodeFromJson(Map json) => ..allowedTypes = (json['allowedTypes'] as List) .map((e) => e as String) .toList() - ..isScrollable = json['isScrollable'] as bool - ..useFlutterListView = json['useFlutterListView'] as bool ..paging = $enumDecodeNullable(_$QueryPagingEnumMap, json['paging']) ?? QueryPaging.scroll @@ -266,7 +276,6 @@ 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, @@ -284,6 +293,7 @@ Map _$GridViewNodeToJson(GridViewNode instance) => 'flex': instance.flex, 'aspectRatioLock': instance.aspectRatioLock, 'alignment': instance.alignment.toJson(), + 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'basicBoxLocal': instance.basicBoxLocal.toJson(), 'margin': instance.margin.toJson(), 'padding': instance.padding.toJson(), @@ -295,6 +305,7 @@ Map _$GridViewNodeToJson(GridViewNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'children': instance.children, 'deniedTypes': instance.deniedTypes, @@ -394,6 +405,7 @@ _$FixedCrossAxisCountGridDelegatePropertiesFromJson(Map json) => crossAxisSpacing: (json['crossAxisSpacing'] as num?)?.toDouble() ?? 0, childAspectRatio: (json['childAspectRatio'] as num?)?.toDouble() ?? 1, mainAxisExtent: (json['mainAxisExtent'] as num?)?.toDouble(), + shrinkWrapRows: json['shrinkWrapRows'] as bool? ?? false, ); Map _$FixedCrossAxisCountGridDelegatePropertiesToJson( @@ -403,6 +415,7 @@ Map _$FixedCrossAxisCountGridDelegatePropertiesToJson( 'crossAxisSpacing': instance.crossAxisSpacing, 'childAspectRatio': instance.childAspectRatio, 'mainAxisExtent': ?instance.mainAxisExtent, + 'shrinkWrapRows': instance.shrinkWrapRows, 'crossAxisCount': instance.crossAxisCount, }; @@ -414,6 +427,7 @@ _$MaxCrossAxisExtentGridDelegatePropertiesFromJson(Map json) => crossAxisSpacing: (json['crossAxisSpacing'] as num?)?.toDouble() ?? 0, childAspectRatio: (json['childAspectRatio'] as num?)?.toDouble() ?? 1, mainAxisExtent: (json['mainAxisExtent'] as num?)?.toDouble(), + shrinkWrapRows: json['shrinkWrapRows'] as bool? ?? false, ); Map _$MaxCrossAxisExtentGridDelegatePropertiesToJson( @@ -423,5 +437,6 @@ Map _$MaxCrossAxisExtentGridDelegatePropertiesToJson( 'crossAxisSpacing': instance.crossAxisSpacing, 'childAspectRatio': instance.childAspectRatio, 'mainAxisExtent': ?instance.mainAxisExtent, + 'shrinkWrapRows': instance.shrinkWrapRows, 'maxCrossAxisExtent': instance.maxCrossAxisExtent, }; diff --git a/lib/src/api/nodes/icon_node.dart b/lib/src/api/nodes/icon_node.dart index c217b72d..da380092 100644 --- a/lib/src/api/nodes/icon_node.dart +++ b/lib/src/api/nodes/icon_node.dart @@ -104,6 +104,8 @@ class IconNode extends SceneNode List get triggerTypes => [ TriggerType.click, TriggerType.longPress, + TriggerType.cursorEnter, + TriggerType.cursorExit, ]; /// Creates an [IconNode] from a JSON data. diff --git a/lib/src/api/nodes/icon_node.g.dart b/lib/src/api/nodes/icon_node.g.dart index 59174871..98f4b871 100644 --- a/lib/src/api/nodes/icon_node.g.dart +++ b/lib/src/api/nodes/icon_node.g.dart @@ -172,6 +172,9 @@ IconNode _$IconNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$IconNodeToJson(IconNode instance) => { @@ -204,6 +207,7 @@ Map _$IconNodeToJson(IconNode instance) => { ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'opacity': instance.opacity, 'blendMode': _$BlendModeCEnumMap[instance.blendMode]!, diff --git a/lib/src/api/nodes/list_tile_node.dart b/lib/src/api/nodes/list_tile_node.dart index b4b07378..00ddf94e 100644 --- a/lib/src/api/nodes/list_tile_node.dart +++ b/lib/src/api/nodes/list_tile_node.dart @@ -134,6 +134,8 @@ class ListTileNode extends SceneNode List get triggerTypes => [ TriggerType.click, TriggerType.longPress, + TriggerType.cursorEnter, + TriggerType.cursorExit, ]; @override diff --git a/lib/src/api/nodes/list_tile_node.g.dart b/lib/src/api/nodes/list_tile_node.g.dart index 326836c6..ee5974bf 100644 --- a/lib/src/api/nodes/list_tile_node.g.dart +++ b/lib/src/api/nodes/list_tile_node.g.dart @@ -178,6 +178,9 @@ ListTileNode _$ListTileNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$ListTileNodeToJson(ListTileNode instance) => @@ -211,6 +214,7 @@ Map _$ListTileNodeToJson(ListTileNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'children': instance.children, 'type': instance.type, diff --git a/lib/src/api/nodes/list_view_node.g.dart b/lib/src/api/nodes/list_view_node.g.dart index 2dce408d..e7d5491d 100644 --- a/lib/src/api/nodes/list_view_node.g.dart +++ b/lib/src/api/nodes/list_view_node.g.dart @@ -221,6 +221,9 @@ ListViewNode _$ListViewNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..deniedTypes = (json['deniedTypes'] as List) .map((e) => e as String) .toList() @@ -245,7 +248,6 @@ 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, @@ -263,6 +265,7 @@ Map _$ListViewNodeToJson(ListViewNode instance) => 'flex': instance.flex, 'aspectRatioLock': instance.aspectRatioLock, 'alignment': instance.alignment.toJson(), + 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'basicBoxLocal': instance.basicBoxLocal.toJson(), 'margin': instance.margin.toJson(), 'padding': instance.padding.toJson(), @@ -274,6 +277,7 @@ Map _$ListViewNodeToJson(ListViewNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'children': instance.children, 'deniedTypes': instance.deniedTypes, diff --git a/lib/src/api/nodes/loading_indicator_node.g.dart b/lib/src/api/nodes/loading_indicator_node.g.dart index 6e5dadc3..9c6ad08b 100644 --- a/lib/src/api/nodes/loading_indicator_node.g.dart +++ b/lib/src/api/nodes/loading_indicator_node.g.dart @@ -161,6 +161,9 @@ LoadingIndicatorNode _$LoadingIndicatorNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$LoadingIndicatorNodeToJson( @@ -195,6 +198,7 @@ Map _$LoadingIndicatorNodeToJson( ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, '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 a441b172..6fd9683a 100644 --- a/lib/src/api/nodes/navigation_bar_node.g.dart +++ b/lib/src/api/nodes/navigation_bar_node.g.dart @@ -186,6 +186,9 @@ NavigationBarNode _$NavigationBarNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$NavigationBarNodeToJson(NavigationBarNode instance) => @@ -219,6 +222,7 @@ Map _$NavigationBarNodeToJson(NavigationBarNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'currentIndex': instance.currentIndex, diff --git a/lib/src/api/nodes/nodes.dart b/lib/src/api/nodes/nodes.dart index 440e5adc..13bcab89 100644 --- a/lib/src/api/nodes/nodes.dart +++ b/lib/src/api/nodes/nodes.dart @@ -30,6 +30,7 @@ export 'root_node.dart'; export 'row_column_node.dart'; export 'scene_node.dart'; export 'segmented_button_node.dart'; +export 'signature_node.dart'; export 'single_placeholder_node.dart'; export 'slider_node.dart'; export 'spacer_node.dart'; diff --git a/lib/src/api/nodes/page_view_node.g.dart b/lib/src/api/nodes/page_view_node.g.dart index bdc21484..5d4deae0 100644 --- a/lib/src/api/nodes/page_view_node.g.dart +++ b/lib/src/api/nodes/page_view_node.g.dart @@ -195,6 +195,9 @@ PageViewNode _$PageViewNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..ephemeral = json['ephemeral'] as bool? ?? false ..allowedTypes = (json['allowedTypes'] as List) .map((e) => e as String) @@ -240,6 +243,7 @@ Map _$PageViewNodeToJson(PageViewNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'children': instance.children, 'clipsContent': instance.clipsContent, diff --git a/lib/src/api/nodes/placeholder_node.g.dart b/lib/src/api/nodes/placeholder_node.g.dart index da90e37b..5e7b4ae4 100644 --- a/lib/src/api/nodes/placeholder_node.g.dart +++ b/lib/src/api/nodes/placeholder_node.g.dart @@ -140,6 +140,9 @@ PlaceholderNode _$PlaceholderNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$PlaceholderNodeToJson(PlaceholderNode instance) => @@ -173,6 +176,7 @@ Map _$PlaceholderNodeToJson(PlaceholderNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'leadingAsset': ?instance.leadingAsset, diff --git a/lib/src/api/nodes/progressbar_node.g.dart b/lib/src/api/nodes/progressbar_node.g.dart index 9591fbd0..d1c78b02 100644 --- a/lib/src/api/nodes/progressbar_node.g.dart +++ b/lib/src/api/nodes/progressbar_node.g.dart @@ -174,6 +174,9 @@ ProgressBarNode _$ProgressBarNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$ProgressBarNodeToJson(ProgressBarNode instance) => @@ -207,6 +210,7 @@ Map _$ProgressBarNodeToJson(ProgressBarNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'properties': instance.properties.toJson(), diff --git a/lib/src/api/nodes/radio_node.dart b/lib/src/api/nodes/radio_node.dart index a978e43a..cf4a9a55 100644 --- a/lib/src/api/nodes/radio_node.dart +++ b/lib/src/api/nodes/radio_node.dart @@ -97,7 +97,11 @@ class RadioNode extends SceneNode with CustomPropertiesMixin, ScalableMixin { }; @override - List get triggerTypes => [TriggerType.click]; + List get triggerTypes => [ + TriggerType.click, + TriggerType.cursorEnter, + TriggerType.cursorExit, + ]; /// Creates a [RadioNode] instance from a JSON object. factory RadioNode.fromJson(Map json) => _$RadioNodeFromJson(json); diff --git a/lib/src/api/nodes/radio_node.g.dart b/lib/src/api/nodes/radio_node.g.dart index d1ff5b62..4001ef10 100644 --- a/lib/src/api/nodes/radio_node.g.dart +++ b/lib/src/api/nodes/radio_node.g.dart @@ -161,9 +161,13 @@ RadioNode _$RadioNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$RadioNodeToJson(RadioNode instance) => { + 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'variables': instance.variables, 'multipleVariables': instance.multipleVariables, 'componentId': ?instance.componentId, @@ -181,7 +185,6 @@ Map _$RadioNodeToJson(RadioNode instance) => { 'flex': instance.flex, 'aspectRatioLock': instance.aspectRatioLock, 'alignment': instance.alignment.toJson(), - 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'basicBoxLocal': instance.basicBoxLocal.toJson(), 'margin': instance.margin.toJson(), 'padding': instance.padding.toJson(), @@ -193,6 +196,7 @@ Map _$RadioNodeToJson(RadioNode instance) => { ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'properties': instance.properties.toJson(), diff --git a/lib/src/api/nodes/rectangle_node.dart b/lib/src/api/nodes/rectangle_node.dart index 4875b198..32330af8 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.cursorEnter, + TriggerType.cursorExit, ]; @override diff --git a/lib/src/api/nodes/rectangle_node.g.dart b/lib/src/api/nodes/rectangle_node.g.dart index 3e3fc760..37d0e5d4 100644 --- a/lib/src/api/nodes/rectangle_node.g.dart +++ b/lib/src/api/nodes/rectangle_node.g.dart @@ -209,6 +209,9 @@ RectangleNode _$RectangleNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..inkWell = json['inkWell'] == null ? null : InkWellModel.fromJson( @@ -218,13 +221,6 @@ RectangleNode _$RectangleNodeFromJson(Map json) => Map _$RectangleNodeToJson(RectangleNode 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, @@ -236,6 +232,13 @@ Map _$RectangleNodeToJson(RectangleNode instance) => 'flex': instance.flex, 'aspectRatioLock': instance.aspectRatioLock, 'alignment': instance.alignment.toJson(), + '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, 'basicBoxLocal': instance.basicBoxLocal.toJson(), 'margin': instance.margin.toJson(), 'padding': instance.padding.toJson(), @@ -247,6 +250,7 @@ Map _$RectangleNodeToJson(RectangleNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'opacity': instance.opacity, 'blendMode': _$BlendModeCEnumMap[instance.blendMode]!, diff --git a/lib/src/api/nodes/row_column_node.g.dart b/lib/src/api/nodes/row_column_node.g.dart index ccfdfcd2..7e3c8243 100644 --- a/lib/src/api/nodes/row_column_node.g.dart +++ b/lib/src/api/nodes/row_column_node.g.dart @@ -273,6 +273,9 @@ RowColumnNode _$RowColumnNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..inkWell = json['inkWell'] == null ? null : InkWellModel.fromJson( @@ -312,6 +315,7 @@ Map _$RowColumnNodeToJson( ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'children': instance.children, 'opacity': instance.opacity, diff --git a/lib/src/api/nodes/segmented_button_node.g.dart b/lib/src/api/nodes/segmented_button_node.g.dart index 7229d2d8..3c58ec7c 100644 --- a/lib/src/api/nodes/segmented_button_node.g.dart +++ b/lib/src/api/nodes/segmented_button_node.g.dart @@ -177,6 +177,9 @@ SegmentedButtonNode _$SegmentedButtonNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$SegmentedButtonNodeToJson( @@ -211,6 +214,7 @@ Map _$SegmentedButtonNodeToJson( ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'properties': instance.properties.toJson(), diff --git a/lib/src/api/nodes/signature_node.dart b/lib/src/api/nodes/signature_node.dart new file mode 100644 index 00000000..1e17c3cd --- /dev/null +++ b/lib/src/api/nodes/signature_node.dart @@ -0,0 +1,433 @@ +import 'package:json_annotation/json_annotation.dart'; + +import '../field_access.dart'; +import '../mixins.dart'; +import '../models/models.dart'; +import 'nodes.dart'; + +part 'signature_node.g.dart'; + +/// Default width of a signature pad. +const double kSignatureDefaultWidth = 320; + +/// Default height of a signature pad. +const double kSignatureDefaultHeight = 140; + +/// Default color of the ink of a signature pad. +const ColorRGBA kSignatureDefaultInkColor = ColorRGBA( + r: 0x13 / 255, + g: 0x30 / 255, + b: 0x6B / 255, + a: 1, +); + +/// Default background color of a signature pad. +const ColorRGBA kSignatureDefaultBackgroundColor = ColorRGBA( + r: 0xF8 / 255, + g: 0xF8 / 255, + b: 0xF5 / 255, + a: 1, +); + +/// Default border, sign line and placeholder color of a signature pad. +const ColorRGBA kSignatureDefaultBorderColor = ColorRGBA( + r: 0xA9 / 255, + g: 0xAD / 255, + b: 0xA5 / 255, + a: 1, +); + +/// Default border color of a signature pad that is in error. +const ColorRGBA kSignatureDefaultErrorColor = ColorRGBA( + r: 0xD3 / 255, + g: 0x30 / 255, + b: 0x2F / 255, + a: 1, +); + +/// Default placeholder of a signature pad. +const String kSignatureDefaultPlaceholder = 'SIGN HERE'; + +/// A pad the user signs on by drawing with a finger, a pen or a mouse. +/// +/// Its [value] is the list of strokes drawn, each a list of `[x, y]` points +/// in the pad's coordinates. An empty list means the pad is not signed. +/// +/// Bind `variables['value']` to a list variable to read the signature +/// (`${signature.isEmpty}`, `${= length(signature) > 0}`) and to clear it +/// (set the variable to `[]`). [TriggerType.changed] fires when a stroke +/// ends, with the strokes as its value. +@JsonSerializable() +class SignatureNode extends SceneNode with CustomPropertiesMixin { + @override + final String type = 'signature'; + + /// Holds configurable properties of the signature pad. + @override + covariant SignatureProperties properties; + + /// The strokes drawn on the pad: each stroke is a list of `[x, y]` points. + /// Empty when the pad is not signed. + @JsonKey(fromJson: strokesFromJson) + List>> value; + + /// Strictly used for previews. e.g in components panel. + SignatureNode.empty() + : properties = SignatureProperties(), + value = [], + super( + id: 'signature', + name: 'Signature', + basicBoxLocal: NodeBox( + 0, + 0, + kSignatureDefaultWidth, + kSignatureDefaultHeight, + ), + ); + + /// Strictly used for previews. e.g in components panel. + SignatureNode.fromProperties(this.properties) + : value = [], + super( + id: 'signature', + name: 'Signature', + basicBoxLocal: NodeBox( + 0, + 0, + kSignatureDefaultWidth, + kSignatureDefaultHeight, + ), + ); + + /// Creates a [SignatureNode] with the given data. + SignatureNode({ + required super.id, + required super.name, + required super.basicBoxLocal, + List>>? value, + super.outerBoxLocal, + super.retainedOuterBoxLocal, + super.visible, + super.enabled, + super.alignment, + super.padding, + super.margin, + super.rotationDegrees, + super.reactions, + super.horizontalFit, + super.verticalFit, + super.flex, + super.constraints, + super.aspectRatioLock, + super.edgePins, + super.parentID, + super.positioningMode, + // Custom properties. + required this.properties, + super.variables, + super.multipleVariables, + }) : value = value ?? []; + + /// Whether the pad holds no strokes. + bool get isEmpty => value.isEmpty; + + @override + FieldsMap generateFields() => { + ...super.generateFields(), + 'properties': ObjectFieldAccess( + 'Properties', + 'Holds configurable properties of the signature pad.', + () => properties, + (value) => properties = value, + ), + }; + + /// [TriggerType.changed] fires with the strokes when a stroke ends. + @override + List get triggerTypes => [TriggerType.changed]; + + @override + late final List propertyVariables = [ + ...super.propertyVariables, + ObjectValue(name: 'value', value: value), + BoolValue(name: 'error', value: properties.error), + BoolValue(name: 'enabled', value: properties.enabled), + ]; + + @override + List get props => [ + ...super.props, + properties, + value, + variables, + multipleVariables, + ]; + + /// Creates a [SignatureNode] from a JSON data. + factory SignatureNode.fromJson(Map json) => _$SignatureNodeFromJson(json); + + @override + Map toJson() => + _$SignatureNodeOmitDefaults(this, _$SignatureNodeToJson(this)); +} + +/// Reads strokes from JSON: a list of strokes, each a list of `[x, y]` +/// points. Malformed points are skipped. +List>> strokesFromJson(Object? json) { + if (json is! List) return []; + return [ + for (final stroke in json) + if (stroke is List) + [ + for (final point in stroke) + if (point is List && + point.length >= 2 && + point[0] is num && + point[1] is num) + [(point[0] as num).toDouble(), (point[1] as num).toDouble()], + ], + ]; +} + +/// Holds configurable properties of a [SignatureNode]. +@JsonSerializable() +class SignatureProperties extends CustomProperties with FieldsHolder { + /// Color of the ink. + ColorRGBA inkColor; + + /// Width of the ink line. + double inkWidth; + + /// Background color of the pad. + ColorRGBA backgroundColor; + + /// Color of the border of the pad. + ColorRGBA borderColor; + + /// Width of the border of the pad. + double borderWidth; + + /// Color of the border when the pad is in [error]. + ColorRGBA errorBorderColor; + + /// Width of the border when the pad is in [error]. + double errorBorderWidth; + + /// Radius of the corners of the pad. + CornerRadius cornerRadius; + + /// Text shown on the pad while it is empty. Empty shows nothing. + String placeholder; + + /// Color of the [placeholder]. + ColorRGBA placeholderColor; + + /// Font size of the [placeholder]. + double placeholderFontSize; + + /// Whether to draw a sign line with an "×" near the bottom of the pad. + bool showSignLine; + + /// Color of the sign line and its "×". + ColorRGBA signLineColor; + + /// Whether the pad shows its error border. Bind `variables['error']` to + /// show it from a validation rule. + bool error; + + /// Whether the user can draw on the pad. A pad that is not enabled still + /// shows its strokes. Bind `variables['enabled']` to lock it, e.g. once + /// the form is submitted. + bool enabled; + + /// Creates a [SignatureProperties] with the given data. + SignatureProperties({ + this.inkColor = kSignatureDefaultInkColor, + this.inkWidth = 2.6, + this.backgroundColor = kSignatureDefaultBackgroundColor, + this.borderColor = kSignatureDefaultBorderColor, + this.borderWidth = 1.5, + this.errorBorderColor = kSignatureDefaultErrorColor, + this.errorBorderWidth = 2, + this.cornerRadius = const CornerRadius.all(RadiusModel.circular(4)), + this.placeholder = kSignatureDefaultPlaceholder, + this.placeholderColor = kSignatureDefaultBorderColor, + this.placeholderFontSize = 12, + this.showSignLine = true, + this.signLineColor = kSignatureDefaultBorderColor, + this.error = false, + this.enabled = true, + }); + + /// Duplicates this [SignatureProperties] with the given data overrides. + SignatureProperties copyWith({ + ColorRGBA? inkColor, + double? inkWidth, + ColorRGBA? backgroundColor, + ColorRGBA? borderColor, + double? borderWidth, + ColorRGBA? errorBorderColor, + double? errorBorderWidth, + CornerRadius? cornerRadius, + String? placeholder, + ColorRGBA? placeholderColor, + double? placeholderFontSize, + bool? showSignLine, + ColorRGBA? signLineColor, + bool? error, + bool? enabled, + }) => SignatureProperties( + inkColor: inkColor ?? this.inkColor, + inkWidth: inkWidth ?? this.inkWidth, + backgroundColor: backgroundColor ?? this.backgroundColor, + borderColor: borderColor ?? this.borderColor, + borderWidth: borderWidth ?? this.borderWidth, + errorBorderColor: errorBorderColor ?? this.errorBorderColor, + errorBorderWidth: errorBorderWidth ?? this.errorBorderWidth, + cornerRadius: cornerRadius ?? this.cornerRadius, + placeholder: placeholder ?? this.placeholder, + placeholderColor: placeholderColor ?? this.placeholderColor, + placeholderFontSize: placeholderFontSize ?? this.placeholderFontSize, + showSignLine: showSignLine ?? this.showSignLine, + signLineColor: signLineColor ?? this.signLineColor, + error: error ?? this.error, + enabled: enabled ?? this.enabled, + ); + + @override + FieldsMap generateFields() => { + ...super.generateFields(), + 'inkColor': ColorFieldAccess( + 'Ink Color', + 'Color of the ink.', + () => inkColor, + (ColorRGBA? value) { + if (value != null) inkColor = value; + }, + ), + 'inkWidth': NumFieldAccess( + 'Ink Width', + 'Width of the ink line.', + () => inkWidth, + (double value) => inkWidth = value, + ), + 'backgroundColor': ColorFieldAccess( + 'Background Color', + 'Background color of the pad.', + () => backgroundColor, + (ColorRGBA? value) { + if (value != null) backgroundColor = value; + }, + ), + 'borderColor': ColorFieldAccess( + 'Border Color', + 'Color of the border of the pad.', + () => borderColor, + (ColorRGBA? value) { + if (value != null) borderColor = value; + }, + ), + 'borderWidth': NumFieldAccess( + 'Border Width', + 'Width of the border of the pad.', + () => borderWidth, + (double value) => borderWidth = value, + ), + 'errorBorderColor': ColorFieldAccess( + 'Error Border Color', + 'Color of the border when the pad is in error.', + () => errorBorderColor, + (ColorRGBA? value) { + if (value != null) errorBorderColor = value; + }, + ), + 'errorBorderWidth': NumFieldAccess( + 'Error Border Width', + 'Width of the border when the pad is in error.', + () => errorBorderWidth, + (double value) => errorBorderWidth = value, + ), + 'cornerRadius': RadiusFieldAccess( + 'Corner Radius', + 'Radius of the corners of the pad.', + () => cornerRadius, + (CornerRadius value) => cornerRadius = value, + ), + 'placeholder': StringFieldAccess( + 'Placeholder', + 'Text shown on the pad while it is empty.', + () => placeholder, + (String value) => placeholder = value, + ), + 'placeholderColor': ColorFieldAccess( + 'Placeholder Color', + 'Color of the placeholder.', + () => placeholderColor, + (ColorRGBA? value) { + if (value != null) placeholderColor = value; + }, + ), + 'placeholderFontSize': NumFieldAccess( + 'Placeholder Font Size', + 'Font size of the placeholder.', + () => placeholderFontSize, + (double value) => placeholderFontSize = value, + ), + 'showSignLine': BoolFieldAccess( + 'Show Sign Line', + 'Whether to draw a sign line with an "×" near the bottom of the pad.', + () => showSignLine, + (bool value) => showSignLine = value, + ), + 'signLineColor': ColorFieldAccess( + 'Sign Line Color', + 'Color of the sign line and its "×".', + () => signLineColor, + (ColorRGBA? value) { + if (value != null) signLineColor = value; + }, + ), + 'error': BoolFieldAccess( + 'Error', + 'Whether the pad shows its error border.', + () => error, + (bool value) => error = value, + ), + 'enabled': BoolFieldAccess( + 'Enabled', + 'Whether the user can draw on the pad.', + () => enabled, + (bool value) => enabled = value, + ), + }; + + @override + List get props => [ + inkColor, + inkWidth, + backgroundColor, + borderColor, + borderWidth, + errorBorderColor, + errorBorderWidth, + cornerRadius, + placeholder, + placeholderColor, + placeholderFontSize, + showSignLine, + signLineColor, + error, + enabled, + ]; + + /// Creates a [SignatureProperties] from a JSON data. + factory SignatureProperties.fromJson(Map json) => + _$SignaturePropertiesFromJson(json); + + @override + Map toJson() => _$SignaturePropertiesOmitDefaults( + this, + _$SignaturePropertiesToJson(this), + ); +} diff --git a/lib/src/api/nodes/signature_node.g.dart b/lib/src/api/nodes/signature_node.g.dart new file mode 100644 index 00000000..aa94fbc2 --- /dev/null +++ b/lib/src/api/nodes/signature_node.g.dart @@ -0,0 +1,300 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'signature_node.dart'; + +// ************************************************************************** +// JsonDefaultsGenerator +// ************************************************************************** + +Map _$SignatureNodeOmitDefaults( + SignatureNode instance, + Map json, +) { + if (isJsonDefault(instance.value, [])) json.remove('value'); + if (isJsonDefault(instance.visible, true)) json.remove('visible'); + if (excludeConstraintsIf(instance)) json.remove('constraints'); + if (isJsonDefault(instance.constraints, const BoxConstraintsModel())) + json.remove('constraints'); + if (isJsonDefault(instance.edgePins, EdgePinsModel.standard)) + json.remove('edgePins'); + if (isJsonDefault(instance.positioningMode, PositioningMode.align)) + json.remove('positioningMode'); + if (isJsonDefault(instance.horizontalFit, SizeFit.fixed)) + json.remove('horizontalFit'); + if (isJsonDefault(instance.verticalFit, SizeFit.fixed)) + json.remove('verticalFit'); + if (isJsonDefault(instance.flex, 1)) json.remove('flex'); + if (isJsonDefault(instance.aspectRatioLock, false)) + json.remove('aspectRatioLock'); + if (isJsonDefault(instance.alignment, AlignmentModel.none)) + json.remove('alignment'); + if (isJsonDefault(instance.margin, EdgeInsetsModel.zero)) + json.remove('margin'); + if (isJsonDefault(instance.padding, EdgeInsetsModel.zero)) + json.remove('padding'); + if (isJsonDefault(instance.rotationDegrees, 0)) json.remove('rotation'); + if (isJsonDefault(instance.interactionStates, {})) + json.remove('interactionStates'); + if (isJsonDefault(instance.tooltip, '')) json.remove('tooltip'); + if (isJsonDefault(instance.zoomable, false)) json.remove('zoomable'); + if (isJsonDefault(instance.enabled, true)) json.remove('enabled'); + if (isJsonDefault(instance.variables, {})) json.remove('variables'); + if (isJsonDefault(instance.multipleVariables, {})) + json.remove('multipleVariables'); + if (isJsonDefault(instance.componentVersion, 1)) + json.remove('componentVersion'); + if (isJsonDefault(instance.componentSchema, {})) + json.remove('componentSchema'); + if (isJsonDefault(instance.reactions, const [])) json.remove('reactions'); + return json; +} + +Map _$SignaturePropertiesOmitDefaults( + SignatureProperties instance, + Map json, +) { + if (isJsonDefault(instance.inkColor, kSignatureDefaultInkColor)) + json.remove('inkColor'); + if (isJsonDefault(instance.inkWidth, 2.6)) json.remove('inkWidth'); + if (isJsonDefault(instance.backgroundColor, kSignatureDefaultBackgroundColor)) + json.remove('backgroundColor'); + if (isJsonDefault(instance.borderColor, kSignatureDefaultBorderColor)) + json.remove('borderColor'); + if (isJsonDefault(instance.borderWidth, 1.5)) json.remove('borderWidth'); + if (isJsonDefault(instance.errorBorderColor, kSignatureDefaultErrorColor)) + json.remove('errorBorderColor'); + if (isJsonDefault(instance.errorBorderWidth, 2)) + json.remove('errorBorderWidth'); + if (isJsonDefault( + instance.cornerRadius, + const CornerRadius.all(RadiusModel.circular(4)), + )) + json.remove('cornerRadius'); + if (isJsonDefault(instance.placeholder, kSignatureDefaultPlaceholder)) + json.remove('placeholder'); + if (isJsonDefault(instance.placeholderColor, kSignatureDefaultBorderColor)) + json.remove('placeholderColor'); + if (isJsonDefault(instance.placeholderFontSize, 12)) + json.remove('placeholderFontSize'); + if (isJsonDefault(instance.showSignLine, true)) json.remove('showSignLine'); + if (isJsonDefault(instance.signLineColor, kSignatureDefaultBorderColor)) + json.remove('signLineColor'); + if (isJsonDefault(instance.error, false)) json.remove('error'); + if (isJsonDefault(instance.enabled, true)) json.remove('enabled'); + return json; +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SignatureNode _$SignatureNodeFromJson(Map json) => + SignatureNode( + id: json['id'] as String, + name: json['name'] as String, + basicBoxLocal: NodeBox.fromJson(json['basicBoxLocal']), + value: strokesFromJson(json['value']), + visible: json['visible'] as bool? ?? true, + enabled: json['enabled'] as bool? ?? true, + alignment: json['alignment'] == null + ? AlignmentModel.none + : AlignmentModel.fromJson(json['alignment'] as Map), + padding: json['padding'] == null + ? EdgeInsetsModel.zero + : EdgeInsetsModel.fromJson(json['padding']), + margin: json['margin'] == null + ? EdgeInsetsModel.zero + : EdgeInsetsModel.fromJson(json['margin']), + rotationDegrees: json['rotation'] == null + ? 0 + : castRotation(json['rotation']), + reactions: + (json['reactions'] as List?) + ?.map((e) => Reaction.fromJson(e as Map)) + .toList() ?? + const [], + horizontalFit: + $enumDecodeNullable(_$SizeFitEnumMap, json['horizontalFit']) ?? + SizeFit.fixed, + verticalFit: + $enumDecodeNullable(_$SizeFitEnumMap, json['verticalFit']) ?? + SizeFit.fixed, + flex: (json['flex'] as num?)?.toInt() ?? 1, + constraints: json['constraints'] == null + ? const BoxConstraintsModel() + : BoxConstraintsModel.fromJson(json['constraints'] as Map), + aspectRatioLock: json['aspectRatioLock'] as bool? ?? false, + edgePins: json['edgePins'] == null + ? EdgePinsModel.standard + : EdgePinsModel.fromJson( + Map.from(json['edgePins'] as Map), + ), + positioningMode: + $enumDecodeNullable( + _$PositioningModeEnumMap, + json['positioningMode'], + ) ?? + PositioningMode.align, + properties: SignatureProperties.fromJson(json['properties'] as Map), + variables: + (json['variables'] as Map?)?.map( + (k, e) => MapEntry(k as String, e as String), + ) ?? + {}, + multipleVariables: + (json['multipleVariables'] as Map?)?.map( + (k, e) => MapEntry( + k as String, + (e as List).map((e) => e as String).toList(), + ), + ) ?? + {}, + ) + ..componentId = json['componentId'] as String? + ..markerType = $enumDecodeNullable( + _$ComponentMarkerTypeEnumMap, + json['markerType'], + ) + ..componentVersion = (json['componentVersion'] as num?)?.toInt() ?? 1 + ..componentSchema = + (json['componentSchema'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + ) ?? + {} + ..widthFactor = (json['widthFactor'] as num?)?.toDouble() + ..heightFactor = (json['heightFactor'] as num?)?.toDouble() + ..interactionStates = + (json['interactionStates'] as Map?)?.map( + (k, e) => MapEntry( + $enumDecode(_$InteractionStateEnumMap, k), + (e as Map).map( + (k, e) => + MapEntry(k as String, Map.from(e as Map)), + ), + ), + ) ?? + {} + ..tooltip = json['tooltip'] as String? ?? '' + ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) + ..type = json['type'] as String; + +Map _$SignatureNodeToJson(SignatureNode instance) => + { + '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, + 'constraints': instance.constraints.toJson(), + 'edgePins': instance.edgePins.toJson(), + 'positioningMode': _$PositioningModeEnumMap[instance.positioningMode]!, + 'horizontalFit': _$SizeFitEnumMap[instance.horizontalFit]!, + 'verticalFit': _$SizeFitEnumMap[instance.verticalFit]!, + 'flex': instance.flex, + 'aspectRatioLock': instance.aspectRatioLock, + 'alignment': instance.alignment.toJson(), + 'reactions': instance.reactions.map((e) => e.toJson()).toList(), + 'basicBoxLocal': instance.basicBoxLocal.toJson(), + 'margin': instance.margin.toJson(), + 'padding': instance.padding.toJson(), + 'rotation': instance.rotationDegrees, + 'widthFactor': ?instance.widthFactor, + 'heightFactor': ?instance.heightFactor, + 'interactionStates': instance.interactionStates.map( + (k, e) => MapEntry(_$InteractionStateEnumMap[k]!, e), + ), + 'tooltip': instance.tooltip, + 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), + 'enabled': instance.enabled, + 'type': instance.type, + 'properties': instance.properties.toJson(), + 'value': instance.value, + }; + +const _$SizeFitEnumMap = { + SizeFit.locked: 'locked', + SizeFit.fixed: 'fixed', + SizeFit.expanded: 'expanded', + SizeFit.flexible: 'flexible', + SizeFit.shrinkWrap: 'shrinkWrap', +}; + +const _$PositioningModeEnumMap = { + PositioningMode.align: 'align', + PositioningMode.pin: 'pin', +}; + +const _$ComponentMarkerTypeEnumMap = { + ComponentMarkerType.component: 'component', + ComponentMarkerType.instance: 'instance', +}; + +const _$InteractionStateEnumMap = { + InteractionState.hovered: 'hovered', + InteractionState.focused: 'focused', + InteractionState.pressed: 'pressed', + InteractionState.selected: 'selected', + InteractionState.disabled: 'disabled', +}; + +SignatureProperties _$SignaturePropertiesFromJson( + Map json, +) => SignatureProperties( + inkColor: json['inkColor'] == null + ? kSignatureDefaultInkColor + : ColorRGBA.fromJson(json['inkColor']), + inkWidth: (json['inkWidth'] as num?)?.toDouble() ?? 2.6, + backgroundColor: json['backgroundColor'] == null + ? kSignatureDefaultBackgroundColor + : ColorRGBA.fromJson(json['backgroundColor']), + borderColor: json['borderColor'] == null + ? kSignatureDefaultBorderColor + : ColorRGBA.fromJson(json['borderColor']), + borderWidth: (json['borderWidth'] as num?)?.toDouble() ?? 1.5, + errorBorderColor: json['errorBorderColor'] == null + ? kSignatureDefaultErrorColor + : ColorRGBA.fromJson(json['errorBorderColor']), + errorBorderWidth: (json['errorBorderWidth'] as num?)?.toDouble() ?? 2, + cornerRadius: json['cornerRadius'] == null + ? const CornerRadius.all(RadiusModel.circular(4)) + : CornerRadius.fromJson(json['cornerRadius']), + placeholder: json['placeholder'] as String? ?? kSignatureDefaultPlaceholder, + placeholderColor: json['placeholderColor'] == null + ? kSignatureDefaultBorderColor + : ColorRGBA.fromJson(json['placeholderColor']), + placeholderFontSize: (json['placeholderFontSize'] as num?)?.toDouble() ?? 12, + showSignLine: json['showSignLine'] as bool? ?? true, + signLineColor: json['signLineColor'] == null + ? kSignatureDefaultBorderColor + : ColorRGBA.fromJson(json['signLineColor']), + error: json['error'] as bool? ?? false, + enabled: json['enabled'] as bool? ?? true, +); + +Map _$SignaturePropertiesToJson( + SignatureProperties instance, +) => { + 'inkColor': instance.inkColor.toJson(), + 'inkWidth': instance.inkWidth, + 'backgroundColor': instance.backgroundColor.toJson(), + 'borderColor': instance.borderColor.toJson(), + 'borderWidth': instance.borderWidth, + 'errorBorderColor': instance.errorBorderColor.toJson(), + 'errorBorderWidth': instance.errorBorderWidth, + 'cornerRadius': instance.cornerRadius.toJson(), + 'placeholder': instance.placeholder, + 'placeholderColor': instance.placeholderColor.toJson(), + 'placeholderFontSize': instance.placeholderFontSize, + 'showSignLine': instance.showSignLine, + 'signLineColor': instance.signLineColor.toJson(), + 'error': instance.error, + 'enabled': instance.enabled, +}; diff --git a/lib/src/api/nodes/single_placeholder_node.g.dart b/lib/src/api/nodes/single_placeholder_node.g.dart index d8ce6f51..d35e87f4 100644 --- a/lib/src/api/nodes/single_placeholder_node.g.dart +++ b/lib/src/api/nodes/single_placeholder_node.g.dart @@ -155,6 +155,9 @@ SinglePlaceholderNode _$SinglePlaceholderNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$SinglePlaceholderNodeToJson( @@ -189,6 +192,7 @@ Map _$SinglePlaceholderNodeToJson( ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'children': instance.children, 'type': instance.type, diff --git a/lib/src/api/nodes/slider_node.g.dart b/lib/src/api/nodes/slider_node.g.dart index 93ef55f9..a14de108 100644 --- a/lib/src/api/nodes/slider_node.g.dart +++ b/lib/src/api/nodes/slider_node.g.dart @@ -197,6 +197,9 @@ SliderNode _$SliderNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$SliderNodeToJson(SliderNode instance) => @@ -230,6 +233,7 @@ Map _$SliderNodeToJson(SliderNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'value': instance.value, diff --git a/lib/src/api/nodes/spacer_node.dart b/lib/src/api/nodes/spacer_node.dart index 5147c62a..c6f4b9c7 100644 --- a/lib/src/api/nodes/spacer_node.dart +++ b/lib/src/api/nodes/spacer_node.dart @@ -41,6 +41,9 @@ class SpacerNode extends SceneNode { super.edgePins, super.parentID, super.constraints, + super.positioningMode, + super.aspectRatioLock, + super.reactions, }); /// Creates a new [SpacerNode] from a JSON data. @@ -51,6 +54,20 @@ class SpacerNode extends SceneNode { ...json, }); + /// Keys that SDKs up to codelessly_api 6.0.0 require in a spacer's JSON. They are written + /// even at their defaults so those SDKs can still read it. + static const List _legacyRequiredKeys = [ + 'positioningMode', + 'aspectRatioLock', + 'reactions', + ]; + @override - Map toJson() => _$SpacerNodeOmitDefaults(this, _$SpacerNodeToJson(this)); + Map toJson() { + final Map json = _$SpacerNodeToJson(this); + final Map legacy = { + for (final key in _legacyRequiredKeys) key: json[key], + }; + return {..._$SpacerNodeOmitDefaults(this, json), ...legacy}; + } } diff --git a/lib/src/api/nodes/spacer_node.g.dart b/lib/src/api/nodes/spacer_node.g.dart index 9d7b529c..3dd91981 100644 --- a/lib/src/api/nodes/spacer_node.g.dart +++ b/lib/src/api/nodes/spacer_node.g.dart @@ -16,11 +16,15 @@ Map _$SpacerNodeOmitDefaults( json.remove('constraints'); if (isJsonDefault(instance.edgePins, EdgePinsModel.standard)) json.remove('edgePins'); + if (isJsonDefault(instance.positioningMode, PositioningMode.align)) + json.remove('positioningMode'); if (isJsonDefault(instance.horizontalFit, SizeFit.fixed)) json.remove('horizontalFit'); if (isJsonDefault(instance.verticalFit, SizeFit.fixed)) json.remove('verticalFit'); if (isJsonDefault(instance.flex, 1)) json.remove('flex'); + if (isJsonDefault(instance.aspectRatioLock, false)) + json.remove('aspectRatioLock'); if (isJsonDefault(instance.interactionStates, {})) json.remove('interactionStates'); if (isJsonDefault(instance.tooltip, '')) json.remove('tooltip'); @@ -33,6 +37,7 @@ Map _$SpacerNodeOmitDefaults( json.remove('componentVersion'); if (isJsonDefault(instance.componentSchema, {})) json.remove('componentSchema'); + if (isJsonDefault(instance.reactions, const [])) json.remove('reactions'); return json; } @@ -62,6 +67,18 @@ SpacerNode _$SpacerNodeFromJson(Map json) => constraints: json['constraints'] == null ? const BoxConstraintsModel() : BoxConstraintsModel.fromJson(json['constraints'] as Map), + positioningMode: + $enumDecodeNullable( + _$PositioningModeEnumMap, + json['positioningMode'], + ) ?? + PositioningMode.align, + aspectRatioLock: json['aspectRatioLock'] as bool? ?? false, + reactions: + (json['reactions'] as List?) + ?.map((e) => Reaction.fromJson(e as Map)) + .toList() ?? + const [], ) ..variables = (json['variables'] as Map?)?.map( @@ -87,14 +104,6 @@ SpacerNode _$SpacerNodeFromJson(Map json) => (k, e) => MapEntry(k as String, e), ) ?? {} - ..positioningMode = $enumDecode( - _$PositioningModeEnumMap, - json['positioningMode'], - ) - ..aspectRatioLock = json['aspectRatioLock'] as bool - ..reactions = (json['reactions'] as List) - .map((e) => Reaction.fromJson(e as Map)) - .toList() ..widthFactor = (json['widthFactor'] as num?)?.toDouble() ..heightFactor = (json['heightFactor'] as num?)?.toDouble() ..interactionStates = @@ -110,6 +119,9 @@ SpacerNode _$SpacerNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$SpacerNodeToJson(SpacerNode instance) => @@ -139,6 +151,7 @@ Map _$SpacerNodeToJson(SpacerNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, }; @@ -151,16 +164,16 @@ const _$SizeFitEnumMap = { SizeFit.shrinkWrap: 'shrinkWrap', }; -const _$ComponentMarkerTypeEnumMap = { - ComponentMarkerType.component: 'component', - ComponentMarkerType.instance: 'instance', -}; - const _$PositioningModeEnumMap = { PositioningMode.align: 'align', PositioningMode.pin: 'pin', }; +const _$ComponentMarkerTypeEnumMap = { + ComponentMarkerType.component: 'component', + ComponentMarkerType.instance: 'instance', +}; + const _$InteractionStateEnumMap = { InteractionState.hovered: 'hovered', InteractionState.focused: 'focused', diff --git a/lib/src/api/nodes/switch_node.g.dart b/lib/src/api/nodes/switch_node.g.dart index 3c34fb4e..229363ab 100644 --- a/lib/src/api/nodes/switch_node.g.dart +++ b/lib/src/api/nodes/switch_node.g.dart @@ -170,6 +170,9 @@ SwitchNode _$SwitchNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$SwitchNodeToJson(SwitchNode instance) => @@ -203,6 +206,7 @@ Map _$SwitchNodeToJson(SwitchNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'value': instance.value, diff --git a/lib/src/api/nodes/tab_bar_node.g.dart b/lib/src/api/nodes/tab_bar_node.g.dart index b93405cc..cc310976 100644 --- a/lib/src/api/nodes/tab_bar_node.g.dart +++ b/lib/src/api/nodes/tab_bar_node.g.dart @@ -205,6 +205,9 @@ TabBarNode _$TabBarNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..scrollDirection = $enumDecode(_$AxisCEnumMap, json['scrollDirection']) ..reverse = json['reverse'] as bool ..primary = json['primary'] as bool @@ -246,6 +249,7 @@ Map _$TabBarNodeToJson(TabBarNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'isScrollable': instance.isScrollable, 'scrollDirection': _$AxisCEnumMap[instance.scrollDirection]!, diff --git a/lib/src/api/nodes/text_field_node.dart b/lib/src/api/nodes/text_field_node.dart index db991e14..fd569c98 100644 --- a/lib/src/api/nodes/text_field_node.dart +++ b/lib/src/api/nodes/text_field_node.dart @@ -116,6 +116,15 @@ class TextFieldNode extends SceneNode value: properties.decoration.labelText ?? '', ), StringValue(name: 'hintText', value: properties.decoration.hintText ?? ''), + // The error message under the field. Bind it to a formula such as + // `${= showErrors && length(note) < 10 ? "Too short" : ""}`: an empty + // result means no error. + StringValue( + name: 'errorText', + value: properties.decoration.errorText ?? '', + ), + // Shows the error borders without a message when true. + const BoolValue(name: 'error', value: false), ]; @override diff --git a/lib/src/api/nodes/text_field_node.g.dart b/lib/src/api/nodes/text_field_node.g.dart index 1ca7bd8b..641b5e81 100644 --- a/lib/src/api/nodes/text_field_node.g.dart +++ b/lib/src/api/nodes/text_field_node.g.dart @@ -194,6 +194,9 @@ TextFieldNode _$TextFieldNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$TextFieldNodeToJson(TextFieldNode instance) => @@ -227,6 +230,7 @@ Map _$TextFieldNodeToJson(TextFieldNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'properties': instance.properties.toJson(), diff --git a/lib/src/api/nodes/text_node.dart b/lib/src/api/nodes/text_node.dart index 520bdaa1..fb8d6db1 100644 --- a/lib/src/api/nodes/text_node.dart +++ b/lib/src/api/nodes/text_node.dart @@ -148,6 +148,8 @@ class TextNode extends SceneNode with BlendMixin, TextMixin, FontMixin { List get triggerTypes => [ TriggerType.click, TriggerType.longPress, + TriggerType.cursorEnter, + TriggerType.cursorExit, ]; @override diff --git a/lib/src/api/nodes/text_node.g.dart b/lib/src/api/nodes/text_node.g.dart index 01949d07..c3a2c6b9 100644 --- a/lib/src/api/nodes/text_node.g.dart +++ b/lib/src/api/nodes/text_node.g.dart @@ -216,6 +216,9 @@ TextNode _$TextNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$TextNodeToJson(TextNode instance) => { @@ -260,6 +263,7 @@ Map _$TextNodeToJson(TextNode instance) => { ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'opacity': instance.opacity, 'blendMode': _$BlendModeCEnumMap[instance.blendMode]!, diff --git a/lib/src/api/nodes/variance_node.g.dart b/lib/src/api/nodes/variance_node.g.dart index be7a515d..58482916 100644 --- a/lib/src/api/nodes/variance_node.g.dart +++ b/lib/src/api/nodes/variance_node.g.dart @@ -168,6 +168,9 @@ VarianceNode _$VarianceNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..ephemeral = json['ephemeral'] as bool? ?? false ..allowedTypes = (json['allowedTypes'] as List) .map((e) => e as String) @@ -208,6 +211,7 @@ Map _$VarianceNodeToJson(VarianceNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'ephemeral': instance.ephemeral, 'allowedTypes': instance.allowedTypes, diff --git a/lib/src/api/nodes/web_view_node.g.dart b/lib/src/api/nodes/web_view_node.g.dart index 48c3c29e..7d0febff 100644 --- a/lib/src/api/nodes/web_view_node.g.dart +++ b/lib/src/api/nodes/web_view_node.g.dart @@ -151,6 +151,9 @@ WebViewNode _$WebViewNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$WebViewNodeToJson(WebViewNode instance) => @@ -184,6 +187,7 @@ Map _$WebViewNodeToJson(WebViewNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'properties': instance.properties.toJson(), diff --git a/test/json_defaults_test.dart b/test/json_defaults_test.dart index bb2e1328..44647661 100644 --- a/test/json_defaults_test.dart +++ b/test/json_defaults_test.dart @@ -169,6 +169,7 @@ List sampleNodes() { BadgeNode.empty(), ButtonNode.empty(), CheckboxNode.empty(), + SignatureNode.empty(), ChipNode.empty(), DividerNode.empty(), EmbeddedVideoNode.empty(), @@ -380,4 +381,86 @@ void main() { ); }); }); + + group('JSON from build scripts', () { + test('a spacer needs no positioning, aspect lock or reactions', () { + final node = + NodeJsonConverter().fromJson({ + 'type': 'spacer', + 'id': 'sp', + 'name': 'sp', + 'flex': 1, + 'basicBoxLocal': [0, 0, 10, 10], + 'horizontalFit': 'expanded', + 'verticalFit': 'expanded', + })! + as SpacerNode; + expect(node.positioningMode, PositioningMode.align); + expect(node.aspectRatioLock, isFalse); + expect(node.reactions, isEmpty); + // Older readers require these keys, so they are still written. + expect( + node.toJson(), + allOf( + containsPair('positioningMode', 'align'), + containsPair('aspectRatioLock', false), + containsPair('reactions', isEmpty), + ), + ); + }); + + test('a grid needs no useFlutterListView or isScrollable', () { + final node = + NodeJsonConverter().fromJson({ + 'type': 'gridView', + 'id': 'grid', + 'name': 'grid', + 'basicBoxLocal': [0, 0, 300, 300], + 'allowedTypes': [], + 'deniedTypes': ['gridView'], + 'children': ['item'], + 'properties': { + 'gridDelegate': { + 'type': 'fixedCrossAxisCount', + 'crossAxisCount': 2, + 'shrinkWrapRows': true, + }, + }, + })! + as GridViewNode; + expect(node.useFlutterListView, isFalse); + expect(node.isScrollable, isTrue); + expect(node.properties.gridDelegate.shrinkWrapRows, isTrue); + final json = encodeDecode(node.toJson()) as Map; + expect(json, containsPair('useFlutterListView', false)); + expect(json, containsPair('isScrollable', true)); + expect( + (json['properties'] as Map)['gridDelegate'], + containsPair('shrinkWrapRows', true), + ); + expect(NodeJsonConverter().fromJson(json), node); + }); + + test('rows sized to their content leave the item main axis free', () { + final grid = GridViewNode( + id: 'grid', + name: 'grid', + basicBoxLocal: NodeBox(0, 0, 316, 300), + children: ['item'], + properties: GridViewProperties( + gridDelegate: FixedCrossAxisCountGridDelegateProperties( + crossAxisCount: 2, + crossAxisSpacing: 16, + mainAxisExtent: 130, + shrinkWrapRows: true, + ), + ), + ); + final constraints = grid.relegatedConstraintsToChildren(grid)!; + expect(constraints.minWidth, 150); + expect(constraints.maxWidth, 150); + expect(constraints.minHeight, isNull); + expect(constraints.maxHeight, isNull); + }); + }); } diff --git a/test/loop_animation_test.dart b/test/loop_animation_test.dart new file mode 100644 index 00000000..b2d3ca8f --- /dev/null +++ b/test/loop_animation_test.dart @@ -0,0 +1,157 @@ +import 'dart:convert'; + +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:test/test.dart'; + +Map encodeDecode(Map json) => jsonDecode(jsonEncode(json)) as Map; + +void main() { + group('LoopAnimation', () { + test('a pulsing halo round-trips through JSON on any node', () { + final Map json = { + 'type': 'rectangle', + 'id': 'halo', + 'name': 'Halo', + 'basicBoxLocal': NodeBox(0, 0, 58, 58).toJson(), + 'loopAnimation': { + 'type': 'pulse', + 'durationMs': 1400, + 'scaleFrom': 0.655, + 'scaleTo': 1.0, + 'opacityFrom': 0.35, + 'opacityTo': 0.0, + 'curve': 'easeOut', + }, + 'variables': {'loopAnimationEnabled': r'${selectedZone == "cab"}'}, + }; + final BaseNode node = NodeJsonConverter().fromJson(json)!; + expect( + node.loopAnimation, + const LoopAnimation( + type: LoopAnimationType.pulse, + durationMs: 1400, + scaleFrom: 0.655, + scaleTo: 1, + opacityFrom: 0.35, + opacityTo: 0, + curve: LoopAnimationCurve.easeOut, + ), + ); + + final Map written = encodeDecode(node.toJson()); + expect(written['loopAnimation'], { + 'type': 'pulse', + 'durationMs': 1400, + 'curve': 'easeOut', + 'scaleFrom': 0.655, + 'opacityFrom': 0.35, + 'opacityTo': 0.0, + }); + final BaseNode read = NodeJsonConverter().fromJson(written)!; + expect(read.loopAnimation, node.loopAnimation); + expect(read.variables['loopAnimationEnabled'], isNotNull); + }); + + test('nodes without an animation write no key', () { + for (final BaseNode node in [ + RectangleNode(id: 'r', name: 'R', basicBoxLocal: NodeBox(0, 0, 1, 1)), + RowColumnNode( + id: 'c', + name: 'C', + basicBoxLocal: NodeBox(0, 0, 1, 1), + rowColumnType: RowColumnType.column, + children: [], + ), + IconNode(id: 'i', name: 'I', basicBoxLocal: NodeBox(0, 0, 24, 24)), + ]) { + expect(node.toJson().containsKey('loopAnimation'), isFalse); + final BaseNode read = NodeJsonConverter().fromJson( + encodeDecode(node.toJson()), + )!; + expect(read.loopAnimation, isNull); + } + }); + + test('every type and its preset round-trip', () { + for (final LoopAnimationType type in LoopAnimationType.values) { + final LoopAnimation animation = LoopAnimation.preset(type) + .copyWith(delayMs: 200, enabled: false, turns: -2); + expect( + LoopAnimation.fromJson(encodeDecode(animation.toJson())), + animation, + ); + } + // Defaults are left out. + expect(const LoopAnimation(type: LoopAnimationType.spin).toJson(), { + 'type': 'spin', + }); + }); + + test('unknown values fall back instead of throwing', () { + expect( + LoopAnimation.fromJson({'type': 'wobble', 'curve': 'bounce'}), + const LoopAnimation(type: LoopAnimationType.pulse), + ); + }); + + test('loopAnimationEnabled is a bindable field', () { + final RectangleNode node = RectangleNode( + id: 'r', + name: 'R', + basicBoxLocal: NodeBox(0, 0, 1, 1), + )..loopAnimation = LoopAnimation.preset(LoopAnimationType.fade); + final field = node.fields['loopAnimationEnabled']!; + expect(field.getValue(), isTrue); + field.setValue(false); + expect(node.loopAnimation!.enabled, isFalse); + }); + }); + + group('Cursor triggers', () { + test('nodes with a click trigger offer cursor enter and exit', () { + final List nodes = [ + RectangleNode(id: 'r', name: 'R', basicBoxLocal: NodeBox(0, 0, 1, 1)), + FrameNode( + id: 'f', + name: 'F', + basicBoxLocal: NodeBox(0, 0, 1, 1), + children: [], + ), + TextNode( + id: 't', + name: 'T', + basicBoxLocal: NodeBox(0, 0, 1, 1), + characters: 'Hi', + ), + ]; + for (final ReactionMixin node in nodes) { + expect(node.triggerTypes.first, TriggerType.click); + expect( + node.triggerTypes, + containsAll([TriggerType.cursorEnter, TriggerType.cursorExit]), + ); + } + }); + + test('cursor reactions round-trip', () { + final RectangleNode node = + RectangleNode(id: 'r', name: 'R', basicBoxLocal: NodeBox(0, 0, 1, 1)) + ..reactions = [ + Reaction( + trigger: TriggerModel(type: TriggerType.cursorEnter), + action: SetVariableAction( + variable: VariableData(id: 'h', name: 'hoveredId'), + newValue: r'${item.id}', + ), + ), + ]; + final BaseNode read = NodeJsonConverter().fromJson( + encodeDecode(node.toJson()), + )!; + expect( + (read as ReactionMixin).reactions.single.trigger.type, + TriggerType.cursorEnter, + ); + }); + }); +} diff --git a/test/node_json_size_test.dart b/test/node_json_size_test.dart new file mode 100644 index 00000000..bd50ff3b --- /dev/null +++ b/test/node_json_size_test.dart @@ -0,0 +1,109 @@ +import 'dart:convert'; + +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:test/test.dart'; + +/// What a page document pays per node, and that none of it is a default: +/// a key whose removal reads back as the same node is a value `toJson` +/// should have left out (see `tool/json_defaults_builder`). Typical nodes of +/// a built screen, as the editor stores them. +void main() { + test('stored node JSON is small and carries no default values', () { + final box = NodeBox(16, 120, 358, 48); + final nodes = [ + RowColumnNode( + id: 'ZPcdmSV2nPmmNYlEbP5a', + name: 'Rig card', + basicBoxLocal: box, + children: ['a1b2c3d4e5f6g7h8i9j0'], + rowColumnType: RowColumnType.row, + mainAxisAlignment: MainAxisAlignmentC.start, + padding: const EdgeInsetsModel.all(16), + horizontalFit: SizeFit.expanded, + verticalFit: SizeFit.shrinkWrap, + fills: [ + PaintModel.solid(color: const ColorRGB(r: 1, g: 1, b: 1), id: 'p1'), + ], + strokes: [ + PaintModel.solid( + color: const ColorRGB(r: 0.9, g: 0.9, b: 0.9), + id: 'p4', + ), + ], + strokeWeight: 1, + cornerRadius: const CornerRadius.all(RadiusModel.circular(12)), + reactions: [ + Reaction( + trigger: TriggerModel(type: TriggerType.click), + action: NavigationAction(destinationId: 'abcdefghij0123456789'), + ), + ], + ), + TextNode( + id: 'a1b2c3d4e5f6g7h8i9j0', + name: 'Title', + basicBoxLocal: box, + characters: r'Unit ${vehicle.unit}', + horizontalFit: SizeFit.shrinkWrap, + verticalFit: SizeFit.shrinkWrap, + textMixedProps: [ + StartEndProp( + start: 0, + end: 20, + fontSize: 16, + lineHeight: LineHeight.auto, + letterSpacing: LetterSpacing.zero, + fontName: const FontName( + family: 'Inter', + style: 'Bold', + weight: FontWeightNumeric.w700, + ), + fills: [ + PaintModel.solid( + color: const ColorRGB(r: 0.1, g: 0.1, b: 0.1), + id: 'p2', + ), + ], + ), + ], + ), + RectangleNode( + id: 'b1b2c3d4e5f6g7h8i9j0', + name: 'Divider', + basicBoxLocal: box, + fills: [ + PaintModel.solid( + color: const ColorRGB(r: 0.9, g: 0.9, b: 0.9), + id: 'p3', + ), + ], + ), + ]; + const maxBytes = {'rowColumn': 600, 'text': 380, 'rectangle': 160}; + for (final node in nodes) { + final json = + jsonDecode(jsonEncode(node.toJson())) as Map; + final int bytes = utf8.encode(jsonEncode(json)).length; + print('${node.type}: $bytes bytes ${jsonEncode(json)}'); + expect(bytes, lessThanOrEqualTo(maxBytes[node.type]!), reason: node.type); + // Reads back the same. + expect( + jsonEncode(NodeJsonConverter.instance.fromJson(json)!.toJson()), + jsonEncode(json), + ); + for (final key in json.keys) { + if (key == 'type' || key == 'id') continue; + final without = Map.from(json)..remove(key); + Object? back; + try { + back = jsonEncode( + NodeJsonConverter.instance.fromJson(without)!.toJson(), + ); + } catch (_) { + continue; // A required key. + } + expect(back, isNot(jsonEncode(json)), reason: '${node.type}.$key'); + } + } + }); +} diff --git a/test/nodes/input/signature_node_test.dart b/test/nodes/input/signature_node_test.dart new file mode 100644 index 00000000..787b09e4 --- /dev/null +++ b/test/nodes/input/signature_node_test.dart @@ -0,0 +1,91 @@ +import 'dart:convert'; + +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:test/test.dart'; + +/// Encodes [node] to JSON text and reads it back through the registry, the +/// way a stored layout is loaded. +BaseNode roundTrip(BaseNode node) => NodeJsonConverter.instance.fromJson( + jsonDecode(jsonEncode(node.toJson())) as Map, +)!; + +void main() { + test('a default signature pad writes only what it needs', () { + final json = SignatureNode.empty().toJson(); + expect(json['type'], 'signature'); + expect(json.containsKey('value'), isFalse); + expect(json['properties'], isEmpty); + + final read = roundTrip(SignatureNode.empty()) as SignatureNode; + expect(read.value, isEmpty); + expect(read.properties, SignatureProperties()); + expect(read.basicBoxLocal.width, kSignatureDefaultWidth); + expect(read.basicBoxLocal.height, kSignatureDefaultHeight); + }); + + test('a signature pad keeps its strokes, properties and bindings', () { + final node = SignatureNode( + id: 'sig', + name: 'Signature', + basicBoxLocal: NodeBox(0, 0, 320, 140), + value: [ + [ + [10, 20], + [30.5, 40], + ], + [ + [50, 60], + ], + ], + variables: {'value': r'${signature}', 'error': r'${signatureError}'}, + reactions: [ + Reaction( + trigger: TriggerModel(type: TriggerType.changed), + action: SetVariableAction( + variable: VariableData(id: 'signed', name: 'signed'), + newValue: 'true', + ), + ), + ], + properties: SignatureProperties( + inkColor: ColorRGBA.black, + inkWidth: 3, + placeholder: 'Sign', + showSignLine: false, + error: true, + enabled: false, + ), + ); + + final read = roundTrip(node) as SignatureNode; + expect(read.value, node.value); + expect(read.properties, node.properties); + expect(read.variables, node.variables); + expect(read.reactions.single.trigger.type, TriggerType.changed); + expect(read.triggerTypes, [TriggerType.changed]); + expect(read.toJson(), node.toJson()); + expect( + read.propertyVariables.map((value) => value.name), + containsAll(['value', 'error', 'enabled']), + ); + }); + + test('strokes skip malformed points', () { + expect( + strokesFromJson([ + [ + [1, 2], + ['x', 3], + [4], + ], + 'nope', + ]), + [ + [ + [1.0, 2.0], + ], + ], + ); + expect(strokesFromJson(null), isEmpty); + }); +} diff --git a/test/rigcheck_fields_test.dart b/test/rigcheck_fields_test.dart new file mode 100644 index 00000000..f0f75afd --- /dev/null +++ b/test/rigcheck_fields_test.dart @@ -0,0 +1,76 @@ +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:test/test.dart'; + +void main() { + test('ConfirmAction reads presentation like ShowDialogAction', () { + final ConfirmAction action = ConfirmAction.fromJson({ + 'type': 'confirm', + 'title': 'Reopen DF-3104?', + 'dialogId': 'reopen', + 'presentation': 'sideSheet', + 'sheetWidth': 440, + 'showDragHandle': true, + }); + expect(action.presentation, DialogPresentation.sideSheet); + expect(action.sheetWidth, 440); + expect(action.showDragHandle, isTrue); + expect(ConfirmAction.fromJson(action.toJson()), action); + expect( + ConfirmAction.fromJson({ + 'type': 'confirm', + 'title': 'x', + 'presentation': 'popover', + }).presentation, + DialogPresentation.dialog, + ); + expect( + ConfirmAction(title: 'x').toJson().keys, + isNot(contains('sheetWidth')), + ); + }); + + test('ShowToastAction keeps its accent, icon color, font and radius', () { + final ShowToastAction toast = ShowToastAction( + message: 'Saved', + accentColor: ColorRGBA.black, + iconColor: ColorRGBA.white, + fontFamily: 'Barlow', + cornerRadius: 4, + ); + final ShowToastAction copy = ShowToastAction.fromJson(toast.toJson()); + expect(copy, toast); + expect(copy.accentWidth, 4); + expect( + copy.copyWith(clearAccentColor: true, clearFontFamily: true), + isA() + .having((t) => t.accentColor, 'accentColor', isNull) + .having((t) => t.fontFamily, 'fontFamily', isNull), + ); + }); + + test('a text field can bind errorText and error', () { + final TextFieldNode field = TextFieldNode( + id: 'f', + name: 'f', + basicBoxLocal: NodeBox(0, 0, 100, 40), + properties: TextFieldProperties(), + ); + expect( + field.propertyVariables.map((value) => value.name), + containsAll(['errorText', 'error']), + ); + }); + + test('frame contentFit reads from JSON', () { + final FrameNode frame = FrameNode.fromJson({ + 'id': 'f', + 'name': 'f', + 'type': 'frame', + 'basicBoxLocal': [0, 0, 300, 660], + 'children': [], + 'contentFit': 'contain', + }); + expect(frame.contentFit, Fit.contain); + expect(frame.contentAlignment, AlignmentModel.center); + }); +} diff --git a/test/show_dialog_action_test.dart b/test/show_dialog_action_test.dart new file mode 100644 index 00000000..d54d1ad1 --- /dev/null +++ b/test/show_dialog_action_test.dart @@ -0,0 +1,66 @@ +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:test/test.dart'; + +void main() { + test('A saved ShowDialogAction stays a dialog and omits defaults', () { + final action = ActionModel.fromJson({ + 'type': 'showDialog', + 'destinationId': 'canvas-1', + 'params': {'id': r'${data.id}'}, + }) as ShowDialogAction; + expect(action.presentation, DialogPresentation.dialog); + expect(action.sheetWidth, isNull); + expect(action.maxHeightFactor, 1); + expect(action.showDragHandle, isFalse); + expect(action.toJson(), { + 'type': 'showDialog', + 'destinationId': 'canvas-1', + 'params': {'id': r'${data.id}'}, + }); + }); + + test('ShowDialogAction round-trips a bottom sheet', () { + final json = { + 'type': 'showDialog', + 'destinationId': 'account', + 'params': {}, + 'presentation': 'bottomSheet', + 'maxHeightFactor': 0.8, + 'showDragHandle': true, + }; + final action = ActionModel.fromJson(json) as ShowDialogAction; + expect(action.presentation, DialogPresentation.bottomSheet); + expect(action.maxHeightFactor, 0.8); + expect(action.showDragHandle, isTrue); + expect(action.toJson(), json); + expect(ActionModel.fromJson(action.toJson()), action); + }); + + test('ShowDialogAction round-trips a side sheet', () { + final action = ShowDialogAction( + destinationId: 'flag-defect', + presentation: DialogPresentation.sideSheet, + sheetWidth: 480, + barrierDismissible: false, + ); + final Map json = action.toJson(); + expect(json['presentation'], 'sideSheet'); + expect(json['sheetWidth'], 480); + expect(json.containsKey('maxHeightFactor'), isFalse); + expect(ActionModel.fromJson(json), action); + + final cleared = action.copyWith(clearSheetWidth: true); + expect(cleared.sheetWidth, isNull); + expect(cleared.presentation, DialogPresentation.sideSheet); + }); + + test('An unknown presentation falls back to a dialog', () { + final action = ActionModel.fromJson({ + 'type': 'showDialog', + 'destinationId': 'x', + 'presentation': 'popover', + }) as ShowDialogAction; + expect(action.presentation, DialogPresentation.dialog); + expect(DialogPresentation.bottomSheet.prettify, 'Bottom Sheet'); + }); +}