Skip to content

Commit

Permalink
Merge pull request #64 from JD557/add-mouse-area
Browse files Browse the repository at this point in the history
Add mouse area layout
  • Loading branch information
JD557 committed Nov 4, 2023
2 parents 7832e10 + 20a39ff commit 1ee5ad4
Show file tree
Hide file tree
Showing 7 changed files with 58 additions and 41 deletions.
52 changes: 25 additions & 27 deletions core/src/main/scala/eu/joaocosta/interim/InputState.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,8 @@ import scala.annotation.tailrec
/** Input State to be used by the components. */
sealed trait InputState:

/** Mouse X position, from the left */
def mouseX: Int

/** Mouse Y position, from the top */
def mouseY: Int

/** @param mouseDown whether the mouse is pressed */
def mouseDown: Boolean
/** Current Mouse state */
def mouseInput: InputState.MouseInput

/** String generated from the keyboard inputs since the last frame. Usually this will be a single character.
* A `\u0008` character is interpreted as a backspace.
Expand Down Expand Up @@ -45,63 +39,67 @@ sealed trait InputState:
def clip(area: Rect): InputState

object InputState:
/** Creates a new Input State

/** Creates a new InputState.
*
* @param mouseX mouse X position, from the left
* @param mouseY mouse Y position, from the top
* @param mouseDown whether the body is pressed
* @param mousePressed whether the mouse is pressed
* @param keyboardInput
* String generated from the keyboard inputs since the last frame. Usually this will be a single character.
* A `\u0008` character is interpreted as a backspace.
*/
def apply(mouseX: Int, mouseY: Int, mouseDown: Boolean, keyboardInput: String): InputState =
InputState.Current(mouseX, mouseY, mouseDown, keyboardInput: String)
InputState.Current(InputState.MouseInput(mouseX, mouseY, mouseDown), keyboardInput)

/** Mouse position and button state.
*
* @param x mouse X position, from the left
* @param y mouse Y position, from the top
* @param isPressed whether the mouse is pressed
*/
final case class MouseInput(x: Int, y: Int, isPressed: Boolean)

/** Input state at the current point in time
*
* @param mouseX mouse X position, from the left
* @param mouseY mouse Y position, from the top
* @param mouseDown whether the body is pressed
* @param mouseInput the current mouse state
* @param keyboardInput
* String generated from the keyboard inputs since the last frame. Usually this will be a single character.
* A `\u0008` character is interpreted as a backspace.
*/
final case class Current(mouseX: Int, mouseY: Int, mouseDown: Boolean, keyboardInput: String) extends InputState:
final case class Current(mouseInput: InputState.MouseInput, keyboardInput: String) extends InputState:

def clip(area: Rect): InputState.Current =
if (area.isMouseOver(using this)) this
else this.copy(mouseX = Int.MinValue, mouseY = Int.MinValue)
else this.copy(mouseInput = mouseInput.copy(x = Int.MinValue, y = Int.MinValue))

/** Input state at the current point in time and in the previous frame
*
* @param previousMouseX previous mouse X position, from the left
* @param previousMouseY previous mouse Y position, from the top
* @param mouseX mouse X position, from the left
* @param mouseY mouse Y position, from the top
* @param mouseDown whether the body is pressed
* @param mouseDown whether the mouse is pressed
* @param keyboardInput
* String generated from the keyboard inputs since the last frame. Usually this will be a single character.
* A `\u0008` character is interpreted as a backspace.
*/
final case class Historical(
previousMouseX: Int,
previousMouseY: Int,
mouseX: Int,
mouseY: Int,
mouseDown: Boolean,
previousMouseInput: MouseInput,
mouseInput: MouseInput,
keyboardInput: String
) extends InputState:

/** How much the mouse moved in the X axis */
lazy val deltaX: Int =
if (previousMouseX == Int.MinValue || mouseX == Int.MinValue) 0
else mouseX - previousMouseX
if (previousMouseInput.x == Int.MinValue || mouseInput.x == Int.MinValue) 0
else mouseInput.x - previousMouseInput.x

/** How much the mouse moved in the Y axis */
lazy val deltaY: Int =
if (previousMouseY == Int.MinValue || mouseY == Int.MinValue) 0
else mouseY - previousMouseY
if (previousMouseInput.y == Int.MinValue || mouseInput.y == Int.MinValue) 0
else mouseInput.y - previousMouseInput.y

def clip(area: Rect): InputState.Historical =
if (area.isMouseOver(using this)) this
else this.copy(mouseX = Int.MinValue, mouseY = Int.MinValue)
else this.copy(mouseInput = mouseInput.copy(x = Int.MinValue, y = Int.MinValue))
4 changes: 2 additions & 2 deletions core/src/main/scala/eu/joaocosta/interim/InterIm.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ object InterIm extends api.Primitives with api.Layouts with api.Components with
uiContext.currentZ = 0
uiContext.hotItem = None
val historicalInputState = uiContext.pushInputState(inputState)
if (inputState.mouseDown) uiContext.selectedItem = None
if (inputState.mouseInput.isPressed) uiContext.selectedItem = None
// run
val res = run(using historicalInputState, uiContext)
// finish
if (!historicalInputState.mouseDown) uiContext.activeItem = None
if (!historicalInputState.mouseInput.isPressed) uiContext.activeItem = None
// return
(uiContext.getOrderedOps(), res)
2 changes: 1 addition & 1 deletion core/src/main/scala/eu/joaocosta/interim/Rect.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ final case class Rect(x: Int, y: Int, w: Int, h: Int):
/** Checks if the mouse is over this area.
*/
def isMouseOver(using inputState: InputState): Boolean =
!(inputState.mouseX < x || inputState.mouseY < y || inputState.mouseX >= x + w || inputState.mouseY >= y + h)
!(inputState.mouseInput.x < x || inputState.mouseInput.y < y || inputState.mouseInput.x >= x + w || inputState.mouseInput.y >= y + h)

