import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:infinite_lazy_2d_grid/core/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 StatelessWidget { final CanvasBackground canvasBackground; final CanvasController controller; const CanvasView({required this.controller, required this.canvasBackground, super.key}); @override Widget build(BuildContext context) { return Container( color: canvasBackground.bgColor, child: GestureDetector( behavior: HitTestBehavior.translucent, onScaleUpdate: controller.onScaleUpdate, onScaleStart: controller.onScaleStart, child: ListenableBuilder( listenable: controller, builder: (_, _) { final childrenWithPositions = controller.widgetsWithScreenPositions(); final ssPositions = childrenWithPositions.map((e) => e.ssPosition).toList(); final children = childrenWithPositions.map((e) => e.child).toList(); final canvas = _CanvasRenderObject( canvasBackground: canvasBackground, ssPositions: ssPositions, scale: controller.scale, onCanvasSizeChange: controller.onCanvasSizeChange, children: children, ); if (controller.debug) { return Stack( children: [ canvas, Positioned( top: 16, left: 16, child: Text( 'Offset: ${controller.offset.coord()}\nScale: ${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 ssPositions; final double scale; final CanvasBackground canvasBackground; final Function onCanvasSizeChange; const _CanvasRenderObject({ required this.ssPositions, required this.scale, required this.canvasBackground, required this.onCanvasSizeChange, 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( ssPositions: ssPositions, scale: scale, canvasBackground: canvasBackground, onCanvasSizeChange: onCanvasSizeChange, ); } @override void updateRenderObject(BuildContext context, _CanvasRenderBox renderObject) { renderObject ..ssPositions = ssPositions ..canvasBackground = canvasBackground ..scale = scale; } } class _CanvasWidgetParentData extends ContainerBoxParentData { // there is already an "offset" defined in BoxParentdata that is exactly what I want 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, RenderBoxContainerDefaultsMixin { CanvasBackground _canvasBackground; List _ssPositions; double _scale; Function onCanvasSizeChange; _CanvasRenderBox({required ssPositions, required scale, required canvasBackground, required this.onCanvasSizeChange}) : _ssPositions = ssPositions, _scale = scale, _canvasBackground = canvasBackground; @override void setupParentData(RenderBox child) { if (child.parentData is! _CanvasWidgetParentData) { child.parentData = _CanvasWidgetParentData(); } } set ssPositions(List ssPositions) { assert(ssPositions.length == childCount); if (_ssPositions != ssPositions) { _ssPositions = ssPositions; markNeedsLayout(); } } 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.scale = _scale; // loosen so like a stack can take it's own size inside parent // like a stack parent is not using size and is always expanded hence the second arg child.layout(constraints.loosen(), parentUsesSize: false); 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; } }