• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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