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: ArkThemeScope[] = []; 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 update listeners 57 */ 58 private listeners: ViewPuInternal[] = []; 59 60 /** 61 * The System Theme 62 */ 63 private static SystemTheme = new ArkSystemTheme(); 64 65 /** 66 * The default Theme 67 */ 68 private defaultTheme: ThemeInternal | undefined = undefined; 69 70 /** 71 * Handle component before rendering 72 * 73 * @param componentName component name 74 * @param elmtId component elmtId 75 * @param isFirstRender component render state 76 * @param ownerComponent CustomComponent which defines component 77 */ 78 onComponentCreateEnter(componentName: string, elmtId: number, isFirstRender: boolean, ownerComponent: ViewPuInternal) { 79 this.handledIsFirstRender = isFirstRender; 80 this.handledOwnerComponentId = ownerComponent.id__(); 81 82 // no need to handle render for WithTheme container 83 if (this.themeScopes.length === 0 || componentName === 'WithTheme') { 84 return; 85 } 86 87 88 let scope: ArkThemeScope = undefined; 89 90 // we need to keep component to the theme scope before first render 91 if (isFirstRender) { 92 const currentLocalScope = this.localThemeScopes[this.localThemeScopes.length - 1]; 93 const currentIfElseScope = this.ifElseScopes[this.ifElseScopes.length - 1]; 94 if (currentLocalScope) { 95 // keep component to the current constructed scope 96 scope = currentLocalScope; 97 scope.addComponentToScope(elmtId, ownerComponent.id__(), componentName) 98 } else if (currentIfElseScope) { 99 // keep component to the current ifElse scope 100 scope = currentIfElseScope; 101 scope.addComponentToScope(elmtId, ownerComponent.id__(), componentName); 102 } else { 103 // keep component to the same scope as is used by CustomComponen that defines component 104 const parentScope = ownerComponent.themeScope_; 105 if (parentScope) { 106 scope = parentScope; 107 scope.addComponentToScope(elmtId, ownerComponent.id__(), componentName) 108 } 109 } 110 // if component didn`t hit any theme scope then we have to use SystemTheme 111 } 112 113 if (scope === undefined) { 114 scope = this.scopeForElmtId(elmtId); 115 } 116 // keep color mode for handled container 117 this.handledColorMode = scope?.colorMode(); 118 // trigger for enter local color mode for the component before rendering 119 if (this.handledColorMode === ThemeColorMode.LIGHT || this.handledColorMode === ThemeColorMode.DARK) { 120 this.onEnterLocalColorMode(this.handledColorMode); 121 } 122 123 if (componentName === 'If') { 124 // keep last ifElse scope 125 this.ifElseLastScope = scope; 126 } 127 } 128 129 /** 130 * Handle component after rendering 131 * 132 * @param elmtId component elmtId 133 */ 134 onComponentCreateExit(elmtId: number) { 135 // trigger for exit local color mode for the component after rendering 136 if (this.handledColorMode === ThemeColorMode.LIGHT || this.handledColorMode === ThemeColorMode.DARK) { 137 this.onExitLocalColorMode(); 138 } 139 } 140 141 /** 142 * Handle enter to the theme scope 143 * 144 * @param withThemeId WithTheme container`s elmtId 145 * @param withThemeOptions WithTheme container`s options 146 */ 147 onScopeEnter(withThemeId: number, withThemeOptions: WithThemeOptions, theme: ThemeInternal) { 148 if (this.handledIsFirstRender === true) { 149 // create theme scope 150 let themeScope = new ArkThemeScope(this.handledOwnerComponentId, withThemeId, withThemeOptions, theme); 151 // keep created scope to the array of the scopes under construction 152 this.localThemeScopes.push(themeScope); 153 // keep created scope to the array of all theme scopes 154 this.themeScopes.push(themeScope); 155 } else { 156 // retrieve existing theme scope by WithTheme elmtId 157 const scope = this.themeScopes.find(item => item.getWithThemeId() === withThemeId); 158 // update WithTheme options 159 scope.updateWithThemeOptions(withThemeOptions, theme); 160 // re-render all components from the scope 161 this.forceRerenderScope(scope); 162 } 163 } 164 165 /** 166 * Handle exit from the theme scope 167 */ 168 onScopeExit() { 169 // remove top theme scope from the array of scopes under construction 170 if (this.handledIsFirstRender === true) { 171 this.localThemeScopes.pop(); 172 } 173 } 174 175 /** 176 * Handle create event for CustomComponent which can keep theme scopes 177 * 178 * @param ownerComponent theme scope changes listener 179 */ 180 onViewPUCreate(ownerComponent: ViewPuInternal) { 181 this.subscribeListener(ownerComponent); 182 ownerComponent.themeScope_ = this.scopeForElmtId(ownerComponent.id__()); 183 } 184 185 /** 186 * Handle close event for CustomComponent which can keep theme scopes 187 * 188 * @param ownerComponent is the closing CustomComponent 189 */ 190 onViewPUDelete(ownerComponent: ViewPuInternal) { 191 // unsubscribe 192 this.unsubscribeListener(ownerComponent); 193 194 // remove scopes that are related to CustomComponent 195 const ownerComponentId: number = ownerComponent.id__(); 196 this.themeScopes = this.themeScopes.filter((scope) => { 197 if (scope.getOwnerComponentId() === ownerComponentId) { 198 const index = this.localThemeScopes.indexOf(scope); 199 if (index !== -1) { 200 this.localThemeScopes.splice(index, 1); 201 } 202 // @ts-ignore 203 WithTheme.removeThemeInNative(scope.getWithThemeId()); 204 return false; 205 } 206 return true; 207 }); 208 } 209 210 /** 211 * Start for IfElse branch update 212 */ 213 onIfElseBranchUpdateEnter() { 214 this.ifElseScopes.push(this.ifElseLastScope); 215 } 216 217 /** 218 * End for IfElse branch update 219 * 220 * @param removedElmtIds elmtIds of the removed components 221 */ 222 onIfElseBranchUpdateExit(removedElmtIds: number[]) { 223 const scope = this.ifElseScopes.pop(); 224 if (removedElmtIds && scope) { 225 removedElmtIds.forEach(elmtId => scope.removeComponentFromScope(elmtId)); 226 } 227 } 228 229 /** 230 * Start for deep rendering with theme scope 231 * 232 * @param themeScope add scope for deep render components 233 * @returns true if scope is successfully added, otherwise false 234 */ 235 onDeepRenderScopeEnter(themeScope: ArkThemeScope): boolean { 236 if (themeScope) { 237 this.localThemeScopes.push(themeScope); 238 return true; 239 } 240 return false; 241 } 242 243 /** 244 * End of deep rendering with theme scope 245 */ 246 onDeepRenderScopeExit() { 247 this.localThemeScopes.pop(); 248 } 249 250 /** 251 * Subscribe listener to theme scope changes events 252 * 253 * @param listener theme scope changes listener 254 */ 255 private subscribeListener(listener: ViewPuInternal) { 256 if (this.listeners.includes(listener)) { 257 return; 258 } 259 this.listeners.push(listener); 260 } 261 262 /** 263 * Unsubscribe listener from theme scope changes events 264 * 265 * @param listener theme scope changes listener 266 */ 267 private unsubscribeListener(listener: ViewPuInternal) { 268 const index = this.listeners.indexOf(listener, 0); 269 if (index > -1) { 270 this.listeners.splice(index, 1); 271 } 272 } 273 274 /** 275 * Obtain theme by component`s elmtId 276 * 277 * @param elmtId component`s elmtId as number 278 * @returns theme instance associated with this Theme Scope 279 * or previously set Default Theme or 'undefined' (in case of necessary to use the native theme) 280 */ 281 themeForElmtId(elmtId: number): Theme { 282 const scope = this.scopeForElmtId(elmtId); 283 return scope?.getTheme() ?? this.defaultTheme; 284 } 285 286 /** 287 * Obtain final theme by component`s elmtId 288 * 289 * @param elmtId component`s elmtId as number 290 * @returns theme instance associated with this Theme Scope 291 * or previously set Default Theme or System Theme 292 */ 293 getFinalTheme(elmtId: number): Theme { 294 return this.themeForElmtId(elmtId) ?? ArkThemeScopeManager.SystemTheme; 295 } 296 297 /** 298 * Obtain scope by component`s elmtId 299 * 300 * @param elmtId component`s elmtId as number 301 * @returns ArkThemeScope instance or undefined 302 */ 303 scopeForElmtId(elmtId: number): ArkThemeScope { 304 // fast way to get theme scope for the first rendered component 305 if (this.handledIsFirstRender) { 306 if (this.localThemeScopes.length > 0) { // current cunstructed scope 307 return this.localThemeScopes[this.localThemeScopes.length - 1]; 308 } 309 } 310 311 // common way to get scope for the component 312 return this.themeScopes.find(item => item.isComponentInScope(elmtId)); 313 } 314 315 /** 316 * Get the actual theme scope used for current components construction 317 * 318 * @returns Theme Scope instance 319 */ 320 lastLocalThemeScope(): ArkThemeScope { 321 if (this.localThemeScopes.length > 0) { 322 return this.localThemeScopes[this.localThemeScopes.length - 1]; 323 } 324 return undefined; 325 } 326 327 /** 328 * Enter to the local color mode scope 329 * 330 * @param colorMode local color mode 331 */ 332 onEnterLocalColorMode(colorMode: ThemeColorMode) { 333 getUINativeModule().resource.updateColorMode(colorMode); 334 } 335 336 /** 337 * Exit from the local color mode scope 338 */ 339 onExitLocalColorMode() { 340 getUINativeModule().resource.restore(); 341 } 342 343 /** 344 * Trigger re-render for all components in scope 345 * 346 * @param scope scope need to be re-rendered 347 * @returns 348 */ 349 private forceRerenderScope(scope: ArkThemeScope) { 350 if (scope === undefined) { 351 return; 352 } 353 const components = scope.componentsInScope(); 354 if (components) { 355 components.forEach((item) => { 356 this.notifyScopeThemeChanged(item, scope); 357 }) 358 } 359 } 360 361 /** 362 * Notify listeners to re-render component 363 * 364 * @param elmtId component`s elmtId as number 365 */ 366 private notifyScopeThemeChanged(item: ArkThemeScopeItem, scope: ArkThemeScope) { 367 this.listeners.forEach((listener) => { 368 const listenerId = listener.id__(); 369 if (listenerId === item.owner) { 370 if (scope.isColorModeChanged()) { 371 // we need to redraw all nodes if developer set new local colorMode 372 listener.forceRerenderNode(item.elmtId); 373 } else { 374 // take whitelist info from cache item 375 let isInWhiteList = item.isInWhiteList; 376 if (isInWhiteList === undefined) { 377 // if whitelist info is undefined we have check whitelist directly 378 isInWhiteList = ArkThemeWhiteList.isInWhiteList(item.name); 379 // keep result in cache item for the next checks 380 item.isInWhiteList = isInWhiteList; 381 } 382 if (isInWhiteList === true) { 383 // redraw node only if component within whitelist 384 listener.forceRerenderNode(item.elmtId); 385 } 386 } 387 } else if (listenerId === item.elmtId) { 388 listener.onWillApplyTheme(scope?.getTheme() ?? this.defaultTheme ?? ArkThemeScopeManager.SystemTheme); 389 } 390 }) 391 } 392 393 /** 394 * Create Theme instance based on 395 * - given Custom Theme 396 * - and Default or System Theme (defined in this class) 397 */ 398 makeTheme(customTheme: CustomThemeInternal): ThemeInternal { 399 if (!customTheme) { 400 return this.defaultTheme ?? ArkThemeScopeManager.SystemTheme; 401 } 402 // create Theme based on Custom Theme tokens and Baseline Theme 403 // and return this instance 404 return new ArkThemeImpl( 405 this.defaultTheme ?? ArkThemeScopeManager.SystemTheme, 406 customTheme.colors, 407 customTheme.shapes, 408 customTheme.typography 409 ); 410 } 411 412 /** 413 * Set the default Theme 414 * 415 * @param theme is the CustomTheme and the default Theme will be built on base of it. 416 * If theme is 'undefined' then the native system theme will be used as default one. 417 */ 418 setDefaultTheme(customTheme: CustomThemeInternal) { 419 this.defaultTheme = ArkThemeScopeManager.SystemTheme; 420 this.defaultTheme = this.makeTheme(customTheme); 421 ArkThemeNativeHelper.sendThemeToNative(this.defaultTheme, 0); // 0 means default Theme scope id 422 this.notifyGlobalThemeChanged(); 423 } 424 425 /** 426 * Notifies listeners about app Theme change 427 */ 428 private notifyGlobalThemeChanged() { 429 this.listeners.forEach(listener => { 430 if (listener.parent_ === undefined) { 431 listener.onGlobalThemeChanged(); 432 } 433 }) 434 } 435 436 getWithThemeIdForElmtId(elmtId: number): number { 437 return this.scopeForElmtId(elmtId)?.getWithThemeId() ?? 0; 438 } 439 440 private static instance: ArkThemeScopeManager | undefined = undefined 441 static getInstance() : ArkThemeScopeManager { 442 if (!ArkThemeScopeManager.instance) { 443 ArkThemeScopeManager.instance = new ArkThemeScopeManager(); 444 PUV2ViewBase.setArkThemeScopeManager(ArkThemeScopeManager.instance); 445 } 446 return ArkThemeScopeManager.instance; 447 } 448} 449 450globalThis.themeScopeMgr = ArkThemeScopeManager.getInstance(); 451