Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Arrays of UI elements, type maintenance #71

Closed
wants to merge 2 commits into from
Closed
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
168 changes: 89 additions & 79 deletions src/Builtins.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ import Dates

export Slider, NumberField, Button, CheckBox, TextField, PasswordField, Select, MultiSelect, Radio, FilePicker, DateField, TimeField, ColorStringPicker

struct Slider
range::AbstractRange
default::Number
abstract type AbstractUIElement{T}
end
get(e::AbstractUIElement) = e.default

struct Slider{T<:AbstractRange{<:Number}} <: AbstractUIElement{eltype(T)}
range::T
default::eltype(T)
show_value::Bool
end

Expand All @@ -19,25 +23,23 @@ end
`@bind x Slider(1:10; default=8, show_value=true)`

"""
Slider(range::AbstractRange; default=missing, show_value=false) = Slider(range, (default === missing) ? first(range) : default, show_value)
Slider(range::T; default=missing, show_value=false) where T<:AbstractRange = Slider{T}(range, (default === missing) ? first(range) : default, show_value)

function show(io::IO, ::MIME"text/html", slider::Slider)
print(io, """<input
type="range"
min="$(first(slider.range))"
step="$(step(slider.range))"
max="$(last(slider.range))"
print(io, """<input
type="range"
min="$(first(slider.range))"
step="$(step(slider.range))"
max="$(last(slider.range))"
value="$(slider.default)"
$(slider.show_value ? "oninput=\"this.nextElementSibling.value=this.value\"" : "")
>""")

if slider.show_value
print(io, """<output>$(slider.default)</output>""")
end
end

get(slider::Slider) = slider.default

"""A box where you can type in a number, within a specific range.

## Examples
Expand All @@ -48,19 +50,17 @@ get(slider::Slider) = slider.default
`@bind x NumberField(1:10; default=8)`

"""
struct NumberField
range::AbstractRange
default::Number
struct NumberField{T<:AbstractRange{<:Number}} <: AbstractUIElement{eltype(T)}
range::T
default::eltype(T)
end

NumberField(range::AbstractRange; default=missing) = NumberField(range, (default === missing) ? first(range) : default)
NumberField(range::T; default=missing) where T<:AbstractRange{<:Number} = NumberField{T}(range, (default === missing) ? first(range) : default)

function show(io::IO, ::MIME"text/html", numberfield::NumberField)
print(io, """<input type="number" min="$(first(numberfield.range))" step="$(step(numberfield.range))" max="$(last(numberfield.range))" value="$(numberfield.default)">""")
end

get(numberfield::NumberField) = numberfield.default


"""A button that sends back the same value every time that it is clicked.

Expand All @@ -85,8 +85,8 @@ begin
end
```
"""
struct Button
label::AbstractString
struct Button{T<:AbstractString} <: AbstractUIElement{T}
label::T
end
Button() = Button("Click")

Expand All @@ -107,7 +107,7 @@ get(button::Button) = button.label

`md"Would you like the thing? \$(@bind enable_thing CheckBox())"`
"""
struct CheckBox
struct CheckBox <: AbstractUIElement{Bool}
default::Bool
end

Expand All @@ -117,9 +117,6 @@ function show(io::IO, ::MIME"text/html", button::CheckBox)
print(io, """<input type="checkbox"$(button.default ? " checked" : "")>""")
end

get(checkbox::CheckBox) = checkbox.default


"""A text input (`<input type="text">`) - the user can type text, the text is returned as `String` via `@bind`.

