diff --git a/boot_js.ml b/boot_js.ml index ac98411..0d1972b 100644 --- a/boot_js.ml +++ b/boot_js.ml @@ -88,46 +88,12 @@ let _ = let open Js_of_ocaml_lwt.Lwt_js_events in let edit_me = Lwd.var ("edit me?", 0) in let root = - Lwd_utils.pack Nottui.Ui.pack_y - [ - (* Lwd_utils.pack Nottui.Ui.pack_x - [ - Lwd.pure @@ Nottui_widgets.string "hello daddy"; - Lwd.pure - @@ Nottui_widgets.string - "What is going to be displayed here?"; - ]; - Lwd_utils.pack Nottui.Ui.pack_x - [ - Lwd.pure @@ Nottui_widgets.string "hello daddy"; - Lwd.pure - @@ Nottui_widgets.string - "What is going to be displayed here?"; - ]; - Lwd_utils.pack Nottui.Ui.pack_x - [ - Lwd.pure @@ Nottui_widgets.string "hello"; - Lwd.pure @@ Nottui_widgets.string "hello"; - Lwd.pure @@ Nottui_widgets.string "hello"; - Lwd.pure @@ Nottui_widgets.string "hello"; - Lwd.pure @@ Nottui_widgets.string "hello"; - ]; - Lwd_utils.pack Nottui.Ui.pack_x - [ - Lwd.pure @@ Nottui_widgets.string "it "; - Lwd.pure @@ Nottui_widgets.string "want "; - Lwd.pure @@ Nottui_widgets.string "you "; - Lwd.pure @@ Nottui_widgets.string "do "; - Lwd.pure @@ Nottui_widgets.string "when "; - ]; - Lwd.pure @@ Nottui_widgets.string "when when when when"; - Lwd.pure @@ Nottui_widgets.string "when when when when";*) - Nottui_widgets.edit_field (Lwd.get edit_me) - ~on_change:(fun ((text, pos) as state) -> - Log.debug (fun m -> m "--- on_change (%s,%d)" text pos); - Lwd.set edit_me state) - ~on_submit:ignore; - ] + let open Nottui_widgets in + edit_field (Lwd.get edit_me) + ~on_change:(fun ((text, pos) as state) -> + Log.debug (fun m -> m "--- on_change (%s,%d)" text pos); + Lwd.set edit_me state) + ~on_submit:ignore in let events, push_event = Lwt_stream.create () in @@ -140,7 +106,9 @@ let _ = render_stream canvas webgl_ctx vg (fun vg ?(time = 0.) p i -> Log.debug (fun m -> - m "Drawing image: p=%a n=%a" Gg.V2.pp p I.Draw.pp i); + m "Drawing image: p=%a n=%a" Gg.V2.pp p + (I.Draw.pp ~attr:A.dark) + i); let p' = I.Draw.node vg A.dark p i in Logs.debug (fun m -> m "Drawing finished: p'=%a" Gg.V2.pp p')) diff --git a/human.ml b/human.ml index 9073447..ca2c247 100644 --- a/human.ml +++ b/human.ml @@ -584,17 +584,19 @@ module NVG = struct 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 = + { + 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 pp ppf t : unit = F.( fmt "%a" ppf - (record - [ - field "r" (fun t -> t.r) F.float; - field "g" (fun t -> t.g) F.float; - field "b" (fun t -> t.b) F.float; - field "a" (fun t -> t.a) F.float; - ]) - t) + (list ~sep:(any "|") float) + [ t.r; t.g; t.b; t.a ]) end end @@ -607,12 +609,15 @@ let str_of_box 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; - Box2.max b + 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 @@ -622,7 +627,7 @@ let path_box vg color ?(width = 0.) b = if width != 0. then NVG.set_stroke_width vg ~width; NVG.set_stroke_color vg ~color; NVG.stroke vg; - Box2.max b + Box2.size b module Style = struct module Font = struct @@ -634,6 +639,11 @@ module Style = struct underline : [ `Underline | `None ]; } + let pp ppf = + F.( + fmt "%a" ppf + (record [ field "size" (fun a -> a.size) (option float) ])) + let empty = { size = None; @@ -691,6 +701,16 @@ module Style = struct type t = { fg : Color.t; bg : Color.t; font : Font.t } type attr = t + let pp ppf = + F.( + fmt "%a" ppf + (record + [ + field "fg" (fun a -> a.fg) Color.pp; + field "bg" (fun a -> a.bg) Color.pp; + field "font" (fun a -> a.font) Font.pp; + ])) + let equal = ( == ) let empty = @@ -703,14 +723,11 @@ module Style = struct let dark = { empty with fg = Color.light; bg = Color.dark } let ( ++ ) a1 a2 = - if a1 == empty then a2 - else if a2 == empty then a1 - else - { - a1 with - fg = Color.lerp a1.fg a2.fg ~a:0.5; - bg = Color.lerp a1.bg a2.bg ~a:0.5; - } + { + fg = Color.(a1.fg ++ a2.fg); + bg = Color.(a1.bg ++ a2.bg); + font = Font.merge a1.font a2.font; + } let fg ?(t = empty) c = { t with fg = c } let bg ?(t = empty) c = { t with bg = c } @@ -809,7 +826,7 @@ module Text = struct let replicatec w c = String (String.make (int_of_float w) c) let pp ppf : t -> unit = function - | String s -> F.(fmt "String %s" ppf s) + | String s -> F.(fmt "String \"%s\"" ppf s) end module A = Style @@ -1041,20 +1058,22 @@ module I = struct (Float.max_num (V2.x a) (V2.x b)) (Float.max_num (V2.y a) (V2.y b)) - let rec pp ppf : t -> unit = function + 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 -> F.(fmt "Empty" ppf) | Segment v -> F.(fmt "Segment %a" ppf (parens Text.pp) v) - | Attr v -> - F.(fmt "Attr %a" ppf (pair (parens pp) (any "...")) v) - | Hcompose a -> - F.(fmt "Hcompose %a" ppf (pair (parens pp) (parens pp)) a) - | Vcompose a -> - F.(fmt "Vcompose %a" ppf (pair (parens pp) (parens pp)) a) - | Zcompose a -> - F.(fmt "Zcompose %a" ppf (pair (parens pp) (parens pp)) a) - | Hcrop (t, h, w) -> F.(fmt "Hcrop (%a,%f,%f)" ppf pp t h w) - | Vcrop (t, h, w) -> F.(fmt "Vcrop (%a,%f,%f)" ppf pp t h w) - | Void dim -> F.(fmt "Void %a" ppf (parens V2.pp) dim) + | Attr (t, a) -> + fmt "attr %a" ppf + (pair A.pp (parens (pp ~attr:A.(attr ++ a)))) + (A.(attr ++ 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, h, w) -> fmt "Hcrop (%a,%f,%f)" ppf (pp ~attr) t h w + | Vcrop (t, h, w) -> fmt "Vcrop (%a,%f,%f)" ppf (pp ~attr) t h w + | Void dim -> fmt "Void %a" ppf (parens V2.pp) dim let rec segment vg p : Text.t -> P2.t = function | String s -> @@ -1062,6 +1081,7 @@ module I = struct let metrics = NVG.Text.metrics vg in NVG.Text.text vg ~x:(V2.x p) ~y:(V2.y p +. metrics.ascender) + (* TODO make segments include neighbors so kerning is correct *) s; let NVG.Bounds.{ xmin; ymin; xmax; ymax } = (NVG.Text.bounds vg ~x:(V2.x p) ~y:(V2.y p) s).box @@ -1069,14 +1089,21 @@ module I = struct V2.v (xmax -. xmin) (ymax -. ymin) 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, a0) -> - if Style.(attr.fg) != a0.fg then - NVG.set_fill_color vg ~color:Style.(attr.fg); - node vg A.(attr ++ a0) p i + | Attr (i, a) -> + let a0 = A.(attr ++ a) in + if + A.(a0.bg) != NVG.Color.transparent + && A.(a0.bg) != A.(attr.bg) + then fill_box vg a0.bg (Box2.v p (size vg p i)) |> ignore; + if A.(attr.fg) != a0.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 @@ -1228,8 +1255,7 @@ module Nottui = struct val shift : t -> float -> t val fst : t -> float val snd : t -> float - - (*val size : t -> int*) + val size : t -> float val zero : t end = struct type t = float * float @@ -2113,7 +2139,8 @@ module Nottui_widgets = struct let attr_menu_main = A.(bg Color.green ++ fg Color.black) let attr_menu_sub = A.(bg Color.lightgreen ++ fg Color.black) - let attr_clickable = A.(fg Color.lightblue) + let attr_clickable = A.(bg @@ Color.rgbf ~r:0.2 ~g:0.2 ~b:0.5) + let attr_cursor = A.(bg @@ Color.rgbf ~r:0.4 ~g:0.4 ~b:0.1) type window_manager = { overlays : ui Lwd.t Lwd_table.t; @@ -2136,316 +2163,326 @@ module Nottui_widgets = struct 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) - ~bg:A.(bg 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 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) + ~bg: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 + (* 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 - 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) + Lwd_table.set row view + in + Ui.transient_sensor sensor ui) + else ui *) + + let scroll_step = 4. + + type scroll_state = { + position : float; + bound : float; + visible : float; + total : float; + } + + let default_scroll_state = + { position = 0.; bound = 0.; visible = 0.; total = 0. } + + let vscroll_area ~state ~change t = + let visible = ref (-1.) in + let total = ref (-1.) in + let scroll state delta = + let position = state.position +. delta in + let position = max 0. (min state.bound position) in + if position <> state.position then + change `Action { state with position }; + `Handled + in + let focus_handler state = function + (*| `Arrow `Left , _ -> scroll (-scroll_step) 0*) + (*| `Arrow `Right, _ -> scroll (+scroll_step) 0*) + | `Arrow `Up, [] -> scroll state (-.scroll_step) + | `Arrow `Down, [] -> scroll state (+.scroll_step) + | `Page `Up, [] -> scroll state (-.scroll_step *. 8.) + | `Page `Down, [] -> scroll state (+.scroll_step *. 8.) + | _ -> `Unhandled + in + let scroll_handler state ~x:_ ~y:_ = function + | `Scroll `Up -> scroll state (-.scroll_step) + | `Scroll `Down -> scroll state (+.scroll_step) + | _ -> `Unhandled + in + Lwd.map2 t state ~f:(fun t state -> + t + |> Ui.shift_area 0. state.position + |> Ui.resize ~h:0. ~sh:1. + |> Ui.size_sensor (fun ~w:_ ~h -> + let tchange = + if !total <> Ui.(layout_spec t).h then ( + total := Ui.(layout_spec t).h; + true) + else false 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 vchange = + if !visible <> h then ( + visible := h; + true) + else false + in + if tchange || vchange then + change `Content + { + state with + visible = !visible; + total = !total; + bound = max 0. (!total -. !visible); + }) + |> Ui.mouse_area (scroll_handler state) + |> Ui.keyboard_area (focus_handler state)) - let scroll_step = 1 + 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) - type scroll_state = { - position : int; - bound : int; - visible : int; - total : int; - } + let main_menu_item wm text f = + let text = string ~attr:attr_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 default_scroll_state = - { position = 0; bound = 0; visible = 0; total = 0 } + let sub_menu_item wm text f = + let text = string ~attr:attr_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 vscroll_area ~state ~change t = - let visible = ref (-1) in - let total = ref (-1) in - let scroll state delta = - let position = state.position + delta in - let position = max 0 (min state.bound position) in - if position <> state.position then - change `Action { state with position }; - `Handled - in - let focus_handler state = function - (*| `Arrow `Left , _ -> scroll (-scroll_step) 0*) - (*| `Arrow `Right, _ -> scroll (+scroll_step) 0*) - | `Arrow `Up, [] -> scroll state (-scroll_step) - | `Arrow `Down, [] -> scroll state (+scroll_step) - | `Page `Up, [] -> scroll state (-scroll_step * 8) - | `Page `Down, [] -> scroll state (+scroll_step * 8) - | _ -> `Unhandled - in - let scroll_handler state ~x:_ ~y:_ = function - | `Scroll `Up -> scroll state (-scroll_step) - | `Scroll `Down -> scroll state (+scroll_step) - | _ -> `Unhandled - in - Lwd.map2 t state ~f:(fun t state -> - t - |> Ui.shift_area 0 state.position - |> Ui.resize ~h:0 ~sh:1 - |> Ui.size_sensor (fun ~w:_ ~h -> - let tchange = - if !total <> (Ui.layout_spec t).Ui.h then ( - total := (Ui.layout_spec t).Ui.h; - true) - else false - in - let vchange = - if !visible <> h then ( - visible := h; - true) - else false - in - if tchange || vchange then - change `Content - { - state with - visible = !visible; - total = !total; - bound = max 0 (!total - !visible); - }) - |> Ui.mouse_area (scroll_handler state) - |> Ui.keyboard_area (focus_handler state)) + let sub_entry text f = + let text = string ~attr:attr_menu_sub text in + let on_click ~x:_ ~y:_ = function + | `Left -> + f (); + `Handled + | _ -> `Unhandled + in + Ui.mouse_area on_click text - 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 - |> keyboard_area focus_handler) + type pane_state = + | Split of { pos : float; max : float } + | Re_split of { pos : float; max : float; at : float } - let main_menu_item wm text f = - let text = string ~attr:attr_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 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 + ~bg: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 sub_menu_item wm text f = - let text = string ~attr:attr_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:attr_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 : int; max : int } - | Re_split of { pos : int; max : int; at : int } - - 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 - ~bg:Notty.A.(bg 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: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 - 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 - ~bg:Notty.A.(bg 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 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 + ~bg: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 sub' str p l = if p = 0 && l = String.length str then str else String.sub str p l @@ -2465,7 +2502,7 @@ module Nottui_widgets = struct @ if pos < String.length text then [ - I.string ~attr:A.(bg Color.lightred) (sub' text pos 1); + I.string ~attr:attr_cursor (sub' text pos 1); I.string ~attr (sub' text (pos + 1) (len - pos - 1)); ] else [ I.string ~attr:A.(bg Color.lightred) " " ] @@ -2542,34 +2579,36 @@ module Nottui_widgets = struct Lwd.map2 state node ~f:(fun state content -> Ui.mouse_area (mouse_grab state) content *) - (* - (** 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.(st underline) 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 + open Lwd.Infix + (** 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. @@ -2643,340 +2682,349 @@ module Nottui_widgets = struct 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 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) + |> Lwd_utils.pack Ui.pack_y - 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) - |> Lwd_utils.pack Ui.pack_y + (** 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 - (** 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 rec iterate n f x = if n = 0 then x else iterate (n - 1) f (f x) - 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 + (** 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 -> - 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.mouse_area - (fun ~x:_ ~y:_ _ -> - f (); - `Handled) - ui - - (** A clickable button that calls [f] when clicked, labelled with a string. *) - let button ?(attr = attr_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.(st underline) - path - (fun () -> on_select path) - with e -> - Lwd.return - @@ Ui.vcat - [ - printf ~attr:A.(bg 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:attr_clickable (if st_v then "✔" else "×"); - string "]"; - ] + let row_h = + List.fold_left + (fun n c -> max n (Ui.layout_spec c).Ui.h) + 0 row in - button_of lbl (fun () -> - let new_st = not st_v in - Lwd.set st new_st; - f new_st) + 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.mouse_area + (fun ~x:_ ~y:_ _ -> + f (); + `Handled) + ui + + (** A clickable button that calls [f] when clicked, labelled with a string. *) + let button ?(attr = attr_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.(st underline) + path + (fun () -> on_select path) + with e -> + Lwd.return + @@ Ui.vcat + [ + printf ~attr:A.(bg 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:attr_clickable (if st_v then "✔" else "×"); + string "]"; + ] in - Lwd.map2 ~f:mk_but (Lwd.get st) lbl + button_of lbl (fun () -> + let new_st = not st_v in + Lwd.set st new_st; + f new_st) 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') + 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; + } - type scrollbox_state = { w : int; h : int; x : int; y : int } + 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 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 decr_if x cond = if cond then x -. 1. else x + let scrollbar_bg = Color.gray 0.4 + let scrollbar_fg = Color.gray 0.7 - let decr_if x cond = if cond then x - 1 else x - let scrollbar_bg = Notty.A.gray 4 - let scrollbar_fg = Notty.A.gray 7 + let scrollbar_click_step = + 3. (* Clicking scrolls one third of the screen *) - 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 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.char ~attr:(A.bg color) ' ' size 1.) + 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 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 Notty.(I.char (A.bg color) ' ' size 1) - 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 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 Notty.(I.char (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:1 ~sh:1 ~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 1 1) - in - (* Render final box *) - Lwd.map2 t (Lwd.get state_var) ~f:(fun ui size -> - measure_size (compose_bars ui size)) *) + 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:1. ~sh:1. ~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 1. 1.) + in + (* Render final box *) + Lwd.map2 t (Lwd.get state_var) ~f:(fun ui size -> + measure_size (compose_bars ui size)) end