Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions platform/native/lui_gallery/lui_gallery.ml
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ let hooks_of ?(text_ops = fun id r c s -> stub_text_ops id r c s) e =
| Some s -> s
| None -> Lui_paint.state_neutral);
scroll_of = (fun id -> Hashtbl.find_opt e.scrolls id);
mounted_children = (fun _ -> None);
text_ops;
image_of =
(fun id -> if Hashtbl.mem e.images id then Some test_image else None);
Expand Down
81 changes: 76 additions & 5 deletions platform/native/lui_host/lui_host.ml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ module type LAYOUT_ENGINE = sig
val sync :
?scale:float -> ?measure:text_measure -> ?hidden:(int -> bool) ->
?ctx_anchor:(float * float) ->
?vport:(int -> (float * float) option) ->
width:float -> height:float -> t -> Lui_store.t -> unit

val invalidate : t -> unit
Expand All @@ -35,6 +36,17 @@ module type LAYOUT_ENGINE = sig

val rect : t -> int -> Lui_scene.rect option

(* Virtual-list surface: engines without virtualization return
[[]]/[None] everywhere, which reproduces the unwindowed path. *)
val vlists : t -> int list
val mounted_range : t -> int -> (int * int) option
val mounted_children : t -> int -> int list option
val virtual_extent : t -> int -> float option
val virtual_item_rect : t -> int -> index:int -> Lui_scene.rect option
val virtual_index_of_key : t -> Lui_store.t -> int -> string -> int option
val virtual_index_of_id : t -> Lui_store.t -> int -> int -> int option
val virtual_view : t -> int -> (float * float) option

(** Store ids whose absolute rect changed under the last {!sync};
drained by the paint pass to re-emit only moved subtrees. *)
val take_rect_changes : t -> (int, unit) Hashtbl.t
Expand All @@ -46,10 +58,19 @@ type layout_state = {
lsync :
scale:float -> measure:text_measure -> hidden:(int -> bool) ->
ctx_anchor:(float * float) option ->
vport:(int -> (float * float) option) ->
width:float -> height:float -> Lui_store.t -> unit;
linvalidate : unit -> unit;
lhook : int -> Lui_paint.placement;
lrect : int -> rect option;
llists : unit -> int list;
lrange : int -> (int * int) option;
lchildren : int -> int list option;
lextent : int -> float option;
litem : int -> index:int -> rect option;
lkey : int -> string -> int option;
lid : int -> int -> int option;
lview : int -> (float * float) option;
lchanges : unit -> (int, unit) Hashtbl.t;
}

Expand Down Expand Up @@ -77,36 +98,49 @@ type t = {
(* Pointer point an unanchored context-menu opens at — a thunk so the
accessor reads the live position each repaint. *)
mutable ctx_anchor : unit -> (float * float) option;
(* Virtual-list viewport feed: node id → content-space visible range
(start, extent) evaluated during every layout sync. *)
mutable vport : int -> (float * float) option;
}

let no_measure _id _text _offered_w _offered_h = None

