Skip to content
Open
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,37 @@
- A variable whose value is a `${= ...}` formula keeps it for every type
(computed variables).

### Nodes

- `SignatureNode` (`signature`): a box to sign in. Its `value` is a list of
strokes, each a list of `[x, y]` points normalized to the box; `inkColor`,
`strokeWidth` and `readOnly` are in `SignatureProperties`.
- `SpacerNode` parses JSON without `positioningMode`, `aspectRatioLock` or
`reactions`, like other nodes. Its constructor didn't take them, so the
generated `fromJson` required them.

### Triggers and actions

- `TriggerType.hold` fires once the target has been held for the trigger's
`duration` (milliseconds, 1200 by default). Frames, rectangles, rows and
columns offer it.
- Rows and columns declare the click and long-press triggers they already
ran, so the actions panel offers them.
- `DialogPresentation.fullScreen`: a show dialog action can cover the
whole screen, like a photo viewer.
- A navigate-back (`pop`) action reads from JSON without `destinationId`,
which it has no use for. It is still written, as `''`, for readers that
require it.

### Bindings

- `strokeWeight` is a bindable style property of every node with strokes.
- `flex` is one of the properties every node can bind, set by a condition
or change with an action.
- `IconNode` declares `iconColor` and `iconName` as properties that can be
bound, set by conditions or changed by actions; `iconName` names the
Material Symbol to show.

### JSON

