Skip to content

Commit 79d0eb7

Browse files
feat(windows): custom application titlebar (#23)
* Simplify for example * fix: include modules that annotate with REACT_TURBO_MODULE * use turbo module * Fix fabric imports * Add passthrough module * Group titlebar components * Code style fixes * lint fix * Simplify module * Fix component registration * Fix memory leak issue that's caused by mounting and unmounting passthroughviews
1 parent 52d2458 commit 79d0eb7

15 files changed

+335
-110
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,7 @@ macos/.xcode.env.local
5252

5353
msbuild.binlog
5454

55-
.claude/*
55+
.claude/*
56+
57+
# Windows reserved device names
58+
nul

app/app.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { useTheme, themed } from "./theme/theme"
1010
import { useEffect, useMemo } from "react"
1111
import { TimelineScreen } from "./screens/TimelineScreen"
1212
import { useMenuItem } from "./utils/useMenuItem"
13-
import { Titlebar } from "./components/Titlebar"
13+
import { Titlebar } from "./components/Titlebar/Titlebar"
1414
import { Sidebar } from "./components/Sidebar/Sidebar"
1515
import { useSidebar } from "./state/useSidebar"
1616
import { AppHeader } from "./components/AppHeader"
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* PassthroughView - Windows Title Bar Click-Through Component
3+
*
4+
* Creates regions within the extended title bar that allow mouse clicks to pass through
5+
* to underlying interactive elements (buttons, inputs, etc.) while keeping the rest of
6+
* the title bar draggable.
7+
*
8+
* This is necessary because Windows title bars with ExtendsContentIntoTitleBar(true)
9+
* capture all mouse events by default. PassthroughView creates "punch-out" regions
10+
* using Windows InputNonClientPointerSource passthrough regions.
11+
*
12+
* On macOS, this simply returns a regular View since macOS title bars don't intercept
13+
* mouse events the same way - interactive elements in the title bar work normally.
14+
*
15+
* Usage: Wrap interactive elements that need to remain clickable in the title bar area.
16+
* Example: <PassthroughView><Button>Settings</Button></PassthroughView>
17+
*/
18+
import { View, Platform } from "react-native"
19+
import type { ViewProps } from "react-native"
20+
import NativePassthroughView from "../../native/IRPassthroughView/IRPassthroughViewNativeComponent"
21+
22+
export const PassthroughView = (props: ViewProps) => {
23+
return Platform.select({
24+
windows: <NativePassthroughView {...props} />,
25+
default: <View {...props} />, // macOS and other platforms use regular View
26+
})
27+
}

app/components/Titlebar.tsx renamed to app/components/Titlebar/Titlebar.tsx

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { themed, useTheme } from "../theme/theme"
1+
import { themed, useTheme } from "../../theme/theme"
22
import { Platform, View, ViewStyle } from "react-native"
3-
import { Icon } from "./Icon"
4-
import ActionButton from "./ActionButton"
5-
import { useSidebar } from "../state/useSidebar"
3+
import { Icon } from "../Icon"
4+
import ActionButton from "../ActionButton"
5+
import { useSidebar } from "../../state/useSidebar"
6+
import { PassthroughView } from "./PassthroughView"
67

78
export const Titlebar = () => {
89
const theme = useTheme()
@@ -12,16 +13,18 @@ export const Titlebar = () => {
1213
<View style={$borderContainer()}>
1314
<View style={$container()}>
1415
<TrafficLightSpacer />
15-
<ActionButton
16-
icon={() => (
17-
<Icon
18-
icon={isOpen ? "panelLeftClose" : "panelLeftOpen"}
19-
size={18}
20-
color={theme.colors.neutral}
21-
/>
22-
)}
23-
onClick={toggleSidebar}
24-
/>
16+
<PassthroughView>
17+
<ActionButton
18+
icon={() => (
19+
<Icon
20+
icon={isOpen ? "panelLeftClose" : "panelLeftOpen"}
21+
size={18}
22+
color={theme.colors.neutral}
23+
/>
24+
)}
25+
onClick={toggleSidebar}
26+
/>
27+
</PassthroughView>
2528
</View>
2629
</View>
2730
)

app/native/IRMenuItemManager/IRMenuItemManager.windows.cpp

Lines changed: 17 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -8,72 +8,26 @@
88
#include "pch.h"
99
#include "IRMenuItemManager.windows.h"
1010

11+
using winrt::reactotron::implementation::IRMenuItemManager;
12+
1113
namespace winrt::reactotron::implementation
1214
{
13-
IRMenuItemManager::IRMenuItemManager() noexcept
14-
{
15-
// TurboModule initialization
16-
}
17-
18-
Microsoft::ReactNative::JSValue IRMenuItemManager::getAvailableMenus() noexcept
19-
{
20-
// TODO: Get available Windows application menus
21-
Microsoft::ReactNative::JSValueArray menus;
22-
// Stub implementation
23-
return Microsoft::ReactNative::JSValue(std::move(menus));
24-
}
25-
26-
Microsoft::ReactNative::JSValue IRMenuItemManager::getMenuStructure() noexcept
27-
{
28-
// TODO: Get Windows application menu structure
29-
Microsoft::ReactNative::JSValueArray structure;
30-
// Stub implementation
31-
return Microsoft::ReactNative::JSValue(std::move(structure));
32-
}
33-
34-
void IRMenuItemManager::createMenu(std::string menuName, Microsoft::ReactNative::ReactPromise<Microsoft::ReactNative::JSValue> const &promise) noexcept
15+
void IRMenuItemManager::createMenu(std::string menuName,
16+
::React::ReactPromise<CreateRet> &&result) noexcept
3517
{
36-
// TODO: Create a new Windows menu
37-
Microsoft::ReactNative::JSValueObject result;
38-
result["success"] = false;
39-
result["existed"] = false;
40-
result["menuName"] = menuName;
41-
promise.Resolve(Microsoft::ReactNative::JSValue(std::move(result)));
42-
}
43-
44-
void IRMenuItemManager::addMenuItemAtPath(Microsoft::ReactNative::JSValue parentPath, std::string const &title, std::string const &keyEquivalent, Microsoft::ReactNative::ReactPromise<Microsoft::ReactNative::JSValue> const &promise) noexcept
45-
{
46-
// TODO: Add menu item at specified path in Windows
47-
Microsoft::ReactNative::JSValueObject result;
48-
result["success"] = false;
49-
result["error"] = "Not implemented";
50-
promise.Resolve(Microsoft::ReactNative::JSValue(std::move(result)));
51-
}
52-
53-
void IRMenuItemManager::insertMenuItemAtPath(Microsoft::ReactNative::JSValue parentPath, std::string const &title, int atIndex, std::string const &keyEquivalent, Microsoft::ReactNative::ReactPromise<Microsoft::ReactNative::JSValue> const &promise) noexcept
54-
{
55-
// TODO: Insert menu item at specified index and path in Windows
56-
Microsoft::ReactNative::JSValueObject result;
57-
result["success"] = false;
58-
result["error"] = "Not implemented";
59-
promise.Resolve(Microsoft::ReactNative::JSValue(std::move(result)));
60-
}
18+
// THE PROBLEM: onMenuItemPressed is nullptr/undefined at runtime
19+
if (onMenuItemPressed)
20+
{
21+
PressEvent evt{};
22+
evt.menuPath = {"Test", "Event"};
23+
onMenuItemPressed(evt);
24+
}
6125

62-
void IRMenuItemManager::removeMenuItemAtPath(Microsoft::ReactNative::JSValue path, Microsoft::ReactNative::ReactPromise<Microsoft::ReactNative::JSValue> const &promise) noexcept
63-
{
64-
// TODO: Remove menu item at specified path in Windows
65-
Microsoft::ReactNative::JSValueObject result;
66-
result["success"] = false;
67-
result["error"] = "Not implemented";
68-
promise.Resolve(Microsoft::ReactNative::JSValue(std::move(result)));
26+
CreateRet ret{};
27+
ret.success = true;
28+
ret.existed = false;
29+
ret.menuName = menuName;
30+
result.Resolve(std::move(ret));
6931
}
7032

71-
void IRMenuItemManager::setMenuItemEnabledAtPath(Microsoft::ReactNative::JSValue path, bool enabled, Microsoft::ReactNative::ReactPromise<Microsoft::ReactNative::JSValue> const &promise) noexcept
72-
{
73-
// TODO: Enable/disable menu item at specified path in Windows
74-
Microsoft::ReactNative::JSValueObject result;
75-
result["success"] = false;
76-
result["error"] = "Not implemented";
77-
promise.Resolve(Microsoft::ReactNative::JSValue(std::move(result)));
78-
}
79-
}
33+
} // namespace winrt::reactotron::implementation
Lines changed: 18 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,28 @@
11
#pragma once
2-
#include "NativeModules.h"
2+
3+
#include <NativeModules.h>
4+
#include <winrt/Microsoft.ReactNative.h>
5+
6+
// Generated (DataTypes before Spec)
7+
#include "..\..\..\windows\reactotron\codegen\NativeIRMenuItemManagerDataTypes.g.h"
8+
#include "..\..\..\windows\reactotron\codegen\NativeIRMenuItemManagerSpec.g.h"
39

410
namespace winrt::reactotron::implementation
511
{
6-
REACT_MODULE(IRMenuItemManager)
7-
struct IRMenuItemManager
12+
REACT_TURBO_MODULE(IRMenuItemManager)
13+
struct IRMenuItemManager : reactotronCodegen::IRMenuItemManagerSpec
814
{
9-
IRMenuItemManager() noexcept;
10-
11-
REACT_SYNC_METHOD(getAvailableMenus)
12-
Microsoft::ReactNative::JSValue getAvailableMenus() noexcept;
13-
14-
REACT_SYNC_METHOD(getMenuStructure)
15-
Microsoft::ReactNative::JSValue getMenuStructure() noexcept;
15+
// Only the essential types needed for the event
16+
using PressEvent = reactotronCodegen::IRMenuItemManagerSpec_MenuItemPressedEvent;
17+
using CreateRet = reactotronCodegen::IRMenuItemManagerSpec_createMenu_returnType;
1618

19+
// One simple method to test event emission
1720
REACT_METHOD(createMenu)
18-
void createMenu(std::string menuName, Microsoft::ReactNative::ReactPromise<Microsoft::ReactNative::JSValue> const& promise) noexcept;
19-
20-
REACT_METHOD(addMenuItemAtPath)
21-
void addMenuItemAtPath(Microsoft::ReactNative::JSValue parentPath, std::string const& title, std::string const& keyEquivalent, Microsoft::ReactNative::ReactPromise<Microsoft::ReactNative::JSValue> const& promise) noexcept;
22-
23-
REACT_METHOD(insertMenuItemAtPath)
24-
void insertMenuItemAtPath(Microsoft::ReactNative::JSValue parentPath, std::string const& title, int atIndex, std::string const& keyEquivalent, Microsoft::ReactNative::ReactPromise<Microsoft::ReactNative::JSValue> const& promise) noexcept;
25-
26-
REACT_METHOD(removeMenuItemAtPath)
27-
void removeMenuItemAtPath(Microsoft::ReactNative::JSValue path, Microsoft::ReactNative::ReactPromise<Microsoft::ReactNative::JSValue> const& promise) noexcept;
28-
29-
REACT_METHOD(setMenuItemEnabledAtPath)
30-
void setMenuItemEnabledAtPath(Microsoft::ReactNative::JSValue path, bool enabled, Microsoft::ReactNative::ReactPromise<Microsoft::ReactNative::JSValue> const& promise) noexcept;
21+
void createMenu(std::string menuName, ::React::ReactPromise<CreateRet> &&result) noexcept;
3122

23+
// --- THE ISSUE: This event is undefined in JavaScript ---
3224
REACT_EVENT(onMenuItemPressed)
33-
std::function<void(Microsoft::ReactNative::JSValue)> onMenuItemPressed;
25+
std::function<void(PressEvent)> onMenuItemPressed;
3426
};
35-
}
27+
28+
} // namespace winrt::reactotron::implementation

0 commit comments

Comments
 (0)