let create ~hooks ?layout ?(measure = no_measure) ~renderer ~width ~height ~scale () =
let store = Lui_store.create () in
let layout =
match layout with
| None -> None
| Some (module L : LAYOUT_ENGINE) ->
let engine = L.create () in
Some
{ lsync =
(fun ~scale ~measure ~hidden ~ctx_anchor ~width ~height store ->
L.sync ~scale ~measure ~hidden ?ctx_anchor ~width ~height
(fun ~scale ~measure ~hidden ~ctx_anchor ~vport ~width ~height store ->
L.sync ~scale ~measure ~hidden ?ctx_anchor ~vport ~width ~height
engine store);
linvalidate = (fun () -> L.invalidate engine);
lhook = L.layout_hook engine;
lrect = L.rect engine;
llists = (fun () -> L.vlists engine);
lrange = (fun id -> L.mounted_range engine id);
lchildren = (fun id -> L.mounted_children engine id);
lextent = (fun id -> L.virtual_extent engine id);
litem = (fun id ~index -> L.virtual_item_rect engine id ~index);
lkey = (fun id key -> L.virtual_index_of_key engine store id key);
lid = (fun id cid -> L.virtual_index_of_id engine store id cid);
lview = (fun id -> L.virtual_view engine id);
lchanges = (fun () -> L.take_rect_changes engine) }
in
let mask_atlas = Atlas.create ~bpp:1 ~w:512 ~h:512 in
let color_atlas = Atlas.create ~bpp:4 ~w:512 ~h:512 in
{ store = Lui_store.create ();
{ store;
scene = Lui_scene.create ~mask_atlas ~color_atlas;
hooks; layout; measure; renderer; width; height; scale;
dirty = false; retained = Lui_retained.create ();
paint_hints = Hashtbl.create 16;
moved_hints = Hashtbl.create 16;
hidden = (fun _ -> false);
ctx_anchor = (fun () -> None) }
ctx_anchor = (fun () -> None);
vport = (fun _ -> None) }

let store h = h.store
let scene h = h.scene
Expand All @@ -120,6 +154,39 @@ let set_ctx_anchor h accessor =
h.ctx_anchor <-
(match accessor with Some f -> f | None -> fun () -> None)

(* Install the virtual-list viewport feed: [f id] reports the node's
content-space visible range (start, extent, device px) — scroll
offset for a directly-scrolled list, the mapped clip for one nested
in an outer scroller. Evaluated inside every layout sync. *)
let set_vport h f = h.vport <- f

(* Virtual-list queries the driver needs for hit-test slicing, scroll
caps and scroll-to-item: all forward to the layout engine and
answer [[]]/[None] when no engine is installed. *)
let layout_vlists h =
match h.layout with Some l -> l.llists () | None -> []

let layout_mounted_range h id =
match h.layout with Some l -> l.lrange id | None -> None

let layout_mounted_children h id =
match h.layout with Some l -> l.lchildren id | None -> None

let layout_virtual_extent h id =
match h.layout with Some l -> l.lextent id | None -> None

let layout_virtual_item h id ~index =
match h.layout with Some l -> l.litem id ~index | None -> None

let layout_virtual_index_of_key h id key =
match h.layout with Some l -> l.lkey id key | None -> None

let layout_virtual_index_of_id h id cid =
match h.layout with Some l -> l.lid id cid | None -> None

let layout_virtual_view h id =
match h.layout with Some l -> l.lview id | None -> None

(* The layout engine memoizes on (store revision, frame, input
identities); a measure/hidden closure whose answers changed under a
stable identity — e.g. a font stack that finished loading — needs
Expand Down Expand Up @@ -170,7 +237,7 @@ let repaint h =
paints) and the current frame size — layout is the single
place that resolves both. *)
l.lsync ~scale:h.scale ~measure:h.measure ~hidden:h.hidden
~ctx_anchor:(h.ctx_anchor ())
~ctx_anchor:(h.ctx_anchor ()) ~vport:h.vport
~width:(float_of_int h.width) ~height:(float_of_int h.height)
h.store;
(* The engine hook yields to an explicitly supplied
Expand All @@ -195,7 +262,11 @@ let repaint h =
| Some c -> c
| None -> { r = 0; g = 0; b = 0; a = 0 }
in
let vlists =
match h.layout with Some l -> Some (l.llists ()) | None -> None
in
Lui_retained.paint_retained h.retained hooks h.store h.scene ?moved
?vlists
~hints:h.paint_hints ~width:h.width ~height:h.height ~scale:h.scale
~clear ();
Hashtbl.reset h.paint_hints;
Expand Down
33 changes: 33 additions & 0 deletions platform/native/lui_host/lui_host.mli
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ module type LAYOUT_ENGINE = sig
val sync :
?scale:float -> ?measure:text_measure -> ?hidden:(int -> bool) ->
?ctx_anchor:(float * float) ->
?vport:(int -> (float * float) option) ->
width:float -> height:float -> t -> Lui_store.t -> unit

val invalidate : t -> unit
Expand All @@ -39,6 +40,17 @@ module type LAYOUT_ENGINE = sig

val rect : t -> int -> Lui_scene.rect option

(* Virtual-list surface: engines without virtualization return
[[]]/[None] everywhere, which reproduces the unwindowed path. *)
val vlists : t -> int list
val mounted_range : t -> int -> (int * int) option
val mounted_children : t -> int -> int list option
val virtual_extent : t -> int -> float option
val virtual_item_rect : t -> int -> index:int -> Lui_scene.rect option
val virtual_index_of_key : t -> Lui_store.t -> int -> string -> int option
val virtual_index_of_id : t -> Lui_store.t -> int -> int -> int option
val virtual_view : t -> int -> (float * float) option

(** Store ids whose absolute rect changed under the last {!sync};
drained by the paint pass to re-emit only moved subtrees. *)
val take_rect_changes : t -> (int, unit) Hashtbl.t
Expand Down Expand Up @@ -137,4 +149,25 @@ val layout_rect : t -> int -> Lui_scene.rect option
the hit-testing surface for the window driver. [None] when the host
has no engine or the mirror skipped the id. *)

