@fusion.dev/helix-core 0.4.7 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/custom-elements.json +327 -8
- package/dist/controllers/virtualize-controller/heights.d.ts +65 -0
- package/dist/controllers/virtualize-controller/heights.js +80 -0
- package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +81 -0
- package/dist/controllers/virtualize-controller/virtualize-controller.js +59 -9
- package/dist/index.d.ts +1 -1
- package/package.json +4 -4
package/custom-elements.json
CHANGED
|
@@ -1645,6 +1645,215 @@
|
|
|
1645
1645
|
}
|
|
1646
1646
|
]
|
|
1647
1647
|
},
|
|
1648
|
+
{
|
|
1649
|
+
"kind": "javascript-module",
|
|
1650
|
+
"path": "src/controllers/virtualize-controller/heights.ts",
|
|
1651
|
+
"declarations": [
|
|
1652
|
+
{
|
|
1653
|
+
"kind": "class",
|
|
1654
|
+
"description": "Where each item starts, when the items are not all the same height.\n\n**The fixed-estimate window is a straight line and this is the other case\nthe controller's own note defers.** With one estimate, offset and index\nconvert by division, which can never disagree with itself. A transcript whose\nlast message grows every token, or a table row that wraps, has no such line:\nthe only way to place item 900 is to know what the 899 above it came to.\n\n**Measured where it has been seen, estimated everywhere else.** An item is\nmeasured once it has been rendered, and until then it stands at the estimate,\nso a set of ten thousand costs ten thousand numbers rather than ten thousand\nlayouts. The estimate is read per call rather than captured, because a host\nmay change it and the sums have to move with it.\n\n**The prefix sums are rebuilt from the first index that changed**, not from\nzero and not on every read. Recording a height marks that index dirty;\nnothing is recomputed until somebody asks a question that needs it. A scroll\nthat records twelve rows therefore pays one rebuild rather than twelve, and a\nscroll that records none pays nothing.",
|
|
1655
|
+
"name": "Heights",
|
|
1656
|
+
"members": [
|
|
1657
|
+
{
|
|
1658
|
+
"kind": "field",
|
|
1659
|
+
"name": "estimate",
|
|
1660
|
+
"type": {
|
|
1661
|
+
"text": "() => number"
|
|
1662
|
+
},
|
|
1663
|
+
"privacy": "private",
|
|
1664
|
+
"readonly": true,
|
|
1665
|
+
"default": "estimate"
|
|
1666
|
+
},
|
|
1667
|
+
{
|
|
1668
|
+
"kind": "field",
|
|
1669
|
+
"name": "seen",
|
|
1670
|
+
"privacy": "private",
|
|
1671
|
+
"readonly": true,
|
|
1672
|
+
"default": "new Map<number, number>()",
|
|
1673
|
+
"description": "Heights that have been seen, by index. Sparse on purpose."
|
|
1674
|
+
},
|
|
1675
|
+
{
|
|
1676
|
+
"kind": "field",
|
|
1677
|
+
"name": "sums",
|
|
1678
|
+
"type": {
|
|
1679
|
+
"text": "number[]"
|
|
1680
|
+
},
|
|
1681
|
+
"privacy": "private",
|
|
1682
|
+
"default": "[0]",
|
|
1683
|
+
"description": "`sums[i]` is the room items `[0, i)` take. One longer than the count."
|
|
1684
|
+
},
|
|
1685
|
+
{
|
|
1686
|
+
"kind": "field",
|
|
1687
|
+
"name": "dirty",
|
|
1688
|
+
"type": {
|
|
1689
|
+
"text": "number"
|
|
1690
|
+
},
|
|
1691
|
+
"privacy": "private",
|
|
1692
|
+
"default": "0",
|
|
1693
|
+
"description": "The first index whose sum is no longer trustworthy."
|
|
1694
|
+
},
|
|
1695
|
+
{
|
|
1696
|
+
"kind": "field",
|
|
1697
|
+
"name": "built",
|
|
1698
|
+
"type": {
|
|
1699
|
+
"text": "number"
|
|
1700
|
+
},
|
|
1701
|
+
"privacy": "private",
|
|
1702
|
+
"default": "0",
|
|
1703
|
+
"description": "The estimate the sums were built against, so a change to it rebuilds."
|
|
1704
|
+
},
|
|
1705
|
+
{
|
|
1706
|
+
"kind": "method",
|
|
1707
|
+
"name": "at",
|
|
1708
|
+
"return": {
|
|
1709
|
+
"type": {
|
|
1710
|
+
"text": "number"
|
|
1711
|
+
}
|
|
1712
|
+
},
|
|
1713
|
+
"parameters": [
|
|
1714
|
+
{
|
|
1715
|
+
"name": "index",
|
|
1716
|
+
"type": {
|
|
1717
|
+
"text": "number"
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
],
|
|
1721
|
+
"description": "How tall one item is: what was measured, or what is assumed."
|
|
1722
|
+
},
|
|
1723
|
+
{
|
|
1724
|
+
"kind": "method",
|
|
1725
|
+
"name": "record",
|
|
1726
|
+
"return": {
|
|
1727
|
+
"type": {
|
|
1728
|
+
"text": "boolean"
|
|
1729
|
+
}
|
|
1730
|
+
},
|
|
1731
|
+
"parameters": [
|
|
1732
|
+
{
|
|
1733
|
+
"name": "index",
|
|
1734
|
+
"type": {
|
|
1735
|
+
"text": "number"
|
|
1736
|
+
}
|
|
1737
|
+
},
|
|
1738
|
+
{
|
|
1739
|
+
"name": "size",
|
|
1740
|
+
"type": {
|
|
1741
|
+
"text": "number"
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
],
|
|
1745
|
+
"description": "Records a height, and says whether it was news.\n\nThe answer matters to the caller rather than here: a height that differs\nfrom what was assumed moves the window, and the controller has to know that\nthe move came from outside rather than from itself."
|
|
1746
|
+
},
|
|
1747
|
+
{
|
|
1748
|
+
"kind": "method",
|
|
1749
|
+
"name": "before",
|
|
1750
|
+
"return": {
|
|
1751
|
+
"type": {
|
|
1752
|
+
"text": "number"
|
|
1753
|
+
}
|
|
1754
|
+
},
|
|
1755
|
+
"parameters": [
|
|
1756
|
+
{
|
|
1757
|
+
"name": "index",
|
|
1758
|
+
"type": {
|
|
1759
|
+
"text": "number"
|
|
1760
|
+
}
|
|
1761
|
+
},
|
|
1762
|
+
{
|
|
1763
|
+
"name": "count",
|
|
1764
|
+
"type": {
|
|
1765
|
+
"text": "number"
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
],
|
|
1769
|
+
"description": "The room items `[0, index)` take."
|
|
1770
|
+
},
|
|
1771
|
+
{
|
|
1772
|
+
"kind": "method",
|
|
1773
|
+
"name": "total",
|
|
1774
|
+
"return": {
|
|
1775
|
+
"type": {
|
|
1776
|
+
"text": "number"
|
|
1777
|
+
}
|
|
1778
|
+
},
|
|
1779
|
+
"parameters": [
|
|
1780
|
+
{
|
|
1781
|
+
"name": "count",
|
|
1782
|
+
"type": {
|
|
1783
|
+
"text": "number"
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
],
|
|
1787
|
+
"description": "The room the whole set takes."
|
|
1788
|
+
},
|
|
1789
|
+
{
|
|
1790
|
+
"kind": "method",
|
|
1791
|
+
"name": "indexAt",
|
|
1792
|
+
"return": {
|
|
1793
|
+
"type": {
|
|
1794
|
+
"text": "number"
|
|
1795
|
+
}
|
|
1796
|
+
},
|
|
1797
|
+
"parameters": [
|
|
1798
|
+
{
|
|
1799
|
+
"name": "offset",
|
|
1800
|
+
"type": {
|
|
1801
|
+
"text": "number"
|
|
1802
|
+
}
|
|
1803
|
+
},
|
|
1804
|
+
{
|
|
1805
|
+
"name": "count",
|
|
1806
|
+
"type": {
|
|
1807
|
+
"text": "number"
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
],
|
|
1811
|
+
"description": "The last item that starts at or before `offset`.\n\nA binary search rather than a walk, so a reader dragging the thumb to the\nend of ten thousand rows costs fourteen comparisons rather than ten\nthousand."
|
|
1812
|
+
},
|
|
1813
|
+
{
|
|
1814
|
+
"kind": "method",
|
|
1815
|
+
"name": "forget",
|
|
1816
|
+
"return": {
|
|
1817
|
+
"type": {
|
|
1818
|
+
"text": "void"
|
|
1819
|
+
}
|
|
1820
|
+
},
|
|
1821
|
+
"description": "Drops everything, for a set that is no longer the same set.\n\nA host that swaps its data has heights recorded against indices that now\nname different items, and keeping them is worse than having none: the\nestimate is honest about not knowing where a stale measurement is not."
|
|
1822
|
+
},
|
|
1823
|
+
{
|
|
1824
|
+
"kind": "method",
|
|
1825
|
+
"name": "build",
|
|
1826
|
+
"privacy": "private",
|
|
1827
|
+
"return": {
|
|
1828
|
+
"type": {
|
|
1829
|
+
"text": "void"
|
|
1830
|
+
}
|
|
1831
|
+
},
|
|
1832
|
+
"parameters": [
|
|
1833
|
+
{
|
|
1834
|
+
"name": "count",
|
|
1835
|
+
"type": {
|
|
1836
|
+
"text": "number"
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
],
|
|
1840
|
+
"description": "Brings the sums up to date, from the first index that changed."
|
|
1841
|
+
}
|
|
1842
|
+
],
|
|
1843
|
+
"helix": {}
|
|
1844
|
+
}
|
|
1845
|
+
],
|
|
1846
|
+
"exports": [
|
|
1847
|
+
{
|
|
1848
|
+
"kind": "js",
|
|
1849
|
+
"name": "Heights",
|
|
1850
|
+
"declaration": {
|
|
1851
|
+
"name": "Heights",
|
|
1852
|
+
"module": "src/controllers/virtualize-controller/heights.ts"
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
]
|
|
1856
|
+
},
|
|
1648
1857
|
{
|
|
1649
1858
|
"kind": "javascript-module",
|
|
1650
1859
|
"path": "src/controllers/virtualize-controller/virtualize-controller.ts",
|
|
@@ -1729,6 +1938,17 @@
|
|
|
1729
1938
|
"default": "{ after: 0, before: 0, first: 0, last: 0 }",
|
|
1730
1939
|
"description": "The slice to render, recomputed on scroll and on resize."
|
|
1731
1940
|
},
|
|
1941
|
+
{
|
|
1942
|
+
"kind": "field",
|
|
1943
|
+
"name": "heights",
|
|
1944
|
+
"type": {
|
|
1945
|
+
"text": "Heights"
|
|
1946
|
+
},
|
|
1947
|
+
"privacy": "private",
|
|
1948
|
+
"readonly": true,
|
|
1949
|
+
"description": "Where each item starts, once any of them has been seen.",
|
|
1950
|
+
"default": "new Heights(() => this.options.estimate())"
|
|
1951
|
+
},
|
|
1732
1952
|
{
|
|
1733
1953
|
"kind": "method",
|
|
1734
1954
|
"name": "hostConnected",
|
|
@@ -1774,6 +1994,105 @@
|
|
|
1774
1994
|
},
|
|
1775
1995
|
"description": "Reads the scroll offset and settles the window.\n\nAsks for an update only when the slice actually moved. A scroll fires many\nevents per frame and most of them land inside the row already at the top,\nso requesting one per event would re-render the list for every pixel."
|
|
1776
1996
|
},
|
|
1997
|
+
{
|
|
1998
|
+
"kind": "method",
|
|
1999
|
+
"name": "uniform",
|
|
2000
|
+
"privacy": "private",
|
|
2001
|
+
"return": {
|
|
2002
|
+
"type": {
|
|
2003
|
+
"text": "Window"
|
|
2004
|
+
}
|
|
2005
|
+
},
|
|
2006
|
+
"parameters": [
|
|
2007
|
+
{
|
|
2008
|
+
"name": "box",
|
|
2009
|
+
"type": {
|
|
2010
|
+
"text": "HTMLElement"
|
|
2011
|
+
}
|
|
2012
|
+
},
|
|
2013
|
+
{
|
|
2014
|
+
"name": "total",
|
|
2015
|
+
"type": {
|
|
2016
|
+
"text": "number"
|
|
2017
|
+
}
|
|
2018
|
+
},
|
|
2019
|
+
{
|
|
2020
|
+
"name": "size",
|
|
2021
|
+
"type": {
|
|
2022
|
+
"text": "number"
|
|
2023
|
+
}
|
|
2024
|
+
},
|
|
2025
|
+
{
|
|
2026
|
+
"name": "overscan",
|
|
2027
|
+
"type": {
|
|
2028
|
+
"text": "number"
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
],
|
|
2032
|
+
"description": "The straight line: one estimate, so offset and index convert by division."
|
|
2033
|
+
},
|
|
2034
|
+
{
|
|
2035
|
+
"kind": "method",
|
|
2036
|
+
"name": "measuring",
|
|
2037
|
+
"privacy": "private",
|
|
2038
|
+
"return": {
|
|
2039
|
+
"type": {
|
|
2040
|
+
"text": "boolean"
|
|
2041
|
+
}
|
|
2042
|
+
},
|
|
2043
|
+
"description": "Whether this pass places by measurement: offered, and wanted."
|
|
2044
|
+
},
|
|
2045
|
+
{
|
|
2046
|
+
"kind": "method",
|
|
2047
|
+
"name": "placed",
|
|
2048
|
+
"privacy": "private",
|
|
2049
|
+
"return": {
|
|
2050
|
+
"type": {
|
|
2051
|
+
"text": "Window"
|
|
2052
|
+
}
|
|
2053
|
+
},
|
|
2054
|
+
"parameters": [
|
|
2055
|
+
{
|
|
2056
|
+
"name": "box",
|
|
2057
|
+
"type": {
|
|
2058
|
+
"text": "HTMLElement"
|
|
2059
|
+
}
|
|
2060
|
+
},
|
|
2061
|
+
{
|
|
2062
|
+
"name": "total",
|
|
2063
|
+
"type": {
|
|
2064
|
+
"text": "number"
|
|
2065
|
+
}
|
|
2066
|
+
},
|
|
2067
|
+
{
|
|
2068
|
+
"name": "overscan",
|
|
2069
|
+
"type": {
|
|
2070
|
+
"text": "number"
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
],
|
|
2074
|
+
"description": "The measured version, which asks where each item starts rather than dividing.\n\nThe scan for `last` walks forward from `first` rather than searching,\nbecause it stops at the bottom of the viewport: it is bounded by what fits\non screen plus the overscan, whatever the set's length."
|
|
2075
|
+
},
|
|
2076
|
+
{
|
|
2077
|
+
"kind": "method",
|
|
2078
|
+
"name": "hostUpdated",
|
|
2079
|
+
"return": {
|
|
2080
|
+
"type": {
|
|
2081
|
+
"text": "void"
|
|
2082
|
+
}
|
|
2083
|
+
},
|
|
2084
|
+
"description": "Measures what was just drawn, and holds the reader still if it moved.\n\n**This is the pass the fixed-estimate note warns about, made to terminate.**\nMeasuring after a render, and re-windowing on what was measured, is a loop\nthat feeds itself. What bounds it is that a height is news exactly once:\n`record` answers false for a size it already holds, so an item can move the\nwindow on the render that first shows it and never again. A set of ten\nthousand can therefore surprise this ten thousand times over its life and\nnot twice for the same row.\n\n**The scroll offset is corrected before anything is drawn again.** An item\nabove the window turning out taller than its estimate moves everything\nbelow it, including what the reader is looking at. Adding the same\ndifference to `scrollTop` puts the content back under them, which is why a\nlist of variable rows can be scrolled through without it walking away.\nSkipped at the very top, where there is nothing above to have moved."
|
|
2085
|
+
},
|
|
2086
|
+
{
|
|
2087
|
+
"kind": "method",
|
|
2088
|
+
"name": "forget",
|
|
2089
|
+
"return": {
|
|
2090
|
+
"type": {
|
|
2091
|
+
"text": "void"
|
|
2092
|
+
}
|
|
2093
|
+
},
|
|
2094
|
+
"description": "Drops every measurement, for a host that swapped its data.\n\nNot called on a count change, which would be the obvious place and is\nwrong: appending to a transcript leaves every existing index naming the\nsame item, and forgetting there would re-measure the whole set on every\nmessage. A host that replaces its set calls this, because only it knows\nthat index 900 is now a different row."
|
|
2095
|
+
},
|
|
1777
2096
|
{
|
|
1778
2097
|
"kind": "method",
|
|
1779
2098
|
"name": "report",
|
|
@@ -1787,14 +2106,6 @@
|
|
|
1787
2106
|
}
|
|
1788
2107
|
],
|
|
1789
2108
|
"helix": {}
|
|
1790
|
-
},
|
|
1791
|
-
{
|
|
1792
|
-
"kind": "variable",
|
|
1793
|
-
"name": "first"
|
|
1794
|
-
},
|
|
1795
|
-
{
|
|
1796
|
-
"kind": "variable",
|
|
1797
|
-
"name": "last"
|
|
1798
2109
|
}
|
|
1799
2110
|
],
|
|
1800
2111
|
"exports": [
|
|
@@ -3602,6 +3913,14 @@
|
|
|
3602
3913
|
"module": "./controllers/token-color-controller/token-color-controller.js"
|
|
3603
3914
|
}
|
|
3604
3915
|
},
|
|
3916
|
+
{
|
|
3917
|
+
"kind": "js",
|
|
3918
|
+
"name": "Rendered",
|
|
3919
|
+
"declaration": {
|
|
3920
|
+
"name": "Rendered",
|
|
3921
|
+
"module": "./controllers/virtualize-controller/virtualize-controller.js"
|
|
3922
|
+
}
|
|
3923
|
+
},
|
|
3605
3924
|
{
|
|
3606
3925
|
"kind": "js",
|
|
3607
3926
|
"name": "VirtualizeController",
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where each item starts, when the items are not all the same height.
|
|
3
|
+
*
|
|
4
|
+
* **The fixed-estimate window is a straight line and this is the other case
|
|
5
|
+
* the controller's own note defers.** With one estimate, offset and index
|
|
6
|
+
* convert by division, which can never disagree with itself. A transcript whose
|
|
7
|
+
* last message grows every token, or a table row that wraps, has no such line:
|
|
8
|
+
* the only way to place item 900 is to know what the 899 above it came to.
|
|
9
|
+
*
|
|
10
|
+
* **Measured where it has been seen, estimated everywhere else.** An item is
|
|
11
|
+
* measured once it has been rendered, and until then it stands at the estimate,
|
|
12
|
+
* so a set of ten thousand costs ten thousand numbers rather than ten thousand
|
|
13
|
+
* layouts. The estimate is read per call rather than captured, because a host
|
|
14
|
+
* may change it and the sums have to move with it.
|
|
15
|
+
*
|
|
16
|
+
* **The prefix sums are rebuilt from the first index that changed**, not from
|
|
17
|
+
* zero and not on every read. Recording a height marks that index dirty;
|
|
18
|
+
* nothing is recomputed until somebody asks a question that needs it. A scroll
|
|
19
|
+
* that records twelve rows therefore pays one rebuild rather than twelve, and a
|
|
20
|
+
* scroll that records none pays nothing.
|
|
21
|
+
*/
|
|
22
|
+
export declare class Heights {
|
|
23
|
+
private readonly estimate;
|
|
24
|
+
/** Heights that have been seen, by index. Sparse on purpose. */
|
|
25
|
+
private readonly seen;
|
|
26
|
+
/** `sums[i]` is the room items `[0, i)` take. One longer than the count. */
|
|
27
|
+
private sums;
|
|
28
|
+
/** The first index whose sum is no longer trustworthy. */
|
|
29
|
+
private dirty;
|
|
30
|
+
/** The estimate the sums were built against, so a change to it rebuilds. */
|
|
31
|
+
private built;
|
|
32
|
+
constructor(estimate: () => number);
|
|
33
|
+
/** How tall one item is: what was measured, or what is assumed. */
|
|
34
|
+
at(index: number): number;
|
|
35
|
+
/**
|
|
36
|
+
* Records a height, and says whether it was news.
|
|
37
|
+
*
|
|
38
|
+
* The answer matters to the caller rather than here: a height that differs
|
|
39
|
+
* from what was assumed moves the window, and the controller has to know that
|
|
40
|
+
* the move came from outside rather than from itself.
|
|
41
|
+
*/
|
|
42
|
+
record(index: number, size: number): boolean;
|
|
43
|
+
/** The room items `[0, index)` take. */
|
|
44
|
+
before(index: number, count: number): number;
|
|
45
|
+
/** The room the whole set takes. */
|
|
46
|
+
total(count: number): number;
|
|
47
|
+
/**
|
|
48
|
+
* The last item that starts at or before `offset`.
|
|
49
|
+
*
|
|
50
|
+
* A binary search rather than a walk, so a reader dragging the thumb to the
|
|
51
|
+
* end of ten thousand rows costs fourteen comparisons rather than ten
|
|
52
|
+
* thousand.
|
|
53
|
+
*/
|
|
54
|
+
indexAt(offset: number, count: number): number;
|
|
55
|
+
/**
|
|
56
|
+
* Drops everything, for a set that is no longer the same set.
|
|
57
|
+
*
|
|
58
|
+
* A host that swaps its data has heights recorded against indices that now
|
|
59
|
+
* name different items, and keeping them is worse than having none: the
|
|
60
|
+
* estimate is honest about not knowing where a stale measurement is not.
|
|
61
|
+
*/
|
|
62
|
+
forget(): void;
|
|
63
|
+
/** Brings the sums up to date, from the first index that changed. */
|
|
64
|
+
private build;
|
|
65
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export class Heights {
|
|
2
|
+
constructor(estimate) {
|
|
3
|
+
this.seen = new Map();
|
|
4
|
+
this.sums = [0];
|
|
5
|
+
this.dirty = 0;
|
|
6
|
+
this.built = 0;
|
|
7
|
+
this.estimate = estimate;
|
|
8
|
+
}
|
|
9
|
+
at(index) {
|
|
10
|
+
return this.seen.get(index) ?? Math.max(1, this.estimate());
|
|
11
|
+
}
|
|
12
|
+
record(index, size) {
|
|
13
|
+
if (index < 0 || !Number.isFinite(size) || size <= 0) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
const rounded = Math.round(size);
|
|
17
|
+
if (this.seen.get(index) === rounded) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
const was = this.at(index);
|
|
21
|
+
this.seen.set(index, rounded);
|
|
22
|
+
if (rounded === was) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
this.dirty = Math.min(this.dirty, index);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
before(index, count) {
|
|
29
|
+
this.build(count);
|
|
30
|
+
return this.sums[Math.max(0, Math.min(index, count))] ?? 0;
|
|
31
|
+
}
|
|
32
|
+
total(count) {
|
|
33
|
+
return this.before(count, count);
|
|
34
|
+
}
|
|
35
|
+
indexAt(offset, count) {
|
|
36
|
+
if (count <= 0) {
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
this.build(count);
|
|
40
|
+
const target = Math.max(0, offset);
|
|
41
|
+
let low = 0;
|
|
42
|
+
let high = count;
|
|
43
|
+
while (low < high) {
|
|
44
|
+
const mid = (low + high) >> 1;
|
|
45
|
+
if ((this.sums[mid + 1] ?? 0) <= target) {
|
|
46
|
+
low = mid + 1;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
high = mid;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return Math.min(low, count - 1);
|
|
53
|
+
}
|
|
54
|
+
forget() {
|
|
55
|
+
this.seen.clear();
|
|
56
|
+
this.dirty = 0;
|
|
57
|
+
this.sums = [0];
|
|
58
|
+
}
|
|
59
|
+
build(count) {
|
|
60
|
+
const size = Math.max(1, this.estimate());
|
|
61
|
+
if (size !== this.built) {
|
|
62
|
+
this.built = size;
|
|
63
|
+
this.dirty = 0;
|
|
64
|
+
}
|
|
65
|
+
if (this.dirty > count && this.sums.length === count + 1) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const from = Math.min(this.dirty, count);
|
|
69
|
+
if (this.sums.length !== count + 1) {
|
|
70
|
+
this.sums.length = count + 1;
|
|
71
|
+
}
|
|
72
|
+
let running = this.sums[from] ?? 0;
|
|
73
|
+
for (let index = from; index < count; index++) {
|
|
74
|
+
this.sums[index] = running;
|
|
75
|
+
running += this.at(index);
|
|
76
|
+
}
|
|
77
|
+
this.sums[count] = running;
|
|
78
|
+
this.dirty = count + 1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -13,6 +13,44 @@ export interface VirtualizeOptions {
|
|
|
13
13
|
* fast scrolling at a 32 pixel row.
|
|
14
14
|
*/
|
|
15
15
|
overscan?: number;
|
|
16
|
+
/**
|
|
17
|
+
* The rendered rows, in window order, for a set whose items differ in height.
|
|
18
|
+
*
|
|
19
|
+
* **Supplying this is what turns measurement on**, and leaving it off keeps
|
|
20
|
+
* the straight line the note below defends. A host that returns its row
|
|
21
|
+
* elements gets each one measured after it renders and placed by what it
|
|
22
|
+
* came to; a host that returns nothing gets every item at `estimate`, which
|
|
23
|
+
* is right whenever the rows really are uniform and is cheaper.
|
|
24
|
+
*
|
|
25
|
+
* **Each row carries its own index rather than being counted off from
|
|
26
|
+
* `window.first`.** A host is free to place its rows whenever it likes, and
|
|
27
|
+
* `hostUpdated` runs *before* `updated`, so a host that appends there has the
|
|
28
|
+
* previous window's rows in the tree when this is asked. Counting positionally
|
|
29
|
+
* would then record the heights of the right elements against the wrong
|
|
30
|
+
* items, and be silently wrong exactly when the window moved.
|
|
31
|
+
*/
|
|
32
|
+
rows?: () => Iterable<Rendered>;
|
|
33
|
+
/**
|
|
34
|
+
* Whether to place by measurement on this pass, for a host that can be told.
|
|
35
|
+
*
|
|
36
|
+
* **Separate from `rows` because the two answer different questions**: `rows`
|
|
37
|
+
* says a host is able to report what it drew, and this says it wants the
|
|
38
|
+
* measured placement today. A host with a `measured` attribute needs to
|
|
39
|
+
* change its mind at runtime, and a controller is constructed once.
|
|
40
|
+
*
|
|
41
|
+
* True by default, so a host that supplies `rows` and nothing else measures.
|
|
42
|
+
* The uniform path is kept for the case it is better at: it converts offset
|
|
43
|
+
* to index by division, where the measured one builds prefix sums across the
|
|
44
|
+
* whole set the first time it is asked.
|
|
45
|
+
*/
|
|
46
|
+
measured?: () => boolean;
|
|
47
|
+
}
|
|
48
|
+
/** A row that has been drawn, and which item of the set it drew. */
|
|
49
|
+
export interface Rendered {
|
|
50
|
+
/** The element, measured along the scroll axis. */
|
|
51
|
+
element: HTMLElement;
|
|
52
|
+
/** Which item it is, in the whole set rather than in the window. */
|
|
53
|
+
index: number;
|
|
16
54
|
}
|
|
17
55
|
/** Which slice is rendered, and how much empty space stands in for the rest. */
|
|
18
56
|
export interface Window {
|
|
@@ -98,6 +136,8 @@ export declare class VirtualizeController implements ReactiveController {
|
|
|
98
136
|
private last;
|
|
99
137
|
/** The slice to render, recomputed on scroll and on resize. */
|
|
100
138
|
window: Window;
|
|
139
|
+
/** Where each item starts, once any of them has been seen. */
|
|
140
|
+
private readonly heights;
|
|
101
141
|
constructor(host: ReactiveControllerHost, options: VirtualizeOptions);
|
|
102
142
|
hostConnected(): void;
|
|
103
143
|
hostDisconnected(): void;
|
|
@@ -112,6 +152,47 @@ export declare class VirtualizeController implements ReactiveController {
|
|
|
112
152
|
* so requesting one per event would re-render the list for every pixel.
|
|
113
153
|
*/
|
|
114
154
|
measure(): void;
|
|
155
|
+
/** The straight line: one estimate, so offset and index convert by division. */
|
|
156
|
+
private uniform;
|
|
157
|
+
/** Whether this pass places by measurement: offered, and wanted. */
|
|
158
|
+
private measuring;
|
|
159
|
+
/**
|
|
160
|
+
* The measured version, which asks where each item starts rather than dividing.
|
|
161
|
+
*
|
|
162
|
+
* The scan for `last` walks forward from `first` rather than searching,
|
|
163
|
+
* because it stops at the bottom of the viewport: it is bounded by what fits
|
|
164
|
+
* on screen plus the overscan, whatever the set's length.
|
|
165
|
+
*/
|
|
166
|
+
private placed;
|
|
167
|
+
/**
|
|
168
|
+
* Measures what was just drawn, and holds the reader still if it moved.
|
|
169
|
+
*
|
|
170
|
+
* **This is the pass the fixed-estimate note warns about, made to terminate.**
|
|
171
|
+
* Measuring after a render, and re-windowing on what was measured, is a loop
|
|
172
|
+
* that feeds itself. What bounds it is that a height is news exactly once:
|
|
173
|
+
* `record` answers false for a size it already holds, so an item can move the
|
|
174
|
+
* window on the render that first shows it and never again. A set of ten
|
|
175
|
+
* thousand can therefore surprise this ten thousand times over its life and
|
|
176
|
+
* not twice for the same row.
|
|
177
|
+
*
|
|
178
|
+
* **The scroll offset is corrected before anything is drawn again.** An item
|
|
179
|
+
* above the window turning out taller than its estimate moves everything
|
|
180
|
+
* below it, including what the reader is looking at. Adding the same
|
|
181
|
+
* difference to `scrollTop` puts the content back under them, which is why a
|
|
182
|
+
* list of variable rows can be scrolled through without it walking away.
|
|
183
|
+
* Skipped at the very top, where there is nothing above to have moved.
|
|
184
|
+
*/
|
|
185
|
+
hostUpdated(): void;
|
|
186
|
+
/**
|
|
187
|
+
* Drops every measurement, for a host that swapped its data.
|
|
188
|
+
*
|
|
189
|
+
* Not called on a count change, which would be the obvious place and is
|
|
190
|
+
* wrong: appending to a transcript leaves every existing index naming the
|
|
191
|
+
* same item, and forgetting there would re-measure the whole set on every
|
|
192
|
+
* message. A host that replaces its set calls this, because only it knows
|
|
193
|
+
* that index 900 is now a different row.
|
|
194
|
+
*/
|
|
195
|
+
forget(): void;
|
|
115
196
|
/**
|
|
116
197
|
* Says once that the box cannot be measured, and why.
|
|
117
198
|
*
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Heights } from './heights.js';
|
|
1
2
|
const UNSTABLE = 10;
|
|
2
3
|
export class VirtualizeController {
|
|
3
4
|
constructor(host, options) {
|
|
@@ -11,6 +12,7 @@ export class VirtualizeController {
|
|
|
11
12
|
};
|
|
12
13
|
this.host = host;
|
|
13
14
|
this.options = options;
|
|
15
|
+
this.heights = new Heights(() => this.options.estimate());
|
|
14
16
|
host.addController(this);
|
|
15
17
|
}
|
|
16
18
|
hostConnected() {
|
|
@@ -45,15 +47,9 @@ export class VirtualizeController {
|
|
|
45
47
|
return;
|
|
46
48
|
}
|
|
47
49
|
const overscan = this.options.overscan ?? 4;
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const next = {
|
|
52
|
-
after: Math.max(0, (total - last) * size),
|
|
53
|
-
before: first * size,
|
|
54
|
-
first,
|
|
55
|
-
last,
|
|
56
|
-
};
|
|
50
|
+
const next = this.measuring()
|
|
51
|
+
? this.placed(box, total, overscan)
|
|
52
|
+
: this.uniform(box, total, size, overscan);
|
|
57
53
|
if (this.last?.count !== total || this.last.estimate !== size) {
|
|
58
54
|
this.last = { count: total, estimate: size };
|
|
59
55
|
this.settling = 0;
|
|
@@ -74,6 +70,60 @@ export class VirtualizeController {
|
|
|
74
70
|
this.window = next;
|
|
75
71
|
this.host.requestUpdate();
|
|
76
72
|
}
|
|
73
|
+
uniform(box, total, size, overscan) {
|
|
74
|
+
const first = Math.max(0, Math.floor(box.scrollTop / size) - overscan);
|
|
75
|
+
const fits = Math.ceil(box.clientHeight / size) + overscan * 2;
|
|
76
|
+
const last = Math.min(total, first + fits);
|
|
77
|
+
return { after: Math.max(0, (total - last) * size), before: first * size, first, last };
|
|
78
|
+
}
|
|
79
|
+
measuring() {
|
|
80
|
+
return this.options.rows !== undefined && (this.options.measured?.() ?? true);
|
|
81
|
+
}
|
|
82
|
+
placed(box, total, overscan) {
|
|
83
|
+
const first = Math.max(0, this.heights.indexAt(box.scrollTop, total) - overscan);
|
|
84
|
+
const bottom = box.scrollTop + box.clientHeight;
|
|
85
|
+
let last = this.heights.indexAt(box.scrollTop, total);
|
|
86
|
+
while (last < total && this.heights.before(last, total) < bottom) {
|
|
87
|
+
last += 1;
|
|
88
|
+
}
|
|
89
|
+
last = Math.min(total, last + overscan);
|
|
90
|
+
return {
|
|
91
|
+
after: Math.max(0, this.heights.total(total) - this.heights.before(last, total)),
|
|
92
|
+
before: this.heights.before(first, total),
|
|
93
|
+
first,
|
|
94
|
+
last,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
hostUpdated() {
|
|
98
|
+
if (!this.measuring()) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const rows = this.options.rows?.();
|
|
102
|
+
if (!rows) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const total = Math.max(0, this.options.count());
|
|
106
|
+
const was = this.heights.before(this.window.first, total);
|
|
107
|
+
let news = false;
|
|
108
|
+
for (const { element, index } of rows) {
|
|
109
|
+
if (this.heights.record(index, element.getBoundingClientRect().height)) {
|
|
110
|
+
news = true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (!news) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.settling = 0;
|
|
117
|
+
const box = this.options.container();
|
|
118
|
+
const now = this.heights.before(this.window.first, total);
|
|
119
|
+
if (box && box.scrollTop > 0 && now !== was) {
|
|
120
|
+
box.scrollTop += now - was;
|
|
121
|
+
}
|
|
122
|
+
this.measure();
|
|
123
|
+
}
|
|
124
|
+
forget() {
|
|
125
|
+
this.heights.forget();
|
|
126
|
+
}
|
|
77
127
|
report() {
|
|
78
128
|
if (this.warned) {
|
|
79
129
|
return;
|
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export { KeyboardNavigationController, type KeyboardNavigationOptions, type Navi
|
|
|
12
12
|
export { PopoverController, type PopoverOptions, } from './controllers/popover-controller/popover-controller.js';
|
|
13
13
|
export { SourceController, type SourceHost, } from './controllers/source-controller/source-controller.js';
|
|
14
14
|
export { type TokenColor, TokenColorController, } from './controllers/token-color-controller/token-color-controller.js';
|
|
15
|
-
export { VirtualizeController, type VirtualizeOptions, type Window as VirtualizeWindow, } from './controllers/virtualize-controller/virtualize-controller.js';
|
|
15
|
+
export { type Rendered, VirtualizeController, type VirtualizeOptions, type Window as VirtualizeWindow, } from './controllers/virtualize-controller/virtualize-controller.js';
|
|
16
16
|
export { eventsForTag, registerElementEvents } from './events/element-events.js';
|
|
17
17
|
export { type CaretDetail, type ChangeDetail, type CompleteDetail, type ConsoleDetail, type CopyDetail, type CreateDetail, type DismissDetail, type FilterDetail, HelixActivateEvent, HelixBeforeActivateEvent, HelixBeforeChangeEvent, HelixBeforeCloseEvent, HelixBeforeCollapseEvent, HelixBeforeEditEvent, HelixBeforeOpenEvent, HelixBeforePanelResizeEvent, HelixBeforePinEvent, HelixBeforeResizeEvent, HelixBeforeSplitEvent, HelixBeforeToggleEvent, HelixCaretEvent, HelixCompleteEvent, HelixConsoleEvent, HelixCopyEvent, HelixCreateEvent, HelixDismissEvent, HelixFilterEvent, HelixNavigateEvent, HelixNextEvent, HelixPreviousEvent, HelixReadEvent, HelixResizeEvent, HelixSampleEndEvent, HelixSampleStartEvent, HelixScrolledEvent, HelixSelectEvent, HelixSendEvent, HelixShortcutEvent, HelixSortEvent, HelixTourAskEvent, type NavigateDetail, type OpenDetail, type ReadDetail, type ResizeDetail, type ScrolledDetail, type SelectDetail, type SendDetail, type ShortcutDetail, type SkipDetail, type SortDetail, type ToggleDetail, } from './events/events.js';
|
|
18
18
|
export { HelixElement } from './helix-element/helix-element.js';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusion.dev/helix-core",
|
|
3
3
|
"description": "Helix element primitives: base classes, mixins, decorators, utilities.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ross Sheppard <ross.sheppard@fusion.dev>",
|
|
7
7
|
"repository": {
|
|
@@ -34,12 +34,12 @@
|
|
|
34
34
|
"bugs": "https://github.com/project-fusion/fusion/issues",
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@floating-ui/dom": "^1.8.0",
|
|
37
|
-
"@fusion.dev/helix-icons": "0.
|
|
37
|
+
"@fusion.dev/helix-icons": "0.6.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@custom-elements-manifest/analyzer": "^0.11.0",
|
|
41
|
-
"@fusion.dev/helix-cem": "0.
|
|
42
|
-
"@fusion.dev/helix-testing": "0.
|
|
41
|
+
"@fusion.dev/helix-cem": "0.6.0",
|
|
42
|
+
"@fusion.dev/helix-testing": "0.6.0",
|
|
43
43
|
"lit": "^3.2.1"
|
|
44
44
|
},
|
|
45
45
|
"homepage": "https://fusion.dev",
|