Skip to content

Commit

Permalink
Replaced imperative impl with CE
Browse files Browse the repository at this point in the history
  • Loading branch information
davesmith00000 committed Mar 31, 2024
1 parent 543bb01 commit 208206d
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 47 deletions.
100 changes: 55 additions & 45 deletions tyrian/js/src/main/scala/tyrian/runtime/Renderer.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package tyrian.runtime

import cats.effect.kernel.Async
import cats.effect.kernel.Clock
import cats.effect.kernel.Ref
import cats.effect.std.Dispatcher
import cats.syntax.all.*
Expand All @@ -9,71 +10,80 @@ import snabbdom.VNode
import tyrian.Html
import tyrian.Location

import scala.scalajs.js.Date
final case class Renderer(vnode: VNode, state: RendererState):

final case class Renderer(vnode: VNode, state: RendererState, lastTriggered: Long, runningTime: Long)
def runningAt(t: Long): Renderer =
this.copy(state = RendererState.Running(t))

object Renderer:

def init[F[_]](vnode: VNode)(using F: Async[F]): F[Ref[F, Renderer]] =
F.ref(
Renderer(vnode, RendererState.Idle, 0, 0)
Renderer(vnode, RendererState.Idle)
)

// Renderer is stored here (and pointlessly, atm, in a Ref), because the
// render function is a callback and I can't return values back to the
// initiating call site under this arrangement
@SuppressWarnings(Array("scalafix:DisableSyntax.var", "scalafix:DisableSyntax.null"))
var renderer: Renderer = null
val timeout: Long = 1000
private val timeout: Long = 1000

def render[F[_], Model, Msg](
// This function gets called on every model update
def redraw[F[_], Model, Msg](
dispatcher: Dispatcher[F],
renderer: Ref[F, Renderer],
model: Ref[F, Model],
view: Model => Html[Msg],
onMsg: Msg => Unit,
router: Location => Msg
)(t: Long)(using Async[F]): Unit =
if t - renderer.lastTriggered >= timeout then
renderer = renderer.copy(
state = RendererState.Idle,
runningTime = t
)
()
else
dispatcher.unsafeRunAndForget {
model.get.map { m =>
renderer = renderer.copy(
vnode = Rendering.render(renderer.vnode, m, view, onMsg, router),
state = RendererState.Running,
runningTime = t
)
dom.window.requestAnimationFrame(_ => render(dispatcher, model, view, onMsg, router)(Date.now().toLong))
()
}
)(using F: Async[F], clock: Clock[F]): F[Unit] =
clock.realTime.flatMap { time =>
renderer.modify { r =>
r.state match
case RendererState.Idle =>
// If the render state is idle, update the last triggered time and begin.
r.runningAt(time.toMillis) ->
F.delay(
dom.window.requestAnimationFrame(_ =>
render(dispatcher, renderer, model, view, onMsg, router)(time.toMillis)
)
).void

case RendererState.Running(_) =>
// If the render state is running, just update the triggered time.
r.runningAt(time.toMillis) -> F.unit
}
}.flatten

// This function gets called on every model update
@SuppressWarnings(Array("scalafix:DisableSyntax.null"))
def redraw[F[_], Model, Msg](
private def render[F[_], Model, Msg](
dispatcher: Dispatcher[F],
r: Renderer,
renderer: Ref[F, Renderer],
model: Ref[F, Model],
view: Model => Html[Msg],
onMsg: Msg => Unit,
router: Location => Msg
)(using Async[F]): Renderer =
// Sadly, needs a null check for as long as I'm using a var to hold the state.
if renderer == null then renderer = r
)(t: Long)(using F: Async[F], clock: Clock[F]): Unit =
dispatcher.unsafeRunAndForget {
for {
time <- clock.realTime.map(_.toMillis)
m <- model.get

renderer.state match
case RendererState.Idle =>
// If the render state is idle, begin.
renderer = renderer.copy(lastTriggered = Date.now().toLong)
dom.window.requestAnimationFrame(_ => render(dispatcher, model, view, onMsg, router)(Date.now().toLong))
renderer
res <- renderer.modify { r =>
r.state match
case RendererState.Idle =>
// Something has gone wrong, do nothing.
r -> F.unit

case RendererState.Running =>
// If the render state is running, just update the triggered time.
renderer = renderer.copy(lastTriggered = Date.now().toLong)
renderer
case RendererState.Running(lastTriggered) =>
// If nothing has happened, set to idle and do not loop
if t - lastTriggered >= timeout then r.copy(state = RendererState.Idle) -> F.unit
else
// Otherwise, re-render and set the state appropriately
r.copy(
vnode = Rendering.render(r.vnode, m, view, onMsg, router)
) ->
F.delay(
// Loop
dom.window.requestAnimationFrame(_ =>
render(dispatcher, renderer, model, view, onMsg, router)(time)
)
).void
}.flatten
} yield res
}
3 changes: 2 additions & 1 deletion tyrian/js/src/main/scala/tyrian/runtime/RendererState.scala
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
package tyrian.runtime

enum RendererState derives CanEqual:
case Idle, Running
case Idle
case Running(lastTriggered: Long)
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ object TyrianRuntime:
}

_ <- runCmd(cmdsAndSubs._1) *> runSub(cmdsAndSubs._2)
_ <- renderer.update(r => Renderer.redraw(dispatcher, r, model, view, onMsg, router))
_ <- Renderer.redraw(dispatcher, renderer, model, view, onMsg, router)
} yield ()
}.foreverM

Expand Down

0 comments on commit 208206d

Please sign in to comment.