// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// This example shows how to use process input events in the underlying render
// tree.

import 'package:flutter/material.dart'; // Imported just for its color palette.
import 'package:flutter/rendering.dart';

// Material design colors. :p
List<Color> _kColors = <Color>[
  Colors.teal,
  Colors.amber,
  Colors.purple,
  Colors.lightBlue,
  Colors.deepPurple,
  Colors.lime,
];

/// A simple model object for a dot that reacts to pointer pressure.
class Dot {
  Dot({ required Color color }) : _paint = Paint()..color = color;

  final Paint _paint;
  Offset position = Offset.zero;
  double radius = 0.0;

  void update(PointerEvent event) {
    position = event.position;
    radius = 5 + (95 * event.pressure);
  }

  void paint(Canvas canvas, Offset offset) {
    canvas.drawCircle(position + offset, radius, _paint);
  }
}

/// A render object that draws dots under each pointer.
class RenderDots extends RenderBox {
  RenderDots();

  /// State to remember which dots to paint.
  final Map<int, Dot> _dots = <int, Dot>{};

  /// Indicates that the size of this render object depends only on the
  /// layout constraints provided by the parent.
  @override
  bool get sizedByParent => true;

  /// By selecting the biggest value permitted by the incoming constraints
  /// during layout, this function makes this render object as large as
  /// possible (i.e., fills the entire screen).
  @override
  void performResize() {
    size = constraints.biggest;
  }

  /// Makes this render object hittable so that it receives pointer events.
  @override
  bool hitTestSelf(Offset position) => true;

  /// Processes pointer events by mutating state and invalidating its previous
  /// painting commands.
  @override
  void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
    if (event is PointerDownEvent) {
      final Color color = _kColors[event.pointer.remainder(_kColors.length)];
      _dots[event.pointer] = Dot(color: color)..update(event);
      // We call markNeedsPaint to indicate that our painting commands have
      // changed and that paint needs to be called before displaying a new frame
      // to the user. It's harmless to call markNeedsPaint multiple times
      // because the render tree will ignore redundant calls.
      markNeedsPaint();
    } else if (event is PointerUpEvent || event is PointerCancelEvent) {
      _dots.remove(event.pointer);
      markNeedsPaint();
    } else if (event is PointerMoveEvent) {
      _dots[event.pointer]!.update(event);
      markNeedsPaint();
    }
  }

  /// Issues new painting commands.
  @override
  void paint(PaintingContext context, Offset offset) {
    final Canvas canvas = context.canvas;
    // The "size" property indicates the size of that this render box was
    // allotted during layout. Here we paint our bounds white. Notice that we're
    // located at "offset" from the origin of the canvas' coordinate system.
    // Passing offset during the render tree's paint walk is an optimization to
    // avoid having to change the origin of the canvas's coordinate system too
    // often.
    canvas.drawRect(offset & size, Paint()..color = const Color(0xFFFFFFFF));

    // We iterate through our model and paint each dot.
    for (final Dot dot in _dots.values)
      dot.paint(canvas, offset);
  }
}

void main() {
  // Create some styled text to tell the user to interact with the app.
  final RenderParagraph paragraph = RenderParagraph(
    const TextSpan(
      style: TextStyle(color: Colors.black87),
      text: 'Touch me!',
    ),
    textDirection: TextDirection.ltr,
  );
  // A stack is a render object that layers its children on top of each other.
  // The bottom later is our RenderDots object, and on top of that we show the
  // text.
  final RenderStack stack = RenderStack(
    textDirection: TextDirection.ltr,
    children: <RenderBox>[
      RenderDots(),
      paragraph,
    ],
  );
  // The "parentData" field of a render object is controlled by the render
  // object's parent render object. Now that we've added the paragraph as a
  // child of the RenderStack, the paragraph's parentData field has been
  // populated with a StackParentData, which we can use to provide input to the
  // stack's layout algorithm.
  //
  // We use the StackParentData of the paragraph to position the text in the top
  // left corner of the screen.
  final StackParentData paragraphParentData = paragraph.parentData! as StackParentData;
  paragraphParentData
    ..top = 40.0
    ..left = 20.0;

  // Finally, we attach the render tree we've built to the screen.
  RenderingFlutterBinding(root: stack).scheduleFrame();
}
