1/* 2 * Copyright (c) 2022-2023 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 */ 15import { float32, int32 } from "@koalaui/common" 16import { Access, ptrToBits, withFloat32Array, KFloat32ArrayPtr, Wrapper, pointer, bitsToPtr, 17 KPointer, KInt, KFloat, wrapCallback, 18 nullptr, Finalizable } from "@koalaui/interop" 19import { ArkUINativeModule } from "./generated/ArkUINativeModule" 20import { DataNode, IncrementalNode, contextNode } from "@koalaui/runtime" 21import { 22 AreaChangePeerEvent, 23 CallbackPeerEvent, 24 GesturePeerEvent, 25 ImageCompletePeerEvent, ImageErrorPeerEvent, 26 KeyPeerEvent, 27 MousePeerEvent, 28 PeerEvent, 29 SinglePointerPeerEvent, 30 XComponentLoadPeerEvent, 31 PeerEnterKeyType, 32 PeerSubmitEvent 33} from "./PeerEvents" 34import { NativePeerNode } from "./NativePeerNode" 35import { WebLoadInterceptDataType } from "./WebResourceRequest" 36import { ArkAlignment } from "./PeerLayout" 37 38export function IntToFloatArray(array: Int32Array, offset: number = 0, length?: number): Float32Array { 39 length = length ?? (array.length - offset) 40 const data = new Float32Array(length) 41 for (let i = 0; i < data.length; i++) { 42 data[i] = array[i + offset] 43 } 44 return data 45} 46 47// This class doesn't manage its pointer, so not Finalizable. 48export class PeerCanvas extends Wrapper { 49 constructor(ptr: pointer) { 50 super(ptr) 51 } 52 // TODO: commonize with Skoala! 53 drawRect(left: float32, top: float32, right: float32, botton: float32, paint: PeerPaint) { 54 console.log("drawRect: method not implemented") 55 } 56} 57 58 59export class PeerPaint extends Finalizable { 60 private constructor(ptr: pointer) { 61 super(ptr, 0) 62 } 63 // TODO: commonize with Skoala! 64 static make(): PeerPaint { 65 console.log("make: method not implemented") 66 return new PeerPaint(0) 67 } 68 setColor(color: int32) { 69 console.log("setColor: method not implemented") 70 } 71 getColor(): int32 { 72 console.log("getColor: method not implemented") 73 return 0; 74 } 75} 76 77 78export enum DirtyFlags { 79 Geometry = 1, // 1 << 0, 80 Visual = 2, // 1 << 1, 81} 82 83export interface CustomizableNode { 84 onMeasure(args: Float32Array): int32 85 onLayout(args: Float32Array): int32 86 onDraw(canvas: PeerCanvas, args: Float32Array): int32 87 88 firstChildNode(): CustomizableNode|undefined 89 previousSiblingNode(): CustomizableNode|undefined 90 nextSiblingNode(): CustomizableNode|undefined 91 parentNode(): CustomizableNode|undefined 92 93 markDirty(flags: int32): void 94 getDirty(): int32 95 clearDirty(flags: int32): void 96} 97 98export const PeerNodeType = 11 99export const PartialPropertiesType = 13 100export const GeneratedPartialPropertiesType = 14 101export const LegacyNodeType = 17 102 103function parentWithPeer(node?: IncrementalNode): PeerNode | undefined { 104 node = node?.parent 105 while (node) { 106 if (node.isKind(PeerNodeType)) return node as PeerNode 107 node = node.parent 108 } 109 return undefined 110} 111 112export interface Sized { 113 size(): number 114} 115 116export class LazyRangeStartMarker { 117 public onRangeUpdate: (startIndex: number, endIndex: number) => void 118 public currentStart: () => number 119 public currentEnd: () => number 120 private sizedRange: Sized 121 122 constructor(onRangeUpdate: (startIndex: number, endIndex: number) => void, sizedRange: Sized, 123 currentStart: () => number, currentEnd: () => number) { 124 this.sizedRange = sizedRange 125 this.currentEnd = currentEnd 126 this.currentStart = currentStart 127 this.onRangeUpdate = onRangeUpdate 128 } 129 130 rangeSize(): number { 131 return this.sizedRange.size() 132 } 133} 134 135export class LazyRangeEndMarker { 136 constructor() { } 137} 138 139export interface Properties { 140 onClick: (event: SinglePointerPeerEvent) => void 141 onSwiperChange: (value: number) => void 142 onTabsChange: (value: number) => void 143 onVisibleAreaChange: (isVisible: boolean, currentRatio: number) => void 144 lazyRangeStart: LazyRangeStartMarker 145 lazyRangeEnd: LazyRangeEndMarker 146 onAppear: () => void 147 onDisappear: () => void 148 onScrollIndex: (first: number, last: number) => void 149 onNavigatorClick: () => void 150 onAction: (event: GesturePeerEvent) => void 151 onActionStart: (event: GesturePeerEvent) => void 152 onActionUpdate: (event: GesturePeerEvent) => void 153 onActionEnd: (event: GesturePeerEvent) => void 154 onActionCancel: () => void 155 onTextInput: (text: string) => void 156 onSwiperAnimationStart: (index: number, targetIndex: number, currentOffset: number, targetOffset: number, velocity: number) => void 157 onSwiperAnimationEnd: (index: number, currentOffset: number, targetOffset: number, velocity: number) => void 158 onSwiperGestureSwipe: (index: number, currentOffset: number, targetOffset: number, velocity: number) => void 159 onAreaChange: (event: AreaChangePeerEvent) => void 160 onBlur: () => void 161 onCanvasReady: () => void 162 onListScroll:(scrollOffset: number, scrollState: number) => void 163 onListScrollIndex:(start: number, end: number, center: number) => void 164 onListScrollStart:() => void 165 onListScrollStop:() => void 166 onWebLoadIntercept: (event: WebLoadInterceptDataType) => boolean 167 onToggleChange: (isOn: boolean) => void 168 onTextInputEditChange: (isEditing: boolean) => void 169 onTextInputSubmit: (enterKey: PeerEnterKeyType, event: PeerSubmitEvent) => void 170 onTextInputChange: (value: string) => void 171 onSliderChange: (value: number, mode: number) => void 172 onHover: (isHover: boolean) => void 173 onKeyEvent: (event: KeyPeerEvent) => void 174 onMouse: (event: MousePeerEvent) => void 175 onImageComplete: (event: ImageCompletePeerEvent) => void 176 onImageError: (event: ImageErrorPeerEvent) => void 177 onRefreshStateChange: (state: number) => void 178 onRefreshing:() => void 179 onRadioChange: (isChecked: boolean) => void 180 onGridScroll:(scrollOffset: number, scrollState: number) => void 181 onGridScrollStart:() => void 182 onGridScrollStop:() => void 183 onSideBarChange:(value: boolean) => void 184 onXComponentLoad: (event: XComponentLoadPeerEvent) => void 185 onXComponentDestroy: () => void 186 onNavBarStateChange: (isVisible: boolean) => void 187 navDestination: (name: string, param: unknown) => void 188} 189 190/** @memo */ 191export function UseProperties(properties: Partial<Properties>) { 192 const parent = contextNode<PeerNode>(PeerNodeType) 193 DataNode.attach(PartialPropertiesType, properties, () => { 194 parent.invalidateProperties() 195 }) 196} 197 198function findPeerNode(node: IncrementalNode): PeerNode | undefined { 199 if (node.isKind(PeerNodeType)) return node as PeerNode 200 for (let child = node.firstChild; child; child = child.nextSibling) { 201 let peer = findPeerNode(child) 202 if (peer) return peer 203 } 204 return undefined 205} 206 207let currentPostman = (node: PeerNode, peerEvent: PeerEvent, props: Partial<Properties>) => { } 208 209export function setEventDeliverer(postman: 210 (node: PeerNode, peerEvent: PeerEvent, props: Partial<Properties>) => void) { 211 currentPostman = postman 212} 213 214function intersect(interval1: number[], interval2: number[]): number[] | undefined { 215 let min = Math.max(interval1[0], interval2[0]) 216 let max = Math.min(interval1[1], interval2[1]) 217 return max < min ? undefined : [min, max] 218} 219 220export enum NativeNodeFlags { 221 None = 0, 222 CustomMeasure = 1, // 1 << 0, 223 CustomLayout = 2, // 1 << 1, 224 CustomDraw = 4, // 1 << 2, 225} 226 227export enum CustomNodeOpType { 228 Measure = 1, 229 Layout = 2, 230 Draw = 3 231} 232 233export const UndefinedDimensionUnit = -1 234const initialId = 999 235export class PeerNode extends IncrementalNode implements CustomizableNode { 236 peer: NativePeerNode 237 private id: number 238 private insertMark: pointer = nullptr 239 private insertDirection: int32 = 0 240 241 setInsertMark(mark: pointer, upDirection: boolean) { 242 // console.log(`setInsertMark 0x${mark.toString(16)} ${upDirection ? "up" : "down"}`) 243 this.insertMark = mark 244 this.insertDirection = upDirection ? 0 : 1 245 } 246 247 getId(): number { return this.id } 248 getPeerId(): number { return this.peer.id } 249 getPeerPtr(): pointer { return this.peer.ptr } 250 static nextId(): int32 { return ++PeerNode.currentId } 251 252 private flags: int32 253 getFlags(): int32 { return this.flags } 254 255 alignment: int32 = ArkAlignment.Center 256 measureResult: Float32Array = new Float32Array(4) 257 layoutResult: Float32Array = new Float32Array(4) 258 259 protected static currentId = initialId 260 private static peerNodeMap = new Map<number, PeerNode>() 261 262 static findPeerByNativeId(id: number): PeerNode | undefined { 263 return PeerNode.peerNodeMap.get(id) 264 } 265 266 static deliverEventFromPeer(event: PeerEvent) { 267 let peer = PeerNode.findPeerByNativeId(event.nodeId) 268 peer?.deliverEvent(event) 269 } 270 271 _name: string = "PeerNode" 272 constructor(peerPtr: pointer, id: int32, name: string, flags: int32) { 273 super(PeerNodeType) 274 this.id = id 275 this.peer = NativePeerNode.create(this, peerPtr, flags) 276 277 this.flags = flags 278 this._name = name 279 PeerNode.peerNodeMap.set(this.id, this) 280 this.onChildInserted = (child: IncrementalNode) => { 281 let peer = findPeerNode(child)! 282 if (peer) { 283 let peerPtr = peer.getPeerPtr() 284 if (this.insertMark != nullptr) { 285 if (this.insertDirection == 0) { 286 this.peer.insertChildBefore(peerPtr, this.insertMark) 287 } else { 288 this.peer.insertChildAfter(peerPtr, this.insertMark) 289 } 290 this.insertMark = peerPtr 291 return 292 } 293 294 // Find the closest peer node backward. 295 // TODO: rework to avoid search 296 let sibling: PeerNode | undefined = undefined 297 for (let node = child.previousSibling; node; node = node.previousSibling) { 298 if (node.isKind(PeerNodeType)) { 299 sibling = node as PeerNode 300 break 301 } 302 } 303 this.peer.insertChildAfter(peerPtr, sibling?.getPeerPtr() ?? nullptr) 304 } 305 } 306 this.onChildRemoved = (child: IncrementalNode) => { 307 let peer = findPeerNode(child) 308 if (peer) { 309 this.peer.removeChild(peer.peer) 310 } 311 } 312 } 313 314 customizationCallback(args: Int32Array): int32 { 315 switch (args[0]) { 316 case CustomNodeOpType.Measure: { 317 return this.onMeasure(IntToFloatArray(args, 1)) 318 } 319 case CustomNodeOpType.Layout: { 320 return this.onLayout(IntToFloatArray(args, 1)) 321 } 322 case CustomNodeOpType.Draw: return this.onDraw(new PeerCanvas(bitsToPtr(args, 1)), IntToFloatArray(args, 3)) 323 } 324 return 0 325 } 326 327 onMeasure(args: Float32Array): int32 { 328 if ((this.flags & NativeNodeFlags.CustomMeasure) == 0) { 329 // This node doesn't have managed measure, call native instead. 330 let result = 0 331 withFloat32Array(args, Access.READ | Access.WRITE, (argsPtr: KFloat32ArrayPtr) => { 332 // Call native measure 333 result = ArkUINativeModule._MeasureNode(this.peer.ptr, argsPtr) 334 }) 335 return result 336 } 337 if ((this.getDirty() & DirtyFlags.Geometry) == 0) return 0 338 // default behavior 339 const measureArray = new Float32Array(args) 340 for (let child = this.firstChildNode(); child != undefined; child = child.nextSiblingNode()) { 341 let childPeer = child as PeerNode 342 childPeer.onMeasure(measureArray) 343 } 344 this.measureResult.set(args) 345 ArkUINativeModule._SetMeasureWidth(this.peer.ptr, this.measureResult[0]) 346 ArkUINativeModule._SetMeasureHeight(this.peer.ptr, this.measureResult[1]) 347 return this._dirtyFlags 348 } 349 350 onLayout(args: Float32Array): int32 { 351 if ((this.flags & NativeNodeFlags.CustomLayout) == 0) { 352 // This node doesn't have managed layout, call native instead. 353 let result = 0 354 withFloat32Array(args, Access.READ | Access.WRITE, (argsPtr: KFloat32ArrayPtr) => { 355 // Call native layout. 356 result = ArkUINativeModule._LayoutNode(this.peer.ptr, argsPtr) 357 }) 358 return result 359 } 360 if ((this.getDirty() & DirtyFlags.Geometry) == 0) return 0 361 // default behavior 362 let layoutArray = new Float32Array(args) 363 for (let child = this.firstChildNode(); child != undefined; child = child.nextSiblingNode()) { 364 let childPeer = child as PeerNode 365 childPeer.onLayout(layoutArray) 366 } 367 this.layoutResult.set(args) 368 let dirty = this._dirtyFlags 369 this.clearDirty(DirtyFlags.Geometry) 370 return dirty 371 } 372 373 onDraw(canvas: PeerCanvas, args: Float32Array): int32 { 374 if ((this.flags & NativeNodeFlags.CustomDraw) == 0) { 375 // This node doesn't have managed layout, call native instead. 376 let result = 0 377 withFloat32Array(args, Access.READ | Access.WRITE, (argsPtr: KFloat32ArrayPtr) => { 378 // Call native draw. 379 result = ArkUINativeModule._DrawNode(this.peer.ptr, argsPtr) 380 }) 381 return result 382 } 383 // default behavior 384 let drawArray = new Float32Array(4) 385 for (let child = this.firstChildNode(); child != undefined; child = child.nextSiblingNode()) { 386 let childPeer = child as PeerNode 387 drawArray.set(args) 388 childPeer.onDraw(canvas, drawArray) 389 } 390 return this._dirtyFlags 391 } 392 393 394 protected _dirtyFlags: int32 = 0 395 markDirty(flags: int32) { 396 this._dirtyFlags |= flags 397 let node: PeerNode = this 398 while (node != undefined) { 399 node._dirtyFlags |= flags 400 node = node.parent as PeerNode 401 } 402 } 403 getDirty(): int32 { 404 return this._dirtyFlags 405 } 406 clearDirty(flags: int32): void { 407 this._dirtyFlags &= ~(flags as number) 408 } 409 firstChildNode(): CustomizableNode|undefined { 410 let child = this.firstChild 411 if (child?.isKind(PeerNodeType)) 412 return child as Object as CustomizableNode 413 else 414 return undefined 415 } 416 previousSiblingNode(): CustomizableNode|undefined { 417 let sibling = this.previousSibling 418 if (sibling?.isKind(PeerNodeType)) 419 return sibling as Object as CustomizableNode 420 else 421 return undefined 422 423 } 424 nextSiblingNode(): CustomizableNode|undefined { 425 let sibling = this.nextSibling 426 if (sibling?.isKind(PeerNodeType)) 427 return sibling as Object as CustomizableNode 428 else 429 return undefined 430 } 431 parentNode(): CustomizableNode|undefined { 432 return this.parent as Object as CustomizableNode 433 } 434 435 get childrenCountOfPeer(): number { 436 // TODO: add some cache for it. 437 let totalCount = 0 438 let inLazyRange = false 439 for (let child = this.firstChild; child; child = child.nextSibling) { 440 if (child.isKind(PeerNodeType)) { 441 if (!inLazyRange) { 442 ++totalCount 443 } 444 continue 445 } 446 const properties = DataNode.extract<Partial<Properties>>(PartialPropertiesType, child) 447 if (properties) { 448 const lazyRangeStart = properties.lazyRangeStart 449 if (lazyRangeStart != undefined) { 450 inLazyRange = true 451 totalCount = totalCount + lazyRangeStart.rangeSize() 452 continue 453 } 454 if (properties.lazyRangeEnd) { 455 inLazyRange = false 456 } 457 } 458 } 459 return totalCount 460 } 461 462 // We only need that for container nodes where LazyForEach can appear. 463 private hasRangeUpdater = false 464 setRangeUpdater(needed: boolean) { 465 if (!needed && this.hasRangeUpdater) { 466 ArkUINativeModule._SetRangeUpdater(this.peer.ptr, 0) 467 this.hasRangeUpdater = false 468 return 469 } 470 if (needed && !this.hasRangeUpdater) { 471 this.hasRangeUpdater = true 472 ArkUINativeModule._SetLazyItemIndexer(this.peer.ptr, wrapCallback((args1: Uint8Array, length: int32) => { 473 const args = new Int32Array(args1.buffer) 474 let requestedIndex = args[0] 475 let logicalIndex = 0 476 let currentRangeStart = 0, currentRangeEnd = 0, currentRangeSize = 0 477 for (let child = this.firstChild; child; child = child.nextSibling) { 478 if (child.isKind(PeerNodeType)) { 479 if (logicalIndex == requestedIndex) { 480 let bits = ptrToBits((child as PeerNode).peer.ptr)! 481 args[1] = bits[0] 482 args[2] = bits[1] 483 return 1 484 } 485 logicalIndex++ 486 continue 487 } 488 const properties = DataNode.extract<Partial<Properties>>(PartialPropertiesType, child) 489 if (properties) { 490 const lazyRangeStart = properties.lazyRangeStart 491 if (lazyRangeStart != undefined) { 492 currentRangeSize = lazyRangeStart.rangeSize() 493 currentRangeStart = lazyRangeStart.currentStart() 494 currentRangeEnd = lazyRangeStart.currentEnd() 495 logicalIndex += currentRangeStart 496 } 497 if (properties.lazyRangeEnd) { 498 logicalIndex += currentRangeSize - (currentRangeEnd - currentRangeStart) 499 currentRangeStart = 0 500 currentRangeEnd = 0 501 currentRangeSize = 0 502 } 503 } 504 } 505 return 0 506 }, false)) 507 /** 508 * Theory of operations. 509 * 510 * Native side can send us range update request for essentially range of items it wants to materialize, 511 * we call it `nativeStartIndex` and `nativeEndIndex`. 512 * When there's LazyForEach we insert two markers: `start` and `end` into the tree. 513 * `start` contains a reference to data source, so it knows the presumable span generatable by this lazy list. 514 * We walk through the children of container node, and compute the actual native index for every node. 515 * We compute intersection between lazy range span and requested range, and update LazyForEach range accordingly. 516 * It adds or remove elements as needed. 517 */ 518 ArkUINativeModule._SetRangeUpdater(this.peer.ptr, CallbackPeerEvent.wrap((args: Int32Array) => { 519 let nativeStartIndex = args[0] 520 let nativeEndIndex = args[1] 521 let inLazyRange = false 522 let currentNativeIndex = 0 523 // TODO: we may want some caching here. 524 for (let child = this.firstChild; child; child = child.nextSibling) { 525 if (child.isKind(PeerNodeType)) { 526 if (!inLazyRange) currentNativeIndex++ 527 } 528 const properties = DataNode.extract<Partial<Properties>>(PartialPropertiesType, child) 529 if (properties) { 530 const lazyRangeStart = properties.lazyRangeStart 531 if (lazyRangeStart != undefined) { 532 inLazyRange = true 533 let rangeSize = lazyRangeStart.rangeSize() 534 // We need to intersect `[currentNativeIndex, currentNativeIndex + rangeSize]` with `[nativeStartIndex, nativeEndIndex]` 535 // produce `[intersectionStart, intersectionEnd]` and use `intersectionStart - currentNativeIndex` as base of new range. 536 let intersection = intersect( 537 [currentNativeIndex, currentNativeIndex + rangeSize], 538 [nativeStartIndex, nativeEndIndex]) 539 if (intersection != undefined) { 540 lazyRangeStart.onRangeUpdate(intersection[0] - currentNativeIndex, intersection[1] - currentNativeIndex) 541 } 542 currentNativeIndex += rangeSize 543 } 544 if (properties.lazyRangeEnd) { 545 inLazyRange = false 546 } 547 } 548 // Nothing else to update. 549 if (currentNativeIndex > nativeEndIndex) break 550 } 551 }, false)) 552 } 553 } 554 555 invalidateProperties() { 556 // This is probably a bit heavyweight - we iterate over all partial properties and if there 557 // is lazy iterators - install range updater. 558 let needed = false 559 for (let child = this.firstChild; child; child = child.nextSibling) { 560 const properties = DataNode.extract<Partial<Properties>>(PartialPropertiesType, child) 561 if (properties?.lazyRangeStart != undefined) { 562 needed = true 563 break 564 } 565 } 566 // can be replaced with the following code when lambda processing will be fast enough: 567 // const needed = DataNode.findFirst<Partial<Properties>>(PartialPropertiesType, this.children, properties => properties.lazyRangeStart != undefined) != undefined 568 this.setRangeUpdater(needed) 569 } 570 571 deliverEvent(event: PeerEvent) { 572 for (let child = this.firstChild; child; child = child.nextSibling) { 573 const properties = DataNode.extract<Partial<Properties>>(PartialPropertiesType, child) 574 if (properties) currentPostman(this, event, properties) 575 } 576 // can be replaced with the following code when lambda processing will be fast enough: 577 // DataNode.forEach<Partial<Properties>>(PartialPropertiesType, this.children, properties => currentPostman(this, event, properties)) 578 } 579 580 get name(): string { 581 return this.peer.getAttribute("name") 582 } 583 584 set name(value: string) { 585 this.peer.setAttribute("name", value) 586 } 587 588 dispose(): void { 589 let parent = parentWithPeer(this) 590 parent?.peer?.removeChild(this.peer) 591 this.peer.dispose() 592 PeerNode.peerNodeMap.delete(this.id) 593 super.dispose() 594 } 595 596 toString(): string { 597 return `${this.constructor.name}: peer=[${this.peer}], size=[${this.getMeasureWidth(this.peer.ptr)}x${this.getMeasureHeight(this.peer.ptr)}], id=${this.getId()}` 598 } 599 600 // access to NativeModule 601 protected measureNode(node: KPointer, data: KFloat32ArrayPtr): KInt { 602 return ArkUINativeModule._MeasureNode(node, data) 603 } 604 protected layoutNode(node: KPointer, data: KFloat32ArrayPtr): KInt { 605 return ArkUINativeModule._LayoutNode(node, data) 606 } 607 protected drawNode(node: KPointer, data: KFloat32ArrayPtr): KInt { 608 return ArkUINativeModule._DrawNode(node, data) 609 } 610 protected setMeasureWidth(node: KPointer, value: int32) { 611 ArkUINativeModule._SetMeasureWidth(node, value) 612 } 613 protected getMeasureWidth(node: KPointer): KFloat { 614 return ArkUINativeModule._GetMeasureWidth(node) 615 } 616 protected setMeasureHeight(node: KPointer, value: KFloat) { 617 ArkUINativeModule._SetMeasureHeight(node, value) 618 } 619 protected getMeasureHeight(node: KPointer): KFloat { 620 return ArkUINativeModule._GetMeasureHeight(node) 621 } 622 protected setX(node: KPointer, value: int32) { 623 ArkUINativeModule._SetX(node, value) 624 } 625 protected setY(node: KPointer, value: int32) { 626 ArkUINativeModule._SetY(node, value) 627 } 628 protected getAlignment(node: KPointer): KInt { 629 return ArkUINativeModule._GetAlignment(node) 630 } 631 632 applyAttributes(attributes: object): void {} 633}