diff --git a/change/@office-iss-react-native-win32-f99cfd3f-e8be-4146-8f14-07aca4f2c5b1.json b/change/@office-iss-react-native-win32-f99cfd3f-e8be-4146-8f14-07aca4f2c5b1.json new file mode 100644 index 00000000000..42f931c6ced --- /dev/null +++ b/change/@office-iss-react-native-win32-f99cfd3f-e8be-4146-8f14-07aca4f2c5b1.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "adds default Modal", + "packageName": "@office-iss/react-native-win32", + "email": "tatianakapos@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@react-native-windows-codegen-132ed636-192e-42e3-808f-a259a253bb75.json b/change/@react-native-windows-codegen-132ed636-192e-42e3-808f-a259a253bb75.json new file mode 100644 index 00000000000..84b605655fc --- /dev/null +++ b/change/@react-native-windows-codegen-132ed636-192e-42e3-808f-a259a253bb75.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Implement accessibilityState checked", + "packageName": "@react-native-windows/codegen", + "email": "34109996+chiaramooney@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/@react-native-windows-codegen-4ec7c1b2-8541-47d9-a5d0-c245b25fbac1.json b/change/@react-native-windows-codegen-4ec7c1b2-8541-47d9-a5d0-c245b25fbac1.json new file mode 100644 index 00000000000..c72803a4b26 --- /dev/null +++ b/change/@react-native-windows-codegen-4ec7c1b2-8541-47d9-a5d0-c245b25fbac1.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Fix issue with prop cloning with custom native props", + "packageName": "@react-native-windows/codegen", + "email": "53619745+rnbot@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/react-native-windows-0f3d0823-4941-4a2d-95bd-ffa2a401aa53.json b/change/react-native-windows-0f3d0823-4941-4a2d-95bd-ffa2a401aa53.json new file mode 100644 index 00000000000..312657f9106 --- /dev/null +++ b/change/react-native-windows-0f3d0823-4941-4a2d-95bd-ffa2a401aa53.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Update Provider Guardrails", + "packageName": "react-native-windows", + "email": "34109996+chiaramooney@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/react-native-windows-18d4918c-6f73-4fe4-a6e4-42f5ac6ac39e.json b/change/react-native-windows-18d4918c-6f73-4fe4-a6e4-42f5ac6ac39e.json new file mode 100644 index 00000000000..ca924199f94 --- /dev/null +++ b/change/react-native-windows-18d4918c-6f73-4fe4-a6e4-42f5ac6ac39e.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Implement accessibilityState checked", + "packageName": "react-native-windows", + "email": "34109996+chiaramooney@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/react-native-windows-283ad80a-9f48-40a3-b7a3-227795b6f110.json b/change/react-native-windows-283ad80a-9f48-40a3-b7a3-227795b6f110.json new file mode 100644 index 00000000000..78812308f9a --- /dev/null +++ b/change/react-native-windows-283ad80a-9f48-40a3-b7a3-227795b6f110.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "adds default modal that hosts fabric components", + "packageName": "react-native-windows", + "email": "tatianakapos@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/react-native-windows-2b4a6ca8-3a50-4be5-8b1d-559946fa0be1.json b/change/react-native-windows-2b4a6ca8-3a50-4be5-8b1d-559946fa0be1.json new file mode 100644 index 00000000000..473857208aa --- /dev/null +++ b/change/react-native-windows-2b4a6ca8-3a50-4be5-8b1d-559946fa0be1.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Cleanup ReactNativeAppBuilder and ReactNativeWin32App", + "packageName": "react-native-windows", + "email": "jthysell@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/react-native-windows-433ea974-44c7-465f-a6e9-1f34927c7217.json b/change/react-native-windows-433ea974-44c7-465f-a6e9-1f34927c7217.json new file mode 100644 index 00000000000..6e543a6fe00 --- /dev/null +++ b/change/react-native-windows-433ea974-44c7-465f-a6e9-1f34927c7217.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Focus should notify island host when tab loop wraps to give host oportunity to take focus", + "packageName": "react-native-windows", + "email": "30809111+acoates-ms@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/react-native-windows-621a9112-d00c-469f-83a8-ae391abefe18.json b/change/react-native-windows-621a9112-d00c-469f-83a8-ae391abefe18.json new file mode 100644 index 00000000000..321b102eecd --- /dev/null +++ b/change/react-native-windows-621a9112-d00c-469f-83a8-ae391abefe18.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Fix issue with prop cloning with custom native props", + "packageName": "react-native-windows", + "email": "53619745+rnbot@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/react-native-windows-6b83103d-e55d-427a-a482-d2706e24f6a0.json b/change/react-native-windows-6b83103d-e55d-427a-a482-d2706e24f6a0.json new file mode 100644 index 00000000000..c9dc36b776d --- /dev/null +++ b/change/react-native-windows-6b83103d-e55d-427a-a482-d2706e24f6a0.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Default scroll to bring a component into view should have padding around the viewport", + "packageName": "react-native-windows", + "email": "30809111+acoates-ms@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/react-native-windows-7edd982f-196f-4feb-a824-cbf08e0a2c96.json b/change/react-native-windows-7edd982f-196f-4feb-a824-cbf08e0a2c96.json new file mode 100644 index 00000000000..c7efa05de92 --- /dev/null +++ b/change/react-native-windows-7edd982f-196f-4feb-a824-cbf08e0a2c96.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Add very basic box-shadow support", + "packageName": "react-native-windows", + "email": "30809111+acoates-ms@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/react-native-windows-aa22348e-c6b1-4cfc-9a85-c8b2bc2df484.json b/change/react-native-windows-aa22348e-c6b1-4cfc-9a85-c8b2bc2df484.json new file mode 100644 index 00000000000..9a42a1c276e --- /dev/null +++ b/change/react-native-windows-aa22348e-c6b1-4cfc-9a85-c8b2bc2df484.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Introduced autocapitalize prop in TextInput", + "packageName": "react-native-windows", + "email": "14967941+danielayala94@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/react-native-windows-ce27643e-687f-4f35-815b-4e030e9cc358.json b/change/react-native-windows-ce27643e-687f-4f35-815b-4e030e9cc358.json new file mode 100644 index 00000000000..76fed193842 --- /dev/null +++ b/change/react-native-windows-ce27643e-687f-4f35-815b-4e030e9cc358.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Update focus visuals to use cornerRadius and inner/outer strokes.", + "packageName": "react-native-windows", + "email": "30809111+acoates-ms@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/packages/@office-iss/react-native-win32-tester/src/js/utils/RNTesterList.win32.js b/packages/@office-iss/react-native-win32-tester/src/js/utils/RNTesterList.win32.js index f3c6dd9890b..09d21eee259 100644 --- a/packages/@office-iss/react-native-win32-tester/src/js/utils/RNTesterList.win32.js +++ b/packages/@office-iss/react-native-win32-tester/src/js/utils/RNTesterList.win32.js @@ -59,6 +59,11 @@ const Components: Array = [ key: 'ImageWin32Test', module: require('@office-iss/react-native-win32/Libraries/Image/Tests/ImageWin32Test'), }, + { + key: 'ModalExample', + category: 'UI', + module: require('../examples/Modal/ModalExample'), + }, /* { key: 'JSResponderHandlerExample', @@ -68,11 +73,6 @@ const Components: Array = [ key: 'KeyboardAvoidingViewExample', module: require('../examples/KeyboardAvoidingView/KeyboardAvoidingViewExample'), }, - { - key: 'ModalExample', - category: 'UI', - module: require('../examples/Modal/ModalExample'), - }, { key: 'NewAppScreenExample', module: require('../examples/NewAppScreen/NewAppScreenExample'), diff --git a/packages/@react-native-windows/codegen/src/generators/GenerateComponentWindows.ts b/packages/@react-native-windows/codegen/src/generators/GenerateComponentWindows.ts index 2fdee7dbd2c..803024dbeeb 100644 --- a/packages/@react-native-windows/codegen/src/generators/GenerateComponentWindows.ts +++ b/packages/@react-native-windows/codegen/src/generators/GenerateComponentWindows.ts @@ -38,7 +38,14 @@ const headerTemplate = `/* const propsTemplate = `REACT_STRUCT(::_PROPS_NAME_::) struct ::_PROPS_NAME_:: : winrt::implements<::_PROPS_NAME_::, winrt::Microsoft::ReactNative::IComponentProps> { - ::_PROPS_NAME_::(winrt::Microsoft::ReactNative::ViewProps props) : ViewProps(props) {} + ::_PROPS_NAME_::(winrt::Microsoft::ReactNative::ViewProps props, const winrt::Microsoft::ReactNative::IComponentProps& cloneFrom) + : ViewProps(props) + { + if (cloneFrom) { + auto cloneFromProps = cloneFrom.as<::_PROPS_NAME_::>(); +::_PROP_INITIALIZERS_:: + } + } void SetProp(uint32_t hash, winrt::hstring propName, winrt::Microsoft::ReactNative::IJSValueReader value) noexcept { winrt::Microsoft::ReactNative::ReadProp(hash, propName, value, *this); @@ -144,8 +151,10 @@ void Register::_COMPONENT_NAME_::NativeComponent( L"::_COMPONENT_NAME_::", [builderCallback](winrt::Microsoft::ReactNative::IReactViewComponentBuilder const &builder) noexcept { auto compBuilder = builder.as(); - builder.SetCreateProps( - [](winrt::Microsoft::ReactNative::ViewProps props) noexcept { return winrt::make<::_COMPONENT_NAME_::Props>(props); }); + builder.SetCreateProps([](winrt::Microsoft::ReactNative::ViewProps props, + const winrt::Microsoft::ReactNative::IComponentProps& cloneFrom) noexcept { + return winrt::make<::_COMPONENT_NAME_::Props>(props, cloneFrom); + }); builder.SetUpdatePropsHandler([](const winrt::Microsoft::ReactNative::ComponentView &view, const winrt::Microsoft::ReactNative::IComponentProps &newProps, @@ -294,6 +303,12 @@ export function createComponentGenerator({ }) .join('\n'); + const propInitializers = componentShape.props + .map(prop => { + return ` ${prop.name} = cloneFromProps->${prop.name};`; + }) + .join('\n'); + const propObjectTypes = propObjectAliases.jobs .map(propObjectTypeName => { const propObjectType = propObjectAliases.types[propObjectTypeName]!; @@ -508,6 +523,7 @@ ${ .replace(/::_EVENT_EMITTER_NAME_::/g, eventEmitterName) .replace(/::_PROPS_NAME_::/g, propsName) .replace(/::_COMPONENT_NAME_::/g, componentName) + .replace(/::_PROP_INITIALIZERS_::/g, propInitializers) .replace(/::_PROPS_FIELDS_::/g, propsFields) .replace(/::_NAMESPACE_::/g, namespace) .replace(/\n\n\n+/g, '\n\n'); diff --git a/packages/@react-native-windows/tester/overrides.json b/packages/@react-native-windows/tester/overrides.json index 3450be826ce..a30b7f1e39f 100644 --- a/packages/@react-native-windows/tester/overrides.json +++ b/packages/@react-native-windows/tester/overrides.json @@ -27,6 +27,18 @@ "type": "platform", "file": "src/js/examples/HTTP/HTTPExampleMultiPartFormData.js" }, + { + "type": "patch", + "file": "src/js/examples/Modal/ModalOnShow.windows.js", + "baseFile": "packages/rn-tester/js/examples/Modal/ModalOnShow.js", + "baseHash": "5098723f16d232ef3c5971a6f153522f42f87f61" + }, + { + "type": "patch", + "file": "src/js/examples/Modal/ModalPresentation.windows.js", + "baseFile": "packages/rn-tester/js/examples/Modal/ModalPresentation.js", + "baseHash": "97e0408dad28bd62e6ec22a8454af764d69d8ed5" + }, { "type": "patch", "file": "src/js/examples/Pressable/PressableExample.windows.js", diff --git a/packages/@react-native-windows/tester/src/js/examples/Modal/ModalOnShow.windows.js b/packages/@react-native-windows/tester/src/js/examples/Modal/ModalOnShow.windows.js new file mode 100644 index 00000000000..e4e2468c967 --- /dev/null +++ b/packages/@react-native-windows/tester/src/js/examples/Modal/ModalOnShow.windows.js @@ -0,0 +1,149 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; + +import * as React from 'react'; +import {Modal, Pressable, StyleSheet, Text, View} from 'react-native'; + +function ModalOnShowOnDismiss(): React.Node { + const [modalShowComponent, setModalShowComponent] = React.useState(true); + const [modalVisible, setModalVisible] = React.useState(false); + const [onShowCount, setOnShowCount] = React.useState(0); + const [onDismissCount, setOnDismissCount] = React.useState(0); + + return ( + + {modalShowComponent && ( + { + setOnShowCount(onShowCount + 1); + }} + onDismiss={() => { + setOnDismissCount(onDismissCount + 1); + }} + onRequestClose={() => { + setModalVisible(false); + }}> + + + + onShow is called {onShowCount} times + + + onDismiss is called {onDismissCount} times + + setModalVisible(false)}> + + Hide modal by setting visible to false + + + setModalShowComponent(false)}> + + Hide modal by removing component + + + + + + )} + onShow is called {onShowCount} times + + onDismiss is called {onDismissCount} times + + { + setModalShowComponent(true); + setModalVisible(true); + }}> + + Show Modal + + + + ); +} + +const styles = StyleSheet.create({ + container: { + display: 'flex', + alignItems: 'center', + paddingVertical: 30, + }, + centeredView: { + // flex: 1, [Windows] + justifyContent: 'center', + alignItems: 'center', + }, + modalBackdrop: { + backgroundColor: 'rgba(0, 0, 0, 0.5)', + }, + modalView: { + margin: 20, + backgroundColor: 'white', + borderRadius: 20, + padding: 35, + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.25, + shadowRadius: 4, + elevation: 5, + }, + button: { + borderRadius: 20, + padding: 10, + marginVertical: 20, + elevation: 2, + }, + buttonOpen: { + backgroundColor: '#F194FF', + }, + buttonClose: { + backgroundColor: '#2196F3', + }, + textStyle: { + color: 'white', + fontWeight: 'bold', + textAlign: 'center', + }, + // [Windows + widthHeight: { + width: 300, + height: 400, + }, + // Windows] +}); + +export default ({ + title: "Modal's onShow/onDismiss", + name: 'onShow', + description: + 'onShow and onDismiss (iOS only) callbacks are called when a modal is shown/dismissed', + render: (): React.Node => , +}: RNTesterModuleExample); diff --git a/packages/@react-native-windows/tester/src/js/examples/Modal/ModalPresentation.windows.js b/packages/@react-native-windows/tester/src/js/examples/Modal/ModalPresentation.windows.js new file mode 100644 index 00000000000..4ed2de275a8 --- /dev/null +++ b/packages/@react-native-windows/tester/src/js/examples/Modal/ModalPresentation.windows.js @@ -0,0 +1,321 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +/* eslint-disable no-alert */ + +import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; +import type {Props as ModalProps} from 'react-native/Libraries/Modal/Modal'; + +import RNTOption from '../../components/RNTOption'; +import * as React from 'react'; +import {Modal, Platform, StyleSheet, Switch, Text, View} from 'react-native'; + +const RNTesterButton = require('../../components/RNTesterButton'); + +const animationTypes = ['slide', 'none', 'fade']; +const presentationStyles = [ + 'fullScreen', + 'pageSheet', + 'formSheet', + 'overFullScreen', +]; +const supportedOrientations = [ + 'portrait', + 'portrait-upside-down', + 'landscape', + 'landscape-left', + 'landscape-right', +]; + +function ModalPresentation() { + const onDismiss = React.useCallback(() => { + alert('onDismiss'); + }, []); + + const onShow = React.useCallback(() => { + alert('onShow'); + }, []); + + const onRequestClose = React.useCallback(() => { + console.log('onRequestClose'); + }, []); + + const [props, setProps] = React.useState({ + animationType: 'none', + transparent: false, + hardwareAccelerated: false, + statusBarTranslucent: false, + presentationStyle: Platform.select({ + ios: 'fullScreen', + default: undefined, + }), + supportedOrientations: Platform.select({ + ios: ['portrait'], + default: undefined, + }), + onDismiss: undefined, + onShow: undefined, + visible: false, + }); + const presentationStyle = props.presentationStyle; + const hardwareAccelerated = props.hardwareAccelerated; + const statusBarTranslucent = props.statusBarTranslucent; + + const [currentOrientation, setCurrentOrientation] = React.useState('unknown'); + + /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's + * LTI update could not be added via codemod */ + const onOrientationChange = event => + setCurrentOrientation(event.nativeEvent.orientation); + + const controls = ( + <> + + Status Bar Translucent 🟢 + + setProps(prev => ({...prev, statusBarTranslucent: enabled})) + } + /> + + + Hardware Acceleration 🟢 + + setProps(prev => ({ + ...prev, + hardwareAccelerated: enabled, + })) + } + /> + + + Presentation Style ⚫️ + + {presentationStyles.map(type => ( + + setProps(prev => { + if (type === 'overFullScreen' && prev.transparent === true) { + return { + ...prev, + presentationStyle: type, + transparent: false, + }; + } + return { + ...prev, + presentationStyle: + type === prev.presentationStyle ? undefined : type, + }; + }) + } + selected={type === presentationStyle} + /> + ))} + + + + + Transparent + + setProps(prev => ({...prev, transparent: enabled})) + } + /> + + {Platform.OS === 'ios' && presentationStyle !== 'overFullScreen' ? ( + + iOS Modal can only be transparent with 'overFullScreen' Presentation + Style + + ) : null} + + + Supported Orientation ⚫️ + + {supportedOrientations.map(orientation => ( + + setProps(prev => { + if (prev.supportedOrientations?.includes(orientation)) { + return { + ...prev, + supportedOrientations: prev.supportedOrientations?.filter( + o => o !== orientation, + ), + }; + } + return { + ...prev, + supportedOrientations: [ + ...(prev.supportedOrientations ?? []), + orientation, + ], + }; + }) + } + selected={props.supportedOrientations?.includes(orientation)} + /> + ))} + + + + Actions + + + setProps(prev => ({ + ...prev, + onShow: prev.onShow ? undefined : onShow, + })) + } + selected={!!props.onShow} + /> + + setProps(prev => ({ + ...prev, + onDismiss: prev.onDismiss ? undefined : onDismiss, + })) + } + selected={!!props.onDismiss} + /> + + + + ); + + return ( + + setProps(prev => ({...prev, visible: true}))}> + Show Modal + + + + + + This modal was presented with animationType: ' + {props.animationType}' + + {Platform.OS === 'ios' ? ( + + It is currently displayed in {currentOrientation} mode. + + ) : null} + setProps(prev => ({...prev, visible: false}))}> + Close + + {controls} + + + + + Animation Type + + {animationTypes.map(type => ( + setProps(prev => ({...prev, animationType: type}))} + selected={type === props.animationType} + /> + ))} + + + {controls} + + ); +} + +const styles = StyleSheet.create({ + row: { + flexWrap: 'wrap', + flexDirection: 'row', + }, + rowWithSpaceBetween: { + flexDirection: 'row', + justifyContent: 'space-between', + }, + block: { + borderColor: 'rgba(0,0,0, 0.1)', + borderBottomWidth: 1, + padding: 6, + }, + inlineBlock: { + padding: 6, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + borderColor: 'rgba(0,0,0, 0.1)', + borderBottomWidth: 1, + }, + title: { + margin: 3, + fontWeight: 'bold', + }, + option: { + marginRight: 8, + marginTop: 6, + }, + modalContainer: { + // [Windows + width: 500, + height: 500, + // flex: 1, + // justifyContent: 'center', + // padding: 20, + // Windows ] + }, + modalInnerContainer: { + borderRadius: 10, + backgroundColor: '#fff', + padding: 10, + }, + warning: { + margin: 3, + fontSize: 12, + color: 'red', + }, +}); + +export default ({ + title: 'Modal Presentation', + name: 'basic', + description: 'Modals can be presented with or without animation', + render: (): React.Node => , +}: RNTesterModuleExample); diff --git a/packages/@react-native-windows/tester/src/js/examples/View/ViewExample.windows.js b/packages/@react-native-windows/tester/src/js/examples/View/ViewExample.windows.js index 955e6accef2..0b80648f011 100644 --- a/packages/@react-native-windows/tester/src/js/examples/View/ViewExample.windows.js +++ b/packages/@react-native-windows/tester/src/js/examples/View/ViewExample.windows.js @@ -477,7 +477,11 @@ class AccessibilityExample extends React.Component< {name: 'expand', label: 'expand'}, {name: 'collapse', label: 'collapse'}, ]} - accessibilityState={{expanded: this.state.expanded, busy: true}} + accessibilityState={{ + expanded: this.state.expanded, + busy: true, + checked: true, + }} accessibilityPosInSet={1} accessibilitySetSize={1} accessibilityLiveRegion="polite" diff --git a/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js b/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js index 532fa24d7de..5f70508d4b4 100644 --- a/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js +++ b/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js @@ -63,11 +63,11 @@ const Components: Array = [ category: 'UI', module: require('../examples-win/Glyph/GlyphExample'), }, - // { - // key: 'ModalExample', - // category: 'UI', - // module: require('../examples/Modal/ModalExample'), - // }, + { + key: 'ModalExample', + category: 'UI', + module: require('../examples/Modal/ModalExample'), + }, { key: 'Native Component', module: require('../examples-win/NativeComponents/NativeComponent'), diff --git a/packages/e2e-test-app-fabric/test/TextInputComponentTest.test.ts b/packages/e2e-test-app-fabric/test/TextInputComponentTest.test.ts index 1feb55c9102..c5c38c61b7b 100644 --- a/packages/e2e-test-app-fabric/test/TextInputComponentTest.test.ts +++ b/packages/e2e-test-app-fabric/test/TextInputComponentTest.test.ts @@ -96,10 +96,11 @@ describe('TextInput Tests', () => { await component.waitForDisplayed({timeout: 5000}); const dump = await dumpVisualTree('capitalize-none'); expect(dump).toMatchSnapshot(); + await app.waitUntil( async () => { - await component.setValue('Hello World'); - return (await component.getText()) === 'Hello World'; + await component.setValue('hello world'); + return (await component.getText()) === 'hello world'; }, { interval: 1500, @@ -107,28 +108,76 @@ describe('TextInput Tests', () => { timeoutMsg: `Unable to enter correct text.`, }, ); - expect(await component.getText()).toBe('Hello World'); + expect(await component.getText()).toBe('hello world'); }); - test('TextInputs can autocapitalize: Autocapitalize Sentences', async () => { + // Comment out once the sentences mode has been implemented. + /*test('TextInputs can autocapitalize: Autocapitalize Sentences', async () => { const component = await app.findElementByTestID('capitalize-sentences'); await component.waitForDisplayed({timeout: 5000}); const dump = await dumpVisualTree('capitalize-sentences'); expect(dump).toMatchSnapshot(); - // Behavior not supported yet. - }); - test('TextInputs can autocapitalize: Autocapitalize Words', async () => { + + // Test behavior when text is set from JS. + // These TextInputs are currently empty. Setting defaultValue prop for them in TextInputSharedExamples.js + // leads to an "override error". This file is expected to be a exact copy of its RN Core parent. + expect(await component.getText()).toBe('initial text is not capitalized'); + + await app.waitUntil( + async () => { + await component.setValue('hey here is a sentence. one more sentence? yeah one more sentence! and a last one.'); + return (await component.getText()) === 'Hey here is a sentence. One more sentence? Yeah one more sentence! And a last one.'; + }, + { + interval: 1500, + timeout: 5000, + timeoutMsg: `Unable to enter correct text.`, + }); + });*/ + // Comment out once the words mode has been implemented. + /*test('TextInputs can autocapitalize: Autocapitalize Words', async () => { const component = await app.findElementByTestID('capitalize-words'); await component.waitForDisplayed({timeout: 5000}); const dump = await dumpVisualTree('capitalize-words'); expect(dump).toMatchSnapshot(); - // Behavior not supported yet. - }); + + // Test behavior when text is set from JS. + // These TextInputs are currently empty. Setting defaultValue prop for them in TextInputSharedExamples.js + // leads to an "override error". This file is expected to be a exact copy of its RN Core parent. + expect(await component.getText()).toBe('initial text is not capitalized'); + + await app.waitUntil( + async () => { + await component.setValue('hi i am autocapitalizing all words.'); + return (await component.getText()) === 'Hi I Am Autocapitalizing All Words.'; + }, + { + interval: 1500, + timeout: 5000, + timeoutMsg: `Unable to enter correct text.`, + }); + });*/ test('TextInputs can autocapitalize: Autocapitalize Characters', async () => { const component = await app.findElementByTestID('capitalize-characters'); await component.waitForDisplayed({timeout: 5000}); const dump = await dumpVisualTree('capitalize-characters'); expect(dump).toMatchSnapshot(); - // Behavior not supported yet. + + await app.waitUntil( + async () => { + await component.setValue( + 'hi i am setting up this whole UPPERCASE sentence.', + ); + return ( + (await component.getText()) === + 'HI I AM SETTING UP THIS WHOLE UPPERCASE SENTENCE.' + ); + }, + { + interval: 1500, + timeout: 5000, + timeoutMsg: `Unable to enter correct text.`, + }, + ); }); test('TextInputs can have attributed text', async () => { const component = await app.findElementByTestID('text-input'); diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/HomeUIADump.test.ts.snap b/packages/e2e-test-app-fabric/test/__snapshots__/HomeUIADump.test.ts.snap index d91de9186e5..bb20fd84d8f 100644 --- a/packages/e2e-test-app-fabric/test/__snapshots__/HomeUIADump.test.ts.snap +++ b/packages/e2e-test-app-fabric/test/__snapshots__/HomeUIADump.test.ts.snap @@ -3274,12 +3274,12 @@ exports[`Home UIA Tree Dump Fabric Native Component 1`] = ` }, { "Offset": "15, 69, 0", - "Size": "65, 19", + "Size": "65, 20", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "65, 19", + "Size": "65, 20", "Visual Type": "SpriteVisual", }, { @@ -3291,12 +3291,12 @@ exports[`Home UIA Tree Dump Fabric Native Component 1`] = ` }, { "Offset": "853, 69, 0", - "Size": "22, 19", + "Size": "22, 20", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "22, 19", + "Size": "22, 20", "Visual Type": "SpriteVisual", }, { @@ -3308,12 +3308,12 @@ exports[`Home UIA Tree Dump Fabric Native Component 1`] = ` }, { "Offset": "901, 69, 0", - "Size": "52, 19", + "Size": "52, 20", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "52, 19", + "Size": "52, 20", "Visual Type": "SpriteVisual", }, { @@ -3410,12 +3410,12 @@ exports[`Home UIA Tree Dump Fabric Native Component Yoga 1`] = ` "__Children": [ { "Offset": "15, 12, 0", - "Size": "273, 28", + "Size": "273, 27", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "273, 28", + "Size": "273, 27", "Visual Type": "SpriteVisual", }, { @@ -4974,12 +4974,12 @@ exports[`Home UIA Tree Dump Keyboard extension Example 1`] = ` }, { "Offset": "15, 69, 0", - "Size": "65, 19", + "Size": "65, 20", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "65, 19", + "Size": "65, 20", "Visual Type": "SpriteVisual", }, { @@ -4991,12 +4991,12 @@ exports[`Home UIA Tree Dump Keyboard extension Example 1`] = ` }, { "Offset": "853, 69, 0", - "Size": "22, 19", + "Size": "22, 20", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "22, 19", + "Size": "22, 20", "Visual Type": "SpriteVisual", }, { @@ -5008,12 +5008,12 @@ exports[`Home UIA Tree Dump Keyboard extension Example 1`] = ` }, { "Offset": "901, 69, 0", - "Size": "52, 19", + "Size": "52, 20", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "52, 19", + "Size": "52, 20", "Visual Type": "SpriteVisual", }, { @@ -5615,7 +5615,7 @@ exports[`Home UIA Tree Dump LegacyControlStyleTest 1`] = ` }, "Comment": "LegacyControlStyleTest", "Offset": "0, 0, 0", - "Size": "968, 99", + "Size": "968, 100", "Visual Type": "SpriteVisual", "__Children": [ { @@ -5824,12 +5824,12 @@ exports[`Home UIA Tree Dump LegacyImageTest 1`] = ` }, { "Offset": "15, 69, 0", - "Size": "65, 19", + "Size": "65, 20", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "65, 19", + "Size": "65, 20", "Visual Type": "SpriteVisual", }, { @@ -5841,12 +5841,12 @@ exports[`Home UIA Tree Dump LegacyImageTest 1`] = ` }, { "Offset": "853, 69, 0", - "Size": "22, 19", + "Size": "22, 20", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "22, 19", + "Size": "22, 20", "Visual Type": "SpriteVisual", }, { @@ -5858,12 +5858,12 @@ exports[`Home UIA Tree Dump LegacyImageTest 1`] = ` }, { "Offset": "901, 69, 0", - "Size": "52, 19", + "Size": "52, 20", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "52, 19", + "Size": "52, 20", "Visual Type": "SpriteVisual", }, { @@ -6130,12 +6130,12 @@ exports[`Home UIA Tree Dump LegacySelectableTextTest 1`] = ` "__Children": [ { "Offset": "15, 12, 0", - "Size": "220, 28", + "Size": "220, 27", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "220, 28", + "Size": "220, 27", "Visual Type": "SpriteVisual", }, { @@ -6295,17 +6295,17 @@ exports[`Home UIA Tree Dump LegacyTextHitTestTest 1`] = ` }, "Comment": "LegacyTextHitTestTest", "Offset": "0, 0, 0", - "Size": "968, 99", + "Size": "968, 100", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "15, 12, 0", - "Size": "190, 27", + "Size": "190, 28", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "190, 27", + "Size": "190, 28", "Visual Type": "SpriteVisual", }, { @@ -6334,12 +6334,12 @@ exports[`Home UIA Tree Dump LegacyTextHitTestTest 1`] = ` }, { "Offset": "15, 69, 0", - "Size": "65, 20", + "Size": "65, 19", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "65, 20", + "Size": "65, 19", "Visual Type": "SpriteVisual", }, { @@ -6351,12 +6351,12 @@ exports[`Home UIA Tree Dump LegacyTextHitTestTest 1`] = ` }, { "Offset": "853, 69, 0", - "Size": "22, 20", + "Size": "22, 19", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "22, 20", + "Size": "22, 19", "Visual Type": "SpriteVisual", }, { @@ -6368,12 +6368,12 @@ exports[`Home UIA Tree Dump LegacyTextHitTestTest 1`] = ` }, { "Offset": "901, 69, 0", - "Size": "52, 20", + "Size": "52, 19", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "52, 20", + "Size": "52, 19", "Visual Type": "SpriteVisual", }, { @@ -6465,17 +6465,17 @@ exports[`Home UIA Tree Dump LegacyTextInputTest 1`] = ` }, "Comment": "LegacyTextInputTest", "Offset": "0, 0, 0", - "Size": "968, 100", + "Size": "968, 99", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "15, 12, 0", - "Size": "177, 27", + "Size": "177, 28", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "177, 27", + "Size": "177, 28", "Visual Type": "SpriteVisual", }, { @@ -6504,12 +6504,12 @@ exports[`Home UIA Tree Dump LegacyTextInputTest 1`] = ` }, { "Offset": "15, 69, 0", - "Size": "65, 20", + "Size": "65, 19", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "65, 20", + "Size": "65, 19", "Visual Type": "SpriteVisual", }, { @@ -6521,12 +6521,12 @@ exports[`Home UIA Tree Dump LegacyTextInputTest 1`] = ` }, { "Offset": "853, 69, 0", - "Size": "22, 20", + "Size": "22, 19", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "22, 20", + "Size": "22, 19", "Visual Type": "SpriteVisual", }, { @@ -6538,12 +6538,12 @@ exports[`Home UIA Tree Dump LegacyTextInputTest 1`] = ` }, { "Offset": "901, 69, 0", - "Size": "52, 20", + "Size": "52, 19", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "52, 20", + "Size": "52, 19", "Visual Type": "SpriteVisual", }, { @@ -6728,6 +6728,176 @@ exports[`Home UIA Tree Dump Linking 1`] = ` } `; +exports[`Home UIA Tree Dump Modal 1`] = ` +{ + "Automation Tree": { + "AutomationId": "Modal", + "ControlType": 50026, + "IsKeyboardFocusable": true, + "LocalizedControlType": "group", + "Name": "Modal Component for presenting modal views.", + "__Children": [ + { + "AutomationId": "", + "ControlType": 50020, + "LocalizedControlType": "text", + "Name": "Modal", + }, + { + "AutomationId": "", + "ControlType": 50020, + "LocalizedControlType": "text", + "Name": "Component for presenting modal views.", + }, + { + "AutomationId": "", + "ControlType": 50020, + "LocalizedControlType": "text", + "Name": "UI", + }, + { + "AutomationId": "", + "ControlType": 50020, + "LocalizedControlType": "text", + "Name": "iOS", + }, + { + "AutomationId": "", + "ControlType": 50020, + "LocalizedControlType": "text", + "Name": "Android", + }, + ], + }, + "Component Tree": { + "Type": "Microsoft.ReactNative.Composition.ViewComponentView", + "_Props": { + "AccessibilityLabel": "Modal Component for presenting modal views.", + "TestId": "Modal", + }, + "__Children": [ + { + "Type": "Microsoft.ReactNative.Composition.ParagraphComponentView", + "_Props": {}, + }, + { + "Type": "Microsoft.ReactNative.Composition.ParagraphComponentView", + "_Props": {}, + }, + { + "Type": "Microsoft.ReactNative.Composition.ParagraphComponentView", + "_Props": {}, + }, + { + "Type": "Microsoft.ReactNative.Composition.ParagraphComponentView", + "_Props": {}, + }, + { + "Type": "Microsoft.ReactNative.Composition.ParagraphComponentView", + "_Props": {}, + }, + ], + }, + "Visual Tree": { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(255, 255, 255, 255)", + }, + "Comment": "Modal", + "Offset": "0, 0, 0", + "Size": "968, 99", + "Visual Type": "SpriteVisual", + "__Children": [ + { + "Offset": "15, 12, 0", + "Size": "57, 28", + "Visual Type": "SpriteVisual", + "__Children": [ + { + "Offset": "0, 0, 0", + "Size": "57, 28", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "0, 0, 0", + "Size": "0, 0", + "Visual Type": "SpriteVisual", + }, + ], + }, + { + "Offset": "15, 43, 0", + "Size": "938, 20", + "Visual Type": "SpriteVisual", + "__Children": [ + { + "Offset": "0, 0, 0", + "Size": "938, 20", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "0, 0, 0", + "Size": "0, 0", + "Visual Type": "SpriteVisual", + }, + ], + }, + { + "Offset": "15, 69, 0", + "Size": "65, 19", + "Visual Type": "SpriteVisual", + "__Children": [ + { + "Offset": "0, 0, 0", + "Size": "65, 19", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "0, 0, 0", + "Size": "0, 0", + "Visual Type": "SpriteVisual", + }, + ], + }, + { + "Offset": "853, 69, 0", + "Size": "22, 19", + "Visual Type": "SpriteVisual", + "__Children": [ + { + "Offset": "0, 0, 0", + "Size": "22, 19", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "0, 0, 0", + "Size": "0, 0", + "Visual Type": "SpriteVisual", + }, + ], + }, + { + "Offset": "901, 69, 0", + "Size": "52, 19", + "Visual Type": "SpriteVisual", + "__Children": [ + { + "Offset": "0, 0, 0", + "Size": "52, 19", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "0, 0, 0", + "Size": "0, 0", + "Visual Type": "SpriteVisual", + }, + ], + }, + ], + }, +} +`; + exports[`Home UIA Tree Dump Mouse Click Events 1`] = ` { "Automation Tree": { @@ -7315,7 +7485,7 @@ exports[`Home UIA Tree Dump New App Screen 1`] = ` }, "Comment": "New App Screen", "Offset": "0, 0, 0", - "Size": "968, 99", + "Size": "968, 100", "Visual Type": "SpriteVisual", "__Children": [ { @@ -7825,7 +7995,7 @@ exports[`Home UIA Tree Dump Performance Comparison Examples 1`] = ` }, "Comment": "Performance Comparison Examples", "Offset": "0, 0, 0", - "Size": "968, 100", + "Size": "968, 99", "Visual Type": "SpriteVisual", "__Children": [ { @@ -8505,17 +8675,17 @@ exports[`Home UIA Tree Dump Pressable 1`] = ` }, "Comment": "Pressable", "Offset": "0, 0, 0", - "Size": "968, 100", + "Size": "968, 99", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "15, 12, 0", - "Size": "83, 27", + "Size": "83, 28", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "83, 27", + "Size": "83, 28", "Visual Type": "SpriteVisual", }, { @@ -8544,12 +8714,12 @@ exports[`Home UIA Tree Dump Pressable 1`] = ` }, { "Offset": "15, 69, 0", - "Size": "65, 20", + "Size": "65, 19", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "65, 20", + "Size": "65, 19", "Visual Type": "SpriteVisual", }, { @@ -8561,12 +8731,12 @@ exports[`Home UIA Tree Dump Pressable 1`] = ` }, { "Offset": "853, 69, 0", - "Size": "22, 20", + "Size": "22, 19", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "22, 20", + "Size": "22, 19", "Visual Type": "SpriteVisual", }, { @@ -8578,12 +8748,12 @@ exports[`Home UIA Tree Dump Pressable 1`] = ` }, { "Offset": "901, 69, 0", - "Size": "52, 20", + "Size": "52, 19", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "52, 20", + "Size": "52, 19", "Visual Type": "SpriteVisual", }, { @@ -8850,12 +9020,12 @@ exports[`Home UIA Tree Dump ScrollView 1`] = ` "__Children": [ { "Offset": "15, 12, 0", - "Size": "91, 28", + "Size": "91, 27", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "91, 28", + "Size": "91, 27", "Visual Type": "SpriteVisual", }, { @@ -9015,7 +9185,7 @@ exports[`Home UIA Tree Dump ScrollViewAnimated 1`] = ` }, "Comment": "ScrollViewAnimated", "Offset": "0, 0, 0", - "Size": "968, 100", + "Size": "968, 99", "Visual Type": "SpriteVisual", "__Children": [ { @@ -9185,17 +9355,17 @@ exports[`Home UIA Tree Dump ScrollViewSimpleExample 1`] = ` }, "Comment": "ScrollViewSimpleExample", "Offset": "0, 0, 0", - "Size": "968, 99", + "Size": "968, 100", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "15, 12, 0", - "Size": "224, 27", + "Size": "224, 28", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "224, 27", + "Size": "224, 28", "Visual Type": "SpriteVisual", }, { @@ -9224,12 +9394,12 @@ exports[`Home UIA Tree Dump ScrollViewSimpleExample 1`] = ` }, { "Offset": "15, 69, 0", - "Size": "65, 20", + "Size": "65, 19", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "65, 20", + "Size": "65, 19", "Visual Type": "SpriteVisual", }, { @@ -9241,12 +9411,12 @@ exports[`Home UIA Tree Dump ScrollViewSimpleExample 1`] = ` }, { "Offset": "853, 69, 0", - "Size": "22, 20", + "Size": "22, 19", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "22, 20", + "Size": "22, 19", "Visual Type": "SpriteVisual", }, { @@ -9258,12 +9428,12 @@ exports[`Home UIA Tree Dump ScrollViewSimpleExample 1`] = ` }, { "Offset": "901, 69, 0", - "Size": "52, 20", + "Size": "52, 19", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "52, 20", + "Size": "52, 19", "Visual Type": "SpriteVisual", }, { @@ -9504,12 +9674,12 @@ exports[`Home UIA Tree Dump SectionList 1`] = ` }, { "Offset": "15, 69, 0", - "Size": "65, 19", + "Size": "65, 20", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "65, 19", + "Size": "65, 20", "Visual Type": "SpriteVisual", }, { @@ -9521,12 +9691,12 @@ exports[`Home UIA Tree Dump SectionList 1`] = ` }, { "Offset": "853, 69, 0", - "Size": "22, 19", + "Size": "22, 20", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "22, 19", + "Size": "22, 20", "Visual Type": "SpriteVisual", }, { @@ -9538,12 +9708,12 @@ exports[`Home UIA Tree Dump SectionList 1`] = ` }, { "Offset": "901, 69, 0", - "Size": "52, 19", + "Size": "52, 20", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "52, 19", + "Size": "52, 20", "Visual Type": "SpriteVisual", }, { @@ -9810,12 +9980,12 @@ exports[`Home UIA Tree Dump SwipeableCard 1`] = ` "__Children": [ { "Offset": "15, 12, 0", - "Size": "131, 28", + "Size": "131, 27", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "131, 28", + "Size": "131, 27", "Visual Type": "SpriteVisual", }, { @@ -9975,7 +10145,7 @@ exports[`Home UIA Tree Dump Switch 1`] = ` }, "Comment": "Switch", "Offset": "0, 0, 0", - "Size": "968, 99", + "Size": "968, 100", "Visual Type": "SpriteVisual", "__Children": [ { @@ -10145,17 +10315,17 @@ exports[`Home UIA Tree Dump Text 1`] = ` }, "Comment": "Text", "Offset": "0, 0, 0", - "Size": "968, 100", + "Size": "968, 99", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "15, 12, 0", - "Size": "35, 27", + "Size": "35, 28", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "35, 27", + "Size": "35, 28", "Visual Type": "SpriteVisual", }, { @@ -10184,12 +10354,12 @@ exports[`Home UIA Tree Dump Text 1`] = ` }, { "Offset": "15, 69, 0", - "Size": "65, 20", + "Size": "65, 19", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "65, 20", + "Size": "65, 19", "Visual Type": "SpriteVisual", }, { @@ -10201,12 +10371,12 @@ exports[`Home UIA Tree Dump Text 1`] = ` }, { "Offset": "853, 69, 0", - "Size": "22, 20", + "Size": "22, 19", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "22, 20", + "Size": "22, 19", "Visual Type": "SpriteVisual", }, { @@ -10218,12 +10388,12 @@ exports[`Home UIA Tree Dump Text 1`] = ` }, { "Offset": "901, 69, 0", - "Size": "52, 20", + "Size": "52, 19", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "52, 20", + "Size": "52, 19", "Visual Type": "SpriteVisual", }, { @@ -10524,12 +10694,12 @@ exports[`Home UIA Tree Dump TextInputs with key prop 1`] = ` }, { "Offset": "15, 69, 0", - "Size": "65, 19", + "Size": "65, 20", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "65, 19", + "Size": "65, 20", "Visual Type": "SpriteVisual", }, { @@ -10541,12 +10711,12 @@ exports[`Home UIA Tree Dump TextInputs with key prop 1`] = ` }, { "Offset": "853, 69, 0", - "Size": "22, 19", + "Size": "22, 20", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "22, 19", + "Size": "22, 20", "Visual Type": "SpriteVisual", }, { @@ -10558,12 +10728,12 @@ exports[`Home UIA Tree Dump TextInputs with key prop 1`] = ` }, { "Offset": "901, 69, 0", - "Size": "52, 19", + "Size": "52, 20", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "52, 19", + "Size": "52, 20", "Visual Type": "SpriteVisual", }, { @@ -10830,12 +11000,12 @@ exports[`Home UIA Tree Dump Touchable* and onPress 1`] = ` "__Children": [ { "Offset": "15, 12, 0", - "Size": "211, 28", + "Size": "211, 27", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "211, 28", + "Size": "211, 27", "Visual Type": "SpriteVisual", }, { @@ -10995,17 +11165,17 @@ exports[`Home UIA Tree Dump TransferProperties 1`] = ` }, "Comment": "TransferProperties", "Offset": "0, 0, 0", - "Size": "968, 99", + "Size": "968, 100", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "15, 12, 0", - "Size": "159, 27", + "Size": "159, 28", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "159, 27", + "Size": "159, 28", "Visual Type": "SpriteVisual", }, { @@ -11034,12 +11204,12 @@ exports[`Home UIA Tree Dump TransferProperties 1`] = ` }, { "Offset": "15, 69, 0", - "Size": "65, 20", + "Size": "65, 19", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "65, 20", + "Size": "65, 19", "Visual Type": "SpriteVisual", }, { @@ -11051,12 +11221,12 @@ exports[`Home UIA Tree Dump TransferProperties 1`] = ` }, { "Offset": "853, 69, 0", - "Size": "22, 20", + "Size": "22, 19", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "22, 20", + "Size": "22, 19", "Visual Type": "SpriteVisual", }, { @@ -11068,12 +11238,12 @@ exports[`Home UIA Tree Dump TransferProperties 1`] = ` }, { "Offset": "901, 69, 0", - "Size": "52, 20", + "Size": "52, 19", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "52, 20", + "Size": "52, 19", "Visual Type": "SpriteVisual", }, { @@ -11335,7 +11505,7 @@ exports[`Home UIA Tree Dump TransparentHitTestExample 1`] = ` }, "Comment": "TransparentHitTestExample", "Offset": "0, 0, 0", - "Size": "968, 100", + "Size": "968, 99", "Visual Type": "SpriteVisual", "__Children": [ { @@ -11714,12 +11884,12 @@ exports[`Home UIA Tree Dump View 1`] = ` }, { "Offset": "15, 69, 0", - "Size": "65, 19", + "Size": "65, 20", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "65, 19", + "Size": "65, 20", "Visual Type": "SpriteVisual", }, { @@ -11731,12 +11901,12 @@ exports[`Home UIA Tree Dump View 1`] = ` }, { "Offset": "853, 69, 0", - "Size": "22, 19", + "Size": "22, 20", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "22, 19", + "Size": "22, 20", "Visual Type": "SpriteVisual", }, { @@ -11748,12 +11918,12 @@ exports[`Home UIA Tree Dump View 1`] = ` }, { "Offset": "901, 69, 0", - "Size": "52, 19", + "Size": "52, 20", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "52, 19", + "Size": "52, 20", "Visual Type": "SpriteVisual", }, { @@ -12020,12 +12190,12 @@ exports[`Home UIA Tree Dump XAML 1`] = ` "__Children": [ { "Offset": "15, 12, 0", - "Size": "53, 28", + "Size": "53, 27", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "53, 28", + "Size": "53, 27", "Visual Type": "SpriteVisual", }, { diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/TextInputComponentTest.test.ts.snap b/packages/e2e-test-app-fabric/test/__snapshots__/TextInputComponentTest.test.ts.snap index 63a81293046..063014b9942 100644 --- a/packages/e2e-test-app-fabric/test/__snapshots__/TextInputComponentTest.test.ts.snap +++ b/packages/e2e-test-app-fabric/test/__snapshots__/TextInputComponentTest.test.ts.snap @@ -425,82 +425,6 @@ exports[`TextInput Tests TextInputs can autocapitalize: Autocapitalize Character } `; -exports[`TextInput Tests TextInputs can autocapitalize: Autocapitalize Sentences 1`] = ` -{ - "Automation Tree": { - "AutomationId": "capitalize-sentences", - "ControlType": 50004, - "IsKeyboardFocusable": true, - "LocalizedControlType": "edit", - "ValuePattern.IsReadOnly": false, - }, - "Component Tree": { - "Type": "Microsoft.ReactNative.Composition.WindowsTextInputComponentView", - "_Props": { - "TestId": "capitalize-sentences", - }, - }, - "Visual Tree": { - "Comment": "capitalize-sentences", - "Offset": "0, 0, 0", - "Size": "791, 28", - "Visual Type": "SpriteVisual", - "__Children": [ - { - "Offset": "0, 0, 0", - "Size": "1, 1", - "Visual Type": "SpriteVisual", - }, - { - "Offset": "1, 0, 0", - "Size": "-2, 1", - "Visual Type": "SpriteVisual", - }, - { - "Offset": "-1, 0, 0", - "Size": "1, 1", - "Visual Type": "SpriteVisual", - }, - { - "Offset": "-1, 1, 0", - "Size": "1, -2", - "Visual Type": "SpriteVisual", - }, - { - "Offset": "-1, -1, 0", - "Size": "1, 1", - "Visual Type": "SpriteVisual", - }, - { - "Offset": "1, -1, 0", - "Size": "-2, 1", - "Visual Type": "SpriteVisual", - }, - { - "Offset": "0, -1, 0", - "Size": "1, 1", - "Visual Type": "SpriteVisual", - }, - { - "Offset": "0, 1, 0", - "Size": "1, -2", - "Visual Type": "SpriteVisual", - }, - { - "Brush": { - "Brush Type": "ColorBrush", - "Color": "rgba(0, 0, 0, 228)", - }, - "Offset": "0, 0, 0", - "Opacity": 0, - "Size": "0, 0", - "Visual Type": "SpriteVisual", - }, - ], - }, -} -`; - exports[`TextInput Tests TextInputs can autocapitalize: Autocapitalize Turned Off 1`] = ` { "Automation Tree": { @@ -577,82 +501,6 @@ exports[`TextInput Tests TextInputs can autocapitalize: Autocapitalize Turned Of } `; -exports[`TextInput Tests TextInputs can autocapitalize: Autocapitalize Words 1`] = ` -{ - "Automation Tree": { - "AutomationId": "capitalize-words", - "ControlType": 50004, - "IsKeyboardFocusable": true, - "LocalizedControlType": "edit", - "ValuePattern.IsReadOnly": false, - }, - "Component Tree": { - "Type": "Microsoft.ReactNative.Composition.WindowsTextInputComponentView", - "_Props": { - "TestId": "capitalize-words", - }, - }, - "Visual Tree": { - "Comment": "capitalize-words", - "Offset": "0, 0, 0", - "Size": "791, 29", - "Visual Type": "SpriteVisual", - "__Children": [ - { - "Offset": "0, 0, 0", - "Size": "1, 1", - "Visual Type": "SpriteVisual", - }, - { - "Offset": "1, 0, 0", - "Size": "-2, 1", - "Visual Type": "SpriteVisual", - }, - { - "Offset": "-1, 0, 0", - "Size": "1, 1", - "Visual Type": "SpriteVisual", - }, - { - "Offset": "-1, 1, 0", - "Size": "1, -2", - "Visual Type": "SpriteVisual", - }, - { - "Offset": "-1, -1, 0", - "Size": "1, 1", - "Visual Type": "SpriteVisual", - }, - { - "Offset": "1, -1, 0", - "Size": "-2, 1", - "Visual Type": "SpriteVisual", - }, - { - "Offset": "0, -1, 0", - "Size": "1, 1", - "Visual Type": "SpriteVisual", - }, - { - "Offset": "0, 1, 0", - "Size": "1, -2", - "Visual Type": "SpriteVisual", - }, - { - "Brush": { - "Brush Type": "ColorBrush", - "Color": "rgba(0, 0, 0, 228)", - }, - "Offset": "0, 0, 0", - "Opacity": 0, - "Size": "0, 0", - "Visual Type": "SpriteVisual", - }, - ], - }, -} -`; - exports[`TextInput Tests TextInputs can autocomplete, address country 1`] = ` { "Automation Tree": { diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/ViewComponentTest.test.ts.snap b/packages/e2e-test-app-fabric/test/__snapshots__/ViewComponentTest.test.ts.snap index 157746a491f..4770360ef8c 100644 --- a/packages/e2e-test-app-fabric/test/__snapshots__/ViewComponentTest.test.ts.snap +++ b/packages/e2e-test-app-fabric/test/__snapshots__/ViewComponentTest.test.ts.snap @@ -1103,6 +1103,7 @@ exports[`View Tests Views can have customized accessibility 1`] = ` "Name": "A View with accessibility values", "PositionInSet": 1, "SizeofSet": 1, + "TogglePattern.ToggleState": "On", "__Children": [ { "AutomationId": "", diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/snapshotPages.test.js.snap b/packages/e2e-test-app-fabric/test/__snapshots__/snapshotPages.test.js.snap index 1d19235ffe7..e153401a879 100644 --- a/packages/e2e-test-app-fabric/test/__snapshots__/snapshotPages.test.js.snap +++ b/packages/e2e-test-app-fabric/test/__snapshots__/snapshotPages.test.js.snap @@ -30291,6 +30291,1411 @@ exports[`snapshotAllPages Linking 2`] = ` `; +exports[`snapshotAllPages Modal 1`] = ` + + + + Show Modal + + + + + Animation Type + + + + + + slide + + + + + + + none + + + + + + + fade + + + + + + + + Status Bar Translucent 🟢 + + + + + + Hardware Acceleration 🟢 + + + + + + Presentation Style ⚫️ + + + + + + fullScreen + + + + + + + pageSheet + + + + + + + formSheet + + + + + + + overFullScreen + + + + + + + + + Transparent + + + + + + + Supported Orientation ⚫️ + + + + + + portrait + + + + + + + portrait-upside-down + + + + + + + landscape + + + + + + + landscape-left + + + + + + + landscape-right + + + + + + + + Actions + + + + + + onShow + + + + + + + onDismiss ⚫️ + + + + + + +`; + +exports[`snapshotAllPages Modal 2`] = ` + + + onShow is called + 0 + times + + + onDismiss is called + 0 + times + + + + Show Modal + + + +`; + exports[`snapshotAllPages Mouse Click Events 1`] = ` { - CustomXamlComponentProps(winrt::Microsoft::ReactNative::ViewProps props) : m_props(props) {} + CustomXamlComponentProps( + winrt::Microsoft::ReactNative::ViewProps props, + const winrt::Microsoft::ReactNative::IComponentProps &cloneFrom) + : m_props(props) { + if (cloneFrom) { + auto cloneFromProps = cloneFrom.as(); + label = cloneFromProps->label; + } + } void SetProp(uint32_t hash, winrt::hstring propName, winrt::Microsoft::ReactNative::IJSValueReader value) noexcept { winrt::Microsoft::ReactNative::ReadProp(hash, propName, value, *this); @@ -140,8 +148,9 @@ struct CustomComponentUserData : winrt::implements(props); + builder.SetCreateProps([](winrt::Microsoft::ReactNative::ViewProps props, + const winrt::Microsoft::ReactNative::IComponentProps &cloneFrom) noexcept { + return winrt::make(props, cloneFrom); }); builder.SetFinalizeUpdateHandler([](const winrt::Microsoft::ReactNative::ComponentView &source, diff --git a/packages/playground/windows/playground-composition/DrawingIsland.cpp b/packages/playground/windows/playground-composition/DrawingIsland.cpp index d4fd5ffb69f..b3940b7cb1c 100644 --- a/packages/playground/windows/playground-composition/DrawingIsland.cpp +++ b/packages/playground/windows/playground-composition/DrawingIsland.cpp @@ -937,7 +937,10 @@ void DrawingIsland::Window_OnStateChanged(winrt::ContentIslandEnvironment const REACT_STRUCT(DrawingIslandComponentProps) struct DrawingIslandComponentProps : winrt::implements { - DrawingIslandComponentProps(winrt::Microsoft::ReactNative::ViewProps props) : m_props(props) {} + DrawingIslandComponentProps( + winrt::Microsoft::ReactNative::ViewProps props, + const winrt::Microsoft::ReactNative::IComponentProps &) + : m_props(props) {} void SetProp(uint32_t hash, winrt::hstring propName, winrt::Microsoft::ReactNative::IJSValueReader value) noexcept { winrt::Microsoft::ReactNative::ReadProp(hash, propName, value, *this); @@ -953,8 +956,9 @@ void RegisterDrawingIslandComponentView(winrt::Microsoft::ReactNative::IReactPac packageBuilder.as().AddViewComponent( L"CustomXamlComponentWithYogaLayout", [](winrt::Microsoft::ReactNative::IReactViewComponentBuilder const &builder) noexcept { - builder.SetCreateProps([](winrt::Microsoft::ReactNative::ViewProps props) noexcept { - return winrt::make(props); + builder.SetCreateProps([](winrt::Microsoft::ReactNative::ViewProps props, + const winrt::Microsoft::ReactNative::IComponentProps &cloneFrom) noexcept { + return winrt::make(props, cloneFrom); }); auto compBuilder = builder.as(); diff --git a/vnext/Microsoft.ReactNative/CompositionComponentView.idl b/vnext/Microsoft.ReactNative/CompositionComponentView.idl index 9c7bfb098f7..7cfa2740d2a 100644 --- a/vnext/Microsoft.ReactNative/CompositionComponentView.idl +++ b/vnext/Microsoft.ReactNative/CompositionComponentView.idl @@ -22,8 +22,9 @@ namespace Microsoft.ReactNative.Composition NativeBorder = 0x00000001, ShadowProps = 0x00000002, Background = 0x00000004, + FocusVisual = 0x00000008, - Default = 0x00000007, // ShadowProps | NativeBorder | Background + Default = 0x0000000F, // ShadowProps | NativeBorder | Background | FocusVisual }; namespace Experimental { diff --git a/vnext/Microsoft.ReactNative/Fabric/AbiComponentDescriptor.cpp b/vnext/Microsoft.ReactNative/Fabric/AbiComponentDescriptor.cpp index 00d808d7905..6a3b69f4c50 100644 --- a/vnext/Microsoft.ReactNative/Fabric/AbiComponentDescriptor.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/AbiComponentDescriptor.cpp @@ -106,7 +106,10 @@ facebook::react::Props::Shared AbiComponentDescriptor::cloneProps( rawProps); auto userProps = winrt::get_self(m_builder) - ->CreateProps(nullptr); + ->CreateProps( + nullptr, + props ? static_cast(*props).UserProps() + : nullptr); shadowNodeProps->SetUserProps(userProps); rawProps.iterateOverValues( diff --git a/vnext/Microsoft.ReactNative/Fabric/AbiViewComponentDescriptor.cpp b/vnext/Microsoft.ReactNative/Fabric/AbiViewComponentDescriptor.cpp index 1c8ffc99762..f60f841c76a 100644 --- a/vnext/Microsoft.ReactNative/Fabric/AbiViewComponentDescriptor.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/AbiViewComponentDescriptor.cpp @@ -103,7 +103,7 @@ facebook::react::Props::Shared AbiViewComponentDescriptor::cloneProps( winrt::make(shadowNodeProps, false /*holdRef*/); auto userProps = winrt::get_self(m_builder) - ->CreateProps(viewProps); + ->CreateProps(viewProps, props ? static_cast(*props).UserProps() : nullptr); shadowNodeProps->SetUserProps(userProps, viewProps); rawProps.iterateOverValues( diff --git a/vnext/Microsoft.ReactNative/Fabric/AbiViewProps.cpp b/vnext/Microsoft.ReactNative/Fabric/AbiViewProps.cpp index d89b520b357..02263560c72 100644 --- a/vnext/Microsoft.ReactNative/Fabric/AbiViewProps.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/AbiViewProps.cpp @@ -65,6 +65,13 @@ winrt::Microsoft::ReactNative::Composition::Experimental::IBrush Color::AsIntern return winrt::get_self(theme)->Brush(*m_color); } +bool Color::Equals(const winrt::Microsoft::ReactNative::Color &color) const noexcept { + if (!color) { + return false; + } + return m_color == winrt::get_self(color)->m_color; +} + winrt::Microsoft::ReactNative::Color Color::ReadValue( const winrt::Microsoft::ReactNative::IJSValueReader &reader) noexcept { switch (reader.ValueType()) { diff --git a/vnext/Microsoft.ReactNative/Fabric/AbiViewProps.h b/vnext/Microsoft.ReactNative/Fabric/AbiViewProps.h index 6269e7f118c..c5391b6d92c 100644 --- a/vnext/Microsoft.ReactNative/Fabric/AbiViewProps.h +++ b/vnext/Microsoft.ReactNative/Fabric/AbiViewProps.h @@ -50,6 +50,8 @@ struct Color : ColorT { winrt::Microsoft::ReactNative::Composition::Experimental::IBrush AsInternalBrush( const winrt::Microsoft::ReactNative::Composition::Theme theme) noexcept; + bool Equals(const winrt::Microsoft::ReactNative::Color &color) const noexcept; + static winrt::Microsoft::ReactNative::Color ReadValue( const winrt::Microsoft::ReactNative::IJSValueReader &reader) noexcept; static void WriteValue( diff --git a/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp index 0ae2872dc4c..8d5e5b8d008 100644 --- a/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp @@ -73,6 +73,10 @@ void ComponentView::onMounted() noexcept { m_mountedEvent(*this, *this); } +bool ComponentView::isMounted() noexcept { + return m_mounted; +} + winrt::event_token ComponentView::Mounted( winrt::Windows::Foundation::EventHandler const &handler) noexcept { return m_mountedEvent.add(handler); @@ -342,10 +346,13 @@ bool ComponentView::runOnChildren( return true; } } else { - // TODO is this conversion from rend correct? - for (auto it = m_children.end(); it != m_children.begin(); --it) { - if (fn(*winrt::get_self(*it))) - return true; + if (m_children.Size()) { + auto it = m_children.end(); + do { + it--; + if (fn(*winrt::get_self(*it))) + return true; + } while (it != m_children.begin()); } } return false; diff --git a/vnext/Microsoft.ReactNative/Fabric/ComponentView.h b/vnext/Microsoft.ReactNative/Fabric/ComponentView.h index 77ab9f7e408..66fe76f09b2 100644 --- a/vnext/Microsoft.ReactNative/Fabric/ComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/ComponentView.h @@ -37,11 +37,11 @@ struct BringIntoViewOptions { bool AnimationDesired{false}; // NaN will bring the element fully into view aligned to the nearest edge of the viewport float HorizontalAlignmentRatio{std::numeric_limits::quiet_NaN()}; - float HorizontalOffset{0}; + float HorizontalOffset{20}; std::optional TargetRect; // NaN will bring the element fully into view aligned to the nearest edge of the viewport float VerticalAlignmentRatio{std::numeric_limits::quiet_NaN()}; - float VerticalOffset{0}; + float VerticalOffset{20}; }; struct LayoutMetricsChangedArgs : public LayoutMetricsChangedArgsT { @@ -113,6 +113,7 @@ struct ComponentView : public ComponentViewT { virtual void onGotFocus(const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs &args) noexcept; void MarkAsCustomComponent() noexcept; virtual void onMounted() noexcept; + bool isMounted() noexcept; virtual void onUnmounted() noexcept; void onDestroying() noexcept; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/BorderPrimitive.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/BorderPrimitive.cpp new file mode 100644 index 00000000000..1f307ad9bb6 --- /dev/null +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/BorderPrimitive.cpp @@ -0,0 +1,926 @@ + +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "BorderPrimitive.h" + +#include +#include "CompositionViewComponentView.h" + +namespace winrt::Microsoft::ReactNative::Composition::implementation { + +// Ideally isColorMeaningful would be sufficient here. But it appears to detect platformColors as not meaningful +// https://github.com/microsoft/react-native-windows/issues/14006 +bool isColorMeaningful( + const facebook::react::SharedColor &color, + winrt::Microsoft::ReactNative::Composition::implementation::Theme *theme) noexcept { + if (!color) { + return false; + } + + return theme->Color(*color).A > 0; +} + +// We don't want half pixel borders, or border radii - they lead to blurry borders +// Also apply scale factor to the radii at this point +void pixelRoundBorderRadii(facebook::react::BorderRadii &borderRadii, float scaleFactor) noexcept { + // Always round radii down to avoid spikey circles + borderRadii.topLeft = std::floor(borderRadii.topLeft * scaleFactor); + borderRadii.topRight = std::floor(borderRadii.topRight * scaleFactor); + borderRadii.bottomLeft = std::floor(borderRadii.bottomLeft * scaleFactor); + borderRadii.bottomRight = std::floor(borderRadii.bottomRight * scaleFactor); +} + +void scaleAndPixelRoundBorderWidths( + facebook::react::LayoutMetrics const &layoutMetrics, + facebook::react::BorderMetrics &borderMetrics, + float scaleFactor) noexcept { + borderMetrics.borderWidths.left = (borderMetrics.borderWidths.left == 0) + ? 0.f + : std::max(1.f, std::round(borderMetrics.borderWidths.left * scaleFactor)); + borderMetrics.borderWidths.top = (borderMetrics.borderWidths.top == 0) + ? 0.f + : std::max(1.f, std::round(borderMetrics.borderWidths.top * scaleFactor)); + borderMetrics.borderWidths.right = (borderMetrics.borderWidths.right == 0) + ? 0.f + : std::max(1.f, std::round(borderMetrics.borderWidths.right * scaleFactor)); + borderMetrics.borderWidths.bottom = (borderMetrics.borderWidths.bottom == 0) + ? 0.f + : std::max(1.f, std::round(borderMetrics.borderWidths.bottom * scaleFactor)); + + // If we rounded both sides of the borderWidths up, we may have made the borderWidths larger than the total + if (layoutMetrics.frame.size.width * scaleFactor < + (borderMetrics.borderWidths.left + borderMetrics.borderWidths.right)) { + borderMetrics.borderWidths.right--; + } + if (layoutMetrics.frame.size.height * scaleFactor < + (borderMetrics.borderWidths.top + borderMetrics.borderWidths.bottom)) { + borderMetrics.borderWidths.bottom--; + } +} + +// react-native uses black as a default color when none is specified. +void assignDefaultBlackBorders(facebook::react::BorderMetrics &borderMetrics) noexcept { + if (!borderMetrics.borderColors.left) { + borderMetrics.borderColors.left = facebook::react::blackColor(); + } + if (!borderMetrics.borderColors.top) { + borderMetrics.borderColors.top = facebook::react::blackColor(); + } + if (!borderMetrics.borderColors.right) { + borderMetrics.borderColors.right = facebook::react::blackColor(); + } + if (!borderMetrics.borderColors.bottom) { + borderMetrics.borderColors.bottom = facebook::react::blackColor(); + } +} + +facebook::react::BorderMetrics BorderPrimitive::resolveAndAlignBorderMetrics( + facebook::react::LayoutMetrics const &layoutMetrics, + const facebook::react::ViewProps &viewProps) noexcept { + auto borderMetrics = viewProps.resolveBorderMetrics(layoutMetrics); + + pixelRoundBorderRadii(borderMetrics.borderRadii, layoutMetrics.pointScaleFactor); + scaleAndPixelRoundBorderWidths(layoutMetrics, borderMetrics, layoutMetrics.pointScaleFactor); + assignDefaultBlackBorders(borderMetrics); + return borderMetrics; +} + +struct RoundedPathParameters { + float topLeftRadiusX = 0; + float topLeftRadiusY = 0; + float topRightRadiusX = 0; + float topRightRadiusY = 0; + float bottomRightRadiusX = 0; + float bottomRightRadiusY = 0; + float bottomLeftRadiusX = 0; + float bottomLeftRadiusY = 0; +}; + +/* + * Creates and returns a PathGeometry object used to clip the visuals of an element when a BorderRadius is set. + * Can also be used as part of a GeometryGroup for drawing a rounded border / innerstroke when called from + * GetGeometryForRoundedBorder. "params" defines the radii (horizontal and vertical) for each corner (top left, top + * right, bottom right, bottom left). "rectPathGeometry" defines the bounding box of the generated shape. + */ +static winrt::com_ptr GenerateRoundedRectPathGeometry( + winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext, + const RoundedPathParameters ¶ms, + const facebook::react::RectangleEdges &rectPathGeometry) noexcept { + winrt::com_ptr pathGeometry; + winrt::com_ptr spD2dFactory; + compContext.as<::Microsoft::ReactNative::Composition::Experimental::ICompositionContextInterop>()->D2DFactory( + spD2dFactory.put()); + + // Create a path geometry. + HRESULT hr = spD2dFactory->CreatePathGeometry(pathGeometry.put()); + if (FAILED(hr)) { + assert(false); + return nullptr; + } + + // Write to the path geometry using the geometry sink. + winrt::com_ptr spSink = nullptr; + hr = pathGeometry->Open(spSink.put()); + + if (FAILED(hr)) { + assert(false); + return nullptr; + } + + float left = rectPathGeometry.left; + float right = rectPathGeometry.right; + float top = rectPathGeometry.top; + float bottom = rectPathGeometry.bottom; + + // This function uses Cubic Beziers to approximate Arc segments, even though D2D supports arcs. + // This is INTENTIONAL. D2D Arc Segments are eventually converted into cubic beziers, but this + // is done in such a way that we don't have control over how many bezier curve segments are used + // for each arc. We need to ensure that we always use the same number of control points so that + // our paths can be used in a PathKeyFrameAnimation. + // Value for control point scale factor derived from methods described in: + // https://web.archive.org/web/20200322075504/http://itc.ktu.lt/index.php/ITC/article/download/11812/6479 + constexpr float controlPointScaleFactor = 0.44771528244f; // 1 - (4 * (sqrtf(2.0f) - 1) / 3.0f); + +#ifdef DEBUG + // std::sqrtf is not constexpr, so we precalculated this and wrote it in a constexpr form above. + // On debug, we should still check that the values are equivalent, though. + static float calculatedScaleFactor = 1 - (4 * (sqrtf(2.0f) - 1) / 3.0f); + assert(controlPointScaleFactor == calculatedScaleFactor); +#endif // DEBUG + + bool needsConsistentNumberOfControlPoints = true; // VisualVersion::IsUseWinCompClippingRegionEnabled(); + + if (needsConsistentNumberOfControlPoints || (params.topLeftRadiusX != 0.0 && params.topLeftRadiusY != 0.0)) { + spSink->BeginFigure(D2D1::Point2F(left + params.topLeftRadiusX, top), D2D1_FIGURE_BEGIN_FILLED); + } else { + spSink->BeginFigure(D2D1::Point2F(left, top), D2D1_FIGURE_BEGIN_FILLED); + } + + // Move to the top right corner + spSink->AddLine(D2D1::Point2F(right - params.topRightRadiusX, top)); + if (needsConsistentNumberOfControlPoints) { + D2D1_BEZIER_SEGMENT arcSegmentTopRight = { + D2D1::Point2F(right - controlPointScaleFactor * params.topRightRadiusX, top), + D2D1::Point2F(right, top + controlPointScaleFactor * params.topRightRadiusY), + D2D1::Point2F(right, top + params.topRightRadiusY)}; + + spSink->AddBezier(&arcSegmentTopRight); + } else if (params.topRightRadiusX != 0.0 && params.topRightRadiusY != 0.0) { + D2D1_ARC_SEGMENT arcSegmentTopRight = { + D2D1::Point2F(right, top + params.topRightRadiusY), + D2D1::SizeF(params.topRightRadiusX, params.topRightRadiusY), + 0.0f, + D2D1_SWEEP_DIRECTION_CLOCKWISE, + D2D1_ARC_SIZE_SMALL}; + + spSink->AddArc(&arcSegmentTopRight); + } else { + spSink->AddLine(D2D1::Point2F(right, top)); + } + + // Move to the bottom right corner + spSink->AddLine(D2D1::Point2F(right, bottom - params.bottomRightRadiusY)); + if (needsConsistentNumberOfControlPoints) { + D2D1_BEZIER_SEGMENT arcSegmentBottomRight = { + D2D1::Point2F(right, bottom - controlPointScaleFactor * params.bottomRightRadiusY), + D2D1::Point2F(right - controlPointScaleFactor * params.bottomRightRadiusX, bottom), + D2D1::Point2F(right - params.bottomRightRadiusX, bottom)}; + + spSink->AddBezier(&arcSegmentBottomRight); + } else if (params.bottomRightRadiusX != 0.0 && params.bottomRightRadiusY != 0.0) { + D2D1_ARC_SEGMENT arcSegmentBottomRight = { + D2D1::Point2F(right - params.bottomRightRadiusX, bottom), + D2D1::SizeF(params.bottomRightRadiusX, params.bottomRightRadiusY), + 0.0f, + D2D1_SWEEP_DIRECTION_CLOCKWISE, + D2D1_ARC_SIZE_SMALL}; + + spSink->AddArc(&arcSegmentBottomRight); + } else { + spSink->AddLine(D2D1::Point2F(right, bottom)); + } + + // Move to the bottom left corner + spSink->AddLine(D2D1::Point2F(left + params.bottomLeftRadiusX, bottom)); + if (needsConsistentNumberOfControlPoints) { + D2D1_BEZIER_SEGMENT arcSegmentBottomLeft = { + D2D1::Point2F(left + controlPointScaleFactor * params.bottomLeftRadiusX, bottom), + D2D1::Point2F(left, bottom - controlPointScaleFactor * params.bottomLeftRadiusY), + D2D1::Point2F(left, bottom - params.bottomLeftRadiusY)}; + + spSink->AddBezier(&arcSegmentBottomLeft); + } else if (params.bottomLeftRadiusX != 0.0 && params.bottomLeftRadiusY != 0.0) { + D2D1_ARC_SEGMENT arcSegmentBottomLeft = { + D2D1::Point2F(left, bottom - params.bottomLeftRadiusY), + D2D1::SizeF(params.bottomLeftRadiusX, params.bottomLeftRadiusY), + 0.0f, + D2D1_SWEEP_DIRECTION_CLOCKWISE, + D2D1_ARC_SIZE_SMALL}; + + spSink->AddArc(&arcSegmentBottomLeft); + } else { + spSink->AddLine(D2D1::Point2F(left, bottom)); + } + + // Move to the top left corner + spSink->AddLine(D2D1::Point2F(left, top + params.topLeftRadiusY)); + if (needsConsistentNumberOfControlPoints) { + D2D1_BEZIER_SEGMENT arcSegmentTopLeft = { + D2D1::Point2F(left, top + controlPointScaleFactor * params.topLeftRadiusY), + D2D1::Point2F(left + controlPointScaleFactor * params.topLeftRadiusX, top), + D2D1::Point2F(left + params.topLeftRadiusX, top)}; + + spSink->AddBezier(&arcSegmentTopLeft); + } else if (params.topLeftRadiusX != 0.0 && params.topLeftRadiusY != 0.0) { + D2D1_ARC_SEGMENT arcSegmentTopLeft = { + D2D1::Point2F(left + params.topLeftRadiusX, top), + D2D1::SizeF(params.topLeftRadiusX, params.topLeftRadiusY), + 0.0f, + D2D1_SWEEP_DIRECTION_CLOCKWISE, + D2D1_ARC_SIZE_SMALL}; + + spSink->AddArc(&arcSegmentTopLeft); + } else { + spSink->AddLine(D2D1::Point2F(left, top)); + } + + spSink->EndFigure(D2D1_FIGURE_END_CLOSED); + spSink->Close(); + + return pathGeometry; +} + +RoundedPathParameters GenerateRoundedPathParameters( + const facebook::react::RectangleCorners &baseRadius, + const facebook::react::RectangleEdges &inset, + const facebook::react::Size &pathSize) noexcept { + RoundedPathParameters result; + + if (pathSize.width == 0 || pathSize.height == 0) { + return result; + } + + float totalTopRadius = baseRadius.topLeft + baseRadius.topRight; + float totalRightRadius = baseRadius.topRight + baseRadius.bottomRight; + float totalBottomRadius = baseRadius.bottomRight + baseRadius.bottomLeft; + float totalLeftRadius = baseRadius.bottomLeft + baseRadius.topLeft; + + float maxHorizontalRadius = std::max(totalTopRadius, totalBottomRadius); + float maxVerticalRadius = std::max(totalLeftRadius, totalRightRadius); + + double totalWidth = inset.left + inset.right + pathSize.width; + double totalHeight = inset.top + inset.bottom + pathSize.height; + + float scaleHoriz = static_cast(maxHorizontalRadius / totalWidth); + float scaleVert = static_cast(maxVerticalRadius / totalHeight); + + float maxScale = std::max(1.0f, std::max(scaleHoriz, scaleVert)); + + result.topLeftRadiusX = std::max(0.0f, baseRadius.topLeft / maxScale - inset.left); + result.topLeftRadiusY = std::max(0.0f, baseRadius.topLeft / maxScale - inset.top); + result.topRightRadiusX = std::max(0.0f, baseRadius.topRight / maxScale - inset.right); + result.topRightRadiusY = std::max(0.0f, baseRadius.topRight / maxScale - inset.top); + result.bottomRightRadiusX = std::max(0.0f, baseRadius.bottomRight / maxScale - inset.right); + result.bottomRightRadiusY = std::max(0.0f, baseRadius.bottomRight / maxScale - inset.bottom); + result.bottomLeftRadiusX = std::max(0.0f, baseRadius.bottomLeft / maxScale - inset.left); + result.bottomLeftRadiusY = std::max(0.0f, baseRadius.bottomLeft / maxScale - inset.bottom); + + return result; +} + +winrt::com_ptr BorderPrimitive::GenerateRoundedRectPathGeometry( + winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext, + const facebook::react::RectangleCorners &baseRadius, + const facebook::react::RectangleEdges &inset, + const facebook::react::RectangleEdges &rectPathGeometry) noexcept { + RoundedPathParameters params = GenerateRoundedPathParameters( + baseRadius, + inset, + {rectPathGeometry.right - rectPathGeometry.left, rectPathGeometry.bottom - rectPathGeometry.top}); + + return winrt::Microsoft::ReactNative::Composition::implementation::GenerateRoundedRectPathGeometry( + compContext, params, rectPathGeometry); +} + +void DrawShape( + ID2D1RenderTarget *pRT, + const D2D1_RECT_F &rect, + ID2D1Brush *brush, + FLOAT strokeWidth, + ID2D1StrokeStyle *strokeStyle) { + pRT->DrawRectangle(rect, brush, strokeWidth, strokeStyle); +} + +void DrawShape(ID2D1RenderTarget *pRT, ID2D1GeometryGroup &geometry, ID2D1Brush *brush, FLOAT, ID2D1StrokeStyle *) { + pRT->FillGeometry(&geometry, brush); +} + +void DrawShape( + ID2D1RenderTarget *pRT, + ID2D1PathGeometry &geometry, + ID2D1Brush *brush, + FLOAT strokeWidth, + ID2D1StrokeStyle *strokeStyle) { + pRT->DrawGeometry(&geometry, brush, strokeWidth, strokeStyle); +} + +template +void SetBorderLayerPropertiesCommon( + winrt::Microsoft::ReactNative::Composition::implementation::Theme *theme, + winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext, + winrt::Microsoft::ReactNative::Composition::Experimental::ISpriteVisual &layer, + TShape &shape, + winrt::com_ptr<::Microsoft::ReactNative::Composition::Experimental::ICompositionDrawingSurfaceInterop> + &borderTexture, + const D2D1_RECT_F &textureRect, + facebook::react::Point anchorPoint, + facebook::react::Point anchorOffset, + winrt::Windows::Foundation::Numerics::float2 size, + winrt::Windows::Foundation::Numerics::float2 relativeSizeAdjustment, + FLOAT strokeWidth, + const facebook::react::SharedColor &borderColor, + facebook::react::BorderStyle borderStyle) { + layer.Offset({anchorOffset.x, anchorOffset.y, 0}, {anchorPoint.x, anchorPoint.y, 0}); + layer.RelativeSizeWithOffset(size, relativeSizeAdjustment); + layer.as<::Microsoft::ReactNative::Composition::Experimental::IVisualInterop>()->SetClippingPath(nullptr); + + if ((textureRect.right - textureRect.left) <= 0 || (textureRect.bottom - textureRect.top) <= 0) + return; + + auto surface = compContext.CreateDrawingSurfaceBrush( + {(textureRect.right - textureRect.left), (textureRect.bottom - textureRect.top)}, + winrt::Windows::Graphics::DirectX::DirectXPixelFormat::B8G8R8A8UIntNormalized, + winrt::Windows::Graphics::DirectX::DirectXAlphaMode::Premultiplied); + surface.as(borderTexture); + + layer.Brush(surface); + + POINT offset; + ::Microsoft::ReactNative::Composition::AutoDrawDrawingSurface autoDraw( + surface, 1.0f /* We have already done the dpi scaling */, &offset); + if (auto pRT = autoDraw.GetRenderTarget()) { + // Clear with transparency + pRT->Clear(); + + if (!isColorMeaningful(borderColor, theme)) { + return; + } + + winrt::com_ptr spFactory; + pRT->GetFactory(spFactory.put()); + assert(spFactory); + if (spFactory == nullptr) + return; + + winrt::com_ptr spBorderBrush; + pRT->CreateSolidColorBrush(theme->D2DColor(*borderColor), spBorderBrush.put()); + assert(spBorderBrush); + if (spBorderBrush == nullptr) + return; + + winrt::com_ptr spStrokeStyle; + + enum class BorderStyle { Solid, Dotted, Dashed }; + + if (borderStyle == facebook::react::BorderStyle::Dotted || borderStyle == facebook::react::BorderStyle::Dashed) { + const auto capStyle = + borderStyle == facebook::react::BorderStyle::Dashed ? D2D1_CAP_STYLE_FLAT : D2D1_CAP_STYLE_ROUND; + const auto strokeStyleProps = D2D1::StrokeStyleProperties( + capStyle, + capStyle, + capStyle, + D2D1_LINE_JOIN_MITER, + 10.0f, + borderStyle == facebook::react::BorderStyle::Dashed ? D2D1_DASH_STYLE_DASH : D2D1_DASH_STYLE_DOT, + 0.0f); + spFactory->CreateStrokeStyle(&strokeStyleProps, nullptr, 0, spStrokeStyle.put()); + } + D2D1::Matrix3x2F originalTransform; + D2D1::Matrix3x2F translationTransform = + D2D1::Matrix3x2F::Translation(-textureRect.left + offset.x, -textureRect.top + offset.y); + + pRT->GetTransform(&originalTransform); + translationTransform = originalTransform * translationTransform; + + pRT->SetTransform(translationTransform); + + DrawShape(pRT, shape, spBorderBrush.get(), strokeWidth, spStrokeStyle.get()); + + pRT->SetTransform(originalTransform); + } +} + +template +void SetBorderLayerProperties( + winrt::Microsoft::ReactNative::Composition::implementation::Theme *theme, + winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext, + winrt::Microsoft::ReactNative::Composition::Experimental::ISpriteVisual &layer, + TShape &shape, + winrt::com_ptr<::Microsoft::ReactNative::Composition::Experimental::ICompositionDrawingSurfaceInterop> + &borderTexture, + const D2D1_RECT_F &textureRect, + facebook::react::Point anchorPoint, + facebook::react::Point anchorOffset, + winrt::Windows::Foundation::Numerics::float2 size, + winrt::Windows::Foundation::Numerics::float2 relativeSizeAdjustment, + FLOAT strokeWidth, + const facebook::react::SharedColor &borderColor, + facebook::react::BorderStyle borderStyle) { + if constexpr (!std::is_base_of_v) { + SetBorderLayerPropertiesCommon( + theme, + compContext, + layer, + shape, + borderTexture, + textureRect, + anchorPoint, + anchorOffset, + size, + relativeSizeAdjustment, + strokeWidth, + borderColor, + borderStyle); + } else { + // if (VisualVersion::IsUseWinCompClippingRegionEnabled()) + { + layer.Offset({anchorOffset.x, anchorOffset.y, 0}, {anchorPoint.x, anchorPoint.y, 0}); + layer.RelativeSizeWithOffset( + {textureRect.right - textureRect.left, textureRect.bottom - textureRect.top}, {0.0f, 0.0f}); + + layer.Brush(theme->Brush(*borderColor)); + + winrt::com_ptr spD2dFactory; + compContext.as<::Microsoft::ReactNative::Composition::Experimental::ICompositionContextInterop>()->D2DFactory( + spD2dFactory.put()); + + winrt::com_ptr transformedShape; + D2D1::Matrix3x2F translationTransform = D2D1::Matrix3x2F::Translation(-textureRect.left, -textureRect.top); + winrt::check_hresult( + spD2dFactory->CreateTransformedGeometry(&shape, &translationTransform, transformedShape.put())); + + layer.as<::Microsoft::ReactNative::Composition::Experimental::IVisualInterop>()->SetClippingPath( + transformedShape.get()); + } + /* + else + { + SetBorderLayerPropertiesCommon(theme, comContext, layer, shape, borderTexture, textureRect, + anchorPoint, anchorOffset, strokeWidth, borderColor, borderStyle); + } + */ + } +} + +namespace AnchorPosition { +const float Left = 0.0; +const float Center = 0.5; +const float Right = 1.0; +const float Top = 0.0; +const float Bottom = 1.0; +} // namespace AnchorPosition + +template +void DrawAllBorderLayers( + winrt::Microsoft::ReactNative::Composition::implementation::Theme *theme, + winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext, + std::array< + winrt::Microsoft::ReactNative::Composition::Experimental::ISpriteVisual, + BorderPrimitive::SpecialBorderLayerCount> &spBorderLayers, + TShape &shape, + const facebook::react::BorderWidths &borderWidths, + const facebook::react::BorderRadii &borderRadii, + float textureWidth, + float textureHeight, + const facebook::react::BorderColors &borderColors, + facebook::react::BorderStyle borderStyle) { + // Now that we've drawn our nice border in one layer, split it into its component layers + winrt::com_ptr<::Microsoft::ReactNative::Composition::Experimental::ICompositionDrawingSurfaceInterop> + spTextures[BorderPrimitive::SpecialBorderLayerCount]; + + // Set component border properties + // Top Left Corner + SetBorderLayerProperties( + theme, + compContext, + spBorderLayers[0], + shape, + spTextures[0], // Target Layer, Source Texture, Target Texture + {0, + 0, + borderRadii.topLeft + borderWidths.left, + borderRadii.topLeft + borderWidths.top}, // Texture Left, Top, Width, Height + {AnchorPosition::Left, AnchorPosition::Top}, // Layer Anchor Point + {0, 0}, // Layer Anchor Offset + {borderRadii.topLeft + borderWidths.left, borderRadii.topLeft + borderWidths.top}, // size + {0.0f, 0.0f}, // relativeSize + std::max(borderWidths.left, borderWidths.top), + borderColors.left ? borderColors.left : borderColors.top, + borderStyle); + + // Top Edge Border + SetBorderLayerProperties( + theme, + compContext, + spBorderLayers[1], + shape, + spTextures[1], + {borderRadii.topLeft + borderWidths.left, + 0, + textureWidth - (borderRadii.topRight + borderWidths.right), + borderWidths.top}, + {AnchorPosition::Left, AnchorPosition::Top}, + {borderRadii.topLeft + borderWidths.left, 0}, + {-(borderRadii.topLeft + borderWidths.left + borderRadii.topRight + borderWidths.right), + borderWidths.top}, // size + {1.0f, 0.0f}, // relativeSize + borderWidths.top, + borderColors.top, + borderStyle); + + // Top Right Corner Border + SetBorderLayerProperties( + theme, + compContext, + spBorderLayers[2], + shape, + spTextures[2], + {textureWidth - (borderRadii.topRight + borderWidths.right), + 0, + textureWidth, + borderRadii.topRight + borderWidths.top}, + {AnchorPosition::Right, AnchorPosition::Top}, + {-(borderRadii.topRight + borderWidths.right), 0}, + {borderRadii.topRight + borderWidths.right, borderRadii.topRight + borderWidths.top}, + {0.0f, 0.0f}, + std::max(borderWidths.right, borderWidths.top), + borderColors.right ? borderColors.right : borderColors.top, + borderStyle); + + // Right Edge Border + SetBorderLayerProperties( + theme, + compContext, + spBorderLayers[3], + shape, + spTextures[3], + {textureWidth - borderWidths.right, + borderWidths.top + borderRadii.topRight, + textureWidth, + textureHeight - (borderWidths.bottom + borderRadii.bottomRight)}, + {AnchorPosition::Right, AnchorPosition::Top}, + {-borderWidths.right, borderWidths.top + borderRadii.topRight}, + {borderWidths.right, + -(borderWidths.top + borderRadii.topRight + borderWidths.bottom + borderRadii.bottomRight)}, // size + {0.0f, 1.0f}, + borderWidths.right, + borderColors.right, + borderStyle); + + // Bottom Right Corner Border + SetBorderLayerProperties( + theme, + compContext, + spBorderLayers[4], + shape, + spTextures[4], + {textureWidth - (borderWidths.right + borderRadii.bottomRight), + textureHeight - (borderWidths.bottom + borderRadii.bottomRight), + textureWidth, + textureHeight}, + {AnchorPosition::Right, AnchorPosition::Bottom}, + {-(borderWidths.right + borderRadii.bottomRight), -(borderWidths.bottom + borderRadii.bottomRight)}, + {borderWidths.right + borderRadii.bottomRight, borderWidths.bottom + borderRadii.bottomRight}, + {0, 0}, + std::max(borderWidths.right, borderWidths.bottom), + borderColors.right ? borderColors.right : borderColors.bottom, + borderStyle); + + // Bottom Edge Border + SetBorderLayerProperties( + theme, + compContext, + spBorderLayers[5], + shape, + spTextures[5], + {borderWidths.left + borderRadii.bottomLeft, + textureHeight - borderWidths.bottom, + textureWidth - (borderWidths.right + borderRadii.bottomRight), + textureHeight}, + {AnchorPosition::Left, AnchorPosition::Bottom}, + {borderWidths.left + borderRadii.bottomLeft, -borderWidths.bottom}, + {-(borderWidths.right + borderRadii.bottomLeft + borderWidths.left + borderRadii.bottomRight), + borderWidths.bottom}, + {1.0f, 0.0f}, + borderWidths.bottom, + borderColors.bottom, + borderStyle); + + // Bottom Left Corner Border + SetBorderLayerProperties( + theme, + compContext, + spBorderLayers[6], + shape, + spTextures[6], + {0, + textureHeight - (borderWidths.bottom + borderRadii.bottomLeft), + borderWidths.left + borderRadii.bottomLeft, + textureHeight}, + {AnchorPosition::Left, AnchorPosition::Bottom}, + {0, -(borderWidths.bottom + borderRadii.bottomLeft)}, + {borderWidths.left + borderRadii.bottomLeft, borderWidths.bottom + borderRadii.bottomLeft}, + {0, 0}, + std::max(borderWidths.left, borderWidths.bottom), + borderColors.left ? borderColors.left : borderColors.bottom, + borderStyle); + + // Left Edge Border + SetBorderLayerProperties( + theme, + compContext, + spBorderLayers[7], + shape, + spTextures[7], + {0, + borderWidths.top + borderRadii.topLeft, + borderWidths.left, + textureHeight - (borderWidths.bottom + borderRadii.bottomLeft)}, + {AnchorPosition::Left, AnchorPosition::Top}, + {0, borderWidths.top + borderRadii.topLeft}, + {borderWidths.left, -(borderWidths.top + borderRadii.topLeft + borderWidths.bottom + borderRadii.bottomLeft)}, + {0, 1}, + borderWidths.left, + borderColors.left, + borderStyle); +} + +winrt::com_ptr GetGeometryForRoundedBorder( + winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext, + const facebook::react::RectangleCorners &radius, + const facebook::react::RectangleEdges &inset, + const facebook::react::RectangleEdges &thickness, + const facebook::react::RectangleEdges &rectPathGeometry) noexcept { + winrt::com_ptr outerPathGeometry = + BorderPrimitive::GenerateRoundedRectPathGeometry(compContext, radius, inset, rectPathGeometry); + + if (outerPathGeometry == nullptr) { + assert(false); + return nullptr; + } + + facebook::react::RectangleEdges rectInnerPathGeometry = { + rectPathGeometry.left + thickness.left, + rectPathGeometry.top + thickness.top, + rectPathGeometry.right - thickness.right, + rectPathGeometry.bottom - thickness.bottom}; + + // Total thickness is larger than original element size. + // Clamp inner rect to have a width/height of 0, but placed such that the ratio of side-thicknesses is respected. + // We need to respect this ratio so that any animations work properly. + + if (rectInnerPathGeometry.left > rectInnerPathGeometry.right) { + float leftRatio = thickness.left / (thickness.left + thickness.right); + auto x = std::floor(rectPathGeometry.left + ((rectPathGeometry.right - rectPathGeometry.left) * leftRatio)); + rectInnerPathGeometry.left = x; + rectInnerPathGeometry.right = x; + } + + if (rectInnerPathGeometry.top > rectInnerPathGeometry.bottom) { + float topRatio = thickness.top / (thickness.top + thickness.bottom); + auto y = rectPathGeometry.top + std::floor((rectPathGeometry.top - rectPathGeometry.bottom) * topRatio); + rectInnerPathGeometry.top = y; + rectInnerPathGeometry.bottom = y; + } + + facebook::react::RectangleEdges innerInset = { + inset.left + thickness.left, + inset.top + thickness.top, + inset.right + thickness.right, + inset.bottom + thickness.bottom}; + + winrt::com_ptr innerPathGeometry = + BorderPrimitive::GenerateRoundedRectPathGeometry(compContext, radius, innerInset, rectInnerPathGeometry); + + if (innerPathGeometry == nullptr) { + assert(false); // Failed to create inner pathGeometry for rounded border + return nullptr; + } + + ID2D1Geometry *ppGeometries[] = {outerPathGeometry.get(), innerPathGeometry.get()}; + winrt::com_ptr spD2dFactory; + compContext.as<::Microsoft::ReactNative::Composition::Experimental::ICompositionContextInterop>()->D2DFactory( + spD2dFactory.put()); + + winrt::com_ptr geometryGroup = nullptr; + // Create a geometry group. + HRESULT hr = spD2dFactory->CreateGeometryGroup( + D2D1_FILL_MODE_ALTERNATE, ppGeometries, ARRAYSIZE(ppGeometries), geometryGroup.put()); + + if (SUCCEEDED(hr)) { + return geometryGroup; + } + return nullptr; +} + +BorderPrimitive::BorderPrimitive( + winrt::Microsoft::ReactNative::Composition::implementation::ComponentView &outer, + const winrt::Microsoft::ReactNative::Composition::Experimental::IVisual &rootVisual) + : m_outer(&outer), m_rootVisual(rootVisual) {} + +BorderPrimitive::BorderPrimitive(winrt::Microsoft::ReactNative::Composition::implementation::ComponentView &outer) + : m_outer(&outer), m_rootVisual(outer.CompositionContext().CreateSpriteVisual()) {} + +winrt::Microsoft::ReactNative::Composition::Experimental::IVisual BorderPrimitive::RootVisual() const noexcept { + return m_rootVisual; +} + +bool BorderPrimitive::requiresBorder( + const facebook::react::BorderMetrics &borderMetrics, + winrt::Microsoft::ReactNative::Composition::implementation::Theme *theme) noexcept { + // We only handle a single borderStyle for now + auto borderStyle = borderMetrics.borderStyles.left; + + bool hasMeaningfulColor = + !borderMetrics.borderColors.isUniform() || !isColorMeaningful(borderMetrics.borderColors.left, theme); + bool hasMeaningfulWidth = !borderMetrics.borderWidths.isUniform() || (borderMetrics.borderWidths.left != 0); + if (!hasMeaningfulColor && !hasMeaningfulWidth) { + return false; + } + return true; +} + +void BorderPrimitive::updateProps( + const facebook::react::ViewProps &oldViewProps, + const facebook::react::ViewProps &newViewProps) noexcept { + if (oldViewProps.borderColors != newViewProps.borderColors || oldViewProps.borderRadii != newViewProps.borderRadii || + !(oldViewProps.yogaStyle.border(facebook::yoga::Edge::All) == + newViewProps.yogaStyle.border(facebook::yoga::Edge::All)) || + oldViewProps.borderStyles != newViewProps.borderStyles) { + m_needsUpdate = true; + } +} + +void BorderPrimitive::markNeedsUpdate() noexcept { + m_needsUpdate = true; +} + +void BorderPrimitive::finalize( + facebook::react::LayoutMetrics const &layoutMetrics, + const facebook::react::BorderMetrics &borderMetrics) noexcept { + if (!m_needsUpdate) { + return; + } + + auto theme = m_outer->theme(); + if (!theme || theme->IsEmpty()) { + return; + } + + m_needsUpdate = false; + auto spBorderLayers = FindSpecialBorderLayers(); + + if (!TryUpdateSpecialBorderLayers(m_outer->theme(), spBorderLayers, layoutMetrics, borderMetrics)) { + for (auto &spBorderLayer : spBorderLayers) { + if (spBorderLayer) { + spBorderLayer.as().Brush(nullptr); + } + } + } +} + +void BorderPrimitive::onThemeChanged( + facebook::react::LayoutMetrics const &layoutMetrics, + const facebook::react::BorderMetrics &borderMetrics) noexcept { + m_needsUpdate = true; + finalize(layoutMetrics, borderMetrics); +} + +std::array< + winrt::Microsoft::ReactNative::Composition::Experimental::ISpriteVisual, + BorderPrimitive::SpecialBorderLayerCount> +BorderPrimitive::FindSpecialBorderLayers() const noexcept { + std::array layers{ + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}; + + if (m_numBorderVisuals) { + for (uint8_t i = 0; i < m_numBorderVisuals; i++) { + auto visual = m_rootVisual.GetAt(i); + layers[i] = visual.as(); + } + } + + return layers; +} + +uint8_t BorderPrimitive::numberOfVisuals() const noexcept { + return m_numBorderVisuals; +} + +bool BorderPrimitive::TryUpdateSpecialBorderLayers( + winrt::Microsoft::ReactNative::Composition::implementation::Theme *theme, + std::array + &spBorderVisuals, + facebook::react::LayoutMetrics const &layoutMetrics, + const facebook::react::BorderMetrics &borderMetrics) noexcept { + // We only handle a single borderStyle for now + auto borderStyle = borderMetrics.borderStyles.left; + + bool hasMeaningfulColor = + !borderMetrics.borderColors.isUniform() || !isColorMeaningful(borderMetrics.borderColors.left, theme); + bool hasMeaningfulWidth = !borderMetrics.borderWidths.isUniform() || (borderMetrics.borderWidths.left != 0); + if (!hasMeaningfulColor && !hasMeaningfulWidth) { + return false; + } + + // Create the special border layers if they don't exist yet + if (!spBorderVisuals[0]) { + for (uint8_t i = 0; i < SpecialBorderLayerCount; i++) { + auto visual = m_outer->CompositionContext().CreateSpriteVisual(); + m_rootVisual.InsertAt(visual, i); + spBorderVisuals[i] = std::move(visual); + m_numBorderVisuals++; + } + } + + float extentWidth = layoutMetrics.frame.size.width * layoutMetrics.pointScaleFactor; + float extentHeight = layoutMetrics.frame.size.height * layoutMetrics.pointScaleFactor; + + if (borderMetrics.borderRadii.topLeft != 0 || borderMetrics.borderRadii.bottomLeft != 0 || + borderMetrics.borderRadii.topRight != 0 || borderMetrics.borderRadii.bottomRight != 0) { + auto compContext = m_outer->CompositionContext(); + if (borderStyle == facebook::react::BorderStyle::Dotted || borderStyle == facebook::react::BorderStyle::Dashed) { + // Because in DirectX geometry starts at the center of the stroke, we need to deflate + // rectangle by half the stroke width to render correctly. + facebook::react::RectangleEdges rectPathGeometry = { + borderMetrics.borderWidths.left / 2.0f, + borderMetrics.borderWidths.top / 2.0f, + extentWidth - borderMetrics.borderWidths.right / 2.0f, + extentHeight - borderMetrics.borderWidths.bottom / 2.0f}; + + winrt::com_ptr pathGeometry = + GenerateRoundedRectPathGeometry(compContext, borderMetrics.borderRadii, {0, 0, 0, 0}, rectPathGeometry); + + if (pathGeometry) { + DrawAllBorderLayers( + theme, + compContext, + spBorderVisuals, + *pathGeometry, + borderMetrics.borderWidths, + borderMetrics.borderRadii, + extentWidth, + extentHeight, + borderMetrics.borderColors, + borderStyle); + } else { + assert(false); + } + } else { + facebook::react::RectangleEdges rectPathGeometry = {0, 0, extentWidth, extentHeight}; + + winrt::com_ptr pathGeometry = GetGeometryForRoundedBorder( + compContext, + borderMetrics.borderRadii, + {0, 0, 0, 0}, // inset + borderMetrics.borderWidths, + rectPathGeometry); + + DrawAllBorderLayers( + theme, + compContext, + spBorderVisuals, + *pathGeometry, + borderMetrics.borderWidths, + borderMetrics.borderRadii, + extentWidth, + extentHeight, + borderMetrics.borderColors, + borderStyle); + } + } else { + auto compContext = m_outer->CompositionContext(); + // Because in DirectX geometry starts at the center of the stroke, we need to deflate rectangle by half the stroke + // width / height to render correctly. + D2D1_RECT_F rectShape{ + borderMetrics.borderWidths.left / 2.0f, + borderMetrics.borderWidths.top / 2.0f, + extentWidth - (borderMetrics.borderWidths.right / 2.0f), + extentHeight - (borderMetrics.borderWidths.bottom / 2.0f)}; + DrawAllBorderLayers( + theme, + compContext, + spBorderVisuals, + rectShape, + borderMetrics.borderWidths, + borderMetrics.borderRadii, + extentWidth, + extentHeight, + borderMetrics.borderColors, + borderStyle); + } + return true; +} + +} // namespace winrt::Microsoft::ReactNative::Composition::implementation diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/BorderPrimitive.h b/vnext/Microsoft.ReactNative/Fabric/Composition/BorderPrimitive.h new file mode 100644 index 00000000000..82ba1b92985 --- /dev/null +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/BorderPrimitive.h @@ -0,0 +1,76 @@ + +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include +#include +#include +#include +#include "CompositionHelpers.h" + +namespace winrt::Microsoft::ReactNative::Composition::implementation { + +struct ComponentView; + +// Controls adding/removing appropriate visuals to a parent to render a specific border without requiring +struct BorderPrimitive { + static constexpr size_t SpecialBorderLayerCount = 8; + + // Use when attaching to an existing visual - We do this for View's border properties, to optimize the number of + // visuals for each view. + BorderPrimitive( + winrt::Microsoft::ReactNative::Composition::implementation::ComponentView &outer, + const winrt::Microsoft::ReactNative::Composition::Experimental::IVisual &rootVisual); + + // This ctor creates a new root visual that can then be added to the visual tree + BorderPrimitive(winrt::Microsoft::ReactNative::Composition::implementation::ComponentView &outer); + + void markNeedsUpdate() noexcept; + + void updateProps( + const facebook::react::ViewProps &oldViewProps, + const facebook::react::ViewProps &newViewProps) noexcept; + void finalize( + facebook::react::LayoutMetrics const &layoutMetrics, + const facebook::react::BorderMetrics &borderMetrics) noexcept; + void onThemeChanged( + facebook::react::LayoutMetrics const &layoutMetrics, + const facebook::react::BorderMetrics &borderMetrics) noexcept; + + uint8_t numberOfVisuals() const noexcept; + winrt::Microsoft::ReactNative::Composition::Experimental::IVisual RootVisual() const noexcept; + + static facebook::react::BorderMetrics resolveAndAlignBorderMetrics( + facebook::react::LayoutMetrics const &layoutMetrics, + const facebook::react::ViewProps &viewProps) noexcept; + + static bool requiresBorder( + const facebook::react::BorderMetrics &borderMetrics, + winrt::Microsoft::ReactNative::Composition::implementation::Theme *theme) noexcept; + + static winrt::com_ptr GenerateRoundedRectPathGeometry( + winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext, + const facebook::react::RectangleCorners &baseRadius, + const facebook::react::RectangleEdges &inset, + const facebook::react::RectangleEdges &rectPathGeometry) noexcept; + + private: + std::array + FindSpecialBorderLayers() const noexcept; + + bool TryUpdateSpecialBorderLayers( + winrt::Microsoft::ReactNative::Composition::implementation::Theme *theme, + std::array + &spBorderVisuals, + facebook::react::LayoutMetrics const &layoutMetrics, + const facebook::react::BorderMetrics &borderMetrics) noexcept; + + uint8_t m_numBorderVisuals{0}; + winrt::Microsoft::ReactNative::Composition::implementation::ComponentView *m_outer; + winrt::Microsoft::ReactNative::Composition::Experimental::IVisual m_rootVisual{nullptr}; + bool m_needsUpdate{true}; +}; + +} // namespace winrt::Microsoft::ReactNative::Composition::implementation diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp index a3d0d816a2e..e5160eee6a6 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp @@ -1,6 +1,8 @@ #include "pch.h" #include "CompositionDynamicAutomationProvider.h" #include +#include +#include #include #include "RootComponentView.h" #include "UiaHelpers.h" @@ -141,6 +143,20 @@ bool expandableControl(const facebook::react::SharedViewProps props) { return false; } +bool togglableControl(const facebook::react::SharedViewProps props) { + if (props->accessibilityState.has_value() && + props->accessibilityState->checked != facebook::react::AccessibilityState::None) { + return true; + } + auto accessibilityActions = props->accessibilityActions; + for (size_t i = 0; i < accessibilityActions.size(); i++) { + if (accessibilityActions[i].name == "toggle") { + return true; + } + } + return false; +} + HRESULT __stdcall CompositionDynamicAutomationProvider::GetPatternProvider(PATTERNID patternId, IUnknown **pRetVal) { if (pRetVal == nullptr) return E_POINTER; @@ -158,10 +174,7 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::GetPatternProvider(PATTE auto accessibilityRole = props->accessibilityRole; // Invoke control pattern is used to support controls that do not maintain state // when activated but rather initiate or perform a single, unambiguous action. - if (patternId == UIA_InvokePatternId && - (accessibilityRole == "button" || accessibilityRole == "imagebutton" || accessibilityRole == "link" || - accessibilityRole == "splitbutton" || (accessibilityRole == "menuitem" && props->onAccessibilityTap) || - (accessibilityRole == "treeitem" && props->onAccessibilityTap))) { + if (patternId == UIA_InvokePatternId && (props->onAccessibilityTap)) { *pRetVal = static_cast(this); AddRef(); } @@ -171,21 +184,21 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::GetPatternProvider(PATTE AddRef(); } - if (patternId == UIA_ValuePatternId) { + if (patternId == UIA_ValuePatternId && + (strongView.try_as() || + accessibilityValueHasValue(props->accessibilityValue))) { *pRetVal = static_cast(this); AddRef(); } - if (patternId == UIA_TogglePatternId && (accessibilityRole == "switch" || accessibilityRole == "checkbox")) { + if (patternId == UIA_TogglePatternId && + (strongView.try_as() || + togglableControl(props))) { *pRetVal = static_cast(this); AddRef(); } - if (patternId == UIA_ExpandCollapsePatternId && - (accessibilityRole == "combobox" || accessibilityRole == "splitbutton" || accessibilityRole == "treeitem" || - (expandableControl(props) && - (accessibilityRole == "toolbar" || accessibilityRole == "menuitem" || accessibilityRole == "menubar" || - accessibilityRole == "listitem" || accessibilityRole == "group" || accessibilityRole == "button")))) { + if (patternId == UIA_ExpandCollapsePatternId && expandableControl(props)) { *pRetVal = static_cast(this); AddRef(); } @@ -480,8 +493,13 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::get_ToggleState(ToggleSt if (!strongView) return UIA_E_ELEMENTNOTAVAILABLE; - *pRetVal = - winrt::get_self(strongView)->getToggleState(); + auto props = std::static_pointer_cast( + winrt::get_self(strongView)->props()); + + *pRetVal = (props->accessibilityState.has_value() && + props->accessibilityState->checked != facebook::react::AccessibilityState::None) + ? GetToggleState(props->accessibilityState) + : winrt::get_self(strongView)->getToggleState(); return S_OK; } diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp index 3acf0840507..ebaa245d1cf 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp @@ -139,8 +139,9 @@ struct CompositionInputKeyboardSource : winrt::implements< CompositionEventHandler::CompositionEventHandler( const winrt::Microsoft::ReactNative::ReactContext &context, - const winrt::Microsoft::ReactNative::ReactNativeIsland &reactNativeIsland) - : m_context(context), m_wkRootView(reactNativeIsland) {} + const winrt::Microsoft::ReactNative::ReactNativeIsland &reactNativeIsland, + const int fragmentTag) + : m_fragmentTag(fragmentTag), m_context(context), m_wkRootView(reactNativeIsland) {} void CompositionEventHandler::Initialize() noexcept { #ifdef USE_WINUI3 @@ -831,7 +832,30 @@ void CompositionEventHandler::getTargetPointerArgs( ptLocal.y = ptScaled.y - (clientRect.top / strongRootView.ScaleFactor()); } } else { - tag = RootComponentView().hitTest(ptScaled, ptLocal); + if (m_fragmentTag == -1) { + tag = RootComponentView().hitTest(ptScaled, ptLocal); + return; + } + + // check if the fragment tag exists + if (!fabricuiManager->GetViewRegistry().findComponentViewWithTag(m_fragmentTag)) { + return; + } + + auto fagmentView = fabricuiManager->GetViewRegistry().componentViewDescriptorWithTag(m_fragmentTag).view; + auto fagmentchildren = fagmentView.Children(); + + // call the hitTest with the fargment as the RootComponent + for (auto index = fagmentchildren.Size(); index > 0; index--) { + auto childView = fagmentchildren.GetAt(index - 1); + auto targetTag = + winrt::get_self(childView)->hitTest( + ptScaled, ptLocal); + if (targetTag != -1) { + tag = targetTag; + break; + } + } } } diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.h b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.h index 44a1c12b129..a114ac08773 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.h @@ -31,7 +31,8 @@ class CompositionEventHandler : public std::enable_shared_from_this m_activeTouches; // iOS is map of touch event args to ActiveTouch..? PointerId m_touchId = 0; + int m_fragmentTag = -1; std::map> m_currentlyHoveredViewsPerPointer; winrt::weak_ref m_wkRootView; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp index ad07315615d..0189827a7f7 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp @@ -32,6 +32,23 @@ namespace winrt::Microsoft::ReactNative::Composition::implementation { +constexpr float FOCUS_VISUAL_WIDTH = 2.0f; + +// m_outerVisual +// | +// | +// ----- m_visual <-- Background / clip - Can be a custom visual depending on Component type +// | +// ----- Border Visuals x N (BorderPrimitive attached to m_visual) +// ------Focus Visual Container +// | +// |------Inner Focus Visual +// | +// ------ Border Visuals x N (BorderPrimitive) +// |------Outer Focus Visual +// | +// ------ Border Visuals x N (BorderPrimitive) + ComponentView::ComponentView( const winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext, facebook::react::Tag tag, @@ -40,8 +57,9 @@ ComponentView::ComponentView( : base_type(tag, reactContext), m_compContext(compContext), m_flags(flags) { m_outerVisual = compContext.CreateSpriteVisual(); // TODO could be a raw ContainerVisual if we had a // CreateContainerVisual in ICompositionContext - m_focusVisual = compContext.CreateFocusVisual(); - m_outerVisual.InsertAt(m_focusVisual.InnerVisual(), 0); + m_focusVisual = compContext.CreateSpriteVisual(); // TODO could be a raw ContainerVisual if we had a + // CreateContainerVisual in ICompositionContext + m_outerVisual.InsertAt(m_focusVisual, 0); } ComponentView::~ComponentView() { @@ -68,9 +86,17 @@ void ComponentView::onThemeChanged() noexcept { } } - if ((m_flags & ComponentViewFeatures::NativeBorder) == ComponentViewFeatures::NativeBorder) { - m_needsBorderUpdate = true; - finalizeBorderUpdates(m_layoutMetrics, *viewProps()); + if (m_borderPrimitive) { + m_borderPrimitive->onThemeChanged( + m_layoutMetrics, BorderPrimitive::resolveAndAlignBorderMetrics(m_layoutMetrics, *viewProps())); + } + if (m_focusInnerPrimitive) { + auto innerFocusMetrics = focusLayoutMetrics(true /*inner*/); + m_focusInnerPrimitive->onThemeChanged(innerFocusMetrics, focusBorderMetrics(true /*inner*/, innerFocusMetrics)); + } + if (m_focusOuterPrimitive) { + auto outerFocusMetrics = focusLayoutMetrics(true /*inner*/); + m_focusOuterPrimitive->onThemeChanged(outerFocusMetrics, focusBorderMetrics(false /*inner*/, outerFocusMetrics)); } if ((m_flags & ComponentViewFeatures::ShadowProps) == ComponentViewFeatures::ShadowProps) { @@ -131,13 +157,21 @@ void ComponentView::updateProps( } } - if ((m_flags & ComponentViewFeatures::NativeBorder) == ComponentViewFeatures::NativeBorder) { - updateBorderProps(oldViewProps, newViewProps); + if (m_borderPrimitive) { + m_borderPrimitive->updateProps(oldViewProps, newViewProps); + } + if (!newViewProps.enableFocusRing) { + showFocusVisual(false); + } + if (m_focusInnerPrimitive) { + m_focusInnerPrimitive->updateProps(oldViewProps, newViewProps); + } + if (m_focusOuterPrimitive) { + m_focusOuterPrimitive->updateProps(oldViewProps, newViewProps); } if ((m_flags & ComponentViewFeatures::ShadowProps) == ComponentViewFeatures::ShadowProps) { updateShadowProps(oldViewProps, newViewProps); } - if (oldViewProps.tooltip != newViewProps.tooltip) { if (!m_tooltipTracked && newViewProps.tooltip) { TooltipService::GetCurrent(m_reactContext.Properties())->StartTracking(*this); @@ -155,13 +189,58 @@ void ComponentView::updateLayoutMetrics( facebook::react::LayoutMetrics const &layoutMetrics, facebook::react::LayoutMetrics const &oldLayoutMetrics) noexcept { if ((m_flags & ComponentViewFeatures::NativeBorder) == ComponentViewFeatures::NativeBorder) { - updateBorderLayoutMetrics(layoutMetrics, *viewProps()); + updateClippingPath(layoutMetrics, *viewProps()); + OuterVisual().Size( + {layoutMetrics.frame.size.width * layoutMetrics.pointScaleFactor, + layoutMetrics.frame.size.height * layoutMetrics.pointScaleFactor}); + OuterVisual().Offset({ + layoutMetrics.frame.origin.x * layoutMetrics.pointScaleFactor, + layoutMetrics.frame.origin.y * layoutMetrics.pointScaleFactor, + 0.0f, + }); + } + + updateFocusLayoutMetrics(layoutMetrics); + + if (layoutMetrics != oldLayoutMetrics) { + if (m_borderPrimitive) { + m_borderPrimitive->markNeedsUpdate(); + } + if (m_focusInnerPrimitive) { + m_focusInnerPrimitive->markNeedsUpdate(); + } + if (m_focusOuterPrimitive) { + m_focusOuterPrimitive->markNeedsUpdate(); + } } base_type::updateLayoutMetrics(layoutMetrics, oldLayoutMetrics); UpdateCenterPropertySet(); } +void ComponentView::updateFocusLayoutMetrics(facebook::react::LayoutMetrics const &layoutMetrics) noexcept { + if (m_focusInnerPrimitive) { + auto innerFocusMetrics = focusLayoutMetrics(true /*inner*/); + m_focusInnerPrimitive->RootVisual().Size( + {innerFocusMetrics.frame.size.width * layoutMetrics.pointScaleFactor, + innerFocusMetrics.frame.size.height * layoutMetrics.pointScaleFactor}); + m_focusInnerPrimitive->RootVisual().Offset( + {-FOCUS_VISUAL_WIDTH * layoutMetrics.pointScaleFactor, + -FOCUS_VISUAL_WIDTH * layoutMetrics.pointScaleFactor, + 0.0f}); + } + if (m_focusOuterPrimitive) { + auto outerFocusMetrics = focusLayoutMetrics(false /*inner*/); + m_focusOuterPrimitive->RootVisual().Size( + {outerFocusMetrics.frame.size.width * layoutMetrics.pointScaleFactor, + outerFocusMetrics.frame.size.height * layoutMetrics.pointScaleFactor}); + m_focusOuterPrimitive->RootVisual().Offset( + {-(FOCUS_VISUAL_WIDTH * 2 * m_layoutMetrics.pointScaleFactor), + -(FOCUS_VISUAL_WIDTH * 2 * m_layoutMetrics.pointScaleFactor), + 0.0f}); + } +} + const facebook::react::LayoutMetrics &ComponentView::layoutMetrics() const noexcept { return m_layoutMetrics; } @@ -199,7 +278,23 @@ void ComponentView::FinalizeTransform( void ComponentView::FinalizeUpdates(winrt::Microsoft::ReactNative::ComponentViewUpdateMask updateMask) noexcept { if ((m_flags & ComponentViewFeatures::NativeBorder) == ComponentViewFeatures::NativeBorder) { - finalizeBorderUpdates(m_layoutMetrics, *viewProps()); + auto borderMetrics = BorderPrimitive::resolveAndAlignBorderMetrics(m_layoutMetrics, *viewProps()); + if (!m_borderPrimitive && BorderPrimitive::requiresBorder(borderMetrics, theme())) { + m_borderPrimitive = std::make_shared(*this, Visual()); + } + + if (m_borderPrimitive) { + m_borderPrimitive->finalize(m_layoutMetrics, borderMetrics); + } + } + + if (m_focusInnerPrimitive) { + auto innerFocusMetrics = focusLayoutMetrics(true /*inner*/); + m_focusInnerPrimitive->finalize(innerFocusMetrics, focusBorderMetrics(true /*inner*/, innerFocusMetrics)); + } + if (m_focusOuterPrimitive) { + auto outerFocusMetrics = focusLayoutMetrics(false /*inner*/); + m_focusOuterPrimitive->finalize(outerFocusMetrics, focusBorderMetrics(false /*inner*/, outerFocusMetrics)); } if (m_FinalizeTransform) { @@ -226,7 +321,7 @@ void ComponentView::onGotFocus( const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs &args) noexcept { if (args.OriginalSource() == Tag()) { m_eventEmitter->onFocus(); - if (m_enableFocusVisual) { + if (viewProps()->enableFocusRing) { showFocusVisual(true); } if (m_uiaProvider) { @@ -325,6 +420,10 @@ void ComponentView::ReleasePointerCapture( ->ReleasePointerCapture(pointer, static_cast(Tag())); } +void ComponentView::SetViewFeatures(ComponentViewFeatures viewFeatures) noexcept { + m_flags = viewFeatures; +} + RECT ComponentView::getClientRect() const noexcept { RECT rc{0}; facebook::react::Point parentOffset{0}; @@ -356,877 +455,81 @@ const facebook::react::SharedViewEventEmitter &ComponentView::GetEventEmitter() return m_eventEmitter; } -std::array< - winrt::Microsoft::ReactNative::Composition::Experimental::ISpriteVisual, - ComponentView::SpecialBorderLayerCount> -ComponentView::FindSpecialBorderLayers() const noexcept { - std::array layers{ - nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}; - - if (m_numBorderVisuals) { - for (uint8_t i = 0; i < m_numBorderVisuals; i++) { - auto visual = Visual().GetAt(i); - layers[i] = visual.as(); - } - } - - return layers; -} - -struct RoundedPathParameters { - float topLeftRadiusX = 0; - float topLeftRadiusY = 0; - float topRightRadiusX = 0; - float topRightRadiusY = 0; - float bottomRightRadiusX = 0; - float bottomRightRadiusY = 0; - float bottomLeftRadiusX = 0; - float bottomLeftRadiusY = 0; -}; - -/* - * Creates and returns a PathGeometry object used to clip the visuals of an element when a BorderRadius is set. - * Can also be used as part of a GeometryGroup for drawing a rounded border / innerstroke when called from - * GetGeometryForRoundedBorder. "params" defines the radii (horizontal and vertical) for each corner (top left, top - * right, bottom right, bottom left). "rectPathGeometry" defines the bounding box of the generated shape. - */ -static winrt::com_ptr GenerateRoundedRectPathGeometry( - winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext, - const RoundedPathParameters ¶ms, - const facebook::react::RectangleEdges &rectPathGeometry) noexcept { - winrt::com_ptr pathGeometry; - winrt::com_ptr spD2dFactory; - compContext.as<::Microsoft::ReactNative::Composition::Experimental::ICompositionContextInterop>()->D2DFactory( - spD2dFactory.put()); - - // Create a path geometry. - HRESULT hr = spD2dFactory->CreatePathGeometry(pathGeometry.put()); - if (FAILED(hr)) { - assert(false); - return nullptr; - } - - // Write to the path geometry using the geometry sink. - winrt::com_ptr spSink = nullptr; - hr = pathGeometry->Open(spSink.put()); - - if (FAILED(hr)) { - assert(false); - return nullptr; - } - - float left = rectPathGeometry.left; - float right = rectPathGeometry.right; - float top = rectPathGeometry.top; - float bottom = rectPathGeometry.bottom; - - // This function uses Cubic Beziers to approximate Arc segments, even though D2D supports arcs. - // This is INTENTIONAL. D2D Arc Segments are eventually converted into cubic beziers, but this - // is done in such a way that we don't have control over how many bezier curve segments are used - // for each arc. We need to ensure that we always use the same number of control points so that - // our paths can be used in a PathKeyFrameAnimation. - // Value for control point scale factor derived from methods described in: - // https://web.archive.org/web/20200322075504/http://itc.ktu.lt/index.php/ITC/article/download/11812/6479 - constexpr float controlPointScaleFactor = 0.44771528244f; // 1 - (4 * (sqrtf(2.0f) - 1) / 3.0f); - -#ifdef DEBUG - // std::sqrtf is not constexpr, so we precalculated this and wrote it in a constexpr form above. - // On debug, we should still check that the values are equivalent, though. - static float calculatedScaleFactor = 1 - (4 * (sqrtf(2.0f) - 1) / 3.0f); - assert(controlPointScaleFactor == calculatedScaleFactor); -#endif // DEBUG - - bool needsConsistentNumberOfControlPoints = true; // VisualVersion::IsUseWinCompClippingRegionEnabled(); - - if (needsConsistentNumberOfControlPoints || (params.topLeftRadiusX != 0.0 && params.topLeftRadiusY != 0.0)) { - spSink->BeginFigure(D2D1::Point2F(left + params.topLeftRadiusX, top), D2D1_FIGURE_BEGIN_FILLED); - } else { - spSink->BeginFigure(D2D1::Point2F(left, top), D2D1_FIGURE_BEGIN_FILLED); - } - - // Move to the top right corner - spSink->AddLine(D2D1::Point2F(right - params.topRightRadiusX, top)); - if (needsConsistentNumberOfControlPoints) { - D2D1_BEZIER_SEGMENT arcSegmentTopRight = { - D2D1::Point2F(right - controlPointScaleFactor * params.topRightRadiusX, top), - D2D1::Point2F(right, top + controlPointScaleFactor * params.topRightRadiusY), - D2D1::Point2F(right, top + params.topRightRadiusY)}; - - spSink->AddBezier(&arcSegmentTopRight); - } else if (params.topRightRadiusX != 0.0 && params.topRightRadiusY != 0.0) { - D2D1_ARC_SEGMENT arcSegmentTopRight = { - D2D1::Point2F(right, top + params.topRightRadiusY), - D2D1::SizeF(params.topRightRadiusX, params.topRightRadiusY), - 0.0f, - D2D1_SWEEP_DIRECTION_CLOCKWISE, - D2D1_ARC_SIZE_SMALL}; - - spSink->AddArc(&arcSegmentTopRight); - } else { - spSink->AddLine(D2D1::Point2F(right, top)); - } - - // Move to the bottom right corner - spSink->AddLine(D2D1::Point2F(right, bottom - params.bottomRightRadiusY)); - if (needsConsistentNumberOfControlPoints) { - D2D1_BEZIER_SEGMENT arcSegmentBottomRight = { - D2D1::Point2F(right, bottom - controlPointScaleFactor * params.bottomRightRadiusY), - D2D1::Point2F(right - controlPointScaleFactor * params.bottomRightRadiusX, bottom), - D2D1::Point2F(right - params.bottomRightRadiusX, bottom)}; - - spSink->AddBezier(&arcSegmentBottomRight); - } else if (params.bottomRightRadiusX != 0.0 && params.bottomRightRadiusY != 0.0) { - D2D1_ARC_SEGMENT arcSegmentBottomRight = { - D2D1::Point2F(right - params.bottomRightRadiusX, bottom), - D2D1::SizeF(params.bottomRightRadiusX, params.bottomRightRadiusY), - 0.0f, - D2D1_SWEEP_DIRECTION_CLOCKWISE, - D2D1_ARC_SIZE_SMALL}; - - spSink->AddArc(&arcSegmentBottomRight); - } else { - spSink->AddLine(D2D1::Point2F(right, bottom)); - } - - // Move to the bottom left corner - spSink->AddLine(D2D1::Point2F(left + params.bottomLeftRadiusX, bottom)); - if (needsConsistentNumberOfControlPoints) { - D2D1_BEZIER_SEGMENT arcSegmentBottomLeft = { - D2D1::Point2F(left + controlPointScaleFactor * params.bottomLeftRadiusX, bottom), - D2D1::Point2F(left, bottom - controlPointScaleFactor * params.bottomLeftRadiusY), - D2D1::Point2F(left, bottom - params.bottomLeftRadiusY)}; - - spSink->AddBezier(&arcSegmentBottomLeft); - } else if (params.bottomLeftRadiusX != 0.0 && params.bottomLeftRadiusY != 0.0) { - D2D1_ARC_SEGMENT arcSegmentBottomLeft = { - D2D1::Point2F(left, bottom - params.bottomLeftRadiusY), - D2D1::SizeF(params.bottomLeftRadiusX, params.bottomLeftRadiusY), - 0.0f, - D2D1_SWEEP_DIRECTION_CLOCKWISE, - D2D1_ARC_SIZE_SMALL}; - - spSink->AddArc(&arcSegmentBottomLeft); - } else { - spSink->AddLine(D2D1::Point2F(left, bottom)); - } - - // Move to the top left corner - spSink->AddLine(D2D1::Point2F(left, top + params.topLeftRadiusY)); - if (needsConsistentNumberOfControlPoints) { - D2D1_BEZIER_SEGMENT arcSegmentTopLeft = { - D2D1::Point2F(left, top + controlPointScaleFactor * params.topLeftRadiusY), - D2D1::Point2F(left + controlPointScaleFactor * params.topLeftRadiusX, top), - D2D1::Point2F(left + params.topLeftRadiusX, top)}; - - spSink->AddBezier(&arcSegmentTopLeft); - } else if (params.topLeftRadiusX != 0.0 && params.topLeftRadiusY != 0.0) { - D2D1_ARC_SEGMENT arcSegmentTopLeft = { - D2D1::Point2F(left + params.topLeftRadiusX, top), - D2D1::SizeF(params.topLeftRadiusX, params.topLeftRadiusY), - 0.0f, - D2D1_SWEEP_DIRECTION_CLOCKWISE, - D2D1_ARC_SIZE_SMALL}; - - spSink->AddArc(&arcSegmentTopLeft); - } else { - spSink->AddLine(D2D1::Point2F(left, top)); - } - - spSink->EndFigure(D2D1_FIGURE_END_CLOSED); - spSink->Close(); - - return pathGeometry; -} - -RoundedPathParameters GenerateRoundedPathParameters( - const facebook::react::RectangleCorners &baseRadius, - const facebook::react::RectangleEdges &inset, - const facebook::react::Size &pathSize) noexcept { - RoundedPathParameters result; - - if (pathSize.width == 0 || pathSize.height == 0) { - return result; - } - - float totalTopRadius = baseRadius.topLeft + baseRadius.topRight; - float totalRightRadius = baseRadius.topRight + baseRadius.bottomRight; - float totalBottomRadius = baseRadius.bottomRight + baseRadius.bottomLeft; - float totalLeftRadius = baseRadius.bottomLeft + baseRadius.topLeft; - - float maxHorizontalRadius = std::max(totalTopRadius, totalBottomRadius); - float maxVerticalRadius = std::max(totalLeftRadius, totalRightRadius); - - double totalWidth = inset.left + inset.right + pathSize.width; - double totalHeight = inset.top + inset.bottom + pathSize.height; - - float scaleHoriz = static_cast(maxHorizontalRadius / totalWidth); - float scaleVert = static_cast(maxVerticalRadius / totalHeight); - - float maxScale = std::max(1.0f, std::max(scaleHoriz, scaleVert)); - - result.topLeftRadiusX = std::max(0.0f, baseRadius.topLeft / maxScale - inset.left); - result.topLeftRadiusY = std::max(0.0f, baseRadius.topLeft / maxScale - inset.top); - result.topRightRadiusX = std::max(0.0f, baseRadius.topRight / maxScale - inset.right); - result.topRightRadiusY = std::max(0.0f, baseRadius.topRight / maxScale - inset.top); - result.bottomRightRadiusX = std::max(0.0f, baseRadius.bottomRight / maxScale - inset.right); - result.bottomRightRadiusY = std::max(0.0f, baseRadius.bottomRight / maxScale - inset.bottom); - result.bottomLeftRadiusX = std::max(0.0f, baseRadius.bottomLeft / maxScale - inset.left); - result.bottomLeftRadiusY = std::max(0.0f, baseRadius.bottomLeft / maxScale - inset.bottom); - - return result; -} - -static winrt::com_ptr GenerateRoundedRectPathGeometry( - winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext, - const facebook::react::RectangleCorners &baseRadius, - const facebook::react::RectangleEdges &inset, - const facebook::react::RectangleEdges &rectPathGeometry) noexcept { - RoundedPathParameters params = GenerateRoundedPathParameters( - baseRadius, - inset, - {rectPathGeometry.right - rectPathGeometry.left, rectPathGeometry.bottom - rectPathGeometry.top}); - - return GenerateRoundedRectPathGeometry(compContext, params, rectPathGeometry); -} - -void DrawShape( - ID2D1RenderTarget *pRT, - const D2D1_RECT_F &rect, - ID2D1Brush *brush, - FLOAT strokeWidth, - ID2D1StrokeStyle *strokeStyle) { - pRT->DrawRectangle(rect, brush, strokeWidth, strokeStyle); -} - -void DrawShape(ID2D1RenderTarget *pRT, ID2D1GeometryGroup &geometry, ID2D1Brush *brush, FLOAT, ID2D1StrokeStyle *) { - pRT->FillGeometry(&geometry, brush); -} - -void DrawShape( - ID2D1RenderTarget *pRT, - ID2D1PathGeometry &geometry, - ID2D1Brush *brush, - FLOAT strokeWidth, - ID2D1StrokeStyle *strokeStyle) { - pRT->DrawGeometry(&geometry, brush, strokeWidth, strokeStyle); -} - -template -void SetBorderLayerPropertiesCommon( - winrt::Microsoft::ReactNative::Composition::implementation::Theme *theme, - winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext, - winrt::Microsoft::ReactNative::Composition::Experimental::ISpriteVisual &layer, - TShape &shape, - winrt::com_ptr<::Microsoft::ReactNative::Composition::Experimental::ICompositionDrawingSurfaceInterop> - &borderTexture, - const D2D1_RECT_F &textureRect, - facebook::react::Point anchorPoint, - facebook::react::Point anchorOffset, - winrt::Windows::Foundation::Numerics::float2 size, - winrt::Windows::Foundation::Numerics::float2 relativeSizeAdjustment, - FLOAT strokeWidth, - const facebook::react::SharedColor &borderColor, - facebook::react::BorderStyle borderStyle) { - layer.Offset({anchorOffset.x, anchorOffset.y, 0}, {anchorPoint.x, anchorPoint.y, 0}); - layer.RelativeSizeWithOffset(size, relativeSizeAdjustment); - layer.as<::Microsoft::ReactNative::Composition::Experimental::IVisualInterop>()->SetClippingPath(nullptr); - - if ((textureRect.right - textureRect.left) <= 0 || (textureRect.bottom - textureRect.top) <= 0) - return; - - auto surface = compContext.CreateDrawingSurfaceBrush( - {(textureRect.right - textureRect.left), (textureRect.bottom - textureRect.top)}, - winrt::Windows::Graphics::DirectX::DirectXPixelFormat::B8G8R8A8UIntNormalized, - winrt::Windows::Graphics::DirectX::DirectXAlphaMode::Premultiplied); - surface.as(borderTexture); - - layer.Brush(surface); - - POINT offset; - ::Microsoft::ReactNative::Composition::AutoDrawDrawingSurface autoDraw( - surface, 1.0f /* We have already done the dpi scaling */, &offset); - if (auto pRT = autoDraw.GetRenderTarget()) { - // Clear with transparency - pRT->Clear(); - - if (!facebook::react::isColorMeaningful(borderColor)) { - return; - } - - winrt::com_ptr spFactory; - pRT->GetFactory(spFactory.put()); - assert(spFactory); - if (spFactory == nullptr) - return; - - winrt::com_ptr spBorderBrush; - pRT->CreateSolidColorBrush(theme->D2DColor(*borderColor), spBorderBrush.put()); - assert(spBorderBrush); - if (spBorderBrush == nullptr) - return; - - winrt::com_ptr spStrokeStyle; - - enum class BorderStyle { Solid, Dotted, Dashed }; - - if (borderStyle == facebook::react::BorderStyle::Dotted || borderStyle == facebook::react::BorderStyle::Dashed) { - const auto capStyle = - borderStyle == facebook::react::BorderStyle::Dashed ? D2D1_CAP_STYLE_FLAT : D2D1_CAP_STYLE_ROUND; - const auto strokeStyleProps = D2D1::StrokeStyleProperties( - capStyle, - capStyle, - capStyle, - D2D1_LINE_JOIN_MITER, - 10.0f, - borderStyle == facebook::react::BorderStyle::Dashed ? D2D1_DASH_STYLE_DASH : D2D1_DASH_STYLE_DOT, - 0.0f); - spFactory->CreateStrokeStyle(&strokeStyleProps, nullptr, 0, spStrokeStyle.put()); - } - D2D1::Matrix3x2F originalTransform; - D2D1::Matrix3x2F translationTransform = - D2D1::Matrix3x2F::Translation(-textureRect.left + offset.x, -textureRect.top + offset.y); - - pRT->GetTransform(&originalTransform); - translationTransform = originalTransform * translationTransform; - - pRT->SetTransform(translationTransform); - - DrawShape(pRT, shape, spBorderBrush.get(), strokeWidth, spStrokeStyle.get()); - - pRT->SetTransform(originalTransform); - } -} - -template -void SetBorderLayerProperties( - winrt::Microsoft::ReactNative::Composition::implementation::Theme *theme, - winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext, - winrt::Microsoft::ReactNative::Composition::Experimental::ISpriteVisual &layer, - TShape &shape, - winrt::com_ptr<::Microsoft::ReactNative::Composition::Experimental::ICompositionDrawingSurfaceInterop> - &borderTexture, - const D2D1_RECT_F &textureRect, - facebook::react::Point anchorPoint, - facebook::react::Point anchorOffset, - winrt::Windows::Foundation::Numerics::float2 size, - winrt::Windows::Foundation::Numerics::float2 relativeSizeAdjustment, - FLOAT strokeWidth, - const facebook::react::SharedColor &borderColor, - facebook::react::BorderStyle borderStyle) { - if constexpr (!std::is_base_of_v) { - SetBorderLayerPropertiesCommon( - theme, - compContext, - layer, - shape, - borderTexture, - textureRect, - anchorPoint, - anchorOffset, - size, - relativeSizeAdjustment, - strokeWidth, - borderColor, - borderStyle); - } else { - // if (VisualVersion::IsUseWinCompClippingRegionEnabled()) - { - layer.Offset({anchorOffset.x, anchorOffset.y, 0}, {anchorPoint.x, anchorPoint.y, 0}); - layer.RelativeSizeWithOffset( - {textureRect.right - textureRect.left, textureRect.bottom - textureRect.top}, {0.0f, 0.0f}); - - layer.Brush(theme->Brush(*borderColor)); - - winrt::com_ptr spD2dFactory; - compContext.as<::Microsoft::ReactNative::Composition::Experimental::ICompositionContextInterop>()->D2DFactory( - spD2dFactory.put()); - - winrt::com_ptr transformedShape; - D2D1::Matrix3x2F translationTransform = D2D1::Matrix3x2F::Translation(-textureRect.left, -textureRect.top); - winrt::check_hresult( - spD2dFactory->CreateTransformedGeometry(&shape, &translationTransform, transformedShape.put())); - - layer.as<::Microsoft::ReactNative::Composition::Experimental::IVisualInterop>()->SetClippingPath( - transformedShape.get()); - } - /* - else - { - SetBorderLayerPropertiesCommon(theme, comContext, layer, shape, borderTexture, textureRect, - anchorPoint, anchorOffset, strokeWidth, borderColor, borderStyle); - } - */ - } -} - -namespace AnchorPosition { -const float Left = 0.0; -const float Center = 0.5; -const float Right = 1.0; -const float Top = 0.0; -const float Bottom = 1.0; -} // namespace AnchorPosition - -template -void DrawAllBorderLayers( - winrt::Microsoft::ReactNative::Composition::implementation::Theme *theme, - winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext, - std::array< - winrt::Microsoft::ReactNative::Composition::Experimental::ISpriteVisual, - ComponentView::SpecialBorderLayerCount> &spBorderLayers, - TShape &shape, - const facebook::react::BorderWidths &borderWidths, - const facebook::react::BorderRadii &borderRadii, - float textureWidth, - float textureHeight, - const facebook::react::BorderColors &borderColors, - facebook::react::BorderStyle borderStyle) { - // Now that we've drawn our nice border in one layer, split it into its component layers - winrt::com_ptr<::Microsoft::ReactNative::Composition::Experimental::ICompositionDrawingSurfaceInterop> - spTextures[ComponentView::SpecialBorderLayerCount]; - - // Set component border properties - // Top Left Corner - SetBorderLayerProperties( - theme, - compContext, - spBorderLayers[0], - shape, - spTextures[0], // Target Layer, Source Texture, Target Texture - {0, - 0, - borderRadii.topLeft + borderWidths.left, - borderRadii.topLeft + borderWidths.top}, // Texture Left, Top, Width, Height - {AnchorPosition::Left, AnchorPosition::Top}, // Layer Anchor Point - {0, 0}, // Layer Anchor Offset - {borderRadii.topLeft + borderWidths.left, borderRadii.topLeft + borderWidths.top}, // size - {0.0f, 0.0f}, // relativeSize - std::max(borderWidths.left, borderWidths.top), - borderColors.left ? borderColors.left : borderColors.top, - borderStyle); - - // Top Edge Border - SetBorderLayerProperties( - theme, - compContext, - spBorderLayers[1], - shape, - spTextures[1], - {borderRadii.topLeft + borderWidths.left, - 0, - textureWidth - (borderRadii.topRight + borderWidths.right), - borderWidths.top}, - {AnchorPosition::Left, AnchorPosition::Top}, - {borderRadii.topLeft + borderWidths.left, 0}, - {-(borderRadii.topLeft + borderWidths.left + borderRadii.topRight + borderWidths.right), - borderWidths.top}, // size - {1.0f, 0.0f}, // relativeSize - borderWidths.top, - borderColors.top, - borderStyle); - - // Top Right Corner Border - SetBorderLayerProperties( - theme, - compContext, - spBorderLayers[2], - shape, - spTextures[2], - {textureWidth - (borderRadii.topRight + borderWidths.right), - 0, - textureWidth, - borderRadii.topRight + borderWidths.top}, - {AnchorPosition::Right, AnchorPosition::Top}, - {-(borderRadii.topRight + borderWidths.right), 0}, - {borderRadii.topRight + borderWidths.right, borderRadii.topRight + borderWidths.top}, - {0.0f, 0.0f}, - std::max(borderWidths.right, borderWidths.top), - borderColors.right ? borderColors.right : borderColors.top, - borderStyle); - - // Right Edge Border - SetBorderLayerProperties( - theme, - compContext, - spBorderLayers[3], - shape, - spTextures[3], - {textureWidth - borderWidths.right, - borderWidths.top + borderRadii.topRight, - textureWidth, - textureHeight - (borderWidths.bottom + borderRadii.bottomRight)}, - {AnchorPosition::Right, AnchorPosition::Top}, - {-borderWidths.right, borderWidths.top + borderRadii.topRight}, - {borderWidths.right, - -(borderWidths.top + borderRadii.topRight + borderWidths.bottom + borderRadii.bottomRight)}, // size - {0.0f, 1.0f}, - borderWidths.right, - borderColors.right, - borderStyle); - - // Bottom Right Corner Border - SetBorderLayerProperties( - theme, - compContext, - spBorderLayers[4], - shape, - spTextures[4], - {textureWidth - (borderWidths.right + borderRadii.bottomRight), - textureHeight - (borderWidths.bottom + borderRadii.bottomRight), - textureWidth, - textureHeight}, - {AnchorPosition::Right, AnchorPosition::Bottom}, - {-(borderWidths.right + borderRadii.bottomRight), -(borderWidths.bottom + borderRadii.bottomRight)}, - {borderWidths.right + borderRadii.bottomRight, borderWidths.bottom + borderRadii.bottomRight}, - {0, 0}, - std::max(borderWidths.right, borderWidths.bottom), - borderColors.right ? borderColors.right : borderColors.bottom, - borderStyle); - - // Bottom Edge Border - SetBorderLayerProperties( - theme, - compContext, - spBorderLayers[5], - shape, - spTextures[5], - {borderWidths.left + borderRadii.bottomLeft, - textureHeight - borderWidths.bottom, - textureWidth - (borderWidths.right + borderRadii.bottomRight), - textureHeight}, - {AnchorPosition::Left, AnchorPosition::Bottom}, - {borderWidths.left + borderRadii.bottomLeft, -borderWidths.bottom}, - {-(borderWidths.right + borderRadii.bottomLeft + borderWidths.left + borderRadii.bottomRight), - borderWidths.bottom}, - {1.0f, 0.0f}, - borderWidths.bottom, - borderColors.bottom, - borderStyle); - - // Bottom Left Corner Border - SetBorderLayerProperties( - theme, - compContext, - spBorderLayers[6], - shape, - spTextures[6], - {0, - textureHeight - (borderWidths.bottom + borderRadii.bottomLeft), - borderWidths.left + borderRadii.bottomLeft, - textureHeight}, - {AnchorPosition::Left, AnchorPosition::Bottom}, - {0, -(borderWidths.bottom + borderRadii.bottomLeft)}, - {borderWidths.left + borderRadii.bottomLeft, borderWidths.bottom + borderRadii.bottomLeft}, - {0, 0}, - std::max(borderWidths.left, borderWidths.bottom), - borderColors.left ? borderColors.left : borderColors.bottom, - borderStyle); - - // Left Edge Border - SetBorderLayerProperties( - theme, - compContext, - spBorderLayers[7], - shape, - spTextures[7], - {0, - borderWidths.top + borderRadii.topLeft, - borderWidths.left, - textureHeight - (borderWidths.bottom + borderRadii.bottomLeft)}, - {AnchorPosition::Left, AnchorPosition::Top}, - {0, borderWidths.top + borderRadii.topLeft}, - {borderWidths.left, -(borderWidths.top + borderRadii.topLeft + borderWidths.bottom + borderRadii.bottomLeft)}, - {0, 1}, - borderWidths.left, - borderColors.left, - borderStyle); -} - -winrt::com_ptr GetGeometryForRoundedBorder( - winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext, - const facebook::react::RectangleCorners &radius, - const facebook::react::RectangleEdges &inset, - const facebook::react::RectangleEdges &thickness, - const facebook::react::RectangleEdges &rectPathGeometry) noexcept { - winrt::com_ptr outerPathGeometry = - GenerateRoundedRectPathGeometry(compContext, radius, inset, rectPathGeometry); - - if (outerPathGeometry == nullptr) { - assert(false); - return nullptr; - } - - facebook::react::RectangleEdges rectInnerPathGeometry = { - rectPathGeometry.left + thickness.left, - rectPathGeometry.top + thickness.top, - rectPathGeometry.right - thickness.right, - rectPathGeometry.bottom - thickness.bottom}; - - // Total thickness is larger than original element size. - // Clamp inner rect to have a width/height of 0, but placed such that the ratio of side-thicknesses is respected. - // We need to respect this ratio so that any animations work properly. - - if (rectInnerPathGeometry.left > rectInnerPathGeometry.right) { - float leftRatio = thickness.left / (thickness.left + thickness.right); - auto x = std::floor(rectPathGeometry.left + ((rectPathGeometry.right - rectPathGeometry.left) * leftRatio)); - rectInnerPathGeometry.left = x; - rectInnerPathGeometry.right = x; - } - - if (rectInnerPathGeometry.top > rectInnerPathGeometry.bottom) { - float topRatio = thickness.top / (thickness.top + thickness.bottom); - auto y = rectPathGeometry.top + std::floor((rectPathGeometry.top - rectPathGeometry.bottom) * topRatio); - rectInnerPathGeometry.top = y; - rectInnerPathGeometry.bottom = y; - } - - facebook::react::RectangleEdges innerInset = { - inset.left + thickness.left, - inset.top + thickness.top, - inset.right + thickness.right, - inset.bottom + thickness.bottom}; - - winrt::com_ptr innerPathGeometry = - GenerateRoundedRectPathGeometry(compContext, radius, innerInset, rectInnerPathGeometry); - - if (innerPathGeometry == nullptr) { - assert(false); // Failed to create inner pathGeometry for rounded border - return nullptr; - } - - ID2D1Geometry *ppGeometries[] = {outerPathGeometry.get(), innerPathGeometry.get()}; - winrt::com_ptr spD2dFactory; - compContext.as<::Microsoft::ReactNative::Composition::Experimental::ICompositionContextInterop>()->D2DFactory( - spD2dFactory.put()); - - winrt::com_ptr geometryGroup = nullptr; - // Create a geometry group. - HRESULT hr = spD2dFactory->CreateGeometryGroup( - D2D1_FILL_MODE_ALTERNATE, ppGeometries, ARRAYSIZE(ppGeometries), geometryGroup.put()); - - if (SUCCEEDED(hr)) { - return geometryGroup; - } - return nullptr; -} - -// We don't want half pixel borders, or border radii - they lead to blurry borders -// Also apply scale factor to the radii at this point -void pixelRoundBorderRadii(facebook::react::BorderRadii &borderRadii, float scaleFactor) noexcept { - // Always round radii down to avoid spikey circles - borderRadii.topLeft = std::floor(borderRadii.topLeft * scaleFactor); - borderRadii.topRight = std::floor(borderRadii.topRight * scaleFactor); - borderRadii.bottomLeft = std::floor(borderRadii.bottomLeft * scaleFactor); - borderRadii.bottomRight = std::floor(borderRadii.bottomRight * scaleFactor); -} - -void scaleAndPixelRoundBorderWidths( - facebook::react::LayoutMetrics const &layoutMetrics, - facebook::react::BorderMetrics &borderMetrics, - float scaleFactor) noexcept { - borderMetrics.borderWidths.left = (borderMetrics.borderWidths.left == 0) - ? 0.f - : std::max(1.f, std::round(borderMetrics.borderWidths.left * scaleFactor)); - borderMetrics.borderWidths.top = (borderMetrics.borderWidths.top == 0) - ? 0.f - : std::max(1.f, std::round(borderMetrics.borderWidths.top * scaleFactor)); - borderMetrics.borderWidths.right = (borderMetrics.borderWidths.right == 0) - ? 0.f - : std::max(1.f, std::round(borderMetrics.borderWidths.right * scaleFactor)); - borderMetrics.borderWidths.bottom = (borderMetrics.borderWidths.bottom == 0) - ? 0.f - : std::max(1.f, std::round(borderMetrics.borderWidths.bottom * scaleFactor)); - - // If we rounded both sides of the borderWidths up, we may have made the borderWidths larger than the total - if (layoutMetrics.frame.size.width * scaleFactor < - (borderMetrics.borderWidths.left + borderMetrics.borderWidths.right)) { - borderMetrics.borderWidths.right--; - } - if (layoutMetrics.frame.size.height * scaleFactor < - (borderMetrics.borderWidths.top + borderMetrics.borderWidths.bottom)) { - borderMetrics.borderWidths.bottom--; - } -} - -// react-native uses black as a default color when none is specified. -void assignDefaultBlackBorders(facebook::react::BorderMetrics &borderMetrics) noexcept { - if (!borderMetrics.borderColors.left) { - borderMetrics.borderColors.left = facebook::react::blackColor(); - } - if (!borderMetrics.borderColors.top) { - borderMetrics.borderColors.top = facebook::react::blackColor(); - } - if (!borderMetrics.borderColors.right) { - borderMetrics.borderColors.right = facebook::react::blackColor(); - } - if (!borderMetrics.borderColors.bottom) { - borderMetrics.borderColors.bottom = facebook::react::blackColor(); - } +winrt::Microsoft::ReactNative::Composition::Experimental::IVisual ComponentView::OuterVisual() const noexcept { + return m_outerVisual ? m_outerVisual : Visual(); } -facebook::react::BorderMetrics resolveAndAlignBorderMetrics( - facebook::react::LayoutMetrics const &layoutMetrics, - const facebook::react::ViewProps &viewProps) noexcept { - auto borderMetrics = viewProps.resolveBorderMetrics(layoutMetrics); - - pixelRoundBorderRadii(borderMetrics.borderRadii, layoutMetrics.pointScaleFactor); - scaleAndPixelRoundBorderWidths(layoutMetrics, borderMetrics, layoutMetrics.pointScaleFactor); - assignDefaultBlackBorders(borderMetrics); - return borderMetrics; +facebook::react::LayoutMetrics ComponentView::focusLayoutMetrics(bool inner) const noexcept { + facebook::react::LayoutMetrics layoutMetrics = m_layoutMetrics; + layoutMetrics.frame.origin.x -= FOCUS_VISUAL_WIDTH * (inner ? 1 : 2); + layoutMetrics.frame.origin.y -= FOCUS_VISUAL_WIDTH * (inner ? 1 : 2); + layoutMetrics.frame.size.height += FOCUS_VISUAL_WIDTH * (inner ? 2 : 4); + layoutMetrics.frame.size.width += FOCUS_VISUAL_WIDTH * (inner ? 2 : 4); + return layoutMetrics; +} + +facebook::react::BorderMetrics ComponentView::focusBorderMetrics( + bool inner, + const facebook::react::LayoutMetrics &layoutMetrics) const noexcept { + facebook::react::BorderMetrics metrics = BorderPrimitive::resolveAndAlignBorderMetrics(layoutMetrics, *viewProps()); + facebook::react::Color innerColor; + innerColor.m_color = {1, 0, 0, 0}; + innerColor.m_platformColor.push_back(inner ? "FocusVisualSecondary" : "FocusVisualPrimary"); + metrics.borderColors.bottom = metrics.borderColors.left = metrics.borderColors.right = metrics.borderColors.top = + innerColor; + if (metrics.borderRadii.bottomLeft != 0) + metrics.borderRadii.bottomLeft += FOCUS_VISUAL_WIDTH * (inner ? 1 : 2); + if (metrics.borderRadii.bottomRight != 0) + metrics.borderRadii.bottomRight += FOCUS_VISUAL_WIDTH * (inner ? 1 : 2); + if (metrics.borderRadii.topLeft != 0) + metrics.borderRadii.topLeft += FOCUS_VISUAL_WIDTH * (inner ? 1 : 2); + if (metrics.borderRadii.topRight != 0) + metrics.borderRadii.topRight += FOCUS_VISUAL_WIDTH * (inner ? 1 : 2); + + metrics.borderStyles.bottom = metrics.borderStyles.left = metrics.borderStyles.right = metrics.borderStyles.top = + facebook::react::BorderStyle::Solid; + metrics.borderWidths.bottom = metrics.borderWidths.left = metrics.borderWidths.right = metrics.borderWidths.top = + FOCUS_VISUAL_WIDTH * layoutMetrics.pointScaleFactor; + return metrics; } -bool ComponentView::TryUpdateSpecialBorderLayers( - winrt::Microsoft::ReactNative::Composition::implementation::Theme *theme, - std::array - &spBorderVisuals, - facebook::react::LayoutMetrics const &layoutMetrics, - const facebook::react::ViewProps &viewProps) noexcept { - auto borderMetrics = resolveAndAlignBorderMetrics(layoutMetrics, viewProps); - // We only handle a single borderStyle for now - auto borderStyle = borderMetrics.borderStyles.left; - - bool hasMeaningfulColor = - !borderMetrics.borderColors.isUniform() || !facebook::react::isColorMeaningful(borderMetrics.borderColors.left); - bool hasMeaningfulWidth = !borderMetrics.borderWidths.isUniform() || (borderMetrics.borderWidths.left != 0); - if (!hasMeaningfulColor && !hasMeaningfulWidth) { - return false; - } - - // Create the special border layers if they don't exist yet - if (!spBorderVisuals[0]) { - for (uint8_t i = 0; i < SpecialBorderLayerCount; i++) { - auto visual = m_compContext.CreateSpriteVisual(); - Visual().InsertAt(visual, i); - spBorderVisuals[i] = std::move(visual); - m_numBorderVisuals++; - } - } - - float extentWidth = layoutMetrics.frame.size.width * layoutMetrics.pointScaleFactor; - float extentHeight = layoutMetrics.frame.size.height * layoutMetrics.pointScaleFactor; - - if (borderMetrics.borderRadii.topLeft != 0 || borderMetrics.borderRadii.topRight != 0 || - borderMetrics.borderRadii.bottomLeft != 0 || borderMetrics.borderRadii.bottomRight != 0) { - if (borderStyle == facebook::react::BorderStyle::Dotted || borderStyle == facebook::react::BorderStyle::Dashed) { - // Because in DirectX geometry starts at the center of the stroke, we need to deflate - // rectangle by half the stroke width to render correctly. - facebook::react::RectangleEdges rectPathGeometry = { - borderMetrics.borderWidths.left / 2.0f, - borderMetrics.borderWidths.top / 2.0f, - extentWidth - borderMetrics.borderWidths.right / 2.0f, - extentHeight - borderMetrics.borderWidths.bottom / 2.0f}; - - winrt::com_ptr pathGeometry = - GenerateRoundedRectPathGeometry(m_compContext, borderMetrics.borderRadii, {0, 0, 0, 0}, rectPathGeometry); - - if (pathGeometry) { - DrawAllBorderLayers( - theme, - m_compContext, - spBorderVisuals, - *pathGeometry, - borderMetrics.borderWidths, - borderMetrics.borderRadii, - extentWidth, - extentHeight, - borderMetrics.borderColors, - borderStyle); - } else { - assert(false); +void ComponentView::showFocusVisual(bool show) noexcept { + if ((m_flags & ComponentViewFeatures::FocusVisual) == ComponentViewFeatures::FocusVisual) { + if (show && !m_showingFocusVisual) { + m_showingFocusVisual = true; + + m_focusVisual.IsVisible(true); + assert(viewProps()->enableFocusRing); + if (!m_focusInnerPrimitive) { + m_focusInnerPrimitive = std::make_shared(*this); + m_focusVisual.InsertAt(m_focusInnerPrimitive->RootVisual(), 0); } - } else { - facebook::react::RectangleEdges rectPathGeometry = {0, 0, extentWidth, extentHeight}; - - winrt::com_ptr pathGeometry = GetGeometryForRoundedBorder( - m_compContext, - borderMetrics.borderRadii, - {0, 0, 0, 0}, // inset - borderMetrics.borderWidths, - rectPathGeometry); - - DrawAllBorderLayers( - theme, - m_compContext, - spBorderVisuals, - *pathGeometry, - borderMetrics.borderWidths, - borderMetrics.borderRadii, - extentWidth, - extentHeight, - borderMetrics.borderColors, - borderStyle); - } - } else { - // Because in DirectX geometry starts at the center of the stroke, we need to deflate rectangle by half the stroke - // width / height to render correctly. - D2D1_RECT_F rectShape{ - borderMetrics.borderWidths.left / 2.0f, - borderMetrics.borderWidths.top / 2.0f, - extentWidth - (borderMetrics.borderWidths.right / 2.0f), - extentHeight - (borderMetrics.borderWidths.bottom / 2.0f)}; - DrawAllBorderLayers( - theme, - m_compContext, - spBorderVisuals, - rectShape, - borderMetrics.borderWidths, - borderMetrics.borderRadii, - extentWidth, - extentHeight, - borderMetrics.borderColors, - borderStyle); - } - return true; -} - -void ComponentView::finalizeBorderUpdates( - facebook::react::LayoutMetrics const &layoutMetrics, - const facebook::react::ViewProps &viewProps) noexcept { - if (!m_needsBorderUpdate || theme()->IsEmpty()) { - return; - } - - m_needsBorderUpdate = false; - auto spBorderLayers = FindSpecialBorderLayers(); - - if (!TryUpdateSpecialBorderLayers(theme(), spBorderLayers, layoutMetrics, viewProps)) { - for (auto &spBorderLayer : spBorderLayers) { - if (spBorderLayer) { - spBorderLayer.as().Brush(nullptr); + if (!m_focusOuterPrimitive) { + m_focusOuterPrimitive = std::make_shared(*this); + m_focusVisual.InsertAt(m_focusOuterPrimitive->RootVisual(), 0); + } + updateFocusLayoutMetrics(m_layoutMetrics); + auto innerFocusMetrics = focusLayoutMetrics(true /*inner*/); + m_focusInnerPrimitive->finalize(innerFocusMetrics, focusBorderMetrics(true /*inner*/, innerFocusMetrics)); + auto outerFocusMetrics = focusLayoutMetrics(false /*inner*/); + m_focusOuterPrimitive->finalize(outerFocusMetrics, focusBorderMetrics(false /*inner*/, outerFocusMetrics)); + } else if (!show && m_showingFocusVisual) { + m_showingFocusVisual = false; + m_focusVisual.IsVisible(false); + if (m_focusInnerPrimitive) { + m_focusInnerPrimitive->markNeedsUpdate(); + auto innerFocusMetrics = focusLayoutMetrics(true /*inner*/); + m_focusInnerPrimitive->finalize(innerFocusMetrics, focusBorderMetrics(true /*inner*/, innerFocusMetrics)); + } + if (m_focusOuterPrimitive) { + m_focusOuterPrimitive->markNeedsUpdate(); + auto outerFocusMetrics = focusLayoutMetrics(false /*inner*/); + m_focusOuterPrimitive->finalize(outerFocusMetrics, focusBorderMetrics(false /*inner*/, outerFocusMetrics)); } } } } -winrt::Microsoft::ReactNative::Composition::Experimental::IVisual ComponentView::OuterVisual() const noexcept { - return m_outerVisual ? m_outerVisual : Visual(); -} - -void ComponentView::showFocusVisual(bool show) noexcept { - if (show) { - assert(m_enableFocusVisual); - m_focusVisual.IsFocused(true); - } else { - m_focusVisual.IsFocused(false); - } -} - -void ComponentView::updateBorderProps( - const facebook::react::ViewProps &oldViewProps, - const facebook::react::ViewProps &newViewProps) noexcept { - if (oldViewProps.borderColors != newViewProps.borderColors || oldViewProps.borderRadii != newViewProps.borderRadii || - !(oldViewProps.yogaStyle.border(facebook::yoga::Edge::All) == - newViewProps.yogaStyle.border(facebook::yoga::Edge::All)) || - oldViewProps.borderStyles != newViewProps.borderStyles) { - m_needsBorderUpdate = true; - } - - m_enableFocusVisual = newViewProps.enableFocusRing; - if (!m_enableFocusVisual) { - showFocusVisual(false); - } -} - void ComponentView::updateShadowProps( const facebook::react::ViewProps &oldViewProps, const facebook::react::ViewProps &newViewProps) noexcept { @@ -1240,11 +543,13 @@ void ComponentView::updateShadowProps( void ComponentView::applyShadowProps(const facebook::react::ViewProps &viewProps) noexcept { auto shadow = m_compContext.CreateDropShadow(); + shadow.Offset({viewProps.shadowOffset.width, viewProps.shadowOffset.height, 0}); shadow.Opacity(viewProps.shadowOpacity); shadow.BlurRadius(viewProps.shadowRadius); if (viewProps.shadowColor) shadow.Color(theme()->Color(*viewProps.shadowColor)); + Visual().as().Shadow(shadow); } @@ -1357,16 +662,16 @@ void ComponentView::Toggle() noexcept { // no-op } -void ComponentView::updateBorderLayoutMetrics( +void ComponentView::updateClippingPath( facebook::react::LayoutMetrics const &layoutMetrics, const facebook::react::ViewProps &viewProps) noexcept { - auto borderMetrics = resolveAndAlignBorderMetrics(layoutMetrics, viewProps); + auto borderMetrics = BorderPrimitive::resolveAndAlignBorderMetrics(layoutMetrics, viewProps); if (borderMetrics.borderRadii.topLeft == 0 && borderMetrics.borderRadii.topRight == 0 && borderMetrics.borderRadii.bottomLeft == 0 && borderMetrics.borderRadii.bottomRight == 0) { Visual().as<::Microsoft::ReactNative::Composition::Experimental::IVisualInterop>()->SetClippingPath(nullptr); } else { - winrt::com_ptr pathGeometry = GenerateRoundedRectPathGeometry( + winrt::com_ptr pathGeometry = BorderPrimitive::GenerateRoundedRectPathGeometry( m_compContext, borderMetrics.borderRadii, {0, 0, 0, 0}, @@ -1378,24 +683,12 @@ void ComponentView::updateBorderLayoutMetrics( Visual().as<::Microsoft::ReactNative::Composition::Experimental::IVisualInterop>()->SetClippingPath( pathGeometry.get()); } - - if (m_layoutMetrics != layoutMetrics) { - m_needsBorderUpdate = true; - } - - m_focusVisual.ScaleFactor(layoutMetrics.pointScaleFactor); - OuterVisual().Size( - {layoutMetrics.frame.size.width * layoutMetrics.pointScaleFactor, - layoutMetrics.frame.size.height * layoutMetrics.pointScaleFactor}); - OuterVisual().Offset({ - layoutMetrics.frame.origin.x * layoutMetrics.pointScaleFactor, - layoutMetrics.frame.origin.y * layoutMetrics.pointScaleFactor, - 0.0f, - }); } void ComponentView::indexOffsetForBorder(uint32_t &index) const noexcept { - index += m_numBorderVisuals; + if (m_borderPrimitive) { + index += m_borderPrimitive->numberOfVisuals(); + } } void ComponentView::OnRenderingDeviceLost() noexcept {} @@ -1564,6 +857,11 @@ winrt::Microsoft::ReactNative::ComponentView ViewComponentView::Create( ViewComponentView::defaultProps(), compContext, tag, reactContext, ComponentViewFeatures::Default); } +winrt::Microsoft::ReactNative::Composition::Experimental::IVisual +ViewComponentView::VisualToMountChildrenInto() noexcept { + return Visual(); +} + void ViewComponentView::MountChildComponentView( const winrt::Microsoft::ReactNative::ComponentView &childComponentView, uint32_t index) noexcept { @@ -1583,7 +881,7 @@ void ViewComponentView::MountChildComponentView( } } } - Visual().InsertAt(compositionChild->OuterVisual(), visualIndex); + VisualToMountChildrenInto().InsertAt(compositionChild->OuterVisual(), visualIndex); } else { m_hasNonVisualChildren = true; } @@ -1596,7 +894,7 @@ void ViewComponentView::UnmountChildComponentView( indexOffsetForBorder(index); if (auto compositionChild = childComponentView.try_as()) { - Visual().Remove(compositionChild->OuterVisual()); + VisualToMountChildrenInto().Remove(compositionChild->OuterVisual()); } } @@ -1898,6 +1196,9 @@ winrt::Microsoft::ReactNative::ComponentView lastDeepChild( return current; } +// Walks the tree calling the function fn on each node. +// If fn returns true, then walkTree stops itterating over the tree, and returns true. +// If the tree walk completes without fn returning true, then walkTree returns false. bool walkTree( const winrt::Microsoft::ReactNative::ComponentView &view, bool forward, diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h index f0e5f584b3e..cff3775239c 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h @@ -8,6 +8,7 @@ #include #include #include +#include "BorderPrimitive.h" #include "CompositionHelpers.h" #include "Composition.ComponentView.g.h" @@ -23,8 +24,6 @@ struct ComponentView : public ComponentViewT< ComponentView, winrt::Microsoft::ReactNative::implementation::ComponentView, winrt::Microsoft::ReactNative::Composition::Experimental::IInternalComponentView> { - static constexpr size_t SpecialBorderLayerCount = 8; - ComponentView( const winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext, facebook::react::Tag tag, @@ -56,6 +55,7 @@ struct ComponentView : public ComponentViewT< void onGotFocus(const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs &args) noexcept override; bool CapturePointer(const winrt::Microsoft::ReactNative::Composition::Input::Pointer &pointer) noexcept; void ReleasePointerCapture(const winrt::Microsoft::ReactNative::Composition::Input::Pointer &pointer) noexcept; + void SetViewFeatures(ComponentViewFeatures viewFeatures) noexcept; std::vector supplementalComponentDescriptorProviders() noexcept override; @@ -123,39 +123,33 @@ struct ComponentView : public ComponentViewT< winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext m_compContext; comp::CompositionPropertySet m_centerPropSet{nullptr}; facebook::react::SharedViewEventEmitter m_eventEmitter; - bool m_needsBorderUpdate{false}; - bool m_hasTransformMatrixFacade{false}; - bool m_enableFocusVisual{false}; - uint8_t m_numBorderVisuals{0}; private: - void updateBorderProps( - const facebook::react::ViewProps &oldViewProps, - const facebook::react::ViewProps &newViewProps) noexcept; - void updateBorderLayoutMetrics( + void updateFocusLayoutMetrics(facebook::react::LayoutMetrics const &layoutMetrics) noexcept; + void updateClippingPath( facebook::react::LayoutMetrics const &layoutMetrics, const facebook::react::ViewProps &viewProps) noexcept; - void finalizeBorderUpdates( + void finalizeFocusVisual( facebook::react::LayoutMetrics const &layoutMetrics, const facebook::react::ViewProps &viewProps) noexcept; - bool TryUpdateSpecialBorderLayers( - winrt::Microsoft::ReactNative::Composition::implementation::Theme *theme, - std::array - &spBorderVisuals, - facebook::react::LayoutMetrics const &layoutMetrics, - const facebook::react::ViewProps &viewProps) noexcept; - std::array - FindSpecialBorderLayers() const noexcept; void UpdateCenterPropertySet() noexcept; void FinalizeTransform( facebook::react::LayoutMetrics const &layoutMetrics, const facebook::react::ViewProps &viewProps) noexcept; + facebook::react::LayoutMetrics focusLayoutMetrics(bool inner) const noexcept; + facebook::react::BorderMetrics focusBorderMetrics(bool inner, const facebook::react::LayoutMetrics &layoutMetrics) + const noexcept; - bool m_FinalizeTransform{false}; - bool m_tooltipTracked{false}; + bool m_hasTransformMatrixFacade : 1 {false}; + bool m_showingFocusVisual : 1 {false}; + bool m_FinalizeTransform : 1 {false}; + bool m_tooltipTracked : 1 {false}; ComponentViewFeatures m_flags; void showFocusVisual(bool show) noexcept; - winrt::Microsoft::ReactNative::Composition::Experimental::IFocusVisual m_focusVisual{nullptr}; + std::shared_ptr m_borderPrimitive; + std::shared_ptr m_focusInnerPrimitive; + std::shared_ptr m_focusOuterPrimitive; + winrt::Microsoft::ReactNative::Composition::Experimental::IVisual m_focusVisual{nullptr}; winrt::Microsoft::ReactNative::Composition::Experimental::IVisual m_outerVisual{nullptr}; winrt::event> m_themeChangedEvent; }; @@ -169,6 +163,7 @@ struct ViewComponentView : public ViewComponentViewT< facebook::react::Tag tag, winrt::Microsoft::ReactNative::ReactContext const &reactContext) noexcept; + virtual winrt::Microsoft::ReactNative::Composition::Experimental::IVisual VisualToMountChildrenInto() noexcept; void MountChildComponentView( const winrt::Microsoft::ReactNative::ComponentView &childComponentView, uint32_t index) noexcept override; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/DebuggingOverlayComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/DebuggingOverlayComponentView.cpp index f49951ecd31..fd84303f8e1 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/DebuggingOverlayComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/DebuggingOverlayComponentView.cpp @@ -21,7 +21,7 @@ DebuggingOverlayComponentView::DebuggingOverlayComponentView( reactContext, ComponentViewFeatures::Default & ~(ComponentViewFeatures::Background | ComponentViewFeatures::ShadowProps | - ComponentViewFeatures::NativeBorder)) {} + ComponentViewFeatures::NativeBorder | ComponentViewFeatures::FocusVisual)) {} void DebuggingOverlayComponentView::MountChildComponentView( const winrt::Microsoft::ReactNative::ComponentView &childComponentView, diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp index 90ea200458c..d4be4afbd20 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp @@ -10,8 +10,16 @@ #include "../CompositionDynamicAutomationProvider.h" #include "Unicode.h" +#include +#include #include #include +#include +#include +#include +#include +#include +#include #include #include "IReactContext.h" #include "ReactHost/ReactInstanceWin.h" @@ -22,13 +30,15 @@ WindowsModalHostComponentView::WindowsModalHostComponentView( const winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext, facebook::react::Tag tag, winrt::Microsoft::ReactNative::ReactContext const &reactContext) - : Super( - WindowsModalHostComponentView::defaultProps(), - compContext, - tag, - reactContext, - ComponentViewFeatures::Default & ~ComponentViewFeatures::Background) { - m_context = reactContext; // save context + : Super(compContext, tag, reactContext) {} + +WindowsModalHostComponentView::~WindowsModalHostComponentView() { + // Check if the window handle (m_hwnd) exists and destroy it if necessary + if (m_hwnd) { + // Close/Destroy the modal window + SendMessage(m_hwnd, WM_DESTROY, 0, 0); + m_hwnd = nullptr; + } } winrt::Microsoft::ReactNative::ComponentView WindowsModalHostComponentView::Create( @@ -38,34 +48,35 @@ winrt::Microsoft::ReactNative::ComponentView WindowsModalHostComponentView::Crea return winrt::make(compContext, tag, reactContext); } -// constants for creating a new windows (code mostly taken from LogBox) +// constants for creating a new windows constexpr PCWSTR c_modalWindowClassName = L"MS_REACTNATIVE_MODAL"; constexpr auto CompHostProperty = L"CompHost"; -const int MODAL_DEFAULT_WIDTH = 500; -const int MODAL_DEFAULT_HEIGHT = 500; +const int MODAL_MIN_WIDTH = 50; +const int MODAL_MIN_HEIGHT = 50; + +float ScaleFactor(HWND hwnd) noexcept { + return GetDpiForWindow(hwnd) / static_cast(USER_DEFAULT_SCREEN_DPI); +} // creates a new modal window void WindowsModalHostComponentView::EnsureModalCreated() { auto host = - winrt::Microsoft::ReactNative::implementation::ReactNativeHost::GetReactNativeHost(m_context.Properties()); + winrt::Microsoft::ReactNative::implementation::ReactNativeHost::GetReactNativeHost(m_reactContext.Properties()); + // return if hwnd already exists if (!host || m_hwnd) { return; } - RegisterWndClass(); // creates and register a windows class - auto CompositionHwndHost = winrt::Microsoft::ReactNative::CompositionHwndHost(); - winrt::Microsoft::ReactNative::ReactViewOptions viewOptions; - viewOptions.ComponentName(L"Modal"); - CompositionHwndHost.ReactViewHost(winrt::Microsoft::ReactNative::ReactCoreInjection::MakeViewHost(host, viewOptions)); + RegisterWndClass(); + HINSTANCE hInstance = GetModuleHandle(NULL); - winrt::impl::abi::type *pHost{nullptr}; winrt::com_ptr<::IUnknown> spunk; - CompositionHwndHost.as(spunk); // get the root hwnd - auto roothwnd = reinterpret_cast( - winrt::Microsoft::ReactNative::ReactCoreInjection::GetTopLevelWindowId(m_context.Properties().Handle())); + m_prevWindowID = + winrt::Microsoft::ReactNative::ReactCoreInjection::GetTopLevelWindowId(m_reactContext.Properties().Handle()); + auto roothwnd = reinterpret_cast(m_prevWindowID); m_hwnd = CreateWindow( c_modalWindowClassName, @@ -73,8 +84,8 @@ void WindowsModalHostComponentView::EnsureModalCreated() { WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, - MODAL_DEFAULT_WIDTH, - MODAL_DEFAULT_HEIGHT, + MODAL_MIN_WIDTH, + MODAL_MIN_HEIGHT, roothwnd, // parent nullptr, hInstance, @@ -85,11 +96,47 @@ void WindowsModalHostComponentView::EnsureModalCreated() { throw std::exception("Failed to create new hwnd for Modal: " + GetLastError()); } + // Disable user sizing of the hwnd + ::SetWindowLong(m_hwnd, GWL_STYLE, GetWindowLong(m_hwnd, GWL_STYLE) & ~WS_SIZEBOX); + + // set the top-level windows as the new hwnd + winrt::Microsoft::ReactNative::ReactCoreInjection::SetTopLevelWindowId( + host.InstanceSettings().Properties(), reinterpret_cast(m_hwnd)); + + // get current compositor - handles the creation/manipulation of visual objects + auto compositionContext = CompositionContext(); + auto compositor = + winrt::Microsoft::ReactNative::Composition::Experimental::MicrosoftCompositionContextHelper::InnerCompositor( + compositionContext); + + // create a react native island - code taken from CompositionHwndHost + auto bridge = winrt::Microsoft::UI::Content::DesktopChildSiteBridge::Create( + compositor, winrt::Microsoft::UI::GetWindowIdFromWindow(m_hwnd)); + m_reactNativeIsland = winrt::Microsoft::ReactNative::ReactNativeIsland(compositor, m_reactContext.Handle(), *this); + auto contentIsland = m_reactNativeIsland.Island(); + bridge.Connect(contentIsland); + bridge.Show(); + + // set ScaleFactor + ScaleFactor(m_hwnd); + + // set layout contraints + winrt::Microsoft::ReactNative::LayoutConstraints constraints; + constraints.LayoutDirection = winrt::Microsoft::ReactNative::LayoutDirection::Undefined; + + RECT rc; + GetClientRect(roothwnd, &rc); + // Maximum size is set to size of parent hwnd + constraints.MaximumSize = {(rc.right - rc.left) * ScaleFactor(m_hwnd), (rc.bottom - rc.top) / ScaleFactor(m_hwnd)}; + constraints.MinimumSize = {MODAL_MIN_WIDTH * ScaleFactor(m_hwnd), MODAL_MIN_HEIGHT * ScaleFactor(m_hwnd)}; + m_reactNativeIsland.Arrange(constraints, {0, 0}); + bridge.ResizePolicy(winrt::Microsoft::UI::Content::ContentSizePolicy::ResizeContentToParentWindow); + spunk.detach(); } void WindowsModalHostComponentView::ShowOnUIThread() { - if (m_hwnd) { + if (m_hwnd && !IsWindowVisible(m_hwnd)) { ShowWindow(m_hwnd, SW_NORMAL); BringWindowToTop(m_hwnd); SetFocus(m_hwnd); @@ -98,7 +145,15 @@ void WindowsModalHostComponentView::ShowOnUIThread() { void WindowsModalHostComponentView::HideOnUIThread() noexcept { if (m_hwnd) { - ::ShowWindow(m_hwnd, SW_HIDE); + SendMessage(m_hwnd, WM_CLOSE, 0, 0); + } + + // reset the topWindowID + if (m_prevWindowID) { + auto host = + winrt::Microsoft::ReactNative::implementation::ReactNativeHost::GetReactNativeHost(m_reactContext.Properties()); + winrt::Microsoft::ReactNative::ReactCoreInjection::SetTopLevelWindowId( + host.InstanceSettings().Properties(), m_prevWindowID); } } @@ -121,23 +176,22 @@ LRESULT CALLBACK ModalBoxWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM } switch (message) { - case WM_NCCREATE: { // sent before WM_CREATE, lparam should be identical to members of CreateWindowEx + case WM_NCCREATE: { // called before WM_CREATE, lparam should be identical to members of CreateWindowEx auto createStruct = reinterpret_cast(lparam); // CreateStruct data = static_cast<::IUnknown *>(createStruct->lpCreateParams); SetProp(hwnd, CompHostProperty, data); // adds new properties to window break; } - case WM_CREATE: { // recieves after window is created but before visible - // host.Initialize((uint64_t)hwnd); cause Modal to throw a not registered error - break; - } case WM_CLOSE: { // Just hide the window instead of destroying it ::ShowWindow(hwnd, SW_HIDE); return 0; } case WM_DESTROY: { // called when we want to destroy the window - data->Release(); + ::ShowWindow(hwnd, SW_HIDE); + if (data) { + data->Release(); + } SetProp(hwnd, CompHostProperty, nullptr); break; } @@ -174,121 +228,74 @@ void WindowsModalHostComponentView::RegisterWndClass() noexcept { registered = true; } +winrt::Microsoft::ReactNative::Composition::Experimental::IVisual +WindowsModalHostComponentView::VisualToMountChildrenInto() noexcept { + return m_reactNativeIsland + .as() + .InternalRootVisual(); +} + +// childComponentView - reference to the child component view +// index - the position in which the childComponentView should be mounted void WindowsModalHostComponentView::MountChildComponentView( const winrt::Microsoft::ReactNative::ComponentView &childComponentView, uint32_t index) noexcept { - // Disabled due to partial Modal implementation. Tracking re-enablement with task list here: - // https://github.com/microsoft/react-native-windows/issues/11157 assert(false); + EnsureModalCreated(); base_type::MountChildComponentView(childComponentView, index); } void WindowsModalHostComponentView::UnmountChildComponentView( const winrt::Microsoft::ReactNative::ComponentView &childComponentView, uint32_t index) noexcept { - // Disabled due to partial Modal implementation.Tracking re-enablement with task list here : https : // - // github.com/microsoft/react-native-windows/issues/11157 assert(false); base_type::UnmountChildComponentView(childComponentView, index); } -void WindowsModalHostComponentView::HandleCommand( - const winrt::Microsoft::ReactNative::HandleCommandArgs &args) noexcept { - Super::HandleCommand(args); +void WindowsModalHostComponentView::updateLayoutMetrics( + facebook::react::LayoutMetrics const &layoutMetrics, + facebook::react::LayoutMetrics const &oldLayoutMetrics) noexcept { + base_type::updateLayoutMetrics(layoutMetrics, oldLayoutMetrics); + if (m_hwnd) { + EnsureModalCreated(); + AdjustWindowSize(); + ShowOnUIThread(); + } } +void WindowsModalHostComponentView::AdjustWindowSize() noexcept { + if (m_layoutMetrics.overflowInset.right == 0 && m_layoutMetrics.overflowInset.bottom == 0) { + return; + } + // Modal's size is based on it's children, use the overflow to calculate the width/height + float xPos = (-m_layoutMetrics.overflowInset.right * (m_layoutMetrics.pointScaleFactor)); + float yPos = (-m_layoutMetrics.overflowInset.bottom * (m_layoutMetrics.pointScaleFactor)); + RECT rc; + GetClientRect(m_hwnd, &rc); + RECT rect = {0, 0, (int)xPos, (int)yPos}; + AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, FALSE); // Adjust for title bar and borders + MoveWindow(m_hwnd, 0, 0, (int)(rect.right - rect.left), (int)(rect.bottom - rect.top), true); + + // set the layoutMetrics + m_layoutMetrics.frame.size = { + (float)rect.right - rect.left + m_layoutMetrics.frame.origin.x, + (float)rect.bottom - rect.top + m_layoutMetrics.frame.origin.y}; + m_layoutMetrics.overflowInset.right = 0; + m_layoutMetrics.overflowInset.bottom = 0; + + // Let RNWIsland know that Modal's size has changed + winrt::get_self(m_reactNativeIsland) + ->NotifySizeChanged(); +}; + void WindowsModalHostComponentView::updateProps( facebook::react::Props::Shared const &props, facebook::react::Props::Shared const &oldProps) noexcept { const auto &oldModalProps = *std::static_pointer_cast(oldProps ? oldProps : viewProps()); const auto &newModalProps = *std::static_pointer_cast(props); - - // currently Modal only gets Destroyed by closing the window - if (newModalProps.visible) { - EnsureModalCreated(); - ShowOnUIThread(); - } - + newModalProps.visible ? m_isVisible = true : m_isVisible = false; base_type::updateProps(props, oldProps); } -void WindowsModalHostComponentView::updateLayoutMetrics( - facebook::react::LayoutMetrics const &layoutMetrics, - facebook::react::LayoutMetrics const &oldLayoutMetrics) noexcept { - Super::updateLayoutMetrics(layoutMetrics, oldLayoutMetrics); - - // Temporary placeholder for Modal, draws on main hwnd - if (m_layoutMetrics.frame.size != layoutMetrics.frame.size || - m_layoutMetrics.pointScaleFactor != layoutMetrics.pointScaleFactor || m_layoutMetrics.frame.size.width == 0) { - // Always make visual a min size, so that even if its laid out at zero size, its clear an unimplemented view was - // rendered - float width = std::max(m_layoutMetrics.frame.size.width, 200.0f); - float height = std::max(m_layoutMetrics.frame.size.width, 50.0f); - - winrt::Windows::Foundation::Size surfaceSize = { - width * m_layoutMetrics.pointScaleFactor, height * m_layoutMetrics.pointScaleFactor}; - auto drawingSurface = m_compContext.CreateDrawingSurfaceBrush( - surfaceSize, - winrt::Windows::Graphics::DirectX::DirectXPixelFormat::B8G8R8A8UIntNormalized, - winrt::Windows::Graphics::DirectX::DirectXAlphaMode::Premultiplied); - - drawingSurface.HorizontalAlignmentRatio(0.f); - drawingSurface.VerticalAlignmentRatio(0.f); - drawingSurface.Stretch(winrt::Microsoft::ReactNative::Composition::Experimental::CompositionStretch::None); - Visual().as().Brush(drawingSurface); - Visual().Size(surfaceSize); - Visual().Offset({ - layoutMetrics.frame.origin.x * layoutMetrics.pointScaleFactor, - layoutMetrics.frame.origin.y * layoutMetrics.pointScaleFactor, - 0.0f, - }); - - POINT offset; - { - ::Microsoft::ReactNative::Composition::AutoDrawDrawingSurface autoDraw( - drawingSurface, m_layoutMetrics.pointScaleFactor, &offset); - if (auto d2dDeviceContext = autoDraw.GetRenderTarget()) { - d2dDeviceContext->Clear(D2D1::ColorF(D2D1::ColorF::Blue, 0.3f)); - assert(d2dDeviceContext->GetUnitMode() == D2D1_UNIT_MODE_DIPS); - - float offsetX = static_cast(offset.x / m_layoutMetrics.pointScaleFactor); - float offsetY = static_cast(offset.y / m_layoutMetrics.pointScaleFactor); - - winrt::com_ptr spTextFormat; - winrt::check_hresult(::Microsoft::ReactNative::DWriteFactory()->CreateTextFormat( - L"Segoe UI", - nullptr, // Font collection (nullptr sets it to use the system font collection). - DWRITE_FONT_WEIGHT_REGULAR, - DWRITE_FONT_STYLE_NORMAL, - DWRITE_FONT_STRETCH_NORMAL, - 12, - L"", - spTextFormat.put())); - - winrt::com_ptr textBrush; - winrt::check_hresult( - d2dDeviceContext->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White), textBrush.put())); - - const D2D1_RECT_F rect = { - static_cast(offset.x), static_cast(offset.y), width + offset.x, height + offset.y}; - - auto label = ::Microsoft::Common::Unicode::Utf8ToUtf16(std::string("This is a Modal")); - d2dDeviceContext->DrawText( - label.c_str(), - static_cast(label.length()), - spTextFormat.get(), - rect, - textBrush.get(), - D2D1_DRAW_TEXT_OPTIONS_NONE, - DWRITE_MEASURING_MODE_NATURAL); - } - } - } -} - -void WindowsModalHostComponentView::updateState( - facebook::react::State::Shared const &state, - facebook::react::State::Shared const &oldState) noexcept {} - facebook::react::SharedViewProps WindowsModalHostComponentView::defaultProps() noexcept { static auto const defaultProps = std::make_shared(); return defaultProps; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.h index 547b9f3cc50..a53fb56da16 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.h @@ -9,34 +9,38 @@ #include "Composition.WindowsModalHostComponentView.g.h" #include "../CompositionViewComponentView.h" +#include #include namespace winrt::Microsoft::ReactNative::Composition::implementation { struct WindowsModalHostComponentView - : WindowsModalHostComponentViewT { - using Super = WindowsModalHostComponentViewT; + : WindowsModalHostComponentViewT { + using Super = WindowsModalHostComponentViewT; + + ~WindowsModalHostComponentView(); [[nodiscard]] static winrt::Microsoft::ReactNative::ComponentView Create( const winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext, facebook::react::Tag tag, winrt::Microsoft::ReactNative::ReactContext const &reactContext) noexcept; + winrt::Microsoft::ReactNative::Composition::Experimental::IVisual VisualToMountChildrenInto() noexcept override; void MountChildComponentView( const winrt::Microsoft::ReactNative::ComponentView &childComponentView, uint32_t index) noexcept override; void UnmountChildComponentView( const winrt::Microsoft::ReactNative::ComponentView &childComponentView, uint32_t index) noexcept override; - void HandleCommand(const winrt::Microsoft::ReactNative::HandleCommandArgs &args) noexcept override; - void updateState(facebook::react::State::Shared const &state, facebook::react::State::Shared const &oldState) noexcept - override; - void updateProps(facebook::react::Props::Shared const &props, facebook::react::Props::Shared const &oldProps) noexcept - override; + void AdjustWindowSize() noexcept; + void updateLayoutMetrics( facebook::react::LayoutMetrics const &layoutMetrics, facebook::react::LayoutMetrics const &oldLayoutMetrics) noexcept override; + + void updateProps(facebook::react::Props::Shared const &props, facebook::react::Props::Shared const &oldProps) noexcept + override; static facebook::react::SharedViewProps defaultProps() noexcept; const facebook::react::ModalHostViewProps &modalHostViewProps() const noexcept; bool focusable() const noexcept override; @@ -57,7 +61,9 @@ struct WindowsModalHostComponentView private: HWND m_hwnd{nullptr}; - winrt::Microsoft::ReactNative::ReactContext m_context; + uint64_t m_prevWindowID; + bool m_isVisible{false}; + winrt::Microsoft::ReactNative::ReactNativeIsland m_reactNativeIsland; }; } // namespace winrt::Microsoft::ReactNative::Composition::implementation diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ReactCompositionViewComponentBuilder.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ReactCompositionViewComponentBuilder.cpp index 02b06f945c8..7059334941f 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ReactCompositionViewComponentBuilder.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ReactCompositionViewComponentBuilder.cpp @@ -21,8 +21,10 @@ void ReactCompositionViewComponentBuilder::SetCreateProps(ViewPropsFactory impl) m_propsFactory = impl; } -IComponentProps ReactCompositionViewComponentBuilder::CreateProps(ViewProps props) noexcept { - return m_propsFactory(props); +IComponentProps ReactCompositionViewComponentBuilder::CreateProps( + ViewProps props, + const IComponentProps &cloneFrom) noexcept { + return m_propsFactory(props, cloneFrom); } void ReactCompositionViewComponentBuilder::CreateShadowNode(ShadowNode shadowNode) noexcept { @@ -72,12 +74,15 @@ void ReactCompositionViewComponentBuilder::InitializeComponentView( void ReactCompositionViewComponentBuilder::SetComponentViewInitializer( const ComponentViewInitializer &initializer) noexcept { - m_fnCreateView = - [initializer](const IReactContext &reactContext, int32_t tag, const Experimental::ICompositionContext &context) { - auto view = winrt::make(tag, reactContext); - initializer(view); - return view; - }; + m_fnCreateView = [initializer]( + const IReactContext &reactContext, + int32_t tag, + const Experimental::ICompositionContext &context, + ComponentViewFeatures) { + auto view = winrt::make(tag, reactContext); + initializer(view); + return view; + }; m_descriptorConstructorFactory = []() { return &facebook::react::concreteComponentDescriptorConstructor<::Microsoft::ReactNative::AbiComponentDescriptor>; }; @@ -85,14 +90,16 @@ void ReactCompositionViewComponentBuilder::SetComponentViewInitializer( void ReactCompositionViewComponentBuilder::SetViewComponentViewInitializer( const ViewComponentViewInitializer &initializer) noexcept { - m_fnCreateView = - [initializer](const IReactContext &reactContext, int32_t tag, const Experimental::ICompositionContext &context) { - auto view = winrt::Microsoft::ReactNative::Composition::implementation::ViewComponentView::Create( - context, tag, reactContext) - .as(); - initializer(view); - return view; - }; + m_fnCreateView = [initializer]( + const IReactContext &reactContext, + int32_t tag, + const Experimental::ICompositionContext &context, + ComponentViewFeatures features) { + auto view = winrt::make( + implementation::ViewComponentView::defaultProps(), context, tag, reactContext, features); + initializer(view); + return view; + }; m_descriptorConstructorFactory = []() { return &facebook::react::concreteComponentDescriptorConstructor< ::Microsoft::ReactNative::AbiViewComponentDescriptor>; @@ -101,9 +108,12 @@ void ReactCompositionViewComponentBuilder::SetViewComponentViewInitializer( void ReactCompositionViewComponentBuilder::SetContentIslandComponentViewInitializer( const ComponentIslandComponentViewInitializer &initializer) noexcept { - m_fnCreateView = [initializer]( - const IReactContext &reactContext, int32_t tag, const Experimental::ICompositionContext &context) - -> winrt::Microsoft::ReactNative::Composition::ContentIslandComponentView { + m_fnCreateView = + [initializer]( + const IReactContext &reactContext, + int32_t tag, + const Experimental::ICompositionContext &context, + ComponentViewFeatures) -> winrt::Microsoft::ReactNative::Composition::ContentIslandComponentView { auto view = winrt::make( context, tag, reactContext); initializer(view); @@ -186,12 +196,16 @@ void ReactCompositionViewComponentBuilder::SetCreateVisualHandler(CreateVisualDe m_createVisualHandler = impl; } +void ReactCompositionViewComponentBuilder::SetViewFeatures(ComponentViewFeatures viewFeatures) noexcept { + m_features = viewFeatures; +} + winrt::Microsoft::ReactNative::ComponentView ReactCompositionViewComponentBuilder::CreateView( const IReactContext &reactContext, int32_t tag, const Experimental::ICompositionContext &context) noexcept { assert(m_fnCreateView); - auto view = m_fnCreateView(reactContext, tag, context); + auto view = m_fnCreateView(reactContext, tag, context, m_features); InitializeComponentView(view); return view; } diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ReactCompositionViewComponentBuilder.h b/vnext/Microsoft.ReactNative/Fabric/Composition/ReactCompositionViewComponentBuilder.h index 14e1a21ff11..220884bd6c6 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ReactCompositionViewComponentBuilder.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ReactCompositionViewComponentBuilder.h @@ -42,11 +42,11 @@ struct ReactCompositionViewComponentBuilder : winrt::implements< public: // Composition::IReactCompositionViewComponentBuilder void SetViewComponentViewInitializer(const ViewComponentViewInitializer &initializer) noexcept; void SetContentIslandComponentViewInitializer(const ComponentIslandComponentViewInitializer &initializer) noexcept; - void SetCreateVisualHandler(CreateVisualDelegate impl) noexcept; + void SetViewFeatures(ComponentViewFeatures viewFeatures) noexcept; public: - IComponentProps CreateProps(ViewProps props) noexcept; + IComponentProps CreateProps(ViewProps props, const IComponentProps &cloneFrom) noexcept; void CreateShadowNode(ShadowNode shadowNode) noexcept; void CloneShadowNode(ShadowNode shadowNode, ShadowNode sourceShadowNode) noexcept; winrt::Windows::Foundation::IInspectable InitialStateData( @@ -69,10 +69,12 @@ struct ReactCompositionViewComponentBuilder : winrt::implements< InitialStateDataFactory m_initialStateDataFactory; winrt::Microsoft::ReactNative::MeasureContentHandler m_measureContent; winrt::Microsoft::ReactNative::LayoutHandler m_layoutHandler; + ComponentViewFeatures m_features{ComponentViewFeatures::Default}; std::function + const Experimental::ICompositionContext &context, + ComponentViewFeatures features)> m_fnCreateView; std::function m_descriptorConstructorFactory; winrt::Microsoft::ReactNative::HandleCommandDelegate m_customCommandHandler; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.cpp index 29178668c21..ce44ca56a1d 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.cpp @@ -127,6 +127,20 @@ ReactNativeIsland::ReactNativeIsland(const winrt::Microsoft::UI::Composition::Co InitTextScaleMultiplier(); } +// Constructor to initialize ReactNativeIsland with context and componentView +ReactNativeIsland::ReactNativeIsland( + const winrt::Microsoft::UI::Composition::Compositor &compositor, + winrt::Microsoft::ReactNative::IReactContext context, + winrt::Microsoft::ReactNative::ComponentView componentView) noexcept + : m_compositor(compositor), + m_context(context), + m_layoutConstraints({{0, 0}, {0, 0}, winrt::Microsoft::ReactNative::LayoutDirection::Undefined}), + m_isFragment(true) { + m_rootTag = componentView.Tag(); + InitTextScaleMultiplier(); + AddFragmentCompositionEventHandler(context, componentView); +} + ReactNativeIsland::ReactNativeIsland() noexcept : ReactNativeIsland(nullptr) {} ReactNativeIsland::~ReactNativeIsland() noexcept { @@ -152,6 +166,7 @@ ReactNative::IReactViewHost ReactNativeIsland::ReactViewHost() noexcept { } void ReactNativeIsland::ReactViewHost(winrt::Microsoft::ReactNative::IReactViewHost const &value) noexcept { + assert(!m_isFragment); // make sure this isn't a FragmentIsalnd if (m_reactViewHost == value) { return; } @@ -404,7 +419,7 @@ void ReactNativeIsland::InitRootView( m_context = winrt::Microsoft::ReactNative::ReactContext(std::move(context)); m_reactViewOptions = std::move(viewOptions); - m_CompositionEventHandler = std::make_shared<::Microsoft::ReactNative::CompositionEventHandler>(m_context, *this); + m_CompositionEventHandler = std::make_shared<::Microsoft::ReactNative::CompositionEventHandler>(m_context, *this, -1); m_CompositionEventHandler->Initialize(); UpdateRootViewInternal(); @@ -412,6 +427,27 @@ void ReactNativeIsland::InitRootView( m_isInitialized = true; } +void ReactNativeIsland::AddFragmentCompositionEventHandler( + winrt::Microsoft::ReactNative::IReactContext context, + winrt::Microsoft::ReactNative::ComponentView componentView) noexcept { + m_uiDispatcher = context.Properties() + .Get(winrt::Microsoft::ReactNative::ReactDispatcherHelper::UIDispatcherProperty()) + .try_as(); + VerifyElseCrash(m_uiDispatcher.HasThreadAccess()); + VerifyElseCrash(m_rootTag != -1); + auto uiManager = ::Microsoft::ReactNative::FabricUIManager::FromProperties( + winrt::Microsoft::ReactNative::ReactPropertyBag(context.Properties())); + + if (!m_CompositionEventHandler) { + // Create CompositionEventHandler if not already created + m_context = winrt::Microsoft::ReactNative::ReactContext(context); + m_CompositionEventHandler = + std::make_shared<::Microsoft::ReactNative::CompositionEventHandler>(m_context, *this, componentView.Tag()); + m_CompositionEventHandler->Initialize(); + m_isInitialized = true; + } +} + void ReactNativeIsland::UpdateRootView() noexcept { VerifyElseCrash(m_uiDispatcher.HasThreadAccess()); VerifyElseCrash(m_isInitialized); @@ -827,7 +863,9 @@ void ReactNativeIsland::OnMounted() noexcept { return; m_mounted = true; if (auto componentView = GetComponentView()) { - componentView->onMounted(); + if (!componentView->isMounted()) { + componentView->onMounted(); + } } } diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.h b/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.h index f280ddc6a1d..b74be73460b 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.h @@ -48,6 +48,10 @@ struct ReactNativeIsland ~ReactNativeIsland() noexcept; ReactNativeIsland(const winrt::Microsoft::UI::Composition::Compositor &compositor) noexcept; + ReactNativeIsland( + const winrt::Microsoft::UI::Composition::Compositor &compositor, + winrt::Microsoft::ReactNative::IReactContext context, + winrt::Microsoft::ReactNative::ComponentView componentView) noexcept; winrt::Microsoft::UI::Content::ContentIsland Island(); // property ReactViewHost @@ -117,6 +121,10 @@ struct ReactNativeIsland const winrt::Microsoft::ReactNative::Composition::Input::Pointer &pointer, facebook::react::Tag tag) noexcept; + void AddFragmentCompositionEventHandler( + winrt::Microsoft::ReactNative::IReactContext context, + winrt::Microsoft::ReactNative::ComponentView componentView) noexcept; + public: // IReactViewInstance UI-thread implementation void InitRootView( winrt::Microsoft::ReactNative::IReactContext &&context, @@ -136,6 +144,7 @@ struct ReactNativeIsland #endif HWND m_hwnd{0}; + bool m_isFragment{false}; bool m_isInitialized{false}; bool m_isJSViewAttached{false}; bool m_hasRenderedVisual{false}; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp index 282cba603b8..7847e9ec5d5 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp @@ -7,6 +7,7 @@ #include "RootComponentView.h" #include +#include #include "CompositionRootAutomationProvider.h" #include "ReactNativeIsland.h" #include "Theme.h" @@ -24,7 +25,7 @@ RootComponentView::RootComponentView( reactContext, ComponentViewFeatures::Default & ~(ComponentViewFeatures::Background | ComponentViewFeatures::ShadowProps | - ComponentViewFeatures::NativeBorder)) {} + ComponentViewFeatures::NativeBorder | ComponentViewFeatures::FocusVisual)) {} RootComponentView::~RootComponentView() { if (auto rootView = m_wkRootView.get()) { @@ -106,7 +107,10 @@ bool RootComponentView::TrySetFocusedComponent( auto target = view; auto selfView = winrt::get_self(target); if (selfView && !selfView->focusable()) { - target = FocusManager::FindFirstFocusableElement(target); + target = (direction == winrt::Microsoft::ReactNative::FocusNavigationDirection::Last || + direction == winrt::Microsoft::ReactNative::FocusNavigationDirection::Previous) + ? FocusManager::FindLastFocusableElement(target) + : FocusManager::FindFirstFocusableElement(target); if (!target) return false; selfView = winrt::get_self(target); @@ -147,6 +151,9 @@ bool RootComponentView::TryMoveFocus(bool next) noexcept { [currentlyFocused = m_focusedComponent, next](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { if (view == currentlyFocused) return false; + auto selfView = winrt::get_self(view); + if (!selfView->focusable()) + return false; return winrt::get_self(view) ->rootComponentView() @@ -156,7 +163,28 @@ bool RootComponentView::TryMoveFocus(bool next) noexcept { : winrt::Microsoft::ReactNative::FocusNavigationDirection::Previous); }; - return winrt::Microsoft::ReactNative::implementation::walkTree(m_focusedComponent, next, fn); + if (winrt::Microsoft::ReactNative::implementation::walkTree(m_focusedComponent, next, fn)) { + return true; + } + + // We reached the end of the focus loop. Notify the island in case the host wants to move focus somewhere outside the + // island. + auto island = parentContentIsland(); + if (island) { + auto focusController = winrt::Microsoft::UI::Input::InputFocusController::GetForIsland(island); + auto request = winrt::Microsoft::UI::Input::FocusNavigationRequest::Create( + next ? winrt::Microsoft::UI::Input::FocusNavigationReason::Last + : winrt::Microsoft::UI::Input::FocusNavigationReason::First); + auto result = focusController.DepartFocus(request); + if (result == winrt::Microsoft::UI::Input::FocusNavigationResult::Moved) { + return true; + } + } + + // Wrap focus around if nothing outside the island takes focus + return NavigateFocus(winrt::Microsoft::ReactNative::FocusNavigationRequest( + next ? winrt::Microsoft::ReactNative::FocusNavigationReason::First + : winrt::Microsoft::ReactNative::FocusNavigationReason::Last)); } HRESULT RootComponentView::GetFragmentRoot(IRawElementProviderFragmentRoot **pRetVal) noexcept { diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp index f6d57e9d4d7..51d20a0501b 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp @@ -1108,9 +1108,9 @@ void ScrollViewComponentView::StartBringIntoView( options.TargetRect->origin.y > m_scrollVisual.ScrollPosition().y) { needsScroll = true; if (options.TargetRect->size.height > viewerHeight) { - scrollToVertical = options.TargetRect->origin.y + options.VerticalOffset; + scrollToVertical = options.TargetRect->origin.y + options.VerticalOffset * m_layoutMetrics.pointScaleFactor; } else { - scrollToVertical = (targetBottom - viewerHeight) + options.VerticalOffset; + scrollToVertical = (targetBottom - viewerHeight) + options.VerticalOffset * m_layoutMetrics.pointScaleFactor; } // Scroll Up } else if ( @@ -1118,9 +1118,9 @@ void ScrollViewComponentView::StartBringIntoView( targetBottom < (m_scrollVisual.ScrollPosition().y + viewerHeight)) { needsScroll = true; if (options.TargetRect->size.height > viewerHeight) { - scrollToVertical = targetBottom - viewerHeight - options.VerticalOffset; + scrollToVertical = targetBottom - viewerHeight - options.VerticalOffset * m_layoutMetrics.pointScaleFactor; } else { - scrollToVertical = options.TargetRect->origin.y - options.VerticalOffset; + scrollToVertical = options.TargetRect->origin.y - options.VerticalOffset * m_layoutMetrics.pointScaleFactor; } } } else { @@ -1134,9 +1134,9 @@ void ScrollViewComponentView::StartBringIntoView( options.TargetRect->origin.x > m_scrollVisual.ScrollPosition().x) { needsScroll = true; if (options.TargetRect->size.width > viewerWidth) { - scrollToHorizontal = options.TargetRect->origin.x + options.HorizontalOffset; + scrollToHorizontal = options.TargetRect->origin.x + options.HorizontalOffset * m_layoutMetrics.pointScaleFactor; } else { - scrollToHorizontal = (targetRight - viewerWidth) + options.HorizontalOffset; + scrollToHorizontal = (targetRight - viewerWidth) + options.HorizontalOffset * m_layoutMetrics.pointScaleFactor; } // Scroll Left } else if ( @@ -1144,9 +1144,9 @@ void ScrollViewComponentView::StartBringIntoView( targetRight < (m_scrollVisual.ScrollPosition().x + viewerWidth)) { needsScroll = true; if (options.TargetRect->size.width > viewerWidth) { - scrollToHorizontal = targetRight - viewerWidth - options.HorizontalOffset; + scrollToHorizontal = targetRight - viewerWidth - options.HorizontalOffset * m_layoutMetrics.pointScaleFactor; } else { - scrollToHorizontal = options.TargetRect->origin.x - options.HorizontalOffset; + scrollToHorizontal = options.TargetRect->origin.x - options.HorizontalOffset * m_layoutMetrics.pointScaleFactor; } } } else { diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp index 9e4ca978f87..5be5d39a5f2 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp @@ -870,6 +870,7 @@ void WindowsTextInputComponentView::OnCharacterReceived( emitter->onKeyPress(onKeyPressArgs); WPARAM wParam = static_cast(args.KeyCode()); + LPARAM lParam = 0; lParam = args.KeyStatus().RepeatCount; // bits 0-15 lParam |= args.KeyStatus().ScanCode << 16; // bits 16-23 @@ -1022,6 +1023,10 @@ void WindowsTextInputComponentView::updateProps( m_submitKeyEvents.clear(); } + if (oldTextInputProps.autoCapitalize != newTextInputProps.autoCapitalize) { + autoCapitalizeOnUpdateProps(oldTextInputProps.autoCapitalize, newTextInputProps.autoCapitalize); + } + UpdatePropertyBits(); } @@ -1476,4 +1481,29 @@ winrt::Microsoft::ReactNative::ComponentView WindowsTextInputComponentView::Crea return winrt::make(compContext, tag, reactContext); } -} // namespace winrt::Microsoft::ReactNative::Composition::implementation \ No newline at end of file +// This function assumes that previous and new capitalization types are different. +void WindowsTextInputComponentView::autoCapitalizeOnUpdateProps( + const std::string &previousCapitalizationType, + const std::string &newCapitalizationType) noexcept { + /* + Possible values are: + Characters - All characters. + Words - First letter of each word. + Sentences - First letter of each sentence. + None - Do not autocapitalize anything. + + For now, only characters and none are supported. + */ + + if (previousCapitalizationType == "characters") { + winrt::check_hresult(m_textServices->TxSendMessage( + EM_SETEDITSTYLE, 0 /* disable */, SES_UPPERCASE /* flag affected */, nullptr /* LRESULT */)); + } + + if (newCapitalizationType == "characters") { + winrt::check_hresult(m_textServices->TxSendMessage( + EM_SETEDITSTYLE, SES_UPPERCASE /* enable */, SES_UPPERCASE /* flag affected */, nullptr /* LRESULT */)); + } +} + +} // namespace winrt::Microsoft::ReactNative::Composition::implementation diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.h index fb3084663cc..e8919325a05 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.h @@ -104,6 +104,10 @@ struct WindowsTextInputComponentView void InternalFinalize() noexcept; void UpdatePropertyBits() noexcept; + void autoCapitalizeOnUpdateProps( + const std::string &previousCapitalizationType, + const std::string &newcapitalizationType) noexcept; + winrt::Windows::UI::Composition::CompositionSurfaceBrush m_brush{nullptr}; winrt::Microsoft::ReactNative::Composition::Experimental::ICaretVisual m_caretVisual{nullptr}; winrt::Microsoft::ReactNative::Composition::Experimental::IDrawingSurfaceBrush m_drawingSurface{nullptr}; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/Theme.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/Theme.cpp index 95536fa1faa..2c4a3e72ad3 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/Theme.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/Theme.cpp @@ -330,7 +330,9 @@ bool Theme::TryGetPlatformColor(const std::string &platformColor, winrt::Windows {"AcrylicInAppFillColorDefault", {0x9E, 0xFF, 0xFF, 0xFF}}, {"SystemChromeMediumLowColor", {0xFF, 0xF2, 0xF2, 0xF2}}, {"SystemControlForegroundBaseHighColor", {0xFF, 0x00, 0x00, 0x00}}, - {"SystemControlTransientBorderColor", {0x24, 0x00, 0x00, 0x00}}}; + {"SystemControlTransientBorderColor", {0x24, 0x00, 0x00, 0x00}}, + {"FocusVisualPrimary", {0xFF, 0x00, 0x00, 0x00}}, + {"FocusVisualSecondary", {0x99, 0xFF, 0xFF, 0xFF}}}; static std::unordered_map, std::equal_to<>> s_darkColors = { @@ -362,7 +364,9 @@ bool Theme::TryGetPlatformColor(const std::string &platformColor, winrt::Windows {"AcrylicInAppFillColorDefault", {0x9E, 0x00, 0x00, 0x00}}, {"SystemChromeMediumLowColor", {0xFF, 0x2B, 0x2B, 0x2B}}, {"SystemControlForegroundBaseHighColor", {0xFF, 0xFF, 0xFF, 0xFF}}, - {"SystemControlTransientBorderColor", {0x5C, 0x00, 0x00, 0x00}}}; + {"SystemControlTransientBorderColor", {0x5C, 0x00, 0x00, 0x00}}, + {"FocusVisualPrimary", {0xFF, 0xFF, 0xFF, 0xFF}}, + {"FocusVisualSecondary", {0x99, 0x00, 0x00, 0x00F}}}; static std::unordered_map< std::string, @@ -399,7 +403,9 @@ bool Theme::TryGetPlatformColor(const std::string &platformColor, winrt::Windows {"ControlStrongFillColorDisabled", {winrt::Windows::UI::ViewManagement::UIElementType::ButtonFace, {}}}, {"SystemChromeMediumLowColor", {winrt::Windows::UI::ViewManagement::UIElementType::ButtonFace, {}}}, {"SystemControlForegroundBaseHighColor", {winrt::Windows::UI::ViewManagement::UIElementType::ButtonText, {}}}, - {"SystemControlTransientBorderColor", {winrt::Windows::UI::ViewManagement::UIElementType::ButtonText, {}}}}; + {"SystemControlTransientBorderColor", {winrt::Windows::UI::ViewManagement::UIElementType::ButtonText, {}}}, + {"FocusVisualPrimary", {winrt::Windows::UI::ViewManagement::UIElementType::ButtonText, {}}}, + {"FocusVisualSecondary", {winrt::Windows::UI::ViewManagement::UIElementType::ButtonFace, {}}}}; auto alias = s_xamlAliasedColors.find(platformColor); if (alias != s_xamlAliasedColors.end()) { diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp index 9689b046f60..db919ea10bf 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp @@ -214,4 +214,15 @@ ExpandCollapseState GetExpandCollapseState(const bool &expanded) noexcept { } } +ToggleState GetToggleState(const std::optional &state) noexcept { + if (state.has_value()) { + if (state->checked == facebook::react::AccessibilityState::Checked) { + return ToggleState::ToggleState_On; + } else if (state->checked == facebook::react::AccessibilityState::Mixed) { + return ToggleState::ToggleState_Indeterminate; + } + } + return ToggleState::ToggleState_Off; +} + } // namespace winrt::Microsoft::ReactNative::implementation diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.h b/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.h index 42a31aa131e..20b03314aac 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.h @@ -36,4 +36,6 @@ std::string extractAccessibilityValue(const facebook::react::AccessibilityValue void DispatchAccessibilityAction(::Microsoft::ReactNative::ReactTaggedView &view, const std::string &action) noexcept; ExpandCollapseState GetExpandCollapseState(const bool &expanded) noexcept; + +ToggleState GetToggleState(const std::optional &state) noexcept; } // namespace winrt::Microsoft::ReactNative::implementation diff --git a/vnext/Microsoft.ReactNative/IReactCompositionViewComponentBuilder.idl b/vnext/Microsoft.ReactNative/IReactCompositionViewComponentBuilder.idl index 2c0660c2908..021103285d9 100644 --- a/vnext/Microsoft.ReactNative/IReactCompositionViewComponentBuilder.idl +++ b/vnext/Microsoft.ReactNative/IReactCompositionViewComponentBuilder.idl @@ -33,6 +33,7 @@ namespace Microsoft.ReactNative.Composition void SetViewComponentViewInitializer(ViewComponentViewInitializer initializer); void SetContentIslandComponentViewInitializer(ComponentIslandComponentViewInitializer initializer); void SetCreateVisualHandler(CreateVisualDelegate impl); + void SetViewFeatures(ComponentViewFeatures viewFeatures); }; } // namespace Microsoft.ReactNative diff --git a/vnext/Microsoft.ReactNative/IReactViewComponentBuilder.idl b/vnext/Microsoft.ReactNative/IReactViewComponentBuilder.idl index 4de452ce1d9..fa2197a0596 100644 --- a/vnext/Microsoft.ReactNative/IReactViewComponentBuilder.idl +++ b/vnext/Microsoft.ReactNative/IReactViewComponentBuilder.idl @@ -56,7 +56,7 @@ namespace Microsoft.ReactNative [experimental] DOC_STRING("A delegate that creates a @IComponentProps object for an instance of @ViewProps. See @IReactViewComponentBuilder.SetCreateProps") - delegate IComponentProps ViewPropsFactory(ViewProps props); + delegate IComponentProps ViewPropsFactory(ViewProps props, IComponentProps cloneFrom); [experimental] delegate Windows.Foundation.Size MeasureContentHandler(ShadowNode shadowNode, LayoutContext layoutContext, LayoutConstraints layoutConstraints); diff --git a/vnext/Microsoft.ReactNative/ReactInstanceSettingsBuilder.cpp b/vnext/Microsoft.ReactNative/ReactInstanceSettingsBuilder.cpp deleted file mode 100644 index 5f847101095..00000000000 --- a/vnext/Microsoft.ReactNative/ReactInstanceSettingsBuilder.cpp +++ /dev/null @@ -1,59 +0,0 @@ -#include "pch.h" -#include "ReactInstanceSettingsBuilder.h" -#include "ReactInstanceSettingsBuilder.g.cpp" -#include "ReactInstanceSettings.h" - -namespace winrt::ReactNative { -using namespace winrt::Microsoft::ReactNative; -} - -namespace winrt::UI { -using namespace winrt::Microsoft::UI; -} - -namespace winrt::Microsoft::ReactNative::implementation { -ReactInstanceSettingsBuilder::ReactInstanceSettingsBuilder() { - m_reactInstanceSettings = winrt::make(); -} - -winrt::ReactNative::ReactInstanceSettings ReactInstanceSettingsBuilder::ReactInstanceSettings() { - return m_reactInstanceSettings; -} - -winrt::ReactNative::ReactInstanceSettingsBuilder ReactInstanceSettingsBuilder::UseDirectDebugger(bool const &state) { - m_reactInstanceSettings.UseDirectDebugger(state); - - return *this; -} - -winrt::ReactNative::ReactInstanceSettingsBuilder ReactInstanceSettingsBuilder::UseDeveloperSupport(bool const &state) { - m_reactInstanceSettings.UseDeveloperSupport(state); - - return *this; -} - -winrt::ReactNative::ReactInstanceSettingsBuilder ReactInstanceSettingsBuilder::BundleRootPath(hstring const &path) { - m_reactInstanceSettings.BundleRootPath(std::wstring(L"file://").append(path.c_str()).append(L"\\Bundle\\").c_str()); - - return *this; -} - -winrt::ReactNative::ReactInstanceSettingsBuilder ReactInstanceSettingsBuilder::DebugBundlePath(hstring const &path) { - m_reactInstanceSettings.DebugBundlePath(path.c_str()); - - return *this; -} - -winrt::ReactNative::ReactInstanceSettingsBuilder ReactInstanceSettingsBuilder::JavaScriptBundleFile( - hstring const &file) { - m_reactInstanceSettings.JavaScriptBundleFile(file.c_str()); - - return *this; -} - -winrt::ReactNative::ReactInstanceSettingsBuilder ReactInstanceSettingsBuilder::UseFastRefresh(bool const &state) { - m_reactInstanceSettings.UseFastRefresh(state); - - return *this; -} -} // namespace winrt::Microsoft::ReactNative::implementation diff --git a/vnext/Microsoft.ReactNative/ReactInstanceSettingsBuilder.h b/vnext/Microsoft.ReactNative/ReactInstanceSettingsBuilder.h deleted file mode 100644 index 7cd712a9297..00000000000 --- a/vnext/Microsoft.ReactNative/ReactInstanceSettingsBuilder.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once -#include "ReactInstanceSettingsBuilder.g.h" - -namespace winrt::Microsoft::ReactNative::implementation { -struct ReactInstanceSettingsBuilder : ReactInstanceSettingsBuilderT { - ReactInstanceSettingsBuilder(); - - winrt::Microsoft::ReactNative::ReactInstanceSettings ReactInstanceSettings(); - winrt::Microsoft::ReactNative::ReactInstanceSettingsBuilder UseDirectDebugger(bool const &state); - winrt::Microsoft::ReactNative::ReactInstanceSettingsBuilder UseDeveloperSupport(bool const &state); - winrt::Microsoft::ReactNative::ReactInstanceSettingsBuilder BundleRootPath(hstring const &path); - winrt::Microsoft::ReactNative::ReactInstanceSettingsBuilder DebugBundlePath(hstring const &path); - winrt::Microsoft::ReactNative::ReactInstanceSettingsBuilder JavaScriptBundleFile(hstring const &file); - winrt::Microsoft::ReactNative::ReactInstanceSettingsBuilder UseFastRefresh(bool const &state); - - private: - winrt::Microsoft::ReactNative::ReactInstanceSettings m_reactInstanceSettings{nullptr}; -}; -} // namespace winrt::Microsoft::ReactNative::implementation -namespace winrt::Microsoft::ReactNative::factory_implementation { -struct ReactInstanceSettingsBuilder - : ReactInstanceSettingsBuilderT {}; -} // namespace winrt::Microsoft::ReactNative::factory_implementation diff --git a/vnext/Microsoft.ReactNative/ReactNativeAppBuilder.cpp b/vnext/Microsoft.ReactNative/ReactNativeAppBuilder.cpp index 80f568e152f..743ffa34c4e 100644 --- a/vnext/Microsoft.ReactNative/ReactNativeAppBuilder.cpp +++ b/vnext/Microsoft.ReactNative/ReactNativeAppBuilder.cpp @@ -1,43 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + #include "pch.h" #include "ReactNativeAppBuilder.h" #include "ReactNativeAppBuilder.g.cpp" -#include "IReactDispatcher.h" -#include "ReactNativeHost.h" + #include "ReactNativeWin32App.h" + +#include "winrt/Microsoft.ReactNative.h" #include "winrt/Microsoft.UI.Composition.h" #include "winrt/Microsoft.UI.Dispatching.h" +#include "winrt/Microsoft.UI.Interop.h" #include "winrt/Microsoft.UI.Windowing.h" -#include "winrt/microsoft.UI.Interop.h" - -// Scaling factor for the window's content based on the DPI of the display where the window is located. -float ScaleFactor(HWND hwnd) noexcept { - return GetDpiForWindow(hwnd) / static_cast(USER_DEFAULT_SCREEN_DPI); -} - -void UpdateRootViewSizeToAppWindow( - winrt::Microsoft::ReactNative::ReactNativeIsland const &rootView, - winrt::Microsoft::UI::Windowing::AppWindow const &window) { - auto hwnd = winrt::Microsoft::UI::GetWindowFromWindowId(window.Id()); - auto scaleFactor = ScaleFactor(hwnd); - winrt::Windows::Foundation::Size size{ - window.ClientSize().Width / scaleFactor, window.ClientSize().Height / scaleFactor}; - // Do not relayout when minimized - if (window.Presenter().as().State() != - winrt::Microsoft::UI::Windowing::OverlappedPresenterState::Minimized) { - winrt::Microsoft::ReactNative::LayoutConstraints constraints; - constraints.LayoutDirection = winrt::Microsoft::ReactNative::LayoutDirection::Undefined; - constraints.MaximumSize = constraints.MinimumSize = size; - rootView.Arrange(constraints, {0, 0}); - } -} - -namespace winrt::ReactNative { -using namespace winrt::Microsoft::ReactNative; -} - -namespace winrt::UI { -using namespace winrt::Microsoft::UI; -} namespace winrt::Microsoft::ReactNative::implementation { ReactNativeAppBuilder::ReactNativeAppBuilder() { @@ -46,133 +20,55 @@ ReactNativeAppBuilder::ReactNativeAppBuilder() { ReactNativeAppBuilder::~ReactNativeAppBuilder() {} -winrt::ReactNative::ReactNativeAppBuilder ReactNativeAppBuilder::AddPackageProviders( - winrt::Windows::Foundation::Collections::IVector const - &packageProviders) { - for (auto const &provider : packageProviders) { - m_reactNativeWin32App.ReactNativeHost().PackageProviders().Append(provider); - } - - return *this; -} - -winrt::ReactNative::ReactNativeAppBuilder ReactNativeAppBuilder::SetReactInstanceSettings( - winrt::Microsoft::ReactNative::ReactInstanceSettings const &settings) { - m_reactNativeWin32App.ReactNativeHost().InstanceSettings(settings); - +winrt::Microsoft::ReactNative::ReactNativeAppBuilder ReactNativeAppBuilder::SetDispatcherQueueController( + winrt::Microsoft::UI::Dispatching::DispatcherQueueController const &dispatcherQueueController) { + m_reactNativeWin32App.as().get()->DispatcherQueueController( + dispatcherQueueController); return *this; } -winrt::ReactNative::ReactNativeAppBuilder ReactNativeAppBuilder::SetCompositor( +winrt::Microsoft::ReactNative::ReactNativeAppBuilder ReactNativeAppBuilder::SetCompositor( winrt::Microsoft::UI::Composition::Compositor const &compositor) { m_reactNativeWin32App.as().get()->Compositor(compositor); return *this; } -winrt::ReactNative::ReactNativeAppBuilder ReactNativeAppBuilder::SetAppWindow( +winrt::Microsoft::ReactNative::ReactNativeAppBuilder ReactNativeAppBuilder::SetAppWindow( winrt::Microsoft::UI::Windowing::AppWindow const &appWindow) { m_reactNativeWin32App.as().get()->AppWindow(appWindow); return *this; } -winrt::Microsoft::ReactNative::ReactNativeAppBuilder ReactNativeAppBuilder::SetReactViewOptions( - winrt::Microsoft::ReactNative::ReactViewOptions const &reactViewOptions) { - m_reactViewOptions = reactViewOptions; +winrt::Microsoft::ReactNative::ReactNativeWin32App ReactNativeAppBuilder::Build() { + // Create the DispatcherQueueController if the app developer doesn't provide one + if (m_reactNativeWin32App.as().get()->DispatcherQueueController() == nullptr) { + assert(m_reactNativeWin32App.as().get()->Compositor() == nullptr); - return *this; -} - -winrt::ReactNative::ReactNativeWin32App ReactNativeAppBuilder::Build() { - if (m_reactNativeWin32App.Compositor() == nullptr) { - // Create a DispatcherQueue for this thread. This is needed for Composition, Content, and - // Input APIs. + // Create a DispatcherQueue for this thread. This is needed for Composition, Content, and Input APIs. auto dispatcherQueueController = winrt::Microsoft::UI::Dispatching::DispatcherQueueController::CreateOnCurrentThread(); - m_reactNativeWin32App.as().get()->DispatchQueueController( + m_reactNativeWin32App.as().get()->DispatcherQueueController( dispatcherQueueController); + } - // Create the compositor on behalf of the App Developer + // Create the Compositor if the app developer doesn't provide one + if (m_reactNativeWin32App.as().get()->Compositor() == nullptr) { + // Create the compositor on behalf of the App Developer. auto compositor = winrt::Microsoft::UI::Composition::Compositor(); m_reactNativeWin32App.as().get()->Compositor(compositor); } - // Create the AppWindow if the developer doesn't provide one + // Create the AppWindow if the app developer doesn't provide one if (m_reactNativeWin32App.AppWindow() == nullptr) { auto appWindow = winrt::Microsoft::UI::Windowing::AppWindow::Create(); - appWindow.Title(L"SampleApplication"); + appWindow.Title(L"ReactNativeWin32App"); appWindow.Resize({1000, 1000}); - appWindow.Show(); m_reactNativeWin32App.as().get()->AppWindow(appWindow); } - // Currently set the property to use current thread dispatcher as a default UI dispatcher. - // TODO: Provision for setting dispatcher based on the thread dispatcherQueueController is created. - m_reactNativeWin32App.ReactNativeHost().InstanceSettings().Properties().Set( - ReactDispatcherHelper::UIDispatcherProperty(), ReactDispatcherHelper::UIThreadDispatcher()); - - auto hwnd{winrt::UI::GetWindowFromWindowId(m_reactNativeWin32App.AppWindow().Id())}; - - winrt::ReactNative::ReactCoreInjection::SetTopLevelWindowId( - m_reactNativeWin32App.ReactNativeHost().InstanceSettings().Properties(), reinterpret_cast(hwnd)); - - winrt::ReactNative::Composition::CompositionUIService::SetCompositor( - m_reactNativeWin32App.ReactNativeHost().InstanceSettings(), m_reactNativeWin32App.Compositor()); - - // Start the react-native instance, which will create a JavaScript runtime and load the applications bundle. - m_reactNativeWin32App.ReactNativeHost().ReloadInstance(); - - // Create a RootView which will present a react-native component - auto reactNativeIsland = winrt::Microsoft::ReactNative::ReactNativeIsland(m_reactNativeWin32App.Compositor()); - reactNativeIsland.ReactViewHost(winrt::Microsoft::ReactNative::ReactCoreInjection::MakeViewHost( - m_reactNativeWin32App.ReactNativeHost(), m_reactViewOptions)); - - m_reactNativeWin32App.as().get()->ReactNativeIsland( - std::move(reactNativeIsland)); - - // Update the size of the RootView when the AppWindow changes size - m_reactNativeWin32App.AppWindow().Changed( - [wkRootView = winrt::make_weak(m_reactNativeWin32App.ReactNativeIsland())]( - winrt::Microsoft::UI::Windowing::AppWindow const &window, - winrt::Microsoft::UI::Windowing::AppWindowChangedEventArgs const &args) { - if (args.DidSizeChange() || args.DidVisibilityChange()) { - if (auto rootView = wkRootView.get()) { - UpdateRootViewSizeToAppWindow(rootView, window); - } - } - }); - - // Quit application when main window is closed - m_reactNativeWin32App.AppWindow().Destroying([this]( - winrt::Microsoft::UI::Windowing::AppWindow const &window, - winrt::Windows::Foundation::IInspectable const & /*args*/) { - // Before we shutdown the application - unload the ReactNativeHost to give the javascript a chance to save any - // state - auto async = m_reactNativeWin32App.ReactNativeHost().UnloadInstance(); - async.Completed([this](auto asyncInfo, winrt::Windows::Foundation::AsyncStatus asyncStatus) { - assert(asyncStatus == winrt::Windows::Foundation::AsyncStatus::Completed); - m_reactNativeWin32App.ReactNativeHost().InstanceSettings().UIDispatcher().Post([]() { PostQuitMessage(0); }); - }); - }); - - // DesktopChildSiteBridge create a ContentSite that can host the RootView ContentIsland - auto desktopChildSiteBridge = winrt::Microsoft::UI::Content::DesktopChildSiteBridge::Create( - m_reactNativeWin32App.Compositor(), m_reactNativeWin32App.AppWindow().Id()); - - desktopChildSiteBridge.Connect(m_reactNativeWin32App.ReactNativeIsland().Island()); - - desktopChildSiteBridge.ResizePolicy(winrt::Microsoft::UI::Content::ContentSizePolicy::ResizeContentToParentWindow); - - auto scaleFactor = ScaleFactor(hwnd); - m_reactNativeWin32App.ReactNativeIsland().ScaleFactor(scaleFactor); - - UpdateRootViewSizeToAppWindow(reactNativeIsland, m_reactNativeWin32App.AppWindow()); - - m_reactNativeWin32App.as().get()->DesktopChildSiteBridge( - std::move(desktopChildSiteBridge)); - return m_reactNativeWin32App; } diff --git a/vnext/Microsoft.ReactNative/ReactNativeAppBuilder.h b/vnext/Microsoft.ReactNative/ReactNativeAppBuilder.h index 1a077f0be38..309d4f5dd86 100644 --- a/vnext/Microsoft.ReactNative/ReactNativeAppBuilder.h +++ b/vnext/Microsoft.ReactNative/ReactNativeAppBuilder.h @@ -1,6 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. #pragma once + #include "ReactNativeAppBuilder.g.h" -#include namespace winrt::Microsoft::ReactNative::implementation { struct ReactNativeAppBuilder : ReactNativeAppBuilderT { @@ -8,25 +10,15 @@ struct ReactNativeAppBuilder : ReactNativeAppBuilderT { ~ReactNativeAppBuilder(); - winrt::Microsoft::ReactNative::ReactNativeAppBuilder AddPackageProviders( - winrt::Windows::Foundation::Collections::IVector const - &packageProviders); - winrt::Microsoft::ReactNative::ReactNativeAppBuilder SetReactInstanceSettings( - winrt::Microsoft::ReactNative::ReactInstanceSettings const &settings); - - // TODO: Currently, SetCompositor API is not exposed to the developer. - // Compositor depends on the DispatcherQueue created by DispatcherQueueController on a current thread - // or dedicated thread. So we also have to make a provision for setting DispatcherQueueController. + winrt::Microsoft::ReactNative::ReactNativeAppBuilder SetDispatcherQueueController( + winrt::Microsoft::UI::Dispatching::DispatcherQueueController const &dispatcherQueueController); winrt::Microsoft::ReactNative::ReactNativeAppBuilder SetCompositor( winrt::Microsoft::UI::Composition::Compositor const &compositor); winrt::Microsoft::ReactNative::ReactNativeAppBuilder SetAppWindow( winrt::Microsoft::UI::Windowing::AppWindow const &appWindow); - winrt::Microsoft::ReactNative::ReactNativeAppBuilder SetReactViewOptions( - winrt::Microsoft::ReactNative::ReactViewOptions const &reactViewOptions); winrt::Microsoft::ReactNative::ReactNativeWin32App Build(); private: - winrt::Microsoft::ReactNative::ReactViewOptions m_reactViewOptions{}; winrt::Microsoft::ReactNative::ReactNativeWin32App m_reactNativeWin32App{nullptr}; }; } // namespace winrt::Microsoft::ReactNative::implementation diff --git a/vnext/Microsoft.ReactNative/ReactNativeAppBuilder.idl b/vnext/Microsoft.ReactNative/ReactNativeAppBuilder.idl index 616cf035b57..b1940549616 100644 --- a/vnext/Microsoft.ReactNative/ReactNativeAppBuilder.idl +++ b/vnext/Microsoft.ReactNative/ReactNativeAppBuilder.idl @@ -9,60 +9,39 @@ import "IReactPackageProvider.idl"; namespace Microsoft.ReactNative { [experimental] - DOC_STRING("ReactNativeWin32App is used to manage resources appropriately to be able to host ReactNative component in a contentIsland.") + DOC_STRING("ReactNativeWin32App sets up the infrastructure for the default experience of a ReactNative application filling a WinAppSDK window.") runtimeclass ReactNativeWin32App { // Properties Microsoft.UI.Windowing.AppWindow AppWindow {get;}; - Microsoft.UI.Composition.Compositor Compositor {get;}; - - Microsoft.UI.Content.DesktopChildSiteBridge DesktopChildSiteBridge {get;}; - ReactNativeHost ReactNativeHost {get;}; - ReactNativeIsland ReactNativeIsland {get;}; + ReactViewOptions ReactViewOptions {get;}; + + // TODO: Hide these APIs for now until we're sure we need to expose them and can do so safely + // Microsoft.UI.Composition.Compositor Compositor {get;}; + // Microsoft.UI.Content.DesktopChildSiteBridge DesktopChildSiteBridge {get;}; + // Microsoft.UI.Dispatching DispatcherQueueController {get;}; + // ReactNativeIsland ReactNativeIsland {get;}; // Methods void Start(); } [experimental] - DOC_STRING("This is the builder for creating ReactInstanceSettings.") - runtimeclass ReactInstanceSettingsBuilder - { - ReactInstanceSettingsBuilder(); - - // Properties - ReactInstanceSettings ReactInstanceSettings {get;}; - - // Methods - ReactInstanceSettingsBuilder UseDirectDebugger(Boolean state); - - ReactInstanceSettingsBuilder UseDeveloperSupport(Boolean state); - - ReactInstanceSettingsBuilder BundleRootPath(String path); - - ReactInstanceSettingsBuilder DebugBundlePath(String path); - - ReactInstanceSettingsBuilder JavaScriptBundleFile(String file); - - ReactInstanceSettingsBuilder UseFastRefresh(Boolean state); - } - - [experimental] - DOC_STRING("ReactNativeAppBuilder initializes all the required infrastructure for a Win32 Fabric Application.") + DOC_STRING("ReactNativeAppBuilder builds a ReactNativeWin32App with the base WinAppSDK infrastructure.") runtimeclass ReactNativeAppBuilder { ReactNativeAppBuilder(); // Methods - ReactNativeAppBuilder AddPackageProviders(Windows.Foundation.Collections.IVector packageProviders); - - ReactNativeAppBuilder SetReactInstanceSettings(ReactInstanceSettings settings); ReactNativeAppBuilder SetAppWindow(Microsoft.UI.Windowing.AppWindow appWindow); - ReactNativeAppBuilder SetReactViewOptions(Microsoft.ReactNative.ReactViewOptions reactViewOptions); + // TODO: Hide these APIs for now until we're sure we need to expose them and can do so safely + // Compositor depends on the DispatcherQueue created by DispatcherQueueController + // ReactNativeAppBuilder SetCompositor(Microsoft.UI.Composition.Compositor compositor); + // ReactNativeAppBuilder SetDispatcherQueueController(Microsoft.UI.Dispatching DispatcherQueueController); ReactNativeWin32App Build(); } diff --git a/vnext/Microsoft.ReactNative/ReactNativeIsland.idl b/vnext/Microsoft.ReactNative/ReactNativeIsland.idl index 5c1b3f1311f..453bbb25107 100644 --- a/vnext/Microsoft.ReactNative/ReactNativeIsland.idl +++ b/vnext/Microsoft.ReactNative/ReactNativeIsland.idl @@ -78,11 +78,12 @@ namespace Microsoft.ReactNative DOC_STRING("A windows composition component that hosts React Native UI elements.") runtimeclass ReactNativeIsland { - DOC_STRING("Creates a new instance of @ReactNativeIsland.") + DOC_STRING("Creates a new instance of @ReactNativeIsland. Can be implemented with a ReactViewHost or a ComponentView with reactContext") ReactNativeIsland(); #ifdef USE_WINUI3 ReactNativeIsland(Microsoft.UI.Composition.Compositor compositor); + ReactNativeIsland(Microsoft.UI.Composition.Compositor compositor, Microsoft.ReactNative.IReactContext context, Microsoft.ReactNative.ComponentView componentView); #endif DOC_STRING( @@ -97,7 +98,7 @@ namespace Microsoft.ReactNative DOC_STRING("ScaleFactor for this windows (DPI/96)") Single ScaleFactor {get; set;}; - + Single FontSizeMultiplier { get; }; DOC_STRING("Move focus to this @ReactNativeIsland") diff --git a/vnext/Microsoft.ReactNative/ReactNativeWin32App.cpp b/vnext/Microsoft.ReactNative/ReactNativeWin32App.cpp index 0477c740fb6..0ed20cb1251 100644 --- a/vnext/Microsoft.ReactNative/ReactNativeWin32App.cpp +++ b/vnext/Microsoft.ReactNative/ReactNativeWin32App.cpp @@ -1,12 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + #include "pch.h" #include "ReactNativeWin32App.h" #include "ReactNativeWin32App.g.cpp" + +#include "IReactDispatcher.h" +#include "ReactCoreInjection.h" #include "ReactNativeHost.h" + #include "winrt/Microsoft.UI.Composition.h" #include "winrt/Microsoft.UI.Content.h" +#include "winrt/Microsoft.UI.Dispatching.h" #include "winrt/Microsoft.UI.Interop.h" #include "winrt/Microsoft.UI.Windowing.h" +// Scaling factor for the window's content based on the DPI of the display where the window is located. +float ScaleFactor(HWND hwnd) noexcept { + return GetDpiForWindow(hwnd) / static_cast(USER_DEFAULT_SCREEN_DPI); +} + +void UpdateRootViewSizeToAppWindow( + winrt::Microsoft::ReactNative::ReactNativeIsland const &rootView, + winrt::Microsoft::UI::Windowing::AppWindow const &window) { + auto hwnd = winrt::Microsoft::UI::GetWindowFromWindowId(window.Id()); + auto scaleFactor = ScaleFactor(hwnd); + winrt::Windows::Foundation::Size size{ + window.ClientSize().Width / scaleFactor, window.ClientSize().Height / scaleFactor}; + // Do not relayout when minimized + if (window.Presenter().as().State() != + winrt::Microsoft::UI::Windowing::OverlappedPresenterState::Minimized) { + winrt::Microsoft::ReactNative::LayoutConstraints constraints; + constraints.LayoutDirection = winrt::Microsoft::ReactNative::LayoutDirection::Undefined; + constraints.MaximumSize = constraints.MinimumSize = size; + rootView.Arrange(constraints, {0, 0}); + } +} + namespace winrt::Microsoft::ReactNative::implementation { ReactNativeWin32App::ReactNativeWin32App() {} @@ -14,41 +44,44 @@ ReactNativeWin32App::~ReactNativeWin32App() { m_desktopChildSiteBridge = nullptr; // Destroy all Composition objects - m_compositor.Close(); - m_compositor = nullptr; -} - -void ReactNativeWin32App::AppWindow(winrt::Microsoft::UI::Windowing::AppWindow const &appWindow) { - m_appWindow = appWindow; + if (m_compositor != nullptr) { + m_compositor.Close(); + m_compositor = nullptr; + } } winrt::Microsoft::UI::Windowing::AppWindow ReactNativeWin32App::AppWindow() { return m_appWindow; } -void ReactNativeWin32App::Compositor(winrt::Microsoft::UI::Composition::Compositor const &compositor) { - m_compositor = compositor; +void ReactNativeWin32App::AppWindow(winrt::Microsoft::UI::Windowing::AppWindow const &appWindow) { + m_appWindow = appWindow; } winrt::Microsoft::UI::Composition::Compositor ReactNativeWin32App::Compositor() { return m_compositor; } -winrt::Microsoft::ReactNative::ReactNativeIsland ReactNativeWin32App::ReactNativeIsland() { - return m_reactNativeIsland; +void ReactNativeWin32App::Compositor(winrt::Microsoft::UI::Composition::Compositor const &compositor) { + m_compositor = compositor; } -void ReactNativeWin32App::ReactNativeIsland(winrt::Microsoft::ReactNative::ReactNativeIsland const &reactNativeIsland) { - m_reactNativeIsland = reactNativeIsland; +void ReactNativeWin32App::DesktopChildSiteBridge( + winrt::Microsoft::UI::Content::DesktopChildSiteBridge const &desktopChildSiteBridge) { + m_desktopChildSiteBridge = desktopChildSiteBridge; } winrt::Microsoft::UI::Content::DesktopChildSiteBridge ReactNativeWin32App::DesktopChildSiteBridge() { return m_desktopChildSiteBridge; } -void ReactNativeWin32App::DesktopChildSiteBridge( - winrt::Microsoft::UI::Content::DesktopChildSiteBridge const &desktopChildSiteBridge) { - m_desktopChildSiteBridge = desktopChildSiteBridge; +winrt::Microsoft::UI::Dispatching::DispatcherQueueController ReactNativeWin32App::DispatcherQueueController() { + return m_dispatcherQueueController; +} + +void ReactNativeWin32App::DispatcherQueueController( + winrt::Microsoft::UI::Dispatching::DispatcherQueueController const &dispatcherQueueController) { + m_dispatcherQueueController = dispatcherQueueController; } winrt::Microsoft::ReactNative::ReactNativeHost ReactNativeWin32App::ReactNativeHost() { @@ -62,12 +95,90 @@ void ReactNativeWin32App::ReactNativeHost(winrt::Microsoft::ReactNative::ReactNa m_host = host; } -void ReactNativeWin32App::DispatchQueueController( - winrt::Microsoft::UI::Dispatching::DispatcherQueueController const &dispatcherQueueController) { - m_dispatcherQueueController = dispatcherQueueController; +winrt::Microsoft::ReactNative::ReactNativeIsland ReactNativeWin32App::ReactNativeIsland() { + return m_reactNativeIsland; +} + +void ReactNativeWin32App::ReactNativeIsland(winrt::Microsoft::ReactNative::ReactNativeIsland const &reactNativeIsland) { + m_reactNativeIsland = reactNativeIsland; +} + +winrt::Microsoft::ReactNative::ReactViewOptions ReactNativeWin32App::ReactViewOptions() { + if (m_reactViewOptions == nullptr) { + m_reactViewOptions = winrt::make(); + } + return m_reactViewOptions; +} + +void ReactNativeWin32App::ReactViewOptions(winrt::Microsoft::ReactNative::ReactViewOptions const &viewOptions) { + m_reactViewOptions = viewOptions; } void ReactNativeWin32App::Start() { + // Show the hosting AppWindow + m_appWindow.Show(); + + // Currently set the property to use current thread dispatcher as a default UI dispatcher. + // TODO: Use the correct dispatcher from a developer-provided DispatcherQueueController + ReactNativeHost().InstanceSettings().Properties().Set( + ReactDispatcherHelper::UIDispatcherProperty(), ReactDispatcherHelper::UIThreadDispatcher()); + + auto hwnd{winrt::Microsoft::UI::GetWindowFromWindowId(m_appWindow.Id())}; + + winrt::Microsoft::ReactNative::ReactCoreInjection::SetTopLevelWindowId( + ReactNativeHost().InstanceSettings().Properties(), reinterpret_cast(hwnd)); + + winrt::Microsoft::ReactNative::Composition::CompositionUIService::SetCompositor( + ReactNativeHost().InstanceSettings(), m_compositor); + + // Start the react-native instance, which will create a JavaScript runtime and load the applications bundle. + ReactNativeHost().ReloadInstance(); + + // Create a RootView which will present a react-native component + if (m_reactNativeIsland == nullptr) { + m_reactNativeIsland = winrt::Microsoft::ReactNative::ReactNativeIsland(m_compositor); + } + + m_reactNativeIsland.ReactViewHost( + winrt::Microsoft::ReactNative::ReactCoreInjection::MakeViewHost(ReactNativeHost(), ReactViewOptions())); + + // Update the size of the RootView when the AppWindow changes size + m_appWindow.Changed([wkRootView = winrt::make_weak(m_reactNativeIsland)]( + winrt::Microsoft::UI::Windowing::AppWindow const &window, + winrt::Microsoft::UI::Windowing::AppWindowChangedEventArgs const &args) { + if (args.DidSizeChange() || args.DidVisibilityChange()) { + if (auto rootView = wkRootView.get()) { + UpdateRootViewSizeToAppWindow(rootView, window); + } + } + }); + + // Quit application when main window is closed + m_appWindow.Destroying([this]( + winrt::Microsoft::UI::Windowing::AppWindow const &window, + winrt::Windows::Foundation::IInspectable const & /*args*/) { + // Before we shutdown the application - unload the ReactNativeHost to give the javascript a chance to save any + // state + auto async = ReactNativeHost().UnloadInstance(); + async.Completed([this](auto asyncInfo, winrt::Windows::Foundation::AsyncStatus asyncStatus) { + assert(asyncStatus == winrt::Windows::Foundation::AsyncStatus::Completed); + ReactNativeHost().InstanceSettings().UIDispatcher().Post([]() { PostQuitMessage(0); }); + }); + }); + + // DesktopChildSiteBridge create a ContentSite that can host the RootView ContentIsland + m_desktopChildSiteBridge = + winrt::Microsoft::UI::Content::DesktopChildSiteBridge::Create(m_compositor, m_appWindow.Id()); + + m_desktopChildSiteBridge.Connect(m_reactNativeIsland.Island()); + + m_desktopChildSiteBridge.ResizePolicy(winrt::Microsoft::UI::Content::ContentSizePolicy::ResizeContentToParentWindow); + + auto scaleFactor = ScaleFactor(hwnd); + m_reactNativeIsland.ScaleFactor(scaleFactor); + + UpdateRootViewSizeToAppWindow(m_reactNativeIsland, m_appWindow); + m_desktopChildSiteBridge.Show(); // Run the main application event loop diff --git a/vnext/Microsoft.ReactNative/ReactNativeWin32App.h b/vnext/Microsoft.ReactNative/ReactNativeWin32App.h index bb424cad169..dea84c1cdf3 100644 --- a/vnext/Microsoft.ReactNative/ReactNativeWin32App.h +++ b/vnext/Microsoft.ReactNative/ReactNativeWin32App.h @@ -1,5 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. #pragma once + #include "ReactNativeWin32App.g.h" + #include "winrt/Microsoft.UI.Dispatching.h" namespace winrt::Microsoft::ReactNative::implementation { @@ -13,26 +17,31 @@ struct ReactNativeWin32App : ReactNativeWin32AppT { winrt::Microsoft::UI::Composition::Compositor Compositor(); void Compositor(winrt::Microsoft::UI::Composition::Compositor const &compositor); - winrt::Microsoft::ReactNative::ReactNativeIsland ReactNativeIsland(); - void ReactNativeIsland(winrt::Microsoft::ReactNative::ReactNativeIsland const &reactNativeIsland); - winrt::Microsoft::UI::Content::DesktopChildSiteBridge DesktopChildSiteBridge(); void DesktopChildSiteBridge(winrt::Microsoft::UI::Content::DesktopChildSiteBridge const &desktopChildSiteBridge); - void DispatchQueueController( + winrt::Microsoft::UI::Dispatching::DispatcherQueueController DispatcherQueueController(); + void DispatcherQueueController( winrt::Microsoft::UI::Dispatching::DispatcherQueueController const &dispatcherQueueController); winrt::Microsoft::ReactNative::ReactNativeHost ReactNativeHost(); void ReactNativeHost(winrt::Microsoft::ReactNative::ReactNativeHost const &host); + winrt::Microsoft::ReactNative::ReactNativeIsland ReactNativeIsland(); + void ReactNativeIsland(winrt::Microsoft::ReactNative::ReactNativeIsland const &reactNativeIsland); + + winrt::Microsoft::ReactNative::ReactViewOptions ReactViewOptions(); + void ReactViewOptions(winrt::Microsoft::ReactNative::ReactViewOptions const &reactViewOptions); + void Start(); private: winrt::Microsoft::UI::Windowing::AppWindow m_appWindow{nullptr}; winrt::Microsoft::UI::Composition::Compositor m_compositor{nullptr}; + winrt::Microsoft::UI::Content::DesktopChildSiteBridge m_desktopChildSiteBridge{nullptr}; winrt::Microsoft::UI::Dispatching::DispatcherQueueController m_dispatcherQueueController{nullptr}; winrt::Microsoft::ReactNative::ReactNativeHost m_host{nullptr}; winrt::Microsoft::ReactNative::ReactNativeIsland m_reactNativeIsland{nullptr}; - winrt::Microsoft::UI::Content::DesktopChildSiteBridge m_desktopChildSiteBridge{nullptr}; + winrt::Microsoft::ReactNative::ReactViewOptions m_reactViewOptions{nullptr}; }; } // namespace winrt::Microsoft::ReactNative::implementation diff --git a/vnext/Microsoft.ReactNative/ViewProps.idl b/vnext/Microsoft.ReactNative/ViewProps.idl index 01cedda350d..577bb33af71 100644 --- a/vnext/Microsoft.ReactNative/ViewProps.idl +++ b/vnext/Microsoft.ReactNative/ViewProps.idl @@ -24,6 +24,8 @@ namespace Microsoft.ReactNative { Microsoft.UI.Composition.CompositionBrush AsBrush(Microsoft.ReactNative.Composition.Theme theme); #endif + Boolean Equals(Color color); + static Color Black(); static Color Transparent(); static Color ReadValue(IJSValueReader reader); diff --git a/vnext/Shared/Shared.vcxitems b/vnext/Shared/Shared.vcxitems index 27ca9ca8a8c..5e0edcf774b 100644 --- a/vnext/Shared/Shared.vcxitems +++ b/vnext/Shared/Shared.vcxitems @@ -89,6 +89,9 @@ $(ReactNativeWindowsDir)Microsoft.ReactNative\UriImageManager.idl Code + + true + true $(ReactNativeWindowsDir)Microsoft.ReactNative\CompositionComponentView.idl @@ -232,11 +235,6 @@ true $(ReactNativeWindowsDir)Microsoft.ReactNative\ReactNativeAppBuilder.idl Code - - - true - $(ReactNativeWindowsDir)Microsoft.ReactNative\ReactNativeAppBuilder.idl - Code @@ -349,11 +347,6 @@ true $(MSBuildThisFileDirectory)..\Microsoft.ReactNative\ReactNativeAppBuilder.idl Code - - - true - $(MSBuildThisFileDirectory)..\Microsoft.ReactNative\ReactNativeAppBuilder.idl - Code $(MSBuildThisFileDirectory)..\Microsoft.ReactNative\IJSValueReader.idl diff --git a/vnext/Shared/Shared.vcxitems.filters b/vnext/Shared/Shared.vcxitems.filters index b4b84e17920..1a6676626ff 100644 --- a/vnext/Shared/Shared.vcxitems.filters +++ b/vnext/Shared/Shared.vcxitems.filters @@ -326,6 +326,7 @@ + diff --git a/vnext/templates/cpp-app/windows/MyApp/MyApp.cpp b/vnext/templates/cpp-app/windows/MyApp/MyApp.cpp index f9b05f15862..e84bd5cdb3c 100644 --- a/vnext/templates/cpp-app/windows/MyApp/MyApp.cpp +++ b/vnext/templates/cpp-app/windows/MyApp/MyApp.cpp @@ -8,6 +8,7 @@ #include "NativeModules.h" +// A PackageProvider containing any turbo modules you define within this app project struct CompReactPackageProvider : winrt::implements { public: // IReactPackageProvider @@ -16,151 +17,66 @@ struct CompReactPackageProvider } }; -// Global Variables: -constexpr PCWSTR windowTitle = L"{{ mainComponentName }}"; -constexpr PCWSTR mainComponentName = L"{{ mainComponentName }}"; - -float ScaleFactor(HWND hwnd) noexcept { - return GetDpiForWindow(hwnd) / static_cast(USER_DEFAULT_SCREEN_DPI); -} +// The entry point of the Win32 application +_Use_decl_annotations_ int CALLBACK WinMain(HINSTANCE instance, HINSTANCE, PSTR /* commandLine */, int showCmd) { + // Initialize WinRT + winrt::init_apartment(winrt::apartment_type::single_threaded); -void UpdateRootViewSizeToAppWindow( - winrt::Microsoft::ReactNative::ReactNativeIsland const &rootView, - winrt::Microsoft::UI::Windowing::AppWindow const &window) { - auto hwnd = winrt::Microsoft::UI::GetWindowFromWindowId(window.Id()); - auto scaleFactor = ScaleFactor(hwnd); - winrt::Windows::Foundation::Size size{ - window.ClientSize().Width / scaleFactor, window.ClientSize().Height / scaleFactor}; - // Do not relayout when minimized - if (window.Presenter().as().State() != - winrt::Microsoft::UI::Windowing::OverlappedPresenterState::Minimized) { - winrt::Microsoft::ReactNative::LayoutConstraints constraints; - constraints.LayoutDirection = winrt::Microsoft::ReactNative::LayoutDirection::Undefined; - constraints.MaximumSize = constraints.MinimumSize = size; - rootView.Arrange(constraints, {0,0}); - } -} + // Enable per monitor DPI scaling + SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); -// Create and configure the ReactNativeHost -winrt::Microsoft::ReactNative::ReactNativeHost CreateReactNativeHost( - HWND hwnd, - const winrt::Microsoft::UI::Composition::Compositor &compositor) { + // Find the path hosting the app exe file WCHAR appDirectory[MAX_PATH]; GetModuleFileNameW(NULL, appDirectory, MAX_PATH); PathCchRemoveFileSpec(appDirectory, MAX_PATH); - auto host = winrt::Microsoft::ReactNative::ReactNativeHost(); - - // Include any autolinked modules - RegisterAutolinkedNativeModulePackages(host.PackageProviders()); + // Create a ReactNativeWin32App with the ReactNativeAppBuilder + auto reactNativeWin32App{winrt::Microsoft::ReactNative::ReactNativeAppBuilder().Build()}; - host.PackageProviders().Append(winrt::make()); + // Configure the initial InstanceSettings for the app's ReactNativeHost + auto settings{reactNativeWin32App.ReactNativeHost().InstanceSettings()}; + // Register any autolinked native modules + RegisterAutolinkedNativeModulePackages(settings.PackageProviders()); + // Register any native modules defined within this app project + settings.PackageProviders().Append(winrt::make()); #if BUNDLE - host.InstanceSettings().JavaScriptBundleFile(L"index.windows"); - host.InstanceSettings().BundleRootPath(std::wstring(L"file://").append(appDirectory).append(L"\\Bundle\\").c_str()); - host.InstanceSettings().UseFastRefresh(false); + // Load the JS bundle from a file (not Metro): + // Set the path (on disk) where the .bundle file is located + settings.BundleRootPath(std::wstring(L"file://").append(appDirectory).append(L"\\Bundle\\").c_str()); + // Set the name of the bundle file (without the .bundle extension) + settings.JavaScriptBundleFile(L"index.windows"); + // Disable hot reload + settings.UseFastRefresh(false); #else - host.InstanceSettings().JavaScriptBundleFile(L"index"); - host.InstanceSettings().UseFastRefresh(true); + // Load the JS bundle from Metro + settings.JavaScriptBundleFile(L"index"); + // Enable hot reload + settings.UseFastRefresh(true); #endif - #if _DEBUG - host.InstanceSettings().UseDirectDebugger(true); - host.InstanceSettings().UseDeveloperSupport(true); + // For Debug builds + // Enable Direct Debugging of JS + settings.UseDirectDebugger(true); + // Enable the Developer Menu + settings.UseDeveloperSupport(true); #else - host.InstanceSettings().UseDirectDebugger(false); - host.InstanceSettings().UseDeveloperSupport(false); + // For Release builds: + // Disable Direct Debugging of JS + settings.UseDirectDebugger(false); + // Disable the Developer Menu + settings.UseDeveloperSupport(false); #endif - winrt::Microsoft::ReactNative::ReactCoreInjection::SetTopLevelWindowId( - host.InstanceSettings().Properties(), reinterpret_cast(hwnd)); - - winrt::Microsoft::ReactNative::Composition::CompositionUIService::SetCompositor( - host.InstanceSettings(), compositor); - - return host; -} - -_Use_decl_annotations_ int CALLBACK WinMain(HINSTANCE instance, HINSTANCE, PSTR /* commandLine */, int showCmd) { - // Initialize WinRT. - winrt::init_apartment(winrt::apartment_type::single_threaded); - - // Enable per monitor DPI scaling - SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); - - // Create a DispatcherQueue for this thread. This is needed for Composition, Content, and - // Input APIs. - auto dispatcherQueueController{winrt::Microsoft::UI::Dispatching::DispatcherQueueController::CreateOnCurrentThread()}; - - // Create a Compositor for all Content on this thread. - auto compositor{winrt::Microsoft::UI::Composition::Compositor()}; - - // Create a top-level window. - auto window = winrt::Microsoft::UI::Windowing::AppWindow::Create(); - window.Title(windowTitle); - window.Resize({1000, 1000}); - window.Show(); - auto hwnd = winrt::Microsoft::UI::GetWindowFromWindowId(window.Id()); - auto scaleFactor = ScaleFactor(hwnd); - - auto host = CreateReactNativeHost(hwnd, compositor); - - // Start the react-native instance, which will create a JavaScript runtime and load the applications bundle - host.ReloadInstance(); - - // Create a RootView which will present a react-native component - winrt::Microsoft::ReactNative::ReactViewOptions viewOptions; - viewOptions.ComponentName(mainComponentName); - auto rootView = winrt::Microsoft::ReactNative::ReactNativeIsland(compositor); - rootView.ReactViewHost(winrt::Microsoft::ReactNative::ReactCoreInjection::MakeViewHost(host, viewOptions)); - - // Update the size of the RootView when the AppWindow changes size - window.Changed([wkRootView = winrt::make_weak(rootView)]( - winrt::Microsoft::UI::Windowing::AppWindow const &window, - winrt::Microsoft::UI::Windowing::AppWindowChangedEventArgs const &args) { - if (args.DidSizeChange() || args.DidVisibilityChange()) { - if (auto rootView = wkRootView.get()) { - UpdateRootViewSizeToAppWindow(rootView, window); - } - } - }); - - // Quit application when main window is closed - window.Destroying( - [host](winrt::Microsoft::UI::Windowing::AppWindow const &window, winrt::IInspectable const & /*args*/) { - // Before we shutdown the application - unload the ReactNativeHost to give the javascript a chance to save any - // state - auto async = host.UnloadInstance(); - async.Completed([host](auto asyncInfo, winrt::Windows::Foundation::AsyncStatus asyncStatus) { - assert(asyncStatus == winrt::Windows::Foundation::AsyncStatus::Completed); - host.InstanceSettings().UIDispatcher().Post([]() { PostQuitMessage(0); }); - }); - }); - - // DesktopChildSiteBridge create a ContentSite that can host the RootView ContentIsland - auto bridge = winrt::Microsoft::UI::Content::DesktopChildSiteBridge::Create(compositor, window.Id()); - bridge.Connect(rootView.Island()); - bridge.ResizePolicy(winrt::Microsoft::UI::Content::ContentSizePolicy::ResizeContentToParentWindow); - - rootView.ScaleFactor(scaleFactor); - - // Set the intialSize of the root view - UpdateRootViewSizeToAppWindow(rootView, window); - - bridge.Show(); - - // Run the main application event loop - dispatcherQueueController.DispatcherQueue().RunEventLoop(); - - // Rundown the DispatcherQueue. This drains the queue and raises events to let components - // know the message loop has finished. - dispatcherQueueController.ShutdownQueue(); + // Get the AppWindow so we can configure its initial title and size + auto appWindow{reactNativeWin32App.AppWindow()}; + appWindow.Title(L"{{ mainComponentName }}"); + appWindow.Resize({1000, 1000}); - bridge.Close(); - bridge = nullptr; + // Get the ReactViewOptions so we can set the initial RN component to load + auto viewOptions{reactNativeWin32App.ReactViewOptions()}; + viewOptions.ComponentName(L"{{ mainComponentName }}"); - // Destroy all Composition objects - compositor.Close(); - compositor = nullptr; + // Start the app + reactNativeWin32App.Start(); }