diff --git a/.gitee/CODEOWNERS b/.gitee/CODEOWNERS index 368d8b09b2daaeec55b10f7376e3a004a6b5fa53..6986f3130043e41fb5e7b3e2ea86da515aa397b6 100644 --- a/.gitee/CODEOWNERS +++ b/.gitee/CODEOWNERS @@ -3139,13 +3139,13 @@ interfaces/napi/kits/text_menu_controller/ @huawei_g_five interfaces/napi/kits/utils/ @arkuiframework [Arkoala] -frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/handwritten/component/forEach.ts @arkuieco3 -frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/handwritten/component/lazyForEach.ts @arkuieco3 -frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/handwritten/component/repeat.ts @arkuieco3 -frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/handwritten/LazyForEachImpl.ts @arkuieco3 -frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/handwritten/LazyItemNode.ts @arkuieco3 -frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/handwritten/RepeatImpl.ts @arkuieco3 -frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/DataChangeListener.ts @arkuieco3 +frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/component/forEach.ts @arkuieco3 +frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/component/lazyForEach.ts @arkuieco3 +frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/component/repeat.ts @arkuieco3 +frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/base/LazyForEachImpl.ts @arkuieco3 +frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/base/LazyItemNode.ts @arkuieco3 +frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/base/RepeatImpl.ts @arkuieco3 +frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/base/DataChangeListener.ts @arkuieco3 frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/ReusablePool.ts @arkuieco3 frameworks/bridge/arkts_frontend/koala_projects/incremental/ @arkuistatemgmt frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/handwritten/component/web.ts @arkwebinarkuireview diff --git a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/ForEach.ets b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/ForEach.ets deleted file mode 100644 index 7cfe7eaaab0f74284954f20db104670aeea1acbf..0000000000000000000000000000000000000000 --- a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/ForEach.ets +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (c) 2022-2025 Huawei Device Co., Ltd. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { hashCodeFromString } from "@koalaui/common" -import { RepeatByArray } from "@koalaui/runtime" -import { memo } from "@koalaui/runtime/annotations" - -@memo -export function ForEach(data: Array, - @memo - itemGenerator: (item: T, index?: int) => void, - keyGenerator?: (item: T, index?: int) => string, -) { - RepeatByArray(data, - (element: T, index: int): int => keyGenerator ? hashCodeFromString(keyGenerator!(element, index)) : index, - (element: T, index: int): void => { itemGenerator(element, (index)) }) -} \ No newline at end of file diff --git a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/LazyForEach.ets b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/LazyForEach.ets deleted file mode 100644 index 3e2fbdc63a3408757c0f450adccb50628bc21983..0000000000000000000000000000000000000000 --- a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/LazyForEach.ets +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Copyright (c) 2025 Huawei Device Co., Ltd. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { memo, memo_intrinsic, memo_entry, memo_stable, memo_skip } from "@koalaui/runtime/annotations" -import { __context, __id, contextNode, DataNode, memoEntry2, remember, rememberMutableState, scheduleCallback } from "@koalaui/runtime" -import { hashCodeFromString, int32, KoalaCallsiteKey } from "@koalaui/common" -import { nullptr, pointer } from "@koalaui/interop"; -import { LazyForEachType, PeerNode, PeerNodeType } from "./PeerNode"; -import { LazyForEachOps } from "#generated" -import { DataChangeListener, InternalListener } from "./DataChangeListener"; - -export { DataChangeListener } from "./DataChangeListener"; - -/** - * Developers need to implement this interface to provide data to LazyForEach component. - * @since 7 - */ - export interface IDataSource { - /** - * Total data count. - * @since 7 - */ - totalCount(): number; - /** - * Return the data of index. - * @since 7 - */ - getData(index: number): T; - /** - * Register data change listener. - * @since 7 - */ - registerDataChangeListener(listener: DataChangeListener): void; - /** - * Unregister data change listener. - * @since 7 - */ - unregisterDataChangeListener(listener: DataChangeListener): void; -} - -class LazyForEachManager { - static isDummy: boolean = false - // Special pointer to mark that more elements needed. - static specialPointer: pointer = 1 - static OnRangeUpdate(parent: pointer, totalCount: int32, updater: (currentIndex: int32, currentMark: pointer, end: int32) => void) { - if (LazyForEachManager.isDummy) - scheduleCallback((): void => updater(0, LazyForEachManager.specialPointer, 100)) - // else - // LazyForEachOps.OnRangeUpdate(parent, totalCount, updater) - } - - static NeedMoreElements(parent: pointer, mark: pointer, direction: int32): pointer { - // if (LazyForEachManager.isDummy) - return LazyForEachManager.specialPointer - // else - // return LazyForEachOps.NeedMoreElements(parent, mark, direction) - } - - static SetCurrentIndex(node: pointer, index: int32): void { - // if (!LazyForEachManager.isDummy) - // LazyForEachOps.SetCurrentIndex(node, index) - } - - static SetInsertMark(parent: PeerNode, mark: pointer, moreUp: boolean): void { - if (!LazyForEachManager.isDummy) - parent.setInsertMark(mark, moreUp) - } - - /** - * @param parent - * @param itemCount - * @param offset of LazyForEach in parent's children - */ - static Prepare(parent: PeerNode, itemCount: int32, offset: int32): void { - // if (!LazyForEachManager.isDummy) - // LazyForEachOps.Prepare(parent.peer.ptr, itemCount, offset) - } -} - -class VisibleRange { - parent: pointer = nullptr - markUp: pointer = nullptr - markDown: pointer = nullptr - indexUp: int32 - indexDown: int32 - - constructor(parent: PeerNode, indexUp: int32, indexDown: int32) { - this.parent = parent.peer.ptr - this.indexUp = indexUp - this.indexDown = indexDown - } - needFillUp(): boolean { - let more = LazyForEachManager.NeedMoreElements(this.parent, this.markUp, 0) - if (more == nullptr) return false - this.markUp = more - return true - } - - needFillDown(): boolean { - let more = LazyForEachManager.NeedMoreElements(this.parent, this.markDown, 1) - if (more == nullptr) return false - this.markDown = more - return true - } - - get activeCount(): int32 { - return this.indexDown >= 0 ? this.indexDown - this.indexUp + 1 : 0 - } -} - -class LazyForEachIdentifier { - constructor(id: KoalaCallsiteKey, totalCnt: int32, activeCnt: int32) { - this.id = id - this.totalCnt = totalCnt - this.activeCnt = activeCnt - } - readonly id: KoalaCallsiteKey - readonly totalCnt: int32 - readonly activeCnt: int32 -} - -/** - * @param id unique identifier of LazyForEach - * @returns item offset of LazyForEach in parent's children - */ -@memo -function getOffset(parent: PeerNode, id: KoalaCallsiteKey): int32 { - let offset = 0 - for (let child = parent.firstChild; child; child = child!.nextSibling) { - // corresponding DataNode is attached after the generated items - let info = DataNode.extract(LazyForEachType, child!!) - if (info?.id === id) { - offset -= info!.activeCnt - // console.log(`offset = ${offset}`) - return offset - } else if (info) { - offset += info!.totalCnt - info!.activeCnt // active nodes are already counted - } else if (child!.isKind(PeerNodeType)) { - ++offset - } - } - return offset // DataNode not found, maybe throw error? -} - -@memo -export function LazyForEach(dataSource: IDataSource, - @memo - itemGenerator: (item: T, index: number) => void, - keyGenerator?: (item: T, index: number) => string, -) { - let current = rememberMutableState(-1) - let mark = rememberMutableState(nullptr) - let version = rememberMutableState(0) - // console.log(`LazyForEach current=${current.value} version=${version.value} mark=${mark.value}`) - - let parent = contextNode() - const offset = getOffset(parent, __id()) - - let listener = remember((): InternalListener => new InternalListener(parent.peer.ptr, version)) - const changeIndex = listener.flush(offset) // first item index that's affected by DataChange - - const currentLocal = current.value >= 0 ? Math.max(current.value - offset, 0) as int32 : -1; // translated to local index - const visibleRange = new VisibleRange(parent, currentLocal, currentLocal) - remember((): void => { - dataSource.registerDataChangeListener(listener) - LazyForEachManager.OnRangeUpdate(visibleRange.parent, dataSource.totalCount() as int32, (currentIndex: int32, currentMark: pointer, end: int32) => { - // console.log(`LazyForEach[${parent}]: current updated to ${currentIndex} ${currentMark} end=${end}`) - current.value = currentIndex - mark.value = currentMark - version.value++ - }) - }) - // Subscribe to version changes. - version.value - - let generator = (element: T, index: number): int32 => keyGenerator ? hashCodeFromString(keyGenerator!(element, index)) : index as int32 - let index: number = visibleRange.indexUp as number - - LazyForEachManager.Prepare(parent, dataSource.totalCount() as int32, offset) - LazyForEachManager.SetInsertMark(parent, mark.value, false) - - while (index >= 0 && index < dataSource.totalCount()) { - // console.log(`LazyForEach[${parent}]: index=${index}`) - const element: T = dataSource.getData(index as number) - memoEntry2( - __context(), - generator(element, index), - itemGenerator, - element, - index - ) - let moreUp = visibleRange.needFillUp() - if (moreUp && visibleRange.indexUp > 0) { - index = --visibleRange.indexUp - } else if (visibleRange.needFillDown()) { - index = ++visibleRange.indexDown - } else { - // console.log("No more needed") - index = -1 - } - LazyForEachManager.SetInsertMark(parent, moreUp ? visibleRange.markUp : visibleRange.markDown, moreUp) - } - parent.setInsertMark(nullptr, false) - - // create DataNode to provide count information to parent - const identifier = new LazyForEachIdentifier(__id(), dataSource.totalCount() as int32, visibleRange.activeCount) - DataNode.attach(LazyForEachType, identifier) -} diff --git a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/PeerNode.ets b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/PeerNode.ets index 2ad1c600b38842a566cfd1774aecab40d5d9ca88..4e816e6ac507ab852319172c42b03b718f87df94 100644 --- a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/PeerNode.ets +++ b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/PeerNode.ets @@ -23,7 +23,7 @@ import { StateUpdateLoop } from "./stateManagement" export const PeerNodeType = 11 export const RootPeerType = 33 -export const LazyForEachType = 13 +export const LazyItemNodeType = 17 // LazyItems are detached node trees that are stored privately in LazyForEach export const BuilderRootNodeType = 19 // BuilderRootNode are detached node trees that are stored privately in BuilderNode const INITIAL_ID = 1000 @@ -77,34 +77,59 @@ export class PeerNode extends IncrementalNode { } /* reuse and recycle object on RootPeers */ - override reuse(reuseKey: string, id: KoalaCallsiteKey): Disposable | undefined { - if (!this.isRootNode()) { - return this.parent?.reuse(reuseKey, id) + override reuse(reuseKey: string | undefined, id: KoalaCallsiteKey): Disposable | undefined { + if (reuseKey === undefined) { + return undefined; } + if (!this.isKind(RootPeerType)) + return this.parent?.reuse(reuseKey, id) + if (this._reusePool === undefined) return undefined if (this._reusePool!.has(reuseKey)) { const pool = this._reusePool!.get(reuseKey)!; - return pool.get(id); + return pool.get(); } return undefined; } - override recycle(reuseKey: string, child: Disposable, id: KoalaCallsiteKey): boolean { - if (!this.isRootNode()) { - return this.parent?.recycle(reuseKey, child, id) ?? false + override recycle(reuseKey: string | undefined, child: Disposable, id: KoalaCallsiteKey): boolean { + if (reuseKey === undefined) { + return false; } + if (!this.isKind(RootPeerType)) { + return this.parent?.recycle(reuseKey, child, id) ?? false + } if (!this._reusePool) this._reusePool = new Map() if (!this._reusePool!.has(reuseKey)) { this._reusePool!.set(reuseKey, new ReusablePool()); } - this._reusePool!.get(reuseKey)!.put(id, child); + this._reusePool!.get(reuseKey)!.put(child); return true } + setReusePoolSize(size: number, reuseKey: string): void { + if (size < 0) { + return + } + if (!this.isKind(RootPeerType)) { + if (this.parent?.isKind(PeerNodeType)) { + (this.parent! as PeerNode).setReusePoolSize(size, reuseKey) + } + return + } + if (!this._reusePool) { + this._reusePool = new Map() + } + if (!this._reusePool?.has(reuseKey)) { + this._reusePool?.set(reuseKey, new ReusablePool()) + } + this._reusePool?.get(reuseKey)?.setMaxSize(size) + } + setOnRecycle(cb: () => void): void { this._onRecycle = cb } diff --git a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/ReusablePool.ets b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/ReusablePool.ets index 850f74b10664ec4d80e9ef0f44fecd20836f8775..cc45abe3e4adf8456f0cb46f3ae1e1247b639bce 100644 --- a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/ReusablePool.ets +++ b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/ReusablePool.ets @@ -13,48 +13,50 @@ * limitations under the License. */ -import { Disposable } from "@koalaui/runtime"; -import { KoalaCallsiteKey } from "@koalaui/common" +import { Disposable, scheduleCallback } from "@koalaui/runtime"; export class ReusablePool implements Disposable { - private cache: Map; + private _maxSize: number = Number.POSITIVE_INFINITY + private _cache: Disposable[] = [] + private _disposed: boolean = false; - constructor() { - this.cache = new Map(); + get disposed(): boolean { + return this._disposed } - disposed: boolean = false /** - * prioritize reusing the same scope. If not found, use the earliest inserted scope + * Returns and removes the first available item from the pool */ - get(key: KoalaCallsiteKey): Disposable | undefined { + get(): Disposable | undefined { if (this.disposed) return undefined; - if (!this.cache.has(key)) { - const leastUsedKey = this.cache.keys().next().value; - if (!leastUsedKey) return undefined - const leastUsedValue = this.cache.get(leastUsedKey!); - this.cache.delete(leastUsedKey!); - return leastUsedValue; - } - const value = this.cache.get(key)!; - this.cache.delete(key); - return value; + return this._cache.shift(); } - put(key: KoalaCallsiteKey, value: Disposable): void { - if (this.disposed) return - if (this.cache.has(key)) { - throw Error("the same scope is recycled twice") - } - this.cache.set(key, value); + /** + * Adds an item to the pool + */ + put(value: Disposable): void { + if (this.disposed || this._cache.length >= this._maxSize) return; + this._cache.push(value); } dispose(): void { - if (this.disposed) return - this.disposed = true - for (const value of this.cache.values()) { + if (this.disposed) return; + this._disposed = true; + for (const value of this._cache) { value.dispose(); } - this.cache.clear(); + this._cache = []; + } + + setMaxSize(value: number) { + this._maxSize = value + if (this._cache.length > this._maxSize) { + const removed = this._cache.splice(this._maxSize); + scheduleCallback(() => { + for (const value of removed) + value.dispose(); + }) + } } } \ No newline at end of file diff --git a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/DataChangeListener.ets b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/base/DataChangeListener.ets similarity index 65% rename from frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/DataChangeListener.ets rename to frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/base/DataChangeListener.ets index 0b39929784630d76705123d7bd61a2f239d75d3e..fc784d1b8963975e3bbf38e4a266930ebf3a4fd8 100644 --- a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/DataChangeListener.ets +++ b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/base/DataChangeListener.ets @@ -13,34 +13,19 @@ * limitations under the License. */ -import { ArkUIAniModule } from "arkui.ani"; -import { pointer } from "@koalaui/interop"; -import { DataOperation, DataOperationType, DataAddOperation, DataDeleteOperation, DataChangeOperation, DataMoveOperation, DataExchangeOperation, LazyForEachOps } from "#generated"; -import { int32 } from "@koalaui/common" -import { MutableState } from "@koalaui/runtime"; - -export interface DataChangeListener { - onDataReloaded(): void; - onDataAdded(index: number): void; - onDataAdd(index: number): void; - onDataMoved(from: number, to: number): void; - onDataMove(from: number, to: number): void; - onDataDeleted(index: number): void; - onDataDelete(index: number): void; - onDataChanged(index: number): void; - onDataChange(index: number): void; - onDatasetChange(dataOperations: DataOperation[]): void; -} +import { pointer } from '@koalaui/interop' +import { int32 } from '@koalaui/common' +import { MutableState } from '@koalaui/runtime' +import { DataOperation, DataOperationType, DataAddOperation, DataDeleteOperation, DataChangeOperation, + DataMoveOperation, DataExchangeOperation, DataChangeListener } from '../component/lazyForEach' export class InternalListener implements DataChangeListener { - parent: pointer - startIndex: number // Tracks the minimum item index that has changed - endIndex: number - changeCount: number // Tracks the number of items added or deleted - version: MutableState // reference to mark LazyForEach dirty + private startIndex: number // Tracks the minimum item index that has changed + private endIndex: number + private changeCount: number // Tracks the number of items added or deleted + private version: MutableState // reference to mark LazyForEach dirty - constructor(parent: pointer, version: MutableState) { - this.parent = parent + constructor(version: MutableState) { this.startIndex = Number.POSITIVE_INFINITY this.endIndex = Number.NEGATIVE_INFINITY this.changeCount = 0 @@ -50,16 +35,10 @@ export class InternalListener implements DataChangeListener { * Notify the change of data to backend * @return the index of the first changed item */ - flush(offset: int32): number { + flush(nodePtr: pointer): number { if (this.startIndex === Number.POSITIVE_INFINITY) { return Number.POSITIVE_INFINITY // none affected } - // LazyForEachOps.NotifyChange( - // this.parent, - // this.startIndex as int32 + offset, - // this.endIndex as int32 + offset, - // this.changeCount as int32 - // ); const firstAffected = this.startIndex // Reset the cache after flushing this.startIndex = Number.POSITIVE_INFINITY; @@ -77,7 +56,9 @@ export class InternalListener implements DataChangeListener { } onDataAdd(index: number): void { - if (index < 0) return + if (index < 0) { + return + } if (this.startIndex === Number.POSITIVE_INFINITY) { ++this.version.value } @@ -86,7 +67,9 @@ export class InternalListener implements DataChangeListener { } onDataMove(from: number, to: number): void { - if (from < 0 || to < 0) return + if (from < 0 || to < 0) { + return + } if (this.startIndex === Number.POSITIVE_INFINITY) { ++this.version.value } @@ -95,7 +78,9 @@ export class InternalListener implements DataChangeListener { } onDataDelete(index: number): void { - if (index < 0) return + if (index < 0) { + return + } if (this.startIndex === Number.POSITIVE_INFINITY) { ++this.version.value } @@ -104,10 +89,11 @@ export class InternalListener implements DataChangeListener { } onDataChange(index: number): void { - if (index < 0) return + if (index < 0) { + return + } if (this.startIndex === Number.POSITIVE_INFINITY) { ++this.version.value - ArkUIAniModule._CustomNode_RequestFrame(); } this.startIndex = Math.min(this.startIndex, index) } @@ -117,34 +103,34 @@ export class InternalListener implements DataChangeListener { let endIndex = Number.NEGATIVE_INFINITY let changeCount = 0 for (const operation of dataOperations) { - switch (operation.type.valueOf()) { - case DataOperationType.ADD.valueOf(): { + switch (operation.type) { + case DataOperationType.ADD: { startIndex = Math.min(startIndex, (operation as DataAddOperation).index); ++changeCount break; } - case DataOperationType.DELETE.valueOf(): { + case DataOperationType.DELETE: { startIndex = Math.min(startIndex, (operation as DataDeleteOperation).index); --changeCount break; } - case DataOperationType.CHANGE.valueOf(): { + case DataOperationType.CHANGE: { startIndex = Math.min(startIndex, (operation as DataChangeOperation).index); break; } - case DataOperationType.MOVE.valueOf(): { + case DataOperationType.MOVE: { const moveOp = operation as DataMoveOperation; startIndex = Math.min(startIndex, Math.min(moveOp.index.from, moveOp.index.to)); endIndex = Math.max(endIndex, Math.max(moveOp.index.from, moveOp.index.to)); break; } - case DataOperationType.EXCHANGE.valueOf(): { + case DataOperationType.EXCHANGE: { const exchangeOp = operation as DataExchangeOperation; startIndex = Math.min(startIndex, Math.min(exchangeOp.index.start, exchangeOp.index.end)); endIndex = Math.max(endIndex, Math.max(exchangeOp.index.start, exchangeOp.index.end)); break; } - case DataOperationType.RELOAD.valueOf(): { + case DataOperationType.RELOAD: { startIndex = 0; endIndex = Number.POSITIVE_INFINITY break; @@ -159,17 +145,13 @@ export class InternalListener implements DataChangeListener { this.changeCount = changeCount } - /* deprecated */ - onDataAdded(index: number): void { - this.onDataAdd(index) - } - onDataMoved(from: number, to: number): void { - this.onDataMove(from, to) - } - onDataDeleted(index: number): void { - this.onDataDelete(index) - } - onDataChanged(index: number): void { - this.onDataChange(index) + /** + * @internal + * Notify data change without updating MutableState. Safe to call during composition. + */ + update(start: number, end: number, countDiff: number): void { + this.startIndex = Math.min(start, this.startIndex) + this.endIndex = Math.max(end, this.endIndex) + this.changeCount += countDiff } } diff --git a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/base/LazyForEachImpl.ets b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/base/LazyForEachImpl.ets new file mode 100644 index 0000000000000000000000000000000000000000..dabe4884866ece1dcc07e373b12929e24612159e --- /dev/null +++ b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/base/LazyForEachImpl.ets @@ -0,0 +1,313 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { __id, NodeAttach, ComputableState, GlobalStateManager, Disposable, memoEntry2, remember, + rememberDisposable, rememberMutableState, StateContext, scheduleCallback } from '@koalaui/runtime' +import { InteropNativeModule, nullptr, pointer } from '@koalaui/interop' +import { int32 } from '@koalaui/common' +import { PeerNode } from '../PeerNode' +import { InternalListener } from './DataChangeListener' +import { setNeedCreate } from '../ArkComponentRoot' +import { IDataSource } from '../component/lazyForEach' +import { OnMoveHandler, ItemDragEventHandler } from '#generated' +import { LazyItemNode } from './LazyItemNode' +import { ArkUIAniModule } from '../ani/arkts/ArkUIAniModule' +import { CustomComponent } from '../component/customComponent' + +let globalLazyItems = new Map, int32>() // V: age + +export function updateLazyItems() { + let postponed = false + globalLazyItems.forEach((age, node, map) => { + if (age === 0) { + postponed = true + map.set(node, 1) + } else { + node.value + } + }) + if (postponed) { + // requestFrame() + } +} + +/** @memo:intrinsic */ +export function LazyForEachImplForOptions(dataSource: IDataSource, + /** @memo */ + itemGenerator: (item: T, index: number) => void, + keyGenerator?: (item: T, index: number) => string, + isRepeat: boolean = false, + onMove?: OnMoveHandler, + itemDragEvent?: ItemDragEventHandler, +) { + const node = createLazyNode(isRepeat) + + let pool = rememberDisposable(() => new LazyItemPool(node, CustomComponent.current), (pool?: LazyItemPool) => { + pool?.dispose() + }) + let changeCounter = rememberMutableState(0) + changeCounter.value //subscribe + let listener = remember(() => { + let res = new InternalListener(changeCounter) + dataSource.registerDataChangeListener(res) + return res + }) + const changeIndex = listener.flush(node.getPeerPtr()) // first item index that's affected by DataChange + if (changeIndex < Number.POSITIVE_INFINITY) { + scheduleCallback(() => { + pool.pruneBy((index: int32) => index >= changeIndex) + }) + } + + /** + * provide totalCount and callbacks to the backend + */ + let createCallback = (index: int32) => { + try { + return pool.getOrCreate(index, dataSource.getData(index), itemGenerator) + } catch (error) { + InteropNativeModule._NativeLog(`error during createLazyItem: ${error}`) + return nullptr + } + } +} + +export class NodeHolder { + node?: PeerNode +} + +/** @memo:intrinsic */ +function createLazyNode(isRepeat: boolean): PeerNode { + /** + * We don't want cache behavior with LazyForEach (to support Repeat's non-memo data updates), + * therefore LazyForEach implementation is outside NodeAttach, + * and LazyForEachNode is provided through a remembered NodeHolder. + */ + let nodeHolder = remember(() => new NodeHolder()) + NodeAttach( + () => { + const peerId = PeerNode.nextId() + const _peerPtr = ArkUIAniModule._LazyForEachNode_Construct(peerId) + if (!_peerPtr) { + throw new Error(`Failed to create LazyNodePeer with id: ${peerId}`) + } + const _peer = new PeerNode(_peerPtr, peerId, isRepeat ? 'Repeat' : 'LazyForEach', 0) + return _peer + }, + (node: PeerNode) => { + nodeHolder.node = node + } + ) + return nodeHolder.node! +} + +class LazyItemCompositionContext { + private prevFrozen: boolean + private prevNeedCreate: boolean + private prevCurrent?: Object + + constructor(parentComponent?: Object) { + const manager = GlobalStateManager.instance + this.prevFrozen = manager.frozen + manager.frozen = true + this.prevNeedCreate = setNeedCreate(true) // ensure synchronous creation of all inner CustomComponent + this.prevCurrent = CustomComponent.current + CustomComponent.current = parentComponent // setup CustomComponent context to link with @Provide variables + } + + exit(): void { + CustomComponent.current = this.prevCurrent + setNeedCreate(this.prevNeedCreate) + GlobalStateManager.instance.frozen = this.prevFrozen + } +} + +class LazyItemPool implements Disposable { + private _activeItems = new Map>() + private _parent: PeerNode + private _componentRoot?: Object + private _moveFromTo?: [int32, int32] = undefined // used to track moveFromTo in onMove event + disposed: boolean = false + + /** + * + * @param parent direct parent node (should be the scroll container node) + * @param root root object of the current CustomComponent + */ + constructor(parent: PeerNode, root?: Object) { + this._parent = parent + this._componentRoot = root + } + + dispose(): void { + if (this.disposed) { + return + } + + this.pruneBy(() => true) + this.disposed = true + } + + get activeCount(): int32 { + return this._activeItems.size as int32 + } + + getOrCreate( + index: int32, + data: T, + /** @memo */ + itemGenerator: (item: T, index: number) => void, + ): pointer { + if (this._activeItems.has(index)) { + const node = this._activeItems.get(index)! + return node.value.getPeerPtr() + } + + const manager = GlobalStateManager.instance + const node = manager.updatableNode(new LazyItemNode(this._parent), + (context: StateContext) => { + let scope = new LazyItemCompositionContext(this._componentRoot) + memoEntry2( + context, + 0, + itemGenerator, + data, + index + ) + scope.exit() + } + ) + + this._activeItems.set(index, node) + globalLazyItems.set(node, 0) + return node.value.getPeerPtr() + } + + /** + * + * @param criteria predicate to determine if the item needs to be removed + */ + pruneBy(criteria: (index: int32) => boolean) { + this._activeItems.forEach((node, index) => { + if (criteria(index)) { + node.dispose() + this._activeItems.delete(index) + globalLazyItems.delete(node) + } + }) + } + + /** + * prune items outside the range [start, end] + * @param start + * @param end + */ + updateActiveRange(start: int32, end: int32) { + if (start > end) { + return + } + try { + this.pruneBy(index => { + index = this.convertFromToIndexRevert(index) + return index < start || index > end + }) + } catch (error) { + InteropNativeModule._NativeLog(`error during LazyItem pruning: ${error}`) + } + } + + /** + * set onMoveFromTo value used for functions convertFromToIndex and convertFromToIndexRevert + * @param moveFrom + * @param moveTo + */ + onMoveFromTo(moveFrom: int32, moveTo: int32): void { + if (moveFrom < 0 || moveTo < 0) { + this.updateActiveItemsForOnMove(); + this._moveFromTo = undefined; + InteropNativeModule._NativeLog(`onMoveFromTo param invalid, reset moveFromTo`); + return; + } + if (this._moveFromTo != undefined) { + this._moveFromTo![1] = moveTo; + if (this._moveFromTo![1] === this._moveFromTo![0]) { + this._moveFromTo = undefined; + } + } else { + this._moveFromTo = [moveFrom, moveTo]; + } + if (this._moveFromTo != undefined) { + InteropNativeModule._NativeLog(`onMoveFromTo updated (${this._moveFromTo![0]}, ${this._moveFromTo![1]})`); + } else { + InteropNativeModule._NativeLog(`onMoveFromTo data moved to original pos, reset moveFromTo.`); + } + } + + // before onMove called, update _activeItems according to the _moveFromTo + private updateActiveItemsForOnMove(): void { + if (this._moveFromTo == undefined) { + return; + } + const fromIndex = Math.min(this._moveFromTo![0], this._moveFromTo![1]); + const toIndex = Math.max(this._moveFromTo![0], this._moveFromTo![1]); + if (fromIndex === toIndex) { + return; + } + let tempActiveItems = new Map>(); + this._activeItems.forEach((node, index) => { + if (index < fromIndex || index > toIndex) { + tempActiveItems.set(index, node); + } else { + const newIndex = this.convertFromToIndexRevert(index); + tempActiveItems.set(newIndex, node); + } + }) + this._activeItems = tempActiveItems; + } + + // currently unused, but may be useful in the future. + private convertFromToIndex(index: int32): int32 { + if (this._moveFromTo == undefined) { + return index; + } + if (this._moveFromTo![1] === index) { + return this._moveFromTo![0]; + } + if (this._moveFromTo![0] <= index && index < this._moveFromTo![1]) { + return index + 1; + } + if (this._moveFromTo![1] < index && index <= this._moveFromTo![0]) { + return index - 1; + } + return index; + } + + // used for updateActiveRange. + private convertFromToIndexRevert(index: int32): int32 { + if (this._moveFromTo == undefined) { + return index; + } + if (this._moveFromTo![0] === index) { + return this._moveFromTo![1]; + } + if (this._moveFromTo![0] < index && index <= this._moveFromTo![1]) { + return index - 1; + } + if (this._moveFromTo![1] <= index && index < this._moveFromTo![0]) { + return index + 1; + } + return index; + } +} diff --git a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/base/LazyItemNode.ets b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/base/LazyItemNode.ets new file mode 100644 index 0000000000000000000000000000000000000000..5b227d71126aea836b484708f056b722d49361eb --- /dev/null +++ b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/base/LazyItemNode.ets @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { IncrementalNode, Disposable } from '@koalaui/runtime' +import { KoalaCallsiteKey } from '@koalaui/common' +import { nullptr, pointer } from '@koalaui/interop' +import { PeerNode, LazyItemNodeType, PeerNodeType } from '../PeerNode' + +/** + * LazyItemNode is the root node of an item in LazyForEach. + * LazyForEach items are never attached to the main tree, but stored in a separate pool in LazyForEach. + */ +export class LazyItemNode extends IncrementalNode { + constructor(parent: PeerNode) { + super(LazyItemNodeType) + this._container = parent + this.onChildInserted = (node: IncrementalNode) => { + if (!node.isKind(PeerNodeType)) { + return + } + const peer = node as PeerNode + peer.reusable ? peer.onReuse() : peer.reusable = true + } + this.onChildRemoved = (node: IncrementalNode) => { + if (!node.isKind(PeerNodeType)) { + return + } + const peer = node as PeerNode + if (!peer.disposed) { + peer.onRecycle() + } + } + } + private _container: PeerNode + + /** + * Supports Reusable through redirecting requests to the parent node. + */ + reuse(reuseKey: string, id: KoalaCallsiteKey): Disposable | undefined { + return this._container.reuse(reuseKey, id) + } + + recycle(reuseKey: string, child: Disposable, id: KoalaCallsiteKey): boolean { + return this._container.recycle(reuseKey, child, id) + } + + getPeerPtr(): pointer { + const peer = this.firstChild + return peer?.isKind(PeerNodeType) ? (peer as PeerNode).getPeerPtr() : nullptr + } +} diff --git a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/base/RepeatImpl.ets b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/base/RepeatImpl.ets new file mode 100644 index 0000000000000000000000000000000000000000..3c1aeee7b517537eac968cee58833f34c9d73e27 --- /dev/null +++ b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/base/RepeatImpl.ets @@ -0,0 +1,329 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +// HANDWRITTEN, DO NOT REGENERATE + +import { int32, hashCodeFromString, KoalaCallsiteKey } from '@koalaui/common' +import { KPointer } from '@koalaui/interop' +import { __context, __id, RepeatByArray, remember, NodeAttach, contextNode, scheduleCallback } from '@koalaui/runtime' +import { RepeatItem, RepeatAttribute, RepeatArray, RepeatItemBuilder, TemplateTypedFunc, VirtualScrollOptions, TemplateOptions } from '../component/repeat' +import { IDataSource, DataChangeListener } from '../component/lazyForEach' +import { OnMoveHandler, ItemDragEventHandler } from '#generated' +import { LazyForEachImplForOptions, NodeHolder } from './LazyForEachImpl' +import { InternalListener } from './DataChangeListener' +import { PeerNode } from '../PeerNode' +import { ArkUIAniModule } from '../ani/arkts/ArkUIAniModule' + +/** @memo:intrinsic */ +export function RepeatImplForOptions( + /** @memo */ + style: ((attributes: RepeatAttribute) => void) | undefined, + arr: RepeatArray +): void { + const repeat = remember(() => { + return new RepeatAttributeImpl(); + }); + style?.(repeat); + if (!repeat.itemGenFuncs_.get(REPEAT_EACH_FUNC_TYPE)) { + throw new Error('Repeat item builder function unspecified. Usage error!'); + } + if (repeat.disableVirtualScroll_) { + nonVirtualRender(arr, repeat.itemGenFuncs_.get(REPEAT_EACH_FUNC_TYPE)!, repeat.keyGenFunc_, + repeat.onMove_, repeat.itemDragEvent_); + } else { + const repeatId = __id(); + const node = contextNode(); + scheduleCallback(() => // postpone until node is attached + repeat.templateCacheSize_.forEach((size: number, template: string) => + node.setReusePoolSize(size, template + repeatId)) + ); + virtualRender(arr, repeat, repeatId); + } +} + +class RepeatItemImpl implements RepeatItem { + item_: T; + index_: number; + + constructor(initialItem: T, initialIndex: number) { + this.item_ = initialItem; + this.index_ = initialIndex; + } + + get item(): T { + return this.item_; + } + + get index(): number { + return this.index_; + } + + public updateItem(newItem: T): void { + this.item_ = newItem; + } + + public updateIndex(newIndex: number): void { + this.index_ = newIndex; + } +} + +class RepeatDataSource implements IDataSource { + private arr_: RepeatArray; + private listener_?: InternalListener; + private total_: number; + private onLazyLoading_?: (index: number) => void; + + constructor(arr: RepeatArray) { + this.arr_ = arr; + } + + totalCount(): number { + return this.total_; + } + + updateData(newArr: RepeatArray, totalCount: number) { + if (this.total_ != totalCount) { + this.listener_?.update( + Math.min(this.total_, totalCount), Number.POSITIVE_INFINITY, totalCount - this.total_); + } + this.total_ = totalCount; + // Compare array references first + if (this.arr_ === newArr) { + return; + } + // Shallow compare: check length and each element by reference + if (this.arr_.length !== newArr.length) { + this.listener_?.update(0, Number.POSITIVE_INFINITY, this.arr_.length - newArr.length); + this.arr_ = newArr; + return; + } + for (let i = 0; i < newArr.length; i++) { + if (this.arr_[i] !== newArr[i]) { + this.listener_?.update(i, Number.POSITIVE_INFINITY, 0); + this.arr_ = newArr; + return; + } + } + // No changes detected + } + + getData(index: number): T { + if (index < 0 || index >= this.total_) { + throw new Error('index out of range. Application error!'); + } + if (index >= this.arr_.length && index < this.total_) { + try { + this.onLazyLoading_?.(index); + } catch (error) { + console.error(`onLazyLoading function execute error: ${error}`); + } + } + return this.arr_[index as int32]; + } + + setOnLazyLoading(onLazyLoading?: (index: number) => void): void { + this.onLazyLoading_ = onLazyLoading; + } + + registerDataChangeListener(listener: DataChangeListener): void { + if (listener instanceof InternalListener) { + this.listener_ = listener as InternalListener; + } else { + throw new Error( + 'Invalid listener registration. Repeat\'s data source object shouldn\'t be exposed to other modules'); + } + } + + unregisterDataChangeListener(listener: DataChangeListener): void { + if (listener !== this.listener_) { + throw new Error('Invalid deregistration'); + } + this.listener_ = undefined; + } +} + +// should be empty string, don't change it +const REPEAT_EACH_FUNC_TYPE: string = ''; + +export class RepeatAttributeImpl implements RepeatAttribute { + arr: RepeatArray = []; + itemGenFuncs_: Map> = new Map>(); + keyGenFunc_?: (item: T, index: number) => string; + templateCacheSize_: Map = new Map(); // size of spare nodes for each template + ttypeGenFunc_: TemplateTypedFunc = () => REPEAT_EACH_FUNC_TYPE; + + userDefinedTotal_?: number; // if totalCount is specified + onLazyLoading_?: (index: number) => void; + onMove_?: OnMoveHandler; + itemDragEvent_?: ItemDragEventHandler; + + reusable_: boolean = true; + disableVirtualScroll_: boolean = false; + setRepeatOptions(arr: RepeatArray): this { + return this; + } + + each(itemGenerator: RepeatItemBuilder): RepeatAttributeImpl { + if (itemGenerator === undefined || typeof itemGenerator !== 'function') { + throw new Error('item generator function missing. Application error!'); + } + this.itemGenFuncs_.set(REPEAT_EACH_FUNC_TYPE, itemGenerator); + this.templateCacheSize_.set(REPEAT_EACH_FUNC_TYPE, Number.POSITIVE_INFINITY); + return this; + } + + key(keyGenerator: (item: T, index: number) => string): RepeatAttributeImpl { + this.keyGenFunc_ = keyGenerator; + return this; + } + + virtualScroll(options?: VirtualScrollOptions): RepeatAttributeImpl { + this.userDefinedTotal_ = options?.onTotalCount?.() ?? options?.totalCount; + this.reusable_ = options?.reusable ?? true; + this.onLazyLoading_ = options?.onLazyLoading; + + this.disableVirtualScroll_ = options?.disableVirtualScroll ?? false; + return this; + } + + template( + type: string, itemBuilder: RepeatItemBuilder, templateOptions?: TemplateOptions): RepeatAttributeImpl { + if (itemBuilder === undefined || typeof itemBuilder !== 'function') { + throw new Error('template generator function missing. Application error!'); + } + this.itemGenFuncs_.set(type, itemBuilder); + this.templateCacheSize_.set(type, templateOptions?.cachedCount ?? Number.POSITIVE_INFINITY); + return this; + } + + templateId(typedFunc: TemplateTypedFunc): RepeatAttributeImpl { + if (typedFunc === undefined || typeof typedFunc !== 'function') { + throw new Error('templateId generator function missing. Application error!'); + } + this.ttypeGenFunc_ = typedFunc; + return this; + } + + onMove(handler: OnMoveHandler | undefined): this { + this.onMove_ = handler; + return this; + } + + onMove(handler: OnMoveHandler | undefined, + eventHandler: ItemDragEventHandler | undefined): this { + this.onMove_ = handler; + this.itemDragEvent_ = eventHandler; + return this; + } +} + +export class SyntaxItemPeer extends PeerNode { + public static create(): SyntaxItemPeer { + const peerId = PeerNode.nextId(); + const _peerPtr = ArkUIAniModule._SyntaxItem_Construct(peerId); + if (!_peerPtr) { + throw new Error(`Failed to create SyntaxItemPeer with id: ${peerId}`); + } + return new SyntaxItemPeer(_peerPtr, peerId, 'SyntaxItem'); + } + + protected constructor(peerPtr: KPointer, id: int32, name: string = '', flags: int32 = 0) { + super(peerPtr, id, name, flags); + } +} + +export class ForEachNodePeer extends PeerNode { + public static create(isRepeat: boolean = false): ForEachNodePeer { + const peerId = PeerNode.nextId(); + const _peerPtr = ArkUIAniModule._ForEachNode_Construct(peerId); + if (!_peerPtr) { + throw new Error(`Failed to create ForEachNodePeer with id: ${peerId}`); + } + return new ForEachNodePeer(_peerPtr, peerId, isRepeat ? 'Repeat' : 'ForEach'); + } + + protected constructor(peerPtr: KPointer, id: int32, name: string = '', flags: int32 = 0) { + super(peerPtr, id, name, flags); + } +} + +/** @memo:intrinsic */ +function virtualRender( + arr: RepeatArray, + attributes: RepeatAttributeImpl, + repeatId: KoalaCallsiteKey, +): void { + let dataSource = remember(() => new RepeatDataSource(arr)); + const total = attributes.userDefinedTotal_ ?? arr.length.toDouble(); + dataSource.updateData(arr, (Number.isInteger(total) && total >= 0) ? total : arr.length); + if (!attributes.onLazyLoading_ && dataSource.totalCount() > arr.length) { + console.error(`(${repeatId}) totalCount must not exceed the array length without onLazyLoading callback.`); + } + dataSource.setOnLazyLoading(attributes.onLazyLoading_); + + /** @memo */ + const itemGen = (item: T, index: number): void => { + const ri = new RepeatItemImpl(item, index); + let _type: string = attributes.ttypeGenFunc_(item, index); + if (!attributes.itemGenFuncs_.has(_type)) { + _type = REPEAT_EACH_FUNC_TYPE; + } + /** @memo */ + const itemBuilder = attributes.itemGenFuncs_.get(_type)!; + /** + * wrap in reusable node. + * To optimize performance, insert reuseKey through compiler plugin to the content of itemBuilder. + */ + if (attributes.reusable_) { + NodeAttach(() => SyntaxItemPeer.create(), (node: SyntaxItemPeer) => { + itemBuilder(ri); + }, _type + repeatId); // using type as reuseKey + } else { + itemBuilder(ri); + } + }; + LazyForEachImplForOptions(dataSource, itemGen, attributes.keyGenFunc_, true, + attributes.onMove_, attributes.itemDragEvent_); +} + +function onMoveFromTo(moveFrom: int32, moveTo: int32): void {} + +/** @memo */ +function nonVirtualRender(arr: RepeatArray, + /** @memo */ + itemGenerator: RepeatItemBuilder, + keyGenerator?: (element: T, index: number) => string, + onMove?: OnMoveHandler, + itemDragEvent?: ItemDragEventHandler, +): void { + if (keyGenerator && typeof keyGenerator !== 'function') { + throw new Error('key generator is not a function. Application error!'); + } + let nodeHolder = remember(() => new NodeHolder()) + const keyGen = (ele: T, i: int32): KoalaCallsiteKey => + keyGenerator ? hashCodeFromString(keyGenerator!(ele, (i as number))) : i; + /** @memo */ + const action = (ele: T, i: int32) => { + const ri = new RepeatItemImpl(ele, (i as number)); + NodeAttach(() => SyntaxItemPeer.create(), (node: SyntaxItemPeer) => { + itemGenerator(ri); + }); + }; + NodeAttach(() => ForEachNodePeer.create(true), (node: ForEachNodePeer) => { + RepeatByArray(arr, keyGen, action); + nodeHolder.node = node; + }); +} diff --git a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/component/forEach.ets b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/component/forEach.ets new file mode 100644 index 0000000000000000000000000000000000000000..80e9cf013a386ebce38b3004fdb771dc129c1594 --- /dev/null +++ b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/component/forEach.ets @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2024-2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +// HANDWRITTEN, DO NOT REGENERATE + +import { int32, hashCodeFromString } from '@koalaui/common' +import { memoEntry2, __context, NodeAttach } from '@koalaui/runtime' +import { InteropNativeModule } from '@koalaui/interop' +import { SyntaxItemPeer, ForEachNodePeer } from '../base/RepeatImpl' + +/** @memo */ +export function ForEach( + arr: () => Array, + /** @memo */ + itemGenerator: (item: T, index: number) => void, + keyGenerator?: (item: T, index: number) => string, +) { + if (arr === null || arr === undefined) { + InteropNativeModule._NativeLog('input array function is null or undefined error. Application error!'); + return; + } + if (typeof itemGenerator !== 'function') { + InteropNativeModule._NativeLog('item generator function missing. Application error!'); + return; + } + if (keyGenerator !== undefined && typeof keyGenerator !== 'function') { + InteropNativeModule._NativeLog('key generator is not a function. Application error!'); + return; + } + /** @memo */ + const createAndUpdate = (): void => { + const array: Array = arr(); + if (array === null || array === undefined) { + InteropNativeModule._NativeLog('input array is null or undefined error. Application error!'); + return; + } + const length: number = array.length; + const key = (element: T, index: int32): int32 => keyGenerator + ? hashCodeFromString(keyGenerator!(element, (index as number))) + : index; + /** @memo */ + const action = (element: T, index: int32): void => { + NodeAttach(() => SyntaxItemPeer.create(), (node: SyntaxItemPeer) => { + itemGenerator(element, (index as number)); + }); + }; + for (let i = 0; i < length; i++) { + const e: T = array[i]; + memoEntry2(__context(), key(e, i), action, e, i); + } + } + NodeAttach(() => ForEachNodePeer.create(), (node: ForEachNodePeer) => { + createAndUpdate(); + }); +} diff --git a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/component/lazyForEach.ets b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/component/lazyForEach.ets index f2b324fbf8c083211144afca46610a4ecf37cd57..47c1615df179236c99dca2d6cbe1725abbaf06d9 100644 --- a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/component/lazyForEach.ets +++ b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/component/lazyForEach.ets @@ -14,19 +14,20 @@ */ -// WARNING! THIS FILE IS AUTO-GENERATED, DO NOT MAKE CHANGES, THEY WILL BE LOST ON NEXT GENERATION! +// HANDWRITTEN, DO NOT REGENERATE + +import { __context, __id, remember } from '@koalaui/runtime' +import { ArkCommonMethodComponent, CommonMethod, DynamicNode, OnMoveHandler, ItemDragEventHandler } from '#generated' +import { LazyForEachImplForOptions } from '../base/LazyForEachImpl' +import { InteropNativeModule } from '@koalaui/interop' -import { int32, int64, float32 } from "@koalaui/common" -import { KInt, KPointer, KBoolean, NativeBuffer, KStringPtr, wrapCallback } from "@koalaui/interop" -import { memo, memo_stable } from "@koalaui/runtime/annotations" -import { ComponentBuilder } from "@koalaui/builderLambda" export enum DataOperationType { - ADD = "'add'", - DELETE = "'delete'", - EXCHANGE = "'exchange'", - MOVE = "'move'", - CHANGE = "'change'", - RELOAD = "'reload'" + ADD = 'add', + DELETE = 'delete', + EXCHANGE = 'exchange', + MOVE = 'move', + CHANGE = 'change', + RELOAD = 'reload' } export interface DataAddOperation { type: DataOperationType; @@ -70,3 +71,106 @@ export interface DataReloadOperation { type: DataOperationType; } export type DataOperation = DataAddOperation | DataDeleteOperation | DataChangeOperation | DataMoveOperation | DataExchangeOperation | DataReloadOperation; + +export interface DataChangeListener { + onDataReloaded(): void; + onDataAdd(index: number): void; + onDataMove(from: number, to: number): void; + onDataDelete(index: number): void; + onDataChange(index: number): void; + onDatasetChange(dataOperations: DataOperation[]): void; +} + +/** + * Developers need to implement this interface to provide data to LazyForEach component. + * @since 7 + */ +export interface IDataSource { + /** + * Total data count. + * @since 7 + */ + totalCount(): number; + /** + * Return the data of index. + * @since 7 + */ + getData(index: number): T; + /** + * Register data change listener. + * @since 7 + */ + registerDataChangeListener(listener: DataChangeListener): void; + /** + * Unregister data change listener. + * @since 7 + */ + unregisterDataChangeListener(listener: DataChangeListener): void; +} + +export interface LazyForEachAttribute extends CommonMethod, DynamicNode { + dataSource: IDataSource | null; + setLazyForEachOptions(dataSource: IDataSource, + /** @memo */ + itemGenerator: (item: T, index: number) => void, + keyGenerator?: (item: T, index: number) => string): this { + return this; + } + onMove(handler?: OnMoveHandler): this { + return this; + } + onMove(handler?: OnMoveHandler, eventHandler?: ItemDragEventHandler): this { + return this; + } +} +export class ArkLazyForEachComponent extends ArkCommonMethodComponent implements LazyForEachAttribute { + dataSource: IDataSource | null = null; + /** @memo */ + itemGenerator: (item: T, index: number) => void = (item: T, index: number) => {}; + keyGenerator?: (item: T, index: number) => string = undefined; + onMoveEvent?: OnMoveHandler = undefined; + itemDragEvent?: ItemDragEventHandler = undefined; + + public onMove(handler?: OnMoveHandler): this { + this.onMoveEvent = handler; + return this; + } + + public onMove(handler?: OnMoveHandler, eventHandler?: ItemDragEventHandler): this { + this.onMoveEvent = handler; + this.itemDragEvent = eventHandler; + return this; + } + + public setLazyForEachOptions(dataSource: IDataSource, + /** @memo */ + itemGenerator: (item: T, index: number) => void, + keyGenerator?: (item: T, index: number) => string): this { + this.dataSource = dataSource; + this.itemGenerator = itemGenerator; + this.keyGenerator = keyGenerator; + return this; + } +} +/** @memo */ +export function LazyForEachImpl( + /** @memo */ + style: ((attributes: LazyForEachAttribute) => void) | undefined +) { + const receiver = remember(() => { + return new ArkLazyForEachComponent() + }) + style?.(receiver) + if(!receiver.dataSource) { + InteropNativeModule._NativeLog('LazyForEach receiver.dataSource null !') + } + else { + LazyForEachImplForOptions( + receiver.dataSource!, + receiver.itemGenerator, + receiver.keyGenerator, + false, + receiver.onMoveEvent, + receiver.itemDragEvent); + } +} diff --git a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/component/repeat.ets b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/component/repeat.ets new file mode 100644 index 0000000000000000000000000000000000000000..458b34130d8121c203f4679043e4b82f86dfed27 --- /dev/null +++ b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/component/repeat.ets @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +// HANDWRITTEN, DO NOT REGENERATE + +import { __context, __id, remember } from '@koalaui/runtime' +import { RepeatImplForOptions } from '../base/RepeatImpl' +import { ArkCommonMethodComponent, CommonMethod, DynamicNode, OnMoveHandler, ItemDragEventHandler } from '#generated' + +export interface RepeatItem { + readonly item: T; + readonly index: number; +} + +export type RepeatArray = Array | ReadonlyArray | Readonly>; +export type RepeatItemBuilder = + /** @memo */ + (repeatItem: RepeatItem) => void; +export type TemplateTypedFunc = (item: T, index: number) => string; + +export interface VirtualScrollOptions { + totalCount?: number; + reusable?: boolean; + onLazyLoading?: (index: number) => void; + onTotalCount?: () => number; + disableVirtualScroll?: boolean; +} + +export interface TemplateOptions { + cachedCount?: number; +} + +export interface RepeatAttribute extends CommonMethod, DynamicNode { + arr: RepeatArray; + each(itemGenerator: RepeatItemBuilder): RepeatAttribute; + key(keyGenerator: (item: T, index: number) => string): RepeatAttribute; + virtualScroll(options?: VirtualScrollOptions): RepeatAttribute; + template(type: string, itemBuilder: RepeatItemBuilder, templateOptions?: TemplateOptions): RepeatAttribute; + templateId(typedFunc: TemplateTypedFunc): RepeatAttribute; + onMove(handler?: OnMoveHandler): this; + onMove(handler?: OnMoveHandler, eventHandler?: ItemDragEventHandler): this; + setRepeatOptions(arr: RepeatArray): this { + return this; + } +} + +// export class ArkRepeatStyle extends ArkCommonMethodStyle implements RepeatAttribute { +// public arr: RepeatArray = []; +// public setRepeatOptions(arr: RepeatArray): this { +// this.arr = arr; +// return this; +// } +// } +export class ArkRepeatComponent extends ArkCommonMethodComponent implements RepeatAttribute { + public arr: RepeatArray = []; + public setRepeatOptions(arr: RepeatArray): this { + this.arr = arr; + return this; + } + public each(itemGenerator: RepeatItemBuilder): RepeatAttribute { + return this; + }; + public key(keyGenerator: (item: T, index: number) => string): RepeatAttribute { + return this; + }; + public virtualScroll(options?: VirtualScrollOptions): RepeatAttribute { + return this; + }; + public template(type: string, itemBuilder: RepeatItemBuilder, templateOptions?: TemplateOptions): RepeatAttribute { + return this; + }; + public templateId(typedFunc: TemplateTypedFunc): RepeatAttribute { + return this; + }; + public onMove(handler?: OnMoveHandler): this { + return this; + } + public onMove(handler?: OnMoveHandler, eventHandler?: ItemDragEventHandler): this { + return this; + } +} +/** @memo */ +export function RepeatImpl( + /** @memo */ + style: ((attributes: RepeatAttribute) => void) | undefined, +): void { + const receiver = remember(() => { + return new ArkRepeatComponent() + }) + style?.(receiver) + RepeatImplForOptions(style, receiver.arr); +} diff --git a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/components.gni b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/components.gni index 6e0edc18b8e5dd72f1c461e45befef2533c2df44..14b94041b2a73a85bc2794b3cc9e03496d2d9a1d 100644 --- a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/components.gni +++ b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/components.gni @@ -69,7 +69,6 @@ arkui_files = [ "arkui-preprocessed/arkui/CounterModifier.ets", "arkui-preprocessed/arkui/CustomBuilderRootModifier.ets", "arkui-preprocessed/arkui/CustomLayoutRootModifier.ets", - "arkui-preprocessed/arkui/DataChangeListener.ets", "arkui-preprocessed/arkui/DataPanelModifier.ets", "arkui-preprocessed/arkui/DatePickerModifier.ets", "arkui-preprocessed/arkui/DividerModifier.ets", @@ -80,7 +79,6 @@ arkui_files = [ "arkui-preprocessed/arkui/FlexModifier.ets", "arkui-preprocessed/arkui/FlowItemModifier.ets", "arkui-preprocessed/arkui/FolderStackModifier.ets", - "arkui-preprocessed/arkui/ForEach.ets", "arkui-preprocessed/arkui/FormComponentModifier.ets", "arkui-preprocessed/arkui/FormLinkModifier.ets", "arkui-preprocessed/arkui/FrameNode.ets", @@ -95,7 +93,6 @@ arkui_files = [ "arkui-preprocessed/arkui/ImageModifier.ets", "arkui-preprocessed/arkui/ImageSpanModifier.ets", "arkui-preprocessed/arkui/IndicatorComponentModifier.ets", - "arkui-preprocessed/arkui/LazyForEach.ets", "arkui-preprocessed/arkui/LineModifier.ets", "arkui-preprocessed/arkui/LinearIndicatorModifier.ets", "arkui-preprocessed/arkui/ListItemGroupModifier.ets", @@ -230,6 +227,7 @@ arkui_files = [ "arkui-preprocessed/arkui/component/flowItem.ets", "arkui-preprocessed/arkui/component/focus.ets", "arkui-preprocessed/arkui/component/folderStack.ets", + "arkui-preprocessed/arkui/component/forEach.ets", "arkui-preprocessed/arkui/component/formComponent.ets", "arkui-preprocessed/arkui/component/formLink.ets", "arkui-preprocessed/arkui/component/gauge.ets", @@ -280,6 +278,7 @@ arkui_files = [ "arkui-preprocessed/arkui/component/refresh.ets", "arkui-preprocessed/arkui/component/relativeContainer.ets", "arkui-preprocessed/arkui/component/remoteWindow.ets", + "arkui-preprocessed/arkui/component/repeat.ets", "arkui-preprocessed/arkui/component/resources.ets", "arkui-preprocessed/arkui/component/richEditor.ets", "arkui-preprocessed/arkui/component/richText.ets",