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
64 changes: 64 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,67 @@
## Unreleased

Needs the `codelessly_api` release after 6.2.0 (signature, loop animation,
side sheet, confirm presentation, toast styling, `contentFit` and
`shrinkWrapRows`).

### Nodes and actions

- `PassiveSignatureTransformer` draws a `SignatureNode`: strokes in
round-cap ink, the sign line and placeholder, the error border when its
`error` binding is true. A finished stroke writes all strokes to the
bound value and fires `changed`; setting the value to `[]` clears it.
- Loop animations: nodes with a `loopAnimation` build inside a
`LoopAnimationBox` (pulse, fade, scale or spin) that `loopAnimationEnabled`
turns on and off without remounting, and that holds still under a
disabled `TickerMode` (the editor's design canvas).
- `cursorEnter`, `cursorExit` and `hover` reactions run, through a
translucent `MouseRegion`.
- Dialog canvases also open as side sheets (`DialogPresentation.sideSheet`,
`sheetWidth`, `LayoutPresentationRoute`), and bottom sheets take
`maxHeightFactor` and `showDragHandle`. What a dialog canvas passes back
on Navigate back becomes the chain's result.
- `ConfirmAction.presentation`: the built-in dialog and a `dialogId` canvas
open as a dialog, bottom sheet, adaptively or as a side sheet, like
`showDialog`.
- Toasts: `accentColor`/`accentWidth` draw a bar down the left edge,
`iconColor` colors the icon, `fontFamily` sets the font and
`cornerRadius` the corners (default still a pill).
- A node whose `disabled` binding is true and that has no Disabled
interaction state of its own fades to 40 % (it already ignored taps).

### Forms and formulas

- A text field's boolean `error` binding shows its error borders without a
message.
- Formulas: `left(text, n)`, `right(text, n)` and `toMap(list, key,
value)`. A lambda can take the item's index as a second parameter:
`map(steps, (s, i) => i == length(steps) - 1)`, also in filter, find,
count, any, all, sortBy, unique, sum, join and toMap.
- `isEmpty` treats null as empty, so a text variable holding "" is empty,
and the `isEmpty` / `isNotEmpty` accessors of list variables work.

### Layout

- `FrameNode.contentFit` lays the frame's children out at its design size
and scales them into the frame (`ScaledContent`, a filling `FittedBox`).
- The `parent` variable: `${parent.width}` and `${parent.height}` are the
size a node's parent gives it (`ParentSizeScope`), for layouts that change
with their container's width rather than the screen's.
- A spacer that flexes on its parent's main axis is wrapped in its
`Expanded`/`Flexible` by the row or column, outside every wrapper built
around it, so it takes the free space like Flutter's `Spacer` also under
a `NodeBuildDecorator` (the editor's render canvas drew it at its design
size).
- A child that fills across a row or column that hugs that axis stretches
to the size its other children give it (the row is measured with
`IntrinsicHeight` / `IntrinsicWidth`), like a card's colored edge as tall
as its text.
- A `fixedCrossAxisCount` grid keeps its count at every width and divides
the grid's width between the cells (`DesignCellGridDelegate` is gone).
- `GridDelegateProperties.shrinkWrapRows`: rows as tall as their tallest
item, built as a list of rows of `Expanded` cells in an `IntrinsicHeight`
(`GridViewBuilder.intrinsicRows`).

## 8.3.0

Requires codelessly_api 6.2.0.
Expand Down
236 changes: 156 additions & 80 deletions lib/src/functions/app_confirm_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../../codelessly_sdk.dart';
import '../ui/layout_presentation_route.dart';
import 'action_chain.dart';

/// How a [ConfirmAction]'s dialog closed.
Expand Down Expand Up @@ -45,6 +46,10 @@ class AppConfirmDialog {
params: action.params,
barrierDismissible: action.barrierDismissible,
barrierColor: action.barrierColor?.toFlutterColor(),
presentation: action.presentation,
sheetWidth: action.sheetWidth,
maxHeightFactor: action.maxHeightFactor,
showDragHandle: action.showDragHandle,
);
final bool confirmed = ConfirmAction.isConfirmResult(result);
return (
Expand Down Expand Up @@ -79,6 +84,42 @@ class AppConfirmDialog {
// the layout.
final ResolvedFeedbackTheme theme = FeedbackTheme.of(context);

Widget buildDialog(BuildContext context) => ThemedConfirmDialog(
theme: theme,
presentation: action.presentation.resolveFor(context),
title: title,
message: message,
confirmLabel: confirmLabel.isEmpty ? 'OK' : confirmLabel,
cancelLabel: cancelLabel.isEmpty ? 'Cancel' : cancelLabel,
destructive: action.destructive,
backgroundColor: action.backgroundColor?.toFlutterColor(),
textColor: action.textColor?.toFlutterColor(),
confirmColor: action.confirmColor?.toFlutterColor(),
cornerRadius: action.cornerRadius,
);

final DialogPresentation presentation = action.presentation.resolveFor(
context,
);
if (presentation != DialogPresentation.dialog) {
final Object? answer = await Navigator.of(context).push<Object?>(
LayoutPresentationRoute<Object?>(
context: context,
presentation: presentation,
barrierDismissible: action.barrierDismissible,
barrierColor:
action.barrierColor?.toFlutterColor() ??
Theme.of(context).dialogTheme.barrierColor ??
Colors.black54,
sheetWidth: action.sheetWidth,
maxHeightFactor: action.maxHeightFactor,
showDragHandle: action.showDragHandle,
builder: buildDialog,
),
);
return (confirmed: answer == true, values: null);
}

final bool? confirmed = await showDialog<bool>(
context: context,
useRootNavigator: false,
Expand Down Expand Up @@ -108,6 +149,7 @@ class ThemedConfirmDialog extends StatelessWidget {
const ThemedConfirmDialog({
super.key,
required this.theme,
this.presentation = DialogPresentation.dialog,
required this.title,
required this.message,
required this.confirmLabel,
Expand All @@ -122,6 +164,10 @@ class ThemedConfirmDialog extends StatelessWidget {
/// The app's look.
final ResolvedFeedbackTheme theme;

/// Whether it is a centered dialog or fills a bottom or side sheet. A
/// sheet's route ([LayoutPresentationRoute]) places and clips it.
final DialogPresentation presentation;

/// Title; hidden when empty.
final String title;

Expand Down Expand Up @@ -171,93 +217,123 @@ class ThemedConfirmDialog extends StatelessWidget {
const Size buttonSize = Size(64, 44);
const EdgeInsets buttonPadding = EdgeInsets.symmetric(horizontal: 18);

final Widget content = Semantics(
scopesRoute: true,
namesRoute: true,
explicitChildNodes: true,
label: title.isEmpty ? null : title,
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 24, 24, 16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (title.isNotEmpty)
Text(
title,
style: theme.titleStyle.copyWith(
fontSize: 18,
height: 1.3,
color: onSurface,
),
),
if (title.isNotEmpty && message.isNotEmpty)
const SizedBox(height: 8),
if (message.isNotEmpty)
Flexible(
child: SingleChildScrollView(
child: Text(
message,
style: theme.bodyStyle.copyWith(
fontSize: 15,
height: 1.45,
color: onSurface.withValues(alpha: 0.74),
),
),
),
),
const SizedBox(height: 20),
OverflowBar(
alignment: MainAxisAlignment.end,
overflowAlignment: OverflowBarAlignment.end,
spacing: 8,
overflowSpacing: 8,
children: [
TextButton(
key: const ValueKey('codelessly-confirm-cancel'),
onPressed: () => Navigator.of(context).pop(false),
style: TextButton.styleFrom(
foregroundColor: onSurface,
textStyle: buttonText,
shape: buttonShape,
minimumSize: buttonSize,
padding: buttonPadding,
),
child: Text(cancelLabel),
),
FilledButton(
key: const ValueKey('codelessly-confirm-ok'),
onPressed: () => Navigator.of(context).pop(true),
style: FilledButton.styleFrom(
backgroundColor: confirmFill,
foregroundColor: onConfirm,
textStyle: buttonText,
shape: buttonShape,
minimumSize: buttonSize,
padding: buttonPadding,
),
child: Text(confirmLabel),
),
],
),
],
),
),
);

return Theme(
data: theme.apply(Theme.of(context)),
child: Dialog(
backgroundColor: surface,
surfaceTintColor: Colors.transparent,
elevation: 8,
insetPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radius),
child: switch (presentation) {
DialogPresentation.dialog => Dialog(
backgroundColor: surface,
surfaceTintColor: Colors.transparent,
elevation: 8,
insetPadding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 24,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radius),
),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: content,
),
),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: Semantics(
scopesRoute: true,
namesRoute: true,
explicitChildNodes: true,
label: title.isEmpty ? null : title,
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 24, 24, 16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (title.isNotEmpty)
Text(
title,
style: theme.titleStyle.copyWith(
fontSize: 18,
height: 1.3,
color: onSurface,
),
),
if (title.isNotEmpty && message.isNotEmpty)
const SizedBox(height: 8),
if (message.isNotEmpty)
Flexible(
child: SingleChildScrollView(
child: Text(
message,
style: theme.bodyStyle.copyWith(
fontSize: 15,
height: 1.45,
color: onSurface.withValues(alpha: 0.74),
),
),
),
),
const SizedBox(height: 20),
OverflowBar(
alignment: MainAxisAlignment.end,
overflowAlignment: OverflowBarAlignment.end,
spacing: 8,
overflowSpacing: 8,
children: [
TextButton(
key: const ValueKey('codelessly-confirm-cancel'),
onPressed: () => Navigator.of(context).pop(false),
style: TextButton.styleFrom(
foregroundColor: onSurface,
textStyle: buttonText,
shape: buttonShape,
minimumSize: buttonSize,
padding: buttonPadding,
),
child: Text(cancelLabel),
),
FilledButton(
key: const ValueKey('codelessly-confirm-ok'),
onPressed: () => Navigator.of(context).pop(true),
style: FilledButton.styleFrom(
backgroundColor: confirmFill,
foregroundColor: onConfirm,
textStyle: buttonText,
shape: buttonShape,
minimumSize: buttonSize,
padding: buttonPadding,
),
child: Text(confirmLabel),
),
],
),
],
// A sheet is as wide as its route makes it; its content keeps clear
// of the home indicator and the bottom sheet's grab handle.
DialogPresentation.bottomSheet ||
DialogPresentation.adaptive => SizedBox(
width: double.infinity,
child: Material(
color: surface,
child: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.only(top: 8),
child: content,
),
),
),
),
),
DialogPresentation.sideSheet => Material(
color: surface,
child: SafeArea(
left: false,
child: Align(alignment: Alignment.topLeft, child: content),
),
),
},
);
}
}
Loading
Loading