From a7ea05c00d78b655807b52494b8d48362ee70a82 Mon Sep 17 00:00:00 2001 From: ruinivist Date: Tue, 1 Jul 2025 03:49:45 +0000 Subject: [PATCH] simple view layer: static screenspace widgets with working hittest --- lib/core/background.dart | 6 +- lib/core/render.dart | 163 ++++++++++++++------------------------- 2 files changed, 59 insertions(+), 110 deletions(-) diff --git a/lib/core/background.dart b/lib/core/background.dart index d293a60..c57ce8f 100644 --- a/lib/core/background.dart +++ b/lib/core/background.dart @@ -7,21 +7,21 @@ abstract class CanvasBackground { /// draw the backround on this context. Implement this to have /// different kinds of backgrounds - void paint(Canvas canvas, Offset offset, double scale, Size canvasSize); + void paint(Canvas canvas, Size canvasSize); } class NoBackground extends CanvasBackground { const NoBackground(); @override - void paint(Canvas canvas, Offset offset, double scale, Size canvasSize) {} + void paint(Canvas canvas, Size canvasSize) {} } class SingleColorBackround extends CanvasBackground { const SingleColorBackround(Color backgroundColor) : super(bgColor: backgroundColor); @override - void paint(Canvas canvas, Offset offset, double scale, Size canvasSize) { + void paint(Canvas canvas, Size canvasSize) { final paint = Paint() ..color = bgColor ..style = PaintingStyle.fill; diff --git a/lib/core/render.dart b/lib/core/render.dart index a9c3872..623a56f 100644 --- a/lib/core/render.dart +++ b/lib/core/render.dart @@ -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 positions; - final List children; + final List ssPositions; // screen space positions; + final List 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 positions; + final List 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 {} +/// Dummy empty parent data for each child +class _WidgetParentData extends ContainerBoxParentData {} class _CanvasRenderBox extends RenderBox with - ContainerRenderObjectMixin, - RenderBoxContainerDefaultsMixin { - CanvasBackground canvasBackground; - Offset _offset; - double _scale; - List _positions; + ContainerRenderObjectMixin, + RenderBoxContainerDefaultsMixin { + final CanvasBackground canvasBackground; + final List 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 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; } }