257 lines
8.6 KiB
Dart
257 lines
8.6 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:infinite_lazy_2d_grid/core/controller/controller.dart';
|
|
import 'package:infinite_lazy_2d_grid/utils/offset_extensions.dart';
|
|
import 'package:infinite_lazy_2d_grid/utils/styles.dart';
|
|
|
|
import 'background.dart';
|
|
|
|
/// An infinite canvas that places all the children at the specified positions.
|
|
class CanvasView extends StatefulWidget {
|
|
final CanvasBackground canvasBackground;
|
|
final CanvasController controller;
|
|
|
|
const CanvasView({required this.controller, required this.canvasBackground, super.key});
|
|
|
|
@override
|
|
State<CanvasView> createState() => _CanvasViewState();
|
|
}
|
|
|
|
class _CanvasViewState extends State<CanvasView> with TickerProviderStateMixin<CanvasView> {
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
widget.controller.setTickerProvider(this);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
widget.controller.dispose(); // for the change notifier
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GestureDetector(
|
|
behavior: HitTestBehavior.translucent,
|
|
onScaleUpdate: widget.controller.onScaleUpdate,
|
|
onScaleStart: widget.controller.onScaleStart,
|
|
child: ListenableBuilder(
|
|
listenable: widget.controller,
|
|
builder: (_, _) {
|
|
final childrenWithPositions = widget.controller.widgetsWithScreenPositions(context);
|
|
final ssPositions = childrenWithPositions.map((e) => e.ssPosition).toList();
|
|
final childrenIds = childrenWithPositions.map((e) => e.id).toList();
|
|
final children = childrenWithPositions.map((e) => e.child).toList();
|
|
final canvas = _CanvasRenderObject(
|
|
childrenIds: childrenIds,
|
|
canvasBackground: widget.canvasBackground,
|
|
ssPositions: ssPositions,
|
|
scale: widget.controller.scale,
|
|
onCanvasSizeChange: widget.controller.onCanvasSizeChange,
|
|
onChildSizeChange: widget.controller.onChildSizeChange,
|
|
children: children,
|
|
);
|
|
|
|
if (widget.controller.debug) {
|
|
return Stack(
|
|
children: [
|
|
canvas,
|
|
Positioned(
|
|
top: 16,
|
|
left: 16,
|
|
child: Text(
|
|
'Offset: ${widget.controller.offset.coord()}\nScale: ${widget.controller.scale.toStringAsFixed(1)}',
|
|
style: monospaceStyle,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
} else {
|
|
return canvas;
|
|
}
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A combined widget for all the render object of the children + background.
|
|
/// Everything is in screen space here
|
|
class _CanvasRenderObject extends MultiChildRenderObjectWidget {
|
|
final List<int> childrenIds;
|
|
final List<Offset> ssPositions;
|
|
final double scale;
|
|
final CanvasBackground canvasBackground;
|
|
final Function onCanvasSizeChange;
|
|
final Function onChildSizeChange;
|
|
|
|
const _CanvasRenderObject({
|
|
required this.childrenIds,
|
|
required this.ssPositions,
|
|
required this.scale,
|
|
required this.canvasBackground,
|
|
required this.onCanvasSizeChange,
|
|
required this.onChildSizeChange,
|
|
required super.children, // children go to the MultiChildRenderObjectWidget
|
|
}) : assert(ssPositions.length == children.length, 'Children and positions must have the same length'),
|
|
assert(scale != 0);
|
|
|
|
@override
|
|
RenderObject createRenderObject(BuildContext context) {
|
|
return _CanvasRenderBox(
|
|
childrenIds: childrenIds,
|
|
ssPositions: ssPositions,
|
|
scale: scale,
|
|
canvasBackground: canvasBackground,
|
|
onCanvasSizeChange: onCanvasSizeChange,
|
|
onChildSizeChange: onChildSizeChange,
|
|
);
|
|
}
|
|
|
|
@override
|
|
void updateRenderObject(BuildContext context, _CanvasRenderBox renderObject) {
|
|
renderObject
|
|
..childrenIds = childrenIds
|
|
..ssPositions = ssPositions
|
|
..canvasBackground = canvasBackground
|
|
..scale = scale;
|
|
}
|
|
}
|
|
|
|
class _CanvasWidgetParentData extends ContainerBoxParentData<RenderBox> {
|
|
// there is already an "offset" defined in BoxParentdata that is exactly what I want
|
|
late int id;
|
|
late double scale; // scale is same for all rn but this makes it trivial to expand to children with diff scales
|
|
}
|
|
|
|
class _CanvasRenderBox extends RenderBox
|
|
with
|
|
ContainerRenderObjectMixin<RenderBox, _CanvasWidgetParentData>,
|
|
RenderBoxContainerDefaultsMixin<RenderBox, _CanvasWidgetParentData> {
|
|
CanvasBackground _canvasBackground;
|
|
List<int> _childrenIds;
|
|
List<Offset> _ssPositions;
|
|
double _scale;
|
|
Function onCanvasSizeChange;
|
|
Function onChildSizeChange;
|
|
|
|
_CanvasRenderBox({
|
|
required childrenIds,
|
|
required ssPositions,
|
|
required scale,
|
|
required canvasBackground,
|
|
required this.onCanvasSizeChange,
|
|
required this.onChildSizeChange,
|
|
}) : assert(childrenIds.length == ssPositions.length),
|
|
_childrenIds = childrenIds,
|
|
_ssPositions = ssPositions,
|
|
_scale = scale,
|
|
_canvasBackground = canvasBackground;
|
|
|
|
@override
|
|
void setupParentData(RenderBox child) {
|
|
if (child.parentData is! _CanvasWidgetParentData) {
|
|
child.parentData = _CanvasWidgetParentData();
|
|
}
|
|
}
|
|
|
|
set ssPositions(List<Offset> ssPositions) {
|
|
// at this point childCount may not be equal to ssPositions.length
|
|
// but since we are only marking for layout this is fine
|
|
// at the actual performLayout this will be asserted
|
|
if (_ssPositions != ssPositions) {
|
|
_ssPositions = ssPositions;
|
|
markNeedsLayout();
|
|
}
|
|
}
|
|
|
|
set childrenIds(List<int> childrenIds) {
|
|
if (_childrenIds != childrenIds) {
|
|
_childrenIds = childrenIds;
|
|
}
|
|
}
|
|
|
|
set scale(double scale) {
|
|
if (_scale != scale) {
|
|
_scale = scale;
|
|
markNeedsLayout();
|
|
}
|
|
}
|
|
|
|
set canvasBackground(CanvasBackground canvasBackground) {
|
|
if (_canvasBackground != canvasBackground) {
|
|
_canvasBackground = canvasBackground;
|
|
markNeedsPaint();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void performLayout() {
|
|
assert(childCount == _ssPositions.length);
|
|
size = constraints.biggest; // expand as much as possible for the parent
|
|
onCanvasSizeChange(size); // notify the controller about the size
|
|
|
|
RenderBox? child = firstChild;
|
|
|
|
int index = 0;
|
|
while (child != null) {
|
|
final _CanvasWidgetParentData childParentData = child.parentData! as _CanvasWidgetParentData;
|
|
childParentData.offset = _ssPositions[index];
|
|
childParentData.id = _childrenIds[index];
|
|
childParentData.scale = _scale;
|
|
index++;
|
|
// loosen so like a stack can take it's own size inside parent
|
|
child.layout(constraints.loosen(), parentUsesSize: true);
|
|
|
|
// notify the controller about the size of the child
|
|
onChildSizeChange(childParentData.id, child.size);
|
|
|
|
child = childParentData.nextSibling;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void paint(PaintingContext context, Offset canvasStartOffset) {
|
|
assert(childCount == _ssPositions.length);
|
|
// use the canvas background painter, pass it the canvas and that should handle drawing the background
|
|
_canvasBackground.paint(context.canvas, size, _scale);
|
|
|
|
// though using ssPositionns here directly worked for me but docs using the parentData
|
|
// to get this info is the convention as child can be reordered ( though this will always
|
|
// change the offset as well so should work for me ) and this is the flutter way of implementation
|
|
// on most other stuff ( single source of truth for paint & hit test etc )
|
|
RenderBox? child = firstChild;
|
|
while (child != null) {
|
|
final _CanvasWidgetParentData childParentData = child.parentData! as _CanvasWidgetParentData;
|
|
|
|
context.canvas.save();
|
|
// relying purely on canvas manips here
|
|
final drawAt = canvasStartOffset + childParentData.offset;
|
|
context.canvas.translate(drawAt.dx, drawAt.dy);
|
|
context.canvas.scale(childParentData.scale, childParentData.scale);
|
|
context.paintChild(child, Offset.zero); // pain at 0 as already translated
|
|
|
|
context.canvas.restore();
|
|
child = childParentData.nextSibling;
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
|
|
RenderBox? child = lastChild;
|
|
while (child != null) {
|
|
// need to get a coordinate in child space
|
|
final childParentData = child.parentData as _CanvasWidgetParentData;
|
|
final Offset childSpacePosition =
|
|
(position - childParentData.offset /* <- distance in screen space */ ) / childParentData.scale;
|
|
// divide by scale so if screen space is 2x, it's only x in child space
|
|
if (child.hitTest(result, position: childSpacePosition)) {
|
|
return true;
|
|
}
|
|
child = childParentData.previousSibling;
|
|
}
|
|
return false;
|
|
}
|
|
}
|