From c91359699f724428ea03e35fafc7d2e39b4fb30f Mon Sep 17 00:00:00 2001 From: Noel Welsh Date: Fri, 20 Sep 2024 11:17:38 +0100 Subject: [PATCH] [WIP] Refactor Java2d backend `frame.canvas()` now returns a `Resource[IO, Canvas]`, to manage the resources a `Canvas` can control. Additionally, for the Java2d backend: - Clear separation between Swing / Java threads and Cats Effect components, makes reasoning easier - Better concurrency control avoids deadlock - Better resource management shutdowns cleanly when it should - Miscellaneous simplifications Still to be done: - Remove debugging statements - Simplify `Reified` representation --- .github/workflows/ci.yml | 2 +- .scalafix.conf | 1 + CHANGELOG.md | 2 + build.sbt | 5 +- .../scala/doodle/canvas/algebra/Path.scala | 2 +- .../scala/doodle/canvas/algebra/Shape.scala | 2 +- .../scala/doodle/canvas/algebra/Text.scala | 2 +- .../scala/doodle/canvas/effect/Canvas.scala | 5 +- .../doodle/canvas/effect/CanvasRenderer.scala | 3 +- .../doodle/syntax/Base64WriterSyntax.scala | 2 +- .../main/scala/doodle/algebra/Transform.scala | 2 +- .../doodle/algebra/generic/Finalized.scala | 2 +- .../doodle/algebra/generic/GenericPath.scala | 2 +- .../doodle/algebra/generic/GenericShape.scala | 2 +- .../doodle/algebra/generic/GenericText.scala | 2 +- .../algebra/generic/GenericTransform.scala | 2 +- .../doodle/algebra/generic/package.scala | 2 +- .../scala/doodle/effect/Base64Writer.scala | 2 +- .../main/scala/doodle/effect/Renderer.scala | 3 +- .../shared/src/main/scala/doodle/random.scala | 2 +- .../syntax/AbstractRendererSyntax.scala | 7 +- .../scala/doodle/syntax/TransformSyntax.scala | 2 +- .../doodle/algebra/generic/Generators.scala | 2 +- .../algebra/generic/GenericAlgebraSpec.scala | 2 +- .../doodle/algebra/generic/LayoutSpec.scala | 4 +- .../doodle/algebra/generic/SizeSpec.scala | 4 +- .../doodle/algebra/generic/StyleSpec.scala | 2 +- .../doodle/algebra/generic/TextSpec.scala | 2 +- .../algebra/generic/reified/Reified.scala | 2 +- .../algebra/generic/reified/ReifiedPath.scala | 2 +- .../generic/reified/ReifiedShape.scala | 2 +- .../algebra/generic/reified/ReifiedText.scala | 2 +- .../test/scala/doodle/core/AngleSpec.scala | 2 +- .../scala/doodle/core/ClosedPathSpec.scala | 2 +- .../test/scala/doodle/core/ColorSpec.scala | 2 +- .../scala/doodle/core/CoordinateSpec.scala | 2 +- .../test/scala/doodle/core/OpenPathSpec.scala | 2 +- .../scala/doodle/core/TransformSpec.scala | 2 +- .../test/scala/doodle/syntax/AngleSpec.scala | 2 +- .../scala/doodle/syntax/NormalizedSpec.scala | 2 +- docs/src/pages/pictures/drawing.md | 35 +-- .../doodle/image/syntax/JvmImageSyntax.scala | 2 +- .../src/main/scala/doodle/image/Image.scala | 2 +- .../AbstractAnimationRendererSyntax.scala | 4 +- .../animation/InterpolationSpec.scala | 2 +- .../interact/animation/TransducerSpec.scala | 2 +- .../doodle/interact/easing/EasingSpec.scala | 2 +- .../algebra/Graphics2DGraphicsContext.scala | 2 +- .../scala/doodle/java2d/algebra/Java2D.scala | 6 +- .../java2d/algebra/Java2dFromBase64.scala | 2 +- .../java2d/algebra/reified/Reified.scala | 2 +- .../java2d/algebra/reified/ReifiedPath.scala | 2 +- .../java2d/algebra/reified/ReifiedShape.scala | 2 +- .../java2d/algebra/reified/ReifiedText.scala | 2 +- .../java2d/effect/BlockingCircularQueue.scala | 71 ++++++ .../scala/doodle/java2d/effect/Canvas.scala | 227 ++++++++---------- .../doodle/java2d/effect/Java2DPanel.scala | 155 +++++++----- .../scala/doodle/java2d/effect/Java2d.scala | 4 +- .../effect/Java2dBufferedImageWriter.scala | 2 +- .../doodle/java2d/effect/Java2dRenderer.scala | 23 +- .../doodle/java2d/effect/Java2dWindow.scala | 117 +++++++++ .../doodle/java2d/effect/Java2dWriter.scala | 6 +- .../doodle/java2d/effect/RenderRequest.scala | 53 ++++ .../doodle/java2d/effect/RenderResult.scala | 32 +++ .../doodle/java2d/examples/Pointillism.scala | 56 +++++ .../doodle/java2d/examples/Ripples.scala | 14 +- .../scala/doodle/java2d/ToPictureSuite.scala | 2 +- .../effect/BlockingCircularQueueSuite.scala | 51 ++++ project/plugins.sbt | 4 +- .../scala/doodle/reactor/BaseReactor.scala | 32 +-- .../main/scala/doodle/svg/algebra/Text.scala | 2 +- .../scala/doodle/svg/effect/SvgRenderer.scala | 5 +- .../svg/examples/ConcentricCircles.scala | 7 +- .../main/scala/doodle/svg/algebra/Text.scala | 4 +- .../scala/doodle/svg/effect/SvgWriter.scala | 4 +- .../main/scala/doodle/svg/algebra/Path.scala | 2 +- .../main/scala/doodle/svg/algebra/Shape.scala | 2 +- .../main/scala/doodle/svg/algebra/Svg.scala | 2 +- .../doodle/turtle/examples/Geometry.scala | 2 +- 79 files changed, 717 insertions(+), 325 deletions(-) create mode 100644 java2d/src/main/scala/doodle/java2d/effect/BlockingCircularQueue.scala create mode 100644 java2d/src/main/scala/doodle/java2d/effect/Java2dWindow.scala create mode 100644 java2d/src/main/scala/doodle/java2d/effect/RenderRequest.scala create mode 100644 java2d/src/main/scala/doodle/java2d/effect/RenderResult.scala create mode 100644 java2d/src/main/scala/doodle/java2d/examples/Pointillism.scala create mode 100644 java2d/src/test/scala/doodle/java2d/effect/BlockingCircularQueueSuite.scala diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 03e7d35b..0c8e90ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -156,7 +156,7 @@ jobs: dependency-submission: name: Submit Dependencies - if: github.event_name != 'pull_request' + if: github.event.repository.fork == false && github.event_name != 'pull_request' strategy: matrix: os: [ubuntu-latest] diff --git a/.scalafix.conf b/.scalafix.conf index 4eedd04e..9ba6d293 100644 --- a/.scalafix.conf +++ b/.scalafix.conf @@ -3,3 +3,4 @@ rules = [ ] OrganizeImports.removeUnused = false OrganizeImports.coalesceToWildcardImportThreshold = 5 +OrganizeImports.targetDialect = Scala3 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index eb4eadb0..faa412b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ - Fix monospaced font rendering on SVG (by `@kulsoom2003`) +- Complete reworking of Java2D backend to remove race conditions and manage resources correctly. + ## 0.23.0 26-Jul-2024 diff --git a/build.sbt b/build.sbt index bf62a974..5dd9cbc3 100644 --- a/build.sbt +++ b/build.sbt @@ -31,8 +31,7 @@ ThisBuild / developers := List( tlGitHubDev("noelwelsh", "Noel Welsh") ) -// true by default, set to false to publish to s01.oss.sonatype.org -ThisBuild / tlSonatypeUseLegacyHost := true +ThisBuild / sonatypeCredentialHost := xerial.sbt.Sonatype.sonatypeLegacy lazy val scala3 = "3.3.3" @@ -62,6 +61,8 @@ commands += Command.command("build") { state => lazy val css = taskKey[Unit]("Build the CSS") lazy val commonSettings = Seq( + // This is needed when running examples + Compile / run / fork := true, libraryDependencies ++= Seq( Dependencies.munit.value, Dependencies.munitScalaCheck.value diff --git a/canvas/src/main/scala/doodle/canvas/algebra/Path.scala b/canvas/src/main/scala/doodle/canvas/algebra/Path.scala index 824eb625..1438fed2 100644 --- a/canvas/src/main/scala/doodle/canvas/algebra/Path.scala +++ b/canvas/src/main/scala/doodle/canvas/algebra/Path.scala @@ -19,7 +19,7 @@ package doodle.canvas.algebra import doodle.algebra.Algebra import doodle.algebra.generic.* import doodle.core.* -import doodle.core.{Transform as Tx} +import doodle.core.Transform as Tx trait Path extends GenericPath[CanvasDrawing] { self: Algebra { type Drawing[A] = Finalized[CanvasDrawing, A] } => diff --git a/canvas/src/main/scala/doodle/canvas/algebra/Shape.scala b/canvas/src/main/scala/doodle/canvas/algebra/Shape.scala index 803f7965..ac7000e5 100644 --- a/canvas/src/main/scala/doodle/canvas/algebra/Shape.scala +++ b/canvas/src/main/scala/doodle/canvas/algebra/Shape.scala @@ -20,7 +20,7 @@ import doodle.algebra.Algebra import doodle.algebra.generic.* import doodle.core.ClosedPath import doodle.core.Point -import doodle.core.{Transform as Tx} +import doodle.core.Transform as Tx trait Shape extends GenericShape[CanvasDrawing] { self: Algebra { type Drawing[A] = Finalized[CanvasDrawing, A] } => diff --git a/canvas/src/main/scala/doodle/canvas/algebra/Text.scala b/canvas/src/main/scala/doodle/canvas/algebra/Text.scala index 1338345f..6565ebab 100644 --- a/canvas/src/main/scala/doodle/canvas/algebra/Text.scala +++ b/canvas/src/main/scala/doodle/canvas/algebra/Text.scala @@ -19,8 +19,8 @@ package doodle.canvas.algebra import doodle.algebra.Algebra import doodle.algebra.generic.* import doodle.core.BoundingBox +import doodle.core.Transform as Tx import doodle.core.font.Font -import doodle.core.{Transform as Tx} import org.scalajs.dom trait Text extends GenericText[CanvasDrawing] { diff --git a/canvas/src/main/scala/doodle/canvas/effect/Canvas.scala b/canvas/src/main/scala/doodle/canvas/effect/Canvas.scala index bc0d04de..2a7b65fb 100644 --- a/canvas/src/main/scala/doodle/canvas/effect/Canvas.scala +++ b/canvas/src/main/scala/doodle/canvas/effect/Canvas.scala @@ -17,6 +17,7 @@ package doodle.canvas.effect import cats.effect.IO +import cats.effect.Resource import doodle.algebra.generic.Finalized import doodle.canvas.Picture import doodle.canvas.algebra.CanvasAlgebra @@ -74,7 +75,7 @@ final case class Canvas(target: dom.Node, frame: Frame) { } } object Canvas { - def fromFrame(frame: Frame): IO[Canvas] = { + def fromFrame(frame: Frame): Resource[IO, Canvas] = { IO { val target = dom.document.getElementById(frame.id) if target == null then { @@ -82,6 +83,6 @@ object Canvas { s"Doodle Canvas could not be created, as could not find a DOM element with the requested id ${frame.id}" ) } else Canvas(target, frame) - } + }.toResource } } diff --git a/canvas/src/main/scala/doodle/canvas/effect/CanvasRenderer.scala b/canvas/src/main/scala/doodle/canvas/effect/CanvasRenderer.scala index 26d3fcd5..48c3a207 100644 --- a/canvas/src/main/scala/doodle/canvas/effect/CanvasRenderer.scala +++ b/canvas/src/main/scala/doodle/canvas/effect/CanvasRenderer.scala @@ -17,13 +17,14 @@ package doodle.canvas.effect import cats.effect.IO +import cats.effect.Resource import doodle.canvas.Algebra import doodle.canvas.Picture import doodle.effect.Renderer object CanvasRenderer extends Renderer[Algebra, Frame, Canvas] { - def canvas(description: Frame): IO[Canvas] = + def canvas(description: Frame): Resource[IO, Canvas] = Canvas.fromFrame(description) def render[A](canvas: Canvas)(picture: Picture[A]): IO[A] = diff --git a/core/jvm/src/main/scala/doodle/syntax/Base64WriterSyntax.scala b/core/jvm/src/main/scala/doodle/syntax/Base64WriterSyntax.scala index d62f9fa3..7d05e72b 100644 --- a/core/jvm/src/main/scala/doodle/syntax/Base64WriterSyntax.scala +++ b/core/jvm/src/main/scala/doodle/syntax/Base64WriterSyntax.scala @@ -21,8 +21,8 @@ import cats.effect.IO import cats.effect.unsafe.IORuntime import doodle.algebra.Algebra import doodle.algebra.Picture +import doodle.core.Base64 as B64 import doodle.core.format.Format -import doodle.core.{Base64 as B64} import doodle.effect.Base64Writer import doodle.effect.DefaultFrame diff --git a/core/shared/src/main/scala/doodle/algebra/Transform.scala b/core/shared/src/main/scala/doodle/algebra/Transform.scala index c78957c4..981c4126 100644 --- a/core/shared/src/main/scala/doodle/algebra/Transform.scala +++ b/core/shared/src/main/scala/doodle/algebra/Transform.scala @@ -18,8 +18,8 @@ package doodle package algebra import doodle.core.Angle +import doodle.core.Transform as Tx import doodle.core.Vec -import doodle.core.{Transform as Tx} trait Transform extends Algebra { def transform[A](img: Drawing[A], tx: Tx): Drawing[A] diff --git a/core/shared/src/main/scala/doodle/algebra/generic/Finalized.scala b/core/shared/src/main/scala/doodle/algebra/generic/Finalized.scala index c8ccda31..5474660a 100644 --- a/core/shared/src/main/scala/doodle/algebra/generic/Finalized.scala +++ b/core/shared/src/main/scala/doodle/algebra/generic/Finalized.scala @@ -22,7 +22,7 @@ import cats.Eval import cats.Later import cats.data.* import doodle.core.BoundingBox -import doodle.core.{Transform as Tx} +import doodle.core.Transform as Tx /** A [[Finalized]] represents an effect that, when run, produces all the * information needed to layout an image (it "finalizes" all the information diff --git a/core/shared/src/main/scala/doodle/algebra/generic/GenericPath.scala b/core/shared/src/main/scala/doodle/algebra/generic/GenericPath.scala index 2b99e50f..7ded038b 100644 --- a/core/shared/src/main/scala/doodle/algebra/generic/GenericPath.scala +++ b/core/shared/src/main/scala/doodle/algebra/generic/GenericPath.scala @@ -20,7 +20,7 @@ package generic import cats.data.State import doodle.core.* -import doodle.core.{Transform as Tx} +import doodle.core.Transform as Tx import scala.annotation.tailrec diff --git a/core/shared/src/main/scala/doodle/algebra/generic/GenericShape.scala b/core/shared/src/main/scala/doodle/algebra/generic/GenericShape.scala index 3d3891eb..e7738fb6 100644 --- a/core/shared/src/main/scala/doodle/algebra/generic/GenericShape.scala +++ b/core/shared/src/main/scala/doodle/algebra/generic/GenericShape.scala @@ -20,7 +20,7 @@ package generic import cats.data.State import doodle.core.BoundingBox -import doodle.core.{Transform as Tx} +import doodle.core.Transform as Tx trait GenericShape[G[_]] extends Shape { self: Algebra { type Drawing[A] = Finalized[G, A] } => diff --git a/core/shared/src/main/scala/doodle/algebra/generic/GenericText.scala b/core/shared/src/main/scala/doodle/algebra/generic/GenericText.scala index 2e02f8d5..d3aef62c 100644 --- a/core/shared/src/main/scala/doodle/algebra/generic/GenericText.scala +++ b/core/shared/src/main/scala/doodle/algebra/generic/GenericText.scala @@ -20,8 +20,8 @@ package generic import cats.data.State import doodle.core.BoundingBox +import doodle.core.Transform as Tx import doodle.core.font.Font -import doodle.core.{Transform as Tx} trait GenericText[G[_]] extends Text { self: Algebra { type Drawing[A] = Finalized[G, A] } => diff --git a/core/shared/src/main/scala/doodle/algebra/generic/GenericTransform.scala b/core/shared/src/main/scala/doodle/algebra/generic/GenericTransform.scala index f43c15b0..b48bd5b8 100644 --- a/core/shared/src/main/scala/doodle/algebra/generic/GenericTransform.scala +++ b/core/shared/src/main/scala/doodle/algebra/generic/GenericTransform.scala @@ -18,7 +18,7 @@ package doodle package algebra package generic -import doodle.core.{Transform as Tx} +import doodle.core.Transform as Tx trait GenericTransform[G[_]] extends Transform { self: Algebra { type Drawing[A] = Finalized[G, A] } => diff --git a/core/shared/src/main/scala/doodle/algebra/generic/package.scala b/core/shared/src/main/scala/doodle/algebra/generic/package.scala index 2288fe00..c67b95d0 100644 --- a/core/shared/src/main/scala/doodle/algebra/generic/package.scala +++ b/core/shared/src/main/scala/doodle/algebra/generic/package.scala @@ -20,7 +20,7 @@ package algebra import cats.* import cats.data.* import cats.syntax.all.* -import doodle.core.{Transform as Tx} +import doodle.core.Transform as Tx package object generic { type ContextTransform = DrawingContext => DrawingContext diff --git a/core/shared/src/main/scala/doodle/effect/Base64Writer.scala b/core/shared/src/main/scala/doodle/effect/Base64Writer.scala index 03ce25c8..ee34cc5f 100644 --- a/core/shared/src/main/scala/doodle/effect/Base64Writer.scala +++ b/core/shared/src/main/scala/doodle/effect/Base64Writer.scala @@ -20,8 +20,8 @@ package effect import cats.effect.IO import doodle.algebra.Algebra import doodle.algebra.Picture +import doodle.core.Base64 as B64 import doodle.core.format.Format -import doodle.core.{Base64 as B64} /** The Base64Writer type represent the ability to encode an image as a Base64 * String in a given format. diff --git a/core/shared/src/main/scala/doodle/effect/Renderer.scala b/core/shared/src/main/scala/doodle/effect/Renderer.scala index 8f2d63df..47d2a0f7 100644 --- a/core/shared/src/main/scala/doodle/effect/Renderer.scala +++ b/core/shared/src/main/scala/doodle/effect/Renderer.scala @@ -18,6 +18,7 @@ package doodle package effect import cats.effect.IO +import cats.effect.Resource import doodle.algebra.Algebra import doodle.algebra.Picture @@ -28,7 +29,7 @@ import doodle.algebra.Picture trait Renderer[+Alg <: Algebra, Frame, Canvas] { /** Construct a Canvas from a description. */ - def canvas(description: Frame): IO[Canvas] + def canvas(description: Frame): Resource[IO, Canvas] /** Render a picture to a Canvas. */ def render[A](canvas: Canvas)(picture: Picture[Alg, A]): IO[A] diff --git a/core/shared/src/main/scala/doodle/random.scala b/core/shared/src/main/scala/doodle/random.scala index a28c2d1b..547fa701 100644 --- a/core/shared/src/main/scala/doodle/random.scala +++ b/core/shared/src/main/scala/doodle/random.scala @@ -20,7 +20,7 @@ import cats.Comonad import cats.free.Free import scala.annotation.tailrec -import scala.util.{Random as Rng} +import scala.util.Random as Rng object random { type Random[A] = Free[RandomOp, A] diff --git a/core/shared/src/main/scala/doodle/syntax/AbstractRendererSyntax.scala b/core/shared/src/main/scala/doodle/syntax/AbstractRendererSyntax.scala index 6cb4214f..61219c62 100644 --- a/core/shared/src/main/scala/doodle/syntax/AbstractRendererSyntax.scala +++ b/core/shared/src/main/scala/doodle/syntax/AbstractRendererSyntax.scala @@ -18,6 +18,7 @@ package doodle package syntax import cats.effect.IO +import cats.effect.Resource import cats.effect.unsafe.IORuntime import doodle.algebra.Algebra import doodle.algebra.Picture @@ -79,7 +80,7 @@ trait AbstractRendererSyntax { ): IO[A] = renderer .canvas(frame.default) - .flatMap(canvas => drawWithCanvasToIO(canvas)) + .use(canvas => drawWithCanvasToIO(canvas)) /** Create an effect that, when run, will draw the `Picture` using the given * `Frame` options. @@ -87,7 +88,7 @@ trait AbstractRendererSyntax { def drawWithFrameToIO[Frame, Canvas](frame: Frame)(implicit renderer: Renderer[Alg, Frame, Canvas] ): IO[A] = - renderer.canvas(frame).flatMap(canvas => drawWithCanvasToIO(canvas)) + renderer.canvas(frame).use(canvas => drawWithCanvasToIO(canvas)) /** Create an effect that, when run, will draw the `Picture` on the given * `Canvas`. @@ -102,7 +103,7 @@ trait AbstractRendererSyntax { implicit class RendererFrameOps[Frame](frame: Frame) { def canvas[Alg <: Algebra, Canvas]()(implicit renderer: Renderer[Alg, Frame, Canvas] - ): IO[Canvas] = + ): Resource[IO, Canvas] = renderer.canvas(frame) } } diff --git a/core/shared/src/main/scala/doodle/syntax/TransformSyntax.scala b/core/shared/src/main/scala/doodle/syntax/TransformSyntax.scala index dc3c2029..58820150 100644 --- a/core/shared/src/main/scala/doodle/syntax/TransformSyntax.scala +++ b/core/shared/src/main/scala/doodle/syntax/TransformSyntax.scala @@ -21,8 +21,8 @@ import doodle.algebra.Algebra import doodle.algebra.Picture import doodle.algebra.Transform import doodle.core.Angle +import doodle.core.Transform as Tx import doodle.core.Vec -import doodle.core.{Transform as Tx} trait TransformSyntax { implicit class TransformPictureOps[Alg <: Algebra, A]( diff --git a/core/shared/src/test/scala/doodle/algebra/generic/Generators.scala b/core/shared/src/test/scala/doodle/algebra/generic/Generators.scala index 99ddf68e..7e6b3ec9 100644 --- a/core/shared/src/test/scala/doodle/algebra/generic/Generators.scala +++ b/core/shared/src/test/scala/doodle/algebra/generic/Generators.scala @@ -22,7 +22,7 @@ import cats.instances.unit.* import doodle.algebra.generic.* import doodle.algebra.generic.reified.Reification import doodle.algebra.generic.reified.Reified -import doodle.core.{Transform as Tx} +import doodle.core.Transform as Tx import org.scalacheck.* trait Generators extends doodle.core.Generators { diff --git a/core/shared/src/test/scala/doodle/algebra/generic/GenericAlgebraSpec.scala b/core/shared/src/test/scala/doodle/algebra/generic/GenericAlgebraSpec.scala index 09a60512..495353d6 100644 --- a/core/shared/src/test/scala/doodle/algebra/generic/GenericAlgebraSpec.scala +++ b/core/shared/src/test/scala/doodle/algebra/generic/GenericAlgebraSpec.scala @@ -19,8 +19,8 @@ package algebra package generic import cats.instances.unit.* -import org.scalacheck.Prop.* import org.scalacheck.* +import org.scalacheck.Prop.* object GenericAlgebraspec extends Properties("Generic algebra properties") { implicit val algebra: TestAlgebra = TestAlgebra() diff --git a/core/shared/src/test/scala/doodle/algebra/generic/LayoutSpec.scala b/core/shared/src/test/scala/doodle/algebra/generic/LayoutSpec.scala index fd0b50cd..eb6d59e5 100644 --- a/core/shared/src/test/scala/doodle/algebra/generic/LayoutSpec.scala +++ b/core/shared/src/test/scala/doodle/algebra/generic/LayoutSpec.scala @@ -21,9 +21,9 @@ package generic import cats.implicits.* import doodle.algebra.generic.reified.Reification import doodle.core.BoundingBox -import doodle.core.{Transform as Tx} -import org.scalacheck.Prop.* +import doodle.core.Transform as Tx import org.scalacheck.* +import org.scalacheck.Prop.* object LayoutSpec extends Properties("Layout properties") { val style = TestAlgebra() diff --git a/core/shared/src/test/scala/doodle/algebra/generic/SizeSpec.scala b/core/shared/src/test/scala/doodle/algebra/generic/SizeSpec.scala index b30e44b3..4b80b6c5 100644 --- a/core/shared/src/test/scala/doodle/algebra/generic/SizeSpec.scala +++ b/core/shared/src/test/scala/doodle/algebra/generic/SizeSpec.scala @@ -20,9 +20,9 @@ package generic import cats.implicits.* import doodle.algebra.generic.reified.Reification -import doodle.core.{Transform as Tx} -import org.scalacheck.Prop.* +import doodle.core.Transform as Tx import org.scalacheck.* +import org.scalacheck.Prop.* object SizeSpec extends Properties("Size properties") { implicit val algebra: TestAlgebra = TestAlgebra() diff --git a/core/shared/src/test/scala/doodle/algebra/generic/StyleSpec.scala b/core/shared/src/test/scala/doodle/algebra/generic/StyleSpec.scala index 4656fe04..79b7591b 100644 --- a/core/shared/src/test/scala/doodle/algebra/generic/StyleSpec.scala +++ b/core/shared/src/test/scala/doodle/algebra/generic/StyleSpec.scala @@ -18,8 +18,8 @@ package doodle package algebra package generic -import org.scalacheck.Prop.* import org.scalacheck.* +import org.scalacheck.Prop.* object StyleSpec extends Properties("Style properties") { val style = TestAlgebra() diff --git a/core/shared/src/test/scala/doodle/algebra/generic/TextSpec.scala b/core/shared/src/test/scala/doodle/algebra/generic/TextSpec.scala index 9338c5d6..1bf274e0 100644 --- a/core/shared/src/test/scala/doodle/algebra/generic/TextSpec.scala +++ b/core/shared/src/test/scala/doodle/algebra/generic/TextSpec.scala @@ -18,8 +18,8 @@ package doodle package algebra package generic -import org.scalacheck.Prop.* import org.scalacheck.* +import org.scalacheck.Prop.* object TextSpec extends Properties("Text properties") { val algebra = TestAlgebra() diff --git a/core/shared/src/test/scala/doodle/algebra/generic/reified/Reified.scala b/core/shared/src/test/scala/doodle/algebra/generic/reified/Reified.scala index a701f459..2bd12ed2 100644 --- a/core/shared/src/test/scala/doodle/algebra/generic/reified/Reified.scala +++ b/core/shared/src/test/scala/doodle/algebra/generic/reified/Reified.scala @@ -21,8 +21,8 @@ package reified import doodle.core.PathElement import doodle.core.Point +import doodle.core.Transform as Tx import doodle.core.font.Font -import doodle.core.{Transform as Tx} sealed abstract class Reified extends Product with Serializable { def transform: Tx diff --git a/core/shared/src/test/scala/doodle/algebra/generic/reified/ReifiedPath.scala b/core/shared/src/test/scala/doodle/algebra/generic/reified/ReifiedPath.scala index af4d12d3..e56268ae 100644 --- a/core/shared/src/test/scala/doodle/algebra/generic/reified/ReifiedPath.scala +++ b/core/shared/src/test/scala/doodle/algebra/generic/reified/ReifiedPath.scala @@ -22,7 +22,7 @@ package reified import cats.data.WriterT import doodle.algebra.generic.* import doodle.core.* -import doodle.core.{Transform as Tx} +import doodle.core.Transform as Tx trait ReifiedPath extends GenericPath[Reification] { self: Algebra { type Drawing[A] = TestAlgebra.Drawing[A] } => diff --git a/core/shared/src/test/scala/doodle/algebra/generic/reified/ReifiedShape.scala b/core/shared/src/test/scala/doodle/algebra/generic/reified/ReifiedShape.scala index 47e37a1a..6cab4564 100644 --- a/core/shared/src/test/scala/doodle/algebra/generic/reified/ReifiedShape.scala +++ b/core/shared/src/test/scala/doodle/algebra/generic/reified/ReifiedShape.scala @@ -22,7 +22,7 @@ package reified import cats.data.WriterT import doodle.algebra.generic.* import doodle.core.Point -import doodle.core.{Transform as Tx} +import doodle.core.Transform as Tx trait ReifiedShape extends GenericShape[Reification] { self: Algebra { type Drawing[A] = TestAlgebra.Drawing[A] } => diff --git a/core/shared/src/test/scala/doodle/algebra/generic/reified/ReifiedText.scala b/core/shared/src/test/scala/doodle/algebra/generic/reified/ReifiedText.scala index f355fd25..9ed84605 100644 --- a/core/shared/src/test/scala/doodle/algebra/generic/reified/ReifiedText.scala +++ b/core/shared/src/test/scala/doodle/algebra/generic/reified/ReifiedText.scala @@ -22,8 +22,8 @@ package reified import cats.data.WriterT import doodle.algebra.generic.* import doodle.core.BoundingBox +import doodle.core.Transform as Tx import doodle.core.font.Font -import doodle.core.{Transform as Tx} trait ReifiedText extends GenericText[Reification] { self: Algebra { type Drawing[A] = TestAlgebra.Drawing[A] } => diff --git a/core/shared/src/test/scala/doodle/core/AngleSpec.scala b/core/shared/src/test/scala/doodle/core/AngleSpec.scala index dc46fdda..74c5168a 100644 --- a/core/shared/src/test/scala/doodle/core/AngleSpec.scala +++ b/core/shared/src/test/scala/doodle/core/AngleSpec.scala @@ -17,8 +17,8 @@ package doodle package core -import org.scalacheck.Prop.* import org.scalacheck.* +import org.scalacheck.Prop.* object AngleSpec extends Properties("Angle properties") { import doodle.arbitrary.* diff --git a/core/shared/src/test/scala/doodle/core/ClosedPathSpec.scala b/core/shared/src/test/scala/doodle/core/ClosedPathSpec.scala index 2ab559a5..2b82cf87 100644 --- a/core/shared/src/test/scala/doodle/core/ClosedPathSpec.scala +++ b/core/shared/src/test/scala/doodle/core/ClosedPathSpec.scala @@ -17,8 +17,8 @@ package doodle package core -import org.scalacheck.Prop.* import org.scalacheck.* +import org.scalacheck.Prop.* object ClosedPathSpec extends Properties("ClosedPath properties") { import Generators.* diff --git a/core/shared/src/test/scala/doodle/core/ColorSpec.scala b/core/shared/src/test/scala/doodle/core/ColorSpec.scala index ce558ff3..e733b7a0 100644 --- a/core/shared/src/test/scala/doodle/core/ColorSpec.scala +++ b/core/shared/src/test/scala/doodle/core/ColorSpec.scala @@ -17,8 +17,8 @@ package doodle package core -import org.scalacheck.Prop.* import org.scalacheck.* +import org.scalacheck.Prop.* object ColorSpec extends Properties("Color properties") { import doodle.arbitrary.* diff --git a/core/shared/src/test/scala/doodle/core/CoordinateSpec.scala b/core/shared/src/test/scala/doodle/core/CoordinateSpec.scala index 5dc2c6e4..ef77a7b2 100644 --- a/core/shared/src/test/scala/doodle/core/CoordinateSpec.scala +++ b/core/shared/src/test/scala/doodle/core/CoordinateSpec.scala @@ -18,8 +18,8 @@ package doodle package core import doodle.syntax.approximatelyEqual.* -import org.scalacheck.Prop.* import org.scalacheck.* +import org.scalacheck.Prop.* object CoordinateSpec extends Properties("Coordinate properties") { val smallNumber = Gen.choose(-300.0, 300.0) diff --git a/core/shared/src/test/scala/doodle/core/OpenPathSpec.scala b/core/shared/src/test/scala/doodle/core/OpenPathSpec.scala index 6c347d5b..28f04fa1 100644 --- a/core/shared/src/test/scala/doodle/core/OpenPathSpec.scala +++ b/core/shared/src/test/scala/doodle/core/OpenPathSpec.scala @@ -17,8 +17,8 @@ package doodle package core -import org.scalacheck.Prop.* import org.scalacheck.* +import org.scalacheck.Prop.* object OpenPathSpec extends Properties("OpenPath properties") { import Generators.* diff --git a/core/shared/src/test/scala/doodle/core/TransformSpec.scala b/core/shared/src/test/scala/doodle/core/TransformSpec.scala index 94216d5a..2babad8a 100644 --- a/core/shared/src/test/scala/doodle/core/TransformSpec.scala +++ b/core/shared/src/test/scala/doodle/core/TransformSpec.scala @@ -17,8 +17,8 @@ package doodle package core -import org.scalacheck.Prop.* import org.scalacheck.* +import org.scalacheck.Prop.* class TransformSpec extends Properties("Transform") { import doodle.arbitrary.* diff --git a/core/shared/src/test/scala/doodle/syntax/AngleSpec.scala b/core/shared/src/test/scala/doodle/syntax/AngleSpec.scala index 84d35d75..b0c43a85 100644 --- a/core/shared/src/test/scala/doodle/syntax/AngleSpec.scala +++ b/core/shared/src/test/scala/doodle/syntax/AngleSpec.scala @@ -18,8 +18,8 @@ package doodle package syntax import doodle.core.Angle -import org.scalacheck.Prop.* import org.scalacheck.* +import org.scalacheck.Prop.* class AngleSpec extends Properties("Angle syntax properties") { import doodle.syntax.angle.* diff --git a/core/shared/src/test/scala/doodle/syntax/NormalizedSpec.scala b/core/shared/src/test/scala/doodle/syntax/NormalizedSpec.scala index c758335c..96137174 100644 --- a/core/shared/src/test/scala/doodle/syntax/NormalizedSpec.scala +++ b/core/shared/src/test/scala/doodle/syntax/NormalizedSpec.scala @@ -18,8 +18,8 @@ package doodle package syntax import doodle.core.Normalized -import org.scalacheck.Prop.* import org.scalacheck.* +import org.scalacheck.Prop.* class NormalizedSpec extends Properties("Normalized syntax") { import doodle.syntax.normalized.* diff --git a/docs/src/pages/pictures/drawing.md b/docs/src/pages/pictures/drawing.md index e3798d03..bdad0c0d 100644 --- a/docs/src/pages/pictures/drawing.md +++ b/docs/src/pages/pictures/drawing.md @@ -17,6 +17,13 @@ import doodle.java2d.* import cats.effect.unsafe.implicits.global ``` +For some of the examples below we also need + +```scala mdoc:silent +import cats.effect.{IO, Resource} +``` + + ## Drawing The normal way to draw output is by calling the `draw` method. @@ -46,24 +53,12 @@ Once we have a frame, we can pass it to `drawWithFrame`. picture.drawWithFrame(frame) ``` -Sometimes you'll want to draw several pictures on the same canvas. This is how animations work, repeatedly drawing to the same canvas. To do this you need to get a reference to a `Canvas`. The `canvas` syntax method on `Frame` will produce an `IO[Canvas]` +Sometimes you'll want to draw several pictures on the same canvas. By repeatedly drawing to the same canvas we can, for example, create animations. To do this we need to work with the underling `IO` and `Resource` types that the conveniences above hide. -```scala mdoc:silent -val canvas = frame.canvas() -``` -You can then call `drawWithCanvas`. +## Drawing with IO and Resource -```scala mdoc:silent -canvas.map(c => picture.drawWithCanvas(c)) -``` - -However, this is not very idiomatic code. The above methods are all conveniences that hide the underlying use of `IO`. As soon as we introduce `IO` we should use the methods described below. - - -## Drawing with IO - -It can be useful to convert a `Picture[A]` to an `IO[A]`. This could be because the picture is part of a larger program using `IO`, you are working with an `IO[Canvas]`, or you want to access the `A` value that is discarded by the `draw` variants discussed above. The `drawToIO` method does this conversion. +It can be useful to convert a `Picture[A]` to an `IO[A]`. This could be because the picture is part of a larger program using `IO`, you are working with an `Resource[IO, Canvas]`, or you want to access the `A` value that is discarded by the `draw` variants discussed above. The `drawToIO` method does this conversion. ```scala mdoc:silent picture.drawToIO() @@ -75,10 +70,16 @@ There are also variants `drawWithFrameToIO` and `drawWithCanvasToIO`. picture.drawWithFrameToIO(frame) ``` -Using `drawWithCanvasToIO` is the idiomatic way to work with an `IO[Canvas]`. +Using `drawWithCanvasToIO` requires we get hold of a `Canvas`. We call the `canvas()` method on a `Frame` to do so. This returns a `Resource[IO, Canvas]`. + +```scala mdoc:silent +val canvas: Resource[IO, Canvas] = frame.canvas() +``` + +When we have a `Resource[IO, Canvas]` we can `use` it to access the `Canvas` it manages. This is the idiomatic way to work with a `Resource[IO, Canvas]`. ```scala mdoc:silent -canvas.flatMap(c => picture.drawWithCanvasToIO(c)) +canvas.use(c => picture.drawWithCanvasToIO(c)) ``` Once you have an `IO`, you can run it in the usual way as part of an `IOApp`, with `unsafeRunSync`, or one of the other methods. diff --git a/image/jvm/src/main/scala/doodle/image/syntax/JvmImageSyntax.scala b/image/jvm/src/main/scala/doodle/image/syntax/JvmImageSyntax.scala index 6488ecf1..b5de189f 100644 --- a/image/jvm/src/main/scala/doodle/image/syntax/JvmImageSyntax.scala +++ b/image/jvm/src/main/scala/doodle/image/syntax/JvmImageSyntax.scala @@ -21,8 +21,8 @@ package syntax import cats.effect.IO import cats.effect.unsafe.IORuntime import doodle.algebra.Picture +import doodle.core.Base64 as B64 import doodle.core.format.Format -import doodle.core.{Base64 as B64} import doodle.effect.Base64Writer import doodle.effect.DefaultFrame import doodle.effect.FileWriter diff --git a/image/shared/src/main/scala/doodle/image/Image.scala b/image/shared/src/main/scala/doodle/image/Image.scala index baf963a4..8e8d70e3 100644 --- a/image/shared/src/main/scala/doodle/image/Image.scala +++ b/image/shared/src/main/scala/doodle/image/Image.scala @@ -18,7 +18,7 @@ package doodle package image import doodle.core.* -import doodle.core.font.{Font as CoreFont} +import doodle.core.font.Font as CoreFont import doodle.language.Basic sealed abstract class Image extends Product with Serializable { diff --git a/interact/shared/src/main/scala/doodle/interact/syntax/AbstractAnimationRendererSyntax.scala b/interact/shared/src/main/scala/doodle/interact/syntax/AbstractAnimationRendererSyntax.scala index 813ccab3..39d4b088 100644 --- a/interact/shared/src/main/scala/doodle/interact/syntax/AbstractAnimationRendererSyntax.scala +++ b/interact/shared/src/main/scala/doodle/interact/syntax/AbstractAnimationRendererSyntax.scala @@ -94,7 +94,7 @@ trait AbstractAnimationRendererSyntax { e: Renderer[Alg, Frame, Canvas], m: Monoid[A] ): IO[A] = - e.canvas(frame).flatMap(c => animateWithCanvasToIO(c)) + e.canvas(frame).use(c => animateWithCanvasToIO(c)) /** Create an effect that, when run, will render a `Stream` that is * generating frames an appropriate rate for animation. @@ -155,7 +155,7 @@ trait AbstractAnimationRendererSyntax { m: Monoid[A] ): IO[A] = { e.canvas(frame) - .flatMap(c => animateFramesWithCanvasToIO(c)) + .use(c => animateFramesWithCanvasToIO(c)) } /** Create an effect that, when run, will animate a source of frames that is diff --git a/interact/shared/src/test/scala/doodle/interact/animation/InterpolationSpec.scala b/interact/shared/src/test/scala/doodle/interact/animation/InterpolationSpec.scala index 0a4a0601..6531418e 100644 --- a/interact/shared/src/test/scala/doodle/interact/animation/InterpolationSpec.scala +++ b/interact/shared/src/test/scala/doodle/interact/animation/InterpolationSpec.scala @@ -21,8 +21,8 @@ package animation import cats.implicits.* import doodle.interact.syntax.all.* import munit.ScalaCheckSuite -import org.scalacheck.Prop.* import org.scalacheck.* +import org.scalacheck.Prop.* class InterpolationSpec extends ScalaCheckSuite { property("upTo empty range produces no output") { diff --git a/interact/shared/src/test/scala/doodle/interact/animation/TransducerSpec.scala b/interact/shared/src/test/scala/doodle/interact/animation/TransducerSpec.scala index 2498a04a..42ad45dc 100644 --- a/interact/shared/src/test/scala/doodle/interact/animation/TransducerSpec.scala +++ b/interact/shared/src/test/scala/doodle/interact/animation/TransducerSpec.scala @@ -20,8 +20,8 @@ package animation import cats.implicits.* import munit.ScalaCheckSuite -import org.scalacheck.Prop.* import org.scalacheck.* +import org.scalacheck.Prop.* class TransducerSpec extends ScalaCheckSuite { property("empty produces no output") { diff --git a/interact/shared/src/test/scala/doodle/interact/easing/EasingSpec.scala b/interact/shared/src/test/scala/doodle/interact/easing/EasingSpec.scala index 6f2a4587..a136312e 100644 --- a/interact/shared/src/test/scala/doodle/interact/easing/EasingSpec.scala +++ b/interact/shared/src/test/scala/doodle/interact/easing/EasingSpec.scala @@ -19,8 +19,8 @@ package interact package easing import doodle.syntax.approximatelyEqual.* -import org.scalacheck.Prop.* import org.scalacheck.* +import org.scalacheck.Prop.* object EasingSpec extends Properties("Easing properties") { property("identity is the identity") = forAllNoShrink { (t: Double) => diff --git a/java2d/src/main/scala/doodle/java2d/algebra/Graphics2DGraphicsContext.scala b/java2d/src/main/scala/doodle/java2d/algebra/Graphics2DGraphicsContext.scala index c38f566a..1827e6f1 100644 --- a/java2d/src/main/scala/doodle/java2d/algebra/Graphics2DGraphicsContext.scala +++ b/java2d/src/main/scala/doodle/java2d/algebra/Graphics2DGraphicsContext.scala @@ -21,8 +21,8 @@ package algebra import doodle.algebra.generic.* import doodle.core.PathElement import doodle.core.Point +import doodle.core.Transform as Tx import doodle.core.font.Font -import doodle.core.{Transform as Tx} import doodle.java2d.algebra.reified.GraphicsContext import java.awt.Graphics2D diff --git a/java2d/src/main/scala/doodle/java2d/algebra/Java2D.scala b/java2d/src/main/scala/doodle/java2d/algebra/Java2D.scala index ca17d3c8..65e1dfec 100644 --- a/java2d/src/main/scala/doodle/java2d/algebra/Java2D.scala +++ b/java2d/src/main/scala/doodle/java2d/algebra/Java2D.scala @@ -28,10 +28,12 @@ import doodle.core.Gradient import doodle.core.Join import doodle.core.PathElement import doodle.core.Point +import doodle.core.Transform as Tx import doodle.core.font.* -import doodle.core.{Transform as Tx} import java.awt.BasicStroke +import java.awt.Color as AwtColor +import java.awt.Font as AwtFont import java.awt.FontMetrics import java.awt.Graphics2D import java.awt.LinearGradientPaint @@ -42,8 +44,6 @@ import java.awt.geom.AffineTransform import java.awt.geom.Path2D import java.awt.geom.Point2D import java.awt.geom.Rectangle2D -import java.awt.{Color as AwtColor} -import java.awt.{Font as AwtFont} /** Various utilities for using Java2D */ object Java2D { diff --git a/java2d/src/main/scala/doodle/java2d/algebra/Java2dFromBase64.scala b/java2d/src/main/scala/doodle/java2d/algebra/Java2dFromBase64.scala index 76eeb6b9..2cc3086c 100644 --- a/java2d/src/main/scala/doodle/java2d/algebra/Java2dFromBase64.scala +++ b/java2d/src/main/scala/doodle/java2d/algebra/Java2dFromBase64.scala @@ -29,7 +29,7 @@ import doodle.core.format.* import doodle.java2d.algebra.reified.* import java.io.ByteArrayInputStream -import java.util.{Base64 as JBase64} +import java.util.Base64 as JBase64 import javax.imageio.ImageIO trait Java2dFromBase64 diff --git a/java2d/src/main/scala/doodle/java2d/algebra/reified/Reified.scala b/java2d/src/main/scala/doodle/java2d/algebra/reified/Reified.scala index 7103f48b..f41d1642 100644 --- a/java2d/src/main/scala/doodle/java2d/algebra/reified/Reified.scala +++ b/java2d/src/main/scala/doodle/java2d/algebra/reified/Reified.scala @@ -23,8 +23,8 @@ import doodle.algebra.generic.Fill import doodle.algebra.generic.Stroke import doodle.core.PathElement import doodle.core.Point +import doodle.core.Transform as Tx import doodle.core.font.Font -import doodle.core.{Transform as Tx} import java.awt.geom.Rectangle2D import java.awt.image.BufferedImage diff --git a/java2d/src/main/scala/doodle/java2d/algebra/reified/ReifiedPath.scala b/java2d/src/main/scala/doodle/java2d/algebra/reified/ReifiedPath.scala index c7ceeedc..11c4c5bb 100644 --- a/java2d/src/main/scala/doodle/java2d/algebra/reified/ReifiedPath.scala +++ b/java2d/src/main/scala/doodle/java2d/algebra/reified/ReifiedPath.scala @@ -22,7 +22,7 @@ package reified import cats.data.WriterT import doodle.algebra.generic.* import doodle.core.* -import doodle.core.{Transform as Tx} +import doodle.core.Transform as Tx trait ReifiedPath extends GenericPath[Reification] { self: Algebra { type Drawing[A] <: doodle.java2d.Drawing[A] } => diff --git a/java2d/src/main/scala/doodle/java2d/algebra/reified/ReifiedShape.scala b/java2d/src/main/scala/doodle/java2d/algebra/reified/ReifiedShape.scala index 4bdcd05a..3923ba74 100644 --- a/java2d/src/main/scala/doodle/java2d/algebra/reified/ReifiedShape.scala +++ b/java2d/src/main/scala/doodle/java2d/algebra/reified/ReifiedShape.scala @@ -22,7 +22,7 @@ package reified import cats.data.WriterT import doodle.algebra.generic.* import doodle.core.Point -import doodle.core.{Transform as Tx} +import doodle.core.Transform as Tx trait ReifiedShape extends GenericShape[Reification] { self: Algebra { type Drawing[A] <: doodle.java2d.Drawing[A] } => diff --git a/java2d/src/main/scala/doodle/java2d/algebra/reified/ReifiedText.scala b/java2d/src/main/scala/doodle/java2d/algebra/reified/ReifiedText.scala index 68666d35..d97b93c0 100644 --- a/java2d/src/main/scala/doodle/java2d/algebra/reified/ReifiedText.scala +++ b/java2d/src/main/scala/doodle/java2d/algebra/reified/ReifiedText.scala @@ -22,8 +22,8 @@ package reified import cats.data.WriterT import doodle.algebra.generic.* import doodle.core.BoundingBox +import doodle.core.Transform as Tx import doodle.core.font.Font -import doodle.core.{Transform as Tx} import java.awt.Graphics2D import java.awt.geom.Rectangle2D diff --git a/java2d/src/main/scala/doodle/java2d/effect/BlockingCircularQueue.scala b/java2d/src/main/scala/doodle/java2d/effect/BlockingCircularQueue.scala new file mode 100644 index 00000000..1349b9fd --- /dev/null +++ b/java2d/src/main/scala/doodle/java2d/effect/BlockingCircularQueue.scala @@ -0,0 +1,71 @@ +/* + * 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.java2d.effect + +import scala.reflect.ClassTag + +/** A BlockingQueue with finite capacity where writes always succeed. + * + * Writes always succeed, overwriting existing values in a FIFO manner. Reads + * may block until a value is available. This is appropriate for interact + * applications, where getting the latest data is more important than getting + * all the data. + */ +final class BlockingCircularQueue[A: ClassTag](capacity: Int) { + private val data: Array[A] = Array.ofDim(capacity) + private var writeIdx = 0 + private var readIdx = 0 + // Number of elements that can be read from data. 0 <= readable < capacity + var readable = 0 + + def add(e: A): Boolean = { + // Acquire the monitor, so we can safely modify internal state and notify + // readers waiting on it when we've added the element. + this.synchronized { + try { + data(writeIdx) = e + readable = (readable + 1).min(capacity) + + // If we just caught up to readIdx, increement readIdx so it now points + // to the oldest data + if writeIdx == readIdx && readable == capacity + then readIdx = (readIdx + 1) % capacity + + writeIdx = (writeIdx + 1) % capacity + } finally { + // Wake up any readers waiting for data + this.notifyAll() + } + } + + true + } + + def take(): A = { + this.synchronized { + while readable == 0 do { + this.wait() + } + + val elt = data(readIdx) + readIdx = (readIdx + 1) % capacity + readable = readable - 1 + + elt + } + } +} diff --git a/java2d/src/main/scala/doodle/java2d/effect/Canvas.scala b/java2d/src/main/scala/doodle/java2d/effect/Canvas.scala index 9a70c54d..fe309d49 100644 --- a/java2d/src/main/scala/doodle/java2d/effect/Canvas.scala +++ b/java2d/src/main/scala/doodle/java2d/effect/Canvas.scala @@ -19,151 +19,120 @@ package java2d package effect import cats.effect.IO -import cats.effect.std.Queue -import cats.effect.unsafe.IORuntime +import cats.effect.kernel.Resource +import cats.syntax.all.* import doodle.core.Point -import doodle.core.Transform -import fs2.Stream +import fs2.* +import fs2.concurrent.Topic -import java.awt.event.* -import java.util.concurrent.atomic.AtomicReference -import javax.swing.JFrame -import javax.swing.Timer -import javax.swing.WindowConstants +import javax.swing.SwingUtilities +import scala.concurrent.duration.* +import scala.reflect.ClassTag /** A [[Canvas]] is an area on the screen to which Pictures can be drawn. */ final class Canvas private ( frame: Frame, - redrawQueue: Queue[IO, Int], - mouseClickQueue: Queue[IO, Point], - mouseMoveQueue: Queue[IO, Point] -)(implicit runtime: IORuntime) - extends JFrame(frame.title) { - private val panel = new Java2DPanel(frame) - - /** The current global transform from logical to screen coordinates + redrawTopic: Topic[IO, Int], + mouseClickTopic: Topic[IO, Point], + mouseMoveTopic: Topic[IO, Point] +) { + + /** Construct the type of event queue we use to send events from Swing to Cats + * Effect land. We choose a circular buffer queue so that we consume + * unbounded memory if the Cats Effect side do pull from the queue (which + * will be a common case) nor does it block if the queue's capacity is + * reached. */ - private val currentInverseTx: AtomicReference[Transform] = - new AtomicReference(Transform.identity) - - /** Draw the given Picture to this [[Canvas]]. + private def eventQueue[A: ClassTag]: BlockingCircularQueue[A] = + BlockingCircularQueue(8) + + private val redrawQueue: BlockingCircularQueue[Int] = eventQueue + private val mouseClickQueue: BlockingCircularQueue[Point] = eventQueue + private val mouseMoveQueue: BlockingCircularQueue[Point] = eventQueue + + private var window: Java2dWindow = _ + + /** IO that evaluates when the underlying window has been closed. */ + private var windowClosed: IO[Boolean] = _ + SwingUtilities.invokeAndWait(() => { + window = new Java2dWindow( + frame, + 16.67.milliseconds, + redrawQueue, + mouseClickQueue, + mouseMoveQueue + ) + + windowClosed = IO.fromCompletableFuture(IO.blocking(window.closed)) + }) + + val closed: IO[Unit] = windowClosed.void + + def pump[A]( + queue: BlockingCircularQueue[A], + topic: Topic[IO, A] + ): Stream[IO, A] = + Stream.repeatEval(IO.interruptible(queue.take())).through(topic.publish) + + /** The stream that runs everything the Canvas' internals need to work. You + * must make sure this is executed if you create a Canvas by hand. */ - def render[A](picture: Picture[A]): IO[A] = { - // Possible race condition here setting the currentInverseTx - def register( - cb: Either[Throwable, Java2DPanel.RenderResult[A]] => Unit - ): Unit = { - // val drawing = picture(algebra) - // val (bb, rdr) = drawing.run(List.empty).value - // val (w, h) = Java2d.size(bb, frame.size) - - // val rr = Java2DPanel.RenderRequest(bb, w, h, rdr, cb) - panel.render(Java2DPanel.RenderRequest(picture, frame, cb)) - } - - IO.async_(register).map { result => - val inverseTx = Java2d.inverseTransform( - result.boundingBox, - result.width, - result.height, - frame.center + val stream: Stream[IO, Nothing] = { + val redraw = pump(redrawQueue, redrawTopic).drain + val mouseClick = + pump(mouseClickQueue, mouseClickTopic).debug(a => s"Mouse click $a").drain + val mouseMove = pump(mouseMoveQueue, mouseMoveTopic).drain + val closeStream = Stream + .eval( + windowClosed >> IO.println("canvas.stop begin") >> + ( + redrawTopic.close, + mouseClickTopic.close, + mouseMoveTopic.close + ).parTupled.void >> IO.println("canvas.stop end") ) - currentInverseTx.set(inverseTx) - result.value - } - } + .drain - val redraw: Stream[IO, Int] = Stream.fromQueueUnterminated(redrawQueue) - private val frameRateMs = (1000.0 * (1 / 60.0)).toInt - private val frameEvent = { - - /** Delay between frames when rendering at 60fps */ - var firstFrame = true - var lastFrameTime = 0L - new ActionListener { - def actionPerformed(e: ActionEvent): Unit = { - val now = e.getWhen() - if firstFrame then { - firstFrame = false - lastFrameTime = now - redrawQueue.offer(0).unsafeRunSync() - () - } else { - redrawQueue.offer((now - lastFrameTime).toInt).unsafeRunSync() - lastFrameTime = now - } - } - } + redraw.merge(mouseClick).merge(mouseMove).merge(closeStream) } - private val timer = new Timer(frameRateMs, frameEvent) - - val mouseClick: Stream[IO, Point] = - Stream.fromQueueUnterminated(mouseClickQueue) - - this.addMouseListener( - new MouseListener { - - def mouseClicked(e: MouseEvent): Unit = { - val pt = e.getPoint() - val inverseTx = currentInverseTx.get() - // ack - mouseClickQueue - .offer(inverseTx(Point(pt.getX(), pt.getY()))) - .unsafeRunSync() - () - } - - def mouseEntered(e: MouseEvent): Unit = () - def mouseExited(e: MouseEvent): Unit = () - def mousePressed(e: MouseEvent): Unit = () - def mouseReleased(e: MouseEvent): Unit = () - } - ) + private val interruptWhen = windowClosed.void.attempt + val redraw: Stream[IO, Int] = + redrawTopic.subscribe(4).interruptWhen(interruptWhen) + val mouseClick: Stream[IO, Point] = mouseClickTopic + .subscribe(4) + .debug(a => s"subscribed mouse click $a") + .interruptWhen(interruptWhen) val mouseMove: Stream[IO, Point] = - Stream.fromQueueUnterminated(mouseMoveQueue) - this.addMouseMotionListener( - new MouseMotionListener { - - def mouseDragged(e: MouseEvent): Unit = () - def mouseMoved(e: MouseEvent): Unit = { - val pt = e.getPoint() - val inverseTx = currentInverseTx.get() - // ack - mouseMoveQueue - .offer(inverseTx(Point(pt.getX(), pt.getY()))) - .unsafeRunSync() - () - } - } - ) - - this.addWindowListener( - new WindowAdapter { - override def windowClosed(evt: WindowEvent): Unit = - timer.stop() - } - ) - - getContentPane().add(panel) - pack() - setVisible(true) - setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE) - repaint() - timer.start() - -} + mouseMoveTopic.subscribe(4).interruptWhen(interruptWhen) -object Canvas { + /** Draw the given Picture to this [[Canvas]]. + */ + def render[A](picture: Picture[A]): IO[A] = { + println("Rendering") + val f = window.render(picture) - def apply(frame: Frame)(implicit runtime: IORuntime): IO[Canvas] = { - import cats.implicits.* - def eventQueue[A]: IO[Queue[IO, A]] = Queue.circularBuffer[IO, A](1) - (eventQueue[Int], eventQueue[Point], eventQueue[Point]).mapN { - (redrawQueue, mouseClickQueue, mouseMoveQueue) => - new Canvas(frame, redrawQueue, mouseClickQueue, mouseMoveQueue) - } + IO.fromCompletableFuture(IO(f)) } + def close(): IO[Boolean] = { + IO.println("Canvas close()") >> + IO(window.close()) >> + windowClosed + } +} +object Canvas { + def apply(frame: Frame): Resource[IO, Canvas] = { + (Topic[IO, Int], Topic[IO, Point], Topic[IO, Point]) + .mapN { (redrawTopic, mouseClickTopic, mouseMoveTopic) => + new Canvas(frame, redrawTopic, mouseClickTopic, mouseMoveTopic) + } + .toResource + .flatMap(canvas => + canvas.stream.compile.drain.background + .as(canvas) + .onFinalize(canvas.close().void) + ) + } } diff --git a/java2d/src/main/scala/doodle/java2d/effect/Java2DPanel.scala b/java2d/src/main/scala/doodle/java2d/effect/Java2DPanel.scala index cef4cb20..2f5ee36e 100644 --- a/java2d/src/main/scala/doodle/java2d/effect/Java2DPanel.scala +++ b/java2d/src/main/scala/doodle/java2d/effect/Java2DPanel.scala @@ -18,35 +18,39 @@ package doodle package java2d package effect -import cats.effect.IO -import cats.effect.unsafe.IORuntime -import doodle.algebra.generic.Finalized -import doodle.algebra.generic.* import doodle.core.BoundingBox import doodle.core.Normalized +import doodle.core.Point import doodle.core.Transform import doodle.java2d.algebra.Algebra import doodle.java2d.algebra.Java2D -import doodle.java2d.algebra.reified.Reification import doodle.java2d.algebra.reified.Reified +import doodle.java2d.effect.Size.FitToImage +import doodle.java2d.effect.Size.FixedSize import java.awt.Dimension import java.awt.Graphics import java.awt.Graphics2D -import java.util.concurrent.LinkedBlockingQueue -import java.util.concurrent.TimeUnit +import java.awt.event.* +import java.util.concurrent.CompletableFuture import javax.swing.JPanel import javax.swing.SwingUtilities +import scala.collection.mutable import scala.collection.mutable.ArrayBuffer -final class Java2DPanel(frame: Frame)(implicit runtime: IORuntime) - extends JPanel { - import Java2DPanel.RenderRequest - - /** The channel communicates between the Swing thread and outside threads +/** frameDelay is the time between rendering frames of animations. It is 1 / + * frameRate. + */ +final class Java2DPanel( + frame: Frame, + mouseClickQueue: BlockingCircularQueue[Point], + mouseMoveQueue: BlockingCircularQueue[Point] +) extends JPanel { + + /** The pictures we've been requested to render, but have not yet done so. + * Access to this should only be done via the Swing thread. */ - private val channel: LinkedBlockingQueue[RenderRequest[?]] = - new LinkedBlockingQueue(1) + private val requests: mutable.Queue[RenderRequest[?]] = mutable.Queue() /** The pictures we've rendered, along with the bounding box for each picture. * Ordered so the last element is the most recent picture (which should be @@ -62,6 +66,11 @@ final class Java2DPanel(frame: Frame)(implicit runtime: IORuntime) private val pictures: ArrayBuffer[(BoundingBox, List[Reified])] = new ArrayBuffer(1) + /** Converts from the screen coordinates to Doodle's coordinates. Must only be + * accessed from the Swing thread to avoid race conditions. + */ + private var inverseTx: Transform = Transform.identity + /** True if the redraw is an opaque color and hence we don't need to keep * earlier pictures around. */ @@ -76,16 +85,71 @@ final class Java2DPanel(frame: Frame)(implicit runtime: IORuntime) c.alpha == Normalized.MaxValue } + this.addMouseListener( + new MouseListener { + + def mouseClicked(e: MouseEvent): Unit = { + val pt = e.getPoint() + println(s"Mouse click at $pt") + mouseClickQueue + .add(inverseTx(Point(pt.getX(), pt.getY()))) + () + } + + def mouseEntered(e: MouseEvent): Unit = () + def mouseExited(e: MouseEvent): Unit = () + def mousePressed(e: MouseEvent): Unit = () + def mouseReleased(e: MouseEvent): Unit = () + } + ) + + this.addMouseMotionListener( + new MouseMotionListener { + + def mouseDragged(e: MouseEvent): Unit = () + def mouseMoved(e: MouseEvent): Unit = { + val pt = e.getPoint() + mouseMoveQueue + .add(inverseTx(Point(pt.getX(), pt.getY()))) + () + } + } + ) + + // A fixed size frame allows us to set the panel size and inverse transform + // without a picture present + frame.size match { + case FitToImage(border) => () + case FixedSize(width, height) => + setSize(width.toInt, height.toInt) + // resize(width, height) + inverseTx = Java2d.inverseTransform( + BoundingBox.centered(width, height), + width, + height, + frame.center + ) + } + def resize(width: Double, height: Double): Unit = { - setPreferredSize(new Dimension(width.toInt, height.toInt)) + setPreferredSize(Dimension(width.toInt, height.toInt)) SwingUtilities.windowForComponent(this).pack() } - def render[A](request: RenderRequest[A]): Unit = { - channel.put(request) - // println("Java2DPanel put in the channel") - this.repaint() - // println("Java2DPanel repaint request sent") + /** Queue a picture to be drawn. Returns a CompletableFuture that will be + * completed when the pictures has been drawn. + */ + def render[A](picture: Picture[A]): CompletableFuture[A] = { + val f: CompletableFuture[A] = CompletableFuture() + + // This runs on the Swing thread, and so it's safe to access requests + SwingUtilities.invokeLater { () => + val request = RenderRequest(picture, f) + requests.enqueue(request) + this.repaint() + } + + f } /** Draw all images this [[Java2DPanel]] has received. We assume the @@ -159,48 +223,27 @@ final class Java2DPanel(frame: Frame)(implicit runtime: IORuntime) val algebra = Algebra(gc) - val rr = channel.poll(10L, TimeUnit.MILLISECONDS) - if rr == null then () - else { - val result = rr.render(algebra).unsafeRunSync() + while requests.size > 0 do { + val r = requests.dequeue + val result = r.render(frame, algebra) + val bb = result.boundingBox - val picture = result.reified + val reified = result.reified + resize(result.width, result.height) if opaqueRedraw && pictures.size > 0 then - pictures.update(0, (bb, picture)) - else pictures += ((bb, picture)) - + pictures.update(0, (bb, reified)) + else pictures += ((bb, reified)) + + inverseTx = Java2d.inverseTransform( + result.boundingBox, + result.width, + result.height, + frame.center + ) } draw(gc) } -} -object Java2DPanel { - final case class RenderResult[A]( - boundingBox: BoundingBox, - width: Double, - height: Double, - reified: List[Reified], - value: A - ) - final case class RenderRequest[A]( - picture: Picture[A], - frame: Frame, - cb: Either[Throwable, RenderResult[A]] => Unit - ) { - def render(algebra: Algebra): IO[RenderResult[A]] = { - IO { - val drawing: Finalized[Reification, A] = picture(algebra) - val (bb, rdr) = drawing.run(List.empty).value - val (w, h) = Java2d.size(bb, frame.size) - val (_, fa) = rdr.run(Transform.identity).value - val (reified, a) = fa.run.value - val result = RenderResult(bb, w, h, reified, a) - cb(Right(result)) - - result - } - } - } } diff --git a/java2d/src/main/scala/doodle/java2d/effect/Java2d.scala b/java2d/src/main/scala/doodle/java2d/effect/Java2d.scala index ebe91b99..1528294f 100644 --- a/java2d/src/main/scala/doodle/java2d/effect/Java2d.scala +++ b/java2d/src/main/scala/doodle/java2d/effect/Java2d.scala @@ -23,12 +23,12 @@ import doodle.algebra.generic.* import doodle.core.BoundingBox import doodle.core.Color import doodle.core.Transform -import doodle.core.{Transform as Tx} +import doodle.core.Transform as Tx import doodle.java2d.algebra.Algebra import doodle.java2d.algebra.Graphics2DGraphicsContext +import doodle.java2d.algebra.Java2D as Java2dAlgebra import doodle.java2d.algebra.reified.Reification import doodle.java2d.algebra.reified.Reified -import doodle.java2d.algebra.{Java2D as Java2dAlgebra} import java.awt.Graphics2D import java.awt.RenderingHints diff --git a/java2d/src/main/scala/doodle/java2d/effect/Java2dBufferedImageWriter.scala b/java2d/src/main/scala/doodle/java2d/effect/Java2dBufferedImageWriter.scala index 2c0e3ff0..9bbfc489 100644 --- a/java2d/src/main/scala/doodle/java2d/effect/Java2dBufferedImageWriter.scala +++ b/java2d/src/main/scala/doodle/java2d/effect/Java2dBufferedImageWriter.scala @@ -20,7 +20,7 @@ package effect import cats.effect.IO import doodle.effect.* -import doodle.java2d.effect.{Java2d as Java2dEffect} +import doodle.java2d.effect.Java2d as Java2dEffect import java.awt.image.BufferedImage diff --git a/java2d/src/main/scala/doodle/java2d/effect/Java2dRenderer.scala b/java2d/src/main/scala/doodle/java2d/effect/Java2dRenderer.scala index ca4bd2e2..712790ba 100644 --- a/java2d/src/main/scala/doodle/java2d/effect/Java2dRenderer.scala +++ b/java2d/src/main/scala/doodle/java2d/effect/Java2dRenderer.scala @@ -19,30 +19,13 @@ package java2d package effect import cats.effect.IO +import cats.effect.Resource import doodle.effect.Renderer -import javax.swing.JFrame - object Java2dRenderer extends Renderer[Algebra, Frame, Canvas] { - - import cats.effect.unsafe.implicits.global - - private var jFrames: List[JFrame] = List.empty - - def canvas(description: Frame): IO[Canvas] = - Canvas(description).flatMap { jFrame => - IO { - jFrames.synchronized { jFrames = jFrame :: jFrames } - }.as(jFrame) - } + def canvas(description: Frame): Resource[IO, Canvas] = + Canvas(description) def render[A](canvas: Canvas)(picture: Picture[A]): IO[A] = canvas.render(picture) - - def stop(): Unit = { - jFrames.synchronized { - jFrames.foreach(_.dispose) - jFrames = List.empty - } - } } diff --git a/java2d/src/main/scala/doodle/java2d/effect/Java2dWindow.scala b/java2d/src/main/scala/doodle/java2d/effect/Java2dWindow.scala new file mode 100644 index 00000000..75358f8d --- /dev/null +++ b/java2d/src/main/scala/doodle/java2d/effect/Java2dWindow.scala @@ -0,0 +1,117 @@ +/* + * 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.java2d.effect + +import doodle.core.Point +import doodle.java2d.Picture +import doodle.java2d.effect.Size.FixedSize + +import java.awt.event.ActionEvent +import java.awt.event.ActionListener +import java.awt.event.WindowAdapter +import java.awt.event.WindowEvent +import java.util.concurrent.CompletableFuture +import javax.swing.JFrame +import javax.swing.SwingUtilities +import javax.swing.Timer +import javax.swing.WindowConstants +import scala.concurrent.duration.FiniteDuration + +/** Create a GUI window (a Frame in Swing parlance) around a Java2dPanel. */ +final class Java2dWindow( + frame: Frame, + frameDelay: FiniteDuration, + redrawQueue: BlockingCircularQueue[Int], + mouseClickQueue: BlockingCircularQueue[Point], + mouseMoveQueue: BlockingCircularQueue[Point] +) extends JFrame(frame.title) { + def render[A](picture: Picture[A]): CompletableFuture[A] = + panel.render(picture) + + /** This `CompletedFuture` is completed when this `Java2dWindow` is closed. */ + val closed: CompletableFuture[Boolean] = CompletableFuture() + + // Only call from within the event thread + private def internalClose(): Unit = { + println("window.close begin") + timer.stop() + dispose() + closed.complete(true) + println("window.close end") + } + + def close(): CompletableFuture[Boolean] = { + println("Java2dWindow close()") + SwingUtilities.invokeAndWait(() => internalClose()) + closed + } + + val panel = Java2DPanel(frame, mouseClickQueue, mouseMoveQueue) + + /** Event listener for redraw events. Translates events into the time since + * the last frame (TODO: what are the units)? + */ + private val frameEvent = { + + /** Delay between frames when rendering at 60fps */ + var firstFrame = true + var lastFrameTime = 0L + new ActionListener { + def actionPerformed(e: ActionEvent): Unit = { + val now = e.getWhen() + if firstFrame then { + firstFrame = false + lastFrameTime = now + redrawQueue.add(0) + () + } else { + redrawQueue.add((now - lastFrameTime).toInt) + lastFrameTime = now + } + } + } + } + + /** Timer that ticks every frameDelay */ + private val timer = new Timer(frameDelay.toMillis.toInt, frameEvent) + this.addWindowListener( + new WindowAdapter { + override def windowClosed(evt: WindowEvent): Unit = { + println("Java2dWindow windowClosed") + internalClose() + () + } + } + ) + + println("a") + getContentPane().add(panel) + frame.size match { + case FixedSize(width, height) => + println("b") + setSize(width.toInt, height.toInt) + case _ => () + } + println("c") + pack() + setVisible(true) + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE) + repaint() + timer.start() + println("end") + +} diff --git a/java2d/src/main/scala/doodle/java2d/effect/Java2dWriter.scala b/java2d/src/main/scala/doodle/java2d/effect/Java2dWriter.scala index 64b0aea0..adc5cef1 100644 --- a/java2d/src/main/scala/doodle/java2d/effect/Java2dWriter.scala +++ b/java2d/src/main/scala/doodle/java2d/effect/Java2dWriter.scala @@ -22,18 +22,18 @@ import cats.effect.IO import de.erichseifert.vectorgraphics2d.intermediate.CommandSequence import de.erichseifert.vectorgraphics2d.pdf.PDFProcessor import de.erichseifert.vectorgraphics2d.util.PageSize +import doodle.core.Base64 as B64 import doodle.core.BoundingBox import doodle.core.format.* -import doodle.core.{Base64 as B64} import doodle.effect.* -import doodle.java2d.effect.{Java2d as Java2dEffect} +import doodle.java2d.effect.Java2d as Java2dEffect import java.awt.image.BufferedImage import java.io.ByteArrayOutputStream import java.io.File import java.io.FileOutputStream import java.io.OutputStream -import java.util.{Base64 as JBase64} +import java.util.Base64 as JBase64 import javax.imageio.ImageIO trait Java2dWriter[Fmt <: Format] diff --git a/java2d/src/main/scala/doodle/java2d/effect/RenderRequest.scala b/java2d/src/main/scala/doodle/java2d/effect/RenderRequest.scala new file mode 100644 index 00000000..bb8b77d2 --- /dev/null +++ b/java2d/src/main/scala/doodle/java2d/effect/RenderRequest.scala @@ -0,0 +1,53 @@ +/* + * 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.java2d.effect + +import doodle.algebra.generic.Finalized +import doodle.core.Transform +import doodle.java2d.Picture +import doodle.java2d.algebra.Algebra +import doodle.java2d.algebra.reified.Reification +import doodle.java2d.algebra.reified.Reified + +import java.util.concurrent.CompletableFuture + +/** Event that is passed into Java2DPanel to request rendering of a Picture. + * This crosses the boundary between the Cats Effect and Swing threading model. + */ +private[effect] final case class RenderRequest[A]( + picture: Picture[A], + onComplete: CompletableFuture[A] +) { + + /** Convert the picture into a form that can drawn and as a side effect + * complete onComplete with the RenderResult. + */ + def render( + frame: Frame, + algebra: Algebra + ): RenderResult[A] = { + val drawing: Finalized[Reification, A] = picture(algebra) + val (bb, rdr) = drawing.run(List.empty).value + val (w, h) = Java2d.size(bb, frame.size) + val (_, fa) = rdr.run(Transform.identity).value + val (reified, a) = fa.run.value + + val result = RenderResult(reified, bb, w, h, a) + onComplete.complete(a) + result + } +} diff --git a/java2d/src/main/scala/doodle/java2d/effect/RenderResult.scala b/java2d/src/main/scala/doodle/java2d/effect/RenderResult.scala new file mode 100644 index 00000000..79eee843 --- /dev/null +++ b/java2d/src/main/scala/doodle/java2d/effect/RenderResult.scala @@ -0,0 +1,32 @@ +/* + * 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.java2d.effect + +import doodle.core.BoundingBox +import doodle.java2d.algebra.reified.Reified + +/** Event that is returned from Java2DPanel to represent result of rendering of + * a Picture. This crosses the boundary between the Cats Effect and Swing + * threading model. + */ +private[effect] final case class RenderResult[A]( + reified: List[Reified], + boundingBox: BoundingBox, + width: Double, + height: Double, + value: A +) diff --git a/java2d/src/main/scala/doodle/java2d/examples/Pointillism.scala b/java2d/src/main/scala/doodle/java2d/examples/Pointillism.scala new file mode 100644 index 00000000..48e2c074 --- /dev/null +++ b/java2d/src/main/scala/doodle/java2d/examples/Pointillism.scala @@ -0,0 +1,56 @@ +/* + * 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.java2d.examples + +import cats.effect.* +import doodle.core.* +import doodle.java2d.* +import doodle.java2d.effect.Frame +import doodle.syntax.all.* + +object Pointillism extends IOApp { + def run(args: List[String]): IO[ExitCode] = { + val frame = + Frame.default + .withSize(600, 600) + .withBackground(Color.midnightBlue) + + def curve(points: Seq[Point]): Picture[Unit] = { + OpenPath + .interpolatingSpline(points) + .path + .strokeWidth(7.0) + .strokeColor(Color.hotpink) + } + + frame + .canvas() + .use { canvas => + val clicks = canvas.mouseClick + + IO.println("We got canvas lets go") >> + clicks + .scan(List.empty[Point])((pts, pt) => pt :: pts) + .debug(a => s"Point $a") + .map(pts => curve(pts)) + .evalMap(picture => canvas.render(picture)) + .compile + .drain + .as(ExitCode.Success) + } + } +} diff --git a/java2d/src/main/scala/doodle/java2d/examples/Ripples.scala b/java2d/src/main/scala/doodle/java2d/examples/Ripples.scala index 939cb477..4dfb883f 100644 --- a/java2d/src/main/scala/doodle/java2d/examples/Ripples.scala +++ b/java2d/src/main/scala/doodle/java2d/examples/Ripples.scala @@ -84,10 +84,14 @@ object Ripples { def go(): Unit = { // frame.canvas.flatMap(canvas => ripples(canvas.mouseMove).map(_.animateToCanvas(canvas))).unsafeRunSync() - (for { - canvas <- frame.canvas() - frames <- ripples(canvas) - a <- frames.animateWithCanvasToIO(canvas) - } yield a).unsafeRunAsync(println _) + frame + .canvas() + .use(canvas => + for { + frames <- ripples(canvas) + a <- frames.animateWithCanvasToIO(canvas) + } yield a + ) + .unsafeRunAsync(println _) } } diff --git a/java2d/src/test/scala/doodle/java2d/ToPictureSuite.scala b/java2d/src/test/scala/doodle/java2d/ToPictureSuite.scala index 01f6ab1d..508cd902 100644 --- a/java2d/src/test/scala/doodle/java2d/ToPictureSuite.scala +++ b/java2d/src/test/scala/doodle/java2d/ToPictureSuite.scala @@ -19,8 +19,8 @@ package java2d import cats.effect.unsafe.implicits.global import doodle.algebra.ToPicture +import doodle.core.Base64 as B64 import doodle.core.format.* -import doodle.core.{Base64 as B64} import doodle.effect.* import doodle.syntax.all.* import munit.FunSuite diff --git a/java2d/src/test/scala/doodle/java2d/effect/BlockingCircularQueueSuite.scala b/java2d/src/test/scala/doodle/java2d/effect/BlockingCircularQueueSuite.scala new file mode 100644 index 00000000..fb189de1 --- /dev/null +++ b/java2d/src/test/scala/doodle/java2d/effect/BlockingCircularQueueSuite.scala @@ -0,0 +1,51 @@ +/* + * 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.java2d.effect + +import munit.FunSuite + +class BlockingCircularQueueSuite extends FunSuite { + test("Sequential access returns expected values") { + val q = BlockingCircularQueue[Int](4) + q.add(1) + q.add(2) + q.add(3) + q.add(4) + + assertEquals(1, q.take()) + assertEquals(2, q.take()) + assertEquals(3, q.take()) + assertEquals(4, q.take()) + + q.add(5) + q.add(6) + + assertEquals(5, q.take()) + assertEquals(6, q.take()) + + q.add(7) + q.add(8) + q.add(9) + q.add(10) + q.add(11) + + assertEquals(8, q.take()) + assertEquals(9, q.take()) + assertEquals(10, q.take()) + assertEquals(11, q.take()) + } +} diff --git a/project/plugins.sbt b/project/plugins.sbt index 30967139..c546bac6 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -3,5 +3,5 @@ addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.4") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") addSbtPlugin("com.github.sbt" % "sbt-unidoc" % "0.5.0") addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.12.1") -addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.7.2") -addSbtPlugin("org.typelevel" % "sbt-typelevel-site" % "0.7.2") +addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.7.3") +addSbtPlugin("org.typelevel" % "sbt-typelevel-site" % "0.7.3") diff --git a/reactor/shared/src/main/scala/doodle/reactor/BaseReactor.scala b/reactor/shared/src/main/scala/doodle/reactor/BaseReactor.scala index 75bc4ece..fef441ff 100644 --- a/reactor/shared/src/main/scala/doodle/reactor/BaseReactor.scala +++ b/reactor/shared/src/main/scala/doodle/reactor/BaseReactor.scala @@ -140,20 +140,24 @@ trait BaseReactor[A] { } ( - for - canvas <- frame.canvas[Alg, Canvas]() - 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(_ => ()) + 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 { diff --git a/svg/js/src/main/scala/doodle/svg/algebra/Text.scala b/svg/js/src/main/scala/doodle/svg/algebra/Text.scala index 988a8d95..49a19b44 100644 --- a/svg/js/src/main/scala/doodle/svg/algebra/Text.scala +++ b/svg/js/src/main/scala/doodle/svg/algebra/Text.scala @@ -20,8 +20,8 @@ package algebra import doodle.algebra.generic.* import doodle.core.BoundingBox +import doodle.core.Transform as Tx import doodle.core.font.Font -import doodle.core.{Transform as Tx} import org.scalajs.dom.svg.Rect import scalatags.JsDom.svgAttrs diff --git a/svg/js/src/main/scala/doodle/svg/effect/SvgRenderer.scala b/svg/js/src/main/scala/doodle/svg/effect/SvgRenderer.scala index ff69d274..4e7e4989 100644 --- a/svg/js/src/main/scala/doodle/svg/effect/SvgRenderer.scala +++ b/svg/js/src/main/scala/doodle/svg/effect/SvgRenderer.scala @@ -19,14 +19,15 @@ package svg package effect import cats.effect.IO +import cats.effect.Resource import doodle.effect.Renderer object SvgRenderer extends Renderer[Algebra, Frame, Canvas] { import cats.effect.unsafe.implicits.global - def canvas(description: Frame): IO[Canvas] = - Canvas.fromFrame(description) + def canvas(description: Frame): Resource[IO, Canvas] = + Canvas.fromFrame(description).toResource def render[A](canvas: Canvas)(picture: Picture[A]): IO[A] = canvas.render(picture) diff --git a/svg/js/src/main/scala/doodle/svg/examples/ConcentricCircles.scala b/svg/js/src/main/scala/doodle/svg/examples/ConcentricCircles.scala index 728bc0b6..c58c38c0 100644 --- a/svg/js/src/main/scala/doodle/svg/examples/ConcentricCircles.scala +++ b/svg/js/src/main/scala/doodle/svg/examples/ConcentricCircles.scala @@ -31,8 +31,7 @@ object ConcentricCircles extends IOApp.Simple { .under(circles(count - 1)) val run = - for { - canvas <- Frame("svg-root").canvas() - a <- circles(10).drawWithCanvasToIO(canvas) - } yield a + Frame("svg-root") + .canvas() + .use(canvas => circles(10).drawWithCanvasToIO(canvas)) } diff --git a/svg/jvm/src/main/scala/doodle/svg/algebra/Text.scala b/svg/jvm/src/main/scala/doodle/svg/algebra/Text.scala index 5f530bfa..5f22a450 100644 --- a/svg/jvm/src/main/scala/doodle/svg/algebra/Text.scala +++ b/svg/jvm/src/main/scala/doodle/svg/algebra/Text.scala @@ -18,11 +18,11 @@ package doodle package svg package algebra -import doodle.algebra.generic.Finalized import doodle.algebra.generic.* +import doodle.algebra.generic.Finalized import doodle.core.* +import doodle.core.Transform as Tx import doodle.core.font.Font -import doodle.core.{Transform as Tx} import java.awt.geom.Rectangle2D import scala.collection.mutable diff --git a/svg/jvm/src/main/scala/doodle/svg/effect/SvgWriter.scala b/svg/jvm/src/main/scala/doodle/svg/effect/SvgWriter.scala index cad95b3b..fa356463 100644 --- a/svg/jvm/src/main/scala/doodle/svg/effect/SvgWriter.scala +++ b/svg/jvm/src/main/scala/doodle/svg/effect/SvgWriter.scala @@ -20,13 +20,13 @@ package effect import cats.effect.IO import doodle.algebra.Picture +import doodle.core.Base64 as B64 import doodle.core.format -import doodle.core.{Base64 as B64} import doodle.effect.* import java.io.File import java.nio.file.Files -import java.util.{Base64 as JBase64} +import java.util.Base64 as JBase64 object SvgWriter extends FileWriter[Algebra, Frame, format.Svg] diff --git a/svg/shared/src/main/scala/doodle/svg/algebra/Path.scala b/svg/shared/src/main/scala/doodle/svg/algebra/Path.scala index d072f6fe..6b6bb334 100644 --- a/svg/shared/src/main/scala/doodle/svg/algebra/Path.scala +++ b/svg/shared/src/main/scala/doodle/svg/algebra/Path.scala @@ -20,7 +20,7 @@ package algebra import doodle.algebra.generic.* import doodle.core.PathElement -import doodle.core.{Transform as Tx} +import doodle.core.Transform as Tx import scala.collection.mutable diff --git a/svg/shared/src/main/scala/doodle/svg/algebra/Shape.scala b/svg/shared/src/main/scala/doodle/svg/algebra/Shape.scala index b58d6aa6..e1440b7c 100644 --- a/svg/shared/src/main/scala/doodle/svg/algebra/Shape.scala +++ b/svg/shared/src/main/scala/doodle/svg/algebra/Shape.scala @@ -20,7 +20,7 @@ package algebra import doodle.algebra.generic.* import doodle.core.Point -import doodle.core.{Transform as Tx} +import doodle.core.Transform as Tx import scala.collection.mutable diff --git a/svg/shared/src/main/scala/doodle/svg/algebra/Svg.scala b/svg/shared/src/main/scala/doodle/svg/algebra/Svg.scala index 045ada45..f2cf72d1 100644 --- a/svg/shared/src/main/scala/doodle/svg/algebra/Svg.scala +++ b/svg/shared/src/main/scala/doodle/svg/algebra/Svg.scala @@ -24,8 +24,8 @@ import doodle.algebra.Picture import doodle.algebra.generic.Fill import doodle.algebra.generic.Stroke import doodle.core.* -import doodle.core.font.FontSize.Points import doodle.core.font.* +import doodle.core.font.FontSize.Points import doodle.svg.effect.Size import scala.collection.mutable diff --git a/turtle/shared/src/main/scala/doodle/turtle/examples/Geometry.scala b/turtle/shared/src/main/scala/doodle/turtle/examples/Geometry.scala index aa987791..3d5af790 100644 --- a/turtle/shared/src/main/scala/doodle/turtle/examples/Geometry.scala +++ b/turtle/shared/src/main/scala/doodle/turtle/examples/Geometry.scala @@ -20,8 +20,8 @@ package examples import doodle.core.* import doodle.syntax.all.* -import doodle.turtle.Instruction.* import doodle.turtle.* +import doodle.turtle.Instruction.* object Geometry { val instructions =