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
+3 -3
View File
@@ -7,21 +7,21 @@ abstract class CanvasBackground {
/// draw the backround on this context. Implement this to have /// draw the backround on this context. Implement this to have
/// different kinds of backgrounds /// different kinds of backgrounds
void paint(Canvas canvas, Offset offset, double scale, Size canvasSize); void paint(Canvas canvas, Size canvasSize);
} }
class NoBackground extends CanvasBackground { class NoBackground extends CanvasBackground {
const NoBackground(); const NoBackground();
@override @override
void paint(Canvas canvas, Offset offset, double scale, Size canvasSize) {} void paint(Canvas canvas, Size canvasSize) {}
} }
class SingleColorBackround extends CanvasBackground { class SingleColorBackround extends CanvasBackground {
const SingleColorBackround(Color backgroundColor) : super(bgColor: backgroundColor); const SingleColorBackround(Color backgroundColor) : super(bgColor: backgroundColor);
@override @override
void paint(Canvas canvas, Offset offset, double scale, Size canvasSize) { void paint(Canvas canvas, Size canvasSize) {
final paint = Paint() final paint = Paint()
..color = bgColor ..color = bgColor
..style = PaintingStyle.fill; ..style = PaintingStyle.fill;
+56 -107
View File
@@ -3,126 +3,84 @@ import 'package:flutter/rendering.dart';
import 'background.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. /// An infinite canvas that places all the children at the specified positions.
class CanvasView extends StatelessWidget { class CanvasView extends StatelessWidget {
final Offset offset; final List<Offset> ssPositions; // screen space positions;
final List<Offset> positions; final List<Widget> children; // children, in the same order as positions
final List<Widget> children;
final CanvasBackground canvasBackground; final CanvasBackground canvasBackground;
final double scale;
final void Function(ScaleUpdateDetails details) handleScaleUpdate;
final void Function(ScaleStartDetails details) handleScaleStart;
const CanvasView({ const CanvasView({required this.ssPositions, required this.children, required this.canvasBackground, super.key})
required this.children, : assert(children.length == ssPositions.length, 'Children and positions must have the same length');
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');
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
color: canvasBackground.bgColor, color: canvasBackground.bgColor,
child: GestureDetector( child: GestureDetector(
behavior: HitTestBehavior.translucent, child: _CanvasRenderObject(canvasBackground: canvasBackground, ssPositions: ssPositions, children: children),
onScaleUpdate: handleScaleUpdate,
onScaleStart: handleScaleStart,
child: _CanvasRenderObject(
offset: offset,
scale: scale,
positions: positions,
canvasBackground: canvasBackground,
children: children,
),
), ),
); );
} }
} }
/// A combined widget for all the render object of the children + background.
class _CanvasRenderObject extends MultiChildRenderObjectWidget { class _CanvasRenderObject extends MultiChildRenderObjectWidget {
final Offset offset; final List<Offset> ssPositions;
final double scale;
final List<Offset> positions;
final CanvasBackground canvasBackground; final CanvasBackground canvasBackground;
const _CanvasRenderObject({ const _CanvasRenderObject({
required this.offset, required this.ssPositions,
required this.scale, required super.children, // children go to the MultiChildRenderObjectWidget
required this.positions,
required super.children,
required this.canvasBackground, required this.canvasBackground,
}); }) : assert(ssPositions.length == children.length, 'Children and positions must have the same length');
@override @override
RenderObject createRenderObject(BuildContext context) { RenderObject createRenderObject(BuildContext context) {
return _CanvasRenderBox(offset: offset, scale: scale, positions: positions, canvasBackground: canvasBackground); return _CanvasRenderBox(ssPositions: ssPositions, canvasBackground: canvasBackground);
} }
@override @override
void updateRenderObject(BuildContext context, _CanvasRenderBox renderObject) { void updateRenderObject(BuildContext context, _CanvasRenderBox renderObject) {
renderObject // static for now
..offset = offset
..positions = positions
..scale = scale;
} }
} }
class _CanvasParentData extends ContainerBoxParentData<RenderBox> {} /// Dummy empty parent data for each child
class _WidgetParentData extends ContainerBoxParentData<RenderBox> {}
class _CanvasRenderBox extends RenderBox class _CanvasRenderBox extends RenderBox
with with
ContainerRenderObjectMixin<RenderBox, _CanvasParentData>, ContainerRenderObjectMixin<RenderBox, _WidgetParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, _CanvasParentData> { RenderBoxContainerDefaultsMixin<RenderBox, _WidgetParentData> {
CanvasBackground canvasBackground; final CanvasBackground canvasBackground;
Offset _offset; final List<Offset> ssPositions;
double _scale;
List<Offset> _positions;
_CanvasRenderBox({required positions, required offset, required scale, required this.canvasBackground}) _CanvasRenderBox({required this.ssPositions, 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();
}
@override @override
void setupParentData(RenderBox child) { void setupParentData(RenderBox child) {
if (child.parentData is! _CanvasParentData) { if (child.parentData is! _WidgetParentData) {
child.parentData = _CanvasParentData(); child.parentData = _WidgetParentData();
} }
} }
@override @override
void performLayout() { void performLayout() {
size = constraints.biggest; size = constraints.biggest; // expand as much as possible for the parent
RenderBox? child = firstChild; RenderBox? child = firstChild;
while (child != null) { while (child != null) {
final _CanvasParentData childParentData = child.parentData! as _CanvasParentData; final _WidgetParentData parentData = child.parentData! as _WidgetParentData;
child.layout(constraints.loosen(), parentUsesSize: false); // TODO: parentUsesSize should be false, but idk // loosen so like a stack can take it's own size inside parent
child = childParentData.nextSibling; // 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) { void paint(PaintingContext context, Offset canvasStartOffset) {
final canvas = context.canvas; 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; RenderBox? child = firstChild;
for (int idx = 0; child != null; idx++) { for (int idx = 0; child != null; idx++) {
final _CanvasParentData childParentData = child.parentData! as _CanvasParentData; final _WidgetParentData parentData = child.parentData! as _WidgetParentData;
final paintAt = _positions[idx] * _scale - _offset; context.paintChild(child, ssPositions[idx]);
context.paintChild(child, paintAt); child = parentData.nextSibling;
child = childParentData.nextSibling;
} }
} }
// do i need to override hitTest? yes I do
@override @override
bool hitTest(BoxHitTestResult result, {required Offset position}) { bool hitTest(BoxHitTestResult result, {required Offset position}) {
// args are propagating so this may happen, just ignore it final ssHitPosition = position;
//is it a double render bug?
if (childCount != _positions.length) { if (!size.contains(ssHitPosition)) {
return false; 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; RenderBox? child = lastChild;
for (int i = childCount - 1; i >= 0; i--) { for (int idx = ssPositions.length - 1; child != null; idx--) {
if (child != null) { final _WidgetParentData parentData = child.parentData! as _WidgetParentData;
final _CanvasParentData childParentData = child.parentData! as _CanvasParentData;
// child is at _positions[i] in grid space // Transform the hit position relative to the child's position
final ssChildPosition = _positions[i] * _scale - _offset; // child position in screen space final childPosition = ssHitPosition - ssPositions[idx];
final positionInChildSpace = position - ssChildPosition; // position in child space
if ((child.size * _scale).contains(positionInChildSpace)) { if (child.hitTest(BoxHitTestResult.wrap(result), position: childPosition)) {
final isHit = result.addWithPaintOffset( return true;
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;
} }
child = parentData.previousSibling;
} }
return hitChild; // If no child was hit, the canvas itself handles the hit
result.add(BoxHitTestEntry(this, ssHitPosition));
return true;
} }
} }