Skip to content

Commit

Permalink
Merge pull request #30 from JD557/ref-tests
Browse files Browse the repository at this point in the history
Add Ref tests
  • Loading branch information
JD557 authored Jul 23, 2023
2 parents b7aa576 + 48f91ad commit 063d7d2
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 74 deletions.
147 changes: 85 additions & 62 deletions core/src/main/scala/eu/joaocosta/interim/api/Components.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ trait Components:

type Component[+T] = (inputState: InputState, uiState: UiState) ?=> T

trait ComponentWithValue[T] {
def apply(value: Ref[T]): Component[T]
def apply(value: T): Component[T] =
apply(Ref(value))
inline def applyUnion(value: T | Ref[T]): Component[T] = inline value match
case x: T => apply(x)
case x: Ref[T] => apply(x)
}

/** Button component. Returns true if it's being clicked, false otherwise.
*
* @param label text label to show on this button
Expand All @@ -30,71 +39,85 @@ trait Components:

/** Checkbox component. Returns true if it's enabled, false otherwise.
*/
final def checkbox(id: ItemId, area: Rect, skin: CheckboxSkin = CheckboxSkin.default())(
value: Boolean | Ref[Boolean]
): Component[Boolean] =
val checkboxArea = skin.checkboxArea(area)
val itemStatus = UiState.registerItem(id, checkboxArea)
skin.renderCheckbox(area, Ref.get(value), itemStatus)
if (itemStatus.hot && itemStatus.active && summon[InputState].mouseDown == false) Ref.modify(value, v => !v)
else Ref.get(value)
final def checkbox(id: ItemId, area: Rect, skin: CheckboxSkin = CheckboxSkin.default()): ComponentWithValue[Boolean] =
new ComponentWithValue[Boolean]:
def apply(value: Ref[Boolean]): Component[Boolean] =
val checkboxArea = skin.checkboxArea(area)
val itemStatus = UiState.registerItem(id, checkboxArea)
skin.renderCheckbox(area, Ref.get[Boolean](value), itemStatus)
if (itemStatus.hot && itemStatus.active && summon[InputState].mouseDown == false)
Ref.modify[Boolean](value, v => !v)
else Ref.get(value)

/** Radio button component. Returns value currently selected.
*
* @param buttonIndex the index of this button (value that this button returns when selected)
* @param buttonValue the value of this button (value that this button returns when selected)
* @param label text label to show on this button
*/
final def radioButton(
final def radioButton[T](
id: ItemId,
area: Rect,
buttonIndex: Int,
buttonValue: T,
label: String,
skin: ButtonSkin = ButtonSkin.default()
)(value: Int | Ref[Int]): Component[Int] =
val buttonArea = skin.buttonArea(area)
val itemStatus = UiState.registerItem(id, buttonArea)
val newValue =
if (itemStatus.hot && itemStatus.active && summon[InputState].mouseDown == false) Ref.set[Int](value, buttonIndex)
else Ref.get[Int](value)
if (newValue == buttonIndex) skin.renderButton(area, label, itemStatus.copy(hot = true, active = true))
else (skin.renderButton(area, label, itemStatus))
newValue
): ComponentWithValue[T] =
new ComponentWithValue[T]:
def apply(value: Ref[T]): Component[T] =
val buttonArea = skin.buttonArea(area)
val itemStatus = UiState.registerItem(id, buttonArea)
val newValue =
if (itemStatus.hot && itemStatus.active && summon[InputState].mouseDown == false)
Ref.set[T](value, buttonValue)
else Ref.get[T](value)
if (newValue == buttonValue) skin.renderButton(area, label, itemStatus.copy(hot = true, active = true))
else (skin.renderButton(area, label, itemStatus))
newValue

