From ee1d660223fd9085a6c720b87b5305405ae4994e Mon Sep 17 00:00:00 2001 From: treeform Date: Wed, 7 Oct 2026 14:19:54 -0700 Subject: [PATCH] reuse UI buffers and input handlers --- .github/workflows/build.yml | 2 +- src/silky/contexts.nim | 11 ++- src/silky/internal/inputs.nim | 136 ++++++++++++++++++++-------------- tests/test_inputs.nim | 15 +++- 4 files changed, 99 insertions(+), 65 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c4287a7..3229351 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -58,7 +58,7 @@ jobs: if: ${{ !matrix.emscripten }} run: | nim r tests/test_textboxes.nim - nim r -d:silkyTesting tests/test_inputs.nim + nim r -d:silkyTesting -d:nimTypeNames tests/test_inputs.nim nim r tests/test_atlas_png.nim nim r -d:silkyTesting tests/test_layout.nim nim r -d:silkyTesting tests/test_layout_regressions.nim diff --git a/src/silky/contexts.nim b/src/silky/contexts.nim index aa6bf25..0f79620 100644 --- a/src/silky/contexts.nim +++ b/src/silky/contexts.nim @@ -83,6 +83,7 @@ type ## Multiplies 9-patch borders for a denser atlas. sliceScale*: int = 1 layoutStack*: seq[LayoutScope] + scaledVertices: seq[DrawerVertex] textStyle*: string = "Default" padding*: float32 = 12 theme*: Theme = Theme() @@ -965,19 +966,17 @@ proc endUi*(sk: Silky) = scale = sk.uiScale quadCount = sk.drawer.layers[NormalLayer].len needsScale = not (scale ~= 1.0f) - var - quadsPtr: pointer - scaledVertices: seq[DrawerVertex] + var quadsPtr: pointer if quadCount > 0: if needsScale: - scaledVertices = newSeqOfCap[DrawerVertex](quadCount) + sk.scaledVertices.setLen(quadCount) for i in 0 ..< quadCount: var vertex = sk.drawer.layers[NormalLayer][i] vertex.pos *= scale vertex.clipPos *= scale vertex.clipSize *= scale - scaledVertices.add(vertex) - quadsPtr = cast[pointer](unsafeAddr scaledVertices[0]) + sk.scaledVertices[i] = vertex + quadsPtr = cast[pointer](unsafeAddr sk.scaledVertices[0]) else: quadsPtr = cast[pointer](unsafeAddr sk.drawer.layers[NormalLayer][0]) else: diff --git a/src/silky/internal/inputs.nim b/src/silky/internal/inputs.nim index e301f83..63c3c92 100644 --- a/src/silky/internal/inputs.nim +++ b/src/silky/internal/inputs.nim @@ -87,70 +87,92 @@ proc addEvent(inputs: TextInputs, event: InputEvent) = raise newException(SilkyError, "Too many pending text input events") inputs.events.add(event) +proc installPress(inputs: TextInputs, window: Window) = + ## Installs this handler only when the window's callback changes. + let windowPointer = cast[pointer](window) + # The window owns the callback, so a pointer avoids a reference cycle. + let previousPress = window.onButtonPress + window.onButtonPress = proc(button: Button) = + let window = cast[Window](windowPointer) + if inputs.active and button >= Key0: + var event = inputs.modifiers(window) + event.kind = KeyDown + event.button = button + inputs.consumedKeys.incl(button) + inputs.addEvent(event) + return + if button >= Key0: + inputs.forwardedKeys.incl(button) + inputs.consumedKeys.excl(button) + if previousPress != nil: + previousPress(button) + inputs.callbacks[0] = identity(window.onButtonPress) + +proc installRelease(inputs: TextInputs, window: Window) = + ## Installs this handler only when the window's callback changes. + let windowPointer = cast[pointer](window) + # The window owns the callback, so a pointer avoids a reference cycle. + let previousRelease = window.onButtonRelease + inputs.releaseCallback = previousRelease + window.onButtonRelease = proc(button: Button) = + let window = cast[Window](windowPointer) + if inputs.active and button >= Key0: + var event = inputs.modifiers(window) + event.kind = KeyUp + event.button = button + inputs.consumedKeys.excl(button) + inputs.addEvent(event) + return + if button >= Key0: + inputs.forwardedKeys.excl(button) + if button in inputs.consumedKeys: + inputs.consumedKeys.excl(button) + return + if previousRelease != nil: + previousRelease(button) + inputs.callbacks[1] = identity(window.onButtonRelease) + +proc installRune(inputs: TextInputs, window: Window) = + ## Installs this handler only when the window's callback changes. + let windowPointer = cast[pointer](window) + # The window owns the callback, so a pointer avoids a reference cycle. + let previousRune = window.onRune + window.onRune = proc(rune: Rune) = + let window = cast[Window](windowPointer) + if inputs.active: + if inputs.editable: + var event = inputs.modifiers(window) + event.kind = TextInput + event.rune = rune + inputs.addEvent(event) + return + if previousRune != nil: + previousRune(rune) + inputs.callbacks[2] = identity(window.onRune) + +proc installFocus(inputs: TextInputs, window: Window) = + ## Installs this handler only when the window's callback changes. + let windowPointer = cast[pointer](window) + # The window owns the callback, so a pointer avoids a reference cycle. + let previousFocus = window.onFocusChange + window.onFocusChange = proc() = + inputs.stopInput(cast[Window](windowPointer)) + if previousFocus != nil: + previousFocus() + inputs.callbacks[3] = identity(window.onFocusChange) + proc overrideCallbacks( inputs: TextInputs, window: Window, initialize = false ) = - ## Automatically installs handlers for any changed callback slots. - let windowPointer = cast[pointer](window) - # The window owns these callbacks, so a pointer avoids a reference cycle. + ## Checks handlers without allocating closure state on unchanged frames. if initialize or identity(window.onButtonPress) != inputs.callbacks[0]: - let previousPress = window.onButtonPress - window.onButtonPress = proc(button: Button) = - let window = cast[Window](windowPointer) - if inputs.active and button >= Key0: - var event = inputs.modifiers(window) - event.kind = KeyDown - event.button = button - inputs.consumedKeys.incl(button) - inputs.addEvent(event) - return - if button >= Key0: - inputs.forwardedKeys.incl(button) - inputs.consumedKeys.excl(button) - if previousPress != nil: - previousPress(button) - inputs.callbacks[0] = identity(window.onButtonPress) + inputs.installPress(window) if initialize or identity(window.onButtonRelease) != inputs.callbacks[1]: - let previousRelease = window.onButtonRelease - inputs.releaseCallback = previousRelease - window.onButtonRelease = proc(button: Button) = - let window = cast[Window](windowPointer) - if inputs.active and button >= Key0: - var event = inputs.modifiers(window) - event.kind = KeyUp - event.button = button - inputs.consumedKeys.excl(button) - inputs.addEvent(event) - return - if button >= Key0: - inputs.forwardedKeys.excl(button) - if button in inputs.consumedKeys: - inputs.consumedKeys.excl(button) - return - if previousRelease != nil: - previousRelease(button) - inputs.callbacks[1] = identity(window.onButtonRelease) + inputs.installRelease(window) if initialize or identity(window.onRune) != inputs.callbacks[2]: - let previousRune = window.onRune - window.onRune = proc(rune: Rune) = - let window = cast[Window](windowPointer) - if inputs.active: - if inputs.editable: - var event = inputs.modifiers(window) - event.kind = TextInput - event.rune = rune - inputs.addEvent(event) - return - if previousRune != nil: - previousRune(rune) - inputs.callbacks[2] = identity(window.onRune) + inputs.installRune(window) if initialize or identity(window.onFocusChange) != inputs.callbacks[3]: - let previousFocus = window.onFocusChange - window.onFocusChange = proc() = - inputs.stopInput(cast[Window](windowPointer)) - if previousFocus != nil: - previousFocus() - inputs.callbacks[3] = identity(window.onFocusChange) + inputs.installFocus(window) proc newTextInputs*(window: Window): TextInputs = ## Sets up all text input handling directly from the window. diff --git a/tests/test_inputs.nim b/tests/test_inputs.nim index 5a95153..2ad63d4 100644 --- a/tests/test_inputs.nim +++ b/tests/test_inputs.nim @@ -1,7 +1,7 @@ import std/unicode, vmath, - silky + silky, silky/internal/inputs when not defined(silkyTesting): {.error: "Compile with -d:silkyTesting".} @@ -537,4 +537,17 @@ block: f.frame() doAssert not f.state.focused and not window.runeInputEnabled +when defined(nimTypeNames): + block: + echo "Testing unchanged input frames allocate zero" + let + window = newWindow() + inputs = newTextInputs(window) + let before = getMemCounters() + for i in 0 ..< 100: + inputs.beginInputFrame(window) + inputs.endInputFrame(window) + let after = getMemCounters() + doAssert after[0] == before[0], "Unchanged handlers must reuse closures." + echo "All ordered text input tests passed"