1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
type 'msg element =
| Button of Widget.box * string * 'msg * bool
| Label of Widget.box * string
| Field of Widget.box * string * (string -> 'msg) * bool
| Slider of Widget.box * float * float * float * (float -> 'msg)
| Progress of Widget.box * float
| Canvas of Widget.box * Widget.paint list * (Widget.canvas_event -> 'msg option)
| Group of 'msg element list
let button ?(enabled = true) box s msg = Button (box, s, msg, enabled)
let label box s = Label (box, s)
let field ?(enabled = true) box s f = Field (box, s, f, enabled)
let slider box ~from ~to_ v f = Slider (box, from, to_, v, f)
let progress box fraction = Progress (box, fraction)
let box items chosen f = Menu (box, items, chosen, f)
let group kids = Group kids
let canvas box drawing f = Canvas (box, drawing, f)
let at items f = Context_menu (at, items, f)
type t = {
focus : Widget.id option;
caret : int;
was_down : bool;
was_rdown : bool;
keys_before : string list;
held : Widget.id option;
open_menu : Widget.id option;
}
let empty = { focus = None; caret = 0; was_down = false; was_rdown = false; keys_before = []; held = None; open_menu = None }
let rec leaves = function Group kids -> List.concat_map leaves kids | e -> [ e ]
let box_of = function
| Button (b, _, _, _) | Label (b, _) | Field (b, _, _, _) | Slider (b, _, _, _, _) | Progress (b, _) | Menu (b, _, _, _) | Canvas (b, _, _) -> b
| Context_menu (at, items, _) -> Look.context_box Theme.default at items
| Group _ -> assert false
let takes_keys = function Field (_, _, _, enabled) -> enabled | _ -> false
let enabled = function Button (_, _, _, e) | Field (_, _, _, e) -> e | Label _ | Progress _ | Context_menu _ -> false | _ -> true
let events (th : Theme.t) (i : Widget.input) (t : t) view =
let pressed k = List.mem k i.keys && not (List.mem k t.keys_before) in
let press = i.mdown && not t.was_down in
let rpress = i.mrdown && not t.was_rdown in
let widgets = leaves view in
let id e = Widget.id (box_of e) in
let grabbed = List.exists (function Context_menu _ -> true | _ -> false) widgets in
let hot e =
enabled e && Widget.contains (box_of e) i.mx i.my
&& (match t.open_menu with Some m -> m = id e | None -> true)
&& not grabbed
in
let fields = List.filter takes_keys widgets in
let focus =
if pressed "Tab" then (
let order = if List.mem "Shift" i.keys then List.rev fields else fields in
let rec after = function
| [] -> ( match order with e :: _ -> Some (id e) | [] -> None)
| [ last ] -> if Some (id last) = t.focus then (match order with e :: _ -> Some (id e) | [] -> None) else after []
| a :: (b :: _ as rest) -> if Some (id a) = t.focus then Some (id b) else after rest
in
after order)
else t.focus
in
let held =
if press then List.find_opt hot widgets |> Option.map id
else if i.mdown || i.mclick then t.held
else None
in
let clicked = if i.mclick then List.find_opt (fun e -> hot e && Some (id e) = held) widgets else None in
let focus =
match clicked with
| Some e when takes_keys e -> Some (id e)
| Some _ -> focus
| None -> if i.mclick && held = None then None else focus
in
let caret =
match (clicked, focus) with
| Some (Field (b, text, _, _) as e), _ when Some (id e) = focus ->
Text.byte_of_column text (Look.field_column_at th b text ~caret:t.caret i.mx)
| _ ->
if focus <> t.focus then
match List.find_opt (fun e -> Some (id e) = focus) widgets with
| Some (Field (_, text, _, _)) -> String.length text
| _ -> t.caret
else t.caret
in
let e =
match e with
| Menu (b, items, _, _) when t.open_menu = Some (id e) ->
List.find_opt (fun k -> Widget.contains (Look.menu_item th b k) i.mx i.my) (List.init (List.length items) Fun.id)
| _ -> None
in
let =
match clicked with
| Some (Menu _ as e) -> if t.open_menu = Some (id e) then None else Some (id e)
| _ -> if i.mclick then None else t.open_menu
in
let msgs =
List.concat_map
(fun e ->
match e with
| Button (_, _, msg, _) when clicked = Some e -> [ msg ]
| Field (_, text, to_msg, _) when Some (id e) = focus ->
let after, _ = Text.edit ~typed:i.typed ~pressed text caret in
if after <> text then [ to_msg after ] else []
| Slider (b, from, to_, _, to_msg) when held = Some (id e) && i.mdown -> (
match Look.slider_value th b ~from ~to_ i.mx with Some v -> [ to_msg v ] | None -> [])
| Menu (_, _, _, to_msg) when i.mclick -> ( match menu_under e with Some k -> [ to_msg k ] | None -> [])
| Canvas (_, _, to_msg) ->
let at = (i.mx, i.my) in
List.filter_map to_msg
((if hot e then [ Widget.Hover at ] else [])
@ (if press && held = Some (id e) then [ Widget.Press at ] else [])
@ if rpress && hot e then [ Widget.Right_press at ] else [])
| Context_menu (at, items, to_msg) when i.mclick ->
let b = Look.context_box th at items in
[ to_msg (List.find_opt (fun k -> Widget.contains (Look.menu_item th b k) i.mx i.my) (List.init (List.length items) Fun.id)) ]
| _ -> [])
widgets
in
let caret =
match List.find_opt (fun e -> Some (id e) = focus) widgets with
| Some (Field (_, text, _, _)) -> snd (Text.edit ~typed:i.typed ~pressed text caret)
| _ -> caret
in
let t' = { focus; caret; was_down = i.mdown; was_rdown = i.mrdown; keys_before = i.keys; held = (if i.mdown then held else None); open_menu } in
let , others = List.partition (function Context_menu _ -> true | _ -> false) widgets in
let paint =
List.concat_map
(fun e ->
match e with
| Button (b, s, _, enabled) -> Look.button th b s ~hot:(hot e) ~held:(Some (id e) = held && i.mdown) ~enabled
| Label (b, s) -> Look.label th b s
| Field (b, text, _, enabled) ->
Look.field th b text ~caret:(if Some (id e) = focus && enabled then Some caret else None) ~enabled
| Slider (b, from, to_, v, _) ->
let fraction = if to_ = from then 0. else max 0. (min 1. ((v -. from) /. (to_ -. from))) in
Look.slider th b ~fraction ~hot:(hot e) ~held:(Some (id e) = held && i.mdown)
| Progress (b, fraction) -> Look.progress th b fraction
| Menu (b, items, chosen, _) ->
let label = match List.nth_opt items chosen with Some s -> s | None -> "" in
Look.menu_closed th b label ~hot:(hot e) ~held:(Some (id e) = held && i.mdown)
@ if open_menu = Some (id e) then Look.menu_items th b items ~under:(menu_under e) else []
| Canvas (_, drawing, _) -> drawing
| Context_menu (at, items, _) ->
let b = Look.context_box th at items in
Look.menu_items th b items
~under:(List.find_opt (fun k -> Widget.contains (Look.menu_item th b k) i.mx i.my) (List.init (List.length items) Fun.id))
| Group _ -> [])
(others @ menus)
in
(t', msgs, paint)
let step (th : Theme.t) (i : Widget.input) (t : t) ~view ~update model =
let t, msgs, _ = events th i t (view model) in
let model = List.fold_left (fun m msg -> update msg m) model msgs in
let _, _, paint = events th { i with typed = ""; mclick = false; keys = t.keys_before } t (view model) in
(t, model, paint)