From 7a5c354b944d23e87b58aeb94e972d3474bcccd4 Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Wed, 15 Jul 2026 16:35:24 -0400 Subject: [PATCH] feat: add SuperDeck layout diagnostics --- .../helpers/fake_slide_capture_service.dart | 1 + .../src/capture/slide_capture_service.dart | 7 +- .../src/rendering/blocks/block_widget.dart | 40 ++-- .../src/rendering/layout_debug_overlay.dart | 193 ++++++++++++++++++ .../lib/src/rendering/slides/slide_view.dart | 47 +++-- .../src/styling/components/block_styler.dart | 4 +- .../lib/src/styling/components/slide.dart | 9 + .../lib/src/styling/components/slide.g.dart | 22 ++ .../lib/src/styling/default_style.dart | 2 + .../test/goldens/decorated_block_frame.png | Bin 8764 -> 8764 bytes .../test/goldens/inset_slide_container.png | Bin 6241 -> 6241 bytes .../test/goldens/plain_markdown_slide.png | Bin 7283 -> 7283 bytes .../test/goldens/section_two_blocks.png | Bin 6762 -> 6762 bytes .../helpers/fake_slide_capture_service.dart | 1 + .../thumbnail_capture_timing_test.dart | 24 +++ .../src/styling/components/slide_test.dart | 52 +++++ 16 files changed, 364 insertions(+), 38 deletions(-) create mode 100644 packages/superdeck/lib/src/rendering/layout_debug_overlay.dart create mode 100644 packages/superdeck/test/src/styling/components/slide_test.dart diff --git a/packages/plugins/pdf/test/helpers/fake_slide_capture_service.dart b/packages/plugins/pdf/test/helpers/fake_slide_capture_service.dart index 20197f9b..857b75e4 100644 --- a/packages/plugins/pdf/test/helpers/fake_slide_capture_service.dart +++ b/packages/plugins/pdf/test/helpers/fake_slide_capture_service.dart @@ -18,6 +18,7 @@ class FakeSlideCaptureService extends SlideCaptureService { SlideCaptureQuality quality = SlideCaptureQuality.thumbnail, required SlideConfiguration slide, required BuildContext context, + bool includeDebugLayout = false, }) async { return bytes; } diff --git a/packages/superdeck/lib/src/capture/slide_capture_service.dart b/packages/superdeck/lib/src/capture/slide_capture_service.dart index a9852309..5656c76c 100644 --- a/packages/superdeck/lib/src/capture/slide_capture_service.dart +++ b/packages/superdeck/lib/src/capture/slide_capture_service.dart @@ -45,8 +45,11 @@ class SlideCaptureService { SlideCaptureQuality quality = SlideCaptureQuality.thumbnail, required SlideConfiguration slide, required BuildContext context, + bool includeDebugLayout = false, }) async { - final queueKey = shortHash(slide.key + quality.name); + final queueKey = shortHash( + '${slide.key}${quality.name}$includeDebugLayout', + ); try { while (_generationQueue.length >= _maxConcurrentGenerations) { await Future.delayed(_kQueuePollInterval); @@ -55,7 +58,7 @@ class SlideCaptureService { _generationQueue.add(queueKey); final staticRenderingSlide = slide.copyWith( - debug: false, + debug: includeDebugLayout, isStaticRendering: true, ); diff --git a/packages/superdeck/lib/src/rendering/blocks/block_widget.dart b/packages/superdeck/lib/src/rendering/blocks/block_widget.dart index 4a982ff5..bc8bdff9 100644 --- a/packages/superdeck/lib/src/rendering/blocks/block_widget.dart +++ b/packages/superdeck/lib/src/rendering/blocks/block_widget.dart @@ -1,6 +1,5 @@ import 'dart:math' as math; -import 'package:flutter/material.dart' show Colors; import 'package:flutter/widgets.dart'; import 'package:mix/mix.dart'; import 'package:superdeck_core/superdeck_core.dart'; @@ -12,6 +11,7 @@ import '../../ui/widgets/error_widgets.dart'; import '../../ui/widgets/overflow_clip.dart'; import '../../ui/widgets/provider.dart'; import '../../utils/converters.dart'; +import '../layout_debug_overlay.dart'; import 'block_provider.dart'; import 'markdown_viewer.dart'; @@ -141,13 +141,11 @@ class _BlockContainerState extends State<_BlockContainer> { ), ); - // Add debug border if needed - if (widget.configuration.debug) { - content = DecoratedBox( - position: DecorationPosition.foreground, - decoration: BoxDecoration( - border: Border.all(color: Colors.cyan, width: 2), - ), + final debugLayoutEnabled = widget.configuration.debug; + if (debugLayoutEnabled) { + content = BlockLayoutDebugOverlay( + margin: spec.blockContainer.spec.margin, + padding: spec.blockContainer.spec.padding, child: content, ); } @@ -336,20 +334,15 @@ class SectionWidget extends StatelessWidget { final SectionBlock section; final int sectionIndex; - Positioned _renderDebugInfo(Block block, Size size) { - const textStyle = TextStyle(color: Colors.black, fontSize: 12); - final label = - ''' -@${block.type} -${size.width.toStringAsFixed(2)} x ${size.height.toStringAsFixed(2)}'''; - + Positioned _renderDebugInfo(Block block, int blockIndex, Size size) { return Positioned( top: 0, right: 0, - child: Container( - color: Colors.cyan, - padding: const EdgeInsets.all(8), - child: Text(label, style: textStyle), + child: LayoutDebugLabel( + color: debugBlockColor, + text: + 'BLOCK ${blockIndex + 1} @${block.type}\n' + '${size.width.toStringAsFixed(0)} × ${size.height.toStringAsFixed(0)}', ), ); } @@ -401,13 +394,18 @@ ${size.width.toStringAsFixed(2)} x ${size.height.toStringAsFixed(2)}'''; ), }; - if (!configuration.debug) return blockWidget; + final debugLayoutEnabled = configuration.debug; + if (!debugLayoutEnabled) return blockWidget; return LayoutBuilder( builder: (context, constraints) => Stack( fit: StackFit.expand, children: [ blockWidget, - _renderDebugInfo(block, constraints.biggest), + _renderDebugInfo( + block, + blockIndex, + constraints.biggest, + ), ], ), ); diff --git a/packages/superdeck/lib/src/rendering/layout_debug_overlay.dart b/packages/superdeck/lib/src/rendering/layout_debug_overlay.dart new file mode 100644 index 00000000..e4469865 --- /dev/null +++ b/packages/superdeck/lib/src/rendering/layout_debug_overlay.dart @@ -0,0 +1,193 @@ +import 'dart:math' as math; + +import 'package:flutter/widgets.dart'; + +const debugSectionColor = Color(0xFFFF4DCD); +const debugBlockColor = Color(0xFF00D5FF); +const debugMarginColor = Color(0xFFFFA62B); +const debugPaddingColor = Color(0xFF35E06F); + +/// Draws a debug-only frame without participating in layout. +class LayoutDebugFrame extends StatelessWidget { + final Color color; + final Widget child; + final double strokeWidth; + + const LayoutDebugFrame({ + super.key, + required this.color, + required this.child, + this.strokeWidth = 3, + }); + + @override + Widget build(BuildContext context) { + return CustomPaint( + foregroundPainter: _FramePainter(color, strokeWidth), + child: child, + ); + } +} + +/// Draws the allocated block, resolved margin, and resolved padding edges. +class BlockLayoutDebugOverlay extends StatelessWidget { + final EdgeInsetsGeometry? margin; + final EdgeInsetsGeometry? padding; + final Widget child; + + const BlockLayoutDebugOverlay({ + super.key, + required this.margin, + required this.padding, + required this.child, + }); + + @override + Widget build(BuildContext context) { + final textDirection = Directionality.maybeOf(context) ?? TextDirection.ltr; + return CustomPaint( + foregroundPainter: _BlockInsetsPainter( + margin: margin?.resolve(textDirection) ?? EdgeInsets.zero, + padding: padding?.resolve(textDirection) ?? EdgeInsets.zero, + ), + child: child, + ); + } +} + +class LayoutDebugLabel extends StatelessWidget { + final Color color; + final String text; + + const LayoutDebugLabel({super.key, required this.color, required this.text}); + + @override + Widget build(BuildContext context) { + return ColoredBox( + color: color, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3), + child: Text( + text, + style: const TextStyle( + color: Color(0xFF071018), + fontSize: 11, + fontWeight: FontWeight.w700, + height: 1.2, + ), + ), + ), + ); + } +} + +class LayoutDebugLegend extends StatelessWidget { + const LayoutDebugLegend({super.key}); + + @override + Widget build(BuildContext context) { + return ColoredBox( + color: const Color(0xE6071018), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + child: Row( + mainAxisSize: MainAxisSize.min, + children: const [ + _LegendItem(color: debugSectionColor, label: 'SECTION'), + SizedBox(width: 10), + _LegendItem(color: debugBlockColor, label: 'BLOCK'), + SizedBox(width: 10), + _LegendItem(color: debugMarginColor, label: 'MARGIN'), + SizedBox(width: 10), + _LegendItem(color: debugPaddingColor, label: 'PADDING'), + ], + ), + ), + ); + } +} + +class _LegendItem extends StatelessWidget { + final Color color; + final String label; + + const _LegendItem({required this.color, required this.label}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox.square(dimension: 10, child: ColoredBox(color: color)), + const SizedBox(width: 4), + Text( + label, + style: const TextStyle( + color: Color(0xFFFFFFFF), + fontSize: 10, + fontWeight: FontWeight.w700, + ), + ), + ], + ); + } +} + +class _BlockInsetsPainter extends CustomPainter { + final EdgeInsets margin; + final EdgeInsets padding; + + const _BlockInsetsPainter({required this.margin, required this.padding}); + + @override + void paint(Canvas canvas, Size size) { + final blockRect = Offset.zero & size; + final marginRect = _inset(blockRect, margin); + final paddingRect = _inset(marginRect, padding); + + _drawFrame(canvas, blockRect, debugBlockColor, 4); + _drawFrame(canvas, marginRect, debugMarginColor, 3); + _drawFrame(canvas, paddingRect, debugPaddingColor, 2); + } + + @override + bool shouldRepaint(_BlockInsetsPainter oldDelegate) { + return oldDelegate.margin != margin || oldDelegate.padding != padding; + } +} + +class _FramePainter extends CustomPainter { + final Color color; + final double strokeWidth; + + const _FramePainter(this.color, this.strokeWidth); + + @override + void paint(Canvas canvas, Size size) { + _drawFrame(canvas, Offset.zero & size, color, strokeWidth); + } + + @override + bool shouldRepaint(_FramePainter oldDelegate) { + return oldDelegate.color != color || oldDelegate.strokeWidth != strokeWidth; + } +} + +Rect _inset(Rect rect, EdgeInsets insets) { + final left = math.min(rect.right, rect.left + insets.left); + final top = math.min(rect.bottom, rect.top + insets.top); + final right = math.max(left, rect.right - insets.right); + final bottom = math.max(top, rect.bottom - insets.bottom); + return Rect.fromLTRB(left, top, right, bottom); +} + +void _drawFrame(Canvas canvas, Rect rect, Color color, double strokeWidth) { + if (rect.width <= strokeWidth || rect.height <= strokeWidth) return; + canvas.drawRect( + rect.deflate(strokeWidth / 2), + Paint() + ..color = color + ..style = PaintingStyle.stroke + ..strokeWidth = strokeWidth, + ); +} diff --git a/packages/superdeck/lib/src/rendering/slides/slide_view.dart b/packages/superdeck/lib/src/rendering/slides/slide_view.dart index 6df779c4..2325d614 100644 --- a/packages/superdeck/lib/src/rendering/slides/slide_view.dart +++ b/packages/superdeck/lib/src/rendering/slides/slide_view.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart' show Colors; import 'package:flutter/widgets.dart'; import 'package:mix/mix.dart'; import 'package:superdeck_core/superdeck_core.dart'; @@ -7,6 +6,7 @@ import '../../deck/slide_configuration.dart'; import '../../styling/components/slide.dart'; import '../../utils/constants.dart'; import '../blocks/block_widget.dart'; +import '../layout_debug_overlay.dart'; class SlideView extends StatelessWidget { final SlideConfiguration slide; @@ -18,18 +18,20 @@ class SlideView extends StatelessWidget { : const SizedBox.shrink(); } - Positioned _renderDebugInfo(SectionBlock section, Size slideSize) { - final label = ''' -@section | blocks: ${section.blocks.length} | ${slideSize.width.toStringAsFixed(2)} x ${slideSize.height.toStringAsFixed(2)} | align: ${section.align} | flex: ${section.flex}'''; - - const textStyle = TextStyle(color: Colors.black, fontSize: 12); + Positioned _renderDebugInfo( + SectionBlock section, + int sectionIndex, + Size slideSize, + ) { return Positioned( bottom: 0, - left: 0, - child: Container( - color: Colors.cyan, - padding: const EdgeInsets.all(8), - child: Text(label, style: textStyle), + right: 0, + child: LayoutDebugLabel( + color: debugSectionColor, + text: + 'SECTION ${sectionIndex + 1} blocks:${section.blocks.length} ' + '${slideSize.width.toStringAsFixed(0)} × ' + '${slideSize.height.toStringAsFixed(0)}', ), ); } @@ -57,9 +59,22 @@ class SlideView extends StatelessWidget { return Stack( fit: StackFit.expand, children: [ - SectionWidget(section: section, sectionIndex: sectionIndex), if (configuration.debug) - _renderDebugInfo(section, sectionSize), + LayoutDebugFrame( + color: debugSectionColor, + strokeWidth: 4, + child: SectionWidget( + section: section, + sectionIndex: sectionIndex, + ), + ) + else + SectionWidget( + section: section, + sectionIndex: sectionIndex, + ), + if (configuration.debug) + _renderDebugInfo(section, sectionIndex, sectionSize), ], ); }, @@ -133,6 +148,12 @@ class SlideView extends StatelessWidget { }, ), ), + if (slide.debug) + const Positioned( + left: 8, + bottom: 8, + child: IgnorePointer(child: LayoutDebugLegend()), + ), ], ), ); diff --git a/packages/superdeck/lib/src/styling/components/block_styler.dart b/packages/superdeck/lib/src/styling/components/block_styler.dart index c5e69db5..4e586918 100644 --- a/packages/superdeck/lib/src/styling/components/block_styler.dart +++ b/packages/superdeck/lib/src/styling/components/block_styler.dart @@ -18,8 +18,8 @@ import 'package:mix/mix.dart'; /// It intentionally cannot express widget modifiers, width/height or other /// constraints, transforms, box alignment, or widget-state variants — those /// would create competing geometry owners with section `spacing`, block -/// `flex`, and block/section `align`. Extending this allow-list is a design -/// decision, not a convenience patch; see `.planning` notes for PR #99. +/// `flex`, and block/section `align`. Extending this allow-list is a framework +/// design decision because it changes which layer owns block geometry. final class BlockStyler extends Style with Diagnosticable, diff --git a/packages/superdeck/lib/src/styling/components/slide.dart b/packages/superdeck/lib/src/styling/components/slide.dart index e12ccccb..f49acaa6 100644 --- a/packages/superdeck/lib/src/styling/components/slide.dart +++ b/packages/superdeck/lib/src/styling/components/slide.dart @@ -62,6 +62,13 @@ final class SlideSpec with _$SlideSpec { @override final TextScaler? textScaleFactor; + /// Vertical gap between top-level markdown elements. + /// + /// An unset value resolves to zero so spacing is owned by SuperDeck styles, + /// not by flutter_markdown_plus package defaults. + @override + final double? blockSpacing; + @override final StyleSpec alert; @override @@ -106,6 +113,7 @@ final class SlideSpec with _$SlideSpec { this.img, this.link, this.textScaleFactor, + this.blockSpacing, StyleSpec? alert, this.horizontalRuleDecoration, this.blockquote, @@ -141,6 +149,7 @@ final class SlideSpec with _$SlideSpec { del: del, code: code?.spec.textStyle, textScaler: textScaleFactor, + blockSpacing: blockSpacing ?? 0, listBullet: list?.spec.bullet?.spec.style, orderedListAlign: list?.spec.orderedAlignment ?? WrapAlignment.start, unorderedListAlign: list?.spec.unorderedAlignment ?? WrapAlignment.start, diff --git a/packages/superdeck/lib/src/styling/components/slide.g.dart b/packages/superdeck/lib/src/styling/components/slide.g.dart index cc724f41..17453704 100644 --- a/packages/superdeck/lib/src/styling/components/slide.g.dart +++ b/packages/superdeck/lib/src/styling/components/slide.g.dart @@ -21,6 +21,7 @@ mixin _$SlideSpec implements Spec, Diagnosticable { TextStyle? get img; TextStyle? get link; TextScaler? get textScaleFactor; + double? get blockSpacing; StyleSpec get alert; BoxDecoration? get horizontalRuleDecoration; StyleSpec? get blockquote; @@ -51,6 +52,7 @@ mixin _$SlideSpec implements Spec, Diagnosticable { TextStyle? img, TextStyle? link, TextScaler? textScaleFactor, + double? blockSpacing, StyleSpec? alert, BoxDecoration? horizontalRuleDecoration, StyleSpec? blockquote, @@ -77,6 +79,7 @@ mixin _$SlideSpec implements Spec, Diagnosticable { img: img ?? this.img, link: link ?? this.link, textScaleFactor: textScaleFactor ?? this.textScaleFactor, + blockSpacing: blockSpacing ?? this.blockSpacing, alert: alert ?? this.alert, horizontalRuleDecoration: horizontalRuleDecoration ?? this.horizontalRuleDecoration, @@ -112,6 +115,7 @@ mixin _$SlideSpec implements Spec, Diagnosticable { other?.textScaleFactor, t, ), + blockSpacing: MixOps.lerp(blockSpacing, other?.blockSpacing, t), alert: alert.lerp(other?.alert, t), horizontalRuleDecoration: MixOps.lerp( horizontalRuleDecoration, @@ -145,6 +149,7 @@ mixin _$SlideSpec implements Spec, Diagnosticable { img, link, textScaleFactor, + blockSpacing, alert, horizontalRuleDecoration, blockquote, @@ -211,6 +216,7 @@ mixin _$SlideSpec implements Spec, Diagnosticable { ..add(DiagnosticsProperty('img', img)) ..add(DiagnosticsProperty('link', link)) ..add(DiagnosticsProperty('textScaleFactor', textScaleFactor)) + ..add(DoubleProperty('blockSpacing', blockSpacing)) ..add(DiagnosticsProperty('alert', alert)) ..add( DiagnosticsProperty( @@ -253,6 +259,7 @@ class SlideStyler extends MixStyler { final Prop? $img; final Prop? $link; final Prop? $textScaleFactor; + final Prop? $blockSpacing; final Prop>? $alert; final Prop? $horizontalRuleDecoration; final Prop>? $blockquote; @@ -279,6 +286,7 @@ class SlideStyler extends MixStyler { Prop? img, Prop? link, Prop? textScaleFactor, + Prop? blockSpacing, Prop>? alert, Prop? horizontalRuleDecoration, Prop>? blockquote, @@ -306,6 +314,7 @@ class SlideStyler extends MixStyler { $img = img, $link = link, $textScaleFactor = textScaleFactor, + $blockSpacing = blockSpacing, $alert = alert, $horizontalRuleDecoration = horizontalRuleDecoration, $blockquote = blockquote, @@ -332,6 +341,7 @@ class SlideStyler extends MixStyler { TextStyleMix? img, TextStyleMix? link, TextScaler? textScaleFactor, + double? blockSpacing, MarkdownAlertStyler? alert, BoxDecoration? horizontalRuleDecoration, MarkdownBlockquoteStyler? blockquote, @@ -360,6 +370,7 @@ class SlideStyler extends MixStyler { img: Prop.maybeMix(img), link: Prop.maybeMix(link), textScaleFactor: Prop.maybe(textScaleFactor), + blockSpacing: Prop.maybe(blockSpacing), alert: Prop.maybeMix(alert), horizontalRuleDecoration: Prop.maybe(horizontalRuleDecoration), blockquote: Prop.maybeMix(blockquote), @@ -390,6 +401,8 @@ class SlideStyler extends MixStyler { factory SlideStyler.link(TextStyleMix value) => SlideStyler().link(value); factory SlideStyler.textScaleFactor(TextScaler value) => SlideStyler().textScaleFactor(value); + factory SlideStyler.blockSpacing(double value) => + SlideStyler().blockSpacing(value); factory SlideStyler.alert(MarkdownAlertStyler value) => SlideStyler().alert(value); factory SlideStyler.horizontalRuleDecoration(BoxDecoration value) => @@ -480,6 +493,11 @@ class SlideStyler extends MixStyler { return merge(SlideStyler(textScaleFactor: value)); } + /// Sets the blockSpacing. + SlideStyler blockSpacing(double value) { + return merge(SlideStyler(blockSpacing: value)); + } + /// Sets the alert. SlideStyler alert(MarkdownAlertStyler value) { return merge(SlideStyler(alert: value)); @@ -571,6 +589,7 @@ class SlideStyler extends MixStyler { img: MixOps.merge($img, other?.$img), link: MixOps.merge($link, other?.$link), textScaleFactor: MixOps.merge($textScaleFactor, other?.$textScaleFactor), + blockSpacing: MixOps.merge($blockSpacing, other?.$blockSpacing), alert: MixOps.merge($alert, other?.$alert), horizontalRuleDecoration: MixOps.merge( $horizontalRuleDecoration, @@ -608,6 +627,7 @@ class SlideStyler extends MixStyler { img: MixOps.resolve(context, $img), link: MixOps.resolve(context, $link), textScaleFactor: MixOps.resolve(context, $textScaleFactor), + blockSpacing: MixOps.resolve(context, $blockSpacing), alert: MixOps.resolve(context, $alert), horizontalRuleDecoration: MixOps.resolve( context, @@ -648,6 +668,7 @@ class SlideStyler extends MixStyler { ..add(DiagnosticsProperty('img', $img)) ..add(DiagnosticsProperty('link', $link)) ..add(DiagnosticsProperty('textScaleFactor', $textScaleFactor)) + ..add(DiagnosticsProperty('blockSpacing', $blockSpacing)) ..add(DiagnosticsProperty('alert', $alert)) ..add( DiagnosticsProperty( @@ -681,6 +702,7 @@ class SlideStyler extends MixStyler { $img, $link, $textScaleFactor, + $blockSpacing, $alert, $horizontalRuleDecoration, $blockquote, diff --git a/packages/superdeck/lib/src/styling/default_style.dart b/packages/superdeck/lib/src/styling/default_style.dart index f46acb95..c483fe6e 100644 --- a/packages/superdeck/lib/src/styling/default_style.dart +++ b/packages/superdeck/lib/src/styling/default_style.dart @@ -80,6 +80,8 @@ SlideStyler _createDefaultSlideStyle() { } return SlideStyler( + blockSpacing: 12, + h1: TextStyler() .style( TextStyleMix( diff --git a/packages/superdeck/test/goldens/decorated_block_frame.png b/packages/superdeck/test/goldens/decorated_block_frame.png index 91f7d6dcbcfac8ed8ed95a646d74dcaff7532917..9a0abae1d78f38db0ad0c6f6e37e41cf1902cb92 100644 GIT binary patch delta 508 zcmdnvvd3jZ6TgDloo@^_PdEO~&#h)*V2GG(_v>WzE8F+K9d0Kx99Z>~bMr)QNv3*; zs$JLjuYM~SwzhkFd_c5W?iyxx{;L}wtKXJOzj|{|=BBco{Tt@l)Gd;;u2}N6Fl;Wb zbldAq>F-wkY;CQ2@o;Z$|7NY&vX|>_8Q8?|+wR%^OkC_1P-h{(mF@Qhmy7wWj3JbD zd%w-iZyXF8R`K@q_kX`7m!zt+_fOx;eYfB5et7FxIwJ!E!vfptynS2Wf4d!Tbn%Cd z?uKSt;|p$mH&JC|Al!!?@YffmwdJI?ET-3zb>r*KYQEp zhKhHNe|NGouBj8>|KQd(_XENWKtmZO_-+>D5@M>)+3}XK;#BGS`taMzYof&S_b=F% zb1Qd5yqxm(MqQ$9Cfa!OseX{t!?*rZ=A*h3jiYg|B&kk$p4#{iY?@1Njmc+WV4sTJw}oC?9&%B Sw!C!#`OnkU&t;ucLK6V;D*|Ew delta 510 zcmdnvvd3jZ6aVA`%yROFbNLSh%kBPuZJRp-Lxbtd#q;+pz4v(^{}x*|hBZ<1HVbk| zG1Y?=t*idK`<7hd)m;_8S+}NMK4w+$Awbr;ZqwVsu(@Yvt7RYm#{K%_WA#@Xo8528 zrC+_dA#sOZ^|N)e;v-hH%iX*E&0_VX)amb5@g6?>^}^xm?7GB-K%KJR7hEppx3c}d zz!*YpF8*<1a&A8Z!|GcHe*CCd|5h>VlG5q-Gv4j~yH9#uZjBfN5Zv1Q{8{wxy#0Sw zg?rCmQjuq5o1Dnw?jW64%`orNjlbKs*)lOK|G2IGr~IpF=WBPqH4K}3{m)+c75smH z?a!>{>3A-8Wq!55fmh`px!2`Zv)C|lfK0u)*^x(>slM&DT*LC1@4w2o+8$a}`tcig z*6C%vr4Jq~v?}|Pa{KWbtJ&th*ZSUyUz>c~|4K4@{}$VGt2QSdE{OehCf0QSh5&xs zJ==jkTY6gWZm4uy`|E}6->>~S#TIh1{F&=gz1d6Q9;1k0?&*tP Sg!KDB-t%8cWTS$_-JI9ts8-3$Hvo_X+?2EU8By zdvZMg!b@|n0RYU*8?)g95CVU;n0o>C=b%M_0$O3FmKousQzlr@u}% ze>j6ZeSCFm9KB^_viW0-@$n~HQ`{Q56=H|zf&Hu`mzBoRA z`ku+=JA=JBgBy6pA^$$Ouy$#@`0{I$%|CYR==lD3f4(-^{1G?fBr#y4qiKVVf^D?Z%j7-XFokNK6~HYlg)PqdvgYRJM7;F|9AXEt~J`6JIgaA3RqFQ@Lgf7a&%0000iyY)-wXMO(D z51!lg=MO)1?7()Jf;Y2q5No%Z002ovPDHLkV1gXc B<5>Uz delta 883 zcmV-(1C0FPFySz;$On@k0S}W93I#H`@XE7uuK>U<9)0Y|@%#%f&ArCb`PSFJJbwL) zAJ4r80J~Vb`1bh5LyykA1^}M|Z?lmD5CVVLV(talpNGyoKKBYs>yFz_jayG1AD6GZ zJ2o~p=U!*&oIG)S+Ru+qK`o6oy*0pP6?ajC6US}ztzW462 zvT|gsU3_b7ZEerJ&eFPg>GIqwETzBw{dM1AyKo2X%^kEme+TW(-$6I#ldY|}R{$j7 zQz8H!a%W$EW9}6o>(#U8=U!o{xqsM?_4hW$`g<3Ou_wp#FT6DO8UVn|yaBU;1P}s$i@6tIe;zvX_}nWjtvhZzHEun5d|bZr?%3Ga zoO_+6bMnOTap&!)#`_x|j(4uC&%MslSy?Q`>HF>)Ti33QwKw0Md!40p`rf<8%F2I!nhLv^#gu?%Y9pa|hj+i;{>BHJ z;}3uO>tyqXGuYF|SGUH|TUI8UKgJjzf3h`>erCF79QHHtIoMm_bMU)A{$*@#emvRy zuYBo?3no*xQRQ z|8rbe`_E+a0hopWc*y$H^1N5AFQz?d{3KhcnpS&d$!b`Q~X~ zHs0RZ8Am33#$i7LpM&}5AH?V2wR0E7KmPT`Wb=Ra(=+3<_uW0&d}pvXXRx=!{(bQ8 zH{TlH{N9fyoBxH+-8-K69LWzRn?C|);B&CI-~Rs3E};6{y?7L4Ca4`GnoGw&R}mlJ3EukcLsZN27Aq)bo_xlTX(HT|J1ep~*aRW^>n{Kl0oI2e!-qa_XM@XMH{Z006+U zTfcOE*5^O{;JICY{_s=B4s4excmV^mK@o8TlfV*Jv$7SN0u;ci;=eFsUG@L~002ov JPDHLkV1fh-<*xt$ diff --git a/packages/superdeck/test/goldens/plain_markdown_slide.png b/packages/superdeck/test/goldens/plain_markdown_slide.png index 6abd62b6bdf54a8d7ebfac84da79485c4f2a8da3..96c92f11857383eff13148f8b4ad597d5c7ab134 100644 GIT binary patch delta 867 zcmY+BZ%h++7{{*^DSM$eh=8oIjd95qNroA$Wj!p6sL8@ac>!)qpv{(eo2611*s(%O zhRKqlXmZ<_J1>XBM6@f%at~+NeW?$9I_kL8az{d^33N&eygD>-0^oZdPc=C$*JsMM!~vX90V9dR8|#hm<7e z$IA$DnV(5c^4cd9jo8(IGm>RQn$lZ}k9QOTNkw%FEf87`xwqy@++V6M-v@^xVV|oF2LLJ#w+kt@rf-81H{)^|A=b@ z(xmWDM3zyF)MoFghsh7jX7k@PmzS6K%e{Ldm6~gds%MVh{%5WhnS_p9E%`rg@66+?4oI$$YTZ%7^TS-?cuQZzcS2LvG$6pNls9P}Vr>3xUKN8}6b zdM!2apT$q;;TcX8lshs`%(NH^#qb4hq%0#%M%HG(oQ8PP_g6d|*w}Th**N=BSpx3?(6#; zIk1GSwY7$(Y0OKgLRQIx6`9IDUm(rs96!?b^u|Vx|4HSmQu5tnRYwOaj9>l>&H2Tl delta 906 zcmZ9LVN4Te9LA5@wIy+s7?lBA>%Pcz(Jm==hEpobG%*=AEFqmSFb&3z#fCL>5G-v~ z3}!=^$t^BeZZI*xl+Y$Ox~9;cM4=(BEv#C0ZC#IbAksqV&U)P0_PjI8KJ43*_xQRe2c%oPJ}oMVdXQ1M0c zP}*>dq7(owhEt*sps6=X8(N-j!P_f8L_O5_WhTPVv@^gahr7|vv1Dp*4wyEgLeglW z>K?t!)b^dpd0Ar^{Z{^4lDRe()ztafe=U>dPUVK6O;8=sGvR#J6u8Nn`2*$zRA zyqd;AcadXuYN~22kuuLo#s{p}MSQ~w7E) z@L2>ZQ_=No5`d$Of5f2?bX0LA2IL|8n!b`fwpFm*OAf5J1AYFdu#Z_#w#=p5xHhP4 z|KfMo*pYjyY>3a?yIBbUE(Z|>_?)^?2IcW*xRpZioque3y=0D(;k@k~iJ#xyCE#!8 PhK?US@$uM4r!T((4j1T2 diff --git a/packages/superdeck/test/goldens/section_two_blocks.png b/packages/superdeck/test/goldens/section_two_blocks.png index e7a278308f36d679e2fdfc68571f49142cceff95..bdc4219e6f993c70c67072880b8eb45e5fc5bc51 100644 GIT binary patch delta 935 zcmZ{gZ%i6@7{?Fjs!OeubeTG8>6)%==@M&Zkb*$6>`#`gwF}v7qE=HAO&ZuqL8bih zT$9aY6G?a3x{V&bSmM&O1qqAb6me^nnNElq+b|%1#>!P1r0h=Y!Ex-!x)*)1SI?6? zzuzaH?>F6&Zkah+4U6~H9EH?9hTO7HB0+Rq0ODH9z_!corF10zYr7m|o%;xMCaNbH z@X2SdK%r=m>t4gqz<1G~JVP2yOD&u2+uGU^ip3V4c-NmuB!nW-3j+fKNgUUtGZAsI zSWG^CtnKRVHo-7_!=<*DsA5(QGUDJe7V}-E=D$Y07J6*va=B0}#^dw($#nW6{kPLg z6&?mVfe*fa&9e5T9ApC3004w)WQ@HqXU|(N<)@=dN`HWMVFI@@#jKYX8!cvn;j!C) z66`T$iZB;{Zgo0X2bhooOz)4R2n>~jiKvJ+93LOA7+yz^(;h^RK6*5GsIk%HbUIZ! zooHux8!44aDubcTX0xrVuC4~dbJr%#W*rfATOs>}QZe!~M>URQ7r$ z%F?mzQY*lqx3Qh8j^l{$R{>S5z*gPe=aRoFC}}LHK=k-MhIvY}o`vCV4b}b0lU07m5D= zb%7 delta 938 zcmZ{je@q$&6vq##t(~dZbdxszxNObbvdlEy+<~F%CCeJuq(GV_TeEh9%uJI7MBAzq zm6QInWvI91{%9HymY7KzQ;}XGT^f3>&R&`p7-0tS*j<&*tDqHT5KiIP(Pn@2kG+52 zd*Ao{ev+4HPBc%yQGsd260E7R@@PtyotJ8c!{P7^uPEhR<|L2JyE{oYsfG*98EELW z++NJGpTU2uI4(jqls*e}0^tV|O|kQ>t*!A?s>|+hjN&+c!+UA7uAyOce0;pSw--{W z)h;^V!wCW&MNx8LVL{%~a^0lJE^QLUlbITT|LjC@(9ROJEBu}7EJ4+SAX+FCx-VXQ zZ)$34LfFkD-Gs_lu05r|qq>$%BLI{Gz>q(M{Z{r#qt1{x5L?aX1t-Al(%mAzo8Is; zD&K*{cN|4vvE5O;jSh(MvwNz`knIV2R_}lj;q0@UC4WYvT<}LmqtQR9U;C;g5;7DD z$rXxPm&^63fu*Ai1Ck^ePb9vz+wEt3WBqHnoN;|~6U8vByQk-aOz*S$?9WKhKop=% zjW%iIcja07zbK_d{eKYT7MKkMbkHm3RlfazP}=t~=E!BNTHM}9*-T>ZmGsKI@t-FT z?yowXPP7$m&~N0eR%<%7JU8MetZh0>d+is%fIm4?FfrR)U-z({V0U84pfG>Cs*|mo)pT?~ zHa#DBp5@F6jfksDsm?jEpDNVIj4ZRab8BVZntt*C8}Y+145AGmE-i6VsWiSk_g$?G zk&-!%b&|)3{4EcsF>`$TT=Tmbz66QS~jv37Um0PtL z{#V{>U`AbPXQz(GJA(I<5%c4511a1HT diff --git a/packages/superdeck/test/helpers/fake_slide_capture_service.dart b/packages/superdeck/test/helpers/fake_slide_capture_service.dart index 7323d87e..e584d1cc 100644 --- a/packages/superdeck/test/helpers/fake_slide_capture_service.dart +++ b/packages/superdeck/test/helpers/fake_slide_capture_service.dart @@ -28,6 +28,7 @@ class FakeSlideCaptureService extends SlideCaptureService { SlideCaptureQuality quality = SlideCaptureQuality.thumbnail, required SlideConfiguration slide, required BuildContext context, + bool includeDebugLayout = false, }) async { captureCalls++; capturedKeys.add(slide.key); diff --git a/packages/superdeck/test/src/capture/thumbnail_capture_timing_test.dart b/packages/superdeck/test/src/capture/thumbnail_capture_timing_test.dart index fcbf2de0..24f0427d 100644 --- a/packages/superdeck/test/src/capture/thumbnail_capture_timing_test.dart +++ b/packages/superdeck/test/src/capture/thumbnail_capture_timing_test.dart @@ -234,6 +234,30 @@ void main() { }); }); + testWidgets('only includes debug layout guides when requested', ( + tester, + ) async { + final context = await _pumpContext(tester); + final slide = _slide('debug-layout', '# Layout guides'); + final capture = SlideCaptureService(); + + await tester.runAsync(() async { + final cleanBytes = await capture.capture( + quality: SlideCaptureQuality.good, + slide: slide, + context: context, + ); + final debugBytes = await capture.capture( + quality: SlideCaptureQuality.good, + slide: slide, + context: context, + includeDebugLayout: true, + ); + + expect(cleanBytes, isNot(equals(debugBytes))); + }); + }); + testWidgets('waits longer for asynchronous image widget content', ( tester, ) async { diff --git a/packages/superdeck/test/src/styling/components/slide_test.dart b/packages/superdeck/test/src/styling/components/slide_test.dart new file mode 100644 index 00000000..4f7f50a2 --- /dev/null +++ b/packages/superdeck/test/src/styling/components/slide_test.dart @@ -0,0 +1,52 @@ +import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:superdeck/superdeck.dart'; +import 'package:superdeck_core/superdeck_core.dart'; + +import '../../../helpers/slide_test_harness.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('SlideSpec markdown spacing', () { + test('disables flutter_markdown_plus implicit block spacing', () { + expect(const SlideSpec().toStyle().blockSpacing, 0); + }); + + testWidgets('default slide style owns the block spacing', (tester) async { + await SlideTestHarness.pumpSlide( + tester, + Slide( + key: 'markdown-spacing', + sections: [ + SectionBlock([ContentBlock('## Heading\n\nParagraph')]), + ], + ), + ); + + final markdown = tester.widget(find.byType(MarkdownBody)); + final heading = tester.getRect(find.text('Heading')); + final paragraph = tester.getRect(find.text('Paragraph')); + + expect(markdown.styleSheet!.blockSpacing, 12); + expect(paragraph.top - heading.bottom, closeTo(24, 0.1)); + }); + + testWidgets('deck styles can override the default spacing', (tester) async { + await SlideTestHarness.pumpSlide( + tester, + Slide( + key: 'custom-markdown-spacing', + sections: [ + SectionBlock([ContentBlock('## Heading\n\nParagraph')]), + ], + ), + style: defaultSlideStyle.merge(SlideStyler(blockSpacing: 20)), + ); + + final markdown = tester.widget(find.byType(MarkdownBody)); + + expect(markdown.styleSheet!.blockSpacing, 20); + }); + }); +}