If `dims` is a tuple `(cols::Integer, row::Integer)`, a `<textarea>` will be shown, with the given dimensions
Expand All @@ -132,11 +129,11 @@ See the [Mozilla docs about `<input type="text">`](https://developer.mozilla.org
`@bind poem TextField()`

`@bind poem TextField((30,5); default="Hello\nJuliaCon!")`"""
struct TextField
struct TextField{T<:AbstractString} <: AbstractUIElement{T}
dims::Union{Tuple{Integer,Integer},Nothing}
default::AbstractString
default::T
end
TextField(dims::Union{Tuple{Integer,Integer},Nothing}=nothing; default::AbstractString="") = TextField(dims, default)
TextField(dims::Union{Tuple{Integer,Integer},Nothing}=nothing; default::T="") where T<:AbstractString = TextField{T}(dims, default)

function show(io::IO, ::MIME"text/html", textfield::TextField)
if textfield.dims === nothing
Expand All @@ -146,10 +143,6 @@ function show(io::IO, ::MIME"text/html", textfield::TextField)
end
end

get(textfield::TextField) = textfield.default



"""A password input (`<input type="password">`) - the user can type text, the text is returned as `String` via `@bind`.

This does not provide any special security measures, it just renders black dots (•••) instead of the typed characters.
Expand All @@ -162,16 +155,14 @@ See the [Mozilla docs about `<input type="password">`](https://developer.mozilla
`@bind secret_poem PasswordField()`

`@bind secret_poem PasswordField(default="Te dansen omdat men leeft")`"""
Base.@kwdef struct PasswordField
default::AbstractString=""
Base.@kwdef struct PasswordField{T<:AbstractString} <: AbstractUIElement{T}
default::T=""
end

function show(io::IO, ::MIME"text/html", passwordfield::PasswordField)
print(io, """<input type="password" value="$(htmlesc(passwordfield.default))">""")
end

get(passwordfield::PasswordField) = passwordfield.default


"""A dropdown menu (`<select>`) - the user can choose one of the `options`, an array of `String`s.

Expand All @@ -187,28 +178,30 @@ See the [Mozilla docs about `select`](https://developer.mozilla.org/en-US/docs/W
`@bind veg Select(["potato" => "🥔", "carrot" => "🥕"])`

`@bind veg Select(["potato" => "🥔", "carrot" => "🥕"], default="carrot")`"""
struct Select
options::Array{Pair{<:AbstractString,<:Any},1}
default::Union{Missing, AbstractString}
struct Select{K<:Union{Missing,AbstractString},T<:AbstractDict{K,<:Any}} <: AbstractUIElement{K}
options::T
default::K
end
Select(options::Array{<:AbstractString,1}; default=missing) = Select([o => o for o in options], default)
Select(options::Array{<:Pair{<:AbstractString,<:Any},1}; default=missing) = Select(options, default)
# TODO replace Dict with an ordered Dict
Select(options::T; default::K=missing) where {K<:Union{Missing,AbstractString}, T <: AbstractDict{K,<:Any}} = Select{K,T}(options, default)
Select(options::T; default=missing) where T <: AbstractVector{<:AbstractString} = Select(Dict(o => o for o in options), default)
Select(options::T; default=missing) where T <: AbstractVector{<:Pair{<:AbstractString,<:Any}} = Select(Dict(options), default)

function show(io::IO, ::MIME"text/html", select::Select)
withtag(io, :select) do
for o in select.options
print(io, """<option value="$(htmlesc(o.first))"$(select.default === o.first ? " selected" : "")>""")
if showable(MIME"text/html"(), o.second)
show(io, MIME"text/html"(), o.second)
for (k,v) in select.options
print(io, """<option value="$(htmlesc(k))"$(select.default === k ? " selected" : "")>""")
if showable(MIME"text/html"(), v)
show(io, MIME"text/html"(), v)
else
print(io, o.second)
print(io, v)
end
print(io, "</option>")
end
end
end

get(select::Select) = ismissing(select.default) ? first(select.options).first : select.default
get(select::Select) = ismissing(select.default) ? first(keys(select.options)) : select.default


"""A multi-selector (`<select multi>`) - the user can choose one or more of the `options`, an array of `Strings.
Expand All @@ -225,28 +218,29 @@ See the [Mozilla docs about `select`](https://developer.mozilla.org/en-US/docs/W
`@bind veg MultiSelect(["potato" => "🥔", "carrot" => "🥕"])`

`@bind veg MultiSelect(["potato" => "🥔", "carrot" => "🥕"], default=["carrot"])`"""
struct MultiSelect
options::Array{Pair{<:AbstractString,<:Any},1}
default::Union{Missing, AbstractVector{AbstractString}}
struct MultiSelect{K<:Union{Missing,AbstractString},T<:AbstractDict{K,<:Any}} <: AbstractUIElement{K}
options::T
default::K
end
MultiSelect(options::Array{<:AbstractString,1}; default=missing) = MultiSelect([o => o for o in options], default)
MultiSelect(options::Array{<:Pair{<:AbstractString,<:Any},1}; default=missing) = MultiSelect(options, default)
MultiSelect(options::T; default::K=missing) where {K<:Union{Missing,AbstractString},T<:AbstractDict{K,<:Any}} = Multiselect{K,T}(options, default)
MultiSelect(options::T; default=missing) where T<:AbstractVector{<:AbstractString} = MultiSelect(Dict(o => o for o in options), default)
MultiSelect(options::T; default=missing) where T<:AbstractVector{<:Pair{<:AbstractString,<:Any}} = MultiSelect(Dict(options), default)

function show(io::IO, ::MIME"text/html", select::MultiSelect)
withtag(io, Symbol("select multiple")) do
for o in select.options
print(io, """<option value="$(htmlesc(o.first))"$(!ismissing(select.default) && o.first ∈ select.default ? " selected" : "")>""")
if showable(MIME"text/html"(), o.second)
show(io, MIME"text/html"(), o.second)
for (k,v) in select.options
print(io, """<option value="$(htmlesc(k))"$(!ismissing(select.default) && k ∈ select.default ? " selected" : "")>""")
if showable(MIME"text/html"(), v)
show(io, MIME"text/html"(), v)
else
print(io, o.second)
print(io, v)
end
print(io, "</option>")
end
end
end

get(select::MultiSelect) = ismissing(select.default) ? Any[] : select.default
get(select::MultiSelect{T}) where T = ismissing(select.default) ? keytype(T)[] : select.default

"""A file upload box. The chosen file will be read by the browser, and the bytes are sent back to Julia.

Expand All @@ -266,8 +260,8 @@ You can limit the allowed MIME types:
@bind image_data FilePicker([MIME("image/*")])
```
"""
struct FilePicker
accept::Array{MIME,1}
struct FilePicker{T<:AbstractVector{MIME}} <: AbstractUIElement{NamedTuple{(:name,:data,:type),Tuple{String,Array{UInt8},String}}}
accept::T
end
FilePicker() = FilePicker(MIME[])

Expand All @@ -277,9 +271,9 @@ function show(io::IO, ::MIME"text/html", filepicker::FilePicker)
print(io, "'>")
end

get(select::FilePicker) = Dict("name" => "", "data" => UInt8[], "type" => "")
get(select::FilePicker) = (name="", data=UInt8[], type="")

"""A group of radio buttons - the user can choose one of the `options`, an array of `String`s.
"""A group of radio buttons - the user can choose one of the `options`, an array of `String`s.

`options` can also be an array of pairs `key::String => value::Any`. The `key` is returned via `@bind`; the `value` is shown.

Expand All @@ -292,25 +286,26 @@ get(select::FilePicker) = Dict("name" => "", "data" => UInt8[], "type" => "")
`@bind veg Radio(["potato" => "🥔", "carrot" => "🥕"], default="carrot")`

"""
struct Radio
options::Array{Pair{<:AbstractString,<:Any},1}
default::Union{Missing, AbstractString}
struct Radio{K<:Union{Missing,AbstractString},T<:AbstractDict{K,<:Any}} <: AbstractUIElement{K}
options::T
default::K
end
Radio(options::Array{<:AbstractString,1}; default=missing) = Radio([o => o for o in options], default)
Radio(options::Array{<:Pair{<:AbstractString,<:Any},1}; default=missing) = Radio(options, default)
Radio(options::T; default::K=missing) where {K<:Union{Missing,AbstractString},T<:AbstractDict{K,<:Any}} = Radio{K,T}(options,default)
Radio(options::T; default=missing) where T<:AbstractVector{<:AbstractString} = Radio(Dict(o => o for o in options), default)
Radio(options::T; default=missing) where T<:AbstractVector{Pair{<:AbstractString,<:Any}} = Radio(Dict(options), default)

function show(io::IO, ::MIME"text/html", radio::Radio)
groupname = randstring('a':'z')
withtag(io, :form, :id=>groupname) do
for o in radio.options
for (k,v) in radio.options
withtag(io, :div) do
print(io, """<input type="radio" id="$(htmlesc(groupname * o.first))" name="$(groupname)" value="$(htmlesc(o.first))"$(radio.default === o.first ? " checked" : "")>""")
print(io, """<input type="radio" id="$(htmlesc(groupname * k))" name="$(groupname)" value="$(htmlesc(k))"$(radio.default === k ? " checked" : "")>""")

withtag(io, :label, :for=>(groupname * o.first)) do
if showable(MIME"text/html"(), o.second)
show(io, MIME"text/html"(), o.second)
withtag(io, :label, :for=>(groupname * k)) do
if showable(MIME"text/html"(), v)
show(io, MIME"text/html"(), v)
else
print(io, o.second)
print(io, v)
end
end
end
Expand All @@ -334,7 +329,6 @@ function show(io::IO, ::MIME"text/html", radio::Radio)
end
end

get(radio::Radio) = radio.default

"""A date input (`<input type="date">`) - the user can pick a date, the date is returned as `Dates.DateTime` via `@bind`.

Expand All @@ -346,14 +340,13 @@ See the [Mozilla docs about `<input type="date">`](https://developer.mozilla.org
`@bind best_day_of_my_live DateField()`

`@bind best_day_of_my_live DateField(default=today())`"""
Base.@kwdef struct DateField
Base.@kwdef struct DateField <: AbstractUIElement{Union{Dates.TimeType,Missing}}
default::Union{Dates.TimeType,Missing}=missing
end

function show(io::IO, ::MIME"text/html", datefield::DateField)
withtag(() -> (), io, :input, :type=>"date", :value=>datefield.default === missing ? "" : Dates.format(datefield.default, "Y-mm-dd"))
end
get(datefield::DateField) = datefield.default


"""A time input (`<input type="time">`) - the user can pick a time, the time is returned as `Dates.DateTime` via `@bind`.
Expand All @@ -366,14 +359,13 @@ See the [Mozilla docs about `<input type="time">`](https://developer.mozilla.org
`@bind lunch_time TimeField()`

`@bind lunch_time TimeField(default=now())`"""
Base.@kwdef struct TimeField
Base.@kwdef struct TimeField <: AbstractUIElement{Union{Dates.TimeType,Missing}}
default::Union{Dates.TimeType,Missing}=missing
end

function show(io::IO, ::MIME"text/html", timefield::TimeField)
withtag(() -> (), io, :input, :type=>"time", :value=>timefield.default === missing ? "" : Dates.format(timefield.default, "HH:MM:SS"))
end
get(timefield::TimeField) = timefield.default


"""A color input (`<input type="color">`) - the user can pick an RGB color, the color is returned as color hex `String` via `@bind`. The value is lowercase and starts with `#`.
Expand All @@ -387,11 +379,29 @@ See the [Mozilla docs about `<input type="color">`](https://developer.mozilla.or

`@bind color ColorStringPicker(default="#aabbcc")`
"""
Base.@kwdef struct ColorStringPicker
Base.@kwdef struct ColorStringPicker <: AbstractUIElement{String}
default::String="#000000"
end

function show(io::IO, ::MIME"text/html", colorStringPicker::ColorStringPicker)
withtag(() -> (), io, :input, :type=>"color", :value=>colorStringPicker.default)
end
get(colorStringPicker::ColorStringPicker) = colorStringPicker.default

function show(io::IO, mime::MIME"text/html", elements::AbstractArray{<:AbstractUIElement})
if length(elements) == 1
show(io,mime,elements...)
return
end
print(io,"<table>")
for r = eachrow(elements)
print(io,"<tr>")
for e in r
print(io,"<td>")
show(io,mime,e)
print(io,"</td>")
end
print(io,"</tr>")
end
end

get(elements::AbstractArray{<:AbstractUIElement}) = get.(elements)
6 changes: 3 additions & 3 deletions src/Clock.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export Clock

struct Clock
struct Clock <: AbstractUIElement{Int64}
interval::Real
fixed::Bool
start_running::Bool
Expand All @@ -14,7 +14,7 @@ function show(io::IO, ::MIME"text/html", clock::Clock)
cz = read(joinpath(PKG_ROOT_DIR, "assets", "clock_zoof.svg"), String)
js = read(joinpath(PKG_ROOT_DIR, "assets", "clock.js"), String)
css = read(joinpath(PKG_ROOT_DIR, "assets", "clock.css"), String)

result = """
<clock class='$(clock.fixed ? " fixed" : "")$(clock.start_running ? "" : " stopped")'>
<analog>
Expand All @@ -37,4 +37,4 @@ function show(io::IO, ::MIME"text/html", clock::Clock)
write(io, result)
end

get(clock::Clock) = 1
get(clock::Clock) = 1
Loading