val set_vport : t -> (int -> (float * float) option) -> unit
(** Install the virtual-list viewport feed: [f id] reports the node's
content-space visible range (start, extent, device px) — the scroll
offset for a directly-scrolled list, or the on-screen clip mapped
into the list's content when an outer scroller clips it. Evaluated
inside every engine sync, including memo hits. *)

(** {2 Virtual lists}

Forwards to the layout engine's virtual-list surface; all answer
[[]]/[None] when no engine is installed. *)

val layout_vlists : t -> int list
val layout_mounted_range : t -> int -> (int * int) option
val layout_mounted_children : t -> int -> int list option
val layout_virtual_extent : t -> int -> float option
val layout_virtual_item : t -> int -> index:int -> Lui_scene.rect option
val layout_virtual_index_of_key : t -> int -> string -> int option
val layout_virtual_index_of_id : t -> int -> int -> int option
val layout_virtual_view : t -> int -> (float * float) option

val backend : t -> Lui_protocol.platform_profile -> Lui_protocol.backend
116 changes: 113 additions & 3 deletions platform/native/lui_input/lui_scroll.ml
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,27 @@ type state = {
(* tracked-list ids found by the last store scan *)
}

(* Layout-engine forwards for virtual-list math: item counts,
content-space item rects (mounted or not), key → index lookup and
the last fed viewport — the pieces scroll-target, visible-range
and reveal logic need on logical items that may not be mounted. *)
type vlist = {
vh_count : int -> int option;
vh_item : int -> index:int -> rect option;
vh_index_of_key : int -> string -> int option;
vh_view : int -> (float * float) option;
}

