@gtkx/navigation 2.0.0-beta.12 → 2.0.0-beta.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +1 -1
  2. package/dist/drawer/create-drawer-navigator.d.ts +3 -1
  3. package/dist/drawer/create-drawer-navigator.d.ts.map +1 -1
  4. package/dist/drawer/create-drawer-navigator.js +10 -10
  5. package/dist/drawer/create-drawer-navigator.js.map +1 -1
  6. package/dist/drawer/drawer-view.js +2 -2
  7. package/dist/drawer/drawer-view.js.map +1 -1
  8. package/dist/index.d.ts +5 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +4 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/shared/scene-page.d.ts.map +1 -1
  13. package/dist/shared/scene-page.js +2 -5
  14. package/dist/shared/scene-page.js.map +1 -1
  15. package/dist/shared/use-has-loaded.d.ts +3 -0
  16. package/dist/shared/use-has-loaded.d.ts.map +1 -0
  17. package/dist/shared/use-has-loaded.js +10 -0
  18. package/dist/shared/use-has-loaded.js.map +1 -0
  19. package/dist/split-view/create-split-view-navigator.d.ts +7 -2
  20. package/dist/split-view/create-split-view-navigator.d.ts.map +1 -1
  21. package/dist/split-view/create-split-view-navigator.js +2 -6
  22. package/dist/split-view/create-split-view-navigator.js.map +1 -1
  23. package/dist/split-view/split-view-collapsed.d.ts +11 -0
  24. package/dist/split-view/split-view-collapsed.d.ts.map +1 -0
  25. package/dist/split-view/split-view-collapsed.js +17 -0
  26. package/dist/split-view/split-view-collapsed.js.map +1 -0
  27. package/dist/split-view/split-view.d.ts.map +1 -1
  28. package/dist/split-view/split-view.js +39 -14
  29. package/dist/split-view/split-view.js.map +1 -1
  30. package/dist/split-view/types.d.ts +9 -2
  31. package/dist/split-view/types.d.ts.map +1 -1
  32. package/dist/split-view/types.js.map +1 -1
  33. package/dist/stack/create-stack-navigator.d.ts +3 -1
  34. package/dist/stack/create-stack-navigator.d.ts.map +1 -1
  35. package/dist/stack/create-stack-navigator.js +0 -1
  36. package/dist/stack/create-stack-navigator.js.map +1 -1
  37. package/dist/stack/stack-view.js +1 -1
  38. package/dist/stack/stack-view.js.map +1 -1
  39. package/dist/tab-view/create-tab-view-navigator.d.ts +32 -0
  40. package/dist/tab-view/create-tab-view-navigator.d.ts.map +1 -0
  41. package/dist/tab-view/create-tab-view-navigator.js +21 -0
  42. package/dist/tab-view/create-tab-view-navigator.js.map +1 -0
  43. package/dist/tab-view/page-registry.d.ts +34 -0
  44. package/dist/tab-view/page-registry.d.ts.map +1 -0
  45. package/dist/tab-view/page-registry.js +70 -0
  46. package/dist/tab-view/page-registry.js.map +1 -0
  47. package/dist/tab-view/tab-view-host.d.ts +25 -0
  48. package/dist/tab-view/tab-view-host.d.ts.map +1 -0
  49. package/dist/tab-view/tab-view-host.js +284 -0
  50. package/dist/tab-view/tab-view-host.js.map +1 -0
  51. package/dist/tab-view/tab-view-router.d.ts +34 -0
  52. package/dist/tab-view/tab-view-router.d.ts.map +1 -0
  53. package/dist/tab-view/tab-view-router.js +359 -0
  54. package/dist/tab-view/tab-view-router.js.map +1 -0
  55. package/dist/tab-view/types.d.ts +240 -0
  56. package/dist/tab-view/types.d.ts.map +1 -0
  57. package/dist/tab-view/types.js +2 -0
  58. package/dist/tab-view/types.js.map +1 -0
  59. package/dist/tabs/create-tab-navigator.d.ts +3 -1
  60. package/dist/tabs/create-tab-navigator.d.ts.map +1 -1
  61. package/dist/tabs/create-tab-navigator.js +0 -1
  62. package/dist/tabs/create-tab-navigator.js.map +1 -1
  63. package/dist/tabs/tab-view.js +1 -1
  64. package/dist/tabs/tab-view.js.map +1 -1
  65. package/dist/theme.js +2 -2
  66. package/dist/theme.js.map +1 -1
  67. package/package.json +6 -6
  68. package/src/drawer/create-drawer-navigator.tsx +23 -11
  69. package/src/drawer/drawer-view.tsx +2 -2
  70. package/src/index.ts +30 -0
  71. package/src/shared/scene-page.tsx +2 -6
  72. package/src/shared/use-has-loaded.ts +13 -0
  73. package/src/split-view/create-split-view-navigator.tsx +16 -1
  74. package/src/split-view/split-view-collapsed.ts +21 -0
  75. package/src/split-view/split-view.tsx +92 -45
  76. package/src/split-view/types.ts +9 -2
  77. package/src/stack/create-stack-navigator.tsx +11 -1
  78. package/src/stack/stack-view.tsx +1 -1
  79. package/src/tab-view/create-tab-view-navigator.tsx +122 -0
  80. package/src/tab-view/page-registry.ts +139 -0
  81. package/src/tab-view/tab-view-host.tsx +474 -0
  82. package/src/tab-view/tab-view-router.ts +528 -0
  83. package/src/tab-view/types.ts +291 -0
  84. package/src/tabs/create-tab-navigator.tsx +11 -1
  85. package/src/tabs/tab-view.tsx +2 -2
  86. package/src/theme.ts +2 -2
