Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
## Unreleased

### Layout

- Row/column `spacing`: a gap between children along the main axis, taken
out before expanded children share the free space.
- Row/column `wrap` and `runSpacing`: children wrap onto further runs, like
Flutter's `Wrap`.

### Variables

- A variable whose value is a `${= ...}` formula keeps it for every type
(computed variables).

## 6.1.0

### States and conditions
Expand Down
46 changes: 46 additions & 0 deletions lib/src/api/mixins.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1109,6 +1109,46 @@ mixin RowColumnMixin on BaseNode {
/// for details on available alignments.
late CrossAxisAlignmentC crossAxisAlignment;

/// The gap between consecutive children along the main axis, in logical
/// pixels. Corresponds to the `spacing` property of [Row] and [Column] in
/// Flutter and to item spacing in Figma auto-layout.
///
/// Unlike margins, the spacing is subtracted from the main axis extent
/// before free space is distributed to flexible children, so expanded
/// children with equal flex receive equal sizes.
@JsonKey(defaultValue: 0.0)
double spacing = 0;

/// Whether the children wrap onto further runs (lines) when they do not
/// fit along the main axis, like Flutter's [Wrap] and Figma auto-layout
/// wrap. A wrapping row lays its children out like
/// `Wrap(direction: Axis.horizontal)` and a wrapping column like
/// `Wrap(direction: Axis.vertical)`.
///
/// While wrapping:
/// - [spacing] is the gap between consecutive children within a run and
/// [runSpacing] the gap between runs. Children hidden by a visibility
/// formula take no space and no gaps.
/// - [mainAxisAlignment] aligns the children within each run (Flutter's
/// `Wrap.alignment`).
/// - [crossAxisAlignment] aligns the children within their run
/// (`Wrap.crossAxisAlignment`; stretch and baseline act as start) and
/// also aligns the runs in the cross-axis free space
/// (`Wrap.runAlignment`: start, center or end), as in Figma.
/// - Children cannot be flexible: an expanded (or flex) child is laid out
/// at its own size, as a fixed child, like inside a Flutter [Wrap].
/// A child that fills the main axis takes a whole run.
/// - A shrink-wrapping cross axis hugs the runs; a shrink-wrapping main
/// axis hugs the widest run.
@JsonKey(defaultValue: false)
bool wrap = false;

/// The gap between consecutive runs along the cross axis, in logical
/// pixels, when [wrap] is on. Corresponds to `Wrap.runSpacing` in
/// Flutter. Has no effect when [wrap] is off.
@JsonKey(defaultValue: 0.0)
double runSpacing = 0;

/// Returns the main axis based on whether this is a row or column.
AxisC get mainAxis =>
(rowColumnType == RowColumnType.row) ? AxisC.horizontal : AxisC.vertical;
Expand All @@ -1122,10 +1162,16 @@ mixin RowColumnMixin on BaseNode {
required RowColumnType rowColumnType,
required MainAxisAlignmentC mainAxisAlignment,
required CrossAxisAlignmentC crossAxisAlignment,
double spacing = 0,
bool wrap = false,
double runSpacing = 0,
}) {
this.rowColumnType = rowColumnType;
this.mainAxisAlignment = mainAxisAlignment;
this.crossAxisAlignment = crossAxisAlignment;
this.spacing = spacing;
this.wrap = wrap;
this.runSpacing = runSpacing;
}

