1/* 2 * Copyright (c) 2023-2024 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15/// <reference path="../../state_mgmt/src/lib/common/ifelse_native.d.ts" /> 16/// <reference path="../../state_mgmt/src/lib/puv2_common/puv2_viewstack_processor.d.ts" /> 17 18class BuilderNode { 19 private _JSBuilderNode: JSBuilderNode; 20 // the name of "nodePtr_" is used in ace_engine/interfaces/native/node/native_node_napi.cpp. 21 private nodePtr_: NodePtr; 22 constructor(uiContext: UIContext, options: RenderOptions) { 23 let jsBuilderNode = new JSBuilderNode(uiContext, options); 24 this._JSBuilderNode = jsBuilderNode; 25 let id = Symbol('BuilderRootFrameNode'); 26 BuilderNodeFinalizationRegisterProxy.ElementIdToOwningBuilderNode_.set(id, jsBuilderNode); 27 BuilderNodeFinalizationRegisterProxy.register(this, { name: 'BuilderRootFrameNode', idOfNode: id }); 28 } 29 public update(params: Object) { 30 this._JSBuilderNode.update(params); 31 } 32 public build(builder: WrappedBuilder<Object[]>, params?: Object, options?: BuildOptions,): void { 33 this._JSBuilderNode.build(builder, params, options); 34 this.nodePtr_ = this._JSBuilderNode.getNodePtr(); 35 } 36 public getNodePtr(): NodePtr { 37 return this._JSBuilderNode.getValidNodePtr(); 38 } 39 public getFrameNode(): FrameNode { 40 return this._JSBuilderNode.getFrameNode(); 41 } 42 public getFrameNodeWithoutCheck(): FrameNode | null { 43 return this._JSBuilderNode.getFrameNodeWithoutCheck(); 44 } 45 public postTouchEvent(touchEvent: TouchEvent): boolean { 46 __JSScopeUtil__.syncInstanceId(this._JSBuilderNode.getInstanceId()); 47 let ret = this._JSBuilderNode.postTouchEvent(touchEvent); 48 __JSScopeUtil__.restoreInstanceId(); 49 return ret; 50 } 51 public dispose(): void { 52 this._JSBuilderNode.dispose(); 53 } 54 public reuse(param?: Object): void { 55 this._JSBuilderNode.reuse(param); 56 } 57 public recycle(): void { 58 this._JSBuilderNode.recycle(); 59 } 60 public updateConfiguration(): void { 61 this._JSBuilderNode.updateConfiguration(); 62 } 63 public onReuseWithBindObject(param?: Object): void { 64 this._JSBuilderNode.onReuseWithBindObject(param); 65 } 66 public onRecycleWithBindObject(): void { 67 this._JSBuilderNode.onRecycleWithBindObject(); 68 } 69} 70 71class JSBuilderNode extends BaseNode { 72 private params_: Object; 73 private uiContext_: UIContext; 74 private frameNode_: FrameNode; 75 private _nativeRef: NativeStrongRef; 76 private _supportNestingBuilder: boolean; 77 private _proxyObjectParam: Object; 78 private bindedViewOfBuilderNode:ViewPU; 79 80 constructor(uiContext: UIContext, options?: RenderOptions) { 81 super(uiContext, options); 82 this.uiContext_ = uiContext; 83 this.updateFuncByElmtId = new UpdateFuncsByElmtId(); 84 this._supportNestingBuilder = false; 85 } 86 public reuse(param: Object): void { 87 this.updateStart(); 88 this.childrenWeakrefMap_.forEach((weakRefChild) => { 89 const child = weakRefChild.deref(); 90 if (child) { 91 if (child instanceof ViewPU) { 92 child.aboutToReuseInternal(param); 93 } 94 else { 95 // FIXME fix for mixed V2 - V3 Hierarchies 96 throw new Error('aboutToReuseInternal: Recycle not implemented for ViewV2, yet'); 97 } 98 } // if child 99 }); 100 this.updateEnd(); 101 } 102 public recycle(): void { 103 this.childrenWeakrefMap_.forEach((weakRefChild) => { 104 const child = weakRefChild.deref(); 105 if (child) { 106 if (child instanceof ViewPU) { 107 child.aboutToRecycleInternal(); 108 } 109 else { 110 // FIXME fix for mixed V2 - V3 Hierarchies 111 throw new Error('aboutToRecycleInternal: Recycle not yet implemented for ViewV2'); 112 } 113 } // if child 114 }); 115 } 116 public onReuseWithBindObject(param?: Object): void { 117 __JSScopeUtil__.syncInstanceId(this.instanceId_); 118 super.onReuseWithBindObject(param); 119 __JSScopeUtil__.restoreInstanceId(); 120 } 121 public onRecycleWithBindObject(): void { 122 __JSScopeUtil__.syncInstanceId(this.instanceId_); 123 super.onRecycleWithBindObject(); 124 __JSScopeUtil__.restoreInstanceId(); 125 } 126 public getCardId(): number { 127 return -1; 128 } 129 private isObject(param: Object): boolean { 130 const typeName = Object.prototype.toString.call(param); 131 const objectName = `[object Object]`; 132 if (typeName === objectName) { 133 return true; 134 } else { 135 return false; 136 } 137 } 138 private buildWithNestingBuilder(builder: WrappedBuilder<Object[]>, supportLazyBuild: boolean): void { 139 if (this._supportNestingBuilder && this.isObject(this.params_)) { 140 this._proxyObjectParam = new Proxy(this.params_, { 141 set(target, property, val): boolean { 142 throw Error(`@Builder : Invalid attempt to set(write to) parameter '${property.toString()}' error!`); 143 }, 144 get: (target, property, receiver): Object => { return this.params_?.[property] } 145 }); 146 this.nodePtr_ = super.create(builder.builder?.bind(this.bindedViewOfBuilderNode ? this.bindedViewOfBuilderNode : this), 147 this._proxyObjectParam, this.updateNodeFromNative, this.updateConfiguration, supportLazyBuild); 148 } else { 149 this.nodePtr_ = super.create(builder.builder?.bind(this.bindedViewOfBuilderNode ? this.bindedViewOfBuilderNode : this), 150 this.params_, this.updateNodeFromNative, this.updateConfiguration, supportLazyBuild); 151 } 152 } 153 public build(builder: WrappedBuilder<Object[]>, params?: Object, options?: BuildOptions): void { 154 __JSScopeUtil__.syncInstanceId(this.instanceId_); 155 this._supportNestingBuilder = options?.nestingBuilderSupported ? options.nestingBuilderSupported : false; 156 const supportLazyBuild = options?.lazyBuildSupported ? options.lazyBuildSupported : false; 157 this.bindedViewOfBuilderNode = options?.bindedViewOfBuilderNode; 158 this.params_ = params; 159 this.updateFuncByElmtId.clear(); 160 if(this.bindedViewOfBuilderNode){ 161 globalThis.__viewPuStack__?.push(this.bindedViewOfBuilderNode); 162 } 163 this.buildWithNestingBuilder(builder, supportLazyBuild); 164 if(this.bindedViewOfBuilderNode){ 165 globalThis.__viewPuStack__?.pop(); 166 } 167 this._nativeRef = getUINativeModule().nativeUtils.createNativeStrongRef(this.nodePtr_); 168 if (this.frameNode_ === undefined || this.frameNode_ === null) { 169 this.frameNode_ = new BuilderRootFrameNode(this.uiContext_); 170 } 171 this.frameNode_.setNodePtr(this._nativeRef, this.nodePtr_); 172 this.frameNode_.setRenderNode(this._nativeRef); 173 this.frameNode_.setBaseNode(this); 174 __JSScopeUtil__.restoreInstanceId(); 175 } 176 public update(param: Object) { 177 __JSScopeUtil__.syncInstanceId(this.instanceId_); 178 this.updateStart(); 179 this.purgeDeletedElmtIds(); 180 this.params_ = param; 181 Array.from(this.updateFuncByElmtId.keys()).sort((a: number, b: number): number => { 182 return (a < b) ? -1 : (a > b) ? 1 : 0; 183 }).forEach(elmtId => this.UpdateElement(elmtId)); 184 this.updateEnd(); 185 __JSScopeUtil__.restoreInstanceId(); 186 } 187 public updateConfiguration(): void { 188 __JSScopeUtil__.syncInstanceId(this.instanceId_); 189 this.updateStart(); 190 this.purgeDeletedElmtIds(); 191 Array.from(this.updateFuncByElmtId.keys()).sort((a: number, b: number): number => { 192 return (a < b) ? -1 : (a > b) ? 1 : 0; 193 }).forEach(elmtId => this.UpdateElement(elmtId)); 194 for (const child of this.childrenWeakrefMap_.values()) { 195 const childView = child.deref(); 196 if (childView) { 197 childView.forceCompleteRerender(true); 198 } 199 } 200 this.updateEnd(); 201 __JSScopeUtil__.restoreInstanceId(); 202 } 203 private UpdateElement(elmtId: number): void { 204 // do not process an Element that has been marked to be deleted 205 const obj: UpdateFunc | UpdateFuncRecord | undefined = this.updateFuncByElmtId.get(elmtId); 206 const updateFunc = (typeof obj === 'object') ? obj.getUpdateFunc() : null; 207 if (typeof updateFunc === 'function') { 208 updateFunc(elmtId, /* isFirstRender */ false); 209 this.finishUpdateFunc(); 210 } 211 } 212 213 public purgeDeleteElmtId(rmElmtId: number): boolean { 214 const result = this.updateFuncByElmtId.delete(rmElmtId); 215 if (result) { 216 UINodeRegisterProxy.ElementIdToOwningViewPU_.delete(rmElmtId); 217 } 218 return result; 219 } 220 221 public getFrameNode(): FrameNode | null { 222 if ( 223 this.frameNode_ !== undefined && 224 this.frameNode_ !== null && 225 this.frameNode_.getNodePtr() !== null 226 ) { 227 return this.frameNode_; 228 } 229 return null; 230 } 231 232 public getFrameNodeWithoutCheck(): FrameNode | null | undefined { 233 return this.frameNode_; 234 } 235 236 public observeComponentCreation(func: (arg0: number, arg1: boolean) => void) { 237 let elmId: number = ViewStackProcessor.AllocateNewElmetIdForNextComponent(); 238 UINodeRegisterProxy.ElementIdToOwningViewPU_.set(elmId, new WeakRef(this)); 239 try { 240 func(elmId, true); 241 } catch (error) { 242 // avoid the incompatible change that move set function before updateFunc. 243 UINodeRegisterProxy.ElementIdToOwningViewPU_.delete(elmId); 244 throw error; 245 } 246 } 247 248 public observeComponentCreation2(compilerAssignedUpdateFunc: UpdateFunc, classObject: { prototype: Object; pop?: () => void }): void { 249 const _componentName: string = classObject && 'name' in classObject ? (Reflect.get(classObject, 'name') as string) : 'unspecified UINode'; 250 const _popFunc: () => void = 251 classObject && 'pop' in classObject ? classObject.pop! : () => { }; 252 const updateFunc = (elmtId: number, isFirstRender: boolean): void => { 253 __JSScopeUtil__.syncInstanceId(this.instanceId_); 254 ViewBuildNodeBase.arkThemeScopeManager?.onComponentCreateEnter(_componentName, elmtId, isFirstRender, this); 255 ViewStackProcessor.StartGetAccessRecordingFor(elmtId); 256 // if V2 @Observed/@Track used anywhere in the app (there is no more fine grained criteria), 257 // enable V2 object deep observation 258 // FIXME: A @Component should only use PU or V2 state, but ReactNative dynamic viewer uses both. 259 if (ConfigureStateMgmt.instance.needsV2Observe()) { 260 // FIXME: like in V2 setting bindId_ in ObserveV2 does not work with 'stacked' 261 // update + initial render calls, like in if and ForEach case, convert to stack as well 262 ObserveV2.getObserve().startRecordDependencies(this, elmtId, true); 263 } 264 if (this._supportNestingBuilder) { 265 compilerAssignedUpdateFunc(elmtId, isFirstRender); 266 } else { 267 compilerAssignedUpdateFunc(elmtId, isFirstRender, this.params_); 268 } 269 if (!isFirstRender) { 270 _popFunc(); 271 } 272 if (ConfigureStateMgmt.instance.needsV2Observe()) { 273 ObserveV2.getObserve().stopRecordDependencies(); 274 } 275 ViewStackProcessor.StopGetAccessRecording(); 276 ViewBuildNodeBase.arkThemeScopeManager?.onComponentCreateExit(elmtId); 277 __JSScopeUtil__.restoreInstanceId(); 278 }; 279 280 const elmtId = ViewStackProcessor.AllocateNewElmetIdForNextComponent(); 281 // needs to move set before updateFunc. 282 // make sure the key and object value exist since it will add node in attributeModifier during updateFunc. 283 this.updateFuncByElmtId.set(elmtId, { updateFunc: updateFunc, classObject: classObject }); 284 UINodeRegisterProxy.ElementIdToOwningViewPU_.set(elmtId, new WeakRef(this)); 285 try { 286 updateFunc(elmtId, /* is first render */ true); 287 } catch (error) { 288 // avoid the incompatible change that move set function before updateFunc. 289 this.updateFuncByElmtId.delete(elmtId); 290 UINodeRegisterProxy.ElementIdToOwningViewPU_.delete(elmtId); 291 throw error; 292 } 293 } 294 295 /** 296 Partial updates for ForEach. 297 * @param elmtId ID of element. 298 * @param itemArray Array of items for use of itemGenFunc. 299 * @param itemGenFunc Item generation function to generate new elements. If index parameter is 300 * given set itemGenFuncUsesIndex to true. 301 * @param idGenFunc ID generation function to generate unique ID for each element. If index parameter is 302 * given set idGenFuncUsesIndex to true. 303 * @param itemGenFuncUsesIndex itemGenFunc optional index parameter is given or not. 304 * @param idGenFuncUsesIndex idGenFunc optional index parameter is given or not. 305 */ 306 public forEachUpdateFunction( 307 elmtId: number, 308 itemArray: Array<any>, 309 itemGenFunc: (item: any, index?: number) => void, 310 idGenFunc?: (item: any, index?: number) => string, 311 itemGenFuncUsesIndex: boolean = false, 312 idGenFuncUsesIndex: boolean = false 313 ): void { 314 if (itemArray === null || itemArray === undefined) { 315 return; 316 } 317 318 if (itemGenFunc === null || itemGenFunc === undefined) { 319 return; 320 } 321 322 if (idGenFunc === undefined) { 323 idGenFuncUsesIndex = true; 324 // catch possible error caused by Stringify and re-throw an Error with a meaningful (!) error message 325 idGenFunc = (item: any, index: number): string => { 326 try { 327 return `${index}__${JSON.stringify(item)}`; 328 } catch (e) { 329 throw new Error( 330 ` ForEach id ${elmtId}: use of default id generator function not possible on provided data structure. Need to specify id generator function (ForEach 3rd parameter). Application Error!` 331 ); 332 } 333 }; 334 } 335 336 let diffIndexArray = []; // New indexes compared to old one. 337 let newIdArray = []; 338 let idDuplicates = []; 339 const arr = itemArray; // just to trigger a 'get' onto the array 340 341 // ID gen is with index. 342 if (idGenFuncUsesIndex) { 343 // Create array of new ids. 344 arr.forEach((item, indx) => { 345 newIdArray.push(idGenFunc(item, indx)); 346 }); 347 } else { 348 // Create array of new ids. 349 arr.forEach((item, index) => { 350 newIdArray.push( 351 `${itemGenFuncUsesIndex ? index + '_' : ''}` + idGenFunc(item) 352 ); 353 }); 354 } 355 // removedChildElmtIds will be filled with the elmtIds of all children and their children will be deleted in response to foreach change 356 let removedChildElmtIds = []; 357 // Set new array on C++ side. 358 // C++ returns array of indexes of newly added array items. 359 // these are indexes in new child list. 360 ForEach.setIdArray(elmtId, newIdArray, diffIndexArray, idDuplicates, removedChildElmtIds); 361 // Item gen is with index. 362 diffIndexArray.forEach((indx) => { 363 ForEach.createNewChildStart(newIdArray[indx], this); 364 if (itemGenFuncUsesIndex) { 365 itemGenFunc(arr[indx], indx); 366 } else { 367 itemGenFunc(arr[indx]); 368 } 369 ForEach.createNewChildFinish(newIdArray[indx], this); 370 }); 371 // un-registers the removed child elementIDs using proxy 372 UINodeRegisterProxy.unregisterRemovedElmtsFromViewPUs(removedChildElmtIds); 373 // purging these elmtIds from state mgmt will make sure no more update function on any deleted child will be executed 374 this.purgeDeletedElmtIds(); 375 } 376 public getNodePtr(): NodePtr { 377 return this.nodePtr_; 378 } 379 public getValidNodePtr(): NodePtr { 380 return this._nativeRef?.getNativeHandle(); 381 } 382 public dispose(): void { 383 this.frameNode_?.dispose(); 384 } 385 public disposeNode(): void { 386 super.disposeNode(); 387 this.nodePtr_ = null; 388 this._nativeRef = null; 389 this.frameNode_?.resetNodePtr(); 390 } 391 updateInstance(uiContext: UIContext): void { 392 this.uiContext_ = uiContext; 393 this.instanceId_ = uiContext.instanceId_; 394 if (this.frameNode_ !== undefined && this.frameNode_ !== null) { 395 this.frameNode_.updateInstance(uiContext); 396 } 397 } 398 399 private updateNodePtr(nodePtr: NodePtr) 400 { 401 if (nodePtr != this.nodePtr_) { 402 this.dispose(); 403 this.nodePtr_ = nodePtr; 404 this._nativeRef = getUINativeModule().nativeUtils.createNativeStrongRef(this.nodePtr_); 405 this.frameNode_.setNodePtr(this._nativeRef, this.nodePtr_); 406 } 407 } 408 409 private updateInstanceId(instanceId: number) 410 { 411 this.instanceId_ = instanceId; 412 } 413 414 protected updateNodeFromNative(instanceId: number, nodePtr: NodePtr) 415 { 416 this.updateNodePtr(nodePtr); 417 this.updateInstanceId(instanceId); 418 } 419 420 public observeRecycleComponentCreation(name: string, recycleUpdateFunc: RecycleUpdateFunc): void { 421 throw new Error('custom component in @Builder used by BuilderNode does not support @Reusable'); 422 } 423 public ifElseBranchUpdateFunctionDirtyRetaken(): void {} 424 public forceCompleteRerender(deep: boolean): void {} 425 public forceRerenderNode(elmtId: number): void {} 426} 427