Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Ref tests #30

Merged
merged 5 commits into from
Jul 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Loading