• 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: 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