react-native-gtkx 0.1.0-alpha.2 → 0.1.0-alpha.3

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.
Files changed (85) hide show
  1. package/README.md +4 -5
  2. package/dist/apis/back-handler.d.ts +9 -0
  3. package/dist/apis/back-handler.js +30 -0
  4. package/dist/apis/back-handler.js.map +1 -0
  5. package/dist/apis/host.d.ts +7 -1
  6. package/dist/apis/host.gtkx.js +12 -1
  7. package/dist/apis/host.gtkx.js.map +1 -1
  8. package/dist/apis/host.js.map +1 -1
  9. package/dist/apis/i18n-manager.d.ts +14 -0
  10. package/dist/apis/i18n-manager.js +20 -0
  11. package/dist/apis/i18n-manager.js.map +1 -0
  12. package/dist/apis/index.d.ts +17 -0
  13. package/dist/apis/index.js +5 -0
  14. package/dist/apis/index.js.map +1 -1
  15. package/dist/apis/interaction-manager.d.ts +16 -0
  16. package/dist/apis/interaction-manager.js +96 -0
  17. package/dist/apis/interaction-manager.js.map +1 -0
  18. package/dist/apis/linking.d.ts +4 -0
  19. package/dist/apis/linking.js +9 -0
  20. package/dist/apis/linking.js.map +1 -1
  21. package/dist/components/app-registry.d.ts +2 -0
  22. package/dist/components/app-registry.js +19 -2
  23. package/dist/components/app-registry.js.map +1 -1
  24. package/dist/components/flat-list.d.ts +2 -0
  25. package/dist/components/flat-list.js.map +1 -1
  26. package/dist/components/index.d.ts +1 -1
  27. package/dist/components/index.js +1 -1
  28. package/dist/components/index.js.map +1 -1
  29. package/dist/components/modal.d.ts +1 -1
  30. package/dist/components/root.d.ts +10 -1
  31. package/dist/components/root.js +39 -1
  32. package/dist/components/root.js.map +1 -1
  33. package/dist/components/scroll-view.js +58 -5
  34. package/dist/components/scroll-view.js.map +1 -1
  35. package/dist/components/text-input.d.ts +1 -0
  36. package/dist/components/text-input.js +31 -3
  37. package/dist/components/text-input.js.map +1 -1
  38. package/dist/components/use-layout-child.d.ts +1 -1
  39. package/dist/components/use-layout-child.js +22 -2
  40. package/dist/components/use-layout-child.js.map +1 -1
  41. package/dist/components/virtualized-list.d.ts +2 -0
  42. package/dist/components/virtualized-list.js +132 -10
  43. package/dist/components/virtualized-list.js.map +1 -1
  44. package/dist/gtkx/bridge/index.d.ts +4 -2
  45. package/dist/gtkx/bridge/index.js +6 -2
  46. package/dist/gtkx/bridge/index.js.map +1 -1
  47. package/dist/gtkx/bridge/measure.js +5 -0
  48. package/dist/gtkx/bridge/measure.js.map +1 -1
  49. package/dist/gtkx/bridge/use-signal.d.ts +2 -0
  50. package/dist/gtkx/bridge/use-signal.js +30 -0
  51. package/dist/gtkx/bridge/use-signal.js.map +1 -0
  52. package/dist/index.d.ts +2 -2
  53. package/dist/index.js +2 -2
  54. package/dist/index.js.map +1 -1
  55. package/dist/layout/engine.d.ts +4 -0
  56. package/dist/layout/engine.js +97 -5
  57. package/dist/layout/engine.js.map +1 -1
  58. package/dist/layout/node.d.ts +1 -1
  59. package/dist/layout/node.js +8 -5
  60. package/dist/layout/node.js.map +1 -1
  61. package/dist/metro/index.js +1 -0
  62. package/dist/metro/index.js.map +1 -1
  63. package/dist/navigation/index.d.ts +44 -0
  64. package/dist/navigation/index.js +216 -0
  65. package/dist/navigation/index.js.map +1 -0
  66. package/dist/navigation/option-warnings.d.ts +2 -0
  67. package/dist/navigation/option-warnings.js +61 -0
  68. package/dist/navigation/option-warnings.js.map +1 -0
  69. package/dist/navigation/sidebar.d.ts +37 -0
  70. package/dist/navigation/sidebar.js +71 -0
  71. package/dist/navigation/sidebar.js.map +1 -0
  72. package/dist/perf.d.ts +9 -0
  73. package/dist/perf.js +156 -0
  74. package/dist/perf.js.map +1 -0
  75. package/dist/runner/host-dev.js +15 -6
  76. package/dist/runner/host-dev.js.map +1 -1
  77. package/dist/runner/host.js +24 -9
  78. package/dist/runner/host.js.map +1 -1
  79. package/dist/runner/index.js +4 -1
  80. package/dist/runner/index.js.map +1 -1
  81. package/dist/vite/index.d.ts +5 -4
  82. package/dist/vite/index.js +41 -7
  83. package/dist/vite/index.js.map +1 -1
  84. package/package.json +18 -10
  85. package/types.d.ts +13 -0
