All Downloads are FREE. Search and download functionalities are using the official Maven repository.

doodle.reactor.BaseReactor.scala Maven / Gradle / Ivy

There is a newer version: 0.25.0
Show newest version
/*
 * Copyright 2015 Creative Scala
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package doodle
package reactor

import cats.effect.IO
import cats.effect.std.Queue
import cats.effect.unsafe.IORuntime
import cats.syntax.all.*
import doodle.core.Point
import doodle.effect.Renderer
import doodle.image.Image
import doodle.image.syntax.all.*
import doodle.interact.algebra.MouseClick
import doodle.interact.algebra.MouseMove
import doodle.interact.effect.AnimationRenderer
import doodle.interact.syntax.all.*
import doodle.language.Basic
import doodle.syntax.renderer.*
import fs2.Stream

import scala.concurrent.duration.*

/** A reactor is a simple way to express an interactive program. It allows us to
  * write programs in terms of some initial state and transformations of that
  * state in response to inputs and clock ticks.
  *
  * This is the basic interface. See [[Reactor]] for a more user friendly
  * implementation.
  *
  * It is based on the same abstraction in Pyret.
  */
trait BaseReactor[A] {
  def initial: A
  def onTick(state: A): A
  def onMouseMove(location: Point, state: A): A
  def onMouseClick(location: Point, state: A): A
  def tickRate: FiniteDuration
  def render(value: A): Image
  def stop(value: A): Boolean

  /** Run one tick of this reactor, drawing on the given `frame`. Returns the
    * next state, or None if the Reactor has stopped.
    */
  def tick[Frame, Canvas](
      frame: Frame
  )(implicit
      e: Renderer[Basic, Frame, Canvas],
      runtime: IORuntime
  ): Option[A] = {
    if stop(initial) then None
    else {
      (render(initial)).drawWithFrame(frame)
      val next = onTick(initial)
      Some(next)
    }
  }

  /** Runs this reactor, drawing on the given `frame`, until `stop` indicates it
    * should stop.
    */
  def run[Alg <: Basic, Frame, Canvas](frame: Frame)(implicit
      a: AnimationRenderer[Canvas],
      e: Renderer[Alg, Frame, Canvas],
      m: MouseClick[Canvas] & MouseMove[Canvas],
      runtime: IORuntime
  ): Unit = {
    import BaseReactor.*

    def mouseEventProducer(
        mouseEventQueue: Queue[IO, MouseEvent],
        canvas: Canvas
    ): IO[Unit] = {
      val mouseMove = canvas.mouseMove.map(pt => MouseMove(pt))
      val mouseClick = canvas.mouseClick.map(pt => MouseClick(pt))

      mouseMove
        .merge(mouseClick)
        .foreach(mouseEventQueue.offer)
        .compile
        .drain
    }

    def tickProducer(
        tickQueue: Queue[IO, A],
        mouseEventQueue: Queue[IO, MouseEvent]
    ): IO[Unit] = {
      Stream
        .fixedRate[IO](this.tickRate)
        .evalScan[IO, A](this.initial)((prev, _) =>
          def drainMouseQueue(a: A): IO[A] =
            for
              mouseEvent <- mouseEventQueue.tryTake
              state <- mouseEvent match
                case Some(MouseMove(pt)) =>
                  drainMouseQueue(this.onMouseMove(pt, a))
                case Some(MouseClick(pt)) =>
                  drainMouseQueue(this.onMouseClick(pt, a))
                case None => IO.pure(a)
            yield state

          for
            mouseState <- drainMouseQueue(prev)
            state = this.onTick(mouseState)
          yield state
        )
        .takeWhile(a => !this.stop(a))
        .foreach(tickQueue.offer)
        .compile
        .drain
    }

    def consumer(tickQueue: Queue[IO, A], canvas: Canvas): IO[Unit] = {
      Stream.unit.repeat
        .evalScan[IO, A](this.initial)((prev, _) =>
          for
            maybeTaken <- tickQueue.tryTake
            state = maybeTaken match
              case Some(a) => a
              case None    => prev
          yield state
        )
        .takeWhile(a => !this.stop(a))
        .map(a => Image.compile[Alg](this.render(a)))
        .animateWithCanvasToIO(canvas)
    }

    (
      frame
        .canvas[Alg, Canvas]()
        .use(canvas =>
          for {
            tickQueue <- Queue.circularBuffer[IO, A](1)
            // mouseEventQueue <- Queue.circularBuffer[IO, MouseEvent](1)
            mouseEventQueue <- Queue.unbounded[IO, MouseEvent]
            _ <-
              (
                mouseEventProducer(mouseEventQueue, canvas),
                tickProducer(tickQueue, mouseEventQueue),
                consumer(tickQueue, canvas)
              )
                .parMapN((_, _, _) => ())
          } yield ()
        )
        .unsafeRunAsync(_ => ())
    )
  }
}
object BaseReactor {
  sealed abstract class MouseEvent extends Product with Serializable
  final case class MouseMove(location: Point) extends MouseEvent
  final case class MouseClick(location: Point) extends MouseEvent
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy