@@ -12,8 +12,9 @@ typedef ConfirmOutcome = ({bool confirmed, Map<String, dynamic>? values});
1212///
1313/// Without a [ConfirmAction.dialogId] the dialog is built here and takes the
1414/// look of the layout that shows it ([FeedbackTheme.of] ): its font, surface,
15- /// text and primary colors and its buttons' corner radius. The action's own
16- /// colors and radius override that look.
15+ /// text and primary colors and its buttons' corner radius, outline and
16+ /// shadows. The action's own colors, radii, borders, shadows and text styles
17+ /// override that look.
1718///
1819/// With a [ConfirmAction.dialogId] the dialog is that canvas. A button in it
1920/// answers with a "Navigate back" action whose params hold
@@ -95,6 +96,21 @@ class AppConfirmDialog {
9596 textColor: action.textColor? .toFlutterColor (),
9697 confirmColor: action.confirmColor? .toFlutterColor (),
9798 cornerRadius: action.cornerRadius,
99+ border: action.border == null ? null : borderSideOf (action.border! ),
100+ shadows: action.shadows == null ? null : boxShadowsOf (action.shadows),
101+ titleStyle: action.titleStyle,
102+ messageStyle: action.messageStyle,
103+ buttonStyle: action.buttonStyle,
104+ buttonRadius: action.buttonRadius,
105+ buttonBorder: action.buttonBorder == null
106+ ? null
107+ : borderSideOf (action.buttonBorder! ),
108+ buttonShadows: action.buttonShadows == null
109+ ? null
110+ : boxShadowsOf (action.buttonShadows),
111+ confirmTextColor: action.confirmTextColor? .toFlutterColor (),
112+ cancelColor: action.cancelColor? .toFlutterColor (),
113+ cancelTextColor: action.cancelTextColor? .toFlutterColor (),
98114 ),
99115 );
100116 return (confirmed: confirmed ?? false , values: null );
@@ -117,6 +133,17 @@ class ThemedConfirmDialog extends StatelessWidget {
117133 this .textColor,
118134 this .confirmColor,
119135 this .cornerRadius,
136+ this .border,
137+ this .shadows,
138+ this .titleStyle,
139+ this .messageStyle,
140+ this .buttonStyle,
141+ this .buttonRadius,
142+ this .buttonBorder,
143+ this .buttonShadows,
144+ this .confirmTextColor,
145+ this .cancelColor,
146+ this .cancelTextColor,
120147 });
121148
122149 /// The app's look.
@@ -149,6 +176,40 @@ class ThemedConfirmDialog extends StatelessWidget {
149176 /// Overrides the dialog's corner radius.
150177 final double ? cornerRadius;
151178
179+ /// Overrides the theme's dialog outline.
180+ final BorderSide ? border;
181+
182+ /// Overrides the theme's dialog shadows; set, they replace the elevation.
183+ final List <BoxShadow >? shadows;
184+
185+ /// Changes to the title's font, size and color.
186+ final TextStyleModel ? titleStyle;
187+
188+ /// Changes to the message's font, size and color.
189+ final TextStyleModel ? messageStyle;
190+
191+ /// Changes to the buttons' font, size and label color.
192+ final TextStyleModel ? buttonStyle;
193+
194+ /// Overrides the theme's button corner radius.
195+ final double ? buttonRadius;
196+
197+ /// Overrides the theme's button outline.
198+ final BorderSide ? buttonBorder;
199+
200+ /// Overrides the theme's button shadows. The cancel button takes them only
201+ /// when it has a [cancelColor] .
202+ final List <BoxShadow >? buttonShadows;
203+
204+ /// Label color of the confirm button.
205+ final Color ? confirmTextColor;
206+
207+ /// Fill of the cancel button; null leaves it a text button.
208+ final Color ? cancelColor;
209+
210+ /// Label color of the cancel button.
211+ final Color ? cancelTextColor;
212+
152213 @override
153214 Widget build (BuildContext context) {
154215 final Color surface = backgroundColor ?? theme.surface;
@@ -159,105 +220,157 @@ class ThemedConfirmDialog extends StatelessWidget {
159220 : ResolvedFeedbackTheme .onColor (surface));
160221 final Color confirmFill =
161222 confirmColor ?? (destructive ? theme.error : theme.primary);
162- final Color onConfirm = ResolvedFeedbackTheme .onColor (confirmFill);
163223 final double radius = cornerRadius ?? theme.radius;
224+ final double buttonCorner = buttonRadius ?? theme.buttonRadius;
225+ final BorderSide buttonSide =
226+ buttonBorder ?? theme.buttonBorder ?? BorderSide .none;
227+ final List <BoxShadow > buttonShadowList =
228+ buttonShadows ?? theme.buttonShadows ?? const [];
164229 final OutlinedBorder buttonShape = RoundedRectangleBorder (
165- borderRadius: BorderRadius .circular (theme.buttonRadius ),
230+ borderRadius: BorderRadius .circular (buttonCorner ),
166231 );
167- final TextStyle buttonText = theme.buttonStyle.copyWith (
168- fontSize: 15 ,
169- height: 1.2 ,
232+ final TextStyle buttonText = buttonStyle.applyTo (
233+ theme.buttonStyle.copyWith (fontSize: 15 , height: 1.2 ),
170234 );
235+ final Color onConfirm =
236+ buttonStyle? .color? .toFlutterColor () ??
237+ confirmTextColor ??
238+ ResolvedFeedbackTheme .onColor (confirmFill);
239+ final Color onCancel =
240+ buttonStyle? .color? .toFlutterColor () ?? cancelTextColor ?? onSurface;
171241 const Size buttonSize = Size (64 , 44 );
172242 const EdgeInsets buttonPadding = EdgeInsets .symmetric (horizontal: 18 );
243+ final BorderSide ? dialogSide = border ?? theme.border;
244+ final List <BoxShadow >? dialogShadows = shadows ?? theme.shadows;
173245
174- return Theme (
175- data: theme.apply (Theme .of (context)),
176- child: Dialog (
177- backgroundColor: surface,
178- surfaceTintColor: Colors .transparent,
179- elevation: 8 ,
180- insetPadding: const EdgeInsets .symmetric (horizontal: 24 , vertical: 24 ),
181- shape: RoundedRectangleBorder (
182- borderRadius: BorderRadius .circular (radius),
183- ),
184- child: ConstrainedBox (
185- constraints: const BoxConstraints (maxWidth: 400 ),
186- child: Semantics (
187- scopesRoute: true ,
188- namesRoute: true ,
189- explicitChildNodes: true ,
190- label: title.isEmpty ? null : title,
191- child: Padding (
192- padding: const EdgeInsets .fromLTRB (24 , 24 , 24 , 16 ),
193- child: Column (
194- mainAxisSize: MainAxisSize .min,
195- crossAxisAlignment: CrossAxisAlignment .start,
196- children: [
197- if (title.isNotEmpty)
198- Text (
199- title,
200- style: theme.titleStyle.copyWith (
201- fontSize: 18 ,
202- height: 1.3 ,
203- color: onSurface,
204- ),
246+ Widget withShadow (Widget button) => buttonShadowList.isEmpty
247+ ? button
248+ : DecoratedBox (
249+ decoration: BoxDecoration (
250+ borderRadius: BorderRadius .circular (buttonCorner),
251+ boxShadow: buttonShadowList,
252+ ),
253+ child: button,
254+ );
255+
256+ final Widget cancel = TextButton (
257+ key: const ValueKey ('codelessly-confirm-cancel' ),
258+ onPressed: () => Navigator .of (context).pop (false ),
259+ style: TextButton .styleFrom (
260+ foregroundColor: onCancel,
261+ backgroundColor: cancelColor,
262+ textStyle: buttonText,
263+ shape: buttonShape,
264+ side: buttonSide,
265+ minimumSize: buttonSize,
266+ padding: buttonPadding,
267+ ),
268+ child: Text (cancelLabel),
269+ );
270+ final Widget confirm = FilledButton (
271+ key: const ValueKey ('codelessly-confirm-ok' ),
272+ onPressed: () => Navigator .of (context).pop (true ),
273+ style: FilledButton .styleFrom (
274+ backgroundColor: confirmFill,
275+ foregroundColor: onConfirm,
276+ textStyle: buttonText,
277+ shape: buttonShape,
278+ side: buttonSide,
279+ minimumSize: buttonSize,
280+ padding: buttonPadding,
281+ ),
282+ child: Text (confirmLabel),
283+ );
284+
285+ Widget content = ConstrainedBox (
286+ constraints: const BoxConstraints (maxWidth: 400 ),
287+ child: Semantics (
288+ scopesRoute: true ,
289+ namesRoute: true ,
290+ explicitChildNodes: true ,
291+ label: title.isEmpty ? null : title,
292+ child: Padding (
293+ padding: const EdgeInsets .fromLTRB (24 , 24 , 24 , 16 ),
294+ child: Column (
295+ mainAxisSize: MainAxisSize .min,
296+ crossAxisAlignment: CrossAxisAlignment .start,
297+ children: [
298+ if (title.isNotEmpty)
299+ Text (
300+ title,
301+ style: titleStyle.applyTo (
302+ theme.titleStyle.copyWith (
303+ fontSize: 18 ,
304+ height: 1.3 ,
305+ color: onSurface,
205306 ),
206- if (title.isNotEmpty && message.isNotEmpty)
207- const SizedBox (height: 8 ),
208- if (message.isNotEmpty)
209- Flexible (
210- child: SingleChildScrollView (
211- child: Text (
212- message,
213- style: theme.bodyStyle.copyWith (
214- fontSize: 15 ,
215- height: 1.45 ,
216- color: onSurface.withValues (alpha: 0.74 ),
217- ),
307+ ),
308+ ),
309+ if (title.isNotEmpty && message.isNotEmpty)
310+ const SizedBox (height: 8 ),
311+ if (message.isNotEmpty)
312+ Flexible (
313+ child: SingleChildScrollView (
314+ child: Text (
315+ message,
316+ style: messageStyle.applyTo (
317+ theme.bodyStyle.copyWith (
318+ fontSize: 15 ,
319+ height: 1.45 ,
320+ color: onSurface.withValues (alpha: 0.74 ),
218321 ),
219322 ),
220323 ),
221- const SizedBox (height: 20 ),
222- OverflowBar (
223- alignment: MainAxisAlignment .end,
224- overflowAlignment: OverflowBarAlignment .end,
225- spacing: 8 ,
226- overflowSpacing: 8 ,
227- children: [
228- TextButton (
229- key: const ValueKey ('codelessly-confirm-cancel' ),
230- onPressed: () => Navigator .of (context).pop (false ),
231- style: TextButton .styleFrom (
232- foregroundColor: onSurface,
233- textStyle: buttonText,
234- shape: buttonShape,
235- minimumSize: buttonSize,
236- padding: buttonPadding,
237- ),
238- child: Text (cancelLabel),
239- ),
240- FilledButton (
241- key: const ValueKey ('codelessly-confirm-ok' ),
242- onPressed: () => Navigator .of (context).pop (true ),
243- style: FilledButton .styleFrom (
244- backgroundColor: confirmFill,
245- foregroundColor: onConfirm,
246- textStyle: buttonText,
247- shape: buttonShape,
248- minimumSize: buttonSize,
249- padding: buttonPadding,
250- ),
251- child: Text (confirmLabel),
252- ),
253- ],
254324 ),
325+ ),
326+ const SizedBox (height: 20 ),
327+ OverflowBar (
328+ alignment: MainAxisAlignment .end,
329+ overflowAlignment: OverflowBarAlignment .end,
330+ // Outlined or shadowed buttons need room between them.
331+ spacing: buttonShadowList.isEmpty ? 8 : 12 ,
332+ overflowSpacing: 8 ,
333+ children: [
334+ cancelColor != null ? withShadow (cancel) : cancel,
335+ withShadow (confirm),
255336 ],
256337 ),
257- ) ,
338+ ] ,
258339 ),
259340 ),
260341 ),
261342 );
343+ // Shadows of its own replace the dialog's elevation: they are drawn
344+ // around a box that carries its fill and outline.
345+ if (dialogShadows != null ) {
346+ content = DecoratedBox (
347+ key: const ValueKey ('codelessly-confirm-surface' ),
348+ decoration: BoxDecoration (
349+ color: surface,
350+ borderRadius: BorderRadius .circular (radius),
351+ border: dialogSide == null ? null : Border .fromBorderSide (dialogSide),
352+ boxShadow: dialogShadows,
353+ ),
354+ child: content,
355+ );
356+ }
357+
358+ return Theme (
359+ data: theme.apply (Theme .of (context)),
360+ child: Dialog (
361+ backgroundColor: dialogShadows != null ? Colors .transparent : surface,
362+ surfaceTintColor: Colors .transparent,
363+ shadowColor: dialogShadows != null ? Colors .transparent : null ,
364+ elevation: dialogShadows != null ? 0 : 8 ,
365+ insetPadding: const EdgeInsets .symmetric (horizontal: 24 , vertical: 24 ),
366+ shape: RoundedRectangleBorder (
367+ borderRadius: BorderRadius .circular (radius),
368+ side: dialogShadows == null
369+ ? dialogSide ?? BorderSide .none
370+ : BorderSide .none,
371+ ),
372+ child: content,
373+ ),
374+ );
262375 }
263376}
0 commit comments