/** Slider component. Returns the current position of the slider, between min and max.
*
* @param min minimum value for this slider
* @param max maximum value fr this slider
*/
final def slider(id: ItemId, area: Rect, min: Int, max: Int, skin: SliderSkin = SliderSkin.default())(
value: Int | Ref[Int]
): Component[Int] =
val sliderArea = skin.sliderArea(area)
val sliderSize = skin.sliderSize(area, min, max)
val range = max - min
val itemStatus = UiState.registerItem(id, sliderArea)
val clampedValue = math.max(min, math.min(Ref.get[Int](value), max))
skin.renderSlider(area, min, clampedValue, max, itemStatus)
if (itemStatus.active)
if (area.w > area.h)
val mousePos = summon[InputState].mouseX - sliderArea.x - sliderSize / 2
val maxPos = sliderArea.w - sliderSize
Ref.set(value, math.max(min, math.min(min + (mousePos * range) / maxPos, max)))
else
val mousePos = summon[InputState].mouseY - sliderArea.y - sliderSize / 2
val maxPos = sliderArea.h - sliderSize
Ref.set(value, math.max(min, math.min((mousePos * range) / maxPos, max)))
else Ref.get(value)
final def slider(
id: ItemId,
area: Rect,
min: Int,
max: Int,
skin: SliderSkin = SliderSkin.default()
): ComponentWithValue[Int] =
new ComponentWithValue[Int]:
def apply(value: Ref[Int]): Component[Int] =
val sliderArea = skin.sliderArea(area)
val sliderSize = skin.sliderSize(area, min, max)
val range = max - min
val itemStatus = UiState.registerItem(id, sliderArea)
val clampedValue = math.max(min, math.min(Ref.get[Int](value), max))
skin.renderSlider(area, min, clampedValue, max, itemStatus)
if (itemStatus.active)
if (area.w > area.h)
val mousePos = summon[InputState].mouseX - sliderArea.x - sliderSize / 2
val maxPos = sliderArea.w - sliderSize
Ref.set(value, math.max(min, math.min(min + (mousePos * range) / maxPos, max)))
else
val mousePos = summon[InputState].mouseY - sliderArea.y - sliderSize / 2
val maxPos = sliderArea.h - sliderSize
Ref.set(value, math.max(min, math.min((mousePos * range) / maxPos, max)))
else Ref.get(value)

/** Text input component. Returns the current string inputed.
*/
final def textInput(id: ItemId, area: Rect, skin: TextInputSkin = TextInputSkin.default())(
value: String | Ref[String]
): Component[String] =
val textInputArea = skin.textInputArea(area)
val itemStatus = UiState.registerItem(id, textInputArea)
skin.renderTextInput(area, Ref.get(value), itemStatus)
if (itemStatus.keyboardFocus) Ref.modify(value, summon[InputState].appendKeyboardInput)
else Ref.get(value)
final def textInput(
id: ItemId,
area: Rect,
skin: TextInputSkin = TextInputSkin.default()
): ComponentWithValue[String] =
new ComponentWithValue[String]:
def apply(value: Ref[String]): Component[String] =
val textInputArea = skin.textInputArea(area)
val itemStatus = UiState.registerItem(id, textInputArea)
skin.renderTextInput(area, Ref.get(value), itemStatus)
if (itemStatus.keyboardFocus) Ref.modify(value, summon[InputState].appendKeyboardInput)
else Ref.get(value)