type env = {
content_rect : int -> rect;
(* unshifted (content-space) rect — the driver's layout view *)
rects_epoch : int;
(* layout epoch; bumped whenever the rects feeding
[content_rect] were recomputed *)
vlist : vlist option;
(* extent model for "virtual-list" containers; [None] keeps the
mounted-children path, which is also what engines without
virtualization answer *)
}

let initial =
Expand Down Expand Up @@ -138,6 +153,50 @@ let report_vrange env store st id =
( { st with vrange = IdMap.add id (f, l) st.vrange },
[ `Dispatch (VisibleRange (id, f, l)) ] ))
end
else if Lui_query.kind_name store id = "virtual-list" then
(* Same report over the extent model: the fed viewport and a
binary search on item rects — no item walk at any size. *)
match
( env.vlist,
Lui_query.data_attr store id "data-track-visible-range" )
with
| Some vh, Some "true" -> (
match (vh.vh_view id, vh.vh_count id) with
| Some (vs, ve), Some n when n > 0 ->
let key = (Lui_store.revision store, vs, env.rects_epoch) in
(match IdMap.find_opt id st.vrange_key with
| Some k when k = key -> (st, [])
| _ ->
let st =
{ st with vrange_key = IdMap.add id key st.vrange_key }
in
let cr = env.content_rect id in
if rect_empty cr then (st, [])
else begin
(* Largest index whose content-space top is ≤ y; -1
before the first item. *)
let item_at y =
let rec go lo hi =
if lo > hi then hi
else
let mid = (lo + hi) / 2 in
match vh.vh_item id ~index:mid with
| Some r when r.Lui_scene.y -. cr.Lui_scene.y <= y ->
go (mid + 1) hi
| _ -> go lo (mid - 1)
in
go 0 (n - 1)
in
let f = max 0 (item_at vs) in
let l = max 0 (item_at (vs +. ve -. 0.001)) in
match IdMap.find_opt id st.vrange with
| Some (f', l') when f' = f && l' = l -> (st, [])
| _ ->
( { st with vrange = IdMap.add id (f, l) st.vrange },
[ `Dispatch (VisibleRange (id, f, l)) ] )
end)
| _ -> (st, []))
| _ -> (st, [])
else (st, [])

let set_offset env store st axis id off =
Expand Down Expand Up @@ -294,7 +353,54 @@ let sync env store st =
acts :=
`Dispatch (ScrollCompleted (id, tok, "missing-target"))
:: !acts))
| None -> ())
| None -> ()
else if Lui_query.kind_name store id = "virtual-list" then
(* The schema wires scroll-target on ListContainer only;
virtual lists take the same triple over data-attrs and
resolve the key through the extent model — the target
row does not need to be mounted. *)
match
( Option.bind
(Lui_query.data_attr store id "data-scroll-token")
(fun s -> int_of_string_opt (String.trim s)),
Lui_query.data_attr store id "data-scroll-target",
env.vlist )
with
| Some tok, Some target, Some vh when target <> "" -> (
match IdMap.find_opt id !st.tokens with
| Some prev when prev = tok -> ()
| _ ->
st := { !st with tokens = IdMap.add id tok !st.tokens };
(match vh.vh_index_of_key id target with
| Some i ->
let cid = Lui_store.child_id_at store id i in
if cid >= 0 then begin
let anchor =
match
Lui_query.data_attr store id "data-scroll-anchor"
with
| Some a -> a
| None -> ""
in
let st', a =
scroll_align env store !st id cid anchor
in
st := st';
acts := List.rev_append a !acts;
acts :=
`Dispatch (ScrollCompleted (id, tok, "succeeded"))
:: !acts
end
else
acts :=
`Dispatch
(ScrollCompleted (id, tok, "missing-target"))
:: !acts
| None ->
acts :=
`Dispatch (ScrollCompleted (id, tok, "missing-target"))
:: !acts))
| _ -> ())
(Lui_store.preorder store);
(* The tracked-list ids are store-derived too — refreshed under
the same gate so the per-sync report loop only walks actual
Expand All @@ -304,8 +410,12 @@ let sync env store st =
lists =
List.filter
(fun id ->
Lui_query.kind_name store id = "list"
&& Lui_store.bool_prop store id "track-visible-range")
let k = Lui_query.kind_name store id in
(k = "list"
&& Lui_store.bool_prop store id "track-visible-range")
|| (k = "virtual-list"
&& Lui_query.data_attr store id "data-track-visible-range"
= Some "true"))
(Lui_store.preorder store) };
(!st, List.rev !acts)

Expand Down
3 changes: 2 additions & 1 deletion platform/native/lui_input/test/lui_input_test.ml
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,8 @@ let scroll_env () =
(fun id ->
if id = 5 then rect 0. 0. 100. 100.
else rect 0. 0. 100. 20.);
rects_epoch = 1 }
rects_epoch = 1;
vlist = None }

let test_scroll_offsets () =
let store = store_of base_ops in
Expand Down
Loading
Loading