Skip to content

Commit

Permalink
Add Animate API
Browse files Browse the repository at this point in the history
MR feedback: remove overload in question

MR feedback

MR feedback

MR feedback

Fix kdocs

Add Animate API


Merge-request: KDS-MR-26
Merged-by: Ilya Muradyan <Ilya.Muradyan@jetbrains.com>
  • Loading branch information
ileasile authored and Space Team committed Apr 23, 2024
1 parent 835d524 commit 8c50f7c
Show file tree
Hide file tree
Showing 5 changed files with 393 additions and 15 deletions.
219 changes: 219 additions & 0 deletions jupyter-lib/lib/src/main/kotlin/jupyter/kotlin/Animate.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
@file:Suppress("FunctionName", "unused")

package jupyter.kotlin

import java.util.UUID
import kotlin.time.Duration

private fun generateId() = UUID.randomUUID().toString()

/**
* The `Frame` class represents a single frame in an animation.
*
* @param delayBefore The delay duration before displaying the content of the frame.
* @param content The content of the frame. In the end, the content is passed to [DISPLAY] or [UPDATE_DISPLAY]
* and handled by the dedicated display handler.
* @param T The type of the content.
*/
class Frame<out T>(
val delayBefore: Duration,
val content: T,
)

/**
* Displays all the frames left in this [frames] iterator, with respect to the delays.
*
* @param frames An iterator over [Frame] elements to be displayed.
* @throws InterruptedException if any thread has interrupted the current thread
* while the current thread is waiting for the frame delays.
*/
@Throws(InterruptedException::class)
fun ScriptTemplateWithDisplayHelpers.ANIMATE(frames: Iterator<Frame<*>>) {
val displayId = generateId()
var isFirst = true
frames.forEachRemaining { frame ->
val frameContent = frame.content ?: "null"
val frameDelay = frame.delayBefore.inWholeMilliseconds
Thread.sleep(frameDelay)
if (isFirst) {
DISPLAY(frameContent, displayId)
isFirst = false
} else {
UPDATE_DISPLAY(frameContent, displayId)
}
}
}

/**
* Displays all the frames in this sequence, with respect to the delays.
*
* @param sequence A sequence of [Frame] elements to be displayed.
*/
fun ScriptTemplateWithDisplayHelpers.ANIMATE(sequence: Sequence<Frame<*>>) {
ANIMATE(sequence.iterator())
}

/**
* Displays all the frames in this iterable, with respect to the delays.
*
* @param iterable Iterable of [Frame] elements to be displayed.
*/
fun ScriptTemplateWithDisplayHelpers.ANIMATE(iterable: Iterable<Frame<*>>) {
ANIMATE(iterable.iterator())
}

/**
* Triggers the animation using a chain of frames, beginning with [firstFrame]
* and subsequently obtained through the [nextFrame] function.
*
* @param firstFrame The first frame of the animation.
* @param nextFrame Function that takes the current frame and returns the next frame,
* or null if the animation is finished.
*/
fun <T> ScriptTemplateWithDisplayHelpers.ANIMATE(
firstFrame: Frame<T>,
nextFrame: (Frame<T>) -> Frame<T>?,
) {
ANIMATE(generateSequence(firstFrame, nextFrame))
}

/**
* Triggers the animation sequence consisting of frames that are produced by the [nextFrame] function.
*
* @param nextFrame A function that returns the next frame in the animation sequence.
*/
fun ScriptTemplateWithDisplayHelpers.ANIMATE(nextFrame: () -> Frame<*>?) {
ANIMATE(generateSequence(nextFrame))
}

/**
* Animates a sequence of frames by displaying them with respect to their delays.
*
* @param framesCount The number of frames to animate.
* @param frameByIndex A function that maps an index to a frame.
* The index represents the position of the frame in the sequence.
*/
fun ScriptTemplateWithDisplayHelpers.ANIMATE(
framesCount: Int,
frameByIndex: (index: Int) -> Frame<*>,
) {
ANIMATE(
sequence {
repeat(framesCount) { frameIndex ->
yield(frameByIndex(frameIndex))
}
},
)
}

/**
* Animates a sequence of values by displaying them with specified delay.
* The first value is displayed without a delay.
*
* @param delay The delay duration between each frame.
* @param iterator An iterator over the values to be animated.
*
* @throws InterruptedException if any thread is interrupted while waiting for frame delays.
*/
fun ScriptTemplateWithDisplayHelpers.ANIMATE(
delay: Duration,
iterator: Iterator<*>,
) {
ANIMATE(
object : Iterator<Frame<*>> {
private var isFirst = true

override fun hasNext(): Boolean {
return iterator.hasNext()
}

override fun next(): Frame<*> {
val content = iterator.next()
return Frame(if (isFirst) Duration.ZERO else delay, content).also {
isFirst = false
}
}
},
)
}

/**
* Animates a sequence of values with a specified delay between each frame.
* The first value is displayed without a delay.
*
* @param delay The delay duration between each frame.
* @param sequence The sequence of frames to animate.
*/
fun ScriptTemplateWithDisplayHelpers.ANIMATE(
delay: Duration,
sequence: Sequence<*>,
) {
ANIMATE(delay, sequence.iterator())
}

/**
* Animates a sequence of values with a specified delay between each frame.
* The first value is displayed without a delay.
*
* @param delay The delay duration between each frame.
* @param nextValue A function that provides the next value in the sequence.
*/
fun ScriptTemplateWithDisplayHelpers.ANIMATE(
delay: Duration,
nextValue: () -> Any?,
) {
ANIMATE(delay, generateSequence(nextValue))
}

