3379 lines
105 KiB
OCaml
3379 lines
105 KiB
OCaml
(* why *)
|
||
|
||
(*
|
||
|
||
names?:
|
||
- universal tool, unitool [was thinking about how this is trying to unify a lot of my "tools for thought"]
|
||
* because it has always bothered me that it's easier to use google search as a calculator than the purpose built app!!!!!!!!
|
||
- universal console, unicon (UNICOrN) [unicon is nice ;3]
|
||
- non-magical systems (NMS) un-magical
|
||
- console is an interface to allow you to program your computer more easily.
|
||
|
||
describe exactly every case you can think of that you want this drawing and layout system to handle:
|
||
|
||
* draw text on variously coloured backgrounds that can be defined locally or globally
|
||
* TODO
|
||
|
||
*)
|
||
|
||
(*
|
||
|
||
ALWAYS BREAK UP THE PROBLEM INTO SMALLER CHUNKS BITCH!!
|
||
|
||
Times I would have solved it faster if i broke it up instead of trying to understand it all at once: 2
|
||
|
||
a computation console
|
||
|
||
- irmin store provides a tree of data objects
|
||
- the tree can be navigated in the default view
|
||
- the selected object can be edited <enter> or executed as an ocaml top level phrase <C-enter>
|
||
- each execution stores any edited modifications and the command to execute that phrase in the current irmin store context as a commit message
|
||
- while editing a data object <ctrl-enter> wille search for the previous and next `;;` or BOF/EOF and execute the enclosed text and the commit message includes the character offsets of the executed text.
|
||
- executions can modify the window system creating new windows and redirecting input focus. They define their own input handling however C-g,C-g,C-g will restore the window system to the default??
|
||
|
||
but how do we integrate this with the ocaml environment and name spaces??
|
||
some options:
|
||
- always wrap execution units from data objects in some sort of local namespace so opens are not global?
|
||
- dig into the toplevel environment and manipulate it, this will also help with things like completion and context help
|
||
|
||
*)
|
||
open Js_of_ocaml
|
||
module F = Fmt
|
||
|
||
module Logs_reporter = struct
|
||
(* Console reporter *)
|
||
|
||
open Jsoo_runtime
|
||
|
||
let console : Logs.level -> string -> unit =
|
||
fun level s ->
|
||
let meth =
|
||
match level with
|
||
| Logs.Error -> "error"
|
||
| Logs.Warning -> "warn"
|
||
| Logs.Info -> "info"
|
||
| Logs.Debug -> "debug"
|
||
| Logs.App -> "log"
|
||
in
|
||
ignore
|
||
(Js.meth_call
|
||
(Js.pure_js_expr "console")
|
||
meth
|
||
[| Js.string s |])
|
||
|
||
let ppf, flush =
|
||
let b = Buffer.create 255 in
|
||
let flush () =
|
||
let s = Buffer.contents b in
|
||
Buffer.clear b;
|
||
s
|
||
in
|
||
(Format.formatter_of_buffer b, flush)
|
||
|
||
let hook =
|
||
ref (fun level s ->
|
||
ignore (Logs.level_to_string (Some level) ^ ": " ^ s))
|
||
|
||
let console_report _src level ~over k msgf =
|
||
let k _ =
|
||
let s = flush () in
|
||
console level s;
|
||
!hook level s;
|
||
over ();
|
||
k ()
|
||
in
|
||
msgf @@ fun ?header ?tags fmt ->
|
||
let _tags = tags in
|
||
match header with
|
||
| None -> Format.kfprintf k ppf ("@[" ^^ fmt ^^ "@]@.")
|
||
| Some h -> Format.kfprintf k ppf ("[%s] @[" ^^ fmt ^^ "@]@.") h
|
||
|
||
let console_reporter () = { Logs.report = console_report }
|
||
end
|
||
|
||
let _ =
|
||
Logs.set_reporter (Logs_reporter.console_reporter ());
|
||
Logs.set_level (Some Debug)
|
||
|
||
module Log = Logs
|
||
module Cohttp_backend = Cohttp_lwt_jsoo
|
||
|
||
module Git_af = struct
|
||
open Lwt.Infix
|
||
|
||
type error = |
|
||
|
||
let git_af_scheme : [ `HTTP | `HTTPS ] Mimic.value =
|
||
Mimic.make ~name:"git-af-scheme"
|
||
|
||
let git_af_port : int Mimic.value = Mimic.make ~name:"git-af-port"
|
||
|
||
let git_af_hostname : string Mimic.value =
|
||
Mimic.make ~name:"git-af-hostname"
|
||
|
||
let pp_error : error Fmt.t = fun _ppf -> function _ -> .
|
||
|
||
let with_redirects ?(max = 10) ~f uri =
|
||
if max < 10 then invalid_arg "with_redirects";
|
||
let tbl = Hashtbl.create 0x10 in
|
||
let rec go max uri =
|
||
f uri >>= fun (resp, body) ->
|
||
let status_code =
|
||
Cohttp.(Response.status resp |> Code.code_of_status)
|
||
in
|
||
if Cohttp.Code.is_redirection status_code then
|
||
match
|
||
Cohttp.(Response.headers resp |> Header.get_location)
|
||
with
|
||
| Some uri' when Hashtbl.mem tbl uri' || max = 0 ->
|
||
Lwt.return (resp, body)
|
||
| Some uri' ->
|
||
Hashtbl.add tbl uri' ();
|
||
Cohttp_lwt.Body.drain_body body >>= fun () ->
|
||
go (pred max) uri'
|
||
| None -> Lwt.return (resp, body)
|
||
else Lwt.return (resp, body)
|
||
in
|
||
go max uri
|
||
|
||
let get ~ctx:_ ?(headers = []) uri =
|
||
Firebug.console##log (Js.string "Git_Cohttp_console.get()\n");
|
||
let headers = Cohttp.Header.of_list headers in
|
||
let f uri = Cohttp_backend.Client.get ~headers uri in
|
||
with_redirects ~f uri >>= fun (_resp, body) ->
|
||
Cohttp_lwt.Body.to_string body >>= fun body ->
|
||
Lwt.return_ok ((), body)
|
||
|
||
let post ~ctx:_ ?(headers = []) uri body =
|
||
let headers = Cohttp.Header.of_list headers in
|
||
let body = Cohttp_lwt.Body.of_string body in
|
||
let f uri =
|
||
Cohttp_backend.Client.post ~headers ~chunked:false ~body uri
|
||
in
|
||
with_redirects ~f uri >>= fun (_resp, body) ->
|
||
Cohttp_lwt.Body.to_string body >>= fun body ->
|
||
Lwt.return_ok ((), body)
|
||
end
|
||
|
||
module Git_console_http = struct
|
||
open Lwt.Infix
|
||
|
||
let context ctx =
|
||
(* HTTP *)
|
||
let edn = Mimic.make ~name:"af-http-endpoint" in
|
||
let k1 git_af_scheme git_af_hostname git_af_port =
|
||
match git_af_scheme with
|
||
| `HTTP -> Lwt.return_some (git_af_hostname, git_af_port)
|
||
| _ -> Lwt.return_none
|
||
in
|
||
let ctx =
|
||
Mimic.fold edn
|
||
Mimic.Fun.
|
||
[
|
||
req Git_af.git_af_scheme;
|
||
req Git_af.git_af_hostname;
|
||
dft Git_af.git_af_port 80;
|
||
]
|
||
~k:k1 ctx
|
||
in
|
||
|
||
(* HTTPS *)
|
||
let edn = Mimic.make ~name:"af-https-endpoint" in
|
||
let k1 git_af_scheme git_af_hostname git_af_port =
|
||
match git_af_scheme with
|
||
| `HTTPS -> Lwt.return_some (git_af_hostname, git_af_port)
|
||
| _ -> Lwt.return_none
|
||
in
|
||
|
||
let ctx =
|
||
Mimic.fold edn
|
||
Mimic.Fun.
|
||
[
|
||
req Git_af.git_af_scheme;
|
||
req Git_af.git_af_hostname;
|
||
dft Git_af.git_af_port 443;
|
||
]
|
||
~k:k1 ctx
|
||
in
|
||
|
||
ctx
|
||
|
||
module HTTP = struct
|
||
type state =
|
||
| Handshake
|
||
| Get of {
|
||
advertised_refs : string;
|
||
uri : Uri.t;
|
||
headers : (string * string) list;
|
||
ctx : Mimic.ctx;
|
||
}
|
||
| Post of {
|
||
mutable output : string;
|
||
uri : Uri.t;
|
||
headers : (string * string) list;
|
||
ctx : Mimic.ctx;
|
||
}
|
||
| Error
|
||
|
||
type flow = { endpoint : Uri.t; mutable state : state }
|
||
type error = [ `Msg of string ]
|
||
type write_error = [ `Closed | `Msg of string ]
|
||
|
||
let pp_error ppf (`Msg err) = Fmt.string ppf err
|
||
|
||
let pp_write_error ppf = function
|
||
| `Closed -> Fmt.string ppf "Connection closed by peer"
|
||
| `Msg err -> Fmt.string ppf err
|
||
|
||
let write t cs =
|
||
match t.state with
|
||
| Handshake | Get _ ->
|
||
Lwt.return_error (`Msg "Handshake has not been done")
|
||
| Error -> Lwt.return_error (`Msg "Handshake got an error")
|
||
| Post ({ output; _ } as v) ->
|
||
let output = output ^ Cstruct.to_string cs in
|
||
v.output <- output;
|
||
Lwt.return_ok ()
|
||
|
||
let writev t css =
|
||
let rec go = function
|
||
| [] -> Lwt.return_ok ()
|
||
| x :: r -> (
|
||
write t x >>= function
|
||
| Ok () -> go r
|
||
| Error _ as err -> Lwt.return err)
|
||
in
|
||
go css
|
||
|
||
let read t =
|
||
match t.state with
|
||
| Handshake ->
|
||
Lwt.return_error (`Msg "Handshake has not been done")
|
||
| Error -> Lwt.return_error (`Msg "Handshake got an error")
|
||
| Get { advertised_refs; uri; headers; ctx } ->
|
||
t.state <- Post { output = ""; uri; headers; ctx };
|
||
Lwt.return_ok (`Data (Cstruct.of_string advertised_refs))
|
||
| Post { output; uri; headers; ctx } -> (
|
||
Git_af.post ~ctx ~headers uri output >>= function
|
||
| Ok (_resp, contents) ->
|
||
Lwt.return_ok (`Data (Cstruct.of_string contents))
|
||
| Error err ->
|
||
Lwt.return_error (`Msg (Fmt.str "%a" pp_error err)))
|
||
|
||
let close _ = Lwt.return_unit
|
||
|
||
type endpoint = Uri.t
|
||
|
||
let connect endpoint =
|
||
Firebug.console##log
|
||
(Js.string "Git_Console_http.HTTP.connect()\n");
|
||
Lwt.return_ok { endpoint; state = Handshake }
|
||
end
|
||
|
||
let http_endpoint, http_protocol =
|
||
Mimic.register ~name:"http" (module HTTP)
|
||
|
||
let connect (ctx : Mimic.ctx) =
|
||
Firebug.console##log (Js.string "Git_Console_http.connect()\n");
|
||
let module T = (val Mimic.repr http_protocol) in
|
||
let edn = Mimic.make ~name:"http-endpoint" in
|
||
let k0 uri = Lwt.return_some uri in
|
||
let k1 git_transmission git_scheme =
|
||
match (git_transmission, git_scheme) with
|
||
| `HTTP (uri, _), (`HTTP | `HTTPS) -> Lwt.return_some uri
|
||
| _ -> Lwt.return_none
|
||
in
|
||
let k2 git_scheme git_uri git_http_headers =
|
||
match git_scheme with
|
||
| `Git | `SSH | `Scheme _ -> Lwt.return_none
|
||
| `HTTP | `HTTPS ->
|
||
let headers =
|
||
("content-type", "application/x-git-upload-pack-request")
|
||
:: git_http_headers
|
||
in
|
||
let handshake ~uri0 ~uri1 = function
|
||
| T.T flow -> (
|
||
Firebug.console##log
|
||
(Js.string
|
||
(F.str
|
||
"Git_Console_http.connect.k2.handshake \
|
||
uri0='%s' uri1='%s'\n"
|
||
(Uri.to_string uri0) (Uri.to_string uri1)));
|
||
let ctx = context Mimic.empty in
|
||
Git_af.get ~ctx ~headers uri0 >>= function
|
||
| Ok (_resp, advertised_refs) ->
|
||
flow.state <-
|
||
HTTP.Get
|
||
{ advertised_refs; uri = uri1; headers; ctx };
|
||
Lwt.return_unit
|
||
| Error _ ->
|
||
flow.state <- Error;
|
||
Lwt.return_unit)
|
||
| _ -> Lwt.return_unit
|
||
in
|
||
let git_transmission = `HTTP (git_uri, handshake) in
|
||
Lwt.return_some git_transmission
|
||
in
|
||
let ctx =
|
||
Mimic.fold http_endpoint Mimic.Fun.[ req edn ] ~k:k0 ctx
|
||
in
|
||
let ctx =
|
||
Mimic.fold edn
|
||
Mimic.Fun.
|
||
[ req Smart_git.git_transmission; req Smart_git.git_scheme ]
|
||
~k:k1 ctx
|
||
in
|
||
let ctx =
|
||
Mimic.fold Smart_git.git_transmission
|
||
Mimic.Fun.
|
||
[
|
||
req Smart_git.git_scheme;
|
||
req Smart_git.git_uri;
|
||
dft Smart_git.git_http_headers List.[];
|
||
]
|
||
~k:k2 ctx
|
||
in
|
||
Lwt.return ctx
|
||
end
|
||
|
||
module Nav = struct
|
||
open Lwt.Infix
|
||
|
||
module Config = struct
|
||
open Irmin.Backend.Conf
|
||
|
||
let spec = Spec.v "console_js_git"
|
||
|
||
module Key = struct
|
||
let reference : Git.Reference.t Irmin.Type.t =
|
||
let of_string str =
|
||
Git.Reference.of_string str |> Result.get_ok
|
||
in
|
||
let to_string r = Git.Reference.to_string r in
|
||
Irmin.Type.(map string) of_string to_string
|
||
|
||
let head =
|
||
key ~spec ~doc:"The main branch of the Git repository." "head"
|
||
Irmin.Type.(option reference)
|
||
None
|
||
|
||
let bare =
|
||
key ~spec ~doc:"Do not expand the filesystem on the disk."
|
||
"bare" Irmin.Type.bool false
|
||
|
||
let level =
|
||
key ~spec ~doc:"The Zlib compression level." "level"
|
||
Irmin.Type.(option int)
|
||
None
|
||
|
||
let buffers =
|
||
key ~spec ~doc:"The number of 4K pre-allocated buffers."
|
||
"buffers"
|
||
Irmin.Type.(option int)
|
||
None
|
||
end
|
||
|
||
let init ?head ?level ?buffers _root =
|
||
let module C = Irmin.Backend.Conf in
|
||
let config = C.empty spec in
|
||
|
||
let config = C.add config Key.head head in
|
||
let config = C.add config Key.level level in
|
||
let config = C.add config Key.buffers buffers in
|
||
C.verify config
|
||
end
|
||
|
||
module S = struct
|
||
module Schema =
|
||
Irmin_git.Schema.Make (Git.Mem.Store) (Irmin.Contents.String)
|
||
(Irmin_git.Branch.Make (Irmin.Branch.String))
|
||
|
||
module Sync' = struct
|
||
module GitMemSync = Git.Mem.Sync (Git.Mem.Store)
|
||
include GitMemSync
|
||
(* This is where the fetch and push are broken *)
|
||
end
|
||
|
||
module SMaker = Irmin_git.Maker (Git.Mem.Store) (Sync')
|
||
module SMade = SMaker.Make (Schema)
|
||
include SMade
|
||
|
||
type endpoint = Mimic.ctx * Smart_git.Endpoint.t
|
||
|
||
let remote ?(ctx = Mimic.empty) ?headers uri =
|
||
E
|
||
(Firebug.console##log (Js.string "Nav.S.remote()\n");
|
||
let ( ! ) f a b = f b a in
|
||
match Smart_git.Endpoint.of_string uri with
|
||
| Ok edn ->
|
||
let edn =
|
||
Option.fold ~none:edn
|
||
~some:(!Smart_git.Endpoint.with_headers_if_http edn)
|
||
headers
|
||
in
|
||
Firebug.console##log
|
||
(Js.string "Nav.S.remote() = (ctx, edn) \n");
|
||
(ctx, edn)
|
||
| Error (`Msg err) -> Fmt.invalid_arg "remote: %s" err)
|
||
|
||
module Backend = struct
|
||
include Backend
|
||
module R = Remote
|
||
|
||
module Remote = struct
|
||
include R
|
||
|
||
type endpoint = Mimic.ctx * Smart_git.Endpoint.t
|
||
|
||
let ctx e = fst e
|
||
let edn e = snd e
|
||
|
||
let fetch t ?depth endpoint branch =
|
||
Firebug.console##log
|
||
(Js.string "S.Backend.Remote.wrapped_fetch()\n");
|
||
R.fetch t ?depth endpoint branch
|
||
end
|
||
end
|
||
end
|
||
|
||
module Sync = Irmin.Sync.Make (S)
|
||
|
||
type t = S.tree
|
||
type tree = t
|
||
type step = S.step
|
||
type path = step list
|
||
|
||
let init () = S.Repo.v (Irmin_mem.config ()) >>= S.main >>= S.tree
|
||
|
||
let test_populate () : t Lwt.t =
|
||
let add p s t = S.Tree.add t p s in
|
||
add [ "hello" ] "world" (S.Tree.empty ())
|
||
>>= add [ "hello"; "daddy" ] "ily"
|
||
>>= add [ "beep"; "beep" ] "motherfucker"
|
||
|
||
let test_pull () : t Lwt.t =
|
||
(* test_populate ()*)
|
||
Firebug.console##log (Js.string "Nav.test_pull()\n");
|
||
S.Repo.v (Config.init "") >>= fun repo ->
|
||
Firebug.console##log (Js.string "Nav.test_pull(2)\n");
|
||
S.of_branch repo "current" >>= fun t ->
|
||
Firebug.console##log (Js.string "Nav.test_pull(3)\n");
|
||
Git_console_http.connect Mimic.empty >>= fun ctx ->
|
||
Firebug.console##log (Js.string "Nav.test_pull(4)\n");
|
||
let upstream =
|
||
S.remote ~ctx "https://localhost:8080/console/rootstore.git"
|
||
in
|
||
Firebug.console##log (Js.string "Nav.test_pull(5)\n");
|
||
Sync.fetch_exn t upstream >>= fun _ -> S.tree t
|
||
(* irmin/src/irmin/sync.ml: calls S.Remote.Backend.fetch *)
|
||
end
|
||
|
||
module Input = struct
|
||
type button =
|
||
[ `Left | `Middle | `Right | `Scroll of [ `Up | `Down ] ]
|
||
|
||
type special =
|
||
[ `Enter
|
||
| `Escape
|
||
| `Tab
|
||
| `Arrow of [ `Up | `Down | `Left | `Right ]
|
||
| `Function of int
|
||
| `Page of [ `Up | `Down ]
|
||
| `Home
|
||
| `End
|
||
| `Insert
|
||
| `Delete
|
||
| `Backspace
|
||
| `Unknown of string ]
|
||
|
||
(* Type of key code. *)
|
||
type code =
|
||
[ `Uchar of Uchar.t (* A unicode character. *) | special ]
|
||
|
||
(* stolen from lambda-term/src/lTerm_{edit,key}.ml{,i} *)
|
||
let string_of_code = function
|
||
| `Uchar ch ->
|
||
if Uchar.is_char ch then F.str "Char '%c'" (Uchar.to_char ch)
|
||
else F.str "Char 0x%02x" (Uchar.to_int ch)
|
||
| `Enter -> "Enter"
|
||
| `Escape -> "Escape"
|
||
| `Tab -> "Tab"
|
||
| `Arrow `Up -> "Up"
|
||
| `Arrow `Down -> "Down"
|
||
| `Arrow `Left -> "Left"
|
||
| `Arrow `Right -> "Right"
|
||
| `Function i -> F.str "F%d" i
|
||
| `Page `Up -> "Page Up"
|
||
| `Page `Down -> "Page Down"
|
||
| `Home -> "Home"
|
||
| `End -> "End"
|
||
| `Insert -> "Insert"
|
||
| `Delete -> "Delete"
|
||
| `Backspace -> "Backspace"
|
||
| `Unknown s -> String.concat "Unknown " [ "\""; s; "\"" ]
|
||
| _ -> "Code Unknown!"
|
||
|
||
let pp_code ppf v = F.pf ppf "%s" (string_of_code v)
|
||
|
||
type mods = [ `Super | `Meta | `Ctrl | `Shift ] list
|
||
|
||
let pp_mods =
|
||
F.(
|
||
list ~sep:F.sp (fun ppf -> function
|
||
| `Super -> pf ppf "`Super"
|
||
| `Meta -> pf ppf "`Meta"
|
||
| `Ctrl -> pf ppf "`Ctrl"
|
||
| `Shift -> pf ppf "`Shift"))
|
||
|
||
type mouse =
|
||
[ `Press of button | `Drag | `Release ] * (float * float) * mods
|
||
|
||
type paste = [ `Start | `End ]
|
||
type keyaction = [ `Press | `Release | `Repeat ]
|
||
end
|
||
|
||
module Event_js = struct
|
||
open Js_of_ocaml
|
||
|
||
let evt_of_jskey (evt : Dom_html.keyboardEvent Js.t) =
|
||
( (match Js.Optdef.to_option evt##.key with
|
||
| Some s -> (
|
||
match Js.to_string s with
|
||
| "Enter" -> `Enter
|
||
| "Escape" -> `Escape
|
||
| "Tab" -> `Tab
|
||
| "ArrowUp" -> `Arrow `Up
|
||
| "ArrowDown" -> `Arrow `Down
|
||
| "ArrowLeft" -> `Arrow `Left
|
||
| "ArrowRight" -> `Arrow `Right
|
||
| "PageUp" -> `Page `Up
|
||
| "PageDown" -> `Page `Down
|
||
| "Home" -> `Home
|
||
| "End" -> `End
|
||
| "Insert" -> `Insert
|
||
| "Delete" -> `Delete
|
||
| "Backspace" -> `Backspace
|
||
| s -> (
|
||
match Dom_html.Keyboard_key.of_event evt with
|
||
| Some s' -> `Uchar s'
|
||
| None -> `Unknown s))
|
||
| None -> `Unknown "keypress .key is None?"),
|
||
(if Js.to_bool evt##.altKey then [ `Meta ] else [])
|
||
@ (if Js.to_bool evt##.shiftKey then [ `Shift ] else [])
|
||
@ (if Js.to_bool evt##.ctrlKey then [ `Ctrl ] else [])
|
||
@ if Js.to_bool evt##.metaKey then [ `Super ] else [] )
|
||
end
|
||
|
||
open Gg
|
||
|
||
module NVG = struct
|
||
include Graphv_webgl
|
||
|
||
module Color = struct
|
||
include Graphv_webgl.Color
|
||
|
||
let none = Color.transparent
|
||
let rgbf = Color.rgbf
|
||
let gray a = rgbf ~r:a ~g:a ~b:a
|
||
let light = gray 0.8
|
||
let dark = gray 0.2
|
||
|
||
let black = gray 0.
|
||
and red = rgbf ~r:1. ~g:0. ~b:0.
|
||
and green = rgbf ~r:0. ~g:1. ~b:0.
|
||
and yellow = rgbf ~r:1. ~g:1. ~b:0.
|
||
and blue = rgbf ~r:0. ~g:0. ~b:1.
|
||
and magenta = rgbf ~r:1. ~g:0. ~b:1.
|
||
and cyan = rgbf ~r:0. ~g:1. ~b:1.
|
||
and white = rgbf ~r:1. ~g:1. ~b:1.
|
||
and lightblack = gray 0.5
|
||
and lightred = rgbf ~r:1.0 ~g:0.5 ~b:0.5
|
||
and lightgreen = rgbf ~r:0.5 ~g:1.0 ~b:0.5
|
||
and lightyellow = rgbf ~r:1.0 ~g:1.0 ~b:0.5
|
||
and lightblue = rgbf ~r:0.5 ~g:0.5 ~b:1.0
|
||
and lightmagenta = rgbf ~r:1.0 ~g:0.5 ~b:1.0
|
||
and lightcyan = rgbf ~r:0.5 ~g:1.0 ~b:1.0
|
||
and lightwhite = rgbf ~r:1.0 ~g:1.0 ~b:1.0
|
||
|
||
let ( = ) a b = a.r = b.r && a.g = b.g && a.b = b.b && a.a = b.a
|
||
|
||
let ( ++ ) a b =
|
||
{
|
||
r = Float.clamp ~min:0. ~max:1. (a.r +. b.r);
|
||
g = Float.clamp ~min:0. ~max:1. (a.g +. b.g);
|
||
b = Float.clamp ~min:0. ~max:1. (a.b +. b.b);
|
||
a = Float.clamp ~min:0. ~max:1. (a.a +. b.a);
|
||
}
|
||
|
||
let replace ~prev ~next = if next = none then prev else next
|
||
|
||
let pp ppf t : unit =
|
||
if t != none then
|
||
F.(
|
||
fmt "%a" ppf
|
||
(list (fmt "%02X"))
|
||
(List.map
|
||
(fun e -> int_of_float (e *. 255.))
|
||
[ t.r; t.g; t.b; t.a ]))
|
||
else F.fmt "_" ppf
|
||
end
|
||
end
|
||
|
||
module Color = NVG.Color
|
||
|
||
(* from notty.ml *)
|
||
let btw (x : int) a b = a <= x && x <= b
|
||
|
||
module Buffer = struct
|
||
include Stdlib.Buffer
|
||
|
||
let buf = create 1024
|
||
|
||
let mkstring f =
|
||
f buf;
|
||
let res = contents buf in
|
||
reset buf;
|
||
res
|
||
|
||
let add_decimal b = function
|
||
| x when btw x 0 999 ->
|
||
let d1 = x / 100 and d2 = x mod 100 / 10 and d3 = x mod 10 in
|
||
if d1 > 0 then 0x30 + d1 |> Char.unsafe_chr |> add_char b;
|
||
if d1 + d2 > 0 then 0x30 + d2 |> Char.unsafe_chr |> add_char b;
|
||
0x30 + d3 |> Char.unsafe_chr |> add_char b
|
||
| x -> string_of_int x |> add_string b
|
||
|
||
let add_chars b c n =
|
||
for _ = 1 to n do
|
||
add_char b c
|
||
done
|
||
end
|
||
|
||
module String = struct
|
||
include String
|
||
|
||
let sub0cp s i len =
|
||
if i > 0 || len < length s then sub s i len else s
|
||
|
||
let of_chars_rev = function
|
||
| [] -> ""
|
||
| [ c ] -> String.make 1 c
|
||
| cs ->
|
||
let n = List.length cs in
|
||
let rec go bs i =
|
||
Bytes.(
|
||
function
|
||
| [] -> unsafe_to_string bs
|
||
| x :: xs ->
|
||
unsafe_set bs i x;
|
||
go bs (pred i) xs)
|
||
in
|
||
go (Bytes.create n) (n - 1) cs
|
||
end
|
||
|
||
module Text = struct
|
||
include NVG.Text
|
||
|
||
type t = String of string (* | Uchars of Uchar.t list*)
|
||
|
||
let empty = String ""
|
||
|
||
let equal = function
|
||
| String a -> ( function String b -> String.equal a b)
|
||
|
||
let of_string s = String s
|
||
let to_string = function String s -> s
|
||
|
||
let of_uchars ucs =
|
||
of_string @@ Buffer.mkstring
|
||
@@ fun buf -> Array.iter (Buffer.add_utf_8_uchar buf) ucs
|
||
|
||
let replicatec w c = String (String.make (int_of_float w) c)
|
||
|
||
let pp ppf : t -> unit = function
|
||
| String s -> F.(fmt "\"%s\"" ppf s)
|
||
end
|
||
|
||
module Style = struct
|
||
module Font = struct
|
||
type t = {
|
||
size : float option;
|
||
font : [ `Sans | `Serif | `Mono | `None ];
|
||
weight : [ `Bold | `Regular | `Light | `None ];
|
||
italic : [ `Italic | `None ];
|
||
underline : [ `Underline | `None ];
|
||
}
|
||
|
||
let pp ppf =
|
||
F.(
|
||
fmt "%a" ppf
|
||
(record [ field "size" (fun a -> a.size) (option float) ]))
|
||
|
||
let empty =
|
||
{
|
||
size = None;
|
||
font = `None;
|
||
weight = `None;
|
||
italic = `None;
|
||
underline = `None;
|
||
}
|
||
|
||
let underline = { empty with underline = `Underline }
|
||
let size { size; _ } = match size with None -> 20. | Some s -> s
|
||
|
||
let merge a b =
|
||
{
|
||
size =
|
||
(match (a.size, b.size) with
|
||
| None, None -> None
|
||
| Some s, None | None, Some s -> Some s
|
||
| Some s1, Some s2 -> Some (Float.max_num s1 s2));
|
||
font =
|
||
(match (a.font, b.font) with
|
||
| `Sans, _ | _, `Sans -> `Sans
|
||
| `Serif, (`Serif | `Mono | `None) | (`Mono | `None), `Serif
|
||
->
|
||
`Serif
|
||
| `Mono, (`Mono | `None) | `None, `Mono -> `Mono
|
||
| `None, `None -> `None);
|
||
weight =
|
||
(match (a.weight, b.weight) with
|
||
| `Bold, _ | _, `Bold -> `Bold
|
||
| `Regular, (`Regular | `Light | `None)
|
||
| (`Light | `None), `Regular ->
|
||
`Regular
|
||
| `Light, (`Light | `None) | `None, `Light -> `Light
|
||
| `None, `None -> `None);
|
||
italic =
|
||
(match (a.italic, b.italic) with
|
||
| `Italic, _ | _, `Italic -> `Italic
|
||
| _ -> `None);
|
||
underline =
|
||
(match (a.underline, b.underline) with
|
||
| `Underline, _ | _, `Underline -> `Underline
|
||
| _ -> `None);
|
||
}
|
||
|
||
let set vg t =
|
||
(match t.size with
|
||
| Some size -> Text.set_size vg ~size
|
||
| None -> ());
|
||
match t.font with
|
||
| `Sans -> Text.set_font_face vg ~name:"sans"
|
||
| _ -> ()
|
||
|
||
let replace ~prev ~next = merge prev next
|
||
end
|
||
|
||
type t = { fg : Color.t; bg : Color.t; font : Font.t }
|
||
type attr = t
|
||
|
||
let pp ppf a =
|
||
F.(fmt "@[<h>%a/%a@]" ppf Color.pp a.fg Color.pp a.bg)
|
||
|
||
let equal = ( == )
|
||
|
||
let empty =
|
||
{
|
||
fg = Color.transparent;
|
||
bg = Color.transparent;
|
||
font = Font.empty;
|
||
}
|
||
|
||
let dark = { empty with fg = Color.light; bg = Color.dark }
|
||
|
||
let ( ++ ) a1 a2 =
|
||
{
|
||
fg = Color.(a1.fg ++ a2.fg);
|
||
bg = Color.(a1.bg ++ a2.bg);
|
||
font = Font.merge a1.font a2.font;
|
||
}
|
||
|
||
let replace ~prev ~next =
|
||
{
|
||
fg = Color.replace ~prev:prev.fg ~next:next.fg;
|
||
bg = Color.replace ~prev:prev.bg ~next:next.bg;
|
||
font = Font.replace ~prev:prev.font ~next:next.font;
|
||
}
|
||
|
||
let fg ?(t = empty) c = { t with fg = c }
|
||
let bg ?(t = empty) c = { t with bg = c }
|
||
let font ?(t = empty) c = { t with font = c }
|
||
|
||
let merge a b =
|
||
{
|
||
fg = Color.lerp a.fg b.fg ~a:0.5;
|
||
bg = Color.lerp a.bg b.bg ~a:0.5;
|
||
font = Font.merge a.font b.font;
|
||
}
|
||
|
||
let set vg s =
|
||
F.epr "Style.set @.";
|
||
NVG.set_fill_color vg ~color:s.bg;
|
||
NVG.set_stroke_color vg ~color:s.fg;
|
||
Font.set vg s.font
|
||
|
||
let menu_main = bg Color.green ++ fg Color.black
|
||
let menu_sub = bg Color.lightgreen ++ fg Color.black
|
||
|
||
let clickable =
|
||
(bg @@ Color.rgbf ~r:0.2 ~g:0.2 ~b:0.5) ++ (fg @@ Color.light)
|
||
|
||
let cursor = (fg @@ Color.dark) ++ (bg @@ Color.yellow)
|
||
end
|
||
|
||
module Pad = struct
|
||
open Gg
|
||
|
||
type t = { t : size1; b : size1; l : size1; r : size1 }
|
||
|
||
let empty =
|
||
{ t = Size1.zero; b = Size1.zero; l = Size1.zero; r = Size1.zero }
|
||
|
||
let all v = { t = v; b = v; l = v; r = v }
|
||
end
|
||
|
||
module A = Style
|
||
|
||
module I = struct
|
||
open Gg
|
||
|
||
type dim = p2
|
||
|
||
type t =
|
||
| Empty
|
||
| Segment of Text.t (* box2 is +crop/-pad of drawn Text.t *)
|
||
| Attr of (t * A.t)
|
||
| Hcompose of (t * t)
|
||
| Vcompose of (t * t)
|
||
| Zcompose of (t * t)
|
||
| Hcrop of (t * float * float)
|
||
| Vcrop of (t * float * float)
|
||
| Void of dim
|
||
|
||
let p2_max p1 p2 : p2 =
|
||
V2.(v (Float.max (x p1) (x p2)) (Float.max (y p1) (y p2)))
|
||
[@@inline]
|
||
|
||
let bounds_segment vg p : Text.t -> NVG.Text.bounds = function
|
||
| String s ->
|
||
let open NVG.Text in
|
||
let { ascender; _ } = NVG.Text.metrics vg in
|
||
bounds vg ~x:(V2.x p) ~y:(V2.y p +. ascender) s
|
||
|
||
let rec size vg p = function
|
||
| Empty -> V2.zero
|
||
| Segment s ->
|
||
let NVG.Text.{ box = { ymax; ymin; _ }; advance } =
|
||
bounds_segment vg p s
|
||
in
|
||
V2.v advance (ymax -. ymin)
|
||
| Attr (t, _a) -> size vg p t
|
||
| Hcompose (t1, t2) ->
|
||
let p1 = size vg p t1 in
|
||
let p2 = size vg V2.(p + v (x p1) 0.) t2 in
|
||
V2.(v (x p1 +. x p2) (Float.max (y p1) (y p2)))
|
||
| Vcompose (t1, t2) ->
|
||
let p1 = size vg p t1 in
|
||
let p2 = size vg V2.(p + v 0. (y p1)) t2 in
|
||
V2.(v (Float.max (x p1) (x p2)) (y p1 +. y p2))
|
||
| Zcompose (t1, t2) -> p2_max (size vg p t1) (size vg p t2)
|
||
| Hcrop (t, left, right) ->
|
||
(* positive values are crop, negative is pad *)
|
||
V2.(size vg (p - v left 0.) t - v right 0.)
|
||
| Vcrop (t, top, bottom) ->
|
||
V2.(size vg (p - v 0. top) t - v 0. bottom)
|
||
| Void p' -> V2.(p + p')
|
||
|
||
let empty = Empty
|
||
let void w h = Void (P2.v w h)
|
||
|
||
let attr a = function
|
||
| Attr (t, a0) -> Attr (t, A.(replace ~prev:a0 ~next:a))
|
||
| t -> Attr (t, a)
|
||
|
||
let ( <|> ) t1 t2 =
|
||
match (t1, t2) with
|
||
| _, Empty -> t1
|
||
| Empty, _ -> t2
|
||
| _ -> Hcompose (t1, t2)
|
||
|
||
let ( <-> ) t1 t2 =
|
||
match (t1, t2) with
|
||
| _, Empty -> t1
|
||
| Empty, _ -> t2
|
||
| _ -> Vcompose (t1, t2)
|
||
|
||
let ( </> ) t1 t2 =
|
||
match (t1, t2) with
|
||
| _, Empty -> t1
|
||
| Empty, _ -> t2
|
||
| _ -> Zcompose (t1, t2)
|
||
|
||
(* crop is positive value, pad is negative *)
|
||
|
||
let hcrop left right img =
|
||
(* Log.debug (fun m -> m "Hcrop (%f, %f)" left right); *)
|
||
Hcrop (img, left, right)
|
||
|
||
let vcrop top bottom img =
|
||
(* Log.debug (fun m -> m "Vcrop (%f, %f)" top bottom); *)
|
||
Vcrop (img, top, bottom)
|
||
|
||
let crop ?(l = 0.) ?(r = 0.) ?(t = 0.) ?(b = 0.) img =
|
||
let img = if l <> 0. || r <> 0. then hcrop l r img else img in
|
||
if t <> 0. || b <> 0. then vcrop t b img else img
|
||
|
||
let hpad left right img = hcrop (-.left) (-.right) img
|
||
let vpad top bottom img = vcrop (-.top) (-.bottom) img
|
||
|
||
let pad ?(l = 0.) ?(r = 0.) ?(t = 0.) ?(b = 0.) img =
|
||
crop ~l:(-.l) ~r:(-.r) ~t:(-.t) ~b:(-.b) img
|
||
|
||
let rec concatm z ( @ ) xs =
|
||
let rec accum ( @ ) = function
|
||
| ([] | [ _ ]) as xs -> xs
|
||
| a :: b :: xs -> (a @ b) :: accum ( @ ) xs
|
||
in
|
||
match xs with
|
||
| [] -> z
|
||
| [ x ] -> x
|
||
| xs -> concatm z ( @ ) (accum ( @ ) xs)
|
||
|
||
let hcat = concatm empty ( <|> )
|
||
let vcat = concatm empty ( <-> )
|
||
let zcat xs = List.fold_right ( </> ) xs empty
|
||
|
||
let text attr tx =
|
||
match attr with Some a -> Attr (Segment tx, a) | _ -> Segment tx
|
||
|
||
let string ?attr s = text attr (Text.of_string s)
|
||
let uchars ?attr a = text attr (Text.of_uchars a)
|
||
|
||
let rec linspcm z ( @ ) x n f =
|
||
match n with
|
||
| 0. -> z
|
||
| 1. -> f x
|
||
| _ ->
|
||
let m = n /. 2. in
|
||
linspcm z ( @ ) x m f @ linspcm z ( @ ) (x +. m) (n -. m) f
|
||
|
||
let tabulate m n f =
|
||
let m = max m 0. and n = max n 0. in
|
||
linspcm empty ( <-> ) 0. n (fun y ->
|
||
linspcm empty ( <|> ) 0. m (fun x -> f x y))
|
||
|
||
let chars ctor ?attr c w h =
|
||
let w = max 0. w and h = max 0. h in
|
||
if w < 1. || h < 1. then void w h
|
||
else
|
||
let line = text attr (ctor w c) in
|
||
tabulate 1. h (fun _ _ -> line)
|
||
|
||
let char = chars Text.replicatec
|
||
(* let uchar = chars Text.replicateu *)
|
||
|
||
(* module Fmt = struct
|
||
open Format
|
||
|
||
type stag += Attr of A.t
|
||
|
||
let push r x = r := x :: !r
|
||
let pop r = r := match !r with _ :: xs -> xs | _ -> []
|
||
let top_a r = match !r with a :: _ -> a | _ -> A.empty
|
||
|
||
let create () =
|
||
let img, line, attr = (ref empty, ref empty, ref []) in
|
||
let fmt =
|
||
formatter_of_out_functions
|
||
{
|
||
out_flush =
|
||
(fun () ->
|
||
img := !img <-> !line;
|
||
line := empty;
|
||
attr := []);
|
||
out_newline =
|
||
(fun () ->
|
||
img := !img <-> !line;
|
||
line := void 0. 1.);
|
||
out_string =
|
||
(fun s i n ->
|
||
line :=
|
||
!line
|
||
<|> string ~attr:(top_a attr)
|
||
String.(sub0cp s i n))
|
||
(* Not entirely clear; either or both could be void: *);
|
||
out_spaces =
|
||
(fun w ->
|
||
line := !line <|> char ~attr:(top_a attr) ' ' w 1);
|
||
out_indent =
|
||
(fun w ->
|
||
line := !line <|> char ~attr:(top_a attr) ' ' w 1);
|
||
}
|
||
in
|
||
pp_set_formatter_stag_functions fmt
|
||
{
|
||
(pp_get_formatter_stag_functions fmt ()) with
|
||
mark_open_stag =
|
||
(function
|
||
| Attr a ->
|
||
push attr A.(top_a attr ++ a);
|
||
""
|
||
| _ -> "");
|
||
mark_close_stag =
|
||
(fun _ ->
|
||
pop attr;
|
||
"");
|
||
};
|
||
pp_set_mark_tags fmt true;
|
||
( fmt,
|
||
fun () ->
|
||
let i = !img in
|
||
img := empty;
|
||
line := empty;
|
||
attr := [];
|
||
i )
|
||
|
||
let ppf, reset = create ()
|
||
|
||
let kstrf ?(attr = A.empty) ?(w = 1000000) k format =
|
||
let m = ref 0 in
|
||
let f1 _ () =
|
||
m := pp_get_margin ppf ();
|
||
pp_set_margin ppf w;
|
||
pp_open_stag ppf (Attr attr)
|
||
and k _ =
|
||
pp_print_flush ppf ();
|
||
pp_set_margin ppf !m;
|
||
reset () |> k
|
||
in
|
||
kfprintf k ppf ("%a" ^^ format) f1 ()
|
||
|
||
let strf ?attr ?w format = kstrf ?attr ?w (fun i -> i) format
|
||
|
||
let attr attr f fmt x =
|
||
pp_open_stag fmt (Attr attr);
|
||
f fmt x;
|
||
pp_close_stag fmt ()
|
||
end
|
||
|
||
let kstrf, strf, pp_attr = Fmt.(kstrf, strf, attr) *)
|
||
|
||
module Draw = struct
|
||
type attr = Style.t
|
||
type p = P2.t
|
||
type d = [ `X | `Y | `Z ]
|
||
|
||
let str_of_box b =
|
||
Printf.sprintf "(ox:%0.1f oy:%0.1f ex%0.1f ey%0.1f)" (Box2.ox b)
|
||
(Box2.oy b) (Box2.maxx b) (Box2.maxy b)
|
||
|
||
let fill_box vg color b =
|
||
let module Path = NVG.Path in
|
||
let open NVG in
|
||
NVG.save vg;
|
||
Path.begin_ vg;
|
||
Path.rect vg ~x:(Box2.ox b) ~y:(Box2.oy b) ~w:(Box2.w b)
|
||
~h:(Box2.h b);
|
||
set_fill_color vg ~color;
|
||
fill vg;
|
||
NVG.restore vg;
|
||
(* Log.debug (fun m -> m "fill_box: %a" Box2.pp b); *)
|
||
Box2.size b
|
||
|
||
let path_box vg color ?(width = 0.) b =
|
||
let module Path = NVG.Path in
|
||
Path.begin_ vg;
|
||
Path.rect vg ~x:(Box2.ox b) ~y:(Box2.oy b) ~w:(Box2.w b)
|
||
~h:(Box2.h b);
|
||
if width != 0. then NVG.set_stroke_width vg ~width;
|
||
NVG.set_stroke_color vg ~color;
|
||
NVG.stroke vg;
|
||
Box2.size b
|
||
|
||
let vcat d a b =
|
||
match d with
|
||
| `X ->
|
||
V2.v (V2.x a +. V2.x b) (Float.max_num (V2.y a) (V2.y b))
|
||
| `Y -> V2.v (Float.max_num (V2.x a) (V2.x b)) (V2.y a +. V2.y b)
|
||
| `Z ->
|
||
V2.v
|
||
(Float.max_num (V2.x a) (V2.x b))
|
||
(Float.max_num (V2.y a) (V2.y b))
|
||
|
||
let rec pp ?(attr = A.empty) ppf : t -> unit =
|
||
let open F in
|
||
let compose = pair (parens (pp ~attr)) (parens (pp ~attr)) in
|
||
function
|
||
| Empty -> fmt "Empty" ppf
|
||
| Segment v -> fmt "@[<h>Segment %a@]" ppf Text.pp v
|
||
| Attr (t, a) ->
|
||
fmt "@[<h>Attr %a@]" ppf
|
||
(pair ~sep:comma A.pp
|
||
(pp ~attr:A.(replace ~prev:attr ~next:a)))
|
||
(A.(replace ~prev:attr ~next:a), t)
|
||
| Hcompose a -> fmt "Hcat %a" ppf compose a
|
||
| Vcompose a -> fmt "Vcat %a" ppf compose a
|
||
| Zcompose a -> fmt "Zcat %a" ppf compose a
|
||
| Hcrop (t, left, right) ->
|
||
fmt "Hcrop (%.1f,%.1f,%a)" ppf left right (pp ~attr) t
|
||
| Vcrop (t, top, bottom) ->
|
||
fmt "Vcrop (%.1f,%.1f,%a)" ppf top bottom (pp ~attr) t
|
||
| Void dim -> fmt "Void %a" ppf (parens V2.pp) dim
|
||
|
||
let segment_kern_cache = ref (Box2.zero, "")
|
||
|
||
let rec segment vg p : Text.t -> P2.t = function
|
||
| String s ->
|
||
(* Log.debug (fun m -> m "I.Draw.segment p=%a %s" Gg.V2.pp p s); *)
|
||
(* let p' =
|
||
let cache_p, cache_s = !segment_kern_cache in
|
||
(* tries to get the kerning right across segments *)
|
||
if V2.(equal (Box2.max cache_p) p) then
|
||
V2.(
|
||
Box2.o cache_p
|
||
+ v
|
||
((bounds_segment vg (Box2.o cache_p)
|
||
(Text.of_string (cache_s ^ s)))
|
||
.advance
|
||
-. (bounds_segment vg p (Text.of_string s)).advance
|
||
)
|
||
0.)
|
||
else p
|
||
in *)
|
||
let metrics = NVG.Text.metrics vg in
|
||
NVG.Text.text vg ~x:(V2.x p)
|
||
~y:(V2.y p +. metrics.ascender)
|
||
s;
|
||
|
||
let sz =
|
||
V2.v (bounds_segment vg p (Text.of_string s)).advance
|
||
metrics.line_height
|
||
in
|
||
segment_kern_cache := (Box2.(v p sz), s);
|
||
sz
|
||
|
||
and node vg attr p n : p2 =
|
||
(* returns the *size* of the drawn area not the max coordinates anymore *)
|
||
let b' =
|
||
match n with
|
||
| Empty | Void _ -> p
|
||
| Segment text -> segment vg p text
|
||
| Attr (i, a) ->
|
||
let a0 = A.(replace ~prev:attr ~next:a) in
|
||
if
|
||
(A.(a.bg) != A.(attr.bg))
|
||
&& A.(a0.bg) != NVG.Color.transparent
|
||
then fill_box vg a0.bg (Box2.v p (size vg p i)) |> ignore;
|
||
if A.(attr.fg) != a.fg then (
|
||
NVG.set_fill_color vg ~color:Style.(a0.fg);
|
||
NVG.set_stroke_color vg ~color:Style.(a0.fg));
|
||
node vg a0 p i
|
||
| Hcompose (i1, i2) ->
|
||
let p1 = node vg attr p i1 in
|
||
let p2 = node vg attr V2.(p + v (V2.x p1) 0.) i2 in
|
||
V2.(v (x p1 +. x p2) (Float.max (y p1) (y p2)))
|
||
| Vcompose (i1, i2) ->
|
||
let p1 = node vg attr p i1 in
|
||
let p2 = node vg attr V2.(p + v 0. (V2.y p1)) i2 in
|
||
V2.(v (Float.max (x p1) (x p2)) (y p1 +. y p2))
|
||
| Zcompose (i1, i2) ->
|
||
let p1 = node vg attr p i1 in
|
||
let p2 = node vg attr p i2 in
|
||
p2_max p1 p2
|
||
| Hcrop (i, left, right) ->
|
||
let p0 = size vg p i in
|
||
NVG.save vg;
|
||
NVG.Scissor.scissor vg ~x:(V2.x p) ~y:(V2.y p)
|
||
~w:(V2.x p0 -. right)
|
||
~h:(V2.y p0);
|
||
let p1 = node vg attr V2.(p - v left 0.) i in
|
||
NVG.restore vg;
|
||
V2.(p1 - v (left +. right) 0.)
|
||
| Vcrop (i, top, bottom) ->
|
||
let p0 = size vg p i in
|
||
NVG.save vg;
|
||
NVG.Scissor.scissor vg ~x:(V2.x p) ~y:(V2.y p)
|
||
~w:(V2.x p0)
|
||
~h:(V2.y p0 -. bottom);
|
||
let p1 = node vg attr V2.(p - v 0. top) i in
|
||
NVG.restore vg;
|
||
V2.(p1 - v 0. (top +. bottom))
|
||
in
|
||
|
||
(* ignore
|
||
(path_box vg.vg
|
||
(NVG.Color.rgbaf ~r:1. ~g:0. ~b:0. ~a:0.2)
|
||
(Box2.of_pts b b')); *)
|
||
b'
|
||
end
|
||
end
|
||
|
||
module Nottui = struct
|
||
let maxi x y : int = if x > y then x else y
|
||
let mini x y : int = if x < y then x else y
|
||
|
||
module Focus : sig
|
||
type var = int Lwd.var
|
||
type handle
|
||
|
||
val make : unit -> handle
|
||
val request : handle -> unit
|
||
val request_var : var -> unit
|
||
val release : handle -> unit
|
||
|
||
type status = Empty | Handle of int * var | Conflict of int
|
||
|
||
val empty : status
|
||
|
||
(*val is_empty : status -> bool*)
|
||
val status : handle -> status Lwd.t
|
||
val var : handle -> var
|
||
val has_focus : status -> bool
|
||
val merge : status -> status -> status
|
||
val pp_var : Format.formatter -> var -> unit
|
||
val pp_status : Format.formatter -> status -> unit
|
||
end = struct
|
||
type var = int Lwd.var
|
||
type status = Empty | Handle of int * var | Conflict of int
|
||
type handle = var * status Lwd.t
|
||
|
||
let make () =
|
||
let v = Lwd.var 0 in
|
||
(v, Lwd.map ~f:(fun i -> Handle (i, v)) (Lwd.get v))
|
||
|
||
let empty : status = Empty
|
||
let var (h : handle) : var = fst h
|
||
let status (h : handle) : status Lwd.t = snd h
|
||
|
||
let has_focus = function
|
||
| Empty -> false
|
||
| Handle (i, _) | Conflict i -> i > 0
|
||
|
||
let clock = ref 0
|
||
|
||
let request_var (v : var) =
|
||
incr clock;
|
||
Log.debug (fun m ->
|
||
m "Focus.request_var v=%d clock=%d" (Lwd.peek v) !clock);
|
||
Lwd.set v !clock
|
||
|
||
let request ((v, _) : handle) = request_var v
|
||
|
||
let release ((v, _) : handle) =
|
||
Log.debug (fun m ->
|
||
m "Focus.release v=%d clock=%d" (Lwd.peek v) !clock);
|
||
incr clock;
|
||
Lwd.set v 0
|
||
|
||
let merge s1 s2 : status =
|
||
match (s1, s2) with
|
||
| (Empty | Handle (0, _)), x | x, (Empty | Handle (0, _)) -> x
|
||
| Handle (i1, _), Handle (i2, _) when i1 = i2 -> s1
|
||
| (Handle (i1, _) | Conflict i1), Conflict i2 when i1 < i2 -> s2
|
||
| (Handle (i1, _) | Conflict i1), Handle (i2, _) when i1 < i2 ->
|
||
Conflict i2
|
||
| Conflict _, (Handle (_, _) | Conflict _) -> s1
|
||
| Handle (i1, _), (Handle (_, _) | Conflict _) -> Conflict i1
|
||
|
||
let pp_var ppf v = F.pf ppf "%d" (Lwd.peek v)
|
||
|
||
let pp_status ppf = function
|
||
| Empty -> F.pf ppf "Empty"
|
||
| Handle (i, v) -> F.pf ppf "Handle (%d, %a)" i pp_var v
|
||
| Conflict i -> F.pf ppf "Conflict %d" i
|
||
end
|
||
|
||
module Gravity : sig
|
||
type direction = [ `Negative | `Neutral | `Positive ]
|
||
|
||
val pp_direction : Format.formatter -> direction -> unit
|
||
|
||
type t
|
||
|
||
val pp : Format.formatter -> t -> unit
|
||
val make : h:direction -> v:direction -> t
|
||
val default : t
|
||
val h : t -> direction
|
||
val v : t -> direction
|
||
|
||
type t2
|
||
|
||
val pair : t -> t -> t2
|
||
val p1 : t2 -> t
|
||
val p2 : t2 -> t
|
||
end = struct
|
||
type direction = [ `Negative | `Neutral | `Positive ]
|
||
type t = { h : direction; v : direction }
|
||
|
||
let default = { h = `Neutral; v = `Neutral }
|
||
let make ~h ~v = { h; v }
|
||
let h x = x.h
|
||
let v x = x.v
|
||
|
||
let pp_direction ppf dir =
|
||
let text =
|
||
match dir with
|
||
| `Negative -> "`Negative"
|
||
| `Neutral -> "`Neutral"
|
||
| `Positive -> "`Positive"
|
||
in
|
||
Format.pp_print_string ppf text
|
||
|
||
let pp ppf g =
|
||
Format.fprintf ppf "{ h = %a; v = %a }" pp_direction (h g)
|
||
pp_direction (v g)
|
||
|
||
type t2 = t * t
|
||
|
||
let pair t1 t2 = (t1, t2)
|
||
let p1 (t, _) = t
|
||
let p2 (_, t) = t
|
||
end
|
||
|
||
type gravity = Gravity.t
|
||
|
||
module Interval : sig
|
||
type t
|
||
|
||
val make : float -> float -> t
|
||
val shift : t -> float -> t
|
||
val fst : t -> float
|
||
val snd : t -> float
|
||
val size : t -> float
|
||
val zero : t
|
||
end = struct
|
||
type t = float * float
|
||
|
||
let make x y = (x, y)
|
||
let shift (x, y) d = (x +. d, y +. d)
|
||
let fst (x, _) = x
|
||
let size (x, y) = y -. x
|
||
let snd (_, y) = y
|
||
let zero = (0., 0.)
|
||
end
|
||
|
||
module Ui = struct
|
||
type may_handle = [ `Unhandled | `Handled ]
|
||
|
||
let may_handle (type a) (v : a option) (f : a -> may_handle) :
|
||
may_handle =
|
||
match v with Some v' -> f v' | None -> `Unhandled
|
||
|
||
let pp_may_handle ppf = function
|
||
| `Unhandled -> F.pf ppf "`Unhandled"
|
||
| `Handled -> F.pf ppf "`Handled"
|
||
|
||
type mouse_handler =
|
||
x:float ->
|
||
y:float ->
|
||
Input.button ->
|
||
[ `Unhandled
|
||
| `Handled
|
||
| `Grab of
|
||
(x:float -> y:float -> unit) * (x:float -> y:float -> unit)
|
||
]
|
||
|
||
type semantic_key =
|
||
[ (* Clipboard *)
|
||
`Copy
|
||
| `Paste
|
||
| (* Focus management *)
|
||
`Focus of
|
||
[ `Next | `Prev | `Left | `Right | `Up | `Down ] ]
|
||
|
||
type key =
|
||
[ Input.special | `Uchar of Uchar.t | semantic_key ]
|
||
* Input.mods
|
||
|
||
let pp_key =
|
||
F.(
|
||
pair ~sep:F.sp
|
||
(fun ppf v ->
|
||
match v with
|
||
| `Copy -> pf ppf "`Copy"
|
||
| `Paste -> pf ppf "`Paste"
|
||
| `Focus v ->
|
||
pf ppf "`Focus %s"
|
||
(match v with
|
||
| `Next -> "`Next"
|
||
| `Prev -> "`Prev"
|
||
| `Left -> "`Left"
|
||
| `Right -> "`Right"
|
||
| `Up -> "`Up"
|
||
| `Down -> "`Down")
|
||
| a -> pf ppf "%a" Input.pp_code a)
|
||
Input.pp_mods)
|
||
|
||
type mouse = Input.mouse
|
||
|
||
type event =
|
||
[ `Key of key | `Mouse of mouse | `Paste of Input.paste ]
|
||
|
||
type layout_spec = {
|
||
w : float;
|
||
h : float;
|
||
sw : float;
|
||
sh : float;
|
||
}
|
||
|
||
let pp_float ppf = F.fmt "%.1f" ppf
|
||
|
||
type flags = int
|
||
|
||
let flags_none = 0
|
||
let flag_transient_sensor = 1
|
||
let flag_permanent_sensor = 2
|
||
|
||
type size_sensor = w:float -> h:float -> unit
|
||
|
||
type frame_sensor =
|
||
x:float -> y:float -> w:float -> h:float -> unit -> unit
|
||
|
||
type image = I.t
|
||
type cache = { vx : Interval.t; vy : Interval.t; image : image }
|
||
|
||
type 'a desc =
|
||
| Atom of image
|
||
| Size_sensor of 'a * size_sensor
|
||
| Transient_sensor of 'a * frame_sensor
|
||
| Permanent_sensor of 'a * frame_sensor
|
||
| Resize of 'a * float option * float option * Gravity.t2
|
||
| Mouse_handler of 'a * mouse_handler
|
||
| Focus_area of 'a * (key -> may_handle)
|
||
| Shift_area of 'a * float * float
|
||
| Event_filter of
|
||
'a * ([ `Key of key | `Mouse of mouse ] -> may_handle)
|
||
| X of 'a * 'a
|
||
| Y of 'a * 'a
|
||
| Z of 'a * 'a
|
||
|
||
type t = {
|
||
mutable w : float;
|
||
mutable h : float;
|
||
sw : float;
|
||
sh : float;
|
||
mutable desc : t desc;
|
||
focus : Focus.status;
|
||
mutable flags : flags;
|
||
mutable sensor_cache : (float * float * float * float) option;
|
||
mutable cache : cache;
|
||
}
|
||
|
||
let layout_spec (t : t) : layout_spec =
|
||
{ w = t.w; h = t.h; sw = t.sw; sh = t.sh }
|
||
|
||
let layout_width t = t.w
|
||
let layout_stretch_width t = t.sw
|
||
let layout_height t = t.h
|
||
let layout_stretch_height t = t.sh
|
||
|
||
let pp_layout_spec ppf { w; h; sw; sh; _ } =
|
||
let p = pp_float in
|
||
Format.fprintf ppf "{w=%a;@ h=%a;@ sw=%a;@ sh=%a}" p w p h p sw
|
||
p sh
|
||
|
||
let cache : cache =
|
||
{ vx = Interval.zero; vy = Interval.zero; image = I.empty }
|
||
|
||
let empty : t =
|
||
{
|
||
w = 0.;
|
||
h = 0.;
|
||
sw = 0.;
|
||
sh = 0.;
|
||
flags = flags_none;
|
||
focus = Focus.empty;
|
||
desc = Atom I.empty;
|
||
sensor_cache = None;
|
||
cache;
|
||
}
|
||
|
||
let atom img : t =
|
||
{
|
||
w = 0.;
|
||
h = 0.;
|
||
sw = 0.;
|
||
sh = 0.;
|
||
focus = Focus.empty;
|
||
flags = flags_none;
|
||
desc = Atom img;
|
||
sensor_cache = None;
|
||
cache;
|
||
}
|
||
|
||
(* let space_1_0 = atom (I.void 1 0)
|
||
let space_0_1 = atom (I.void 0 1)
|
||
let space_1_1 = atom (I.void 1 1)
|
||
|
||
let space x y =
|
||
match (x, y) with
|
||
| 0, 0 -> empty
|
||
| 1, 0 -> space_1_0
|
||
| 0, 1 -> space_0_1
|
||
| 1, 1 -> space_1_1
|
||
| _ -> atom (I.void x y) *)
|
||
|
||
let space x y = atom (I.void x y)
|
||
let mouse_area f t : t = { t with desc = Mouse_handler (t, f) }
|
||
|
||
let keyboard_area ?focus f (t : t) : t =
|
||
let focus =
|
||
match focus with
|
||
| None -> t.focus
|
||
| Some focus -> Focus.merge focus t.focus
|
||
in
|
||
{ t with desc = Focus_area (t, f); focus }
|
||
|
||
let shift_area x y t : t = { t with desc = Shift_area (t, x, y) }
|
||
|
||
let size_sensor handler t : t =
|
||
{ t with desc = Size_sensor (t, handler) }
|
||
|
||
let transient_sensor frame_sensor t =
|
||
{
|
||
t with
|
||
desc = Transient_sensor (t, frame_sensor);
|
||
flags = t.flags lor flag_transient_sensor;
|
||
}
|
||
|
||
let permanent_sensor frame_sensor t =
|
||
{
|
||
t with
|
||
desc = Permanent_sensor (t, frame_sensor);
|
||
flags = t.flags lor flag_permanent_sensor;
|
||
}
|
||
|
||
let prepare_gravity = function
|
||
| None, None -> Gravity.(pair default default)
|
||
| Some g, None | None, Some g -> Gravity.(pair g g)
|
||
| Some pad, Some crop -> Gravity.(pair pad crop)
|
||
|
||
let resize ?w ?h ?sw ?sh ?pad ?crop ?(attr = A.empty) (t : t) : t
|
||
=
|
||
let _ = attr in
|
||
let wo, ho = (w, h) in
|
||
let g = prepare_gravity (pad, crop) in
|
||
match ((w, t.w), (h, t.h), (sw, t.sw), (sh, t.sh)) with
|
||
| ( (Some w, _ | _, w),
|
||
(Some h, _ | _, h),
|
||
(Some sw, _ | _, sw),
|
||
(Some sh, _ | _, sh) ) ->
|
||
{ t with w; h; sw; sh; desc = Resize (t, wo, ho, g) }
|
||
|
||
let resize_to (l : layout_spec) ?pad ?crop ?(attr = A.empty) t : t
|
||
=
|
||
let _ = attr in
|
||
let g = prepare_gravity (pad, crop) in
|
||
{ t with desc = Resize (t, Some l.w, Some l.h, g) }
|
||
|
||
let event_filter ?focus f (t : t) : t =
|
||
let focus =
|
||
match focus with None -> t.focus | Some focus -> focus
|
||
in
|
||
{ t with desc = Event_filter (t, f); focus }
|
||
|
||
let join_x (a : t) (b : t) =
|
||
{
|
||
empty with
|
||
w = a.w +. b.w;
|
||
h = max a.h b.h;
|
||
sw = a.sw +. b.sw;
|
||
sh = max a.sh b.sh;
|
||
flags = a.flags lor b.flags;
|
||
focus = Focus.merge a.focus b.focus;
|
||
desc = X (a, b);
|
||
}
|
||
|
||
let join_y (a : t) (b : t) =
|
||
{
|
||
empty with
|
||
w = max a.w b.w;
|
||
h = a.h +. b.h;
|
||
sw = max a.sw b.sw;
|
||
sh = a.sh +. b.sh;
|
||
flags = a.flags lor b.flags;
|
||
focus = Focus.merge a.focus b.focus;
|
||
desc = Y (a, b);
|
||
}
|
||
|
||
let join_z (a : t) (b : t) =
|
||
{
|
||
empty with
|
||
w = max a.w b.w;
|
||
h = max a.h b.h;
|
||
sw = max a.sw b.sw;
|
||
sh = max a.sh b.sh;
|
||
flags = a.flags lor b.flags;
|
||
focus = Focus.merge a.focus b.focus;
|
||
desc = Z (a, b);
|
||
}
|
||
|
||
let pack_x = (empty, join_x)
|
||
let pack_y = (empty, join_y)
|
||
let pack_z = (empty, join_z)
|
||
let hcat xs = Lwd_utils.reduce pack_x xs
|
||
let vcat xs = Lwd_utils.reduce pack_y xs
|
||
let zcat xs = Lwd_utils.reduce pack_z xs
|
||
let has_focus t = Focus.has_focus t.focus
|
||
|
||
let rec pp ppf t =
|
||
F.pf ppf "@[<hov>focus=%a %a@]" Focus.pp_status t.focus pp_desc
|
||
t.desc
|
||
|
||
and pp_desc ppf = function
|
||
| Atom a ->
|
||
Format.fprintf ppf "Atom @[<hov>(%a)@]"
|
||
(I.Draw.pp ?attr:None) a
|
||
| Size_sensor (desc, _) ->
|
||
Format.fprintf ppf "Size_sensor (%a, _)" pp desc
|
||
| Transient_sensor (desc, _) ->
|
||
Format.fprintf ppf "Transient_sensor (%a,@ _)" pp desc
|
||
| Permanent_sensor (desc, _) ->
|
||
Format.fprintf ppf "Permanent_sensor (%a,@ _)" pp desc
|
||
| Resize (desc, x, y, _gravity) ->
|
||
F.pf ppf "Resize (%a, %a,@ _,@ %a)" (F.option pp_float) x
|
||
(F.option pp_float) y
|
||
(* Gravity.pp (Gravity.p1 gravity) Gravity.pp
|
||
(Gravity.p2 gravity) *)
|
||
pp desc
|
||
| Mouse_handler (n, _) ->
|
||
Format.fprintf ppf "%a" (*"Mouse (%a,@ _)"*) pp n
|
||
| Focus_area (n, _) -> Format.fprintf ppf "Focus (%a,@ _)" pp n
|
||
| Shift_area (n, x, y) ->
|
||
Format.fprintf ppf "Shift (%.0f,%.0f,%a)" x y pp n
|
||
| Event_filter (n, _) ->
|
||
Format.fprintf ppf "Event (%a,@ _)" pp n
|
||
| X (a, b) -> Format.fprintf ppf "X (@[%a,@ %a@])" pp a pp b
|
||
| Y (a, b) -> Format.fprintf ppf "Y (@[%a,@ %a@])" pp a pp b
|
||
| Z (a, b) -> Format.fprintf ppf "Z (@[%a,@ %a@])" pp a pp b
|
||
|
||
let iter f ui =
|
||
match ui.desc with
|
||
| Atom _ -> ()
|
||
| Size_sensor (u, _)
|
||
| Transient_sensor (u, _)
|
||
| Permanent_sensor (u, _)
|
||
| Resize (u, _, _, _)
|
||
| Mouse_handler (u, _)
|
||
| Focus_area (u, _)
|
||
| Shift_area (u, _, _)
|
||
| Event_filter (u, _) ->
|
||
f u
|
||
| X (u1, u2) | Y (u1, u2) | Z (u1, u2) ->
|
||
f u1;
|
||
f u2
|
||
end
|
||
|
||
type ui = Ui.t
|
||
|
||
module Renderer = struct
|
||
open Ui
|
||
|
||
type size = Gg.p2
|
||
|
||
type grab_function =
|
||
(x:float -> y:float -> unit) * (x:float -> y:float -> unit)
|
||
|
||
type t = {
|
||
vg : NVG.t;
|
||
mutable size : size;
|
||
mutable view : ui;
|
||
mutable mouse_grab : grab_function option;
|
||
}
|
||
|
||
let make vg () =
|
||
{ vg; mouse_grab = None; size = P2.o; view = Ui.empty }
|
||
|
||
let size t = t.size
|
||
|
||
let solve_focus (ui : ui) i =
|
||
let rec aux ui =
|
||
match ui.focus with
|
||
| Focus.Empty | Focus.Handle (0, _) -> ()
|
||
| Focus.Handle (i', _) when i = i' -> ()
|
||
| Focus.Handle (_, v) -> Lwd.set v 0
|
||
| Focus.Conflict _ -> iter aux ui
|
||
in
|
||
aux ui
|
||
|
||
let split ~a ~sa ~b ~sb total =
|
||
let stretch = sa +. sb in
|
||
let flex = total -. a -. b in
|
||
let a', b' =
|
||
if stretch > 0. && flex > 0. then
|
||
let ratio =
|
||
if sa > sb then flex *. sa /. stretch
|
||
else flex -. (flex *. sb /. stretch)
|
||
in
|
||
(a +. ratio, b +. flex -. ratio)
|
||
else (a, b)
|
||
in
|
||
(* Log.debug (fun m ->
|
||
m
|
||
"split: a=%.1f sa=%.1f b=%.1f sb=%.1f total=%.1f (%.1f, \
|
||
%.1f)"
|
||
a sa b sb total a' b'); *)
|
||
(a', b')
|
||
|
||
let pack ~fixed ~stretch total g1 g2 =
|
||
let v1, v2 =
|
||
let flex = total -. fixed in
|
||
if stretch > 0. && flex > 0. then (0., total)
|
||
else
|
||
let gravity = if flex >= 0. then g1 else g2 in
|
||
match gravity with
|
||
| `Negative -> (0., fixed)
|
||
| `Neutral -> (flex /. 2., fixed)
|
||
| `Positive -> (flex, fixed)
|
||
in
|
||
(* Log.debug (fun m ->
|
||
m "pack fixed=%.1f stretch=%.1f total=%.1f (%.1f, %.1f)"
|
||
fixed stretch total v1 v2); *)
|
||
(v1, v2)
|
||
|
||
let has_transient_sensor flags =
|
||
flags land flag_transient_sensor <> 0
|
||
|
||
let has_permanent_sensor flags =
|
||
flags land flag_permanent_sensor <> 0
|
||
|
||
let rec update_sensors ox oy sw sh (ui : ui) =
|
||
if
|
||
has_transient_sensor ui.flags
|
||
|| has_permanent_sensor ui.flags
|
||
&&
|
||
match ui.sensor_cache with
|
||
| None -> false
|
||
| Some (ox', oy', sw', sh') ->
|
||
ox = ox' && oy = oy' && sw = sw' && sh = sh'
|
||
then (
|
||
ui.flags <- ui.flags land lnot flag_transient_sensor;
|
||
if has_permanent_sensor ui.flags then
|
||
ui.sensor_cache <- Some (ox, oy, sw, sh);
|
||
match ui.desc with
|
||
| Atom _ -> ()
|
||
| Size_sensor (t, _)
|
||
| Mouse_handler (t, _)
|
||
| Focus_area (t, _)
|
||
| Event_filter (t, _) ->
|
||
update_sensors ox oy sw sh t
|
||
| Transient_sensor (t, sensor) ->
|
||
ui.desc <- t.desc;
|
||
let sensor = sensor ~x:ox ~y:oy ~w:sw ~h:sh in
|
||
update_sensors ox oy sw sh t;
|
||
sensor ()
|
||
| Permanent_sensor (t, sensor) ->
|
||
let sensor = sensor ~x:ox ~y:oy ~w:sw ~h:sh in
|
||
update_sensors ox oy sw sh t;
|
||
sensor ()
|
||
| Resize (t, x, y, g) ->
|
||
let open Gravity in
|
||
let dx, rw =
|
||
pack
|
||
~fixed:(Option.value x ~default:t.w)
|
||
~stretch:t.sw sw
|
||
(h (p1 g))
|
||
(h (p2 g))
|
||
in
|
||
let dy, rh =
|
||
pack
|
||
~fixed:(Option.value y ~default:t.h)
|
||
~stretch:t.sh sh
|
||
(v (p1 g))
|
||
(v (p2 g))
|
||
in
|
||
update_sensors (ox +. dx) (oy +. dy) rw rh t
|
||
| Shift_area (t, sx, sy) ->
|
||
update_sensors (ox -. sx) (oy -. sy) sw sh t
|
||
| X (a, b) ->
|
||
let aw, bw = split ~a:a.w ~sa:a.sw ~b:b.w ~sb:b.sw sw in
|
||
update_sensors ox oy aw sh a;
|
||
update_sensors (ox +. aw) oy bw sh b
|
||
| Y (a, b) ->
|
||
let ah, bh = split ~a:a.h ~sa:a.sh ~b:b.h ~sb:b.sh sh in
|
||
update_sensors ox oy sw ah a;
|
||
update_sensors ox (oy +. ah) sw bh b
|
||
| Z (a, b) ->
|
||
update_sensors ox oy sw sh a;
|
||
update_sensors ox oy sw sh b)
|
||
|
||
let update_focus ui =
|
||
match ui.focus with
|
||
| Focus.Empty | Focus.Handle _ -> ()
|
||
| Focus.Conflict i -> solve_focus ui i
|
||
|
||
let rec t_size_desc_of_t vg (size : box2) (ui : Ui.t desc) =
|
||
match ui with
|
||
| Atom _ as a -> a
|
||
| Size_sensor (t, v) -> Size_sensor (t_size_of_t vg size t, v)
|
||
| Mouse_handler (t, v) ->
|
||
Mouse_handler (t_size_of_t vg size t, v)
|
||
| Focus_area (t, v) -> Focus_area (t_size_of_t vg size t, v)
|
||
| Event_filter (t, v) -> Event_filter (t_size_of_t vg size t, v)
|
||
| Transient_sensor (t, v) ->
|
||
Transient_sensor (t_size_of_t vg size t, v)
|
||
| Permanent_sensor (t, v) ->
|
||
Permanent_sensor (t_size_of_t vg size t, v)
|
||
| Resize (t, w, h, g2) ->
|
||
Resize
|
||
( t_size_of_t vg
|
||
(Box2.v (Box2.o size)
|
||
(V2.v
|
||
(Option.value w ~default:(Box2.w size))
|
||
(Option.value h ~default:(Box2.h size))))
|
||
t,
|
||
w,
|
||
h,
|
||
g2 )
|
||
| Shift_area (t, sx, sy) ->
|
||
Shift_area
|
||
( t_size_of_t vg
|
||
(Box2.of_pts
|
||
V2.(Box2.o size - of_tuple (sx, sy))
|
||
(Box2.max size))
|
||
t,
|
||
sx,
|
||
sy )
|
||
| X (a, b) ->
|
||
let a' = t_size_of_t vg size a in
|
||
let b' =
|
||
t_size_of_t vg
|
||
(Box2.of_pts
|
||
V2.(v (Box2.minx size +. a'.w) (Box2.miny size))
|
||
(Box2.max size))
|
||
b
|
||
in
|
||
X (a', b')
|
||
| Y (a, b) ->
|
||
let a' = t_size_of_t vg size a in
|
||
let b' =
|
||
t_size_of_t vg
|
||
(Box2.of_pts
|
||
V2.(v (Box2.minx size) (Box2.miny size +. a'.h))
|
||
(Box2.max size))
|
||
b
|
||
in
|
||
Y (a', b')
|
||
| Z (a, b) -> Z (t_size_of_t vg size a, t_size_of_t vg size b)
|
||
|
||
and t_size_of_t vg (size : box2) (ui : Ui.t) : ui =
|
||
let desc = t_size_desc_of_t vg size ui.desc in
|
||
let w, h =
|
||
match desc with
|
||
| Atom i -> V2.to_tuple (I.size vg (Box2.o size) i)
|
||
| Size_sensor (t, _)
|
||
| Mouse_handler (t, _)
|
||
| Focus_area (t, _)
|
||
| Event_filter (t, _)
|
||
| Transient_sensor (t, _)
|
||
| Permanent_sensor (t, _) ->
|
||
(t.w, t.h)
|
||
| Resize (t, w, h, _) ->
|
||
(Option.value w ~default:t.w, Option.value h ~default:t.h)
|
||
| Shift_area (t, x, y) -> (t.w +. x, t.h +. y)
|
||
| X (a, b) -> (a.w +. b.w, max a.h b.h)
|
||
| Y (a, b) -> (max a.w b.w, a.h +. b.h)
|
||
| Z (a, b) -> (max a.w b.w, max a.h b.h)
|
||
in
|
||
{ ui with w; h; desc; sensor_cache = None; cache }
|
||
|
||
let update t size (ui : Ui.t) =
|
||
t.size <- size;
|
||
t.view <- t_size_of_t t.vg (Box2.v V2.zero size) ui;
|
||
update_sensors 0. 0. (P2.x size) (P2.y size) t.view;
|
||
update_focus t.view
|
||
|
||
let dispatch_mouse st x y btn w h t =
|
||
let handle ox oy f =
|
||
match f ~x:(x -. ox) ~y:(y -. oy) btn with
|
||
| `Unhandled -> false
|
||
| `Handled -> true
|
||
| `Grab f ->
|
||
st.mouse_grab <- Some f;
|
||
true
|
||
in
|
||
let rec aux ox oy sw sh t =
|
||
match t.desc with
|
||
| Atom _ -> false
|
||
| X (a, b) ->
|
||
let aw, bw = split ~a:a.w ~sa:a.sw ~b:b.w ~sb:b.sw sw in
|
||
if x -. ox < aw then aux ox oy aw sh a
|
||
else aux (ox +. aw) oy bw sh b
|
||
| Y (a, b) ->
|
||
let ah, bh = split ~a:a.h ~sa:a.sh ~b:b.h ~sb:b.sh sh in
|
||
if y -. oy < ah then aux ox oy sw ah a
|
||
else aux ox (oy +. ah) sw bh b
|
||
| Z (a, b) -> aux ox oy sw sh b || aux ox oy sw sh a
|
||
| Mouse_handler (t, f) ->
|
||
let _offsetx, rw =
|
||
pack ~fixed:t.w ~stretch:t.sw sw `Negative `Negative
|
||
and _offsety, rh =
|
||
pack ~fixed:t.h ~stretch:t.sh sh `Negative `Negative
|
||
in
|
||
assert (_offsetx = 0. && _offsety = 0.);
|
||
(x -. ox >= 0.
|
||
&& x -. ox <= rw
|
||
&& y -. oy >= 0.
|
||
&& y -. oy <= rh)
|
||
&& (aux ox oy sw sh t || handle ox oy f)
|
||
| Size_sensor (desc, _)
|
||
| Transient_sensor (desc, _)
|
||
| Permanent_sensor (desc, _)
|
||
| Focus_area (desc, _) ->
|
||
aux ox oy sw sh desc
|
||
| Shift_area (desc, sx, sy) ->
|
||
aux (ox -. sx) (oy -. sy) sw sh desc
|
||
| Resize (t, _, _, g) ->
|
||
let open Gravity in
|
||
let dx, rw =
|
||
pack ~fixed:t.w ~stretch:t.sw sw (h (p1 g)) (h (p2 g))
|
||
in
|
||
let dy, rh =
|
||
pack ~fixed:t.h ~stretch:t.sh sh (v (p1 g)) (v (p2 g))
|
||
in
|
||
aux (ox +. dx) (oy +. dy) rw rh t
|
||
| Event_filter (n, f) -> (
|
||
match f (`Mouse (`Press btn, (x, y), [])) with
|
||
| `Handled -> true
|
||
| `Unhandled -> aux ox oy sw sh n)
|
||
in
|
||
aux 0. 0. w h t
|
||
|
||
let release_grab st x y =
|
||
match st.mouse_grab with
|
||
| None -> ()
|
||
| Some (_, release) ->
|
||
st.mouse_grab <- None;
|
||
release ~x ~y
|
||
|
||
let resize_canvas vg rw rh image =
|
||
let w, h = V2.to_tuple @@ I.size vg V2.zero image in
|
||
(* Log.debug (fun m ->
|
||
m "resize_canvas: w=%.1f rw=%.1f h=%.1f rh=%.1f" w rw h rh); *)
|
||
if w <> rw || h <> rh then I.pad ~r:(rw -. w) ~b:(rh -. h) image
|
||
else image
|
||
|
||
let resize_canvas2 vg ox oy rw rh image =
|
||
let w, h = V2.to_tuple @@ I.size vg V2.zero image in
|
||
I.pad ~l:ox ~t:oy ~r:(rw -. w -. ox) ~b:(rh -. h -. oy) image
|
||
|
||
let same_size vg w h image =
|
||
V2.(equal (of_tuple (w, h)) (I.size vg V2.zero image))
|
||
|
||
let dispatch_mouse t (event, (x, y), _mods) =
|
||
if
|
||
match event with
|
||
| `Press btn ->
|
||
release_grab t x y;
|
||
let w, h = V2.to_tuple t.size in
|
||
dispatch_mouse t x y btn w h t.view
|
||
| `Drag -> (
|
||
match t.mouse_grab with
|
||
| None -> false
|
||
| Some (drag, _) ->
|
||
drag ~x ~y;
|
||
true)
|
||
| `Release ->
|
||
release_grab t x y;
|
||
true
|
||
then `Handled
|
||
else `Unhandled
|
||
|
||
let rec render_node vg (vx1 : size1) (vy1 : size1) (vx2 : size1)
|
||
(vy2 : size1) (sw : size1) (sh : size1) (t : ui) : cache =
|
||
(* Log.debug (fun m ->
|
||
m
|
||
"render_node vx1=%.0f@ vy1=%.0f@ vx2=%.0f@ vy2=%.0f@ \
|
||
sw=%.0f@ sh=%.0f@ @[%a@]"
|
||
vx1 vy1 vx2 vy2 sw sh pp t); *)
|
||
if
|
||
let cache = t.cache in
|
||
vx1 >= Interval.fst cache.vx
|
||
&& vy1 >= Interval.fst cache.vy
|
||
&& vx2 <= Interval.snd cache.vx
|
||
&& vy2 <= Interval.snd cache.vy
|
||
then t.cache
|
||
else if vx2 < 0. || vy2 < 0. || sw < vx1 || sh < vy1 then
|
||
{
|
||
vx = Interval.make vx1 vx2;
|
||
vy = Interval.make vy1 vy2;
|
||
image = I.void sw sh;
|
||
}
|
||
else
|
||
let cache =
|
||
match t.desc with
|
||
| Atom image ->
|
||
let image =
|
||
if Focus.has_focus t.focus then (
|
||
Log.debug (fun m -> m "render_node Atom has_focus");
|
||
I.attr A.clickable image)
|
||
else image
|
||
in
|
||
{
|
||
vx = Interval.make 0. sw;
|
||
vy = Interval.make 0. sh;
|
||
image = resize_canvas vg sw sh image;
|
||
}
|
||
| Size_sensor (desc, handler) ->
|
||
handler ~w:sw ~h:sh;
|
||
render_node vg vx1 vy1 vx2 vy2 sw sh desc
|
||
| Transient_sensor (desc, _) | Permanent_sensor (desc, _) ->
|
||
render_node vg vx1 vy1 vx2 vy2 sw sh desc
|
||
| Focus_area (desc, _) | Mouse_handler (desc, _) ->
|
||
render_node vg vx1 vy1 vx2 vy2 sw sh desc
|
||
| Shift_area (t', sx, sy) ->
|
||
let cache =
|
||
render_node vg (vx1 +. sx) (vy1 +. sy) (vx2 +. sx)
|
||
(vy2 +. sy) (sx +. sw) (sy +. sh) t'
|
||
in
|
||
let vx = Interval.make vx1 vx2
|
||
and vy = Interval.make vy1 vy2 in
|
||
let image =
|
||
resize_canvas vg sw sh
|
||
(I.crop ~l:sx ~t:sy cache.image)
|
||
in
|
||
{ vx; vy; image }
|
||
| X (a, b) ->
|
||
let aw, bw = split ~a:a.w ~sa:a.sw ~b:b.w ~sb:b.sw sw in
|
||
let ca = render_node vg vx1 vy1 vx2 vy2 aw sh a in
|
||
let cb =
|
||
render_node vg (vx1 -. aw) vy1 (vx2 -. aw) vy2 bw sh b
|
||
in
|
||
let vx =
|
||
Interval.make
|
||
(max (Interval.fst ca.vx)
|
||
(Interval.fst cb.vx +. aw))
|
||
(min (Interval.snd ca.vx)
|
||
(Interval.snd cb.vx +. aw))
|
||
and vy =
|
||
Interval.make
|
||
(max (Interval.fst ca.vy) (Interval.fst cb.vy))
|
||
(min (Interval.snd ca.vy) (Interval.snd cb.vy))
|
||
and image =
|
||
resize_canvas vg sw sh I.(ca.image <|> cb.image)
|
||
in
|
||
{ vx; vy; image }
|
||
| Y (a, b) ->
|
||
let ah, bh = split ~a:a.h ~sa:a.sh ~b:b.h ~sb:b.sh sh in
|
||
let ca = render_node vg vx1 vy1 vx2 vy2 sw ah a in
|
||
let cb =
|
||
render_node vg vx1 (vy1 -. ah) vx2 (vy2 -. ah) sw bh b
|
||
in
|
||
let vx =
|
||
Interval.make
|
||
(max (Interval.fst ca.vx) (Interval.fst cb.vx))
|
||
(min (Interval.snd ca.vx) (Interval.snd cb.vx))
|
||
and vy =
|
||
Interval.make
|
||
(max (Interval.fst ca.vy)
|
||
(Interval.fst cb.vy +. ah))
|
||
(min (Interval.snd ca.vy)
|
||
(Interval.snd cb.vy +. ah))
|
||
and image =
|
||
resize_canvas vg sw sh (I.( <-> ) ca.image cb.image)
|
||
in
|
||
{ vx; vy; image }
|
||
| Z (a, b) ->
|
||
let ca = render_node vg vx1 vy1 vx2 vy2 sw sh a in
|
||
let cb = render_node vg vx1 vy1 vx2 vy2 sw sh b in
|
||
let vx =
|
||
Interval.make
|
||
(max (Interval.fst ca.vx) (Interval.fst cb.vx))
|
||
(min (Interval.snd ca.vx) (Interval.snd cb.vx))
|
||
and vy =
|
||
Interval.make
|
||
(max (Interval.fst ca.vy) (Interval.fst cb.vy))
|
||
(min (Interval.snd ca.vy) (Interval.snd cb.vy))
|
||
and image =
|
||
resize_canvas vg sw sh (I.( </> ) cb.image ca.image)
|
||
in
|
||
{ vx; vy; image }
|
||
| Resize (t, _, _, g) ->
|
||
let open Gravity in
|
||
let dx, rw =
|
||
pack ~fixed:t.w ~stretch:t.sw sw (h (p1 g)) (h (p2 g))
|
||
in
|
||
let dy, rh =
|
||
pack ~fixed:t.h ~stretch:t.sh sh (v (p1 g)) (v (p2 g))
|
||
in
|
||
let c =
|
||
render_node vg (vx1 -. dx) (vy1 -. dy) (vx2 -. dx)
|
||
(vy2 -. dy) rw rh t
|
||
in
|
||
let image = resize_canvas2 vg dx dy sw sh c.image in
|
||
let vx = Interval.shift c.vx dx in
|
||
let vy = Interval.shift c.vy dy in
|
||
{ vx; vy; image }
|
||
| Event_filter (t, _f) ->
|
||
render_node vg vx1 vy1 vx2 vy2 sw sh t
|
||
in
|
||
t.cache <- cache;
|
||
cache
|
||
|
||
let image vg { size; view; _ } =
|
||
let w, h = V2.to_tuple size in
|
||
Log.debug (fun m -> m "Renderer.image view=%a " Ui.pp view);
|
||
(render_node vg 0. 0. w h w h view).image
|
||
|
||
let dispatch_raw_key st key =
|
||
let rec iter (st : ui list) : [> `Unhandled ] =
|
||
match st with
|
||
| [] -> `Unhandled
|
||
| ui :: tl -> (
|
||
match ui.desc with
|
||
| Atom _ -> iter tl
|
||
| X (a, b) | Y (a, b) | Z (a, b) ->
|
||
(* Try left/top most branch first *)
|
||
let st' =
|
||
if Focus.has_focus b.focus then b :: tl
|
||
else a :: b :: tl
|
||
in
|
||
iter st'
|
||
| Focus_area (t, f) -> (
|
||
match iter [ t ] with
|
||
| `Handled -> `Handled
|
||
| `Unhandled -> (
|
||
match f key with
|
||
| `Handled -> `Handled
|
||
| `Unhandled -> iter tl))
|
||
| Mouse_handler (t, _)
|
||
| Size_sensor (t, _)
|
||
| Transient_sensor (t, _)
|
||
| Permanent_sensor (t, _)
|
||
| Shift_area (t, _, _)
|
||
| Resize (t, _, _, _) ->
|
||
iter (t :: tl)
|
||
| Event_filter (t, f) -> (
|
||
match f (`Key key) with
|
||
| `Unhandled -> iter (t :: tl)
|
||
| `Handled -> `Handled))
|
||
in
|
||
iter [ st.view ]
|
||
|
||
exception Acquired_focus
|
||
|
||
let grab_focus ui =
|
||
let rec aux ui =
|
||
match ui.focus with
|
||
| Focus.Empty -> ()
|
||
| Focus.Handle (_, v) ->
|
||
Focus.request_var v;
|
||
raise Acquired_focus
|
||
| Focus.Conflict _ -> iter aux ui
|
||
in
|
||
Log.debug (fun m -> m "grab_focus");
|
||
try
|
||
aux ui;
|
||
false
|
||
with Acquired_focus ->
|
||
Log.warn (fun m -> m "grab_focus Acquired_focus -> true");
|
||
true
|
||
|
||
let rec dispatch_focus t dir =
|
||
match t.desc with
|
||
| Atom _ -> false
|
||
| Mouse_handler (t, _)
|
||
| Size_sensor (t, _)
|
||
| Transient_sensor (t, _)
|
||
| Permanent_sensor (t, _)
|
||
| Shift_area (t, _, _)
|
||
| Resize (t, _, _, _)
|
||
| Event_filter (t, _) ->
|
||
dispatch_focus t dir
|
||
| Focus_area (t', _) ->
|
||
if Focus.has_focus t'.focus then (
|
||
Log.debug (fun m ->
|
||
m
|
||
"dispatch_focus Focus_area has_focus t'.focus = \
|
||
true");
|
||
|
||
dispatch_focus t' dir || grab_focus t)
|
||
else if Focus.has_focus t.focus then false
|
||
else grab_focus t
|
||
| X (a, b) -> (
|
||
if Focus.has_focus a.focus then
|
||
dispatch_focus a dir
|
||
||
|
||
match dir with
|
||
| `Next | `Right -> dispatch_focus b dir
|
||
| _ -> false
|
||
else if Focus.has_focus b.focus then
|
||
dispatch_focus b dir
|
||
||
|
||
match dir with
|
||
| `Prev | `Left -> dispatch_focus a dir
|
||
| _ -> false
|
||
else
|
||
match dir with
|
||
| `Prev | `Left | `Up ->
|
||
dispatch_focus b dir || dispatch_focus a dir
|
||
| `Next | `Down | `Right ->
|
||
dispatch_focus a dir || dispatch_focus b dir)
|
||
| Y (a, b) -> (
|
||
if Focus.has_focus a.focus then
|
||
dispatch_focus a dir
|
||
||
|
||
match dir with
|
||
| `Next | `Down -> dispatch_focus b dir
|
||
| _ -> false
|
||
else if Focus.has_focus b.focus then
|
||
dispatch_focus b dir
|
||
||
|
||
match dir with
|
||
| `Prev | `Up -> dispatch_focus a dir
|
||
| _ -> false
|
||
else
|
||
match dir with
|
||
| `Prev | `Up ->
|
||
dispatch_focus b dir || dispatch_focus a dir
|
||
| `Next | `Left | `Down | `Right ->
|
||
dispatch_focus a dir || dispatch_focus b dir)
|
||
| Z (a, b) ->
|
||
if Focus.has_focus a.focus then dispatch_focus a dir
|
||
else dispatch_focus b dir || dispatch_focus a dir
|
||
|
||
let rec dispatch_key st key =
|
||
match (dispatch_raw_key st key, key) with
|
||
| `Handled, _ -> `Handled
|
||
| `Unhandled, (`Arrow dir, []) ->
|
||
let dir : [ `Down | `Left | `Right | `Up ] :>
|
||
[ `Down | `Left | `Right | `Up | `Next | `Prev ] =
|
||
dir
|
||
in
|
||
dispatch_key st (`Focus dir, [ `Meta ])
|
||
| `Unhandled, (`Tab, mods) ->
|
||
let dir = if List.mem `Shift mods then `Prev else `Next in
|
||
dispatch_key st (`Focus dir, mods)
|
||
| `Unhandled, (`Focus dir, _) ->
|
||
let r = dispatch_focus st.view dir in
|
||
(if r then Log.debug else Log.warn) (fun m ->
|
||
m "Renderer.dispatch_focus key:%a -> %b" pp_key key r);
|
||
if r then `Handled else `Unhandled
|
||
| `Unhandled, _ -> `Unhandled
|
||
|
||
let dispatch_event t = function
|
||
| `Key key -> dispatch_key t key
|
||
| `Mouse mouse -> dispatch_mouse t mouse
|
||
| `Paste _ -> `Unhandled
|
||
end
|
||
end
|
||
|
||
module Nottui_lwt = struct
|
||
open Nottui
|
||
|
||
(* stolen from let-def/lwd/lib/nottui/nottui.ml* etc... *)
|
||
let copy_until quit ~f input =
|
||
let quit = Lwt.map (fun () -> None) quit in
|
||
let stream, push = Lwt_stream.create () in
|
||
let rec aux () =
|
||
Lwt.bind (Lwt.choose [ quit; Lwt_stream.peek input ])
|
||
@@ fun result ->
|
||
match result with
|
||
| None ->
|
||
push None;
|
||
Lwt.return_unit
|
||
| Some x ->
|
||
push (Some (f x));
|
||
Lwt.bind (Lwt_stream.junk input) aux
|
||
in
|
||
Lwt.async aux;
|
||
stream
|
||
|
||
let render vg ?quit ~size events doc =
|
||
let renderer = Renderer.make vg () in
|
||
let refresh_stream, push_refresh = Lwt_stream.create () in
|
||
let root =
|
||
Lwd.observe
|
||
~on_invalidate:(fun _ ->
|
||
if not (Lwt_stream.is_closed refresh_stream) then
|
||
push_refresh (Some ()))
|
||
doc
|
||
in
|
||
let quit, do_quit =
|
||
match quit with
|
||
| Some quit -> (quit, None)
|
||
| None ->
|
||
let t, u = Lwt.wait () in
|
||
(t, Some u)
|
||
in
|
||
let events =
|
||
copy_until quit events ~f:(fun e ->
|
||
(e
|
||
: [ `Resize of _ | Ui.event ]
|
||
:> [ `Resize of _ | Ui.event ]))
|
||
in
|
||
let size = ref size in
|
||
let result, push = Lwt_stream.create () in
|
||
let refresh () =
|
||
(* FIXME This should use [Lwd.sample] with proper release management. *)
|
||
let ui = Lwd.quick_sample root in
|
||
Renderer.update renderer !size ui;
|
||
push (Some (Renderer.image vg renderer))
|
||
in
|
||
refresh ();
|
||
let process_event e =
|
||
match e with
|
||
| `Key (`Uchar c, [ `Meta ]) as event
|
||
when Uchar.(equal c (of_char 'q')) -> (
|
||
match do_quit with
|
||
| Some u -> Lwt.wakeup u ()
|
||
| None -> ignore (Renderer.dispatch_event renderer event))
|
||
| #Ui.event as event -> (
|
||
match Renderer.dispatch_event renderer event with
|
||
| `Handled -> ()
|
||
| `Unhandled ->
|
||
(* Log.warn (fun m ->
|
||
m
|
||
"Nottui_lwt.render process_event #Ui.event -> \
|
||
`Unhandled") *)
|
||
())
|
||
| `Resize size' ->
|
||
size := size';
|
||
refresh ()
|
||
in
|
||
Lwt.async (fun () ->
|
||
Lwt.finalize
|
||
(fun () -> Lwt_stream.iter process_event events)
|
||
(fun () ->
|
||
push None;
|
||
Lwt.return_unit));
|
||
Lwt.async (fun () -> Lwt_stream.iter refresh refresh_stream);
|
||
result
|
||
|
||
(* let run ?quit doc =
|
||
let term = Term.create () in
|
||
let images =
|
||
render ?quit ~size:(Term.size term) (Term.events term) doc
|
||
in
|
||
Lwt.finalize
|
||
(fun () -> Lwt_stream.iter_s (Term.image term) images)
|
||
(fun () -> Term.release term) *)
|
||
end
|
||
|
||
module Nottui_widgets = struct
|
||
open Nottui
|
||
|
||
let string ?(attr = A.empty) str = Ui.atom (I.string ~attr str)
|
||
let int ?attr x = string ?attr (string_of_int x)
|
||
let bool ?attr x = string ?attr (string_of_bool x)
|
||
let float_ ?attr x = string ?attr (string_of_float x)
|
||
let printf ?attr fmt = Printf.ksprintf (string ?attr) fmt
|
||
let fmt ?attr fmt = Format.kasprintf (string ?attr) fmt
|
||
|
||
let kprintf k ?attr fmt =
|
||
Printf.ksprintf (fun str -> k (string ?attr str)) fmt
|
||
|
||
let kfmt k ?attr fmt =
|
||
Format.kasprintf (fun str -> k (string ?attr str)) fmt
|
||
|
||
type window_manager = {
|
||
overlays : ui Lwd.t Lwd_table.t;
|
||
view : ui Lwd.t;
|
||
}
|
||
|
||
let window_manager base =
|
||
let overlays = Lwd_table.make () in
|
||
let composition =
|
||
Lwd.join
|
||
(Lwd_table.reduce (Lwd_utils.lift_monoid Ui.pack_z) overlays)
|
||
in
|
||
let view =
|
||
Lwd.map2 base composition ~f:(fun base composite ->
|
||
Ui.join_z base
|
||
(Ui.resize_to (Ui.layout_spec base) composite))
|
||
in
|
||
{ overlays; view }
|
||
|
||
let window_manager_view wm = wm.view
|
||
let window_manager_overlays wm = wm.overlays
|
||
|
||
let menu_overlay wm g ?(dx = 0.) ?(dy = 0.) body around =
|
||
let sensor ~x ~y ~w ~h () =
|
||
let row = Lwd_table.append (window_manager_overlays wm) in
|
||
let h_pad =
|
||
match Gravity.h g with
|
||
| `Negative -> Ui.space (x +. dx) 0.
|
||
| `Neutral -> Ui.space (x +. dx +. (w /. 2.)) 0.
|
||
| `Positive -> Ui.space (x +. dx +. w) 0.
|
||
in
|
||
let v_pad =
|
||
match Gravity.v g with
|
||
| `Negative -> Ui.space 0. (y +. dy)
|
||
| `Neutral -> Ui.space 0. (y +. dy +. (h /. 2.))
|
||
| `Positive -> Ui.space 0. (y +. dy +. h)
|
||
in
|
||
let view =
|
||
Lwd.map body ~f:(fun body ->
|
||
let body =
|
||
let pad = Ui.space 1. 0. in
|
||
Ui.join_x pad (Ui.join_x body pad)
|
||
in
|
||
let bg =
|
||
Ui.resize_to (Ui.layout_spec body)
|
||
~attr:A.(bg Color.lightgreen)
|
||
Ui.empty
|
||
in
|
||
let catchall =
|
||
Ui.mouse_area
|
||
(fun ~x:_ ~y:_ -> function
|
||
| `Left ->
|
||
Lwd_table.remove row;
|
||
`Handled
|
||
| _ -> `Handled)
|
||
(Ui.resize ~sw:1. ~sh:1. Ui.empty)
|
||
in
|
||
Ui.join_z catchall @@ Ui.join_y v_pad @@ Ui.join_x h_pad
|
||
@@ Ui.join_z bg body)
|
||
in
|
||
Lwd_table.set row view
|
||
in
|
||
Ui.transient_sensor sensor around
|
||
|
||
(* let menu_overlay wm ?(dx = 0) ?(dy = 0) handler body =
|
||
let refresh = Lwd.var () in
|
||
let clicked = ref false in
|
||
Lwd.map' body @@ fun body ->
|
||
let body =
|
||
let pad = Ui.space 1 0 in
|
||
Ui.join_x pad (Ui.join_x body pad)
|
||
in
|
||
let bg =
|
||
Ui.resize_to (Ui.layout_spec body)
|
||
~bg:A.(bg lightgreen)
|
||
Ui.empty
|
||
in
|
||
let click_handler ~x:_ ~y:_ = function
|
||
| `Left ->
|
||
clicked := true;
|
||
Lwd.set refresh ();
|
||
`Handled
|
||
| _ -> `Unhandled
|
||
in
|
||
let ui = Ui.mouse_area click_handler (Ui.join_z bg body) in
|
||
if !clicked then (
|
||
clicked := false;
|
||
let sensor ~x ~y ~w:_ ~h () =
|
||
let row = Lwd_table.append (window_manager_overlays wm) in
|
||
let h_pad = Ui.space (x + dx) 0 in
|
||
let v_pad = Ui.space 0 (y + h + dy) in
|
||
let view =
|
||
Lwd.map' (handler ()) @@ fun view ->
|
||
let catchall =
|
||
Ui.mouse_area
|
||
(fun ~x:_ ~y:_ -> function
|
||
| `Left ->
|
||
Lwd_table.remove row;
|
||
`Handled
|
||
| _ -> `Handled)
|
||
(Ui.resize ~sw:1 ~sh:1 Ui.empty)
|
||
in
|
||
Ui.join_z catchall (Ui.join_y v_pad (Ui.join_x h_pad view))
|
||
in
|
||
Lwd_table.set row view
|
||
in
|
||
Ui.transient_sensor sensor ui)
|
||
else ui *)
|
||
|
||
let scroll_step = 7.
|
||
|
||
let scroll_area ?(offset = (0., 0.)) t =
|
||
let offset = Lwd.var offset in
|
||
let scroll d_x d_y =
|
||
let s_x, s_y = Lwd.peek offset in
|
||
let s_x = max 0. (s_x +. d_x) in
|
||
let s_y = max 0. (s_y +. d_y) in
|
||
Lwd.set offset (s_x, s_y);
|
||
`Handled
|
||
in
|
||
let focus_handler = function
|
||
| `Arrow `Left, [] -> scroll (-.scroll_step) 0.
|
||
| `Arrow `Right, [] -> scroll (+.scroll_step) 0.
|
||
| `Arrow `Up, [] -> scroll 0. (-.scroll_step)
|
||
| `Arrow `Down, [] -> scroll 0. (+.scroll_step)
|
||
| `Page `Up, [] -> scroll 0. (-.scroll_step *. 8.)
|
||
| `Page `Down, [] -> scroll 0. (+.scroll_step *. 8.)
|
||
| _ -> `Unhandled
|
||
in
|
||
(* let scroll_handler ~x:_ ~y:_ = function
|
||
| `Scroll `Up -> scroll 0. (-.scroll_step)
|
||
| `Scroll `Down -> scroll 0. (+.scroll_step)
|
||
| _ -> `Unhandled
|
||
in *)
|
||
Lwd.map2 t (Lwd.get offset) ~f:(fun t (s_x, s_y) ->
|
||
t |> Ui.shift_area s_x s_y
|
||
(*|> Ui.mouse_area scroll_handler*)
|
||
|> Ui.keyboard_area focus_handler)
|
||
|
||
let main_menu_item wm text f =
|
||
let text = string ~attr:A.menu_main (" " ^ text ^ " ") in
|
||
let refresh = Lwd.var () in
|
||
let overlay = ref false in
|
||
let on_click ~x:_ ~y:_ = function
|
||
| `Left ->
|
||
overlay := true;
|
||
Lwd.set refresh ();
|
||
`Handled
|
||
| _ -> `Unhandled
|
||
in
|
||
Lwd.map (Lwd.get refresh) ~f:(fun () ->
|
||
let ui = Ui.mouse_area on_click text in
|
||
if !overlay then (
|
||
overlay := false;
|
||
menu_overlay wm
|
||
(Gravity.make ~h:`Negative ~v:`Positive)
|
||
(f ()) ui)
|
||
else ui)
|
||
|
||
let sub_menu_item wm text f =
|
||
let text = string ~attr:A.menu_sub text in
|
||
let refresh = Lwd.var () in
|
||
let overlay = ref false in
|
||
let on_click ~x:_ ~y:_ = function
|
||
| `Left ->
|
||
overlay := true;
|
||
Lwd.set refresh ();
|
||
`Handled
|
||
| _ -> `Unhandled
|
||
in
|
||
Lwd.map (Lwd.get refresh) ~f:(fun () ->
|
||
let ui = Ui.mouse_area on_click text in
|
||
if !overlay then (
|
||
overlay := false;
|
||
menu_overlay wm
|
||
(Gravity.make ~h:`Positive ~v:`Negative)
|
||
(f ()) ui)
|
||
else ui)
|
||
|
||
let sub_entry text f =
|
||
let text = string ~attr:A.menu_sub text in
|
||
let on_click ~x:_ ~y:_ = function
|
||
| `Left ->
|
||
f ();
|
||
`Handled
|
||
| _ -> `Unhandled
|
||
in
|
||
Ui.mouse_area on_click text
|
||
|
||
type pane_state =
|
||
| Split of { pos : float; max : float }
|
||
| Re_split of { pos : float; max : float; at : float }
|
||
|
||
let h_pane left right =
|
||
let state_var = Lwd.var (Split { pos = 5.; max = 10. }) in
|
||
let render state (l, r) =
|
||
let (Split { pos; max } | Re_split { pos; max; _ }) = state in
|
||
let l = Ui.resize ~w:0. ~h:0. ~sh:1. ~sw:pos l in
|
||
let r = Ui.resize ~w:0. ~h:0. ~sh:1. ~sw:(max -. pos) r in
|
||
let splitter =
|
||
Ui.resize
|
||
~attr:A.(bg Color.lightyellow)
|
||
~w:1. ~h:0. ~sw:0. ~sh:1. Ui.empty
|
||
in
|
||
let splitter =
|
||
Ui.mouse_area
|
||
(fun ~x:_ ~y:_ -> function
|
||
| `Left ->
|
||
`Grab
|
||
( (fun ~x ~y:_ ->
|
||
match Lwd.peek state_var with
|
||
| Split { pos; max } ->
|
||
Lwd.set state_var
|
||
(Re_split { pos; max; at = x })
|
||
| Re_split { pos; max; at } ->
|
||
if at <> x then
|
||
Lwd.set state_var
|
||
(Re_split { pos; max; at = x })),
|
||
fun ~x:_ ~y:_ -> () )
|
||
| _ -> `Unhandled)
|
||
splitter
|
||
in
|
||
let ui = Ui.join_x l (Ui.join_x splitter r) in
|
||
let ui = Ui.resize ~w:100. ~h:100. ~sw:10. ~sh:10. ui in
|
||
let ui =
|
||
match state with
|
||
| Split _ -> ui
|
||
| Re_split { at; _ } ->
|
||
Ui.transient_sensor
|
||
(fun ~x ~y:_ ~w ~h:_ () ->
|
||
Lwd.set state_var (Split { pos = at -. x; max = w }))
|
||
ui
|
||
in
|
||
ui
|
||
in
|
||
Lwd.map2 ~f:render (Lwd.get state_var) (Lwd.pair left right)
|
||
|
||
let v_pane top bot =
|
||
let state_var = Lwd.var (Split { pos = 5.; max = 10. }) in
|
||
let render state (top, bot) =
|
||
let (Split { pos; max } | Re_split { pos; max; _ }) = state in
|
||
let top = Ui.resize ~w:0. ~h:0. ~sw:1. ~sh:pos top in
|
||
let bot = Ui.resize ~w:0. ~h:0. ~sw:1. ~sh:(max -. pos) bot in
|
||
let splitter =
|
||
Ui.resize
|
||
~attr:A.(bg Color.lightyellow)
|
||
~w:0. ~h:1. ~sw:1. ~sh:0. Ui.empty
|
||
in
|
||
let splitter =
|
||
Ui.mouse_area
|
||
(fun ~x:_ ~y:_ -> function
|
||
| `Left ->
|
||
`Grab
|
||
( (fun ~x:_ ~y ->
|
||
match Lwd.peek state_var with
|
||
| Split { pos; max } ->
|
||
Lwd.set state_var
|
||
(Re_split { pos; max; at = y })
|
||
| Re_split { pos; max; at } ->
|
||
if at <> y then
|
||
Lwd.set state_var
|
||
(Re_split { pos; max; at = y })),
|
||
fun ~x:_ ~y:_ -> () )
|
||
| _ -> `Unhandled)
|
||
splitter
|
||
in
|
||
let ui = Ui.join_y top (Ui.join_y splitter bot) in
|
||
let ui = Ui.resize ~w:10. ~h:10. ~sw:1. ~sh:1. ui in
|
||
let ui =
|
||
match state with
|
||
| Split _ -> ui
|
||
| Re_split { at; _ } ->
|
||
Ui.transient_sensor
|
||
(fun ~x:_ ~y ~w:_ ~h () ->
|
||
Lwd.set state_var (Split { pos = at -. y; max = h }))
|
||
ui
|
||
in
|
||
ui
|
||
in
|
||
Lwd.map2 ~f:render (Lwd.get state_var) (Lwd.pair top bot)
|
||
|
||
let eq_uc_c uc c = Uchar.(equal uc (of_char c))
|
||
|
||
let edit_field ?(focus = Focus.make ()) ?(on_change = Fun.id) state
|
||
=
|
||
let update focus_h focus (text, pos) =
|
||
let pos = min (max 0 pos) (String.length text) in
|
||
let content =
|
||
Ui.atom @@ I.hcat
|
||
@@
|
||
if Focus.has_focus focus then
|
||
let attr = A.clickable in
|
||
let len = String.length text in
|
||
if pos >= len then
|
||
[ I.string ~attr text; I.string ~attr:A.cursor " " ]
|
||
else
|
||
[
|
||
I.string ~attr (String.sub text 0 pos);
|
||
I.string ~attr:A.cursor (String.sub text pos 1);
|
||
I.string ~attr
|
||
(String.sub text (pos + 1) (len - pos - 1));
|
||
]
|
||
else [ I.string (if text = "" then " " else text) ]
|
||
in
|
||
let handler k =
|
||
let on_change a =
|
||
Lwd.set state (on_change a);
|
||
`Handled
|
||
in
|
||
(match k with
|
||
| `Uchar c, [ `Ctrl ] when Uchar.(equal c (of_char 'U')) ->
|
||
on_change ("", 0) (* clear *)
|
||
| `Uchar u, [ `Ctrl ] when eq_uc_c u 'k' ->
|
||
(* TODO put killed text into kill-ring *)
|
||
if pos < String.length text then
|
||
on_change (String.sub text 0 pos, pos)
|
||
else `Unhandled (* kill *)
|
||
| `Backspace, [] ->
|
||
if pos > 0 then
|
||
let text =
|
||
if pos < String.length text then
|
||
String.sub text 0 (pos - 1)
|
||
^ String.sub text pos (String.length text - pos)
|
||
else if String.length text > 0 then
|
||
String.sub text 0 (String.length text - 1)
|
||
else text
|
||
in
|
||
let pos = max 0 (pos - 1) in
|
||
on_change (text, pos)
|
||
else `Unhandled
|
||
| `Uchar k, [] ->
|
||
let k = Uchar.unsafe_to_char k in
|
||
let text =
|
||
if pos < String.length text then
|
||
String.sub text 0 pos ^ String.make 1 k
|
||
^ String.sub text pos (String.length text - pos)
|
||
else text ^ String.make 1 k
|
||
in
|
||
on_change (text, pos + 1)
|
||
| `Escape, [] ->
|
||
Focus.release focus_h;
|
||
`Handled
|
||
(* | `Enter, _ ->
|
||
on_submit (text, pos);
|
||
`Handled *)
|
||
| `Arrow `Left, [] ->
|
||
if pos > 0 then on_change (text, pos - 1) else `Unhandled
|
||
| `Arrow `Right, [] ->
|
||
let pos = pos + 1 in
|
||
if pos <= String.length text then on_change (text, pos)
|
||
else `Unhandled
|
||
| _ -> `Unhandled)
|
||
|> fun r ->
|
||
Log.debug (fun m ->
|
||
m "edit_field keyboard_area handler %a -> %a" Ui.pp_key k
|
||
Ui.pp_may_handle r);
|
||
r
|
||
in
|
||
Ui.keyboard_area ~focus handler content
|
||
in
|
||
let node =
|
||
Lwd.map2 ~f:(update focus) (Focus.status focus) (Lwd.get state)
|
||
in
|
||
node
|
||
(* let mouse_grab (text, pos) ~x ~y:_ = function
|
||
| `Left ->
|
||
if x <> pos then on_change (text, x);
|
||
Nottui.Focus.request focus;
|
||
`Handled
|
||
| _ -> `Unhandled
|
||
in
|
||
Lwd.map2 state node ~f:(fun state content ->
|
||
Ui.mouse_area (mouse_grab state) content *)
|
||
|
||
open Lwd.Infix
|
||
|
||
type line = {
|
||
focus : Focus.handle;
|
||
state : (string * int) Lwd.var;
|
||
ui : Ui.t Lwd.t;
|
||
}
|
||
|
||
type lines = line Lwd_table.t
|
||
|
||
let line_empty () =
|
||
let focus = Focus.make () in
|
||
let state = Lwd.var ("", 0) in
|
||
{ focus; state; ui = edit_field ~focus state }
|
||
|
||
let line_make ?(focus = Focus.make ()) str =
|
||
let state = Lwd.var (str, 0) in
|
||
{ focus; state; ui = edit_field ~focus state }
|
||
|
||
let line_append ?(table = Lwd_table.make ()) ?focus str =
|
||
let row = Lwd_table.append table in
|
||
Lwd_table.set row (line_make ?focus str)
|
||
|
||
let copy_line_cursor (x : line) (y : line) =
|
||
let _, xi = Lwd.peek x.state in
|
||
let ys, _ = Lwd.peek y.state in
|
||
let yi = Int.max 0 (Int.min xi (String.length ys)) in
|
||
Lwd.set y.state (ys, yi)
|
||
|
||
let row_of_cursor cursor f =
|
||
Ui.may_handle (Lwd.peek cursor) (fun row -> f row)
|
||
|
||
let line_of_cursor cursor
|
||
(f : line Lwd_table.row -> line -> Ui.may_handle) :
|
||
Ui.may_handle =
|
||
Ui.may_handle (Lwd.peek cursor) (fun row ->
|
||
Ui.may_handle (Lwd_table.get row) (fun line -> f row line))
|
||
|
||
let cursor_move cursor
|
||
(f : line Lwd_table.row -> line Lwd_table.row option) =
|
||
match Lwd.peek cursor with
|
||
| Some cursor_row -> (
|
||
match f cursor_row with
|
||
| Some new_row ->
|
||
(match Lwd_table.get new_row with
|
||
| Some new_line ->
|
||
cursor_row |> Lwd_table.get
|
||
|> Option.iter (fun cursor_line ->
|
||
copy_line_cursor cursor_line new_line;
|
||
Focus.release cursor_line.focus);
|
||
Focus.request new_line.focus
|
||
| None -> ());
|
||
Lwd.set cursor (Some new_row);
|
||
`Handled
|
||
| None -> `Unhandled)
|
||
| None -> `Unhandled
|
||
|
||
let edit_area_of_string ?(table = Lwd_table.make ()) (s : string) :
|
||
line Lwd_table.t =
|
||
(* Append lines from s to table *)
|
||
List.iter (line_append ~table) (String.split_on_char '\n' s);
|
||
table
|
||
|
||
let focus_val focus : int =
|
||
Focus.(
|
||
match focus with
|
||
| Empty -> 0
|
||
| Handle (i, _) -> i
|
||
| Conflict i -> i)
|
||
|
||
let rec find_focus (ui : ui) : ui =
|
||
Focus.(
|
||
match (ui.focus, ui.desc) with
|
||
| Empty, _ -> Ui.empty
|
||
| Handle (_, _), _ -> ui
|
||
| Conflict _, (X (a, b) | Y (a, b) | Z (a, b)) ->
|
||
if focus_val a.focus < focus_val b.focus then find_focus b
|
||
else find_focus a
|
||
| Conflict _, Atom _ -> Ui.empty
|
||
| ( Conflict _,
|
||
( Size_sensor (t, _)
|
||
| Mouse_handler (t, _)
|
||
| Focus_area (t, _)
|
||
| Event_filter (t, _)
|
||
| Transient_sensor (t, _)
|
||
| Permanent_sensor (t, _)
|
||
| Resize (t, _, _, _)
|
||
| Shift_area (t, _, _) ) ) ->
|
||
find_focus t)
|
||
|
||
let focus_handle_compare a b =
|
||
if
|
||
Lwd.peek (Focus.var (snd a).focus)
|
||
< Lwd.peek (Focus.var (snd b).focus)
|
||
then b
|
||
else a
|
||
|
||
let focused_row_of_table (table : line Lwd_table.t) =
|
||
Lwd_table.map_reduce
|
||
(fun row (line : line) -> (Some row, line))
|
||
((None, line_empty ()), focus_handle_compare)
|
||
table
|
||
|
||
let edit_area ?(table = Lwd_table.make ()) ?(focus = Focus.make ())
|
||
() : Ui.t Lwd.t =
|
||
let cursor = Lwd.var @@ Lwd_table.first table in
|
||
Option.iter
|
||
(fun cursor ->
|
||
Option.iter (fun first -> Focus.request first.focus)
|
||
@@ Lwd_table.get cursor)
|
||
(Lwd.peek cursor);
|
||
|
||
(* Build view of table *)
|
||
Lwd_table.map_reduce
|
||
(fun _ { ui; _ } -> ui)
|
||
(Lwd_utils.lift_monoid Ui.pack_y)
|
||
table
|
||
|> Lwd.join
|
||
|> Lwd.map2
|
||
~f:(fun (focus, _) ->
|
||
Ui.keyboard_area ~focus (fun k ->
|
||
Log.debug (fun m ->
|
||
m "edit_area handler %a" Ui.pp_key k);
|
||
match k with
|
||
| `Uchar u, [ `Ctrl ] when eq_uc_c u 'n' ->
|
||
cursor_move cursor (fun c -> Lwd_table.next c)
|
||
| `Arrow `Down, _ ->
|
||
cursor_move cursor (fun c -> Lwd_table.next c)
|
||
| `Uchar u, [ `Ctrl ] when eq_uc_c u 'p' ->
|
||
cursor_move cursor (fun c -> Lwd_table.prev c)
|
||
| `Arrow `Up, _ ->
|
||
cursor_move cursor (fun c -> Lwd_table.prev c)
|
||
| `Uchar u, [ `Meta ] when eq_uc_c u '<' ->
|
||
cursor_move cursor (fun _ -> Lwd_table.first table)
|
||
| `Uchar u, [ `Meta ] when eq_uc_c u '>' ->
|
||
cursor_move cursor (fun _ -> Lwd_table.last table)
|
||
| `Enter, [] ->
|
||
line_of_cursor cursor (fun old_row old_line ->
|
||
let str, pos = Lwd.peek old_line.state in
|
||
let o_str = String.sub str 0 pos in
|
||
let n_str =
|
||
String.(sub str pos (length str - pos))
|
||
in
|
||
Lwd.set old_line.state (o_str, pos);
|
||
let new_line = line_make n_str in
|
||
Focus.release old_line.focus;
|
||
Focus.request new_line.focus;
|
||
Lwd.set cursor
|
||
(Some (Lwd_table.after old_row ~set:new_line));
|
||
`Handled)
|
||
| `Backspace, [] ->
|
||
line_of_cursor cursor (fun row line ->
|
||
let str, pos = Lwd.peek line.state in
|
||
Ui.may_handle (Lwd_table.prev row)
|
||
(fun row_prev ->
|
||
if pos = 0 then
|
||
Ui.may_handle (Lwd_table.get row_prev)
|
||
(fun line_prev ->
|
||
let str_prev, _ =
|
||
Lwd.peek line_prev.state
|
||
in
|
||
Focus.release line.focus;
|
||
Focus.request line_prev.focus;
|
||
Lwd.set line_prev.state
|
||
( str_prev ^ str,
|
||
String.length str_prev );
|
||
Lwd_table.remove row;
|
||
`Handled)
|
||
else `Unhandled))
|
||
| `Uchar u, [ `Ctrl ] when eq_uc_c u 'k' -> `Handled
|
||
| _ -> `Unhandled))
|
||
(Lwd.pair (Focus.status focus) (focused_row_of_table table))
|
||
|
||
(** Tab view, where exactly one element of [l] is shown at a time. *)
|
||
let tabs (tabs : (string * (unit -> Ui.t Lwd.t)) list) : Ui.t Lwd.t
|
||
=
|
||
match tabs with
|
||
| [] -> Lwd.return Ui.empty
|
||
| _ ->
|
||
let cur = Lwd.var 0 in
|
||
Lwd.get cur >>= fun idx_sel ->
|
||
let _, f = List.nth tabs idx_sel in
|
||
let tab_bar =
|
||
tabs
|
||
|> List.mapi (fun i (s, _) ->
|
||
let attr =
|
||
if i = idx_sel then A.(bg Color.blue) else A.empty
|
||
in
|
||
let tab_annot = printf ~attr "[%s]" s in
|
||
Ui.mouse_area
|
||
(fun ~x:_ ~y:_ l ->
|
||
if l = `Left then (
|
||
Lwd.set cur i;
|
||
`Handled)
|
||
else `Unhandled)
|
||
tab_annot)
|
||
|> Ui.hcat
|
||
in
|
||
f () >|= Ui.join_y tab_bar
|
||
|
||
(*
|
||
(** Horizontal/vertical box. We fill lines until there is no room,
|
||
and then go to the next ligne. All widgets in a line are considered to
|
||
have the same height.
|
||
@param width dynamic width (default 80)
|
||
*)
|
||
let flex_box ?(w = Lwd.return 80) (l : Ui.t Lwd.t list) : Ui.t Lwd.t
|
||
=
|
||
Lwd_utils.flatten_l l >>= fun l ->
|
||
w >|= fun w_limit ->
|
||
let rec box_render (acc : Ui.t) (i : int) l : Ui.t =
|
||
match l with
|
||
| [] -> acc
|
||
| ui0 :: tl ->
|
||
let w0 = (Ui.layout_spec ui0).Ui.w in
|
||
if i + w0 >= w_limit then
|
||
(* newline starting with ui0 *)
|
||
Ui.join_y acc (box_render ui0 w0 tl)
|
||
else
|
||
(* same line *)
|
||
box_render (Ui.join_x acc ui0) (i + w0) tl
|
||
in
|
||
box_render Ui.empty 0 l
|
||
*)
|
||
|
||
(** Prints the summary, but calls [f()] to compute a sub-widget
|
||
when clicked on. Useful for displaying deep trees. *)
|
||
let unfoldable ?(folded_by_default = true) summary
|
||
(f : unit -> Ui.t Lwd.t) : Ui.t Lwd.t =
|
||
let open Lwd.Infix in
|
||
let opened = Lwd.var (not folded_by_default) in
|
||
let fold_content =
|
||
Lwd.get opened >>= function
|
||
| true ->
|
||
(* call [f] and pad a bit *)
|
||
f () |> Lwd.map ~f:(Ui.join_x (string " "))
|
||
| false -> Lwd.return Ui.empty
|
||
in
|
||
(* pad summary with a "> " when it's opened *)
|
||
let summary =
|
||
Lwd.get opened >>= fun op ->
|
||
summary >|= fun s ->
|
||
Ui.hcat
|
||
[
|
||
string ~attr:A.clickable (if op then "v" else ">");
|
||
string " ";
|
||
s;
|
||
]
|
||
in
|
||
let cursor ~x:_ ~y:_ = function
|
||
| `Left when Lwd.peek opened ->
|
||
Lwd.set opened false;
|
||
`Handled
|
||
| `Left ->
|
||
Lwd.set opened true;
|
||
`Handled
|
||
| _ -> `Unhandled
|
||
in
|
||
let mouse =
|
||
Lwd.map ~f:(fun m -> Ui.mouse_area cursor m) summary
|
||
in
|
||
Lwd.map2 mouse fold_content ~f:(fun summary fold ->
|
||
(* TODO: make this configurable/optional *)
|
||
(* newline if it's too big to fit on one line nicely *)
|
||
let spec_sum = Ui.layout_spec summary in
|
||
let spec_fold = Ui.layout_spec fold in
|
||
(* TODO: somehow, probe for available width here? *)
|
||
let too_big =
|
||
spec_fold.Ui.h > 20.
|
||
|| spec_fold.Ui.h > 20.
|
||
&& spec_sum.Ui.w +. spec_fold.Ui.w > 240.
|
||
in
|
||
if too_big then
|
||
Ui.join_y summary (Ui.join_x (string " ") fold)
|
||
else Ui.join_x summary fold)
|
||
|
||
let hbox l = Lwd_utils.pack Ui.pack_x l
|
||
let vbox l = Lwd_utils.pack Ui.pack_y l
|
||
let zbox l = Lwd_utils.pack Ui.pack_z l
|
||
|
||
let vlist ?(bullet = "- ") (l : Ui.t Lwd.t list) : Ui.t Lwd.t =
|
||
l
|
||
|> List.map (fun ui -> Lwd.map ~f:(Ui.join_x (string bullet)) ui)
|
||
|> vbox
|
||
|
||
(** A list of items with a dynamic filter on the items *)
|
||
let vlist_with ?(bullet = "- ")
|
||
?(filter = Lwd.return (fun _ -> true)) (f : 'a -> Ui.t Lwd.t)
|
||
(l : 'a list Lwd.t) : Ui.t Lwd.t =
|
||
let open Lwd.Infix in
|
||
let rec filter_map_ acc f l =
|
||
match l with
|
||
| [] -> List.rev acc
|
||
| x :: l' ->
|
||
let acc' =
|
||
match f x with None -> acc | Some y -> y :: acc
|
||
in
|
||
filter_map_ acc' f l'
|
||
in
|
||
let l =
|
||
l
|
||
>|= List.map (fun x ->
|
||
(x, Lwd.map ~f:(Ui.join_x (string bullet)) @@ f x))
|
||
in
|
||
let l_filter : _ list Lwd.t =
|
||
filter >>= fun filter ->
|
||
l
|
||
>|= filter_map_ [] (fun (x, ui) ->
|
||
if filter x then Some ui else None)
|
||
in
|
||
l_filter >>= Lwd_utils.pack Ui.pack_y
|
||
|
||
let vlist_of_text ?(focus = Focus.make ())
|
||
?(key_handler = fun _ -> `Unhandled) =
|
||
Lwd.map2 (Focus.status focus) ~f:(fun focus s ->
|
||
Ui.vcat @@ List.map string @@ String.split_on_char '\n' s
|
||
|> Ui.keyboard_area ~focus key_handler)
|
||
|
||
(* let rec iterate n f x = if n = 0 then x else iterate (n - 1) f (f x)
|
||
|
||
(** A grid layout, with alignment in all rows/columns.
|
||
@param max_h maximum height of a cell
|
||
@param max_w maximum width of a cell
|
||
@param bg attribute for controlling background style
|
||
@param h_space horizontal space between each cell in a row
|
||
@param v_space vertical space between each row
|
||
@param pad used to control padding of cells
|
||
@param crop used to control cropping of cells
|
||
TODO: control padding/alignment, vertically and horizontally
|
||
TODO: control align left/right in cells
|
||
TODO: horizontal rule below headers
|
||
TODO: headers *)
|
||
let grid ?max_h ?max_w ?pad ?crop ?bg ?(h_space = 0) ?(v_space = 0)
|
||
?(headers : Ui.t Lwd.t list option)
|
||
(rows : Ui.t Lwd.t list list) : Ui.t Lwd.t =
|
||
let rows =
|
||
match headers with None -> rows | Some r -> r :: rows
|
||
in
|
||
(* build a [ui list list Lwd.t] *)
|
||
Lwd_utils.map_l (fun r -> Lwd_utils.flatten_l r) rows
|
||
>>= fun (rows : Ui.t list list) ->
|
||
(* determine width of each column and height of each row *)
|
||
let n_cols =
|
||
List.fold_left (fun n r -> max n (List.length r)) 0 rows
|
||
in
|
||
let col_widths = Array.make n_cols 1 in
|
||
List.iter
|
||
(fun row ->
|
||
List.iteri
|
||
(fun col_j cell ->
|
||
let w = (Ui.layout_spec cell).Ui.w in
|
||
col_widths.(col_j) <- max col_widths.(col_j) w)
|
||
row)
|
||
rows;
|
||
(match max_w with
|
||
| None -> ()
|
||
| Some max_w ->
|
||
(* limit width *)
|
||
Array.iteri
|
||
(fun i x -> col_widths.(i) <- min x max_w)
|
||
col_widths);
|
||
(* now render, with some padding *)
|
||
let pack_pad_x =
|
||
if h_space <= 0 then (Ui.empty, Ui.join_x)
|
||
else (Ui.empty, fun x y -> Ui.hcat [ x; Ui.space h_space 0; y ])
|
||
and pack_pad_y =
|
||
if v_space = 0 then (Ui.empty, Ui.join_y)
|
||
else (Ui.empty, fun x y -> Ui.vcat [ x; Ui.space v_space 0; y ])
|
||
in
|
||
let rows =
|
||
List.map
|
||
(fun row ->
|
||
let row_h =
|
||
List.fold_left
|
||
(fun n c -> max n (Ui.layout_spec c).Ui.h)
|
||
0 row
|
||
in
|
||
let row_h =
|
||
match max_h with
|
||
| None -> row_h
|
||
| Some max_h -> min row_h max_h
|
||
in
|
||
let row =
|
||
List.mapi
|
||
(fun i c ->
|
||
Ui.resize ~w:col_widths.(i) ~h:row_h ?crop ?pad ?bg c)
|
||
row
|
||
in
|
||
Lwd_utils.reduce pack_pad_x row)
|
||
rows
|
||
in
|
||
(* TODO: mouse and keyboard handling *)
|
||
let ui = Lwd_utils.reduce pack_pad_y rows in
|
||
Lwd.return ui
|
||
*)
|
||
|
||
(** Turn the given [ui] into a clickable button, calls [f] when clicked. *)
|
||
let button_of ui f =
|
||
Ui.keyboard_area
|
||
(function
|
||
| `Enter, _ ->
|
||
f ();
|
||
`Handled
|
||
| _ -> `Unhandled)
|
||
(* @@ Ui.mouse_area
|
||
(fun ~x:_ ~y:_ _ ->
|
||
f ();
|
||
`Handled) *)
|
||
ui
|
||
|
||
(** A clickable button that calls [f] when clicked, labelled with a string. *)
|
||
let button ?(attr = A.clickable) s f = button_of (string ~attr s) f
|
||
|
||
(* file explorer for selecting a file *)
|
||
let file_select ?(abs = false) ?filter ~(on_select : string -> unit)
|
||
() : Ui.t Lwd.t =
|
||
let rec aux ~fold path =
|
||
try
|
||
let p_rel = if path = "" then "." else path in
|
||
if Sys.is_directory p_rel then
|
||
let ui () =
|
||
let arr = Sys.readdir p_rel in
|
||
let l =
|
||
Array.to_list arr |> List.map (Filename.concat path)
|
||
in
|
||
(* apply potential filter *)
|
||
let l =
|
||
match filter with
|
||
| None -> l
|
||
| Some f -> List.filter f l
|
||
in
|
||
let l = Lwd.return @@ List.sort String.compare l in
|
||
vlist_with ~bullet:"" (aux ~fold:true) l
|
||
in
|
||
if fold then
|
||
unfoldable ~folded_by_default:true
|
||
(Lwd.return @@ string @@ path ^ "/")
|
||
ui
|
||
else ui ()
|
||
else
|
||
Lwd.return
|
||
@@ button
|
||
~attr:A.(font Font.underline)
|
||
path
|
||
(fun () -> on_select path)
|
||
with e ->
|
||
Lwd.return
|
||
@@ Ui.vcat
|
||
[
|
||
printf
|
||
~attr:A.(bg Color.red)
|
||
"cannot list directory %s" path;
|
||
string @@ Printexc.to_string e;
|
||
]
|
||
in
|
||
let start = if abs then Sys.getcwd () else "" in
|
||
aux ~fold:false start
|
||
|
||
let toggle, toggle' =
|
||
let toggle_ st (lbl : string Lwd.t) (f : bool -> unit) :
|
||
Ui.t Lwd.t =
|
||
let mk_but st_v lbl_v =
|
||
let lbl =
|
||
Ui.hcat
|
||
[
|
||
printf "[%s|" lbl_v;
|
||
string ~attr:A.clickable (if st_v then "✔" else "×");
|
||
string "]";
|
||
]
|
||
in
|
||
button_of lbl (fun () ->
|
||
let new_st = not st_v in
|
||
Lwd.set st new_st;
|
||
f new_st)
|
||
in
|
||
Lwd.map2 ~f:mk_but (Lwd.get st) lbl
|
||
in
|
||
(* Similar to {!toggle}, except it directly reflects the state of a variable. *)
|
||
let toggle' (lbl : string Lwd.t) (v : bool Lwd.var) : Ui.t Lwd.t =
|
||
toggle_ v lbl (Lwd.set v)
|
||
(* a toggle, with a true/false state *)
|
||
and toggle ?(init = false) (lbl : string Lwd.t) (f : bool -> unit)
|
||
: Ui.t Lwd.t =
|
||
let st = Lwd.var init in
|
||
toggle_ st lbl f
|
||
in
|
||
(toggle, toggle')
|
||
|
||
type scrollbox_state = {
|
||
w : float;
|
||
h : float;
|
||
x : float;
|
||
y : float;
|
||
}
|
||
|
||
let adjust_offset visible total off =
|
||
let off =
|
||
if off +. visible > total then total -. visible else off
|
||
in
|
||
let off = if off < 0. then 0. else off in
|
||
off
|
||
|
||
let scrollbar_width = 10.
|
||
let decr_if x cond = if cond then x -. scrollbar_width else x
|
||
let scrollbar_bg = Color.gray 0.4
|
||
let scrollbar_fg = Color.gray 0.7
|
||
|
||
let scrollbar_click_step =
|
||
3. (* Clicking scrolls one third of the screen *)
|
||
|
||
let scrollbar_wheel_step =
|
||
8. (* Wheel event scrolls 1/8th of the screen *)
|
||
|
||
let hscrollbar visible total offset ~set =
|
||
let prefix = offset *. visible /. total in
|
||
let suffix = (total -. offset -. visible) *. visible /. total in
|
||
let handle = visible -. prefix -. suffix in
|
||
let render size color =
|
||
Ui.atom (I.attr (A.bg color) (I.void size scrollbar_width))
|
||
in
|
||
let mouse_handler ~x ~y:_ = function
|
||
| `Left ->
|
||
if x < prefix then (
|
||
set (offset -. max 1. (visible /. scrollbar_click_step));
|
||
`Handled)
|
||
else if x > prefix +. handle then (
|
||
set (offset +. max 1. (visible /. scrollbar_click_step));
|
||
`Handled)
|
||
else
|
||
`Grab
|
||
( (fun ~x:x' ~y:_ ->
|
||
set (offset +. ((x' -. x) *. total /. visible))),
|
||
fun ~x:_ ~y:_ -> () )
|
||
| `Scroll dir ->
|
||
let dir = match dir with `Down -> 1. | `Up -> -1. in
|
||
set
|
||
(offset
|
||
+. (dir *. max 1. (visible /. scrollbar_wheel_step)));
|
||
`Handled
|
||
| _ -> `Unhandled
|
||
in
|
||
let ( ++ ) = Ui.join_x in
|
||
Ui.mouse_area mouse_handler
|
||
(render prefix scrollbar_bg
|
||
++ render handle scrollbar_fg
|
||
++ render suffix scrollbar_bg)
|
||
|
||
let vscrollbar visible total offset ~set =
|
||
let prefix = offset *. visible /. total in
|
||
let suffix = (total -. offset -. visible) *. visible /. total in
|
||
let handle = visible -. prefix -. suffix in
|
||
let render size color =
|
||
Ui.atom (I.char ~attr:(A.bg color) ' ' 1. size)
|
||
in
|
||
let mouse_handler ~x:_ ~y = function
|
||
| `Left ->
|
||
if y < prefix then (
|
||
set (offset -. max 1. (visible /. scrollbar_click_step));
|
||
`Handled)
|
||
else if y > prefix +. handle then (
|
||
set (offset +. max 1. (visible /. scrollbar_click_step));
|
||
`Handled)
|
||
else
|
||
`Grab
|
||
( (fun ~x:_ ~y:y' ->
|
||
set (offset +. ((y' -. y) *. total /. visible))),
|
||
fun ~x:_ ~y:_ -> () )
|
||
| `Scroll dir ->
|
||
let dir = match dir with `Down -> 1. | `Up -> -1. in
|
||
set
|
||
(offset
|
||
+. (dir *. max 1. (visible /. scrollbar_wheel_step)));
|
||
`Handled
|
||
| _ -> `Unhandled
|
||
in
|
||
let ( ++ ) = Ui.join_y in
|
||
Ui.mouse_area mouse_handler
|
||
(render prefix scrollbar_bg
|
||
++ render handle scrollbar_fg
|
||
++ render suffix scrollbar_bg)
|
||
|
||
let scrollbox t =
|
||
(* Keep track of scroll state *)
|
||
let state_var = Lwd.var { w = 0.; h = 0.; x = 0.; y = 0. } in
|
||
(* Keep track of size available for display *)
|
||
let update_size ~w ~h =
|
||
let state = Lwd.peek state_var in
|
||
if state.w <> w || state.h <> h then
|
||
Lwd.set state_var { state with w; h }
|
||
in
|
||
let measure_size body =
|
||
Ui.size_sensor update_size
|
||
(Ui.resize ~w:0. ~h:0. ~sw:1. ~sh:1. body)
|
||
in
|
||
(* Given body and state, composite scroll bars *)
|
||
let compose_bars body state =
|
||
let bw, bh = (Ui.layout_width body, Ui.layout_height body) in
|
||
(* Logic to determine which scroll bar should be visible *)
|
||
let hvisible = state.w < bw and vvisible = state.h < bh in
|
||
let hvisible = hvisible || (vvisible && state.w = bw) in
|
||
let vvisible = vvisible || (hvisible && state.h = bh) in
|
||
(* Compute size and offsets based on visibility *)
|
||
let state_w = decr_if state.w vvisible in
|
||
let state_h = decr_if state.h hvisible in
|
||
let state_x = adjust_offset state_w bw state.x in
|
||
let state_y = adjust_offset state_h bh state.y in
|
||
(* Composite visible scroll bars *)
|
||
let crop b =
|
||
Ui.resize ~sw:scrollbar_width ~sh:scrollbar_width ~w:0. ~h:0.
|
||
(Ui.shift_area state_x state_y b)
|
||
in
|
||
let set_vscroll y =
|
||
let state = Lwd.peek state_var in
|
||
if state.y <> y then Lwd.set state_var { state with y }
|
||
in
|
||
let set_hscroll x =
|
||
let state = Lwd.peek state_var in
|
||
if state.x <> x then Lwd.set state_var { state with x }
|
||
in
|
||
let ( <-> ) = Ui.join_y and ( <|> ) = Ui.join_x in
|
||
match (hvisible, vvisible) with
|
||
| false, false -> body
|
||
| false, true ->
|
||
crop body <|> vscrollbar state_h bh state_y ~set:set_vscroll
|
||
| true, false ->
|
||
crop body <-> hscrollbar state_w bw state_x ~set:set_hscroll
|
||
| true, true ->
|
||
crop body
|
||
<|> vscrollbar state_h bh state_y ~set:set_vscroll
|
||
<-> (hscrollbar state_w bw state_x ~set:set_hscroll
|
||
<|> Ui.space scrollbar_width scrollbar_width)
|
||
in
|
||
(* Render final box *)
|
||
Lwd.map2 t (Lwd.get state_var) ~f:(fun ui size ->
|
||
measure_size (compose_bars ui size))
|
||
end
|