diff --git a/CHANGELOG.md b/CHANGELOG.md index 50c1990..985a604 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,11 @@ and the project aims to follow [Semantic Versioning](https://semver.org/). ### Added - **Source-code slides** — a "code sheet" with per-language syntax highlighting, - stored as a fenced code block. Background and text colours are part of the style - profile, with a syntax-colouring toggle; turning it off renders the block in a - single colour (e.g. green on black for a CRT-terminal look). + stored as a fenced code block. Background, text colour and monospace font are + part of the style profile, with a syntax-colouring toggle; turning it off renders + the block in a single colour (e.g. green on black for a CRT-terminal look). The + code is sized to fill the panel — larger when there's room, smaller for long + fragments. - **Charts** — bar, line, pie, and **spider/radar** chart slides. Data is entered in an in-app grid or imported from CSV; the spec is stored as JSON in a ```chart block. Data can stay inline or be linked to a CSV in a separate `data/` diff --git a/README.md b/README.md index 4c694b9..30107e5 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Built with Flutter for macOS, Windows, and Linux. ## Features - **Structured slide editors** — dedicated editors per slide type: title, bullets, two-column bullets, bullets + image, single/two images, quote, table, section divider, image-only, video, audio, source code, charts, and free-form Markdown. -- **Source-code slides** — a "code sheet" with per-language syntax highlighting, stored as a fenced code block. Background and text colours come from the style profile, with an optional syntax-colouring toggle (turn it off for a single-colour, CRT-terminal look). +- **Source-code slides** — a "code sheet" with per-language syntax highlighting, stored as a fenced code block. Background, text colour and monospace font come from the style profile, with an optional syntax-colouring toggle (turn it off for a single-colour, CRT-terminal look). The code auto-sizes to fill the panel. - **Charts** — bar, line, pie, and spider/radar charts rendered natively (preview, presenter, PDF, PPTX) and as self-contained SVG in the HTML export. Data is entered in an in-app grid or imported from CSV; the spec is stored as JSON in the Markdown, with optional linking to a CSV kept in a tidy `data/` directory. Optional min/max draw reference lines (bar/line) or fix the scale (radar); legend hover highlights a series, and line tooltips pick the dot under the cursor. - **Live preview** — see each slide rendered as you edit, with inline Markdown, footers, and TLP (Traffic Light Protocol) marking. Free-Markdown slides render fenced code with syntax highlighting and `$…$` / `$$…$$` LaTeX math. - **Traffic Light Protocol** — a deck-wide classification plus an optional **per-slide TLP level**; slides classified stricter than the level the deck is shown at are automatically withheld, both when presenting and exporting. diff --git a/docs/FILE_FORMAT.md b/docs/FILE_FORMAT.md index 92de35d..9731e78 100644 --- a/docs/FILE_FORMAT.md +++ b/docs/FILE_FORMAT.md @@ -138,6 +138,7 @@ JSON heeft deze velden (met standaardwaarden): | `codeBackgroundColor` | `#282C34` | Achtergrond van broncode-slides. | | `codeTextColor` | `#ABB2BF` | Tekstkleur van broncode-slides. | | `codeHighlightSyntax` | `true` | Syntaxkleuring aan/uit. Uit = alles in één kleur (bijv. groen op zwart voor een CRT-look). | +| `codeFontFamily` | `monospace` | Lettertype van broncode-slides (bijv. `Courier New`). | | `logoPath` | `null` | Pad naar logo (relatief in `logos/`). | | `logoPosition` | `bottom-right` | `top-left`/`top-right`/`bottom-left`/`bottom-right`. | | `logoSize` | `96` | Logogrootte in px. | diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index f49dc48..43939f3 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -31,10 +31,12 @@ highlighting and `$…$` / `$$…$$` LaTeX math. ### Source-code slides Choose a programming language for syntax highlighting (or "plain text") and paste -your code. It renders as a "code sheet" whose background and text colour come from -the active **style profile**. Turn **syntax colouring** off to show the whole block -in a single colour — e.g. bright green on black for a classic CRT-terminal look. -Stored as a fenced code block in the Markdown. +your code. It renders as a "code sheet" whose background, text colour and +**monospace font** come from the active **style profile** (e.g. Courier). Turn +**syntax colouring** off to show the whole block in a single colour — e.g. bright +green on black for a classic CRT-terminal look. The text is sized to fill the +panel — larger when there's room, smaller for long fragments. Stored as a fenced +code block in the Markdown. ### Charts @@ -112,10 +114,11 @@ Export to: ## Theming and language -- **Style profiles** control deck colours (including the source-code background and - text, with an optional syntax-colouring toggle), fonts, logo, and footer. Every - colour can be picked from the presets or entered as a custom hex value. The - bundled Marp theme is `assets/themes/ocideck.css`. +- **Style profiles** control deck colours (including the source-code background, + text, font and an optional syntax-colouring toggle), fonts, logo, and footer. + Every colour can be picked from the presets or entered as a custom hex value. The + Colours and Logo tabs show which profile you're editing. The bundled Marp theme + is `assets/themes/ocideck.css`. - **App appearance** (including a dark interface) is configurable in settings. - The interface is available in Dutch, English, Italian, German, French, Spanish, Frisian, and Papiamento. diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 4c14c4c..7adae2d 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2382,6 +2382,9 @@ const _dutchSourceStringAdditions = { 'Eigen kleur (hex)': 'Custom colour (hex)', 'Bijvoorbeeld #33FF33 voor een CRT-groen scherm.': 'For example #33FF33 for a CRT-green screen.', + 'Onderdeel van stijlprofiel ': 'Part of style profile ', + 'Broncode lettertype': 'Code font', + 'Systeem (monospace)': 'System (monospace)', 'Platte tekst': 'Plain text', 'Titel (optioneel)': 'Title (optional)', 'HTML opent in elke browser zonder internet en rendert codeblokken, wiskunde en mermaid-diagrammen.': @@ -2418,6 +2421,9 @@ const _dutchSourceStringAdditions = { 'Eigen kleur (hex)': 'Colore personalizzato (hex)', 'Bijvoorbeeld #33FF33 voor een CRT-groen scherm.': 'Ad esempio #33FF33 per uno schermo verde CRT.', + 'Onderdeel van stijlprofiel ': 'Parte del profilo di stile ', + 'Broncode lettertype': 'Font del codice', + 'Systeem (monospace)': 'Sistema (monospace)', 'Kleur van reeks': 'Colore della serie', 'Kleur van rij': 'Colore della riga', 'Hexkleur': 'Colore esadecimale', @@ -2644,6 +2650,9 @@ const _dutchSourceStringAdditions = { 'Eigen kleur (hex)': 'Eigene Farbe (Hex)', 'Bijvoorbeeld #33FF33 voor een CRT-groen scherm.': 'Zum Beispiel #33FF33 für einen CRT-grünen Bildschirm.', + 'Onderdeel van stijlprofiel ': 'Teil des Stilprofils ', + 'Broncode lettertype': 'Code-Schriftart', + 'Systeem (monospace)': 'System (monospace)', 'Kleur van reeks': 'Reihenfarbe', 'Kleur van rij': 'Zeilenfarbe', 'Hexkleur': 'Hex-Farbe', @@ -2871,6 +2880,9 @@ const _dutchSourceStringAdditions = { 'Eigen kleur (hex)': 'Couleur personnalisée (hex)', 'Bijvoorbeeld #33FF33 voor een CRT-groen scherm.': 'Par exemple #33FF33 pour un écran vert CRT.', + 'Onderdeel van stijlprofiel ': 'Fait partie du profil de style ', + 'Broncode lettertype': 'Police du code', + 'Systeem (monospace)': 'Système (monospace)', 'Kleur van reeks': 'Couleur de la série', 'Kleur van rij': 'Couleur de la ligne', 'Hexkleur': 'Couleur hexadécimale', @@ -3098,6 +3110,9 @@ const _dutchSourceStringAdditions = { 'Eigen kleur (hex)': 'Color personalizado (hex)', 'Bijvoorbeeld #33FF33 voor een CRT-groen scherm.': 'Por ejemplo #33FF33 para una pantalla verde CRT.', + 'Onderdeel van stijlprofiel ': 'Parte del perfil de estilo ', + 'Broncode lettertype': 'Fuente del código', + 'Systeem (monospace)': 'Sistema (monospace)', 'Kleur van reeks': 'Color de la serie', 'Kleur van rij': 'Color de la fila', 'Hexkleur': 'Color hexadecimal', @@ -3325,6 +3340,9 @@ const _dutchSourceStringAdditions = { 'Eigen kleur (hex)': 'Eigen kleur (hex)', 'Bijvoorbeeld #33FF33 voor een CRT-groen scherm.': 'Bygelyks #33FF33 foar in CRT-grien skerm.', + 'Onderdeel van stijlprofiel ': 'Underdiel fan stylprofyl ', + 'Broncode lettertype': 'Boarnekoade lettertype', + 'Systeem (monospace)': 'Systeem (monospace)', 'Kleur van reeks': 'Rigekleur', 'Kleur van rij': 'Rijekleur', 'Hexkleur': 'Hekskleur', @@ -3549,6 +3567,9 @@ const _dutchSourceStringAdditions = { 'Eigen kleur (hex)': 'Koló propio (hex)', 'Bijvoorbeeld #33FF33 voor een CRT-groen scherm.': 'Por ehèmpel #33FF33 pa un pantaya berde CRT.', + 'Onderdeel van stijlprofiel ': 'Parti di e perfil di estilo ', + 'Broncode lettertype': 'Tipo di lèter di kódigo', + 'Systeem (monospace)': 'Sistema (monospace)', 'Kleur van reeks': 'Koló di serie', 'Kleur van rij': 'Koló di liña', 'Hexkleur': 'Koló hexadecimal', diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 96afb90..a059f70 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -19,6 +19,10 @@ class ThemeProfile { /// syntax colours) — required for a believable single-colour CRT screen. final bool codeHighlightSyntax; + /// Monospace font family for code slides. `monospace` uses the system default; + /// e.g. `Courier New` for a typewriter look. + final String codeFontFamily; + final String? logoPath; final String logoPosition; final int logoSize; @@ -54,6 +58,7 @@ class ThemeProfile { this.codeBackgroundColor = '#282C34', this.codeTextColor = '#ABB2BF', this.codeHighlightSyntax = true, + this.codeFontFamily = 'monospace', this.logoPath, this.logoPosition = 'bottom-right', this.logoSize = 96, @@ -87,6 +92,7 @@ class ThemeProfile { String? codeBackgroundColor, String? codeTextColor, bool? codeHighlightSyntax, + String? codeFontFamily, String? logoPath, String? logoPosition, int? logoSize, @@ -112,6 +118,7 @@ class ThemeProfile { codeBackgroundColor: codeBackgroundColor ?? this.codeBackgroundColor, codeTextColor: codeTextColor ?? this.codeTextColor, codeHighlightSyntax: codeHighlightSyntax ?? this.codeHighlightSyntax, + codeFontFamily: codeFontFamily ?? this.codeFontFamily, logoPath: clearLogo ? null : (logoPath ?? this.logoPath), logoPosition: logoPosition ?? this.logoPosition, logoSize: logoSize ?? this.logoSize, @@ -139,6 +146,7 @@ class ThemeProfile { 'codeBackgroundColor': codeBackgroundColor, 'codeTextColor': codeTextColor, 'codeHighlightSyntax': codeHighlightSyntax, + 'codeFontFamily': codeFontFamily, 'logoPath': logoPath, 'logoPosition': logoPosition, 'logoSize': logoSize, @@ -173,6 +181,7 @@ class ThemeProfile { json['codeBackgroundColor'] as String? ?? '#282C34', codeTextColor: json['codeTextColor'] as String? ?? '#ABB2BF', codeHighlightSyntax: json['codeHighlightSyntax'] as bool? ?? true, + codeFontFamily: json['codeFontFamily'] as String? ?? 'monospace', logoPath: json['logoPath'] as String?, logoPosition: json['logoPosition'] as String? ?? 'bottom-right', logoSize: (json['logoSize'] as num?)?.round() ?? 96, @@ -370,6 +379,17 @@ class AppSettings { 'Courier New', ]; + /// Monospace families offered for code slides. `monospace` is the system + /// default; the rest are common typewriter/coding faces. + static const codeFonts = [ + 'monospace', + 'Courier New', + 'Menlo', + 'Consolas', + 'Roboto Mono', + 'Cascadia Code', + ]; + AppSettings copyWith({ String? languageCode, String? homeDirectory, diff --git a/lib/services/marp_html_service.dart b/lib/services/marp_html_service.dart index 712132a..aa509b1 100644 --- a/lib/services/marp_html_service.dart +++ b/lib/services/marp_html_service.dart @@ -589,6 +589,11 @@ class MarpHtmlService { Future _themedCss(ThemeProfile t) async { final fontFace = await _ebGaramondFontFace(t.fontFamily); final family = _cssFontStack(t.fontFamily); + final codePrefix = t.codeFontFamily == 'monospace' + ? '' + : "'${t.codeFontFamily}',"; + final codeFamily = + '${codePrefix}SFMono-Regular,Consolas,"Liberation Mono",monospace'; return '$fontFace\n' '*{box-sizing:border-box}' 'html,body{margin:0;padding:0}' @@ -603,9 +608,9 @@ class MarpHtmlService { '.slide p,.slide li{font-size:24px;line-height:1.45}' '.slide pre{background:${t.codeBackgroundColor};color:${t.codeTextColor};' 'border:1px solid ${t.codeTextColor}38;border-radius:6px;' - 'padding:16px;overflow:auto;font-size:18px}' + 'padding:16px;overflow:auto;font-size:18px;font-family:$codeFamily}' '.slide pre code{color:${t.codeTextColor};background:transparent}' - '.slide code{font-family:SFMono-Regular,Consolas,"Liberation Mono",monospace}' + '.slide code{font-family:$codeFamily}' '.slide pre.mermaid{background:transparent;border:0;text-align:center}' '.slide img{max-width:100%}' '.slide blockquote{border-left:4px solid ${t.accentColor};margin:.5em 0;' diff --git a/lib/widgets/dialogs/settings_dialog.dart b/lib/widgets/dialogs/settings_dialog.dart index fb7f4a2..134eab7 100644 --- a/lib/widgets/dialogs/settings_dialog.dart +++ b/lib/widgets/dialogs/settings_dialog.dart @@ -882,11 +882,47 @@ class _SettingsDialogState extends ConsumerState { ); } + /// A banner shown on tabs that edit the active style profile, so it is clear + /// these settings belong to the loaded profile (and which one). + Widget _profileScopeBanner() { + final name = _themeProfile.name; + return Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: AppTheme.accent.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(8), + border: Border(left: BorderSide(color: AppTheme.accent, width: 3)), + ), + child: Row( + children: [ + Icon(Icons.style_outlined, size: 16, color: AppTheme.accent), + const SizedBox(width: 8), + Expanded( + child: Text.rich( + TextSpan( + children: [ + TextSpan(text: context.l10n.d('Onderdeel van stijlprofiel ')), + TextSpan( + text: '“$name”', + style: const TextStyle(fontWeight: FontWeight.w700), + ), + ], + ), + style: const TextStyle(fontSize: 12, color: Color(0xFF334155)), + ), + ), + ], + ), + ); + } + Widget _colorsTab() { final l10n = context.l10n; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + _profileScopeBanner(), _sectionTitle(l10n.d('Kleuren')), _colorSetting( l10n.d('Achtergrond slides'), @@ -974,6 +1010,33 @@ class _SettingsDialogState extends ConsumerState { contentPadding: EdgeInsets.zero, dense: true, ), + const SizedBox(height: 10), + DropdownButtonFormField( + initialValue: AppSettings.codeFonts.contains(_themeProfile.codeFontFamily) + ? _themeProfile.codeFontFamily + : 'monospace', + decoration: InputDecoration( + labelText: l10n.d('Broncode lettertype'), + isDense: true, + ), + items: [ + for (final f in AppSettings.codeFonts) + DropdownMenuItem( + value: f, + child: Text( + f == 'monospace' ? l10n.d('Systeem (monospace)') : f, + style: TextStyle(fontFamily: f), + ), + ), + ], + onChanged: (v) { + if (v == null) return; + setState(() { + _themeProfile = _themeProfile.copyWith(codeFontFamily: v); + _profileTouched = true; + }); + }, + ), const SizedBox(height: 18), _stylePreview(), ], @@ -985,6 +1048,7 @@ class _SettingsDialogState extends ConsumerState { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + _profileScopeBanner(), _sectionTitle(l10n.d('Logo')), Row( children: [ diff --git a/lib/widgets/slides/slide_preview.dart b/lib/widgets/slides/slide_preview.dart index d7342b9..c770337 100644 --- a/lib/widgets/slides/slide_preview.dart +++ b/lib/widgets/slides/slide_preview.dart @@ -2086,6 +2086,16 @@ class _CodePreview extends StatelessWidget { required this.profile, }); + /// Natural (unwrapped) size of [text] in [style]: width is the longest line, + /// height the full block. Used to scale code to the available space. + static Size _measureMono(String text, TextStyle style) { + final painter = TextPainter( + text: TextSpan(text: text.isEmpty ? ' ' : text, style: style), + textDirection: TextDirection.ltr, + )..layout(); + return painter.size; + } + @override Widget build(BuildContext context) { _ensureHighlightLanguages(); @@ -2098,10 +2108,20 @@ class _CodePreview extends StatelessWidget { final codeBg = _hexColor(profile.codeBackgroundColor); final codeFg = _hexColor(profile.codeTextColor); - final mono = TextStyle( - fontFamily: 'monospace', - fontFamilyFallback: const ['Menlo', 'Consolas', 'Courier New'], - fontSize: w * 0.024, + // The chosen monospace family, always backed by a generic monospace fallback + // so an uninstalled face still renders fixed-width. + final fallback = [ + 'Menlo', + 'Consolas', + 'Courier New', + 'monospace', + ]..removeWhere((f) => f == profile.codeFontFamily); + final baseFont = w * 0.024; + final maxFont = w * 0.040; // grow to fill, but never huge + TextStyle monoAt(double size) => TextStyle( + fontFamily: profile.codeFontFamily, + fontFamilyFallback: fallback, + fontSize: size, height: 1.4, color: codeFg, ); @@ -2109,23 +2129,25 @@ class _CodePreview extends StatelessWidget { // HighlightView throws on an unknown language, so fall back to plain (but // monospace) text. When syntax highlighting is off we always render plain // text so the whole block is one colour — needed for a CRT-green screen. - final Widget codeContent = (known && profile.codeHighlightSyntax) + final useHighlight = known && profile.codeHighlightSyntax; + final highlightTheme = { + ...atomOneDarkTheme, + // Keep atom-one-dark's per-token colours but drop its own background so + // our themed [codeBg] shows through unchanged. + 'root': (atomOneDarkTheme['root'] ?? const TextStyle()).copyWith( + backgroundColor: codeBg, + color: codeFg, + ), + }; + Widget buildCode(TextStyle style) => useHighlight ? HighlightView( code, language: lang, - // Keep atom-one-dark's per-token colours but drop its own - // background so our themed [codeBg] shows through unchanged. - theme: { - ...atomOneDarkTheme, - 'root': (atomOneDarkTheme['root'] ?? const TextStyle()).copyWith( - backgroundColor: codeBg, - color: codeFg, - ), - }, + theme: highlightTheme, padding: EdgeInsets.zero, - textStyle: mono, + textStyle: style, ) - : Text(code, style: mono); + : Text(code, style: style); return Container( color: _hexColor(profile.slideBackgroundColor), @@ -2136,45 +2158,80 @@ class _CodePreview extends StatelessWidget { pad, pad + safe.bottom, ), - child: Container( - width: double.infinity, - decoration: BoxDecoration( - color: codeBg, - borderRadius: BorderRadius.circular(w * 0.012), - border: Border.all(color: codeFg.withValues(alpha: 0.22)), - ), - padding: EdgeInsets.all(w * 0.03), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (slide.title.isNotEmpty) ...[ - _md( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // The slide title belongs to the slide, not inside the code window, + // so it sits above the panel like other slide types. + if (slide.title.isNotEmpty) ...[ + Container( + width: double.infinity, + padding: EdgeInsets.symmetric( + horizontal: w * 0.025, + vertical: w * 0.01, + ), + decoration: BoxDecoration( + color: _hexColor(profile.titleBackgroundColor), + borderRadius: BorderRadius.circular(w * 0.012), + border: Border( + left: BorderSide( + color: _hexColor(profile.accentColor), + width: w * 0.006, + ), + ), + ), + child: _md( context, slide.title, _applyFont( font, TextStyle( - fontSize: w * 0.03, + fontSize: w * 0.032, + height: 1.1, fontWeight: FontWeight.bold, - color: codeFg, + color: _hexColor(profile.titleTextColor), ), ), linkColor: _hexColor(profile.accentColor), ), - SizedBox(height: w * 0.02), - ], - Expanded( - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.topLeft, - // Een onbegrensde breedte laat code-regels op hun natuurlijke - // lengte staan (geen woordafbreking), waarna de FittedBox het - // geheel verkleint tot het past. - child: codeContent, + ), + SizedBox(height: w * 0.018), + ], + Expanded( + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: codeBg, + borderRadius: BorderRadius.circular(w * 0.012), + border: Border.all(color: codeFg.withValues(alpha: 0.22)), + ), + padding: EdgeInsets.all(w * 0.03), + child: LayoutBuilder( + builder: (context, constraints) { + // Size the code to fill the panel: scale up to use spare + // space (capped at [maxFont]) and down so long fragments + // still fit, rather than leaving a small block in a big box. + final measured = useHighlight + ? code.replaceAll('\t', ' ') + : code; + final natural = _measureMono(measured, monoAt(baseFont)); + final availW = math.max(1.0, constraints.maxWidth - 1); + final availH = math.max(1.0, constraints.maxHeight - 1); + var scale = math.min( + availW / natural.width, + availH / natural.height, + ); + if (!scale.isFinite || scale <= 0) scale = 1; + final size = math.min(baseFont * scale, maxFont); + return Align( + alignment: Alignment.topLeft, + child: buildCode(monoAt(size)), + ); + }, ), ), - ], - ), + ), + ], ), ), ); diff --git a/test/code_preview_test.dart b/test/code_preview_test.dart index c3f3eff..6cb91d3 100644 --- a/test/code_preview_test.dart +++ b/test/code_preview_test.dart @@ -80,4 +80,78 @@ void main() { expect(codeText.style?.color, _hex('#33FF33')); expect(tester.takeException(), isNull); }); + + testWidgets('short code is enlarged to use the space; long code shrinks', ( + tester, + ) async { + const profile = ThemeProfile(codeHighlightSyntax: false); + + const short = 'x'; + await tester.pumpWidget( + _host( + Slide.create(SlideType.code).copyWith(customMarkdown: short), + profile, + ), + ); + await tester.pump(); + final shortSize = tester.widget(find.text(short)).style!.fontSize!; + + final long = List.generate( + 40, + (i) => 'final someRatherLongVariableName$i = compute($i);', + ).join('\n'); + await tester.pumpWidget( + _host( + Slide.create(SlideType.code).copyWith(customMarkdown: long), + profile, + ), + ); + await tester.pump(); + final longSize = tester.widget(find.text(long)).style!.fontSize!; + + // A tiny snippet is scaled up to fill; a big one is scaled down to fit. + expect(longSize, lessThan(shortSize)); + expect(tester.takeException(), isNull); + }); + + testWidgets('the slide title sits above the code panel, not inside it', ( + tester, + ) async { + final slide = Slide.create( + SlideType.code, + ).copyWith(title: 'Voorbeeld', customMarkdown: 'print("hi")'); + const profile = ThemeProfile( + titleTextColor: '#FFFFFF', + titleBackgroundColor: '#1C2B47', + codeBackgroundColor: '#000000', + codeTextColor: '#33FF33', + codeHighlightSyntax: false, + ); + + await tester.pumpWidget(_host(slide, profile)); + await tester.pump(); + + // The title is rendered above the code panel rather than inside it. + final titleBottom = tester.getBottomLeft(find.text('Voorbeeld')).dy; + final codeTop = tester.getTopLeft(find.text('print("hi")')).dy; + expect(titleBottom, lessThanOrEqualTo(codeTop)); + expect(tester.takeException(), isNull); + }); + + testWidgets('code uses the chosen monospace font family', (tester) async { + final slide = Slide.create( + SlideType.code, + ).copyWith(customMarkdown: 'void main() {}'); + const profile = ThemeProfile( + codeFontFamily: 'Courier New', + codeHighlightSyntax: false, + ); + + await tester.pumpWidget(_host(slide, profile)); + await tester.pump(); + + final codeText = tester.widget(find.text('void main() {}')); + expect(codeText.style?.fontFamily, 'Courier New'); + expect(tester.takeException(), isNull); + }); } diff --git a/test/marp_html_service_test.dart b/test/marp_html_service_test.dart index 8afd516..319b8c4 100644 --- a/test/marp_html_service_test.dart +++ b/test/marp_html_service_test.dart @@ -106,11 +106,14 @@ void main() {} const theme = ThemeProfile( codeBackgroundColor: '#000000', codeTextColor: '#33FF33', + codeFontFamily: 'Courier New', ); final html = await service.build('```dart\nvoid main() {}\n```', theme: theme); expect(html, contains('.slide pre{background:#000000;color:#33FF33')); expect(html, contains('.slide pre code{color:#33FF33')); + // The chosen code font is used (with a monospace fallback chain). + expect(html, contains("font-family:'Courier New',")); }); test('EB Garamond theme embeds the font for offline rendering', () async { diff --git a/test/settings_provider_test.dart b/test/settings_provider_test.dart index a0249b3..88a4037 100644 --- a/test/settings_provider_test.dart +++ b/test/settings_provider_test.dart @@ -15,24 +15,27 @@ Future _loadedNotifier() async { void main() { TestWidgetsFlutterBinding.ensureInitialized(); - test('ThemeProfile round-trips the code colours through JSON', () { + test('ThemeProfile round-trips the code styling through JSON', () { const profile = ThemeProfile( codeBackgroundColor: '#000000', codeTextColor: '#33FF33', codeHighlightSyntax: false, + codeFontFamily: 'Courier New', ); final back = ThemeProfile.fromJson(profile.toJson()); expect(back.codeBackgroundColor, '#000000'); expect(back.codeTextColor, '#33FF33'); expect(back.codeHighlightSyntax, isFalse); + expect(back.codeFontFamily, 'Courier New'); }); - test('ThemeProfile code colours default to the atom-one-dark look', () { + test('ThemeProfile code styling defaults to the atom-one-dark look', () { // Older decks without the fields fall back to the dark editor defaults. final back = ThemeProfile.fromJson(const {'name': 'Legacy'}); expect(back.codeBackgroundColor, '#282C34'); expect(back.codeTextColor, '#ABB2BF'); expect(back.codeHighlightSyntax, isTrue); + expect(back.codeFontFamily, 'monospace'); }); test('starts with a single default profile', () async {