/**
* Animates a sequence of values with a specified delay between each frame.
* The first value is displayed without a delay.
*
* @param delay The delay duration between each frame.
* @param firstValue The initial value of the sequence.
* @param nextValue A function that generates the next value based on the current value in the sequence.
* @param T The type of the values in the sequence.
*/
fun <T : Any> ScriptTemplateWithDisplayHelpers.ANIMATE(
delay: Duration,
firstValue: T?,
nextValue: (T) -> T?,
) {
ANIMATE(delay, generateSequence(firstValue, nextValue))
}

/**
* Animates an iterable sequence of values, each one (except first) appearing after a specified delay.
*
* @param delay The time delay to wait before displaying each frame.
* @param iterable The set of elements to display as animation frames.
*/
fun ScriptTemplateWithDisplayHelpers.ANIMATE(
delay: Duration,
iterable: Iterable<*>,
) {
ANIMATE(delay, iterable.iterator())
}

/**
* Animates a sequence of values with a specified delay between each frame.
* The first value is displayed without a delay.
*
* @param framesCount The number of frames to animate.
* @param delay The delay duration between each frame.
* @param valueByIndex The function that generates the content of each frame based on the frame index.
*/
fun ScriptTemplateWithDisplayHelpers.ANIMATE(
delay: Duration,
framesCount: Int,
valueByIndex: (index: Int) -> Any?,
) {
ANIMATE(
delay,
sequence {
repeat(framesCount) { frameIndex ->
yield(valueByIndex(frameIndex))
}
},
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,25 @@ package org.jetbrains.kotlinx.jupyter.config
import org.jetbrains.kotlinx.jupyter.api.libraries.KernelRepository
import org.jetbrains.kotlinx.jupyter.repl.MavenRepositoryCoordinates

val defaultGlobalImports =
private val dateTimeImports =
listOf(
"kotlin.math.*",
"jupyter.kotlin.*",
"org.jetbrains.kotlinx.jupyter.api.*",
"org.jetbrains.kotlinx.jupyter.api.libraries.*",
)
"nanoseconds",
"microseconds",
"milliseconds",
"seconds",
"minutes",
"hours",
"days",
).map { "kotlin.time.Duration.Companion.$it" }

val defaultGlobalImports =
buildList {
add("kotlin.math.*")
add("jupyter.kotlin.*")
add("org.jetbrains.kotlinx.jupyter.api.*")
add("org.jetbrains.kotlinx.jupyter.api.libraries.*")
addAll(dateTimeImports)
}

val defaultRepositories =
listOf(
Expand Down
21 changes: 16 additions & 5 deletions src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/TestUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import org.jetbrains.kotlinx.jupyter.api.KernelLoggerFactory
import org.jetbrains.kotlinx.jupyter.api.KotlinKernelVersion
import org.jetbrains.kotlinx.jupyter.api.LibraryLoader
import org.jetbrains.kotlinx.jupyter.api.MimeTypedResult
import org.jetbrains.kotlinx.jupyter.api.MimeTypes
import org.jetbrains.kotlinx.jupyter.api.Notebook
import org.jetbrains.kotlinx.jupyter.api.RenderersProcessor
import org.jetbrains.kotlinx.jupyter.api.ResultsAccessor
Expand Down Expand Up @@ -58,7 +59,9 @@ import org.jetbrains.kotlinx.jupyter.repl.EvalRequestData
import org.jetbrains.kotlinx.jupyter.repl.ReplForJupyter
import org.jetbrains.kotlinx.jupyter.repl.ReplRuntimeProperties
import org.jetbrains.kotlinx.jupyter.repl.creating.ReplComponentsProviderBase
import org.jetbrains.kotlinx.jupyter.repl.notebook.DisplayResultWrapper
import org.jetbrains.kotlinx.jupyter.repl.notebook.MutableCodeCell
import org.jetbrains.kotlinx.jupyter.repl.notebook.MutableDisplayResultWithCell
import org.jetbrains.kotlinx.jupyter.repl.notebook.MutableNotebook
import org.jetbrains.kotlinx.jupyter.repl.renderValue
import org.jetbrains.kotlinx.jupyter.repl.result.EvalResultEx
Expand Down Expand Up @@ -235,16 +238,14 @@ open class TestDisplayHandler(val list: MutableList<Any> = mutableListOf()) : Di
}
}

class TestDisplayHandlerWithRendering(
open class TestDisplayHandlerWithRendering(
private val notebook: MutableNotebook,
list: MutableList<Any> = mutableListOf(),
) : TestDisplayHandler(list) {
) : DisplayHandler {
override fun handleDisplay(
value: Any,
host: ExecutionHost,
id: String?,
) {
super.handleDisplay(value, host, id)
val display = renderValue(notebook, host, value, id)?.let { if (id != null) it.withId(id) else it } ?: return
notebook.currentCell?.addDisplay(display)
}
Expand All @@ -254,7 +255,6 @@ class TestDisplayHandlerWithRendering(
host: ExecutionHost,
id: String?,
) {
super.handleUpdate(value, host, id)
val display = renderValue(notebook, host, value, id) ?: return
val container = notebook.displays
container.update(id, display)
Expand Down Expand Up @@ -434,6 +434,17 @@ fun EvalResultEx.assertSuccess() {
}
}

val MimeTypedResult.text get() = this[MimeTypes.PLAIN_TEXT] as String

fun MutableDisplayResultWithCell.shouldBeText(): String {
shouldBeInstanceOf<DisplayResultWrapper>()

val display = display
display.shouldBeInstanceOf<MimeTypedResult>()

return display.text
}

fun Any?.shouldBeInstanceOf(kclass: KClass<*>) {
if (this == null) {
throw AssertionError("Expected instance of ${kclass.qualifiedName}, but got null")
Expand Down
Loading

0 comments on commit 8c50f7c

Please sign in to comment.