Skip to content

Commit

Permalink
Merge pull request #75 from JD557/optional-mouse
Browse files Browse the repository at this point in the history
Make mouse position optional
  • Loading branch information
JD557 committed Nov 15, 2023
2 parents 3924751 + dbc827f commit 799cb94
Show file tree
Hide file tree
Showing 7 changed files with 58 additions and 26 deletions.
52 changes: 41 additions & 11 deletions core/src/main/scala/eu/joaocosta/interim/InputState.scala
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,22 @@ sealed trait InputState:
.filterNot(Character.isISOControl)
.mkString

/** Clips the mouse position to a rectagle. If the mouse is outside of the region, it will be moved to (Int.MinValue, Int.MinValue) */
/** Clips the mouse position to a rectagle. If the mouse is outside of the region, the position is set to None */
def clip(area: Rect): InputState

object InputState:

/** Creates a new InputState.
*
* @param mousePosition optional mouse (x, y) position, from the top-left
* @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(mousePosition: Option[(Int, Int)], mouseDown: Boolean, keyboardInput: String): InputState =
InputState.Current(InputState.MouseInput(mousePosition, mouseDown), keyboardInput)

/** Creates a new InputState.
*
* @param mouseX mouse X position, from the left
Expand All @@ -50,15 +61,36 @@ object InputState:
* A `\u0008` character is interpreted as a backspace.
*/
def apply(mouseX: Int, mouseY: Int, mouseDown: Boolean, keyboardInput: String): InputState =
InputState.Current(InputState.MouseInput(mouseX, mouseY, mouseDown), keyboardInput)
InputState.Current(InputState.MouseInput(Some((mouseX, mouseY)), mouseDown), keyboardInput)

/** Creates a new InputState with an unknown mouse position.
*
* @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(mouseDown: Boolean, keyboardInput: String): InputState =
InputState.Current(InputState.MouseInput(None, mouseDown), keyboardInput)

/** Mouse position and button state.
*
* @param x mouse X position, from the left
* @param y mouse Y position, from the top
* @param position mouse position in a (x, y) tuple. None if the mouse is off-screen.
* @param isPressed whether the mouse is pressed
*/
final case class MouseInput(x: Int, y: Int, isPressed: Boolean)
final case class MouseInput(position: Option[(Int, Int)], isPressed: Boolean):
def x = position.map(_._1)
def y = position.map(_._2)

object MouseInput:
/** Mouse position and button state.
*
* @param x mouse position from the left side
* @param y mouse position from the top
* @param isPressed whether the mouse is pressed
*/
def apply(x: Int, y: Int, isPressed: Boolean): MouseInput =
MouseInput(position = Some((x, y)), isPressed = isPressed)

/** Input state at the current point in time
*
Expand All @@ -71,7 +103,7 @@ object InputState:

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

/** Input state at the current point in time and in the previous frame
*
Expand All @@ -95,14 +127,12 @@ object InputState:

/** How much the mouse moved in the X axis */
lazy val deltaX: Int =
if (previousMouseInput.x == Int.MinValue || mouseInput.x == Int.MinValue) 0
else mouseInput.x - previousMouseInput.x
mouseInput.x.zip(previousMouseInput.x).fold(0)((curr, prev) => curr - prev)

/** How much the mouse moved in the Y axis */
lazy val deltaY: Int =
if (previousMouseInput.y == Int.MinValue || mouseInput.y == Int.MinValue) 0
else mouseInput.y - previousMouseInput.y
mouseInput.y.zip(previousMouseInput.y).fold(0)((curr, prev) => curr - prev)

def clip(area: Rect): InputState.Historical =
if (area.isMouseOver(using this)) this
else this.copy(mouseInput = mouseInput.copy(x = Int.MinValue, y = Int.MinValue))
else this.copy(mouseInput = mouseInput.copy(position = None))
4 changes: 3 additions & 1 deletion core/src/main/scala/eu/joaocosta/interim/Rect.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ 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.mouseInput.x < x || inputState.mouseInput.y < y || inputState.mouseInput.x >= x + w || inputState.mouseInput.y >= y + h)
inputState.mouseInput.position.exists((mouseX, mouseY) =>
!(mouseX < x || mouseY < y || mouseX >= x + w || mouseY >= y + h)
)

/** Translates the area to another position.
*/
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/scala/eu/joaocosta/interim/UiContext.scala
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ final class UiContext private (
val history = InputState.Historical(
previousMouseInput = previousInputState
.map(_.mouseInput)
.getOrElse(InputState.MouseInput(Int.MinValue, Int.MinValue, false)),
.getOrElse(InputState.MouseInput(None, false)),
mouseInput = inputState.mouseInput,
keyboardInput = inputState.keyboardInput
)
Expand Down
17 changes: 9 additions & 8 deletions core/src/main/scala/eu/joaocosta/interim/api/Components.scala
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,15 @@ trait Components:
val clampedValue = math.max(min, math.min(value.get, max))
skin.renderSlider(area, min, clampedValue, max, itemStatus)
if (itemStatus.active)
if (area.w > area.h)
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].mouseInput.y - sliderArea.y - sliderSize / 2
val maxPos = sliderArea.h - sliderSize
value := math.max(min, math.min((mousePos * range) / maxPos, max))
summon[InputState].mouseInput.position.foreach: (mouseX, mouseY) =>
if (area.w > area.h)
val mousePos = mouseX - sliderArea.x - sliderSize / 2
val maxPos = sliderArea.w - sliderSize
value := math.max(min, math.min(min + (mousePos * range) / maxPos, max))
else
val mousePos = mouseY - sliderArea.y - sliderSize / 2
val maxPos = sliderArea.h - sliderSize
value := math.max(min, math.min((mousePos * range) / maxPos, max))

/** Text input component. Returns the current string inputed.
*/
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/scala/eu/joaocosta/interim/api/Layouts.scala
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,6 @@ trait Layouts:
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)
inputState.mouseInput.copy(position = inputState.mouseInput.position.map((x, y) => (x - area.x, y - area.y)))
)
)
4 changes: 2 additions & 2 deletions core/src/test/scala/eu/joaocosta/interim/UiContextSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,6 @@ class UiContextSpec extends munit.FunSuite:
val inputState2 = uiContext.pushInputState(InputState(6, 7, false, ""))
assertEquals(inputState2.deltaX, 1)
assertEquals(inputState2.deltaY, 2)
val inputState3 = uiContext.pushInputState(InputState(Int.MinValue, 6, false, ""))
val inputState3 = uiContext.pushInputState(InputState(false, ""))
assertEquals(inputState3.deltaX, 0)
assertEquals(inputState3.deltaY, -1)
assertEquals(inputState3.deltaY, 0)
3 changes: 1 addition & 2 deletions examples/snapshot/example-minart-backend.scala
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,7 @@ object MinartBackend:
.mkString

private def getInputState(canvas: Canvas): InputState = InputState(
canvas.getPointerInput().position.map(_.x).getOrElse(Int.MinValue),
canvas.getPointerInput().position.map(_.y).getOrElse(Int.MinValue),
canvas.getPointerInput().position.map(pos => (pos.x, pos.y)),
canvas.getPointerInput().isPressed,
processKeyboard(canvas.getKeyboardInput())
)
Expand Down

0 comments on commit 799cb94

Please sign in to comment.