Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 38 additions & 14 deletions lib/features/erd/widgets/erd_relation_painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,34 +33,58 @@ class ErdRelationPainter extends material.CustomPainter {
(r.relation.fromTable == focus || r.relation.toTable == focus);
}

void _label(material.Canvas canvas, String text, material.Offset at,
material.Color c) {
final tp = material.TextPainter(
/// The markers are only ever `1` and `*`, in at most three colours (others,
/// others while something is focused, highlight), so each is laid out once
/// and reused by every relation and every repaint (#1518). A theme change
/// brings new colours; past a handful of entries the cache starts over.
static final _labels = <(String, int), material.TextPainter>{};
static const _maxLabels = 16;

static material.TextPainter _labelPainter(String text, material.Color c) {
final key = (text, c.toARGB32());
final cached = _labels[key];
if (cached != null) return cached;
if (_labels.length >= _maxLabels) {
for (final old in _labels.values) {
old.dispose();
}
_labels.clear();
}
return _labels[key] = material.TextPainter(
text: material.TextSpan(
text: text,
style: material.TextStyle(
fontSize: 10, color: c, fontWeight: material.FontWeight.w600)),
textDirection: material.TextDirection.ltr,
)..layout();
}

/// Rounded outline of a route, built once per [ErdRoute]: a repaint that
/// only changes the focus or the picked relation reuses the same routes.
static final _paths = Expando<material.Path>('erdRoundedPath');

void _label(material.Canvas canvas, String text, material.Offset at,
material.Color c) {
final tp = _labelPainter(text, c);
tp.paint(canvas, at - material.Offset(tp.width / 2, tp.height / 2));
tp.dispose();
}

@override
void paint(material.Canvas canvas, material.Size size) {
// One Paint for every route; only its colour and width change per pass.
final paint = material.Paint()
..style = material.PaintingStyle.stroke
..strokeCap = material.StrokeCap.round
..strokeJoin = material.StrokeJoin.round;
final dimmed = color.withValues(alpha: _anyFocus ? 0.35 : 0.85);
// Others first, focused on top.
for (final pass in [false, true]) {
for (final pass in const [false, true]) {
paint
..color = pass ? highlight : dimmed
..strokeWidth = pass ? 2 : 1.4;
for (final r in routes) {
if (_focused(r) != pass || r.points.length < 2) continue;
final paint = material.Paint()
..color = pass
? highlight
: color.withValues(alpha: _anyFocus ? 0.35 : 0.85)
..style = material.PaintingStyle.stroke
..strokeWidth = pass ? 2 : 1.4
..strokeCap = material.StrokeCap.round
..strokeJoin = material.StrokeJoin.round;
canvas.drawPath(roundedPath(r.points), paint);
canvas.drawPath(_paths[r] ??= roundedPath(r.points), paint);
// FK end: a crow's foot ("many"), or a bar when the FK column is
// unique on its own ("one", #1281).
if (r.relation.oneToOne) {
Expand Down
98 changes: 98 additions & 0 deletions test/features/erd/erd_relation_painter_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import 'dart:ui' as ui;

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:querya_desktop/core/erd/erd_model.dart';
import 'package:querya_desktop/core/erd/erd_router.dart';
import 'package:querya_desktop/features/erd/widgets/erd_relation_painter.dart';

ErdRoute _route(String from, String to, {bool oneToOne = false}) => ErdRoute(
ErdRelation(
fromTable: from,
fromColumn: 'id',
toTable: to,
toColumn: 'id',
oneToOne: oneToOne,
),
const [Offset(0, 0), Offset(40, 0), Offset(40, 60), Offset(120, 60)],
);

void _paint(ErdRelationPainter painter) {
final recorder = ui.PictureRecorder();
painter.paint(Canvas(recorder), const Size(200, 100));
recorder.endRecording().dispose();
}

void main() {
TestWidgetsFlutterBinding.ensureInitialized();

final routes = [_route('a', 'b'), _route('b', 'c', oneToOne: true)];

// The painter reuses one Paint, the rounded paths and the laid-out labels
// (#1518); these pin that every state still draws.
group('ErdRelationPainter', () {
test('paints with nothing focused, a focused table and a picked relation',
() {
const blue = Color(0xFF2196F3);
const orange = Color(0xFFFF9800);
_paint(ErdRelationPainter(
routes: routes, color: blue, highlight: orange, focus: null));
_paint(ErdRelationPainter(
routes: routes, color: blue, highlight: orange, focus: 'b'));
_paint(ErdRelationPainter(
routes: routes,
color: blue,
highlight: orange,
focus: null,
picked: routes.first.relation));
});

test('repeated paints of the same routes keep working', () {
final painter = ErdRelationPainter(
routes: routes,
color: Colors.grey,
highlight: Colors.red,
focus: 'a',
);
for (var i = 0; i < 5; i++) {
_paint(painter);
}
});

test('more distinct colours than the label cache holds still paint', () {
for (var i = 0; i < 40; i++) {
_paint(ErdRelationPainter(
routes: routes,
color: Color(0xFF000000 + i * 0x010203),
highlight: Color(0xFF800000 + i),
focus: i.isEven ? 'a' : null,
));
}
});

test('a route with fewer than two points is skipped', () {
_paint(ErdRelationPainter(
routes: [
ErdRoute(routes.first.relation, const [Offset(1, 1)]),
...routes,
],
color: Colors.grey,
highlight: Colors.red,
focus: null,
));
});

test('repaints only when something it draws changed', () {
ErdRelationPainter make({String? focus, List<ErdRoute>? r}) =>
ErdRelationPainter(
routes: r ?? routes,
color: Colors.grey,
highlight: Colors.red,
focus: focus,
);
expect(make().shouldRepaint(make()), isFalse);
expect(make(focus: 'a').shouldRepaint(make()), isTrue);
expect(make(r: [...routes]).shouldRepaint(make()), isTrue);
});
});
}
Loading