Browse Source

Added store pattern using monix

also added relevant implicits to use it
master
Rohan Sircar 5 months ago
parent
commit
b988ad267e
  1. 2
      .gitignore
  2. 56
      build.sbt
  3. 2
      project/build.properties
  4. 1
      project/plugin.sbt
  5. 58
      src/main/scala/nova/monadic_sfx/Main.scala
  6. 159
      src/main/scala/nova/monadic_sfx/MainApp.scala
  7. 2
      src/main/scala/nova/monadic_sfx/MainModule.scala
  8. 9
      src/main/scala/nova/monadic_sfx/Types.scala
  9. 70
      src/main/scala/nova/monadic_sfx/actors/ActorModule.scala
  10. 20
      src/main/scala/nova/monadic_sfx/actors/TestActor.scala
  11. 26
      src/main/scala/nova/monadic_sfx/executors/GUIExecutor.scala
  12. 25
      src/main/scala/nova/monadic_sfx/executors/Schedulers.scala
  13. 4
      src/main/scala/nova/monadic_sfx/http/HttpModule.scala
  14. 38
      src/main/scala/nova/monadic_sfx/http/requests/DummyRequest.scala
  15. 205
      src/main/scala/nova/monadic_sfx/implicits/JavaFxMonixObservables.scala
  16. 50
      src/main/scala/nova/monadic_sfx/implicits/package.scala
  17. 7
      src/main/scala/nova/monadic_sfx/models/DummyModels.scala
  18. 65
      src/main/scala/nova/monadic_sfx/ui/DefaultUI.scala
  19. 144
      src/main/scala/nova/monadic_sfx/ui/MyFxApp.scala
  20. 129
      src/main/scala/nova/monadic_sfx/ui/MyFxAppOld.scala
  21. 18
      src/main/scala/nova/monadic_sfx/ui/UiModule.scala
  22. 556
      src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListComponent.scala
  23. 95
      src/main/scala/nova/monadic_sfx/ui/controller/TodoController.scala
  24. 28
      src/main/scala/nova/monadic_sfx/ui/screens/HomeScreen.scala
  25. 34
      src/main/scala/nova/monadic_sfx/ui/screens/LoginScreen.scala
  26. 2
      src/main/scala/nova/monadic_sfx/ui/screens/Screen.scala
  27. 4
      src/main/scala/nova/monadic_sfx/util/Action.scala
  28. 21
      src/main/scala/nova/monadic_sfx/util/IOUtils.scala
  29. 76
      src/main/scala/org/slf4j/impl/StaticLoggerBuilder.scala

2
.gitignore

@ -22,3 +22,5 @@ metals.sbt
.idea/
.vscode
/project/project
.bsp

56
build.sbt

@ -5,7 +5,7 @@ name := "ScalaFX Hello World"
version := "14-R19"
// Version of Scala used by the project
scalaVersion := "2.13.3"
scalaVersion := "2.13.4"
// Add dependency on ScalaFX library
libraryDependencies += "org.scalafx" %% "scalafx" % "14-R19"
@ -17,30 +17,53 @@ libraryDependencies ++= Seq(
"org.typelevel" %% "cats-core" % "2.1.1",
"org.typelevel" %% "cats-effect" % "2.1.4",
"io.monix" %% "monix" % "3.2.2",
"io.monix" %% "monix-bio" % "1.0.0",
"io.monix" %% "monix-bio" % "1.1.0",
"io.circe" %% "circe-core" % "0.13.0",
"io.circe" %% "circe-generic" % "0.13.0",
"com.softwaremill.sttp.client" %% "core" % "2.2.5",
"com.softwaremill.sttp.client" %% "monix" % "2.2.5",
"com.softwaremill.sttp.client" %% "circe" % "2.2.5",
"com.softwaremill.sttp.client" %% "async-http-client-backend-monix" % "2.2.5",
"com.softwaremill.sttp.client" %% "core" % "2.2.9",
"com.softwaremill.sttp.client" %% "monix" % "2.2.9",
"com.softwaremill.sttp.client" %% "circe" % "2.2.9",
"com.softwaremill.sttp.client" %% "async-http-client-backend-monix" % "2.2.9",
"com.softwaremill.sttp.client" %% "httpclient-backend-monix" % "2.2.9",
"com.github.valskalla" %% "odin-monix" % "0.8.1",
"com.typesafe.akka" %% "akka-actor-typed" % "2.6.8",
"com.softwaremill.macwire" %% "util" % "2.3.7",
"com.softwaremill.macwire" %% "macros" % "2.3.6" % "provided",
"com.softwaremill.macwire" %% "macrosakka" % "2.3.6" % "provided",
"com.github.valskalla" %% "odin-slf4j" % "0.8.1"
"com.github.valskalla" %% "odin-slf4j" % "0.8.1",
"com.github.valskalla" %% "odin-json" % "0.9.1",
"com.typesafe.scala-logging" %% "scala-logging" % "3.9.2",
"com.jfoenix" % "jfoenix" % "9.0.10",
"org.kordamp.ikonli" % "ikonli-core" % "12.0.0",
"org.kordamp.ikonli" % "ikonli-javafx" % "12.0.0",
"org.kordamp.ikonli" % "ikonli-fontawesome5-pack" % "12.0.0",
"org.kordamp.ikonli" % "ikonli-material-pack" % "12.0.0",
"io.github.typhon0" % "AnimateFX" % "1.2.1",
"com.beachape" %% "enumeratum" % "1.6.1",
"com.chuusai" %% "shapeless" % "2.3.3",
"org.gerweck.scalafx" %% "scalafx-utils" % "0.15.0"
)
scalacOptions ++= Seq(
"-unchecked",
"-deprecation",
"-Xcheckinit",
"-encoding",
"utf8",
"UTF-8",
"-deprecation",
"-feature",
"-Ywarn-unused:imports"
"-language:existentials",
"-language:experimental.macros",
"-language:higherKinds",
"-language:implicitConversions",
"-unchecked",
"-Xlint",
"-Ywarn-numeric-widen",
"-Ymacro-annotations",
//silence warnings for by-name implicits
"-Wconf:cat=lint-byname-implicit:s",
//give errors on non exhaustive matches
"-Wconf:msg=match may not be exhaustive:e",
"-explaintypes" // Explain type errors in more detail.
)
javacOptions ++= Seq("-source", "11", "-target", "11")
// Fork a new JVM for 'run' and 'test:run', to avoid JavaFX double initialization problems
fork := true
@ -59,3 +82,12 @@ lazy val javaFXModules =
libraryDependencies ++= javaFXModules.map(m =>
"org.openjfx" % s"javafx-$m" % "14.0.1" classifier osName
)
addCompilerPlugin("com.olegpy" %% "better-monadic-for" % "0.3.1")
ThisBuild / scalafixDependencies += "com.github.liancheng" %% "organize-imports" % "0.4.3"
inThisBuild(
List(
scalaVersion := scalaVersion.value, // 2.11.12, or 2.13.3
semanticdbEnabled := true, // enable SemanticDB
semanticdbVersion := "4.4.2" // use Scalafix compatible version
)
)