@@ -1 +1 @@
1
- {"version":3,"file":"text-input.js","sourceRoot":"","sources":["../../src/components/text-input.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useLayoutEffect, useRef, useState } from \"react\"\nimport type { StyleProp } from \"../contracts\"\nimport {\n Gtk,\n GtkEntry,\n GtkLabel,\n GtkScrolledWindow,\n GtkTextView,\n type Gtk as GtkNs,\n} from \"../gtkx/bridge/index\"\nimport { useLayoutChild, type LayoutEvent } from \"./use-layout-child\"\nimport { View } from \"./view\"\n\nexport type TextInputProps = {\n value?: string\n defaultValue?: string\n onChangeText?: (text: string) => void\n onSubmitEditing?: (event: { nativeEvent: { text: string } }) => void\n onFocus?: () => void\n onBlur?: () => void\n placeholder?: string\n secureTextEntry?: boolean\n editable?: boolean\n keyboardType?: \"default\" | \"numeric\" | \"email-address\" | \"phone-pad\" | \"url\"\n // RN: a multiline input needs a height in the style; Enter inserts a\n // newline and never fires onSubmitEditing.\n multiline?: boolean\n style?: StyleProp\n onLayout?: (event: LayoutEvent) => void\n testID?: string\n}\n\nconst INPUT_PURPOSE: Record<\n NonNullable<TextInputProps[\"keyboardType\"]>,\n Gtk.InputPurpose\n> = {\n default: Gtk.InputPurpose.FREE_FORM,\n numeric: Gtk.InputPurpose.NUMBER,\n \"email-address\": Gtk.InputPurpose.EMAIL,\n \"phone-pad\": Gtk.InputPurpose.PHONE,\n url: Gtk.InputPurpose.URL,\n}\n\n// RC1-WORKAROUND(controllers-as-children): see docs/gtkx-rc1-vs-main.md\n// rc.1: controllers attach imperatively (JSX children work on main only).\nconst useFocusController = (\n widgetRef: React.RefObject<GtkNs.Widget | null>,\n onFocus?: () => void,\n onBlur?: () => void,\n): void => {\n const handlers = useRef({ onFocus, onBlur })\n handlers.current = { onFocus, onBlur }\n useLayoutEffect(() => {\n const widget = widgetRef.current\n if (!widget) {\n return\n }\n const focus = new Gtk.EventControllerFocus()\n const enter = (): void => handlers.current.onFocus?.()\n const leave = (): void => handlers.current.onBlur?.()\n focus.on(\"enter\", enter)\n focus.on(\"leave\", leave)\n widget.addController(focus)\n return () => {\n widget.removeController(focus)\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [])\n}\n\n// Single-line input over GtkEntry. gtkx's controlled-text behavior keeps the\n// widget in sync with the `text` prop (RN controlled semantics); uncontrolled\n// usage passes defaultValue once.\nconst SingleLineTextInput = ({\n value,\n defaultValue,\n onChangeText,\n onSubmitEditing,\n onFocus,\n onBlur,\n placeholder,\n secureTextEntry = false,\n editable = true,\n keyboardType = \"default\",\n style,\n onLayout,\n testID,\n}: TextInputProps) => {\n const widgetRef = useRef<GtkNs.Entry | null>(null)\n\n useLayoutChild(widgetRef, {\n style,\n onLayout,\n // The entry measures itself (theme-accurate height); width is driven by\n // the style (flex/width), with a small floor for usability.\n measureFromWidget: true,\n extraLayout: { minWidth: 60 },\n })\n\n const controlled = value !== undefined\n\n useFocusController(widgetRef, onFocus, onBlur)\n\n return (\n <GtkEntry\n ref={widgetRef}\n name={testID}\n text={controlled ? value : (defaultValue ?? \"\")}\n placeholderText={placeholder ?? \"\"}\n visibility={!secureTextEntry}\n editable={editable}\n sensitive={editable}\n inputPurpose={INPUT_PURPOSE[keyboardType]}\n onChanged={() => {\n const widget = widgetRef.current\n if (widget) {\n onChangeText?.(widget.getText())\n }\n }}\n onActivate={() => {\n const widget = widgetRef.current\n if (widget) {\n onSubmitEditing?.({ nativeEvent: { text: widget.getText() } })\n }\n }}\n />\n )\n}\n\nconst readBuffer = (buffer: GtkNs.TextBuffer): string =>\n buffer.getText(buffer.getStartIter(), buffer.getEndIter(), true)\n\n// GtkTextView has no native placeholder — a dim, click-transparent label\n// sits over the empty view (RN shows the placeholder while empty, focused\n// or not).\nconst MultilinePlaceholder = ({ text }: { text: string }) => {\n const labelRef = useRef<GtkNs.Label | null>(null)\n useLayoutChild(labelRef, {\n style: { position: \"absolute\", left: 12, top: 8 },\n onLayout: undefined,\n })\n useLayoutEffect(() => {\n labelRef.current?.setCanTarget(false)\n labelRef.current?.addCssClass(\"dim-label\")\n }, [])\n return (\n <GtkLabel\n ref={labelRef}\n label={text}\n />\n )\n}\n\n// Multiline input over GtkTextView in a GtkScrolledWindow: word wrap, the\n// style sets the box (RN requires a height for multiline), overflowing\n// content scrolls inside. The buffer syncs with `value` under RN controlled\n// semantics; Enter inserts a newline (onSubmitEditing never fires — RN\n// multiline behavior).\nconst MultilineTextInput = ({\n value,\n defaultValue,\n onChangeText,\n onFocus,\n onBlur,\n placeholder,\n editable = true,\n style,\n onLayout,\n testID,\n}: TextInputProps) => {\n const scrolledRef = useRef<GtkNs.ScrolledWindow | null>(null)\n const viewRef = useRef<GtkNs.TextView | null>(null)\n const controlled = value !== undefined\n const [empty, setEmpty] = useState(\n (controlled ? (value ?? \"\") : (defaultValue ?? \"\")) === \"\",\n )\n\n const suppressEcho = useRef(false)\n const latest = useRef({ onChangeText })\n latest.current = { onChangeText }\n\n useLayoutEffect(() => {\n const view = viewRef.current\n if (!view) {\n return\n }\n // Breathing room akin to GtkEntry's built-in padding.\n view.setLeftMargin(10)\n view.setRightMargin(10)\n view.setTopMargin(8)\n view.setBottomMargin(8)\n const buffer = view.getBuffer()\n const initial = controlled ? (value ?? \"\") : (defaultValue ?? \"\")\n if (initial !== \"\") {\n suppressEcho.current = true\n buffer.setText(initial, -1)\n suppressEcho.current = false\n }\n const changed = (): void => {\n const text = readBuffer(buffer)\n setEmpty(text === \"\")\n if (!suppressEcho.current) {\n latest.current.onChangeText?.(text)\n }\n }\n buffer.on(\"changed\", changed)\n return () => {\n buffer.off(\"changed\", changed)\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [])\n\n // RN controlled semantics: the prop wins over whatever was typed.\n useLayoutEffect(() => {\n if (!controlled) {\n return\n }\n const view = viewRef.current\n if (!view) {\n return\n }\n const buffer = view.getBuffer()\n const next = value ?? \"\"\n if (readBuffer(buffer) !== next) {\n suppressEcho.current = true\n buffer.setText(next, -1)\n suppressEcho.current = false\n setEmpty(next === \"\")\n }\n }, [controlled, value])\n\n useFocusController(viewRef, onFocus, onBlur)\n\n return (\n <View\n style={[style, { minHeight: 36, minWidth: 60 }]}\n onLayout={onLayout}\n >\n <ScrolledFill scrolledRef={scrolledRef}>\n <GtkTextView\n ref={viewRef}\n name={testID}\n editable={editable}\n sensitive={editable}\n wrapMode={Gtk.WrapMode.WORD_CHAR}\n />\n </ScrolledFill>\n {empty && placeholder ? (\n <MultilinePlaceholder text={placeholder} />\n ) : null}\n </View>\n )\n}\n\n// The scrolled window fills the styled box exactly (its own layout child).\nconst ScrolledFill = ({\n scrolledRef,\n children,\n}: {\n scrolledRef: React.RefObject<GtkNs.ScrolledWindow | null>\n children: React.ReactNode\n}) => {\n useLayoutChild(scrolledRef, {\n style: { position: \"absolute\", left: 0, right: 0, top: 0, bottom: 0 },\n onLayout: undefined,\n })\n return <GtkScrolledWindow ref={scrolledRef}>{children}</GtkScrolledWindow>\n}\n\nexport const TextInput = (props: TextInputProps) =>\n props.multiline ? (\n <MultilineTextInput {...props} />\n ) : (\n <SingleLineTextInput {...props} />\n )\n"]}
1
+ {"version":3,"file":"text-input.js","sourceRoot":"","sources":["../../src/components/text-input.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useLayoutEffect, useRef, useState } from \"react\"\nimport type { StyleProp } from \"../contracts\"\nimport {\n Gtk,\n GtkEntry,\n GtkLabel,\n GtkScrolledWindow,\n GtkTextView,\n type Gtk as GtkNs,\n} from \"../gtkx/bridge/index\"\nimport { useLayoutChild, type LayoutEvent } from \"./use-layout-child\"\nimport { View } from \"./view\"\n\nexport type TextInputProps = {\n value?: string\n defaultValue?: string\n onChangeText?: (text: string) => void\n onSubmitEditing?: (event: { nativeEvent: { text: string } }) => void\n onFocus?: () => void\n onBlur?: () => void\n placeholder?: string\n secureTextEntry?: boolean\n editable?: boolean\n keyboardType?: \"default\" | \"numeric\" | \"email-address\" | \"phone-pad\" | \"url\"\n // RN: a multiline input needs a height in the style; Enter inserts a\n // newline and never fires onSubmitEditing.\n multiline?: boolean\n // RN's iOS-only clear affordance, native here: GtkEntry's built-in\n // secondary icon (inside the field, so nothing shifts around it).\n // Single-line only, like RN.\n clearButtonMode?: \"never\" | \"while-editing\" | \"unless-editing\" | \"always\"\n style?: StyleProp\n onLayout?: (event: LayoutEvent) => void\n testID?: string\n}\n\nconst INPUT_PURPOSE: Record<\n NonNullable<TextInputProps[\"keyboardType\"]>,\n Gtk.InputPurpose\n> = {\n default: Gtk.InputPurpose.FREE_FORM,\n numeric: Gtk.InputPurpose.NUMBER,\n \"email-address\": Gtk.InputPurpose.EMAIL,\n \"phone-pad\": Gtk.InputPurpose.PHONE,\n url: Gtk.InputPurpose.URL,\n}\n\n// RC1-WORKAROUND(controllers-as-children): see docs/gtkx-rc1-vs-main.md\n// rc.1: controllers attach imperatively (JSX children work on main only).\nconst useFocusController = (\n widgetRef: React.RefObject<GtkNs.Widget | null>,\n onFocus?: () => void,\n onBlur?: () => void,\n): void => {\n const handlers = useRef({ onFocus, onBlur })\n handlers.current = { onFocus, onBlur }\n useLayoutEffect(() => {\n const widget = widgetRef.current\n if (!widget) {\n return\n }\n const focus = new Gtk.EventControllerFocus()\n const enter = (): void => handlers.current.onFocus?.()\n const leave = (): void => handlers.current.onBlur?.()\n focus.on(\"enter\", enter)\n focus.on(\"leave\", leave)\n widget.addController(focus)\n return () => {\n widget.removeController(focus)\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [])\n}\n\n// Single-line input over GtkEntry. gtkx's controlled-text behavior keeps the\n// widget in sync with the `text` prop (RN controlled semantics); uncontrolled\n// usage passes defaultValue once.\nconst SingleLineTextInput = ({\n value,\n defaultValue,\n onChangeText,\n onSubmitEditing,\n onFocus,\n onBlur,\n placeholder,\n secureTextEntry = false,\n editable = true,\n keyboardType = \"default\",\n clearButtonMode = \"never\",\n style,\n onLayout,\n testID,\n}: TextInputProps) => {\n const widgetRef = useRef<GtkNs.Entry | null>(null)\n const [focused, setFocused] = useState(false)\n\n useLayoutChild(widgetRef, {\n style,\n onLayout,\n // The entry measures itself (theme-accurate height); width is driven by\n // the style (flex/width), with a small floor for usability.\n measureFromWidget: true,\n extraLayout: { minWidth: 60 },\n })\n\n const controlled = value !== undefined\n\n useFocusController(\n widgetRef,\n () => {\n setFocused(true)\n onFocus?.()\n },\n () => {\n setFocused(false)\n onBlur?.()\n },\n )\n\n // RN semantics: the affordance only shows when there is something to\n // clear, and while-editing / unless-editing follow focus.\n const text = controlled ? value : undefined\n const hasText =\n text !== undefined\n ? text.length > 0\n : (widgetRef.current?.getText().length ?? 0) > 0\n const showClear =\n hasText &&\n (clearButtonMode === \"always\" ||\n (clearButtonMode === \"while-editing\" && focused) ||\n (clearButtonMode === \"unless-editing\" && !focused))\n\n return (\n <GtkEntry\n ref={widgetRef}\n name={testID}\n secondaryIconName={showClear ? \"edit-clear-symbolic\" : null}\n secondaryIconActivatable={showClear}\n secondaryIconTooltipText={showClear ? \"Clear\" : null}\n onIconRelease={() => {\n const widget = widgetRef.current\n if (!widget) {\n return\n }\n // Uncontrolled inputs clear themselves; controlled ones report an\n // empty string and let the owner drive the value (RN contract).\n if (!controlled) {\n widget.setText(\"\")\n }\n onChangeText?.(\"\")\n }}\n text={controlled ? value : (defaultValue ?? \"\")}\n placeholderText={placeholder ?? \"\"}\n visibility={!secureTextEntry}\n editable={editable}\n sensitive={editable}\n inputPurpose={INPUT_PURPOSE[keyboardType]}\n onChanged={() => {\n const widget = widgetRef.current\n if (widget) {\n onChangeText?.(widget.getText())\n }\n }}\n onActivate={() => {\n const widget = widgetRef.current\n if (widget) {\n onSubmitEditing?.({ nativeEvent: { text: widget.getText() } })\n }\n }}\n />\n )\n}\n\nconst readBuffer = (buffer: GtkNs.TextBuffer): string =>\n buffer.getText(buffer.getStartIter(), buffer.getEndIter(), true)\n\n// GtkTextView has no native placeholder — a dim, click-transparent label\n// sits over the empty view (RN shows the placeholder while empty, focused\n// or not).\nconst MultilinePlaceholder = ({ text }: { text: string }) => {\n const labelRef = useRef<GtkNs.Label | null>(null)\n useLayoutChild(labelRef, {\n style: { position: \"absolute\", left: 12, top: 8 },\n onLayout: undefined,\n })\n useLayoutEffect(() => {\n labelRef.current?.setCanTarget(false)\n labelRef.current?.addCssClass(\"dim-label\")\n }, [])\n return (\n <GtkLabel\n ref={labelRef}\n label={text}\n />\n )\n}\n\n// Multiline input over GtkTextView in a GtkScrolledWindow: word wrap, the\n// style sets the box (RN requires a height for multiline), overflowing\n// content scrolls inside. The buffer syncs with `value` under RN controlled\n// semantics; Enter inserts a newline (onSubmitEditing never fires — RN\n// multiline behavior).\nconst MultilineTextInput = ({\n value,\n defaultValue,\n onChangeText,\n onFocus,\n onBlur,\n placeholder,\n editable = true,\n style,\n onLayout,\n testID,\n}: TextInputProps) => {\n const scrolledRef = useRef<GtkNs.ScrolledWindow | null>(null)\n const viewRef = useRef<GtkNs.TextView | null>(null)\n const controlled = value !== undefined\n const [empty, setEmpty] = useState(\n (controlled ? (value ?? \"\") : (defaultValue ?? \"\")) === \"\",\n )\n\n const suppressEcho = useRef(false)\n const latest = useRef({ onChangeText })\n latest.current = { onChangeText }\n\n useLayoutEffect(() => {\n const view = viewRef.current\n if (!view) {\n return\n }\n // Breathing room akin to GtkEntry's built-in padding.\n view.setLeftMargin(10)\n view.setRightMargin(10)\n view.setTopMargin(8)\n view.setBottomMargin(8)\n const buffer = view.getBuffer()\n const initial = controlled ? (value ?? \"\") : (defaultValue ?? \"\")\n if (initial !== \"\") {\n suppressEcho.current = true\n buffer.setText(initial, -1)\n suppressEcho.current = false\n }\n const changed = (): void => {\n const text = readBuffer(buffer)\n setEmpty(text === \"\")\n if (!suppressEcho.current) {\n latest.current.onChangeText?.(text)\n }\n }\n buffer.on(\"changed\", changed)\n return () => {\n buffer.off(\"changed\", changed)\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [])\n\n // RN controlled semantics: the prop wins over whatever was typed.\n useLayoutEffect(() => {\n if (!controlled) {\n return\n }\n const view = viewRef.current\n if (!view) {\n return\n }\n const buffer = view.getBuffer()\n const next = value ?? \"\"\n if (readBuffer(buffer) !== next) {\n suppressEcho.current = true\n buffer.setText(next, -1)\n suppressEcho.current = false\n setEmpty(next === \"\")\n }\n }, [controlled, value])\n\n useFocusController(viewRef, onFocus, onBlur)\n\n return (\n <View\n style={[style, { minHeight: 36, minWidth: 60 }]}\n onLayout={onLayout}\n >\n <ScrolledFill scrolledRef={scrolledRef}>\n <GtkTextView\n ref={viewRef}\n name={testID}\n editable={editable}\n sensitive={editable}\n wrapMode={Gtk.WrapMode.WORD_CHAR}\n />\n </ScrolledFill>\n {empty && placeholder ? (\n <MultilinePlaceholder text={placeholder} />\n ) : null}\n </View>\n )\n}\n\n// The scrolled window fills the styled box exactly (its own layout child).\nconst ScrolledFill = ({\n scrolledRef,\n children,\n}: {\n scrolledRef: React.RefObject<GtkNs.ScrolledWindow | null>\n children: React.ReactNode\n}) => {\n useLayoutChild(scrolledRef, {\n style: { position: \"absolute\", left: 0, right: 0, top: 0, bottom: 0 },\n onLayout: undefined,\n })\n return <GtkScrolledWindow ref={scrolledRef}>{children}</GtkScrolledWindow>\n}\n\nexport const TextInput = (props: TextInputProps) =>\n props.multiline ? (\n <MultilineTextInput {...props} />\n ) : (\n <SingleLineTextInput {...props} />\n )\n"]}
@@ -30,7 +30,7 @@ export type LayoutChild = {
30
30
  };
