diff --git a/src/ThrottleDebounce.jl b/src/ThrottleDebounce.jl new file mode 100644 index 00000000..94d60ee0 --- /dev/null +++ b/src/ThrottleDebounce.jl @@ -0,0 +1,303 @@ +### A Pluto.jl notebook ### +# v0.14.5 + +using Markdown +using InteractiveUtils + +# This Pluto notebook uses @bind for interactivity. When running this notebook outside of Pluto, the following 'mock version' of @bind gives bound variables a default value (instead of an error). +macro bind(def, element) + quote + local el = $(esc(element)) + global $(esc(def)) = Core.applicable(Base.get, el) ? Base.get(el) : missing + el + end +end + +# ╔═╡ d921bec2-b491-471f-9373-ae4cfbc7b210 +using HypertextLiteral + +# ╔═╡ e6b13f1f-cdc6-447f-9c23-e994f901965a +md""" +# `throttle` and `debounce` wrappers +_Limit the frequency of input events!_ + +When moving a slider from 0 to 100, it will fire many **intermediate values**, and you can see that the notebook updates many times. +""" + +# ╔═╡ 15f9f9b5-e5b5-4743-bf48-9f2322d1e863 +md""" +### Throttled +Normally, this is great, since it gives you very interactive notebooks! However, in some cases, you want to **limit how many events are fired per second**. + +This is done with the `throttled` function, which takes two arguments: +1. the element to throttle, and +2. the minimum time _(in seconds)_ between two input events. +""" + +# ╔═╡ 01b4ba88-8d86-4905-b5c3-20c05917c024 +md""" +You can give two additional keyword arguments: +- `leading::Bool=true`: After the cooldown period, fire the first input immediately? +- `trailing::Bool=true`: After the last cooldown period, also fire the last throttled event? + +For more info, see the [lodash documentation](https://lodash.com/docs/4.17.15#throttle). +""" + +# ╔═╡ a98461b6-883f-4e89-bb7a-7e60d7ab807d +md""" +### Debounced +Besides _throttling_ the number of inputs fired per second, you can also **discard all intermediate events**. In the case of a slider, an event will only be fired when you stop moving the slider for a short while. + +This is done with the `debounced` function, which takes two arguments: +1. the element to debounce, and +2. the cooldown time _(in seconds)_ during which events will be discarded. +""" + +# ╔═╡ 4e87b559-c0ca-450a-ba75-e713c86d6c26 +md""" +You can give three additional keyword arguments: +- `leading::Bool=false`: After the cooldown period, fire the first input immediately? +- `maxwait::Union{Nothing,Real}=nothing`: The maximum time that events are allowed to be delayed. +- `trailing::Bool=true`: After the last cooldown period, also fire the last throttled event? + +For more info, see the [lodash documentation](https://lodash.com/docs/4.17.15#debounce). +""" + +# ╔═╡ ba7e3059-a12a-4031-84c7-be0113c3a1dc + + +# ╔═╡ 7d955dc4-4ad3-4d0f-9e86-9c5595604ff4 +"Fake slider used in examples" +Slider(args...) = html""; + +# ╔═╡ 003440c7-cf98-4830-9648-ea7b7101683c +begin + struct CoolSlider + end + Base.get(::CoolSlider) = 50 + Base.show(io::IO, m::MIME"text/html", t::CoolSlider) = write(io, "") +end + +# ╔═╡ 820df928-69a0-4704-8c36-d46d73085318 +md""" +# More tests +""" + +# ╔═╡ d17f533b-7b15-4c76-8a42-a723de409476 +function renderlodash(el, wait_ms::Real, method::String, options) + @htl(""" + + + $(el) + + + + """) +end + +# ╔═╡ 9f8b24de-6f9f-4af1-b73e-8c6da073520a +begin + struct ThrottleDebounced{T} + x::T + wait_ms::Real + method::String + options + end + + + + Base.get(t::ThrottleDebounced{T}) where T = if hasmethod(Base.get, Tuple{T}) + Base.get(t.x) + else + missing + end + + function Base.show(io::IO, m::MIME"text/html", t::ThrottleDebounced) + + + Base.show(io, m, renderlodash(t.x, t.wait_ms, t.method, t.options)) + end + + + +end + +# ╔═╡ 2104e758-282e-458c-9595-9254718b4645 +@bind x Slider(0:100) + +# ╔═╡ f7694341-411b-4247-b5a9-5cca33a39e0f +x + +# ╔═╡ 9eedad1d-8ebe-4005-aec6-1ecb2816af63 +""" + throttled(widget, delay_seconds::Real) + +A wrapper around an input `widget` that limits how many input events are fired per second. + +# Example + +```julia +@bind x throttled(Slider(1:100), 0.5) + +x +``` + +`x` will be updated at most 2 times per second. + +""" +function throttled(el, wait=0; leading::Bool=true, trailing::Bool=true) + ThrottleDebounced( + el, + wait * 1000, + "throttle", + Dict( + :leading=>leading, + :trailing=>trailing, + ) + ) +end + +# ╔═╡ 559f3670-604c-49bf-9769-f8e852e9536b +@bind y throttled(Slider(0:100), 1.0) + +# ╔═╡ 4014c7fc-5c0a-487c-9811-85fc4fc3afe4 +y + +# ╔═╡ 07345774-7c05-400f-85bc-faa9531d589a +@bind y_non_trailing throttled(Slider(0:100), 1.0; trailing=false) + +# ╔═╡ e0e20ff5-40c4-4fee-b902-2248afc3b397 +y_non_trailing + +# ╔═╡ 7f9b2c9c-38dd-45b4-a2f6-8bcb8ef7c1ac +""" + debounced(widget, delay_seconds::Real) + +A wrapper around an input `widget` that discards all intermediate inputs events, and only relays the last value, when no input event is fired in an `delay_seconds` interval. + +# Example + +```julia +@bind x debounced(Slider(1:100), 0.5) + +x +``` + +`x` will only be updated when you do not move the slider for 0.5 seconds. + +""" +function debounced(el, wait=0; leading::Bool=false, maxwait::Union{Nothing,Real}=nothing, trailing::Bool=true) + ThrottleDebounced( + el, + wait * 1000, + "debounce", + Dict( + :leading=>leading, + :maxwait=>maxwait, + :trailing=>trailing, + ) + ) +end + +# ╔═╡ 1d81d96e-8642-4f8d-aa7c-2ad53fae2cb8 +@bind z debounced(Slider(0:100), 1.0) + +# ╔═╡ 251fa612-e621-4ed8-b517-1d0715e9f98e +z + +# ╔═╡ 31d187e1-9ce7-4738-8dc6-0643d2da16cd +export throttled, debounced + +# ╔═╡ 5f82a82b-10a5-4702-837a-fb4e2dcaff3f +@bind x1 Slider() + +# ╔═╡ 400fc0b0-3c0b-4bac-9575-4a969aa8b8fe +begin + x2s = [] + @bind x2 throttled(Slider(), 0.5) +end + +# ╔═╡ bf73c955-243a-4969-b2f9-60a1116fd021 +push!(x2s, x2) + +# ╔═╡ a993d308-29a6-4933-9b6f-7d28a8db4d56 +begin + x3s = [] + @bind x3 throttled(CoolSlider(), 0.5) +end + +# ╔═╡ 1113967d-09fb-4645-8396-d5c1f9b30f51 +push!(x3s, x3) + +# ╔═╡ b6d3d6c5-9087-4878-a06c-42bc6f823ea9 +begin + x4s = [] + @bind x4 debounced(CoolSlider(), 0.5) +end + +# ╔═╡ 45500c96-acaa-43f5-90ea-b3aac8a9a46c +push!(x4s, x4) + +# ╔═╡ Cell order: +# ╟─e6b13f1f-cdc6-447f-9c23-e994f901965a +# ╠═2104e758-282e-458c-9595-9254718b4645 +# ╠═f7694341-411b-4247-b5a9-5cca33a39e0f +# ╟─15f9f9b5-e5b5-4743-bf48-9f2322d1e863 +# ╠═559f3670-604c-49bf-9769-f8e852e9536b +# ╠═4014c7fc-5c0a-487c-9811-85fc4fc3afe4 +# ╟─01b4ba88-8d86-4905-b5c3-20c05917c024 +# ╠═07345774-7c05-400f-85bc-faa9531d589a +# ╠═e0e20ff5-40c4-4fee-b902-2248afc3b397 +# ╟─a98461b6-883f-4e89-bb7a-7e60d7ab807d +# ╠═1d81d96e-8642-4f8d-aa7c-2ad53fae2cb8 +# ╠═251fa612-e621-4ed8-b517-1d0715e9f98e +# ╟─4e87b559-c0ca-450a-ba75-e713c86d6c26 +# ╠═31d187e1-9ce7-4738-8dc6-0643d2da16cd +# ╠═9eedad1d-8ebe-4005-aec6-1ecb2816af63 +# ╠═7f9b2c9c-38dd-45b4-a2f6-8bcb8ef7c1ac +# ╠═ba7e3059-a12a-4031-84c7-be0113c3a1dc +# ╠═7d955dc4-4ad3-4d0f-9e86-9c5595604ff4 +# ╠═003440c7-cf98-4830-9648-ea7b7101683c +# ╟─820df928-69a0-4704-8c36-d46d73085318 +# ╠═5f82a82b-10a5-4702-837a-fb4e2dcaff3f +# ╠═400fc0b0-3c0b-4bac-9575-4a969aa8b8fe +# ╠═bf73c955-243a-4969-b2f9-60a1116fd021 +# ╠═a993d308-29a6-4933-9b6f-7d28a8db4d56 +# ╠═1113967d-09fb-4645-8396-d5c1f9b30f51 +# ╠═b6d3d6c5-9087-4878-a06c-42bc6f823ea9 +# ╠═45500c96-acaa-43f5-90ea-b3aac8a9a46c +# ╠═9f8b24de-6f9f-4af1-b73e-8c6da073520a +# ╠═d17f533b-7b15-4c76-8a42-a723de409476 +# ╠═d921bec2-b491-471f-9373-ae4cfbc7b210