@@ -48,6 +48,7 @@ const createSplitViewScreen: StaticScreenFactory<SplitViewTypeBag> = createScree
48
48
  /** Renders the screens of a {@link createSplitViewNavigator} in an `AdwNavigationSplitView`. */
49
49
  function SplitViewNavigator({
50
50
  collapsed,
51
+ collapseCondition,
51
52
  contentPlaceholder,
52
53
  sidebarPosition,
53
54
  minSidebarWidth,
@@ -71,6 +72,7 @@ function SplitViewNavigator({
71
72
  descriptors={descriptors}
72
73
  describe={describe}
73
74
  collapsed={collapsed}
75
+ collapseCondition={collapseCondition}
74
76
  contentPlaceholder={contentPlaceholder}
75
77
  sidebarPosition={sidebarPosition}
76
78
  minSidebarWidth={minSidebarWidth}
@@ -89,7 +91,20 @@ function createSplitViewNavigator<
89
91
  const ParamList extends ParamListBase,
90
92
  const NavigatorID extends string | undefined = string | undefined,
91
93
  const TypeBag extends NavigatorTypeBagBase = SplitViewTypeBag<ParamList, NavigatorID>,
92
- const Config extends StaticConfig<TypeBag> | undefined = undefined,
94
+ >(): TypedNavigator<TypeBag, undefined>;
95
+ /**
96
+ * Creates a master/detail navigator rendered with `AdwNavigationSplitView`: the first screen fills the sidebar
97
+ * pane, the rest are a stack in the content pane, and a narrow window collapses the two into one.
98
+ */
99
+ function createSplitViewNavigator<
100
+ const ParamList extends ParamListBase,
101
+ const NavigatorID extends string | undefined = string | undefined,
102
+ const TypeBag extends NavigatorTypeBagBase = SplitViewTypeBag<ParamList, NavigatorID>,
103
+ const Config extends StaticConfig<TypeBag> = StaticConfig<TypeBag>,
104
+ >(config: Config): TypedNavigator<TypeBag, Config>;
105
+ function createSplitViewNavigator<
106
+ const TypeBag extends NavigatorTypeBagBase,
107
+ const Config extends StaticConfig<TypeBag> | undefined,
93
108
  >(config?: Config): TypedNavigator<TypeBag, Config> {
94
109
  return createNavigatorFactory(SplitViewNavigator)(config) as TypedNavigator<TypeBag, Config>;
95
110
  }
@@ -0,0 +1,21 @@
1
+ import { type Context, createContext, useContext } from "react";
2
+
3
+ const SplitViewCollapsedContext: Context<boolean | null> = createContext<boolean | null>(null);
4
+
5
+ /**
6
+ * Returns whether the surrounding split view navigator shows one pane at a time, for its sidebar and content
7
+ * screens. It follows the split view's `collapsed` property, so it holds for both the `collapsed` prop and
8
+ * `collapseCondition`, and changes after libadwaita has laid out the collapsed or widened panes. Use it to
9
+ * adapt a screen's presentation, such as switching an `AdwSidebar` to `Adw.SidebarMode.PAGE`.
10
+ */
11
+ const useSplitViewCollapsed = (): boolean => {
12
+ const isCollapsed = useContext(SplitViewCollapsedContext);
13
+
14
+ if (isCollapsed === null) {
15
+ throw new Error("useSplitViewCollapsed must be called inside a split view navigator.");
16
+ }
17
+
18
+ return isCollapsed;
19
+ };
20
+
21
+ export { SplitViewCollapsedContext, useSplitViewCollapsed };
@@ -1,12 +1,21 @@
1
- import type * as Adw from "@gtkx/gi/adw";
2
1
  import type { ParamListBase, RouteProp, StackNavigationState } from "@react-navigation/core";
3
2
  import type { ReactNode } from "react";
3
+ import * as Adw from "@gtkx/gi/adw";
4
4
  import * as Gdk from "@gtkx/gi/gdk";
5
+ import * as GObject from "@gtkx/gi/gobject";
5
6
  import * as Gtk from "@gtkx/gi/gtk";
6
- import { AdwNavigationPage, AdwNavigationSplitView, AdwToolbarView } from "@gtkx/jsx/adw";
7
+ import {
8
+ AdwBreakpoint,
9
+ AdwBreakpointBin,
10
+ AdwHeaderBar,
11
+ AdwNavigationPage,
12
+ AdwNavigationSplitView,
13
+ AdwToolbarView,
14
+ } from "@gtkx/jsx/adw";
7
15
  import { GtkEventControllerKey } from "@gtkx/jsx/gtk";
16
+ import { useProperty } from "@gtkx/react";
8
17
  import { StackActions } from "@react-navigation/core";
9
- import { useCallback, useRef } from "react";
18
+ import { useCallback, useLayoutEffect, useMemo, useState } from "react";
10
19
  import type {
11
20
  SplitViewDescriptor,
12
21
  SplitViewDescriptorMap,
@@ -16,8 +25,7 @@ import type {
16
25
  import { HeaderBar } from "../shared/header-bar.js";
17
26
  import { requireDescriptor } from "../shared/routes.js";
18
27
  import { StackView } from "../stack/stack-view.js";
19
-
20
- type ViewRef = { current: Adw.NavigationSplitView | null };
28
+ import { SplitViewCollapsedContext } from "./split-view-collapsed.js";
21
29
 
22
30
  type SplitViewProps = SplitViewNavigationConfig & {
23
31
  state: StackNavigationState<ParamListBase>;
@@ -47,14 +55,43 @@ const popContent = (navigation: SplitViewNavigationHelpers): void => {
47
55
  navigation.dispatch({ ...StackActions.pop(state.routes.length - 1), target: state.key });
48
56
  };
49
57
 
50
- const settleShowContent = (viewRef: ViewRef, isShown: boolean, isSettled: boolean): void => {
51
- if (isSettled !== isShown && viewRef.current !== null) {
52
- viewRef.current.setShowContent(isSettled);
58
+ const collapsedValue = (): GObject.Value => {
59
+ const value = new GObject.Value();
60
+ value.init(GObject.TYPE_BOOLEAN);
61
+ value.setBoolean(true);
62
+
63
+ return value;
64
+ };
65
+
66
+ const useCollapseBreakpoint = (
67
+ view: Adw.NavigationSplitView | null,
68
+ collapseCondition: string | undefined,
69
+ ): ReactNode => {
70
+ const [breakpoint, setBreakpoint] = useState<Adw.Breakpoint | null>(null);
71
+ const condition = useMemo(
72
+ () => (collapseCondition === undefined ? null : Adw.BreakpointCondition.parse(collapseCondition)),
73
+ [collapseCondition],
74
+ );
75
+
76
+ useLayoutEffect(() => {
77
+ if (breakpoint === null || view === null) {
78
+ return;
79
+ }
80
+
81
+ breakpoint.addSetter(view, "collapsed", collapsedValue());
82
+ }, [view, breakpoint]);
83
+
84
+ return condition === null ? null : <AdwBreakpoint ref={setBreakpoint} condition={condition} />;
85
+ };
86
+
87
+ const settleShowContent = (view: Adw.NavigationSplitView | null, isShown: boolean, isSettled: boolean): void => {
88
+ if (isSettled !== isShown && view !== null) {
89
+ view.setShowContent(isSettled);
53
90
  }
54
91
  };
55
92
 
56
93
  const useContentSync = (
57
- viewRef: ViewRef,
94
+ view: Adw.NavigationSplitView | null,
58
95
  navigation: SplitViewNavigationHelpers,
59
96
  ): ((isShown: boolean | null) => void) =>
60
97
  useCallback(
@@ -67,9 +104,9 @@ const useContentSync = (
67
104
  popContent(navigation);
68
105
  }
69
106
 
70
- settleShowContent(viewRef, isShown, hasContent(navigation.getState()));
107
+ settleShowContent(view, isShown, hasContent(navigation.getState()));
71
108
  },
72
- [viewRef, navigation],
109
+ [view, navigation],
73
110
  );
74
111
 
75
112
  const EscapeGuard = (): ReactNode => (
@@ -95,48 +132,58 @@ const SidebarPage = ({ descriptor }: { descriptor: SplitViewDescriptor }): React
95
132
 
96
133
  return (
97
134
  <AdwNavigationPage title={options.title ?? route.name}>
98
- <AdwToolbarView topBar={topBar}>{descriptor.render()}</AdwToolbarView>
135
+ <AdwToolbarView top={topBar}>{descriptor.render()}</AdwToolbarView>
99
136
  </AdwNavigationPage>
100
137
  );
101
138
  };
102
139
 
103
140
  const SplitView = (props: SplitViewProps): ReactNode => {
104
- const { state, navigation, descriptors, describe, contentPlaceholder } = props;
105
- const { collapsed = false, sidebarPosition = "start", minSidebarWidth, maxSidebarWidth } = props;
106
- const viewRef = useRef<Adw.NavigationSplitView | null>(null);
107
- const onShowContentChanged = useContentSync(viewRef, navigation);
141
+ const { state, navigation, descriptors, describe, contentPlaceholder, collapseCondition } = props;
142
+ const { sidebarPosition = "start", minSidebarWidth, maxSidebarWidth } = props;
143
+ const [view, setView] = useState<Adw.NavigationSplitView | null>(null);
144
+ const onShowContentChanged = useContentSync(view, navigation);
145
+ const collapseBreakpoint = useCollapseBreakpoint(view, collapseCondition);
108
146
  const sidebar = requireDescriptor(descriptors, state.routes[0]?.key ?? "");
147
+ const collapsed = collapseCondition === undefined ? (props.collapsed ?? false) : undefined;
148
+ const isCollapsed = useProperty(view, "collapsed") ?? collapsed ?? false;
109
149
 
150
+ // AdwBreakpointBin warns without a size request; the window's size request sets the smallest supported size.
110
151
  return (
111
- <AdwNavigationSplitView
112
- ref={viewRef}
113
- collapsed={collapsed}
114
- showContent={hasContent(state)}
115
- sidebarPosition={PACK_TYPES[sidebarPosition]}
116
- minSidebarWidth={minSidebarWidth}
117
- maxSidebarWidth={maxSidebarWidth}
118
- sidebarWidthFraction={props.sidebarWidthFraction}
119
- onNotifyShowContent={onShowContentChanged}
120
- sidebar={<SidebarPage descriptor={sidebar} />}
121
- >
122
- <AdwNavigationPage
123
- title={contentTitle(state, descriptors)}
124
- controllers={collapsed && props.popOnEscape === false ? <EscapeGuard /> : null}
125
- >
126
- {hasContent(state) ? (
127
- <StackView
128
- offset={1}
129
- popOnEscape={props.popOnEscape}
130
- state={state}
131
- navigation={navigation}
132
- descriptors={descriptors}
133
- describe={describe}
134
- />
135
- ) : (
136
- <>{contentPlaceholder}</>
137
- )}
138
- </AdwNavigationPage>
139
- </AdwNavigationSplitView>
152
+ <SplitViewCollapsedContext value={isCollapsed}>
153
+ <AdwBreakpointBin widthRequest={1} heightRequest={1} breakpoints={collapseBreakpoint}>
154
+ <AdwNavigationSplitView
155
+ ref={setView}
156
+ collapsed={collapsed}
157
+ showContent={hasContent(state)}
158
+ sidebarPosition={PACK_TYPES[sidebarPosition]}
159
+ minSidebarWidth={minSidebarWidth}
160
+ maxSidebarWidth={maxSidebarWidth}
161
+ sidebarWidthFraction={props.sidebarWidthFraction}
162
+ onNotifyShowContent={onShowContentChanged}
163
+ sidebar={<SidebarPage descriptor={sidebar} />}
164
+ >
165
+ <AdwNavigationPage
166
+ title={contentTitle(state, descriptors)}
167
+ controllers={isCollapsed && props.popOnEscape === false ? <EscapeGuard /> : null}
168
+ >
169
+ {hasContent(state) ? (
170
+ <StackView
171
+ offset={1}
172
+ popOnEscape={props.popOnEscape}
173
+ state={state}
174
+ navigation={navigation}
175
+ descriptors={descriptors}
176
+ describe={describe}
177
+ />
178
+ ) : (
179
+ <AdwToolbarView top={<AdwHeaderBar showTitle={false} />}>
180
+ {contentPlaceholder}
181
+ </AdwToolbarView>
182
+ )}
183
+ </AdwNavigationPage>
184
+ </AdwNavigationSplitView>
185
+ </AdwBreakpointBin>
186
+ </SplitViewCollapsedContext>
140
187
  );
141
188
  };
142
189
 
@@ -60,9 +60,16 @@ type SplitViewDescriptorMap = Record<string, SplitViewDescriptor>;
60
60
 
61
61
  /** Props of the split view navigator beyond the options every navigator shares. */
62
62
  type SplitViewNavigationConfig = {
63
- /** Whether the panes stack into one instead of sitting side by side; defaults to `false`. */
63
+ /** Whether the panes stack into one instead of sitting side by side; defaults to `false`. Ignored while `collapseCondition` is set. */
64
64
  collapsed?: boolean;
65
- /** Fills the content pane while no content route is open; defaults to nothing. */
65
+ /**
66
+ * An `Adw.BreakpointCondition` string, such as `"max-width: 500sp"`, measured against the navigator's size.
67
+ * The panes collapse while it holds. libadwaita applies it during layout, before GTK moves focus, so
68
+ * widening the window does not reselect the focused sidebar row the way a `collapsed` prop set from a
69
+ * breakpoint handler can. Give the window a `widthRequest` and `heightRequest`, as for window breakpoints.
70
+ */
71
+ collapseCondition?: string;
72
+ /** Fills the content pane, below an untitled header bar, while no content route is open; defaults to nothing. */
66
73
  contentPlaceholder?: ReactNode;
67
74
  /** Which side the sidebar sits on; defaults to `"start"`. */
68
75
  sidebarPosition?: "start" | "end";
@@ -70,7 +70,17 @@ function createStackNavigator<
70
70
  const ParamList extends ParamListBase,
71
71
  const NavigatorID extends string | undefined = string | undefined,
72
72
  const TypeBag extends NavigatorTypeBagBase = StackTypeBag<ParamList, NavigatorID>,
73
- const Config extends StaticConfig<TypeBag> | undefined = undefined,
73
+ >(): TypedNavigator<TypeBag, undefined>;
74
+ /** Creates a stack navigator rendered with `AdwNavigationView`: pushes and pops animate like native Adwaita pages. */
75
+ function createStackNavigator<
76
+ const ParamList extends ParamListBase,
77
+ const NavigatorID extends string | undefined = string | undefined,
78
+ const TypeBag extends NavigatorTypeBagBase = StackTypeBag<ParamList, NavigatorID>,
79
+ const Config extends StaticConfig<TypeBag> = StaticConfig<TypeBag>,
80
+ >(config: Config): TypedNavigator<TypeBag, Config>;
81
+ function createStackNavigator<
82
+ const TypeBag extends NavigatorTypeBagBase,
83
+ const Config extends StaticConfig<TypeBag> | undefined,
74
84
  >(config?: Config): TypedNavigator<TypeBag, Config> {
75
85
  return createNavigatorFactory(StackNavigator)(config) as TypedNavigator<TypeBag, Config>;
76
86
  }
@@ -112,7 +112,7 @@ const StackPageContent = ({ descriptor, previous }: Omit<StackPageProps, "naviga
112
112
  <>{options.header({ route, navigation, options, back })}</>
113
113
  );
114
114
 
115
- return <AdwToolbarView topBar={topBar}>{descriptor.render()}</AdwToolbarView>;
115
+ return <AdwToolbarView top={topBar}>{descriptor.render()}</AdwToolbarView>;
116
116
  };
117
117
 
118
118
  const StackPage = ({ descriptor, previous, navigation, onHidden }: StackPageProps): ReactNode => {
@@ -0,0 +1,122 @@
1
+ import type {
2
+ NavigatorTypeBagBase,
3
+ ParamListBase,
4
+ StaticConfig,
5
+ StaticScreenFactory,
6
+ TypedNavigator,
7
+ } from "@react-navigation/core";
8
+ import type { ReactNode } from "react";
9
+ import * as Adw from "@gtkx/gi/adw";
10
+ import { createNavigatorFactory, createScreenFactory, useNavigationBuilder } from "@react-navigation/core";
11
+ import { useState } from "react";
12
+ import type {
13
+ TabViewActionHelpers,
14
+ TabViewNavigationEventMap,
15
+ TabViewNavigationOptions,
16
+ TabViewNavigationProp,
17
+ TabViewNavigationState,
18
+ TabViewNavigatorProps,
19
+ TabViewRouterOptions,
20
+ } from "./types.js";
21
+ import { readExistingTabs } from "./page-registry.js";
22
+ import { TabViewHost } from "./tab-view-host.js";
23
+ import { createTabViewRouter } from "./tab-view-router.js";
24
+
25
+ /** Type bag describing the tab view navigator to React Navigation's typed APIs. */
26
+ type TabViewTypeBag<
27
+ ParamList extends ParamListBase = ParamListBase,
28
+ NavigatorID extends string | undefined = string | undefined,
29
+ > = {
30
+ /** Route names and their params. */
31
+ ParamList: ParamList;
32
+ /** Optional navigator id. */
33
+ NavigatorID: NavigatorID;
34
+ /** Shape of the navigator state. */
35
+ State: TabViewNavigationState<ParamList>;
36
+ /** Per-screen options. */
37
+ ScreenOptions: TabViewNavigationOptions;
38
+ /** Events the navigator emits. */
39
+ EventMap: TabViewNavigationEventMap;
40
+ /** Navigation object per route. */
41
+ NavigationList: {
42
+ [RouteName in keyof ParamList]: TabViewNavigationProp<ParamList, RouteName, NavigatorID>;
43
+ };
44
+ /** The navigator component. */
45
+ Navigator: typeof TabViewNavigator;
46
+ };
47
+
48
+ /** Declares a typed screen of a tab view navigator for the static configuration API. */
49
+ const createTabViewScreen: StaticScreenFactory<TabViewTypeBag> = createScreenFactory<TabViewTypeBag>();
50
+
51
+ /** Renders each open route of a {@link createTabViewNavigator} as a page of an `Adw.TabView`. */
52
+ function TabViewNavigator({
53
+ view: providedView,
54
+ tabBarVisible,
55
+ tabBarAutohide,
56
+ tabBarExpand,
57
+ tabBarInverted,
58
+ overview,
59
+ menuModel,
60
+ renderMenu,
61
+ extraDropTarget,
62
+ onExtraDragDrop,
63
+ onCreateWindow,
64
+ onSetupMenu,
65
+ shortcuts,
66
+ defaultIcon,
67
+ ...options
68
+ }: TabViewNavigatorProps): ReactNode {
69
+ const [view] = useState(() => providedView ?? new Adw.TabView());
70
+ const [router] = useState(() => createTabViewRouter(() => readExistingTabs(view)));
71
+ const { state, descriptors, navigation, render } = useNavigationBuilder<
72
+ TabViewNavigationState<ParamListBase>,
73
+ TabViewRouterOptions,
74
+ TabViewActionHelpers<ParamListBase>,
75
+ TabViewNavigationOptions,
76
+ TabViewNavigationEventMap
77
+ >(router, options);
78
+
79
+ return render(
80
+ <TabViewHost
81
+ view={view}
82
+ state={state}
83
+ navigation={navigation}
84
+ descriptors={descriptors}
85
+ tabBarVisible={tabBarVisible}
86
+ tabBarAutohide={tabBarAutohide}
87
+ tabBarExpand={tabBarExpand}
88
+ tabBarInverted={tabBarInverted}
89
+ overview={overview}
90
+ menuModel={menuModel}
91
+ renderMenu={renderMenu}
92
+ extraDropTarget={extraDropTarget}
93
+ onExtraDragDrop={onExtraDragDrop}
94
+ onCreateWindow={onCreateWindow}
95
+ onSetupMenu={onSetupMenu}
96
+ shortcuts={shortcuts}
97
+ defaultIcon={defaultIcon}
98
+ />,
99
+ );
100
+ }
101
+
102
+ /** Creates a navigator whose routes are the pages of an `Adw.TabView`, with a tab bar and an optional overview. */
103
+ function createTabViewNavigator<
104
+ const ParamList extends ParamListBase,
105
+ const NavigatorID extends string | undefined = string | undefined,
106
+ const TypeBag extends NavigatorTypeBagBase = TabViewTypeBag<ParamList, NavigatorID>,
107
+ >(): TypedNavigator<TypeBag, undefined>;
108
+ /** Creates a navigator whose routes are the pages of an `Adw.TabView`, with a tab bar and an optional overview. */
109
+ function createTabViewNavigator<
110
+ const ParamList extends ParamListBase,
111
+ const NavigatorID extends string | undefined = string | undefined,
112
+ const TypeBag extends NavigatorTypeBagBase = TabViewTypeBag<ParamList, NavigatorID>,
113
+ const Config extends StaticConfig<TypeBag> = StaticConfig<TypeBag>,
114
+ >(config: Config): TypedNavigator<TypeBag, Config>;
115
+ function createTabViewNavigator<
116
+ const TypeBag extends NavigatorTypeBagBase,
117
+ const Config extends StaticConfig<TypeBag> | undefined,
118
+ >(config?: Config): TypedNavigator<TypeBag, Config> {
119
+ return createNavigatorFactory(TabViewNavigator)(config) as TypedNavigator<TypeBag, Config>;
120
+ }
121
+
122
+ export { createTabViewNavigator, createTabViewScreen, TabViewNavigator, type TabViewTypeBag };
@@ -0,0 +1,139 @@
1
+ import type * as Gtk from "@gtkx/gi/gtk";
2
+ import * as Adw from "@gtkx/gi/adw";
3
+ import type { ParamListBase } from "@react-navigation/core";
4
+ import type { ExistingTab } from "./tab-view-router.js";
5
+ import type { TabViewNavigationOptions, TabViewNavigationState } from "./types.js";
6
+
7
+ type Route = TabViewNavigationState<ParamListBase>["routes"][number];
8
+
9
+ /** Page property values last written from a route's options. */
10
+ type AppliedOptions = Pick<
11
+ TabViewNavigationOptions,
12
+ | "icon"
13
+ | "indicatorActivatable"
14
+ | "indicatorIcon"
15
+ | "indicatorTooltip"
16
+ | "keyword"
17
+ | "liveThumbnail"
18
+ | "loading"
19
+ | "needsAttention"
20
+ | "thumbnailXalign"
21
+ | "thumbnailYalign"
22
+ | "tooltip"
23
+ > & { title?: string };
24
+
25
+ /** A native page a tab view navigator created or adopted, with the host its content renders into. */
26
+ type PageRecord = {
27
+ route: Route;
28
+ page: Adw.TabPage;
29
+ host: Adw.Bin;
30
+ applied: AppliedOptions;
31
+ };
32
+
33
+ /** The pages of one navigator by route key, notifying React when the set changes. */
34
+ type PageStore = {
35
+ get: (key: string) => PageRecord | undefined;
36
+ set: (record: PageRecord) => void;
37
+ delete: (key: string) => void;
38
+ keys: () => string[];
39
+ subscribe: (listener: () => void) => () => void;
40
+ getVersion: () => number;
41
+ /** Key of the route whose page was selected by the last sync, so native selection changes are not undone. */
42
+ lastFocus: string | undefined;
43
+ };
44
+
45
+ const records = new WeakMap<Adw.TabPage, PageRecord>();
46
+
47
+ const recordOf = (page: Adw.TabPage): PageRecord | undefined => records.get(page);
48
+
49
+ const registerPage = (record: PageRecord): void => {
50
+ records.set(record.page, record);
51
+ };
52
+
53
+ const ownerOf = (page: Adw.TabPage): Adw.TabView | null => {
54
+ let widget: Gtk.Widget | null = page.getChild();
55
+
56
+ while (widget !== null) {
57
+ if (widget instanceof Adw.TabView) {
58
+ return widget;
59
+ }
60
+
61
+ widget = widget.getParent();
62
+ }
63
+
64
+ return null;
65
+ };
66
+
67
+ const readExistingTabs = (view: Adw.TabView): ExistingTab[] => {
68
+ const selected = view.getSelectedPage();
69
+ const tabs: ExistingTab[] = [];
70
+
71
+ for (let position = 0; position < view.getNPages(); position += 1) {
72
+ const page = view.getNthPage(position);
73
+ const record = records.get(page);
74
+
75
+ if (record === undefined) {
76
+ continue;
77
+ }
78
+
79
+ const parent = page.getParent();
80
+ const parentRecord = parent === null ? undefined : records.get(parent);
81
+
82
+ tabs.push({
83
+ route: record.route,
84
+ pinned: page.getPinned(),
85
+ parent: parentRecord?.route.key,
86
+ selected: page === selected,
87
+ });
88
+ }
89
+
90
+ return tabs;
91
+ };
92
+
93
+ const createPageStore = (): PageStore => {
94
+ const byKey = new Map<string, PageRecord>();
95
+ const listeners = new Set<() => void>();
96
+ let version = 0;
97
+
98
+ const notify = (): void => {
99
+ version += 1;
100
+
101
+ for (const listener of listeners) {
102
+ listener();
103
+ }
104
+ };
105
+
106
+ return {
107
+ get: (key) => byKey.get(key),
108
+ set: (record) => {
109
+ byKey.set(record.route.key, record);
110
+ notify();
111
+ },
112
+ delete: (key) => {
113
+ if (byKey.delete(key)) {
114
+ notify();
115
+ }
116
+ },
117
+ keys: () => [...byKey.keys()],
118
+ subscribe: (listener) => {
119
+ listeners.add(listener);
120
+
121
+ return () => {
122
+ listeners.delete(listener);
123
+ };
124
+ },
125
+ getVersion: () => version,
126
+ lastFocus: undefined,
127
+ };
128
+ };
129
+
130
+ export {
131
+ type AppliedOptions,
132
+ createPageStore,
133
+ ownerOf,
134
+ type PageRecord,
135
+ type PageStore,
136
+ readExistingTabs,
137
+ recordOf,
138
+ registerPage,
139
+ };