simple view layer: static screenspace widgets with working hittest

This commit is contained in:
2025-07-01 03:49:45 +00:00
parent 4535894dc4
commit a7ea05c00d
2 changed files with 59 additions and 110 deletions
+56 -107
View File
@@ -3,126 +3,84 @@ import 'package:flutter/rendering.dart';
import 'background.dart';
/**
* Working of some of the flutter code used here:
*/
/// An infinite canvas that places all the children at the specified positions.
class CanvasView extends StatelessWidget {
final Offset offset;
final List<Offset> positions;
final List<Widget> children;
final List<Offset> ssPositions; // screen space positions;
final List<Widget> children; // children, in the same order as positions
final CanvasBackground canvasBackground;
final double scale;
final void Function(ScaleUpdateDetails details) handleScaleUpdate;
final void Function(ScaleStartDetails details) handleScaleStart;
const CanvasView({
required this.children,
required this.positions,
required this.offset,
required this.scale,
required this.handleScaleUpdate,
required this.handleScaleStart,
required this.canvasBackground,
super.key,
}) : assert(children.length == positions.length, 'Children and positions must have the same length');
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');
@override
Widget build(BuildContext context) {
return Container(
color: canvasBackground.bgColor,
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onScaleUpdate: handleScaleUpdate,
onScaleStart: handleScaleStart,
child: _CanvasRenderObject(
offset: offset,
scale: scale,
positions: positions,
canvasBackground: canvasBackground,
children: children,
),
child: _CanvasRenderObject(canvasBackground: canvasBackground, ssPositions: ssPositions, children: children),
),
);
}
}
/// A combined widget for all the render object of the children + background.
class _CanvasRenderObject extends MultiChildRenderObjectWidget {
final Offset offset;
final double scale;
final List<Offset> positions;
final List<Offset> ssPositions;
final CanvasBackground canvasBackground;
const _CanvasRenderObject({
required this.offset,
required this.scale,
required this.positions,
required super.children,
required this.ssPositions,
required super.children, // children go to the MultiChildRenderObjectWidget
required this.canvasBackground,
});
}) : assert(ssPositions.length == children.length, 'Children and positions must have the same length');
@override
RenderObject createRenderObject(BuildContext context) {
return _CanvasRenderBox(offset: offset, scale: scale, positions: positions, canvasBackground: canvasBackground);
return _CanvasRenderBox(ssPositions: ssPositions, canvasBackground: canvasBackground);
}
@override
void updateRenderObject(BuildContext context, _CanvasRenderBox renderObject) {
renderObject
..offset = offset
..positions = positions
..scale = scale;
// static for now
}
}
class _CanvasParentData extends ContainerBoxParentData<RenderBox> {}
/// Dummy empty parent data for each child
class _WidgetParentData extends ContainerBoxParentData<RenderBox> {}
class _CanvasRenderBox extends RenderBox
with
ContainerRenderObjectMixin<RenderBox, _CanvasParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, _CanvasParentData> {
CanvasBackground canvasBackground;
Offset _offset;
double _scale;
List<Offset> _positions;
ContainerRenderObjectMixin<RenderBox, _WidgetParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, _WidgetParentData> {
final CanvasBackground canvasBackground;
final List<Offset> ssPositions;
_CanvasRenderBox({required positions, required offset, required scale, required this.canvasBackground})
: _offset = offset,
_positions = positions,
_scale = scale;
set offset(Offset value) {
if (_offset == value) return;
_offset = value;
markNeedsPaint();
}
set positions(List<Offset> value) {
if (_positions == value) return;
_positions = value;
markNeedsLayout();
}
set scale(double value) {
if (_scale == value) return;
_scale = value;
markNeedsPaint();
}
_CanvasRenderBox({required this.ssPositions, required this.canvasBackground});
@override
void setupParentData(RenderBox child) {
if (child.parentData is! _CanvasParentData) {
child.parentData = _CanvasParentData();
if (child.parentData is! _WidgetParentData) {
child.parentData = _WidgetParentData();
}
}
@override
void performLayout() {
size = constraints.biggest;
size = constraints.biggest; // expand as much as possible for the parent
RenderBox? child = firstChild;
while (child != null) {
final _CanvasParentData childParentData = child.parentData! as _CanvasParentData;
child.layout(constraints.loosen(), parentUsesSize: false); // TODO: parentUsesSize should be false, but idk
child = childParentData.nextSibling;
final _WidgetParentData parentData = child.parentData! as _WidgetParentData;
// 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 = parentData.nextSibling;
}
}
@@ -130,51 +88,42 @@ class _CanvasRenderBox extends RenderBox
void paint(PaintingContext context, Offset canvasStartOffset) {
final canvas = context.canvas;
canvasBackground.paint(canvas, _offset, _scale, size);
// use the canvas background painter, pass it the canvas and that should handle drawing the background
canvasBackground.paint(canvas, size);
RenderBox? child = firstChild;
for (int idx = 0; child != null; idx++) {
final _CanvasParentData childParentData = child.parentData! as _CanvasParentData;
final paintAt = _positions[idx] * _scale - _offset;
context.paintChild(child, paintAt);
child = childParentData.nextSibling;
final _WidgetParentData parentData = child.parentData! as _WidgetParentData;
context.paintChild(child, ssPositions[idx]);
child = parentData.nextSibling;
}
}
// do i need to override hitTest? yes I do
@override
bool hitTest(BoxHitTestResult result, {required Offset position}) {
// args are propagating so this may happen, just ignore it
//is it a double render bug?
if (childCount != _positions.length) {
final ssHitPosition = position;
if (!size.contains(ssHitPosition)) {
return false;
}
// TODO: this is bit slow, so the tap is shown slightly after the actual tap
bool hitChild = false;
// Check children in reverse order (last painted = first hit)
RenderBox? child = lastChild;
for (int i = childCount - 1; i >= 0; i--) {
if (child != null) {
final _CanvasParentData childParentData = child.parentData! as _CanvasParentData;
// child is at _positions[i] in grid space
final ssChildPosition = _positions[i] * _scale - _offset; // child position in screen space
final positionInChildSpace = position - ssChildPosition; // position in child space
if ((child.size * _scale).contains(positionInChildSpace)) {
final isHit = result.addWithPaintOffset(
offset: null, // i convert it directly to child space
position: positionInChildSpace,
hitTest: (BoxHitTestResult result, Offset transformed) {
return child!.hitTest(result, position: transformed);
},
);
if (isHit) {
hitChild = true;
break;
}
}
child = childParentData.previousSibling;
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];
if (child.hitTest(BoxHitTestResult.wrap(result), position: childPosition)) {
return true;
}
child = parentData.previousSibling;
}
return hitChild;
// If no child was hit, the canvas itself handles the hit
result.add(BoxHitTestEntry(this, ssHitPosition));
return true;
}
}