nandi/vflutter_ffipublic Fork 0
4506a586d7400b00d732dcb97167be75ab41cab1
Commits
Clone
git clone https://git.rickub.com/nandi/vflutter_ffi.git
git clone ssh://git@rickub.com/nandi/vflutter_ffi.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

main.dart · 587 lines · 18.9 KBDart Blame HistoryRaw
Replace V with Nim 472eb49 nandithebull 9h ago1// A Mandelbrot explorer whose pixels are computed in Nim.
Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago2//
3// What this is actually demonstrating:
4// * bulk data across the FFI boundary — a full RGBA frame per render,
Replace V with Nim 472eb49 nandithebull 9h ago5// into a buffer Dart allocates and Nim fills in place (nothing to nf_free)
Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago6// * band parallelism — the image is split into horizontal strips rendered
7// on separate isolates, which is safe because the library is -gc none
Replace V with Nim 472eb49 nandithebull 9h ago8// * the same algorithm written twice, in Nim and in Dart, so the cost of the
Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago9// bridge is a number on screen rather than a claim
10import 'dart:async';
11import 'dart:math' as math;
12import 'dart:typed_data';
13import 'dart:ui' as ui;
14
15import 'package:flutter/gestures.dart';
16import 'package:flutter/material.dart';
Replace V with Nim 472eb49 nandithebull 9h ago17import 'package:nimflutter_ffi/nimflutter_ffi.dart' as v;
Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago18
19import 'mandelbrot_dart.dart';
20
21void main() => runApp(const ExampleApp());
22
23class ExampleApp extends StatelessWidget {
24 const ExampleApp({super.key});
25
26 @override
27 Widget build(BuildContext context) => MaterialApp(
Replace V with Nim 472eb49 nandithebull 9h ago28 title: 'nimflutter_ffi',
Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago29 debugShowCheckedModeBanner: false,
30 theme: ThemeData.dark(useMaterial3: true).copyWith(
31 scaffoldBackgroundColor: const Color(0xFF12141A),
32 ),
33 home: const ExplorerPage(),
34 );
35}
36
37class ExplorerPage extends StatefulWidget {
38 const ExplorerPage({super.key});
39
40 @override
41 State<ExplorerPage> createState() => _ExplorerPageState();
42}
43
Add drag-to-pan 0ed90f3 nandithebull 11h ago44/// Identifies the interactive canvas, so tests can drive gestures on it
45/// without depending on whether a frame has finished rendering yet.
46const Key canvasKey = Key('fractal-canvas');
47
Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago48class _ExplorerPageState extends State<ExplorerPage> {
49 static const int _renderWidth = 720;
50 static const int _renderHeight = 540;
51
52 v.FractalView _view = const v.FractalView(
53 width: _renderWidth,
54 height: _renderHeight,
55 maxIter: 400,
56 );
57
58 ui.Image? _image;
59 bool _rendering = false;
60 bool _renderQueued = false;
61 int _lastMs = 0;
Raise the isolate ceiling to 32, and add the sweep that justifies it 59a731a nandithebull 11h ago62 int _tiles = 16;
Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago63 _Benchmark? _benchmark;
Add a WebAssembly backend for the example 728acaa nandithebull 9h ago64 String? _bootError;
65
66 /// Bands run on isolates natively and sequentially on web, so the word the
67 /// UI uses for them has to follow the backend.
Render in parallel on web, via a pool of Web Workers 4506a58 nandithebull 8h ago68 String get _unit =>
69 v.supportsIsolateParallelism ? v.parallelUnitName : 'band';
Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago70
71 @override
72 void initState() {
73 super.initState();
Add a WebAssembly backend for the example 728acaa nandithebull 9h ago74 _boot();
75 }
76
77 /// Brings the bridge up and draws the first frame.
78 Future<void> _boot() => _render();
79
80 /// Waits for the bridge, returning false if it could not be brought up.
81 ///
82 /// On native this resolves immediately. On web it is where the wasm module
83 /// is fetched and instantiated, which cannot be synchronous.
84 ///
85 /// Every path that asks for a frame goes through here, not just startup:
86 /// the first layout pass calls _fitToCanvas -> _render before initState's
87 /// future has resolved, so gating only the boot path would still let the
88 /// very first render race the module load. initialize() is idempotent and
89 /// hands back the in-flight future, so the extra calls just wait.
90 Future<bool> _ensureReady() async {
91 if (v.isInitialized) return true;
92 try {
93 await v.initialize();
94 return true;
95 } catch (e) {
96 if (mounted) setState(() => _bootError = '$e');
97 return false;
98 }
Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago99 }
100
101 @override
102 void dispose() {
103 _image?.dispose();
104 super.dispose();
105 }
106
107 /// Renders the current view, coalescing requests that arrive mid-frame.
108 ///
109 /// A wheel gesture emits far more events than we can render. Dropping them
110 /// outright would leave the view mutated but never drawn, so instead the
111 /// latest request is remembered and serviced once the in-flight frame lands.
112 Future<void> _render() async {
Add a WebAssembly backend for the example 728acaa nandithebull 9h ago113 if (!await _ensureReady()) return;
Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago114 if (_rendering) {
115 _renderQueued = true;
116 return;
117 }
118 setState(() => _rendering = true);
119
120 do {
121 _renderQueued = false;
122 final view = _view;
123
124 final watch = Stopwatch()..start();
125 final pixels = _tiles > 1
126 ? await v.renderParallel(view, tiles: _tiles)
127 : v.render(view);
128 watch.stop();
129
130 final image = await _decode(pixels, view.width, view.height);
131 if (!mounted) {
132 image.dispose();
133 return;
134 }
135 setState(() {
136 _image?.dispose();
137 _image = image;
138 _lastMs = watch.elapsedMilliseconds;
139 });
140 } while (_renderQueued);
141
142 if (!mounted) return;
143 setState(() => _rendering = false);
144 }
145
146 static Future<ui.Image> _decode(Uint8List rgba, int w, int h) {
147 final completer = Completer<ui.Image>();
148 ui.decodeImageFromPixels(rgba, w, h, ui.PixelFormat.rgba8888,
149 completer.complete);
150 return completer.future;
151 }
152
Add drag-to-pan 0ed90f3 nandithebull 11h ago153 /// Drags the view by a pixel delta, so the image follows the cursor.
154 void _panBy(Offset delta, Size widgetSize) {
155 if (widgetSize.isEmpty || delta == Offset.zero) return;
156 setState(() {
157 _view = _view.pannedBy(
158 delta.dx / widgetSize.width,
159 delta.dy / widgetSize.height,
160 );
161 });
162 _render();
163 }
164
Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago165 /// Zooms about a point, keeping that point under the cursor.
166 void _zoomAt(Offset local, Size widgetSize, double factor) {
167 if (widgetSize.isEmpty) return;
168 setState(() {
169 _view = _view.zoomedAt(
170 local.dx / widgetSize.width,
171 local.dy / widgetSize.height,
172 factor,
173 );
174 });
175 _render();
176 }
177
178 /// Restores the starting framing.
179 ///
180 /// Only the pan/zoom is reset. Width and height belong to the canvas (see
181 /// _fitToCanvas) and maxIter belongs to its slider — clobbering those made
182 /// "Reset view" silently undo settings the button does not own, and briefly
183 /// render at the wrong aspect ratio before the canvas corrected it.
184 void _reset() {
185 const home = v.FractalView(width: 1, height: 1);
186 setState(() {
187 _view = _view.copyWith(
188 centerX: home.centerX,
189 centerY: home.centerY,
190 scale: home.scale,
191 );
192 });
193 _render();
194 }
195
Replace V with Nim 472eb49 nandithebull 9h ago196 /// Renders the identical frame in Nim and in Dart on this isolate, so the
197 /// comparison is like-for-like rather than Nim-plus-parallelism vs Dart.
Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago198 Future<void> _runBenchmark() async {
Add a WebAssembly backend for the example 728acaa nandithebull 9h ago199 if (!await _ensureReady()) return;
Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago200 setState(() {
201 _rendering = true;
202 _benchmark = null;
203 });
204
205 final swV = Stopwatch()..start();
206 v.render(_view);
207 swV.stop();
208
209 final swP = Stopwatch()..start();
210 await v.renderParallel(_view, tiles: _tiles);
211 swP.stop();
212
213 final swD = Stopwatch()..start();
214 renderDart(_view);
215 swD.stop();
216
217 if (!mounted) return;
218 setState(() {
219 _rendering = false;
220 _benchmark = _Benchmark(
Add a WebAssembly backend for the example 728acaa nandithebull 9h ago221 unit: _unit,
222 parallel: v.supportsIsolateParallelism,
Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago223 vSerialMs: swV.elapsedMilliseconds,
224 vParallelMs: swP.elapsedMilliseconds,
225 dartMs: swD.elapsedMilliseconds,
226 tiles: _tiles,
227 );
228 });
229 }
230
231 @override
232 Widget build(BuildContext context) {
233 return Scaffold(
234 body: SafeArea(
235 child: LayoutBuilder(
236 builder: (context, constraints) {
237 final wide = constraints.maxWidth > 900;
238 final canvas = _buildCanvas();
239 final controls = _buildControls();
240 return wide
241 ? Row(
242 crossAxisAlignment: CrossAxisAlignment.stretch,
243 children: [
244 Expanded(child: canvas),
245 SizedBox(
246 width: 340,
247 child: SingleChildScrollView(child: controls),
248 ),
249 ],
250 )
251 : ListView(
252 children: [
253 AspectRatio(
254 aspectRatio: _renderWidth / _renderHeight,
255 child: canvas,
256 ),
257 controls,
258 ],
259 );
260 },
261 ),
262 ),
263 );
264 }
265
266 /// Keeps the rendered buffer at the canvas's aspect ratio.
267 ///
268 /// Without this the image would be letterboxed or cropped, and the
269 /// click-to-zoom maths — which maps widget coordinates straight into the
270 /// complex plane — would point at the wrong place.
271 void _fitToCanvas(Size size) {
272 if (size.isEmpty) return;
273 final height = (_renderWidth * size.height / size.width).round();
274 if ((height - _view.height).abs() <= 2) return;
275 WidgetsBinding.instance.addPostFrameCallback((_) {
276 if (!mounted) return;
277 setState(() => _view = _view.copyWith(height: height));
278 _render();
279 });
280 }
281
282 Widget _buildCanvas() {
283 return LayoutBuilder(
284 builder: (context, constraints) {
285 final size = Size(constraints.maxWidth, constraints.maxHeight);
286 _fitToCanvas(size);
287 return Listener(
Add drag-to-pan 0ed90f3 nandithebull 11h ago288 key: canvasKey,
Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago289 // Wheel zoom, anchored at the cursor like the tap zoom. One notch is
290 // a small step so a scroll feels continuous rather than octave-wise;
291 // the render loop coalesces whatever it cannot keep up with.
292 onPointerSignal: (event) {
293 if (event is! PointerScrollEvent) return;
294 final dy = event.scrollDelta.dy;
295 if (dy == 0) return;
296 _zoomAt(event.localPosition, size, math.pow(1.0015, dy).toDouble());
297 },
298 child: GestureDetector(
Add drag-to-pan 0ed90f3 nandithebull 11h ago299 // Zoom on tap up rather than tap down. The gesture arena already
300 // keeps onTapDown from firing once a pan claims the pointer, so
301 // this is not what makes drag-to-pan work — it just means the
302 // zoom commits on a completed tap.
303 onTapUp: (d) => _zoomAt(d.localPosition, size, 0.5),
304 onSecondaryTapUp: (d) => _zoomAt(d.localPosition, size, 2.0),
305 onPanUpdate: (d) => _panBy(d.delta, size),
Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago306 child: Stack(
307 fit: StackFit.expand,
308 children: [
Add a WebAssembly backend for the example 728acaa nandithebull 9h ago309 if (_bootError != null)
310 // Most likely on web: the wasm glue did not load, so there is
Replace V with Nim 472eb49 nandithebull 9h ago311 // no Nim to call. Say so rather than spinning forever.
Add a WebAssembly backend for the example 728acaa nandithebull 9h ago312 Center(
313 child: Padding(
314 padding: const EdgeInsets.all(24),
315 child: Text(
Replace V with Nim 472eb49 nandithebull 9h ago316 'Could not start the Nim bridge.\n\n$_bootError',
Add a WebAssembly backend for the example 728acaa nandithebull 9h ago317 textAlign: TextAlign.center,
318 style: Theme.of(context).textTheme.bodySmall,
319 ),
320 ),
321 )
322 else if (_image != null)
Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago323 RawImage(image: _image, fit: BoxFit.fill)
324 else
325 const Center(child: CircularProgressIndicator()),
326 Positioned(
327 left: 12,
328 bottom: 12,
329 child: _Chip(
330 'zoom ${(3.0 / _view.scale).toStringAsFixed(1)}x · '
331 '$_lastMs ms · '
Add a WebAssembly backend for the example 728acaa nandithebull 9h ago332 '${_tiles > 1 ? "$_tiles ${_unit}s" : "1 $_unit"}',
Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago333 ),
334 ),
335 if (_rendering)
336 const Positioned(
337 right: 12,
338 top: 12,
339 child: SizedBox(
340 width: 18,
341 height: 18,
342 child: CircularProgressIndicator(strokeWidth: 2),
343 ),
344 ),
345 ],
346 ),
347 ),
348 );
349 },
350 );
351 }
352
353 /// Returns a non-scrolling column.
354 ///
355 /// The wide layout wraps this in its own scroll view; the narrow layout
356 /// embeds it in the page's ListView. Making this a ListView itself would
357 /// nest two scrollables and give the inner one unbounded height.
358 Widget _buildControls() {
359 final theme = Theme.of(context);
360 return Padding(
361 padding: const EdgeInsets.all(20),
362 child: Column(
363 crossAxisAlignment: CrossAxisAlignment.stretch,
364 children: [
Replace V with Nim 472eb49 nandithebull 9h ago365 Text('Mandelbrot in Nim', style: theme.textTheme.titleLarge),
Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago366 const SizedBox(height: 4),
367 Text(
Add drag-to-pan 0ed90f3 nandithebull 11h ago368 'Scroll or tap to zoom in, right-click to zoom out, drag to pan. '
Replace V with Nim 472eb49 nandithebull 9h ago369 'Every pixel is computed by nf_mandelbrot in src/nimflutter.v.',
Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago370 style: theme.textTheme.bodySmall,
371 ),
372 const SizedBox(height: 20),
373 _Labelled(
374 label: 'iterations',
375 value: '${_view.maxIter}',
376 child: Slider(
377 value: _view.maxIter.toDouble(),
378 min: 50,
379 max: 2000,
380 divisions: 39,
381 onChanged: (n) =>
382 setState(() => _view = _view.copyWith(maxIter: n.round())),
383 onChangeEnd: (_) => _render(),
384 ),
385 ),
386 _Labelled(
Add a WebAssembly backend for the example 728acaa nandithebull 9h ago387 label: '${_unit}s',
Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago388 value: '$_tiles',
389 child: Slider(
390 value: _tiles.toDouble(),
391 min: 1,
Raise the isolate ceiling to 32, and add the sweep that justifies it 59a731a nandithebull 11h ago392 // Deliberately well past the core count. Bands cost wildly
393 // different amounts — rows through the set's interior run the full
394 // iteration cap, rows in open space escape immediately — so the
395 // frame waits on its slowest band. Over-decomposing lets a free
396 // core pick up the next small band instead of idling. Measured on
397 // an 8-thread machine: 8 tiles 44 ms, 24 tiles 29 ms.
398 max: 32,
399 divisions: 31,
Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago400 onChanged: (n) => setState(() => _tiles = n.round()),
401 onChangeEnd: (_) => _render(),
402 ),
403 ),
404 const SizedBox(height: 8),
405 Row(
406 children: [
407 Expanded(
408 child: FilledButton.tonal(
409 onPressed: _rendering ? null : _reset,
410 child: const Text('Reset view'),
411 ),
412 ),
413 const SizedBox(width: 12),
414 Expanded(
415 child: FilledButton(
416 onPressed: _rendering ? null : _runBenchmark,
417 child: const Text('Benchmark'),
418 ),
419 ),
420 ],
421 ),
422 const SizedBox(height: 20),
423 if (_benchmark != null) _BenchmarkCard(result: _benchmark!),
424 const SizedBox(height: 12),
425 Card(
426 color: theme.colorScheme.surfaceContainerHighest,
427 child: Padding(
428 padding: const EdgeInsets.all(14),
429 child: Text(
Render in parallel on web, via a pool of Web Workers 4506a58 nandithebull 8h ago430 v.backendName == 'dart:ffi'
Replace V with Nim 472eb49 nandithebull 9h ago431 ? 'The Nim kernel fills a buffer Dart allocates, so no Nim '
Add a WebAssembly backend for the example 728acaa nandithebull 9h ago432 'memory crosses the boundary and there is nothing to '
433 'free. Bands are independent, which is what makes the '
Render in parallel on web, via a pool of Web Workers 4506a58 nandithebull 8h ago434 'isolate slider safe: ARC has no stop-the-world phase '
435 'to serialise them.'
Replace V with Nim 472eb49 nandithebull 9h ago436 : 'The same Nim kernel, compiled to WebAssembly and reached '
Add a WebAssembly backend for the example 728acaa nandithebull 9h ago437 'over dart:js_interop instead of dart:ffi — it fills a '
438 'buffer in the wasm heap, so nothing needs freeing on '
Render in parallel on web, via a pool of Web Workers 4506a58 nandithebull 8h ago439 'the Dart side. Each Web Worker holds its own instance '
440 'of the module, so nothing is shared and no '
441 'SharedArrayBuffer or COOP/COEP headers are needed. The '
442 'frame is identical to the native build, byte for byte.',
Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago443 style: theme.textTheme.bodySmall,
444 ),
445 ),
446 ),
447 ],
448 ),
449 );
450 }
451}
452
453class _Benchmark {
454 const _Benchmark({
455 required this.vSerialMs,
456 required this.vParallelMs,
457 required this.dartMs,
458 required this.tiles,
Add a WebAssembly backend for the example 728acaa nandithebull 9h ago459 required this.unit,
460 required this.parallel,
Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago461 });
462
463 final int vSerialMs;
464 final int vParallelMs;
465 final int dartMs;
466 final int tiles;
Add a WebAssembly backend for the example 728acaa nandithebull 9h ago467
468 /// 'isolate' natively, 'band' on web.
469 final String unit;
470
471 /// Whether the split actually ran concurrently.
472 final bool parallel;
Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago473}
474
475class _BenchmarkCard extends StatelessWidget {
476 const _BenchmarkCard({required this.result});
477
478 final _Benchmark result;
479
480 @override
481 Widget build(BuildContext context) {
482 final theme = Theme.of(context);
483 final ratio = result.dartMs / result.vSerialMs;
484 return Card(
485 child: Padding(
486 padding: const EdgeInsets.all(14),
487 child: Column(
488 crossAxisAlignment: CrossAxisAlignment.start,
489 children: [
490 Text('same frame, three ways',
491 style: theme.textTheme.titleSmall),
492 const SizedBox(height: 10),
Replace V with Nim 472eb49 nandithebull 9h ago493 _Row(label: 'Nim, 1 ${result.unit}', value: '${result.vSerialMs} ms'),
Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago494 _Row(
Replace V with Nim 472eb49 nandithebull 9h ago495 label: 'Nim, ${result.tiles} ${result.unit}s',
Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago496 value: '${result.vParallelMs} ms'),
Add a WebAssembly backend for the example 728acaa nandithebull 9h ago497 _Row(label: 'Dart, 1 ${result.unit}', value: '${result.dartMs} ms'),
Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago498 const Divider(height: 20),
499 Text(
Add a WebAssembly backend for the example 728acaa nandithebull 9h ago500 result.parallel
Replace V with Nim 472eb49 nandithebull 9h ago501 ? 'Nim is ${ratio.toStringAsFixed(2)}x Dart on one isolate. '
Add a WebAssembly backend for the example 728acaa nandithebull 9h ago502 'For a scalar FP loop the two are close — the real win '
503 'here is the parallel split, which Dart could also do. '
504 'The bridge is what this demonstrates, not a speed claim.'
Replace V with Nim 472eb49 nandithebull 9h ago505 : 'Nim is ${ratio.toStringAsFixed(2)}x Dart on one thread. '
Add a WebAssembly backend for the example 728acaa nandithebull 9h ago506 'The wasm module is single-threaded, so the '
507 '${result.tiles}-band row above is the same work done '
508 'in sequence, not in parallel — it is here to show the '
509 'split produces identical pixels either way.',
Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago510 style: theme.textTheme.bodySmall,
511 ),
512 ],
513 ),
514 ),
515 );
516 }
517}
518
519class _Row extends StatelessWidget {
520 const _Row({required this.label, required this.value});
521
522 final String label;
523 final String value;
524
525 @override
526 Widget build(BuildContext context) => Padding(
527 padding: const EdgeInsets.symmetric(vertical: 2),
528 child: Row(
529 mainAxisAlignment: MainAxisAlignment.spaceBetween,
530 children: [
531 Text(label, style: Theme.of(context).textTheme.bodyMedium),
532 Text(value,
533 style: Theme.of(context)
534 .textTheme
535 .bodyMedium
536 ?.copyWith(fontFeatures: const [ui.FontFeature.tabularFigures()])),
537 ],
538 ),
539 );
540}
541
542class _Labelled extends StatelessWidget {
543 const _Labelled({
544 required this.label,
545 required this.value,
546 required this.child,
547 });
548
549 final String label;
550 final String value;
551 final Widget child;
552
553 @override
554 Widget build(BuildContext context) {
555 final theme = Theme.of(context);
556 return Column(
557 crossAxisAlignment: CrossAxisAlignment.start,
558 children: [
559 Row(
560 mainAxisAlignment: MainAxisAlignment.spaceBetween,
561 children: [
562 Text(label, style: theme.textTheme.labelLarge),
563 Text(value, style: theme.textTheme.labelLarge),
564 ],
565 ),
566 child,
567 ],
568 );
569 }
570}
571
572class _Chip extends StatelessWidget {
573 const _Chip(this.text);
574
575 final String text;
576
577 @override
578 Widget build(BuildContext context) => Container(
579 padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
580 decoration: BoxDecoration(
581 color: Colors.black.withValues(alpha: 0.55),
582 borderRadius: BorderRadius.circular(8),
583 ),
584 child: Text(text,
585 style: const TextStyle(color: Colors.white, fontSize: 12)),
586 );
587}