Debouncing
May 3, 2026
Consider a treeview and a corresponding panel on its right, as in a file explorer. If we click on an item in the treeview, a select event is generated which, typically, fires a callback function to update the panel on the right. We can be Olympic mouse champions but we can only move the mouse and click on another item so fast. Generally no matter how adept we are with the mouse, the system will be able to refresh the right-hand panel with no apparent lag.
Now consider using the down cursor key to scroll through the items of the tree. It takes no special skill to scroll through hundreds of items at lightning speed. Your cat can do this stepping on your keyboard. Each keypress yields a select event and a subsequent execution of the callback function to refresh the right-hand panel. If the refresh operation takes any significant time, the response will be sluggish, the user experience bad.
The solution to this problem is known as debouncing. Typically on each keypress a timer is created or reset, and only when the timer goes off is the callback executed. So if we hold down a key the timer is constantly being reset, and only when we release the key and pause for a moment does the timer have an opportunity to go off. Instead of running a hundred times, the callback executes only once, when we pause scrolling.
As far as we know, no UI frameworks build debouncing into components. Debouncing must be implemented by hand, on a case-by-case basis.
We attempt to build this functionality right into the Abacus TreeView component, and to make it a general solution for any event that needs debouncing.
This first problem we encounter is that a ⎕WC timer will not help us because we are multithreading and ⎕WC objects do not like to interact with different threads.
Fortunately the architecture of Abacus provides another way. Abacus has a wait loop where the document is waiting for a token with ⎕TGET from the main thread, signifying the user has taken some action in the browser. Previously we had not taken advantage of the left argument of ⎕TGET which allows for a timeout. If we want to debounce some event, we can signal to the system that we want a delay. We can set a timeout, exit ⎕TGET when the user pauses, and then handle the event. First, we add a few new system properties to the Document object:
d.Debounce←⍬
d.DefaultTimeout←2147483
d.Timeout←d.DefaultTimeout
These are system properties; the programmer does not set them. The Debounce property tracks if there is an event being debounced. It is either an empty array, or a two element array containing the callback function and its argument. The DefaultTimeout property effectively specifies no timeout. The Timeout property will be used by ⎕TGET in the event loop. The TreeView OnSelect property (and potentially any event callback on any component) may be set to either a simple string with the name of the callback, or a namespace containing the callback function and a debounce delay:
(CallbackFunction:'MyCallBack' ⋄ DebounceDelay:100)
That's all the programmer needs to do.
In the TreeView component, when a cursor key is pressed, we run DebounceSelect as a cover over FireSelect which actually fires the select event:
DebounceSelect←{
⍝ ⍺←TreeView
d←⍺.Document
l←⍺.OnSelect.DebounceDelay
l=0:FireSelect ⍺
d.Timeout←l÷1000
d.Debounce←(A.FQP'FireSelect')⍺
0
}
This checks the value of DebounceDelay. If 0, the select event is fired immediately. Otherwise, the document Timeout property is set to the delay and the callback with its argument are assigned to the Debounce property, where they await execution when the ⎕TGET times out:
ThreadQueue←{
⍝ Queued Event Loop for Session/Document
r←⍺.Timeout ⎕TGET ⎕TID
0=≢r:⍺ ∇ ⍵⊣Debounce ⍺
c←⊃r
_←LogBrowserEvent c
_←(⍎c.CurrentTarget⍎'On',c.Event)c
_←PutDoneToken c
⍺ ∇ ⍵
}
When a timeout occurs, Debounce is called, which executes a pending debounced event if there is one:
Debounce←{
⍝ ⍵ ←→ Document
0=≢⍵.Debounce:0
f a←⍵.Debounce
⍵.Debounce←⍬
⍵.Timeout←⍵.DefaultTimeout
(⍎f)a
}
What Could Go Wrong
At least two things can go wrong with this technique. A debounced event might not fire when it should, and a debounced event might fire when it should not.
For the first case, consider two treeviews (and associated panels), and a user scrolling through the first treeview. If the debounce delay is long enough, or the user quick enough, the user could tab to the second treeview and start scrolling there. The timeout on the first debounce would not happen, and then the next debounce would overwrite the first debounce, which would thus be lost. The first treeview would not update.
For the second case, again with a debounce delay long enough or a nimble user, the treeview could be deleted by some user action, and then the timeout occurs and the debounced event fires on a nonexistant element.
We can probably code around both of these problems, but as the delay should always be set as short as possible, they are not likely to occur.