diff --git a/src/main/scala/nova/monadic_sfx/MainApp.scala b/src/main/scala/nova/monadic_sfx/MainApp.scala index ef85122..4489892 100644 --- a/src/main/scala/nova/monadic_sfx/MainApp.scala +++ b/src/main/scala/nova/monadic_sfx/MainApp.scala @@ -1,14 +1,21 @@ package nova.monadic_sfx +import java.util.concurrent.TimeUnit + +import scala.util.Random + import com.softwaremill.macwire._ import io.odin.Logger +import monix.bio.IO import monix.bio.Task +import monix.eval.Coeval +import monix.{eval => me} 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.router.BrainNotWorking import nova.monadic_sfx.ui.components.router.FXRouter +import nova.monadic_sfx.ui.components.router.Page import nova.monadic_sfx.ui.components.todo.TodoListStore import nova.monadic_sfx.ui.components.todo.TodoListView import org.gerweck.scalafx.util._ @@ -18,12 +25,16 @@ import scalafx.beans.property.ObjectProperty import scalafx.beans.property.StringProperty import scalafx.collections.ObservableBuffer import scalafx.geometry.Insets +import scalafx.geometry.Pos +import scalafx.scene.Parent import scalafx.scene.Scene +import scalafx.scene.control.Label import scalafx.scene.control.TableColumn import scalafx.scene.control.TableView +import scalafx.scene.layout.BorderPane +import scalafx.scene.layout.FlowPane import scalafx.scene.layout.HBox -import scalafx.scene.paint.Color -import scalafx.scene.shape.Rectangle +import scalafx.scene.layout.Priority class MainApp( // spawnProtocol: ActorSystem[SpawnProtocol.Command], @@ -31,136 +42,108 @@ class MainApp( 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 = 1000 - height = 400 + width = 640 + height = 480 } - // 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 - // ) - _ <- createTodoComponent.executeOn(schedulers.fx) - router <- Task(BrainNotWorking.router) - routerStore <- router.store(BrainNotWorking.Page.Home, logger) - routerNode <- for { - node <- - Task - .deferAction(implicit s => - Task(new HBox { - children <-- router - .render(BrainNotWorking.resolver)(routerStore) - .map(_.delegate) - }) - ) - .executeOn(schedulers.fx) - _ <- Task.deferFuture( - routerStore.onNext(FXRouter.Replace(BrainNotWorking.Page.UserHome(1))) - ) - - } yield node - // _ <- - // BrainNotWorking - // .routerTask(logger) - // .flatMap(node => Task(_scene.getChildren += node)) - // .executeOn(schedulers.fx) - _ <- Task(_scene.getChildren += routerNode).executeOn(schedulers.fx) + _ <- + wire[MainAppDelegate].init + .flatMap(mainSceneNode => Task(_scene.getChildren += mainSceneNode)) + .executeOn(schedulers.fx) + currentTime <- IO.clock.realTime(TimeUnit.MILLISECONDS) _ <- logger.info( - s"Application started in ${(System.currentTimeMillis() - startTime) / 1000f} seconds" + s"Application started in ${(currentTime - startTime) / 1000f} seconds" ) _ <- fxAppFib.join } yield () - // def createTodoComponent: Task[TodoListComponent] = { - // for { - // channel <- - // ConcurrentChannel - // .of[Task, TodoListComponent.Complete, TodoListComponent.Command] - // scheduler = schedulers.fx - // lv <- 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 - // } +} - def createTodoComponent: Task[Unit] = +class MainAppDelegate(schedulers: Schedulers)(implicit logger: Logger[Task]) { + val buttonStyle = """| -fx-padding: 0.7em 0.57em; + | -fx-font-size: 14px; + | -jfx-button-type: RAISED; + | -fx-background-color: rgb(77,102,204); + | -fx-pref-width: 200; + | -fx-text-fill: WHITE; """.stripMargin + + def init = for { - store <- TodoListStore(logger) - rootNode <- TodoListView(store) - _ <- Task(_scene.getChildren += rootNode) - } yield () + router <- Task.pure(new FXRouter[Page]) + routerStore <- router.store(Page.Home, logger) + todoStore <- TodoListStore(logger) + todoComponent <- TodoListView(todoStore) + resolver: PartialFunction[Page, Task[Parent]] = { + case Page.Home => + Task(new Label { + text = "HomePage" + }) + case Page.UserHome(id0) => + Task(new Label { + text = s"User Home, Id = $id0" + }) + case Page.Todo => + Task(todoComponent) + } + routerNode <- + Task + .deferAction(implicit s => + Task(new HBox { + //TODO find a better way to do this + var oldValue: Option[Parent] = None + children <-- router + .render(resolver)(routerStore) + // call cancel on the old component to cancel all subscriptions + .doOnNextF(newValue => + Coeval { oldValue.foreach(_ => ()) } >> Coeval { + oldValue = Some(newValue) + } + ) + .map(_.delegate) + }) + ) + mainSceneNode <- Task.deferAction(implicit s => + Task(new BorderPane { + hgrow = Priority.Always + vgrow = Priority.Always + center = routerNode + bottom = new FlowPane { + alignment = Pos.Center + hgap = 20 + children = Seq( + new JFXButton { + text = "Todo" + style = buttonStyle + obsAction + .useLazyEval( + me.Task.pure(FXRouter.Replace(Page.Todo)) + ) --> routerStore + }, + new JFXButton { + text = "UserHome" + style = buttonStyle + obsAction + .useLazyEval( + me.Task(FXRouter.Replace(Page.UserHome(Random.nextInt(20)))) + ) --> routerStore + } + ) + } + }) + ) + } yield mainSceneNode } class TestModel(_name: String, _age: Int) { diff --git a/src/main/scala/nova/monadic_sfx/MainModule.scala b/src/main/scala/nova/monadic_sfx/MainModule.scala index 0a8c3da..0882d5a 100644 --- a/src/main/scala/nova/monadic_sfx/MainModule.scala +++ b/src/main/scala/nova/monadic_sfx/MainModule.scala @@ -19,23 +19,23 @@ trait MainModule extends ActorModule with UiModule with HttpModule { "nova.monadic_sfx.util.reactive.Store" -> storeLogger ) .withFallback(defaultLogger) - .withAsync() + .withAsync(timeWindow = 1.millis) def makeLogger = for { defaultLogger <- consoleLogger[Task]() .withAsync(timeWindow = 1.millis) |+| fileLogger[Task]( "application.log" - ).withAsync() + ).withAsync(timeWindow = 1.millis) middlewareLogger <- consoleLogger[ Task ](formatter = Middlewares.format) .withMinimalLevel(Level.Trace) - .withAsync() |+| fileLogger[Task]( + .withAsync(timeWindow = 1.millis) |+| fileLogger[Task]( "stores.log", formatter = Middlewares.format - ).withAsync() + ).withAsync(timeWindow = 1.millis) routerLogger <- routerLogger(defaultLogger, middlewareLogger) } yield (routerLogger) } diff --git a/src/main/scala/nova/monadic_sfx/implicits/ActionObservable.scala b/src/main/scala/nova/monadic_sfx/implicits/ActionObservable.scala index 093c317..48b52db 100644 --- a/src/main/scala/nova/monadic_sfx/implicits/ActionObservable.scala +++ b/src/main/scala/nova/monadic_sfx/implicits/ActionObservable.scala @@ -12,7 +12,7 @@ class ActionObservableExecutor[T]( ) extends AnyVal { def -->(sub: Observer[T])(implicit s: Scheduler) = delegate - .doOnNext(el => me.Task(sub.onNext(el))) + .doOnNext(el => me.Task.deferFuture(sub.onNext(el)) >> me.Task.unit) .subscribe() } diff --git a/src/main/scala/nova/monadic_sfx/ui/MyFxApp.scala b/src/main/scala/nova/monadic_sfx/ui/MyFxApp.scala index 34f7400..74e245f 100644 --- a/src/main/scala/nova/monadic_sfx/ui/MyFxApp.scala +++ b/src/main/scala/nova/monadic_sfx/ui/MyFxApp.scala @@ -31,7 +31,8 @@ class MyFxApp(val schedulers: Schedulers)(implicit logger: Logger[Task]) { def init(stage: => PrimaryStage, delay: FiniteDuration = 200.millis) = for { _ <- logger.info("Starting FX App") - fib <- Task(internal.main(Array.empty)).start + fib <- + Task(internal.main(Array.empty)).start.executeOn(schedulers.blocking) _ <- Task.sleep(200.millis) _ <- Task(internal.stage = stage) .executeOn(schedulers.fx) diff --git a/src/main/scala/nova/monadic_sfx/ui/components/router/FXRouter.scala b/src/main/scala/nova/monadic_sfx/ui/components/router/FXRouter.scala index 87d35a2..9738d67 100644 --- a/src/main/scala/nova/monadic_sfx/ui/components/router/FXRouter.scala +++ b/src/main/scala/nova/monadic_sfx/ui/components/router/FXRouter.scala @@ -1,47 +1,52 @@ package nova.monadic_sfx.ui.components.router -import enumeratum._ +import io.circe.Codec +import io.circe.Decoder import io.circe.Encoder import io.circe.generic.JsonCodec +import io.circe.generic.semiauto._ import io.odin.Logger import monix.bio.Task import nova.monadic_sfx.util.IOUtils +import nova.monadic_sfx.util.reactive.Middlewares import nova.monadic_sfx.util.reactive.Reducer import nova.monadic_sfx.util.reactive.Store import scalafx.scene.Parent -import scalafx.scene.control.Label object FXRouter { final case class State[P](page: P) - @JsonCodec - sealed abstract class Action[T] - // final case object Init extends Action + // @JsonCodec + sealed abstract class Action[+T] final case class Replace[T](p: T) extends Action[T] + object Action { + implicit def codec[T: Encoder: Decoder]: Codec[Action[T]] = deriveCodec + } + type FXStore[P] = Store[Action[P], State[P]] - // def resolver2 = resolver.lift.andThen(_.getOrElse(notFound)) - - // def resolver: PartialFunction[P <: Enum[P]][P, Parent] = { - // case Home => new TextField - // } - } -class FXRouter[P <: EnumEntry]( -)(implicit E: Encoder[P]) { +class FXRouter[P]( +)(implicit E: Encoder[P], D: Decoder[P]) { import FXRouter._ - def store(initialPage: P, logger: Logger[Task]) = + def store(initialPage: P, logger: Logger[Task]): Task[FXStore[P]] = Task.deferAction(implicit s => - Store.createL[Action[P], State[P]]( - Replace(initialPage), - State(initialPage), - Reducer.withOptionalEffects[Task, Action[P], State[P]](reducer _) - // Seq(actionLoggerMiddleware(logger, "RouterStore")) - ) + for { + mw <- Middlewares.actionLoggerMiddleware[Action[P], State[P]]( + logger, + "RouterStore" + ) + store <- Store.createL[Action[P], State[P]]( + Replace(initialPage), + State(initialPage), + Reducer.withOptionalEffects[Task, Action[P], State[P]](reducer _), + Seq(mw) + ) + } yield store ) def reducer( @@ -67,28 +72,12 @@ class FXRouter[P <: EnumEntry]( } } -object BrainNotWorking { - - @JsonCodec - sealed trait Page extends EnumEntry - object Page extends Enum[Page] { - val values = findValues - final case object Home extends Page - final case class UserHome(id: Int) extends Page - } - - def resolver: PartialFunction[Page, Task[Parent]] = { - case Page.Home => - Task(new Label { - text = "HomePage" - }) - case Page.UserHome(id0) => - Task(new Label { - text = s"User Home, Id = $id0" - }) - } - - val router = new FXRouter[Page] +@JsonCodec +sealed trait Page +object Page { + final case object Home extends Page + final case class UserHome(id: Int) extends Page + final case object Todo extends Page } // case class State() diff --git a/src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListView.scala b/src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListView.scala index a49313c..e11dc5b 100644 --- a/src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListView.scala +++ b/src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListView.scala @@ -16,7 +16,7 @@ import scalafx.Includes._ import scalafx.beans.property.ObjectProperty import scalafx.beans.property.StringProperty import scalafx.geometry.Insets -import scalafx.scene.Node +import scalafx.scene.Parent import scalafx.scene.control.ContextMenu import scalafx.scene.control.ListCell import scalafx.scene.control.SelectionMode @@ -25,17 +25,12 @@ import scalafx.scene.text.Text object TodoListView { def apply( - store: MonixProSubject[ - TodoListStore.Action, - (TodoListStore.Action, TodoListStore.State) - ] - ): Task[Node] = + store: Store[TodoListStore.Action, TodoListStore.State] + ): Task[Parent] = Task.deferAction(implicit s => Task { val cc = CompositeCancelable() - val todos = - store.map { case (_, state) => state.todos } - // Todo(-1, "").some + val todos = store.map { case (_, state) => state.todos } val _selectedItems = ObjectProperty(Seq.empty[Todo]) new HBox { diff --git a/src/main/scala/nova/monadic_sfx/util/SynchedObject.scala b/src/main/scala/nova/monadic_sfx/util/SynchedObject.scala new file mode 100644 index 0000000..856d96d --- /dev/null +++ b/src/main/scala/nova/monadic_sfx/util/SynchedObject.scala @@ -0,0 +1,44 @@ +package nova.monadic_sfx.util + +import monix.bio.Task +import monix.bio.UIO +import monix.catnap.MVar + +/** + * Synchronization wrapper for a mutable object + * + * @param obj the mutable object + * @param lock lock for synchronization + */ +class SynchedObject[A](obj: A, lock: MLock) { + + def modify(f: A => Task[Unit]): Task[Unit] = + lock.greenLight(f(obj)) + + def get: Task[A] = lock.greenLight(Task(obj)) +} + +object SynchedObject { + def apply[A](obj: A) = + MVar[Task] + .of(()) + .map(m => new MLock(m)) + .flatMap(lock => Task(new SynchedObject(obj, lock))) +} + +final class MLock(mvar: MVar[Task, Unit]) { + def acquire: Task[Unit] = + mvar.take + + def release: Task[Unit] = + mvar.put(()) + + def greenLight[A](fa: Task[A]): Task[A] = + for { + _ <- acquire + a <- fa.doOnCancel( + release.onErrorHandleWith(ex => UIO(println(ex.getMessage()))) + ) + _ <- release + } yield a +} diff --git a/src/main/scala/nova/monadic_sfx/util/reactive/Middlewares.scala b/src/main/scala/nova/monadic_sfx/util/reactive/Middlewares.scala index c1b444c..66c882a 100644 --- a/src/main/scala/nova/monadic_sfx/util/reactive/Middlewares.scala +++ b/src/main/scala/nova/monadic_sfx/util/reactive/Middlewares.scala @@ -21,7 +21,7 @@ import monix.reactive.Observable final case class StoreInfo[A]( name: String, action: A, - time: LocalDateTime = LocalDateTime.now() + time: LocalDateTime ) object StoreInfo { @@ -68,7 +68,8 @@ object Middlewares { Task((obs: Observable[(A, M)]) => obs.doOnNextF { case (a, _) => - logger.debug(StoreInfo(name, a)) + Task(LocalDateTime.now()) + .flatMap(curTime => logger.debug(StoreInfo(name, a, curTime))) } ) ) diff --git a/src/main/scala/nova/monadic_sfx/util/reactive/Store.scala b/src/main/scala/nova/monadic_sfx/util/reactive/Store.scala index db51afe..e6b7915 100644 --- a/src/main/scala/nova/monadic_sfx/util/reactive/Store.scala +++ b/src/main/scala/nova/monadic_sfx/util/reactive/Store.scala @@ -1,5 +1,7 @@ package nova.monadic_sfx.util.reactive +import java.time.LocalDateTime + import io.circe.Encoder import io.odin.Logger import monix.bio.Task @@ -72,9 +74,11 @@ object Store { val obs = subject .doOnNextF(action => - logger.debug( - StoreInfo(storeName, action) - ) // .executeOn(Scheduler.global) + Task(LocalDateTime.now()).flatMap(curTime => + logger.debug( + StoreInfo(storeName, action, curTime) + ) + ) ) // .doOnNextF(action => Coeval(println(action))) .scanEvalF[Task, (A, M)](Task.pure(initialAction -> initialState))(