Skip to content
Open
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
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ jobs:
- name: Run unit tests
if: ${{ !matrix.emscripten }}
run: |
nim r -d:silkyTesting tests/test_interactions.nim
nim r tests/test_textboxes.nim
nim r -d:silkyTesting tests/test_inputs.nim
nim r tests/test_atlas_png.nim
Expand Down
18 changes: 12 additions & 6 deletions src/silky/widgets.nim
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,20 @@ proc interact*(
return ReleasedOutside
if not hover:
return None
# A complete click can arrive between frames. Use the final button state
# to distinguish it from a release followed by another press.
if released and not down:
return Released
if pressed:
return Pressed
if down:
return Held
if released:
return Released
return Hovered

proc pressedThisFrame(sk: Silky, interaction: Interaction): bool =
# A completed click still carries a press, without repeating normal releases.
sk.buttonPressed[MouseLeft] and interaction in [Pressed, Released]

proc vec2(v: SomeNumber): Vec2 =
## Create a Vec2 from a number.
vec2(v.float32, v.float32)
Expand Down Expand Up @@ -189,7 +195,7 @@ proc subWindowStart*(
)
minimizeInteraction = sk.interact(minimizeRect, true)

if minimizeInteraction == Pressed:
if sk.pressedThisFrame(minimizeInteraction):
subWindowState.minimized = not subWindowState.minimized

if subWindowState.minimized:
Expand Down Expand Up @@ -562,7 +568,7 @@ template clickableIcon*(image: string, on: bool, body) =
if sk.hover:
sk.tooltipAnchor = iconRect

if interaction == Pressed:
if sk.pressedThisFrame(interaction):
body

sk.drawImage(image, sk.at, color)
Expand Down Expand Up @@ -919,7 +925,7 @@ template scrubber*[T, U](id: string, value: var T, minVal: T, maxVal: U, label:
handleReleased = handleInteraction in [Released, ReleasedOutside]
controlReleased = controlInteraction in [Released, ReleasedOutside]
released = handleReleased or controlReleased
pressed = handleInteraction == Pressed or controlInteraction == Pressed
pressed = sk.pressedThisFrame(handleInteraction) or sk.pressedThisFrame(controlInteraction)

# Dragging logic.
if scrubState.dragging and released:
Expand All @@ -929,7 +935,7 @@ template scrubber*[T, U](id: string, value: var T, minVal: T, maxVal: U, label:
let t = clamp((sk.mousePos.x - trackStart) / travelSafe, 0f, 1f)
value = (minF + t * range).T
elif pressed:
scrubState.dragging = true
scrubState.dragging = sk.buttonDown[MouseLeft]
let t = clamp((sk.mousePos.x - trackStart) / travelSafe, 0f, 1f)
value = (minF + t * range).T

Expand Down
102 changes: 102 additions & 0 deletions tests/test_interactions.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
## Mouse event batches must not lose release activation between rendered frames.
## Run with: nim r -d:silkyTesting tests/test_interactions.nim
import std/[options, tables]
import bumpy, silky, vmath

let window = newWindow(200, 200)
let sk = newSilky(window, SilkyAtlas(size: 64))
let target = rect(20'f32, 20'f32, 100'f32, 40'f32)

proc sample(enabled = true): Interaction =
sk.beginUI(window, window.size)
result = sk.interact(target, enabled)
sk.endUi()
window.resetInputState()

window.moveMouse(50, 30)
discard sample()
doAssert sample() == Hovered
window.pressButton(MouseLeft)
doAssert sample() == Pressed
doAssert sample() == Held
window.releaseButton(MouseLeft)
doAssert sample() == Released
discard sample()

# A full click between two frames used to return Pressed and lose the release.
window.pressButton(MouseLeft)
window.releaseButton(MouseLeft)
doAssert sample() == Released
doAssert sample() == Hovered

window.pressButton(MouseLeft)
discard sample()
window.releaseButton(MouseLeft)
window.pressButton(MouseLeft)
doAssert sample() == Pressed
doAssert sample() == Held
window.releaseButton(MouseLeft)
doAssert sample() == Released
discard sample()

window.pressButton(MouseLeft)
window.releaseButton(MouseLeft)
doAssert sample(false) == Disabled

window.pressButton(MouseLeft)
window.moveMouse(190, 190)
window.releaseButton(MouseLeft)
doAssert sample() == ReleasedOutside
echo "Batched mouse interaction tests passed"

# Exercise the actual press-driven controls, using a small semantic atlas.
block:
let atlas = SilkyAtlas(size: 64)
for name in ["heart", "maximized", "minimized", "close", "scrubber.handle"]:
atlas.entries[name] = Entry(width: 16, height: 16)
let window = newWindow(240, 240)
let sk = newSilky(window, atlas)
var icons = 0
var volume = 0'f32
var show = true
var panel: SubWindowState
window.onFrame = proc() =
sk.beginUI(window, window.size)
sk.at = vec2(20, 20)
clickableIcon "heart", true: inc icons
sk.at = vec2(20, 60)
scrubber "batch-test", volume, 0'f32, 1'f32
panel = sk.subWindowStart(window, "batch-window", show,
some(vec2(20, 120)), some(vec2(180, 100)))
sk.subWindowEnd(window, panel)
sk.endUi()

proc clickAt(x, y: int, completed: bool) =
window.moveMouse(x, y)
window.pumpFrame(sk)
window.pumpFrame(sk)
window.pressButton(MouseLeft)
if not completed:
window.pumpFrame(sk)
window.releaseButton(MouseLeft)
window.pumpFrame(sk)
window.pumpFrame(sk)

for completed in [false, true]:
let before = icons
clickAt(24, 24, completed)
doAssert icons == before + 1
volume = 0
clickAt(120, 64, completed)
doAssert volume > 0.3 and volume < 0.7
let stopped = volume
window.moveMouse(40, 64)
window.pumpFrame(sk)
doAssert volume == stopped, "released scrubber must not keep dragging"
clickAt(24 + sk.theme.border + sk.theme.textPadding,
124 + sk.theme.border + sk.theme.textPadding, completed)
doAssert panel.minimized
clickAt(24 + sk.theme.border + sk.theme.textPadding,
124 + sk.theme.border + sk.theme.textPadding, completed)
doAssert not panel.minimized
echo "Press-driven widget callback tests passed"