31
31
  export declare const useLayoutChild: (widgetRef: RefObject<Gtk.Widget | null>, options: LayoutChildOptions) => LayoutChild;
32
32
  export type RnContainerOptions = {
33
- measure?: (orientation: "horizontal" | "vertical") => number;
33
+ measure?: (orientation: "horizontal" | "vertical", forSize: number) => number;
34
34
  beforeAllocate?: (width: number, height: number) => void;
35
35
  };
36
36
  export declare const useRnContainer: (widgetRef: RefObject<Gtk.Widget | null>, node: LayoutNode, options?: RnContainerOptions) => void;
@@ -2,8 +2,14 @@ import { useLayoutEffect, useRef, useState } from "react";
2
2
  import { splitStyle, StyleSheet } from "../style/index";
3
3
  import { defaultCssRegistry } from "../style/registry.gtkx";
4
4
  import { allocateChild, attachRnLayout, detachRnLayout, measureWidget, queueAllocate, queueResize, } from "../gtkx/bridge/index";
5
+ import { perfAddTime, perfBurst, perfCount, perfEnabled, perfNow, } from "../perf";
5
6
  import { useHostNode } from "./host-node";
6
7
  import { deferDuringAllocate, getStoredOffset, getStoredRect, setStoredRect, } from "./rect-store";
