1/* 2 * Copyright (c) 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 16/** 17 * Used to measure theme scopes for the application components 18 */ 19class ArkThemeScopeManager { 20 /** 21 * Theme Scopes are in constructed process. 22 */ 23 private localThemeScopes: ArkThemeScope[] = []; 24 25 /** 26 * All existing theme scopes 27 */ 28 private themeScopes: Array<ArkThemeScope> = undefined; 29 30 /** 31 * Temporary link to the theme scope for If container branches update 32 */ 33 private ifElseLastScope: ArkThemeScope = undefined; 34 35 /** 36 * Stack of ifElse Scopes 37 */ 38 private ifElseScopes: ArkThemeScope[] = []; 39 40 /** 41 * Last handled CustomComponent 42 */ 43 private handledOwnerComponentId: number; 44 45 /** 46 * Rendering state of the handled component 47 */ 48 private handledIsFirstRender: boolean; 49 50 /** 51 * Color Mode of the handled component 52 */ 53 private handledColorMode: ThemeColorMode; 54 55 /** 56 * Theme Scope of the handled component 57 */ 58 private handledThemeScope?: ArkThemeScope; 59 60 /** 61 * elmtId of the handled component 62 */ 63 private handledComponentElmtId?: number; 64 65 /** 66 * Theme Scope Id of the last handled component 67 */ 68 private lastThemeScopeId: number = 0; 69 70 /** 71 * Theme update listeners 72 */ 73 private listeners: ViewPuInternal[] = []; 74 75 /** 76 * The System Theme 77 */ 78 private static SystemTheme = new ArkSystemTheme(); 79 80 /** 81 * The default Theme 82 */ 83 private defaultTheme: ArkThemeBase | undefined = undefined; 84 85 /** 86 * The themeId Stack 87 */ 88 private themeIdStack: number[] = []; 89 90 /** 91 * Handle component before rendering 92 * 93 * @param componentName component name 94 * @param elmtId component elmtId 95 * @param isFirstRender component render state 96 * @param ownerComponent CustomComponent which defines component 97 */ 98 onComponentCreateEnter(componentName: string, elmtId: number, isFirstRender: boolean, ownerComponent: ViewPuInternal) { 99 this.handledIsFirstRender = isFirstRender; 100 this.handledOwnerComponentId = ownerComponent.id__(); 101 this.handledComponentElmtId = elmtId; 102 103 // no need to handle component style if themeScope array is undefined or component is WithTheme container 104 if (!this.themeScopes || componentName === 'WithTheme') { 105 return; 106 } 107 108 // no need to handle component style if themeScope array is empty 109 if (this.themeScopes.length === 0) { 110 // probably in the last draw themeScope was not empty 111 // we have to handle this to flush themeScope for built-in components 112 this.handleThemeScopeChange(undefined); 113 return; 114 } 115 116 let scope: ArkThemeScope = undefined; 117 118 // we need to keep component to the theme scope before first render 119 if (isFirstRender) { 120 const currentLocalScope = this.localThemeScopes[this.localThemeScopes.length - 1]; 121 const currentIfElseScope = this.ifElseScopes[this.ifElseScopes.length - 1]; 122 if (currentLocalScope) { 123 // keep component to the current constructed scope 124 scope = currentLocalScope; 125 scope.addComponentToScope(elmtId, ownerComponent, componentName); 126 } else if (currentIfElseScope) { 127 // keep component to the current ifElse scope 128 scope = currentIfElseScope; 129 scope.addComponentToScope(elmtId, ownerComponent, componentName); 130 } else { 131 // keep component to the same scope as is used by CustomComponen that defines component 132 const parentScope = ownerComponent.themeScope_; 133 if (parentScope) { 134 scope = parentScope; 135 scope.addComponentToScope(elmtId, ownerComponent, componentName); 136 } 137 } 138 // if component didn`t hit any theme scope then we have to use SystemTheme 139 } 140 141 if (scope === undefined) { 142 scope = this.scopeForElmtId(elmtId); 143 } 144 // if cannot getscope before, try get from themeId stack 145 if (scope === undefined && (this.themeIdStack.length > 0)) { 146 scope = this.themeScopes.find(item => item.getWithThemeId() === this.themeIdStack[this.themeIdStack.length - 1]); 147 } 148 // keep color mode for handled container 149 this.handledColorMode = scope?.colorMode(); 150 // trigger for enter local color mode for the component before rendering 151 if (this.handledColorMode === ThemeColorMode.LIGHT || this.handledColorMode === ThemeColorMode.DARK) { 152 this.onEnterLocalColorMode(this.handledColorMode); 153 } 154 155 if (componentName === 'If') { 156 // keep last ifElse scope 157 this.ifElseLastScope = scope; 158 } 159 160 // save theme scope for handled component 161 this.handledThemeScope = scope; 162 // probably theme scope changed after previous component draw, handle it 163 this.handleThemeScopeChange(this.handledThemeScope); 164 // save last scope themeId 165 if (scope) { 166 this.themeIdStack.push(scope.getWithThemeId()); 167 } 168 } 169 170 /** 171 * Handle component after rendering 172 * 173 * @param elmtId component elmtId 174 */ 175 onComponentCreateExit(elmtId: number) { 176 // trigger for exit local color mode for the component after rendering 177 if (this.handledColorMode === ThemeColorMode.LIGHT || this.handledColorMode === ThemeColorMode.DARK) { 178 this.onExitLocalColorMode(); 179 } 180 181 // flush theme scope of the handled component 182 this.handledThemeScope = undefined; 183 this.handledComponentElmtId = undefined; 184 // pop theme scope id of the handled component 185 if (this.themeIdStack.length > 0) { 186 this.themeIdStack.pop(); 187 } 188 } 189 190 /** 191 * Handle enter to the theme scope 192 * 193 * @param withThemeId WithTheme container`s elmtId 194 * @param withThemeOptions WithTheme container`s options 195 */ 196 onScopeEnter(withThemeId: number, withThemeOptions: WithThemeOptions, theme: ArkThemeBase) { 197 // save theme scope id on scope enter 198 this.lastThemeScopeId = withThemeId; 199 if (this.handledIsFirstRender === true) { 200 // create theme scope 201 let themeScope = new ArkThemeScope(this.handledOwnerComponentId, withThemeId, withThemeOptions, theme); 202 // keep created scope to the array of the scopes under construction 203 this.localThemeScopes.push(themeScope); 204 if (!this.themeScopes) { 205 this.themeScopes = new Array(); 206 } 207 // keep created scope to the array of all theme scopes 208 this.themeScopes.push(themeScope); 209 } else { 210 // retrieve existing theme scope by WithTheme elmtId 211 const scope = this.themeScopes.find(item => item.getWithThemeId() === withThemeId); 212 // update WithTheme options 213 scope?.updateWithThemeOptions(withThemeOptions, theme); 214 // re-render all components from the scope 215 this.forceRerenderScope(scope); 216 } 217 } 218 219 /** 220 * Handle exit from the theme scope 221 */ 222 onScopeExit() { 223 // remove top theme scope from the array of scopes under construction 224 if (this.handledIsFirstRender === true) { 225 this.localThemeScopes.pop(); 226 } 227 } 228 229 /** 230 * Handle destroy event for theme scope 231 * 232 * @param themeScopeId if of destroyed theme scope 233 */ 234 onScopeDestroy(themeScopeId: number) { 235 this.themeScopes = this.themeScopes?.filter((scope) => { 236 if (scope.getWithThemeId() === themeScopeId) { 237 this.onScopeDestroyInternal(scope); 238 return false; 239 } 240 return true; 241 }); 242 } 243 244 /** 245 * Destroy theme scope 246 * 247 * @param scope theme scope instance 248 */ 249 private onScopeDestroyInternal(scope: ArkThemeScope) { 250 // unbind theme from scope 251 const theme = scope.getTheme(); 252 if (theme) { 253 theme.unbindFromScope(scope.getWithThemeId()); 254 } 255 256 // remove scope from the list of created scopes 257 const index = this.localThemeScopes.indexOf(scope); 258 if (index !== -1) { 259 this.localThemeScopes.splice(index, 1); 260 } 261 // @ts-ignore 262 WithTheme.removeThemeInNative(scope.getWithThemeId()); 263 } 264 265 /** 266 * Handle create event for CustomComponent which can keep theme scopes 267 * 268 * @param ownerComponent theme scope changes listener 269 */ 270 onViewPUCreate(ownerComponent: ViewPuInternal) { 271 if (ownerComponent.parent_ === undefined) { 272 this.subscribeListener(ownerComponent); 273 } 274 ownerComponent.themeScope_ = this.scopeForElmtId(ownerComponent.id__()); 275 ownerComponent.themeScope_?.addCustomListenerInScope(ownerComponent); 276 } 277 278 /** 279 * Handle close event for CustomComponent which can keep theme scopes 280 * 281 * @param ownerComponent is the closing CustomComponent 282 */ 283 onViewPUDelete(ownerComponent: ViewPuInternal) { 284 // unsubscribe 285 this.unsubscribeListener(ownerComponent); 286 287 // remove scopes that are related to CustomComponent 288 const ownerComponentId: number = ownerComponent.id__(); 289 this.themeScopes = this.themeScopes?.filter((scope) => { 290 if (scope.getOwnerComponentId() === ownerComponentId) { 291 this.onScopeDestroyInternal(scope); 292 return false; 293 } 294 return true; 295 }); 296 } 297 298 /** 299 * Start for IfElse branch update 300 */ 301 onIfElseBranchUpdateEnter() { 302 this.ifElseScopes.push(this.ifElseLastScope); 303 } 304 305 /** 306 * End for IfElse branch update 307 * 308 * @param removedElmtIds elmtIds of the removed components 309 */ 310 onIfElseBranchUpdateExit(removedElmtIds: number[]) { 311 const scope = this.ifElseScopes.pop(); 312 if (removedElmtIds && scope) { 313 removedElmtIds.forEach(elmtId => scope.removeComponentFromScope(elmtId)); 314 } 315 } 316 317 /** 318 * Start for deep rendering with theme scope 319 * 320 * @param themeScope add scope for deep render components 321 * @returns true if scope is successfully added, otherwise false 322 */ 323 onDeepRenderScopeEnter(themeScope: ArkThemeScope): boolean { 324 if (themeScope) { 325 this.localThemeScopes.push(themeScope); 326 return true; 327 } 328 return false; 329 } 330 331 /** 332 * End of deep rendering with theme scope 333 */ 334 onDeepRenderScopeExit() { 335 this.localThemeScopes.pop(); 336 } 337 338 /** 339 * Subscribe listener to theme scope changes events 340 * 341 * @param listener theme scope changes listener 342 */ 343 private subscribeListener(listener: ViewPuInternal) { 344 if (this.listeners.includes(listener)) { 345 return; 346 } 347 this.listeners.push(listener); 348 } 349 350 /** 351 * Unsubscribe listener from theme scope changes events 352 * 353 * @param listener theme scope changes listener 354 */ 355 private unsubscribeListener(listener: ViewPuInternal) { 356 const index = this.listeners.indexOf(listener, 0); 357 if (index > -1) { 358 this.listeners.splice(index, 1); 359 } 360 const scope = listener.themeScope_; 361 if (scope) { 362 scope.removeComponentFromScope(listener.id__()); 363 listener.themeScope_ = undefined; 364 } 365 } 366 367 /** 368 * Obtain final theme by component instance 369 * 370 * @param ownerComponent Custom component instance 371 * @returns theme instance associated with this Theme Scope 372 * or previously set Default Theme or System Theme 373 */ 374 getFinalTheme(ownerComponent: ViewPuInternal): Theme { 375 return ownerComponent.themeScope_?.getTheme() ?? this.defaultTheme ?? ArkThemeScopeManager.SystemTheme; 376 } 377 378 /** 379 * Obtain scope by component`s elmtId 380 * 381 * @param elmtId component`s elmtId as number 382 * @returns ArkThemeScope instance or undefined 383 */ 384 scopeForElmtId(elmtId: number): ArkThemeScope { 385 // return theme scope of the handled component if we know it 386 if (this.handledThemeScope && this.handledComponentElmtId === elmtId) { 387 return this.handledThemeScope; 388 } 389 // fast way to get theme scope for the first rendered component 390 if (this.handledIsFirstRender) { 391 if (this.localThemeScopes.length > 0) { // current cunstructed scope 392 return this.localThemeScopes[this.localThemeScopes.length - 1]; 393 } 394 } 395 396 // common way to get scope for the component 397 return this.themeScopes?.find(item => item.isComponentInScope(elmtId)); 398 } 399 400 /** 401 * Get the actual theme scope used for current components construction 402 * 403 * @returns Theme Scope instance 404 */ 405 lastLocalThemeScope(): ArkThemeScope { 406 if (this.localThemeScopes.length > 0) { 407 return this.localThemeScopes[this.localThemeScopes.length - 1]; 408 } 409 return undefined; 410 } 411 412 /** 413 * Enter to the local color mode scope 414 * 415 * @param colorMode local color mode 416 */ 417 onEnterLocalColorMode(colorMode: ThemeColorMode) { 418 getUINativeModule().resource.updateColorMode(colorMode); 419 } 420 421 /** 422 * Exit from the local color mode scope 423 */ 424 onExitLocalColorMode() { 425 getUINativeModule().resource.restore(); 426 } 427 428 /** 429 * Trigger re-render for all components in scope 430 * 431 * @param scope scope need to be re-rendered 432 * @returns 433 */ 434 private forceRerenderScope(scope: ArkThemeScope) { 435 if (scope === undefined) { 436 return; 437 } 438 const theme: Theme = scope?.getTheme() ?? this.defaultTheme ?? ArkThemeScopeManager.SystemTheme; 439 scope.componentsInScope()?.forEach((item) => this.notifyScopeThemeChanged(item, theme, scope.isColorModeChanged())); 440 } 441 442 /** 443 * Notify listeners to re-render component 444 * 445 * @param elmtId component`s elmtId as number 446 * @param themeWillApply Theme that should be passed to onWIllApplyTheme callback 447 * @param isColorModeChanged notifies about specific case 448 */ 449 private notifyScopeThemeChanged(item: ArkThemeScopeItem, themeWillApply: Theme, isColorModeChanged: boolean) { 450 if (item.owner) { 451 const listener = item.owner; 452 if (isColorModeChanged) { 453 // we need to redraw all nodes if developer set new local colorMode 454 listener.forceRerenderNode(item.elmtId); 455 } else { 456 // take whitelist info from cache item 457 let isInWhiteList = item.isInWhiteList; 458 if (isInWhiteList === undefined) { 459 // if whitelist info is undefined we have check whitelist directly 460 isInWhiteList = ArkThemeWhiteList.isInWhiteList(item.name); 461 // keep result in cache item for the next checks 462 item.isInWhiteList = isInWhiteList; 463 } 464 if (isInWhiteList === true) { 465 // redraw node only if component within whitelist 466 listener.forceRerenderNode(item.elmtId); 467 } 468 } 469 } 470 if (item.listener) { 471 const listener = item.listener; 472 listener.onWillApplyTheme(themeWillApply); 473 } 474 } 475 476 /** 477 * Create Theme instance 478 * 479 * @param customTheme instance of CustomTheme used to create theme 480 * @param colorMode local colorm mode used for theme 481 * @returns theme instance 482 */ 483 makeTheme(customTheme: CustomThemeInternal, colorMode: ThemeColorMode): ArkThemeBase { 484 const baselineTheme = this.defaultTheme ?? ArkThemeScopeManager.SystemTheme; 485 // try to take theme from the cache 486 const theme = ArkThemeCache.getInstance().get(baselineTheme.id, customTheme, colorMode); 487 488 // return theme instance from cache or create new theme instance 489 return theme ? theme : new ArkThemeImpl( 490 customTheme, 491 colorMode, 492 baselineTheme 493 ); 494 } 495 496 /** 497 * Create CustomTheme instance based on given Custom theme with the additional expands 498 * 499 * @param customTheme instance of CustomTheme used to create theme 500 * @returns theme instance 501 */ 502 static cloneCustomThemeWithExpand(customTheme: CustomThemeInternal): CustomThemeInternal { 503 const theme = ArkThemeBase.copyCustomTheme(customTheme); 504 if (theme?.colors) { 505 ArkColorsImpl.expandByBrandColor(theme.colors); 506 } 507 if (theme?.darkColors) { 508 ArkColorsImpl.expandByBrandColor(theme.darkColors); 509 } 510 return theme; 511 } 512 513 /** 514 * Set the default Theme 515 * 516 * @param theme is the CustomTheme and the default Theme will be built on base of it. 517 * If theme is 'undefined' then the native system theme will be used as default one. 518 */ 519 setDefaultTheme(customTheme: CustomThemeInternal) { 520 // unbind previous default theme from 0 theme scope 521 this.defaultTheme?.unbindFromScope(0); 522 this.defaultTheme = ArkThemeScopeManager.SystemTheme; 523 const cloneTheme = ArkThemeScopeManager.cloneCustomThemeWithExpand(customTheme); 524 this.defaultTheme = this.makeTheme(cloneTheme, ThemeColorMode.SYSTEM); 525 // bind new default theme to 0 theme scope 526 this.defaultTheme.bindToScope(0); 527 528 // keep for backward compatibility 529 ArkThemeNativeHelper.sendThemeToNative(this.defaultTheme, 0); // 0 means default Theme scope id 530 // new approach to apply theme in native side 531 ArkThemeNativeHelper.setDefaultTheme(cloneTheme); 532 533 this.notifyGlobalThemeChanged(); 534 } 535 536 /** 537 * Obtain System Colors 538 * 539 * @returns System Colors 540 */ 541 static getSystemColors(): Colors { 542 return ArkThemeScopeManager.SystemTheme.colors; 543 } 544 545 /** 546 * Notifies listeners about app Theme change 547 */ 548 private notifyGlobalThemeChanged() { 549 this.listeners.forEach(listener => { 550 if (listener.parent_ === undefined) { 551 listener.onGlobalThemeChanged(); 552 } 553 }) 554 } 555 556 /** 557 * Compares last theme scope id with current. 558 * Notifies native side about theme scope change if need. 559 * 560 * @param scope handled theme scope instance 561 */ 562 private handleThemeScopeChange(scope: ArkThemeScope) { 563 const currentThemeScopeId = scope?.getWithThemeId() ?? 0; 564 if (currentThemeScopeId !== this.lastThemeScopeId) { 565 this.lastThemeScopeId = currentThemeScopeId; 566 // @ts-ignore 567 WithTheme.setThemeScopeId(currentThemeScopeId); 568 } 569 } 570 571 public setIsFirstRender(isFirstRender: boolean) { 572 this.handledIsFirstRender = isFirstRender; 573 } 574 575 private static instance: ArkThemeScopeManager | undefined = undefined 576 static getInstance() : ArkThemeScopeManager { 577 if (!ArkThemeScopeManager.instance) { 578 ArkThemeScopeManager.instance = new ArkThemeScopeManager(); 579 ViewBuildNodeBase.setArkThemeScopeManager(ArkThemeScopeManager.instance); 580 } 581 return ArkThemeScopeManager.instance; 582 } 583} 584 585globalThis.themeScopeMgr = ArkThemeScopeManager.getInstance(); 586