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,33 @@
|
|
|
1
|
+
import { createSharedEmitter } from "./emitter";
|
|
2
|
+
export const createAppearance = (host) => {
|
|
3
|
+
let lastEmitted = null;
|
|
4
|
+
const emitter = createSharedEmitter(() => {
|
|
5
|
+
lastEmitted = host.getColorScheme();
|
|
6
|
+
const subscription = host.onColorSchemeChange(() => {
|
|
7
|
+
const colorScheme = host.getColorScheme();
|
|
8
|
+
if (colorScheme === lastEmitted) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
lastEmitted = colorScheme;
|
|
12
|
+
emitter.emit({ colorScheme });
|
|
13
|
+
});
|
|
14
|
+
return () => {
|
|
15
|
+
subscription.remove();
|
|
16
|
+
lastEmitted = null;
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
return {
|
|
20
|
+
// Always resolved to "light" | "dark" (never null): Adwaita reports an
|
|
21
|
+
// effective scheme even while following the system preference.
|
|
22
|
+
getColorScheme: () => host.getColorScheme(),
|
|
23
|
+
// Implemented for real (not a no-op): forces the scheme app-wide through
|
|
24
|
+
// AdwStyleManager (FORCE_LIGHT / FORCE_DARK); null or undefined returns
|
|
25
|
+
// to following the system preference. Listeners fire via the resulting
|
|
26
|
+
// host change notification.
|
|
27
|
+
setColorScheme: (scheme) => {
|
|
28
|
+
host.setColorScheme(scheme ?? null);
|
|
29
|
+
},
|
|
30
|
+
addChangeListener: (handler) => emitter.add(handler),
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=appearance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"appearance.js","sourceRoot":"","sources":["../../src/apis/appearance.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAA;AAS/C,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,IAAoB,EAAE,EAAE;IACvD,IAAI,WAAW,GAA2B,IAAI,CAAA;IAE9C,MAAM,OAAO,GAAG,mBAAmB,CAA0B,GAAG,EAAE;QAChE,WAAW,GAAG,IAAI,CAAC,cAAc,EAAE,CAAA;QACnC,MAAM,YAAY,GAAG,IAAI,CAAC,mBAAmB,CAAC,GAAG,EAAE;YACjD,MAAM,WAAW,GAAG,IAAI,CAAC,cAAc,EAAE,CAAA;YACzC,IAAI,WAAW,KAAK,WAAW,EAAE,CAAC;gBAChC,OAAM;YACR,CAAC;YACD,WAAW,GAAG,WAAW,CAAA;YACzB,OAAO,CAAC,IAAI,CAAC,EAAE,WAAW,EAAE,CAAC,CAAA;QAC/B,CAAC,CAAC,CAAA;QACF,OAAO,GAAG,EAAE;YACV,YAAY,CAAC,MAAM,EAAE,CAAA;YACrB,WAAW,GAAG,IAAI,CAAA;QACpB,CAAC,CAAA;IACH,CAAC,CAAC,CAAA;IAEF,OAAO;QACL,uEAAuE;QACvE,+DAA+D;QAC/D,cAAc,EAAE,GAAoB,EAAE,CAAC,IAAI,CAAC,cAAc,EAAE;QAC5D,yEAAyE;QACzE,wEAAwE;QACxE,uEAAuE;QACvE,4BAA4B;QAC5B,cAAc,EAAE,CAAC,MAA0C,EAAQ,EAAE;YACnE,IAAI,CAAC,cAAc,CAAC,MAAM,IAAI,IAAI,CAAC,CAAA;QACrC,CAAC;QACD,iBAAiB,EAAE,CAAC,OAAgC,EAAsB,EAAE,CAC1E,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC;KACvB,CAAA;AACH,CAAC,CAAA","sourcesContent":["import type { SubscriptionHandle } from \"../contracts\"\nimport { createSharedEmitter } from \"./emitter\"\nimport type { AppearanceHost, ColorSchemeName } from \"./host\"\n\nexport type AppearancePreferences = { colorScheme: ColorSchemeName }\n\nexport type AppearanceChangeHandler = (\n preferences: AppearancePreferences,\n) => void\n\nexport const createAppearance = (host: AppearanceHost) => {\n let lastEmitted: ColorSchemeName | null = null\n\n const emitter = createSharedEmitter<[AppearancePreferences]>(() => {\n lastEmitted = host.getColorScheme()\n const subscription = host.onColorSchemeChange(() => {\n const colorScheme = host.getColorScheme()\n if (colorScheme === lastEmitted) {\n return\n }\n lastEmitted = colorScheme\n emitter.emit({ colorScheme })\n })\n return () => {\n subscription.remove()\n lastEmitted = null\n }\n })\n\n return {\n // Always resolved to \"light\" | \"dark\" (never null): Adwaita reports an\n // effective scheme even while following the system preference.\n getColorScheme: (): ColorSchemeName => host.getColorScheme(),\n // Implemented for real (not a no-op): forces the scheme app-wide through\n // AdwStyleManager (FORCE_LIGHT / FORCE_DARK); null or undefined returns\n // to following the system preference. Listeners fire via the resulting\n // host change notification.\n setColorScheme: (scheme: ColorSchemeName | null | undefined): void => {\n host.setColorScheme(scheme ?? null)\n },\n addChangeListener: (handler: AppearanceChangeHandler): SubscriptionHandle =>\n emitter.add(handler),\n }\n}\n\nexport type AppearanceModule = ReturnType<typeof createAppearance>\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const DevSettings: {
|
|
2
|
+
/**
|
|
3
|
+
* Adds an entry to the Dev Menu. Registering the same title again
|
|
4
|
+
* replaces the previous handler (RN semantics).
|
|
5
|
+
*/
|
|
6
|
+
addMenuItem(title: string, handler: () => void): void;
|
|
7
|
+
/** Reloads the application (dev mode only; a no-op in release). */
|
|
8
|
+
reload(reason?: string): void;
|
|
9
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// RN DevSettings for the linux platform. In dev mode (`run-linux --dev`)
|
|
2
|
+
// the dev host installs the reload hook and shows registered items in the
|
|
3
|
+
// Dev Menu (Ctrl+Shift+D); in release builds neither exists, so every call
|
|
4
|
+
// is a silent no-op — the same contract as RN release builds. The globals
|
|
5
|
+
// are declared in ../runner/globals.d.ts (the host↔bundle bridge).
|
|
6
|
+
export const DevSettings = {
|
|
7
|
+
/**
|
|
8
|
+
* Adds an entry to the Dev Menu. Registering the same title again
|
|
9
|
+
* replaces the previous handler (RN semantics).
|
|
10
|
+
*/
|
|
11
|
+
addMenuItem(title, handler) {
|
|
12
|
+
const items = (globalThis.__rnGtkxDevMenuItems ??= []);
|
|
13
|
+
const existing = items.findIndex((item) => item.title === title);
|
|
14
|
+
if (existing >= 0) {
|
|
15
|
+
items[existing] = { title, handler };
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
items.push({ title, handler });
|
|
19
|
+
},
|
|
20
|
+
/** Reloads the application (dev mode only; a no-op in release). */
|
|
21
|
+
reload(reason) {
|
|
22
|
+
globalThis.__rnGtkxDevHost?.reload(reason);
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=dev-settings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev-settings.js","sourceRoot":"","sources":["../../src/apis/dev-settings.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,0EAA0E;AAC1E,2EAA2E;AAC3E,0EAA0E;AAC1E,mEAAmE;AAEnE,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB;;;OAGG;IACH,WAAW,CAAC,KAAa,EAAE,OAAmB;QAC5C,MAAM,KAAK,GAAG,CAAC,UAAU,CAAC,oBAAoB,KAAK,EAAE,CAAC,CAAA;QACtD,MAAM,QAAQ,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,KAAK,KAAK,CAAC,CAAA;QAChE,IAAI,QAAQ,IAAI,CAAC,EAAE,CAAC;YAClB,KAAK,CAAC,QAAQ,CAAC,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,CAAA;YACpC,OAAM;QACR,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAA;IAChC,CAAC;IAED,mEAAmE;IACnE,MAAM,CAAC,MAAe;QACpB,UAAU,CAAC,eAAe,EAAE,MAAM,CAAC,MAAM,CAAC,CAAA;IAC5C,CAAC;CACF,CAAA","sourcesContent":["// RN DevSettings for the linux platform. In dev mode (`run-linux --dev`)\n// the dev host installs the reload hook and shows registered items in the\n// Dev Menu (Ctrl+Shift+D); in release builds neither exists, so every call\n// is a silent no-op — the same contract as RN release builds. The globals\n// are declared in ../runner/globals.d.ts (the host↔bundle bridge).\n\nexport const DevSettings = {\n /**\n * Adds an entry to the Dev Menu. Registering the same title again\n * replaces the previous handler (RN semantics).\n */\n addMenuItem(title: string, handler: () => void): void {\n const items = (globalThis.__rnGtkxDevMenuItems ??= [])\n const existing = items.findIndex((item) => item.title === title)\n if (existing >= 0) {\n items[existing] = { title, handler }\n return\n }\n items.push({ title, handler })\n },\n\n /** Reloads the application (dev mode only; a no-op in release). */\n reload(reason?: string): void {\n globalThis.__rnGtkxDevHost?.reload(reason)\n },\n}\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SubscriptionHandle } from "../contracts";
|
|
2
|
+
import type { DimensionsHost, ScaledSize } from "./host";
|
|
3
|
+
export type DimensionKey = "window" | "screen";
|
|
4
|
+
export type DimensionsPayload = {
|
|
5
|
+
window: ScaledSize;
|
|
6
|
+
screen: ScaledSize;
|
|
7
|
+
};
|
|
8
|
+
export type DimensionsChangeHandler = (payload: DimensionsPayload) => void;
|
|
9
|
+
export declare const createDimensions: (host: DimensionsHost) => {
|
|
10
|
+
get: (key: DimensionKey) => ScaledSize;
|
|
11
|
+
set: (dimensions: Partial<Record<DimensionKey, Partial<ScaledSize>>>) => void;
|
|
12
|
+
addEventListener: (type: "change", handler: DimensionsChangeHandler) => SubscriptionHandle;
|
|
13
|
+
};
|
|
14
|
+
export type DimensionsModule = ReturnType<typeof createDimensions>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createSharedEmitter } from "./emitter";
|
|
2
|
+
const sameSize = (a, b) => a.width === b.width &&
|
|
3
|
+
a.height === b.height &&
|
|
4
|
+
a.scale === b.scale &&
|
|
5
|
+
a.fontScale === b.fontScale;
|
|
6
|
+
export const createDimensions = (host) => {
|
|
7
|
+
const cache = {
|
|
8
|
+
window: null,
|
|
9
|
+
screen: null,
|
|
10
|
+
};
|
|
11
|
+
// Reads through to the host while keeping object identity stable when the
|
|
12
|
+
// values are unchanged (required by the useSyncExternalStore snapshot
|
|
13
|
+
// contract in useWindowDimensions).
|
|
14
|
+
const refresh = (key) => {
|
|
15
|
+
const next = key === "window" ? host.getWindowMetrics() : host.getScreenMetrics();
|
|
16
|
+
const previous = cache[key];
|
|
17
|
+
if (previous && sameSize(previous, next)) {
|
|
18
|
+
return previous;
|
|
19
|
+
}
|
|
20
|
+
cache[key] = next;
|
|
21
|
+
return next;
|
|
22
|
+
};
|
|
23
|
+
const emitter = createSharedEmitter(() => {
|
|
24
|
+
// Prime the cache when going live so the first host event can diff.
|
|
25
|
+
refresh("window");
|
|
26
|
+
refresh("screen");
|
|
27
|
+
const subscription = host.onMetricsChange(() => {
|
|
28
|
+
const previousWindow = cache.window;
|
|
29
|
+
const previousScreen = cache.screen;
|
|
30
|
+
const window = refresh("window");
|
|
31
|
+
const screen = refresh("screen");
|
|
32
|
+
if (window !== previousWindow || screen !== previousScreen) {
|
|
33
|
+
emitter.emit({ window, screen });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
return () => subscription.remove();
|
|
37
|
+
});
|
|
38
|
+
const get = (key) => {
|
|
39
|
+
if (key !== "window" && key !== "screen") {
|
|
40
|
+
throw new Error(`Dimensions.get: unknown dimension "${String(key)}"`);
|
|
41
|
+
}
|
|
42
|
+
// While subscribed the cache is kept fresh by host events; when idle we
|
|
43
|
+
// read through to the host on every call.
|
|
44
|
+
if (emitter.hasListeners()) {
|
|
45
|
+
return cache[key] ?? refresh(key);
|
|
46
|
+
}
|
|
47
|
+
return refresh(key);
|
|
48
|
+
};
|
|
49
|
+
// react-native parity escape hatch (RN uses it for bootstrap/tests): merges
|
|
50
|
+
// overrides into the cached values and notifies listeners. The next host
|
|
51
|
+
// read or resize event replaces the override with real metrics.
|
|
52
|
+
const set = (dimensions) => {
|
|
53
|
+
let changed = false;
|
|
54
|
+
for (const key of ["window", "screen"]) {
|
|
55
|
+
const patch = dimensions[key];
|
|
56
|
+
if (!patch) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const base = cache[key] ?? refresh(key);
|
|
60
|
+
const next = { ...base, ...patch };
|
|
61
|
+
if (!sameSize(base, next)) {
|
|
62
|
+
cache[key] = next;
|
|
63
|
+
changed = true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (changed) {
|
|
67
|
+
const window = cache.window ?? refresh("window");
|
|
68
|
+
const screen = cache.screen ?? refresh("screen");
|
|
69
|
+
emitter.emit({ window, screen });
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
const addEventListener = (type, handler) => {
|
|
73
|
+
if (type !== "change") {
|
|
74
|
+
throw new Error(`Dimensions.addEventListener: unsupported event type "${String(type)}"`);
|
|
75
|
+
}
|
|
76
|
+
return emitter.add(handler);
|
|
77
|
+
};
|
|
78
|
+
return { get, set, addEventListener };
|
|
79
|
+
};
|
|
80
|
+
//# sourceMappingURL=dimensions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dimensions.js","sourceRoot":"","sources":["../../src/apis/dimensions.ts"],"names":[],"mappings":"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","sourcesContent":["import type { SubscriptionHandle } from \"../contracts\"\nimport { createSharedEmitter } from \"./emitter\"\nimport type { DimensionsHost, ScaledSize } from \"./host\"\n\nexport type DimensionKey = \"window\" | \"screen\"\n\nexport type DimensionsPayload = { window: ScaledSize; screen: ScaledSize }\n\nexport type DimensionsChangeHandler = (payload: DimensionsPayload) => void\n\nconst sameSize = (a: ScaledSize, b: ScaledSize): boolean =>\n a.width === b.width &&\n a.height === b.height &&\n a.scale === b.scale &&\n a.fontScale === b.fontScale\n\nexport const createDimensions = (host: DimensionsHost) => {\n const cache: Record<DimensionKey, ScaledSize | null> = {\n window: null,\n screen: null,\n }\n\n // Reads through to the host while keeping object identity stable when the\n // values are unchanged (required by the useSyncExternalStore snapshot\n // contract in useWindowDimensions).\n const refresh = (key: DimensionKey): ScaledSize => {\n const next =\n key === \"window\" ? host.getWindowMetrics() : host.getScreenMetrics()\n const previous = cache[key]\n if (previous && sameSize(previous, next)) {\n return previous\n }\n cache[key] = next\n return next\n }\n\n const emitter = createSharedEmitter<[DimensionsPayload]>(() => {\n // Prime the cache when going live so the first host event can diff.\n refresh(\"window\")\n refresh(\"screen\")\n const subscription = host.onMetricsChange(() => {\n const previousWindow = cache.window\n const previousScreen = cache.screen\n const window = refresh(\"window\")\n const screen = refresh(\"screen\")\n if (window !== previousWindow || screen !== previousScreen) {\n emitter.emit({ window, screen })\n }\n })\n return () => subscription.remove()\n })\n\n const get = (key: DimensionKey): ScaledSize => {\n if (key !== \"window\" && key !== \"screen\") {\n throw new Error(`Dimensions.get: unknown dimension \"${String(key)}\"`)\n }\n // While subscribed the cache is kept fresh by host events; when idle we\n // read through to the host on every call.\n if (emitter.hasListeners()) {\n return cache[key] ?? refresh(key)\n }\n return refresh(key)\n }\n\n // react-native parity escape hatch (RN uses it for bootstrap/tests): merges\n // overrides into the cached values and notifies listeners. The next host\n // read or resize event replaces the override with real metrics.\n const set = (\n dimensions: Partial<Record<DimensionKey, Partial<ScaledSize>>>,\n ): void => {\n let changed = false\n for (const key of [\"window\", \"screen\"] as const) {\n const patch = dimensions[key]\n if (!patch) {\n continue\n }\n const base = cache[key] ?? refresh(key)\n const next = { ...base, ...patch }\n if (!sameSize(base, next)) {\n cache[key] = next\n changed = true\n }\n }\n if (changed) {\n const window = cache.window ?? refresh(\"window\")\n const screen = cache.screen ?? refresh(\"screen\")\n emitter.emit({ window, screen })\n }\n }\n\n const addEventListener = (\n type: \"change\",\n handler: DimensionsChangeHandler,\n ): SubscriptionHandle => {\n if (type !== \"change\") {\n throw new Error(\n `Dimensions.addEventListener: unsupported event type \"${String(type)}\"`,\n )\n }\n return emitter.add(handler)\n }\n\n return { get, set, addEventListener }\n}\n\nexport type DimensionsModule = ReturnType<typeof createDimensions>\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SubscriptionHandle } from "../contracts";
|
|
2
|
+
type Teardown = () => void;
|
|
3
|
+
export declare const createSharedEmitter: <Args extends unknown[]>(onActive?: () => Teardown) => {
|
|
4
|
+
add: (handler: (...args: Args) => void) => SubscriptionHandle;
|
|
5
|
+
emit: (...args: Args) => void;
|
|
6
|
+
hasListeners: () => boolean;
|
|
7
|
+
};
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Shared listener registry behind every addEventListener/addChangeListener.
|
|
2
|
+
// Leak policy (documented across the API modules): the upstream host
|
|
3
|
+
// subscription is attached lazily when the first listener arrives and torn
|
|
4
|
+
// down when the last listener is removed, so idle modules hold no GTK signal
|
|
5
|
+
// connections. remove() is idempotent and a removed handler is never invoked
|
|
6
|
+
// again (emit iterates over a snapshot).
|
|
7
|
+
export const createSharedEmitter = (onActive) => {
|
|
8
|
+
const listeners = new Set();
|
|
9
|
+
let teardown = null;
|
|
10
|
+
const add = (handler) => {
|
|
11
|
+
const entry = { handler };
|
|
12
|
+
listeners.add(entry);
|
|
13
|
+
if (listeners.size === 1 && onActive) {
|
|
14
|
+
teardown = onActive();
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
remove: () => {
|
|
18
|
+
if (!listeners.delete(entry)) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (listeners.size === 0 && teardown) {
|
|
22
|
+
teardown();
|
|
23
|
+
teardown = null;
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
const emit = (...args) => {
|
|
29
|
+
for (const entry of [...listeners]) {
|
|
30
|
+
entry.handler(...args);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
return { add, emit, hasListeners: () => listeners.size > 0 };
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=emitter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"emitter.js","sourceRoot":"","sources":["../../src/apis/emitter.ts"],"names":[],"mappings":"AAIA,4EAA4E;AAC5E,qEAAqE;AACrE,2EAA2E;AAC3E,6EAA6E;AAC7E,6EAA6E;AAC7E,yCAAyC;AACzC,MAAM,CAAC,MAAM,mBAAmB,GAAG,CACjC,QAAyB,EACzB,EAAE;IACF,MAAM,SAAS,GAAG,IAAI,GAAG,EAAwC,CAAA;IACjE,IAAI,QAAQ,GAAoB,IAAI,CAAA;IAEpC,MAAM,GAAG,GAAG,CAAC,OAAgC,EAAsB,EAAE;QACnE,MAAM,KAAK,GAAG,EAAE,OAAO,EAAE,CAAA;QACzB,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;QACpB,IAAI,SAAS,CAAC,IAAI,KAAK,CAAC,IAAI,QAAQ,EAAE,CAAC;YACrC,QAAQ,GAAG,QAAQ,EAAE,CAAA;QACvB,CAAC;QACD,OAAO;YACL,MAAM,EAAE,GAAG,EAAE;gBACX,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;oBAC7B,OAAM;gBACR,CAAC;gBACD,IAAI,SAAS,CAAC,IAAI,KAAK,CAAC,IAAI,QAAQ,EAAE,CAAC;oBACrC,QAAQ,EAAE,CAAA;oBACV,QAAQ,GAAG,IAAI,CAAA;gBACjB,CAAC;YACH,CAAC;SACF,CAAA;IACH,CAAC,CAAA;IAED,MAAM,IAAI,GAAG,CAAC,GAAG,IAAU,EAAQ,EAAE;QACnC,KAAK,MAAM,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC,EAAE,CAAC;YACnC,KAAK,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,CAAA;QACxB,CAAC;IACH,CAAC,CAAA;IAED,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,YAAY,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,IAAI,GAAG,CAAC,EAAE,CAAA;AAC9D,CAAC,CAAA","sourcesContent":["import type { SubscriptionHandle } from \"../contracts\"\n\ntype Teardown = () => void\n\n// Shared listener registry behind every addEventListener/addChangeListener.\n// Leak policy (documented across the API modules): the upstream host\n// subscription is attached lazily when the first listener arrives and torn\n// down when the last listener is removed, so idle modules hold no GTK signal\n// connections. remove() is idempotent and a removed handler is never invoked\n// again (emit iterates over a snapshot).\nexport const createSharedEmitter = <Args extends unknown[]>(\n onActive?: () => Teardown,\n) => {\n const listeners = new Set<{ handler: (...args: Args) => void }>()\n let teardown: Teardown | null = null\n\n const add = (handler: (...args: Args) => void): SubscriptionHandle => {\n const entry = { handler }\n listeners.add(entry)\n if (listeners.size === 1 && onActive) {\n teardown = onActive()\n }\n return {\n remove: () => {\n if (!listeners.delete(entry)) {\n return\n }\n if (listeners.size === 0 && teardown) {\n teardown()\n teardown = null\n }\n },\n }\n }\n\n const emit = (...args: Args): void => {\n for (const entry of [...listeners]) {\n entry.handler(...args)\n }\n }\n\n return { add, emit, hasListeners: () => listeners.size > 0 }\n}\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AppearanceModule } from "./appearance";
|
|
2
|
+
import type { DimensionsModule } from "./dimensions";
|
|
3
|
+
import type { ColorSchemeName, ScaledSize } from "./host";
|
|
4
|
+
export type ExternalStore<T> = {
|
|
5
|
+
subscribe: (onStoreChange: () => void) => () => void;
|
|
6
|
+
getSnapshot: () => T;
|
|
7
|
+
};
|
|
8
|
+
export declare const createWindowDimensionsStore: (dimensions: DimensionsModule) => ExternalStore<ScaledSize>;
|
|
9
|
+
export declare const createColorSchemeStore: (appearance: AppearanceModule) => ExternalStore<ColorSchemeName>;
|
|
10
|
+
export declare const createUseWindowDimensions: (dimensions: DimensionsModule) => () => ScaledSize;
|
|
11
|
+
export declare const createUseColorScheme: (appearance: AppearanceModule) => () => ColorSchemeName;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useSyncExternalStore } from "react";
|
|
2
|
+
// The store pairs are exported separately so unit tests can exercise the
|
|
3
|
+
// useSyncExternalStore contract (subscribe/getSnapshot/identity) directly,
|
|
4
|
+
// without rendering React components.
|
|
5
|
+
export const createWindowDimensionsStore = (dimensions) => ({
|
|
6
|
+
subscribe: (onStoreChange) => {
|
|
7
|
+
const subscription = dimensions.addEventListener("change", onStoreChange);
|
|
8
|
+
return () => subscription.remove();
|
|
9
|
+
},
|
|
10
|
+
getSnapshot: () => dimensions.get("window"),
|
|
11
|
+
});
|
|
12
|
+
export const createColorSchemeStore = (appearance) => ({
|
|
13
|
+
subscribe: (onStoreChange) => {
|
|
14
|
+
const subscription = appearance.addChangeListener(onStoreChange);
|
|
15
|
+
return () => subscription.remove();
|
|
16
|
+
},
|
|
17
|
+
getSnapshot: () => appearance.getColorScheme(),
|
|
18
|
+
});
|
|
19
|
+
export const createUseWindowDimensions = (dimensions) => {
|
|
20
|
+
const store = createWindowDimensionsStore(dimensions);
|
|
21
|
+
const useWindowDimensions = () => useSyncExternalStore(store.subscribe, store.getSnapshot);
|
|
22
|
+
return useWindowDimensions;
|
|
23
|
+
};
|
|
24
|
+
export const createUseColorScheme = (appearance) => {
|
|
25
|
+
const store = createColorSchemeStore(appearance);
|
|
26
|
+
const useColorScheme = () => useSyncExternalStore(store.subscribe, store.getSnapshot);
|
|
27
|
+
return useColorScheme;
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=hooks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks.js","sourceRoot":"","sources":["../../src/apis/hooks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAA;AAU5C,yEAAyE;AACzE,2EAA2E;AAC3E,sCAAsC;AAEtC,MAAM,CAAC,MAAM,2BAA2B,GAAG,CACzC,UAA4B,EACD,EAAE,CAAC,CAAC;IAC/B,SAAS,EAAE,CAAC,aAAa,EAAE,EAAE;QAC3B,MAAM,YAAY,GAAG,UAAU,CAAC,gBAAgB,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAA;QACzE,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,CAAA;IACpC,CAAC;IACD,WAAW,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC;CAC5C,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,sBAAsB,GAAG,CACpC,UAA4B,EACI,EAAE,CAAC,CAAC;IACpC,SAAS,EAAE,CAAC,aAAa,EAAE,EAAE;QAC3B,MAAM,YAAY,GAAG,UAAU,CAAC,iBAAiB,CAAC,aAAa,CAAC,CAAA;QAChE,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,CAAA;IACpC,CAAC;IACD,WAAW,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,cAAc,EAAE;CAC/C,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,UAA4B,EAAE,EAAE;IACxE,MAAM,KAAK,GAAG,2BAA2B,CAAC,UAAU,CAAC,CAAA;IACrD,MAAM,mBAAmB,GAAG,GAAe,EAAE,CAC3C,oBAAoB,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,WAAW,CAAC,CAAA;IAC1D,OAAO,mBAAmB,CAAA;AAC5B,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,UAA4B,EAAE,EAAE;IACnE,MAAM,KAAK,GAAG,sBAAsB,CAAC,UAAU,CAAC,CAAA;IAChD,MAAM,cAAc,GAAG,GAAoB,EAAE,CAC3C,oBAAoB,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,WAAW,CAAC,CAAA;IAC1D,OAAO,cAAc,CAAA;AACvB,CAAC,CAAA","sourcesContent":["import { useSyncExternalStore } from \"react\"\nimport type { AppearanceModule } from \"./appearance\"\nimport type { DimensionsModule } from \"./dimensions\"\nimport type { ColorSchemeName, ScaledSize } from \"./host\"\n\nexport type ExternalStore<T> = {\n subscribe: (onStoreChange: () => void) => () => void\n getSnapshot: () => T\n}\n\n// The store pairs are exported separately so unit tests can exercise the\n// useSyncExternalStore contract (subscribe/getSnapshot/identity) directly,\n// without rendering React components.\n\nexport const createWindowDimensionsStore = (\n dimensions: DimensionsModule,\n): ExternalStore<ScaledSize> => ({\n subscribe: (onStoreChange) => {\n const subscription = dimensions.addEventListener(\"change\", onStoreChange)\n return () => subscription.remove()\n },\n getSnapshot: () => dimensions.get(\"window\"),\n})\n\nexport const createColorSchemeStore = (\n appearance: AppearanceModule,\n): ExternalStore<ColorSchemeName> => ({\n subscribe: (onStoreChange) => {\n const subscription = appearance.addChangeListener(onStoreChange)\n return () => subscription.remove()\n },\n getSnapshot: () => appearance.getColorScheme(),\n})\n\nexport const createUseWindowDimensions = (dimensions: DimensionsModule) => {\n const store = createWindowDimensionsStore(dimensions)\n const useWindowDimensions = (): ScaledSize =>\n useSyncExternalStore(store.subscribe, store.getSnapshot)\n return useWindowDimensions\n}\n\nexport const createUseColorScheme = (appearance: AppearanceModule) => {\n const store = createColorSchemeStore(appearance)\n const useColorScheme = (): ColorSchemeName =>\n useSyncExternalStore(store.subscribe, store.getSnapshot)\n return useColorScheme\n}\n"]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { SubscriptionHandle } from "../contracts";
|
|
2
|
+
export type ColorSchemeName = "light" | "dark";
|
|
3
|
+
export type ScaledSize = {
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
scale: number;
|
|
7
|
+
fontScale: number;
|
|
8
|
+
};
|
|
9
|
+
export type AlertButtonStyle = "default" | "cancel" | "destructive";
|
|
10
|
+
export type HostAlertButton = {
|
|
11
|
+
id: string;
|
|
12
|
+
label: string;
|
|
13
|
+
style: AlertButtonStyle;
|
|
14
|
+
isPreferred: boolean;
|
|
15
|
+
};
|
|
16
|
+
export type HostAlertRequest = {
|
|
17
|
+
title: string;
|
|
18
|
+
message?: string;
|
|
19
|
+
buttons: HostAlertButton[];
|
|
20
|
+
cancelable: boolean;
|
|
21
|
+
};
|
|
22
|
+
export interface PlatformHost {
|
|
23
|
+
gtkVersion(): string;
|
|
24
|
+
}
|
|
25
|
+
export interface DimensionsHost {
|
|
26
|
+
getWindowMetrics(): ScaledSize;
|
|
27
|
+
getScreenMetrics(): ScaledSize;
|
|
28
|
+
onMetricsChange(notify: () => void): SubscriptionHandle;
|
|
29
|
+
}
|
|
30
|
+
export interface AppearanceHost {
|
|
31
|
+
getColorScheme(): ColorSchemeName;
|
|
32
|
+
setColorScheme(scheme: ColorSchemeName | null): void;
|
|
33
|
+
onColorSchemeChange(notify: () => void): SubscriptionHandle;
|
|
34
|
+
}
|
|
35
|
+
export interface AppStateHost {
|
|
36
|
+
isActive(): boolean;
|
|
37
|
+
onActiveChange(notify: () => void): SubscriptionHandle;
|
|
38
|
+
}
|
|
39
|
+
export interface AlertHost {
|
|
40
|
+
showAlert(request: HostAlertRequest): Promise<string | null>;
|
|
41
|
+
}
|
|
42
|
+
export interface LinkingHost {
|
|
43
|
+
launchUri(uri: string): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
export type Host = PlatformHost & DimensionsHost & AppearanceHost & AppStateHost & AlertHost & LinkingHost;
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
// Production host adapter: the ONLY module under src/apis/ allowed to import
|
|
2
|
+
// the gtkx bridge. Everything GTK-specific lives here; the API modules stay
|
|
3
|
+
// pure and receive this object (or a mock in unit tests) via injection.
|
|
4
|
+
import { Adw, colorScheme, Gdk, Gio, Gtk, styleManager, toNumber, } from "../gtkx/bridge/index";
|
|
5
|
+
const FALLBACK_METRICS = {
|
|
6
|
+
width: 0,
|
|
7
|
+
height: 0,
|
|
8
|
+
scale: 1,
|
|
9
|
+
fontScale: 1,
|
|
10
|
+
};
|
|
11
|
+
const getApplication = () => Gio.Application.getDefault();
|
|
12
|
+
const getActiveAppWindow = () => {
|
|
13
|
+
const app = getApplication();
|
|
14
|
+
if (!app || typeof app.getActiveWindow !== "function") {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return app.getActiveWindow() ?? null;
|
|
18
|
+
};
|
|
19
|
+
const windowAt = (model, index) => model.getItem(index);
|
|
20
|
+
// The application's active window if any, otherwise the first toplevel. The
|
|
21
|
+
// toplevel fallback covers the test harness, where windows exist without a
|
|
22
|
+
// GtkApplication (`Gio.Application.getDefault()` is null there).
|
|
23
|
+
// RN's Dimensions("window") means the MAIN window: transient toplevels
|
|
24
|
+
// (Modal → modal GtkWindow, dialogs) must never be resolved as "the window" —
|
|
25
|
+
// an open modal becomes the active window and would shrink the app viewport
|
|
26
|
+
// to the modal size.
|
|
27
|
+
const isMainWindow = (window) => window.getTransientFor() === null;
|
|
28
|
+
const resolveWindow = () => {
|
|
29
|
+
const active = getActiveAppWindow();
|
|
30
|
+
if (active && isMainWindow(active)) {
|
|
31
|
+
return active;
|
|
32
|
+
}
|
|
33
|
+
const toplevels = Gtk.Window.getToplevels();
|
|
34
|
+
const count = toNumber(toplevels.getNItems());
|
|
35
|
+
let first = null;
|
|
36
|
+
for (let index = 0; index < count; index += 1) {
|
|
37
|
+
const window = windowAt(toplevels, index);
|
|
38
|
+
if (!window || !isMainWindow(window)) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (window.isActive()) {
|
|
42
|
+
return window;
|
|
43
|
+
}
|
|
44
|
+
first = first ?? window;
|
|
45
|
+
}
|
|
46
|
+
return first;
|
|
47
|
+
};
|
|
48
|
+
const isAnyWindowActive = () => {
|
|
49
|
+
const toplevels = Gtk.Window.getToplevels();
|
|
50
|
+
const count = toNumber(toplevels.getNItems());
|
|
51
|
+
for (let index = 0; index < count; index += 1) {
|
|
52
|
+
if (windowAt(toplevels, index)?.isActive()) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
};
|
|
58
|
+
const windowMetrics = () => {
|
|
59
|
+
const window = resolveWindow();
|
|
60
|
+
if (!window) {
|
|
61
|
+
return FALLBACK_METRICS;
|
|
62
|
+
}
|
|
63
|
+
// RN's Dimensions("window") is the app viewport: the window CHILD's
|
|
64
|
+
// allocation (the content area under the headerbar), not the window widget
|
|
65
|
+
// size. Falls back to the window size before the first allocation.
|
|
66
|
+
const child = window.getChild();
|
|
67
|
+
const childAllocation = child?.getAllocation();
|
|
68
|
+
const allocatedWidth = childAllocation && childAllocation.width > 0
|
|
69
|
+
? toNumber(childAllocation.width)
|
|
70
|
+
: toNumber(window.getWidth());
|
|
71
|
+
const allocatedHeight = childAllocation && childAllocation.height > 0
|
|
72
|
+
? toNumber(childAllocation.height)
|
|
73
|
+
: toNumber(window.getHeight());
|
|
74
|
+
// Before the first allocation everything reports 0 — fall back to
|
|
75
|
+
// the requested default size (which GTK4 also keeps updated on resize).
|
|
76
|
+
const width = allocatedWidth > 0
|
|
77
|
+
? allocatedWidth
|
|
78
|
+
: Math.max(toNumber(window.defaultWidth ?? 0), 0);
|
|
79
|
+
const height = allocatedHeight > 0
|
|
80
|
+
? allocatedHeight
|
|
81
|
+
: Math.max(toNumber(window.defaultHeight ?? 0), 0);
|
|
82
|
+
return {
|
|
83
|
+
width,
|
|
84
|
+
height,
|
|
85
|
+
scale: toNumber(window.getScaleFactor()),
|
|
86
|
+
fontScale: 1,
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
const screenMetrics = () => {
|
|
90
|
+
const display = Gdk.Display.getDefault();
|
|
91
|
+
if (!display) {
|
|
92
|
+
return windowMetrics();
|
|
93
|
+
}
|
|
94
|
+
const monitors = display.getMonitors();
|
|
95
|
+
if (toNumber(monitors.getNItems()) === 0) {
|
|
96
|
+
return windowMetrics();
|
|
97
|
+
}
|
|
98
|
+
const monitor = monitors.getItem(0);
|
|
99
|
+
if (!monitor) {
|
|
100
|
+
return windowMetrics();
|
|
101
|
+
}
|
|
102
|
+
const geometry = monitor.getGeometry();
|
|
103
|
+
return {
|
|
104
|
+
width: toNumber(geometry.width),
|
|
105
|
+
height: toNumber(geometry.height),
|
|
106
|
+
scale: toNumber(monitor.getScaleFactor()),
|
|
107
|
+
fontScale: 1,
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
// GtkNative.getSurface is an interface method merged onto Gtk.Window at
|
|
111
|
+
// runtime via mixins; accessed defensively to stay typecheckable either way.
|
|
112
|
+
const surfaceOf = (window) => {
|
|
113
|
+
const native = window;
|
|
114
|
+
return typeof native.getSurface === "function"
|
|
115
|
+
? (native.getSurface() ?? null)
|
|
116
|
+
: null;
|
|
117
|
+
};
|
|
118
|
+
// Tracks the "current" window across creation/destruction/focus moves:
|
|
119
|
+
// listens on the given signals of the resolved window (and, when requested,
|
|
120
|
+
// its GdkSurface) plus the toplevel list model, re-resolving and re-attaching
|
|
121
|
+
// whenever the tracked window may have changed. Modules dedupe, so
|
|
122
|
+
// over-notifying is fine.
|
|
123
|
+
//
|
|
124
|
+
// Why the surface: notify::default-width fires when setDefaultSize() is
|
|
125
|
+
// called, i.e. before the compositor actually resizes the surface — the
|
|
126
|
+
// allocated size (what Dimensions reports) only changes when the surface
|
|
127
|
+
// does. And GdkSurface notify::width/height fires at configure time, still
|
|
128
|
+
// before the widget allocation pass, so the "layout" signal is also needed:
|
|
129
|
+
// it is emitted during the frame's layout phase and — because GTK's own
|
|
130
|
+
// relayout handler is connected first — our handler observes the updated
|
|
131
|
+
// allocation. The module dedupes, so early/no-op emissions cost nothing.
|
|
132
|
+
const watchWindow = (windowSignals, surfaceSignals, notify) => {
|
|
133
|
+
let window = null;
|
|
134
|
+
let surface = null;
|
|
135
|
+
const attachWindow = () => {
|
|
136
|
+
if (!window) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
for (const signal of windowSignals) {
|
|
140
|
+
window.on(signal, onSignal);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
const detachWindow = () => {
|
|
144
|
+
if (!window) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
for (const signal of windowSignals) {
|
|
148
|
+
window.off(signal, onSignal);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
const attachSurface = () => {
|
|
152
|
+
if (!surface) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
for (const signal of surfaceSignals) {
|
|
156
|
+
surface.on(signal, onSignal);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
const detachSurface = () => {
|
|
160
|
+
if (!surface) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
for (const signal of surfaceSignals) {
|
|
164
|
+
surface.off(signal, onSignal);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
const syncTargets = () => {
|
|
168
|
+
const nextWindow = resolveWindow();
|
|
169
|
+
if (nextWindow !== window) {
|
|
170
|
+
detachWindow();
|
|
171
|
+
window = nextWindow;
|
|
172
|
+
attachWindow();
|
|
173
|
+
}
|
|
174
|
+
const nextSurface = window && surfaceSignals.length > 0 ? surfaceOf(window) : null;
|
|
175
|
+
if (nextSurface !== surface) {
|
|
176
|
+
detachSurface();
|
|
177
|
+
surface = nextSurface;
|
|
178
|
+
attachSurface();
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
const onSignal = () => {
|
|
182
|
+
syncTargets();
|
|
183
|
+
notify();
|
|
184
|
+
};
|
|
185
|
+
const toplevels = Gtk.Window.getToplevels();
|
|
186
|
+
const onToplevelsChanged = () => {
|
|
187
|
+
syncTargets();
|
|
188
|
+
notify();
|
|
189
|
+
};
|
|
190
|
+
toplevels.on("items-changed", onToplevelsChanged);
|
|
191
|
+
syncTargets();
|
|
192
|
+
return {
|
|
193
|
+
remove: () => {
|
|
194
|
+
detachWindow();
|
|
195
|
+
detachSurface();
|
|
196
|
+
window = null;
|
|
197
|
+
surface = null;
|
|
198
|
+
toplevels.off("items-changed", onToplevelsChanged);
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
};
|
|
202
|
+
// realize/map re-run target resolution so the surface listeners attach as
|
|
203
|
+
// soon as the surface exists.
|
|
204
|
+
const WINDOW_SIZE_SIGNALS = [
|
|
205
|
+
"notify::default-width",
|
|
206
|
+
"notify::default-height",
|
|
207
|
+
"realize",
|
|
208
|
+
"map",
|
|
209
|
+
];
|
|
210
|
+
const SURFACE_SIZE_SIGNALS = [
|
|
211
|
+
"notify::width",
|
|
212
|
+
"notify::height",
|
|
213
|
+
"layout",
|
|
214
|
+
];
|
|
215
|
+
const WINDOW_ACTIVE_SIGNALS = ["notify::is-active"];
|
|
216
|
+
const gtkVersion = () => `${toNumber(Gtk.getMajorVersion())}.${toNumber(Gtk.getMinorVersion())}.${toNumber(Gtk.getMicroVersion())}`;
|
|
217
|
+
const setColorScheme = (scheme) => {
|
|
218
|
+
const manager = styleManager();
|
|
219
|
+
if (scheme === "dark") {
|
|
220
|
+
manager.setColorScheme(Adw.ColorScheme.FORCE_DARK);
|
|
221
|
+
}
|
|
222
|
+
else if (scheme === "light") {
|
|
223
|
+
manager.setColorScheme(Adw.ColorScheme.FORCE_LIGHT);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
manager.setColorScheme(Adw.ColorScheme.DEFAULT);
|
|
227
|
+
}
|
|
228
|
+
// Our apps are GtkApplication (not AdwApplication), so Adwaita may not
|
|
229
|
+
// restyle widgets: duplicate the request through the classic GTK setting,
|
|
230
|
+
// which the theme always honors.
|
|
231
|
+
const settings = Gtk.Settings.getDefault();
|
|
232
|
+
if (settings) {
|
|
233
|
+
if (scheme === null) {
|
|
234
|
+
settings.resetProperty("gtk-application-prefer-dark-theme");
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
settings.gtkApplicationPreferDarkTheme = scheme === "dark";
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
const onColorSchemeChange = (notify) => {
|
|
242
|
+
const manager = styleManager();
|
|
243
|
+
const listener = () => notify();
|
|
244
|
+
manager.on("notify::dark", listener);
|
|
245
|
+
return { remove: () => manager.off("notify::dark", listener) };
|
|
246
|
+
};
|
|
247
|
+
const showAlert = async (request) => {
|
|
248
|
+
const dialog = new Adw.AlertDialog();
|
|
249
|
+
dialog.setHeading(request.title);
|
|
250
|
+
if (request.message !== undefined && request.message.length > 0) {
|
|
251
|
+
dialog.setBody(request.message);
|
|
252
|
+
}
|
|
253
|
+
let closeResponse = null;
|
|
254
|
+
for (const button of request.buttons) {
|
|
255
|
+
dialog.addResponse(button.id, button.label);
|
|
256
|
+
if (button.style === "destructive") {
|
|
257
|
+
dialog.setResponseAppearance(button.id, Adw.ResponseAppearance.DESTRUCTIVE);
|
|
258
|
+
}
|
|
259
|
+
if (button.isPreferred) {
|
|
260
|
+
dialog.setResponseAppearance(button.id, Adw.ResponseAppearance.SUGGESTED);
|
|
261
|
+
dialog.setDefaultResponse(button.id);
|
|
262
|
+
}
|
|
263
|
+
if (button.style === "cancel") {
|
|
264
|
+
closeResponse = button.id;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (closeResponse !== null) {
|
|
268
|
+
// Esc / close maps onto the cancel button, like RN's cancel semantics.
|
|
269
|
+
dialog.setCloseResponse(closeResponse);
|
|
270
|
+
}
|
|
271
|
+
else if (!request.cancelable) {
|
|
272
|
+
// No cancel button and not cancelable: block Esc-dismissal. Responses
|
|
273
|
+
// still close the dialog (they bypass the can-close guard).
|
|
274
|
+
dialog.setCanClose(false);
|
|
275
|
+
}
|
|
276
|
+
const response = await dialog.choose(resolveWindow());
|
|
277
|
+
// Anything that is not one of our buttons (e.g. the built-in "close"
|
|
278
|
+
// response) is a dismissal.
|
|
279
|
+
return request.buttons.some((button) => button.id === response)
|
|
280
|
+
? response
|
|
281
|
+
: null;
|
|
282
|
+
};
|
|
283
|
+
const launchUri = async (uri) => {
|
|
284
|
+
const launcher = new Gtk.UriLauncher({ uri });
|
|
285
|
+
// Promisified by the gi codegen; rejects with the GError on failure.
|
|
286
|
+
await launcher.launch(resolveWindow());
|
|
287
|
+
};
|
|
288
|
+
export const gtkxHost = {
|
|
289
|
+
gtkVersion,
|
|
290
|
+
getWindowMetrics: windowMetrics,
|
|
291
|
+
getScreenMetrics: screenMetrics,
|
|
292
|
+
onMetricsChange: (notify) => watchWindow(WINDOW_SIZE_SIGNALS, SURFACE_SIZE_SIGNALS, notify),
|
|
293
|
+
getColorScheme: () => colorScheme(),
|
|
294
|
+
setColorScheme,
|
|
295
|
+
onColorSchemeChange,
|
|
296
|
+
isActive: isAnyWindowActive,
|
|
297
|
+
onActiveChange: (notify) => watchWindow(WINDOW_ACTIVE_SIGNALS, [], notify),
|
|
298
|
+
showAlert,
|
|
299
|
+
launchUri,
|
|
300
|
+
};
|
|
301
|
+
//# sourceMappingURL=host.gtkx.js.map
|