2
project/build.properties

@ -1,2 +1,2 @@
sbt.version=1.3.10
sbt.version=1.4.3

1
project/plugin.sbt

@ -4,3 +4,4 @@ scalacOptions ++= Seq("-unchecked", "-deprecation")
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "5.2.4")
// addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.10")
addSbtPlugin("com.quadstingray" % "sbt-javafx" % "1.5.2")
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.23")

58
src/main/scala/nova/monadic_sfx/Main.scala

@ -1,37 +1,39 @@
package nova.monadic_sfx
import monix.eval.Task
// import sttp.client.asynchttpclient.monix.AsyncHttpClientMonixBackend
// import sttp.client._
// import sttp.client.circe._
// import io.circe.generic.auto._
import nova.monadic_sfx.executors._
import cats.effect.Resource
import sttp.client.asynchttpclient.monix.AsyncHttpClientMonixBackend
import io.odin.syntax._
import io.odin.monix._
import monix.eval.TaskApp
import scala.concurrent.duration._
import _root_.monix.bio.BIOApp
import _root_.monix.bio.Task
import _root_.monix.bio.UIO
import cats.effect.ExitCode
import cats.effect.Resource
import cats.implicits._
import com.softwaremill.macwire._
import io.odin._
import io.odin.syntax._
import nova.monadic_sfx.executors._
import nova.monadic_sfx.util.IOUtils._
import sttp.client.httpclient.monix.HttpClientMonixBackend
object Main extends MainModule with BIOApp {
object Main extends MainModule with TaskApp {
override def run(args: List[String]): Task[ExitCode] = {
// val startTime = Task.clock
// .monotonic(scala.concurrent.duration.MILLISECONDS)
// .map(Duration.fromNanos(_))
lazy val appResource = for {
// clock <- Resource.liftF(Task(Task.clock))
logger <- consoleLogger().withAsync()
backend <- AsyncHttpClientMonixBackend.resource()
def appResource(startTime: Long) =
for {
implicit0(logger: Logger[Task]) <-
consoleLogger().withAsync(timeWindow = 1.millis) |+| fileLogger(
"application.log"
).withAsync()
schedulers = new Schedulers()
backend <- Resource.make(
toIO(HttpClientMonixBackend()(schedulers.async))
)(c => toIO(c.close()))
actorSystem <- actorSystemResource(logger)
reqs <- Resource.liftF(Task(wireWith(requesters _)))
schedulers <- Resource.liftF(Task(new Schedulers()))
fxApp <- wireWith(fxAppResource _)
} yield (fxApp)
appResource
.use(fxApp => Task(fxApp.main(args.toArray)))
_ <- Resource.liftF(wire[MainApp].program)
} yield ()
override def run(args: List[String]): UIO[ExitCode] =
appResource(System.currentTimeMillis())
.use(_ => Task.unit)
.onErrorHandle(_.printStackTrace())
.as(ExitCode.Success)
}
}

159
src/main/scala/nova/monadic_sfx/MainApp.scala

@ -0,0 +1,159 @@
package nova.monadic_sfx
import com.softwaremill.macwire._
import io.odin.Logger
import monix.bio.Task
import monix.catnap.ConcurrentChannel
import nova.monadic_sfx.executors.Schedulers
import nova.monadic_sfx.implicits.JFXButton
import nova.monadic_sfx.implicits.JavaFXMonixObservables._
import nova.monadic_sfx.ui.MyFxApp
import nova.monadic_sfx.ui.components.todo.Todo
import nova.monadic_sfx.ui.components.todo.TodoListComponent
import nova.monadic_sfx.ui.components.todo.TodoListView
import nova.monadic_sfx.util.IOUtils._
import org.gerweck.scalafx.util._
import scalafx.Includes._
import scalafx.application.JFXApp.PrimaryStage
import scalafx.beans.property.ObjectProperty
import scalafx.beans.property.StringProperty
import scalafx.collections.ObservableBuffer
import scalafx.geometry.Insets
import scalafx.scene.Scene
import scalafx.scene.control.TableColumn
import scalafx.scene.control.TableView
import scalafx.scene.layout.HBox
import scalafx.scene.paint.Color
import scalafx.scene.shape.Rectangle
class MainApp(
// spawnProtocol: ActorSystem[SpawnProtocol.Command],
schedulers: Schedulers,
startTime: Long
)(implicit logger: Logger[Task]) {
lazy val addTodoButton = new JFXButton {
text = "Add"
}
lazy val addTodoObs = addTodoButton.observableAction()
lazy val todoListView = TodoListView.defaultListView
lazy val _scene = new Scene {
root = new HBox {
padding = Insets(20)
content = new Rectangle {
width = 400
height = 200
fill = Color.DeepSkyBlue
}
children ++= Seq(
new JFXButton {
text = "DummyButton"
},
new JFXButton {
text = "DummyButton2"
},
addTodoButton,
Test.ttv
// todoListView
)
}
}
private lazy val stage = new PrimaryStage {
title = "Simple ScalaFX App"
scene = _scene
width = 800
height = 400
}
// implicit val l = logger
// implicit val sp = spawnProtocol
val program = for {
(fxApp, fxAppFib) <- wire[MyFxApp].init(stage)
// _ <- Task(fxApp.stage = stage)
// .executeOn(schedulers.fx)
// .delayExecution(2000.millis)
todoComponent <- createTodoComponent
_ <- toIO(
addTodoObs
.mapEval(_ =>
toTask(todoComponent.send(TodoListComponent.Add(Todo(1, "blah"))))
)
.completedL
.executeOn(schedulers.fx)
.startAndForget
)
_ <- logger.info(
s"Application started in ${(System.currentTimeMillis() - startTime) / 1000f} seconds"
)
_ <- fxAppFib.join
} yield ()
def createTodoComponent: Task[TodoListComponent] = {
for {
channel <-
ConcurrentChannel
.of[Task, TodoListComponent.Complete, TodoListComponent.Command]
scheduler = schedulers.fx
(lv, delObs, editObs) <-
TodoListView.defaultListView2.executeOn(scheduler)
todoLV = new TodoListView(lv)
todoComponent <- wire[TodoListComponent.Props].create
// TODO make this a "message pass" instead of mutating directly
_ <- Task(_scene.getChildren += lv).executeOn(scheduler)
_ <- toIO(
delObs
.doOnNext(_ => toTask(logger.debug("Pressed delete")))
.doOnNext(todo =>
toTask(
for {
_ <- logger.debug(s"Got todo $todo")
_ <- todoComponent.send(TodoListComponent.Delete(todo.id))
// _ <- Task.sequence(
// lst.map(todo =>
// todoComponent.send(TodoListComponent.Delete(todo.id))
// )
// )
} yield ()
)
)
.completedL
).startAndForget
_ <- toIO(
editObs
.doOnNext(_ => toTask(logger.debug("Pressed edit")))
.completedL
).startAndForget
} yield todoComponent
}
}
class TestModel(_name: String, _age: Int) {
val name = StringProperty(_name).readOnly
val age = ObjectProperty(_age).readOnly
}
object Test {
val items = ObservableBuffer(
new TestModel("hmm", 1),
new TestModel("hmm2", 2)
)
val ttv = new TableView[TestModel](items) {
columns ++= Seq(
new TableColumn[TestModel, String] {
text = "Name"
cellValueFactory = { _.value.name }
},
new TableColumn[TestModel, Int] {
text = "Age"
cellValueFactory = { _.value.age }
}
)
}
}

2
src/main/scala/nova/monadic_sfx/MainModule.scala

@ -1,7 +1,7 @@
package nova.monadic_sfx
import nova.monadic_sfx.actors.ActorModule
import nova.monadic_sfx.ui.UiModule
import nova.monadic_sfx.http.HttpModule
import nova.monadic_sfx.ui.UiModule
trait MainModule extends ActorModule with UiModule with HttpModule

9
src/main/scala/nova/monadic_sfx/Types.scala

@ -1,13 +1,14 @@
package nova.monadic_sfx
import java.nio.ByteBuffer
import monix.eval.Task
import sttp.client.SttpBackend
import monix.reactive.Observable
import sttp.client.SttpBackend
import sttp.client.asynchttpclient.WebSocketHandler
import java.nio.ByteBuffer
trait AppTypes {}
object AppTypes {
trait AppTypes {
type HttpBackend =
SttpBackend[Task, Observable[ByteBuffer], WebSocketHandler]
}
object AppTypes extends AppTypes {}

70
src/main/scala/nova/monadic_sfx/actors/ActorModule.scala

@ -1,74 +1,28 @@
package nova.monadic_sfx.actors
import io.odin.Logger
import monix.eval.Task
import cats.effect.Resource
import akka.actor.typed.scaladsl.Behaviors
import com.softwaremill.macwire._
import akka.util.Timeout
import scala.concurrent.duration._
import scala.concurrent.Future
import akka.actor.typed._
import akka.actor.typed.scaladsl.AskPattern._
import scala.concurrent.Await
import nova.monadic_sfx.executors.Schedulers
import akka.util.Timeout
import cats.effect.Resource
import io.odin.Logger
import monix.bio.Task
trait ActorModule {
import scala.concurrent.ExecutionContext
implicit val timeout: Timeout = Timeout(3.seconds)
implicit def timeout: Timeout = Timeout(3.seconds)
def actorSystemResource(
logger: Logger[Task]
): Resource[Task, ActorSystem[SpawnProtocol.Command]] =
Resource.make(logger.info("Creating Actor System") >> Task {
ActorSystem(HelloWorldMain(), name = "FXActorSystem")
ActorSystem(SpawnProtocol(), name = "FXActorSystem")
})(sys =>
logger.info("Shutting down actor system") >> Task(
sys.terminate()
) >> logger.info("Actor System terminated")
for {
_ <- Task(sys.terminate())
_ <- Task.fromFuture(sys.whenTerminated)
_ <- logger.info("Actor System Terminated")
} yield ()
)
// def actorsResource(
// system: ActorSystem[SpawnProtocol.Command],
// logger: Logger[Task],
// schedulers: Schedulers
// ): Resource[Task, Task[ActorRef[Counter.Command]]] = {
// implicit val ec: ExecutionContext = system.executionContext
// implicit val scheduler = system.scheduler
// Resource.make(
// Task {
// val actor = Task.deferFuture {
// system.ask[ActorRef[Counter.Command]](
// SpawnProtocol.Spawn(
// behavior = Counter(),
// name = "counterActor",
// // DispatcherSelector.fromConfig("javafx-dispatcher"),
// // Props.empty,
// _
// )
// )
// }
// // system.
// actor
// }
// )(actorTask =>
// for {
// actor <- actorTask
// _ <- logger.info("Stopping actor counter")
// t <- Task(actor ! Counter.Stop)
// _ <- logger.info("Counter actor stopped")
// } yield ()
// )
// }
}
object HelloWorldMain {
def apply(): Behavior[SpawnProtocol.Command] =
Behaviors.setup { context =>
// Start initial tasks
// context.spawn(...)
SpawnProtocol()
}
}

20
src/main/scala/nova/monadic_sfx/actors/TestActor.scala

@ -36,15 +36,13 @@ class Counter(context: ActorContext[Counter.Command])
}
}
override def onSignal: PartialFunction[Signal, Behavior[Counter.Command]] =
PartialFunction.fromFunction((signal: Signal) => {
signal match {
case _: Terminated =>
context.log.info("Recieved shutdown counter actor terminated")
this
case PostStop =>
context.log.info("Recieved shutdown counter actor poststop")
this
}
})
override def onSignal: PartialFunction[Signal, Behavior[Counter.Command]] = {
case _: Terminated =>
context.log.info("Recieved shutdown counter actor terminated")
this
case PostStop =>
context.log.info("Recieved shutdown counter actor poststop")
this
}
}

