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 91f7d6dc..9a0abae1 100644 Binary files a/packages/superdeck/test/goldens/decorated_block_frame.png and b/packages/superdeck/test/goldens/decorated_block_frame.png differ diff --git a/packages/superdeck/test/goldens/inset_slide_container.png b/packages/superdeck/test/goldens/inset_slide_container.png index 25d1fc21..97c66cc2 100644 Binary files a/packages/superdeck/test/goldens/inset_slide_container.png and b/packages/superdeck/test/goldens/inset_slide_container.png differ diff --git a/packages/superdeck/test/goldens/plain_markdown_slide.png b/packages/superdeck/test/goldens/plain_markdown_slide.png index 6abd62b6..96c92f11 100644 Binary files a/packages/superdeck/test/goldens/plain_markdown_slide.png and b/packages/superdeck/test/goldens/plain_markdown_slide.png differ diff --git a/packages/superdeck/test/goldens/section_two_blocks.png b/packages/superdeck/test/goldens/section_two_blocks.png index e7a27830..bdc4219e 100644 Binary files a/packages/superdeck/test/goldens/section_two_blocks.png and b/packages/superdeck/test/goldens/section_two_blocks.png differ 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); + }); + }); +}