- List, grid, page and tab views and spacers parse from minimal node JSON:
Expand Down
9 changes: 7 additions & 2 deletions lib/src/api/mixins.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1045,8 +1045,8 @@ mixin PropertyVariableMixin on BaseNode {
/// Properties of the node that can be used in actions or can be changed by
/// an action.
///
/// Style properties (fill, stroke, opacity, width, height; a text's fill is
/// its color) can be set by
/// Style properties (fill, stroke, strokeWeight, opacity, width, height,
/// flex; a text's fill is its color) can be set by
/// conditions, bound to variables or formulas, or changed by a set value
/// action, on any node that has them.
late final List<ValueModel> propertyVariables = [
Expand All @@ -1055,6 +1055,8 @@ mixin PropertyVariableMixin on BaseNode {
if (this case final GeometryMixin geometry) ...[
PaintValue(name: 'fill', value: geometry.fills.firstOrNull),
PaintValue(name: 'stroke', value: geometry.strokes.firstOrNull),
// A border that thickens when selected or hovered.
DoubleValue(name: 'strokeWeight', value: geometry.strokeWeight),
],
if (this case final TextNode text)
PaintValue(
Expand All @@ -1065,6 +1067,9 @@ mixin PropertyVariableMixin on BaseNode {
DoubleValue(name: 'opacity', value: blend.opacity),
DoubleValue(name: 'width', value: basicBoxLocal.width),
DoubleValue(name: 'height', value: basicBoxLocal.height),
// A child's share of its row or column's free space when it expands,
// e.g. the bars of a gauge sized by data.
IntValue(name: 'flex', value: flex),
];
}

Expand Down
7 changes: 6 additions & 1 deletion lib/src/api/models/action/navigation_action.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,18 @@ enum NavigationType {
}
}

Object? _readDestinationId(Map json, String key) => json[key] ?? '';

/// An action that navigates to a new page.
@JsonSerializable()
class NavigationAction extends ActionModel with Equatable, SerializableMixin {
/// Defines how the navigation should be performed.
final NavigationType navigationType;

/// Destination canvas node's ID.
/// Destination canvas node's ID. Empty for [NavigationType.pop], which
/// has no destination, and still written so older readers, which require
/// it, can read the action.
@JsonKey(readValue: _readDestinationId)
final String destinationId;

/// Parameters to pass to the destination page.
Expand Down
2 changes: 1 addition & 1 deletion lib/src/api/models/action/navigation_action.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions lib/src/api/models/action/show_dialog_action.dart
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,7 @@ enum DialogPresentation {

/// A bottom sheet on phones (narrower than 600 px), a dialog otherwise.
adaptive,

/// Covers the whole screen, like a photo viewer.
fullScreen,
}
1 change: 1 addition & 0 deletions lib/src/api/models/action/show_dialog_action.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 27 additions & 3 deletions lib/src/api/models/trigger.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,13 @@ enum TriggerType {

/// Triggered when the variable named by [TriggerModel.variable] changes,
/// while the screen it is set on shows.
valueChanged;
valueChanged,

/// Triggered when the user presses and holds the target for the trigger's
/// [TriggerModel.duration]. Releasing early cancels it. While held, the
/// target's descendants can read the progress, 0 to 1, as `${hold}`, for
/// example to fill a hold-to-confirm button.
hold;

/// Displayable string representation of the trigger type.
String get prettify {
Expand Down Expand Up @@ -78,10 +84,16 @@ enum TriggerType {
return 'Load';
case TriggerType.valueChanged:
return 'Value Changed';
case TriggerType.hold:
return 'Hold to Confirm';
}
}
}

/// How long a [TriggerType.hold] trigger is held by default, in
/// milliseconds.
const int kDefaultHoldDuration = 1200;

/// A model representing a trigger event data.
@JsonSerializable()
class TriggerModel with Equatable, SerializableMixin {
Expand All @@ -97,21 +109,33 @@ class TriggerModel with Equatable, SerializableMixin {
/// For [TriggerType.valueChanged], the name of the variable to watch.
final String? variable;

/// How long, in milliseconds, a [TriggerType.hold] trigger must be held
/// before it fires.
@JsonKey(defaultValue: kDefaultHoldDuration)
final int duration;

/// Creates a [TriggerModel] with given data.
TriggerModel({required this.type, this.debounceMs = 0, this.variable});
TriggerModel({
required this.type,
this.debounceMs = 0,
this.variable,
this.duration = kDefaultHoldDuration,
});

@override
List<Object?> get props => [type, debounceMs, variable];
List<Object?> get props => [type, debounceMs, variable, duration];

/// Duplicate this [TriggerModel] with given data overrides.
TriggerModel copyWith({
TriggerType? type,
int? debounceMs,
String? variable,
int? duration,
}) => TriggerModel(
type: type ?? this.type,
debounceMs: debounceMs ?? this.debounceMs,
variable: variable ?? this.variable,
duration: duration ?? this.duration,
);

/// Factory constructor for creating a new [TriggerModel] instance from
Expand Down
5 changes: 5 additions & 0 deletions lib/src/api/models/trigger.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions lib/src/api/node_json_converter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class NodeJsonConverter implements JsonConverter<BaseNode?, Map> {
'switch': SwitchNode.fromJson,
'radio': RadioNode.fromJson,
'slider': SliderNode.fromJson,
'signature': SignatureNode.fromJson,
'expansionTile': ExpansionTileNode.fromJson,
'accordion': AccordionNode.fromJson,
'listTile': ListTileNode.fromJson,
Expand Down
1 change: 1 addition & 0 deletions lib/src/api/nodes/frame_node.dart
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ class FrameNode extends DefaultShapeNode
List<TriggerType> get triggerTypes => [
TriggerType.click,
TriggerType.longPress,
TriggerType.hold,
];

/// Creates a [FrameNode] from a JSON data.
Expand Down
11 changes: 11 additions & 0 deletions lib/src/api/nodes/icon_node.dart
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,17 @@ class IconNode extends SceneNode
TriggerType.longPress,
];

/// Besides the shared style properties, an icon's color and which icon it
/// shows can be bound, set by a condition or changed by an action:
/// `iconName` is a Material Symbols name such as `wb_sunny` (the node's
/// style is kept), so one icon node can show the icon its data asks for.
@override
late final List<ValueModel> propertyVariables = [
...super.propertyVariables,
ColorValue(name: 'iconColor', value: properties.icon.color),
StringValue(name: 'iconName', value: properties.icon.icon?.name ?? ''),
];

/// Creates an [IconNode] from a JSON data.
factory IconNode.fromJson(Map json) {
// backwards compatibility: before properties were added to IconNode.
Expand Down
1 change: 1 addition & 0 deletions lib/src/api/nodes/nodes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export 'row_column_node.dart';
export 'scene_node.dart';
export 'segmented_button_node.dart';
export 'single_placeholder_node.dart';
export 'signature_node.dart';
export 'slider_node.dart';
export 'spacer_node.dart';
export 'switch_node.dart';
Expand Down
1 change: 1 addition & 0 deletions lib/src/api/nodes/rectangle_node.dart
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ class RectangleNode extends DefaultShapeNode with CornerMixin, ClipMixin {
List<TriggerType> get triggerTypes => [
TriggerType.click,
TriggerType.longPress,
TriggerType.hold,
];

@override
Expand Down
9 changes: 9 additions & 0 deletions lib/src/api/nodes/row_column_node.dart
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,15 @@ class RowColumnNode extends ParentNode
);
}

/// Rows and columns are the usual buttons and cards of a layout, so they
/// take taps, long presses and holds.
@override
List<TriggerType> get triggerTypes => [
TriggerType.click,
TriggerType.longPress,
TriggerType.hold,
];

@override
FieldsMap generateFields() => {
...super.generateFields(),
Expand Down
Loading
Loading