react-native-gtkx 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/animated/composite.d.ts +5 -0
- package/dist/animated/composite.js +132 -0
- package/dist/animated/composite.js.map +1 -0
- package/dist/animated/create-animated.d.ts +12 -0
- package/dist/animated/create-animated.js +17 -0
- package/dist/animated/create-animated.js.map +1 -0
- package/dist/animated/easing.d.ts +11 -0
- package/dist/animated/easing.js +91 -0
- package/dist/animated/easing.js.map +1 -0
- package/dist/animated/frame-loop.d.ts +6 -0
- package/dist/animated/frame-loop.js +25 -0
- package/dist/animated/frame-loop.js.map +1 -0
- package/dist/animated/index.d.ts +6 -0
- package/dist/animated/index.js +7 -0
- package/dist/animated/index.js.map +1 -0
- package/dist/animated/interpolate.d.ts +2 -0
- package/dist/animated/interpolate.js +90 -0
- package/dist/animated/interpolate.js.map +1 -0
- package/dist/animated/native-driver.d.ts +1 -0
- package/dist/animated/native-driver.js +17 -0
- package/dist/animated/native-driver.js.map +1 -0
- package/dist/animated/spring.d.ts +3 -0
- package/dist/animated/spring.js +67 -0
- package/dist/animated/spring.js.map +1 -0
- package/dist/animated/timing.d.ts +3 -0
- package/dist/animated/timing.js +30 -0
- package/dist/animated/timing.js.map +1 -0
- package/dist/animated/types.d.ts +51 -0
- package/dist/animated/types.js +6 -0
- package/dist/animated/types.js.map +1 -0
- package/dist/animated/value-animation.d.ts +9 -0
- package/dist/animated/value-animation.js +65 -0
- package/dist/animated/value-animation.js.map +1 -0
- package/dist/animated/value.d.ts +46 -0
- package/dist/animated/value.js +130 -0
- package/dist/animated/value.js.map +1 -0
- package/dist/apis/alert.d.ts +16 -0
- package/dist/apis/alert.js +33 -0
- package/dist/apis/alert.js.map +1 -0
- package/dist/apis/app-state.d.ts +11 -0
- package/dist/apis/app-state.js +54 -0
- package/dist/apis/app-state.js.map +1 -0
- package/dist/apis/appearance.d.ts +12 -0
- package/dist/apis/appearance.js +33 -0
- package/dist/apis/appearance.js.map +1 -0
- package/dist/apis/dev-settings.d.ts +9 -0
- package/dist/apis/dev-settings.js +25 -0
- package/dist/apis/dev-settings.js.map +1 -0
- package/dist/apis/dimensions.d.ts +14 -0
- package/dist/apis/dimensions.js +80 -0
- package/dist/apis/dimensions.js.map +1 -0
- package/dist/apis/emitter.d.ts +8 -0
- package/dist/apis/emitter.js +35 -0
- package/dist/apis/emitter.js.map +1 -0
- package/dist/apis/hooks.d.ts +11 -0
- package/dist/apis/hooks.js +29 -0
- package/dist/apis/hooks.js.map +1 -0
- package/dist/apis/host.d.ts +45 -0
- package/dist/apis/host.gtkx.d.ts +2 -0
- package/dist/apis/host.gtkx.js +301 -0
- package/dist/apis/host.gtkx.js.map +1 -0
- package/dist/apis/host.js +6 -0
- package/dist/apis/host.js.map +1 -0
- package/dist/apis/index.d.ts +39 -0
- package/dist/apis/index.js +21 -0
- package/dist/apis/index.js.map +1 -0
- package/dist/apis/linking.d.ts +7 -0
- package/dist/apis/linking.js +28 -0
- package/dist/apis/linking.js.map +1 -0
- package/dist/apis/platform.d.ts +14 -0
- package/dist/apis/platform.js +23 -0
- package/dist/apis/platform.js.map +1 -0
- package/dist/components/activity-indicator.d.ts +10 -0
- package/dist/components/activity-indicator.js +21 -0
- package/dist/components/activity-indicator.js.map +1 -0
- package/dist/components/animated.d.ts +34 -0
- package/dist/components/animated.js +152 -0
- package/dist/components/animated.js.map +1 -0
- package/dist/components/app-registry.d.ts +14 -0
- package/dist/components/app-registry.js +61 -0
- package/dist/components/app-registry.js.map +1 -0
- package/dist/components/flat-list.d.ts +48 -0
- package/dist/components/flat-list.js +23 -0
- package/dist/components/flat-list.js.map +1 -0
- package/dist/components/host-node.d.ts +10 -0
- package/dist/components/host-node.js +10 -0
- package/dist/components/host-node.js.map +1 -0
- package/dist/components/image-loader.d.ts +17 -0
- package/dist/components/image-loader.js +54 -0
- package/dist/components/image-loader.js.map +1 -0
- package/dist/components/image.d.ts +19 -0
- package/dist/components/image.js +73 -0
- package/dist/components/image.js.map +1 -0
- package/dist/components/index.d.ts +14 -0
- package/dist/components/index.js +14 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/modal.d.ts +12 -0
- package/dist/components/modal.js +18 -0
- package/dist/components/modal.js.map +1 -0
- package/dist/components/pressable.d.ts +33 -0
- package/dist/components/pressable.js +116 -0
- package/dist/components/pressable.js.map +1 -0
- package/dist/components/rect-store.d.ts +17 -0
- package/dist/components/rect-store.js +40 -0
- package/dist/components/rect-store.js.map +1 -0
- package/dist/components/root.d.ts +8 -0
- package/dist/components/root.js +59 -0
- package/dist/components/root.js.map +1 -0
- package/dist/components/scroll-view.d.ts +56 -0
- package/dist/components/scroll-view.js +324 -0
- package/dist/components/scroll-view.js.map +1 -0
- package/dist/components/switch.d.ts +11 -0
- package/dist/components/switch.js +20 -0
- package/dist/components/switch.js.map +1 -0
- package/dist/components/text-input.d.ts +23 -0
- package/dist/components/text-input.js +151 -0
- package/dist/components/text-input.js.map +1 -0
- package/dist/components/text.d.ts +11 -0
- package/dist/components/text.js +100 -0
- package/dist/components/text.js.map +1 -0
- package/dist/components/use-layout-child.d.ts +36 -0
- package/dist/components/use-layout-child.js +177 -0
- package/dist/components/use-layout-child.js.map +1 -0
- package/dist/components/view.d.ts +13 -0
- package/dist/components/view.js +60 -0
- package/dist/components/view.js.map +1 -0
- package/dist/components/virtualized-list.d.ts +67 -0
- package/dist/components/virtualized-list.js +519 -0
- package/dist/components/virtualized-list.js.map +1 -0
- package/dist/contracts.d.ts +119 -0
- package/dist/contracts.js +5 -0
- package/dist/contracts.js.map +1 -0
- package/dist/gtkx/bridge/index.d.ts +16 -0
- package/dist/gtkx/bridge/index.js +25 -0
- package/dist/gtkx/bridge/index.js.map +1 -0
- package/dist/gtkx/bridge/layout-manager.d.ts +11 -0
- package/dist/gtkx/bridge/layout-manager.js +87 -0
- package/dist/gtkx/bridge/layout-manager.js.map +1 -0
- package/dist/gtkx/bridge/measure.d.ts +9 -0
- package/dist/gtkx/bridge/measure.js +20 -0
- package/dist/gtkx/bridge/measure.js.map +1 -0
- package/dist/gtkx/bridge/theme.d.ts +4 -0
- package/dist/gtkx/bridge/theme.js +5 -0
- package/dist/gtkx/bridge/theme.js.map +1 -0
- package/dist/gtkx/bridge/view-box.d.ts +7 -0
- package/dist/gtkx/bridge/view-box.js +40 -0
- package/dist/gtkx/bridge/view-box.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/layout/apply-style.d.ts +4 -0
- package/dist/layout/apply-style.js +148 -0
- package/dist/layout/apply-style.js.map +1 -0
- package/dist/layout/engine.d.ts +22 -0
- package/dist/layout/engine.js +90 -0
- package/dist/layout/engine.js.map +1 -0
- package/dist/layout/index.d.ts +3 -0
- package/dist/layout/index.js +4 -0
- package/dist/layout/index.js.map +1 -0
- package/dist/layout/node.d.ts +33 -0
- package/dist/layout/node.js +151 -0
- package/dist/layout/node.js.map +1 -0
- package/dist/layout/yoga.d.ts +13 -0
- package/dist/layout/yoga.js +52 -0
- package/dist/layout/yoga.js.map +1 -0
- package/dist/metro/index.d.ts +42 -0
- package/dist/metro/index.js +119 -0
- package/dist/metro/index.js.map +1 -0
- package/dist/runner/host-dev.d.ts +1 -0
- package/dist/runner/host-dev.js +208 -0
- package/dist/runner/host-dev.js.map +1 -0
- package/dist/runner/host.d.ts +1 -0
- package/dist/runner/host.js +101 -0
- package/dist/runner/host.js.map +1 -0
- package/dist/runner/index.d.ts +26 -0
- package/dist/runner/index.js +179 -0
- package/dist/runner/index.js.map +1 -0
- package/dist/style/colors.d.ts +17 -0
- package/dist/style/colors.js +375 -0
- package/dist/style/colors.js.map +1 -0
- package/dist/style/dev-warning.d.ts +2 -0
- package/dist/style/dev-warning.js +18 -0
- package/dist/style/dev-warning.js.map +1 -0
- package/dist/style/index.d.ts +7 -0
- package/dist/style/index.js +14 -0
- package/dist/style/index.js.map +1 -0
- package/dist/style/registry.d.ts +12 -0
- package/dist/style/registry.gtkx.d.ts +2 -0
- package/dist/style/registry.gtkx.js +9 -0
- package/dist/style/registry.gtkx.js.map +1 -0
- package/dist/style/registry.js +23 -0
- package/dist/style/registry.js.map +1 -0
- package/dist/style/split-style.d.ts +6 -0
- package/dist/style/split-style.js +107 -0
- package/dist/style/split-style.js.map +1 -0
- package/dist/style/style-sheet.d.ts +43 -0
- package/dist/style/style-sheet.js +57 -0
- package/dist/style/style-sheet.js.map +1 -0
- package/dist/style/text-align.d.ts +7 -0
- package/dist/style/text-align.js +22 -0
- package/dist/style/text-align.js.map +1 -0
- package/dist/style/visual-css.d.ts +6 -0
- package/dist/style/visual-css.js +133 -0
- package/dist/style/visual-css.js.map +1 -0
- package/dist/vite/index.d.ts +53 -0
- package/dist/vite/index.js +127 -0
- package/dist/vite/index.js.map +1 -0
- package/package.json +88 -0
- package/react-native.config.js +21 -0
- package/types.d.ts +43 -0
- package/types.js +4 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
import { Gtk, GtkEntry, GtkLabel, GtkScrolledWindow, GtkTextView, } from "../gtkx/bridge/index";
|
|
4
|
+
import { useLayoutChild } from "./use-layout-child";
|
|
5
|
+
import { View } from "./view";
|
|
6
|
+
const INPUT_PURPOSE = {
|
|
7
|
+
default: Gtk.InputPurpose.FREE_FORM,
|
|
8
|
+
numeric: Gtk.InputPurpose.NUMBER,
|
|
9
|
+
"email-address": Gtk.InputPurpose.EMAIL,
|
|
10
|
+
"phone-pad": Gtk.InputPurpose.PHONE,
|
|
11
|
+
url: Gtk.InputPurpose.URL,
|
|
12
|
+
};
|
|
13
|
+
// RC1-WORKAROUND(controllers-as-children): see docs/gtkx-rc1-vs-main.md
|
|
14
|
+
// rc.1: controllers attach imperatively (JSX children work on main only).
|
|
15
|
+
const useFocusController = (widgetRef, onFocus, onBlur) => {
|
|
16
|
+
const handlers = useRef({ onFocus, onBlur });
|
|
17
|
+
handlers.current = { onFocus, onBlur };
|
|
18
|
+
useLayoutEffect(() => {
|
|
19
|
+
const widget = widgetRef.current;
|
|
20
|
+
if (!widget) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const focus = new Gtk.EventControllerFocus();
|
|
24
|
+
const enter = () => handlers.current.onFocus?.();
|
|
25
|
+
const leave = () => handlers.current.onBlur?.();
|
|
26
|
+
focus.on("enter", enter);
|
|
27
|
+
focus.on("leave", leave);
|
|
28
|
+
widget.addController(focus);
|
|
29
|
+
return () => {
|
|
30
|
+
widget.removeController(focus);
|
|
31
|
+
};
|
|
32
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
33
|
+
}, []);
|
|
34
|
+
};
|
|
35
|
+
// Single-line input over GtkEntry. gtkx's controlled-text behavior keeps the
|
|
36
|
+
// widget in sync with the `text` prop (RN controlled semantics); uncontrolled
|
|
37
|
+
// usage passes defaultValue once.
|
|
38
|
+
const SingleLineTextInput = ({ value, defaultValue, onChangeText, onSubmitEditing, onFocus, onBlur, placeholder, secureTextEntry = false, editable = true, keyboardType = "default", style, onLayout, testID, }) => {
|
|
39
|
+
const widgetRef = useRef(null);
|
|
40
|
+
useLayoutChild(widgetRef, {
|
|
41
|
+
style,
|
|
42
|
+
onLayout,
|
|
43
|
+
// The entry measures itself (theme-accurate height); width is driven by
|
|
44
|
+
// the style (flex/width), with a small floor for usability.
|
|
45
|
+
measureFromWidget: true,
|
|
46
|
+
extraLayout: { minWidth: 60 },
|
|
47
|
+
});
|
|
48
|
+
const controlled = value !== undefined;
|
|
49
|
+
useFocusController(widgetRef, onFocus, onBlur);
|
|
50
|
+
return (_jsx(GtkEntry, { ref: widgetRef, name: testID, text: controlled ? value : (defaultValue ?? ""), placeholderText: placeholder ?? "", visibility: !secureTextEntry, editable: editable, sensitive: editable, inputPurpose: INPUT_PURPOSE[keyboardType], onChanged: () => {
|
|
51
|
+
const widget = widgetRef.current;
|
|
52
|
+
if (widget) {
|
|
53
|
+
onChangeText?.(widget.getText());
|
|
54
|
+
}
|
|
55
|
+
}, onActivate: () => {
|
|
56
|
+
const widget = widgetRef.current;
|
|
57
|
+
if (widget) {
|
|
58
|
+
onSubmitEditing?.({ nativeEvent: { text: widget.getText() } });
|
|
59
|
+
}
|
|
60
|
+
} }));
|
|
61
|
+
};
|
|
62
|
+
const readBuffer = (buffer) => buffer.getText(buffer.getStartIter(), buffer.getEndIter(), true);
|
|
63
|
+
// GtkTextView has no native placeholder — a dim, click-transparent label
|
|
64
|
+
// sits over the empty view (RN shows the placeholder while empty, focused
|
|
65
|
+
// or not).
|
|
66
|
+
const MultilinePlaceholder = ({ text }) => {
|
|
67
|
+
const labelRef = useRef(null);
|
|
68
|
+
useLayoutChild(labelRef, {
|
|
69
|
+
style: { position: "absolute", left: 12, top: 8 },
|
|
70
|
+
onLayout: undefined,
|
|
71
|
+
});
|
|
72
|
+
useLayoutEffect(() => {
|
|
73
|
+
labelRef.current?.setCanTarget(false);
|
|
74
|
+
labelRef.current?.addCssClass("dim-label");
|
|
75
|
+
}, []);
|
|
76
|
+
return (_jsx(GtkLabel, { ref: labelRef, label: text }));
|
|
77
|
+
};
|
|
78
|
+
// Multiline input over GtkTextView in a GtkScrolledWindow: word wrap, the
|
|
79
|
+
// style sets the box (RN requires a height for multiline), overflowing
|
|
80
|
+
// content scrolls inside. The buffer syncs with `value` under RN controlled
|
|
81
|
+
// semantics; Enter inserts a newline (onSubmitEditing never fires — RN
|
|
82
|
+
// multiline behavior).
|
|
83
|
+
const MultilineTextInput = ({ value, defaultValue, onChangeText, onFocus, onBlur, placeholder, editable = true, style, onLayout, testID, }) => {
|
|
84
|
+
const scrolledRef = useRef(null);
|
|
85
|
+
const viewRef = useRef(null);
|
|
86
|
+
const controlled = value !== undefined;
|
|
87
|
+
const [empty, setEmpty] = useState((controlled ? (value ?? "") : (defaultValue ?? "")) === "");
|
|
88
|
+
const suppressEcho = useRef(false);
|
|
89
|
+
const latest = useRef({ onChangeText });
|
|
90
|
+
latest.current = { onChangeText };
|
|
91
|
+
useLayoutEffect(() => {
|
|
92
|
+
const view = viewRef.current;
|
|
93
|
+
if (!view) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
// Breathing room akin to GtkEntry's built-in padding.
|
|
97
|
+
view.setLeftMargin(10);
|
|
98
|
+
view.setRightMargin(10);
|
|
99
|
+
view.setTopMargin(8);
|
|
100
|
+
view.setBottomMargin(8);
|
|
101
|
+
const buffer = view.getBuffer();
|
|
102
|
+
const initial = controlled ? (value ?? "") : (defaultValue ?? "");
|
|
103
|
+
if (initial !== "") {
|
|
104
|
+
suppressEcho.current = true;
|
|
105
|
+
buffer.setText(initial, -1);
|
|
106
|
+
suppressEcho.current = false;
|
|
107
|
+
}
|
|
108
|
+
const changed = () => {
|
|
109
|
+
const text = readBuffer(buffer);
|
|
110
|
+
setEmpty(text === "");
|
|
111
|
+
if (!suppressEcho.current) {
|
|
112
|
+
latest.current.onChangeText?.(text);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
buffer.on("changed", changed);
|
|
116
|
+
return () => {
|
|
117
|
+
buffer.off("changed", changed);
|
|
118
|
+
};
|
|
119
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
120
|
+
}, []);
|
|
121
|
+
// RN controlled semantics: the prop wins over whatever was typed.
|
|
122
|
+
useLayoutEffect(() => {
|
|
123
|
+
if (!controlled) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const view = viewRef.current;
|
|
127
|
+
if (!view) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const buffer = view.getBuffer();
|
|
131
|
+
const next = value ?? "";
|
|
132
|
+
if (readBuffer(buffer) !== next) {
|
|
133
|
+
suppressEcho.current = true;
|
|
134
|
+
buffer.setText(next, -1);
|
|
135
|
+
suppressEcho.current = false;
|
|
136
|
+
setEmpty(next === "");
|
|
137
|
+
}
|
|
138
|
+
}, [controlled, value]);
|
|
139
|
+
useFocusController(viewRef, onFocus, onBlur);
|
|
140
|
+
return (_jsxs(View, { style: [style, { minHeight: 36, minWidth: 60 }], onLayout: onLayout, children: [_jsx(ScrolledFill, { scrolledRef: scrolledRef, children: _jsx(GtkTextView, { ref: viewRef, name: testID, editable: editable, sensitive: editable, wrapMode: Gtk.WrapMode.WORD_CHAR }) }), empty && placeholder ? (_jsx(MultilinePlaceholder, { text: placeholder })) : null] }));
|
|
141
|
+
};
|
|
142
|
+
// The scrolled window fills the styled box exactly (its own layout child).
|
|
143
|
+
const ScrolledFill = ({ scrolledRef, children, }) => {
|
|
144
|
+
useLayoutChild(scrolledRef, {
|
|
145
|
+
style: { position: "absolute", left: 0, right: 0, top: 0, bottom: 0 },
|
|
146
|
+
onLayout: undefined,
|
|
147
|
+
});
|
|
148
|
+
return _jsx(GtkScrolledWindow, { ref: scrolledRef, children: children });
|
|
149
|
+
};
|
|
150
|
+
export const TextInput = (props) => props.multiline ? (_jsx(MultilineTextInput, { ...props })) : (_jsx(SingleLineTextInput, { ...props }));
|
|
151
|
+
//# sourceMappingURL=text-input.js.map
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { StyleProp } from "../contracts";
|
|
3
|
+
import { type LayoutEvent } from "./use-layout-child";
|
|
4
|
+
export type TextProps = {
|
|
5
|
+
style?: StyleProp;
|
|
6
|
+
onLayout?: (event: LayoutEvent) => void;
|
|
7
|
+
numberOfLines?: number;
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
testID?: string;
|
|
10
|
+
};
|
|
11
|
+
export declare const Text: ({ style, onLayout, numberOfLines, children, testID, }: TextProps) => React.JSX.Element;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useLayoutEffect, useMemo, useRef, useState, } from "react";
|
|
3
|
+
import { textAlignToLabelProps } from "../style/index";
|
|
4
|
+
import { createTextProbe, Gtk, GtkLabel, measureWidget, Pango, } from "../gtkx/bridge/index";
|
|
5
|
+
import { useLayoutChild } from "./use-layout-child";
|
|
6
|
+
const flattenToString = (children) => {
|
|
7
|
+
if (children === null || children === undefined || children === false) {
|
|
8
|
+
return "";
|
|
9
|
+
}
|
|
10
|
+
if (Array.isArray(children)) {
|
|
11
|
+
return children.map(flattenToString).join("");
|
|
12
|
+
}
|
|
13
|
+
return String(children);
|
|
14
|
+
};
|
|
15
|
+
const JUSTIFICATION = {
|
|
16
|
+
left: Gtk.Justification.LEFT,
|
|
17
|
+
right: Gtk.Justification.RIGHT,
|
|
18
|
+
center: Gtk.Justification.CENTER,
|
|
19
|
+
fill: Gtk.Justification.FILL,
|
|
20
|
+
};
|
|
21
|
+
// Text is a measured Yoga leaf: an offscreen probe label (with the same CSS
|
|
22
|
+
// class, so fonts match) feeds Pango metrics into the layout engine; the
|
|
23
|
+
// visible GtkLabel is positioned by the commit hook like any other widget.
|
|
24
|
+
export const Text = ({ style, onLayout, numberOfLines, children, testID, }) => {
|
|
25
|
+
const widgetRef = useRef(null);
|
|
26
|
+
const text = flattenToString(children);
|
|
27
|
+
// useState lazy init — see the note in use-layout-child.ts (React Compiler).
|
|
28
|
+
const [probe] = useState(() => createTextProbe());
|
|
29
|
+
const lastProbeClass = useRef(null);
|
|
30
|
+
// GTK collapses wrap+ellipsize labels to a single line unless `lines` is
|
|
31
|
+
// set explicitly — remember how many lines the probe wrapped into, the
|
|
32
|
+
// layout callback pushes it onto the visible label.
|
|
33
|
+
const measuredLines = useRef(1);
|
|
34
|
+
const measure = useMemo(() => {
|
|
35
|
+
return (width, widthMode) => {
|
|
36
|
+
const natural = measureWidget(probe, "horizontal").natural;
|
|
37
|
+
const used = widthMode === "undefined"
|
|
38
|
+
? natural
|
|
39
|
+
: Math.min(natural, Math.max(1, Math.floor(width)));
|
|
40
|
+
const height = measureWidget(probe, "vertical", used).natural;
|
|
41
|
+
const singleLine = measureWidget(probe, "vertical", natural).natural;
|
|
42
|
+
measuredLines.current =
|
|
43
|
+
singleLine > 0 ? Math.max(1, Math.round(height / singleLine)) : 1;
|
|
44
|
+
return { width: used, height };
|
|
45
|
+
};
|
|
46
|
+
// The probe is stable; re-measure is triggered via markDirty below.
|
|
47
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
48
|
+
}, []);
|
|
49
|
+
const layoutWithLines = useMemo(() => (event) => {
|
|
50
|
+
const label = widgetRef.current;
|
|
51
|
+
if (label) {
|
|
52
|
+
label.setLines(numberOfLines ?? measuredLines.current);
|
|
53
|
+
}
|
|
54
|
+
onLayout?.(event);
|
|
55
|
+
}, [numberOfLines, onLayout]);
|
|
56
|
+
const { node, cssClass, flat } = useLayoutChild(widgetRef, {
|
|
57
|
+
style,
|
|
58
|
+
onLayout: layoutWithLines,
|
|
59
|
+
measure,
|
|
60
|
+
});
|
|
61
|
+
// RN text clips to its box: an under-allocated GtkLabel would otherwise
|
|
62
|
+
// paint its full text past the allocation (spike finding). Containers keep
|
|
63
|
+
// overflow VISIBLE (paint-overflow); the text leaf clips.
|
|
64
|
+
useLayoutEffect(() => {
|
|
65
|
+
widgetRef.current?.setOverflow(Gtk.Overflow.HIDDEN);
|
|
66
|
+
}, []);
|
|
67
|
+
// Keep the probe in sync with everything that affects metrics, then
|
|
68
|
+
// invalidate the Yoga leaf so the next pass re-measures.
|
|
69
|
+
useLayoutEffect(() => {
|
|
70
|
+
probe.setText(text);
|
|
71
|
+
if (numberOfLines !== undefined) {
|
|
72
|
+
probe.setLines(numberOfLines);
|
|
73
|
+
probe.setEllipsize(Pango.EllipsizeMode.END);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
probe.setLines(-1);
|
|
77
|
+
probe.setEllipsize(Pango.EllipsizeMode.NONE);
|
|
78
|
+
}
|
|
79
|
+
if (lastProbeClass.current !== cssClass) {
|
|
80
|
+
if (lastProbeClass.current) {
|
|
81
|
+
probe.removeCssClass(lastProbeClass.current);
|
|
82
|
+
}
|
|
83
|
+
if (cssClass) {
|
|
84
|
+
probe.addCssClass(cssClass);
|
|
85
|
+
}
|
|
86
|
+
lastProbeClass.current = cssClass;
|
|
87
|
+
}
|
|
88
|
+
node.markDirty();
|
|
89
|
+
}, [probe, node, text, numberOfLines, cssClass]);
|
|
90
|
+
const align = textAlignToLabelProps(flat.textAlign);
|
|
91
|
+
return (_jsx(GtkLabel, { ref: widgetRef, name: testID, label: text, wrap: true, xalign: align.xalign, yalign: 0, justify: JUSTIFICATION[align.justification], lines: numberOfLines,
|
|
92
|
+
// RN semantics: the ellipsis is opt-in via numberOfLines. Plain text
|
|
93
|
+
// wraps naturally; an unbreakable word wider than the rect just clips
|
|
94
|
+
// (overflow HIDDEN above) — the layout manager allocates the rect
|
|
95
|
+
// regardless of the label's own minimum, so nothing pushes siblings.
|
|
96
|
+
ellipsize: numberOfLines !== undefined
|
|
97
|
+
? Pango.EllipsizeMode.END
|
|
98
|
+
: Pango.EllipsizeMode.NONE, cssClasses: cssClass ? [cssClass] : [] }));
|
|
99
|
+
};
|
|
100
|
+
//# sourceMappingURL=text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text.js","sourceRoot":"","sources":["../../src/components/text.tsx"],"names":[],"mappings":"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","sourcesContent":["import {\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from \"react\"\nimport { textAlignToLabelProps } from \"../style/index\"\nimport type { MeasureFn, StyleProp } from \"../contracts\"\nimport {\n createTextProbe,\n Gtk,\n GtkLabel,\n measureWidget,\n Pango,\n} from \"../gtkx/bridge/index\"\nimport { useLayoutChild, type LayoutEvent } from \"./use-layout-child\"\n\nexport type TextProps = {\n style?: StyleProp\n onLayout?: (event: LayoutEvent) => void\n numberOfLines?: number\n children?: ReactNode\n testID?: string\n}\n\nconst flattenToString = (children: ReactNode): string => {\n if (children === null || children === undefined || children === false) {\n return \"\"\n }\n if (Array.isArray(children)) {\n return children.map(flattenToString).join(\"\")\n }\n return String(children)\n}\n\nconst JUSTIFICATION = {\n left: Gtk.Justification.LEFT,\n right: Gtk.Justification.RIGHT,\n center: Gtk.Justification.CENTER,\n fill: Gtk.Justification.FILL,\n} as const\n\n// Text is a measured Yoga leaf: an offscreen probe label (with the same CSS\n// class, so fonts match) feeds Pango metrics into the layout engine; the\n// visible GtkLabel is positioned by the commit hook like any other widget.\nexport const Text = ({\n style,\n onLayout,\n numberOfLines,\n children,\n testID,\n}: TextProps) => {\n const widgetRef = useRef<Gtk.Label | null>(null)\n const text = flattenToString(children)\n\n // useState lazy init — see the note in use-layout-child.ts (React Compiler).\n const [probe] = useState<Gtk.Label>(() => createTextProbe())\n\n const lastProbeClass = useRef<string | null>(null)\n\n // GTK collapses wrap+ellipsize labels to a single line unless `lines` is\n // set explicitly — remember how many lines the probe wrapped into, the\n // layout callback pushes it onto the visible label.\n const measuredLines = useRef(1)\n\n const measure = useMemo<MeasureFn>(() => {\n return (width, widthMode) => {\n const natural = measureWidget(probe, \"horizontal\").natural\n const used =\n widthMode === \"undefined\"\n ? natural\n : Math.min(natural, Math.max(1, Math.floor(width)))\n const height = measureWidget(probe, \"vertical\", used).natural\n const singleLine = measureWidget(probe, \"vertical\", natural).natural\n measuredLines.current =\n singleLine > 0 ? Math.max(1, Math.round(height / singleLine)) : 1\n return { width: used, height }\n }\n // The probe is stable; re-measure is triggered via markDirty below.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [])\n\n const layoutWithLines = useMemo(\n () => (event: LayoutEvent) => {\n const label = widgetRef.current\n if (label) {\n label.setLines(numberOfLines ?? measuredLines.current)\n }\n onLayout?.(event)\n },\n [numberOfLines, onLayout],\n )\n\n const { node, cssClass, flat } = useLayoutChild(widgetRef, {\n style,\n onLayout: layoutWithLines,\n measure,\n })\n\n // RN text clips to its box: an under-allocated GtkLabel would otherwise\n // paint its full text past the allocation (spike finding). Containers keep\n // overflow VISIBLE (paint-overflow); the text leaf clips.\n useLayoutEffect(() => {\n widgetRef.current?.setOverflow(Gtk.Overflow.HIDDEN)\n }, [])\n\n // Keep the probe in sync with everything that affects metrics, then\n // invalidate the Yoga leaf so the next pass re-measures.\n useLayoutEffect(() => {\n probe.setText(text)\n if (numberOfLines !== undefined) {\n probe.setLines(numberOfLines)\n probe.setEllipsize(Pango.EllipsizeMode.END)\n } else {\n probe.setLines(-1)\n probe.setEllipsize(Pango.EllipsizeMode.NONE)\n }\n if (lastProbeClass.current !== cssClass) {\n if (lastProbeClass.current) {\n probe.removeCssClass(lastProbeClass.current)\n }\n if (cssClass) {\n probe.addCssClass(cssClass)\n }\n lastProbeClass.current = cssClass\n }\n node.markDirty()\n }, [probe, node, text, numberOfLines, cssClass])\n\n const align = textAlignToLabelProps(flat.textAlign)\n\n return (\n <GtkLabel\n ref={widgetRef}\n name={testID}\n label={text}\n wrap\n xalign={align.xalign}\n yalign={0}\n justify={JUSTIFICATION[align.justification]}\n lines={numberOfLines}\n // RN semantics: the ellipsis is opt-in via numberOfLines. Plain text\n // wraps naturally; an unbreakable word wider than the rect just clips\n // (overflow HIDDEN above) — the layout manager allocates the rect\n // regardless of the label's own minimum, so nothing pushes siblings.\n ellipsize={\n numberOfLines !== undefined\n ? Pango.EllipsizeMode.END\n : Pango.EllipsizeMode.NONE\n }\n cssClasses={cssClass ? [cssClass] : []}\n />\n )\n}\n"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type RefObject } from "react";
|
|
2
|
+
import type { LayoutNode } from "../layout/index";
|
|
3
|
+
import { StyleSheet } from "../style/index";
|
|
4
|
+
import type { LayoutStyle, MeasureFn, StyleProp, VisualStyle } from "../contracts";
|
|
5
|
+
import { type Gtk } from "../gtkx/bridge/index";
|
|
6
|
+
import { type HostNode } from "./host-node";
|
|
7
|
+
export type LayoutEvent = {
|
|
8
|
+
nativeEvent: {
|
|
9
|
+
layout: {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export type LayoutChildOptions = {
|
|
18
|
+
style: StyleProp | undefined;
|
|
19
|
+
onLayout: ((event: LayoutEvent) => void) | undefined;
|
|
20
|
+
measure?: MeasureFn;
|
|
21
|
+
measureFromWidget?: boolean;
|
|
22
|
+
extraLayout?: LayoutStyle;
|
|
23
|
+
};
|
|
24
|
+
export type LayoutChild = {
|
|
25
|
+
host: HostNode;
|
|
26
|
+
node: LayoutNode;
|
|
27
|
+
flat: ReturnType<typeof StyleSheet.flatten>;
|
|
28
|
+
visual: VisualStyle;
|
|
29
|
+
cssClass: string | null;
|
|
30
|
+
};
|
|
31
|
+
export declare const useLayoutChild: (widgetRef: RefObject<Gtk.Widget | null>, options: LayoutChildOptions) => LayoutChild;
|
|
32
|
+
export type RnContainerOptions = {
|
|
33
|
+
measure?: (orientation: "horizontal" | "vertical") => number;
|
|
34
|
+
beforeAllocate?: (width: number, height: number) => void;
|
|
35
|
+
};
|
|
36
|
+
export declare const useRnContainer: (widgetRef: RefObject<Gtk.Widget | null>, node: LayoutNode, options?: RnContainerOptions) => void;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef, useState } from "react";
|
|
2
|
+
import { splitStyle, StyleSheet } from "../style/index";
|
|
3
|
+
import { defaultCssRegistry } from "../style/registry.gtkx";
|
|
4
|
+
import { allocateChild, attachRnLayout, detachRnLayout, measureWidget, queueAllocate, queueResize, } from "../gtkx/bridge/index";
|
|
5
|
+
import { useHostNode } from "./host-node";
|
|
6
|
+
import { deferDuringAllocate, getStoredOffset, getStoredRect, setStoredRect, } from "./rect-store";
|
|
7
|
+
// Shared plumbing for every leaf/container component: owns one LayoutNode,
|
|
8
|
+
// keeps it in the parent's shadow tree for the component lifetime, applies
|
|
9
|
+
// the layout half of the style and commits Yoga rects into the rect store,
|
|
10
|
+
// where the parent container's RnGtkxLayout allocate() picks them up.
|
|
11
|
+
export const useLayoutChild = (widgetRef, options) => {
|
|
12
|
+
const host = useHostNode();
|
|
13
|
+
// useState's lazy initializer is guaranteed once-per-instance even under
|
|
14
|
+
// the React Compiler — a lazy ref here got its creation memoized ACROSS
|
|
15
|
+
// component instances, sharing one Yoga node and double-freeing it.
|
|
16
|
+
const [node] = useState(() => host.engine.createNode());
|
|
17
|
+
const flat = StyleSheet.flatten(options.style);
|
|
18
|
+
const { layout, visual } = splitStyle(flat);
|
|
19
|
+
const effectiveLayout = options.extraLayout
|
|
20
|
+
? { ...layout, ...options.extraLayout }
|
|
21
|
+
: layout;
|
|
22
|
+
// Re-apply layout style only when it actually changed (keyed effect) —
|
|
23
|
+
// setStyle dirties the whole tree.
|
|
24
|
+
const layoutKey = JSON.stringify(effectiveLayout);
|
|
25
|
+
useLayoutEffect(() => {
|
|
26
|
+
node.setStyle(JSON.parse(layoutKey));
|
|
27
|
+
}, [node, layoutKey]);
|
|
28
|
+
const cssClass = defaultCssRegistry.getClassName(visual);
|
|
29
|
+
useLayoutEffect(() => {
|
|
30
|
+
const parent = host.node;
|
|
31
|
+
// The reconciler has already attached our widget in JSX order, so its
|
|
32
|
+
// sibling position is the correct Yoga index — this keeps the shadow tree
|
|
33
|
+
// ordered even for mid-list mounts (conditional children, list inserts).
|
|
34
|
+
const widget = widgetRef.current;
|
|
35
|
+
const parentWidget = host.widgetRef.current;
|
|
36
|
+
let index = parent.children.length;
|
|
37
|
+
if (widget && parentWidget) {
|
|
38
|
+
let position = 0;
|
|
39
|
+
let sibling = parentWidget.getFirstChild();
|
|
40
|
+
while (sibling !== null && sibling !== widget) {
|
|
41
|
+
position += 1;
|
|
42
|
+
sibling = sibling.getNextSibling();
|
|
43
|
+
}
|
|
44
|
+
if (sibling === widget) {
|
|
45
|
+
index = Math.min(position, parent.children.length);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
parent.insertChild(node, index);
|
|
49
|
+
node.setCommit((rect) => {
|
|
50
|
+
const widget = widgetRef.current;
|
|
51
|
+
const parentWidget = host.widgetRef.current;
|
|
52
|
+
if (!widget || !parentWidget) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const previous = getStoredRect(widget);
|
|
56
|
+
setStoredRect(widget, rect);
|
|
57
|
+
const sizeChanged = !previous ||
|
|
58
|
+
previous.width !== rect.width ||
|
|
59
|
+
previous.height !== rect.height;
|
|
60
|
+
const queue = () => {
|
|
61
|
+
if (sizeChanged) {
|
|
62
|
+
// A size change invalidates this widget's cached measure (nested
|
|
63
|
+
// managers, ScrolledWindow ranges) and re-allocates the ancestors.
|
|
64
|
+
queueResize(widget);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// Pure move: the parent just needs another allocation pass. GTK
|
|
68
|
+
// dedupes queued allocates, so per-child calls batch into one.
|
|
69
|
+
queueAllocate(parentWidget);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
if (!deferDuringAllocate(widget, queue)) {
|
|
73
|
+
queue();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
return () => {
|
|
77
|
+
node.setCommit(null);
|
|
78
|
+
parent.removeChild(node);
|
|
79
|
+
node.free();
|
|
80
|
+
};
|
|
81
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
82
|
+
}, []);
|
|
83
|
+
const onLayout = options.onLayout;
|
|
84
|
+
useLayoutEffect(() => {
|
|
85
|
+
node.setOnLayout(onLayout
|
|
86
|
+
? (rect) => {
|
|
87
|
+
onLayout({ nativeEvent: { layout: rect } });
|
|
88
|
+
}
|
|
89
|
+
: null);
|
|
90
|
+
}, [node, onLayout]);
|
|
91
|
+
// The explicit return type keeps the inner params contextually typed: with a
|
|
92
|
+
// bare lazy initializer TS may match the outer arrow against MeasureFn
|
|
93
|
+
// itself (useState accepts S | () => S) and drop the context.
|
|
94
|
+
const [widgetMeasure] = useState(() => (width, widthMode) => {
|
|
95
|
+
const widget = widgetRef.current;
|
|
96
|
+
if (!widget) {
|
|
97
|
+
return { width: 0, height: 0 };
|
|
98
|
+
}
|
|
99
|
+
const natural = measureWidget(widget, "horizontal").natural;
|
|
100
|
+
const used = widthMode === "undefined"
|
|
101
|
+
? natural
|
|
102
|
+
: Math.min(natural, Math.max(1, Math.floor(width)));
|
|
103
|
+
return {
|
|
104
|
+
width: used,
|
|
105
|
+
height: measureWidget(widget, "vertical", used).natural,
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
const measure = options.measure ?? (options.measureFromWidget ? widgetMeasure : undefined);
|
|
109
|
+
useLayoutEffect(() => {
|
|
110
|
+
if (!measure) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
node.setMeasureFn(measure);
|
|
114
|
+
// Widgets measured before their window is mapped report zero (no realized
|
|
115
|
+
// theme context yet, unlike offscreen probes) and Yoga caches it —
|
|
116
|
+
// invalidate once the widget actually maps.
|
|
117
|
+
const widget = widgetRef.current;
|
|
118
|
+
const remeasure = () => {
|
|
119
|
+
node.markDirty();
|
|
120
|
+
};
|
|
121
|
+
if (options.measureFromWidget && widget) {
|
|
122
|
+
widget.on("map", remeasure);
|
|
123
|
+
}
|
|
124
|
+
return () => {
|
|
125
|
+
if (options.measureFromWidget && widget) {
|
|
126
|
+
widget.off("map", remeasure);
|
|
127
|
+
}
|
|
128
|
+
// Cleared by free() on unmount; explicit unset only when measure is
|
|
129
|
+
// removed while mounted.
|
|
130
|
+
node.setMeasureFn(null);
|
|
131
|
+
};
|
|
132
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
133
|
+
}, [node, measure]);
|
|
134
|
+
return { host, node, flat, visual, cssClass };
|
|
135
|
+
};
|
|
136
|
+
// Containers (Root, View, Animated.View, ScrollView content) drive their GTK
|
|
137
|
+
// side with an RnGtkxLayout: measure() reports the engine rect of `node`
|
|
138
|
+
// (minimum == natural — GTK minimums of children never leak upward), and
|
|
139
|
+
// allocate() places every child widget at exactly its stored engine rect
|
|
140
|
+
// plus the Animated offset.
|
|
141
|
+
export const useRnContainer = (widgetRef, node, options) => {
|
|
142
|
+
const optionsRef = useRef(options);
|
|
143
|
+
optionsRef.current = options;
|
|
144
|
+
useLayoutEffect(() => {
|
|
145
|
+
const widget = widgetRef.current;
|
|
146
|
+
if (!widget) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
attachRnLayout(widget, {
|
|
150
|
+
measure: (orientation) => {
|
|
151
|
+
const custom = optionsRef.current?.measure;
|
|
152
|
+
if (custom) {
|
|
153
|
+
return custom(orientation);
|
|
154
|
+
}
|
|
155
|
+
const rect = node.getRect();
|
|
156
|
+
return Math.round((orientation === "horizontal" ? rect?.width : rect?.height) ?? 0);
|
|
157
|
+
},
|
|
158
|
+
allocate: (width, height) => {
|
|
159
|
+
optionsRef.current?.beforeAllocate?.(width, height);
|
|
160
|
+
let child = widget.getFirstChild();
|
|
161
|
+
while (child) {
|
|
162
|
+
const rect = getStoredRect(child);
|
|
163
|
+
if (rect) {
|
|
164
|
+
const offset = getStoredOffset(child);
|
|
165
|
+
allocateChild(child, rect.x + offset.dx, rect.y + offset.dy, rect.width, rect.height);
|
|
166
|
+
}
|
|
167
|
+
child = child.getNextSibling();
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
return () => {
|
|
172
|
+
detachRnLayout(widget);
|
|
173
|
+
};
|
|
174
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
175
|
+
}, []);
|
|
176
|
+
};
|
|
177
|
+
//# sourceMappingURL=use-layout-child.js.map
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { PointerEventsValue, StyleProp } from "../contracts";
|
|
3
|
+
import { type LayoutEvent } from "./use-layout-child";
|
|
4
|
+
export type ViewProps = {
|
|
5
|
+
style?: StyleProp;
|
|
6
|
+
pointerEvents?: PointerEventsValue;
|
|
7
|
+
onLayout?: (event: LayoutEvent) => void;
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
testID?: string;
|
|
10
|
+
};
|
|
11
|
+
export declare const View: ({ style, pointerEvents, onLayout, children, testID, }: ViewProps) => React.JSX.Element;
|
|
12
|
+
export declare const SafeAreaView: ({ style, pointerEvents, onLayout, children, testID, }: ViewProps) => React.JSX.Element;
|
|
13
|
+
export declare const StatusBar: () => null;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useLayoutEffect, useRef } from "react";
|
|
3
|
+
import { StyleSheet } from "../style/index";
|
|
4
|
+
import { getViewBoxComponent, setBoxPassthrough, } from "../gtkx/bridge/index";
|
|
5
|
+
import { HostNodeContext } from "./host-node";
|
|
6
|
+
import { useLayoutChild, useRnContainer, } from "./use-layout-child";
|
|
7
|
+
// Every View is a GtkBox subclass (RnGtkxViewBox) driven by RnGtkxLayout:
|
|
8
|
+
// Yoga computes the children's rects, the manager's allocate() applies
|
|
9
|
+
// them. Visual styles arrive as a GTK CSS class produced by the style
|
|
10
|
+
// system. pointerEvents maps onto GTK picking:
|
|
11
|
+
// - none: can-target=false — GTK skips the widget WITHOUT descending, the
|
|
12
|
+
// whole subtree is transparent (exact RN semantics);
|
|
13
|
+
// - box-none: the subclass' contains() fails for this widget (see
|
|
14
|
+
// view-box.ts) — the box is never the pick target while children stay
|
|
15
|
+
// pickable, and toggling never remounts the subtree;
|
|
16
|
+
// - box-only: direct children (and thus their subtrees) get
|
|
17
|
+
// can-target=false; restored when the mode changes. Nesting another
|
|
18
|
+
// pointerEvents INSIDE a box-only view is not supported (the restore
|
|
19
|
+
// pass cannot know about it).
|
|
20
|
+
export const View = ({ style, pointerEvents, onLayout, children, testID, }) => {
|
|
21
|
+
const widgetRef = useRef(null);
|
|
22
|
+
const { host, node, cssClass } = useLayoutChild(widgetRef, {
|
|
23
|
+
style,
|
|
24
|
+
onLayout,
|
|
25
|
+
});
|
|
26
|
+
useRnContainer(widgetRef, node);
|
|
27
|
+
const mode = pointerEvents ?? StyleSheet.flatten(style)?.pointerEvents ?? "auto";
|
|
28
|
+
useLayoutEffect(() => {
|
|
29
|
+
const widget = widgetRef.current;
|
|
30
|
+
if (!widget) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
widget.setCanTarget(mode !== "none");
|
|
34
|
+
setBoxPassthrough(widget, mode === "box-none");
|
|
35
|
+
}, [mode]);
|
|
36
|
+
// box-only walks the current children every commit (the set changes with
|
|
37
|
+
// renders) and restores once when the mode moves away.
|
|
38
|
+
const wasBoxOnly = useRef(false);
|
|
39
|
+
useLayoutEffect(() => {
|
|
40
|
+
const widget = widgetRef.current;
|
|
41
|
+
if (!widget) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const boxOnly = mode === "box-only";
|
|
45
|
+
if (boxOnly || wasBoxOnly.current) {
|
|
46
|
+
let child = widget.getFirstChild();
|
|
47
|
+
while (child) {
|
|
48
|
+
child.setCanTarget(!boxOnly);
|
|
49
|
+
child = child.getNextSibling();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
wasBoxOnly.current = boxOnly;
|
|
53
|
+
});
|
|
54
|
+
const ViewBox = getViewBoxComponent();
|
|
55
|
+
return (_jsx(ViewBox, { ref: widgetRef, name: testID, cssClasses: cssClass ? [cssClass] : [], children: _jsx(HostNodeContext.Provider, { value: { engine: host.engine, node, widgetRef }, children: children }) }));
|
|
56
|
+
};
|
|
57
|
+
// RN parity aliases (no notches or status bars on the Linux desktop).
|
|
58
|
+
export const SafeAreaView = View;
|
|
59
|
+
export const StatusBar = () => null;
|
|
60
|
+
//# sourceMappingURL=view.js.map
|