26
src/main/scala/nova/monadic_sfx/executors/GUIExecutor.scala

@ -1,23 +1,21 @@
package nova.monadic_sfx.executors
import akka.dispatch.{
DispatcherPrerequisites,
ExecutorServiceFactory,
ExecutorServiceConfigurator
}
import com.typesafe.config.Config
import java.util.concurrent.{
ExecutorService,
AbstractExecutorService,
ThreadFactory,
TimeUnit
}
import java.util.Collections
import java.util.concurrent.AbstractExecutorService
import java.util.concurrent.Executor
import java.util.concurrent.ExecutorService
import java.util.concurrent.ThreadFactory
import java.util.concurrent.TimeUnit
import javax.swing.SwingUtilities
import scala.concurrent.ExecutionContext
import akka.dispatch.DispatcherPrerequisites
import akka.dispatch.ExecutorServiceConfigurator
import akka.dispatch.ExecutorServiceFactory
import com.typesafe.config.Config
import javafx.application.Platform
import monix.execution.Scheduler
import scala.concurrent.ExecutionContext
import java.util.concurrent.Executor
// First we wrap invokeLater/runLater as an ExecutorService
trait GUIExecutorService extends AbstractExecutorService {

25
src/main/scala/nova/monadic_sfx/executors/Schedulers.scala

@ -1,9 +1,28 @@
package nova.monadic_sfx.executors
import com.typesafe.scalalogging.Logger
import monix.execution.Scheduler
import monix.execution.UncaughtExceptionReporter
import monix.execution.schedulers.TracingScheduler
class Schedulers(
val blockingIO: Scheduler = Scheduler.io(),
val cpu: Scheduler = Scheduler.global,
val fx: Scheduler = JFXExecutionContexts.fxScheduler
val blocking: Scheduler = TracingScheduler(
Scheduler
.io()
.withUncaughtExceptionReporter(Schedulers.reporter)
),
val async: Scheduler = Scheduler.traced
.withUncaughtExceptionReporter(Schedulers.reporter),
val fx: Scheduler = TracingScheduler(
JFXExecutionContexts.fxScheduler
.withUncaughtExceptionReporter(Schedulers.reporter)
)
)
object Schedulers {
val reporter = UncaughtExceptionReporter { ex =>
val logger = Logger[Schedulers]
logger.error("Uncaught exception", ex)
}
}

4
src/main/scala/nova/monadic_sfx/http/HttpModule.scala

@ -1,8 +1,8 @@
package nova.monadic_sfx.http
import nova.monadic_sfx.http.requests.DummyRequest
import nova.monadic_sfx.AppTypes
import akka.actor.typed._
import nova.monadic_sfx.AppTypes
import nova.monadic_sfx.http.requests.DummyRequest
trait HttpModule {
def requesters(

38
src/main/scala/nova/monadic_sfx/http/requests/DummyRequest.scala

@ -1,43 +1,17 @@
package nova.monadic_sfx.http.requests
import nova.monadic_sfx.AppTypes
import nova.monadic_sfx.AppTypes.HttpBackend
import monix.eval.Task
import nova.monadic_sfx.models._
import sttp.client._
import sttp.client.circe._
import io.circe.generic.auto._
import nova.monadic_sfx.models._
import cats.data.EitherT
class DummyRequest(backend: HttpBackend) extends AppTypes {
private implicit val _backend = backend
def send() = {
Task
.suspend {
for {
req <-
basicRequest
.get(uri"https://httpbin.org/get")
.response(asJson[HttpBinResponse])
.send()
} yield (req)
}
}
def send =
basicRequest
.get(uri"https://httpbin.org/get")
.response(asJson[HttpBinResponse])
.send()
def test() = {
for {
res <- send()
res3 <- Task { res.body }
res2 <- Task {
res3.fold(
err => {
err.toString()
},
value => value.toString()
)
}
} yield (res3)
}
}

205
src/main/scala/nova/monadic_sfx/implicits/JavaFxMonixObservables.scala

@ -0,0 +1,205 @@
package nova.monadic_sfx.implicits
import javafx.beans.property.ObjectProperty
import javafx.collections.ObservableList
import javafx.scene.{input => jfxsi}
import javafx.{event => jfxe}
import monix.bio.Task
import monix.execution.Ack
import monix.execution.Cancelable
import monix.execution.Scheduler
import monix.reactive.Observable
import monix.reactive.Observer
import monix.reactive.OverflowStrategy
import monix.tail.Iterant
import monix.{eval => me}
import scalafx.Includes._
import scalafx.beans.property.Property
import scalafx.beans.value.ObservableValue
import scalafx.collections.ObservableBuffer
import scalafx.scene.Scene
import scalafx.scene.control.ButtonBase
object JavaFXMonixObservables {
implicit final class SceneObservables(private val scene: Scene)
extends AnyVal {
def observableMousePressed(): Observable[jfxsi.MouseEvent] = {
import monix.execution.cancelables.SingleAssignCancelable
Observable.create(OverflowStrategy.Unbounded) { sub =>
val c = SingleAssignCancelable()
val l = new jfxe.EventHandler[jfxsi.MouseEvent] {
override def handle(event: jfxsi.MouseEvent): Unit = {
if (sub.onNext(event) == Ack.Stop) c.cancel()
}
}
scene.onMousePressed = l
c := Cancelable(() =>
scene.removeEventHandler(
jfxsi.MouseEvent.MOUSE_PRESSED,
l
)
)
c
}
}
def observableMouseDragged(): Observable[jfxsi.MouseEvent] = {
import monix.execution.cancelables.SingleAssignCancelable
Observable.create(OverflowStrategy.Unbounded) { sub =>
val c = SingleAssignCancelable()
val l = new jfxe.EventHandler[jfxsi.MouseEvent] {
override def handle(event: jfxsi.MouseEvent): Unit = {
if (sub.onNext(event) == Ack.Stop) c.cancel()
}
}
scene.onMouseDragged = l
c := Cancelable(() =>
scene.removeEventHandler(
jfxsi.MouseEvent.MOUSE_DRAGGED,
l
)
)
c
}
}
}
// implicit final class BindObs[T, J](private val prop: Property[T, J])
// extends AnyVal {
// def -->(op: Observer[T]) = {
// op.onNext(prop.value)
// }
// def <--(obs: Observable[T])(implicit s: Scheduler) = {
// obs.doOnNext(v => me.Task(prop.value = v)).subscribe()
// }
// def observableChange[J1 >: J]()
// : Observable[(ObservableValue[T, J], J1, J1)] = {
// import monix.execution.cancelables.SingleAssignCancelable
// Observable.create(OverflowStrategy.Unbounded) { sub =>
// val c = SingleAssignCancelable()
// val canc = prop.onChange((a, b, c) => sub.onNext((a, b, c)))
// c := Cancelable(() => canc.cancel())
// c
// }
// }
// }
implicit final class BindObs2[A](private val prop: ObjectProperty[A])
extends AnyVal {
// def -->(sub: Var[A]) =
// prop.onChange((a, b, c) => sub := c)
def -->(sub: Observer[A]) =
prop.onChange((a, b, c) => if (c != null) sub.onNext(c))
// def -->[J1 >: A, T](
// op: Observable[J1] => me.Task[T]
// )(implicit s: Scheduler) = {
// op(prop.observableChange().map(_._3)).runToFuture
// }
def <--(obs: Observable[A])(implicit s: Scheduler) = {
obs.doOnNext(v => me.Task(prop() = v)).subscribe()
}
def observableChange[J1 >: A]()
: Observable[(ObservableValue[A, A], J1, J1)] = {
import monix.execution.cancelables.SingleAssignCancelable
Observable.create(OverflowStrategy.Unbounded) { sub =>
val c = SingleAssignCancelable()
val canc = prop.onChange((a, b, c) => sub.onNext((a, b, c)))
c := Cancelable(() => canc.cancel())
c
}
}
}
implicit final class ObjectPropertyObservableListExt[A](
private val prop: ObjectProperty[ObservableList[A]]
) extends AnyVal {
def <--(obs: Observable[Seq[A]])(implicit s: Scheduler) = {
obs.doOnNext(v => me.Task(prop() = ObservableBuffer.from(v))).subscribe()
}
def -->(sub: Observer[A])(implicit s: Scheduler) =
prop.onChange((a, b, c) =>
if (c != null)
Iterant[Task]
.fromIterable(c.toIterable)
.consume
.use(consume(sub, _))
.runToFuture
)
private def loop(sub: Observer[A], it: Iterator[A]): Task[Unit] =
if (it.hasNext) {
val next = it.next()
Task.deferFuture(sub.onNext(next)).flatMap {
case Ack.Continue => loop(sub, it)
case Ack.Stop => Task.unit
}
} else Task.unit
private def consume(
sub: Observer[A],
consumer: Iterant.Consumer[Task, A]
): Task[Unit] =
consumer.pull.flatMap {
case Left(value) => Task.unit
case Right(value) =>
Task.deferFuture(sub.onNext(value)).flatMap {
case Ack.Continue => consume(sub, consumer)
case Ack.Stop => Task.unit
}
}
}
implicit final class OnActionObservable(
private val button: ButtonBase
) extends AnyVal {
// def -->[T](
// op: Observable[jfxe.ActionEvent] => me.Task[T]
// )(implicit s: Scheduler) = {
// op(button.observableAction()).runToFuture
// }
// def -->(
// sub: ConcurrentSubject[jfxe.ActionEvent, jfxe.ActionEvent]
// ) = {
// button.onAction = value => sub.onNext(value)
// }
def observableAction(): Observable[jfxe.ActionEvent] = {
import monix.execution.cancelables.SingleAssignCancelable
Observable.create(OverflowStrategy.Unbounded) { sub =>
val c = SingleAssignCancelable()
val l = new jfxe.EventHandler[jfxe.ActionEvent] {
override def handle(event: jfxe.ActionEvent): Unit = {
if (sub.onNext(event) == Ack.Stop) c.cancel()
}
}
button.onAction = l
c := Cancelable(() =>
button.removeEventHandler(
jfxe.ActionEvent.ACTION,
l
)
)
c
}
}
}
}

50
src/main/scala/nova/monadic_sfx/implicits/package.scala

@ -0,0 +1,50 @@
package nova.monadic_sfx
import javafx.event.ActionEvent
import monix.execution.Ack
import monix.execution.Cancelable
import monix.reactive.Observable
import monix.reactive.OverflowStrategy
import scalafx.scene.control._
package object implicits {
implicit class MyButtonExt(val button: Button) extends AnyVal {
def observableAction(): Observable[ActionEvent] = {
import monix.execution.cancelables.SingleAssignCancelable
Observable.create(OverflowStrategy.Unbounded) { sub =>
val c = SingleAssignCancelable()
val l = new javafx.event.EventHandler[ActionEvent] {
override def handle(event: ActionEvent): Unit = {
if (sub.onNext(event) == Ack.Stop) c.cancel()
}
}
button.onAction = l
c := Cancelable(() =>
button.removeEventHandler(
ActionEvent.ACTION,
l
)
)
c
}
}
}
// implicit class NodeExt(val node: Node) {
// def lookup2[T <: SFXDelegate[_]](
// selector: String
// )(implicit c: ClassTag[T]) = {
// val t = c.runtimeClass
// Option(node.delegate.lookup(selector)) match {
// case Some(value) =>
// if (value.getClass == t) Some(value) else None
// case None => None
// }
// }
// val x = node.lookup2("")
// }
}

7
src/main/scala/nova/monadic_sfx/models/DummyModels.scala

@ -1,7 +1,10 @@
package nova.monadic_sfx.models
case class RequestPayload(data: String)
final case class HttpBinResponse(
import io.circe.generic.JsonCodec
final case class RequestPayload(data: String)
@JsonCodec final case class HttpBinResponse(
url: String,
origin: String,
headers: Map[String, String]

65
src/main/scala/nova/monadic_sfx/ui/DefaultUI.scala

@ -1,55 +1,52 @@
package nova.monadic_sfx.ui
import nova.monadic_sfx.implicits.JFXSpinner
import scalafx.geometry.Insets
import scalafx.geometry.Pos
import scalafx.scene.Scene
import scalafx.scene.effect.DropShadow
import scalafx.scene.layout.HBox
import scalafx.scene.layout.VBox
import scalafx.scene.paint.Color._
import scalafx.scene.paint._
import scalafx.scene.text.Text
import monix.eval.Coeval
class DefaultUI {
object DefaultUI {
val scene =
new Scene {
fill = Color.rgb(38, 38, 38)
content = new HBox {
content = new VBox {
alignment = Pos.Center
padding = Insets(50, 80, 50, 80)
children = Seq(
new Text {
text = "Scala"
style = "-fx-font: normal bold 100pt sans-serif"
fill = new LinearGradient(endX = 0, stops = Stops(Red, DarkRed))
},
new Text {
text = "FX"
style = "-fx-font: italic bold 100pt sans-serif"
fill = new LinearGradient(
endX = 0,
stops = Stops(White, DarkGray)
new HBox {
padding = Insets(50, 80, 50, 80)
children = Seq(
new Text {
text = "Scala"
style = "-fx-font: normal bold 100pt sans-serif"
fill = new LinearGradient(endX = 0, stops = Stops(Red, DarkRed))
},
new Text {
text = "FX"
style = "-fx-font: italic bold 100pt sans-serif"
fill = new LinearGradient(
endX = 0,
stops = Stops(White, DarkGray)
)
effect = new DropShadow {
color = DarkGray
radius = 15
spread = 0.25
}
}
)
effect = new DropShadow {
color = DarkGray
radius = 15
spread = 0.25
}
},
new JFXSpinner {
radius = 50
// style = "-fx-text-fill: red"
}
)
}
}
// val program = Coeval
// .suspend {
// Coeval(println("hello")) >>
// Coeval(println(Thread.currentThread().getName())) >>
// Coeval {
// stage = new PrimaryStage {
// // initStyle(StageStyle.Unified)
// title = "ScalaFX Hello World"
// scene = scn
// }
// }
// }
}

144
src/main/scala/nova/monadic_sfx/ui/MyFxApp.scala

@ -1,129 +1,41 @@
package nova.monadic_sfx.ui
import scalafx.application.JFXApp
import nova.monadic_sfx.executors.Schedulers
import monix.execution.Scheduler
import monix.eval.Task
import nova.monadic_sfx.screens.LoginScreen
import nova.monadic_sfx.AppTypes
import scala.concurrent.duration._
import io.odin.Logger
import monix.execution.Callback
import com.softwaremill.macwire._
import nova.monadic_sfx.http.Requesters
import monix.bio.Task
import nova.monadic_sfx.executors.Schedulers
import nova.monadic_sfx.ui.DefaultUI
import scalafx.application.JFXApp
import scalafx.application.JFXApp.PrimaryStage
import akka.actor.typed._
import nova.monadic_sfx.actors.Counter
import akka.util.Timeout
class MyFxApp(val schedulers: Schedulers)(implicit logger: Logger[Task]) {
class MyFxApp(
logger: Logger[Task],
backend: AppTypes.HttpBackend,
actorSystem: ActorSystem[SpawnProtocol.Command],
requesters: Requesters,
schedulers: Schedulers
) extends JFXApp {
private lazy val internal = new JFXApp {
stage = new PrimaryStage {
scene = DefaultUI.scene
}
}
implicit lazy val defaultScheduler: Scheduler = schedulers.fx
// def stage = Task(internal.stage)
lazy val fxActor: Task[ActorRef[Counter.Command]] = wireWith(
MyFxApp.makeCounterActor _
)
// def stage_=(stage: PrimaryStage) = Task(internal.stage = stage)
lazy val application =
def useInternal[T](f: JFXApp => Task[T]): Task[T] =
for {
appStage <- Task(wireWith(UiModule.makePrimaryStage _))
// _ <- Task {
// val counterActor = testActor(actorSystem)
// counterActor ! (Counter.Increment)
// }
// ta <- testActor2(actorSystem)
// actor <-
// actorTask.bracket(actor => Task(actor ! (Counter.Increment)))(actor =>
// Task(actor ! (Counter.Stop))
// )
// actor <- actorTask
actor <- fxActor
_ <- Task(actor ! (Counter.Increment))
_ <- Task { stage = appStage }
_ <- Task.sleep(2.seconds)
loginScene <- wire[LoginScreen].render
_ <- Task {
// appStage.maximized = true
appStage.height = 800
appStage.width = 800
appStage
.scene()
.setRoot(
loginScene
)
}
} yield ()
// def testActor(
// system: ActorSystem
// ): akka.actor.typed.ActorRef[Counter.Command] = {
// val behaviour: Behavior[Counter.Command] =
// Behaviors.setup(context => wire[Counter])
// system.spawn(
// behaviour,
// "CounterActor",
// DispatcherSelector.fromConfig("javafx-dispatcher")
// )
// }
application.timed.runAsync(
new Callback[Throwable, (FiniteDuration, Unit)] {
_ <- logger.debug("Request for using internal value")
res <- f(internal).executeOn(schedulers.fx)
_ <- logger.debug(s"Result was ${res.toString()}")
} yield (res)
override def onSuccess(value: (FiniteDuration, Unit)): Unit = {
val (duration, _) = value
println(
s"Application started successfully in ${duration.toSeconds} seconds"
)
}
override def onError(e: Throwable): Unit = {
println("Application start failed. Reason -")
e.printStackTrace()
}
}
)
override def stopApp() = {
val stop = for {
actor <- fxActor
_ <- logger.info("Stopping actor counter")
// _ <- Task.fromFuture { actor.ask[Counter.Value](Counter.GetValue) }
t <- Task(actor ! Counter.Stop)
// _ <- Task.sleep(1.second)
_ <- logger.info("Counter actor stopped")
} yield ()
stop.runAsyncAndForget
// Platform.exit()
}
}
object MyFxApp {
def makeCounterActor(
system: ActorSystem[SpawnProtocol.Command]
): Task[ActorRef[Counter.Command]] = {
import akka.actor.typed.scaladsl.AskPattern._
import scala.concurrent.ExecutionContext
def init(stage: => PrimaryStage, delay: FiniteDuration = 2000.millis) =
for {
_ <- logger.info("Starting FX App")
fib <- Task(internal.main(Array.empty)).start
_ <- Task.sleep(200.millis)
_ <- Task(internal.stage = stage)
.executeOn(schedulers.fx)
.delayExecution(delay)
} yield (this, fib)
implicit val timeout: Timeout = Timeout(3.seconds)
implicit val ec: ExecutionContext = system.executionContext
implicit val scheduler = system.scheduler
Task.fromFuture {
system.ask(
SpawnProtocol.Spawn(
behavior = wireWith(Counter.apply _),
name = "counterActor",
DispatcherSelector.fromConfig("javafx-dispatcher"),
_
)
)
}
}
}

129
src/main/scala/nova/monadic_sfx/ui/MyFxAppOld.scala

@ -0,0 +1,129 @@
package nova.monadic_sfx.ui
import scala.concurrent.duration._
import akka.actor.typed._
import akka.util.Timeout
import com.softwaremill.macwire._
import io.odin.Logger
import monix.eval.Task
import monix.execution.Callback
import monix.execution.Scheduler
import nova.monadic_sfx.AppTypes
import nova.monadic_sfx.actors.Counter
import nova.monadic_sfx.executors.Schedulers
import nova.monadic_sfx.http.Requesters
import scalafx.application.JFXApp
class MyFxAppOld(
logger: Logger[Task],
backend: AppTypes.HttpBackend,
actorSystem: ActorSystem[SpawnProtocol.Command],
requesters: Requesters,
schedulers: Schedulers
) extends JFXApp {
implicit lazy val defaultScheduler: Scheduler = schedulers.fx
// lazy val fxActor: Task[ActorRef[Counter.Command]] = wireWith(
// MyFxApp.makeCounterActor _
// )
lazy val application =
for {
appStage <- Task(wireWith(UiModule.makePrimaryStage _))
// _ <- Task {
// val counterActor = testActor(actorSystem)
// counterActor ! (Counter.Increment)
// }
// ta <- testActor2(actorSystem)
// actor <-
// actorTask.bracket(actor => Task(actor ! (Counter.Increment)))(actor =>
// Task(actor ! (Counter.Stop))
// )
// actor <- actorTask
// actor <- fxActor
// _ <- Task(actor ! Counter.Increment)
_ <- Task { stage = appStage }
_ <- Task.sleep(2.seconds)
// loginScene <- wire[LoginScreen].render
// _ <- Task {
// // appStage.maximized = true
// appStage.height = 800
// appStage.width = 800
// appStage
// .scene()
// .setRoot(
// loginScene
// )
// }
} yield ()
// def testActor(
// system: ActorSystem
// ): akka.actor.typed.ActorRef[Counter.Command] = {
// val behaviour: Behavior[Counter.Command] =
// Behaviors.setup(context => wire[Counter])
// system.spawn(
// behaviour,
// "CounterActor",
// DispatcherSelector.fromConfig("javafx-dispatcher")
// )
// }
application.timed.runAsync(
new Callback[Throwable, (FiniteDuration, Unit)] {
override def onSuccess(value: (FiniteDuration, Unit)): Unit = {
val (duration, _) = value
println(
s"Application started successfully in ${duration.toSeconds} seconds"
)
}
override def onError(e: Throwable): Unit = {
println("Application start failed. Reason -")
e.printStackTrace()
}
}
)
override def stopApp() = {
// val stop = for {
// actor <- fxActor
// _ <- logger.info("Stopping actor counter")
// // _ <- Task.fromFuture { actor.ask[Counter.Value](Counter.GetValue) }
// t <- Task(actor ! Counter.Stop)
// // _ <- Task.sleep(1.second)
// _ <- logger.info("Counter actor stopped")
// } yield ()
// stop.runAsyncAndForget
// // Platform.exit()
}
}
object MyFxAppOld {
def makeCounterActor(
system: ActorSystem[SpawnProtocol.Command],
logger: Logger[Task]
): Task[ActorRef[Counter.Command]] = {
import akka.actor.typed.scaladsl.AskPattern._
import scala.concurrent.ExecutionContext
implicit val timeout: Timeout = Timeout(3.seconds)
implicit val ec: ExecutionContext = system.executionContext
implicit val scheduler = system.scheduler
Task.fromFuture {
system.ask(
SpawnProtocol.Spawn(
behavior = wireWith(Counter.apply _),
name = "counterActor",
DispatcherSelector.fromConfig("javafx-dispatcher"),
_
)
)
}
}
}

18
src/main/scala/nova/monadic_sfx/ui/UiModule.scala

@ -1,15 +1,15 @@
package nova.monadic_sfx.ui
import scalafx.application.JFXApp
import monix.eval.Task
import nova.monadic_sfx.AppTypes
import scalafx.application.JFXApp.PrimaryStage
import io.odin.Logger
import akka.actor.typed._
import cats.effect.Resource
import com.softwaremill.macwire._
import nova.monadic_sfx.http.Requesters
import io.odin.Logger
import monix.eval.Task
import nova.monadic_sfx.AppTypes
import nova.monadic_sfx.executors.Schedulers
import akka.actor.typed._
import nova.monadic_sfx.http.Requesters
import scalafx.application.JFXApp
import scalafx.application.JFXApp.PrimaryStage
trait UiModule {
def fxAppResource(
@ -21,7 +21,7 @@ trait UiModule {
): Resource[Task, JFXApp] =
Resource.make(for {
_ <- logger.info("Creating FX Application")
app <- Task { wire[MyFxApp] }
app <- Task { wire[MyFxAppOld] }
} yield (app))(app => logger.info("Stopping FX Application"))
}
@ -31,7 +31,7 @@ object UiModule {
actorSystem: ActorSystem[SpawnProtocol.Command]
) = {
new PrimaryStage {
scene = new DefaultUI().scene
scene = DefaultUI.scene
}
}
}

556
src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListComponent.scala

@ -0,0 +1,556 @@
package nova.monadic_sfx.ui.components.todo
import scala.concurrent.Future
import scala.concurrent.duration.FiniteDuration
import cats.effect.Sync
import cats.effect.concurrent.Deferred
import io.odin.Logger
import monix.bio.Task
import monix.catnap.ConcurrentChannel
import monix.catnap.ConsumerF
import monix.execution.Ack
import monix.execution.Cancelable
import monix.execution.Scheduler
import monix.reactive.Observable
import monix.reactive.Observer
import monix.reactive.OverflowStrategy
import monix.reactive.observers.Subscriber
import monix.reactive.subjects.ConcurrentSubject
import nova.monadic_sfx.implicits.FontIcon
import nova.monadic_sfx.implicits.IconLiteral
import nova.monadic_sfx.implicits.JFXListView
import nova.monadic_sfx.implicits.JavaFXMonixObservables._
import nova.monadic_sfx.ui.components.todo.TodoListComponent.Add
import nova.monadic_sfx.ui.components.todo.TodoListComponent.Delete
import nova.monadic_sfx.ui.components.todo.TodoListComponent.Edit
import scalafx.Includes._
import scalafx.beans.property.StringProperty
import scalafx.collections.ObservableBuffer
import scalafx.scene.control.ContextMenu
import scalafx.scene.control.ListCell
import scalafx.scene.control.MenuItem
import scalafx.scene.control.SelectionMode
import scalafx.scene.layout.HBox
import scalafx.scene.text.Text
import nova.monadic_sfx.ui.components.todo.Store.MonixProSubject
import nova.monadic_sfx.util.IOUtils
import monix.tail.Iterant
case class Todo(id: Int, content: String)
class TodoListView(
val listView: JFXListView[Todo] = TodoListView.defaultListView,
val lvObs: ObservableBuffer[Todo] = ObservableBuffer.empty
) {
listView.items = lvObs
}
object TodoListView {
def defaultListView =
new JFXListView[Todo] {
// cellFactory = _ =>
// new ListCell[Todo] {
// // item.onChange((a, b, c) => ())
// overr
// }
contextMenu = new ContextMenu {
items ++= Seq(
new MenuItem {
text = "delete"
},
new MenuItem {
text = "edit"
}
)
}
}
// import scalafx.scene.control.MultipleSelectionModel
// .getOrElse(Todo(-1, "blah"))
implicit class Operations[A](val sink: Observer[A]) extends AnyVal {}
// def reducer(
// stateC: Coeval[ObservableBuffer[Todo]],
// action: TodoListComponent.Command
// ) =
// action match {
// case Add(todo) =>
// for {
// state <- stateC
// } yield state :+ todo
// // case Find(id, result) =>
// case Edit(id, content) => stateC
// case Delete(id) =>
// for {
// state <- stateC
// } yield state.filterNot(_.id == id)
// case _ => stateC
// }
def reducer(
state: Vector[Todo],
action: TodoListComponent.Command
) =
action match {
case Add(todo) => state :+ todo
// case Find(id, result) =>
case Edit(id, content) => state
case Delete(id) =>
state.filterNot(_.id == id)
case _ => state
}
def defaultListView2: Task[
(
JFXListView[Todo],
Observable[Todo],
Observable[Todo]
)
] =
Task.deferAction(implicit s =>
Store
.createL[TodoListComponent.Command, Vector[Todo]](
TodoListComponent.Delete(0),
Vector.empty[Todo],
(s: Vector[Todo], a: TodoListComponent.Command) =>
reducer(s, a) -> Observable.empty
)
.flatMap(store =>
Task {
val deleteSub = ConcurrentSubject.publish[Todo]
val editSub = ConcurrentSubject.publish[Todo]
// store.flatMap(st => Task(st.sink))
// val deleteSub2 =
// deleteSub.map(todo => (buf: ObservableBuffer[Todo]) => buf :+ todo)
// val addSub =
// ConcurrentSubject
// .publish[Todo]
// .map(todo => (buf: ObservableBuffer[Todo]) => buf :+ todo)
// val state = Observable(deleteSub2, addSub).merge.scan0(
// ObservableBuffer.empty[Todo]
// )((buf, fn) => fn(buf))
val todos =
store.map { case (_, items) => items }
val listView = new JFXListView[Todo] { lv =>
def selectedItems = lv.selectionModel().selectedItems.view
// items = todos
items <-- todos
// .map(ObservableBuffer.from(_))
cellFactory = _ =>
new ListCell[Todo] {
val _text = StringProperty("")
val _graphic = new HBox {
children = Seq(
new FontIcon {
iconSize = 10
iconLiteral = IconLiteral.Gmi10k
},
new Text {
text <== _text
}
)
}
item.onChange((_, _, todo) => {
println("called")
if (todo != null) {
_text() = s"${todo.id} - ${todo.content}"
graphic = _graphic
} else {
_text() = ""
graphic = null
}
})
}
selectionModel().selectionMode = SelectionMode.Multiple
contextMenu = new ContextMenu {
items ++= Seq(
new MenuItem {
text = "Add"
onAction = _ =>
store.sink
.onNext(TodoListComponent.Add(Todo(1, "blah3")))
},
new MenuItem {
text = "Delete"
// onAction = _ =>
// for {
// items <- Option(lv.selectionModel().selectedItems)
// _ <- Some(items.foreach(item => deleteSub.onNext(item)))
// } yield ()
onAction = _ =>
selectedItems
.map(todo => TodoListComponent.Delete(todo.id))
.foreach(store.sink.onNext)
},
new MenuItem {
text = "Edit"
// onAction = _ =>
// Option(lv.selectionModel().selectedItems).foreach(items =>
// items.foreach(item => editSub.onNext(item))
// )
}
)
}
}
(listView, deleteSub, editSub)
}
)
)
}
private[todo] class TodoListComponentImpure(
todoListView: TodoListView
) {
def add(todo: Todo) = todoListView.lvObs += todo
def find(id: Int) = todoListView.lvObs.find(_.id == id)
def edit(id: Int, content: String) =
find(id)
.map(todo =>
todoListView.lvObs.replaceAll(
todo,
Todo(id, content)
)
)
.getOrElse(false)
}
class TodoListOps private (
props: TodoListOps.Props
) {
import props._
// lazy val internal = new TodoListComponentImpure(todoListView)
// def add(todo: Todo) = Task(internal.add(todo))
def add(todo: Todo) = Task(todoListView.lvObs += todo).executeOn(fxScheduler)
def find(id: Int) =
Task(todoListView.lvObs.find(_.id == id)).executeOn(fxScheduler)
def delete(id: Int) =
(for {
mbTodo <- find(id)
_ <- logger.debug(mbTodo.toString())
res <- Task(
mbTodo.map(todo => todoListView.lvObs.removeAll(todo))
)
_ <- logger.debug(todoListView.lvObs.toString())
} yield res.getOrElse(false)).executeOn(fxScheduler)
def edit(id: Int, content: String) =
(for {
mbTodo <- find(id)
res <- Task(
mbTodo.map(todo =>
todoListView.lvObs.replaceAll(
todo,
Todo(id, content)
)
)
)
} yield res.getOrElse(false)).executeOn(fxScheduler)
}
object TodoListOps {
class Props(
val todoListView: TodoListView,
val fxScheduler: Scheduler,
val logger: Logger[Task]
) {
def create = Task(new TodoListOps(this))
}
}
object TodoListComponent {
sealed trait Complete
object Complete extends Complete
sealed trait Command
// sealed trait Tell extends Command
// sealed abstract class Ask extends Command
case class Add(todo: Todo) extends Command
case class Find(id: Int, result: Deferred[Task, Option[Todo]]) extends Command
case class Edit(id: Int, content: String) extends Command
case class Delete(id: Int) extends Command
// private case class FindInternal(id: Int, result: Deferred[Task, Todo])
// extends Ask
class Props(
val todoListView: