Skip to content
Open
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
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,38 @@ Requires codelessly_api 6.2.0.
- Row/column `spacing`, and `wrap` with `runSpacing` (`CodelesslyWrap`).
Children hidden by a visibility formula take no space and no gap.
- `flex` and `rotation` can be bound to data like fill, opacity and size.
- An expanded child whose bound `flex` is 0 takes no space, where it used to
keep a sliver.

### Signing

- Signature nodes take a signature by finger, pen or mouse and write its
strokes to the bound `value` as the pen goes down and lifts, firing
`changed`. Setting the variable redraws the box: `[]` clears it, and a
saved signature replays it. Read-only boxes only show.
- The hold trigger runs its actions once the node has been held for its
duration. Letting go or moving off drains it. While it is held, the node
and the nodes inside read the progress, 0 to 1, as `${hold}`, to fill a
bar or change a label.
- `strokeWeight` can be bound, set by a condition or changed by an action,
for a border that thickens when picked.

### Dialogs

- A responsive layout in a centered dialog shows the layout for the screen,
as a layout group in a dialog picks its canvas. It went by the dialog's
own width, so a 520 wide dialog always showed its mobile layout.
- Show dialog actions present their canvas by `presentation`: a centered
dialog, a bottom sheet (full width, as tall as its content up to 90% of
the screen), a sheet on mobile and a dialog on larger screens, or full
screen. `CanvasSizing.sheet` sizes a canvas for a sheet.

### Icons

- An icon's `iconName` can be bound to data, set by a condition or changed by
an action. It names a Material Symbol, so one icon node can show the
icon each list item asks for; a Material Icon becomes the symbol of the
same style.

### Formulas and variables

Expand All @@ -20,6 +52,11 @@ Requires codelessly_api 6.2.0.
`padLeft`, `padRight`, `substring`, `split`, `replace`, `coalesce`,
`formatNumber` and `matches` (regular expressions). `money` takes a digit
count.
- Record helpers: `unset`, `prepend`, `updateWhere`, `removeWhere`,
`findIndex` and `newId`; `set` also takes a list and an index, and
`indexOf` and `slice` also take text. They return new values, so a set
variable action can write a nested change back, e.g. `updateWhere(logs,
l => l.id == logId, l => set(l, 'crew', append(l.crew, entry)))`.
- A map's own key wins over a built-in accessor of the same name
(`${rig.length}` reads the field).

Expand Down
43 changes: 33 additions & 10 deletions lib/src/functions/functions_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -694,28 +694,51 @@ class FunctionsRepository {
bool showCloseButton = false,
RouteSettings? routeSettings,
}) {
final Size? screen = MediaQuery.maybeSizeOf(context);
final bool asSheet = switch (presentation) {
DialogPresentation.dialog => false,
DialogPresentation.dialog || DialogPresentation.fullScreen => false,
DialogPresentation.bottomSheet => true,
DialogPresentation.adaptive =>
(MediaQuery.maybeSizeOf(context)?.width ?? 1024) < 600,
DialogPresentation.adaptive => (screen?.width ?? 1024) < 600,
};
Widget content(BuildContext context) => CodelesslyDialogWidget(
showCloseButton: showCloseButton,
alignment: asSheet ? Alignment.bottomCenter : alignment,
builder: builder,
);
Widget content(BuildContext context, CanvasSizing sizing) =>
CodelesslyDialogWidget(
showCloseButton: showCloseButton,
sizing: sizing,
alignment: asSheet ? Alignment.bottomCenter : alignment,
builder: builder,
);
if (asSheet) {
// As wide as the screen and as tall as its content, up to 90% of the
// screen; the canvas draws its own background and corners.
return showModalBottomSheet<T>(
context: context,
useRootNavigator: false,
isScrollControlled: true,
isDismissible: barrierDismissible,
enableDrag: barrierDismissible,
barrierColor: barrierColor,
backgroundColor: Colors.transparent,
elevation: 0,
constraints: screen == null
? null
: BoxConstraints(maxHeight: screen.height * 0.9),
routeSettings: routeSettings,
builder: (context) => content(context, CanvasSizing.sheet),
);
}
if (presentation == DialogPresentation.fullScreen) {
return showGeneralDialog<T>(
context: context,
useRootNavigator: false,
barrierDismissible: barrierDismissible,
barrierLabel: MaterialLocalizations.of(context)
.modalBarrierDismissLabel,
barrierColor: barrierColor ?? Colors.black,
routeSettings: routeSettings,
builder: content,
transitionDuration: const Duration(milliseconds: 200),
transitionBuilder: (context, animation, _, child) =>
FadeTransition(opacity: animation, child: child),
pageBuilder: (context, _, _) => content(context, CanvasSizing.fill),
);
}
return showDialog<T>(
Expand All @@ -724,7 +747,7 @@ class FunctionsRepository {
barrierDismissible: barrierDismissible,
barrierColor: barrierColor,
routeSettings: routeSettings,
builder: content,
builder: (context) => content(context, CanvasSizing.design),
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export 'passive_rectangle_transformer.dart';
export 'passive_row_column_transformer.dart';
export 'passive_segmented_button_transformer.dart';
export 'passive_single_placeholder_transformer.dart';
export 'passive_signature_transformer.dart';
export 'passive_slider_transformer.dart';
export 'passive_spacer_transformer.dart';
export 'passive_stack_transformer.dart';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,11 @@ enum CanvasSizing {
/// given. Used where the host only centers the layout and lets it choose
/// its size, such as a dialog.
design,

/// The canvas spans the width it is given and keeps its design height (or
/// its content's height when it shrink-wraps vertically), within the
/// height it is given. Used for bottom sheets.
sheet,
}

/// Sets the [CanvasSizing] of the canvases below it. Without one, canvases
Expand Down Expand Up @@ -514,8 +519,11 @@ class CanvasViewport extends StatelessWidget {
node.verticalFit == SizeFit.shrinkWrap &&
node.scaleMode == ScaleMode.responsive &&
!node.isAScaffold;
if (sizing == CanvasSizing.design) {
final double width = constraints.constrainWidth(node.basicBoxLocal.width);
if (sizing == CanvasSizing.design || sizing == CanvasSizing.sheet) {
final double width =
sizing == CanvasSizing.sheet && constraints.hasBoundedWidth
? constraints.maxWidth
: constraints.constrainWidth(node.basicBoxLocal.width);
if (contentHeight) return (width: width, height: null);
final double height = node.scaleMode == ScaleMode.autoScale
? autoScaleHeight(node, width)
Expand Down Expand Up @@ -544,11 +552,15 @@ class CanvasViewport extends StatelessWidget {
return LayoutBuilder(
builder: (context, constraints) {
final box = resolve(node, constraints, sizing: sizing);
// The width the canvas content is laid out at: the canvas box for a
// The width that picks responsive layouts: the canvas box for a
// responsive canvas, the design width for an autoScale one (its
// content is laid out at design size and then scaled).
// content is laid out at design size and then scaled). A centered
// dialog keeps its design width on every screen, so it goes by the
// screen, as a layout group in a dialog picks its canvas.
final Widget content = CanvasWidthScope(
width: node.scaleMode == ScaleMode.autoScale
width: sizing == CanvasSizing.design
? MediaQuery.sizeOf(context).width
: node.scaleMode == ScaleMode.autoScale
? node.basicBoxLocal.width
: box.width,
child: child,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,57 @@ class PassiveIconWidget extends StatelessWidget {

@override
Widget build(BuildContext context) {
final ScopedValues scopedValues = ScopedValues.of(context);
final ColorRGBA? iconColor =
PropertyValueDelegate.getPropertyValue<ColorRGBA>(
node,
'iconColor',
scopedValues: ScopedValues.of(context),
scopedValues: scopedValues,
);
final MultiSourceIconModel icon = iconColor == null
final String? iconName = PropertyValueDelegate.getPropertyValue<String>(
node,
'iconName',
scopedValues: scopedValues,
);
MultiSourceIconModel icon = iconColor == null
? node.properties.icon
: node.properties.icon.copyWith(color: iconColor);
if (iconName != null && iconName.trim().isNotEmpty) {
icon = withIconName(icon, iconName.trim());
}
return retrieveIconWidget(icon, node.basicBoxLocal.width) ??
SizedBox(
width: node.basicBoxLocal.width,
height: node.basicBoxLocal.height,
);
}
}

/// [icon] showing the Material Symbol [name] instead, for an `iconName`
/// bound to data. Material Symbols load by name alone (Material Icons need
/// each icon's version), so a Material Icon becomes the Material Symbol of
/// the same style, filled when it was filled.
MultiSourceIconModel withIconName(MultiSourceIconModel icon, String name) {
final IconModel? current = icon.icon;
if (current is MaterialSymbol) {
return icon.copyWith(icon: current.copyWith(name: name), show: true);
}
final MaterialIconStyle? style = current is MaterialIcon
? current.style
: null;
return icon.copyWith(
type: IconTypeEnum.icon,
show: true,
icon: MaterialSymbol(
codepoint: 0,
fontFamily: 'MaterialSymbols',
name: name,
style: switch (style) {
MaterialIconStyle.rounded => MaterialSymbolStyle.rounded,
MaterialIconStyle.sharp => MaterialSymbolStyle.sharp,
_ => MaterialSymbolStyle.outlined,
},
),
fill: style == MaterialIconStyle.filled ? 1 : icon.fill,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -494,17 +494,36 @@ class PassiveRowColumnWidget extends StatelessWidget {
if (sizedWidget is! Flexible) {
return FlexChildVisibility(collapsed: hidden, child: sizedWidget);
}
// Without flex the hidden child would be laid out with unbounded
// A flex bound to data (a bar's share of a gauge) or set by a
// condition replaces the node's own: the row lays out the Flexible, so
// the child's own style overrides can't change its share. One bound to
// no share (an empty progress fill) takes no space.
final num? boundFlex = child.variables.containsKey('flex')
? PropertyValueDelegate.getPropertyValue<num>(
child,
'flex',
scopedValues: ScopedValues.of(context),
)
: null;
final bool empty = boundFlex != null && boundFlex <= 0;
final bool collapsed = hidden || empty;
final int flex = boundFlex != null && boundFlex > 0
? boundFlex.round()
: sizedWidget.flex;
final bool isRow =
node is RowColumnMixin &&
(node as RowColumnMixin).rowColumnType == RowColumnType.row;
// Without flex the collapsed child would be laid out with unbounded
// main-axis constraints, which a scrollable inside it can't take
// (a hidden grid computed NaN offsets), so it gets no space at all.
return FlexChildVisibility(
collapsed: hidden,
collapsed: collapsed,
child: Flexible(
flex: hidden ? 0 : sizedWidget.flex,
flex: collapsed ? 0 : flex,
fit: sizedWidget.fit,
child: SizedBox(
width: hidden ? 0 : null,
height: hidden ? 0 : null,
width: hidden || (empty && isRow) ? 0 : null,
height: hidden || (empty && !isRow) ? 0 : null,
child: sizedWidget.child,
),
),
Expand Down
Loading
Loading