Source file navigable_tree.ml
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
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
open! Core
open Bonsai.Let_syntax
module Node = struct
type 'a t =
| Branch of
{ value : 'a
; children : 'a t list
; expanded : bool
}
| Leaf of 'a
[@@deriving sexp, equal]
let value = function
| Branch { value; _ } -> value
| Leaf value -> value
;;
let is_branch = function
| Branch _ -> true
| Leaf _ -> false
;;
let children = function
| Branch { children; expanded; _ } -> if expanded then children else []
| Leaf _ -> []
;;
let is_expanded = function
| Branch { expanded; _ } -> expanded
| Leaf _ -> false
;;
end
type 'a t = 'a Node.t [@@deriving sexp, equal]
module Row = struct
type 'a t =
{ value : 'a
; depth : int
; is_branch : bool
; is_expanded : bool
; is_last_sibling : bool
; ancestor_is_last : bool list
}
end
let flatten (tree : 'a t) : 'a Row.t list =
let rec go_children ~depth ~ancestor_is_last nodes =
let num_nodes = List.length nodes in
List.concat_mapi nodes ~f:(fun i node ->
let is_last_sibling = i = num_nodes - 1 in
let row =
{ Row.value = Node.value node
; depth
; is_branch = Node.is_branch node
; is_expanded = Node.is_expanded node
; is_last_sibling
; ancestor_is_last
}
in
let children_rows =
go_children
~depth:(depth + 1)
~ancestor_is_last:(ancestor_is_last @ [ is_last_sibling ])
(Node.children node)
in
row :: children_rows)
in
go_children ~depth:0 ~ancestor_is_last:[] [ tree ]
;;
let toggle_at_index (tree : 'a t) ~(index : int) : 'a t =
let current_index = ref 0 in
let rec go_node node =
let this_index = !current_index in
incr current_index;
if this_index = index
then (
match node with
| Node.Branch { value; children; expanded } ->
if expanded
then Node.Branch { value; children; expanded = false }
else (
let new_node = Node.Branch { value; children; expanded = true } in
ignore (go_children children : 'a Node.t list);
new_node)
| Node.Leaf _ -> node)
else (
match node with
| Node.Branch { value; children; expanded } ->
if expanded
then (
let new_children = go_children children in
Node.Branch { value; children = new_children; expanded })
else node
| Node.Leaf _ -> node)
and go_children nodes = List.map nodes ~f:go_node in
go_node tree
;;
let count_visible (tree : 'a t) : int = List.length (flatten tree)
let render_prefix ~(row : _ Row.t) ~strip_depth : string =
if row.depth <= strip_depth
then ""
else (
let prefix_parts =
List.drop row.ancestor_is_last (strip_depth + 1)
|> List.map ~f:(fun is_last -> if is_last then " " else "│ ")
in
let final =
if row.is_last_sibling then "└─ " else "├─ "
in
String.concat (prefix_parts @ [ final ]))
;;
module Action = struct
type 'a t =
| Move_up
| Move_down
| Toggle_expand
| Select_parent
| Expand_or_select_next_at_lower_depth
| Replace_leaf of ('a -> 'a Node.t option)
(** Replace matching leaf nodes (including root) with the returned node *)
| Select_path of ('a -> bool)
(** Select the first visible node whose value matches the predicate *)
[@@deriving sexp_of]
end
let rec collect_all_leaves_from_node (node : 'a Node.t) : 'a list =
match node with
| Node.Leaf value -> [ value ]
| Node.Branch { children; _ } ->
List.concat_map children ~f:collect_all_leaves_from_node
;;
let collect_all_leaves_with_path_from_node (node : 'a Node.t) ~(get_name : 'a -> 'name)
: ('a * path:'name list) list
=
let rec go ~path node =
match node with
| Node.Leaf value -> [ value, ~path:(List.rev path) ]
| Node.Branch { children; _ } ->
List.concat_map children ~f:(fun child ->
let child_name = get_name (Node.value child) in
go ~path:(child_name :: path) child)
in
go ~path:[] node
;;
let find_node_at_index (tree : 'a t) ~(index : int) : 'a Node.t option =
let rec walk nodes current_index =
match nodes with
| [] -> None, current_index
| node :: rest ->
if current_index = index
then Some node, current_index
else (
let children = Node.children node in
let result, after_node = walk children (current_index + 1) in
match result with
| Some _ -> result, after_node
| None -> walk rest after_node)
in
fst (walk [ tree ] 0)
;;
module Model = struct
type 'a t =
{ tree : 'a Node.t
; selected_index : int option
; strip_depth : int
}
[@@deriving sexp, equal]
let create tree ~strip_depth = { tree; selected_index = None; strip_depth }
let visible_rows t = flatten t.tree
let num_visible t = count_visible t.tree
let selected_index t = Option.value t.selected_index ~default:0
let is_selection_explicit t = Option.is_some t.selected_index
let selected_row t =
let rows = visible_rows t in
List.nth rows (selected_index t)
;;
let selected_descendants t =
match find_node_at_index t.tree ~index:(selected_index t) with
| None -> []
| Some node -> collect_all_leaves_from_node node
;;
let selected_descendants' t ~get_name =
match find_node_at_index t.tree ~index:(selected_index t) with
| None -> []
| Some node -> collect_all_leaves_with_path_from_node node ~get_name
;;
let visible_values t = visible_rows t |> List.map ~f:(fun (row : _ Row.t) -> row.value)
end
let apply_action (model : 'a Model.t) (action : 'a Action.t) : 'a Model.t =
let current_index = Model.selected_index model in
match action with
| Move_down ->
let num_visible = Model.num_visible model in
if num_visible = 0 || current_index >= num_visible - 1
then model
else { model with selected_index = Some (current_index + 1) }
| Move_up ->
if current_index <= 0
then model
else { model with selected_index = Some (current_index - 1) }
| Toggle_expand ->
let tree = toggle_at_index model.tree ~index:current_index in
let num_visible = count_visible tree in
let new_index = Int.min current_index (num_visible - 1) in
{ model with tree; selected_index = Some new_index }
| Select_parent ->
let rows = Model.visible_rows model in
(match List.nth rows current_index with
| None -> model
| Some current_row ->
if current_row.depth = 0
then model
else (
let parent_depth = current_row.depth - 1 in
let parent_index =
List.foldi rows ~init:None ~f:(fun i acc row ->
if i < current_index && row.is_branch && row.depth = parent_depth
then Some i
else acc)
in
match parent_index with
| Some idx -> { model with selected_index = Some idx }
| None -> model))
| Expand_or_select_next_at_lower_depth ->
let rows = Model.visible_rows model in
(match List.nth rows current_index with
| None -> model
| Some current_row ->
if current_row.is_branch && not current_row.is_expanded
then (
let tree = toggle_at_index model.tree ~index:current_index in
{ model with tree; selected_index = Some current_index })
else if current_row.is_branch && current_row.is_expanded
then (
let num_visible = Model.num_visible model in
if current_index + 1 < num_visible
then { model with selected_index = Some (current_index + 1) }
else model)
else (
let current_depth = current_row.depth in
let next_lower_depth_index =
List.findi rows ~f:(fun i row ->
i > current_index && row.depth < current_depth)
|> Option.map ~f:fst
in
match next_lower_depth_index with
| Some idx -> { model with selected_index = Some idx }
| None -> model))
| Replace_leaf f ->
let rec replace_in_node node =
match node with
| Node.Leaf value -> Option.value (f value) ~default:node
| Node.Branch { value; children; expanded } ->
Node.Branch { value; children = List.map children ~f:replace_in_node; expanded }
in
let new_tree = replace_in_node model.tree in
let num_visible = count_visible new_tree in
let selected_index =
Option.map model.selected_index ~f:(fun idx ->
Int.min idx (Int.max 0 (num_visible - 1)))
in
{ model with tree = new_tree; selected_index }
| Select_path predicate ->
let rows = Model.visible_rows model in
(match List.findi rows ~f:(fun _ row -> predicate row.value) with
| Some (index, _) -> { model with selected_index = Some index }
| None -> model)
;;
(** Per-item display information for rendering. The views should include styling. *)
module Item_display = struct
type t =
{ label : Bonsai_term.View.t
; status : Bonsai_term.View.t
}
end
let render_view
~(render_item : int -> 'a Row.t -> Item_display.t)
~(highlight : Bonsai_term.View.t -> Bonsai_term.View.t)
~(flavor : Bonsai_term_color_scheme.Flavor.t)
(model : 'a Model.t)
: Bonsai_term.View.t
=
let open Bonsai_term in
let subtext_color = Bonsai_term_color_scheme.color ~flavor Subtext0 in
let content_rows = Model.visible_rows model in
let selected_index = Model.selected_index model in
let items, statuses =
List.mapi content_rows ~f:(fun i row ->
let is_selected = selected_index = i in
let display = render_item i row in
let tree_prefix = render_prefix ~row ~strip_depth:model.strip_depth in
let expand_indicator = if row.is_branch && not row.is_expanded then "+ " else "" in
let prefix_attrs = [ Attr.fg subtext_color ] in
let item =
View.hcat
[ View.text ~attrs:prefix_attrs tree_prefix
; View.text ~attrs:prefix_attrs expand_indicator
; display.label
]
in
let status =
View.zcat [ display.status; View.transparent_rectangle ~width:1 ~height:1 ]
in
let maybe_highlight v = if is_selected then highlight v else v in
maybe_highlight item, maybe_highlight status)
|> List.unzip
in
let view = View.hcat [ View.vcat items |> View.pad ~r:1; View.vcat statuses ] in
let selection =
View.vcat
[ View.transparent_rectangle ~width:(View.width view) ~height:selected_index
; highlight (View.rectangle ~width:(View.width view) ~height:1 ())
]
in
View.zcat [ view; selection ]
;;
let calculate_scroll_offset ~selected_index ~visible_height ~total_rows ~current_offset =
let padding = 1 in
if total_rows <= visible_height
then 0
else if selected_index < current_offset + padding
then Int.max 0 (selected_index - padding)
else if selected_index >= current_offset + visible_height - padding
then
Int.min (total_rows - visible_height) (selected_index - visible_height + padding + 1)
else current_offset
;;
let component
(type a)
~(initial_tree : a t)
~(render_item : (int -> a Row.t -> Item_display.t) Bonsai.t)
~(highlight : (Bonsai_term.View.t -> Bonsai_term.View.t) Bonsai.t)
~(dimensions : Bonsai_term.Dimensions.t Bonsai.t)
~strip_depth
(local_ graph)
=
let model, inject =
Bonsai.state_machine
~default_model:(Model.create initial_tree ~strip_depth)
~apply_action:(fun _ model action -> apply_action model action)
graph
in
let scroll_offset, set_scroll_offset = Bonsai.state 0 graph in
let () =
let selected_index =
let%arr model in
Model.selected_index model
in
Bonsai.Edge.on_change
selected_index
~trigger:`After_display
~equal:[%equal: int]
~callback:
(let%arr set_scroll_offset and dimensions and model and scroll_offset in
fun selected_index ->
let visible_height = dimensions.Bonsai_term.Dimensions.height in
let total_rows = Model.num_visible model in
let new_offset =
calculate_scroll_offset
~selected_index
~visible_height
~total_rows
~current_offset:scroll_offset
in
if new_offset <> scroll_offset
then set_scroll_offset new_offset
else Bonsai_term.Effect.Ignore)
graph
in
let view =
let%arr model
and render_item
and flavor = Bonsai_term_color_scheme.flavor graph
and dimensions
and scroll_offset
and highlight in
let full_view = render_view ~render_item ~flavor ~highlight model in
let visible_height = dimensions.Bonsai_term.Dimensions.height in
let total_rows = Bonsai_term.View.height full_view in
if total_rows <= visible_height
then full_view
else (
let crop_top = scroll_offset in
let crop_bottom = Int.max 0 (total_rows - scroll_offset - visible_height) in
Bonsai_term.View.crop ~t:crop_top ~b:crop_bottom full_view)
in
~view, ~model, ~inject
;;