theme.ts 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. /**
  2. * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  5. */
  6. import { ColorTheme } from './color';
  7. import { SizeTheme } from './size';
  8. import { Structure } from '../mol-model/structure';
  9. import { Volume } from '../mol-model/volume';
  10. import { ParamDefinition as PD } from '../mol-util/param-definition';
  11. import { Shape } from '../mol-model/shape';
  12. import { CustomProperty } from '../mol-model-props/common/custom-property';
  13. import { objectForEach } from '../mol-util/object';
  14. import { ColorType } from '../mol-geo/geometry/color-data';
  15. export interface ThemeRegistryContext {
  16. colorThemeRegistry: ColorTheme.Registry
  17. sizeThemeRegistry: SizeTheme.Registry
  18. }
  19. export interface ThemeDataContext {
  20. [k: string]: any
  21. structure?: Structure
  22. volume?: Volume
  23. shape?: Shape
  24. }
  25. export { Theme };
  26. interface Theme {
  27. color: ColorTheme<any, any>
  28. size: SizeTheme<any>
  29. // label: LabelTheme // TODO
  30. }
  31. namespace Theme {
  32. type Props = { [k: string]: any }
  33. export function create(ctx: ThemeRegistryContext, data: ThemeDataContext, props: Props, theme?: Theme) {
  34. theme = theme || createEmpty();
  35. const colorProps = props.colorTheme as PD.NamedParams;
  36. const sizeProps = props.sizeTheme as PD.NamedParams;
  37. theme.color = ctx.colorThemeRegistry.create(colorProps.name, data, colorProps.params);
  38. theme.size = ctx.sizeThemeRegistry.create(sizeProps.name, data, sizeProps.params);
  39. return theme;
  40. }
  41. export function createEmpty(): Theme {
  42. return { color: ColorTheme.Empty, size: SizeTheme.Empty };
  43. }
  44. export async function ensureDependencies(ctx: CustomProperty.Context, theme: ThemeRegistryContext, data: ThemeDataContext, props: Props) {
  45. await theme.colorThemeRegistry.get(props.colorTheme.name).ensureCustomProperties?.attach(ctx, data);
  46. await theme.sizeThemeRegistry.get(props.sizeTheme.name).ensureCustomProperties?.attach(ctx, data);
  47. }
  48. export function releaseDependencies(theme: ThemeRegistryContext, data: ThemeDataContext, props: Props) {
  49. theme.colorThemeRegistry.get(props.colorTheme.name).ensureCustomProperties?.detach(data);
  50. theme.sizeThemeRegistry.get(props.sizeTheme.name).ensureCustomProperties?.detach(data);
  51. }
  52. }
  53. //
  54. export interface ThemeProvider<T extends ColorTheme<P, G> | SizeTheme<P>, P extends PD.Params, Id extends string = string, G extends ColorType = ColorType> {
  55. readonly name: Id
  56. readonly label: string
  57. readonly category: string
  58. readonly factory: (ctx: ThemeDataContext, props: PD.Values<P>) => T
  59. readonly getParams: (ctx: ThemeDataContext) => P
  60. readonly defaultValues: PD.Values<P>
  61. readonly isApplicable: (ctx: ThemeDataContext) => boolean
  62. readonly ensureCustomProperties?: {
  63. attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => Promise<void>,
  64. detach: (data: ThemeDataContext) => void
  65. }
  66. }
  67. function getTypes(list: { name: string, provider: ThemeProvider<any, any> }[]) {
  68. return list.map(e => [e.name, e.provider.label, e.provider.category] as [string, string, string]);
  69. }
  70. export class ThemeRegistry<T extends ColorTheme<any, any> | SizeTheme<any>> {
  71. private _list: { name: string, provider: ThemeProvider<T, any> }[] = [];
  72. private _map = new Map<string, ThemeProvider<T, any>>();
  73. private _name = new Map<ThemeProvider<T, any>, string>();
  74. get default() { return this._list[0]; }
  75. get list() { return this._list; }
  76. get types(): [string, string, string][] { return getTypes(this._list); }
  77. constructor(builtInThemes: { [k: string]: ThemeProvider<T, any> }, private emptyProvider: ThemeProvider<T, any>) {
  78. objectForEach(builtInThemes, (p, k) => {
  79. if (p.name !== k) throw new Error(`Fix build in themes to have matching names. ${p.name} ${k}`);
  80. this.add(p as any);
  81. });
  82. }
  83. private sort() {
  84. this._list.sort((a, b) => {
  85. if (a.provider.category === b.provider.category) {
  86. return a.provider.label < b.provider.label ? -1 : a.provider.label > b.provider.label ? 1 : 0;
  87. }
  88. return a.provider.category < b.provider.category ? -1 : 1;
  89. });
  90. }
  91. add<P extends PD.Params>(provider: ThemeProvider<T, P>) {
  92. if (this._map.has(provider.name)) {
  93. throw new Error(`${provider.name} already registered.`);
  94. }
  95. const name = provider.name;
  96. this._list.push({ name, provider });
  97. this._map.set(name, provider);
  98. this._name.set(provider, name);
  99. this.sort();
  100. }
  101. remove(provider: ThemeProvider<T, any>) {
  102. this._list.splice(this._list.findIndex(e => e.name === provider.name), 1);
  103. const p = this._map.get(provider.name);
  104. if (p) {
  105. this._map.delete(provider.name);
  106. this._name.delete(p);
  107. }
  108. }
  109. has(provider: ThemeProvider<T, any>): boolean {
  110. return this._map.has(provider.name);
  111. }
  112. get<P extends PD.Params>(name: string): ThemeProvider<T, P> {
  113. return this._map.get(name) || this.emptyProvider;
  114. }
  115. getName(provider: ThemeProvider<T, any>): string {
  116. if (!this._name.has(provider)) throw new Error(`'${provider.label}' is not a registered theme provider.`);
  117. return this._name.get(provider)!;
  118. }
  119. create(name: string, ctx: ThemeDataContext, props = {}) {
  120. const provider = this.get(name);
  121. return provider.factory(ctx, { ...PD.getDefaultValues(provider.getParams(ctx)), ...props });
  122. }
  123. getApplicableList(ctx: ThemeDataContext) {
  124. return this._list.filter(e => e.provider.isApplicable(ctx));
  125. }
  126. getApplicableTypes(ctx: ThemeDataContext) {
  127. return getTypes(this.getApplicableList(ctx));
  128. }
  129. }