couple view and controller + offset working

This commit is contained in:
2025-07-02 01:21:45 +00:00
parent a7ea05c00d
commit 0e4125a583
5 changed files with 149 additions and 24 deletions
+13 -11
View File
@@ -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<App> {
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)),
);
}
}
+78
View File
@@ -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<int, _CanvasItem> _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();
}
}
+43 -13
View File
@@ -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<Offset> ssPositions; // screen space positions;
final List<Widget> 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<RenderBox, _WidgetParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, _WidgetParentData> {
final CanvasBackground canvasBackground;
final List<Offset> ssPositions;
CanvasBackground _canvasBackground;
List<Offset> _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<Offset> 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;
+11
View File
@@ -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;
}
+4
View File
@@ -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);
}
}