mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-08 09:24:41 +00:00
fix(gpui): stabilize macOS packaging, connections and appearance
Release terminal WebSocket clients on the JS thread so daemon retries do not exhaust descriptors or join their own IO thread. Preserve Cargo symbols and seal ad-hoc macOS bundles with a library-loading policy that works with hardened runtime. Use rounded border decoration without changing Fabric child layout, and route desktop hover through pointer events and Pressable render props. Include native regressions and reproducible package verification.
This commit is contained in:
Vendored
+24
@@ -13,8 +13,32 @@ Local patches (all other retained files match the revision above):
|
||||
install their own pointer area. Otherwise an empty portal overlay redirects
|
||||
clicks to its parent, blocking Paseo's sidebar. native.test.ts exercises this
|
||||
against the actual app.
|
||||
- packages/core/ReactGpui/rn-gpui/src/{node_view,style}.rs: paint uniform solid
|
||||
borders with GPUI's rounded border primitive on a decoration child, keeping
|
||||
Fabric child coordinates intact. Edge strips cannot draw the corner arcs.
|
||||
- packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiJsEngine.h: route Pressability
|
||||
hover through the pointer events GPUI emits. The desktop Pressable wrapper
|
||||
in src-gpui/Libraries/Components/Pressable also supplies hovered to render
|
||||
props; metro.config.js lets a wrapper import its upstream component without
|
||||
resolving back to itself. native.test.ts covers callbacks, render props,
|
||||
nested row hover and border pixels using appearance-regression.fixture.js.
|
||||
- packages/core/src-gpui/Libraries/Components/StatusBar/StatusBar.desktop.js:
|
||||
desktop StatusBar implementation, because there is no mobile OS status bar.
|
||||
- packages/cli/src/package-desktop.js: ad-hoc sign macOS packages so their
|
||||
bundle resources are sealed, with templates/adhoc.entitlements.plist allowing
|
||||
bundled ad-hoc dylibs under hardened runtime; covered by package-desktop.test.js.
|
||||
- packages/cli/src/commands.js: preserve Cargo's generated dSYM symlink so
|
||||
subsequent builds can replace it; copy symbol contents into the distribution
|
||||
instead of linking back into the build tree. Covered by commands.test.js.
|
||||
- packages/core/ReactCxxPlatform/react/io/WebSocketModule.{cpp,h}: release
|
||||
failed and closed connections on the JS thread. Keeping terminal clients
|
||||
exhausts file descriptors during daemon retries; deleting them on the IO
|
||||
thread would join that same thread. websocket.native.test.ts exercises the
|
||||
real RN API against a loopback peer. ReactGpui/rn-gpui-sys/build.rs tracks
|
||||
these sources so incremental builds include the fix.
|
||||
- packages/core/ReactGpui/rn-gpui-sys/build.rs: pass Cargo's
|
||||
MACOSX_DEPLOYMENT_TARGET to CMake. Otherwise source builds default to the
|
||||
installed SDK's version even when the app targets an older macOS release.
|
||||
|
||||
GPUI itself remains a pinned Cargo git dependency. Hermes/React Native prebuilt
|
||||
libraries are downloaded by upstream build tooling; this is not an offline or
|
||||
|
||||
+6
-34
@@ -821,7 +821,7 @@ export function scaffoldDesktop(
|
||||
beforeWrite(cargoPath);
|
||||
fs.writeFileSync(
|
||||
cargoPath,
|
||||
`[package]\nname = ${JSON.stringify(appPackageName(projectRoot, packageJson))}\nversion = "0.1.0"\nedition = "2021"\n\n# Set CARGO_PROFILE_DEV_DEBUG=full to opt into full debug information.\n[profile.dev]\ndebug = "line-tables-only"\nsplit-debuginfo = "packed"\n\n# Dependencies are optimized even in a dev build: GPUI rebuilds and paints the\n# element tree on every frame, and they are only compiled once.\n[profile.dev.package."*"]\ndebug = false\nopt-level = 3\n\n# The C++ bridge picks its CMake build type from this crate's opt-level, and a\n# Release build of React Native has no dev server, inspector or Fast Refresh.\n[profile.dev.package.rn-gpui-sys]\nopt-level = 0\n\n[profile.release]\ndebug = "line-tables-only"\n# Cargo strips inside the linker, before dsymutil, so package-desktop writes the\n# dSYM and then strips. Full LTO: 2 MB smaller for 1.4x the build time.\nstrip = "none"\nsplit-debuginfo = "packed"\nlto = "fat"\ncodegen-units = 1\n\n[build-dependencies]\nserde_json = "1"\n\n[dependencies]\n`,
|
||||
`[package]\nname = ${JSON.stringify(appPackageName(projectRoot, packageJson))}\nversion = "0.1.0"\nedition = "2021"\n\n# Set CARGO_PROFILE_DEV_DEBUG=full to opt into full debug information.\n[profile.dev]\ndebug = "line-tables-only"\nsplit-debuginfo = "packed"\n\n# Dependencies are optimized even in a dev build: GPUI rebuilds and paints the\n# element tree on every frame, and they are only compiled once.\n[profile.dev.package."*"]\ndebug = false\nopt-level = 3\n\n# The C++ bridge picks its CMake build type from this crate's opt-level, and a\n# Release build of React Native has no dev server, inspector or Fast Refresh.\n[profile.dev.package.rn-gpui-sys]\nopt-level = 0\n\n[profile.release]\ndebug = "line-tables-only"\n# Cargo strips inside the linker, before dsymutil, so package-desktop preserves the\n# Cargo dSYM and then strips. Full LTO: 2 MB smaller for 1.4x the build time.\nstrip = "none"\nsplit-debuginfo = "packed"\nlto = "fat"\ncodegen-units = 1\n\n[build-dependencies]\nserde_json = "1"\n\n[dependencies]\n`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1538,10 +1538,9 @@ export async function packageDesktop({
|
||||
const binaryName = desktopCrateName(crateDir);
|
||||
const buildDir = path.join(crateDir, 'build');
|
||||
|
||||
// The binary is finished. Ask for the debug file first — it can only be
|
||||
// collected from a binary that still has its debug info — then take the symbols
|
||||
// off, so the packager copies the small binary.
|
||||
ensureDsym({ crateDir, binaryName });
|
||||
// Cargo owns the packed debug output and its symlink in the profile directory.
|
||||
// Replacing that symlink with a directory makes the next Cargo build fail.
|
||||
// Keep its output intact and copy the symbols after packaging.
|
||||
stripReleaseBinary({ crateDir, binaryName });
|
||||
|
||||
// 2. Icons, from the one PNG the config names.
|
||||
@@ -1634,33 +1633,6 @@ export function ensureCargoPackager() {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the release dSYM if it is not there yet.
|
||||
*
|
||||
* Cargo does not invoke `dsymutil`, so a binary built with `strip = "none"` still
|
||||
* has no dSYM until one is asked for. This must happen before `stripReleaseBinary`
|
||||
* — the debug info has to be in the binary to be collected out of it.
|
||||
*/
|
||||
function ensureDsym({ crateDir, binaryName }) {
|
||||
const profileDir = targetProfileDir(crateDir, 'release');
|
||||
const binary = path.join(profileDir, binaryName);
|
||||
const dsym = path.join(profileDir, `${binaryName}.dSYM`);
|
||||
if (!fs.existsSync(binary)) return;
|
||||
// `dsymutil` writes the bundle read-only, and Cargo then cannot replace the
|
||||
// binary it belongs to: "failed to remove file … Operation not permitted". Make
|
||||
// it writable before removing it.
|
||||
if (fs.existsSync(dsym)) fs.chmodSync(dsym, 0o755);
|
||||
fs.rmSync(dsym, { recursive: true, force: true });
|
||||
try {
|
||||
execFileSync('dsymutil', [binary, '-o', dsym], { stdio: 'inherit' });
|
||||
// `dsymutil` writes the bundle read-only, and Cargo then cannot replace the
|
||||
// binary it belongs to: "failed to remove file … Operation not permitted".
|
||||
if (fs.existsSync(dsym)) fs.chmodSync(dsym, 0o755);
|
||||
} catch (error) {
|
||||
console.log(`could not write a dSYM for ${binaryName}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `file` is a directory with no files anywhere below it.
|
||||
function isEmptyDirectory(file) {
|
||||
const stat = fs.statSync(file);
|
||||
@@ -1684,7 +1656,7 @@ function isEmptyDirectory(file) {
|
||||
* function names, and it must not ship: it is larger than the binary and useless
|
||||
* to anyone who is not symbolizing.
|
||||
*/
|
||||
function copyDebugSymbols({ crateDir, binaryName, outDir }) {
|
||||
export function copyDebugSymbols({ crateDir, binaryName, outDir }) {
|
||||
const profileDir = targetProfileDir(crateDir, 'release');
|
||||
// `split-debuginfo = "packed"` gives a dSYM on macOS, a `.dwp` on Linux and a
|
||||
// `.pdb` on Windows. The unpacked `.debug` file is listed too because a
|
||||
@@ -1708,7 +1680,7 @@ function copyDebugSymbols({ crateDir, binaryName, outDir }) {
|
||||
for (const file of found) {
|
||||
const target = path.join(outDir, path.basename(file));
|
||||
fs.rmSync(target, { recursive: true, force: true });
|
||||
fs.cpSync(file, target, { recursive: true });
|
||||
fs.cpSync(file, target, { recursive: true, dereference: true });
|
||||
// A read-only directory here would make the next run fail to replace it.
|
||||
fs.chmodSync(target, 0o755);
|
||||
console.log(`debug symbols: ${target}`);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url';
|
||||
import { afterAll, expect, test } from 'vitest';
|
||||
import {
|
||||
collectNodeAddons,
|
||||
copyDebugSymbols,
|
||||
createLibrary,
|
||||
desktopLaunchConfig,
|
||||
linuxBuildDependencyProblem,
|
||||
@@ -33,6 +34,29 @@ const repoRoot = (() => {
|
||||
})();
|
||||
const temporaryRoots = [];
|
||||
|
||||
test('packaging copies Cargo dSYM contents without retaining its build-tree symlink', () => {
|
||||
fs.mkdirSync(nodePath.join(repoRoot, '.artifacts'), { recursive: true });
|
||||
const root = fs.mkdtempSync(nodePath.join(repoRoot, '.artifacts/gpui-dsym-'));
|
||||
temporaryRoots.push(root);
|
||||
const crateDir = nodePath.join(root, 'desktop');
|
||||
const release = nodePath.join(crateDir, 'target/release');
|
||||
const symbols = nodePath.join(release, 'deps/app-hash.dSYM');
|
||||
const dwarf = 'Contents/Resources/DWARF/app';
|
||||
fs.mkdirSync(nodePath.dirname(nodePath.join(symbols, dwarf)), { recursive: true });
|
||||
fs.writeFileSync(nodePath.join(symbols, dwarf), 'debug symbols');
|
||||
const cargoLink = nodePath.join(release, 'app.dSYM');
|
||||
fs.symlinkSync('deps/app-hash.dSYM', cargoLink);
|
||||
const outDir = nodePath.join(root, 'dist');
|
||||
fs.mkdirSync(outDir);
|
||||
|
||||
copyDebugSymbols({ crateDir, binaryName: 'app', outDir });
|
||||
|
||||
expect(fs.lstatSync(cargoLink).isSymbolicLink()).toBe(true);
|
||||
expect(fs.lstatSync(nodePath.join(outDir, 'app.dSYM')).isSymbolicLink()).toBe(false);
|
||||
fs.rmSync(release, { recursive: true });
|
||||
expect(fs.readFileSync(nodePath.join(outDir, 'app.dSYM', dwarf), 'utf8')).toBe('debug symbols');
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
for (const root of temporaryRoots) {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
|
||||
@@ -660,6 +660,16 @@ export function toPackagerConfig(config, build) {
|
||||
const macos = {
|
||||
minimumSystemVersion: config.bundle.macos.minimumSystemVersion,
|
||||
};
|
||||
// The linker's executable signature does not seal the packaged app's
|
||||
// resources. Local macOS packages need a complete ad-hoc bundle signature.
|
||||
if (os === 'macos') {
|
||||
macos.signingIdentity = '-';
|
||||
// cargo-packager enables hardened runtime. Ad-hoc signatures have no Team
|
||||
// ID, so library validation otherwise rejects even the bundled dylibs.
|
||||
macos.entitlements = relative(
|
||||
require.resolve('../templates/adhoc.entitlements.plist'),
|
||||
);
|
||||
}
|
||||
if (config.bundle.macos.entitlements !== null) {
|
||||
macos.entitlements = relative(
|
||||
path.resolve(process.cwd(), config.bundle.macos.entitlements),
|
||||
|
||||
@@ -263,6 +263,24 @@ describe('toPackagerConfig', () => {
|
||||
expect(config.outDir).toBe('/project/desktop/build/dist');
|
||||
});
|
||||
|
||||
test('macOS packages seal the app with an ad-hoc signature', () => {
|
||||
const config = toPackagerConfig(desktopConfig(), build({ os: 'macos' }));
|
||||
expect(config.macos.signingIdentity).toBe('-');
|
||||
});
|
||||
|
||||
test('ad-hoc macOS apps can load their bundled libraries without a Team ID', () => {
|
||||
const context = build({ os: 'macos' });
|
||||
const config = toPackagerConfig(desktopConfig(), context);
|
||||
expect(config.macos.entitlements).toBeTypeOf('string');
|
||||
const entitlements = fs.readFileSync(
|
||||
path.resolve(context.crateDir, config.macos.entitlements),
|
||||
'utf8',
|
||||
);
|
||||
expect(entitlements).toMatch(
|
||||
/<key>com\.apple\.security\.cs\.disable-library-validation<\/key>\s*<true\s*\/>/u,
|
||||
);
|
||||
});
|
||||
|
||||
test('the bundle file and the assets directory ship as resources', () => {
|
||||
// A directory containing this run's output cannot be shipped, so the two
|
||||
// are named. The asset directory has to exist to be listed.
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
+52
-29
@@ -51,6 +51,20 @@ WebSocketModule::~WebSocketModule() {
|
||||
connections_.clear();
|
||||
}
|
||||
|
||||
void WebSocketModule::releaseConnection(int32_t socketID) {
|
||||
// Only the JS thread owns connections_. Destroying a client joins its IO
|
||||
// thread, so terminal callbacks must dispatch here rather than delete it
|
||||
// from that IO thread. RN's JS WebSocket is already CLOSED when it receives
|
||||
// a terminal event and does not call the native close method afterward.
|
||||
auto it = connections_.find(socketID);
|
||||
if (it == connections_.end()) {
|
||||
return;
|
||||
}
|
||||
removeSocketSender(socketID);
|
||||
setBlobWebSocketHandler(socketID, false);
|
||||
connections_.erase(it);
|
||||
}
|
||||
|
||||
void WebSocketModule::connect(
|
||||
jsi::Runtime& /*rt*/,
|
||||
const std::string& url,
|
||||
@@ -119,24 +133,29 @@ void WebSocketModule::connect(
|
||||
});
|
||||
connections_[socketID]->setOnClosedFrameCallback(
|
||||
[weakThis = weak_from_this(), socketID](uint16_t code, const std::string& reason) {
|
||||
removeSocketSender(socketID);
|
||||
setBlobWebSocketHandler(socketID, false);
|
||||
auto strongThis = weakThis.lock();
|
||||
if (!strongThis) {
|
||||
return;
|
||||
}
|
||||
strongThis->emitDeviceEvent(
|
||||
"websocketClosed",
|
||||
[socketID, code, reason](jsi::Runtime& rt, std::vector<jsi::Value>& args) {
|
||||
auto arg = jsi::Object(rt);
|
||||
arg.setProperty(rt, "id", jsi::Value(socketID));
|
||||
arg.setProperty(rt, "code", jsi::Value(code));
|
||||
arg.setProperty(
|
||||
rt,
|
||||
"reason",
|
||||
facebook::jsi::String::createFromUtf8(rt, reason));
|
||||
args.emplace_back(rt, arg);
|
||||
});
|
||||
strongThis->jsInvoker_->invokeAsync([weakThis, socketID, code, reason](jsi::Runtime&) {
|
||||
auto owner = weakThis.lock();
|
||||
if (!owner || owner->connections_.find(socketID) == owner->connections_.end()) {
|
||||
return;
|
||||
}
|
||||
owner->releaseConnection(socketID);
|
||||
owner->emitDeviceEvent(
|
||||
"websocketClosed",
|
||||
[socketID, code, reason](jsi::Runtime& rt, std::vector<jsi::Value>& args) {
|
||||
auto arg = jsi::Object(rt);
|
||||
arg.setProperty(rt, "id", jsi::Value(socketID));
|
||||
arg.setProperty(rt, "code", jsi::Value(code));
|
||||
arg.setProperty(
|
||||
rt,
|
||||
"reason",
|
||||
facebook::jsi::String::createFromUtf8(rt, reason));
|
||||
args.emplace_back(rt, arg);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
connections_[socketID]->connect(
|
||||
@@ -156,18 +175,25 @@ void WebSocketModule::connect(
|
||||
args.emplace_back(rt, arg);
|
||||
});
|
||||
} else {
|
||||
strongThis->emitDeviceEvent(
|
||||
"websocketFailed",
|
||||
[socketID](jsi::Runtime& rt, std::vector<jsi::Value>& args) {
|
||||
auto arg = jsi::Object(rt);
|
||||
arg.setProperty(rt, "id", jsi::Value(socketID));
|
||||
arg.setProperty(
|
||||
rt,
|
||||
"message",
|
||||
facebook::jsi::String::createFromAscii(
|
||||
rt, "Could not connect to websocket."));
|
||||
args.emplace_back(rt, arg);
|
||||
});
|
||||
strongThis->jsInvoker_->invokeAsync([weakThis, socketID](jsi::Runtime&) {
|
||||
auto owner = weakThis.lock();
|
||||
if (!owner || owner->connections_.find(socketID) == owner->connections_.end()) {
|
||||
return;
|
||||
}
|
||||
owner->releaseConnection(socketID);
|
||||
owner->emitDeviceEvent(
|
||||
"websocketFailed",
|
||||
[socketID](jsi::Runtime& rt, std::vector<jsi::Value>& args) {
|
||||
auto arg = jsi::Object(rt);
|
||||
arg.setProperty(rt, "id", jsi::Value(socketID));
|
||||
arg.setProperty(
|
||||
rt,
|
||||
"message",
|
||||
facebook::jsi::String::createFromAscii(
|
||||
rt, "Could not connect to websocket."));
|
||||
args.emplace_back(rt, arg);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -237,9 +263,6 @@ void WebSocketModule::close(
|
||||
return;
|
||||
}
|
||||
it->second->close(static_cast<uint16_t>(code), reason);
|
||||
removeSocketSender(socketID);
|
||||
setBlobWebSocketHandler(socketID, false);
|
||||
connections_.erase(it);
|
||||
}
|
||||
|
||||
void WebSocketModule::addListener(
|
||||
|
||||
+2
@@ -48,6 +48,8 @@ class WebSocketModule : public NativeWebSocketModuleCxxSpec<WebSocketModule>,
|
||||
void removeListeners(jsi::Runtime &rt, int32_t count);
|
||||
|
||||
private:
|
||||
void releaseConnection(int32_t socketID);
|
||||
|
||||
WebSocketClientFactory webSocketClientFactory_;
|
||||
std::unordered_map<int32_t, std::unique_ptr<IWebSocketClient>> connections_;
|
||||
};
|
||||
|
||||
@@ -99,6 +99,8 @@ fn main() {
|
||||
for changed_platform_source in [
|
||||
"react/io/ImageLoaderModule.cpp",
|
||||
"react/io/ImageLoaderModule.h",
|
||||
"react/io/WebSocketModule.cpp",
|
||||
"react/io/WebSocketModule.h",
|
||||
"react/runtime/ReactCxxTurboModuleProvider.cpp",
|
||||
"react/runtime/ReactCxxTurboModuleProvider.h",
|
||||
"react/runtime/ReactHost.cpp",
|
||||
@@ -308,6 +310,11 @@ fn main() {
|
||||
// the path (the /usr/bin shim or the Xcode toolchain binary behind it), so
|
||||
// name the shim unless the user chose a compiler.
|
||||
if cfg!(target_os = "macos") && target_triple.contains("apple") {
|
||||
// CMake otherwise defaults to the installed SDK, independently of
|
||||
// Cargo's deployment target (including when reusing its cache).
|
||||
if let Ok(minimum) = env::var("MACOSX_DEPLOYMENT_TARGET") {
|
||||
defines.push(("CMAKE_OSX_DEPLOYMENT_TARGET", minimum));
|
||||
}
|
||||
for (variable, tool, env_name) in [
|
||||
("CMAKE_C_COMPILER", "/usr/bin/cc", "CC"),
|
||||
("CMAKE_CXX_COMPILER", "/usr/bin/c++", "CXX"),
|
||||
|
||||
+5
@@ -31,6 +31,11 @@ class GpuiFeatureFlagsOverrides
|
||||
bool cxxNativeAnimatedEnabled() override {
|
||||
return true;
|
||||
}
|
||||
|
||||
// GPUI dispatches W3C pointer events, not browser mouseenter/mouseleave.
|
||||
bool shouldPressibilityUseW3CPointerEventsForHover() override {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
struct GpuiJsEngineInfo {
|
||||
|
||||
@@ -3932,10 +3932,16 @@ impl Render for RNNodeView {
|
||||
true,
|
||||
),
|
||||
];
|
||||
// GPUI paints a complete rounded border, including the inner curve.
|
||||
// Four edge rules cannot represent it, even with rounded clipping.
|
||||
let native_border = self.view.border_colors[0] != 0
|
||||
&& edges.iter().any(|(_, _, _, _, thickness, _)| *thickness > 0.0)
|
||||
&& self.view.border_colors.iter().all(|color| *color == self.view.border_colors[0])
|
||||
&& self.view.border_styles.iter().all(|style| *style == 0);
|
||||
let mut border_rules = Vec::new();
|
||||
for (edge, x, y, length, thickness, horizontal) in edges {
|
||||
let color = self.view.border_colors[edge];
|
||||
if color != 0 && thickness > 0.0 {
|
||||
if !native_border && color != 0 && thickness > 0.0 {
|
||||
border_rules.extend(rule_segments(
|
||||
x,
|
||||
y,
|
||||
@@ -3947,7 +3953,7 @@ impl Render for RNNodeView {
|
||||
));
|
||||
}
|
||||
}
|
||||
if !border_rules.is_empty() {
|
||||
if native_border || !border_rules.is_empty() {
|
||||
let mut border_clip = div()
|
||||
.absolute()
|
||||
.top_0()
|
||||
@@ -3963,7 +3969,19 @@ impl Render for RNNodeView {
|
||||
radii.bottom_left =
|
||||
Some(px(self.view.border_radii.bl * scale_x.min(scale_y)).into());
|
||||
}
|
||||
element = element.child(border_clip.children(border_rules));
|
||||
if native_border {
|
||||
// Keep the border on a decoration child: adding border widths
|
||||
// to the Fabric node itself would inset its Yoga-laid-out children.
|
||||
let border = border_clip.style();
|
||||
border.border_color = Some(style::color(self.view.border_colors[0]));
|
||||
border.border_widths.left = Some(px(self.view.border_widths.left * scale_x).into());
|
||||
border.border_widths.top = Some(px(self.view.border_widths.top * scale_y).into());
|
||||
border.border_widths.right = Some(px(self.view.border_widths.right * scale_x).into());
|
||||
border.border_widths.bottom = Some(px(self.view.border_widths.bottom * scale_y).into());
|
||||
element = element.child(border_clip);
|
||||
} else {
|
||||
element = element.child(border_clip.children(border_rules));
|
||||
}
|
||||
}
|
||||
let mut outline_rules = Vec::new();
|
||||
if self.view.outline_color != 0 && self.view.outline_width > 0.0 {
|
||||
|
||||
@@ -60,8 +60,8 @@ pub fn to_refinement(v: &ViewPropsFlat) -> StyleRefinement {
|
||||
if v.background_color != 0 {
|
||||
s.background = Some(Fill::from(color(v.background_color)));
|
||||
}
|
||||
// RN borders are painted as four independent edge rules by `node_view`.
|
||||
// GPUI border widths would add a second, default-colored edge on top.
|
||||
// `node_view` owns the border decoration. Border widths on this element
|
||||
// would also inset children that already have their Fabric layout.
|
||||
s.corner_radii.top_left = Some(px(v.border_radii.tl).into());
|
||||
s.corner_radii.top_right = Some(px(v.border_radii.tr).into());
|
||||
s.corner_radii.bottom_right = Some(px(v.border_radii.br).into());
|
||||
|
||||
+5
-1
@@ -231,7 +231,11 @@ function makeMetroConfig({ projectRoot = process.cwd() } = {}) {
|
||||
}
|
||||
}
|
||||
const override = key && overrides.get(key);
|
||||
return override ? withFilePath(resolution, override) : resolution;
|
||||
// A platform wrapper can import the component it wraps without resolving
|
||||
// that import back to itself (e.g. desktop Pressable wrapping RN Pressable).
|
||||
return override && override !== context.originModulePath
|
||||
? withFilePath(resolution, override)
|
||||
: resolution;
|
||||
};
|
||||
|
||||
const config = mergeConfig(getDefaultConfig(projectRoot), {
|
||||
|
||||
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
import React, { forwardRef, memo, useCallback, useState } from 'react';
|
||||
import NativePressable from './Pressable';
|
||||
|
||||
// Keep RN's press, focus, accessibility and delayed-hover handling. Desktop
|
||||
// also exposes hover state to style/children render props, as RN Web does.
|
||||
const Pressable = forwardRef(function DesktopPressable(
|
||||
{ style, children, onHoverIn, onHoverOut, disabled, ...props },
|
||||
ref,
|
||||
) {
|
||||
const [pointerInside, setPointerInside] = useState(false);
|
||||
const hovered = pointerInside && !disabled;
|
||||
const handleHoverIn = useCallback(event => {
|
||||
setPointerInside(true);
|
||||
onHoverIn?.(event);
|
||||
}, [onHoverIn]);
|
||||
const handleHoverOut = useCallback(event => {
|
||||
setPointerInside(false);
|
||||
onHoverOut?.(event);
|
||||
}, [onHoverOut]);
|
||||
const renderStyle = useCallback(state => style({ ...state, hovered }), [style, hovered]);
|
||||
const renderChildren = useCallback(state => children({ ...state, hovered }), [children, hovered]);
|
||||
return (
|
||||
<NativePressable
|
||||
{...props}
|
||||
ref={ref}
|
||||
disabled={disabled}
|
||||
onHoverIn={handleHoverIn}
|
||||
onHoverOut={handleHoverOut}
|
||||
style={typeof style === 'function' ? renderStyle : style}
|
||||
>
|
||||
{typeof children === 'function' ? renderChildren : children}
|
||||
</NativePressable>
|
||||
);
|
||||
});
|
||||
|
||||
export default memo(Pressable);
|
||||
Reference in New Issue
Block a user