@override
Expand Down
8 changes: 6 additions & 2 deletions lib/src/api/models/variables_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,12 @@ class VariableData
String? sanitizeValueForVariableType(Object? value, VariableType type) {
if (value == null) return null;
// if (value.isEmpty) return '';
// Do not sanitize if the value contains a variable path.
if (value.toString().contains(variablePathPattern)) return value.toString();
// Do not sanitize if the value contains a variable path or a formula: a
// computed variable's value is a `${= ...}` formula whatever its type.
if (value.toString().contains(variablePathPattern) ||
value.toString().contains(r'${=')) {
return value.toString();
}
switch (type) {
case VariableType.text:
case VariableType.image:
Expand Down
9 changes: 9 additions & 0 deletions lib/src/api/nodes/accordion_node.g.dart

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

9 changes: 9 additions & 0 deletions lib/src/api/nodes/auto_placeholder_node.g.dart

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

9 changes: 9 additions & 0 deletions lib/src/api/nodes/canvas_node.g.dart

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

9 changes: 9 additions & 0 deletions lib/src/api/nodes/expansion_tile_node.g.dart

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

46 changes: 44 additions & 2 deletions lib/src/api/nodes/row_column_node.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:json_annotation/json_annotation.dart';

import '../field_access.dart';
import '../mixins.dart';
import '../models/models.dart';

Expand All @@ -16,10 +17,14 @@ class RowColumnNode extends ParentNode
@override
final bool supportsPadding = true;

/// A row/column scrolls along its main axis. One that [wrap]s scrolls
/// along its cross axis instead, through its runs: its main axis is where
/// it wraps, so it needs a bounded extent there.
@JsonKey(includeFromJson: false)
@override
AxisC get scrollDirection =>
rowColumnType == RowColumnType.row ? AxisC.horizontal : AxisC.vertical;
AxisC get scrollDirection => (rowColumnType == RowColumnType.row) != wrap
? AxisC.horizontal
: AxisC.vertical;

@override
set scrollDirection(AxisC value) {}
Expand All @@ -34,6 +39,9 @@ class RowColumnNode extends ParentNode
required RowColumnType rowColumnType,
MainAxisAlignmentC mainAxisAlignment = MainAxisAlignmentC.center,
CrossAxisAlignmentC crossAxisAlignment = CrossAxisAlignmentC.center,
double spacing = 0,
bool wrap = false,
double runSpacing = 0,
super.retainedOuterBoxLocal,
super.visible,
super.enabled,
Expand Down Expand Up @@ -86,6 +94,9 @@ class RowColumnNode extends ParentNode
rowColumnType: rowColumnType,
mainAxisAlignment: mainAxisAlignment,
crossAxisAlignment: crossAxisAlignment,
spacing: spacing,
wrap: wrap,
runSpacing: runSpacing,
);

setScrollableMixin(
Expand All @@ -101,6 +112,37 @@ class RowColumnNode extends ParentNode
);
}

@override
FieldsMap generateFields() => {
...super.generateFields(),
'spacing': NumFieldAccess<double>(
'Spacing',
'Gap between consecutive children along the main axis.',
() => spacing,
(value) => spacing = value,
defaultValue: () => 0,
min: 0,
requiresLayout: true,
),
'wrap': BoolFieldAccess(
'Wrap',
'Whether children wrap onto further runs when they do not fit.',
() => wrap,
(value) => wrap = value,
defaultValue: () => false,
requiresLayout: true,
),
'runSpacing': NumFieldAccess<double>(
'Run Spacing',
'Gap between runs along the cross axis when wrapping.',
() => runSpacing,
(value) => runSpacing = value,
defaultValue: () => 0,
min: 0,
requiresLayout: true,
),
};

/// Creates a [RowColumnNode] from a JSON data.
factory RowColumnNode.fromJson(Map json) => _$RowColumnNodeFromJson(json);

Expand Down
9 changes: 9 additions & 0 deletions lib/src/api/nodes/row_column_node.g.dart

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

15 changes: 15 additions & 0 deletions test/computed_variable_value_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import 'package:codelessly_api/codelessly_api.dart';
import 'package:test/test.dart';

void main() {
test('a formula value survives sanitizing for every type', () {
const String formula = r"${= find(trips, 'id', selected)}";
for (final VariableType type in VariableType.values) {
expect(
VariableData(id: 'v', name: 'v', type: type, value: formula).value,
formula,
reason: type.name,
);
}
});
}
Loading
Loading