/** Translates the area to another position.
*/
Expand Down
11 changes: 5 additions & 6 deletions core/src/main/scala/eu/joaocosta/interim/UiContext.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ final class UiContext private (
): UiContext.ItemStatus =
if (area.isMouseOver && hotItem.forall((hotZ, _) => hotZ <= currentZ))
hotItem = Some(currentZ -> id)
if (!passive && (activeItem == None || activeItem == Some(id)) && inputState.mouseDown)
if (!passive && (activeItem == None || activeItem == Some(id)) && inputState.mouseInput.isPressed)
activeItem = Some(id)
selectedItem = Some(id)
UiContext.ItemStatus(hotItem.map(_._2) == Some(id), activeItem == Some(id), selectedItem == Some(id))
Expand All @@ -33,11 +33,10 @@ final class UiContext private (

private[interim] def pushInputState(inputState: InputState): InputState.Historical =
val history = InputState.Historical(
previousMouseX = previousInputState.map(_.mouseX).getOrElse(Int.MinValue),
previousMouseY = previousInputState.map(_.mouseY).getOrElse(Int.MinValue),
mouseX = inputState.mouseX,
mouseY = inputState.mouseY,
mouseDown = inputState.mouseDown,
previousMouseInput = previousInputState
.map(_.mouseInput)
.getOrElse(InputState.MouseInput(Int.MinValue, Int.MinValue, false)),
mouseInput = inputState.mouseInput,
keyboardInput = inputState.keyboardInput
)
previousInputState = Some(inputState)
Expand Down
10 changes: 5 additions & 5 deletions core/src/main/scala/eu/joaocosta/interim/api/Components.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ trait Components:
val buttonArea = skin.buttonArea(area)
val itemStatus = UiContext.registerItem(id, buttonArea)
skin.renderButton(area, label, itemStatus)
itemStatus.hot && itemStatus.active && summon[InputState].mouseDown == false
itemStatus.hot && itemStatus.active && summon[InputState].mouseInput.isPressed == false

/** Checkbox component. Returns true if it's enabled, false otherwise.
*/
Expand All @@ -46,7 +46,7 @@ trait Components:
val checkboxArea = skin.checkboxArea(area)
val itemStatus = UiContext.registerItem(id, checkboxArea)
skin.renderCheckbox(area, value.get, itemStatus)
if (itemStatus.hot && itemStatus.active && summon[InputState].mouseDown == false) value.modify(!_)
if (itemStatus.hot && itemStatus.active && summon[InputState].mouseInput.isPressed == false) value.modify(!_)
value.get

/** Radio button component. Returns value currently selected.
Expand All @@ -65,7 +65,7 @@ trait Components:
def applyRef(value: Ref[T]): Component[T] =
val buttonArea = skin.buttonArea(area)
val itemStatus = UiContext.registerItem(id, buttonArea)
if (itemStatus.hot && itemStatus.active && summon[InputState].mouseDown == false)
if (itemStatus.hot && itemStatus.active && summon[InputState].mouseInput.isPressed == false)
value := buttonValue
if (value.get == buttonValue) skin.renderButton(area, label, itemStatus.copy(hot = true, active = true))
else skin.renderButton(area, label, itemStatus)
Expand Down Expand Up @@ -118,11 +118,11 @@ trait Components:
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 mousePos = summon[InputState].mouseInput.x - sliderArea.x - sliderSize / 2
val maxPos = sliderArea.w - sliderSize
value := math.max(min, math.min(min + (mousePos * range) / maxPos, max))
else
val mousePos = summon[InputState].mouseY - sliderArea.y - sliderSize / 2
val mousePos = summon[InputState].mouseInput.y - sliderArea.y - sliderSize / 2
val maxPos = sliderArea.h - sliderSize
value := math.max(min, math.min((mousePos * range) / maxPos, max))
value.get
Expand Down
13 changes: 13 additions & 0 deletions core/src/main/scala/eu/joaocosta/interim/api/Layouts.scala
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,16 @@ trait Layouts:
currentW -= absWidth + padding
area.copy(x = areaX, w = absWidth)
body(generateRect)

/** Handle mouse events inside a specified area.
*
* The body receives an optional MouseInput with the coordinates adjusted to be relative
* to the enclosing area.
* If the mouse is outside of the area, the body receives None.
*/
final def mouseArea[T](area: Rect)(body: Option[InputState.MouseInput] => T)(using inputState: InputState): T =
body(
Option.when(area.isMouseOver)(
inputState.mouseInput.copy(x = inputState.mouseInput.x - area.x, y = inputState.mouseInput.y - area.y)
)
)
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,10 @@ class LayoutsSpec extends munit.FunSuite:
val expected =
Vector(Rect(10, 10, 16, 100), Rect(78, 10, 32, 100), Rect(34, 10, 36, 100))
assertEquals(areas, expected)

test("mouseArea passes un updated mouse input"):
val inputState = InputState(10, 10, false, "")
val inArea = Layouts.mouseArea(Rect(5, 5, 10, 10))(identity)(using inputState)
assertEquals(inArea, Some(InputState.MouseInput(5, 5, false)))
val outsideArea = Layouts.mouseArea(Rect(5, 5, 1, 1))(identity)(using inputState)
assertEquals(outsideArea, None)

0 comments on commit 1ee5ad4

Please sign in to comment.