nandi/vflutter_ffipublic Fork 0
472eb493bac94bdcb79a38829ccca0c541256fef
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.

Replace V with Nim 472eb49 · on 472eb493bac94bdcb79a38829ccca0c541256fef · nandithebull · 6h ago
widget_test.dart · 121 lines · 4.7 KBDart Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
// Runs against the real Nim library, so it needs the native build on the loader
// path — `flutter test` runs on the host VM, not in the app bundle:
//
//   ./tool/build.sh
//   cd example && LD_LIBRARY_PATH=../build flutter test
//
// This is a smoke test of the bridge as the app uses it, not of the widgets.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:nimflutter_ffi/nimflutter_ffi.dart' as v;
import 'package:nimflutter_ffi_example/mandelbrot_dart.dart';

void main() {
  _zoomTests();

  const view = v.FractalView(width: 64, height: 48, maxIter: 80);

  test('Nim fills the caller-owned buffer', () {
    final pixels = v.render(view);
    expect(pixels.length, view.width * view.height * 4);
    // Alpha is written for every pixel, inside the set and out.
    for (var i = 3; i < pixels.length; i += 4) {
      expect(pixels[i], 255);
    }
    // The frame is not uniform — it actually contains a fractal.
    expect(pixels.toSet().length, greaterThan(8));
  });

  test('band split matches a single-shot render', () async {
    final whole = v.render(view);
    final tiled = await v.renderParallel(view, tiles: 4);
    expect(tiled, equals(whole));
  });

  test('the Nim and Dart kernels agree pixel-for-pixel', () {
    expect(v.render(view), equals(renderDart(view)));
  });

  testWidgets('renders a frame computed in Nim', (tester) async {
    await tester.pumpWidget(const MaterialApp(home: SizedBox()));
    // The explorer renders asynchronously; just assert the bridge is usable
    // from the test binding, which is where FFI setup usually breaks.
    expect(v.add(20, 22), 42);
    expect(v.greet('Flutter'), 'Hello, Flutter, from Nim!');
  });
}

void _zoomTests() {
  group('zoomedAt', () {
    const view = v.FractalView(width: 800, height: 600);

    test('keeps the anchor point fixed in the complex plane', () {
      // The plane coordinate under a given viewport fraction must be the same
      // before and after the zoom. This is the property that broke when the
      // maths lived inline in the widget.
      double planeX(v.FractalView f, double fx) =>
          f.centerX + (fx - 0.5) * f.scale * (f.width / f.height);
      double planeY(v.FractalView f, double fy) =>
          f.centerY + (fy - 0.5) * f.scale;

      for (final p in [0.0, 0.25, 0.5, 0.73, 1.0]) {
        final zoomed = view.zoomedAt(p, p, 0.5);
        expect(planeX(zoomed, p), closeTo(planeX(view, p), 1e-12));
        expect(planeY(zoomed, p), closeTo(planeY(view, p), 1e-12));
      }
    });

    test('one tap is exactly one zoom level', () {
      expect(view.zoomedAt(0.5, 0.5, 0.5).scale, closeTo(view.scale / 2, 1e-12));
      expect(
        view.zoomedAt(0.5, 0.5, 0.5).zoomedAt(0.5, 0.5, 0.5).scale,
        closeTo(view.scale / 4, 1e-12),
      );
    });

    test('panning moves the plane under the viewport, not the other way', () {
      // Dragging right must reveal what was to the LEFT, so the centre moves
      // left. Getting this backwards is the classic panning bug.
      final right = view.pannedBy(0.5, 0.0);
      expect(right.centerX, lessThan(view.centerX));
      expect(right.centerY, view.centerY);

      final down = view.pannedBy(0.0, 0.5);
      expect(down.centerY, lessThan(view.centerY));
      expect(down.centerX, view.centerX);
    });

    test('a full-width drag moves exactly one viewport', () {
      final panned = view.pannedBy(1.0, 0.0);
      final aspect = view.width / view.height;
      expect(panned.centerX,
          closeTo(view.centerX - view.scale * aspect, 1e-12));
    });

    test('panning does not change the zoom level', () {
      expect(view.pannedBy(0.3, -0.7).scale, view.scale);
    });

    test('panning back and forth returns to the starting view', () {
      final round = view.pannedBy(0.4, 0.25).pannedBy(-0.4, -0.25);
      expect(round.centerX, closeTo(view.centerX, 1e-12));
      expect(round.centerY, closeTo(view.centerY, 1e-12));
    });

    test('pan distance scales with zoom depth', () {
      // The same drag must cover less ground when zoomed in, or deep zooms
      // become unnavigable.
      final deep = view.copyWith(scale: view.scale / 1000);
      final shallowShift = (view.pannedBy(0.5, 0).centerX - view.centerX).abs();
      final deepShift = (deep.pannedBy(0.5, 0).centerX - deep.centerX).abs();
      expect(deepShift, lessThan(shallowShift / 100));
    });

    test('zooming in then out returns to the starting view', () {
      final round = view.zoomedAt(0.3, 0.8, 0.5).zoomedAt(0.3, 0.8, 2.0);
      expect(round.scale, closeTo(view.scale, 1e-12));
      expect(round.centerX, closeTo(view.centerX, 1e-12));
      expect(round.centerY, closeTo(view.centerY, 1e-12));
    });
  });
}