/** Draggable handle. Returns the moved area.
*
Expand All @@ -103,18 +126,18 @@ trait Components:
* Instead of using this component directly, it can be easier to use [[eu.joaocosta.interim.api.Panels.window]]
* with movable = true.
*/
final def moveHandle(id: ItemId, area: Rect, skin: HandleSkin = HandleSkin.default())(
value: Rect | Ref[Rect]
): Component[Rect] =
val handleArea = skin.handleArea(area)
val itemStatus = UiState.registerItem(id, handleArea)
skin.renderHandle(area, Ref.get(value), itemStatus)
if (itemStatus.active)
val handleCenterX = handleArea.x + handleArea.w / 2
val handleCenterY = handleArea.y + handleArea.h / 2
val mouseX = summon[InputState].mouseX
val mouseY = summon[InputState].mouseY
val deltaX = mouseX - handleCenterX
val deltaY = mouseY - handleCenterY
Ref.modify(value, _.move(deltaX, deltaY))
else Ref.get(value)
final def moveHandle(id: ItemId, area: Rect, skin: HandleSkin = HandleSkin.default()): ComponentWithValue[Rect] =
new ComponentWithValue[Rect]:
def apply(value: Ref[Rect]): Component[Rect] =
val handleArea = skin.handleArea(area)
val itemStatus = UiState.registerItem(id, handleArea)
skin.renderHandle(area, Ref.get(value), itemStatus)
if (itemStatus.active)
val handleCenterX = handleArea.x + handleArea.w / 2
val handleCenterY = handleArea.y + handleArea.h / 2
val mouseX = summon[InputState].mouseX
val mouseY = summon[InputState].mouseY
val deltaX = mouseX - handleCenterX
val deltaY = mouseY - handleCenterY
Ref.modify(value, _.move(deltaX, deltaY))
else Ref.get(value)
14 changes: 8 additions & 6 deletions core/src/main/scala/eu/joaocosta/interim/api/Panels.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ trait Panels:
* @param title of this window
* @param movable if true, the window will include a move handle in the title bar
*/
final def window[T](
final inline def window[T](
id: ItemId,
area: Rect | Ref[Rect],
title: String,
Expand All @@ -42,10 +42,12 @@ trait Panels:
skin.renderWindow(oldArea, title)
val nextArea: Rect =
if (movable)
Components.moveHandle(
id |> "internal_move_handle",
skin.titleTextArea(oldArea),
handleSkin
)(area)
Components
.moveHandle(
id |> "internal_move_handle",
skin.titleTextArea(oldArea),
handleSkin
)
.applyUnion(area)
else oldArea
(body(skin.panelArea(oldArea)), nextArea)
10 changes: 4 additions & 6 deletions core/src/main/scala/eu/joaocosta/interim/api/Ref.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,20 @@ package eu.joaocosta.interim.api
*
* When a function receives a Ref as an argument, it will probably mutate it.
*/
final case class Ref[T](var value: T) {
final case class Ref[T](var value: T):

/** Assigns a value to this Ref.
* Shorthand for `ref.value = x`
*/
def :=(newValue: T): this.type =
value = newValue
this
}

object Ref {
object Ref:

/** Gets a value from a Ref or from a plain value.
*/
inline def get[T](x: T | Ref[T]): T = x match
inline def get[T](x: T | Ref[T]): T = inline x match
case value: T => value
case ref: Ref[T] => ref.value

Expand All @@ -32,7 +31,7 @@ object Ref {
*
* The new value is returned. Refs will be mutated while immutable values will not.
*/
inline def modify[T](x: T | Ref[T], f: T => T): T = x match
inline def modify[T](x: T | Ref[T], f: T => T): T = inline x match
case value: T =>
f(value)
case ref: Ref[T] =>
Expand All @@ -53,4 +52,3 @@ object Ref {
* Useful to set temporary mutable variables.
*/
extension [T](x: T) def asRef(block: Ref[T] => Unit): T = withRef(x)(block)
}
46 changes: 46 additions & 0 deletions core/src/test/scala/eu/joaocosta/interim/api/RefSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package eu.joaocosta.interim.api

class RefSpec extends munit.FunSuite:
test("A Ref value can be correctly set and retrieved with := and value"):
val x = Ref(1)
assertEquals(x.value, 1)
x := 2
assertEquals(x.value, 2)
x.value = 3
assertEquals(x.value, 3)

test("Ref values and raw values can be fetched with Ref.get"):
val x = Ref(1)
val y = 1
assertEquals(Ref.get[Int](x), Ref.get[Int](y))

test("Ref values and raw values can be set with Ref.set"):
val x = Ref(1)
val y = 1

assertEquals(Ref.set[Int](x, 2), 2)
assertEquals(Ref.set[Int](y, 2), 2)
assertEquals(x.value, 2)
assertEquals(y, 1)

test("Ref values and raw values can be modified with Ref.modify"):
val x = Ref(1)
val y = 1

assertEquals(Ref.modify[Int](x, _ + 1), 2)
assertEquals(Ref.modify[Int](y, _ + 1), 2)
assertEquals(x.value, 2)
assertEquals(y, 1)

test("withRef allows to use a temporary Ref value"):
val result = Ref.withRef(0) { ref =>
Ref.modify[Int](ref, _ + 2)
}
assertEquals(result, 2)

test("asRef allows to use a temporary Ref value"):
import Ref.asRef
val result = 0.asRef { ref =>
Ref.modify[Int](ref, _ + 2)
}
assertEquals(result, 2)

0 comments on commit 063d7d2

Please sign in to comment.