8
+ // Perf: sizeAllocate recurses synchronously into child containers, so a
9
+ // naive per-pass timer double-counts nested passes. Track depth and time
10
+ // only the outermost (top-level) allocate — that is the real wall time GTK
11
+ // spends in OUR layout code for the frame.
12
+ let perfAllocDepth = 0;
7
13
  // Shared plumbing for every leaf/container component: owns one LayoutNode,
8
14
  // keeps it in the parent's shadow tree for the component lifetime, applies
9
15
  // the layout half of the style and commits Yoga rects into the rect store,
@@ -147,25 +153,39 @@ export const useRnContainer = (widgetRef, node, options) => {
147
153
  return;
148
154
  }
149
155
  attachRnLayout(widget, {
150
- measure: (orientation) => {
156
+ measure: (orientation, forSize) => {
151
157
  const custom = optionsRef.current?.measure;
152
158
  if (custom) {
153
- return custom(orientation);
159
+ return custom(orientation, forSize);
154
160
  }
155
161
  const rect = node.getRect();
156
162
  return Math.round((orientation === "horizontal" ? rect?.width : rect?.height) ?? 0);
157
163
  },
158
164
  allocate: (width, height) => {
165
+ const start = perfEnabled ? perfNow() : 0;
166
+ perfAllocDepth += 1;
159
167
  optionsRef.current?.beforeAllocate?.(width, height);
160
168
  let child = widget.getFirstChild();
169
+ let children = 0;
161
170
  while (child) {
162
171
  const rect = getStoredRect(child);
163
172
  if (rect) {
164
173
  const offset = getStoredOffset(child);
165
174
  allocateChild(child, rect.x + offset.dx, rect.y + offset.dy, rect.width, rect.height);
175
+ children += 1;
166
176
  }
167
177
  child = child.getNextSibling();
168
178
  }
179
+ perfAllocDepth -= 1;
180
+ if (perfEnabled) {
181
+ perfCount("gtk.allocPass");
182
+ perfCount("gtk.allocChild", children);
183
+ if (perfAllocDepth === 0) {
184
+ const elapsed = perfNow() - start;
185
+ perfAddTime("gtk.allocTop", elapsed);
186
+ perfBurst("gtk.allocMsPerFrame", elapsed);
187
+ }
188
+ }
169
189
  },
170
190
  });
171
191
  return () => {
@@ -1 +1 @@
1
- {"version":3,"file":"use-layout-child.js","sourceRoot":"","sources":["../../src/components/use-layout-child.ts"],"names":[],"mappings":"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","sourcesContent":["import { useLayoutEffect, useRef, useState, type RefObject } from \"react\"\nimport type { LayoutNode } from \"../layout/index\"\nimport { splitStyle, StyleSheet } from \"../style/index\"\nimport { defaultCssRegistry } from \"../style/registry.gtkx\"\nimport type {\n LayoutStyle,\n MeasureFn,\n Rect,\n StyleProp,\n VisualStyle,\n} from \"../contracts\"\nimport {\n allocateChild,\n attachRnLayout,\n detachRnLayout,\n measureWidget,\n queueAllocate,\n queueResize,\n type Gtk,\n} from \"../gtkx/bridge/index\"\nimport { useHostNode, type HostNode } from \"./host-node\"\nimport {\n deferDuringAllocate,\n getStoredOffset,\n getStoredRect,\n setStoredRect,\n} from \"./rect-store\"\n\nexport type LayoutEvent = {\n nativeEvent: {\n layout: { x: number; y: number; width: number; height: number }\n }\n}\n\nexport type LayoutChildOptions = {\n style: StyleProp | undefined\n onLayout: ((event: LayoutEvent) => void) | undefined\n measure?: MeasureFn\n // Leaves with intrinsic GTK sizes (Entry, Switch, Spinner) measure their\n // own widget so Yoga reserves exactly what the theme will draw.\n measureFromWidget?: boolean\n extraLayout?: LayoutStyle\n}\n\nexport type LayoutChild = {\n host: HostNode\n node: LayoutNode\n flat: ReturnType<typeof StyleSheet.flatten>\n visual: VisualStyle\n cssClass: string | null\n}\n\n// Shared plumbing for every leaf/container component: owns one LayoutNode,\n// keeps it in the parent's shadow tree for the component lifetime, applies\n// the layout half of the style and commits Yoga rects into the rect store,\n// where the parent container's RnGtkxLayout allocate() picks them up.\nexport const useLayoutChild = (\n widgetRef: RefObject<Gtk.Widget | null>,\n options: LayoutChildOptions,\n): LayoutChild => {\n const host = useHostNode()\n\n // useState's lazy initializer is guaranteed once-per-instance even under\n // the React Compiler — a lazy ref here got its creation memoized ACROSS\n // component instances, sharing one Yoga node and double-freeing it.\n const [node] = useState<LayoutNode>(() => host.engine.createNode())\n\n const flat = StyleSheet.flatten(options.style)\n const { layout, visual } = splitStyle(flat)\n const effectiveLayout = options.extraLayout\n ? { ...layout, ...options.extraLayout }\n : layout\n\n // Re-apply layout style only when it actually changed (keyed effect) —\n // setStyle dirties the whole tree.\n const layoutKey = JSON.stringify(effectiveLayout)\n useLayoutEffect(() => {\n node.setStyle(JSON.parse(layoutKey) as LayoutStyle)\n }, [node, layoutKey])\n\n const cssClass = defaultCssRegistry.getClassName(visual)\n\n useLayoutEffect(() => {\n const parent = host.node\n // The reconciler has already attached our widget in JSX order, so its\n // sibling position is the correct Yoga index — this keeps the shadow tree\n // ordered even for mid-list mounts (conditional children, list inserts).\n const widget = widgetRef.current\n const parentWidget = host.widgetRef.current\n let index = parent.children.length\n if (widget && parentWidget) {\n let position = 0\n let sibling = parentWidget.getFirstChild()\n while (sibling !== null && sibling !== widget) {\n position += 1\n sibling = sibling.getNextSibling()\n }\n if (sibling === widget) {\n index = Math.min(position, parent.children.length)\n }\n }\n parent.insertChild(node, index)\n node.setCommit((rect: Rect) => {\n const widget = widgetRef.current\n const parentWidget = host.widgetRef.current\n if (!widget || !parentWidget) {\n return\n }\n const previous = getStoredRect(widget)\n setStoredRect(widget, rect)\n const sizeChanged =\n !previous ||\n previous.width !== rect.width ||\n previous.height !== rect.height\n const queue = (): void => {\n if (sizeChanged) {\n // A size change invalidates this widget's cached measure (nested\n // managers, ScrolledWindow ranges) and re-allocates the ancestors.\n queueResize(widget)\n } else {\n // Pure move: the parent just needs another allocation pass. GTK\n // dedupes queued allocates, so per-child calls batch into one.\n queueAllocate(parentWidget)\n }\n }\n if (!deferDuringAllocate(widget, queue)) {\n queue()\n }\n })\n return () => {\n node.setCommit(null)\n parent.removeChild(node)\n node.free()\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [])\n\n const onLayout = options.onLayout\n useLayoutEffect(() => {\n node.setOnLayout(\n onLayout\n ? (rect: Rect) => {\n onLayout({ nativeEvent: { layout: rect } })\n }\n : null,\n )\n }, [node, onLayout])\n\n // The explicit return type keeps the inner params contextually typed: with a\n // bare lazy initializer TS may match the outer arrow against MeasureFn\n // itself (useState accepts S | () => S) and drop the context.\n const [widgetMeasure] = useState<MeasureFn>(\n (): MeasureFn => (width, widthMode) => {\n const widget = widgetRef.current\n if (!widget) {\n return { width: 0, height: 0 }\n }\n const natural = measureWidget(widget, \"horizontal\").natural\n const used =\n widthMode === \"undefined\"\n ? natural\n : Math.min(natural, Math.max(1, Math.floor(width)))\n return {\n width: used,\n height: measureWidget(widget, \"vertical\", used).natural,\n }\n },\n )\n const measure =\n options.measure ?? (options.measureFromWidget ? widgetMeasure : undefined)\n useLayoutEffect(() => {\n if (!measure) {\n return\n }\n node.setMeasureFn(measure)\n // Widgets measured before their window is mapped report zero (no realized\n // theme context yet, unlike offscreen probes) and Yoga caches it —\n // invalidate once the widget actually maps.\n const widget = widgetRef.current\n const remeasure = (): void => {\n node.markDirty()\n }\n if (options.measureFromWidget && widget) {\n widget.on(\"map\", remeasure)\n }\n return () => {\n if (options.measureFromWidget && widget) {\n widget.off(\"map\", remeasure)\n }\n // Cleared by free() on unmount; explicit unset only when measure is\n // removed while mounted.\n node.setMeasureFn(null)\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [node, measure])\n\n return { host, node, flat, visual, cssClass }\n}\n\nexport type RnContainerOptions = {\n // Overrides the default engine-rect measure (the window root reports 0 so\n // the window can shrink freely below the current content size).\n measure?: (orientation: \"horizontal\" | \"vertical\") => number\n // Runs before children are placed — the window root syncs the engine\n // viewport to the actual allocation here.\n beforeAllocate?: (width: number, height: number) => void\n}\n\n// Containers (Root, View, Animated.View, ScrollView content) drive their GTK\n// side with an RnGtkxLayout: measure() reports the engine rect of `node`\n// (minimum == natural — GTK minimums of children never leak upward), and\n// allocate() places every child widget at exactly its stored engine rect\n// plus the Animated offset.\nexport const useRnContainer = (\n widgetRef: RefObject<Gtk.Widget | null>,\n node: LayoutNode,\n options?: RnContainerOptions,\n): void => {\n const optionsRef = useRef<RnContainerOptions | undefined>(options)\n optionsRef.current = options\n useLayoutEffect(() => {\n const widget = widgetRef.current\n if (!widget) {\n return\n }\n attachRnLayout(widget, {\n measure: (orientation) => {\n const custom = optionsRef.current?.measure\n if (custom) {\n return custom(orientation)\n }\n const rect = node.getRect()\n return Math.round(\n (orientation === \"horizontal\" ? rect?.width : rect?.height) ?? 0,\n )\n },\n allocate: (width, height) => {\n optionsRef.current?.beforeAllocate?.(width, height)\n let child = widget.getFirstChild()\n while (child) {\n const rect = getStoredRect(child)\n if (rect) {\n const offset = getStoredOffset(child)\n allocateChild(\n child,\n rect.x + offset.dx,\n rect.y + offset.dy,\n rect.width,\n rect.height,\n )\n }\n child = child.getNextSibling()\n }\n },\n })\n return () => {\n detachRnLayout(widget)\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [])\n}\n"]}
1
+ {"version":3,"file":"use-layout-child.js","sourceRoot":"","sources":["../../src/components/use-layout-child.ts"],"names":[],"mappings":"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","sourcesContent":["import { useLayoutEffect, useRef, useState, type RefObject } from \"react\"\nimport type { LayoutNode } from \"../layout/index\"\nimport { splitStyle, StyleSheet } from \"../style/index\"\nimport { defaultCssRegistry } from \"../style/registry.gtkx\"\nimport type {\n LayoutStyle,\n MeasureFn,\n Rect,\n StyleProp,\n VisualStyle,\n} from \"../contracts\"\nimport {\n allocateChild,\n attachRnLayout,\n detachRnLayout,\n measureWidget,\n queueAllocate,\n queueResize,\n type Gtk,\n} from \"../gtkx/bridge/index\"\nimport {\n perfAddTime,\n perfBurst,\n perfCount,\n perfEnabled,\n perfNow,\n} from \"../perf\"\nimport { useHostNode, type HostNode } from \"./host-node\"\nimport {\n deferDuringAllocate,\n getStoredOffset,\n getStoredRect,\n setStoredRect,\n} from \"./rect-store\"\n\nexport type LayoutEvent = {\n nativeEvent: {\n layout: { x: number; y: number; width: number; height: number }\n }\n}\n\n// Perf: sizeAllocate recurses synchronously into child containers, so a\n// naive per-pass timer double-counts nested passes. Track depth and time\n// only the outermost (top-level) allocate — that is the real wall time GTK\n// spends in OUR layout code for the frame.\nlet perfAllocDepth = 0\n\nexport type LayoutChildOptions = {\n style: StyleProp | undefined\n onLayout: ((event: LayoutEvent) => void) | undefined\n measure?: MeasureFn\n // Leaves with intrinsic GTK sizes (Entry, Switch, Spinner) measure their\n // own widget so Yoga reserves exactly what the theme will draw.\n measureFromWidget?: boolean\n extraLayout?: LayoutStyle\n}\n\nexport type LayoutChild = {\n host: HostNode\n node: LayoutNode\n flat: ReturnType<typeof StyleSheet.flatten>\n visual: VisualStyle\n cssClass: string | null\n}\n\n// Shared plumbing for every leaf/container component: owns one LayoutNode,\n// keeps it in the parent's shadow tree for the component lifetime, applies\n// the layout half of the style and commits Yoga rects into the rect store,\n// where the parent container's RnGtkxLayout allocate() picks them up.\nexport const useLayoutChild = (\n widgetRef: RefObject<Gtk.Widget | null>,\n options: LayoutChildOptions,\n): LayoutChild => {\n const host = useHostNode()\n\n // useState's lazy initializer is guaranteed once-per-instance even under\n // the React Compiler — a lazy ref here got its creation memoized ACROSS\n // component instances, sharing one Yoga node and double-freeing it.\n const [node] = useState<LayoutNode>(() => host.engine.createNode())\n\n const flat = StyleSheet.flatten(options.style)\n const { layout, visual } = splitStyle(flat)\n const effectiveLayout = options.extraLayout\n ? { ...layout, ...options.extraLayout }\n : layout\n\n // Re-apply layout style only when it actually changed (keyed effect) —\n // setStyle dirties the whole tree.\n const layoutKey = JSON.stringify(effectiveLayout)\n useLayoutEffect(() => {\n node.setStyle(JSON.parse(layoutKey) as LayoutStyle)\n }, [node, layoutKey])\n\n const cssClass = defaultCssRegistry.getClassName(visual)\n\n useLayoutEffect(() => {\n const parent = host.node\n // The reconciler has already attached our widget in JSX order, so its\n // sibling position is the correct Yoga index — this keeps the shadow tree\n // ordered even for mid-list mounts (conditional children, list inserts).\n const widget = widgetRef.current\n const parentWidget = host.widgetRef.current\n let index = parent.children.length\n if (widget && parentWidget) {\n let position = 0\n let sibling = parentWidget.getFirstChild()\n while (sibling !== null && sibling !== widget) {\n position += 1\n sibling = sibling.getNextSibling()\n }\n if (sibling === widget) {\n index = Math.min(position, parent.children.length)\n }\n }\n parent.insertChild(node, index)\n node.setCommit((rect: Rect) => {\n const widget = widgetRef.current\n const parentWidget = host.widgetRef.current\n if (!widget || !parentWidget) {\n return\n }\n const previous = getStoredRect(widget)\n setStoredRect(widget, rect)\n const sizeChanged =\n !previous ||\n previous.width !== rect.width ||\n previous.height !== rect.height\n const queue = (): void => {\n if (sizeChanged) {\n // A size change invalidates this widget's cached measure (nested\n // managers, ScrolledWindow ranges) and re-allocates the ancestors.\n queueResize(widget)\n } else {\n // Pure move: the parent just needs another allocation pass. GTK\n // dedupes queued allocates, so per-child calls batch into one.\n queueAllocate(parentWidget)\n }\n }\n if (!deferDuringAllocate(widget, queue)) {\n queue()\n }\n })\n return () => {\n node.setCommit(null)\n parent.removeChild(node)\n node.free()\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [])\n\n const onLayout = options.onLayout\n useLayoutEffect(() => {\n node.setOnLayout(\n onLayout\n ? (rect: Rect) => {\n onLayout({ nativeEvent: { layout: rect } })\n }\n : null,\n )\n }, [node, onLayout])\n\n // The explicit return type keeps the inner params contextually typed: with a\n // bare lazy initializer TS may match the outer arrow against MeasureFn\n // itself (useState accepts S | () => S) and drop the context.\n const [widgetMeasure] = useState<MeasureFn>(\n (): MeasureFn => (width, widthMode) => {\n const widget = widgetRef.current\n if (!widget) {\n return { width: 0, height: 0 }\n }\n const natural = measureWidget(widget, \"horizontal\").natural\n const used =\n widthMode === \"undefined\"\n ? natural\n : Math.min(natural, Math.max(1, Math.floor(width)))\n return {\n width: used,\n height: measureWidget(widget, \"vertical\", used).natural,\n }\n },\n )\n const measure =\n options.measure ?? (options.measureFromWidget ? widgetMeasure : undefined)\n useLayoutEffect(() => {\n if (!measure) {\n return\n }\n node.setMeasureFn(measure)\n // Widgets measured before their window is mapped report zero (no realized\n // theme context yet, unlike offscreen probes) and Yoga caches it —\n // invalidate once the widget actually maps.\n const widget = widgetRef.current\n const remeasure = (): void => {\n node.markDirty()\n }\n if (options.measureFromWidget && widget) {\n widget.on(\"map\", remeasure)\n }\n return () => {\n if (options.measureFromWidget && widget) {\n widget.off(\"map\", remeasure)\n }\n // Cleared by free() on unmount; explicit unset only when measure is\n // removed while mounted.\n node.setMeasureFn(null)\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [node, measure])\n\n return { host, node, flat, visual, cssClass }\n}\n\nexport type RnContainerOptions = {\n // Overrides the default engine-rect measure (the window root reports 0 so\n // the window can shrink freely below the current content size; the\n // intrinsic root measures its content through Yoga, honoring forSize for\n // height-for-width).\n measure?: (orientation: \"horizontal\" | \"vertical\", forSize: number) => number\n // Runs before children are placed — the window root syncs the engine\n // viewport to the actual allocation here.\n beforeAllocate?: (width: number, height: number) => void\n}\n\n// Containers (Root, View, Animated.View, ScrollView content) drive their GTK\n// side with an RnGtkxLayout: measure() reports the engine rect of `node`\n// (minimum == natural — GTK minimums of children never leak upward), and\n// allocate() places every child widget at exactly its stored engine rect\n// plus the Animated offset.\nexport const useRnContainer = (\n widgetRef: RefObject<Gtk.Widget | null>,\n node: LayoutNode,\n options?: RnContainerOptions,\n): void => {\n const optionsRef = useRef<RnContainerOptions | undefined>(options)\n optionsRef.current = options\n useLayoutEffect(() => {\n const widget = widgetRef.current\n if (!widget) {\n return\n }\n attachRnLayout(widget, {\n measure: (orientation, forSize) => {\n const custom = optionsRef.current?.measure\n if (custom) {\n return custom(orientation, forSize)\n }\n const rect = node.getRect()\n return Math.round(\n (orientation === \"horizontal\" ? rect?.width : rect?.height) ?? 0,\n )\n },\n allocate: (width, height) => {\n const start = perfEnabled ? perfNow() : 0\n perfAllocDepth += 1\n optionsRef.current?.beforeAllocate?.(width, height)\n let child = widget.getFirstChild()\n let children = 0\n while (child) {\n const rect = getStoredRect(child)\n if (rect) {\n const offset = getStoredOffset(child)\n allocateChild(\n child,\n rect.x + offset.dx,\n rect.y + offset.dy,\n rect.width,\n rect.height,\n )\n children += 1\n }\n child = child.getNextSibling()\n }\n perfAllocDepth -= 1\n if (perfEnabled) {\n perfCount(\"gtk.allocPass\")\n perfCount(\"gtk.allocChild\", children)\n if (perfAllocDepth === 0) {\n const elapsed = perfNow() - start\n perfAddTime(\"gtk.allocTop\", elapsed)\n perfBurst(\"gtk.allocMsPerFrame\", elapsed)\n }\n }\n },\n })\n return () => {\n detachRnLayout(widget)\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [])\n}\n"]}
@@ -45,6 +45,8 @@ export type VirtualizedListProps<T> = Omit<ScrollViewProps, "children"> & {
45
45
  getItemLayout?: (data: readonly T[], index: number) => ItemLayout;
46
46
  windowSize?: number;
47
47
  initialNumToRender?: number;
48
+ maxToRenderPerBatch?: number;
49
+ updateCellsBatchingPeriod?: number;
48
50
  extraData?: unknown;
49
51
  inverted?: boolean;
50
52
  refreshing?: boolean;
@@ -7,6 +7,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
7
7
  // first visible row are anchored by compensating the scroll offset in the
8
8
  // same commit (docs/research/list-window.md).
9
9
  import { forwardRef, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react";
10
+ import { perfAddTime, perfBurst, perfCount, perfEnabled, perfNow, } from "../perf";
10
11
  import { ActivityIndicator } from "./activity-indicator";
11
12
  import { ScrollView, StickySlot, } from "./scroll-view";
12
13
  import { View } from "./view";
@@ -36,7 +37,26 @@ const indexAt = (offsets, target) => {
36
37
  }
37
38
  return low;
38
39
  };
39
- const VirtualizedListInner = forwardRef(({ data, renderItem, keyExtractor, estimatedItemSize = 44, getItemLayout, windowSize = 5, initialNumToRender = 10, extraData, inverted = false, refreshing = false, onRefresh, ItemSeparatorComponent, ListHeaderComponent, ListFooterComponent, ListEmptyComponent, onEndReached, onEndReachedThreshold = 0.1, onViewableItemsChanged, viewabilityConfig, onScroll, onContentSizeChange, onLayout, horizontal = false, stickyHeaderIndices, ...scrollProps }, ref) => {
40
+ // Overscan default. RN ships 5 because a phone pays for mounted rows in
41
+ // memory; a desktop app does not, and here the mounted set is what costs
42
+ // frames: every window boundary crossing is a mount + Yoga reflow + GTK
43
+ // allocate burst, so a WIDER window means fewer crossings per scrolled pixel.
44
+ // Measured (docs/research/scroll-performance.md): 11 instead of 5 cuts
45
+ // mount/unmount churn by ~21% and late frames from ~10/s to ~7.7/s.
46
+ const DEFAULT_WINDOW_SIZE = 11;
47
+ // Perf: counts real React mounts/unmounts of windowed cells (rendered as a
48
+ // null child inside each cell only when GTKX_PERF=1).
49
+ const CellMountProbe = () => {
50
+ useLayoutEffect(() => {
51
+ perfCount("vl.cellMount");
52
+ perfBurst("vl.mountsPerFrame");
53
+ return () => {
54
+ perfCount("vl.cellUnmount");
55
+ };
56
+ }, []);
57
+ return null;
58
+ };
59
+ const VirtualizedListInner = forwardRef(({ data, renderItem, keyExtractor, estimatedItemSize = 44, getItemLayout, windowSize = DEFAULT_WINDOW_SIZE, initialNumToRender = 10, maxToRenderPerBatch = 10, updateCellsBatchingPeriod = 50, extraData, inverted = false, refreshing = false, onRefresh, ItemSeparatorComponent, ListHeaderComponent, ListFooterComponent, ListEmptyComponent, onEndReached, onEndReachedThreshold = 0.1, onViewableItemsChanged, viewabilityConfig, onScroll, onContentSizeChange, onLayout, horizontal = false, stickyHeaderIndices, ...scrollProps }, ref) => {
40
60
  const count = data.length;
41
61
  const scrollRef = useRef(null);
42
62
  const measured = useRef([]);
@@ -98,9 +118,24 @@ const VirtualizedListInner = forwardRef(({ data, renderItem, keyExtractor, estim
98
118
  first: 0,
99
119
  last: Math.min(count, initialNumToRender) - 1,
100
120
  }));
101
- const updateRange = () => {
102
- const overscan = Math.max(1, (windowSize - 1) / 2) * viewportH.current;
103
- // Window in visual (scroll) coordinates.
121
+ // Mirror of `range` for event-time math: several scroll events (and a
122
+ // batch timer) can run before React re-renders, and each needs the range
123
+ // the previous one asked for, not the one currently rendered.
124
+ const rangeRef = useRef(range);
125
+ const batchTimer = useRef(null);
126
+ // Mount allowance as a token bucket: maxToRenderPerBatch rows per
127
+ // updateCellsBatchingPeriod, NOT per pass. One frame can run several passes
128
+ // (scroll event, content-size report, measurement version bump) and a
129
+ // per-pass cap would let them stack back into the burst we are avoiding.
130
+ // A steady scroll needs about one row per frame, far under the refill, so
131
+ // its window still follows the offset exactly.
132
+ const batchTokens = useRef(0);
133
+ const batchFilledAt = useRef(0);
134
+ // The batch timer must call the LATEST closure (offsets/data may change).
135
+ const updateRangeRef = useRef(() => { });
136
+ // Index range covering the visual window grown by `overscan` px on both
137
+ // sides (0 → exactly the visible rows).
138
+ const rangeFor = (overscan) => {
104
139
  let start = scrollY.current - overscan;
105
140
  let end = scrollY.current + viewportH.current + overscan;
106
141
  if (inverted) {
@@ -110,12 +145,89 @@ const VirtualizedListInner = forwardRef(({ data, renderItem, keyExtractor, estim
110
145
  const extent = offsets[count];
111
146
  [start, end] = [extent - end, extent - start];
112
147
  }
113
- const first = indexAt(offsets, Math.max(0, start));
114
- const last = Math.min(count - 1, indexAt(offsets, end));
115
- setRange((current) => current.first === first && current.last === last
116
- ? current
117
- : { first, last });
148
+ return {
149
+ first: indexAt(offsets, Math.max(0, start)),
150
+ last: Math.min(count - 1, indexAt(offsets, end)),
151
+ };
152
+ };
153
+ const updateRange = () => {
154
+ perfCount("vl.updateRange");
155
+ const target = rangeFor(Math.max(1, (windowSize - 1) / 2) * viewportH.current);
156
+ const current = rangeRef.current;
157
+ // MOUNT BUDGET (RN maxToRenderPerBatch). A flick or a scrollToOffset
158
+ // moves the offset by whole viewports in a single frame, so the target
159
+ // window can be an almost entirely new set of rows — mounting it in one
160
+ // go is a burst of cell mounts + Yoga reflow + GTK allocate that shows
161
+ // up as a visible mid-flick stall (docs/research/scroll-performance.md).
162
+ // The VISIBLE rows are always mounted at once (a blank row is worse than
163
+ // a late one); the overscan beyond them fills in over the following
164
+ // batches.
165
+ const visible = rangeFor(0);
166
+ // Reuse the mounted set only when it still covers what is on screen.
167
+ // The mounted set is ONE contiguous range, so keeping a window that no
168
+ // longer reaches the visible rows would mean mounting the whole gap
169
+ // between them in this pass — the burst, by another route. Overlapping
170
+ // the target window is not enough: with a wide windowSize a jump of
171
+ // several viewports still overlaps it.
172
+ const reusable = current.last >= visible.first && current.first <= visible.last;
173
+ // Keep what is both mounted and still wanted, always covering the
174
+ // visible rows; after a teleport nothing is reusable and the visible
175
+ // rows are the whole starting window.
176
+ let first = reusable
177
+ ? Math.min(Math.max(current.first, target.first), visible.first)
178
+ : visible.first;
179
+ let last = reusable
180
+ ? Math.max(Math.min(current.last, target.last), visible.last)
181
+ : visible.last;
182
+ // Rows the visible set already costs this pass count against the batch:
183
+ // after a teleport they ARE the batch.
184
+ const kept = reusable
185
+ ? Math.max(0, Math.min(last, current.last) - Math.max(first, current.first) + 1)
186
+ : 0;
187
+ const now = Date.now();
188
+ if (now - batchFilledAt.current >= updateCellsBatchingPeriod) {
189
+ batchTokens.current = maxToRenderPerBatch;
190
+ batchFilledAt.current = now;
191
+ }
192
+ let budget = Math.max(0, batchTokens.current - (last - first + 1 - kept));
193
+ while (budget > 0 && (first > target.first || last < target.last)) {
194
+ if (last < target.last) {
195
+ last += 1;
196
+ budget -= 1;
197
+ }
198
+ if (budget > 0 && first > target.first) {
199
+ first -= 1;
200
+ budget -= 1;
201
+ }
202
+ }
203
+ batchTokens.current = budget;
204
+ if (first !== current.first || last !== current.last) {
205
+ perfCount("vl.rangeChange");
206
+ rangeRef.current = { first, last };
207
+ setRange({ first, last });
208
+ }
209
+ // Still short of the full window: continue on the next batch tick.
210
+ if (first > target.first || last < target.last) {
211
+ perfCount("vl.rangePending");
212
+ if (batchTimer.current === null) {
213
+ batchTimer.current = setTimeout(() => {
214
+ batchTimer.current = null;
215
+ updateRangeRef.current();
216
+ }, updateCellsBatchingPeriod);
217
+ }
218
+ }
118
219
  };
220
+ useLayoutEffect(() => {
221
+ updateRangeRef.current = updateRange;
222
+ });
223
+ useEffect(() => {
224
+ return () => {
225
+ if (batchTimer.current !== null) {
226
+ clearTimeout(batchTimer.current);
227
+ batchTimer.current = null;
228
+ }
229
+ };
230
+ }, []);
119
231
  // Clamp a scroll target to the valid range of the axis.
120
232
  const clampOffset = (target) => {
121
233
  const max = Math.max(0, offsets[count] - viewportH.current);
@@ -227,6 +339,8 @@ const VirtualizedListInner = forwardRef(({ data, renderItem, keyExtractor, estim
227
339
  return;
228
340
  }
229
341
  measured.current[index] = size;
342
+ perfCount("vl.versionBump");
343
+ perfBurst("vl.bumpsPerFrame");
230
344
  // A size change shifts every cell BEFORE the changed one in visual
231
345
  // order — for a normal list that is cells above the window (index <
232
346
  // first), compensated by the anchor. Inverted lists skip the anchor
@@ -487,13 +601,14 @@ const VirtualizedListInner = forwardRef(({ data, renderItem, keyExtractor, estim
487
601
  const measure = (event) => onItemLayout(index, horizontal
488
602
  ? event.nativeEvent.layout.width
489
603
  : event.nativeEvent.layout.height);
490
- const body = (_jsxs(_Fragment, { children: [renderItem({ item, index }), index < count - 1 && ItemSeparatorComponent ? (_jsx(ItemSeparatorComponent, {})) : null] }));
604
+ const body = (_jsxs(_Fragment, { children: [perfEnabled ? _jsx(CellMountProbe, {}) : null, renderItem({ item, index }), index < count - 1 && ItemSeparatorComponent ? (_jsx(ItemSeparatorComponent, {})) : null] }));
491
605
  if (stickySorted.includes(index)) {
492
606
  return (_jsx(StickySlot, { stickyKey: `cell-${key}`, top: cellStart(index), style: cellStyle, onLayout: measure, children: body }, key));
493
607
  }
494
608
  return (_jsx(View, { style: cellStyle, onLayout: measure, children: body }, key));
495
609
  };
496
610
  const cells = [];
611
+ const renderStart = perfEnabled ? perfNow() : 0;
497
612
  if (count > 0) {
498
613
  if (activeStickyIndex >= 0 && activeStickyIndex < range.first) {
499
614
  cells.push(renderCell(activeStickyIndex));
@@ -503,6 +618,13 @@ const VirtualizedListInner = forwardRef(({ data, renderItem, keyExtractor, estim
503
618
  cells.push(renderCell(index));
504
619
  }
505
620
  }
621
+ if (perfEnabled) {
622
+ // Element construction only — reconciliation and host mutations happen
623
+ // inside React afterwards; those are visible in engine.flush and the
624
+ // mount counters instead.
625
+ perfAddTime("vl.renderCells", perfNow() - renderStart);
626
+ perfCount("vl.render");
627
+ }
506
628
  // extraData participates in identity so memoized parents re-render rows.
507
629
  void extraData;
508
630
  // Accepted for RN parity only; no desktop gesture ever calls it.