From 2f40d42dddbc3188021a592fae4057f3231b9ebe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Costa?= Date: Sun, 12 Nov 2023 18:25:11 +0100 Subject: [PATCH] Add clicked helper methods --- .../eu/joaocosta/interim/InputState.scala | 3 +++ .../scala/eu/joaocosta/interim/UiContext.scala | 11 ++++++++--- .../eu/joaocosta/interim/api/Components.scala | 7 +++---- .../joaocosta/interim/skins/ButtonSkin.scala | 10 +++++----- .../joaocosta/interim/skins/CheckboxSkin.scala | 6 +++--- .../joaocosta/interim/skins/HandleSkin.scala | 18 +++++++++--------- .../joaocosta/interim/skins/SelectSkin.scala | 12 ++++++------ .../joaocosta/interim/skins/SliderSkin.scala | 4 ++-- .../interim/skins/TextInputSkin.scala | 6 +++--- .../eu/joaocosta/interim/UiContextSpec.scala | 15 +++++++++++++++ 10 files changed, 57 insertions(+), 35 deletions(-) diff --git a/core/src/main/scala/eu/joaocosta/interim/InputState.scala b/core/src/main/scala/eu/joaocosta/interim/InputState.scala index 45f571b..133e4a6 100644 --- a/core/src/main/scala/eu/joaocosta/interim/InputState.scala +++ b/core/src/main/scala/eu/joaocosta/interim/InputState.scala @@ -90,6 +90,9 @@ object InputState: keyboardInput: String ) extends InputState: + /** If true, then the mouse was released on this frame, performing a click */ + lazy val mouseClicked: Boolean = mouseInput.isPressed == false && previousMouseInput.isPressed == true + /** How much the mouse moved in the X axis */ lazy val deltaX: Int = if (previousMouseInput.x == Int.MinValue || mouseInput.x == Int.MinValue) 0 diff --git a/core/src/main/scala/eu/joaocosta/interim/UiContext.scala b/core/src/main/scala/eu/joaocosta/interim/UiContext.scala index c6120b1..355c954 100644 --- a/core/src/main/scala/eu/joaocosta/interim/UiContext.scala +++ b/core/src/main/scala/eu/joaocosta/interim/UiContext.scala @@ -26,7 +26,11 @@ final class UiContext private ( 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)) + val hot = hotItem.map(_._2) == Some(id) + val active = activeItem == Some(id) + val selected = selectedItem == Some(id) + val clicked = hot && active && inputState.mouseInput.isPressed == false + UiContext.ItemStatus(hot, active, selected, clicked) private[interim] def getOrderedOps(): List[RenderOp] = ops.values.toList.flatten @@ -78,10 +82,11 @@ object UiContext: * @param hot if the mouse is on top of the item * @param active if the mouse clicked the item (and is still pressed down). * This value stays true for one extra frame, so that it's - * possible to trigger an action on mouse up. + * possible to trigger an action on mouse up (see `clicked`). * @param selected if this was the last element clicked + * @param clicked if the mouse clicked this element and was just released. */ - final case class ItemStatus(hot: Boolean, active: Boolean, selected: Boolean) + final case class ItemStatus(hot: Boolean, active: Boolean, selected: Boolean, clicked: Boolean) /** Registers an item on the UI state, taking a certain area. * diff --git a/core/src/main/scala/eu/joaocosta/interim/api/Components.scala b/core/src/main/scala/eu/joaocosta/interim/api/Components.scala index 8018b8d..4581af2 100644 --- a/core/src/main/scala/eu/joaocosta/interim/api/Components.scala +++ b/core/src/main/scala/eu/joaocosta/interim/api/Components.scala @@ -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].mouseInput.isPressed == false + itemStatus.clicked /** Checkbox component. Returns true if it's enabled, false otherwise. */ @@ -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].mouseInput.isPressed == false) value.modify(!_) + if (itemStatus.clicked) value.modify(!_) value.get /** Radio button component. Returns value currently selected. @@ -65,8 +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].mouseInput.isPressed == false) - value := buttonValue + if (itemStatus.clicked) value := buttonValue if (value.get == buttonValue) skin.renderButton(area, label, itemStatus.copy(hot = true, active = true)) else skin.renderButton(area, label, itemStatus) value.get diff --git a/core/src/main/scala/eu/joaocosta/interim/skins/ButtonSkin.scala b/core/src/main/scala/eu/joaocosta/interim/skins/ButtonSkin.scala index e624f92..9c93222 100644 --- a/core/src/main/scala/eu/joaocosta/interim/skins/ButtonSkin.scala +++ b/core/src/main/scala/eu/joaocosta/interim/skins/ButtonSkin.scala @@ -36,16 +36,16 @@ object ButtonSkin extends DefaultSkin: shadowColor ) itemStatus match - case UiContext.ItemStatus(false, false, _) => + case UiContext.ItemStatus(false, false, _, _) => rectangle(buttonArea, inactiveColor) - case UiContext.ItemStatus(true, false, _) => + case UiContext.ItemStatus(true, false, _, _) => rectangle(buttonArea, hotColor) - case UiContext.ItemStatus(false, true, _) => + case UiContext.ItemStatus(false, true, _, _) => rectangle(buttonArea, activeColor) - case UiContext.ItemStatus(true, true, _) => + case UiContext.ItemStatus(true, true, _, _) => rectangle(clickedArea, activeColor) itemStatus match - case UiContext.ItemStatus(true, true, _) => + case UiContext.ItemStatus(true, true, _, _) => text(clickedArea, textColor, label, font, HorizontalAlignment.Center, VerticalAlignment.Center) case _ => text(buttonArea, textColor, label, font, HorizontalAlignment.Center, VerticalAlignment.Center) diff --git a/core/src/main/scala/eu/joaocosta/interim/skins/CheckboxSkin.scala b/core/src/main/scala/eu/joaocosta/interim/skins/CheckboxSkin.scala index e8a295d..571d5db 100644 --- a/core/src/main/scala/eu/joaocosta/interim/skins/CheckboxSkin.scala +++ b/core/src/main/scala/eu/joaocosta/interim/skins/CheckboxSkin.scala @@ -24,11 +24,11 @@ object CheckboxSkin extends DefaultSkin: def renderCheckbox(area: Rect, value: Boolean, itemStatus: UiContext.ItemStatus)(using uiContext: UiContext): Unit = val checkboxArea = this.checkboxArea(area) itemStatus match - case UiContext.ItemStatus(false, false, _) => + case UiContext.ItemStatus(false, false, _, _) => rectangle(checkboxArea, inactiveColor) - case UiContext.ItemStatus(true, false, _) => + case UiContext.ItemStatus(true, false, _, _) => rectangle(checkboxArea, hotColor) - case UiContext.ItemStatus(_, true, _) => + case UiContext.ItemStatus(_, true, _, _) => rectangle(checkboxArea, activeColor) if (value) rectangle( diff --git a/core/src/main/scala/eu/joaocosta/interim/skins/HandleSkin.scala b/core/src/main/scala/eu/joaocosta/interim/skins/HandleSkin.scala index c34b409..2d08e4d 100644 --- a/core/src/main/scala/eu/joaocosta/interim/skins/HandleSkin.scala +++ b/core/src/main/scala/eu/joaocosta/interim/skins/HandleSkin.scala @@ -27,9 +27,9 @@ object HandleSkin extends DefaultSkin: def renderMoveHandle(area: Rect, itemStatus: UiContext.ItemStatus)(using uiContext: UiContext): Unit = val handleArea = this.moveHandleArea(area) val color = itemStatus match - case UiContext.ItemStatus(false, false, _) => inactiveColor - case UiContext.ItemStatus(true, false, _) => hotColor - case UiContext.ItemStatus(_, true, _) => activeColor + case UiContext.ItemStatus(false, false, _, _) => inactiveColor + case UiContext.ItemStatus(true, false, _, _) => hotColor + case UiContext.ItemStatus(_, true, _, _) => activeColor val lineHeight = handleArea.h / 3 rectangle(handleArea.copy(h = lineHeight), color) rectangle(handleArea.copy(y = handleArea.y + 2 * lineHeight, h = lineHeight), color) @@ -41,9 +41,9 @@ object HandleSkin extends DefaultSkin: def renderCloseHandle(area: Rect, itemStatus: UiContext.ItemStatus)(using uiContext: UiContext): Unit = val handleArea = this.closeHandleArea(area) val color = itemStatus match - case UiContext.ItemStatus(false, false, _) => inactiveColor - case UiContext.ItemStatus(true, false, _) => hotColor - case UiContext.ItemStatus(_, true, _) => activeColor + case UiContext.ItemStatus(false, false, _, _) => inactiveColor + case UiContext.ItemStatus(true, false, _, _) => hotColor + case UiContext.ItemStatus(_, true, _, _) => activeColor rectangle(handleArea, color) def resizeHandleArea(area: Rect): Rect = @@ -53,9 +53,9 @@ object HandleSkin extends DefaultSkin: def renderResizeHandle(area: Rect, itemStatus: UiContext.ItemStatus)(using uiContext: UiContext): Unit = val handleArea = this.resizeHandleArea(area) val color = itemStatus match - case UiContext.ItemStatus(false, false, _) => inactiveColor - case UiContext.ItemStatus(true, false, _) => hotColor - case UiContext.ItemStatus(_, true, _) => activeColor + case UiContext.ItemStatus(false, false, _, _) => inactiveColor + case UiContext.ItemStatus(true, false, _, _) => hotColor + case UiContext.ItemStatus(_, true, _, _) => activeColor val lineSize = handleArea.h / 3 rectangle(handleArea.move(dx = handleArea.w - lineSize, dy = 0).copy(w = lineSize), color) rectangle(handleArea.move(dx = 0, dy = handleArea.h - lineSize).copy(h = lineSize), color) diff --git a/core/src/main/scala/eu/joaocosta/interim/skins/SelectSkin.scala b/core/src/main/scala/eu/joaocosta/interim/skins/SelectSkin.scala index 589277d..62e45d1 100644 --- a/core/src/main/scala/eu/joaocosta/interim/skins/SelectSkin.scala +++ b/core/src/main/scala/eu/joaocosta/interim/skins/SelectSkin.scala @@ -35,11 +35,11 @@ object SelectSkin extends DefaultSkin: val selectBoxArea = this.selectBoxArea(area) val selectedLabel = labels.applyOrElse(value, _ => "") itemStatus match - case UiContext.ItemStatus(_, _, true) | UiContext.ItemStatus(_, true, _) => + case UiContext.ItemStatus(_, _, true, _) | UiContext.ItemStatus(_, true, _, _) => rectangle(selectBoxArea, activeColor) - case UiContext.ItemStatus(true, _, _) => + case UiContext.ItemStatus(true, _, _, _) => rectangle(selectBoxArea, hotColor) - case UiContext.ItemStatus(_, _, _) => + case UiContext.ItemStatus(_, _, _, _) => rectangle(selectBoxArea, inactiveColor) text( selectBoxArea.shrink(border), @@ -61,11 +61,11 @@ object SelectSkin extends DefaultSkin: val optionLabel = labels.applyOrElse(value, _ => "") onTop: itemStatus match - case UiContext.ItemStatus(_, _, true) | UiContext.ItemStatus(_, true, _) => + case UiContext.ItemStatus(_, _, true, _) | UiContext.ItemStatus(_, true, _, _) => rectangle(selectOptionArea, activeColor) - case UiContext.ItemStatus(true, _, _) => + case UiContext.ItemStatus(true, _, _, _) => rectangle(selectOptionArea, hotColor) - case UiContext.ItemStatus(_, _, _) => + case _ => rectangle(selectOptionArea, inactiveColor) text( selectOptionArea.shrink(border), diff --git a/core/src/main/scala/eu/joaocosta/interim/skins/SliderSkin.scala b/core/src/main/scala/eu/joaocosta/interim/skins/SliderSkin.scala index 3827c03..3dcff13 100644 --- a/core/src/main/scala/eu/joaocosta/interim/skins/SliderSkin.scala +++ b/core/src/main/scala/eu/joaocosta/interim/skins/SliderSkin.scala @@ -44,9 +44,9 @@ object SliderSkin extends DefaultSkin: Rect(area.x + padding, area.y + padding + pos, sliderFill, sliderSize) rectangle(area, scrollbarColor) // Scrollbar itemStatus match - case UiContext.ItemStatus(false, false, _) => + case UiContext.ItemStatus(false, false, _, _) => rectangle(sliderRect, inactiveColor) - case UiContext.ItemStatus(true, false, _) => + case UiContext.ItemStatus(true, false, _, _) => rectangle(sliderRect, hotColor) case _ => rectangle(sliderRect, activeColor) diff --git a/core/src/main/scala/eu/joaocosta/interim/skins/TextInputSkin.scala b/core/src/main/scala/eu/joaocosta/interim/skins/TextInputSkin.scala index 04ab3f6..d09be97 100644 --- a/core/src/main/scala/eu/joaocosta/interim/skins/TextInputSkin.scala +++ b/core/src/main/scala/eu/joaocosta/interim/skins/TextInputSkin.scala @@ -25,11 +25,11 @@ object TextInputSkin extends DefaultSkin: def renderTextInput(area: Rect, value: String, itemStatus: UiContext.ItemStatus)(using uiContext: UiContext): Unit = val textInputArea = this.textInputArea(area) itemStatus match - case UiContext.ItemStatus(_, _, true) | UiContext.ItemStatus(_, true, _) => + case UiContext.ItemStatus(_, _, true, _) | UiContext.ItemStatus(_, true, _, _) => rectangle(area, activeColor) - case UiContext.ItemStatus(true, _, _) => + case UiContext.ItemStatus(true, _, _, _) => rectangle(area, hotColor) - case UiContext.ItemStatus(_, _, _) => + case _ => rectangle(area, inactiveColor) rectangle(textInputArea, textAreaColor) text( diff --git a/core/src/test/scala/eu/joaocosta/interim/UiContextSpec.scala b/core/src/test/scala/eu/joaocosta/interim/UiContextSpec.scala index 1f94c5d..2cae7d4 100644 --- a/core/src/test/scala/eu/joaocosta/interim/UiContextSpec.scala +++ b/core/src/test/scala/eu/joaocosta/interim/UiContextSpec.scala @@ -9,6 +9,7 @@ class UiContextSpec extends munit.FunSuite: assertEquals(itemStatus.hot, false) assertEquals(itemStatus.active, false) assertEquals(itemStatus.selected, false) + assertEquals(itemStatus.clicked, false) assertEquals(uiContext.hotItem, None) assertEquals(uiContext.activeItem, None) assertEquals(uiContext.selectedItem, None) @@ -20,6 +21,7 @@ class UiContextSpec extends munit.FunSuite: assertEquals(itemStatus.hot, true) assertEquals(itemStatus.active, false) assertEquals(itemStatus.selected, false) + assertEquals(itemStatus.clicked, false) assertEquals(uiContext.hotItem, Some(0 -> 1)) assertEquals(uiContext.activeItem, None) assertEquals(uiContext.selectedItem, None) @@ -31,10 +33,22 @@ class UiContextSpec extends munit.FunSuite: assertEquals(itemStatus.hot, true) assertEquals(itemStatus.active, true) assertEquals(itemStatus.selected, true) + assertEquals(itemStatus.clicked, false) assertEquals(uiContext.hotItem, Some(0 -> 1)) assertEquals(uiContext.activeItem, Some(1)) assertEquals(uiContext.selectedItem, Some(1)) + test("registerItem should mark a clicked item as clicked once the mouse is released"): + val uiContext: UiContext = new UiContext() + val inputState1: InputState = InputState(5, 5, true, "") + UiContext.registerItem(1, Rect(1, 1, 10, 10))(using uiContext, inputState1) + val inputState2: InputState = InputState(5, 5, false, "") + val itemStatus = UiContext.registerItem(1, Rect(1, 1, 10, 10))(using uiContext, inputState2) + assertEquals(itemStatus.hot, true) + assertEquals(itemStatus.active, true) + assertEquals(itemStatus.selected, true) + assertEquals(itemStatus.clicked, true) + test("registerItem should not override an active item with another one"): val uiContext = new UiContext() val inputState1 = InputState(5, 5, true, "") @@ -45,6 +59,7 @@ class UiContextSpec extends munit.FunSuite: assertEquals(itemStatus.hot, true) assertEquals(itemStatus.active, false) assertEquals(itemStatus.selected, false) + assertEquals(itemStatus.clicked, false) assertEquals(uiContext.hotItem, Some(0 -> 2)) assertEquals(uiContext.activeItem, Some(1)) assertEquals(uiContext.selectedItem, Some(1))