Source file partial_render_list.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
open! Core
open Poly
open! Import
open Util
include Partial_render_list_intf
module Make (Row_id : Row_id) (Sort_key : Sort_key with type row_id := Row_id.t) = struct
module Row_id = Row_id
module Sort_key = Sort_key
module Height_cache = struct
type t =
{ cache : float Row_id.Map.t
; height_guess : float
}
[@@deriving fields ~getters, compare, sexp_of]
let empty ~height_guess = { cache = Row_id.Map.empty; height_guess }
let height t row_id = Option.value (Map.find t.cache row_id) ~default:t.height_guess
end
module Heights = struct
include
Splay_tree.Make_with_reduction (Sort_key) (Float)
(struct
type key = Sort_key.t
type data = float
type accum = float
let identity = 0.
let singleton ~key:_ ~data = data
let combine left right = left +. right
end)
(** Returns the row (if any) that is at the specified position *)
let find_by_position (heights : t) position =
search heights ~f:(fun ~left ~right:_ -> if position < left then `Left else `Right)
|> Option.map ~f:fst
;;
(** The cumulative height of all the rows *)
let height = accum
let get_position_and_height t key =
let before, at, (_ : t) = split t key in
height before, at
;;
end
type 'v t =
{ heights : Heights.t (** Acceleration structure for height queries *)
; render_range : Sort_key.t Interval.t
(** Section of keys to put in DOM
This includes extra rows above and below what is actually visible *)
; rows_to_render : 'v Sort_key.Map.t (** Full map of [render_range] *)
; measurements : Measurements.t option
; height_cache : Height_cache.t
(** The height cache is stashed here after trimming so that it can be
accessed later in measure_heights. This way the app doesn't have to
store it in its derived model and pass it back to us. The app still
stores the height cache in its model, it just doesn't also have to store
a trimmed version in its derived model.*)
; min_key : Sort_key.t option
; max_key : Sort_key.t option
}
[@@deriving fields ~getters]
(** How many extra rows will be rendered outside of visible range. Must be even to
preserve parity for alternating row colours. *)
let render_width = 6
let find_by_position t ~position = Heights.find_by_position t.heights position
let focus_offset_to_position t key ~offset =
let key_position, key_height = Heights.get_position_and_height t.heights key in
let key_height = Option.value key_height ~default:0. in
match Ordering.of_int (Float.compare offset 0.) with
| Equal -> key_position
| Less -> key_position +. offset +. key_height
| Greater -> key_position +. offset
;;
let find_by_relative_position t key ~offset =
let find ~default =
let position = focus_offset_to_position t key ~offset in
match find_by_position t ~position with
| Some key -> Some key
| None -> default
in
match Ordering.of_int (Float.compare offset 0.) with
| Equal -> Some key
| Less -> find ~default:t.min_key
| Greater -> find ~default:t.max_key
;;
let get_visible_range
~(measurements : Measurements.t option Incr.t)
~(heights : Heights.t Incr.t)
~(rows : _ Sort_key.Map.t Incr.t)
=
let%map measurements = measurements
and heights = heights
and rows = rows in
match measurements with
| None -> Interval.Empty
| Some { list_rect; view_rect } ->
let module Rect = Js_misc.Rect in
let scroll_top = Rect.top view_rect -. Rect.top list_rect in
let scroll_bot = scroll_top +. Rect.float_height view_rect in
let visible_range : _ Interval.t =
if scroll_top >= Heights.height heights || scroll_bot <= 0.
then Empty
else (
let key_top =
match Heights.find_by_position heights scroll_top with
| Some x -> Some x
| None -> Map.min_elt rows |> Option.map ~f:fst
in
let key_bot =
match Heights.find_by_position heights scroll_bot with
| Some x -> Some x
| None -> Map.max_elt rows |> Option.map ~f:fst
in
match key_top, key_bot with
| None, _ | _, None -> Empty
| Some top, Some bot -> Range (top, bot))
in
visible_range
;;
let create ~rows ~height_cache ~measurements =
let height_guess = height_cache >>| Height_cache.height_guess in
let key_to_height_guess =
let%bind height_guess = height_guess in
Incr.Map.map
~data_equal:(fun _ _ -> true)
~instrumentation:(instrument "key_to_height_guess")
rows
~f:(fun _ -> height_guess)
in
let row_ids =
Incr.Map.unordered_fold
key_to_height_guess
~revert_to_init_when_empty:true
~instrumentation:(instrument "row_ids")
~init:Row_id.Map.empty
~add:(fun ~key ~data:_ map ->
let row_id = Sort_key.row_id key in
let count = Option.value (Map.find map row_id) ~default:0 in
Map.set map ~key:row_id ~data:(count + 1))
~remove:(fun ~key ~data:_ map ->
let row_id = Sort_key.row_id key in
match Map.find map row_id with
| None -> assert false
| Some 1 -> Map.remove map row_id
| Some count ->
assert (count > 1);
Map.set map ~key:row_id ~data:(count - 1))
in
let trimmed_height_cache =
Incr.Map.merge_both_some
row_ids
(height_cache >>| Height_cache.cache)
~instrumentation:(instrument "trimmed_height_cache")
~data_equal_left:(fun _ _ -> true)
~f:(fun ~key:_ _ h -> h)
in
let row_heights =
match%pattern_bind trimmed_height_cache >>| Map.length with
| 0 -> key_to_height_guess
| _ ->
let height_lookup =
Incr.Map.Lookup.create
trimmed_height_cache
~instrumentation:(instrument "height_lookup")
~comparator:Row_id.comparator
~data_equal:Float.equal
in
Incr.Map.mapi'
key_to_height_guess
~instrumentation:(instrument "row_heights")
~data_equal:(fun _ _ -> true)
~f:(fun ~key ~data:height_guess ->
let%mapn lookup = Incr.Map.Lookup.find height_lookup (Sort_key.row_id key)
and height_guess = height_guess in
match lookup with
| Some height -> height
| None -> height_guess)
in
let heights =
Incr.Map.unordered_fold
row_heights
~instrumentation:(instrument "heights")
~init:Heights.empty
~add:(fun ~key ~data acc -> Heights.set acc ~key ~data)
~remove:(fun ~key ~data:_ acc -> Heights.remove acc key)
in
let visible_range = get_visible_range ~measurements ~heights ~rows in
let render_range =
let%map visible_range = visible_range
and key_to_height_guess = key_to_height_guess
and heights = heights in
let parity_fix key =
let num_before = Heights.rank heights key in
num_before % 2
in
let rec move start n get_next =
if n <= 0
then start
else (
match get_next start with
| None -> start
| Some next -> move next (n - 1) get_next)
in
let move start direction amount =
move start amount (fun x ->
Map.closest_key key_to_height_guess direction x |> Option.map ~f:fst)
in
match (visible_range : _ Interval.t) with
| Empty -> Interval.Empty
| Range (top, bot) ->
Interval.Range
( move top `Less_than (render_width + parity_fix top)
, move bot `Greater_than render_width )
in
let rows_to_render =
let sub_range =
match%map render_range with
| Empty -> None
| Range (x, y) -> Some (Incl x, Incl y)
in
Incr.Map.subrange ~instrumentation:(instrument "subrange") rows sub_range
in
let min_and_max_key =
let%map key_to_height_guess = key_to_height_guess in
( Option.map (Map.min_elt key_to_height_guess) ~f:fst
, Option.map (Map.max_elt key_to_height_guess) ~f:fst )
in
let height_cache =
let%map height_guess = height_guess
and trimmed_height_cache = trimmed_height_cache in
{ Height_cache.height_guess; cache = trimmed_height_cache }
in
let%map heights = heights
and rows_to_render = rows_to_render
and render_range = render_range
and height_cache = height_cache
and measurements = measurements
and min_key, max_key = min_and_max_key in
{ heights
; render_range
; rows_to_render
; measurements
; height_cache
; min_key
; max_key
}
;;
let spacer_heights t =
let%map render_range = t >>| render_range
and heights = t >>| heights in
match (render_range : _ Interval.t) with
| Empty -> 0., Heights.height heights
| Range (min_key, max_key) ->
let { Heights.Partition.lt; gt; _ } = Heights.partition heights ~min_key ~max_key in
Heights.height lt, Heights.height gt
;;
let call_scroll_function t ~key ~f =
Option.bind t.measurements ~f:(fun { Measurements.list_rect; view_rect } ->
let position, height = Heights.get_position_and_height t.heights key in
Option.map height ~f:(fun height ->
let elem_start = position +. list_rect.top in
f
~scroll_region_start:view_rect.top
~scroll_region_end:view_rect.bottom
~elem_start
~elem_end:(elem_start +. height)))
;;
let scroll_into_scroll_region ?in_ t ~top_margin ~bottom_margin ~key =
let f =
Scroll.scroll_into_region
?in_
Vertical
~start_margin:top_margin
~end_margin:bottom_margin
in
Option.value (call_scroll_function t ~key ~f) ~default:`Didn't_scroll
;;
let scroll_to_position ?in_ t ~position ~key =
let f ~scroll_region_start ~scroll_region_end:_ ~elem_start ~elem_end:_ =
Scroll.scroll_to_position ?in_ Vertical ~position ~scroll_region_start ~elem_start
in
Option.value (call_scroll_function t ~key ~f) ~default:`Didn't_scroll
;;
let scroll_to_position_and_into_region ?in_ t ~position ~top_margin ~bottom_margin ~key =
let f =
Scroll.scroll_to_position_and_into_region
?in_
Vertical
~position
~start_margin:top_margin
~end_margin:bottom_margin
in
Option.value (call_scroll_function t ~key ~f) ~default:`Didn't_scroll
;;
let is_in_region t ~top_margin ~bottom_margin ~key =
let f = Scroll.is_in_region ~start_margin:top_margin ~end_margin:bottom_margin in
call_scroll_function t ~key ~f
;;
let get_position t ~key =
let f ~scroll_region_start ~scroll_region_end:_ ~elem_start ~elem_end:_ =
Scroll.get_position ~scroll_region_start ~elem_start
in
call_scroll_function t ~key ~f
;;
let get_top_and_bottom t ~key =
let f ~scroll_region_start ~scroll_region_end:_ ~elem_start ~elem_end =
let top = Scroll.get_position ~scroll_region_start ~elem_start in
top, top +. elem_end -. elem_start
in
call_scroll_function t ~key ~f
;;
let update_cache cache ~height_guess ~key height =
match height with
| None -> cache
| Some height ->
let float_approx_equal f1 f2 = Float.(abs (f1 - f2) < 0.001) in
if float_approx_equal height height_guess
then Map.remove cache key
else if Option.equal float_approx_equal (Map.find cache key) (Some height)
then cache
else Map.set cache ~key ~data:height
;;
let measure_heights_simple t ~measure =
let cache =
Map.fold t.rows_to_render ~init:t.height_cache.cache ~f:(fun ~key ~data:_ cache ->
update_cache
cache
~height_guess:t.height_cache.height_guess
~key:(Sort_key.row_id key)
(measure key))
in
{ t.height_cache with cache }
;;
type 'm measure_heights_acc =
{ cache : float Row_id.Map.t
; prev : 'm option
; current : (Row_id.t * 'm option) option
}
let measure_heights t ~measure_row ~get_row_height =
let update_cache cache ~current ~prev ~next =
match current with
| None -> cache
| Some (key, curr) ->
update_cache
cache
~height_guess:t.height_cache.height_guess
~key
(get_row_height ~prev ~curr ~next)
in
let cache =
let { cache; prev; current } =
Map.fold
t.rows_to_render
~init:{ cache = t.height_cache.cache; prev = None; current = None }
~f:(fun ~key:next_key ~data:_ { cache; prev; current } ->
let next = measure_row next_key in
{ cache = update_cache cache ~current ~prev ~next
; prev = Option.bind current ~f:Tuple2.get2
; current = Some (Sort_key.row_id next_key, next)
})
in
update_cache cache ~current ~prev ~next:None
in
{ t.height_cache with cache }
;;
end
module Make_simple (Row_id : Row_id) =
Make
(Row_id)
(struct
include Row_id
let row_id = Fn.id
end)