Skip to content

Commit

Permalink
🐣 New package AbstractPlutoDingetjes.jl for intial bond values, bond …
Browse files Browse the repository at this point in the history
…value transformation and more (#1612)
  • Loading branch information
fonsp authored Nov 2, 2021
1 parent dbdca81 commit fc74c51
Show file tree
Hide file tree
Showing 7 changed files with 369 additions and 48 deletions.
72 changes: 39 additions & 33 deletions sample/Interactivity.jl
Original file line number Diff line number Diff line change
@@ -1,30 +1,31 @@
### A Pluto.jl notebook ###
# v0.14.0
# v0.17.0

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 iv = try Base.loaded_modules[Base.PkgId(Base.UUID("6e696c72-6542-2067-7265-42206c756150"), "AbstractPlutoDingetjes")].Bonds.initial_value catch; b -> missing; end
local el = $(esc(element))
global $(esc(def)) = Core.applicable(Base.get, el) ? Base.get(el) : missing
global $(esc(def)) = Core.applicable(Base.get, el) ? Base.get(el) : iv(el)
el
end
end

# ╔═╡ db24490e-7eac-11ea-094e-9d3fc8f22784
md"# Introducing _bound_ variables
With the new `@bind` macro, Pluto.jl can listen to real-time events from HTML objects!"
With the `@bind` macro, Pluto.jl can synchronize a Julia variable with an HTML object!"

# ╔═╡ bd24d02c-7eac-11ea-14ab-95021678e71e
@bind x html"<input type=range>"

# ╔═╡ cf72c8a2-7ead-11ea-32b7-d31d5b2dacc2
md"This syntax displays the HTML object as the cell's output, and uses its latest value as the definition of `x`. Of course, the variable `x` is _reactive_, and all references to `x` come to life ✨
_Try it out!_ 👆"
_Try moving the slider!_ 👆"

# ╔═╡ cb1fd532-7eac-11ea-307c-ab16b1977819
x
Expand All @@ -39,18 +40,22 @@ The `@bind` macro returns a `Bond` object, which can be used inside Markdown and

# ╔═╡ fc99521c-7eae-11ea-269b-0d124b8cbe48
begin
🐶slider = @bind 🐶 html"<input type=range>"
🐱slider = @bind 🐱 html"<input type=range>"
dog_slider = @bind 🐶 html"<input type=range>"
cat_slider = @bind 🐱 html"<input type=range>"

md"""**How many pets do you have?**
md"""
**How many pets do you have?**
Dogs: $(🐶slider)
Cats: $(🐱slider)"""
Dogs: $(dog_slider)
Cats: $(cat_slider)
"""
end

# ╔═╡ 1cf27d7c-7eaf-11ea-3ee3-456ed1e930ea
md"You have $(🐶) dogs and $(🐱) cats!"
md"""
You have $(🐶) dogs and $(🐱) cats!
"""

# ╔═╡ e3204b38-7eae-11ea-32be-39db6cc9faba
md""
Expand Down Expand Up @@ -90,25 +95,27 @@ Try drawing a rectangle in the canvas below 👇 and notice that the `area` vari

# ╔═╡ 7f4b0e1e-7f16-11ea-02d3-7955921a70bd
@bind dims html"""
<span>
<canvas width="200" height="200" style="position: relative"></canvas>
<script>
// 🐸 `currentScript` is the current script tag - we use it to select elements 🐸 //
const canvas = currentScript.parentElement.querySelector("canvas")
const span = currentScript.parentElement
const canvas = span.querySelector("canvas")
const ctx = canvas.getContext("2d")
var startX = 80
var startY = 40
function onmove(e){
// 🐸 We send the value back to Julia 🐸 //
canvas.value = [e.layerX - startX, e.layerY - startY]
canvas.dispatchEvent(new CustomEvent("input"))
span.value = [e.layerX - startX, e.layerY - startY]
span.dispatchEvent(new CustomEvent("input"))
ctx.fillStyle = '#ffecec'
ctx.fillRect(0, 0, 200, 200)
ctx.fillStyle = '#3f3d6d'
ctx.fillRect(startX, startY, ...canvas.value)
ctx.fillRect(startX, startY, ...span.value)
}
canvas.onpointerdown = e => {
Expand All @@ -125,6 +132,7 @@ canvas.onpointerup = e => {
onmove({layerX: 130, layerY: 160})
</script>
</span>
"""

# ╔═╡ 5876b98e-7f32-11ea-1748-0bb47823cde1
Expand All @@ -141,7 +149,7 @@ md"""## Can I use it?
The `@bind` macro is **built into Pluto.jl** — it works without having to install a package.
You can use the (tiny) package [`PlutoUI`](/~https://github.com/fonsp/PlutoUI.jl) for some predefined `<input>` HTML codes. For example, you use `PlutoUI` to write
You can use the (tiny) package [PlutoUI.jl](/~https://github.com/JuliaPluto/PlutoUI.jl) for some predefined input elements. For example, you use `PlutoUI` to write
```julia
@bind x Slider(5:15)
Expand All @@ -159,11 +167,11 @@ _The `@bind` syntax in not limited to `html"..."` objects, but **can be used for
"""

# ╔═╡ d5b3be4a-7f52-11ea-2fc7-a5835808207d
md"""#### More packages
In fact, **_any package_ can add bindable values to their objects**. For example, a geoplotting package could add a JS `input` event to their plot that contains the cursor coordinates when it is clicked. You can then use those coordinates inside Julia.
md"""
#### More packages
A package _does not need to add `Pluto.jl` as a dependency to do so_: only the `Base.show(io, MIME("text/html"), obj)` function needs to be extended to contain a `<script>` that triggers the `input` event with a value. (It's up to the package creator _when_ and _what_.) This _does not affect_ how the object is displayed outside of Pluto.jl: uncaught events are ignored by your browser."""
In fact, **_any package_ can add bindable values to their objects**. For example, a geoplotting package could add a JS `input` event to their plot that contains the cursor coordinates when it is clicked. You can then use those coordinates inside Julia. Take a look at the [JavaScript sample notebook](./sample/JavaScript.jl) to learn more about these techniques!
"""

# ╔═╡ aa8f6a0e-303a-11eb-02b7-5597c167596d

Expand Down Expand Up @@ -208,22 +216,20 @@ For example, _expanding_ the `@bind` macro turns this expression:
@bind x Slider(5:15)
```
into:
into (simplified):
```julia
begin
local el = Slider(5:15)
global x = if applicable(Base.get, el)
Base.get(el)
else
missing
end
PlutoRunner.Bond(el, :x)
local el = Slider(5:15)
global x = AbstractPlutoDingetjes.intial_value(el)
PlutoRunner.create_bond(el, :x)
end
```
The `if` block in the middle assigns an initial value to `x`, which will be `missing`, unless an extension of `Base.get` has been declared for the element. Most objects (like `html"<input>"` or `md"quelque chose"`) don't have a `Base.get` method defined. In fact, `Base.get` has _no_ single-argument methods by default, but you can write one for your special types!
We see that the macro creates a variable `x`, which is given the value `AbstractPlutoDingetjes.intial_value(el)`. This function returns `missing` by default, unless a method was implemented for your widget type. For example, `PlutoUI` has a `Slider` type, and it defines a method for `intial_value(slider::Slider)` that returns the default number.
Declaring a default value using `AbstractPlutoDingetjes` is **not necessary**, as shown by the earlier examples in this notebook, but the default value will be used for `x` if the `notebook.jl` file is _run as a plain julia file_, without Pluto's interactivity.
Declaring a default value using `Base.get` is **not necessary**, as shown by the examples above, but the default value will be used for `x` if the `notebook.jl` file is _run as a plain julia file_, without Pluto's interactivity. The package [`PlutoUI`](/~https://github.com/fonsp/PlutoUI.jl) defines default values.
You don't need to worry about this if you are just getting started with Pluto and interactive elements, but more advanced users should take a look at [`AbstractPlutoDingetjes.jl`](/~https://github.com/JuliaPluto/AbstractPlutoDingetjes.jl).
"""

Expand All @@ -232,9 +238,9 @@ md"#### JavaScript?
Yes! We are using `Generator.input` from [`observablehq/stdlib`](/~https://github.com/observablehq/stdlib#Generators_input) to create a JS _Generator_ (kind of like an Observable) that listens to `onchange`, `onclick` or `oninput` events, [depending on the element type](/~https://github.com/observablehq/stdlib#Generators_input).
This makes it super easy to create nice HTML/JS-based interaction elements - a package creator simply has to write a `show` method for MIME type `text/html` that creates a DOM object that triggers the `input` event. In other words, _Pluto's `@bind` will behave exactly like `viewof` in observablehq_.
This makes it super easy to create nice HTML/JS-based interaction elements - a package creator simply has to write a `show` method for MIME type `text/html` that creates a DOM object that triggers the `input` event. In other words, _Pluto's `@bind` will behave exactly like [`viewof` in observablehq](https://observablehq.com/@observablehq/introduction-to-views)_.
_If you want to make a cool new UI, go to [observablehq.com/@observablehq/introduction-to-views](https://observablehq.com/@observablehq/introduction-to-views) to learn how._"
_If you want to make a cool new UI for Pluto, go to the [JavaScript sample notebook](./sample/JavaScript.jl) to learn how!_"

# ╔═╡ dddb9f34-7f37-11ea-0abb-272ef1123d6f
md""
Expand All @@ -243,7 +249,7 @@ md""
md""

# ╔═╡ f7555734-7f34-11ea-069a-6bb67e201bdc
md"That's it for now! Let us know what you think using the feedback button below! 👇"
md"That's it for now! Let us know what you think using the feedback box below! 👇"

# ╔═╡ Cell order:
# ╟─db24490e-7eac-11ea-094e-9d3fc8f22784
Expand Down
2 changes: 1 addition & 1 deletion src/evaluation/RunBonds.jl
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function set_bond_values_reactive(; session::ServerSession, notebook::Notebook,
to_delete_vars = Set([to_delete_vars..., to_set...]) # also delete the bound symbols
WorkspaceManager.move_vars((session, notebook), old_workspace_name, new_workspace_name, to_delete_vars, methods_to_delete, to_reimport)
for (bound_sym, new_value) in zip(to_set, new_values)
WorkspaceManager.eval_in_workspace((session, notebook), :($(bound_sym) = $(new_value)))
WorkspaceManager.eval_in_workspace((session, notebook), :($(bound_sym) = Main.PlutoRunner.transform_bond_value($(QuoteNode(bound_sym)), $(new_value))))
end
end
to_reeval = where_referenced(notebook, notebook.topology, Set{Symbol}(to_set))
Expand Down
64 changes: 57 additions & 7 deletions src/runner/PlutoRunner.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const ObjectDimPair = Tuple{ObjectID,Int64}
const ExpandedCallCells = Dict{UUID,Expr}()



const supported_integration_features = Any[]



Expand Down Expand Up @@ -342,7 +342,16 @@ This function is memoized: running the same expression a second time will simply
"""
function run_expression(m::Module, expr::Any, cell_id::UUID, function_wrapped_info::Union{Nothing,Tuple{Set{Symbol},Set{Symbol}}}=nothing, contains_user_defined_macrocalls::Bool=false)
currently_running_cell_id[] = cell_id

# reset published objects
cell_published_objects[cell_id] = Dict{String,Any}()

# reset registered bonds
for s in get(cell_registered_bond_names, cell_id, Set{Symbol}())
delete!(registered_bond_elements, s)
end
cell_registered_bond_names[cell_id] = Set{Symbol}()


result, runtime = if function_wrapped_info === nothing
expr = pop!(ExpandedCallCells, cell_id, expr)
Expand Down Expand Up @@ -570,6 +579,7 @@ const alive_world_val = getfield(methods(Base.sqrt).ms[1], deleted_world) # type
const cell_results = Dict{UUID,Any}()
const cell_runtimes = Dict{UUID,Union{Nothing,UInt64}}()
const cell_published_objects = Dict{UUID,Dict{String,Any}}()
const cell_registered_bond_names = Dict{UUID,Set{Symbol}}()

const tree_display_limit = 30
const tree_display_limit_increase = 40
Expand All @@ -581,7 +591,7 @@ const table_column_display_limit_increase = 30
const tree_display_extra_items = Dict{UUID,Dict{ObjectDimPair,Int64}}()

function formatted_result_of(cell_id::UUID, ends_with_semicolon::Bool, showmore::Union{ObjectDimPair,Nothing}=nothing, workspace::Module=Main)::NamedTuple{(:output_formatted, :errored, :interrupted, :process_exited, :runtime, :published_objects),Tuple{PlutoRunner.MimedOutput,Bool,Bool,Bool,Union{UInt64,Nothing},Dict{String,Any}}}
load_integration_if_needed.(integrations)
load_integrations_if_needed()
currently_running_cell_id[] = cell_id

extra_items = if showmore === nothing
Expand Down Expand Up @@ -643,7 +653,7 @@ end
Base.IOContext(io::IOContext, ::Nothing) = io

"The `IOContext` used for converting arbitrary objects to pretty strings."
const default_iocontext = IOContext(devnull, :color => false, :limit => true, :displaysize => (18, 88), :is_pluto => true)
const default_iocontext = IOContext(devnull, :color => false, :limit => true, :displaysize => (18, 88), :is_pluto => true, :pluto_supported_integration_features => supported_integration_features)

const imagemimes = [MIME"image/svg+xml"(), MIME"image/png"(), MIME"image/jpg"(), MIME"image/jpeg"(), MIME"image/bmp"(), MIME"image/gif"()]
# in descending order of coolness
Expand Down Expand Up @@ -1117,6 +1127,21 @@ end
# We have a super cool viewer for objects that are a Tables.jl table. To avoid version conflicts, we only load this code after the user (indirectly) loaded the package Tables.jl.
# This is similar to how Requires.jl works, except we don't use a callback, we just check every time.
const integrations = Integration[
Integration(
id = Base.PkgId(Base.UUID(reinterpret(Int128, codeunits("Paul Berg Berlin")) |> first), "AbstractPlutoDingetjes"),
code = quote
@assert v"1.0.0" <= AbstractPlutoDingetjes.MY_VERSION < v"2.0.0"
initial_value_getter_ref[] = AbstractPlutoDingetjes.Bonds.initial_value
transform_value_ref[] = AbstractPlutoDingetjes.Bonds.transform_value

push!(supported_integration_features,
AbstractPlutoDingetjes,
AbstractPlutoDingetjes.Bonds,
AbstractPlutoDingetjes.Bonds.initial_value,
AbstractPlutoDingetjes.Bonds.transform_value,
)
end,
),
Integration(
id = Base.PkgId(UUID("0c5d862f-8b57-4792-8d23-62f2024744c7"), "Symbolics"),
code = quote
Expand Down Expand Up @@ -1225,6 +1250,8 @@ function load_integration_if_needed(integration::Integration)
end
end

load_integrations_if_needed() = load_integration_if_needed.(integrations)

function load_integration(integration::Integration)
integration.loaded[] = true
try
Expand Down Expand Up @@ -1398,6 +1425,18 @@ end
# BONDS
###

const registered_bond_elements = Dict{Symbol, Any}()

function transform_bond_value(s::Symbol, value_from_js)
element = get(registered_bond_elements, s, nothing)
return try
transform_value_ref[](element, value_from_js)
catch e
@error "AbstractPlutoDingetjes: Bond value transformation errored." exception=(e, catch_backtrace())
(Text("❌ AbstractPlutoDingetjes: Bond value transformation errored."), e, stacktrace(catch_backtrace()))
end
end

"""
_“The name is Bond, James Bond.”_
Expand Down Expand Up @@ -1425,13 +1464,22 @@ struct Bond
Bond(element, defines::Symbol) = showable(MIME"text/html"(), element) ? new(element, defines) : error("""Can only bind to html-showable objects, ie types T for which show(io, ::MIME"text/html", x::T) is defined.""")
end

function create_bond(element, defines::Symbol)
push!(cell_registered_bond_names[currently_running_cell_id[]], defines)
registered_bond_elements[defines] = element
Bond(element, defines)
end

import Base: show
function show(io::IO, ::MIME"text/html", bond::Bond)
withtag(io, :bond, :def => bond.defines) do
show(io, MIME"text/html"(), bond.element)
end
end

const initial_value_getter_ref = Ref{Function}(element -> missing)
const transform_value_ref = Ref{Function}((element, x) -> x)

"""
`@bind symbol element`
Expand All @@ -1450,12 +1498,13 @@ x^2
The first cell will show a slider as the cell's output, ranging from 0 until 100.
The second cell will show the square of `x`, and is updated in real-time as the slider is moved.
"""
macro bind(def, element)
macro bind(def, element)
if def isa Symbol
quote
$(load_integrations_if_needed)()
local el = $(esc(element))
global $(esc(def)) = Core.applicable(Base.get, el) ? Base.get(el) : missing
PlutoRunner.Bond(el, $(Meta.quot(def)))
global $(esc(def)) = Core.applicable(Base.get, el) ? Base.get(el) : $(initial_value_getter_ref)[](el)
PlutoRunner.create_bond(el, $(Meta.quot(def)))
end
else
:(throw(ArgumentError("""\nMacro example usage: \n\n\t@bind my_number html"<input type='range'>"\n\n""")))
Expand All @@ -1467,8 +1516,9 @@ Will be inserted in saved notebooks that use the @bind macro, make sure that the
"""
const fake_bind = """macro bind(def, element)
quote
local iv = try Base.loaded_modules[Base.PkgId(Base.UUID("6e696c72-6542-2067-7265-42206c756150"), "AbstractPlutoDingetjes")].Bonds.initial_value catch; b -> missing; end
local el = \$(esc(element))
global \$(esc(def)) = Core.applicable(Base.get, el) ? Base.get(el) : missing
global \$(esc(def)) = Core.applicable(Base.get, el) ? Base.get(el) : iv(el)
el
end
end"""
Expand Down
Loading

0 comments on commit fc74c51

Please sign in to comment.