181 lines
6.8 KiB
Dart
181 lines
6.8 KiB
Dart
|
|
// Part of the slide_preview library — see ../slide_preview.dart.
|
||
|
|
// Split out for navigability; all imports live in the main library file.
|
||
|
|
part of '../slide_preview.dart';
|
||
|
|
|
||
|
|
/// Een 'broncode-sheet': de code op een donker editor-vlak, met
|
||
|
|
/// syntaxkleuring wanneer een taal bekend is. De tekst blijft platte tekst maar
|
||
|
|
/// wordt monospace en gekleurd weergegeven. Past zich met een FittedBox aan de
|
||
|
|
/// slide aan zodat lange fragmenten netjes verkleinen i.p.v. af te kappen.
|
||
|
|
class _CodePreview extends StatelessWidget {
|
||
|
|
final Slide slide;
|
||
|
|
final double w;
|
||
|
|
final String font;
|
||
|
|
final ThemeProfile profile;
|
||
|
|
|
||
|
|
const _CodePreview({
|
||
|
|
required this.slide,
|
||
|
|
required this.w,
|
||
|
|
required this.font,
|
||
|
|
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();
|
||
|
|
final pad = w * 0.05;
|
||
|
|
final safe = slide.showLogo ? _logoSafeInsets(w, profile) : EdgeInsets.zero;
|
||
|
|
final code = slide.customMarkdown;
|
||
|
|
final lang = slide.codeLanguage.trim();
|
||
|
|
final known = lang.isNotEmpty && allLanguages.containsKey(lang);
|
||
|
|
|
||
|
|
final codeBg = _hexColor(profile.codeBackgroundColor);
|
||
|
|
final codeFg = _hexColor(profile.codeTextColor);
|
||
|
|
|
||
|
|
// The chosen monospace family, always backed by a generic monospace fallback
|
||
|
|
// so an uninstalled face still renders fixed-width.
|
||
|
|
final fallback = <String>['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,
|
||
|
|
);
|
||
|
|
|
||
|
|
// 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 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,
|
||
|
|
theme: highlightTheme,
|
||
|
|
padding: EdgeInsets.zero,
|
||
|
|
textStyle: style,
|
||
|
|
)
|
||
|
|
: Text(code, style: style);
|
||
|
|
|
||
|
|
return Container(
|
||
|
|
color: _hexColor(profile.slideBackgroundColor),
|
||
|
|
child: Padding(
|
||
|
|
padding: EdgeInsets.fromLTRB(
|
||
|
|
pad,
|
||
|
|
pad + safe.top,
|
||
|
|
pad,
|
||
|
|
pad + safe.bottom,
|
||
|
|
),
|
||
|
|
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.032,
|
||
|
|
height: 1.1,
|
||
|
|
fontWeight: FontWeight.bold,
|
||
|
|
color: _hexColor(profile.titleTextColor),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
linkColor: _hexColor(profile.accentColor),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
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)),
|
||
|
|
);
|
||
|
|
},
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Register highlight.js language definitions once, so [HighlightView] can
|
||
|
|
/// colour any common language without throwing.
|
||
|
|
bool _highlightReady = false;
|
||
|
|
|
||
|
|
void _ensureHighlightLanguages() {
|
||
|
|
if (_highlightReady) return;
|
||
|
|
allLanguages.forEach(highlight.registerLanguage);
|
||
|
|
_highlightReady = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Shared helper ─────────────────────────────────────────────────────────────
|