commit 2ff3fcaa494323aa253a627ebf26f999ea63e604 Author: Swrup Date: Thu Apr 11 15:46:46 2024 +0200 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..69fa449 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +_build/ diff --git a/.ocamlformat b/.ocamlformat new file mode 100644 index 0000000..6f010c4 --- /dev/null +++ b/.ocamlformat @@ -0,0 +1,42 @@ +version=0.28.1 +assignment-operator=end-line +break-cases=fit +break-fun-decl=wrap +break-fun-sig=wrap +break-infix=wrap +break-infix-before-func=false +break-separators=before +break-sequences=true +cases-exp-indent=2 +cases-matching-exp-indent=normal +doc-comments=before +doc-comments-padding=2 +doc-comments-tag-only=default +dock-collection-brackets=false +exp-grouping=preserve +field-space=loose +if-then-else=compact +indicate-multiline-delimiters=space +indicate-nested-or-patterns=unsafe-no +infix-precedence=indent +leading-nested-match-parens=false +let-and=sparse +let-binding-spacing=compact +let-module=compact +margin=80 +max-indent=68 +module-item-spacing=sparse +ocp-indent-compat=false +parens-ite=false +parens-tuple=always +parse-docstrings=true +sequence-blank-line=preserve-one +sequence-style=terminator +single-case=compact +space-around-arrays=true +space-around-lists=true +space-around-records=true +space-around-variants=true +type-decl=sparse +wrap-comments=false +wrap-fun-args=true diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..033b70e --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,8 @@ +The ISC License (ISC) +===================== + +Copyright © 2024, swrup + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e9d84ba --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# gadgetobrr + +utilitarian Brr based library to make widgets. +Help to create input elements and listen to input events on them + +## TODOs + +- use webstorage to make it work accross pages? diff --git a/dune-project b/dune-project new file mode 100644 index 0000000..4adcfdb --- /dev/null +++ b/dune-project @@ -0,0 +1,21 @@ +(lang dune 3.14) +(name gadgetobrr) +(generate_opam_files true) + +(authors "swrup ") +(maintainers "swrup ") +(license ISC) + +(source + (uri git+https://forge.kumikode.org/swrup/gadgetobrr.git)) +(homepage https://forge.kumikode.org/swrup/gadgetobrr) +(bug_reports https://forge.kumikode.org/swrup/gadgetobrr/issues) + +(package + (name gadgetobrr) + (synopsis "Brr based library to help making input elements") + (description "library based on Brr to make input elements and setup listeners \ + on them to tweak values") + (depends ocaml dune js_of_ocaml brr (tiny_httpd :with-dev-setup)) + (tags + (topics js_of_ocaml brr widget))) diff --git a/examples/content/dune b/examples/content/dune new file mode 100644 index 0000000..485ede8 --- /dev/null +++ b/examples/content/dune @@ -0,0 +1,8 @@ +(rule + (target script.js) + (deps + (file ../script.bc.js)) + (action + (with-stdout-to + %{target} + (cat ../script.bc.js)))) diff --git a/examples/content/index.html b/examples/content/index.html new file mode 100644 index 0000000..63804ac --- /dev/null +++ b/examples/content/index.html @@ -0,0 +1,10 @@ + + + + gadgetobrr + + + + + + diff --git a/examples/content/style.css b/examples/content/style.css new file mode 100644 index 0000000..58b6397 --- /dev/null +++ b/examples/content/style.css @@ -0,0 +1,19 @@ +.brridget-container { + display: flex; + flex-direction: column; + top: 5vh; + left: 5vw; + background-color: black; + color: white; + opacity: 0.7; + padding-block: 2vh; + padding-inline: 1vw; +} + +.brridget-grid { + display: grid; + position: fixed; + top: 5vh; + left: 4vw; + gap: 10%; +} diff --git a/examples/dune b/examples/dune new file mode 100644 index 0000000..e42c169 --- /dev/null +++ b/examples/dune @@ -0,0 +1,21 @@ +(executable + (name runweb) + (modules runweb content) + (libraries tiny_httpd)) + +(executable + (name script) + (modules script) + (libraries js_of_ocaml brr gadgetobrr) + (modes js)) + +(rule + (target content.ml) + (deps + (file content/index.html) + (file content/style.css) + (file content/script.js)) + (action + (with-stdout-to + %{null} + (run ocaml-crunch -m plain content -o %{target})))) diff --git a/examples/runweb.ml b/examples/runweb.ml new file mode 100644 index 0000000..0742c3f --- /dev/null +++ b/examples/runweb.ml @@ -0,0 +1,29 @@ +module S = Tiny_httpd + +let asset_loader path = + match Content.read path with None -> assert false | Some asset -> asset + +let () = + let server = S.create ~port:8000 () in + S.add_route_handler ~meth:`GET server S.Route.return (fun _req -> + S.Response.make_string + ~headers:[ ("Content-Type", "text/html") ] + (Ok (asset_loader "index.html")) ); + + S.add_route_handler ~meth:`GET server + S.Route.(exact "script.js" @/ return) + (fun _req -> + S.Response.make_string + ~headers:[ ("Content-Type", "application/javascript") ] + (Ok (asset_loader "script.js")) ); + + S.add_route_handler ~meth:`GET server + S.Route.(exact "style.css" @/ return) + (fun _req -> + S.Response.make_string + ~headers:[ ("Content-Type", "text/css") ] + (Ok (asset_loader "style.css")) ); + + Printf.printf "listening on http://%s:%d\n%!" (S.addr server) (S.port server); + ignore @@ Sys.command "xdg-open http://localhost:8000"; + ignore (match S.run server with Ok () -> () | Error e -> raise e) diff --git a/examples/script.ml b/examples/script.ml new file mode 100644 index 0000000..ec41b4f --- /dev/null +++ b/examples/script.ml @@ -0,0 +1,58 @@ +(* This add a slidder to into
+ moving the slider print its value to the console *) +open Brr +open Gadgetobrr + +let append_el_to_body el = + let body = + match El.find_first_by_selector (Jstr.v "body") with + | Some body -> body + | None -> failwith "append_el_to_body: body element not found" + in + El.append_children body [ el ]; + () + +let () = + let datalist = + mk_datalist (List.map Jstr.v [ "cc"; "sava"; "bb" ]) "my-text-datalist" + in + let text = + mk_text ~min:(Some 0) ~max:(Some 100) ~size:(Some 4) ~value:"?" + ~id:"my-text" ~label:"Important text, input carefully" + ~datalist_id:(Some datalist.datalist_id) + in + add_input_listener text (fun s -> Printf.printf "text value: %s\n" s); + mk_dragable text; + append_el_to_body (el text); + append_el_to_body datalist.datalist_el; + + let datalist = + mk_datalist + (List.map Jstr.of_float [ 2.; 3.; 5.; 8.; 13. ]) + "my-slider-datalist" + in + let slider = + mk_slider ~min:0. ~max:100. ~step:Any ~value:50. ~id:"my-slider" + ~label:"Important slider, slide carefully" + ~datalist_id:(Some datalist.datalist_id) + in + add_input_listener slider (fun x -> Printf.printf "slider value: %f\n" x); + mk_dragable slider; + append_el_to_body (el slider); + append_el_to_body datalist.datalist_el; + + let color = + mk_color ~value:"#00ff00" ~id:"my-color" + ~label:"Important color, pick carefully" + in + add_input_listener color (fun s -> Printf.printf "color value: %s\n" s); + mk_dragable color; + append_el_to_body (el color); + + let brridget_grid : El.t = + El.div ~d:G.document + ~at:[ At.class' (Jstr.v "brridget-grid") ] + [ el text; el slider; el color ] + in + append_el_to_body brridget_grid; + () diff --git a/gadgetobrr.opam b/gadgetobrr.opam new file mode 100644 index 0000000..528025d --- /dev/null +++ b/gadgetobrr.opam @@ -0,0 +1,34 @@ +# This file is generated by dune, edit dune-project instead +opam-version: "2.0" +synopsis: "Brr based library to help making input elements" +description: + "library based on Brr to make input elements and setup listeners on them to tweak values" +maintainer: ["swrup "] +authors: ["swrup "] +license: "ISC" +tags: ["topics" "js_of_ocaml" "brr" "widget"] +homepage: "https://forge.kumikode.org/swrup/gadgetobrr" +bug-reports: "https://forge.kumikode.org/swrup/gadgetobrr/issues" +depends: [ + "ocaml" + "dune" {>= "3.14"} + "js_of_ocaml" + "brr" + "tiny_httpd" {with-dev-setup} + "odoc" {with-doc} +] +build: [ + ["dune" "subst"] {dev} + [ + "dune" + "build" + "-p" + name + "-j" + jobs + "@install" + "@runtest" {with-test} + "@doc" {with-doc} + ] +] +dev-repo: "git+https://forge.kumikode.org/swrup/gadgetobrr.git" diff --git a/src/dune b/src/dune new file mode 100644 index 0000000..4fb7358 --- /dev/null +++ b/src/dune @@ -0,0 +1,4 @@ +(library + (name gadgetobrr) + (public_name gadgetobrr) + (libraries js_of_ocaml brr)) diff --git a/src/gadgetobrr.ml b/src/gadgetobrr.ml new file mode 100644 index 0000000..162b1a9 --- /dev/null +++ b/src/gadgetobrr.ml @@ -0,0 +1,269 @@ +open Brr + +type datalist = + { datalist_id : string + ; datalist_el : El.t + } + +type widget = + { id : string + ; el : El.t + ; input_el : El.t + } + +type _ t = + | Text : widget -> string t + | Slider : widget -> float t + | Color : widget -> string t + +(* Any is for continuous slider *) +type step_kind = + | Any + | Step_value of float + +let slider_class = "brridget-slider" + +let slider_container_class = slider_class ^ "-container" + +let text_class = "brridget-text" + +let text_container_class = text_class ^ "-container" + +let color_class = "brridget-color" + +let color_container_class = color_class ^ "-container" + +let container_class = "brridget-container" + +let to_widget : type a. a t -> widget = function + | Slider r | Text r | Color r -> r + +let el : type a. a t -> El.t = + fun o -> + let r = to_widget o in + r.el + +let input_el : type a. a t -> El.t = + fun o -> + let r = to_widget o in + r.input_el + +let id : type a. a t -> string = + fun o -> + let r = to_widget o in + r.id + +let step_to_string step = + match step with Any -> "any" | Step_value f -> Float.to_string f + +let mk_datalist : Jstr.t list -> string -> datalist = + fun l id -> + let option_el_l = + List.map (fun s -> El.option ~d:G.document ~at:[ At.value s ] []) l + in + let datalist = + El.datalist ~d:G.document ~at:[ At.id (Jstr.v id) ] option_el_l + in + { datalist_id = id; datalist_el = datalist } + +let mk_widget ~label ~id ~at ~container_at = + let input_el = El.input ~d:G.document ~at () in + let label = + El.label ~d:G.document + ~at:[ At.for' (Jstr.v id) ] + [ El.txt' ~d:G.document label ] + in + let container_el = + El.div ~d:G.document ~at:container_at [ label; input_el ] + in + { id; el = container_el; input_el } + +(* make a div of class slippery-slidy-container containing a + slider of class slippery-slidy-slider *) +let mk_slider : + min:float + -> max:float + -> step:step_kind + -> value:float + -> id:string + -> label:string + -> datalist_id:string option + -> float t = + fun ~min ~max ~step ~value ~id ~label ~datalist_id -> + (* some checks *) + let b = min <= max && value <= max && value >= min in + if not b then + raise (Invalid_argument "mk_slider: parameters did not pass validation") + else + let datalist = + match datalist_id with + | None -> [] + | Some datalist_id -> [ At.v (Jstr.v "list") (Jstr.v datalist_id) ] + in + let at = + datalist + @ [ At.type' (Jstr.v "range") + ; At.float (Jstr.v "min") min + ; At.float (Jstr.v "max") max + ; At.v (Jstr.v "step") (Jstr.v (step_to_string step)) + ; At.value (Jstr.of_float value) + ; At.id (Jstr.v id) + ; At.class' (Jstr.v slider_class) + ] + in + let container_at = + [ At.class' (Jstr.v slider_container_class) + ; At.class' (Jstr.v container_class) + ] + in + Slider (mk_widget ~at ~container_at ~label ~id) + +let mk_text : + min:int option + -> max:int option + -> size:int option + -> datalist_id:string option + -> value:string + -> id:string + -> label:string + -> string t = + fun ~min ~max ~size ~datalist_id ~value ~id ~label -> + let min = Option.map (At.int (Jstr.v "min")) min in + let max = Option.map (At.int (Jstr.v "max")) max in + let size = Option.map (At.int (Jstr.v "size")) size in + let datalist = + Option.map (fun id -> At.v (Jstr.v "list") (Jstr.v id)) datalist_id + in + let opts = List.filter_map Fun.id [ min; max; size; datalist ] in + let at = + opts + @ [ At.type' (Jstr.v "text") + ; At.value (Jstr.v value) + ; At.id (Jstr.v id) + ; At.class' (Jstr.v text_class) + ] + in + let container_at = + [ At.class' (Jstr.v text_container_class) + ; At.class' (Jstr.v container_class) + ] + in + Text (mk_widget ~at ~container_at ~label ~id) + +let color_regex = + let regexp = Jv.get Jv.global "RegExp" in + let re = Jv.new' regexp [| Jv.of_string "^#[0-9a-fA-F]{6}$" |] in + re + +let validate_color s = + let r = Jv.call color_regex "test" [| Jv.of_string s |] in + Jv.to_bool r + +(* The value must be in seven-character hexadecimal notation, meaning the "#" character followed by two digits each representing red, green, and blue, like this: #rrggbb. *) +let mk_color : value:string -> id:string -> label:string -> string t = + fun ~value ~id ~label -> + if not (validate_color value) then + raise + (Invalid_argument + (Printf.sprintf "mk_color: `%s` is not a valid hexadecimal color" value) + ) + else + let at = + [ At.type' (Jstr.v "color") + ; At.value (Jstr.v value) + ; At.id (Jstr.v id) + ; At.class' (Jstr.v color_class) + ] + in + let container_at = + [ At.class' (Jstr.v color_container_class) + ; At.class' (Jstr.v container_class) + ] + in + Color (mk_widget ~at ~container_at ~label ~id) + +let conv : type a. a t -> Jv.t -> a = + fun w -> + match w with + | Text _ -> Jv.to_string + | Slider _ -> fun jv -> float_of_string (Jv.to_string jv) + | Color _ -> (* TODO validate here and make a color type? *) Jv.to_string + +let add_input_listener t f = + (* we do not use Ev.input because it makes a Ev.Input.t but: + "For