diff --git a/example/lib/app.dart b/example/lib/app.dart index 843665f..f595d0a 100644 --- a/example/lib/app.dart +++ b/example/lib/app.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:infinite_lazy_2d_grid/core/controller.dart'; import 'package:infinite_lazy_2d_grid/infinite_lazy_2d_grid.dart'; class App extends StatefulWidget { @@ -9,20 +10,21 @@ class App extends StatefulWidget { } class _AppState extends State { + final CanvasController controller = CanvasController(); + + @override + void initState() { + super.initState(); + + controller.addChild(const Offset(100, 100), () => const InfoContainer(color: Colors.red)); + controller.addChild(const Offset(200, 200), () => const InfoContainer(color: Colors.green)); + controller.addChild(const Offset(300, 300), () => const InfoContainer(color: Colors.blue)); + } + @override Widget build(BuildContext context) { return Scaffold( - body: SizedBox.expand( - child: CanvasView( - ssPositions: [Offset(100, 100), Offset(200, 200), Offset(300, 300)], - canvasBackground: SingleColorBackround(Colors.white), - children: [ - InfoContainer(color: Colors.red), - InfoContainer(color: Colors.green), - InfoContainer(color: Colors.blue), - ], - ), - ), + body: CanvasView(controller: controller, canvasBackground: SingleColorBackround(Colors.white)), ); } } diff --git a/lib/core/controller.dart b/lib/core/controller.dart new file mode 100644 index 0000000..47b871c --- /dev/null +++ b/lib/core/controller.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:infinite_lazy_2d_grid/utils/conversions.dart'; + +// ------------------------------ Public Types ------------------------------- + +typedef WidgetBuilder = Widget Function(); + +// ------------------------------ Private Types ------------------------------ + +// some simple exceptions +// ignore: non_constant_identifier_names +final _ChildNotFoundException = Exception('Child with the given ID does not exist'); + +class _CanvasItem { + Offset gsPosition; + final WidgetBuilder builder; + + _CanvasItem({required this.gsPosition, required this.builder}); +} + +// ------------------------------ Controller ------------------------------ + +/// Every canvas view needs one and this handles the positioning logic +class CanvasController with ChangeNotifier { + int _nextId = 0; // surely we won't run out of IDs, Clueless + Offset _gsTopLeftOffset = Offset.zero; + final Map _children = {}; // int for IDs + + // -------------------- public functions -------------------- + + // -------- child manip -------- + + int addChild(Offset position, WidgetBuilder builder) { + _children[_nextId] = _CanvasItem(gsPosition: position, builder: builder); + return _nextId++; + } + + void removeChild(int id) { + _children.remove(id); + } + + int updatePosition(int id, Offset newPosition) { + if (_children.containsKey(id)) { + _children[id]!.gsPosition = newPosition; + return id; + } else { + throw _ChildNotFoundException; + } + } + + Offset getPosition(int id) { + if (_children.containsKey(id)) { + return _children[id]!.gsPosition; + } else { + throw _ChildNotFoundException; + } + } + + // -------- gesture handling -------- + void handleGesture(ScaleUpdateDetails details) { + // handle gestures like pinch to zoom, pan, etc. + + // convention is that if I drag from right to left, dx is negative + // for top to bottom, dy is postive ( so usual display conventions and final vector postion - initial vector position ) + final delta = details.focalPointDelta; + + _gsTopLeftOffset -= delta; + + notifyListeners(); + } + + List<(Offset, Widget)> widgetsWithScreenPositions() { + return _children.values.map((item) { + final ssPosition = gsToSs(item.gsPosition, _gsTopLeftOffset); + return (ssPosition, item.builder()); + }).toList(); + } +} diff --git a/lib/core/render.dart b/lib/core/render.dart index 623a56f..9169a99 100644 --- a/lib/core/render.dart +++ b/lib/core/render.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:infinite_lazy_2d_grid/core/controller.dart'; import 'background.dart'; @@ -10,19 +11,30 @@ import 'background.dart'; /// An infinite canvas that places all the children at the specified positions. class CanvasView extends StatelessWidget { - final List ssPositions; // screen space positions; - final List children; // children, in the same order as positions final CanvasBackground canvasBackground; + final CanvasController controller; - const CanvasView({required this.ssPositions, required this.children, required this.canvasBackground, super.key}) - : assert(children.length == ssPositions.length, 'Children and positions must have the same length'); + const CanvasView({required this.controller, required this.canvasBackground, super.key}); @override Widget build(BuildContext context) { return Container( color: canvasBackground.bgColor, child: GestureDetector( - child: _CanvasRenderObject(canvasBackground: canvasBackground, ssPositions: ssPositions, children: children), + onScaleUpdate: controller.handleGesture, + child: ListenableBuilder( + listenable: controller, + builder: (_, _) { + final childrenWithPositions = controller.widgetsWithScreenPositions(); + final ssPositions = childrenWithPositions.map((e) => e.$1).toList(); + final children = childrenWithPositions.map((e) => e.$2).toList(); + return _CanvasRenderObject( + canvasBackground: canvasBackground, + ssPositions: ssPositions, + children: children, + ); + }, + ), ), ); } @@ -46,7 +58,9 @@ class _CanvasRenderObject extends MultiChildRenderObjectWidget { @override void updateRenderObject(BuildContext context, _CanvasRenderBox renderObject) { - // static for now + renderObject + ..ssPositions = ssPositions + ..canvasBackground = canvasBackground; } } @@ -57,10 +71,12 @@ class _CanvasRenderBox extends RenderBox with ContainerRenderObjectMixin, RenderBoxContainerDefaultsMixin { - final CanvasBackground canvasBackground; - final List ssPositions; + CanvasBackground _canvasBackground; + List _ssPositions; - _CanvasRenderBox({required this.ssPositions, required this.canvasBackground}); + _CanvasRenderBox({required ssPositions, required canvasBackground}) + : _ssPositions = ssPositions, + _canvasBackground = canvasBackground; @override void setupParentData(RenderBox child) { @@ -69,6 +85,20 @@ class _CanvasRenderBox extends RenderBox } } + set ssPositions(List ssPositions) { + if (_ssPositions != ssPositions) { + _ssPositions = ssPositions; + markNeedsPaint(); + } + } + + set canvasBackground(CanvasBackground canvasBackground) { + if (_canvasBackground != canvasBackground) { + _canvasBackground = canvasBackground; + markNeedsPaint(); + } + } + @override void performLayout() { size = constraints.biggest; // expand as much as possible for the parent @@ -89,12 +119,12 @@ class _CanvasRenderBox extends RenderBox final canvas = context.canvas; // use the canvas background painter, pass it the canvas and that should handle drawing the background - canvasBackground.paint(canvas, size); + _canvasBackground.paint(canvas, size); RenderBox? child = firstChild; for (int idx = 0; child != null; idx++) { final _WidgetParentData parentData = child.parentData! as _WidgetParentData; - context.paintChild(child, ssPositions[idx]); + context.paintChild(child, _ssPositions[idx]); child = parentData.nextSibling; } } @@ -109,11 +139,11 @@ class _CanvasRenderBox extends RenderBox // Check children in reverse order (last painted = first hit) RenderBox? child = lastChild; - for (int idx = ssPositions.length - 1; child != null; idx--) { + for (int idx = _ssPositions.length - 1; child != null; idx--) { final _WidgetParentData parentData = child.parentData! as _WidgetParentData; // Transform the hit position relative to the child's position - final childPosition = ssHitPosition - ssPositions[idx]; + final childPosition = ssHitPosition - _ssPositions[idx]; if (child.hitTest(BoxHitTestResult.wrap(result), position: childPosition)) { return true; diff --git a/lib/utils/conversions.dart b/lib/utils/conversions.dart new file mode 100644 index 0000000..840015e --- /dev/null +++ b/lib/utils/conversions.dart @@ -0,0 +1,11 @@ +import 'dart:ui'; + +/// Convert grid space coordinates to screen space coordinates +Offset gsToSs(Offset gsPosition, Offset gsTopLeft) { + return gsPosition - gsTopLeft; +} + +/// Convert screen space coordinates to grid space coordinates +Offset ssToGs(Offset ssPosition, Offset gsTopLeft) { + return ssPosition + gsTopLeft; +} diff --git a/lib/utils/offset_extensions.dart b/lib/utils/offset_extensions.dart index b0b1e69..7f012cd 100644 --- a/lib/utils/offset_extensions.dart +++ b/lib/utils/offset_extensions.dart @@ -6,4 +6,8 @@ extension OffsetCartesionOps on Offset { final dy = this.dy - other.dy; return dx * dx + dy * dy; } + + Offset operator -(Offset other) { + return Offset(dx - other.dx, dy - other.dy); + } }