diff --git a/package-lock.json b/package-lock.json index e8d174db64f94695fff7d142ab262682782e2d2e..7e574f630d3b78f7f2f79cd0b910a5f38116cbf4 100644 Binary files a/package-lock.json and b/package-lock.json differ diff --git a/package.json b/package.json index 2a60147a8a2c4f21c85e8a287ddb62b204c4fa0f..4ec4c0c13a5389960600ee76b858fb7d1aafb13b 100644 --- a/package.json +++ b/package.json @@ -12,13 +12,12 @@ }, "scripts": { "lint": "tslint src/**/*.ts", - "build": "cpx \"src/**/*.{vert,frag,glsl}\" build/node_modules/ && tsc", + "build": "cpx \"src/**/*.{vert,frag,glsl,scss,woff,woff2,ttf,otf,eot,svg,html}\" build/node_modules/ && tsc", "watch": "tsc -watch", - "watch-shader": "cpx \"src/**/*.{vert,frag,glsl}\" build/node_modules/ --watch", + "watch-extra": "cpx \"src/**/*.{vert,frag,glsl,scss,woff,woff2,ttf,otf,eot,svg,html}\" build/node_modules/ --watch", "test": "jest", - "script": "node build/node_modules/script.js", - "app-render-test": "webpack build/node_modules/apps/render-test/index.js --mode development -o web/render-test/index.js", - "app-render-test-watch": "webpack build/node_modules/apps/render-test/index.js -w --mode development -o web/render-test/index.js" + "build-viewer": "webpack build/node_modules/apps/viewer/index.js --mode development -o build/viewer/index.js", + "watch-viewer": "webpack build/node_modules/apps/viewer/index.js -w --mode development -o build/viewer/index.js" }, "jest": { "moduleFileExtensions": [ @@ -34,6 +33,7 @@ "build/node_modules" ], "moduleNameMapper": { + "mol-app($|/.*)": "<rootDir>/src/mol-app$1", "mol-data($|/.*)": "<rootDir>/src/mol-data$1", "mol-geo($|/.*)": "<rootDir>/src/mol-geo$1", "mol-gl($|/.*)": "<rootDir>/src/mol-gl$1", @@ -60,35 +60,42 @@ "@types/benchmark": "^1.0.31", "@types/express": "^4.11.1", "@types/jest": "^22.2.3", - "@types/node": "^9.6.8", + "@types/node": "^9.6.16", "@types/node-fetch": "^1.6.9", - "@types/react": "^16.3.13", + "@types/react": "^16.3.14", "@types/react-dom": "^16.0.5", "benchmark": "^2.1.4", "copyfiles": "^2.0.0", "cpx": "^1.5.0", + "css-loader": "^0.28.11", "extra-watch-webpack-plugin": "^1.0.3", + "extract-text-webpack-plugin": "^4.0.0-beta.0", + "file-loader": "^1.1.11", "glslify-import": "^3.1.0", "glslify-loader": "^1.0.2", "jest": "^22.4.3", "jest-raw-loader": "^1.0.1", + "node-sass": "^4.9.0", "raw-loader": "^0.5.1", - "ts-jest": "^22.4.4", - "tslint": "^5.9.1", + "resolve-url-loader": "^2.3.0", + "sass-loader": "^7.0.1", + "style-loader": "^0.21.0", + "ts-jest": "^22.4.6", + "tslint": "^5.10.0", "typescript": "^2.8.3", - "uglify-js": "^3.3.23", + "uglify-js": "^3.3.25", "util.promisify": "^1.0.0", - "webpack": "^4.6.0", - "webpack-cli": "^2.1.2" + "webpack": "^4.8.3", + "webpack-cli": "^2.1.3" }, "dependencies": { "argparse": "^1.0.10", "express": "^4.16.3", "gl": "^4.0.4", - "material-ui": "^1.0.0-beta.43", + "immutable": "^4.0.0-rc.9", "node-fetch": "^2.1.2", "react": "^16.3.2", "react-dom": "^16.3.2", - "rxjs": "^6.0.0" + "rxjs": "^6.1.0" } } diff --git a/src/apps/cif2bcif/converter.ts b/src/apps/cif2bcif/converter.ts index 1a3a08221bdb79e9808e69dad4651aa141e2cd21..8a0530bc5a9acb1dc25e351ba017fbe066307a25 100644 --- a/src/apps/cif2bcif/converter.ts +++ b/src/apps/cif2bcif/converter.ts @@ -5,7 +5,7 @@ */ import Iterator from 'mol-data/iterator' -import CIF, { Category } from 'mol-io/reader/cif' +import CIF, { CifCategory } from 'mol-io/reader/cif' import * as Encoder from 'mol-io/writer/cif' import * as fs from 'fs' import classify from './field-classifier' @@ -20,14 +20,14 @@ async function getCIF(path: string) { return parsed.result; } -function createDefinition(cat: Category): Encoder.CategoryDefinition { +function createDefinition(cat: CifCategory): Encoder.CategoryDefinition { return { name: cat.name, fields: cat.fieldNames.map(n => classify(n, cat.getField(n)!)) } } -function getCategoryInstanceProvider(cat: Category): Encoder.CategoryProvider { +function getCategoryInstanceProvider(cat: CifCategory): Encoder.CategoryProvider { return function (ctx: any) { return { data: cat, @@ -50,4 +50,3 @@ export default async function convert(path: string, asText = false) { } return encoder.getData(); } - diff --git a/src/apps/cif2bcif/field-classifier.ts b/src/apps/cif2bcif/field-classifier.ts index 9d8d322f6b424884773f9bbbaf26d924fd2a766c..d0725993f29c05f1326ce2ffead2a45ec0083ab5 100644 --- a/src/apps/cif2bcif/field-classifier.ts +++ b/src/apps/cif2bcif/field-classifier.ts @@ -5,7 +5,7 @@ */ import { Column } from 'mol-data/db' -import { Field } from 'mol-io/reader/cif/data-model' +import { CifField } from 'mol-io/reader/cif/data-model' import { FieldDefinition, FieldType } from 'mol-io/writer/cif/encoder' const intRegex = /^-?\d+$/ @@ -13,7 +13,7 @@ const floatRegex = /^-?(([0-9]+)[.]?|([0-9]*[.][0-9]+))([(][0-9]+[)])?([eE][+-]? // Classify a cif field as str, int or float based the data it contains. // To classify a field as int or float all items are checked. -function classify(name: string, field: Field): FieldDefinition { +function classify(name: string, field: CifField): FieldDefinition { let floatCount = 0, hasString = false; for (let i = 0, _i = field.rowCount; i < _i; i++) { const k = field.valueKind(i); diff --git a/src/apps/render-test/components/assemblies.tsx b/src/apps/render-test/components/assemblies.tsx deleted file mode 100644 index ab0bae997994a229b33d5ac3419e72e484f218f9..0000000000000000000000000000000000000000 --- a/src/apps/render-test/components/assemblies.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author Alexander Rose <alexander.rose@weirdbyte.de> - */ - -import * as React from 'react' -import { WithStyles } from 'material-ui/styles'; -import { MenuItem } from 'material-ui/Menu'; -import { InputLabel } from 'material-ui/Input'; -import { FormControl } from 'material-ui/Form'; -import Select from 'material-ui/Select'; - -import State from '../state' -import Observer from './observer'; -import { Assembly } from 'mol-model/structure/model/properties/symmetry'; - -interface AssemblyState { - loading: boolean - assemblies: ReadonlyArray<Assembly> - value: string -} - -export default class Assemblies extends Observer<{ state: State } & WithStyles, AssemblyState> { - state: AssemblyState = { loading: false, assemblies: [], value: '' } - - componentDidMount() { - this.subscribe(this.props.state.loading, value => { - this.setState({ loading: value }); - }); - this.subscribe(this.props.state.model, value => { - this.setState({ assemblies: value ? value.symmetry.assemblies : [] }); - }); - this.subscribe(this.props.state.assembly, value => { - this.setState({ value }); - }); - } - - handleValueChange = (event: React.ChangeEvent<any>) => { - this.props.state.assembly.next(event.target.value) - } - - render() { - const { classes } = this.props; - - const items = this.state.assemblies.map((value, idx) => { - return <MenuItem key={idx} value={value.id}>{value.details}</MenuItem> - }) - - return <FormControl className={classes.formControl}> - <InputLabel htmlFor='assembly-value'>Assembly</InputLabel> - <Select - className={classes.selectField} - value={this.state.value} - onChange={this.handleValueChange} - inputProps={{ - name: 'value', - id: 'assembly-value', - }} - > - {items} - </Select> - </FormControl> - } -} \ No newline at end of file diff --git a/src/apps/render-test/components/color-theme.tsx b/src/apps/render-test/components/color-theme.tsx deleted file mode 100644 index 701247c5db1bc0e2c0ed79caa486f711b2dc99ac..0000000000000000000000000000000000000000 --- a/src/apps/render-test/components/color-theme.tsx +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author Alexander Rose <alexander.rose@weirdbyte.de> - */ - -import * as React from 'react' -import { WithStyles } from 'material-ui/styles'; -import { MenuItem } from 'material-ui/Menu'; -import { InputLabel } from 'material-ui/Input'; -import { FormControl } from 'material-ui/Form'; -import Select from 'material-ui/Select'; - -import State, { ColorTheme as _ColorTheme } from '../state' -import Observer from './observer'; -import { Color, ColorNames } from 'mol-util/color'; - -interface ColorThemeState { - loading: boolean - name: _ColorTheme - value: Color -} - -export default class ColorTheme extends Observer<{ state: State } & WithStyles, ColorThemeState> { - state: ColorThemeState = { loading: false, name: 'element-symbol' as _ColorTheme, value: 0xFF0000 } - - componentDidMount() { - this.subscribe(this.props.state.loading, value => { - this.setState({ loading: value }); - }); - this.subscribe(this.props.state.colorTheme, value => { - this.setState({ name: value }); - }); - this.subscribe(this.props.state.colorValue, value => { - this.setState({ value: value }); - }); - } - - handleNameChange = (event: React.ChangeEvent<any>) => { - this.props.state.colorTheme.next(event.target.value) - } - - handleValueChange = (event: React.ChangeEvent<any>) => { - this.props.state.colorValue.next(event.target.value) - } - - render() { - const { classes } = this.props; - - const colorThemeItems = Object.keys(_ColorTheme).map((name, idx) => { - return <MenuItem key={idx} value={name}>{name}</MenuItem> - }) - - const colorValueItems = Object.keys(ColorNames).map((name, idx) => { - return <MenuItem key={idx} value={(ColorNames as any)[name]}>{name}</MenuItem> - }) - - return <div> - <FormControl className={classes.formControl}> - <InputLabel htmlFor='color-theme-name'>Color Theme</InputLabel> - <Select - className={classes.selectField} - value={this.state.name} - onChange={this.handleNameChange} - inputProps={{ - name: 'name', - id: 'color-theme-name', - }} - > - {colorThemeItems} - </Select> - </FormControl> - <FormControl className={classes.formControl}> - <InputLabel htmlFor='uniform-color-value'>Color Value</InputLabel> - <Select - className={classes.selectField} - value={this.state.value} - onChange={this.handleValueChange} - inputProps={{ - name: 'value', - id: 'uniform-color-value', - }} - > - {colorValueItems} - </Select> - </FormControl> - </div> - } -} \ No newline at end of file diff --git a/src/apps/render-test/components/file-input.tsx b/src/apps/render-test/components/file-input.tsx deleted file mode 100644 index 850ba452e38fe881a9a0c32dc723589e7e4c5658..0000000000000000000000000000000000000000 --- a/src/apps/render-test/components/file-input.tsx +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author Alexander Rose <alexander.rose@weirdbyte.de> - */ - -import * as React from 'react' -import { WithStyles } from 'material-ui/styles'; -import TextField from 'material-ui/TextField'; -import Button from 'material-ui/Button'; - -import State from '../state' -import Observer from './observer'; - -export default class FileInput extends Observer<{ state: State } & WithStyles, { loading: boolean }> { - state = { loading: false } - - componentDidMount() { - this.subscribe(this.props.state.loading, value => { - this.setState({ loading: value }); - }); - } - - render() { - const { classes, state } = this.props; - - return <div> - <TextField - label='PDB ID' - className={classes.textField} - disabled={this.state.loading} - margin='normal' - onChange={(event) => { - state.pdbId = event.target.value - }} - onKeyPress={(event) => { - if (event.key === 'Enter') state.loadPdbId() - }} - /> - <input - accept='*.cif' - className={classes.input} - id='button-file' - type='file' - onChange={(event) => { - if (event.target.files) { - state.loadFile(event.target.files[0]) - } - }} - /> - <label htmlFor='button-file'> - <Button - variant='raised' - component='span' - className={classes.button} - > - Open CIF - </Button> - </label> - </div> - } -} \ No newline at end of file diff --git a/src/apps/render-test/components/observer.tsx b/src/apps/render-test/components/observer.tsx deleted file mode 100644 index 67e48fb23485513daf57f4b7f57fc9bba76bd26a..0000000000000000000000000000000000000000 --- a/src/apps/render-test/components/observer.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author Alexander Rose <alexander.rose@weirdbyte.de> - */ - -import * as React from 'react' -import { Observable, Subscription } from 'rxjs'; - -export default class Observer<S, P> extends React.Component<S, P> { - private _subs: Subscription[] = [] - - subscribe<T>(obs: Observable<T>, onNext: (v: T) => void) { - this._subs.push(obs.subscribe(onNext)); - } - - componentWillUnmount() { - for (const s of this._subs) s.unsubscribe(); - this._subs = []; - } -} \ No newline at end of file diff --git a/src/apps/render-test/components/sphere-detail.tsx b/src/apps/render-test/components/sphere-detail.tsx deleted file mode 100644 index 1b19cca359238d3b5d4a0e99dfb80c7d4ef03c99..0000000000000000000000000000000000000000 --- a/src/apps/render-test/components/sphere-detail.tsx +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author Alexander Rose <alexander.rose@weirdbyte.de> - */ - -import * as React from 'react' -import { WithStyles } from 'material-ui/styles'; -import { MenuItem } from 'material-ui/Menu'; -import { InputLabel } from 'material-ui/Input'; -import { FormControl } from 'material-ui/Form'; -import Select from 'material-ui/Select'; - -import State from '../state' -import Observer from './observer'; - -interface SphereDetailState { - loading: boolean - value: number -} - -export default class SphereDetail extends Observer<{ state: State } & WithStyles, SphereDetailState> { - state: SphereDetailState = { loading: false, value: 2 } - - componentDidMount() { - this.subscribe(this.props.state.loading, value => { - this.setState({ loading: value }); - }); - this.subscribe(this.props.state.sphereDetail, value => { - this.setState({ value }); - }); - } - - handleValueChange = (event: React.ChangeEvent<any>) => { - this.props.state.sphereDetail.next(event.target.value) - } - - render() { - const { classes } = this.props; - - const items = [0, 1, 2, 3].map((value, idx) => { - return <MenuItem key={idx} value={value}>{value.toString()}</MenuItem> - }) - - return <FormControl className={classes.formControl}> - <InputLabel htmlFor='sphere-detail-value'>Sphere Detail</InputLabel> - <Select - className={classes.selectField} - value={this.state.value} - onChange={this.handleValueChange} - inputProps={{ - name: 'value', - id: 'sphere-detail-value', - }} - > - {items} - </Select> - </FormControl> - } -} \ No newline at end of file diff --git a/src/apps/render-test/components/viewport.tsx b/src/apps/render-test/components/viewport.tsx deleted file mode 100644 index 27b0cd63c8cef15ffb5b1c38733cca8c3371c181..0000000000000000000000000000000000000000 --- a/src/apps/render-test/components/viewport.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author Alexander Rose <alexander.rose@weirdbyte.de> - */ - -import * as React from 'react' -import State from '../state' - -export default class Viewport extends React.Component<{ state: State }, { initialized: boolean }> { - private container: HTMLDivElement | null = null; - private canvas: HTMLCanvasElement | null = null; - state = { initialized: false } - - componentDidMount() { - if (this.container && this.canvas) { - this.props.state.initRenderer(this.canvas, this.container).then(() => { - this.setState({ initialized: true }) - }) - } - } - - render() { - return <div ref={elm => this.container = elm} style={{ height: '100%' }}> - <canvas ref={elm => this.canvas = elm}></canvas> - </div> - } -} \ No newline at end of file diff --git a/src/apps/render-test/components/visibility.tsx b/src/apps/render-test/components/visibility.tsx deleted file mode 100644 index a93a9b123ba729f5488cfb159339f731cfc921c0..0000000000000000000000000000000000000000 --- a/src/apps/render-test/components/visibility.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author Alexander Rose <alexander.rose@weirdbyte.de> - */ - -import * as React from 'react' -import { WithStyles } from 'material-ui/styles'; -import { FormLabel, FormControl, FormGroup, FormControlLabel } from 'material-ui/Form'; -import Checkbox from 'material-ui/Checkbox'; - -import State from '../state' -import Observer from './observer'; - -interface VisibilityState { - loading: boolean - spacefill: boolean - point: boolean -} - -export default class Visibility extends Observer<{ state: State } & WithStyles, VisibilityState> { - state: VisibilityState = { loading: false, spacefill: true, point: true } - - componentDidMount() { - this.subscribe(this.props.state.loading, value => { - this.setState({ loading: value }); - }); - this.subscribe(this.props.state.spacefillVisibility, value => { - this.setState({ spacefill: value }); - }); - this.subscribe(this.props.state.pointVisibility, value => { - this.setState({ point: value }); - }); - } - - handleChange = (event: React.ChangeEvent<any>) => { - switch (event.target.name) { - case 'point': this.props.state.pointVisibility.next(event.target.checked); break; - case 'spacefill': this.props.state.spacefillVisibility.next(event.target.checked); break; - } - } - - render() { - const { classes } = this.props - - return <div className={classes.formControl}> - <FormControl component='fieldset'> - <FormLabel component='legend'>Visibility</FormLabel> - <FormGroup> - <FormControlLabel - control={<Checkbox - checked={this.state.point} - onChange={this.handleChange} - name='point' - />} - label='Point' - /> - <FormControlLabel - control={<Checkbox - checked={this.state.spacefill} - onChange={this.handleChange} - name='spacefill' - />} - label='Spacefill' - /> - </FormGroup> - </FormControl> - </div> - } -} \ No newline at end of file diff --git a/src/apps/render-test/index.tsx b/src/apps/render-test/index.tsx deleted file mode 100644 index 2e87d291faecababed3fca35c7a2799742704ce9..0000000000000000000000000000000000000000 --- a/src/apps/render-test/index.tsx +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author Alexander Rose <alexander.rose@weirdbyte.de> - */ - -import UI from './ui' -import State from './state' -import * as React from 'react' -import * as ReactDOM from 'react-dom' - -const state = new State() -ReactDOM.render(<UI state={ state } />, document.getElementById('app')); \ No newline at end of file diff --git a/src/apps/render-test/state.ts b/src/apps/render-test/state.ts deleted file mode 100644 index 1e284a0a12b47738f2d32ca9dd550e07f9fbdbcd..0000000000000000000000000000000000000000 --- a/src/apps/render-test/state.ts +++ /dev/null @@ -1,226 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author Alexander Rose <alexander.rose@weirdbyte.de> - */ - -import { BehaviorSubject } from 'rxjs'; - -// import { ValueCell } from 'mol-util/value-cell' - -// import { Vec3, Mat4 } from 'mol-math/linear-algebra' -import Viewer from 'mol-view/viewer' -// import { createColorTexture } from 'mol-gl/util'; -// import Icosahedron from 'mol-geo/primitive/icosahedron' -// import Box from 'mol-geo/primitive/box' -import Spacefill, { SpacefillProps } from 'mol-geo/representation/structure/spacefill' -import Point, { PointProps } from 'mol-geo/representation/structure/point' - -import { Run } from 'mol-task' -import { Symmetry, Structure, Model } from 'mol-model/structure' - -// import mcubes from './utils/mcubes' -import { getModelFromPdbId, getModelFromFile, log, Volume, getVolumeFromEmdId } from './utils' -import { StructureRepresentation } from 'mol-geo/representation/structure'; -import { Color } from 'mol-util/color'; -import Surface, { SurfaceProps } from 'mol-geo/representation/volume/surface'; -import { VolumeIsoValue } from 'mol-model/volume'; -import { VolumeRepresentation } from 'mol-geo/representation/volume'; -// import Cylinder from 'mol-geo/primitive/cylinder'; - - -export const ColorTheme = { - 'atom-index': {}, - 'chain-id': {}, - 'element-symbol': {}, - 'instance-index': {}, - 'uniform': {} -} -export type ColorTheme = keyof typeof ColorTheme - -export default class State { - viewer: Viewer - pdbId = '1crn' - // pdbId = '5ire' - emdId = '8116' - // pdbId = '6G1K' - // emdId = '4339' - // pdbId = '4cup' - // emdId = '' - model = new BehaviorSubject<Model | undefined>(undefined) - volume = new BehaviorSubject<Volume | undefined>(undefined) - initialized = new BehaviorSubject<boolean>(false) - loading = new BehaviorSubject<boolean>(false) - - colorTheme = new BehaviorSubject<ColorTheme>('element-symbol') - colorValue = new BehaviorSubject<Color>(0xFF4411) - sphereDetail = new BehaviorSubject<number>(0) - assembly = new BehaviorSubject<string>('') - - pointVisibility = new BehaviorSubject<boolean>(true) - spacefillVisibility = new BehaviorSubject<boolean>(true) - - pointRepr: StructureRepresentation<PointProps> - spacefillRepr: StructureRepresentation<SpacefillProps> - surfaceRepr: VolumeRepresentation<SurfaceProps> - - constructor() { - this.colorTheme.subscribe(() => this.update()) - this.colorValue.subscribe(() => this.update()) - this.sphereDetail.subscribe(() => this.update()) - this.assembly.subscribe(() => this.initStructure()) - - this.pointVisibility.subscribe(() => this.updateVisibility()) - this.spacefillVisibility.subscribe(() => this.updateVisibility()) - } - - getSpacefillProps (): SpacefillProps { - const colorThemeName = this.colorTheme.getValue() - return { - doubleSided: true, - detail: this.sphereDetail.getValue(), - colorTheme: colorThemeName === 'uniform' ? - { name: colorThemeName, value: this.colorValue.getValue() } : - { name: colorThemeName } - } - } - - getPointProps (): PointProps { - const colorThemeName = this.colorTheme.getValue() - return { - sizeTheme: { name: 'uniform', value: 0.1 }, - colorTheme: colorThemeName === 'uniform' ? - { name: colorThemeName, value: this.colorValue.getValue() } : - { name: colorThemeName } - } - } - - async initRenderer (canvas: HTMLCanvasElement, container: HTMLDivElement) { - this.viewer = Viewer.create(canvas, container) - this.initialized.next(true) - this.loadPdbId() - this.loadEmdId() - this.viewer.animate() - } - - async getStructure () { - const model = this.model.getValue() - if (!model) return - const assembly = this.assembly.getValue() - let structure: Structure - const assemblies = model.symmetry.assemblies - if (assemblies.length) { - structure = await Run(Symmetry.buildAssembly(Structure.ofModel(model), assembly || '1'), log, 500) - } else { - structure = Structure.ofModel(model) - } - return structure - } - - async initStructure () { - const { viewer } = this - if (!viewer || !this.model.getValue()) return - - if (this.pointRepr) this.viewer.remove(this.pointRepr) - if (this.spacefillRepr) this.viewer.remove(this.spacefillRepr) - - const structure = await this.getStructure() - if (!structure) return - - this.pointRepr = StructureRepresentation(Point) - await Run(this.pointRepr.create(structure, this.getPointProps()), log, 500) - viewer.add(this.pointRepr) - - this.spacefillRepr = StructureRepresentation(Spacefill) - await Run(this.spacefillRepr.create(structure, this.getSpacefillProps()), log, 500) - viewer.add(this.spacefillRepr) - - this.updateVisibility() - viewer.requestDraw() - console.log(viewer.stats) - } - - setModel(model: Model) { - this.model.next(model) - this.initStructure() - this.loading.next(false) - } - - async loadFile (file: File) { - this.viewer.clear() - this.loading.next(true) - this.setModel((await getModelFromFile(file))[0]) - } - - async initVolume () { - const { viewer } = this - const v = this.volume.getValue() - if (!viewer || !v) return - - if (this.surfaceRepr) this.viewer.remove(this.surfaceRepr) - - this.surfaceRepr = VolumeRepresentation(Surface) - await Run(this.surfaceRepr.create(v.volume, { - isoValue: VolumeIsoValue.relative(v.volume.dataStats, 3.0), - alpha: 0.5, - flatShaded: false, - flipSided: true, - doubleSided: true - }), log, 500) - viewer.add(this.surfaceRepr) - - viewer.requestDraw() - console.log(viewer.stats) - } - - async loadPdbId () { - if (this.pointRepr) this.viewer.remove(this.pointRepr) - if (this.spacefillRepr) this.viewer.remove(this.spacefillRepr) - if (this.pdbId.length !== 4) return - this.loading.next(true) - this.setModel((await getModelFromPdbId(this.pdbId))[0]) - } - - setVolume(volume: Volume) { - this.volume.next(volume) - this.initVolume() - this.loading.next(false) - } - - async loadEmdId () { - if (this.surfaceRepr) this.viewer.remove(this.surfaceRepr) - if (this.emdId.length !== 4) return - this.loading.next(true) - this.setVolume(await getVolumeFromEmdId(this.emdId)) - } - - async update () { - if (!this.spacefillRepr) return - await Run(this.spacefillRepr.update(this.getSpacefillProps()), log, 500) - await Run(this.pointRepr.update(this.getPointProps()), log, 500) - this.viewer.add(this.spacefillRepr) - this.viewer.add(this.pointRepr) - this.viewer.update() - this.viewer.requestDraw() - console.log(this.viewer.stats) - } - - updateVisibility () { - if (!this.viewer) return - if (this.pointRepr) { - if (this.pointVisibility.getValue()) { - this.viewer.show(this.pointRepr) - } else { - this.viewer.hide(this.pointRepr) - } - } - if (this.spacefillRepr) { - if (this.spacefillVisibility.getValue()) { - this.viewer.show(this.spacefillRepr) - } else { - this.viewer.hide(this.spacefillRepr) - } - } - this.viewer.requestDraw() - } -} \ No newline at end of file diff --git a/src/apps/render-test/ui.tsx b/src/apps/render-test/ui.tsx deleted file mode 100644 index b302fe512d682a377e6f054c92e85fd9dcd48433..0000000000000000000000000000000000000000 --- a/src/apps/render-test/ui.tsx +++ /dev/null @@ -1,103 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author Alexander Rose <alexander.rose@weirdbyte.de> - */ - -import * as React from 'react' -import { withStyles, WithStyles, Theme, StyleRulesCallback } from 'material-ui/styles'; -import Typography from 'material-ui/Typography'; -import Toolbar from 'material-ui/Toolbar'; -import AppBar from 'material-ui/AppBar'; -import Drawer from 'material-ui/Drawer'; - -import State from './state' - -import Viewport from './components/viewport' -import FileInput from './components/file-input' -import ColorTheme from './components/color-theme' -import SphereDetail from './components/sphere-detail' -import Visibility from './components/visibility' -import Assemblies from './components/assemblies' - - -const styles: StyleRulesCallback = (theme: Theme) => ({ - root: { - flexGrow: 1, - height: 830, - zIndex: 1, - overflow: 'hidden', - position: 'relative', - display: 'flex', - }, - appBar: { - zIndex: theme.zIndex.drawer + 1, - }, - drawerPaper: { - position: 'relative', - width: 240, - }, - content: { - flexGrow: 1, - backgroundColor: theme.palette.background.default, - padding: theme.spacing.unit * 3, - minWidth: 0, // So the Typography noWrap works - }, - toolbar: theme.mixins.toolbar, - formControl: { - margin: theme.spacing.unit, - width: 200, - }, - textField: { - marginLeft: theme.spacing.unit, - marginRight: theme.spacing.unit, - width: 200, - }, - button: { - margin: theme.spacing.unit, - }, - input: { - display: 'none', - }, -} as any); - -const decorate = withStyles(styles); - -interface Props { - state: State; -}; - -class UI extends React.Component<{ state: State } & WithStyles, { }> { - render() { - const { classes, state } = this.props; - return ( - <div className={classes.root}> - <AppBar position='absolute' className={classes.appBar}> - <Toolbar> - <Typography variant='title' color='inherit' noWrap> - Mol* Render Test - </Typography> - </Toolbar> - </AppBar> - <Drawer variant='permanent' classes={{ paper: classes.drawerPaper, }}> - <div className={classes.toolbar} /> - <FileInput state={state} classes={classes}></FileInput> - <form className={classes.root} autoComplete='off'> - <div> - <Assemblies state={state} classes={classes}></Assemblies> - <ColorTheme state={state} classes={classes}></ColorTheme> - <SphereDetail state={state} classes={classes}></SphereDetail> - <Visibility state={state} classes={classes}></Visibility> - </div> - </form> - </Drawer> - <main className={classes.content}> - <div className={classes.toolbar} /> - <Viewport state={state}></Viewport> - </main> - </div> - ); - } -} - -export default decorate<Props>(UI) \ No newline at end of file diff --git a/src/apps/render-test/utils/index.ts b/src/apps/render-test/utils/index.ts deleted file mode 100644 index 54486ffb3323d4808327e4ed013c332e38ed04de..0000000000000000000000000000000000000000 --- a/src/apps/render-test/utils/index.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author Alexander Rose <alexander.rose@weirdbyte.de> - */ - -import CIF from 'mol-io/reader/cif' -import { Run, Progress } from 'mol-task' -import { Model } from 'mol-model/structure' -import { VolumeData, parseDensityServerData } from 'mol-model/volume' -import { DensityServer_Data_Database } from 'mol-io/reader/cif/schema/density-server'; - -export function log(progress: Progress) { - const p = progress.root.progress - console.log(`${p.message} ${(p.current/p.max*100).toFixed(2)}%`) -} - -export async function downloadCif(url: string, isBinary: boolean) { - const data = await fetch(url); - return parseCif(isBinary ? new Uint8Array(await data.arrayBuffer()) : await data.text()); -} - -export async function parseCif(data: string|Uint8Array) { - const comp = CIF.parse(data) - const parsed = await Run(comp, log, 500); - if (parsed.isError) throw parsed; - return parsed.result -} - -export async function getModelFromPdbId(pdbid: string) { - const cif = await downloadCif(`https://files.rcsb.org/download/${pdbid}.cif`, false) - return Model.create({ kind: 'mmCIF', data: CIF.schema.mmCIF(cif.blocks[0]) }) -} - -const readFileAsText = (file: File) => { - const fileReader = new FileReader() - return new Promise<string>((resolve, reject) => { - fileReader.onerror = () => { - fileReader.abort() - reject(new DOMException('Error parsing input file.')) - } - fileReader.onload = () => resolve(fileReader.result) - fileReader.readAsText(file) - }) -} - -export async function getModelFromFile(file: File) { - const cif = await parseCif(await readFileAsText(file)) - return Model.create({ kind: 'mmCIF', data: CIF.schema.mmCIF(cif.blocks[0]) }) -} - -export type Volume = { source: DensityServer_Data_Database, volume: VolumeData } - -export async function getVolumeFromEmdId(emdid: string): Promise<Volume> { - const cif = await downloadCif(`https://webchem.ncbr.muni.cz/DensityServer/em/emd-${emdid}/cell?detail=4`, true) - const data = CIF.schema.densityServer(cif.blocks[1]) - return { source: data, volume: await Run(parseDensityServerData(data)) } -} \ No newline at end of file diff --git a/src/apps/render-test/utils/mcubes.ts b/src/apps/render-test/utils/mcubes.ts deleted file mode 100644 index 92905c7b6f5d111486ee7a1bd6fab7b95a5cd5e6..0000000000000000000000000000000000000000 --- a/src/apps/render-test/utils/mcubes.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author David Sehnal <david.sehnal@gmail.com> - */ - -import { Run } from 'mol-task' -import { computeMarchingCubes } from 'mol-geo/util/marching-cubes/algorithm' -import { Mesh } from 'mol-geo/shape/mesh' -import { Tensor, Mat4, Vec3 } from 'mol-math/linear-algebra' - -function fillField(tensor: Tensor, f: (x: number, y: number, z: number) => number, min: number[], max: number[]): Tensor { - const { space: { set, dimensions: [ii, jj, kk] }, data } = tensor; - - const dx = (max[0] - min[0]) / (ii - 1); - const dy = (max[1] - min[1]) / (jj - 1); - const dz = (max[2] - min[2]) / (kk - 1); - - for (let i = 0, x = min[0]; i < ii; i++, x += dx) { - for (let j = 0, y = min[1]; j < jj; j++, y += dy) { - for (let k = 0, z = min[2]; k < kk; k++, z += dz) { - set(data, i, j, k, f(x, y, z)); - } - } - } - - return tensor -} - -export default async function computeSurface(f: (x: number, y: number, z: number) => number, data?: { field: Tensor, surface: Mesh }) { - let field: Tensor; - if (data) field = data.field; - else { - const space = Tensor.Space([30, 30, 30], [0, 1, 2]); - field = Tensor.create(space, space.create(Float32Array)); - } - - const min = Vec3.create(-1.1, -1.1, -1.1), max = Vec3.create(1.1, 1.1, 1.1); - - fillField(field, f, min, max); - const surface = await Run(computeMarchingCubes({ - scalarField: field, - isoLevel: 0, - oldSurface: data ? data.surface : void 0 - })); - - const translation = Mat4.fromTranslation(Mat4.zero(), min); - const grid = Vec3.zero(); - Vec3.fromArray(grid, field.space.dimensions as any, 0); - const size = Vec3.sub(Vec3.zero(), max, min); - const scale = Mat4.fromScaling(Mat4.zero(), Vec3.create(size[0] / (grid[0] - 1), size[1] / (grid[1] - 1), size[2] / (grid[2] - 1))); - - const transform = Mat4.mul(Mat4.zero(), translation, scale); - Mesh.transformImmediate(surface, transform); - Mesh.computeNormalsImmediate(surface); - return { surface, field }; -} \ No newline at end of file diff --git a/src/apps/schema-generator/schema-from-mmcif-dic.ts b/src/apps/schema-generator/schema-from-mmcif-dic.ts index a6fc5fed2b9cc9a0a022055f77ded38923d5e82e..eb1d7e74ba4a336e5fc70860a753d1bf8a4ec78e 100644 --- a/src/apps/schema-generator/schema-from-mmcif-dic.ts +++ b/src/apps/schema-generator/schema-from-mmcif-dic.ts @@ -10,7 +10,7 @@ import * as fs from 'fs' import fetch from 'node-fetch' import Csv from 'mol-io/reader/csv/parser' -import CIF, { Frame } from 'mol-io/reader/cif' +import CIF, { CifFrame } from 'mol-io/reader/cif' import { generateSchema } from './util/cif-dic' import { generate } from './util/generate' import { Filter } from './util/json-schema' @@ -29,7 +29,7 @@ async function runGenerateSchema(name: string, fieldNamesPath?: string, typescri const ihmDicVersion = CIF.schema.dic(ihmDic.result.blocks[0]).dictionary.version.value(0) const version = `Dictionary versions: mmCIF ${mmcifDicVersion}, IHM ${ihmDicVersion}.` - const frames: Frame[] = [...mmcifDic.result.blocks[0].saveFrames, ...ihmDic.result.blocks[0].saveFrames] + const frames: CifFrame[] = [...mmcifDic.result.blocks[0].saveFrames, ...ihmDic.result.blocks[0].saveFrames] const schema = generateSchema(frames) const filter = fieldNamesPath ? await getFieldNamesFilter(fieldNamesPath) : undefined diff --git a/src/apps/schema-generator/util/cif-dic.ts b/src/apps/schema-generator/util/cif-dic.ts index 5cc6016cfba541708448d83d1881846ee659579c..718c9348394ba52907534642303be0382640ad1e 100644 --- a/src/apps/schema-generator/util/cif-dic.ts +++ b/src/apps/schema-generator/util/cif-dic.ts @@ -6,7 +6,7 @@ import { Database, ValueColumn, ListColumn } from './json-schema' import * as Data from 'mol-io/reader/cif/data-model' -import { Frame } from 'mol-io/reader/cif/data-model'; +import { CifFrame } from 'mol-io/reader/cif/data-model'; export function getFieldType (type: string, values?: string[]): ValueColumn|ListColumn { switch (type) { @@ -76,7 +76,7 @@ export function getFieldType (type: string, values?: string[]): ValueColumn|List return 'str' } -type FrameCategories = { [category: string]: Data.Frame } +type FrameCategories = { [category: string]: Data.CifFrame } type FrameLinks = { [k: string]: string } interface FrameData { @@ -85,7 +85,7 @@ interface FrameData { } // get field from given or linked category -function getField ( category: string, field: string, d: Data.Frame, ctx: FrameData): Data.Field|undefined { +function getField ( category: string, field: string, d: Data.CifFrame, ctx: FrameData): Data.CifField|undefined { const { categories, links } = ctx const cat = d.categories[category] @@ -105,7 +105,7 @@ function getField ( category: string, field: string, d: Data.Frame, ctx: FrameDa } } -function getEnums (d: Data.Frame, ctx: FrameData) { +function getEnums (d: Data.CifFrame, ctx: FrameData) { const value = getField('item_enumeration', 'value', d, ctx) const enums: string[] = [] if (value) { @@ -119,7 +119,7 @@ function getEnums (d: Data.Frame, ctx: FrameData) { } } -function getCode (d: Data.Frame, ctx: FrameData): [string, string[]|undefined]|undefined { +function getCode (d: Data.CifFrame, ctx: FrameData): [string, string[]|undefined]|undefined { const code = getField('item_type', 'code', d, ctx) if (code) { return [ code.str(0), getEnums(d, ctx) ] @@ -128,7 +128,7 @@ function getCode (d: Data.Frame, ctx: FrameData): [string, string[]|undefined]|u } } -function getSubCategory (d: Data.Frame, ctx: FrameData): string|undefined { +function getSubCategory (d: Data.CifFrame, ctx: FrameData): string|undefined { const value = getField('item_sub_category', 'id', d, ctx) if (value) { return value.str(0) @@ -178,7 +178,7 @@ const SPACE_SEPARATED_LIST_FIELDS = [ '_pdbx_soln_scatter.data_analysis_software_list', // SCTPL5 GNOM ]; -export function generateSchema (frames: Frame[]) { +export function generateSchema (frames: CifFrame[]) { const schema: Database = {} const categories: FrameCategories = {} diff --git a/src/apps/viewer/index.html b/src/apps/viewer/index.html new file mode 100644 index 0000000000000000000000000000000000000000..f3faa04f95f9e915ba493d25fdc471b69489e913 --- /dev/null +++ b/src/apps/viewer/index.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"> + <title>Mol* Render Test</title> + <link href='./app.css', rel="stylesheet"> + </head> + <body> + <div id="app"></div> + <script type="text/javascript" src="./index.js"></script> + </body> +</html> \ No newline at end of file diff --git a/src/apps/viewer/index.tsx b/src/apps/viewer/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f828e597af544499d74c9c85afc8dea6e4bd7fd7 --- /dev/null +++ b/src/apps/viewer/index.tsx @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import * as React from 'react' +import * as ReactDOM from 'react-dom' + +import './index.html' +import 'mol-app/skin/molstar-light.scss' + +import { Context } from 'mol-app/context/context'; +import { Viewport } from 'mol-app/ui/visualization/viewport' +import { makeEmptyTargets, LayoutRegion } from 'mol-app/controller/layout'; +import { Layout } from 'mol-app/ui/layout'; +import { LogController } from 'mol-app/controller/misc/log'; +import { Log } from 'mol-app/ui/misc/log'; +import { JobsController } from 'mol-app/controller/misc/jobs'; +import { BackgroundJobs, Overlay } from 'mol-app/ui/misc/jobs'; +import { EntityTree } from 'mol-app/ui/entity/tree'; +import { EntityTreeController } from 'mol-app/controller/entity/tree'; +import { TransformListController } from 'mol-app/controller/transform/list'; +import { TransformList } from 'mol-app/ui/transform/list'; + +const elm = document.getElementById('app') +if (!elm) throw new Error('Can not find element with id "app".') + +const ctx = new Context() +const targets = makeEmptyTargets(); + +targets[LayoutRegion.Main].components.push({ + key: 'molstar-internal-viewport', + controller: ctx.viewport, + region: LayoutRegion.Main, + view: Viewport, + isStatic: true +}); + +targets[LayoutRegion.Bottom].components.push({ + key: 'molstar-log', + controller: new LogController(ctx), + region: LayoutRegion.Bottom, + view: Log, + isStatic: true +}); + +targets[LayoutRegion.Main].components.push({ + key: 'molstar-background-jobs', + controller: new JobsController(ctx, 'Background'), + region: LayoutRegion.Main, + view: BackgroundJobs, + isStatic: true +}); + +targets[LayoutRegion.Root].components.push({ + key: 'molstar-overlay', + controller: new JobsController(ctx, 'Normal'), + region: LayoutRegion.Root, + view: Overlay, + isStatic: true +}); + +targets[LayoutRegion.Right].components.push({ + key: 'molstar-transform-list', + controller: new TransformListController(ctx), + region: LayoutRegion.Right, + view: TransformList, + isStatic: false +}); + +targets[LayoutRegion.Left].components.push({ + key: 'molstar-entity-tree', + controller: new EntityTreeController(ctx), + region: LayoutRegion.Left, + view: EntityTree, + isStatic: true +}); + +ctx.createLayout(targets, elm) +ctx.layout.setState({ + isExpanded: true, + hideControls: false, + collapsedControlsLayout: 0 +}) +// ctx.viewport.setState() + +ReactDOM.render(React.createElement(Layout, { controller: ctx.layout }), elm); diff --git a/src/mol-app/context/context.ts b/src/mol-app/context/context.ts new file mode 100644 index 0000000000000000000000000000000000000000..92a317aad2dc583dcf9f1f9051a6295c26536596 --- /dev/null +++ b/src/mol-app/context/context.ts @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * Adapted from LiteMol + * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info. + */ + +import { UUID } from 'mol-util' +import { PerformanceMonitor } from 'mol-util/performance-monitor'; +import { Dispatcher } from '../service/dispatcher' +import { Logger } from '../service/logger' +import { LayoutTarget, LayoutController } from '../controller/layout'; +import { ViewportController } from '../controller/visualization/viewport'; +import { Stage } from 'mol-view/stage'; +import { AnyTransform } from 'mol-view/state/transform'; +import { BehaviorSubject } from 'rxjs'; +import { AnyEntity } from 'mol-view/state/entity'; + +export class Settings { + private settings = new Map<string, any>(); + + set(key: string, value: any) { + this.settings.set(key, value); + } + + get(key: string) { + return this.settings.get(key); + } +} + +export class Context { + id = UUID.create() + + dispatcher = new Dispatcher(); + logger = new Logger(this); + performance = new PerformanceMonitor(); + + stage = new Stage(); + viewport = new ViewportController(this); + layout: LayoutController; + settings = new Settings(); + + currentEntity = new BehaviorSubject(undefined) as BehaviorSubject<AnyEntity | undefined> + currentTransforms = new BehaviorSubject([] as AnyTransform[]) + + createLayout(targets: LayoutTarget[], target: HTMLElement) { + this.layout = new LayoutController(this, targets, target); + } + + initStage(canvas: HTMLCanvasElement, container: HTMLDivElement) { + this.stage.initRenderer(canvas, container) + return true + } + + destroy() { + if (this.stage) { + this.stage.dispose() + this.stage = null as any + } + } +} \ No newline at end of file diff --git a/src/mol-app/controller/controller.ts b/src/mol-app/controller/controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..bfd6127f6dd7efbb395557f071be100bb4c3b68b --- /dev/null +++ b/src/mol-app/controller/controller.ts @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * Adapted from LiteMol + * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info. + */ + +import { BehaviorSubject } from 'rxjs'; +import { merge } from 'mol-util'; +import { Context } from '../context/context' +import { LayoutRegion } from './layout'; + +export class Controller<State> { + + private _state = new BehaviorSubject<State>(<any>void 0); + private _latestState: State = <any>void 0; + + get dispatcher() { + return this.context.dispatcher; + } + + setState(...states: Partial<State>[]) { + let s = merge(this._latestState, ...states); + if (s !== this._latestState) { + this._latestState = s; + this._state.next(s); + } + } + + get state() { + return this._state; + } + + get latestState() { + return this._latestState; + } + + constructor(public context: Context, initialState: State) { + this._latestState = initialState; + } +} + +export interface ControllerInfo { + key: string; + controller: Controller<any>; + view: any; + region: LayoutRegion; + isStatic?: boolean; +} \ No newline at end of file diff --git a/src/mol-app/controller/entity/tree.ts b/src/mol-app/controller/entity/tree.ts new file mode 100644 index 0000000000000000000000000000000000000000..fdcb620e550079976e69b6595d58ac55c56ebe28 --- /dev/null +++ b/src/mol-app/controller/entity/tree.ts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * Adapted from LiteMol + * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info. + */ + +import { Context } from '../../context/context' +import { Controller } from '../controller'; +import { AnyEntity } from 'mol-view/state/entity'; + +export interface EntityTreeState { + entities: Set<AnyEntity> +} + +export class EntityTreeController extends Controller<EntityTreeState> { + constructor(context: Context) { + super(context, { entities: new Set() }); + + context.stage.ctx.change.subscribe(() => { + if (context.stage.ctx) { + this.state.next({ entities: context.stage.ctx.entities }) // TODO + this.setState({ entities: context.stage.ctx.entities }) + } + }) + } +} \ No newline at end of file diff --git a/src/mol-app/controller/layout.ts b/src/mol-app/controller/layout.ts new file mode 100644 index 0000000000000000000000000000000000000000..388f57788aecd7465ca1d66a40bae2032b2d120b --- /dev/null +++ b/src/mol-app/controller/layout.ts @@ -0,0 +1,216 @@ +/* + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * Adapted from LiteMol + * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info. + */ + +import { Context } from '../context/context' +import { Controller, ControllerInfo } from './controller' +import { CommonEvents, LayoutEvents } from '../event/basic'; + +export enum LayoutRegion { + Main = 0, + Top = 1, + Right = 2, + Bottom = 3, + Left = 4, + Root = 5 +} + +export enum CollapsedControlsLayout { + Outside = 0, + Landscape = 1, + Portrait = 2 +} + +export class LayoutTarget { + components: ControllerInfo[] = []; + constructor(public cssClass: string) { + } +} + +export function makeEmptyTargets() { + let ret: LayoutTarget[] = []; + for (let i = 0; i <= LayoutRegion.Root; i++) { + ret.push(new LayoutTarget(LayoutRegion[i].toLowerCase())); + } + return ret; +} + +export type RegionState = 'Hidden' | 'Sticky' | 'Default' + +export interface LayoutState { + isExpanded: boolean, + hideControls: boolean, + collapsedControlsLayout: CollapsedControlsLayout, + regionStates?: { [region: number]: RegionState } +} + +interface RootState { + top: string | null, + bottom: string | null, + left: string | null, + right: string | null, + + width: string | null; + height: string | null; + maxWidth: string | null; + maxHeight: string | null; + margin: string | null; + marginLeft: string | null; + marginRight: string | null; + marginTop: string | null; + marginBottom: string | null; + + scrollTop: number, + scrollLeft: number, + position: string | null, + overflow: string | null, + viewports: HTMLElement[], + zindex: string | null +} + +export class LayoutController extends Controller<LayoutState> { + + update(state: Partial<LayoutState>) { + let prevExpanded = !!this.latestState.isExpanded; + this.setState(state); + if (typeof state.isExpanded === 'boolean' && state.isExpanded !== prevExpanded) this.handleExpand(); + + this.dispatcher.schedule(() => CommonEvents.LayoutChanged.dispatch(this.context, {})); + } + + private rootState: RootState | undefined = void 0; + private expandedViewport: HTMLMetaElement; + + private getScrollElement() { + if ((document as any).scrollingElement) return (document as any).scrollingElement; + if (document.documentElement) return document.documentElement; + return document.body; + } + + private handleExpand() { + try { + let body = document.getElementsByTagName('body')[0]; + let head = document.getElementsByTagName('head')[0]; + + if (!body || !head) return; + + if (this.latestState.isExpanded) { + + let children = head.children; + let hasExp = false; + let viewports: HTMLElement[] = []; + for (let i = 0; i < children.length; i++) { + if (children[i] === this.expandedViewport) { + hasExp = true; + } else if (((children[i] as any).name || '').toLowerCase() === 'viewport') { + viewports.push(children[i] as any); + } + } + + for (let v of viewports) { + head.removeChild(v); + } + + if (!hasExp) head.appendChild(this.expandedViewport); + + + let s = body.style; + + let doc = this.getScrollElement(); + let scrollLeft = doc.scrollLeft; + let scrollTop = doc.scrollTop; + + this.rootState = { + top: s.top, bottom: s.bottom, right: s.right, left: s.left, scrollTop, scrollLeft, position: s.position, overflow: s.overflow, viewports, zindex: this.root.style.zIndex, + width: s.width, height: s.height, + maxWidth: s.maxWidth, maxHeight: s.maxHeight, + margin: s.margin, marginLeft: s.marginLeft, marginRight: s.marginRight, marginTop: s.marginTop, marginBottom: s.marginBottom + }; + + s.overflow = 'hidden'; + s.position = 'fixed'; + s.top = '0'; + s.bottom = '0'; + s.right = '0'; + s.left = '0'; + + s.width = '100%'; + s.height = '100%'; + s.maxWidth = '100%'; + s.maxHeight = '100%'; + s.margin = '0'; + s.marginLeft = '0'; + s.marginRight = '0'; + s.marginTop = '0'; + s.marginBottom = '0'; + + this.root.style.zIndex = '100000'; + } else { + // root.style.overflow = rootOverflow; + let children = head.children; + for (let i = 0; i < children.length; i++) { + if (children[i] === this.expandedViewport) { + head.removeChild(this.expandedViewport); + break; + } + } + + if (this.rootState) { + let s = body.style, t = this.rootState; + for (let v of t.viewports) { + head.appendChild(v); + } + s.top = t.top; + s.bottom = t.bottom; + s.left = t.left; + s.right = t.right; + + s.width = t.width; + s.height = t.height; + s.maxWidth = t.maxWidth; + s.maxHeight = t.maxHeight; + s.margin = t.margin; + s.marginLeft = t.marginLeft; + s.marginRight = t.marginRight; + s.marginTop = t.marginTop; + s.marginBottom = t.marginBottom; + + s.position = t.position; + s.overflow = t.overflow; + let doc = this.getScrollElement(); + doc.scrollTop = t.scrollTop; + doc.scrollLeft = t.scrollLeft; + this.rootState = void 0; + this.root.style.zIndex = t.zindex; + } + } + } catch (e) { + this.context.logger.error('Layout change error, you might have to reload the page.'); + console.log('Layout change error, you might have to reload the page.', e); + } + } + + updateTargets(targets: LayoutTarget[]) { + this.targets = targets; + this.dispatcher.schedule(() => CommonEvents.ComponentsChanged.dispatch(this.context, {})); + } + + constructor(context: Context, public targets: LayoutTarget[], private root: HTMLElement) { + super(context, { + isExpanded: false, + hideControls: false, + collapsedControlsLayout: CollapsedControlsLayout.Outside, + regionStates: { } + }); + + LayoutEvents.SetState.getStream(this.context).subscribe(e => this.update(e.data)); + + // <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" /> + this.expandedViewport = document.createElement('meta') as any; + this.expandedViewport.name = 'viewport'; + this.expandedViewport.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0'; + } +} \ No newline at end of file diff --git a/src/mol-app/controller/misc/jobs.ts b/src/mol-app/controller/misc/jobs.ts new file mode 100644 index 0000000000000000000000000000000000000000..075c32e0ffb926fa471b9ff2b7050851de8f506a --- /dev/null +++ b/src/mol-app/controller/misc/jobs.ts @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * Adapted from LiteMol + * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info. + */ + +import { Map } from 'immutable' +import { filter } from 'rxjs/operators'; + +import { Controller } from '../controller' +import { JobEvents } from '../../event/basic'; +import { Context } from '../../context/context'; +import { Job } from '../../service/job'; + + +export interface JobInfo { + name: string; + message: string; + abort?: () => void +} + +export interface JobsState { + jobs: Map<number, JobInfo> +} + +export class JobsController extends Controller<JobsState> { + private updated(state: Job.State) { + let isWatched = state.type === this.type; + let jobs = this.latestState.jobs!; + + if (!isWatched) { + if (jobs.has(state.jobId)) { + jobs = jobs.delete(state.jobId); + this.setState({ jobs }); + } + return; + } + + jobs = jobs.set(state.jobId, { + name: state.name, + message: state.message, + abort: state.abort + }); + this.setState({ jobs }); + } + + private started(job: Job.Info) { + this.setState({ + jobs: this.latestState.jobs!.set(job.id, { name: job.name, message: 'Running...' }) + }); + } + + private completed(taskId: number) { + if (!this.latestState.jobs!.has(taskId)) return; + + this.setState({ + jobs: this.latestState.jobs!.delete(taskId) + }); + } + + constructor(context: Context, private type: Job.Type) { + super(context, { + jobs: Map<number, JobInfo>() + }); + + JobEvents.StateUpdated.getStream(this.context) + .subscribe(e => this.updated(e.data)); + + JobEvents.Started.getStream(this.context).pipe( + filter(e => e.data.type === type)) + .subscribe(e => this.started(e.data)); + + JobEvents.Completed.getStream(this.context) + .subscribe(e => this.completed(e.data)); + } +} \ No newline at end of file diff --git a/src/mol-app/controller/misc/log.ts b/src/mol-app/controller/misc/log.ts new file mode 100644 index 0000000000000000000000000000000000000000..c694612ec490bce090543c0ba11c24d72d3a3226 --- /dev/null +++ b/src/mol-app/controller/misc/log.ts @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * Adapted from LiteMol + * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info. + */ + +import { List } from 'immutable' + +import { Controller } from '../controller' +import { Context } from '../../context/context'; +import { LogEvent } from '../../event/basic'; +import { Logger } from '../../service/logger'; + +export class LogController extends Controller<{ entries: List<Logger.Entry> }> { + constructor(context: Context) { + super(context, { entries: List<Logger.Entry>() }); + + LogEvent.getStream(this.context) + .subscribe(e => this.setState({ entries: this.latestState.entries.push(e.data) })) + } +} \ No newline at end of file diff --git a/src/mol-app/controller/transform/list.ts b/src/mol-app/controller/transform/list.ts new file mode 100644 index 0000000000000000000000000000000000000000..75ee2b5c23d3b0553e87af5582cb9079bfee2484 --- /dev/null +++ b/src/mol-app/controller/transform/list.ts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * Adapted from LiteMol + * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info. + */ + +import { Context } from '../../context/context' +import { Controller } from '../controller'; +import { AnyTransform } from 'mol-view/state/transform'; +import { AnyEntity } from 'mol-view/state/entity'; + +export interface TransformListState { + entity?: AnyEntity + transforms: AnyTransform[] +} + +export class TransformListController extends Controller<TransformListState> { + constructor(context: Context) { + super(context, { transforms: [], entity: undefined }); + + context.currentTransforms.subscribe((transforms) => { + this.state.next({ transforms, entity: context.currentEntity.getValue() }) // TODO + this.setState({ transforms, entity: context.currentEntity.getValue() }) + }) + } +} \ No newline at end of file diff --git a/src/mol-app/controller/visualization/viewport.ts b/src/mol-app/controller/visualization/viewport.ts new file mode 100644 index 0000000000000000000000000000000000000000..40a4ac53a3726f7cdf4a22a6867ab4b1fd6d8404 --- /dev/null +++ b/src/mol-app/controller/visualization/viewport.ts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * Adapted from LiteMol + * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info. + */ + +// import { throttle } from 'rxjs/operators'; +// import { interval } from 'rxjs'; + +import { shallowClone } from 'mol-util'; +import { Context } from '../../context/context' +import { Controller } from '../controller'; + +export const DefaultViewportOptions = { + clearColor: { r: 1, g: 1, b: 1 }, + enableFog: true, + cameraFOV: 30, + cameraSpeed: 4 +} +export type ViewportOptions = typeof DefaultViewportOptions + +export class ViewportController extends Controller<ViewportOptions> { + constructor(context: Context) { + super(context, shallowClone(DefaultViewportOptions)); + } +} \ No newline at end of file diff --git a/src/mol-app/event/basic.ts b/src/mol-app/event/basic.ts new file mode 100644 index 0000000000000000000000000000000000000000..99cf366f9c83e66e05af729e3224d74ec7f252de --- /dev/null +++ b/src/mol-app/event/basic.ts @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * Adapted from LiteMol + * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info. + */ + +import { Event } from './event' +import { Logger } from '../service/logger'; +import { Dispatcher } from '../service/dispatcher' +import { LayoutState } from '../controller/layout'; +import { ViewportOptions } from '../controller/visualization/viewport'; +import { Job } from '../service/job'; + +const Lane = Dispatcher.Lane; + +export const LogEvent = Event.create<Logger.Entry>('bs.Log', Lane.Log); + +export namespace CommonEvents { + export const LayoutChanged = Event.create('bs.Common.LayoutChanged', Lane.Slow); + export const ComponentsChanged = Event.create('bs.Common.ComponentsChanged', Lane.Slow); +} + +export namespace JobEvents { + export const Started = Event.create<Job.Info>('bs.Jobs.Started', Lane.Job); + export const Completed = Event.create<number>('bs.Jobs.Completed', Lane.Job); + export const StateUpdated = Event.create<Job.State>('bs.Jobs.StateUpdated', Lane.Busy); +} + +export namespace LayoutEvents { + export const SetState = Event.create<Partial<LayoutState>>('lm.cmd.Layout.SetState', Lane.Slow); + export const SetViewportOptions = Event.create<ViewportOptions>('bs.cmd.Layout.SetViewportOptions', Lane.Slow); +} diff --git a/src/mol-app/event/event.ts b/src/mol-app/event/event.ts new file mode 100644 index 0000000000000000000000000000000000000000..4e6affecc6620074f229179e54ee8c12624846f6 --- /dev/null +++ b/src/mol-app/event/event.ts @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * Adapted from LiteMol + * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info. + */ + +import { Observable } from 'rxjs'; +import { Context } from '../context/context' +import { Dispatcher } from '../service/dispatcher' + +export interface Event<T> { + type: Event.Type<T>; + data: T; +} + +export namespace Event { + export type Stream<T> = Observable<Event<T>>; + + import Lane = Dispatcher.Lane + + export type Any = Event<any> + export type AnyType = Type<any> + + export interface Type<T> { + name: string, + lane: Lane, + dispatch(context: Context, data: T): void; + getStream(context: Context): Stream<T>; + } + + const EventPrototype = { + dispatch<T>(this: any, context: Context, data: T) { context.dispatcher.dispatch({ type: this, data }) }, + getStream(this: any, context: Context) { return context.dispatcher.getStream(this); } + } + + export function create<T>(name: string, lane: Dispatcher.Lane): Type<T> { + return Object.create(EventPrototype, { + name: { writable: false, configurable: false, value: name }, + lane: { writable: false, configurable: false, value: lane } + }); + } +} \ No newline at end of file diff --git a/src/mol-app/service/dispatcher.ts b/src/mol-app/service/dispatcher.ts new file mode 100644 index 0000000000000000000000000000000000000000..58a6345c601a7dfc16cbc6d162c814235efdda2f --- /dev/null +++ b/src/mol-app/service/dispatcher.ts @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * Adapted from LiteMol + * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info. + */ + +import { Subject } from 'rxjs'; +import { filter } from 'rxjs/operators'; +import { Event } from '../event/event' + +export class Dispatcher { + LOG_DISPATCH_STREAM = false; + + private lanes: Subject<Event<any>>[] = []; + constructor() { + for (let i = 0; i <= Dispatcher.Lane.Job; i++) { + this.lanes.push(new Subject<Event<any>>()); + } + } + + dispatch<T>(event: Event<T>) { + if (this.LOG_DISPATCH_STREAM) console.log(event.type.name, Dispatcher.Lane[event.type.lane], event.data); + this.lanes[event.type.lane].next(event); + } + + schedule(action: () => void, onError?: (e: string) => void, timeout = 1000 / 31) { + return setTimeout(() => { + if (onError) { + try { + action.call(null) + } catch (e) { + onError.call(null, '' + e); + } + } else { + action.call(null); + } + }, timeout); + } + + getStream<T>(type: Event.Type<T>): Event.Stream<T> { + return this.lanes[type.lane].pipe(filter(e => e.type === type)); + } + + finished() { + this.lanes.forEach(l => l.complete()); + } +} + +export namespace Dispatcher { + export enum Lane { + Slow = 0, + Fast = 1, + Log = 2, + Busy = 3, + Transformer = 4, + Job = 5 + } +} \ No newline at end of file diff --git a/src/mol-app/service/job.ts b/src/mol-app/service/job.ts new file mode 100644 index 0000000000000000000000000000000000000000..ed2ec6a5da0a65538d2890c587d706f12e901687 --- /dev/null +++ b/src/mol-app/service/job.ts @@ -0,0 +1,132 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * Adapted from LiteMol + * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info. + */ + +import { Context } from '../context/context' +import { JobEvents } from '../event/basic'; +import { PerformanceMonitor } from 'mol-util/performance-monitor'; +import { formatProgress } from 'mol-util'; +import { Progress, Task, Run } from 'mol-task'; + +export class Job<T> { + private info: Job.Info; + get id() { return this.info.id; } + get reportTime() { return this.info.reportTime; } + + run(context: Context) { + return this.runWithContext(context).result; + } + + runWithContext(context: Context): Job.Running<T> { + return new Job.Running(context, this.task, this.info); + } + + setReportTime(report: boolean) { + this.info.reportTime = report; + return this; + } + + constructor(public name: string, public type: Job.Type, private task: Task<T>) { + this.info = { + id: serialJobId++, + name, + type, + reportTime: false + }; + } +} + +let serialJobId = 0; +export namespace Job { + export let __DEBUG_MODE__ = false; + + export type Type = 'Normal' | 'Background' | 'Silent'; + + export interface Info { + id: number, + type: Type, + name: string, + reportTime: boolean + } + + export class Running<T> { + result: Promise<T>; + + private progressUpdated(progress: Progress) { + JobEvents.StateUpdated.dispatch(this.context, { + jobId: this.info.id, + type: this.info.type, + name: this.info.name, + message: formatProgress(progress), + abort: progress.requestAbort + }); + } + + private resolved() { + try { + this.context.performance.end('job' + this.info.id); + if (this.info.reportTime) { + let time = this.context.performance.time('job' + this.info.id); + if (this.info.type !== 'Silent') this.context.logger.info(`${this.info.name} finished in ${PerformanceMonitor.format(time)}.`) + } + } finally { + JobEvents.Completed.dispatch(this.context, this.info.id); + } + } + + private rejected(err: any) { + this.context.performance.end('job' + this.info.id); + this.context.performance.formatTime('job' + this.info.id); + + if (__DEBUG_MODE__) { + console.error(err); + } + + try { + if (this.info.type === 'Silent') { + if (err.warn) this.context.logger.warning(`Warning (${this.info.name}): ${err.message}`); + else console.error(`Error (${this.info.name})`, err); + } else { + if (err.warn) { + this.context.logger.warning(`Warning (${this.info.name}): ${err.message}`); + } else { + let e = '' + err; + if (e.indexOf('Aborted') >= 0) this.context.logger.info(`${this.info.name}: Aborted.`); + else this.context.logger.error(`Error (${this.info.name}): ${err}`); + } + } + } catch (e) { + console.error(e); + } finally { + JobEvents.Completed.dispatch(this.context, this.info.id); + } + } + + private run() { + JobEvents.Started.dispatch(this.context, this.info); + this.context.performance.start('job' + this.info.id); + + this.result = Run(this.task, (p: Progress) => this.progressUpdated(p), 250) + this.result.then(() => this.resolved()).catch(e => this.rejected(e)); + } + + constructor(private context: Context, private task: Task<T>, private info: Info) { + this.run(); + } + } + + export interface State { + jobId: number, + type: Type, + name: string, + message: string, + abort?: () => void + } + + export function create<T>(name: string, type: Type, task: Task<T>) { + return new Job<T>(name, type, task); + } +} \ No newline at end of file diff --git a/src/mol-app/service/logger.ts b/src/mol-app/service/logger.ts new file mode 100644 index 0000000000000000000000000000000000000000..901596719a035024e00f5a3c117ba4dc4e135ed2 --- /dev/null +++ b/src/mol-app/service/logger.ts @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * Adapted from LiteMol + * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info. + */ + +import { LogEvent } from '../event/basic' +import { Context } from '../context/context' + +export class Logger { + + private log(e: Logger.Entry) { + LogEvent.dispatch(this.context, e); + } + + message(m: string) { + this.log({ type: Logger.EntryType.Message, timestamp: new Date(), message: m }); + } + + error(m: string) { + this.log({ type: Logger.EntryType.Error, timestamp: new Date(), message: m }); + } + + warning(m: string) { + this.log({ type: Logger.EntryType.Warning, timestamp: new Date(), message: m }); + } + + info(m: string) { + this.log({ type: Logger.EntryType.Info, timestamp: new Date(), message: m }); + } + + constructor(private context: Context) { + + } +} + +export namespace Logger { + export enum EntryType { + Message, + Error, + Warning, + Info + } + + export interface Entry { + type: EntryType; + timestamp: Date; + message: any + } +} \ No newline at end of file diff --git a/src/mol-app/skin/base.scss b/src/mol-app/skin/base.scss new file mode 100644 index 0000000000000000000000000000000000000000..355f0eccabe5e385cf87fb59e1d0b23c6bfe72fb --- /dev/null +++ b/src/mol-app/skin/base.scss @@ -0,0 +1,45 @@ + +@font-face { + font-family: 'fontello'; + src: url('./fonts/fontello.eot'); + src: url('./fonts/fontello.eot#iefix') format('embedded-opentype'), + url('./fonts/fontello.woff2') format('woff2'), + url('./fonts/fontello.woff') format('woff'), + url('./fonts/fontello.ttf') format('truetype'), + url('./fonts/fontello.svg#fontello') format('svg'); + font-weight: normal; + font-style: normal; +} + +@import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400italic,700); + +.molstar-plugin { + font-family: "Helvetica Neue", "Source Sans Pro", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 1.42857143; + + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + @import 'variables'; + + // for bootstrap + $border-radius-base: 0; + $border-radius-large: 0; + $border-radius-small: 0; + + @import 'bootstrap'; + + @import 'icons'; + @import 'layout'; + @import 'ui'; + @import 'logo'; + + .molstar-plugin-content { + color: $font-color; + } + + background: $default-background; +} \ No newline at end of file diff --git a/src/mol-app/skin/bootstrap.scss b/src/mol-app/skin/bootstrap.scss new file mode 100644 index 0000000000000000000000000000000000000000..f26fdba911a808e6de47e05e6226d84f956a84d8 --- /dev/null +++ b/src/mol-app/skin/bootstrap.scss @@ -0,0 +1,25 @@ +/*! + * Bootstrap v3.3.6 (http://getbootstrap.com) + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ + +// Core variables and mixins +@import "bootstrap/variables"; +@import "bootstrap/mixins"; + +// Reset and dependencies +@import "bootstrap/normalize"; + +// Core CSS +@import "bootstrap/scaffolding"; +@import "bootstrap/type"; +@import "bootstrap/forms"; +@import "bootstrap/buttons"; + +// Components +@import "bootstrap/button-groups"; +@import "bootstrap/input-groups"; + +@import "bootstrap/labels"; +@import "bootstrap/badges"; diff --git a/src/mol-app/skin/bootstrap/badges.scss b/src/mol-app/skin/bootstrap/badges.scss new file mode 100644 index 0000000000000000000000000000000000000000..b03cc5562b6aa3a752dea7960206ad146facd24f --- /dev/null +++ b/src/mol-app/skin/bootstrap/badges.scss @@ -0,0 +1,68 @@ +// +// Badges +// -------------------------------------------------- + + +// Base class +.badge { + display: inline-block; + min-width: 10px; + padding: 3px 7px; + font-size: $font-size-small; + font-weight: $badge-font-weight; + color: $badge-color; + line-height: $badge-line-height; + vertical-align: middle; + white-space: nowrap; + text-align: center; + background-color: $badge-bg; + border-radius: $badge-border-radius; + + // Empty badges collapse automatically (not available in IE8) + &:empty { + display: none; + } + + // Quick fix for badges in buttons + .molstar-btn & { + position: relative; + top: -1px; + } + + .molstar-btn-xs &, + .molstar-btn-group-xs > .molstar-btn & { + top: 0; + padding: 1px 5px; + } + + // [converter] extracted a& to a.badge + + // Account for badges in navs + .list-group-item.active > &, + .nav-pills > .active > a > & { + color: $badge-active-color; + background-color: $badge-active-bg; + } + + .list-group-item > & { + float: right; + } + + .list-group-item > & + & { + margin-right: 5px; + } + + .nav-pills > li > a > & { + margin-left: 3px; + } + } + + // Hover state, but only for links + a.badge { + &:hover, + &:focus { + color: $badge-link-hover-color; + text-decoration: none; + cursor: pointer; + } + } \ No newline at end of file diff --git a/src/mol-app/skin/bootstrap/button-groups.scss b/src/mol-app/skin/bootstrap/button-groups.scss new file mode 100644 index 0000000000000000000000000000000000000000..3fd6d085e2760ba38823f4a32c1de9aaf8e0894f --- /dev/null +++ b/src/mol-app/skin/bootstrap/button-groups.scss @@ -0,0 +1,244 @@ +// +// Button groups +// -------------------------------------------------- + +// Make the div behave like a button +.molstar-btn-group, +.molstar-btn-group-vertical { + position: relative; + display: inline-block; + vertical-align: middle; // match .molstar-btn alignment given font-size hack above + > .molstar-btn { + position: relative; + float: left; + // Bring the "active" button to the front + &:hover, + &:focus, + &:active, + &.active { + z-index: 2; + } + } +} + +// Prevent double borders when buttons are next to each other +.molstar-btn-group { + .molstar-btn + .molstar-btn, + .molstar-btn + .molstar-btn-group, + .molstar-btn-group + .molstar-btn, + .molstar-btn-group + .molstar-btn-group { + margin-left: -1px; + } +} + +// Optional: Group multiple button groups together for a toolbar +.molstar-btn-toolbar { + margin-left: -5px; // Offset the first child's margin + @include clearfix; + + .molstar-btn, + .molstar-btn-group, + .input-group { + float: left; + } + > .molstar-btn, + > .molstar-btn-group, + > .input-group { + margin-left: 5px; + } +} + +.molstar-btn-group > .molstar-btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { + border-radius: 0; +} + +// Set corners individual because sometimes a single button can be in a .molstar-btn-group and we need :first-child and :last-child to both match +.molstar-btn-group > .molstar-btn:first-child { + margin-left: 0; + &:not(:last-child):not(.dropdown-toggle) { + @include border-right-radius(0); + } +} +// Need .dropdown-toggle since :last-child doesn't apply given a .dropdown-menu immediately after it +.molstar-btn-group > .molstar-btn:last-child:not(:first-child), +.molstar-btn-group > .dropdown-toggle:not(:first-child) { + @include border-left-radius(0); +} + +// Custom edits for including molstar-btn-groups within molstar-btn-groups (useful for including dropdown buttons within a molstar-btn-group) +.molstar-btn-group > .molstar-btn-group { + float: left; +} +.molstar-btn-group > .molstar-btn-group:not(:first-child):not(:last-child) > .molstar-btn { + border-radius: 0; +} +.molstar-btn-group > .molstar-btn-group:first-child:not(:last-child) { + > .molstar-btn:last-child, + > .dropdown-toggle { + @include border-right-radius(0); + } +} +.molstar-btn-group > .molstar-btn-group:last-child:not(:first-child) > .molstar-btn:first-child { + @include border-left-radius(0); +} + +// On active and open, don't show outline +.molstar-btn-group .dropdown-toggle:active, +.molstar-btn-group.open .dropdown-toggle { + outline: 0; +} + + +// Sizing +// +// Remix the default button sizing classes into new ones for easier manipulation. + +.molstar-btn-group-xs > .molstar-btn { @extend .molstar-btn-xs; } +.molstar-btn-group-sm > .molstar-btn { @extend .molstar-btn-sm; } +.molstar-btn-group-lg > .molstar-btn { @extend .molstar-btn-lg; } + + +// Split button dropdowns +// ---------------------- + +// Give the line between buttons some depth +.molstar-btn-group > .molstar-btn + .dropdown-toggle { + padding-left: 8px; + padding-right: 8px; +} +.molstar-btn-group > .molstar-btn-lg + .dropdown-toggle { + padding-left: 12px; + padding-right: 12px; +} + +// The clickable button for toggling the menu +// Remove the gradient and set the same inset shadow as the :active state +.molstar-btn-group.open .dropdown-toggle { + @include box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); + + // Show no shadow for `.molstar-btn-link` since it has no other button styles. + &.molstar-btn-link { + @include box-shadow(none); + } +} + + +// Reposition the caret +.molstar-btn .caret { + margin-left: 0; +} +// Carets in other button sizes +.molstar-btn-lg .caret { + border-width: $caret-width-large $caret-width-large 0; + border-bottom-width: 0; +} +// Upside down carets for .dropup +.dropup .molstar-btn-lg .caret { + border-width: 0 $caret-width-large $caret-width-large; +} + + +// Vertical button groups +// ---------------------- + +.molstar-btn-group-vertical { + > .molstar-btn, + > .molstar-btn-group, + > .molstar-btn-group > .molstar-btn { + display: block; + float: none; + width: 100%; + max-width: 100%; + } + + // Clear floats so dropdown menus can be properly placed + > .molstar-btn-group { + @include clearfix; + > .molstar-btn { + float: none; + } + } + + > .molstar-btn + .molstar-btn, + > .molstar-btn + .molstar-btn-group, + > .molstar-btn-group + .molstar-btn, + > .molstar-btn-group + .molstar-btn-group { + margin-top: -1px; + margin-left: 0; + } +} + +.molstar-btn-group-vertical > .molstar-btn { + &:not(:first-child):not(:last-child) { + border-radius: 0; + } + &:first-child:not(:last-child) { + @include border-top-radius($molstar-btn-border-radius-base); + @include border-bottom-radius(0); + } + &:last-child:not(:first-child) { + @include border-top-radius(0); + @include border-bottom-radius($molstar-btn-border-radius-base); + } +} +.molstar-btn-group-vertical > .molstar-btn-group:not(:first-child):not(:last-child) > .molstar-btn { + border-radius: 0; +} +.molstar-btn-group-vertical > .molstar-btn-group:first-child:not(:last-child) { + > .molstar-btn:last-child, + > .dropdown-toggle { + @include border-bottom-radius(0); + } +} +.molstar-btn-group-vertical > .molstar-btn-group:last-child:not(:first-child) > .molstar-btn:first-child { + @include border-top-radius(0); +} + + +// Justified button groups +// ---------------------- + +.molstar-btn-group-justified { + display: table; + width: 100%; + table-layout: fixed; + border-collapse: separate; + > .molstar-btn, + > .molstar-btn-group { + float: none; + display: table-cell; + width: 1%; + } + > .molstar-btn-group .molstar-btn { + width: 100%; + } + + > .molstar-btn-group .dropdown-menu { + left: auto; + } +} + + +// Checkbox and radio options +// +// In order to support the browser's form validation feedback, powered by the +// `required` attribute, we have to "hide" the inputs via `clip`. We cannot use +// `display: none;` or `visibility: hidden;` as that also hides the popover. +// Simply visually hiding the inputs via `opacity` would leave them clickable in +// certain cases which is prevented by using `clip` and `pointer-events`. +// This way, we ensure a DOM element is visible to position the popover from. +// +// See https://github.com/twbs/bootstrap/pull/12794 and +// https://github.com/twbs/bootstrap/pull/14559 for more information. + +[data-toggle="buttons"] { + > .molstar-btn, + > .molstar-btn-group > .molstar-btn { + input[type="radio"], + input[type="checkbox"] { + position: absolute; + clip: rect(0,0,0,0); + pointer-events: none; + } + } +} \ No newline at end of file diff --git a/src/mol-app/skin/bootstrap/buttons.scss b/src/mol-app/skin/bootstrap/buttons.scss new file mode 100644 index 0000000000000000000000000000000000000000..f775ee279506b373097a9aa27beb2a91c3d6950c --- /dev/null +++ b/src/mol-app/skin/bootstrap/buttons.scss @@ -0,0 +1,168 @@ +// +// Buttons +// -------------------------------------------------- + + +// Base styles +// -------------------------------------------------- + +.molstar-btn { + display: inline-block; + margin-bottom: 0; // For input.molstar-btn + font-weight: $molstar-btn-font-weight; + text-align: center; + vertical-align: middle; + touch-action: manipulation; + cursor: pointer; + background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 + border: 1px solid transparent; + white-space: nowrap; + @include button-size($padding-base-vertical, $padding-base-horizontal, $font-size-base, $line-height-base, $molstar-btn-border-radius-base); + @include user-select(none); + + &, + &:active, + &.active { + &:focus, + &.focus { + @include tab-focus; + } + } + + &:hover, + &:focus, + &.focus { + color: $molstar-btn-default-color; + text-decoration: none; + } + + &:active, + &.active { + outline: 0; + background-image: none; + @include box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); + } + + &.disabled, + &[disabled], + fieldset[disabled] & { + cursor: $cursor-disabled; + @include opacity(.65); + @include box-shadow(none); + } + + // [converter] extracted a& to a.molstar-btn + } + + a.molstar-btn { + &.disabled, + fieldset[disabled] & { + pointer-events: none; // Future-proof disabling of clicks on `<a>` elements + } + } + + + // Alternate buttons + // -------------------------------------------------- + + .molstar-btn-default { + @include button-variant($molstar-btn-default-color, $molstar-btn-default-bg, $molstar-btn-default-border); + } + .molstar-btn-primary { + @include button-variant($molstar-btn-primary-color, $molstar-btn-primary-bg, $molstar-btn-primary-border); + } + // Success appears as green + .molstar-btn-success { + @include button-variant($molstar-btn-success-color, $molstar-btn-success-bg, $molstar-btn-success-border); + } + // Info appears as blue-green + .molstar-btn-info { + @include button-variant($molstar-btn-info-color, $molstar-btn-info-bg, $molstar-btn-info-border); + } + // Warning appears as orange + .molstar-btn-warning { + @include button-variant($molstar-btn-warning-color, $molstar-btn-warning-bg, $molstar-btn-warning-border); + } + // Danger and error appear as red + .molstar-btn-danger { + @include button-variant($molstar-btn-danger-color, $molstar-btn-danger-bg, $molstar-btn-danger-border); + } + + + // Link buttons + // ------------------------- + + // Make a button look and behave like a link + .molstar-btn-link { + color: $link-color; + font-weight: normal; + border-radius: 0; + + &, + &:active, + &.active, + &[disabled], + fieldset[disabled] & { + background-color: transparent; + @include box-shadow(none); + } + &, + &:hover, + &:focus, + &:active { + border-color: transparent; + } + &:hover, + &:focus { + color: $link-hover-color; + text-decoration: $link-hover-decoration; + background-color: transparent; + } + &[disabled], + fieldset[disabled] & { + &:hover, + &:focus { + color: $molstar-btn-link-disabled-color; + text-decoration: none; + } + } + } + + + // Button Sizes + // -------------------------------------------------- + + .molstar-btn-lg { + // line-height: ensure even-numbered height of button next to large input + @include button-size($padding-large-vertical, $padding-large-horizontal, $font-size-large, $line-height-large, $molstar-btn-border-radius-large); + } + .molstar-btn-sm { + // line-height: ensure proper height of button next to small input + @include button-size($padding-small-vertical, $padding-small-horizontal, $font-size-small, $line-height-small, $molstar-btn-border-radius-small); + } + .molstar-btn-xs { + @include button-size($padding-xs-vertical, $padding-xs-horizontal, $font-size-small, $line-height-small, $molstar-btn-border-radius-small); + } + + + // Block button + // -------------------------------------------------- + + .molstar-btn-block { + display: block; + width: 100%; + } + + // Vertically space out multiple block buttons + .molstar-btn-block + .molstar-btn-block { + margin-top: 5px; + } + + // Specificity overrides + input[type="submit"], + input[type="reset"], + input[type="button"] { + &.molstar-btn-block { + width: 100%; + } + } \ No newline at end of file diff --git a/src/mol-app/skin/bootstrap/forms.scss b/src/mol-app/skin/bootstrap/forms.scss new file mode 100644 index 0000000000000000000000000000000000000000..b51eb372bc104ddf1fc19d35a212012308634fbc --- /dev/null +++ b/src/mol-app/skin/bootstrap/forms.scss @@ -0,0 +1,617 @@ +// +// Forms +// -------------------------------------------------- + + +// Normalize non-controls +// +// Restyle and baseline non-control form elements. + +fieldset { + padding: 0; + margin: 0; + border: 0; + // Chrome and Firefox set a `min-width: min-content;` on fieldsets, + // so we reset that to ensure it behaves more like a standard block element. + // See https://github.com/twbs/bootstrap/issues/12359. + min-width: 0; + } + + legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: $line-height-computed; + font-size: ($font-size-base * 1.5); + line-height: inherit; + color: $legend-color; + border: 0; + border-bottom: 1px solid $legend-border-color; + } + + label { + display: inline-block; + max-width: 100%; // Force IE8 to wrap long content (see https://github.com/twbs/bootstrap/issues/13141) + margin-bottom: 5px; + font-weight: bold; + } + + + // Normalize form controls + // + // While most of our form styles require extra classes, some basic normalization + // is required to ensure optimum display with or without those classes to better + // address browser inconsistencies. + + // Override content-box in Normalize (* isn't specific enough) + input[type="search"] { + @include box-sizing(border-box); + } + + // Position radios and checkboxes better + input[type="radio"], + input[type="checkbox"] { + margin: 4px 0 0; + margin-top: 1px \9; // IE8-9 + line-height: normal; + } + + input[type="file"] { + display: block; + } + + // Make range inputs behave like textual form controls + input[type="range"] { + display: block; + width: 100%; + } + + // Make multiple select elements height not fixed + select[multiple], + select[size] { + height: auto; + } + + // Focus for file, radio, and checkbox + input[type="file"]:focus, + input[type="radio"]:focus, + input[type="checkbox"]:focus { + @include tab-focus; + } + + // Adjust output element + output { + display: block; + padding-top: ($padding-base-vertical + 1); + font-size: $font-size-base; + line-height: $line-height-base; + color: $input-color; + } + + + // Common form controls + // + // Shared size and type resets for form controls. Apply `.molstar-form-control` to any + // of the following form controls: + // + // select + // textarea + // input[type="text"] + // input[type="password"] + // input[type="datetime"] + // input[type="datetime-local"] + // input[type="date"] + // input[type="month"] + // input[type="time"] + // input[type="week"] + // input[type="number"] + // input[type="email"] + // input[type="url"] + // input[type="search"] + // input[type="tel"] + // input[type="color"] + + .molstar-form-control { + display: block; + width: 100%; + height: $input-height-base; // Make inputs at least the height of their button counterpart (base line-height + padding + border) + padding: $padding-base-vertical $padding-base-horizontal; + font-size: $font-size-base; + line-height: $line-height-base; + color: $input-color; + background-color: $input-bg; + background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 + border: 1px solid $input-border; + border-radius: $input-border-radius; // Note: This has no effect on <select>s in some browsers, due to the limited stylability of <select>s in CSS. + //@include box-shadow(none);//inset 0 1px 1px rgba(0,0,0,.075)); + //@include transition(border-color ease-in-out .15s, box-shadow ease-in-out .15s); + + // Customize the `:focus` state to imitate native WebKit styles. + @include molstar-form-control-focus; + + // Placeholder + @include placeholder; + + // Unstyle the caret on `<select>`s in IE10+. + &::-ms-expand { + border: 0; + background-color: transparent; + } + + // Disabled and read-only inputs + // + // HTML5 says that controls under a fieldset > legend:first-child won't be + // disabled if the fieldset is disabled. Due to implementation difficulty, we + // don't honor that edge case; we style them as disabled anyway. + &[disabled], + &[readonly], + fieldset[disabled] & { + background-color: $input-bg-disabled; + opacity: 1; // iOS fix for unreadable disabled content; see https://github.com/twbs/bootstrap/issues/11655 + } + + &[disabled], + fieldset[disabled] & { + cursor: $cursor-disabled; + } + + // [converter] extracted textarea& to textarea.molstar-form-control + } + + // Reset height for `textarea`s + textarea.molstar-form-control { + height: auto; + } + + + // Search inputs in iOS + // + // This overrides the extra rounded corners on search inputs in iOS so that our + // `.molstar-form-control` class can properly style them. Note that this cannot simply + // be added to `.molstar-form-control` as it's not specific enough. For details, see + // https://github.com/twbs/bootstrap/issues/11586. + + input[type="search"] { + -webkit-appearance: none; + } + + + // Special styles for iOS temporal inputs + // + // In Mobile Safari, setting `display: block` on temporal inputs causes the + // text within the input to become vertically misaligned. As a workaround, we + // set a pixel line-height that matches the given height of the input, but only + // for Safari. See https://bugs.webkit.org/show_bug.cgi?id=139848 + // + // Note that as of 8.3, iOS doesn't support `datetime` or `week`. + + @media screen and (-webkit-min-device-pixel-ratio: 0) { + input[type="date"], + input[type="time"], + input[type="datetime-local"], + input[type="month"] { + &.molstar-form-control { + line-height: $input-height-base; + } + + &.input-sm, + .input-group-sm & { + line-height: $input-height-small; + } + + &.input-lg, + .input-group-lg & { + line-height: $input-height-large; + } + } + } + + + // Form groups + // + // Designed to help with the organization and spacing of vertical forms. For + // horizontal forms, use the predefined grid classes. + + .form-group { + margin-bottom: $form-group-margin-bottom; + } + + + // Checkboxes and radios + // + // Indent the labels to position radios/checkboxes as hanging controls. + + .radio, + .checkbox { + position: relative; + display: block; + margin-top: 10px; + margin-bottom: 10px; + + label { + min-height: $line-height-computed; // Ensure the input doesn't jump when there is no text + padding-left: 20px; + margin-bottom: 0; + font-weight: normal; + cursor: pointer; + } + } + .radio input[type="radio"], + .radio-inline input[type="radio"], + .checkbox input[type="checkbox"], + .checkbox-inline input[type="checkbox"] { + position: absolute; + margin-left: -20px; + margin-top: 4px \9; + } + + .radio + .radio, + .checkbox + .checkbox { + margin-top: -5px; // Move up sibling radios or checkboxes for tighter spacing + } + + // Radios and checkboxes on same line + .radio-inline, + .checkbox-inline { + position: relative; + display: inline-block; + padding-left: 20px; + margin-bottom: 0; + vertical-align: middle; + font-weight: normal; + cursor: pointer; + } + .radio-inline + .radio-inline, + .checkbox-inline + .checkbox-inline { + margin-top: 0; + margin-left: 10px; // space out consecutive inline controls + } + + // Apply same disabled cursor tweak as for inputs + // Some special care is needed because <label>s don't inherit their parent's `cursor`. + // + // Note: Neither radios nor checkboxes can be readonly. + input[type="radio"], + input[type="checkbox"] { + &[disabled], + &.disabled, + fieldset[disabled] & { + cursor: $cursor-disabled; + } + } + // These classes are used directly on <label>s + .radio-inline, + .checkbox-inline { + &.disabled, + fieldset[disabled] & { + cursor: $cursor-disabled; + } + } + // These classes are used on elements with <label> descendants + .radio, + .checkbox { + &.disabled, + fieldset[disabled] & { + label { + cursor: $cursor-disabled; + } + } + } + + + // Static form control text + // + // Apply class to a `p` element to make any string of text align with labels in + // a horizontal form layout. + + .molstar-form-control-static { + // Size it appropriately next to real form controls + padding-top: ($padding-base-vertical + 1); + padding-bottom: ($padding-base-vertical + 1); + // Remove default margin from `p` + margin-bottom: 0; + min-height: ($line-height-computed + $font-size-base); + + &.input-lg, + &.input-sm { + padding-left: 0; + padding-right: 0; + } + } + + + // Form control sizing + // + // Build on `.molstar-form-control` with modifier classes to decrease or increase the + // height and font-size of form controls. + // + // The `.form-group-* molstar-form-control` variations are sadly duplicated to avoid the + // issue documented in https://github.com/twbs/bootstrap/issues/15074. + + @include input-size('.input-sm', $input-height-small, $padding-small-vertical, $padding-small-horizontal, $font-size-small, $line-height-small, $input-border-radius-small); + .form-group-sm { + .molstar-form-control { + height: $input-height-small; + padding: $padding-small-vertical $padding-small-horizontal; + font-size: $font-size-small; + line-height: $line-height-small; + border-radius: $input-border-radius-small; + } + select.molstar-form-control { + height: $input-height-small; + line-height: $input-height-small; + } + textarea.molstar-form-control, + select[multiple].molstar-form-control { + height: auto; + } + .molstar-form-control-static { + height: $input-height-small; + min-height: ($line-height-computed + $font-size-small); + padding: ($padding-small-vertical + 1) $padding-small-horizontal; + font-size: $font-size-small; + line-height: $line-height-small; + } + } + + @include input-size('.input-lg', $input-height-large, $padding-large-vertical, $padding-large-horizontal, $font-size-large, $line-height-large, $input-border-radius-large); + .form-group-lg { + .molstar-form-control { + height: $input-height-large; + padding: $padding-large-vertical $padding-large-horizontal; + font-size: $font-size-large; + line-height: $line-height-large; + border-radius: $input-border-radius-large; + } + select.molstar-form-control { + height: $input-height-large; + line-height: $input-height-large; + } + textarea.molstar-form-control, + select[multiple].molstar-form-control { + height: auto; + } + .molstar-form-control-static { + height: $input-height-large; + min-height: ($line-height-computed + $font-size-large); + padding: ($padding-large-vertical + 1) $padding-large-horizontal; + font-size: $font-size-large; + line-height: $line-height-large; + } + } + + + // Form control feedback states + // + // Apply contextual and semantic states to individual form controls. + + .has-feedback { + // Enable absolute positioning + position: relative; + + // Ensure icons don't overlap text + .molstar-form-control { + padding-right: ($input-height-base * 1.25); + } + } + // Feedback icon (requires .glyphicon classes) + .molstar-form-control-feedback { + position: absolute; + top: 0; + right: 0; + z-index: 2; // Ensure icon is above input groups + display: block; + width: $input-height-base; + height: $input-height-base; + line-height: $input-height-base; + text-align: center; + pointer-events: none; + } + .input-lg + .molstar-form-control-feedback, + .input-group-lg + .molstar-form-control-feedback, + .form-group-lg .molstar-form-control + .molstar-form-control-feedback { + width: $input-height-large; + height: $input-height-large; + line-height: $input-height-large; + } + .input-sm + .molstar-form-control-feedback, + .input-group-sm + .molstar-form-control-feedback, + .form-group-sm .molstar-form-control + .molstar-form-control-feedback { + width: $input-height-small; + height: $input-height-small; + line-height: $input-height-small; + } + + // Feedback states + .has-success { + @include molstar-form-control-validation($state-success-text, $state-success-text, $state-success-bg); + } + .has-warning { + @include molstar-form-control-validation($state-warning-text, $state-warning-text, $state-warning-bg); + } + .has-error { + @include molstar-form-control-validation($state-danger-text, $state-danger-text, $state-danger-bg); + } + + // Reposition feedback icon if input has visible label above + .has-feedback label { + + & ~ .molstar-form-control-feedback { + top: ($line-height-computed + 5); // Height of the `label` and its margin + } + &.sr-only ~ .molstar-form-control-feedback { + top: 0; + } + } + + + // Help text + // + // Apply to any element you wish to create light text for placement immediately + // below a form control. Use for general help, formatting, or instructional text. + + .help-block { + display: block; // account for any element using help-block + margin-top: 5px; + margin-bottom: 10px; + color: lighten($text-color, 25%); // lighten the text some for contrast + } + + + // Inline forms + // + // Make forms appear inline(-block) by adding the `.form-inline` class. Inline + // forms begin stacked on extra small (mobile) devices and then go inline when + // viewports reach <768px. + // + // Requires wrapping inputs and labels with `.form-group` for proper display of + // default HTML form controls and our custom form controls (e.g., input groups). + // + // Heads up! This is mixin-ed into `.navbar-form` in navbars.less. + + // [converter] extracted from `.form-inline` for libsass compatibility + @mixin form-inline { + + // Kick in the inline + @media (min-width: $screen-sm-min) { + // Inline-block all the things for "inline" + .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + + // In navbar-form, allow folks to *not* use `.form-group` + .molstar-form-control { + display: inline-block; + width: auto; // Prevent labels from stacking above inputs in `.form-group` + vertical-align: middle; + } + + // Make static controls behave like regular ones + .molstar-form-control-static { + display: inline-block; + } + + .input-group { + display: inline-table; + vertical-align: middle; + + .input-group-addon, + .input-group-molstar-btn, + .molstar-form-control { + width: auto; + } + } + + // Input groups need that 100% width though + .input-group > .molstar-form-control { + width: 100%; + } + + .control-label { + margin-bottom: 0; + vertical-align: middle; + } + + // Remove default margin on radios/checkboxes that were used for stacking, and + // then undo the floating of radios and checkboxes to match. + .radio, + .checkbox { + display: inline-block; + margin-top: 0; + margin-bottom: 0; + vertical-align: middle; + + label { + padding-left: 0; + } + } + .radio input[type="radio"], + .checkbox input[type="checkbox"] { + position: relative; + margin-left: 0; + } + + // Re-override the feedback icon. + .has-feedback .molstar-form-control-feedback { + top: 0; + } + } + } + // [converter] extracted as `@mixin form-inline` for libsass compatibility + .form-inline { + @include form-inline; + } + + + + // Horizontal forms + // + // Horizontal forms are built on grid classes and allow you to create forms with + // labels on the left and inputs on the right. + + .form-horizontal { + + // Consistent vertical alignment of radios and checkboxes + // + // Labels also get some reset styles, but that is scoped to a media query below. + .radio, + .checkbox, + .radio-inline, + .checkbox-inline { + margin-top: 0; + margin-bottom: 0; + padding-top: ($padding-base-vertical + 1); // Default padding plus a border + } + // Account for padding we're adding to ensure the alignment and of help text + // and other content below items + .radio, + .checkbox { + min-height: ($line-height-computed + ($padding-base-vertical + 1)); + } + + // Make form groups behave like rows + .form-group { + @include make-row; + } + + // Reset spacing and right align labels, but scope to media queries so that + // labels on narrow viewports stack the same as a default form example. + @media (min-width: $screen-sm-min) { + .control-label { + text-align: right; + margin-bottom: 0; + padding-top: ($padding-base-vertical + 1); // Default padding plus a border + } + } + + // Validation states + // + // Reposition the icon because it's now within a grid column and columns have + // `position: relative;` on them. Also accounts for the grid gutter padding. + .has-feedback .molstar-form-control-feedback { + right: floor(($grid-gutter-width / 2)); + } + + // Form group sizes + // + // Quick utility class for applying `.input-lg` and `.input-sm` styles to the + // inputs and labels within a `.form-group`. + .form-group-lg { + @media (min-width: $screen-sm-min) { + .control-label { + padding-top: ($padding-large-vertical + 1); + font-size: $font-size-large; + } + } + } + .form-group-sm { + @media (min-width: $screen-sm-min) { + .control-label { + padding-top: ($padding-small-vertical + 1); + font-size: $font-size-small; + } + } + } + } \ No newline at end of file diff --git a/src/mol-app/skin/bootstrap/input-groups.scss b/src/mol-app/skin/bootstrap/input-groups.scss new file mode 100644 index 0000000000000000000000000000000000000000..5f57fc3df5e9c92cc30cb750ccee0182d7df9bd9 --- /dev/null +++ b/src/mol-app/skin/bootstrap/input-groups.scss @@ -0,0 +1,171 @@ +// +// Input groups +// -------------------------------------------------- + +// Base styles +// ------------------------- +.input-group { + position: relative; // For dropdowns + display: table; + border-collapse: separate; // prevent input groups from inheriting border styles from table cells when placed within a table + + // Undo padding and float of grid classes + &[class*="col-"] { + float: none; + padding-left: 0; + padding-right: 0; + } + + .molstar-form-control { + // Ensure that the input is always above the *appended* addon button for + // proper border colors. + position: relative; + z-index: 2; + + // IE9 fubars the placeholder attribute in text inputs and the arrows on + // select elements in input groups. To fix it, we float the input. Details: + // https://github.com/twbs/bootstrap/issues/11561#issuecomment-28936855 + float: left; + + width: 100%; + margin-bottom: 0; + + &:focus { + z-index: 3; + } + } + } + + // Sizing options + // + // Remix the default form control sizing classes into new ones for easier + // manipulation. + + .input-group-lg > .molstar-form-control, + .input-group-lg > .input-group-addon, + .input-group-lg > .input-group-molstar-btn > .molstar-btn { + @extend .input-lg; + } + .input-group-sm > .molstar-form-control, + .input-group-sm > .input-group-addon, + .input-group-sm > .input-group-molstar-btn > .molstar-btn { + @extend .input-sm; + } + + + // Display as table-cell + // ------------------------- + .input-group-addon, + .input-group-molstar-btn, + .input-group .molstar-form-control { + display: table-cell; + + &:not(:first-child):not(:last-child) { + border-radius: 0; + } + } + // Addon and addon wrapper for buttons + .input-group-addon, + .input-group-molstar-btn { + width: 1%; + white-space: nowrap; + vertical-align: middle; // Match the inputs + } + + // Text input groups + // ------------------------- + .input-group-addon { + padding: $padding-base-vertical $padding-base-horizontal; + font-size: $font-size-base; + font-weight: normal; + line-height: 1; + color: $input-color; + text-align: center; + background-color: $input-group-addon-bg; + border: 1px solid $input-group-addon-border-color; + border-radius: $input-border-radius; + + // Sizing + &.input-sm { + padding: $padding-small-vertical $padding-small-horizontal; + font-size: $font-size-small; + border-radius: $input-border-radius-small; + } + &.input-lg { + padding: $padding-large-vertical $padding-large-horizontal; + font-size: $font-size-large; + border-radius: $input-border-radius-large; + } + + // Nuke default margins from checkboxes and radios to vertically center within. + input[type="radio"], + input[type="checkbox"] { + margin-top: 0; + } + } + + // Reset rounded corners + .input-group .molstar-form-control:first-child, + .input-group-addon:first-child, + .input-group-molstar-btn:first-child > .molstar-btn, + .input-group-molstar-btn:first-child > .molstar-btn-group > .molstar-btn, + .input-group-molstar-btn:first-child > .dropdown-toggle, + .input-group-molstar-btn:last-child > .molstar-btn:not(:last-child):not(.dropdown-toggle), + .input-group-molstar-btn:last-child > .molstar-btn-group:not(:last-child) > .molstar-btn { + @include border-right-radius(0); + } + .input-group-addon:first-child { + border-right: 0; + } + .input-group .molstar-form-control:last-child, + .input-group-addon:last-child, + .input-group-molstar-btn:last-child > .molstar-btn, + .input-group-molstar-btn:last-child > .molstar-btn-group > .molstar-btn, + .input-group-molstar-btn:last-child > .dropdown-toggle, + .input-group-molstar-btn:first-child > .molstar-btn:not(:first-child), + .input-group-molstar-btn:first-child > .molstar-btn-group:not(:first-child) > .molstar-btn { + @include border-left-radius(0); + } + .input-group-addon:last-child { + border-left: 0; + } + + // Button input groups + // ------------------------- + .input-group-molstar-btn { + position: relative; + // Jankily prevent input button groups from wrapping with `white-space` and + // `font-size` in combination with `inline-block` on buttons. + font-size: 0; + white-space: nowrap; + + // Negative margin for spacing, position for bringing hovered/focused/actived + // element above the siblings. + > .molstar-btn { + position: relative; + + .molstar-btn { + margin-left: -1px; + } + // Bring the "active" button to the front + &:hover, + &:focus, + &:active { + z-index: 2; + } + } + + // Negative margin to only have a 1px border between the two + &:first-child { + > .molstar-btn, + > .molstar-btn-group { + margin-right: -1px; + } + } + &:last-child { + > .molstar-btn, + > .molstar-btn-group { + z-index: 2; + margin-left: -1px; + } + } + } \ No newline at end of file diff --git a/src/mol-app/skin/bootstrap/labels.scss b/src/mol-app/skin/bootstrap/labels.scss new file mode 100644 index 0000000000000000000000000000000000000000..c453cd4173c362721e629c88e94c4de362b07fa1 --- /dev/null +++ b/src/mol-app/skin/bootstrap/labels.scss @@ -0,0 +1,66 @@ +// +// Labels +// -------------------------------------------------- + +.label { + display: inline; + padding: .2em .6em .3em; + font-size: 75%; + font-weight: bold; + line-height: 1; + color: $label-color; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25em; + + // [converter] extracted a& to a.label + + // Empty labels collapse automatically (not available in IE8) + &:empty { + display: none; + } + + // Quick fix for labels in buttons + .molstar-btn & { + position: relative; + top: -1px; + } + } + + // Add hover effects, but only for links + a.label { + &:hover, + &:focus { + color: $label-link-hover-color; + text-decoration: none; + cursor: pointer; + } + } + + // Colors + // Contextual variations (linked labels get darker on :hover) + + .label-default { + @include label-variant($label-default-bg); + } + + .label-primary { + @include label-variant($label-primary-bg); + } + + .label-success { + @include label-variant($label-success-bg); + } + + .label-info { + @include label-variant($label-info-bg); + } + + .label-warning { + @include label-variant($label-warning-bg); + } + + .label-danger { + @include label-variant($label-danger-bg); + } \ No newline at end of file diff --git a/src/mol-app/skin/bootstrap/mixins.scss b/src/mol-app/skin/bootstrap/mixins.scss new file mode 100644 index 0000000000000000000000000000000000000000..94b12fed20092a647d8ceaa9a0ad700f6bbad65a --- /dev/null +++ b/src/mol-app/skin/bootstrap/mixins.scss @@ -0,0 +1,40 @@ +// Mixins +// -------------------------------------------------- + +// Utilities +// @import "mixins/hide-text"; +@import "mixins/opacity"; +@import "mixins/image"; +@import "mixins/labels"; +// @import "mixins/reset-filter"; +// @import "mixins/resize"; +// @import "mixins/responsive-visibility"; +// @import "mixins/size"; +@import "mixins/tab-focus"; +// @import "mixins/reset-text"; +@import "mixins/text-emphasis"; +@import "mixins/text-overflow"; +@import "mixins/vendor-prefixes"; + +// Components +// @import "mixins/alerts"; +@import "mixins/buttons"; +// @import "mixins/panels"; +// @import "mixins/pagination"; +// @import "mixins/list-group"; +// @import "mixins/nav-divider"; +@import "mixins/forms"; +// @import "mixins/progress-bar"; +// @import "mixins/table-row"; + +// Skins +@import "mixins/background-variant"; +@import "mixins/border-radius"; +// @import "mixins/gradients"; + +// Layout +@import "mixins/clearfix"; +// @import "mixins/center-block"; +// @import "mixins/nav-vertical-align"; +// @import "mixins/grid-framework"; +@import "mixins/grid"; \ No newline at end of file diff --git a/src/mol-app/skin/bootstrap/mixins/background-variant.scss b/src/mol-app/skin/bootstrap/mixins/background-variant.scss new file mode 100644 index 0000000000000000000000000000000000000000..a3044fff2190918a3f09b8ae795b78a5f45316a0 --- /dev/null +++ b/src/mol-app/skin/bootstrap/mixins/background-variant.scss @@ -0,0 +1,12 @@ +// Contextual backgrounds + +// [converter] $parent hack +@mixin bg-variant($parent, $color) { + #{$parent} { + background-color: $color; + } + a#{$parent}:hover, + a#{$parent}:focus { + background-color: darken($color, 10%); + } +} \ No newline at end of file diff --git a/src/mol-app/skin/bootstrap/mixins/border-radius.scss b/src/mol-app/skin/bootstrap/mixins/border-radius.scss new file mode 100644 index 0000000000000000000000000000000000000000..2ba5d06e81ad3301f657b07e05ed8b9d9e604770 --- /dev/null +++ b/src/mol-app/skin/bootstrap/mixins/border-radius.scss @@ -0,0 +1,18 @@ +// Single side border-radius + +@mixin border-top-radius($radius) { + border-top-right-radius: $radius; + border-top-left-radius: $radius; +} +@mixin border-right-radius($radius) { + border-bottom-right-radius: $radius; + border-top-right-radius: $radius; +} +@mixin border-bottom-radius($radius) { + border-bottom-right-radius: $radius; + border-bottom-left-radius: $radius; +} +@mixin border-left-radius($radius) { + border-bottom-left-radius: $radius; + border-top-left-radius: $radius; +} \ No newline at end of file diff --git a/src/mol-app/skin/bootstrap/mixins/buttons.scss b/src/mol-app/skin/bootstrap/mixins/buttons.scss new file mode 100644 index 0000000000000000000000000000000000000000..c7be1237a5c12b198cd90091617e9bacb0107a62 --- /dev/null +++ b/src/mol-app/skin/bootstrap/mixins/buttons.scss @@ -0,0 +1,65 @@ +// Button variants +// +// Easily pump out default styles, as well as :hover, :focus, :active, +// and disabled options for all buttons + +@mixin button-variant($color, $background, $border) { + color: $color; + background-color: $background; + border-color: $border; + + &:focus, + &.focus { + color: $color; + background-color: darken($background, 10%); + border-color: darken($border, 25%); + } + &:hover { + color: $color; + background-color: darken($background, 10%); + border-color: darken($border, 12%); + } + &:active, + &.active, + .open > &.dropdown-toggle { + color: $color; + background-color: darken($background, 10%); + border-color: darken($border, 12%); + + &:hover, + &:focus, + &.focus { + color: $color; + background-color: darken($background, 17%); + border-color: darken($border, 25%); + } + } + &:active, + &.active, + .open > &.dropdown-toggle { + background-image: none; + } + &.disabled, + &[disabled], + fieldset[disabled] & { + &:hover, + &:focus, + &.focus { + background-color: $background; + border-color: $border; + } + } + + .badge { + color: $background; + background-color: $color; + } + } + + // Button sizes + @mixin button-size($padding-vertical, $padding-horizontal, $font-size, $line-height, $border-radius) { + padding: $padding-vertical $padding-horizontal; + font-size: $font-size; + line-height: $line-height; + border-radius: $border-radius; + } \ No newline at end of file diff --git a/src/mol-app/skin/bootstrap/mixins/clearfix.scss b/src/mol-app/skin/bootstrap/mixins/clearfix.scss new file mode 100644 index 0000000000000000000000000000000000000000..12d42afa77016633e70bc238c2dd741129abf2f7 --- /dev/null +++ b/src/mol-app/skin/bootstrap/mixins/clearfix.scss @@ -0,0 +1,22 @@ +// Clearfix +// +// For modern browsers +// 1. The space content is one way to avoid an Opera bug when the +// contenteditable attribute is included anywhere else in the document. +// Otherwise it causes space to appear at the top and bottom of elements +// that are clearfixed. +// 2. The use of `table` rather than `block` is only necessary if using +// `:before` to contain the top-margins of child elements. +// +// Source: http://nicolasgallagher.com/micro-clearfix-hack/ + +@mixin clearfix() { + &:before, + &:after { + content: " "; // 1 + display: table; // 2 + } + &:after { + clear: both; + } +} \ No newline at end of file diff --git a/src/mol-app/skin/bootstrap/mixins/forms.scss b/src/mol-app/skin/bootstrap/mixins/forms.scss new file mode 100644 index 0000000000000000000000000000000000000000..2bfe4f00a75b7a0132a9412698bbdf7cf2e1cc47 --- /dev/null +++ b/src/mol-app/skin/bootstrap/mixins/forms.scss @@ -0,0 +1,88 @@ +// Form validation states +// +// Used in forms.less to generate the form validation CSS for warnings, errors, +// and successes. + +@mixin molstar-form-control-validation($text-color: #555, $border-color: #ccc, $background-color: #f5f5f5) { + // Color the label and help text + .help-block, + .control-label, + .radio, + .checkbox, + .radio-inline, + .checkbox-inline, + &.radio label, + &.checkbox label, + &.radio-inline label, + &.checkbox-inline label { + color: $text-color; + } + // Set the border and box shadow on specific inputs to match + .molstar-form-control { + border-color: $border-color; + @include box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); // Redeclare so transitions work + &:focus { + border-color: darken($border-color, 10%); + $shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px lighten($border-color, 20%); + @include box-shadow($shadow); + } + } + // Set validation states also for addons + .input-group-addon { + color: $text-color; + border-color: $border-color; + background-color: $background-color; + } + // Optional feedback icon + .molstar-form-control-feedback { + color: $text-color; + } + } + + + // Form control focus state + // + // Generate a customized focus state and for any input with the specified color, + // which defaults to the `$input-border-focus` variable. + // + // We highly encourage you to not customize the default value, but instead use + // this to tweak colors on an as-needed basis. This aesthetic change is based on + // WebKit's default styles, but applicable to a wider range of browsers. Its + // usability and accessibility should be taken into account with any change. + // + // Example usage: change the default blue border and shadow to white for better + // contrast against a dark gray background. + @mixin molstar-form-control-focus($color: $input-border-focus) { + $color-rgba: rgba(red($color), green($color), blue($color), .6); + &:focus { + border-color: $color; + outline: 0; + @include box-shadow(inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px $color-rgba); + } + } + + // Form control sizing + // + // Relative text size, padding, and border-radii changes for form controls. For + // horizontal sizing, wrap controls in the predefined grid classes. `<select>` + // element gets special love because it's special, and that's a fact! + // [converter] $parent hack + @mixin input-size($parent, $input-height, $padding-vertical, $padding-horizontal, $font-size, $line-height, $border-radius) { + #{$parent} { + height: $input-height; + padding: $padding-vertical $padding-horizontal; + font-size: $font-size; + line-height: $line-height; + border-radius: $border-radius; + } + + select#{$parent} { + height: $input-height; + line-height: $input-height; + } + + textarea#{$parent}, + select[multiple]#{$parent} { + height: auto; + } + } \ No newline at end of file diff --git a/src/mol-app/skin/bootstrap/mixins/grid.scss b/src/mol-app/skin/bootstrap/mixins/grid.scss new file mode 100644 index 0000000000000000000000000000000000000000..7457a0abaa5f59fd4a8128f969d61de347e315e7 --- /dev/null +++ b/src/mol-app/skin/bootstrap/mixins/grid.scss @@ -0,0 +1,122 @@ +// Grid system +// +// Generate semantic grid columns with these mixins. + +// Centered container element +@mixin container-fixed($gutter: $grid-gutter-width) { + margin-right: auto; + margin-left: auto; + padding-left: floor(($gutter / 2)); + padding-right: ceil(($gutter / 2)); + @include clearfix; + } + + // Creates a wrapper for a series of columns + @mixin make-row($gutter: $grid-gutter-width) { + margin-left: ceil(($gutter / -2)); + margin-right: floor(($gutter / -2)); + @include clearfix; + } + + // Generate the extra small columns + @mixin make-xs-column($columns, $gutter: $grid-gutter-width) { + position: relative; + float: left; + width: percentage(($columns / $grid-columns)); + min-height: 1px; + padding-left: ($gutter / 2); + padding-right: ($gutter / 2); + } + @mixin make-xs-column-offset($columns) { + margin-left: percentage(($columns / $grid-columns)); + } + @mixin make-xs-column-push($columns) { + left: percentage(($columns / $grid-columns)); + } + @mixin make-xs-column-pull($columns) { + right: percentage(($columns / $grid-columns)); + } + + // Generate the small columns + @mixin make-sm-column($columns, $gutter: $grid-gutter-width) { + position: relative; + min-height: 1px; + padding-left: ($gutter / 2); + padding-right: ($gutter / 2); + + @media (min-width: $screen-sm-min) { + float: left; + width: percentage(($columns / $grid-columns)); + } + } + @mixin make-sm-column-offset($columns) { + @media (min-width: $screen-sm-min) { + margin-left: percentage(($columns / $grid-columns)); + } + } + @mixin make-sm-column-push($columns) { + @media (min-width: $screen-sm-min) { + left: percentage(($columns / $grid-columns)); + } + } + @mixin make-sm-column-pull($columns) { + @media (min-width: $screen-sm-min) { + right: percentage(($columns / $grid-columns)); + } + } + + // Generate the medium columns + @mixin make-md-column($columns, $gutter: $grid-gutter-width) { + position: relative; + min-height: 1px; + padding-left: ($gutter / 2); + padding-right: ($gutter / 2); + + @media (min-width: $screen-md-min) { + float: left; + width: percentage(($columns / $grid-columns)); + } + } + @mixin make-md-column-offset($columns) { + @media (min-width: $screen-md-min) { + margin-left: percentage(($columns / $grid-columns)); + } + } + @mixin make-md-column-push($columns) { + @media (min-width: $screen-md-min) { + left: percentage(($columns / $grid-columns)); + } + } + @mixin make-md-column-pull($columns) { + @media (min-width: $screen-md-min) { + right: percentage(($columns / $grid-columns)); + } + } + + // Generate the large columns + @mixin make-lg-column($columns, $gutter: $grid-gutter-width) { + position: relative; + min-height: 1px; + padding-left: ($gutter / 2); + padding-right: ($gutter / 2); + + @media (min-width: $screen-lg-min) { + float: left; + width: percentage(($columns / $grid-columns)); + } + } + @mixin make-lg-column-offset($columns) { + @media (min-width: $screen-lg-min) { + margin-left: percentage(($columns / $grid-columns)); + } + } + @mixin make-lg-column-push($columns) { + @media (min-width: $screen-lg-min) { + left: percentage(($columns / $grid-columns)); + } + } + @mixin make-lg-column-pull($columns) { + @media (min-width: $screen-lg-min) { + right: percentage(($columns / $grid-columns)); + } + } \ No newline at end of file diff --git a/src/mol-app/skin/bootstrap/mixins/image.scss b/src/mol-app/skin/bootstrap/mixins/image.scss new file mode 100644 index 0000000000000000000000000000000000000000..608cad5a535bf7db769366eef99d77c39a7283a2 --- /dev/null +++ b/src/mol-app/skin/bootstrap/mixins/image.scss @@ -0,0 +1,33 @@ +// Image Mixins +// - Responsive image +// - Retina image + + +// Responsive image +// +// Keep images from scaling beyond the width of their parents. +@mixin img-responsive($display: block) { + display: $display; + max-width: 100%; // Part 1: Set a maximum relative to the parent + height: auto; // Part 2: Scale the height according to the width, otherwise you get stretching +} + + +// Retina image +// +// Short retina mixin for setting background-image and -size. Note that the +// spelling of `min--moz-device-pixel-ratio` is intentional. +@mixin img-retina($file-1x, $file-2x, $width-1x, $height-1x) { + background-image: url(if($bootstrap-sass-asset-helper, twbs-image-path("#{$file-1x}"), "#{$file-1x}")); + + @media + only screen and (-webkit-min-device-pixel-ratio: 2), + only screen and ( min--moz-device-pixel-ratio: 2), + only screen and ( -o-min-device-pixel-ratio: 2/1), + only screen and ( min-device-pixel-ratio: 2), + only screen and ( min-resolution: 192dpi), + only screen and ( min-resolution: 2dppx) { + background-image: url(if($bootstrap-sass-asset-helper, twbs-image-path("#{$file-2x}"), "#{$file-2x}")); + background-size: $width-1x $height-1x; + } +} \ No newline at end of file diff --git a/src/mol-app/skin/bootstrap/mixins/labels.scss b/src/mol-app/skin/bootstrap/mixins/labels.scss new file mode 100644 index 0000000000000000000000000000000000000000..1353b098dd8d4f89ff79da862d9e856f52dd674a --- /dev/null +++ b/src/mol-app/skin/bootstrap/mixins/labels.scss @@ -0,0 +1,12 @@ +// Labels + +@mixin label-variant($color) { + background-color: $color; + + &[href] { + &:hover, + &:focus { + background-color: darken($color, 10%); + } + } +} \ No newline at end of file diff --git a/src/mol-app/skin/bootstrap/mixins/opacity.scss b/src/mol-app/skin/bootstrap/mixins/opacity.scss new file mode 100644 index 0000000000000000000000000000000000000000..f04356c6bb431f3c7f6e027e070fd1b39018c055 --- /dev/null +++ b/src/mol-app/skin/bootstrap/mixins/opacity.scss @@ -0,0 +1,8 @@ +// Opacity + +@mixin opacity($opacity) { + opacity: $opacity; + // IE8 filter + $opacity-ie: ($opacity * 100); + filter: alpha(opacity=$opacity-ie); +} \ No newline at end of file diff --git a/src/mol-app/skin/bootstrap/mixins/tab-focus.scss b/src/mol-app/skin/bootstrap/mixins/tab-focus.scss new file mode 100644 index 0000000000000000000000000000000000000000..414babf68561d780f937be3e206fd8ea7a49f160 --- /dev/null +++ b/src/mol-app/skin/bootstrap/mixins/tab-focus.scss @@ -0,0 +1,9 @@ +// WebKit-style focus + +@mixin tab-focus() { + // Default + outline: thin dotted; + // WebKit + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} \ No newline at end of file diff --git a/src/mol-app/skin/bootstrap/mixins/text-emphasis.scss b/src/mol-app/skin/bootstrap/mixins/text-emphasis.scss new file mode 100644 index 0000000000000000000000000000000000000000..dddb9ae2521c23982487c603f8606cc7c7d5495a --- /dev/null +++ b/src/mol-app/skin/bootstrap/mixins/text-emphasis.scss @@ -0,0 +1,12 @@ +// Typography + +// [converter] $parent hack +@mixin text-emphasis-variant($parent, $color) { + #{$parent} { + color: $color; + } + a#{$parent}:hover, + a#{$parent}:focus { + color: darken($color, 10%); + } +} \ No newline at end of file diff --git a/src/mol-app/skin/bootstrap/mixins/text-overflow.scss b/src/mol-app/skin/bootstrap/mixins/text-overflow.scss new file mode 100644 index 0000000000000000000000000000000000000000..b6ac5f707d4779f51d55e9657dc24e2a4e6cc77f --- /dev/null +++ b/src/mol-app/skin/bootstrap/mixins/text-overflow.scss @@ -0,0 +1,8 @@ +// Text overflow +// Requires inline-block or block for proper styling + +@mixin text-overflow() { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} \ No newline at end of file diff --git a/src/mol-app/skin/bootstrap/mixins/vendor-prefixes.scss b/src/mol-app/skin/bootstrap/mixins/vendor-prefixes.scss new file mode 100644 index 0000000000000000000000000000000000000000..b657e7a26ef78da22d21d1a3a84b337d8b7463d0 --- /dev/null +++ b/src/mol-app/skin/bootstrap/mixins/vendor-prefixes.scss @@ -0,0 +1,222 @@ +// Vendor Prefixes +// +// All vendor mixins are deprecated as of v3.2.0 due to the introduction of +// Autoprefixer in our Gruntfile. They have been removed in v4. + +// - Animations +// - Backface visibility +// - Box shadow +// - Box sizing +// - Content columns +// - Hyphens +// - Placeholder text +// - Transformations +// - Transitions +// - User Select + + +// Animations +@mixin animation($animation) { + -webkit-animation: $animation; + -o-animation: $animation; + animation: $animation; + } + @mixin animation-name($name) { + -webkit-animation-name: $name; + animation-name: $name; + } + @mixin animation-duration($duration) { + -webkit-animation-duration: $duration; + animation-duration: $duration; + } + @mixin animation-timing-function($timing-function) { + -webkit-animation-timing-function: $timing-function; + animation-timing-function: $timing-function; + } + @mixin animation-delay($delay) { + -webkit-animation-delay: $delay; + animation-delay: $delay; + } + @mixin animation-iteration-count($iteration-count) { + -webkit-animation-iteration-count: $iteration-count; + animation-iteration-count: $iteration-count; + } + @mixin animation-direction($direction) { + -webkit-animation-direction: $direction; + animation-direction: $direction; + } + @mixin animation-fill-mode($fill-mode) { + -webkit-animation-fill-mode: $fill-mode; + animation-fill-mode: $fill-mode; + } + + // Backface visibility + // Prevent browsers from flickering when using CSS 3D transforms. + // Default value is `visible`, but can be changed to `hidden` + + @mixin backface-visibility($visibility) { + -webkit-backface-visibility: $visibility; + -moz-backface-visibility: $visibility; + backface-visibility: $visibility; + } + + // Drop shadows + // + // Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's + // supported browsers that have box shadow capabilities now support it. + + @mixin box-shadow($shadow...) { + -webkit-box-shadow: $shadow; // iOS <4.3 & Android <4.1 + box-shadow: $shadow; + } + + // Box sizing + @mixin box-sizing($boxmodel) { + -webkit-box-sizing: $boxmodel; + -moz-box-sizing: $boxmodel; + box-sizing: $boxmodel; + } + + // CSS3 Content Columns + @mixin content-columns($column-count, $column-gap: $grid-gutter-width) { + -webkit-column-count: $column-count; + -moz-column-count: $column-count; + column-count: $column-count; + -webkit-column-gap: $column-gap; + -moz-column-gap: $column-gap; + column-gap: $column-gap; + } + + // Optional hyphenation + @mixin hyphens($mode: auto) { + word-wrap: break-word; + -webkit-hyphens: $mode; + -moz-hyphens: $mode; + -ms-hyphens: $mode; // IE10+ + -o-hyphens: $mode; + hyphens: $mode; + } + + // Placeholder text + @mixin placeholder($color: $input-color-placeholder) { + // Firefox + &::-moz-placeholder { + color: $color; + opacity: 1; // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526 + } + &:-ms-input-placeholder { color: $color; } // Internet Explorer 10+ + &::-webkit-input-placeholder { color: $color; } // Safari and Chrome + } + + // Transformations + @mixin scale($ratio...) { + -webkit-transform: scale($ratio); + -ms-transform: scale($ratio); // IE9 only + -o-transform: scale($ratio); + transform: scale($ratio); + } + + @mixin scaleX($ratio) { + -webkit-transform: scaleX($ratio); + -ms-transform: scaleX($ratio); // IE9 only + -o-transform: scaleX($ratio); + transform: scaleX($ratio); + } + @mixin scaleY($ratio) { + -webkit-transform: scaleY($ratio); + -ms-transform: scaleY($ratio); // IE9 only + -o-transform: scaleY($ratio); + transform: scaleY($ratio); + } + @mixin skew($x, $y) { + -webkit-transform: skewX($x) skewY($y); + -ms-transform: skewX($x) skewY($y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+ + -o-transform: skewX($x) skewY($y); + transform: skewX($x) skewY($y); + } + @mixin translate($x, $y) { + -webkit-transform: translate($x, $y); + -ms-transform: translate($x, $y); // IE9 only + -o-transform: translate($x, $y); + transform: translate($x, $y); + } + @mixin translate3d($x, $y, $z) { + -webkit-transform: translate3d($x, $y, $z); + transform: translate3d($x, $y, $z); + } + @mixin rotate($degrees) { + -webkit-transform: rotate($degrees); + -ms-transform: rotate($degrees); // IE9 only + -o-transform: rotate($degrees); + transform: rotate($degrees); + } + @mixin rotateX($degrees) { + -webkit-transform: rotateX($degrees); + -ms-transform: rotateX($degrees); // IE9 only + -o-transform: rotateX($degrees); + transform: rotateX($degrees); + } + @mixin rotateY($degrees) { + -webkit-transform: rotateY($degrees); + -ms-transform: rotateY($degrees); // IE9 only + -o-transform: rotateY($degrees); + transform: rotateY($degrees); + } + @mixin perspective($perspective) { + -webkit-perspective: $perspective; + -moz-perspective: $perspective; + perspective: $perspective; + } + @mixin perspective-origin($perspective) { + -webkit-perspective-origin: $perspective; + -moz-perspective-origin: $perspective; + perspective-origin: $perspective; + } + @mixin transform-origin($origin) { + -webkit-transform-origin: $origin; + -moz-transform-origin: $origin; + -ms-transform-origin: $origin; // IE9 only + transform-origin: $origin; + } + + + // Transitions + + @mixin transition($transition...) { + -webkit-transition: $transition; + -o-transition: $transition; + transition: $transition; + } + @mixin transition-property($transition-property...) { + -webkit-transition-property: $transition-property; + transition-property: $transition-property; + } + @mixin transition-delay($transition-delay) { + -webkit-transition-delay: $transition-delay; + transition-delay: $transition-delay; + } + @mixin transition-duration($transition-duration...) { + -webkit-transition-duration: $transition-duration; + transition-duration: $transition-duration; + } + @mixin transition-timing-function($timing-function) { + -webkit-transition-timing-function: $timing-function; + transition-timing-function: $timing-function; + } + @mixin transition-transform($transition...) { + -webkit-transition: -webkit-transform $transition; + -moz-transition: -moz-transform $transition; + -o-transition: -o-transform $transition; + transition: transform $transition; + } + + + // User select + // For selecting text on the page + + @mixin user-select($select) { + -webkit-user-select: $select; + -moz-user-select: $select; + -ms-user-select: $select; // IE10+ + user-select: $select; + } \ No newline at end of file diff --git a/src/mol-app/skin/bootstrap/normalize.scss b/src/mol-app/skin/bootstrap/normalize.scss new file mode 100644 index 0000000000000000000000000000000000000000..7caf32b8c46f2f2926cdd3f4b86e91fdcf0e425f --- /dev/null +++ b/src/mol-app/skin/bootstrap/normalize.scss @@ -0,0 +1,424 @@ +/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ + +// +// 1. Set default font family to sans-serif. +// 2. Prevent iOS and IE text size adjust after device orientation change, +// without disabling user zoom. +// + +html { + font-family: sans-serif; // 1 + -ms-text-size-adjust: 100%; // 2 + -webkit-text-size-adjust: 100%; // 2 + } + + // + // Remove default margin. + // + + body { + margin: 0; + } + + // HTML5 display definitions + // ========================================================================== + + // + // Correct `block` display not defined for any HTML5 element in IE 8/9. + // Correct `block` display not defined for `details` or `summary` in IE 10/11 + // and Firefox. + // Correct `block` display not defined for `main` in IE 11. + // + + article, + aside, + details, + figcaption, + figure, + footer, + header, + hgroup, + main, + menu, + nav, + section, + summary { + display: block; + } + + // + // 1. Correct `inline-block` display not defined in IE 8/9. + // 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. + // + + audio, + canvas, + progress, + video { + display: inline-block; // 1 + vertical-align: baseline; // 2 + } + + // + // Prevent modern browsers from displaying `audio` without controls. + // Remove excess height in iOS 5 devices. + // + + audio:not([controls]) { + display: none; + height: 0; + } + + // + // Address `[hidden]` styling not present in IE 8/9/10. + // Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22. + // + + [hidden], + template { + display: none; + } + + // Links + // ========================================================================== + + // + // Remove the gray background color from active links in IE 10. + // + + a { + background-color: transparent; + } + + // + // Improve readability of focused elements when they are also in an + // active/hover state. + // + + a:active, + a:hover { + outline: 0; + } + + // Text-level semantics + // ========================================================================== + + // + // Address styling not present in IE 8/9/10/11, Safari, and Chrome. + // + + abbr[title] { + border-bottom: 1px dotted; + } + + // + // Address style set to `bolder` in Firefox 4+, Safari, and Chrome. + // + + b, + strong { + font-weight: bold; + } + + // + // Address styling not present in Safari and Chrome. + // + + dfn { + font-style: italic; + } + + // + // Address variable `h1` font-size and margin within `section` and `article` + // contexts in Firefox 4+, Safari, and Chrome. + // + + h1 { + font-size: 2em; + margin: 0.67em 0; + } + + // + // Address styling not present in IE 8/9. + // + + mark { + background: #ff0; + color: #000; + } + + // + // Address inconsistent and variable font size in all browsers. + // + + small { + font-size: 80%; + } + + // + // Prevent `sub` and `sup` affecting `line-height` in all browsers. + // + + sub, + sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + } + + sup { + top: -0.5em; + } + + sub { + bottom: -0.25em; + } + + // Embedded content + // ========================================================================== + + // + // Remove border when inside `a` element in IE 8/9/10. + // + + img { + border: 0; + } + + // + // Correct overflow not hidden in IE 9/10/11. + // + + svg:not(:root) { + overflow: hidden; + } + + // Grouping content + // ========================================================================== + + // + // Address margin not present in IE 8/9 and Safari. + // + + figure { + margin: 1em 40px; + } + + // + // Address differences between Firefox and other browsers. + // + + hr { + box-sizing: content-box; + height: 0; + } + + // + // Contain overflow in all browsers. + // + + pre { + overflow: auto; + } + + // + // Address odd `em`-unit font size rendering in all browsers. + // + + code, + kbd, + pre, + samp { + font-family: monospace, monospace; + font-size: 1em; + } + + // Forms + // ========================================================================== + + // + // Known limitation: by default, Chrome and Safari on OS X allow very limited + // styling of `select`, unless a `border` property is set. + // + + // + // 1. Correct color not being inherited. + // Known issue: affects color of disabled elements. + // 2. Correct font properties not being inherited. + // 3. Address margins set differently in Firefox 4+, Safari, and Chrome. + // + + button, + input, + optgroup, + select, + textarea { + color: inherit; // 1 + font: inherit; // 2 + margin: 0; // 3 + } + + // + // Address `overflow` set to `hidden` in IE 8/9/10/11. + // + + button { + overflow: visible; + } + + // + // Address inconsistent `text-transform` inheritance for `button` and `select`. + // All other form control elements do not inherit `text-transform` values. + // Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. + // Correct `select` style inheritance in Firefox. + // + + button, + select { + text-transform: none; + } + + // + // 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` + // and `video` controls. + // 2. Correct inability to style clickable `input` types in iOS. + // 3. Improve usability and consistency of cursor style between image-type + // `input` and others. + // + + button, + html input[type="button"], // 1 + input[type="reset"], + input[type="submit"] { + -webkit-appearance: button; // 2 + cursor: pointer; // 3 + } + + // + // Re-set default cursor for disabled elements. + // + + button[disabled], + html input[disabled] { + cursor: default; + } + + // + // Remove inner padding and border in Firefox 4+. + // + + button::-moz-focus-inner, + input::-moz-focus-inner { + border: 0; + padding: 0; + } + + // + // Address Firefox 4+ setting `line-height` on `input` using `!important` in + // the UA stylesheet. + // + + input { + line-height: normal; + } + + // + // It's recommended that you don't attempt to style these elements. + // Firefox's implementation doesn't respect box-sizing, padding, or width. + // + // 1. Address box sizing set to `content-box` in IE 8/9/10. + // 2. Remove excess padding in IE 8/9/10. + // + + input[type="checkbox"], + input[type="radio"] { + box-sizing: border-box; // 1 + padding: 0; // 2 + } + + // + // Fix the cursor style for Chrome's increment/decrement buttons. For certain + // `font-size` values of the `input`, it causes the cursor style of the + // decrement button to change from `default` to `text`. + // + + input[type="number"]::-webkit-inner-spin-button, + input[type="number"]::-webkit-outer-spin-button { + height: auto; + } + + // + // 1. Address `appearance` set to `searchfield` in Safari and Chrome. + // 2. Address `box-sizing` set to `border-box` in Safari and Chrome. + // + + input[type="search"] { + -webkit-appearance: textfield; // 1 + box-sizing: content-box; //2 + } + + // + // Remove inner padding and search cancel button in Safari and Chrome on OS X. + // Safari (but not Chrome) clips the cancel button when the search input has + // padding (and `textfield` appearance). + // + + input[type="search"]::-webkit-search-cancel-button, + input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; + } + + // + // Define consistent border, margin, and padding. + // + + fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; + } + + // + // 1. Correct `color` not being inherited in IE 8/9/10/11. + // 2. Remove padding so people aren't caught out if they zero out fieldsets. + // + + legend { + border: 0; // 1 + padding: 0; // 2 + } + + // + // Remove default vertical scrollbar in IE 8/9/10/11. + // + + textarea { + overflow: auto; + } + + // + // Don't inherit the `font-weight` (applied by a rule above). + // NOTE: the default cannot safely be changed in Chrome and Safari on OS X. + // + + optgroup { + font-weight: bold; + } + + // Tables + // ========================================================================== + + // + // Remove most spacing between table cells. + // + + table { + border-collapse: collapse; + border-spacing: 0; + } + + td, + th { + padding: 0; + } \ No newline at end of file diff --git a/src/mol-app/skin/bootstrap/scaffolding.scss b/src/mol-app/skin/bootstrap/scaffolding.scss new file mode 100644 index 0000000000000000000000000000000000000000..556b6acdfe77bec08b388e8ecf27bf86633fe3f9 --- /dev/null +++ b/src/mol-app/skin/bootstrap/scaffolding.scss @@ -0,0 +1,161 @@ +// +// Scaffolding +// -------------------------------------------------- + + +// Reset the box-sizing +// +// Heads up! This reset may cause conflicts with some third-party widgets. +// For recommendations on resolving such conflicts, see +// http://getbootstrap.com/getting-started/#third-box-sizing +* { + @include box-sizing(border-box); + } + *:before, + *:after { + @include box-sizing(border-box); + } + + + // Body reset + + html { + font-size: 10px; + -webkit-tap-highlight-color: rgba(0,0,0,0); + } + + body { + font-family: $font-family-base; + font-size: $font-size-base; + line-height: $line-height-base; + color: $text-color; + background-color: $body-bg; + } + + // Reset fonts for relevant elements + input, + button, + select, + textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; + } + + + // Links + + a { + color: $link-color; + text-decoration: none; + + &:hover, + &:focus { + color: $link-hover-color; + text-decoration: $link-hover-decoration; + } + + &:focus { + @include tab-focus; + } + } + + + // Figures + // + // We reset this here because previously Normalize had no `figure` margins. This + // ensures we don't break anyone's use of the element. + + figure { + margin: 0; + } + + + // Images + + img { + vertical-align: middle; + } + + // Responsive images (ensure images don't scale beyond their parents) + .img-responsive { + @include img-responsive; + } + + // Rounded corners + .img-rounded { + border-radius: $border-radius-large; + } + + // Image thumbnails + // + // Heads up! This is mixin-ed into thumbnails.less for `.thumbnail`. + .img-thumbnail { + padding: $thumbnail-padding; + line-height: $line-height-base; + background-color: $thumbnail-bg; + border: 1px solid $thumbnail-border; + border-radius: $thumbnail-border-radius; + @include transition(all .2s ease-in-out); + + // Keep them at most 100% wide + @include img-responsive(inline-block); + } + + // Perfect circle + .img-circle { + border-radius: 50%; // set radius in percents + } + + + // Horizontal rules + + hr { + margin-top: $line-height-computed; + margin-bottom: $line-height-computed; + border: 0; + border-top: 1px solid $hr-border; + } + + + // Only display content to screen readers + // + // See: http://a11yproject.com/posts/how-to-hide-content/ + + .sr-only { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip: rect(0,0,0,0); + border: 0; + } + + // Use in conjunction with .sr-only to only display content when it's focused. + // Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 + // Credit: HTML5 Boilerplate + + .sr-only-focusable { + &:active, + &:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; + } + } + + + // iOS "clickable elements" fix for role="button" + // + // Fixes "clickability" issue (and more generally, the firing of events such as focus as well) + // for traditionally non-focusable elements with role="button" + // see https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile + + [role="button"] { + cursor: pointer; + } \ No newline at end of file diff --git a/src/mol-app/skin/bootstrap/type.scss b/src/mol-app/skin/bootstrap/type.scss new file mode 100644 index 0000000000000000000000000000000000000000..92c0b63cac4e9c378be6c6fa1a41960fb5269634 --- /dev/null +++ b/src/mol-app/skin/bootstrap/type.scss @@ -0,0 +1,298 @@ +// +// Typography +// -------------------------------------------------- + + +// Headings +// ------------------------- + +h1, h2, h3, h4, h5, h6, +.h1, .h2, .h3, .h4, .h5, .h6 { + font-family: $headings-font-family; + font-weight: $headings-font-weight; + line-height: $headings-line-height; + color: $headings-color; + + small, + .small { + font-weight: normal; + line-height: 1; + color: $headings-small-color; + } +} + +h1, .h1, +h2, .h2, +h3, .h3 { + margin-top: $line-height-computed; + margin-bottom: ($line-height-computed / 2); + + small, + .small { + font-size: 65%; + } +} +h4, .h4, +h5, .h5, +h6, .h6 { + margin-top: ($line-height-computed / 2); + margin-bottom: ($line-height-computed / 2); + + small, + .small { + font-size: 75%; + } +} + +h1, .h1 { font-size: $font-size-h1; } +h2, .h2 { font-size: $font-size-h2; } +h3, .h3 { font-size: $font-size-h3; } +h4, .h4 { font-size: $font-size-h4; } +h5, .h5 { font-size: $font-size-h5; } +h6, .h6 { font-size: $font-size-h6; } + + +// Body text +// ------------------------- + +p { + margin: 0 0 ($line-height-computed / 2); +} + +.lead { + margin-bottom: $line-height-computed; + font-size: floor(($font-size-base * 1.15)); + font-weight: 300; + line-height: 1.4; + + @media (min-width: $screen-sm-min) { + font-size: ($font-size-base * 1.5); + } +} + + +// Emphasis & misc +// ------------------------- + +// Ex: (12px small font / 14px base font) * 100% = about 85% +small, +.small { + font-size: floor((100% * $font-size-small / $font-size-base)); +} + +mark, +.mark { + background-color: $state-warning-bg; + padding: .2em; +} + +// Alignment +.text-left { text-align: left; } +.text-right { text-align: right; } +.text-center { text-align: center; } +.text-justify { text-align: justify; } +.text-nowrap { white-space: nowrap; } + +// Transformation +.text-lowercase { text-transform: lowercase; } +.text-uppercase { text-transform: uppercase; } +.text-capitalize { text-transform: capitalize; } + +// Contextual colors +.text-muted { + color: $text-muted; +} + +@include text-emphasis-variant('.text-primary', $brand-primary); + +@include text-emphasis-variant('.text-success', $state-success-text); + +@include text-emphasis-variant('.text-info', $state-info-text); + +@include text-emphasis-variant('.text-warning', $state-warning-text); + +@include text-emphasis-variant('.text-danger', $state-danger-text); + +// Contextual backgrounds +// For now we'll leave these alongside the text classes until v4 when we can +// safely shift things around (per SemVer rules). +.bg-primary { + // Given the contrast here, this is the only class to have its color inverted + // automatically. + color: #fff; +} +@include bg-variant('.bg-primary', $brand-primary); + +@include bg-variant('.bg-success', $state-success-bg); + +@include bg-variant('.bg-info', $state-info-bg); + +@include bg-variant('.bg-warning', $state-warning-bg); + +@include bg-variant('.bg-danger', $state-danger-bg); + + +// Page header +// ------------------------- + +.page-header { + padding-bottom: (($line-height-computed / 2) - 1); + margin: ($line-height-computed * 2) 0 $line-height-computed; + border-bottom: 1px solid $page-header-border-color; +} + + +// Lists +// ------------------------- + +// Unordered and Ordered lists +ul, +ol { + margin-top: 0; + margin-bottom: ($line-height-computed / 2); + ul, + ol { + margin-bottom: 0; + } +} + +// List options + +// [converter] extracted from `.lm-list-unstyled` for libsass compatibility +@mixin lm-list-unstyled { + padding-left: 0; + list-style: none; +} +// [converter] extracted as `@mixin lm-list-unstyled` for libsass compatibility +.lm-list-unstyled { + @include lm-list-unstyled; +} + + +// Inline turns list items into inline-block +.list-inline { + @include lm-list-unstyled; + margin-left: -5px; + + > li { + display: inline-block; + padding-left: 5px; + padding-right: 5px; + } +} + +// Description Lists +dl { + margin-top: 0; // Remove browser default + margin-bottom: $line-height-computed; +} +dt, +dd { + line-height: $line-height-base; +} +dt { + font-weight: bold; +} +dd { + margin-left: 0; // Undo browser default +} + +// Horizontal description lists +// +// Defaults to being stacked without any of the below styles applied, until the +// grid breakpoint is reached (default of ~768px). + +.dl-horizontal { + dd { + @include clearfix; // Clear the floated `dt` if an empty `dd` is present + } + + @media (min-width: $dl-horizontal-breakpoint) { + dt { + float: left; + width: ($dl-horizontal-offset - 20); + clear: left; + text-align: right; + @include text-overflow; + } + dd { + margin-left: $dl-horizontal-offset; + } + } +} + + +// Misc +// ------------------------- + +// Abbreviations and acronyms +abbr[title], +// Add data-* attribute to help out our tooltip plugin, per https://github.com/twbs/bootstrap/issues/5257 +abbr[data-original-title] { + cursor: help; + border-bottom: 1px dotted $abbr-border-color; +} +.initialism { + font-size: 90%; + @extend .text-uppercase; +} + +// Blockquotes +blockquote { + padding: ($line-height-computed / 2) $line-height-computed; + margin: 0 0 $line-height-computed; + font-size: $blockquote-font-size; + border-left: 5px solid $blockquote-border-color; + + p, + ul, + ol { + &:last-child { + margin-bottom: 0; + } + } + + // Note: Deprecated small and .small as of v3.1.0 + // Context: https://github.com/twbs/bootstrap/issues/11660 + footer, + small, + .small { + display: block; + font-size: 80%; // back to default font-size + line-height: $line-height-base; + color: $blockquote-small-color; + + &:before { + content: '\2014 \00A0'; // em dash, nbsp + } + } +} + +// Opposite alignment of blockquote +// +// Heads up: `blockquote.pull-right` has been deprecated as of v3.1.0. +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + border-right: 5px solid $blockquote-border-color; + border-left: 0; + text-align: right; + + // Account for citation + footer, + small, + .small { + &:before { content: ''; } + &:after { + content: '\00A0 \2014'; // nbsp, em dash + } + } +} + +// Addresses +address { + margin-bottom: $line-height-computed; + font-style: normal; + line-height: $line-height-base; +} \ No newline at end of file diff --git a/src/mol-app/skin/bootstrap/variables.scss b/src/mol-app/skin/bootstrap/variables.scss new file mode 100644 index 0000000000000000000000000000000000000000..ead0db3c4d7894471005eef2ce9f105ae82fa1b3 --- /dev/null +++ b/src/mol-app/skin/bootstrap/variables.scss @@ -0,0 +1,353 @@ +//== Colors +// +//## Gray and brand colors for use across Bootstrap. + +$gray-base: #000 !default; +$gray-darker: lighten($gray-base, 13.5%) !default; // #222 +$gray-dark: lighten($gray-base, 20%) !default; // #333 +$gray: lighten($gray-base, 33.5%) !default; // #555 +$gray-light: lighten($gray-base, 46.7%) !default; // #777 +$gray-lighter: lighten($gray-base, 93.5%) !default; // #eee + +$brand-primary: darken(#428bca, 6.5%) !default; // #337ab7 +$brand-success: #5cb85c !default; +$brand-info: #5bc0de !default; +$brand-warning: #f0ad4e !default; +$brand-danger: #d9534f !default; + + +//== Scaffolding +// +//## Settings for some of the most global styles. + +//** Background color for `<body>`. +$body-bg: #fff !default; +//** Global text color on `<body>`. +$text-color: $gray-dark !default; + +//** Global textual link color. +$link-color: $brand-primary !default; +//** Link hover color set via `darken()` function. +$link-hover-color: darken($link-color, 15%) !default; +//** Link hover decoration. +$link-hover-decoration: underline !default; + + +//== Typography +// +//## Font, line-height, and color for body text, headings, and more. + +$font-family-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif !default; +$font-family-serif: Georgia, "Times New Roman", Times, serif !default; +//** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`. +$font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace !default; +$font-family-base: $font-family-sans-serif !default; + +$font-size-base: 14px !default; +$font-size-large: ceil(($font-size-base * 1.25)) !default; // ~18px +$font-size-small: ceil(($font-size-base * 0.85)) !default; // ~12px + +$font-size-h1: floor(($font-size-base * 2.6)) !default; // ~36px +$font-size-h2: floor(($font-size-base * 2.15)) !default; // ~30px +$font-size-h3: ceil(($font-size-base * 1.7)) !default; // ~24px +$font-size-h4: ceil(($font-size-base * 1.25)) !default; // ~18px +$font-size-h5: $font-size-base !default; +$font-size-h6: ceil(($font-size-base * 0.85)) !default; // ~12px + +//** Unit-less `line-height` for use in components like buttons. +$line-height-base: 1.428571429 !default; // 20/14 +//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc. +$line-height-computed: floor(($font-size-base * $line-height-base)) !default; // ~20px + +//** By default, this inherits from the `<body>`. +$headings-font-family: inherit !default; +$headings-font-weight: 500 !default; +$headings-line-height: 1.1 !default; +$headings-color: inherit !default; + + +//== Components +// +//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start). + +$padding-base-vertical: 6px !default; +$padding-base-horizontal: 12px !default; + +$padding-large-vertical: 10px !default; +$padding-large-horizontal: 16px !default; + +$padding-small-vertical: 5px !default; +$padding-small-horizontal: 10px !default; + +$padding-xs-vertical: 1px !default; +$padding-xs-horizontal: 5px !default; + +$line-height-large: 1.3333333 !default; // extra decimals for Win 8.1 Chrome +$line-height-small: 1.5 !default; + +$border-radius-base: 4px !default; +$border-radius-large: 6px !default; +$border-radius-small: 3px !default; + +//** Global color for active items (e.g., navs or dropdowns). +$component-active-color: #fff !default; +//** Global background color for active items (e.g., navs or dropdowns). +$component-active-bg: $brand-primary !default; + +//** Width of the `border` for generating carets that indicator dropdowns. +$caret-width-base: 4px !default; +//** Carets increase slightly in size for larger components. +$caret-width-large: 5px !default; + + +//== Buttons +// +//## For each of Bootstrap's buttons, define text, background and border color. + +$molstar-btn-font-weight: normal !default; + +$molstar-btn-default-color: #333 !default; +$molstar-btn-default-bg: #fff !default; +$molstar-btn-default-border: #ccc !default; + +$molstar-btn-primary-color: #fff !default; +$molstar-btn-primary-bg: $brand-primary !default; +$molstar-btn-primary-border: darken($molstar-btn-primary-bg, 5%) !default; + +$molstar-btn-success-color: #fff !default; +$molstar-btn-success-bg: $brand-success !default; +$molstar-btn-success-border: darken($molstar-btn-success-bg, 5%) !default; + +$molstar-btn-info-color: #fff !default; +$molstar-btn-info-bg: $brand-info !default; +$molstar-btn-info-border: darken($molstar-btn-info-bg, 5%) !default; + +$molstar-btn-warning-color: #fff !default; +$molstar-btn-warning-bg: $brand-warning !default; +$molstar-btn-warning-border: darken($molstar-btn-warning-bg, 5%) !default; + +$molstar-btn-danger-color: #fff !default; +$molstar-btn-danger-bg: $brand-danger !default; +$molstar-btn-danger-border: darken($molstar-btn-danger-bg, 5%) !default; + +$molstar-btn-link-disabled-color: $gray-light !default; + +// Allows for customizing button radius independently from global border radius +$molstar-btn-border-radius-base: $border-radius-base !default; +$molstar-btn-border-radius-large: $border-radius-large !default; +$molstar-btn-border-radius-small: $border-radius-small !default; + + +//== Media queries breakpoints +// +//## Define the breakpoints at which your layout will change, adapting to different screen sizes. + +// Extra small screen / phone +//** Deprecated `$screen-xs` as of v3.0.1 +$screen-xs: 480px !default; +//** Deprecated `$screen-xs-min` as of v3.2.0 +$screen-xs-min: $screen-xs !default; +//** Deprecated `$screen-phone` as of v3.0.1 +$screen-phone: $screen-xs-min !default; + +// Small screen / tablet +//** Deprecated `$screen-sm` as of v3.0.1 +$screen-sm: 768px !default; +$screen-sm-min: $screen-sm !default; +//** Deprecated `$screen-tablet` as of v3.0.1 +$screen-tablet: $screen-sm-min !default; + +// Medium screen / desktop +//** Deprecated `$screen-md` as of v3.0.1 +$screen-md: 992px !default; +$screen-md-min: $screen-md !default; +//** Deprecated `$screen-desktop` as of v3.0.1 +$screen-desktop: $screen-md-min !default; + +// Large screen / wide desktop +//** Deprecated `$screen-lg` as of v3.0.1 +$screen-lg: 1200px !default; +$screen-lg-min: $screen-lg !default; +//** Deprecated `$screen-lg-desktop` as of v3.0.1 +$screen-lg-desktop: $screen-lg-min !default; + +// So media queries don't overlap when required, provide a maximum +$screen-xs-max: ($screen-sm-min - 1) !default; +$screen-sm-max: ($screen-md-min - 1) !default; +$screen-md-max: ($screen-lg-min - 1) !default; + + +//== Grid system +// +//## Define your custom responsive grid. + +//** Number of columns in the grid. +$grid-columns: 12 !default; +//** Padding between columns. Gets divided in half for the left and right. +$grid-gutter-width: 30px !default; +// Navbar collapse +//** Point at which the navbar becomes uncollapsed. +$grid-float-breakpoint: $screen-sm-min !default; +//** Point at which the navbar begins collapsing. +$grid-float-breakpoint-max: ($grid-float-breakpoint - 1) !default; + + +//== Forms +// +//## + +//** `<input>` background color +$input-bg: #fff !default; +//** `<input disabled>` background color +$input-bg-disabled: $gray-lighter !default; + +//** Text color for `<input>`s +$input-color: $gray !default; +//** `<input>` border color +$input-border: #ccc !default; + +// TODO: Rename `$input-border-radius` to `$input-border-radius-base` in v4 +//** Default `.lm-form-control` border radius +// This has no effect on `<select>`s in some browsers, due to the limited stylability of `<select>`s in CSS. +$input-border-radius: $border-radius-base !default; +//** Large `.lm-form-control` border radius +$input-border-radius-large: $border-radius-large !default; +//** Small `.lm-form-control` border radius +$input-border-radius-small: $border-radius-small !default; + +//** Border color for inputs on focus +$input-border-focus: #66afe9 !default; + +//** Placeholder text color +$input-color-placeholder: #999 !default; + +//** Default `.lm-form-control` height +$input-height-base: ($line-height-computed + ($padding-base-vertical * 2) + 2) !default; +//** Large `.lm-form-control` height +$input-height-large: (ceil($font-size-large * $line-height-large) + ($padding-large-vertical * 2) + 2) !default; +//** Small `.lm-form-control` height +$input-height-small: (floor($font-size-small * $line-height-small) + ($padding-small-vertical * 2) + 2) !default; + +//** `.form-group` margin +$form-group-margin-bottom: 15px !default; + +$legend-color: $gray-dark !default; +$legend-border-color: #e5e5e5 !default; + +//** Background color for textual input addons +$input-group-addon-bg: $gray-lighter !default; +//** Border color for textual input addons +$input-group-addon-border-color: $input-border !default; + +//** Disabled cursor for form controls and buttons. +$cursor-disabled: not-allowed !default; + + +//== Thumbnails +// +//## + +//** Padding around the thumbnail image +$thumbnail-padding: 4px !default; +//** Thumbnail background color +$thumbnail-bg: $body-bg !default; +//** Thumbnail border color +$thumbnail-border: #ddd !default; +//** Thumbnail border radius +$thumbnail-border-radius: $border-radius-base !default; + +//** Custom text color for thumbnail captions +$thumbnail-caption-color: $text-color !default; +//** Padding around the thumbnail caption +$thumbnail-caption-padding: 9px !default; + + +//== Type +// +//## + +//** Horizontal offset for forms and lists. +$component-offset-horizontal: 180px !default; +//** Text muted color +$text-muted: $gray-light !default; +//** Abbreviations and acronyms border color +$abbr-border-color: $gray-light !default; +//** Headings small color +$headings-small-color: $gray-light !default; +//** Blockquote small color +$blockquote-small-color: $gray-light !default; +//** Blockquote font size +$blockquote-font-size: ($font-size-base * 1.25) !default; +//** Blockquote border color +$blockquote-border-color: $gray-lighter !default; +//** Page header border color +$page-header-border-color: $gray-lighter !default; +//** Width of horizontal description list titles +$dl-horizontal-offset: $component-offset-horizontal !default; +//** Point at which .dl-horizontal becomes horizontal +$dl-horizontal-breakpoint: $grid-float-breakpoint !default; +//** Horizontal line color. +$hr-border: $gray-lighter !default; + + +//== Form states and alerts +// +//## Define colors for form feedback states and, by default, alerts. + +$state-success-text: #3c763d !default; +$state-success-bg: #dff0d8 !default; +$state-success-border: darken(adjust-hue($state-success-bg, -10), 5%) !default; + +$state-info-text: #31708f !default; +$state-info-bg: #d9edf7 !default; +$state-info-border: darken(adjust-hue($state-info-bg, -10), 7%) !default; + +$state-warning-text: #8a6d3b !default; +$state-warning-bg: #fcf8e3 !default; +$state-warning-border: darken(adjust-hue($state-warning-bg, -10), 5%) !default; + +$state-danger-text: #a94442 !default; +$state-danger-bg: #f2dede !default; +$state-danger-border: darken(adjust-hue($state-danger-bg, -10), 5%) !default; + + +//== Labels +// +//## + +//** Default label background color +$label-default-bg: $gray-light !default; +//** Primary label background color +$label-primary-bg: $brand-primary !default; +//** Success label background color +$label-success-bg: $brand-success !default; +//** Info label background color +$label-info-bg: $brand-info !default; +//** Warning label background color +$label-warning-bg: $brand-warning !default; +//** Danger label background color +$label-danger-bg: $brand-danger !default; + +//** Default label text color +$label-color: #fff !default; +//** Default text color of a linked label +$label-link-hover-color: #fff !default; + + +//== Badges +// +//## + +$badge-color: #fff !default; +//** Linked badge text color on hover +$badge-link-hover-color: #fff !default; +$badge-bg: $gray-light !default; + +//** Badge text color in active nav link +$badge-active-color: $link-color !default; +//** Badge background color in active nav link +$badge-active-bg: #fff !default; + +$badge-font-weight: bold !default; +$badge-line-height: 1 !default; +$badge-border-radius: 10px !default; \ No newline at end of file diff --git a/src/mol-app/skin/colors/blue.scss b/src/mol-app/skin/colors/blue.scss new file mode 100644 index 0000000000000000000000000000000000000000..c31a02bd7acd89bebce6bb407ade29934725f3e5 --- /dev/null +++ b/src/mol-app/skin/colors/blue.scss @@ -0,0 +1,24 @@ +$default-background: #2D3E50; +$font-color: #EDF1F2; +$hover-font-color: #3B9AD9; +$entity-current-font-color: #FFFFFF; +$lm-btn-remove-background: #BF3A31; +$lm-btn-remove-hover-font-color:#ffffff; +$lm-btn-commit-on-font-color: #ffffff; +$entity-badge-font-color: #ccd4e0; + +// used in LOG +$log-message: #0CCA5D; +$log-info: #5E3673; +$log-warning: #FCC937; +$log-error: #FD354B; + +$logo-background: rgba(0,0,0,0.75); + +@function color-lower-contrast($color, $amount) { + @return darken($color, $amount); +} + +@function color-increase-contrast($color, $amount) { + @return lighten($color, $amount); +} \ No newline at end of file diff --git a/src/mol-app/skin/colors/dark.scss b/src/mol-app/skin/colors/dark.scss new file mode 100644 index 0000000000000000000000000000000000000000..6df758473092906b8ceeb171213a18daec8330af --- /dev/null +++ b/src/mol-app/skin/colors/dark.scss @@ -0,0 +1,22 @@ +$default-background: #111318; +$font-color: #ccd4e0; +$hover-font-color: #51A2FB; +$entity-current-font-color: #68BEFD; +$molstar-btn-remove-background: #DE0A28; +$molstar-btn-remove-hover-font-color:#F2F4F7; +$molstar-btn-commit-on-font-color: #68BEFD; +$entity-badge-font-color: #ccd4e0; + +// used in LOG +$log-message: #0CCA5D; +$log-info: #5E3673; +$log-warning: #FCC937; +$log-error: #FD354B; + +@function color-lower-contrast($color, $amount) { + @return darken($color, $amount); +} + +@function color-increase-contrast($color, $amount) { + @return lighten($color, $amount); +} \ No newline at end of file diff --git a/src/mol-app/skin/colors/light.scss b/src/mol-app/skin/colors/light.scss new file mode 100644 index 0000000000000000000000000000000000000000..678b744d5ba2a392d60a23262e955e7f1d28bbc9 --- /dev/null +++ b/src/mol-app/skin/colors/light.scss @@ -0,0 +1,30 @@ +// this is complement of the dark theme + +@function compl($color) { + @return rgb(255 - red($color), 255 - green($color), 255 - blue($color)); +} + +$default-background: compl(#111318); +$font-color: compl(#ccd4e0); +$hover-font-color: compl(#51A2FB); +$entity-current-font-color: compl(#68BEFD); +$molstar-btn-commit-on-font-color: compl(#68BEFD); +$entity-badge-font-color: lighten(#ccd4e0,10%); +$molstar-btn-remove-background: #DE0A28; +$molstar-btn-remove-hover-font-color:#F2F4F7; + +// used in LOG +$log-message: #0CCA5D; +$log-info: #5E3673; +$log-warning: #FCC937; +$log-error: #FD354B; + +$logo-background: rgba(204,201,193,0.85); + +@function color-lower-contrast($color, $amount) { + @return lighten($color, $amount); +} + +@function color-increase-contrast($color, $amount) { + @return darken($color, $amount); +} \ No newline at end of file diff --git a/src/mol-app/skin/components/controls-base.scss b/src/mol-app/skin/components/controls-base.scss new file mode 100644 index 0000000000000000000000000000000000000000..e1f17a6837df972004d8b8feb8a88383884950b2 --- /dev/null +++ b/src/mol-app/skin/components/controls-base.scss @@ -0,0 +1,144 @@ +.molstar-btn { + padding: 0 $control-spacing; + line-height: $row-height; + border: none; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.molstar-btn, .molstar-btn:active, .molstar-btn-link:focus, .molstar-btn:hover { + outline: none !important; +} + +.molstar-btn-icon { + height: $row-height; + width: $row-height; + line-height: $row-height; + padding: 0; + text-align: center; +} + +.molstar-btn-link { + .molstar-icon { + font-size: 100%; + } +} + +.molstar-btn-link, .molstar-btn-link:active, .molstar-btn-link:focus { + color: $molstar-btn-link-font-color; + text-decoration: none; +} + +.molstar-btn-link:hover { + color: $hover-font-color; + text-decoration: none; +} + +.molstar-btn-link-toggle-on { + color: $molstar-btn-link-toggle-on-font-color; +} + +.molstar-btn-link-toggle-off, .molstar-btn-link-toggle-off:active, .molstar-btn-link-toggle-off:focus { + color: $molstar-btn-link-toggle-off-font-color; +} + +.molstar-btn-link-toggle-off:hover, .molstar-btn-link-toggle-on:hover { + color: $hover-font-color; +} + +@mixin molstar-btn($name, $font, $bg) { + .molstar-btn-#{$name}, .molstar-btn-#{$name}:active, .molstar-btn-#{$name}:focus { + color: $font; + background: $bg; + } + .molstar-btn-#{$name}:hover { + color: $hover-font-color; + background: color-lower-contrast($bg, 2.5%); + } + + .molstar-btn-#{$name}[disabled], .molstar-btn-#{$name}[disabled]:hover, + .molstar-btn-#{$name}[disabled]:active, .molstar-btn-#{$name}[disabled]:focus { + color: color-lower-contrast($font, 1%); + } +} + +@include molstar-btn('remove', $molstar-btn-remove-font-color, $molstar-btn-remove-background); +@include molstar-btn('action', $font-color, $molstar-btn-action-background); +@include molstar-btn('commit-on', $molstar-btn-commit-on-font-color, $molstar-btn-commit-on-background); +@include molstar-btn('commit-off', $molstar-btn-commit-off-font-color, $molstar-btn-commit-off-background); + +.molstar-btn-remove:hover { + color: $molstar-btn-remove-hover-font-color; +} +.molstar-btn-commit-on:hover { + color: $molstar-btn-commit-on-hover-font-color; +} + +.molstar-btn-action { + height: $row-height; + line-height: $row-height; +} + +.molstar-form-control { + width: 100%; + background: $molstar-form-control-background; + color: $font-color; + border: none !important; + padding: 0 $control-spacing; + line-height: $row-height - 2px; + height: $row-height; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + box-shadow: none !important; + + &:hover { + color: $hover-font-color; + background-color: color-increase-contrast($molstar-form-control-background, 5%); + border: none; + outline-offset: -1px; + outline: 1px solid color-increase-contrast($molstar-form-control-background, 20%); + } + + &:active, &:focus { + color: $font-color; + background-color: $molstar-form-control-background; + border: none; + outline-offset: 0; + outline: none; + } +} + +.molstar-btn-commit { + text-align: right; + padding-top: 0; + padding-bottom: 0; + padding-right: $control-spacing; + padding-left: 0; + line-height: $row-height; + border: none; + overflow: hidden; + + .molstar-icon { + display: block-inline; + line-height: $row-height; + margin-right: $control-spacing; + width: $row-height; + text-align: center; + float: left; + } +} + +select.molstar-form-control { + background: none; + background-color: $molstar-form-control-background; + background-size: 8px 12px; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAUCAMAAACzvE1FAAAADFBMVEUzMzMzMzMzMzMzMzMKAG/3AAAAA3RSTlMAf4C/aSLHAAAAPElEQVR42q3NMQ4AIAgEQTn//2cLdRKppSGzBYwzVXvznNWs8C58CiussPJj8h6NwgorrKRdTvuV9v16Afn0AYFOB7aYAAAAAElFTkSuQmCC); + background-repeat: no-repeat; + background-position: right $control-spacing top (($row-height - 12px) / 2); +} + +select.molstar-form-control:-moz-focusring { + color: transparent; + text-shadow: 0 0 0 $font-color; +} \ No newline at end of file diff --git a/src/mol-app/skin/components/controls.scss b/src/mol-app/skin/components/controls.scss new file mode 100644 index 0000000000000000000000000000000000000000..2c8aeda0a62cc83464d9dfc128dcf5a1e7f4f1aa --- /dev/null +++ b/src/mol-app/skin/components/controls.scss @@ -0,0 +1,197 @@ + +.molstar-control-row { + position: relative; + height: $row-height; + background: $default-background; + margin-top: 1px; + + > span { + line-height: $row-height; + display: block; + width: $control-label-width + $control-spacing; + text-align: right; + padding: 0 $control-spacing; + color: color-lower-contrast($font-color, 15%); + + @include non-selectable; + } + + select, button, input[type=text] { + @extend .molstar-form-control; + } + + button { + @extend .molstar-btn; + @extend .molstar-btn-block; + } + + > div:nth-child(2) { + background: $molstar-form-control-background; + position: absolute; + left: $control-label-width + $control-spacing; + top: 0; + right: 0; + bottom: 0; + } +} + +.molstar-control-group { + position: relative; +} + +.molstar-toggle-button { + .molstar-icon { + display: inline-block; + margin-right: 6px; + } + + > div > button:hover { + border-color: color-increase-contrast($molstar-form-control-background, 5%) !important; + border: none; + outline-offset: -1px !important; + outline: 1px solid color-increase-contrast($molstar-form-control-background, 20%) !important; + } +} + +.molstar-slider { + > div { + > div:first-child { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + width: 100%; + padding-right: 50px; + display: table; + + > div { + height: $row-height; + display: table-cell; + vertical-align: middle; + padding: 0 ($control-spacing + 4px); + } + } + > div:last-child { + position: absolute; + height: $row-height; + right: 0; + width: 50px; + top: 0; + bottom: 0; + } + } + + input[type=text] { + text-align: right; + } + + input[type=range] { + width: 100%; + } +} + +.molstar-toggle-color-picker { + button { + border: $control-spacing solid $molstar-form-control-background !important; + margin: 0; + text-align: center; + padding-right: $control-spacing; + padding-left: $control-spacing; + + &:hover { + border-color: color-increase-contrast($molstar-form-control-background, 5%) !important; + border: none; + outline-offset: -1px !important; + outline: 1px solid color-increase-contrast($molstar-form-control-background, 20%) !important; + } + } + + .molstar-color-picker { + position: absolute; + z-index: 100000; + background: $default-background; + border-top: 1px solid $default-background; + padding-bottom: $control-spacing / 2; + width: 100%; + + // input[type=text] { + // background: $molstar-form-control-background !important; + // } + } +} + +.molstar-toggle-color-picker-above { + .molstar-color-picker { + top: -2 * 32px - 16px - $control-spacing / 2; + height: 2 * 32px + 16px + $control-spacing / 2; + } +} + +.molstar-toggle-color-picker-below { + .molstar-color-picker { + top: $row-height; + height: 2 * 32px + 16px; + } +} + + +.molstar-control-subgroup { + margin-top: 1px; + + .molstar-control-row { + margin-left: $control-spacing !important; + > span { + width: $control-label-width !important; + } + + > div:nth-child(2) { + left: $control-label-width !important; + } + } +} + +.molstar-conrol-group-expander { + display: block; + position: absolute; + line-height: $row-height; + padding: 0; + left: 0; + top: 0; + width: $control-label-width + $control-spacing; + text-align: left; + + .molstar-icon { + line-height: $row-height - 3; + width: $row-height - 1; + text-align: center; + display: inline-block; + font-size: 100%; + } +} + +.molstar-plugin-layout_controls { + position: absolute; + left: $control-spacing; + top: $control-spacing; +} + +.molstar-plugin-layout_controls > button:first-child { + margin-right: 6px; +} + +.molstar-empty-control { + display: none; +} + +.molstar-control .molstar-btn-block { + margin-bottom: 0px; + margin-top: 0px; +} + +.molstar-row-text { + > div { + line-height: $row-height; + text-align: center; + } +} \ No newline at end of file diff --git a/src/mol-app/skin/components/entity.scss b/src/mol-app/skin/components/entity.scss new file mode 100644 index 0000000000000000000000000000000000000000..3ce2e2305532c1d2a498233331d422ad45467acf --- /dev/null +++ b/src/mol-app/skin/components/entity.scss @@ -0,0 +1,225 @@ + + +.molstar-entity-tree { + overflow: hidden; + position: absolute; + bottom: 0; + left: 0; + right: 0; + top: 0; + padding-top: $control-spacing; + background: $control-background; + + .molstar-entity-tree-children { + overflow-x: hidden; + overflow-y: auto; + position: absolute; + bottom: 0; + left: 0; + right: 0; + top: $row-height + $control-spacing + 1; + padding: $control-spacing 0; + } +} + +.molstar-entity-store-header { + height: $row-height + 1; + position: relative; + + > span { + margin-left: 6px; + display: inline-block; + line-height: $row-height; + font-weight: bold; + + @include non-selectable + } + + button { + display: block !important; + height: $row-height !important; + margin: 0 !important; + line-height: $row-height !important; + border: none !important; + position: absolute; + top: 0; + } + + border-bottom: 1px solid $border-color; +} + +.molstar-entity-store-root { + overflow-x: hidden; + overflow-y: auto; + position: absolute; + bottom: 0; + left: 0; + top: $row-height + 1; + right: 0; +} + +.molstar-entity-tree-entry { + height: $row-height + 1; + position: relative; + border-bottom: 1px solid $control-background; +} + +.molstar-entity-tree-entry-current { + background: color-lower-contrast($default-background, 4%) !important; + + .molstar-entity-tree-entry-label { + color: $entity-current-font-color; + font-weight: bold; + .molstar-entity-tree-entry-label-tag { + font-weight: normal; + } + &:hover { + color: $hover-font-color; + } + } +} + +.molstar-entity-tree-entry-current-path { + background: color-lower-contrast($default-background, 2%) !important; + .molstar-entity-tree-entry-label { + color: color-lower-contrast($entity-current-font-color, 5%); + &:hover { + color: $hover-font-color; + } + } +} + +.molstar-entity-tree-entry button, .molstar-entity-tree-entry > div { + display: block !important; + height: $row-height !important; + margin: 0 !important; + line-height: $row-height !important; + border: none !important; + position: absolute; + top: 0; +} + + +.molstar-entity-tree-entry-toggle-group { + width: $row-height; + height: $row-height; + padding: 0; + left: 0; +} + +.molstar-entity-tree-entry-toggle-visible { + width: $row-height; + right: 0; //$row-height + 6; + padding: 0 !important; + font-size: 80%; +} + +.molstar-entity-tree-entry-toggle-visible-full, .molstar-entity-tree-entry-toggle-visible-full:focus, .molstar-entity-tree-entry-toggle-visible-full:active { + color: $entity-color-fully-visible; +} + +.molstar-entity-tree-entry-toggle-visible-partial, .molstar-entity-tree-entry-toggle-visible-partial:focus, .molstar-entity-tree-entry-toggle-visible-partial:active { + color: $entity-color-partialy-visible; +} + +.molstar-entity-tree-entry-toggle-visible-none, .molstar-entity-tree-entry-toggle-visible-none:focus, .molstar-entity-tree-entry-toggle-visible-none:active { + //background: transparent !important; + color: $entity-color-not-visible; +} + +.molstar-entity-tree-entry-remove { + width: $row-height; + height: $row-height; + right: $row-height; + padding: 0 !important; + text-align: center; + font-size: 80%; + color: color-lower-contrast($font-color, 66%) +} + +.molstar-entity-tree-entry-body { + position: absolute; + left: $row-height; + border-radius: 0 0 0 $entity-subtree-offset; + right: 0; + background: $default-background; +} + +.molstar-entity-tree-entry .molstar-entity-badge { + width: $row-height; + position: absolute; + height: $row-height; + left: 0; + top: 0; + border-radius: 0 $entity-subtree-offset 0 $entity-subtree-offset; +} + +.molstar-entity-tree-entry-label-wrap { + right: 2 * $row-height; + overflow: hidden; + left: $row-height; + height: $row-height; + position: absolute; +} + +.molstar-entity-tree-entry-label { + position: absolute; + right: 0; + top: 0; + left: 0; + text-align: left !important; + width: 100%; + padding: 0 $control-spacing !important; +} + +.molstar-entity-tree-entry-label-tag { + color: $entity-tag-color; + font-size: 70%; + display: inline-block; + margin-left: 6px; +} + + +.molstar-entity-tree-children-wrap { + padding-left: $entity-subtree-offset; +} + +.molstar-entity-tree-root { + > .molstar-entity-tree-entry { + .molstar-entity-badge { + border-top-right-radius: 0; + } + .molstar-entity-tree-entry-label { + font-weight: bold; + } + .molstar-entity-tree-entry-toggle-group { + display: none !important; + } + .molstar-entity-tree-entry-body { + left: $row-height - $entity-subtree-offset !important; + } + background: $default-background; + border-bottom: 1px solid $border-color; + } + + > .molstar-entity-tree-children-wrap { + margin-top: $control-spacing; + padding-left: 0 !important; + } +} + +.molstar-panel { + .molstar-entity-tree-entry-toggle-visible { + position: absolute; + top: 0; + right: 0; + height: $row-height; + font-size: 100%; + + background: $default-background; //color-increase-contrast($default-background, 4%); + } + + // .molstar-entity-tree-entry-toggle-visible-full { + // background: color-increase-contrast($default-background, 8%); + // } +} \ No newline at end of file diff --git a/src/mol-app/skin/components/help.scss b/src/mol-app/skin/components/help.scss new file mode 100644 index 0000000000000000000000000000000000000000..31b400458ef9f0adc6c6792e39e61297b1f904e0 --- /dev/null +++ b/src/mol-app/skin/components/help.scss @@ -0,0 +1,28 @@ + +.molstar-help-row { + position: relative; + height: $row-height; + background: $default-background; + margin-top: 1px; + display: table; + width: 100%; + + > span { + width: $control-label-width + $control-spacing; + text-align: right; + padding: $info-vertical-padding $control-spacing; + color: color-lower-contrast($font-color, 15%); + display: table-cell; + font-weight: bold; + + @include non-selectable; + } + + > div { + background: $molstar-form-control-background; + position: relative; + padding: $info-vertical-padding $control-spacing; + display: table-cell; + color: color-lower-contrast($font-color, 15%); + } +} \ No newline at end of file diff --git a/src/mol-app/skin/components/jobs.scss b/src/mol-app/skin/components/jobs.scss new file mode 100644 index 0000000000000000000000000000000000000000..13bae9ef1cc2d685ba76a4e393f2163b0979fd15 --- /dev/null +++ b/src/mol-app/skin/components/jobs.scss @@ -0,0 +1,131 @@ +.molstar-job-state { + + line-height: $row-height; + //height: $row-height; + //position: relative; + //margin-top: 1px; + + > span { + @include non-selectable; + //display: inline-block; + //padding: 0 $control-spacing; + } + + // > button { + // margin-top: -2px; + // float: left; + // display: block; + // line-height: $row-height; + // height: $row-height; + // } +} + +/* overlay */ + +.molstar-overlay { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + z-index: 1000; + + .molstar-overlay-background { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + background: transparent; + //background: black; + //opacity: 0.5; + } + + .molstar-overlay-content-wrap { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + display: block; + width: 100%; + height: 100%; + } + + .molstar-overlay-content { + text-align: center; + + > div { + + padding-top: 2 * $row-height; + + .molstar-job-state { + $size: $row-height; + text-align: center; + + > div { + height: $size; + margin-top: $control-spacing; + position: relative; + text-align: center; + width: 100%; + + > div { + height: $size; + line-height: $size; + display: inline-block; + background: $default-background; + padding: 0 ($control-spacing); + font-weight: bold; + @include non-selectable; + } + + > button { + display: inline-block; + margin-top: -3px; + font-size: 140%; + } + } + } + } + } +} + +/* background */ + +.molstar-background-jobs { + position: absolute; + left: 0; + bottom: 0; + z-index: 1000; + + .molstar-job-state { + $size: $row-height; + + > div { + height: $size; + margin-top: 1px; + position: relative; + width: 100%; + background: $default-background; + + > div { + height: $size; + line-height: $size; + display: inline-block; + padding: 0 ($control-spacing); + @include non-selectable; + } + + > button { + display: inline-block; + margin-top: -3px; + font-size: 140%; + } + } + } +} + +// .molstar-background-jobs .molstar-job-state { +// color: +// } diff --git a/src/mol-app/skin/components/log.scss b/src/mol-app/skin/components/log.scss new file mode 100644 index 0000000000000000000000000000000000000000..9bc74918c8a11a2958d0843fecf2908d7f3201cd --- /dev/null +++ b/src/mol-app/skin/components/log.scss @@ -0,0 +1,97 @@ + +.molstar-log-wrap { + position: absolute; + right: 0; + top: 0; + left: 0; + bottom: 0; + overflow: hidden; +} + +.molstar-log { + position: absolute; + right: -20px; + top: 0; + left: 0; + bottom: 0; + overflow-y: scroll; + overflow-x: hidden; + font-size: 90%; + background: $control-background; +} + +.molstar-log { + ul { + padding: 0; + margin: 0; + } + + color: $log-font-color; + + li { + clear: both; + margin: 0; + background: $default-background; + position: relative; + + &:not(:last-child) { + border-bottom: 1px solid $border-color; + } + } + + + .molstar-log-entry { + margin-left: $control-label-width; + background: color-lower-contrast($control-background, 5%); + padding: $info-vertical-padding ($control-spacing + 15px) $info-vertical-padding $control-spacing ; + } + + .molstar-log-timestamp { + padding: ($info-vertical-padding + 1) $control-spacing ($info-vertical-padding - 1) $control-spacing; + float: left; + text-align: right; + width: $control-label-width; + color: $log-timestamp-font-color; + //vertical-align: baseline; + //line-height: $row-height; + font-size: 90%; + } + + .molstar-log-timestamp small { + font-size: 90%; + } +} + +// .molstar-log hr { +// border-color: $separator-color; +// margin: 3px 3px 0 5px; +// } + +.molstar-log .label { + margin-top: -3px; + font-size: 7pt; +} + +.molstar-log-entry-badge { + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 6px; +} + +.molstar-log-entry-message { + background: $log-message; +} + +.molstar-log-entry-info { + background: $log-info; +} + +.molstar-log-entry-error { + background: $log-error; +} + +.molstar-log-entry-warning { + background: $log-warning; +} diff --git a/src/mol-app/skin/components/misc.scss b/src/mol-app/skin/components/misc.scss new file mode 100644 index 0000000000000000000000000000000000000000..a19eefd6ad19ccdcadb3c01f71345274e5ef74e5 --- /dev/null +++ b/src/mol-app/skin/components/misc.scss @@ -0,0 +1,69 @@ +.molstar-description { + padding: $control-spacing; + font-size: 85%; + background: $default-background; + text-align: center; + //font-style: italic; + + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ + + /* Rules below not implemented in browsers yet */ + -o-user-select: none; + user-select: none; + + font-weight: light; + + cursor: default; +} + +.molstar-description:not(:first-child) { + border-top: 1px solid $control-background; +} + +.molstar-color-picker input { + color: black !important; +} + +.molstar-no-webgl { + position: absolute; + width: 100%; + height: 100%; + left: 0; + top: 0; + display: table; + text-align: center; + + > div { + b { + font-size: 120%; + } + display: table-cell; + vertical-align: middle; + text-align: center; + width: 100%; + height: 100%; + } +} + +.molstar-loader-molstar-btn-file { + position: relative; + overflow: hidden; +} + +.molstar-loader-molstar-btn-file input[type=file] { + position: absolute; + top: 0; + right: 0; + min-width: 100%; + min-height: 100%; + font-size: 100px; + text-align: right; + filter: alpha(opacity=0); + opacity: 0; + outline: none; + background: white; + cursor: inherit; + display: block; +} \ No newline at end of file diff --git a/src/mol-app/skin/components/panel.scss b/src/mol-app/skin/components/panel.scss new file mode 100644 index 0000000000000000000000000000000000000000..affbcf9f2c83cf1d952656298871e12d71307563 --- /dev/null +++ b/src/mol-app/skin/components/panel.scss @@ -0,0 +1,142 @@ +.molstar-panel-header .molstar-panel-expander { + display: block; + width: 100%; + text-align: left; +} + +.molstar-panel-header { + + //border-bottom-width: 1px; + //border-bottom-style: solid; + height: $row-height; + border-color: $border-color; + position: relative; + + //border-radius: $control-spacing 0 0 0; + + .molstar-panel-expander-wrapper { + + position: absolute; + top: 0; + left: 0; + right: 2 * $row-height; + + button { + // width: 100%; + + display: block; + width: 100%; + text-align: left; + + height: $row-height; + line-height: $row-height; + border: none; + font-weight: bold; + //color: $panel-header-font-color; + padding-left: 0; + background: color-lower-contrast($default-background, 4%); + //text-align: right!important; + + .molstar-icon { + display: inline-block; + margin-right: $control-spacing; + width: $row-height; + text-align: center; + } + + &:hover { + background: color-lower-contrast($default-background, 4%); + } + } + } + + .molstar-panel-description-standalone { + > .molstar-icon { + margin-left: $row-height; + } + + width: 2 * $row-height; + } + + .molstar-panel-description-with-action { + width: $row-height; + margin-right: $row-height; + } + + .molstar-panel-description { + color: $font-color; + float: right; + background: color-lower-contrast($default-background, 4%); + //margin-right: $row-height; + + > .molstar-icon { + display: block; + width: $row-height; + height: $row-height; + line-height: $row-height; + text-align: center; + font-size: 70%; + cursor: default; + background: color-lower-contrast($default-background, 4%); + color: color-lower-contrast($font-color, 66%); + } + + .molstar-panel-description-content { + @include non-selectable; + + color: $font-color; + display: none; + position: absolute; + left: 0; + width: 100%; + background: color-increase-contrast($molstar-form-control-background, 20%); + min-height: $row-height; + z-index: 1000000; + padding: $info-vertical-padding $control-spacing $info-vertical-padding ($row-height + $control-spacing); + text-align: left; + //border-bottom: 1px solid color-lower-contrast($default-background, 4%); + + > .molstar-icon { + position: absolute; + width: $row-height; + height: $row-height; + line-height: $row-height; + text-align: center; + font-size: 80%; + cursor: default; + top: 0; + left: 0; + } + } + + &:hover { + color: $hover-font-color; + > .molstar-icon { + color: $hover-font-color; + } + .molstar-panel-description-content { + display: block; + } + } + } +} + +.molstar-panel-body { + background: $control-background; +} + +.molstar-panel { + margin-bottom: $control-spacing; +} + +.molstar-transform-view { + padding-top: $control-spacing; +} + +.molstar-expandable-group-color-stripe { + position: absolute; + left: 0; + top: $row-height - 2px; + width: $control-label-width + $control-spacing; + height: 2px; +} \ No newline at end of file diff --git a/src/mol-app/skin/components/slider.scss b/src/mol-app/skin/components/slider.scss new file mode 100644 index 0000000000000000000000000000000000000000..2785a3de77b450c00efea05970993147d7083116 --- /dev/null +++ b/src/mol-app/skin/components/slider.scss @@ -0,0 +1,164 @@ +@mixin borderBox { + box-sizing: border-box; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); // remove tap highlight color for mobile safari + + * { + box-sizing: border-box; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); // remove tap highlight color for mobile safari + } + } + + .molstar-slider-base { + position: relative; + height: 14px; + padding: 5px 0; + width: 100%; + border-radius: $slider-border-radius-base; + @include borderBox; + + &-rail { + position: absolute; + width: 100%; + background-color: $border-color; + height: 4px; + border-radius: 2px; + } + + &-track { + position: absolute; + left: 0; + height: 4px; + border-radius: $slider-border-radius-base; + background-color: tint($font-color, 60%); + } + + &-handle { + position: absolute; + margin-left: -11px; + margin-top: -9px; + width: 22px; + height: 22px; + cursor: pointer; + border-radius: 50%; + background-color: $font-color; + border: 4px solid $border-color; + + &:hover { + background-color: $hover-font-color; + } + } + + &-mark { + position: absolute; + top: 18px; + left: 0; + width: 100%; + font-size: 12px; + } + + &-mark-text { + position: absolute; + display: inline-block; + vertical-align: middle; + text-align: center; + cursor: pointer; + color: #999; + + &-active { + color: #666; + } + } + + &-step { + position: absolute; + width: 100%; + height: 4px; + background: transparent; + } + + &-dot { + position: absolute; + bottom: -2px; + margin-left: -4px; + width: 8px; + height: 8px; + border: 2px solid #e9e9e9; + background-color: #fff; + cursor: pointer; + border-radius: 50%; + vertical-align: middle; + &:first-child { + margin-left: -4px; + } + &:last-child { + margin-left: -4px; + } + &-active { + border-color: tint($font-color, 50%); + } + } + + &-disabled { + background-color: #e9e9e9; + + .molstar-slider-base-track { + background-color: $slider-disabledColor; + } + + .molstar-slider-base-handle, .molstar-slider-base-dot { + border-color: $slider-disabledColor; + background-color: #fff; + cursor: not-allowed; + } + + .molstar-slider-base-mark-text, .molstar-slider-base-dot { + cursor: not-allowed!important; + } + } + } + + .molstar-slider-base-vertical { + width: 14px; + height: 100%; + padding: 0 5px; + + .molstar-slider-base { + &-rail { + height: 100%; + width: 4px; + } + + &-track { + left: 5px; + bottom: 0; + width: 4px; + } + + &-handle { + margin-left: -5px; + margin-bottom: -7px; + } + + &-mark { + top: 0; + left: 18px; + height: 100%; + } + + &-step { + height: 100%; + width: 4px; + } + + &-dot { + left: 2px; + margin-bottom: -4px; + &:first-child { + margin-bottom: -4px; + } + &:last-child { + margin-bottom: -4px; + } + } + } + } \ No newline at end of file diff --git a/src/mol-app/skin/components/viewport.scss b/src/mol-app/skin/components/viewport.scss new file mode 100644 index 0000000000000000000000000000000000000000..3ef5c8a6ad5831d446e578e7ddbd4662d945e3c6 --- /dev/null +++ b/src/mol-app/skin/components/viewport.scss @@ -0,0 +1,93 @@ + +.molstar-viewport { + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + background: black; + + .molstar-btn-link { + background: rgba(0,0,0,0.2); + } + +} + +.molstar-viewport-expanded { + position: fixed; + z-index: 1000; +} + +.molstar-viewport-container { + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + -webkit-user-select: none; + -webkit-tap-highlight-color: rgba(0,0,0,0); + -webkit-touch-callout: none; +} + +.molstar-viewport-controls { + position: absolute; + right: $control-spacing; + top: $control-spacing; +} + +.molstar-viewport-controls-buttons { + text-align: right; + + > button { + padding: 0; + text-align: center; + width: $row-height; + } + + > button:last-child { + margin-left: $control-spacing; + } + + .molstar-btn-link, .molstar-btn-link-toggle-on { + color: #eee; + } + + .molstar-btn-link-toggle-off { + color: $molstar-btn-link-toggle-off-font-color; + } + + .molstar-btn-link:hover { + color: $hover-font-color; + } +} + +.molstar-viewport-controls-scene-options { + width: 290px; + background: $control-background; +} + +/* highlight */ + +.molstar-highlight-info { + + color: $highlight-info-font-color; + padding: $info-vertical-padding $control-spacing; + background: $default-background; //$highlight-info-background; + + position: absolute; + top: $control-spacing; + left: $control-spacing; + text-align: left; + min-height: $row-height; + max-width: 95%; + + //border-bottom-right-radius: 6px; + z-index: 10000; + @include non-selectable; +} + +.molstar-highlight-info-additional { + font-size: 85%; + display: inline-block; + color: $highlight-info-additional-font-color; +} \ No newline at end of file diff --git a/src/mol-app/skin/fonts/fontello.eot b/src/mol-app/skin/fonts/fontello.eot new file mode 100644 index 0000000000000000000000000000000000000000..b522411b265bb3854a7f23579001bfe51e4229cb Binary files /dev/null and b/src/mol-app/skin/fonts/fontello.eot differ diff --git a/src/mol-app/skin/fonts/fontello.svg b/src/mol-app/skin/fonts/fontello.svg new file mode 100644 index 0000000000000000000000000000000000000000..753bf788b40774bbb9a88c2b847d0b335975882a --- /dev/null +++ b/src/mol-app/skin/fonts/fontello.svg @@ -0,0 +1,442 @@ +<?xml version="1.0" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg xmlns="http://www.w3.org/2000/svg"> +<metadata>Copyright (C) 2016 by original authors @ fontello.com</metadata> +<defs> +<font id="fontello" horiz-adv-x="1000" > +<font-face font-family="fontello" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" /> +<missing-glyph horiz-adv-x="1000" /> +<glyph glyph-name="palette" unicode="" d="M857 622q72-48 101-110t20-104-35-48q-16-4-54 10t-80 10-80-46q-30-46-21-75t34-65 23-50q-2-26-36-63t-126-74-216-37q-186 0-291 101t-95 245q8 118 104 235t216 151q290 84 536-80z m-318-466q30 0 52 22t22 54-22 53-52 21q-32 0-54-21t-22-53 22-54 54-22z" horiz-adv-x="980" /> + +<glyph glyph-name="search" unicode="" d="M772 78q30-34 6-62l-46-46q-36-32-68 0l-190 190q-74-42-156-42-128 0-223 95t-95 223 90 219 218 91 224-95 96-223q0-88-46-162z m-678 358q0-88 68-156t156-68 151 63 63 153q0 88-68 155t-156 67-151-63-63-151z" horiz-adv-x="789" /> + +<glyph glyph-name="flashlight" unicode="" d="M807 706q62-62 85-130t-5-92l-134-134q-16-16-62-26t-96-4l-408-408q-18-18-57-6t-75 50q-36 36-49 74t5 56l408 408q-6 50 4 96t26 62l136 136q24 28 92 4t130-86z m-448-408q32-32 80 14 46 46 14 82-14 14-38 10t-44-24-23-43 11-39z m336 298q30-30 68-50t62-25 28-1q2 4-4 27t-26 60-50 67-66 50-59 26-27 2 1-28 25-62 48-66z" horiz-adv-x="902" /> + +<glyph glyph-name="mail" unicode="" d="M30 586q-32 18-28 40 2 14 26 14l846 0q38 0 20-32-8-14-24-22-14-6-192-102t-182-98q-16-10-46-10-28 0-46 10-4 2-182 98t-192 102z m850-100q20 10 20-10l0-368q0-16-17-32t-33-16l-800 0q-16 0-33 16t-17 32l0 368q0 20 20 10l384-200q18-10 46-10t46 10z" horiz-adv-x="900" /> + +<glyph glyph-name="heart" unicode="" d="M790 644q70-64 70-156t-70-158l-360-330-360 330q-70 66-70 158t70 156q62 58 151 58t153-58l56-52 58 52q62 58 150 58t152-58z" horiz-adv-x="860" /> + +<glyph glyph-name="heart-empty" unicode="" d="M790 642q70-64 70-156t-70-156l-360-330-360 330q-70 64-70 156t70 156q64 58 152 58t150-58l58-52 56 52q64 58 152 58t152-58z m-54-260q42 40 42 104 0 66-38 100-38 38-102 38-52 0-104-48l-104-92-106 92q-48 48-102 48-64 0-104-38-38-36-38-100 0-66 44-104l306-286z" horiz-adv-x="860" /> + +<glyph glyph-name="star" unicode="" d="M440 790l120-336 320 0-262-196 94-348-272 208-272-208 94 348-262 196 320 0z" horiz-adv-x="880" /> + +<glyph glyph-name="star-empty" unicode="" d="M880 454l-262-196 94-348-272 208-272-208 94 348-262 196 320 0 120 336 120-336 320 0z m-440-238l150-124-62 178 144 114-176-4-56 202-54-202-176 4 142-114-62-178z" horiz-adv-x="880" /> + +<glyph glyph-name="user" unicode="" d="M736 128q204-72 204-122l0-106-940 0 0 106q0 50 204 122 94 34 128 69t34 95q0 22-22 49t-32 73q-2 12-9 18t-14 8-14 17-9 43q0 16 5 26t9 12l4 4q-8 50-12 88-4 54 41 112t157 58 158-58 40-112l-12-88q18-8 18-42-2-28-9-43t-14-17-14-8-9-18q-8-48-31-74t-23-48q0-60 35-95t127-69z" horiz-adv-x="940" /> + +<glyph glyph-name="users" unicode="" d="M1000-90l-224 0 0 150q0 54-30 81t-154 89q40 30 40 84 0 16-13 33t-19 51q-2 8-14 16t-14 42q0 24 12 30-6 34-8 60-4 38 23 78t95 40 96-40 24-78l-8-60q12-6 12-30-2-34-14-42t-14-16q-6-34-19-51t-13-33q0-42 21-66t77-48q112-46 130-80 6-8 9-61t5-101l0-48z m-488 262q182-78 182-124l0-138-694 0 0 184q0 44 84 78 76 32 104 64t28 88q0 20-19 44t-25 68q-2 10-18 22t-20 56q0 14 3 23t7 13l4 2q-6 46-10 82-4 50 33 103t127 53 127-53 33-103l-10-82q14-8 14-38-4-44-20-56t-18-22q-6-44-25-68t-19-44q0-56 28-88t104-64z" horiz-adv-x="1000" /> + +<glyph glyph-name="user-add" unicode="" d="M620 128q180-64 180-122l0-106-800 0 0 202q36 14 82 26 94 34 129 69t35 95q0 22-23 48t-31 74q-2 12-23 25t-25 61q0 16 5 26t9 12l4 4q-8 50-12 88-6 54 40 112t160 58 160-58 42-112l-14-88q18-8 18-42-2-28-9-43t-14-17-14-8-9-18q-10-46-33-73t-23-49q0-60 36-95t130-69z m230 272l150 0 0-100-150 0 0-150-100 0 0 150-150 0 0 100 150 0 0 150 100 0 0-150z" horiz-adv-x="1000" /> + +<glyph glyph-name="video" unicode="" d="M980 600l-100 0 0-100 100 0 0-100-100 0 0-100 100 0 0-100-100 0 0-100 100 0 0-60q0-16-12-28t-28-12l-900 0q-16 0-28 12t-12 28l0 60 100 0 0 100-100 0 0 100 100 0 0 100-100 0 0 100 100 0 0 100-100 0 0 60q0 18 12 29t28 11l900 0q16 0 28-11t12-29l0-60z m-600-400l250 150-250 150 0-300z" horiz-adv-x="980" /> + +<glyph glyph-name="picture" unicode="" d="M856 518l-100 0-124 150-214-150-180 0q-52 0-90-39t-38-91l0-160-108 296q-10 38 22 52l680 248q36 10 50-24z m106-90q16 0 27-12t11-28l0-472q0-16-11-28t-27-12l-724 0q-16 0-27 12t-11 28l0 472q0 16 11 28t27 12l724 0z m-56-452l0 162-72 160-166-60-130-132-138 170-92-214 0-86 598 0z" horiz-adv-x="1000" /> + +<glyph glyph-name="camera" unicode="" d="M500 450q64 0 107-44t43-106-44-106-106-44-106 44-44 106 44 106 106 44z m400 150q42 0 71-29t29-71l0-450q0-40-29-70t-71-30l-800 0q-40 0-70 30t-30 70l0 450q0 42 30 71t70 29l120 0q28 0 40 30l30 92q10 28 40 28l340 0q30 0 40-28l30-92q12-30 40-30l120 0z m-400-550q104 0 177 73t73 177-73 177-177 73-177-73-73-177 73-177 177-73z m366 380q14 0 24 11t10 25-10 24-24 10q-36 0-36-34 0-16 11-26t25-10z" horiz-adv-x="1000" /> + +<glyph glyph-name="layout" unicode="" d="M170 650q80 0 80-80l0-90q0-80-80-80l-90 0q-80 0-80 80l0 90q0 80 80 80l90 0z m350 0q80 0 80-80l0-90q0-80-80-80l-90 0q-80 0-80 80l0 90q0 80 80 80l90 0z m-350-350q80 0 80-80l0-90q0-80-80-80l-90 0q-80 0-80 80l0 90q0 80 80 80l90 0z m350 0q80 0 80-80l0-90q0-80-80-80l-90 0q-80 0-80 80l0 90q0 80 80 80l90 0z" horiz-adv-x="600" /> + +<glyph glyph-name="menu" unicode="" d="M650 400q22 0 36-15t14-35-15-35-35-15l-600 0q-20 0-35 15t-15 35 14 35 36 15l600 0z m-600 100q-20 0-35 15t-15 35 14 35 36 15l600 0q22 0 36-15t14-35-15-35-35-15l-600 0z m600-300q22 0 36-15t14-35-15-35-35-15l-600 0q-20 0-35 15t-15 35 14 35 36 15l600 0z" horiz-adv-x="700" /> + +<glyph glyph-name="check" unicode="" d="M249 0q-34 0-56 28l-180 236q-16 24-12 52t26 46 51 14 47-28l118-154 296 474q16 24 43 30t53-8q24-16 30-43t-8-53l-350-560q-20-32-56-32z" horiz-adv-x="667" /> + +<glyph glyph-name="cancel" unicode="" d="M452 194q18-18 18-43t-18-43q-18-16-43-16t-43 16l-132 152-132-152q-18-16-43-16t-43 16q-16 18-16 43t16 43l138 156-138 158q-16 18-16 43t16 43q18 16 43 16t43-16l132-152 132 152q18 16 43 16t43-16q18-18 18-43t-18-43l-138-158z" horiz-adv-x="470" /> + +<glyph glyph-name="cancel-circled" unicode="" d="M420 770q174 0 297-123t123-297-123-297-297-123-297 123-123 297 123 297 297 123z m86-420l154 154-86 86-154-152-152 152-88-86 154-154-154-152 88-86 152 152 154-152 86 86z" horiz-adv-x="840" /> + +<glyph glyph-name="cancel-squared" unicode="" d="M700 750q42 0 71-29t29-71l0-600q0-40-29-70t-71-30l-600 0q-40 0-70 30t-30 70l0 600q0 42 30 71t70 29l600 0z m-146-638l86 86-154 152 154 154-86 86-154-152-152 152-88-86 154-154-154-152 88-86 152 152z" horiz-adv-x="800" /> + +<glyph glyph-name="plus" unicode="" d="M550 400q30 0 30-50t-30-50l-210 0 0-210q0-30-50-30t-50 30l0 210-210 0q-30 0-30 50t30 50l210 0 0 210q0 30 50 30t50-30l0-210 210 0z" horiz-adv-x="580" /> + +<glyph glyph-name="plus-circled" unicode="" d="M420 770q174 0 297-123t123-297-123-297-297-123-297 123-123 297 123 297 297 123z m52-470l200 0 0 102-200 0 0 202-102 0 0-202-202 0 0-102 202 0 0-202 102 0 0 202z" horiz-adv-x="840" /> + +<glyph glyph-name="plus-squared" unicode="" d="M700 750q42 0 71-29t29-71l0-600q0-40-29-70t-71-30l-600 0q-40 0-70 30t-30 70l0 600q0 42 30 71t70 29l600 0z m-50-450l0 100-200 0 0 200-100 0 0-200-200 0 0-100 200 0 0-200 100 0 0 200 200 0z" horiz-adv-x="800" /> + +<glyph glyph-name="minus" unicode="" d="M550 400q30 0 30-50t-30-50l-520 0q-30 0-30 50t30 50l520 0z" horiz-adv-x="580" /> + +<glyph glyph-name="minus-circled" unicode="" d="M420 770q174 0 297-123t123-297-123-297-297-123-297 123-123 297 123 297 297 123z m252-368l-504 0 0-102 504 0 0 102z" horiz-adv-x="840" /> + +<glyph glyph-name="minus-squared" unicode="" d="M700 750q42 0 71-29t29-71l0-600q0-40-29-70t-71-30l-600 0q-40 0-70 30t-30 70l0 600q0 42 30 71t70 29l600 0z m-50-450l0 100-500 0 0-100 500 0z" horiz-adv-x="800" /> + +<glyph glyph-name="help" unicode="" d="M494 740q86-62 86-184 0-64-42-124-12-20-88-80l-46-30q-40-34-48-60-6-16-8-44 0-14-16-14l-128 0q-16 0-16 12 4 98 28 124 16 22 48 48t56 42l24 14q22 16 34 34 28 44 28 70 0 40-26 78-28 36-92 36-68 0-94-44-28-42-28-92l-166 0q6 162 114 232 70 42 166 42 130 0 214-60z m-216-636q44 0 73-30t27-74q-2-46-32-73t-74-25q-44 0-73 29t-27 75 32 73 74 25z" horiz-adv-x="580" /> + +<glyph glyph-name="help-circled" unicode="" d="M454 810q190 2 326-130t140-322q2-190-131-327t-323-141q-190-2-327 131t-139 323q-4 190 130 327t324 139z m-2-740q30 0 49 19t19 47q2 30-17 49t-49 19l-2 0q-28 0-47-18t-21-46q0-30 19-49t47-21l2 0z m166 328q26 34 26 78 0 78-54 116-52 38-134 38-64 0-104-26-68-42-72-146l0-4 110 0 0 4q0 26 16 54 16 24 54 24 40 0 52-20 16-20 16-44 0-18-16-40-8-12-20-20l-6-4q-6-4-16-11t-20-15-21-17-17-17q-14-20-18-78l0-8 108 0 0 4q0 12 4 28 6 20 28 36l28 18q46 34 56 50z" horiz-adv-x="920" /> + +<glyph glyph-name="info" unicode="" d="M352 850q48 0 74-27t26-69q0-50-39-88t-95-38q-48 0-74 26t-24 72q0 46 35 85t97 39z m-206-1000q-100 0-54 178l60 254q14 56 0 56-12 0-54-18t-72-38l-26 44q90 78 189 126t151 48q78 0 36-162l-70-266q-16-64 6-64 44 0 118 60l30-40q-84-86-175-132t-139-46z" horiz-adv-x="460" /> + +<glyph glyph-name="info-circled" unicode="" d="M454 810q190 2 326-130t140-322q2-190-131-327t-323-141q-190-2-327 131t-139 323q-4 190 130 327t324 139z m52-152q-42 0-65-24t-23-50q-2-28 15-44t49-16q38 0 61 22t23 54q0 58-60 58z m-120-594q30 0 84 26t106 78l-18 24q-48-36-72-36-14 0-4 38l42 160q26 96-22 96-30 0-89-29t-115-75l16-26q52 34 74 34 12 0 0-34l-36-152q-26-104 34-104z" horiz-adv-x="920" /> + +<glyph glyph-name="back" unicode="" d="M750 540q40 0 70-29t30-71l0-290q0-40-30-70t-70-30l-690 0 0 140 650 0 0 210-500 0 0-110-210 180 210 180 0-110 540 0z" horiz-adv-x="850" /> + +<glyph glyph-name="home" unicode="" d="M888 336q16-16 11-27t-27-11l-84 0 0-310q0-14-1-21t-8-13-23-6l-204 0 0 310-204 0 0-310-194 0q-28 0-35 10t-7 30l0 310-84 0q-22 0-27 11t11 27l400 402q16 16 38 16t38-16z" horiz-adv-x="900" /> + +<glyph glyph-name="link" unicode="" d="M294 116q14 14 34 14t36-14q32-34 0-70l-42-40q-56-56-132-56-78 0-134 56t-56 132q0 78 56 134l148 148q70 68 144 77t128-43q16-16 16-36t-16-36q-36-32-70 0-50 48-132-34l-148-146q-26-26-26-64t26-62q26-26 63-26t63 26z m450 574q56-56 56-132 0-78-56-134l-158-158q-74-72-150-72-62 0-112 50-14 14-14 34t14 36q14 14 35 14t35-14q50-48 122 24l158 156q28 28 28 64 0 38-28 62-24 26-56 31t-60-21l-50-50q-16-14-36-14t-34 14q-34 34 0 70l50 50q54 54 127 51t129-61z" horiz-adv-x="800" /> + +<glyph glyph-name="attach" unicode="" d="M244-140q-102 0-170 72-72 70-74 166t84 190l496 496q80 80 174 54 44-12 79-47t47-79q26-96-54-176l-474-474q-40-40-88-46-48-4-80 28-30 24-27 74t47 92l332 334q24 26 50 0t0-50l-332-332q-44-44-20-70 12-8 24-6 24 4 46 26l474 474q50 50 34 108-16 60-76 76-54 14-108-36l-494-494q-66-76-64-143t52-117q50-48 117-50t141 62l496 494q24 24 50 0 26-22 0-48l-496-496q-82-82-186-82z" horiz-adv-x="939" /> + +<glyph glyph-name="lock" unicode="" d="M640 476q20 0 40-19t20-41l0-390q0-48-48-66l-60-18q-42-16-96-16l-290 0q-56 0-98 16l-60 18q-48 18-48 66l0 390q0 22 15 41t35 19l100 0 0 70q0 110 51 170t149 60 149-60 51-170l0-70 90 0z m-390 90l0-90 200 0 0 90q0 52-27 81t-73 29-73-29-27-81z" horiz-adv-x="700" /> + +<glyph glyph-name="lock-open" unicode="" d="M640 450q20 0 40-20t20-40l0-390q0-20-14-39t-34-25l-60-20q-52-16-96-16l-290 0q-46 0-98 16l-60 20q-20 6-34 25t-14 39l0 390q0 22 15 41t35 19l400 0 0 140q0 110-100 110t-100-110l0-40-100 0 0 20q0 110 51 170t149 60q200 0 200-230l0-120 90 0z" horiz-adv-x="700" /> + +<glyph glyph-name="eye" unicode="" d="M500 630q92 0 177-25t141-62 99-77 63-71 20-45-20-44-63-71-99-78-141-62-177-25-177 25-141 62-99 78-63 71-20 44 20 45 63 71 99 77 141 62 177 25z m0-494q92 0 157 63t65 151q0 90-65 153t-157 63-157-63-65-153q0-88 65-151t157-63z m0 214q8-8 37-2t50 11 25-9q0-44-33-75t-79-31-78 31-32 75q0 46 32 77t78 31q14 0 10-23t-12-47 2-38z" horiz-adv-x="1000" /> + +<glyph glyph-name="tag" unicode="" d="M944 830q36-106-8-199t-128-157l18-24q16-28 6-54l-48-158q-12-30-36-46l-464-328q-42-30-64 4l-210 304q-12 18-9 39t21 33l464 328q26 18 54 18l158 0q30 0 48-26l28-40q168 130 114 286-10 28 18 40 32 8 38-20z m-216-468q40 32 34 80l-32-16q-8-4-12-4-18 0-28 18-12 30 16 40l24 14q-48 34-92 0-28-18-34-51t14-61q18-26 51-32t59 12z" horiz-adv-x="960" /> + +<glyph glyph-name="bookmark" unicode="" d="M310 800q22 0 36-15t14-35l0-850-180 180-180-180 0 850q0 50 40 50l270 0z" horiz-adv-x="360" /> + +<glyph glyph-name="bookmarks" unicode="" d="M500 850q20 0 35-15t15-35l0-850-150 180 0 620q0 20-15 35t-35 15l-100 0q0 50 40 50l210 0z m-250-150q20 0 35-15t15-35l0-800-150 180-150-180 0 800q0 50 40 50l210 0z" horiz-adv-x="550" /> + +<glyph glyph-name="flag" unicode="" d="M874 616q14 6 22-1t0-19q-96-138-164-213t-110-90-73-2-60 37-63 40-93-4-139-86l90-352-100 0-184 720 92 34q90 66 152 86t98 3 64-51 62-71 79-62 129-20 198 51z" horiz-adv-x="900" /> + +<glyph glyph-name="thumbs-up" unicode="" d="M582 480q2-6 58-13t108-24 52-47q0-72-61-284t-107-212q-144 0-288 42t-144 88l0 342q0 14 15 34t46 45 53 41 62 43 46 31q50 34 104 100t85 104 41 26q48-76 29-137t-59-119-40-60z m-432-4q14 0 0-14-50-50-50-104l0-318q0-50 52-104 10-10-2-10-26 0-55 8t-62 45-33 99l0 242q0 62 33 100t63 47 54 9z" horiz-adv-x="800" /> + +<glyph glyph-name="thumbs-down" unicode="" d="M218 218q-2 6-57 13t-108 24-53 47q0 72 62 285t106 213q144 0 288-43t144-89l0-342q0-10-8-24t-25-30-32-29-42-32-41-29-41-28l-33-22q-50-34-104-100t-85-104-41-26q-48 76-29 137t59 119 40 60z m432 4q-12 0 2 14 48 50 48 104l0 318q0 50-52 104-10 10 2 10 26 0 55-8t62-45 33-99l0-242q0-48-18-81t-45-48-48-21-39-6z" horiz-adv-x="800" /> + +<glyph glyph-name="download" unicode="" d="M968 198q18-10 27-32t3-40l-28-154q-4-20-22-33t-40-13l-816 0q-22 0-40 13t-22 33l-28 154q-10 48 32 72l158 108 98 0-170-130 178 0q8 0 12-8l40-110 300 0 40 110q8 8 12 8l178 0-170 130 98 0z m-208 322l-260-244-260 244 166 0 0 256 190 0 0-256 164 0z" horiz-adv-x="1000" /> + +<glyph glyph-name="upload" unicode="" d="M500 776l260-244-164 0 0-256-190 0 0 256-166 0z m468-578q18-10 27-32t3-40l-28-154q-4-20-22-33t-40-13l-816 0q-22 0-40 13t-22 33l-28 154q-10 48 32 72l158 108 98 0-170-130 178 0q8 0 12-8l40-110 300 0 40 110q8 8 12 8l178 0-170 130 98 0z" horiz-adv-x="1000" /> + +<glyph glyph-name="upload-cloud" unicode="" d="M760 494q100 0 170-68t70-166-70-166-170-68l-190 0 0 190 106 0-176 230-174-230 104 0 0-190-248 0q-74 0-128 52t-54 124q0 74 53 126t129 52q14 0 20-2-2 12-2 38 0 108 78 184t188 76q90 0 160-52t94-134q28 4 40 4z" horiz-adv-x="1000" /> + +<glyph glyph-name="reply" unicode="" d="M900 10q-86 152-208 197t-330 45l0-218-362 334 362 322 0-192q90 0 168-27t131-70 96-95 69-104 44-95 24-69z" horiz-adv-x="900" /> + +<glyph glyph-name="reply-all" unicode="" d="M362 556l-212-188 212-196 0-138-362 334 362 322 0-134z m250-58q104 0 182-50t115-122 60-144 27-122l4-50q-86 154-168 198t-220 44l0-218-362 334 362 322 0-192z" horiz-adv-x="1000" /> + +<glyph glyph-name="forward" unicode="" d="M540 252q-210 0-332-45t-208-197q4 20 13 53t50 117 96 148 156 117 225 53l0 192 360-322-360-334 0 218z" horiz-adv-x="900" /> + +<glyph glyph-name="quote" unicode="" d="M146 680q146 0 184-146 38-140-40-302-80-168-224-204-32-8-66-8l0 70q112 0 182 108 54 86 26 146-16 36-62 36-60 0-103 44t-43 106 43 106 103 44z m420 0q146 0 184-146 38-140-40-302-80-168-224-204-32-8-66-8l0 70q112 0 182 108 54 86 26 146-16 36-62 36-60 0-103 44t-43 106 43 106 103 44z" horiz-adv-x="762" /> + +<glyph glyph-name="code" unicode="" d="M380 636q16-14 16-32t-16-30l-246-224 246-226q16-12 16-30t-16-32q-30-30-60 0l-320 288 320 286q30 30 60 0z m302 0l318-286-318-288q-32-30-62 0-32 32 0 62l248 226-248 224q-32 30 0 62 30 30 62 0z" horiz-adv-x="1000" /> + +<glyph glyph-name="export" unicode="" d="M750 60l0 56 100 82 0-188q0-20-15-35t-35-15l-750 0q-20 0-35 15t-15 35l0 550q0 22 14 36t36 14l288 0q-32-24-59-49t-39-39l-10-12-130 0 0-450 650 0z m-82 348q-166 0-242-41t-160-181q0 8 1 22t9 56 22 79 44 83 70 79 107 56 149 23l0 156 332-250-332-260 0 178z" horiz-adv-x="1000" /> + +<glyph glyph-name="pencil" unicode="" d="M718 680q32-32 47-64t15-48l0-16-252-252-290-288-238-52 50 240 290 288 252 252q54 12 126-60z m-494-640l24 24q-2 44-52 94-22 22-45 35t-35 13l-14 2-22-24-18-80q28-16 46-34 24-24 36-48z" horiz-adv-x="780" /> + +<glyph glyph-name="feather" unicode="" d="M60-138q-6-20-26-8-18 8-16 34 4 100 50 226-100 154-52 316 10-32 32-78t44-80 32-30q8 4 0 83t-11 166 25 157q22 44 80 94t104 70q-24-46-33-94t-4-78 21-32q12 0 84 120t106 122q46 4 114-29t82-65q12-24 0-79t-40-83q-44-44-146-62t-114-24q-16-10 12-34 54-48 176-20-56-80-136-114t-132-38-54-10q-4-24 49-54t101-14q-30-56-63-84t-54-35-76-11-85-8z" horiz-adv-x="698" /> + +<glyph glyph-name="print" unicode="" d="M66 526q-26 0-22 22 4 10 12 14 2 0 49 17t93 32 58 15l44 0 0 150 380 0 0-150 46 0q12 0 57-15t92-32 49-17q18-8 12-26-4-10-20-10l-850 0z m860-56q20 0 37-19t17-41l0-174q0-22-17-41t-37-19l-100 0 44-250-760 0 44 250-98 0q-20 0-38 19t-18 41l0 174q0 22 18 41t38 19l870 0z m-716-444l560 0-70 324-420 0z" horiz-adv-x="980" /> + +<glyph glyph-name="retweet" unicode="" d="M250 190l272 0 128-140-448 0q-42 0-71 30t-29 70l0 302-102 0 176 198 174-198-100 0 0-262z m650 60l100 0-174-200-176 200 102 0 0 260-274 0-128 140 450 0q40 0 70-29t30-71l0-300z" horiz-adv-x="1000" /> + +<glyph glyph-name="keyboard" unicode="" d="M930 650q28 0 49-21t21-49l0-460q0-30-21-50t-49-20l-860 0q-28 0-49 20t-21 50l0 460q0 28 21 49t49 21l860 0z m-380-100l0-100 100 0 0 100-100 0z m150-150l-100 0 0-100 100 0 0 100z m-300 150l0-100 100 0 0 100-100 0z m150-150l-100 0 0-100 100 0 0 100z m-300 150l0-100 100 0 0 100-100 0z m150-150l-100 0 0-100 100 0 0 100z m-300 150l0-100 100 0 0 100-100 0z m150-150l-100 0 0-100 100 0 0 100z m-50-250l0 100-100 0 0-100 100 0z m550 0l0 100-500 0 0-100 500 0z m150 0l0 100-100 0 0-100 100 0z m-150 150l100 0 0 100-100 0 0-100z m150 150l0 100-200 0 0-100 200 0z" horiz-adv-x="1000" /> + +<glyph glyph-name="comment" unicode="" d="M700 700q42 0 71-29t29-71l0-350q0-40-29-70t-71-30l-200 0 0-150-200 150-200 0q-40 0-70 30t-30 70l0 350q0 42 30 71t70 29l600 0z" horiz-adv-x="800" /> + +<glyph glyph-name="chat" unicode="" d="M290 240l350 0q2 0 6 2l4 0 0-92q0-40-29-70t-71-30l-250 0-150-150 0 150-50 0q-40 0-70 30t-30 70l0 300q0 42 30 71t70 29l190 0 0-310z m610 560q42 0 71-29t29-71l0-300q0-40-29-70t-71-30l-50 0 0-150-150 150-350 0 0 400q0 42 30 71t70 29l450 0z" horiz-adv-x="1000" /> + +<glyph glyph-name="bell" unicode="" d="M632 426q16-34 40-52t45-22 44-23 35-55q22-62-74-161t-252-157q-164-58-297-45t-155 75q-20 54 12 111t18 111q-56 192-47 300t113 192q26 22 29 51t29 39q24 8 46-12t56-18q132 2 198-66t160-268z m-186-404q88 32 159 85t100 91 25 50q-8 22-49 33t-124 1-187-48q-102-38-173-87t-94-84-17-53q4-12 50-22t134-4 176 38z m-62 174q8 2 21 7t17 7l2-2q14-40-17-83t-89-63q-96-36-152 14 78 68 218 120z" horiz-adv-x="800" /> + +<glyph glyph-name="attention" unicode="" d="M957-24q10-16 0-34-10-16-30-16l-892 0q-18 0-28 16-13 18-2 34l446 782q8 18 30 18t30-18z m-420 50l0 100-110 0 0-100 110 0z m0 174l0 300-110 0 0-300 110 0z" horiz-adv-x="962" /> + +<glyph glyph-name="alert" unicode="" d="M885 234q20-16 16-33t-28-23l-78-22q-24-6-40-28t-14-48l4-82q2-24-14-34t-38 0l-86 44q-22 12-47 4t-35-30l-46-88q-12-22-29-23t-33 19l-50 78q-34 48-88 20l-122-70q-22-14-32-6t-2 32l54 164q8 24-4 44t-36 22l-106 12q-24 4-29 18t15 30l86 76q20 16 20 41t-20 41l-86 76q-20 16-16 33t28 23l78 22q24 6 41 28t15 48l-6 82q0 26 15 36t37 0l80-38q24-10 49-2t37 30l46 80q12 22 30 21t30-23l50-86q12-22 35-29t45 7l136 84q22 14 30 6t0-32l-60-170q-10-22 2-41t38-21l114-12q26-2 30-16t-16-30l-86-76q-18-16-18-41t18-41z m-384-92l0 104-100 0 0-104 100 0z m0 160l0 260-100 0 0-260 100 0z" horiz-adv-x="901" /> + +<glyph glyph-name="vcard" unicode="" d="M900 750q42 0 71-29t29-71l0-600q0-40-29-70t-71-30l-800 0q-40 0-70 30t-30 70l0 600q0 42 30 71t70 29l800 0z m0-700l0 600-800 0 0-600 800 0z m-450 196l0-90-250 0 0 90 250 0z m0 150l0-90-250 0 0 90 250 0z m0 150l0-90-250 0 0 90 250 0z m346-320l4-70-250 0q0 70 6 70 84 22 84 66 0 16-27 56t-27 88q0 110 90 110t90-110q0-48-28-88t-28-56q0-20 21-36t43-22z" horiz-adv-x="1000" /> + +<glyph glyph-name="address" unicode="" d="M426 800q20 0 20-20l0-860q0-20-20-20l-46 0q-20 0-20 20l0 440-176 0q-16 0-28 6-12 2-26 12l-120 82q-10 6-10 16t10 16l120 82q14 10 26 12 8 4 28 4l176 0 0 190q0 20 20 20l46 0z m564-208q10-6 10-16t-10-16l-118-82q-22-12-26-12-14-6-28-6l-302 0-40 230 342 0q18 0 28-4t26-12z" horiz-adv-x="1000" /> + +<glyph glyph-name="location" unicode="" d="M250 750q104 0 177-73t73-177q0-106-62-243t-126-223l-62-84q-10 12-27 35t-60 89-76 130-60 147-27 149q0 104 73 177t177 73z m0-388q56 0 96 40t40 96-40 95-96 39-95-39-39-95 39-96 95-40z" horiz-adv-x="500" /> + +<glyph glyph-name="map" unicode="" d="M984 600q16-10 16-30l0-584q0-20-16-30-8-6-16-6t-18 6l-216 136-216-136q-18-10-34 0l-218 136-216-136q-16-10-34 0-16 10-16 30l0 584q0 20 16 30l234 146q18 10 34 0l216-136 218 136q16 10 32 0z m-750-450l0 506-168-104 0-506z m234-104l0 506-168 104 0-506z m234 104l0 506-170-104 0-506z m232-104l0 506-168 104 0-506z" horiz-adv-x="1000" /> + +<glyph glyph-name="direction" unicode="" d="M848 768q8-8 11-16t-2-22-10-26-19-39-24-49q-54-112-147-286t-157-292l-66-118-54 380-380 56q442 246 696 368 20 10 48 25t39 20 25 9 23 1 17-11z m-92-96l-304-280 28-234z" horiz-adv-x="860" /> + +<glyph glyph-name="compass" unicode="" d="M474 830q198 2 340-136t146-336q2-200-136-342t-338-146q-198-2-341 137t-145 337q-4 200 135 342t339 144z m12-858q156 2 266 114t108 270-115 267-269 107q-158-2-267-114t-107-270 114-267 270-107z m-234 154q4 26 12 66t41 128 77 132 125 76 141 42l60 10q-4-26-12-66t-41-128-77-132q-42-42-124-74t-142-42z m180 276q-22-20-22-48t22-50q20-22 49-22t49 22q52 52 88 186-136-36-186-88z" horiz-adv-x="960" /> + +<glyph glyph-name="cup" unicode="" d="M340 760q152 0 249-41t91-87l-72-594q-2-14-34-36t-97-42-137-20-136 20-97 42-35 36l-72 594q-4 28 36 57t121 50 183 21z m0-216q72 0 137 15t98 33 33 30-33 29-98 32-137 15-137-15-98-32-33-29 33-30 98-33 137-15z" horiz-adv-x="681" /> + +<glyph glyph-name="trash" unicode="" d="M50 458q122-70 330-70t330 70l-54-486q-2-14-35-36t-100-43-141-21-140 21-100 43-36 36z m488 300q94-18 158-55t64-71l0-10q0-58-112-99t-268-41-268 41-112 99l0 10q0 34 64 71t158 55l42 48q22 26 70 26l92 0q52 0 70-26z m-54-112l84 0q-92 110-104 126-14 16-32 16l-102 0q-22 0-32-16l-106-126 84 0 64 66 82 0z" horiz-adv-x="760" /> + +<glyph glyph-name="doc" unicode="" d="M600 800q42 0 71-29t29-71l0-700q0-40-29-70t-71-30l-500 0q-40 0-70 30t-30 70l0 700q0 42 30 71t70 29l500 0z m0-800l0 700-500 0 0-700 500 0z" horiz-adv-x="700" /> + +<glyph glyph-name="docs" unicode="" d="M970 480q38-10 30-46l-150-556q-4-16-18-23t-30-3l-406 110q-16 4-24 18t-4 28l24 92-180-48q-40-10-50 26l-160 602q-10 36 28 48l454 122q16 4 30-3t18-23l66-244z m-888 190l144-542 392 106-144 540z m702-742l132 492-298 82 76-282q10-34-28-46l-196-52-26-102z" horiz-adv-x="1001" /> + +<glyph glyph-name="doc-landscape" unicode="" d="M0 600q0 42 30 71t70 29l800 0q42 0 71-29t29-71l0-500q0-40-29-70t-71-30l-800 0q-40 0-70 30t-30 70l0 500z m900 0l-800 0 0-500 800 0 0 500z" horiz-adv-x="1000" /> + +<glyph glyph-name="doc-text" unicode="" d="M212 308l0 90 280 0 0-90-280 0z m388 492q42 0 71-29t29-71l0-700q0-40-29-70t-71-30l-500 0q-40 0-70 30t-30 70l0 700q0 42 30 71t70 29l500 0z m0-800l0 700-500 0 0-700 500 0z m-110 592l0-88-280 0 0 88 280 0z m0-392l0-88-280 0 0 88 280 0z" horiz-adv-x="700" /> + +<glyph glyph-name="doc-text-inv" unicode="" d="M600 800q42 0 71-29t29-71l0-700q0-40-29-70t-71-30l-500 0q-40 0-70 30t-30 70l0 700q0 42 30 71t70 29l500 0z m-460-208l0-88 420 0 0 88-420 0z m420-480l0 88-420 0 0-88 420 0z m0 196l0 90-418 0 0-90 418 0z" horiz-adv-x="700" /> + +<glyph glyph-name="newspaper" unicode="" d="M700 800q42 0 71-29t29-71l0-700q0-40-29-70t-71-30l-600 0q-40 0-70 30t-30 70l0 700q0 42 30 71t70 29l600 0z m0-800l0 700-600 0 0-700 600 0z m-250 250l0-50-250 0 0 50 250 0z m150 200l0-50-200 0 0 50 200 0z m-200 50l0 100 200 0 0-100-200 0z m-50 100l0-200-150 0 0 200 150 0z m-50-250l0-50-100 0 0 50 100 0z m50-50l0 50 250 0 0-50-250 0z m250-150l0-50-400 0 0 50 400 0z m-100 50l0 50 100 0 0-50-100 0z" horiz-adv-x="800" /> + +<glyph glyph-name="book-open" unicode="" d="M340 238l0-68-200 80 0 68z m0 208l0-68-200 80 0 68z m538 346q22-12 22-42l0-640q0-34-32-46l-398-160q-8-2-10-2t-5-1-5-1-5 1-5 1l-10 2-398 160q-32 12-32 46l0 640q0 30 22 42 22 16 46 6l382-154 382 154q24 10 46-6z m-478-788l0 560-320 128 0-560z m420 128l0 560-320-128 0-560z m-60 186l0-68-200-80 0 68z m0 208l0-68-200-80 0 68z" horiz-adv-x="900" /> + +<glyph glyph-name="book" unicode="" d="M682 594q18-8 18-28l0-562q0-14-12-25t-28-11q-46 0-46 36l0 522q0 12-12 18l-404 216q-32 10-68-10-44-20-56-44l408-228q18-8 18-28l0-550q0-22-18-28-6-4-16-4-14 0-20 4-8 6-202 127t-212 131q-26 18-26 34l-6 524q0 28 14 52 28 46 102 77t116 9z" horiz-adv-x="700" /> + +<glyph glyph-name="folder" unicode="" d="M954 500q32 0 40-12t6-36l-42-452q-2-24-12-37t-42-13l-806 0q-52 0-56 50l-42 452q-2 24 6 36t40 12l908 0z m-34 110l10-40-846 0 14 132q4 20 20 34t36 14l164 0q52 0 86-34l30-30q32-36 86-36l340 0q20 0 38-12t22-28z" horiz-adv-x="1001" /> + +<glyph glyph-name="archive" unicode="" d="M840 600l0-50-696 0 0 50q0 22 13 35t25 15l608 0q6 0 14-1t22-14 14-35z m-148 150q6 0 14-1t22-14 14-35l-498 0q0 22 13 35t25 15l410 0z m248-200q34-32 38-46 6-18 0-54l-76-450q-4-22-20-35t-28-15l-710 0q-52 0-60 50-6 26-39 223t-39 227q-10 22-3 44t10 26 21 20l10 10 30 30 0-80 836 0 0 80z m-248-270l0 100-70 0 0-80-260 0 0 80-68 0 0-100q0-50 48-50l300 0q22 0 35 12t13 24z" horiz-adv-x="981" /> + +<glyph glyph-name="box" unicode="" d="M870 750q12 0 21-9t9-21l0-120-900 0 0 120q0 12 9 21t21 9l840 0z m-820-730l0 530 800 0 0-530q0-30-21-50t-49-20l-660 0q-28 0-49 20t-21 50z m250 430l0-100 300 0 0 100-300 0z" horiz-adv-x="900" /> + +<glyph glyph-name="rss" unicode="" d="M0 730q314 0 537-223t223-537l-118 0q0 266-188 453t-454 187l0 120z m0-238q218 0 371-153t153-369l-118 0q0 166-119 285t-287 119l0 118z m114-296q46 0 80-33t34-81q0-46-34-79t-80-33-80 33-34 79q0 48 34 81t80 33z" horiz-adv-x="760" /> + +<glyph glyph-name="phone" unicode="" d="M461 290q162 162 118 206l-8 8q-30 30-41 48t-4 54 49 88q20 24 37 39t35 16 30 1 29-13 24-18 26-25 21-22q48-48-6-194t-204-294q-150-150-295-205t-193-7q-2 2-23 22t-25 25-18 24-13 31 2 30 15 35 38 37q42 34 70 47t54 2 35-18 39-37q44-44 208 120z" horiz-adv-x="800" /> + +<glyph glyph-name="cog" unicode="" d="M760 350q0-72 80-122-12-40-34-82-70 18-136-44-54-58-34-136-40-20-84-36-46 82-132 82t-132-82q-44 16-84 36 20 80-34 136-54 54-136 34-14 26-34 82 82 52 82 132 0 72-82 124 20 56 34 82 74-18 136 44 54 56 34 136 42 22 84 34 46-80 132-80t132 80q42-12 84-34-20-78 34-136 66-62 136-44 22-42 34-82-80-50-80-124z m-340-182q76 0 129 53t53 129-53 130-129 54-129-54-53-130 53-129 129-53z" horiz-adv-x="840" /> + +<glyph glyph-name="tools" unicode="" d="M155 506q-8-8-11-22t-3-25-2-11q-2-2-17-15t-19-17q-16-14-28 4l-70 76q-11 12 2 24 2 2 18 14t20 16q6 6 27 6t37 14q14 14 18 38t10 30q2 0 9 7t26 22 41 31q134 90 186 96 122 0 148-2 12 0-8-8-120-52-152-76-80-56-36-114 34-46 38-48 8-8-2-14-2-2-38-35t-38-35q-14-8-18-4-42 48-71 60t-67-12z m286-26l410-476q18-22-2-38l-48-42q-22-14-38 4l-414 472q-8 8 0 20l72 62q12 8 20-2z m554 202q16-104-16-166-50-88-154-62-56 12-100-32l-82-78-68 78 68 70q24 24 31 53t6 65 5 58q12 56 140 112 12 6 18-3t2-15q-12-12-46-80-14-10-12-35t40-53q58-40 96 22 6 12 26 41t22 33q4 10 13 9t11-17z m-858-684l254 248 76-86-246-242q-20-20-38-4l-46 46q-22 18 0 38z" horiz-adv-x="1000" /> + +<glyph glyph-name="share" unicode="" d="M650 200q62 0 106-43t44-107q0-62-44-106t-106-44-106 44-44 106q0 6 1 14t1 12l-260 156q-42-32-92-32-62 0-106 44t-44 106 44 106 106 44q54 0 92-30l260 156q0 4-1 12t-1 12q0 62 44 106t106 44 106-43 44-107q0-62-44-106t-106-44q-52 0-90 32l-262-156q2-8 2-26 0-16-2-24l262-156q36 30 90 30z" horiz-adv-x="800" /> + +<glyph glyph-name="shareable" unicode="" d="M340 350q0 68 47 114t113 46 113-46 47-114q0-66-47-113t-113-47-113 47-47 113z m-114 60q-14-60-66-60l-160 0 0 120 118 0q40 124 145 202t237 78q164 0 284-116 16-18 16-43t-16-43q-18-16-43-16t-43 16q-78 82-198 82-100 0-176-62t-98-158z m614-60l160 0 0-120-118 0q-40-124-144-202t-238-78q-164 0-282 118-18 18-18 43t18 41q16 18 41 18t43-18q82-82 198-82 100 0 176 63t98 157q12 60 66 60z" horiz-adv-x="1000" /> + +<glyph glyph-name="basket" unicode="" d="M150 0q0 40 30 70t70 30q42 0 71-30t29-70q0-42-29-71t-71-29q-40 0-70 29t-30 71z m500 0q0 40 30 70t70 30q42 0 71-30t29-70q0-42-29-71t-71-29q-40 0-70 29t-30 71z m-322 236q-36-10-34-23t44-13l562 0 0-76q0-20-20-20l-654 0q-20 0-20 20l0 76-10 46-98 454-98 0 0 80q0 20 20 20l156 0q20 0 20-20l0-86 704 0 0-274q0-22-18-26z" horiz-adv-x="900" /> + +<glyph glyph-name="bag" unicode="" d="M835 668q28-26 24-60l-98-648q-8-30-38-30l-586 0q-28 0-40 30-94 620-96 648-5 34 22 60 6 6 54 43t56 43q18 16 56 16l480 0q38 0 56-16 78-58 110-86z m-406-436q56 0 98 34t63 89 30 89 13 66l-92 0q-38-188-112-188t-112 188l-92 0q46-278 204-278z m-352 368l704 0-110 116-484 0z" horiz-adv-x="859" /> + +<glyph glyph-name="calendar" unicode="" d="M800 700q42 0 71-29t29-71l0-600q0-40-29-70t-71-30l-700 0q-40 0-70 30t-30 70l0 600q0 42 30 71t70 29l46 0 0-100 160 0 0 100 290 0 0-100 160 0 0 100 44 0z m0-700l0 400-700 0 0-400 700 0z m-540 800l0-170-70 0 0 170 70 0z m450 0l0-170-70 0 0 170 70 0z" horiz-adv-x="900" /> + +<glyph glyph-name="login" unicode="" d="M800 800q42 0 71-29t29-71l0-700q0-40-29-70t-71-30l-450 0q-40 0-69 30t-29 70l0 100 98 0 0-100 450 0 0 700-450 0 0-150-98 0 0 150q0 42 29 71t69 29l450 0z m-350-670l0 120-450 0 0 150 450 0 0 120 200-194z" horiz-adv-x="900" /> + +<glyph glyph-name="logout" unicode="" d="M502 0l0 100 98 0 0-100q0-40-29-70t-71-30l-400 0q-40 0-70 30t-30 70l0 700q0 42 30 71t70 29l400 0q42 0 71-29t29-71l0-150-98 0 0 150-402 0 0-700 402 0z m398 326l-198-196 0 120-450 0 0 150 450 0 0 120z" horiz-adv-x="900" /> + +<glyph glyph-name="mic" unicode="" d="M620 488q20 0 20-20l0-138q0-92-69-164t-201-84l0-132 130 0q20 0 20-20l0-60q0-20-20-20l-360 0q-20 0-20 20l0 60q0 20 20 20l130 0 0 132q-132 12-201 84t-69 164l0 138q0 20 20 20l30 0q20 0 20-20l0-138q0-66 59-123t191-57 191 57 59 123l0 138q0 20 20 20l30 0z m-300-238q-80 0-115 25t-35 55l0 158 300 0 0-158q0-30-35-55t-115-25z m150 520l0-212-300 0 0 212q0 30 35 55t115 25 115-25 35-55z" horiz-adv-x="640" /> + +<glyph glyph-name="mute" unicode="" d="M868 778q16-16 16-36t-16-36l-782-782q-18-14-34-14-18 0-36 14-16 14-16 36t16 36l782 782q34 32 70 0z m-216-386l50 50q74-92 101-172t-7-116q-24-24-75-57t-131-71-161-45-165 23l278 276q44-32 88-54t67-25 33 1q6 10 2 34t-26 68-54 88z m-276 62l-270-270q-40 132 28 283t132 215q34 32 105 11t159-85l-52-50q-58 38-105 53t-57 5q-4-8-2-28t19-58 43-76z" horiz-adv-x="884" /> + +<glyph glyph-name="sound" unicode="" d="M176 588q42 42 149-5t217-157 157-217 5-149q-28-28-92-67t-156-78-194-29-176 84-84 176 29 194 78 156 67 92z m464-480q8 10-3 49t-49 101-96 118q-56 58-118 96t-101 49-49 3q-8-10 3-49t49-101 94-120q58-56 120-94t101-49 49-3z m6 394q-18 0-34 16-16 14-16 35t16 35l94 96q36 32 72 0 32-36 0-72l-96-94q-16-16-36-16z m-180 124q-18 10-23 30t5 38l54 96q26 44 68 20 18-10 23-30t-5-38l-54-96q-14-26-42-26-14 0-26 6z m438-150q10-18 4-38t-24-30l-96-54q-16-8-24-8-28 0-44 26-10 18-4 38t24 30l96 54q18 10 38 5t30-23z" horiz-adv-x="910" /> + +<glyph glyph-name="volume" unicode="" d="M896 180q0-34-24-57t-56-23l-780 0q-22 0-31 5t-3 15 24 20l802 452q28 18 48 7t20-45l0-374z" horiz-adv-x="896" /> + +<glyph glyph-name="clock" unicode="" d="M460 810q190 0 325-135t135-325-135-325-325-135-325 135-135 325 135 325 325 135z m0-820q150 0 255 106t105 254q0 150-105 255t-255 105q-148 0-254-105t-106-255q0-148 106-254t254-106z m36 620l0-244 150-150-50-50-170 170 0 274 70 0z" horiz-adv-x="920" /> + +<glyph glyph-name="hourglass" unicode="" d="M560 622q0-44-48-96t-97-99-49-77 49-76 97-97 48-97l0-118q0-34-86-73t-194-39-194 39-86 73l0 118q0 46 48 97t97 97 49 76-49 77-97 99-48 96l0 118q0 32 87 71t193 39 193-39 87-71l0-118z m-482 112l-18-14q-4-8 4-14 92-52 216-52 132 0 220 50 14 10-16 30-96 54-202 54-120 0-204-54z m228-384q0 18 4 33t18 33 20 25 31 31 29 28q92 92 92 122l2 50q-100-54-222-54t-222 54l4-50q0-32 90-122 6-6 22-21t23-22l19-19t17-21 11-20 9-23 3-24q0-10-1-19t-6-18-8-16-11-17l-12-15t-15-16-16-15-18-16-17-16q-90-90-90-122l0-66q8 4 66 23t92 43 34 58q0 30 26 30t26-30q0-34 33-58t94-43 67-23l0 66q0 30-92 122-4 4-21 20t-22 21-18 19-18 22-12 20-9 23-2 23z" horiz-adv-x="560" /> + +<glyph glyph-name="lamp" unicode="" d="M209-110l0 104 282 0 0-104q-70-42-142-40-70-2-140 40z m276 164l-270 0q0 72-36 140t-78 113-74 112-26 139q8 120 94 206t254 86q170 0 255-86t95-206q4-60-16-113t-52-96-65-85-57-96-24-114z m-378 496q-4-4 0-20t2-20 5-19 6-18 8-18 11-19 13-19 14-19 15-21 16-23q88-122 112-212l82 0q24 94 112 212 4 6 25 35t25 36 17 29 16 33 6 28 1 35q-16 196-244 196-226 0-242-196z" horiz-adv-x="700" /> + +<glyph glyph-name="light-down" unicode="" d="M350 510q68 0 114-47t46-113q0-68-46-114t-114-46q-66 0-113 46t-47 114q0 66 47 113t113 47z m0-264q44 0 73 30t29 74q0 42-29 72t-73 30q-42 0-72-30t-30-72q0-44 30-74t72-30z m-300 144q20 0 35-12t15-28q0-40-50-40t-50 40q0 16 15 28t35 12z m546 204q28-28-8-64-14-14-33-16t-29 10q-12 12-10 31t16 33q36 34 64 6z m54-204q20 0 35-12t15-28q0-40-50-40-48 0-48 40 0 16 14 28t34 12z m-300-290q16 0 28-15t12-35-12-35-28-15-28 15-12 35 12 35 28 15z m-238 62q36 36 64 8t-8-64q-14-14-33-16t-29 8q-30 28 6 64z m-10 430q28 28 64-8 14-14 16-33t-8-29q-30-28-64 6-36 36-8 64z m432-484q-34 36-6 64t64-8q14-14 16-33t-10-29q-30-28-64 6z m-184 492q-16 0-28 15t-12 35 12 35 28 15 28-15 12-35-12-35-28-15z" horiz-adv-x="700" /> + +<glyph glyph-name="light-up" unicode="" d="M950 390q20 0 35-12t15-28q0-40-50-40l-48 0q-50 0-50 40 0 16 15 28t35 12l48 0z m-450 234q114 0 195-80t81-194q0-116-81-196t-195-80-194 80-80 196q0 114 80 194t194 80z m0-474q82 0 141 58t59 142q0 82-59 141t-141 59-141-59-59-141q0-84 59-142t141-58z m-350 200q0-40-50-40l-50 0q-50 0-50 40 0 16 15 28t35 12l50 0q20 0 35-12t15-28z m350 350q-16 0-28 15t-12 35l0 50q0 20 12 35t28 15 28-15 12-35l0-50q0-20-12-35t-28-15z m0-700q16 0 28-15t12-35l0-50q0-20-12-35t-28-15-28 15-12 35l0 50q0 20 12 35t28 15z m368 660l-34-34q-34-34-64-8-28 28 8 64 4 6 34 36 36 34 64 6t-8-64z m-700-588q14 16 33 18t29-10q12-12 10-31t-16-33l-36-36q-14-14-33-16t-29 10q-30 28 6 64 6 4 36 34z m20 646l36-36q36-36 6-64-10-10-29-8t-33 16q-30 30-36 34-14 14-16 33t10 31q10 12 29 10t33-16z m590-702q-36 36-8 64t64-8l34-34q36-36 8-64t-64 6q-30 30-34 36z" horiz-adv-x="1000" /> + +<glyph glyph-name="adjust" unicode="" d="M950 390q20 0 35-12t15-28q0-40-50-40l-48 0q-50 0-50 40 0 16 15 28t35 12l48 0z m-450 234q114 0 195-80t81-194q0-116-81-196t-195-80-194 80-80 196q0 114 80 194t194 80z m6-474l0 400q-86 0-146-59t-60-141q0-84 60-142t146-58z m-356 200q0-40-50-40l-50 0q-50 0-50 40 0 16 15 28t35 12l50 0q20 0 35-12t15-28z m350 350q-16 0-28 15t-12 35l0 50q0 20 12 35t28 15 28-15 12-35l0-50q0-20-12-35t-28-15z m0-700q16 0 28-15t12-35l0-50q0-20-12-35t-28-15-28 15-12 35l0 50q0 20 12 35t28 15z m368 660l-34-34q-34-34-64-8-28 28 8 64 4 6 34 36 36 34 64 6t-8-64z m-700-588q14 16 33 18t29-10q12-12 10-31t-16-33l-36-36q-14-14-33-16t-29 10q-30 28 6 64 6 4 36 34z m20 646l36-36q36-36 6-64-10-10-29-8t-33 16q-30 30-36 34-14 14-16 33t10 31q10 12 29 10t33-16z m590-702q-36 36-8 64t64-8l34-34q36-36 8-64t-64 6q-30 30-34 36z" horiz-adv-x="1000" /> + +<glyph glyph-name="block" unicode="" d="M480 830q200 0 340-140t140-340q0-198-140-339t-340-141q-198 0-339 141t-141 339q0 200 141 340t339 140z m258-220z m-622-260q0-132 82-230l514 514q-100 82-232 82-152 0-258-107t-106-259z m106-258z m258-106q152 0 259 107t107 257q0 130-82 232l-514-514q98-82 230-82z" horiz-adv-x="960" /> + +<glyph glyph-name="resize-full" unicode="" d="M476 746l316 0 0-316-100 124-146-152-100 100 152 146z m-230-444l100-100-152-146 122-100-316 0 0 316 100-122z" horiz-adv-x="792" /> + +<glyph glyph-name="resize-small" unicode="" d="M156 146l-106 100 296 0 0-296-100 106-146-156-100 100z m744 554l-154-144 104-100-294 0 0 294 100-104 144 154z" horiz-adv-x="900" /> + +<glyph glyph-name="popup" unicode="" d="M700 750q42 0 71-29t29-71l0-400q0-40-29-70t-71-30l-400 0q-40 0-70 30t-30 70l0 402q0 40 29 69t71 29l400 0z m0-500l0 400-400 0 0-400 400 0z m-600 100l0-300 300 0 0-100-300 0q-40 0-70 30t-30 70l0 300 100 0z" horiz-adv-x="800" /> + +<glyph glyph-name="publish" unicode="" d="M900 800q42 0 71-30t29-70l0-600q0-42-29-71t-71-29l-198 0 0 98 200 0 0 462-802 0 0-462 200 0 0-98-200 0q-40 0-70 29t-30 71l0 600q0 40 30 70t70 30l800 0z m-770-168q38 0 38 38 0 16-11 26t-27 10-27-11-11-25q0-16 11-27t27-11z m100 0q38 0 38 38 0 16-11 26t-27 10-27-11-11-25q0-16 11-27t27-11z m672 6l0 62-602 0 0-62 602 0z m-404-198l242-240-150 0 0-300-184 0 0 300-150 0z" horiz-adv-x="1000" /> + +<glyph glyph-name="window" unicode="" d="M900 750q42 0 71-30t29-70l0-600q0-42-29-71t-71-29l-800 0q-40 0-70 29t-30 71l0 600q0 40 30 70t70 30l800 0z m-670-94q-16 0-27-11t-11-25q0-16 11-27t27-11q38 0 38 38 0 16-11 26t-27 10z m-138-36q0-16 11-27t27-11q38 0 38 38 0 16-11 26t-27 10-27-11-11-25z m810-570l0 460-802 0 0-460 802 0z m0 540l0 60-602 0 0-60 602 0z" horiz-adv-x="1000" /> + +<glyph glyph-name="arrow-combo" unicode="" d="M230 850l230-364-460 0z m0-1000l-230 366 460 0z" horiz-adv-x="460" /> + +<glyph glyph-name="down-circled" unicode="" d="M460 810q190 0 325-135t135-325-135-325-325-135-325 135-135 325 135 325 325 135z m0-820q148 0 254 106t106 254q0 150-106 255t-254 105-254-105-106-255q0-148 106-254t254-106z m90 554l0-206 112 0-202-190-202 190 112 0 0 206 180 0z" horiz-adv-x="920" /> + +<glyph glyph-name="left-circled" unicode="" d="M920 350q0-190-135-325t-325-135-325 135-135 325q0 192 135 326t325 134 325-134 135-326z m-820 0q0-148 106-254t254-106 254 106 106 254q0 150-106 255t-254 105-254-105-106-255z m552-90l-204 0 0-112-190 202 190 204 0-114 204 0 0-180z" horiz-adv-x="920" /> + +<glyph glyph-name="right-circled" unicode="" d="M0 350q0 190 135 325t325 135 325-135 135-325-135-325-325-135-325 135-135 325z m820 0q0 150-105 255t-255 105q-148 0-254-105t-106-255q0-148 106-254t254-106q150 0 255 106t105 254z m-552 90l204 0 0 114 190-204-190-202 0 112-204 0 0 180z" horiz-adv-x="920" /> + +<glyph glyph-name="up-circled" unicode="" d="M460-110q-190 0-325 135t-135 325q0 192 135 326t325 134 325-134 135-326q0-190-135-325t-325-135z m0 820q-148 0-254-105t-106-255q0-148 106-254t254-106q150 0 255 106t105 254q0 150-105 255t-255 105z m-90-552l0 204-112 0 202 192 202-192-112 0 0-204-180 0z" horiz-adv-x="920" /> + +<glyph glyph-name="down-open" unicode="" d="M564 422l-234-224q-18-18-40-18t-40 18l-234 224q-16 16-16 41t16 41q38 38 78 0l196-188 196 188q40 38 78 0 16-16 16-41t-16-41z" horiz-adv-x="580" /> + +<glyph glyph-name="left-open" unicode="" d="M242 626q14 16 39 16t41-16q38-36 0-80l-186-196 186-194q38-44 0-80-16-16-40-16t-40 16l-226 236q-16 16-16 38 0 24 16 40 206 214 226 236z" horiz-adv-x="341" /> + +<glyph glyph-name="right-open" unicode="" d="M98 626l226-236q16-16 16-40 0-22-16-38l-226-236q-16-16-40-16t-40 16q-36 36 0 80l186 194-186 196q-36 44 0 80 16 16 41 16t39-16z" horiz-adv-x="340" /> + +<glyph glyph-name="up-open" unicode="" d="M564 280q16-16 16-41t-16-41q-38-38-78 0l-196 188-196-188q-40-38-78 0-16 16-16 41t16 41l234 224q16 16 40 16t40-16z" horiz-adv-x="580" /> + +<glyph glyph-name="down-open-mini" unicode="" d="M405 470q22 26 48 0 26-22 0-48l-196-192q-22-22-48 0l-196 192q-26 26 0 48 24 24 50 0l170-156z" horiz-adv-x="466" /> + +<glyph glyph-name="left-open-mini" unicode="" d="M252 180q26-26 0-48-26-26-48 0l-192 194q-24 24 0 50l192 194q22 26 48 0 26-22 0-48l-156-172z" horiz-adv-x="265" /> + +<glyph glyph-name="right-open-mini" unicode="" d="M13 180l158 170-158 172q-26 26 0 48 26 26 48 0l192-194q24-26 0-50l-192-194q-22-26-48 0-26 22 0 48z" horiz-adv-x="265" /> + +<glyph glyph-name="up-open-mini" unicode="" d="M62 230q-26-22-50 0-24 24 0 50l196 190q26 26 48 0l196-190q24-26 0-50-24-22-50 0l-170 158z" horiz-adv-x="464" /> + +<glyph glyph-name="down-open-big" unicode="" d="M63 570l370-356 372 356q22 26 48 0 26-22 0-48l-396-392q-22-22-48 0l-396 392q-26 26 0 48 24 24 50 0z" horiz-adv-x="866" /> + +<glyph glyph-name="left-open-big" unicode="" d="M452-20q26-26 0-48-26-26-48 0l-392 394q-24 24 0 50l392 394q22 26 48 0 26-22 0-48l-358-372z" horiz-adv-x="465" /> + +<glyph glyph-name="right-open-big" unicode="" d="M13-20l358 370-358 372q-26 26 0 48 26 26 48 0l392-394q24-26 0-50l-392-394q-22-26-48 0-26 22 0 48z" horiz-adv-x="465" /> + +<glyph glyph-name="up-open-big" unicode="" d="M804 130l-372 358-370-358q-26-22-50 0-24 24 0 50l396 390q26 26 48 0l396-390q24-26 0-50-26-22-48 0z" horiz-adv-x="864" /> + +<glyph glyph-name="down" unicode="" d="M660 366l-330-380-330 380 192 0 0 350 276 0 0-350 192 0z" horiz-adv-x="660" /> + +<glyph glyph-name="left" unicode="" d="M378 20l-378 330 378 330 0-190 352 0 0-278-352 0 0-192z" horiz-adv-x="730" /> + +<glyph glyph-name="right" unicode="" d="M350 680l380-330-380-330 0 192-350 0 0 278 350 0 0 190z" horiz-adv-x="730" /> + +<glyph glyph-name="up" unicode="" d="M660 336l-192 0 0-350-276 0 0 350-192 0 330 380z" horiz-adv-x="660" /> + +<glyph glyph-name="down-dir" unicode="" d="M460 550l-230-400-230 400 460 0z" horiz-adv-x="460" /> + +<glyph glyph-name="left-dir" unicode="" d="M400 580l0-460-400 230z" horiz-adv-x="400" /> + +<glyph glyph-name="right-dir" unicode="" d="M0 580l400-230-400-230 0 460z" horiz-adv-x="400" /> + +<glyph glyph-name="up-dir" unicode="" d="M0 150l230 400 230-400-460 0z" horiz-adv-x="460" /> + +<glyph glyph-name="down-bold" unicode="" d="M760 366l-380-380-380 380 192 0 0 350 376 0 0-350 192 0z" horiz-adv-x="760" /> + +<glyph glyph-name="left-bold" unicode="" d="M378 730l0-190 352 0 0-378-352 0 0-192-378 380z" horiz-adv-x="730" /> + +<glyph glyph-name="right-bold" unicode="" d="M350 730l380-380-380-380 0 192-350 0 0 378 350 0 0 190z" horiz-adv-x="730" /> + +<glyph glyph-name="up-bold" unicode="" d="M760 336l-192 0 0-350-376 0 0 350-192 0 380 380z" horiz-adv-x="760" /> + +<glyph glyph-name="down-thin" unicode="" d="M500 100l-250-240-250 240 162 0 0 740 176 0 0-740 162 0z" horiz-adv-x="500" /> + +<glyph glyph-name="left-thin" unicode="" d="M240 100l-240 250 240 250 0-160 740 0 0-178-740 0 0-162z" horiz-adv-x="980" /> + +<glyph glyph-name="right-thin" unicode="" d="M742 100l0 162-742 0 0 178 742 0 0 160 238-250z" horiz-adv-x="980" /> + +<glyph glyph-name="up-thin" unicode="" d="M500 602l-162 0 0-742-176 0 0 742-162 0 250 238z" horiz-adv-x="500" /> + +<glyph glyph-name="ccw" unicode="" d="M532 736q170 0 289-120t119-290-119-290-289-120q-142 0-252 88l70 74q84-60 182-60 126 0 216 90t90 218-90 218-216 90q-124 0-214-87t-92-211l142 0-184-204-184 204 124 0q2 166 122 283t286 117z" horiz-adv-x="940" /> + +<glyph glyph-name="cw" unicode="" d="M408 760q168 0 287-116t123-282l122 0-184-206-184 206 144 0q-4 124-94 210t-214 86q-126 0-216-90t-90-218q0-126 90-216t216-90q104 0 182 60l70-76q-110-88-252-88-168 0-288 120t-120 290 120 290 288 120z" horiz-adv-x="940" /> + +<glyph glyph-name="arrows-ccw" unicode="" d="M186 140l116 116 0-292-276 16 88 86q-116 122-114 290t120 288q100 100 240 116l4-102q-100-16-172-88-88-88-90-213t84-217z m332 598l276-16-88-86q116-122 114-290t-120-288q-96-98-240-118l-2 104q98 16 170 88 88 88 90 213t-84 217l-114-116z" horiz-adv-x="820" /> + +<glyph glyph-name="level-down" unicode="" d="M100 200q-42 0-71 30t-29 70l0 350 140 0 0-310 364 0 0 150 240-220-240-220 0 150-404 0z" horiz-adv-x="744" /> + +<glyph glyph-name="level-up" unicode="" d="M200 350l0-90-200 160 200 170 0-100 550 0q40 0 70-29t30-71l0-280-140 0 0 240-510 0z" horiz-adv-x="850" /> + +<glyph glyph-name="shuffle" unicode="" d="M754 516q-54 0-105-32t-80-66-83-104q-48-62-75-94t-78-77-107-66-122-21l-104 0 0 140 104 0q54 0 106 32t81 66 83 104q62 82 101 126t116 88 163 44l36 0 0 120 210-180-210-180 0 100-36 0z m-484-88q-74 78-166 78l-104 0 0 140 104 0q140 0 254-108-14-16-37-45t-27-33q-8-12-24-32z m520-242l0 100 210-180-210-180 0 120-36 0q-140 0-260 116 46 58 72 92 0 2 6 9t8 11q84-88 174-88l36 0z" horiz-adv-x="1000" /> + +<glyph glyph-name="loop" unicode="" d="M800 540q42 0 71-29t29-71l0-290q0-40-29-70t-71-30l-700 0q-40 0-70 30t-30 70l0 290q0 42 30 71t70 29l250 0 0 110 200-180-200-180 0 110-210 0 0-210 620 0 0 210-150 0 0 140 190 0z" horiz-adv-x="900" /> + +<glyph glyph-name="switch" unicode="" d="M700 592l0-140-500 0 0-90-200 160 200 170 0-100 500 0z m300-420l-200-160 0 90-500 0 0 140 500 0 0 100z" horiz-adv-x="1000" /> + +<glyph glyph-name="play" unicode="" d="M486 376q14-10 14-26 0-14-14-24l-428-266q-24-16-41-6t-17 40l0 514q0 30 17 40t41-6z" horiz-adv-x="500" /> + +<glyph glyph-name="stop" unicode="" d="M526 650q74 0 74-64l0-470q0-66-74-66l-450 0q-76 0-76 66l0 470q0 36 18 50t58 14l450 0z" horiz-adv-x="600" /> + +<glyph glyph-name="pause" unicode="" d="M440 700q90 0 90-64l0-570q0-66-90-66t-90 66l0 570q0 64 90 64z m-350 0q90 0 90-64l0-570q0-66-90-66t-90 66l0 570q0 64 90 64z" horiz-adv-x="530" /> + +<glyph glyph-name="record" unicode="" d="M350 700q146 0 248-102t102-248q0-144-102-247t-248-103-248 103-102 247q0 146 102 248t248 102z" horiz-adv-x="700" /> + +<glyph glyph-name="to-end" unicode="" d="M412 374q14-10 14-24 0-12-14-22l-362-228q-22-14-36-5t-14 35l0 442q0 26 14 35t36-5z m114 268q74 0 74-58l0-466q0-58-74-58-76 0-76 58l0 466q0 58 76 58z" horiz-adv-x="600" /> + +<glyph glyph-name="to-start" unicode="" d="M174 350q0 14 14 24l364 228q20 14 34 5t14-35l0-442q0-26-14-35t-34 5l-364 228q-14 10-14 22z m-174 234q0 58 76 58 74 0 74-58l0-466q0-58-74-58-76 0-76 58l0 466z" horiz-adv-x="600" /> + +<glyph glyph-name="fast-forward" unicode="" d="M866 374q14-10 14-24t-14-22l-372-248q-22-14-37-6t-15 36l0 482q0 28 15 36t37-6z m-454 0q14-10 14-24t-14-22l-360-248q-20-14-36-6t-16 36l0 482q0 28 16 36t36-6z" horiz-adv-x="880" /> + +<glyph glyph-name="fast-backward" unicode="" d="M0 350q0 14 14 24l374 248q20 14 36 6t16-36l0-482q0-28-16-36t-36 6l-374 248q-14 8-14 22z m454 0q0 14 14 24l360 248q20 14 36 6t16-36l0-482q0-28-16-36t-36 6l-360 248q-14 8-14 22z" horiz-adv-x="880" /> + +<glyph glyph-name="progress-0" unicode="" d="M1000 450l0-250q0-42-29-71t-71-29l-800 0q-40 0-70 29t-30 71l0 300q0 40 30 70t70 30l800 0q42 0 71-30t29-70l0-50z m-100-250l0 300-800 0 0-300 800 0z" horiz-adv-x="1000" /> + +<glyph glyph-name="progress-1" unicode="" d="M1000 450l0-250q0-42-29-71t-71-29l-800 0q-40 0-70 29t-30 71l0 300q0 40 30 70t70 30l800 0q42 0 71-30t29-70l0-50z m-100-250l0 300-800 0 0-300 800 0z m-750 50l0 198 200 0 0-198-200 0z" horiz-adv-x="1000" /> + +<glyph glyph-name="progress-2" unicode="" d="M1000 450l0-250q0-42-29-71t-71-29l-800 0q-40 0-70 29t-30 71l0 300q0 40 30 70t70 30l800 0q42 0 71-30t29-70l0-50z m-100-250l0 300-800 0 0-300 800 0z m-750 50l0 198 200 0 0-198-200 0z m250 0l0 198 200 0 0-198-200 0z" horiz-adv-x="1000" /> + +<glyph glyph-name="progress-3" unicode="" d="M1000 450l0-250q0-42-29-71t-71-29l-800 0q-40 0-70 29t-30 71l0 300q0 40 30 70t70 30l800 0q42 0 71-30t29-70l0-50z m-100-250l0 300-800 0 0-300 800 0z m-750 50l0 198 200 0 0-198-200 0z m250 0l0 198 200 0 0-198-200 0z m250 198l200 0 0-198-200 0 0 198z" horiz-adv-x="1000" /> + +<glyph glyph-name="target" unicode="" d="M430 780q178 0 304-126t126-304-126-304-304-126-304 126-126 304 126 304 304 126z m36-778q124 14 212 102t100 212l-192 0 0 70 192 0q-12 124-100 212t-212 102l0-194-70 0 0 194q-124-14-213-102t-101-212l194 0 0-70-194 0q12-124 101-212t213-102l0 194 70 0 0-194z" horiz-adv-x="860" /> + +<glyph glyph-name="list" unicode="" d="M100 200q20 0 35-15t15-35-15-35-35-15l-50 0q-20 0-35 15t-15 35 14 35 36 15l50 0z m0 200q20 0 35-15t15-35-15-35-35-15l-50 0q-20 0-35 15t-15 35 14 35 36 15l50 0z m0 200q20 0 35-15t15-35-15-35-35-15l-50 0q-20 0-35 15t-15 35 14 35 36 15l50 0z m200-100q-20 0-35 15t-15 35 15 35 35 15l350 0q22 0 36-15t14-35-15-35-35-15l-350 0z m350-100q22 0 36-15t14-35-15-35-35-15l-350 0q-20 0-35 15t-15 35 15 35 35 15l350 0z m0-200q22 0 36-15t14-35-15-35-35-15l-350 0q-20 0-35 15t-15 35 15 35 35 15l350 0z" horiz-adv-x="700" /> + +<glyph glyph-name="list-add" unicode="" d="M350 400q22 0 36-15t14-35-15-35-35-15l-300 0q-20 0-35 15t-15 35 14 35 36 15l300 0z m0-200q22 0 36-15t14-35-15-35-35-15l-300 0q-20 0-35 15t-15 35 14 35 36 15l300 0z m620 200q30 0 30-50t-30-50l-170 0 0-170q0-30-50-30t-50 30l0 170-164 0q-30 0-30 50t30 50l164 0 0 170q0 30 50 30t50-30l0-170 170 0z m-620 200q22 0 36-15t14-35-15-35-35-15l-300 0q-20 0-35 15t-15 35 14 35 36 15l300 0z" horiz-adv-x="1000" /> + +<glyph glyph-name="battery" unicode="" d="M770 350q0-98 36-157t78-59l66 0q-30-46-64-65t-118-19l-500 0q-130 0-199 94t-69 206q0 110 69 205t199 95l500 0q84 0 118-19t64-65l-66 0q-42 0-78-60t-36-156z m-136-90q10 12-8 26-136 134-178 164-16 10-26 13t-18-5-10-12-8-18l-22-56-148 66q-26 12-34 0-8-14 8-28 136-132 180-162 34-16 42-11t18 31l24 58 146-68q26-12 34 2z m310 192q22 0 39-27t17-71-17-72-39-28l-38 0q-22 0-38 28t-16 72 16 71 38 27l38 0z" horiz-adv-x="1000" /> + +<glyph glyph-name="back-in-time" unicode="" d="M532 760q170 0 289-120t119-290-119-290-289-120q-138 0-252 88l70 76q82-60 182-60 126 0 216 90t90 216q0 128-90 218t-216 90q-124 0-213-86t-93-210l142 0-184-206-184 206 124 0q4 166 123 282t285 116z m-36-190l70 0 0-204 130-130-50-50-150 150 0 234z" horiz-adv-x="940" /> + +<glyph glyph-name="monitor" unicode="" d="M900 790q42 0 71-30t29-70l0-550q0-42-29-77t-69-43l-218-44 86-38q50-28-20-28l-500 0q-98 0 32 52l36 14-220 44q-40 8-69 43t-29 77l0 550q0 40 30 70t70 30l800 0z m0-646l0 556-800 0 0-556 800 0z" horiz-adv-x="1000" /> + +<glyph glyph-name="mobile" unicode="" d="M480 840q42 0 71-29t29-71l0-780q0-40-29-70t-71-30l-380 0q-40 0-70 30t-30 70l0 780q0 42 30 71t70 29l380 0z m-190-940q30 0 50 15t20 35q0 22-20 36t-50 14q-28 0-49-15t-21-35 21-35 49-15z m210 150l0 660-420 0 0-660 420 0z" horiz-adv-x="580" /> + +<glyph glyph-name="cd" unicode="" d="M460 810q190 0 325-135t135-325-135-325-325-135-325 135-135 325 135 325 325 135z m0-610q62 0 106 44t44 106q0 64-43 107t-107 43q-62 0-106-44t-44-106 44-106 106-44z" horiz-adv-x="920" /> + +<glyph glyph-name="inbox" unicode="" d="M967 398q40-42 30-72l-28-154q-4-20-22-33t-40-13l-816 0q-22 0-40 13t-22 33l-28 154q-8 32 32 72 8 10 36 38t68 67 52 51q22 22 52 22l516 0q30 0 52-22 16-16 53-52t67-65 38-39z m-266-32l178 0-102 114-556 0-102-114 178 0q8 0 12-8l40-100 300 0 40 100q4 8 12 8z" horiz-adv-x="999" /> + +<glyph glyph-name="install" unicode="" d="M884 306q24-52 14-96l-34-184q-2-20-19-35t-39-15l-712 0q-22 0-39 15t-19 35l-34 184q-8 50 14 96l158 374q22 46 72 46l104 0-20-204-134 0 254-210 256 210-136 0-18 204 102 0q50 0 74-46z m-68-132q2 22-10 38t-34 16l-644 0q-22 0-34-16t-10-38l14-74q2-22 19-37t37-15l592 0q22 0 39 15t19 37z" horiz-adv-x="901" /> + +<glyph glyph-name="globe" unicode="" d="M480 830q200 0 340-141t140-339q0-200-140-340t-340-140q-198 0-339 140t-141 340q0 198 141 339t339 141z m410-480q0 132-78 239t-202 149q-18-24-16-32 4-38 18-51t30-7l32 12t20 2q22-24 0-47t-45-56-1-77q34-64 96-64 28-2 43-36t17-66q10-80-14-140-22-44 14-76 86 112 86 250z m-466 404q-112-14-199-84t-127-174q6 0 22-2t28-3 26-4 24-8 12-13q4-12-14-45t-18-61q0-30 38-56t38-46q0-28 8-68t8-44q0-12 36-54t52-42q10 0 11 22t-2 54-3 40q0 32 14 74 12 42 59 70t55 46q16 34 9 61t-17 43-34 28-41 17-37 9-22 4q-16 6-42 7t-36-3-27 11-17 29q0 10 15 27t35 37 28 30q8 14 17 21t22 16 27 21q4 4 25 17t27 23z m-72-794q66-20 128-20 128 0 226 68-26 44-118 34-24-2-65-17t-47-17q-74-16-76-16-12-2-26-14t-22-18z" horiz-adv-x="960" /> + +<glyph glyph-name="cloud" unicode="" d="M760 494q100 0 170-68t70-166-70-166-170-68l-578 0q-74 0-128 52t-54 124q0 74 53 126t129 52q2 0 10-1t10-1q-2 12-2 38 0 108 78 184t188 76q90 0 160-52t94-134q28 4 40 4z" horiz-adv-x="1000" /> + +<glyph glyph-name="cloud-thunder" unicode="" d="M760 494q100 0 170-68t70-166-70-166-170-68l-578 0q-74 0-128 52t-54 124q0 74 53 126t129 52q2 0 10-1t10-1q-2 12-2 38 0 108 78 184t188 76q90 0 160-52t94-134q28 4 40 4z m-192-216q14 16 14 30 0 20-30 32l-4 0q-26 14-38 16l50 116q6 0 6 20 0 14-8 18-16 10-34-8-2-2-30-32t-61-66-45-52q-12-18-12-30 0-22 30-30l4-2q8-4 38-16l-52-114-2-8q-2-8-2-14 0-10 8-18 18-10 34 10 100 100 134 148z" horiz-adv-x="1000" /> + +<glyph glyph-name="flash" unicode="" d="M40-100q-4 4 35 94t79 182 38 98-94 45-98 55q-4 12 84 120t180 209 96 97q6-4-74-186t-78-186 95-43 97-57q4-20-174-227t-186-201z" horiz-adv-x="400" /> + +<glyph glyph-name="moon" unicode="" d="M524 238q106 106 125 252t-53 270q52-26 96-72 128-128 128-309t-128-309-310-128-310 128q-40 40-72 94 124-70 271-51t253 125z" horiz-adv-x="820" /> + +<glyph glyph-name="flight" unicode="" d="M268-120l124 400-180 0-112-100-100 0 80 170-80 170 100 0 112-100 180 0-124 400 100 0 224-400 274 0t36-4 46-11 36-21 16-34q0-32-38-49t-74-19l-38-2-258 0-224-400-100 0z" horiz-adv-x="1000" /> + +<glyph glyph-name="paper-plane" unicode="" d="M894 720q14 4 22-3t4-19q-2-6-72-310t-74-316q-2-14-14-19t-24 1l-248 134-30 16 22 26q388 420 394 426 4 4-1 9t-9 1l-550-402-112 44-190 76q-12 4-12 12t12 12q8 4 441 157t441 155z m-582-728l0 204 160-82q-130-116-142-128-18-14-18 6z" horiz-adv-x="921" /> + +<glyph glyph-name="leaf" unicode="" d="M236 646q182 106 506 66 168-22 196-50 4-6-2-10-76-40-130-109t-78-132-65-132-93-105q-138-96-382-4-66-76-114-176-12-24-47-7t-25 39q44 100 129 193t176 153 176 106 141 68l54 20q-14 0-41-1t-104-14-148-38-162-84-161-141q-22 242 174 358z" horiz-adv-x="940" /> + +<glyph glyph-name="lifebuoy" unicode="" d="M454 810q190 2 326-130t140-322q2-190-131-327t-323-141q-190-2-327 131t-139 323q-4 190 130 327t324 139z m0-60q-94 0-178-44l62-104q56 28 122 28t122-28l62 104q-88 46-190 44z m-246-522q-28 60-28 122 0 64 28 124l-102 62q-46-88-46-190 2-96 46-180z m258-278q98 4 178 46l-62 104q-60-30-122-30t-122 30l-62-104q86-46 190-46z m-6 180q92 0 156 65t64 155q0 92-64 156t-156 64-156-64-64-156q0-90 64-155t156-65z m252 98l104-62q46 96 44 190 0 96-44 180l-104-62q28-60 28-124 0-62-28-122z" horiz-adv-x="920" /> + +<glyph glyph-name="mouse" unicode="" d="M551 130q28-80-17-157t-139-111q-94-28-175 9t-103 117l-106 384q-20 68 6 134t84 106l-96 186q-14 34 14 48 30 18 48-14l98-192q80 22 154-16t102-116z m-324 274q28 10 40 36t4 54q-10 28-35 41t-53 5q-28-10-40-36t-4-54q10-28 35-41t53-5z" horiz-adv-x="561" /> + +<glyph glyph-name="briefcase" unicode="" d="M456 326l0-100-456 0q8 226 10 292 4 108 100 108l160 0q16 26 37 67t23 45q14 26 23 32t37 6l222 0q26 0 36-7t22-31q18-32 60-112l160 0q96 0 100-108l10-292-454 0 0 100-90 0z m-74 354l-28-54 292 0-28 54q-14 26-42 26l-152 0q-28 0-42-26z m164-604l0 100 430 0q-6-88-10-166-6-84-90-84l-750 0q-90 0-90 84l-10 166 430 0 0-100 90 0z" horiz-adv-x="1000" /> + +<glyph glyph-name="suitcase" unicode="" d="M900 650q42 0 71-30t29-70l0-550q0-42-29-71t-71-29l-50 0 0 750 50 0z m-900-100q0 40 30 70t70 30l50 0 0-750-50 0q-40 0-70 29t-30 71l0 550z m670 204l0-104 110 0 0-750-560 0 0 750 110 0 0 104q98 46 170 46t170-46z m-60-104l0 66q-52 24-110 24-54 0-110-24l0-66 220 0z" horiz-adv-x="1000" /> + +<glyph glyph-name="dot" unicode="" d="M110 460q46 0 78-32t32-78q0-44-32-77t-78-33-78 33-32 77q0 46 32 78t78 32z" horiz-adv-x="220" /> + +<glyph glyph-name="dot-2" unicode="" d="M110 460q46 0 78-32t32-78q0-44-32-77t-78-33-78 32-32 78 32 78 78 32z m350 0q46 0 78-32t32-78q0-44-33-77t-77-33q-46 0-78 32t-32 78 32 78 78 32z" horiz-adv-x="570" /> + +<glyph glyph-name="dot-3" unicode="" d="M110 460q46 0 78-32t32-78q0-44-32-77t-78-33-78 33-32 77q0 46 32 78t78 32z m350 0q46 0 78-32t32-78q0-44-33-77t-77-33-77 33-33 77q0 46 32 78t78 32z m350 0q46 0 78-32t32-78q0-44-32-77t-78-33-78 33-32 77q0 46 32 78t78 32z" horiz-adv-x="920" /> + +<glyph glyph-name="brush" unicode="" d="M118 170q38 34 85 29t87-45q42-40 48-87t-30-83q-86-84-228-102-84-12-80 14 0 4 6 10 52 60 64 145t48 119z m840 646q26-26-148-248t-292-338q-38-38-124-104-8-6-16 8-18 34-48 64-32 32-66 48-16 6-8 16 64 84 104 122 118 116 344 287t254 145z" horiz-adv-x="962" /> + +<glyph glyph-name="infinity" unicode="" d="M796 570q84 0 144-53t60-167q0-112-60-166t-144-54q-78 0-157 40t-139 106q-58-66-137-106t-157-40q-86 0-146 54t-60 166q0 114 60 167t146 53q78 0 157-39t137-105q58 66 138 105t158 39z m-590-352q60 0 127 37t113 95q-46 58-112 95t-128 37q-114 0-114-132t114-132z m590 0q114 0 114 132t-114 132q-62 0-129-37t-111-95q44-58 111-95t129-37z" horiz-adv-x="1000" /> + +<glyph glyph-name="erase" unicode="" d="M902 700q42 0 71-29t29-71l0-500q0-40-29-70t-71-30l-478 0q-38 0-70 28l-340 296q-28 26 0 54l340 296q30 26 70 26l478 0z m-140-550l72 74-128 126 128 128-72 72-128-126-128 126-72-72 128-128-128-126 72-74 128 128z" horiz-adv-x="1002" /> + +<glyph glyph-name="chart-pie" unicode="" d="M368 770l0-368-368 0q18 146 121 249t247 119z m106 0q156-20 261-139t105-279q0-174-123-298t-299-124q-160 0-278 105t-140 263l424 0q20 0 35 14t15 36l0 422z" horiz-adv-x="840" /> + +<glyph glyph-name="chart-line" unicode="" d="M34 284q-42 10-32 56 10 42 54 32l98-24-52-80z m890-12q14 12 33 11t31-15q32-32-2-64l-252-226q-12-12-30-12-14 0-28 10l-286 220-54 14 50 80 36-8q12-4 16-8l264-204z m-490 220l-350-550q-12-22-38-22-12 0-24 8-16 10-20 29t6 33l374 588q8 16 28 20 18 6 36-6l246-156 226 326q10 16 28 19t34-9q38-24 12-62l-252-362q-24-36-62-12z" horiz-adv-x="1003" /> + +<glyph glyph-name="chart-bar" unicode="" d="M750 800q22 0 36-15t14-35l0-850-200 0 0 850q0 50 40 50l110 0z m-300-300q22 0 36-15t14-35l0-550-200 0 0 550q0 50 40 50l110 0z m-300-300q22 0 36-15t14-35l0-250-200 0 0 250q0 50 40 50l110 0z" horiz-adv-x="800" /> + +<glyph glyph-name="chart-area" unicode="" d="M964 732q16 22 16-4l0-768-964 0q-12 0-15 7t5 17l230 288q20 22 40 2l74-66q10-8 21-7t17 11l158 238q16 26 38 4l112-104q20-20 38 4z" horiz-adv-x="980" /> + +<glyph glyph-name="tape" unicode="" d="M770 580q96 0 163-67t67-163q0-94-67-162t-163-68l-540 0q-94 0-162 68t-68 162q0 96 68 163t162 67q96 0 163-67t67-163q0-72-40-130l160 0q-40 64-40 130 0 96 68 163t162 67z m-670-230q0-52 38-91t92-39 92 39 38 91q0 54-38 92t-92 38-92-38-38-92z m670-130q54 0 92 39t38 91q0 54-38 92t-92 38-92-38-38-92q0-52 38-91t92-39z" horiz-adv-x="1000" /> + +<glyph glyph-name="graduation-cap" unicode="" d="M166 238l334-168 276 136q-4-22-8-47t-6-35-11-23-24-23-45-22q-40-18-80-41t-63-34-39-11-40 13-64 37-80 40q-72 32-103 69t-47 109z m810 246q24-14 24-33t-24-33l-78-44-308 102q-22 36-90 36-40 0-67-16t-27-40 27-40 67-16q26 0 36 4l292-68-268-152q-60-32-120 0l-416 234q-24 14-24 33t24 33l416 234q60 32 120 0z m-128-442q18 116 13 182t-19 90l-14 22 70 38q6-8 12-28t17-101-7-197q-4-26-22-30t-35 5-15 19z" horiz-adv-x="1000" /> + +<glyph glyph-name="language" unicode="" d="M988 306q30-82-10-176t-134-160q-10 0-12 2t-16 19-16 19q-2 6 2 10 86 60 117 152t-11 148q-16-38-39-76t-59-80-86-65-106-15q-52 6-84 41t-32 93q0 84 60 148 50 50 114 66l-2 100q-140-24-146-24-6-2-10 4 0 2-5 29t-5 31q-2 2 1 4t7 2l156 28q0 110-2 114 0 8 8 8 46 0 52 2 10 0 10-8l0-104q158 22 164 22 8 4 10-6 0-2 4-23t4-25q4-10-4-12l-176-30 0-102 12 0q86 0 148-36t86-100z m-370-160q28-6 62 6l-4 214q-34-12-60-40-44-44-44-108 0-66 46-72z m122 28q28 24 58 68t45 79 7 41q-36 18-96 18-2 0-6-1t-6-1z m-448 382q10-28 53-165t83-261 40-126q0-4-4-4l-86 0q-6 0-6 4l-50 166-176 0q-48-164-50-166 0-4-6-4l-86 0q-4 0-4 4 10 18 176 552 2 8 10 8l96 0q10 0 10-8z m-130-316l144 0-72 264z" horiz-adv-x="1001" /> + +<glyph glyph-name="ticket" unicode="" d="M216 272l326 326 178-178-326-326z m710 244q14-14 14-36t-14-36l-550-550q-16-16-36-16t-36 16l-76 76q12 20 12 48 0 42-29 72t-71 30q-22 0-50-14l-74 76q-16 16-16 36t16 36l550 550q14 14 36 14t36-14l74-76q-12-22-12-48 0-42 30-71t72-29q26 0 48 12z m-532-502l406 406-258 258-408-406z" horiz-adv-x="940" /> + +<glyph glyph-name="water" unicode="" d="M168 844q10-86 50-155t73-123 33-112q0-66-48-113t-114-47-114 47-48 113q0 58 33 112t73 123 50 155q2 4 7 4t5-4z m616 0q10-86 50-155t73-123 33-112q0-66-48-113t-114-47-114 47-48 113q0 48 21 93t48 78 53 92 34 127q2 4 7 4t5-4z m-320-444q2 4 7 4t5-4q10-86 50-155t73-123 33-112q0-66-48-113t-114-47-114 47-48 113q0 58 33 112t73 123 50 155z" horiz-adv-x="940" /> + +<glyph glyph-name="droplet" unicode="" d="M290 822q14-118 60-219t92-159 82-136 36-160q0-114-83-196t-197-82-197 82-83 196q0 82 36 160t82 136 92 159 60 219q2 8 11 8t9-8z m-42-392q2 4-2 14-6 6-14 6t-12-6l-40-58q-32-46-48-70t-34-75-18-101q0-24 17-41t41-17q58 0 58 68 0 94 42 246 2 6 5 17t5 17z" horiz-adv-x="560" /> + +<glyph glyph-name="air" unicode="" d="M85 534q-16-14-36-12t-34 18q-14 14-12 36t18 36q48 40 79 60t89 40 129 4 159-66 155-53 100 16 89 67q38 30 70-6 32-40-6-72-122-110-234-110-100 0-222 70-68 38-119 52t-93 0-65-29-67-51z m736-110q38 32 70-6 32-40-6-72-40-34-65-53t-72-38-97-19q-96 0-222 70-68 38-119 52t-93 0-65-29-67-51q-14-14-35-12t-35 18q-32 40 6 72 38 34 60 50t69 38 88 23 105-15 134-56q68-38 119-52t93 0 65 29 67 51z m0-256q38 32 70-6 14-14 12-36t-18-36q-40-34-65-53t-72-38-97-19q-96 0-222 70-68 38-119 52t-93 1-66-29-66-52q-14-14-35-12t-35 18q-32 40 6 72 38 34 60 50t69 38 88 23 105-15 134-56q68-38 119-52t93 0 65 29 67 51z" horiz-adv-x="905" /> + +<glyph glyph-name="credit-card" unicode="" d="M900 700q42 0 71-30t29-70l0-500q0-42-29-71t-71-29l-800 0q-40 0-70 29t-30 71l0 500q0 40 30 70t70 30l800 0z m0-600l0 300-800 0 0-300 800 0z m0 450l0 50-800 0 0-50 800 0z m-700-256l30 0 0-30-30 0 0 30z m180-60l30 0 0 30 30 0 0 30 60 0 0-30-30 0 0-30-30 0 0-30-60 0 0 30z m120-30l-30 0 0 30 30 0 0-30z m-150 0l-60 0 0 30 60 0 0-30z m30 60l0-30-30 0 0 60 60 0 0-30-30 0z m-120-30l0-30-60 0 0 30 30 0 0 30 30 0 0 30 60 0 0-30-30 0 0-30-30 0z" horiz-adv-x="1000" /> + +<glyph glyph-name="floppy" unicode="" d="M658 750l142-156 0-544q0-40-29-70t-71-30l-600 0q-40 0-70 30t-30 70l0 600q0 42 30 71t70 29l558 0z m-58-300l0 250-400 0 0-250q0-20 15-35t35-15l300 0q20 0 35 15t15 35z m-50 200l0-200-100 0 0 200 100 0z" horiz-adv-x="800" /> + +<glyph glyph-name="clipboard" unicode="" d="M630 750q28 0 49-21t21-49l0-760q0-30-21-50t-49-20l-560 0q-28 0-49 20t-21 50l0 760q0 28 21 49t49 21l60-150 440 0z m-100-100l-360 0-44 100 108 0 36 100 160 0 36-100 110 0z" horiz-adv-x="700" /> + +<glyph glyph-name="megaphone" unicode="" d="M792 500q58-138 67-258t-39-140q-28-12-61 3t-65 40-99 41-149 8q-28-4-42-19t-6-37q22-56 46-108 4-10 24-22t24-20q14-34-22-46-50-22-102-40-30-10-54 42-32 76-58 132-6 12-34 17t-46 31q-30-10-38-14-34-12-74 12t-54 60q-17 32-5 79t43 61q126 52 213 108t124 103 59 92 25 78 15 59 36 36q48 20 130-70t142-228z m-28-300q8 4 10 38t-11 98-41 128q-28 66-67 123t-67 84-36 23-10-42 10-105 40-133 68-119 68-76 36-19z" horiz-adv-x="860" /> + +<glyph glyph-name="database" unicode="" d="M686 208q14 20 14-2l0-100q0-74-104-135t-246-61q-140 0-245 61t-105 135l0 100q0 8 4 10t10-8q32-52 125-86t211-34 211 34 125 86z m2 254q8 16 12 0l0-116q0-68-102-114t-248-46q-144 0-247 46t-103 114l0 116q0 20 14 0 30-46 124-75t212-29 212 29 126 75z m-338 328q144 0 247-39t103-93l0-64q0-58-103-99t-247-41-247 41-103 99l0 64q0 54 103 93t247 39z" horiz-adv-x="700" /> + +<glyph glyph-name="drive" unicode="" d="M884 304q26-44 14-96l-34-184q-2-20-19-35t-39-15l-712 0q-20 0-38 15t-20 35l-34 184q-8 52 14 96l158 374q22 46 72 46l408 0q50 0 74-46z m-68-132q2 22-10 38t-34 16l-644 0q-22 0-34-16t-10-38l14-74q2-22 19-37t39-15l590 0q22 0 39 15t19 37z" horiz-adv-x="902" /> + +<glyph glyph-name="bucket" unicode="" d="M522 780q174 0 286-49t104-105q-6-38-48-307t-44-281q-2-18-37-44t-107-50-154-24-153 24-106 50-37 44q0 2-4 30 82-6 163 35t139 117q28 0 48 20t20 50q0 28-20 49t-50 21q-28 0-49-21t-21-49q0-20 10-36-48-58-115-89t-131-27q-102 10-157 57t-59 109q-8 122 156 184-18 94-22 138-8 56 104 105t284 49z m-452-470q4-32 37-59t91-39l-32 204q-100-44-96-106z m452 212q82 0 157 18t113 39 38 35-38 35-112 39-158 18q-82 0-156-18t-112-39-38-35 38-35 112-39 156-18z" horiz-adv-x="913" /> + +<glyph glyph-name="thermometer" unicode="" d="M400 356q64-36 102-98t38-138q0-112-79-191t-191-79-191 79-79 191q0 76 38 138t102 98l0 444q0 50 40 50l170 0q20 0 35-15t15-35l0-444z m-130-406q70 0 120 50t50 120q0 56-32 100t-84 60l0 370-100 0 0-368q-54-16-89-61t-35-101q0-70 50-120t120-50z" horiz-adv-x="540" /> + +<glyph glyph-name="key" unicode="" d="M774 612q20-116-28-215t-150-117q-66-12-130-2l-118-194-70-12-104-166q-14-28-46-32l-76-14q-12-4-22 4t-12 22l-16 98q-8 30 12 56l258 386q-24 50-38 120-18 106 53 187t185 101q106 20 195-45t107-177z m-126-76q30 44 21 97t-51 83q-42 32-92 22t-80-54q-8-12-12-23t-1-20 5-16 13-17 18-15 22-16 23-17q6-4 22-16t23-16 19-12 19-8 17 1 18 8 16 19z" horiz-adv-x="780" /> + +<glyph glyph-name="flow-cascade" unicode="" d="M520 120q50 0 85-35t35-85-35-85-85-35q-80 0-110 74l-164 0q-88 0-131 54t-43 118l0 464q-72 34-72 110 0 50 35 85t85 35 85-35 35-85q0-76-72-110l0-114q0-78 78-78l164 0q30 72 110 72 50 0 85-35t35-85-35-85-85-35q-80 0-110 74l-164 0q-42 0-78 16l0-194q0-78 78-78l164 0q30 72 110 72z m0 300q-28 0-49-20t-21-50q0-28 21-48t49-20 49 20 21 48q0 30-21 50t-49 20z m-470 280q0-28 21-48t49-20 49 20 21 48q0 30-21 50t-49 20-49-20-21-50z m470-768q28 0 49 20t21 48q0 30-21 50t-49 20-49-20-21-50q0-28 21-48t49-20z" horiz-adv-x="640" /> + +<glyph glyph-name="flow-branch" unicode="" d="M640 650q0-80-74-110-6-58-28-101t-61-69-68-38-75-26q-42-14-63-22t-47-24-38-40-16-60q70-30 70-110 0-50-35-85t-85-35-85 35-35 85q0 78 72 112l0 378q-72 34-72 110 0 50 35 85t85 35 85-35 35-85q0-76-72-110l0-204q40 30 138 60 58 18 84 29t51 41 29 76q-70 32-70 108 0 50 35 85t85 35 85-35 35-85z m-588 0q0-28 20-48t48-20 49 20 21 48q0 30-21 50t-49 20-48-20-20-50z m68-668q28 0 49 20t21 48q0 30-21 50t-49 20-48-20-20-50q0-28 20-48t48-20z m400 600q28 0 49 20t21 48q0 30-21 50t-49 20-48-20-20-50q0-28 20-48t48-20z" horiz-adv-x="640" /> + +<glyph glyph-name="flow-tree" unicode="" d="M868 112q72-34 72-112 0-50-35-85t-85-35-85 35-35 85q0 78 72 112l0 114q0 78-76 78l-100 0q-44 0-78 12l0-204q72-34 72-112 0-50-35-85t-85-35-85 35-35 85q0 78 72 112l0 204q-30-12-76-12l-100 0q-34 0-53-19t-22-33-3-26l0-114q72-34 72-112 0-50-35-85t-85-35-85 35-35 85q0 78 72 112l0 114q0 64 43 118t131 54l100 0q76 0 76 52l0 140q-72 34-72 110 0 50 35 85t85 35 85-35 35-85q0-76-72-110l0-140q0-52 78-52l100 0q86 0 129-54t43-118l0-114z m-678-112q0 30-21 50t-49 20-48-20-20-50q0-28 20-48t48-20 49 20 21 48z m212 700q0-28 20-48t48-20 49 20 21 48q0 30-21 50t-49 20-48-20-20-50z m138-700q0 30-21 50t-49 20-48-20-20-50q0-28 20-48t48-20 49 20 21 48z m280-68q28 0 49 20t21 48q0 30-21 50t-49 20-48-20-20-50q0-28 20-48t48-20z" horiz-adv-x="940" /> + +<glyph glyph-name="flow-line" unicode="" d="M168 162q72-34 72-112 0-50-35-85t-85-35-85 35-35 85q0 78 72 112l0 378q-72 34-72 110 0 50 35 85t85 35 85-35 35-85q0-76-72-110l0-378z m-116 488q0-28 20-48t48-20 49 20 21 48q0 30-21 50t-49 20-48-20-20-50z m68-668q28 0 49 20t21 48q0 30-21 50t-49 20-48-20-20-50q0-28 20-48t48-20z" horiz-adv-x="240" /> + +<glyph glyph-name="flow-parallel" unicode="" d="M240 650q0-76-72-110l0-378q72-34 72-112 0-50-35-85t-85-35-85 35-35 85q0 78 72 112l0 378q-72 34-72 110 0 50 35 85t85 35 85-35 35-85z m-50-600q0 30-21 50t-49 20-48-20-20-50q0-28 20-48t48-20 49 20 21 48z m-70 532q28 0 49 20t21 48q0 30-21 50t-49 20-48-20-20-50q0-28 20-48t48-20z m448-420q72-34 72-112 0-50-35-85t-85-35-85 35-35 85q0 78 72 112l0 378q-72 34-72 110 0 50 35 85t85 35 85-35 35-85q0-76-72-110l0-378z m-116 488q0-28 20-48t48-20 49 20 21 48q0 30-21 50t-49 20-48-20-20-50z m68-668q28 0 49 20t21 48q0 30-21 50t-49 20-48-20-20-50q0-28 20-48t48-20z" horiz-adv-x="640" /> + +<glyph glyph-name="rocket" unicode="" d="M543 236q6-50 8-81t-8-59-13-40-35-32-45-26-70-31-85-37q-32-12-45 4t-3 44l40 110-130 132-106-40q-28-12-43 2t-3 46q12 30 31 79t27 65 22 45 25 36 29 20 41 13l52 0t71-6q10 14 29 39t77 85 118 104 145 75 165 19q8 0 14-6 4-4 6-14 10-82-18-168t-76-151-98-118-86-81z m50 296q22-22 54-22t54 22q22 24 22 56t-22 56q-22 22-54 22t-54-22q-22-24-22-56t22-56z" horiz-adv-x="860" /> + +<glyph glyph-name="gauge" unicode="" d="M406 178q34 56 214 284t194 220q12-6-96-278t-138-326q-50-86-136-36t-38 136z m94 380q-168 0-284-127t-116-311q0-30 2-46 2-22-12-37t-34-17-36 12-18 34q0 8-1 26t-1 28q0 226 145 382t355 156q72 0 134-18l-70-86q-40 4-64 4z m362-62q138-154 138-376 0-38-2-56-2-20-16-33t-34-13l-4 0q-22 4-35 20t-11 36q2 14 2 46 0 150-80 268 6 14 20 51t22 57z" horiz-adv-x="1000" /> +</font> +</defs> +</svg> \ No newline at end of file diff --git a/src/mol-app/skin/fonts/fontello.ttf b/src/mol-app/skin/fonts/fontello.ttf new file mode 100644 index 0000000000000000000000000000000000000000..39a234370e14bbf53e699758840b2ea5de8ccede Binary files /dev/null and b/src/mol-app/skin/fonts/fontello.ttf differ diff --git a/src/mol-app/skin/fonts/fontello.woff b/src/mol-app/skin/fonts/fontello.woff new file mode 100644 index 0000000000000000000000000000000000000000..674807bae0bb1793ab6274a50737b707d9356fd2 Binary files /dev/null and b/src/mol-app/skin/fonts/fontello.woff differ diff --git a/src/mol-app/skin/fonts/fontello.woff2 b/src/mol-app/skin/fonts/fontello.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..49eb9640642432843920fcacac3346c95157ba26 Binary files /dev/null and b/src/mol-app/skin/fonts/fontello.woff2 differ diff --git a/src/mol-app/skin/icons.scss b/src/mol-app/skin/icons.scss new file mode 100644 index 0000000000000000000000000000000000000000..3d1d6cfcbee2d44a08ac532caf8aedc1725326b4 --- /dev/null +++ b/src/mol-app/skin/icons.scss @@ -0,0 +1,135 @@ + +[class^="molstar-icon-"]:before, [class*=" molstar-icon-"]:before { + font-family: "fontello"; + font-style: normal; + font-weight: normal; + speak: none; + + display: inline-block; + text-decoration: inherit; + width: 1em; + margin-right: .2em; + text-align: center; + /* opacity: .8; */ + + /* For safety - reset parent styles, that can break glyph codes*/ + font-variant: normal; + text-transform: none; + + /* fix buttons height, for twitter bootstrap */ + line-height: 1em; + + /* Animation center compensation - margins should be symmetric */ + /* remove if not needed */ + margin-left: .2em; + + /* you can be more comfortable with increased icons size */ + /* font-size: 120%; */ + + /* Font smoothing. That was taken from TWBS */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + /* Uncomment for 3D effect */ + /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ + } + + .molstar-icon-expand-layout:before { + content: "\e84a"; + } + + .molstar-icon-plus:before { + content: "\e816"; + } + + .molstar-icon-minus:before { + content: "\e819"; + } + + .molstar-icon-reset-scene:before { + content: "\e891"; + } + + .molstar-icon-ok:before { + content: "\e812"; + } + + .molstar-icon-cross:before { + content: "\e868"; + } + + .molstar-icon-off:before { + content: "\e813"; + } + + .molstar-icon-expand:before { + content: "\e885"; + } + + .molstar-icon-collapse:before { + content: "\e883"; + } + + .molstar-icon-visual-visibility:before { + content: "\e826"; + } + + .molstar-icon-abort:before { + content: "\e814"; + } + + .molstar-icon-focus-on-visual:before { + content: "\e8a3"; + } + + .molstar-icon-settings:before { + content: "\e855"; + } + + .molstar-icon-tools:before { + content: "\e856"; + } + + .molstar-icon-log:before { + content: "\e8a5"; + } + + .molstar-icon-remove:before { + content: "\e847"; + } + + .molstar-icon-help:before { + content: '\e81c' + } + + .molstar-icon-info:before { + content: '\e81e' + } + + .molstar-icon-left-open-big:before { + content: '\e87c' + } + + .molstar-icon-right-open-big:before { + content: '\e87d' + } + + .molstar-icon-left-open:before { + content: '\e874' + } + + .molstar-icon-right-open:before { + content: '\e875' + } + + .molstar-icon-screenshot:before { + content: "\e80f"; + } + + .molstar-icon-help:before { + content: "\e81c"; + } + + .molstar-icon-help-circle:before { + content: "\e81d"; + } \ No newline at end of file diff --git a/src/mol-app/skin/layout.scss b/src/mol-app/skin/layout.scss new file mode 100644 index 0000000000000000000000000000000000000000..ded62b22132c4b64ff0b276f2523b5b9b377b282 --- /dev/null +++ b/src/mol-app/skin/layout.scss @@ -0,0 +1,29 @@ + +@import 'layout/common'; + +.molstar-layout-standard-outside { + position: absolute; + @import 'layout/outside'; +} + +.molstar-layout-standard-landscape { + position: absolute; + @import 'layout/landscape'; +} + +.molstar-layout-standard-portrait { + position: absolute; + @import 'layout/portrait'; +} + +.molstar-layout-expanded { + position: fixed; + + @media (orientation:landscape) { + @import 'layout/landscape'; + }; + + @media (orientation:portrait) { + @import 'layout/portrait'; + }; +} \ No newline at end of file diff --git a/src/mol-app/skin/layout/common.scss b/src/mol-app/skin/layout/common.scss new file mode 100644 index 0000000000000000000000000000000000000000..b84b69214b3468b2d165104c9a20c6ff0cf542c3 --- /dev/null +++ b/src/mol-app/skin/layout/common.scss @@ -0,0 +1,60 @@ + +.molstar-layout-expanded, .molstar-layout-standard { + left: 0; + right: 0; + top: 0; + bottom: 0; +} + +.molstar-layout-region { + overflow: hidden; + background: $default-background; +} + +.molstar-layout-static, .molstar-layout-scrollable { + position: absolute; +} + +.molstar-layout-scrollable { + overflow-y: auto; +} + +.molstar-layout-static { + overflow: hidden; +} + +.molstar-layout-main, .molstar-layout-bottom { + .molstar-layout-static { + left: 0; + right: 0; + top: 0; + bottom: 0; + } +} + +.molstar-layout-right { + + .molstar-layout-static { + left: 0; + right: 0; + top: 0; + height: $row-height + $control-spacing; + } + + .molstar-layout-scrollable { + left: 0; + right: 0; + top: $row-height + $control-spacing + 1; + bottom: 0; + } + +} + +.molstar-layout-left { + .molstar-layout-static { + left: 0; + right: 0; + bottom: 0; + top: 0; + } +} \ No newline at end of file diff --git a/src/mol-app/skin/layout/landscape.scss b/src/mol-app/skin/layout/landscape.scss new file mode 100644 index 0000000000000000000000000000000000000000..418a7cd4bc0a35f244cc1ce59123527ff679e0d4 --- /dev/null +++ b/src/mol-app/skin/layout/landscape.scss @@ -0,0 +1,81 @@ + +.molstar-layout-main { + position: absolute; + left: $expanded-left-width; + right: $expanded-right-width; + bottom: $expanded-bottom-height; + top: $expanded-top-height; +} + +.molstar-layout-top { + position: absolute; + left: $expanded-left-width; + right: $expanded-right-width; + height: $expanded-top-height; + top: 0; + border-bottom: 1px solid $border-color; +} + +.molstar-layout-bottom { + position: absolute; + left: $expanded-left-width; + right: $expanded-right-width; + height: $expanded-bottom-height; + bottom: 0; + border-top: 1px solid $border-color; +} + +.molstar-layout-right { + position: absolute; + width: $expanded-right-width; + right: 0; + bottom: 0; + top: 0; + border-left: 1px solid $border-color; +} + +.molstar-layout-left { + position: absolute; + width: $expanded-left-width; + left: 0; + bottom: 0; + top: 0; + border-right: 1px solid $border-color; +} + +.molstar-layout-hide-right { + .molstar-layout-right { + display: none; + } + .molstar-layout-main, .molstar-layout-top, .molstar-layout-bottom { + right: 0; + } +} + + +.molstar-layout-hide-left { + .molstar-layout-left { + display: none; + } + .molstar-layout-main, .molstar-layout-top, .molstar-layout-bottom { + left: 0; + } +} + +.molstar-layout-hide-bottom { + .molstar-layout-bottom { + display: none; + } + .molstar-layout-main { + bottom: 0; + } +} + +.molstar-layout-hide-top { + .molstar-layout-top { + display: none; + } + .molstar-layout-main { + top: 0; + } +} \ No newline at end of file diff --git a/src/mol-app/skin/layout/outside.scss b/src/mol-app/skin/layout/outside.scss new file mode 100644 index 0000000000000000000000000000000000000000..74cac3e423634f96522cec4efa20b69e8a9a56e5 --- /dev/null +++ b/src/mol-app/skin/layout/outside.scss @@ -0,0 +1,89 @@ + +.molstar-layout-main { + position: absolute; + left: 0; + right: 0; + bottom: 0; + top: 0; +} + +.molstar-layout-top { + position: absolute; + right: 0; + height: $standard-top-height; + top: -$standard-top-height; + width: 50%; + border-left: 1px solid $border-color; + border-bottom: 1px solid $border-color; +} + +.molstar-layout-bottom { + position: absolute; + left: 0; + right: 0; + height: $standard-top-height; + top: -$standard-top-height; + width: 50%; + border-bottom: 1px solid $border-color; +} + +.molstar-layout-right { + position: absolute; + width: 50%; + right: 0; + bottom: -$standard-bottom-height; + height: $standard-bottom-height; + border-left: 1px solid $border-color; + border-top: 1px solid $border-color; +} + +.molstar-layout-left { + position: absolute; + width: 50%; + left: 0; + bottom: 0; + bottom: -$standard-bottom-height; + height: $standard-bottom-height; + border-top: 1px solid $border-color; +} + +///////////////////////////////////////// +.molstar-layout-hide-right { + .molstar-layout-right { + display: none; + } + .molstar-layout-left { + width: 100%; + } +} + +.molstar-layout-hide-left { + .molstar-layout-left { + display: none; + } + .molstar-layout-right { + width: 100%; + border-left: none; + } +} + +/////////////////////////////////// +.molstar-layout-hide-top { + .molstar-layout-top { + display: none; + } + .molstar-layout-bottom { + width: 100%; + border-left: none; + } +} + +.molstar-layout-hide-bottom { + .molstar-layout-bottom { + display: none; + } + .molstar-layout-top { + width: 100%; + border-left: none; + } +} \ No newline at end of file diff --git a/src/mol-app/skin/layout/portrait.scss b/src/mol-app/skin/layout/portrait.scss new file mode 100644 index 0000000000000000000000000000000000000000..efaf39113d637f9e5b18cf67092274c024c07393 --- /dev/null +++ b/src/mol-app/skin/layout/portrait.scss @@ -0,0 +1,99 @@ + +.molstar-layout-main { + position: absolute; + left: 0; + right: 0; + bottom: $expanded-portrait-bottom-height; + top: $expanded-portrait-top-height; +} + +.molstar-layout-top { + position: absolute; + right: 0; + height: $expanded-portrait-top-height; + top: 0; + width: 50%; + border-left: 1px solid $border-color; + border-bottom: 1px solid $border-color; +} + +.molstar-layout-bottom { + position: absolute; + left: 0; + right: 0; + height: $expanded-portrait-top-height; + width: 50%; + border-bottom: 1px solid $border-color; +} + +.molstar-layout-right { + position: absolute; + width: 50%; + right: 0; + bottom: 0; + height: $expanded-portrait-bottom-height; + border-left: 1px solid $border-color; + border-top: 1px solid $border-color; +} + +.molstar-layout-left { + position: absolute; + width: 50%; + left: 0; + bottom: 0; + height: $expanded-portrait-bottom-height; + border-top: 1px solid $border-color; +} + +///////////////////////////////////////// +.molstar-layout-hide-right { + .molstar-layout-right { + display: none; + } + .molstar-layout-left { + width: 100%; + } +} + +.molstar-layout-hide-left { + .molstar-layout-left { + display: none; + } + .molstar-layout-right { + width: 100%; + border-left: none; + } +} + +.molstar-layout-hide-right.molstar-layout-hide-left { + .molstar-layout-main { + bottom: 0; + } +} + +/////////////////////////////////// +.molstar-layout-hide-top { + .molstar-layout-top { + display: none; + } + .molstar-layout-bottom { + width: 100%; + border-left: none; + } +} + +.molstar-layout-hide-bottom { + .molstar-layout-bottom { + display: none; + } + .molstar-layout-top { + width: 100%; + border-left: none; + } +} + +.molstar-layout-hide-top.molstar-layout-hide-bottom { + .molstar-layout-main { + top: 0; + } +} \ No newline at end of file diff --git a/src/mol-app/skin/logo.scss b/src/mol-app/skin/logo.scss new file mode 100644 index 0000000000000000000000000000000000000000..7bd02b7d3a1a4595d15099b6f52aec9d1ea0f422 --- /dev/null +++ b/src/mol-app/skin/logo.scss @@ -0,0 +1,47 @@ +.molstar-logo { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + + display: table; + width: 100%; + height: 100%; + + > div { + display: table-cell; + vertical-align: middle; + text-align: center; + + > div { + display: inline-block; + position: relative; + width: 50%; + max-width: 390px; + height: 130px; + + > div { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + } + + > div:first-child { + //border-radius: 8px; + background: rgba(0,0,0,0.75) + } + } + } +} + +.molstar-logo-image { +@include non-selectable; + +background-repeat: no-repeat; +background-position: center; +background-size: 90%; +background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAgoAAADKCAIAAABL1P+fAAAAAXNSR0IArs4c6QAAAVlpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KTMInWQAALzZJREFUeNrt3flXG2eaL/D5B+65c3+ZM5Nk7ume7nOSnj7dyXSnE8eOMZlextN27HSWtoPpbJ3Ejh3HSzAk8QrYGINZDBiMWMVuMItYxSKJTQLEjgRIgMQqkMQuhCQgffu+hIzb/RYIkKpKVaXne56THxyo5ZWqPjwq1Vv/8FcIBAKBQAj5BxgCCAQCgQAPEAgEAgEeIBAIBAI8QCAQCAR4gEAgEAjwAIFAIBDgAQKBQCDAAwQCgUCABwgE8te/9oxP7o+I3xvB2xOe8FJ4wi/CEl4IS3z+buLP7iY+F5r8bGjyj0KSfxCS/K8hKc/cQZX6THDKM8GpT9/mP337u/8GpT19a6PSn7r5uDKfCkSV9VRg9lMBqHKerOh6JQw7BHiAQIAH4AECPEAgwAPwAAEeIBDgAXiAAA8QCPAAPECABwgEeAAeIMADBAI8YDx8lltO4vIRBsADBHiAQIAH4AECPEAgwAPwAGEOD221VfcvnyNW7HcVd/X8g426dj7++nne9QuoEm5cTPRfryT/i8kBF5MDL6QEfply82IqqlsX+UFfpn1X6bfXKyN4vbJCfFCVp4S552uga7k7KrkwVnthvO78BKr687qG85OoGs9PSc/rpecNsi8MTV8Ym76Ybv5ipuXsjPzsrPzsXOvZ+fb1Wmg/u9jxuanzc1PXmaWu0+au08s9py3flVX5ma33FKqV3lOr/SfXVCf/33QlvOmBBwiEBB6qctPPHXyJWOf/e70uHnr5S1SH9/i8/vKlIy/7Ht2D6qs3Xvn6D+v1zZt7r7y198rbe6++s+/aH/ddR3Vs343jr/offzXg3X2BXq+iuuW9Xrff248q+H2PsQGFu70AZoOiP+eX/Tm/UOf+x0DeC4N5LwzlP68peF5b8Pxw0c9HBD8bFfxsrPin4yX/PlH6E13ZTybLn5sSPqsXPmuo/PF09Y9QzdT826zoh3PiH8xLnlmofRqVqf5flhr+GdWy9J8ssv9jbfpHVCst/3tV/r/+okuFNz3wAIGwj4fsUB93ewFGxBeBB+ABeIAAD9vwcOcDj3F3aiBME0192S8CD8AD8AABHrbngX/jpPuM/oDgHeABeAAeIMDDjngI+cCjt6nGHYZ+XlvTm/Ui8AA8AA8QjvCQE31H09v9ffX1aJ+s/vUaxkrVM6JSPFmjqNR/q7CTv8d44Pl6ucPQqwVvYzwMFf3aYlRYpr8v68zj6rGhmv2+VjZqrnv1cc2v19rjWuj+9ruyDQYBD8AD8AChiQf0L+RuQWNxJsZD6If7e5s53kDMqIqUmb/EeJjpzSB3LStjfOABeAAeIGzlwWpZCv/s9xgPPD8vq8XM1UFfWzH35f4W40FT9ta3qyTvMvAAPAAPT+Z4uhgbHOqqddQAPJAQpawG4yH0o/1NpVlcfY/qezIVGS9iPCyOkN8wAQ/AA/AAPLCbB5RYn3cxHqI/P8zJBgK1Dr25v8V4GCp5i4p1AQ/AA/AAPLCeB9RAYDzc/cijuYyDDQRqHXoyfoXxsDBMybUW4AF4AB6AB9bzgBJ3yQvjIeYs1xoIm9nQnf4SxoO28mOKVgc8AA/AA/DABR7GBhQYD2F/9qhM49Q8faPSMCIPy0aqbhQHHoAH4AF44AIPKNmhPhgP4Z94LM4auPHWRK1DV9pLGA8j4ovUrRF4AB6AB/s8kHUSp27JwMPfGggiD5xpIEakYUQezAYK55gCHoAH4AF44AgPKDmhPhgPqIwTWrYPtEmv6OS/hPEwVn+D0pUCD8AD8MBSHkzWVeABj2FcS+ThUcQltr8vByp8iDysmKn9+wJ4AB44w0Nh9/BzIQUq/bw78IB2E+0s2mXgAU9FahjGQ+SnByaGWPwWR61DR8rLGA/jTXepXi/wADxwgwd0otzYFyeFYAUPGzZsLMS1QjCRh4VZA5GH/EgWNxDqCh8iD1S3DsAD8MANHh7b4LwQzOfhSRtcLgQTeVhvIPhhGA/3TrK1gZgfbW5PeRnjYbLtAQ2rBh6AB7bzgNngpBAM54Fog2uFYCgPqIGI/vwwxkPqFVZO9N2TdwzjoTf3t2srdNzuBzwAD6zmYVMbnBGCyTxsZYMLhWAoDyhNpVkYD1GnPPrlIna9v6cHRW3JezAe9D2Z9KwdeAAe2MuDHRscFoKxPNi3wVVCMJcHq8Ucc/YwxgPrGoiuh8cwHpQPf0dP6wA8AA/s5WFbGxwTgpk87MQGlwjBXB5QmsuyMB5QqdjTQOh7i+WJezAeaGsdgAfggaU87NAGB4RgIA87t4F+IRjNA2og7p87jPGQ4PO6jQ3z9KEWoT3tIMZDX8Ef6dwG4AF4YB0PdmzwK5E7KQTTeLBjw8m8xq3+F21CMJoHlL4WEcZDzOkDrcJs5r/LJzqykA0YD7MaWlsf4AF4YBcPdmzYOCdu9QM7FIJRPNixAUFo/wfoEYLpPKAkfuOF8cD8BgK1Dm38gxgPvfl/pHkzgAfggUU8bGuD/R/biRDM4WFbG7b9MRqEYAEPqIHAeGB+AzHentWS8ArGw+wQ3VdNgAfggS087NAGJ4VgCA87tMHlQrCAB5Tky14YD4mXXjfNMXRCROuSsZn3CsbDQIUP/VsCPAAPrOBhVzY4IwQTeNiVDa4Vgh08TAwqMR7unzkgzopg5nt9sDaCyINJr6B/S4AH4IH5PDhgg8NCuJwHB2xwoRDs4AElP/ISxkPs554MbCBQ69AUvw/jQe2K1gF4AB6Yz4PDNjgmhGt5cNgGVwnBGh4mhpREHhjYQPRXBhB5cEnrADwADwznwUkbHBDChTw4aYNLhGANDygF9y5hPMR97jmjG2bO231hUil7sA/jYVDk76rtAR6AB8byQIoNuxXCVTyQYgP9QrCJh8U5A5GH4hhf5rzjFcU+RB5sS0bgAQI8UGTDroRwCQ8k2kCzEGziAaUmIxzj4cFZz0kNI979qHWQPtiH8aCpd+WDsoEH4IGBPJBuw86FoJ8H0m2gUwiW8YAaCCIPJfcZ0UD0CC4ReXBh6wA8AA8M5IEiG3YoBM08UGQDbUKwjAcUUWYExkP8F65vIAxqcWPsXoyH0Waea7cKeAAeGMUDpTbsRAg6eUiTD1BnAz1CsI8Hm8Wc4PM6xkN24J9cu1XyDC+Mhzb+Qdom7gYegAfm80CDDdsKsSeqhDYe7EwsSOLOUioE+3hAaRVmYzzwzr820Oayib71KnFD7KsYDxMdWS4fKOABeGAID7TZsO3qXMsDuTZQLQQreUANROKl1zEesgNOuGp7WtK8MB5aU13fOnCGh/mFScOMZr2mNfr/qalp7UatrFqAB4bzQLMNuxKCTh6osIFSIVjJw0YDgfHAO+c52C6mf0tGW7PqY/ZjPIy3ZzFhlFjEw+LihF7fox6o6OzKbJBFiWqDHwnO5ArO5BSdzS48m1l4NqPwXHrBOX7B+dT8Cyn5F5IefZnwyAcVL+9SXK5vXK5fYoF/gSQxX5zUohR3DjRPGEdsq1bgweU8uMSGnQtBGw/U2UCdEGzlATUQ6Te8MR7Svjlqs9L6N/vqilmWcAjjoSPrXYaciZjMg8k0rptoVnTzpQ2B5aUflZV+XFLyiaDkU0HJqYLiU/mCz3bLw/2HX0U//OpezteoInIuh2WjunLv0c2cmlSpom7MMAo8uCQHecJNz1l7okpM1lUX4kQnD1TbsJGTeY2brh29BO7FA4q6VYTxkHjBs7OG1om+R+TZyAaMB+MAUx53yjQeVteWp3SyPkVKXfWp6vL3q8rfF5Z9KCz7iDoeQrKu3Mm8Gpx5NSjz+s2MG5nVfLlKrp/TAw/0/R1gXd1KCPTvLheCBh7osWGrR+k5M8gs5gEl44Y3xkP65SO0NRCrNnMj7xDGQ3umF3PGhyE8rK0tG3TSvs6oukovifCEuMJbVOHtEh5QBaT7+6f7RxZGC1urF8wLwIObC0E1D+y1gfU8oAYC4yHpIn0NxEBDQm30foyH2ZEW4OFxFudUQ70JzZIPG6uONVQdZw4PN9IDrvEDr6bdTKrM6NTQOmGie35zibFCUMoDq21gPQ8oj0I+w3hAtTRP+UTfFpNRErUf40FR7MOowXEhD9OTjf3tgc01b6GSVr/jDA+PSv1qGmKqG2JRVTbEVdbHCevjyuselNXFF9REOsnDZf7Nb/i3/LPCRN2N1hU6LmW77X0PzBSCOh7YbgMXeJjUKIk8NORSPtF3X3UEkYeFSWZNbuMSHmanGhTS0601b8hFb+6KB5ksorMrQz0gnNT3TM8MLVvmdrVenVGLqmugqUUpfiROSiwO2zkPX6Xe8k0NupYZJuqSUo2EO981zUAhKOKBAzZwgQeUkvu+GA/JX1LbQKDWQXzPA+OhR3CJaSNDMw9L8/1DHdc7xUfaRW/shIemRv/+vjzdRIvFMkvF9thWreOGEamiLrsmdSc8XEoJ8km5/U1GRNsQhedTN59ziWlCUMEDN2zgCA+ogSDy0JBHYQPRXRJI5IFprQOdPKytLY/3x/aIf98lPrQtDz0d9yd1MopIsJMxw2h1e1VUYZR9Hi4kB59PvnO3iK+bpeQvDJixlVFCkM4DZ2zgCA8opbF+GA8pPp56LSWXHOcmlKJID4wHtZiJD76mhwfTdGt/4zs9koP2eejrjJrSyVbXll0+LPo5fbGsJCg71A4P55JCziaGVHbKgAduC0EuD1yygTs8mOYMRB6E8ZRM9N2e50vkwWoyMnBYqObh27Xlyf6Yvtrf9Nb+biseWms/GNMIrJZpBo5Pl0ZxNz/GDg+o7hSkzZoWgQeuCkEiDxyzgTs8oNTmRGA8pKIGYpjkBgK1DjUR+zEeVKIIZo4JpTzYzMMj7V/01/5mKx7a694b1xStMaBdsJ9h/VhCZeZWPJxJCPXhR/eMDAEPnBSCLB5Eah3HbOAUD6iBIPJQySO5gWjL8yXywMzWgVIeLAu9Gtkb6rpfb8pDV92fdJoC5sPwZPrGBoPzYjfl4XTC3VO80FplJ/DAPSFYFJpt4BQPKHUPIzEe+L6eY/2k3aem6xNXh3tgPIy2ZjF2QCjiwaSXDNYfGKh/bVMetD1hNus0S49AUZd0Kx5O8sLTaquABxDCTWzgGg82qzn98hGMh4I7pD0pqD7BC+OhkXdolQETd9PJw5JeomnYvykPvfXHF6bb2H4cTs4ZwgSpm/LwSXw432khgAcQghU2cI0HlM6abIyHND9PTScJE33resVVYfsxHkbk2UweDdJ5WDJIhhte2ZQHTec1dn2aZD+CFsmmPHwcH5EqcUoI4AGEYIUNHORho4HAeCClgajnncB4aIg/zOTWgXQezAbJSOPeTXkwDj/i3mEpH+z1TbtH5OGjBxEPZfXAAwjBbRs4yMNGA4HxkO7nqWoqdmaZQ7LsyrseGA/jXcUMHwoSeVgxa8caX9mUh/mpOq4enLpZgw8/isjDBw8im9R9wAMIwWEbuMkDaiByg7wxHnL931hxdKLvVZu5JuowxoMs5QTz315k8bBmM+ia/4vIw6D0TdN0K7cPUd2s0YcfTeQBlVo3ATyAEFy1gZs8oAy2izEeMr56TSFx8DoBah2EoR4YD1P9YuaPA1k8GLpOjjf+CuNBI3vDujTqDgcqEuJiahSRh1NJcRabDXgAIThpA2d5QMkL8sZ4yA046kADgVqH6qjDGA/S5BOsGARSeFgc5k1IXyTysKivdZ82f2LGeCH1PsbDe3GRwcX5wAMIwUkbuMzDUIcY4yHza0caiO6KiIpQD4wHo6aFFYPgPA8riwqd7BdEHtzKhsdCEHnwjrtX3dMFPIAQ3LOByzygCON9MR6yvnnNPL+LO5yXF43lIR4YD+15vmwZAed5mG5/m8iDUR1F7WavWqZnhvrUwtbOLHFjjFAcaudpcSV1CYLahLZekWqkc3p+itINa1b3EXn4JIk3s5tJmejnAZ1xWkcNbCyRWvdcSIH7CLGVDWgQ0FBQNMhuyoNeqyDy0FQQuYvWoTyCyMPchNJNeDCPZeplL2A8THZ+RtHWLi5OKHvza+tvO/kw0dLGrM6B5kXzPBUbya+twnjwio1OFIuYzANXi2NCbGUD1eWmPKBU8nwxHtYbiIUdNRCodSgLPoDx0Mae1sFJHr61GYzy/8R4GGs+tGojeYKp1TXL4EC5WPQ16c+azhMn9w53kbu1lhWrX2YSxsO7sdH9Oh3wAEKwzga35kE/rCDy0FK4owaiKcuPyAOLWgcneTBpQg2yn2M8LOpKyTzVWmYV3fxtnzXtMA8bDxONLwpXaslEYkA3TuThZlEh8ABCsM4Gt+YBRZIRiPGQc8XTMLLNWX5mTFkW7InxoKwMZ9e+O8zDeuvQ9FOMh6nOUyRum2awVFx15slnTVPEw8azpmOLIsYMpH0NN1lSjfFw7P5OGwjgAYRgjg3uzsPSvIHIgzjFz/5vydZbB5wHy6LRTXgwa0KJPFgXeknZKpNpvEV6vbr8/ary92njYeNZ09XtVbYVq/O7MGNaPJ0ch/EQWFQEPIAQ7LLB3XlYP9fnR2A8oDJu3UCg1qHk9gGMB4UwgnU77hgPf/nWPNv8LMbDTL8/KZs0pZPVV58SVXjb56G4/Ms6aXSvWqibUhpnNCurls3tX56fnNb2DEqlXWVFksRteUDFK+ORIkRJWwvGwzv3Y0anZ1zOQ+uoIaZB+WSlyQeKeoafrOPpYu7VrapOdh2ehd0ueCGAh00aCCIPkq0biNqk00QeWNc6OMyDdSKDyMOajYSHak2M1EiEJ8QV3lvxUC26ouwtWFiccHgVWl1/dUtBbH7gVjwEpPsHPwzRz+mdvXCyYj2VFIfxEC8Su5wHCMROWMnDwqxB2VRTnhoW7+vF9z9J+vLbyhMxHnKveurUm9zdNqYQFwcdwHgYkpE/cbda8PZQxSdTHfGmiaYVs4GKUXWMh8WuoxgP00ofEi42qLLrKr224qG5KXx2dpCsHbetWvuGOxOLwzflwX9diFDnhciR1mM8fJCQsLzdNBvAAwR42GkM49pYn3dvv7c/+H2POx94hHzgEfrh/t7mGpJPlFZzbsBRjIeyCG/iT1bf98Z4qI46vGojeeLuOU2NMvOXvVkv9mW/2J/zy/6cX2jK3mICD2uLPfMtP8Z4sC06+8hug07aUHV8Ux6kDYEkwoClXd18Ly+IyMON9IDw/Girc58yzZgWMR7ejompV6mABwjwQE5G1QpkA8YDz8+L9HFRSLIxHvKueY50/90NTWM9YkHQAYwHKloHVdHbGA/q3P9gAg/L2hCMB337H51tDedUjVXHNuVhaLCU6uNh0TyfXZNK5OEaPzClytk3c5SwFOPhbnkF8AABHijkIfQj1ECIyF0RaiDyAo9iPJRF/m1yvRWbuSraG+Oh6h75rcO0SqDIeJGZPCy27cV4ME869ViLtbXlZsmHRB4kVWeoaxqIKW8WEHm4mnZT0t3gzGJl6n6MhzdjYux/vgQ8QIAHZ3mI/vyw1ULyeXmjgXiSh7zrnoPyku//om/ILrrpifEw3F5C7jasrZh7c3/LTB6+XdYstPwQ48HJi9JDvQnS6ncwHuqqT5lM4zQfGGVNxUQeLvNvzpsXnFnsJ0k8jIf6fjXwAAEeKOTh7kcezWVZpI+O4K43xoMg+AjqG1CVhR7BeBDHkT9xt74nsyfjV8zkwTaRjvEw0/2xUx/szKmaa94i8kC/DY+FIPKQJcl3Zpn3hOUYD3F2v78EPECABxJ4iDlLfgOh7RRjPDy68Vp/fc5G64DxMKEk+Zk/qHVQPvwdY3lYVp3FeDCPZTqzAX1tt4g8TOpkrjo2bKtWXhkP4+Eb/q2pOcc7JKlKhfFwip8OPECAB2p5CPuzR3M5+Q1ESZg3xkPxnSOlIUcwHsRx3qSveqo7qzv9JcbyYJL/X4yHVbPW4bWbl8aQDRgPis4Y1x4ehjk9kYd8qeNXyGdMixgPf4i+P71oAh4gwAO1PNw/d3hxluR7Aoa7xBgP+f6ehQGeGA8zYyTPvmczG7rSXmIsD2umHiIPTjVqffFEHiyWWZcfIcWyEoyHr1JvOfMl13PpfIyHOpUaeIAAD9TyEP6JR2VaGOljJE7xs8+DLMuP9JWOSMOYzMOqvgDjYa73gjNrb697D+NBpUhiwhGyYF4g8tAx5PhfA4liEcZDnrwVeIAAD5TzgIr0BsIworTPA/mtw5Kxk/8Sk3mwaoMxHpy58DBn7JCL3sR4sDKgddjIw7pHGA/pkgKHl1bV04XxcL1IADxAgAc6eKhKJ38mbUmK31Y8SDPJbx00In+G87CsOoPxYJt1/JHa2l4exkNfZxRzDhL1+ADGg29qkMNLU+kmMB5OJCQCDxDggQ4eIj89MK3Tkrte44hyKx4WDCSvy6RXdKS8zHAeluRPYzw4c126W3oa40GvkzLqOCHyoNWPObw0jIejMfeBBwjwQD4PPF8vIg/5kZdIHylpTiCRh44y8ifuVlf4sJEHh9e7trbcWvMGxoPVMs2o4yS1MgPjQdrX5vDS/pzIw3jonZgEHiDAA8k8PLzrU8EPw3i4d/LAxBDJ1wPMC0YiD8tkT9xtmlK0p7yM8TAuu8soHr419WA8TMs9Hd/leRXGQ2v9OaYdJ7XdDRgPeY1lDi/tZlEh8AABHujgYWHWQOSh4B75DYRcEPkkD+2l5LcOqnIfIg8rZgPDeZjvPe/wemcmGzEeFK1BTDtOVOMDGA/xQsdvsiHyULfF1BrAAwR4cIoH9O9CfhjGQ9QpDyoaiMc8lIUeWV4guXWYHhS1Je/BeNC18dD/YhQPa/MyEnmY1BZgPIxqBEw7Tkb0oxgPkcXJDi+tsLUV4yFP3gY8QIAHSniwWswxZw9jPKReIX+i766qxA0eVA3kT9zd9fAYxoPy4e/WVsxM42FFx8d4WBp+wG0erCtWjIdLKY63OMADBHigjweU5rIsjAdUKjnZE33bzBuTaqyQPnH3oEieuAfjQd/z/c0EDOdheTzD4fUOdYdjPOhGaxh4qAAPEOCBrTygBuL+ucMYD2nXyJ9FdWNKPipaB4wHRc73rQO3eVC3B2A8LMypgAfgAQI8kMbDegNRnoXxEHP6QHddMbnbsDGhN7nLnOjIQjZgPBj7//YZC/DAJR7atcMYD9cKBcADBHigkAfUQCR+44XxkODzuo3sib7JDWoR2vgHMR568//uqZzAA5d46NfpgAcI8EArDyh9LSKMB1Stwmwmvwbj7VktCa9gPMwOiYAH4IEJPLSOGjhZ7DpTq/Tz9A8R13hASb7shfGQeOl1m5WhDQRqHVpTD2I8KPOOYT8GPAAPVPAQXa8kngLcofxK5OziobB7mAnjxnoe+ltEGA/3zxxoq8xi5qs+WBvRzHsF42F+rBl4AB6AB7CBaUKwngeU/MhLGA+xn3ua5hjXTlqXjE3x+zAe1BWb7BHwADwAD+5sA0OE4AIPE0NKIg/irAimvdiDkggiDya9AngAHoAHsIGBQnCBB5SCe5cwHuIY1kCg1kH2YB/Gg6p8890BHoAH4AFscLkQHOFho4HAeJBkM6iBUBT7EHkwTSmAB+DBhTzsiS49ni5mae2JKuG8DdsKgQaBuhHmCA8oZbwAjIcHZz0nNUomvLoLk0rpg30YD5r6LR+UDTwAD/TwgP6FpWdMlX7+uZACd7DBvhBoENBQ0L89LONhcc5A5KHkvi8TXtoewSUiD7YlI/AAPAAPYAMbhWAZDyiizAiMh/gvXN9AoNahMXYvxoOd1gF4AB6AB7CB4UKwj4eNBgLjoTTWz7WvaEfuSYyHNv5BO60D8AA8AA9gA8OFYB8PKI2FPIwH3vnXBtpErnot9SpxQ+yrGA8THdvctQc8AA/AA9jAZCFYyYPNYk689DrGQ3bACVe9kC1pXhgPrakHH0/cDTwAD8AD2MBGIVjJA0qrMBvjgXfOc7Bd7JLWoT5mP8bDePv2E34AD8AD8AA2MFkItvKw3kD4HsF4yLnpTfOLt7piliUcwniQp/x+29YBeAAegAewgeFCsJUHlLbKLIyHxAuenTW0TvQ9Is9GNmA8GAd2dBUEeAAegAewgclCsJgHlIwb3hgP6ZeP0DbR96rN3Mg7hPHQnum1w18HHoAH4AFsYLIQ7OZB3SrCeEi6SF8DMSzPro3ej/FgVO/0+gfwADwADy63wWRdBSG4yQNKpr83xgM9DYTFZGyIP4zx0Lbj1gF4AB6AB5fbgNZC1jPm0CDTIA3NQrCehxFlM8YDqpaSBKpfp77qCEnUfoyHhcldHIfAA/DgzjwwwQa0LrJ4OJ4uPsgTckwI1vOAUnLfF+Mh+UvPpXkKJ/pGrYP4ngfGQ4/g0q4WAjwAD27LA0NsIJcHtDSOCcEFHiY1SiIPDXkUTvTdWx1B5GFXrQPwADy4LQ/MsYF0HjgmBBd4QCmN9cN4SPGhqoEwTWtFkR4YD7ttHYAH4ME9eWCUDVTwwCUhOMLDegNB4EEYT8lE3+15vkQerCYj8AA8AA/ssoEiHjgjBEd4QKnNicB4SPXx1A8ryN3auQllTcR+jAeVyJEPsoAH4MGteGCgDdTxwA0huMODac5A5KGSR3ID0ZbnS+TBgdYBeAAe3IoHZtpAKQ8cEII7PKDUPYzEeOD7ktlAGDTN1eEeGA+OtQ7AA/DgPjww1gaqeWC7EJziATUQRB5Koz4ja1PrE7wwHhp5h1ZXHLwFD3gAHtyBBybbQAMPrBaCUzygdNZkYzyk+XlqOkmY6FvXK64K24/xMCJ3fAIP4AF4cAcetjpv7okqcbkN9PBAmxCkDzXXeLBZzemXj2A8FNz5EwmtA+8ExkND/GGHWwfgAXhwEx7QaRGdHF3SPWxrA2080CDEVjsL3QPeQGA8pPt5ap1rIEY6SirvemA8DMudmvsPeAAe3OTag0uE2IkNdPJAqRBU2MBNHlADkXHlCMZDYYj3iqPz9K3azDVRhzEeZCkn0L8DD8AD8MBAIXZoA808UCQERTZwkweUwXYxxkPGV68pJA7+sT8kyxaGemA8TPU7ez0DeOASD+3aYeCBIUJsdbpEayduAHU8bHVBnlwhqLOBszyg5AV5YzzkBhx1oIFALUJ11GGMB2nyCee3EHjgEg+Fra0YD6kNUuCBfiHs2IDWTjyJU8eDSK3bamfJEoJSG7jMw1CHGOMh82tHGohBWXZFqAfGw2Q/CV+F4jAPQ93hGA8GndTdeMiTtwEPNAth34ZNT+LU8YCWbGdnnReCahu4zAPKoyBvjIe8wKPm+V3c4by8aCwP8cB4aM8j505shvOwNPzA4fVOagswHkY1AqbZYF2xAg9cEmJbG+jnwf7OOiMEDTZwnAe9VoHxkPXNa00FkTtfQnd5BJGHuQlyDjZG8bA2L8N4mO89z20eRvSjGA+RxcnAA0uF2IkNLuGBCiHosYHjPKBU8nwxHlCZF3bUQKDWoSz4AMZDWx5pkzgxiodvTT0k8mAYq8R4ULQGMe0MpRofwHiIF2Y5vLSIinKMh/LuHuCBHiF2aIOreCBXCNps4D4P+mEFkYeWwh01EN3lkUQeyGodmM/DTMdbjh/58yqMh9b6c0w7PdV2N2A85DWWOby0m0WFGA+9E5PAAw1C7NwGF/JAlhB02sB9HtYbiARfjIecK57z+mH7vzUzpiwL9sR46BQEkrhhjOIBBePB2PRTh9e7traM8dBYdcxqmWbUuSm1MgPjQdTl+PXzL9L5wAP9QuzKBtfy4LwQNNvgFjzMTQ0TeRCn+Nn/LVmWH5EHy6KRxA1jPg+rZq3Dq+6WnsZ40DPsy0vX+IEYD1r9mMNLO3Y/GuNh2WYDHigVYrc2uJwHZ4Sg3wa34GH9XJ8fgfGAyjiitNM6lNw+gPGgEJL88Gqm8bCsOoPxYJttcXjV2l4exkNfZxRzTknq8QEiDw4vbWx6hsjDVj8MPJAihAM2MIEHx4RwiQ3uwsPSvIHIg2TrBkKa6UfkgdzWgYE8WLXBGA/msUyHVz092Yjx0FB1fHVtmSHno2JZCcZDuqTA4aV1DA9jPFwvEgAP1AnhmA0M4WG3QrjKBnfh4bsGIhLjIffq5g3EmEJcHHQA46FfkkD6JjGNh5UJPsbDXO8Fh1e9trZM5GFkqJgJZ6IF88KN9ACMB2l/m8MLzJQ2YjzEisTAA0VCOGwDc3jYuRAutMGNeFixmnMDjmI8lEV4E3+y+r43xkN11GEnZ99jBQ9rph6MB6P8P51Zu7YvHuNBWvMJExoI1DoQebCuWB1e4C1BIcbDVt9qBR6cFMIZGxjFw06EcK0NbsQDikKSjfGQd81zpFv0d61Dj1gQdADjYUiWTcX2MI2H9ffr3/NgkP3cmavTi/MqjIe6Sq9+RYprT0CGOb1/uj/GQ7601JllvhsbjfHQp5sEHkgXYk9UiZNfDGUUD/Z3dqsZ/Wizwb14QA1EXuBRjIeyyL+bXK8q2hvjoeoeJa0DM3kwK/+E8bA86dTHQX1ttzAeJMITRkOXq049tlUrr4xH5GHYie8sqXQTRB7s/Dzw4MxJ08nbypjGw253lk4b3IuHxw3EkzzkXfcclH//pD1VQ3bRTU+Mh0FqWgdm8mAbi8V4mOn+2JkNWJxTEXmorz5lMo275LxT1lQckO6P8ZBUmenMMrOkjRgPNwoFwAM9Quz2lmMG8rDznaXZBrfjATUQgrveGA+C4CMrNjOqstAjGA/iuBPUDT0DeVgz9WA86GUvrNmcOn7U3ZEYD+IKb2mtz+qahX4bbmbcIPIwb15wZrHn0/kYDxVbX3gAHkgUwoEJi5jJw052ln4b3I4HFG2nGOPh0Y3X+utzNloHjIcJpZi6LWEgD+t/77ftxXgwO/f5ktU63Sz5EONBVOEtqTpDZw9R3ixANhB5kHQ3OLPYsRmjV2w0xsPI9AzwQLUQjk1mx1ge/rrddQj6bXBHHlBKwrwxHorvHCkNOYLxII7zpnQzmMnDsjYE48HQ/YmTmzFj7CTyUF3+vrjqjG6imeq3uG3Vml2TGpR5nchDbGmikwvPltZjPHyWlmb/V4AH50+aDk+FzWQettpZV9ngpjwMd4kxHvL9PQsDPDEe9INySjeDmTyszjVhPOhkv7AtKpxt2lTZRB6qyt8Xln3Y2R5nscxSNMjt6uZ7eUHBmVeJPARlhzrzZdaNfJr0AOMhrbEReKBUCGcek8BwHog760Ib3JQHFGHsafs8yLL8qN4GZvKw/vlS11GMh7mBcOc3Rq1M2pQHYdlH5aUfdXenkYtE33BnYnF4SNaVO5lXiTwEPwzVz+mdXIVM3ecddw/jYXrRBDxQJ4STD1ljPg9P7qxrbXBfHgwjSvs8zIxRfkQxlgfrRAbGw4T0RScvUG9EpUjaioey0o9LSj6RyiLHxuUrq45ftdbq+qtbCmLzA8OyL4dlX9mUh+CHIc7bgOKTkYzx4Pfw4ba/BTw4fNL0K5E7+QBOVvCwsbMn8xpda4P78oAiSfHbigdpph8NG8BYHr61GYg8kNJArH+yN1RihwdByaeCklMFxadEtcHdyqJBbb1xRmNfi8lp7bCur61XVCRJjH741b2cr1FF5FzeigdeGW/Bua8qbaRJ3fdeXCTGQ4NKBTwwOWzhgSFxXx6MI8qteKChdWAyDyhmTSjGw3jjr9Zs5ExKODc7UFd9yj4P+YLPHgnO5ArO5BSdzS48m1l4NqPwXKEwoLIhrrI+Lq/iVtKjLxMe+aDi5V2Ky/WNy/W7//CrbXmobq+yOX29YSOXMpMxHj5M3NHEXMAD8AA8MJ0HFGlOIJGHjrIIetbOZB5QA0Hkwagg7UGqq2vLvT2pu+UhveAcv+B8av6FlPwLu+UhtihizDBK1vaj1uGDB5EYDwWtrcAD63hIkw+g87jzRfzGEfDAbh7MC0YiD8tkT9zNRh7WP/3UhGI8jDW+sjxL5re5TKZxeXMY1TzEF4W3qcn8+qxlxXo6OQ7j4c+JvK2e/wM8MJkH6gp4YDcPKHJB5JM8tJdG0LZqhvOAGggiD2PNh75dI3kGKotltqebX1X5Bek8ZAhje4fJn98prrIE2YDxULiz1gF4AB6AB9bwgBqIJ3lYXjDStmqG84CyNByP8TDSuHeqx5eK0Vhdtej1PZ1dGeXCi07ykC9O6hxoXjRT8q2P7hHNRw8iMB7OpfN3vgTgAXgAHtjBA0pXVeIGD6qGbDrXy3we/rJmNsh/jfEw3PDK3OhDSkdmYXFidEzepxbWSqMrJaH2ecgqvy2oTZC0PuoZaNIZtZRu2Kxp8VxKLJGHjuFh4AF4AB44yMOKzbwxqcYKNRN3s5cHFOtsC5EHTcP+pZlW+t+scwuT+mnNRtG/dsuK9WZ+xsfxERgP94S7O7kDD8AD8MAaHlA2puSjeaWs4AFlfiicyINWdsRmHnarM0t8VfEn8eEYD58mPdjhFWnggQm5VdWJhKCnXH5TG/BAWgNBc+vAIh7+smaeajuO8TBYf0Aje8O6NOomp5X0OuFJXjiRh87hXfcxwAMEeGATDy4JW3hYX45Zq2v+L4yHgfrXBqVvuoMQmXWVp3ihRB5ypPUOLA14gAAPwAN3eEAxG8REHtR1v1Y3vsVtITLrhKcT7hJ5CCnOd2yBwAMEeAAeOMXDd0JIiDz01/6mr/Y381N13HuBLCvWkIK0MwmhRB6uP8q0ODo5B/AAAR6AB67xgDIzELkpD721vxvvj11bW+bMq6OZGvdNu3c2MYTIg19mksWJiZuABwjwADxwkAeUaXXkpjz0SA4OtH5p4cQHTdVdsnNJIcgGIg/nUmLHZ5y6dxJ4gAAPwAM3eUCZG324KQ894t93iQ9NaQvY20Zo9WPhgpTzyXc25eFmfobF6QlfgQcI8AA8cJYHlEV97VY8dIqPKKSnpycb2fVyzJsX8hrLfFJuX0gO3pSHW2TYADxAgAfggeM8oNjMw4PSNzfloV30RmvNG13SM0Y2IGFdsYq7G65nhV1KCdqKh/Q6IVmrAx4gwAPwwHEeUL5dWx7turoVD3LRm801b3U2fm6YlK4y8uMm/ZyhUFr6Df/WV6m3fFODNuXBhx/VMtBL4kqBBwjwADxwn4eNGIfz7PCASlr9TrP4z2pl0pJpnAkjb1uxdmsUcSUJV9NuXubftMNDSEGabpbkSXyBBwjwADy4Cw8olqVRTec1Ozw0Vh1rqDpeV+nVVHdxoD93bnaA/gFfMM/3aLvz6nL90/1vpAdc4wfa4eHr9HuVnTIqNgN4gHCWh406/9/rdfHQy1+iOrzH5/WXLx152ffoHlRfvfHK139Yr2/e3Hvlrb1X3t579Z191/647zqqY/tuHH/V//irAe/uC/R6FdUt7/UCHjZ4GMh7YTDvhaH85zUFz2sLnh8u+vmI4Gejgp+NFf90vOTfJ0p/oiv7yWT5c1PCZ/XCZw2VP56u/hGqmZp/mxX9cE78g3nJMwu1T6My1f/LUsM/o1qW/hM9PGxkbqq+T3bSPg8S4QlxhbeowltSdaarPXZosHR2dpC6TVo0zyu1XaL2ytiiiJsZN1AFpPtvy0OKqHBuaYGiTSLy8EJY4vN3E392N/G50ORnQ5N/FJL8g5Dkfw1JeeYOqtRnglOeCU59+jb/6dvf/Tco7elbG5X+1M3HlflUIKqspwKznwrIBh4gwAPwwCweUNbWlqe0BV11f9qWh+ry95981nRD/c3W1nj1QMXYuHx6ZmjZMrfrVmnVojNqJ4wjLUpxXWdFnjjpyWdNB2Ve3wkP8cJsjX6M0iECHiDAA/Dgjjw8RkKnKeiSntk5D1s9a7pSEipuiKlpiKluiEVV2RBXWR8nrI8rr3tQVhdfWhdfUpdg/1nTO+SBV5mlpRgG4AECPAAP7s7D4xgnG/vabjnDgzPPmt4JDzez71a01sybF2gbE+ABAjwAD8DD97Fap0c1Ann9BUbxkCN51KVR0D8awAMEeAAegAc8S6bx8RFRR0uIq3iIKYoskZWoxwesZNz/DDxA3JoHCISizM0ODg2WypvDaqo+p5SH1PIH5c1FCm3Xgnkehh0CAR4gbIrFMjs7OzgwUKHszZfKohpkUY7xwC8NyRcnVbcUNCtrNRP9xnk9jC0EAjxAOJiVVYtxRoPKsFHTGv3/1NS0dqMmp7WmZWgLIBDgAQKBQCDAAwQCgUCABwgEAoEADxAIBAIBHiAQCAQCPEAgEAgEeIBAIBAIR/P/AbAvdi1UGXhFAAAAAElFTkSuQmCC'); +} \ No newline at end of file diff --git a/src/mol-app/skin/molstar-blue.scss b/src/mol-app/skin/molstar-blue.scss new file mode 100644 index 0000000000000000000000000000000000000000..0d870a8c673de9f831209b771d645b4cc511440b --- /dev/null +++ b/src/mol-app/skin/molstar-blue.scss @@ -0,0 +1,2 @@ +@import 'colors/blue'; +@import 'base'; \ No newline at end of file diff --git a/src/mol-app/skin/molstar-dark.scss b/src/mol-app/skin/molstar-dark.scss new file mode 100644 index 0000000000000000000000000000000000000000..cdf80ffd913790db09cc67a424f3392c9ec1c01b --- /dev/null +++ b/src/mol-app/skin/molstar-dark.scss @@ -0,0 +1,2 @@ +@import 'colors/dark'; +@import 'base'; \ No newline at end of file diff --git a/src/mol-app/skin/molstar-light.scss b/src/mol-app/skin/molstar-light.scss new file mode 100644 index 0000000000000000000000000000000000000000..d45b6bc81bedb18f0c0a6a2efd734bcd3d454670 --- /dev/null +++ b/src/mol-app/skin/molstar-light.scss @@ -0,0 +1,2 @@ +@import 'colors/light'; +@import 'base'; \ No newline at end of file diff --git a/src/mol-app/skin/ui.scss b/src/mol-app/skin/ui.scss new file mode 100644 index 0000000000000000000000000000000000000000..41e78fc7465ee379df02eb3565bcac082c6c4584 --- /dev/null +++ b/src/mol-app/skin/ui.scss @@ -0,0 +1,38 @@ +@mixin non-selectable { + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ + /* Rules below not implemented in browsers yet */ + -o-user-select: none; + user-select: none; + + cursor: default; +} + +::-webkit-scrollbar { + width: 10px; + height:10px; +} + +::-webkit-scrollbar-track { + //-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.8); + border-radius: 0; + background-color: color-lower-contrast($control-background, 4%); +} + +::-webkit-scrollbar-thumb { + border-radius: 0; + //-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.9); + background-color: color-lower-contrast($control-background, 8%); +} + +@import 'components/controls-base'; +@import 'components/controls'; +@import 'components/entity'; +@import 'components/help'; +@import 'components/jobs'; +@import 'components/log'; +@import 'components/misc'; +@import 'components/panel'; +@import 'components/slider'; +@import 'components/viewport'; \ No newline at end of file diff --git a/src/mol-app/skin/variables.scss b/src/mol-app/skin/variables.scss new file mode 100644 index 0000000000000000000000000000000000000000..ddbf57aab8c4950b03ff37bcabdcc1295060613c --- /dev/null +++ b/src/mol-app/skin/variables.scss @@ -0,0 +1,78 @@ + +// measures + +$control-label-width: 110px; +$row-height: 32px; +$control-spacing: 10px; +$entity-subtree-offset: 8px; +$info-vertical-padding: 6px; +$slider-border-radius-base: 6px; + +// layout +$expanded-top-height: 100px; +$expanded-bottom-height: 3 * $row-height + 2; +$expanded-right-width: 290px; +$expanded-left-width: 290px; + +$expanded-portrait-bottom-height: 10 * ($row-height + 1) + 3 * $control-spacing + 1; +$expanded-portrait-top-height: 2 * $row-height + 1; + +$standard-bottom-height: 8 * ($row-height + 1) + 3 * $control-spacing + 1; +$standard-top-height: 2 * $row-height + 1; + +////////////////////////////////////////////////// +// ENTITY COLORS + + +// entity colors are "somewhat orthogonal" on the RGB cube +// TypeClass = 'Root' | 'Group' | 'Data' | 'Object' | 'Visual' | 'Selection' | 'Action' | 'Behaviour' + +// DO NOT CHANGE THESE!! +$entity-color-Root: $default-background; +$entity-color-Data: color-lower-contrast(#95a5a6, 15%); +$entity-color-Selection: color-lower-contrast(#e74c3c, 15%); +$entity-color-Action: color-lower-contrast(#34495e, 10%); +$entity-color-Object: color-lower-contrast(#2ecc71, 10%); +$entity-color-Behaviour: color-lower-contrast(#9b59b6, 10%); +$entity-color-Visual: color-lower-contrast(#3498db, 5%); +$entity-color-Group: color-lower-contrast(#e67e22, 5%); + +////////////////////////////////////////////////// +// COLORS and COMPUTED COLORS + +$slider-disabledColor: #ccc; + +$control-background: color-increase-contrast($default-background, 6.5%); +$border-color: color-increase-contrast($default-background, 15%); +$molstar-form-control-background: color-lower-contrast($default-background, 2.5%); + +// buttons +$molstar-btn-link-font-color: $font-color; +$molstar-btn-link-toggle-on-font-color: $font-color; +$molstar-btn-link-toggle-off-font-color: color-lower-contrast($font-color, 33%); + +// used for "actions" -- i.e. + in selection +$molstar-btn-remove-font-color: $font-color; + +$molstar-btn-action-background: $molstar-form-control-background; + +// update selection etc +//!! $molstar-btn-commit-on-font-color: $entity-current-font-color; +$molstar-btn-commit-on-hover-font-color: color-lower-contrast($molstar-btn-commit-on-font-color, 20%); //!!Change +$molstar-btn-commit-on-background: color-lower-contrast($default-background, 2%); +$molstar-btn-commit-off-background: color-lower-contrast($default-background, 4%); //$control-background; +$molstar-btn-commit-off-font-color: $font-color; + +// log +$log-font-color: color-lower-contrast($font-color, 5%); +$log-timestamp-font-color: color-lower-contrast($font-color, 20%); + +// highlight +$highlight-info-font-color: $hover-font-color; +$highlight-info-additional-font-color: color-lower-contrast($hover-font-color, 20%); + +// entity state +$entity-color-fully-visible: $font-color; +$entity-color-not-visible: color-lower-contrast($font-color, 66%); +$entity-color-partialy-visible: color-lower-contrast($font-color, 33%); +$entity-tag-color: color-lower-contrast($font-color, 20%); \ No newline at end of file diff --git a/src/mol-app/ui/controls/common.tsx b/src/mol-app/ui/controls/common.tsx new file mode 100644 index 0000000000000000000000000000000000000000..695273e18cee4b2738f8e563f7aeac75d74ab2e4 --- /dev/null +++ b/src/mol-app/ui/controls/common.tsx @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * Adapted from LiteMol + * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info. + */ + +import * as React from 'react' +import { shallowEqual } from 'mol-util' + +export type ButtonSize = 'xs' | 'sm' | 'normal' | 'lg' + +export type ButtonStyle = 'link' | 'remove' | 'default' + +export abstract class Pure<Props> extends React.Component<Props, {}> { + shouldComponentUpdate(nextProps: any, nextState: any) { + return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState); + } +} + +export class Button extends Pure<{ + onClick: (e: React.MouseEvent<HTMLButtonElement> | React.TouchEvent<HTMLButtonElement>) => void, + size?: ButtonSize, + style?: ButtonStyle, + active?: boolean, + activeStyle?: ButtonStyle, + icon?: string, + activeIcon?: string, + disabled?: boolean, + disabledStyle?: ButtonStyle, + asBlock?: boolean, + title?: string, + customClass?: string, + customStyle?: any +}> { + render() { + + let props = this.props; + + let className = 'molstar-btn'; + if (props.size && props.size !== 'normal') className += ' molstar-btn-' + props.size; + if (props.asBlock) className += ' molstar-btn-block'; + + if (props.disabled) className += ' molstar-btn-' + (props.disabledStyle || props.style || 'default'); + else if (props.active) className += ' molstar-btn-' + (props.activeStyle || props.style || 'default'); + else className += ' molstar-btn-' + (props.style || 'default'); + + if (props.customClass) className += ' ' + props.customClass; + + let icon: any = void 0; + + if (props.icon) { + if (props.active && props.activeIcon) icon = <span className={ `molstar-icon molstar-icon-${props.activeIcon}` }></span> + else icon = <span className={ `molstar-icon molstar-icon-${props.icon}` }></span> + } + //onTouchEnd={(e) => { (e.target as HTMLElement).blur() } } + + return <button + title={props.title} + className={className} + style={props.customStyle} + disabled={props.disabled} + onClick={(e) => { props.onClick.call(null, e); (e.target as HTMLElement).blur() } } + > + {icon}{props.children} + </button> + } +} + +export const TextBox = (props: { + onChange: (v: string) => void, + value?: string, + defaultValue?: string, + onKeyPress?: (e: React.KeyboardEvent<HTMLInputElement>) => void, + onBlur?: (e: React.FormEvent<HTMLInputElement>) => void, + placeholder?: string +}) => <input type='text' className='molstar-form-control' placeholder={props.placeholder} value={props.value} defaultValue={props.defaultValue} + onBlur={e => { if (props.onBlur) props.onBlur.call(null, e) } } + onChange={e => props.onChange.call(null, (e.target as HTMLInputElement).value)} onKeyPress={props.onKeyPress} />; + +export function isEnter(e: React.KeyboardEvent<HTMLInputElement>) { + if ((e.keyCode === 13 || e.charCode === 13)) { + return true; + } + return false; +} + +export function TextBoxGroup(props: { + value: string, + onChange: (v: string) => void, + placeholder?:string, + label: string, + onEnter?: (e: React.KeyboardEvent<HTMLInputElement>) => void + title?: string +}) { + return <div className='molstar-control-row molstar-options-group' title={props.title}> + <span>{props.label}</span> + <div> + <TextBox placeholder={props.placeholder} onChange={props.onChange} value={props.value} onKeyPress={(e) => { + if (isEnter(e) && props.onEnter) props.onEnter.call(null, e) + } } /> + </div> + </div>; +} + +export const CommitButton = (props: { + action: () => void, + isOn: boolean, + on: string, + off?: string, + title?: string +}) => <div style={{ marginTop: '1px' }}><button onClick={e => { props.action(); (e.target as HTMLElement).blur(); }} + className={'molstar-btn molstar-btn-block molstar-btn-commit molstar-btn-commit-' + (props.isOn ? 'on' : 'off')} + disabled={!props.isOn} title={props.title}> + <span className={ `molstar-icon molstar-icon-${props.isOn ? 'ok' : 'cross'}` }></span> + {props.isOn ? <b>{props.on}</b> : (props.off ? props.off : props.on) } + </button></div> ; + +export const Toggle = (props: { + onChange: (v: boolean) => void, + value: boolean, + label: string, + title?: string +}) => <div className='molstar-control-row molstar-toggle-button' title={props.title}> + <span>{props.label}</span> + <div> + <button onClick={e => { props.onChange.call(null, !props.value); (e.target as HTMLElement).blur(); }}> + <span className={ `molstar-icon molstar-icon-${props.value ? 'ok' : 'off'}` }></span> {props.value ? 'On' : 'Off'} + </button> + </div> + </div> + +export const ControlGroupExpander = (props: { onChange: (e: boolean) => void, isExpanded: boolean }) => + <Button style='link' title={`${props.isExpanded ? 'Less' : 'More'} options`} onClick={() => props.onChange.call(null, !props.isExpanded) } + icon={props.isExpanded ? 'minus' : 'plus'} customClass='molstar-conrol-group-expander' /> + + +export const RowText = (props: { + value: any, + label: string, + title?: string +}) => <div className='molstar-control-row molstar-row-text' title={props.title}> + <span>{props.label}</span> + <div> + {props.value} + </div> + </div> + +export const HelpBox = (props: { + title: string, + content: JSX.Element | string +}) => <div className='molstar-help-row'> + <span>{props.title}</span> + <div>{props.content}</div> + </div> + +export function FileInput (props: { + accept: string + onChange: (v: FileList | null) => void, +}) { + return <input + accept={props.accept || '*.*'} + type='file' + className='molstar-form-control' + onChange={e => props.onChange.call(null, e.target.files)} + /> +} \ No newline at end of file diff --git a/src/mol-app/ui/controls/slider.tsx b/src/mol-app/ui/controls/slider.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ce78033fb17f09fb9fa3077d6436e81525bcbc02 --- /dev/null +++ b/src/mol-app/ui/controls/slider.tsx @@ -0,0 +1,814 @@ +/* + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * Adapted from LiteMol + * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info. + */ + +import * as React from 'react' +import { TextBox, isEnter } from './common' + +export class Slider extends React.Component<{ + label: any, + min: number, + max: number, + value: number, + step?: number, + title?: string, + onChange: (v: number) => void +}, { value: string }> { + + state = { value: '0' } + + private firedValue = NaN; + + componentWillMount() { + this.setState({ value: '' + this.props.value }); + } + + componentWillReceiveProps(nextProps: any) { + this.setState({ value: '' + nextProps.value }); + } + + private updateValue(s: string) { + let v = +s; + if (v < this.props.min) { v = this.props.min; s = '' + v; } + else if (v > this.props.max) { v = this.props.max; s = '' + v; } + this.setState({ value: s }) + } + + private fire() { + let v = +this.state.value; + if (isNaN(v)) { v = this.props.value; } + if (v !== this.props.value) { + if (this.firedValue !== v) { + this.firedValue = v; + this.props.onChange.call(null, v); + } + } + } + + render() { + let step = this.props.step; + if (step === void 0) step = 1; + return <div className='molstar-control-row molstar-slider' title={this.props.title}> + <span>{this.props.label}</span> + <div> + <div> + <div> + <SliderBase min={this.props.min} max={this.props.max} step={step} value={+this.state.value} + onChange={v => this.setState({ value: '' + v })} + onAfterChange={v => this.fire()} /> + </div> + </div> + <div> + <TextBox value={this.state.value} onChange={v => this.updateValue(v)} onBlur={() => this.fire()} onKeyPress={e => { + if (isEnter(e)) this.fire(); + } } /> + </div> + </div> + </div>; + } +} + +/** + * The following code was adapted from react-components/slider library. + * + * The MIT License (MIT) + * Copyright (c) 2015-present Alipay.com, https://www.alipay.com/ + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +function classNames(_classes: { [name: string]: boolean | number }) { + let classes = []; + let hasOwn = {}.hasOwnProperty; + + for (let i = 0; i < arguments.length; i++) { + let arg = arguments[i]; + if (!arg) continue; + + let argType = typeof arg; + + if (argType === 'string' || argType === 'number') { + classes.push(arg); + } else if (Array.isArray(arg)) { + classes.push(classNames.apply(null, arg)); + } else if (argType === 'object') { + for (let key in arg) { + if (hasOwn.call(arg, key) && arg[key]) { + classes.push(key); + } + } + } + } + + return classes.join(' '); +} + +function noop() { +} + +function isNotTouchEvent(e: TouchEvent) { + return e.touches.length > 1 || (e.type.toLowerCase() === 'touchend' && e.touches.length > 0); +} + +function getTouchPosition(vertical: boolean, e: TouchEvent) { + return vertical ? e.touches[0].clientY : e.touches[0].pageX; +} + +function getMousePosition(vertical: boolean, e: MouseEvent) { + return vertical ? e.clientY : e.pageX; +} + +function getHandleCenterPosition(vertical: boolean, handle: HTMLElement) { + const coords = handle.getBoundingClientRect(); + return vertical ? + coords.top + (coords.height * 0.5) : + coords.left + (coords.width * 0.5); +} + +function pauseEvent(e: Event) { + e.stopPropagation(); + e.preventDefault(); +} + +export class Handle extends React.Component<Partial<HandleProps>, {}> { + render() { + const { + className, + tipFormatter, + vertical, + offset, + value, + index, + } = this.props as HandleProps; + + const style = vertical ? { bottom: `${offset}%` } : { left: `${offset}%` }; + return ( + <div className={className} style={style} title={tipFormatter(value, index)} + /> + ); + } +} + +export interface SliderBaseProps { + min: number, + max: number, + step?: number, + defaultValue?: number | number[], + value?: number | number[], + marks?: any, + included?: boolean, + className?: string, + prefixCls?: string, + disabled?: boolean, + children?: any, + onBeforeChange?: (value: number | number[]) => void, + onChange?: (value: number | number[]) => void, + onAfterChange?: (value: number | number[]) => void, + handle?: JSX.Element, + tipFormatter?: (value: number, index: number) => any, + dots?: boolean, + range?: boolean | number, + vertical?: boolean, + allowCross?: boolean, + pushable?: boolean | number, +} + +export interface SliderBaseState { + handle: number | null, + recent: number, + bounds: number[] +} + +export class SliderBase extends React.Component<SliderBaseProps, SliderBaseState> { + private sliderElement: HTMLElement | undefined = void 0; + private handleElements: (HTMLElement | undefined)[] = []; + + constructor(props: SliderBaseProps) { + super(props); + + const { range, min, max } = props; + const initialValue = range ? Array.apply(null, Array(+range + 1)).map(() => min) : min; + const defaultValue = ('defaultValue' in props ? props.defaultValue : initialValue); + const value = (props.value !== undefined ? props.value : defaultValue); + + const bounds = (range ? value : [min, value]).map((v: number) => this.trimAlignValue(v)); + + let recent; + if (range && bounds[0] === bounds[bounds.length - 1] && bounds[0] === max) { + recent = 0; + } else { + recent = bounds.length - 1; + } + + this.state = { + handle: null, + recent, + bounds, + }; + } + + public static defaultProps: SliderBaseProps = { + prefixCls: 'molstar-slider-base', + className: '', + min: 0, + max: 100, + step: 1, + marks: {}, + handle: <Handle className='' vertical={false} offset={0} tipFormatter={v => v} value={0} index={0} />, + onBeforeChange: noop, + onChange: noop, + onAfterChange: noop, + tipFormatter: (value, index) => value, + included: true, + disabled: false, + dots: false, + range: false, + vertical: false, + allowCross: true, + pushable: false, + }; + + private dragOffset = 0; + private startPosition = 0; + private startValue = 0; + private _getPointsCache: any = void 0; + + componentWillReceiveProps(nextProps: SliderBaseProps) { + if (!('value' in nextProps || 'min' in nextProps || 'max' in nextProps)) return; + + const { bounds } = this.state; + if (nextProps.range) { + const value = nextProps.value || bounds; + const nextBounds = (value as number[]).map((v: number) => this.trimAlignValue(v, nextProps)); + if (nextBounds.every((v: number, i: number) => v === bounds[i])) return; + + this.setState({ bounds: nextBounds } as SliderBaseState); + if (bounds.some(v => this.isValueOutOfBounds(v, nextProps))) { + this.props.onChange!(nextBounds); + } + } else { + const value = nextProps.value !== undefined ? nextProps.value : bounds[1]; + const nextValue = this.trimAlignValue(value as number, nextProps); + if (nextValue === bounds[1] && bounds[0] === nextProps.min) return; + + this.setState({ bounds: [nextProps.min, nextValue] } as SliderBaseState); + if (this.isValueOutOfBounds(bounds[1], nextProps)) { + this.props.onChange!(nextValue); + } + } + } + + onChange(state: this['state']) { + const props = this.props; + const isNotControlled = !('value' in props); + if (isNotControlled) { + this.setState(state); + } else if (state.handle !== undefined) { + this.setState({ handle: state.handle } as SliderBaseState); + } + + const data = { ...this.state, ...(state as any) }; + const changedValue = props.range ? data.bounds : data.bounds[1]; + props.onChange!(changedValue); + } + + onMouseDown(e: MouseEvent) { + if (e.button !== 0) { return; } + + let position = getMousePosition(this.props.vertical!, e); + if (!this.isEventFromHandle(e)) { + this.dragOffset = 0; + } else { + const handlePosition = getHandleCenterPosition(this.props.vertical!, e.target as HTMLElement); + this.dragOffset = position - handlePosition; + position = handlePosition; + } + this.onStart(position); + this.addDocumentEvents('mouse'); + pauseEvent(e); + } + + onMouseMove(e: MouseEvent) { + const position = getMousePosition(this.props.vertical!, e); + this.onMove(e, position - this.dragOffset); + } + + onMove(e: MouseEvent | TouchEvent, position: number) { + pauseEvent(e); + const props = this.props; + const state = this.state; + + let diffPosition = position - this.startPosition; + diffPosition = this.props.vertical ? -diffPosition : diffPosition; + const diffValue = diffPosition / this.getSliderLength() * (props.max - props.min); + + const value = this.trimAlignValue(this.startValue + diffValue); + const oldValue = state.bounds[state.handle!]; + if (value === oldValue) return; + + const nextBounds = [...state.bounds]; + nextBounds[state.handle!] = value; + let nextHandle = state.handle!; + if (props.pushable !== false) { + const originalValue = state.bounds[nextHandle]; + this.pushSurroundingHandles(nextBounds, nextHandle, originalValue); + } else if (props.allowCross) { + nextBounds.sort((a, b) => a - b); + nextHandle = nextBounds.indexOf(value); + } + this.onChange({ + handle: nextHandle, + bounds: nextBounds, + } as SliderBaseState); + } + + onStart(position: number) { + const props = this.props; + props.onBeforeChange!(this.getValue()); + + const value = this.calcValueByPos(position); + this.startValue = value; + this.startPosition = position; + + const state = this.state; + const { bounds } = state; + + let valueNeedChanging = 1; + if (this.props.range) { + let closestBound = 0; + for (let i = 1; i < bounds.length - 1; ++i) { + if (value > bounds[i]) { closestBound = i; } + } + if (Math.abs(bounds[closestBound + 1] - value) < Math.abs(bounds[closestBound] - value)) { + closestBound = closestBound + 1; + } + valueNeedChanging = closestBound; + + const isAtTheSamePoint = (bounds[closestBound + 1] === bounds[closestBound]); + if (isAtTheSamePoint) { + valueNeedChanging = state.recent; + } + + if (isAtTheSamePoint && (value !== bounds[closestBound + 1])) { + valueNeedChanging = value < bounds[closestBound + 1] ? closestBound : closestBound + 1; + } + } + + this.setState({ + handle: valueNeedChanging, + recent: valueNeedChanging, + } as SliderBaseState); + + const oldValue = state.bounds[valueNeedChanging]; + if (value === oldValue) return; + + const nextBounds = [...state.bounds]; + nextBounds[valueNeedChanging] = value; + this.onChange({ bounds: nextBounds } as SliderBaseState); + } + + onTouchMove(e: TouchEvent) { + if (isNotTouchEvent(e)) { + this.end('touch'); + return; + } + + const position = getTouchPosition(this.props.vertical!, e); + this.onMove(e, position - this.dragOffset); + } + + onTouchStart(e: TouchEvent) { + if (isNotTouchEvent(e)) return; + + let position = getTouchPosition(this.props.vertical!, e); + if (!this.isEventFromHandle(e)) { + this.dragOffset = 0; + } else { + const handlePosition = getHandleCenterPosition(this.props.vertical!, e.target as HTMLElement); + this.dragOffset = position - handlePosition; + position = handlePosition; + } + this.onStart(position); + this.addDocumentEvents('touch'); + pauseEvent(e); + } + + /** + * Returns an array of possible slider points, taking into account both + * `marks` and `step`. The result is cached. + */ + getPoints() { + const { marks, step, min, max } = this.props; + const cache = this._getPointsCache; + if (!cache || cache.marks !== marks || cache.step !== step) { + const pointsObject = { ...marks }; + if (step !== null) { + for (let point = min; point <= max; point += step!) { + pointsObject[point] = point; + } + } + const points = Object.keys(pointsObject).map(parseFloat); + points.sort((a, b) => a - b); + this._getPointsCache = { marks, step, points }; + } + return this._getPointsCache.points; + } + + getPrecision(step: number) { + const stepString = step.toString(); + let precision = 0; + if (stepString.indexOf('.') >= 0) { + precision = stepString.length - stepString.indexOf('.') - 1; + } + return precision; + } + + getSliderLength() { + const slider = this.sliderElement; + if (!slider) { + return 0; + } + + return this.props.vertical ? slider.clientHeight : slider.clientWidth; + } + + getSliderStart() { + const slider = this.sliderElement as HTMLElement; + const rect = slider.getBoundingClientRect(); + + return this.props.vertical ? rect.top : rect.left; + } + + getValue(): number { + const { bounds } = this.state; + return (this.props.range ? bounds : bounds[1]) as number; + } + + private eventHandlers = { + 'touchmove': (e: TouchEvent) => this.onTouchMove(e), + 'touchend': (e: TouchEvent) => this.end('touch'), + 'mousemove': (e: MouseEvent) => this.onMouseMove(e), + 'mouseup': (e: MouseEvent) => this.end('mouse'), + } + + addDocumentEvents(type: 'touch' | 'mouse') { + if (type === 'touch') { + document.addEventListener('touchmove', this.eventHandlers.touchmove); + document.addEventListener('touchend', this.eventHandlers.touchend); + } else if (type === 'mouse') { + document.addEventListener('mousemove', this.eventHandlers.mousemove); + document.addEventListener('mouseup', this.eventHandlers.mouseup); + } + } + + calcOffset(value: number) { + const { min, max } = this.props; + const ratio = (value - min) / (max - min); + return ratio * 100; + } + + calcValue(offset: number) { + const { vertical, min, max } = this.props; + const ratio = Math.abs(offset / this.getSliderLength()); + const value = vertical ? (1 - ratio) * (max - min) + min : ratio * (max - min) + min; + return value; + } + + calcValueByPos(position: number) { + const pixelOffset = position - this.getSliderStart(); + const nextValue = this.trimAlignValue(this.calcValue(pixelOffset)); + return nextValue; + } + + end(type: 'mouse' | 'touch') { + this.removeEvents(type); + this.props.onAfterChange!(this.getValue()); + this.setState({ handle: null } as SliderBaseState); + } + + isEventFromHandle(e: Event) { + for (const h of this.handleElements) { + if (h === e.target) return true; + } + return false; + + // return this.state.bounds.some((x, i) => e.target + + // ( + // //this.handleElements[i] && e.target === ReactDOM.findDOMNode(this.handleElements[i]) + // )); + } + + isValueOutOfBounds(value: number, props: SliderBaseProps) { + return value < props.min || value > props.max; + } + + pushHandle(bounds: number[], handle: number, direction: number, amount: number) { + const originalValue = bounds[handle]; + let currentValue = bounds[handle]; + while (direction * (currentValue - originalValue) < amount) { + if (!this.pushHandleOnePoint(bounds, handle, direction)) { + // can't push handle enough to create the needed `amount` gap, so we + // revert its position to the original value + bounds[handle] = originalValue; + return false; + } + currentValue = bounds[handle]; + } + // the handle was pushed enough to create the needed `amount` gap + return true; + } + + pushHandleOnePoint(bounds: number[], handle: number, direction: number) { + const points = this.getPoints(); + const pointIndex = points.indexOf(bounds[handle]); + const nextPointIndex = pointIndex + direction; + if (nextPointIndex >= points.length || nextPointIndex < 0) { + // reached the minimum or maximum available point, can't push anymore + return false; + } + const nextHandle = handle + direction; + const nextValue = points[nextPointIndex]; + const { pushable: threshold } = this.props; + const diffToNext = direction * (bounds[nextHandle] - nextValue); + if (!this.pushHandle(bounds, nextHandle, direction, +threshold! - diffToNext)) { + // couldn't push next handle, so we won't push this one either + return false; + } + // push the handle + bounds[handle] = nextValue; + return true; + } + + pushSurroundingHandles(bounds: number[], handle: number, originalValue: number) { + const { pushable: threshold } = this.props; + const value = bounds[handle]; + + let direction = 0; + if (bounds[handle + 1] - value < threshold!) { + direction = +1; + } else if (value - bounds[handle - 1] < threshold!) { + direction = -1; + } + + if (direction === 0) { return; } + + const nextHandle = handle + direction; + const diffToNext = direction * (bounds[nextHandle] - value); + if (!this.pushHandle(bounds, nextHandle, direction, +threshold! - diffToNext)) { + // revert to original value if pushing is impossible + bounds[handle] = originalValue; + } + } + + removeEvents(type: 'touch' | 'mouse') { + if (type === 'touch') { + document.removeEventListener('touchmove', this.eventHandlers.touchmove); + document.removeEventListener('touchend', this.eventHandlers.touchend); + } else if (type === 'mouse') { + document.removeEventListener('mousemove', this.eventHandlers.mousemove); + document.removeEventListener('mouseup', this.eventHandlers.mouseup); + } + } + + trimAlignValue(v: number, nextProps?: SliderBaseProps) { + const { handle, bounds } = (this.state || {}) as this['state']; + const { marks, step, min, max, allowCross } = { ...this.props, ...(nextProps || {}) } as SliderBaseProps; + + let val = v; + if (val <= min) { + val = min; + } + if (val >= max) { + val = max; + } + /* eslint-disable eqeqeq */ + if (!allowCross && handle != null && handle > 0 && val <= bounds[handle - 1]) { + val = bounds[handle - 1]; + } + if (!allowCross && handle != null && handle < bounds.length - 1 && val >= bounds[handle + 1]) { + val = bounds[handle + 1]; + } + /* eslint-enable eqeqeq */ + + const points = Object.keys(marks).map(parseFloat); + if (step !== null) { + const closestStep = (Math.round((val - min) / step!) * step!) + min; + points.push(closestStep); + } + + const diffs = points.map((point) => Math.abs(val - point)); + const closestPoint = points[diffs.indexOf(Math.min.apply(Math, diffs))]; + + return step !== null ? parseFloat(closestPoint.toFixed(this.getPrecision(step!))) : closestPoint; + } + + render() { + const { + handle, + bounds, + } = this.state; + const { + className, + prefixCls, + disabled, + vertical, + dots, + included, + range, + step, + marks, + max, min, + tipFormatter, + children, + } = this.props; + + const customHandle = this.props.handle; + + const offsets = bounds.map(v => this.calcOffset(v)); + + const handleClassName = `${prefixCls}-handle`; + + const handlesClassNames = bounds.map((v, i) => classNames({ + [handleClassName]: true, + [`${handleClassName}-${i + 1}`]: true, + [`${handleClassName}-lower`]: i === 0, + [`${handleClassName}-upper`]: i === bounds.length - 1, + })); + + const isNoTip = (step === null) || (tipFormatter === null); + + const commonHandleProps = { + prefixCls, + noTip: isNoTip, + tipFormatter, + vertical, + }; + + this.handleElements = []; + const handles = bounds.map((v, i) => React.cloneElement(customHandle!, { + ...commonHandleProps, + className: handlesClassNames[i], + value: v, + offset: offsets[i], + dragging: handle === i, + index: i, + key: i, + ref: (h: any) => this.handleElements.push(h) //`handle-${i}`, + })); + if (!range) { handles.shift(); } + + const isIncluded = included || range; + + const tracks: JSX.Element[] = []; + // for (let i = 1; i < bounds.length; ++i) { + // const trackClassName = classNames({ + // [`${prefixCls}-track`]: true, + // [`${prefixCls}-track-${i}`]: true, + // }); + // tracks.push( + // <Track className={trackClassName} vertical={vertical} included={isIncluded} + // offset={offsets[i - 1]} length={offsets[i] - offsets[i - 1]} key={i} + // /> + // ); + // } + + const sliderClassName = classNames({ + [prefixCls!]: true, + [`${prefixCls}-with-marks`]: Object.keys(marks).length, + [`${prefixCls}-disabled`]: disabled!, + [`${prefixCls}-vertical`]: this.props.vertical!, + [className!]: !!className, + }); + + return ( + <div ref={e => this.sliderElement = e!} className={sliderClassName} + onTouchStart={disabled ? noop : this.onTouchStart.bind(this)} + onMouseDown={disabled ? noop : this.onMouseDown.bind(this)} + > + <div className={`${prefixCls}-rail`} /> + {tracks} + <Steps prefixCls={prefixCls} vertical={vertical} marks={marks} dots={dots} step={step} + included={isIncluded} lowerBound={bounds[0]} + upperBound={bounds[bounds.length - 1]} max={max} min={min} + /> + {handles} + <Marks className={`${prefixCls}-mark`} vertical={vertical!} marks={marks} + included={isIncluded!} lowerBound={bounds[0]} + upperBound={bounds[bounds.length - 1]} max={max} min={min} + /> + {children} + </div> + ); + } +} + +export interface HandleProps { + className: string, + vertical: boolean, + offset: number, + tipFormatter: (v: number, index: number) => any, + value: number, + index: number, +} + +interface MarksProps { + className: string, + vertical: boolean, + marks: any, + included: boolean | number, + upperBound: number, + lowerBound: number, + max: number, + min: number +} +const Marks = ({ className, vertical, marks, included, upperBound, lowerBound, max, min }: MarksProps) => { + const marksKeys = Object.keys(marks); + const marksCount = marksKeys.length; + const unit = 100 / (marksCount - 1); + const markWidth = unit * 0.9; + + const range = max - min; + const elements = marksKeys.map(parseFloat).sort((a, b) => a - b).map((point) => { + const isActived = (!included && point === upperBound) || + (included && point <= upperBound && point >= lowerBound); + const markClassName = classNames({ + [`${className}-text`]: true, + [`${className}-text-active`]: isActived, + }); + + const bottomStyle = { + // height: markWidth + '%', + marginBottom: '-50%', + bottom: `${(point - min) / range * 100}%`, + }; + + const leftStyle = { + width: `${markWidth}%`, + marginLeft: `${-markWidth / 2}%`, + left: `${(point - min) / range * 100}%`, + }; + + const style = vertical ? bottomStyle : leftStyle; + + const markPoint = marks[point]; + const markPointIsObject = typeof markPoint === 'object' && !React.isValidElement(markPoint); + const markLabel = markPointIsObject ? markPoint.label : markPoint; + const markStyle = markPointIsObject ? { ...style, ...markPoint.style } : style; + return (<span className={markClassName} style={markStyle} key={point}> + {markLabel} + </span>); + }); + + return <div className={className}>{elements}</div>; +}; + +function calcPoints(vertical: boolean, marks: any, dots: boolean, step: number, min: number, max: number) { + const points = Object.keys(marks).map(parseFloat); + if (dots) { + for (let i = min; i <= max; i = i + step) { + if (points.indexOf(i) >= 0) continue; + points.push(i); + } + } + return points; +} + +const Steps = ({ prefixCls, vertical, marks, dots, step, included, + lowerBound, upperBound, max, min }: any) => { + const range = max - min; + const elements = calcPoints(vertical, marks, dots, step, min, max).map((point) => { + const offset = `${Math.abs(point - min) / range * 100}%`; + const style = vertical ? { bottom: offset } : { left: offset }; + + const isActived = (!included && point === upperBound) || + (included && point <= upperBound && point >= lowerBound); + const pointClassName = classNames({ + [`${prefixCls}-dot`]: true, + [`${prefixCls}-dot-active`]: isActived, + }); + + return <span className={pointClassName} style={style} key={point} />; + }); + + return <div className={`${prefixCls}-step`}>{elements}</div>; +}; \ No newline at end of file diff --git a/src/mol-app/ui/entity/tree.tsx b/src/mol-app/ui/entity/tree.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1d9afd1d9624bc0e2dacf49166b9ff24e24777a0 --- /dev/null +++ b/src/mol-app/ui/entity/tree.tsx @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * Adapted from LiteMol + * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import * as React from 'react' + +import { View } from '../view'; +import { EntityTreeController } from '../../controller/entity/tree'; +import { Controller } from '../../controller/controller'; +import { AnyEntity, RootEntity } from 'mol-view/state/entity'; +import { AnyTransform, SpacefillUpdate, UrlToData, DataToCif, FileToData, CifToMmcif, MmcifToModel, ModelToStructure, StructureToSpacefill, MmcifFileToSpacefill } from 'mol-view/state/transform'; + +function getTransforms(entity: AnyEntity): AnyTransform[] { + const transforms: AnyTransform[] = [] + switch (entity.kind) { + case 'root': + transforms.push(MmcifFileToSpacefill) + break; + case 'url': + transforms.push(UrlToData) + break; + case 'file': + transforms.push(FileToData) + break; + case 'data': + transforms.push(DataToCif) + break; + case 'cif': + transforms.push(CifToMmcif) + break; + case 'mmcif': + transforms.push(MmcifToModel) + break; + case 'model': + transforms.push(ModelToStructure) + break; + case 'structure': + transforms.push(StructureToSpacefill) + break; + case 'spacefill': + transforms.push(SpacefillUpdate) + break; + } + return transforms +} + +export class Entity extends View<Controller<any>, {}, { entity: AnyEntity}> { + render() { + const entity = this.props.entity + + return <div className='molstar-entity-tree-entry'> + <div className='molstar-entity-tree-entry-body'> + <div className='molstar-entity-tree-entry-label-wrap'> + <button + className='molstar-entity-tree-entry-label' + onClick={() => { + console.log(entity) + this.controller.context.currentEntity.next(entity) + this.controller.context.currentTransforms.next(getTransforms(entity)) + }} + > + <span>{entity.id} - {entity.kind}</span> + </button> + </div> + </div> + </div>; + } +} + +export class EntityTree extends View<EntityTreeController, {}, {}> { + render() { + const entities: JSX.Element[] = [] + const state = this.controller.state.getValue() + if (state) { + state.entities.forEach(e => { + entities.push( + <div key={e.id}> + <Entity controller={this.controller} entity={e}></Entity> + </div> + ) + }) + } + + return <div className='molstar-entity-tree'> + <div className='molstar-entity-tree-root'> + <Entity controller={this.controller} entity={RootEntity}></Entity> + </div> + <div className='molstar-entity-tree-children'> + <div>{entities}</div> + </div> + </div>; + } +} \ No newline at end of file diff --git a/src/mol-app/ui/layout.tsx b/src/mol-app/ui/layout.tsx new file mode 100644 index 0000000000000000000000000000000000000000..df5184a84a82e6396e471ce069fb74ba2c310e9d --- /dev/null +++ b/src/mol-app/ui/layout.tsx @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * Adapted from LiteMol + * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info. + */ + +import * as React from 'react' +import { LayoutController, LayoutTarget, LayoutRegion, CollapsedControlsLayout } from '../controller/layout'; +import { View } from './view'; + +export class Layout extends View<LayoutController, { }, { }> { + + private renderTarget(target: LayoutTarget) { + const statics: any[] = []; + const scrollable: any[] = []; + + for (let c of target.components) { + if (c.isStatic) statics.push(<c.view key={c.key} controller={c.controller} />); + else scrollable.push(<c.view key={c.key} controller={c.controller} />); + } + + return <div key={target.cssClass} className={'molstar-layout-region molstar-layout-' + target.cssClass}> + { statics.length ? <div className='molstar-layout-static'>{statics}</div> : void 0 } + { scrollable.length ? <div className='molstar-layout-scrollable'>{scrollable}</div> : void 0 } + </div>; + } + + private updateTarget(name: string, regionType: LayoutRegion, layout: { regions: any[], layoutClass: string }) { + const state = this.controller.latestState; + const regionStates = state.regionStates; + const region = this.controller.targets[regionType]; + let show: boolean; + + if (state.hideControls) { + show = regionStates !== void 0 && regionStates[regionType] === 'Sticky' && region.components.length > 0; + } else if (regionStates && regionStates[regionType] === 'Hidden') { + show = false; + } else { + show = region.components.length > 0; + } + + if (show) { + layout.regions.push(this.renderTarget(region)); + } else { + layout.layoutClass += ' molstar-layout-hide-' + name; + } + } + + render() { + let layoutClass = ''; + + const state = this.controller.latestState; + let layoutType: string; + + if (state.isExpanded) { + layoutType = 'molstar-layout-expanded'; + } else { + layoutType = 'molstar-layout-standard '; + switch (state.collapsedControlsLayout) { + case CollapsedControlsLayout.Outside: layoutType += 'molstar-layout-standard-outside'; break; + case CollapsedControlsLayout.Landscape: layoutType += 'molstar-layout-standard-landscape'; break; + case CollapsedControlsLayout.Portrait: layoutType += 'molstar-layout-standard-portrait'; break; + default: layoutType += 'molstar-layout-standard-outside'; break; + } + } + + const targets = this.controller.targets; + const regions = [this.renderTarget(targets[LayoutRegion.Main])]; + + const layout = { regions, layoutClass }; + this.updateTarget('top', LayoutRegion.Top, layout); + this.updateTarget('right', LayoutRegion.Right, layout); + this.updateTarget('bottom', LayoutRegion.Bottom, layout); + this.updateTarget('left', LayoutRegion.Left, layout); + layoutClass = layout.layoutClass; + + let root = targets[LayoutRegion.Root].components.map(c => <c.view key={c.key} controller={c.controller} />); + + return <div className='molstar-plugin'> + <div className={'molstar-plugin-content ' + layoutType}> + <div className={layoutClass}> + {regions} + {root} + </div> + </div> + </div>; + } +} \ No newline at end of file diff --git a/src/mol-app/ui/misc/jobs.tsx b/src/mol-app/ui/misc/jobs.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c8a5fe557d75d6fd473a9367e747205c0b96183a --- /dev/null +++ b/src/mol-app/ui/misc/jobs.tsx @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * Adapted from LiteMol + * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info. + */ + +import * as React from 'react' +import { JobInfo, JobsController } from '../../controller/misc/jobs'; +import { Button } from '../controls/common'; +import { View } from '../view'; + +class JobState extends React.Component<{ info: JobInfo, isSmall?: boolean }, {}> { + render() { + const info = this.props.info; + return <div className='molstar-task-state'> + <div> + { info.abort ? <Button onClick={() => info.abort!.call(null) } style='remove' + icon='abort' title='Abort' customClass='molstar-btn-icon' + /> : void 0 } + <div> + {info.name}: {info.message} + </div> + </div> + </div>; + } +} + +export class Overlay extends View<JobsController, {}, {}> { + render() { + const state = this.controller.latestState; + + if (!state.jobs!.count()) return <div className='molstar-empty-control' /> + + const jobs: any[] = []; + state.jobs!.forEach((t, k) => jobs.push(<JobState key={k} info={t!} />)); + + return <div className='molstar-overlay'> + <div className='molstar-overlay-background' /> + <div className='molstar-overlay-content-wrap'> + <div className='molstar-overlay-content'> + <div> + {jobs} + </div> + </div> + </div> + </div>; + } +} + +export class BackgroundJobs extends View<JobsController, {}, {}> { + render() { + const state = this.controller.latestState; + + if (!state.jobs!.count()) return <div className='molstar-empty-control' /> + + const jobs: any[] = []; + state.jobs!.forEach((t, k) => jobs.push(<JobState key={k} info={t!} isSmall={true} />)); + + return <div className='molstar-background-jobs'> + {jobs} + </div>; + } +} \ No newline at end of file diff --git a/src/mol-app/ui/misc/log.tsx b/src/mol-app/ui/misc/log.tsx new file mode 100644 index 0000000000000000000000000000000000000000..803d86f70ea25417a5f95a51daebac54561faa51 --- /dev/null +++ b/src/mol-app/ui/misc/log.tsx @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * Adapted from LiteMol + * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info. + */ + +import * as React from 'react' +import { View } from '../view'; +import { LogController } from '../../controller/misc/log'; +import { CommonEvents } from '../../event/basic'; +import { formatTime } from 'mol-util'; +import { Logger } from '../../service/logger'; + +export class Log extends View<LogController, {}, {}> { + + private wrapper: HTMLDivElement | undefined = void 0; + + componentWillMount() { + super.componentWillMount(); + this.subscribe(CommonEvents.LayoutChanged.getStream(this.controller.context), () => this.scrollToBottom()); + } + + componentDidUpdate() { + this.scrollToBottom(); + } + + private scrollToBottom() { + const log = this.wrapper; + if (log) log.scrollTop = log.scrollHeight - log.clientHeight - 1; + } + + render() { + const entries = this.controller.latestState.entries; + + return <div className='molstar-log-wrap'> + <div className='molstar-log' ref={log => this.wrapper = log!}> + <ul className='molstar-list-unstyled'> + {entries.map((entry, i, arr) => { + + let label: JSX.Element; + let e = entry!; + switch (e.type) { + case Logger.EntryType.Error: + label = <span className='label label-danger'>Error</span>; + break; + case Logger.EntryType.Warning: + label = <span className='label label-warning'>Warning</span>; + break; + case Logger.EntryType.Info: + label = <span className='label label-info'>Info</span>; + break; + default: + label = <span></span> + } + + let t = formatTime(e.timestamp); + return <li key={i}> + <div className={'molstar-log-entry-badge molstar-log-entry-' + Logger.EntryType[e.type].toLowerCase()} /> + {label} + <div className='molstar-log-timestamp'>{t}</div> + <div className='molstar-log-entry'>{e.message}</div> + </li>; + }) } + </ul> + </div> + </div>; + } +} \ No newline at end of file diff --git a/src/mol-app/ui/transform/file-loader.tsx b/src/mol-app/ui/transform/file-loader.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a88b13f9a5dce132ad90f15923812420459d0b85 --- /dev/null +++ b/src/mol-app/ui/transform/file-loader.tsx @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import * as React from 'react' +import { View } from '../view'; +import { FileInput } from '../controls/common'; +import { TransformListController } from '../../controller/transform/list'; +import { FileEntity } from 'mol-view/state/entity'; +import { MmcifFileToSpacefill } from 'mol-view/state/transform'; +import { StateContext } from 'mol-view/state/context'; + +export class FileLoader extends View<TransformListController, {}, { ctx: StateContext }> { + render() { + return <div className='molstar-file-loader'> + <FileInput + accept='*.cif' + onChange={files => { + if (files) { + const fileEntity = FileEntity.ofFile(this.props.ctx, files[0]) + MmcifFileToSpacefill.apply(this.props.ctx, fileEntity) + } + }} + /> + </div>; + } +} \ No newline at end of file diff --git a/src/mol-app/ui/transform/list.tsx b/src/mol-app/ui/transform/list.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b72e8e6be91a76d34e05e310a8076377c5fcaf87 --- /dev/null +++ b/src/mol-app/ui/transform/list.tsx @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * Adapted from LiteMol + * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import * as React from 'react' + +import { View } from '../view'; +import { Controller } from '../../controller/controller'; +import { TransformListController } from '../../controller/transform/list'; +import { AnyTransform } from 'mol-view/state/transform'; +import { Spacefill } from './spacefill'; +import { AnyEntity } from 'mol-view/state/entity'; +import { FileLoader } from './file-loader'; +import { ModelToStructure } from './model'; + +function getTransformComponent(controller: TransformListController, entity: AnyEntity, transform: AnyTransform) { + switch (transform.kind) { + case 'file-to-spacefill': + return <FileLoader controller={controller} ctx={controller.context.stage.ctx}></FileLoader> + case 'model-to-structure': + return <ModelToStructure controller={controller} entity={entity} transform={transform} ctx={controller.context.stage.ctx}></ModelToStructure> + case 'spacefill-update': + return <Spacefill controller={controller} entity={entity} transform={transform} ctx={controller.context.stage.ctx}></Spacefill> + } + return <Transform controller={controller} entity={entity} transform={transform}></Transform> +} + +export class Transform extends View<Controller<any>, {}, { transform: AnyTransform, entity: AnyEntity }> { + render() { + const { transform, entity } = this.props + + return <div className='molstar-transformer-wrapper'> + <div className='molstar-panel molstar-control molstar-transformer'> + <div className='molstar-panel-header'> + <button + className='molstar-btn molstar-btn-link molstar-panel-expander' + onClick={(e)=> { + console.log(transform, entity) + }} + > + <span>[{transform.kind}] {transform.inputKind} -> {transform.outputKind}</span> + </button> + </div> + </div> + </div>; + } +} + +export class TransformList extends View<TransformListController, {}, {}> { + render() { + const transforms: JSX.Element[] = [] + const state = this.controller.state.getValue() + if (state && state.entity) { + const entity = state.entity + if (entity) { + state.transforms.forEach(t => { + transforms.push( + <div + key={`${t.inputKind}|${t.outputKind}`} + children={getTransformComponent(this.controller, entity, t)} + /> + ) + }) + } + } + + return <div className='molstar-transform-view' children={transforms} />; + } +} \ No newline at end of file diff --git a/src/mol-app/ui/transform/model.tsx b/src/mol-app/ui/transform/model.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ebd4d2edb99c7d9e419005c54799d9284aee5bf8 --- /dev/null +++ b/src/mol-app/ui/transform/model.tsx @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * Adapted from LiteMol + * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import * as React from 'react' + +import { View } from '../view'; +import { Controller } from '../../controller/controller'; +import { ModelEntity } from 'mol-view/state/entity'; +import { StructureProps, ModelToStructure as ModelToStructureTransform } from 'mol-view/state/transform' +import { StateContext } from 'mol-view/state/context'; + +export class ModelToStructure extends View<Controller<any>, StructureProps, { transform: ModelToStructureTransform, entity: ModelEntity, ctx: StateContext }> { + state = { + assembly: '' + } + + create(state?: Partial<StructureProps>) { + const { transform, entity, ctx } = this.props + console.log('create structure', transform, entity) + const newState = { ...this.state, ...state } + this.setState(newState) + transform.apply(ctx, entity, newState) + } + + render() { + const { transform, entity } = this.props + + const assemblyOptions = entity.value[0].symmetry.assemblies.map((value, idx) => { + return <option key={value.id} value={value.id}>{value.details}</option> + }) + + return <div className='molstar-transformer-wrapper'> + <div className='molstar-panel molstar-control molstar-transformer molstar-panel-expanded'> + <div className='molstar-panel-header'> + <button + className='molstar-btn molstar-btn-link molstar-panel-expander' + onClick={() => this.create()} + > + <span>[{transform.kind}] {transform.inputKind} -> {transform.outputKind}</span> + </button> + </div> + <div className='molstar-panel-body'> + <div> + <div className='molstar-control-row molstar-options-group'> + <span>Details</span> + <div> + <select + className='molstar-form-control' + value={this.state.assembly} + onChange={(e) => this.create({ assembly: e.target.value })} + > + {assemblyOptions} + </select> + </div> + </div> + </div> + </div> + </div> + </div>; + } +} \ No newline at end of file diff --git a/src/mol-app/ui/transform/spacefill.tsx b/src/mol-app/ui/transform/spacefill.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5ee8d295991ba9f2b494138531c63b48be78f76c --- /dev/null +++ b/src/mol-app/ui/transform/spacefill.tsx @@ -0,0 +1,143 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * Adapted from LiteMol + * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import * as React from 'react' + +import { View } from '../view'; +import { Controller } from '../../controller/controller'; +import { SpacefillEntity } from 'mol-view/state/entity'; +import { SpacefillUpdate } from 'mol-view/state/transform' +import { StateContext } from 'mol-view/state/context'; +import { ColorTheme } from 'mol-geo/theme'; +import { Color, ColorNames } from 'mol-util/color'; + +export const ColorThemeInfo = { + 'atom-index': {}, + 'chain-id': {}, + 'element-symbol': {}, + 'instance-index': {}, + 'uniform': {} +} +export type ColorThemeInfo = keyof typeof ColorThemeInfo + +interface SpacefillState { + doubleSided: boolean + detail: number + colorTheme: ColorTheme + colorValue: Color +} + +export class Spacefill extends View<Controller<any>, SpacefillState, { transform: SpacefillUpdate, entity: SpacefillEntity, ctx: StateContext }> { + state = { + doubleSided: true, + detail: 2, + colorTheme: { name: 'element-symbol' } as ColorTheme, + colorValue: 0x000000 + } + + update(state?: Partial<SpacefillState>) { + const { transform, entity, ctx } = this.props + console.log('update spacefill', transform, entity) + const newState = { ...this.state, ...state } + this.setState(newState) + transform.apply(ctx, entity, newState) + } + + render() { + const { transform } = this.props + + const sphereDetailOptions = [0, 1, 2, 3].map((value, idx) => { + return <option key={value} value={value}>{value.toString()}</option> + }) + + const colorThemeOptions = Object.keys(ColorThemeInfo).map((name, idx) => { + return <option key={name} value={name}>{name}</option> + }) + + const colorValueOptions = Object.keys(ColorNames).map((name, idx) => { + return <option key={name} value={(ColorNames as any)[name]}>{name}</option> + }) + + return <div className='molstar-transformer-wrapper'> + <div className='molstar-panel molstar-control molstar-transformer molstar-panel-expanded'> + <div className='molstar-panel-header'> + <button + className='molstar-btn molstar-btn-link molstar-panel-expander' + onClick={() => this.update()} + > + <span>[{transform.kind}] {transform.inputKind} -> {transform.outputKind}</span> + </button> + </div> + <div className='molstar-panel-body'> + <div> + <div className='molstar-control-row molstar-options-group'> + <span>Sphere detail</span> + <div> + <select + className='molstar-form-control' + value={this.state.detail} + onChange={(e) => this.update({ detail: parseInt(e.target.value) })} + > + {sphereDetailOptions} + </select> + </div> + </div> + <div className='molstar-control-row molstar-options-group'> + <span>Color theme</span> + <div> + <select + className='molstar-form-control' + value={this.state.colorTheme.name} + onChange={(e) => { + const colorThemeName = e.target.value as ColorThemeInfo + if (colorThemeName === 'uniform') { + this.update({ + colorTheme: { + name: colorThemeName, + value: this.state.colorValue + } + }) + } else { + this.update({ + colorTheme: { name: colorThemeName } + }) + } + }} + > + {colorThemeOptions} + </select> + </div> + </div> + <div className='molstar-control-row molstar-options-group'> + <span>Color value</span> + <div> + <select + className='molstar-form-control' + value={this.state.colorValue} + onChange={(e) => { + const colorValue = parseInt(e.target.value) + this.update({ + colorTheme: { + name: 'uniform', + value: colorValue + }, + colorValue + }) + }} + > + {colorValueOptions} + </select> + </div> + </div> + </div> + </div> + </div> + </div>; + } +} \ No newline at end of file diff --git a/src/mol-app/ui/view.tsx b/src/mol-app/ui/view.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d7374759f3ac87084324a13e580bfa666d82d717 --- /dev/null +++ b/src/mol-app/ui/view.tsx @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * Adapted from LiteMol + * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info. + */ + +import * as React from 'react' +import { Observable, Subscription } from 'rxjs'; +import { merge, shallowEqual } from 'mol-util' +import { Context } from '../context/context'; +import { Controller } from '../controller/controller'; + +export abstract class PureView<State, Props, ViewState> extends React.Component<{ + state: State + onChange: (s: State) => void +} & Props, ViewState> { + + protected update(s: State) { + let ns = merge<State>(this.props.state, s); + if (ns !== this.props.state as any) this.props.onChange(ns); + } + + shouldComponentUpdate(nextProps: any, nextState: any) { + return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState); + } +} + +export abstract class ComponentView<Props> extends React.Component<{ context: Context } & Props, {}> { + + // shouldComponentUpdate(nextProps: any, nextState: any) { + // return !shallowEqual(this.props, nextProps); + // } + + private subs: Subscription[] = []; + protected subscribe<T>(stream: Observable<T>, obs: (n: T) => void) { + let sub = stream.subscribe(obs); + this.subs.push(sub); + return sub; + } + + protected unsubscribe(sub: Subscription) { + let idx = this.subs.indexOf(sub); + for (let i = idx; i < this.subs.length - 1; i++) { + this.subs[i] = this.subs[i + 1]; + } + sub.unsubscribe(); + this.subs.pop(); + } + + componentWillUnmount() { + for (let s of this.subs) s.unsubscribe(); + this.subs = []; + } +} + +export abstract class ObserverView<P, S> extends React.Component<P, S> { + private subs: Subscription[] = []; + + protected subscribe<T>(stream: Observable<T>, obs: (n: T) => void) { + let sub = stream.subscribe(obs); + this.subs.push(sub); + return sub; + } + + protected unsubscribe(sub: Subscription) { + let idx = this.subs.indexOf(sub); + for (let i = idx; i < this.subs.length - 1; i++) { + this.subs[i] = this.subs[i + 1]; + } + sub.unsubscribe(); + this.subs.pop(); + } + + componentWillUnmount() { + for (let s of this.subs) s.unsubscribe(); + this.subs = []; + } +} + +export abstract class View<T extends Controller<any>, State, CustomProps> + extends ObserverView<{ controller: T } & CustomProps, State> { + + public get controller(): T { + return this.props.controller as any; + } + + componentWillMount() { + this.subscribe(this.controller.state as any, (s) => { + this.forceUpdate() + }); + } +} \ No newline at end of file diff --git a/src/mol-app/ui/visualization/viewport.tsx b/src/mol-app/ui/visualization/viewport.tsx new file mode 100644 index 0000000000000000000000000000000000000000..71b3afdeb2376e5d5dcb9c01cb6f24c8c4d5fbd1 --- /dev/null +++ b/src/mol-app/ui/visualization/viewport.tsx @@ -0,0 +1,141 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * Adapted from LiteMol + * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import * as React from 'react' + +import { ViewportController } from '../../controller/visualization/viewport' +import { View } from '../view'; +import { HelpBox, Toggle, Button } from '../controls/common' +import { Slider } from '../controls/slider' + +export class ViewportControls extends View<ViewportController, { showSceneOptions?: boolean, showHelp?: boolean }, {}> { + state = { showSceneOptions: false, showHelp: false }; + + private help() { + return <div className='molstar-viewport-controls-scene-options molstar-control'> + <HelpBox title='Rotate' content={<div><div>Left button</div><div>One finger touch</div></div>} /> + <HelpBox title='Zoom' content={<div><div>Right button</div><div>Pinch</div></div>} /> + <HelpBox title='Move' content={<div><div>Middle button</div><div>Two finger touch</div></div>} /> + <HelpBox title='Slab' content={<div><div>Mouse wheel</div><div>Three finger touch</div></div>} /> + </div> + } + + render() { + let state = this.controller.latestState; + + let options: any; + + let layoutController = this.controller.context.layout; + let layoutState = layoutController.latestState; + if (this.state.showSceneOptions) { + options = <div className='molstar-viewport-controls-scene-options molstar-control'> + <Toggle onChange={v => this.controller.setState({ enableFog: v })} value={state.enableFog!} label='Fog' /> + <Slider label='FOV' min={30} max={90} onChange={v => this.controller.setState({ cameraFOV: v }) } value={state.cameraFOV!} /> + <Slider label='Camera Speed' min={1} max={10} step={0.01} onChange={v => this.controller.setState({ cameraSpeed: v }) } value={state.cameraSpeed!} /> + </div>; + } else if (this.state.showHelp) { + options = this.help(); + } + + let controlsShown = !layoutState.hideControls; + return <div className='molstar-viewport-controls' onMouseLeave={() => this.setState({ showSceneOptions: false, showHelp: false })}> + <div className='molstar-viewport-controls-buttons'> + <Button + style='link' + active={this.state.showHelp} + customClass={'molstar-btn-link-toggle-' + (this.state.showHelp ? 'on' : 'off')} + icon='help-circle' + onClick={(e) => this.setState({ showHelp: !this.state.showHelp, showSceneOptions: false }) } title='Controls Help' /> + <Button + style='link' + active={this.state.showSceneOptions} + customClass={'molstar-btn-link-toggle-' + (this.state.showSceneOptions ? 'on' : 'off')} + icon='settings' + onClick={(e) => this.setState({ showSceneOptions: !this.state.showSceneOptions, showHelp: false }) } title='Scene Options' /> + <Button + style='link' + icon='screenshot' + onClick={(e) => this.controller.context.stage.viewer.downloadScreenshot()} + title='Screenshot' /> + <Button onClick={() => { layoutController.update({ hideControls: controlsShown }); this.forceUpdate(); } } + icon='tools' title={controlsShown ? 'Hide Controls' : 'Show Controls'} active={controlsShown } + customClass={'molstar-btn-link-toggle-' + (controlsShown ? 'on' : 'off')} + style='link' /> + <Button onClick={() => layoutController.update({ isExpanded: !layoutState.isExpanded }) } + icon='expand-layout' title={layoutState.isExpanded ? 'Collapse' : 'Expand'} active={layoutState.isExpanded } + customClass={'molstar-btn-link-toggle-' + (layoutState.isExpanded ? 'on' : 'off')} + style='link' /> + <Button + style='link' + icon='reset-scene' + onClick={(e) => this.controller.context.stage.viewer.resetCamera()} + title='Reset camera' /> + </div> + {options} + </div>; + } +} + +export const Logo = () => + <div className='molstar-logo'> + <div> + <div> + <div /> + <div className='molstar-logo-image' /> + </div> + </div> + </div> + + +export class Viewport extends View<ViewportController, {}, { noWebGl?: boolean, showLogo?: boolean }> { + private container: HTMLDivElement | null = null; + private canvas: HTMLCanvasElement | null = null; + private defaultBg = { r: 1, g: 1, b: 1 } + state = { noWebGl: false, showLogo: true }; + + componentDidMount() { + if (!this.canvas || !this.container || !this.controller.context.initStage(this.canvas, this.container)) { + this.setState({ noWebGl: true }); + } + this.controller.context.stage.viewer.reprCount.subscribe(count => { + this.setState({ + showLogo: false + // showLogo: count === 0 + }) + }) + } + + componentWillUnmount() { + super.componentWillUnmount(); + this.controller.context.destroy(); + } + + renderMissing() { + return <div className='molstar-no-webgl'> + <div> + <p><b>WebGL does not seem to be available.</b></p> + <p>This can be caused by an outdated browser, graphics card driver issue, or bad weather. Sometimes, just restarting the browser helps.</p> + <p>For a list of supported browsers, refer to <a href='http://caniuse.com/#feat=webgl' target='_blank'>http://caniuse.com/#feat=webgl</a>.</p> + </div> + </div> + } + + render() { + if (this.state.noWebGl) return this.renderMissing(); + + const color = this.controller.latestState.clearColor! || this.defaultBg; + return <div className='molstar-viewport' style={{ backgroundColor: `rgb(${255 * color.r}, ${255 * color.g}, ${255 * color.b})` }}> + <div ref={elm => this.container = elm} className='molstar-viewport-container'> + <canvas ref={elm => this.canvas = elm} className='molstar-viewport-canvas'></canvas> + </div> + {this.state.showLogo ? <Logo /> : void 0} + <ViewportControls controller={this.controller} /> + </div>; + } +} \ No newline at end of file diff --git a/src/mol-io/reader/_spec/cif.spec.ts b/src/mol-io/reader/_spec/cif.spec.ts index e7d619f280a0d66f6eead466e4b9163c7363b947..a2fb03ed952f69e1c9c93878a7b5904d174f3ddd 100644 --- a/src/mol-io/reader/_spec/cif.spec.ts +++ b/src/mol-io/reader/_spec/cif.spec.ts @@ -18,8 +18,8 @@ const strField = TextField({ data: columnData, indices: [3, 4, 4, 5, 5, 6], coun const strListField = TextField({ data: columnData, indices: [7, 12], count: 1 }, 1); const intListField = TextField({ data: columnData, indices: [14, 19], count: 1 }, 1); -const testBlock = Data.Block(['test'], { - test: Data.Category('test', 3, ['int', 'str', 'strList', 'intList'], { +const testBlock = Data.CifBlock(['test'], { + test: Data.CifCategory('test', 3, ['int', 'str', 'strList', 'intList'], { int: intField, str: strField, strList: strListField, diff --git a/src/mol-io/reader/cif.ts b/src/mol-io/reader/cif.ts index 440be38553cb525327fd6e376bbd0f29490f4203..54aad885c8c623bb055d221741380dab044c71ca 100644 --- a/src/mol-io/reader/cif.ts +++ b/src/mol-io/reader/cif.ts @@ -7,7 +7,7 @@ import parseText from './cif/text/parser' import parseBinary from './cif/binary/parser' -import { Frame } from './cif/data-model' +import { CifFrame } from './cif/data-model' import { toDatabaseCollection, toDatabase } from './cif/schema' import { mmCIF_Schema, mmCIF_Database } from './cif/schema/mmcif' import { CCD_Schema, CCD_Database } from './cif/schema/ccd' @@ -22,11 +22,11 @@ export default { toDatabaseCollection, toDatabase, schema: { - mmCIF: (frame: Frame) => toDatabase<mmCIF_Schema, mmCIF_Database>(mmCIF_Schema, frame), - CCD: (frame: Frame) => toDatabase<CCD_Schema, CCD_Database>(CCD_Schema, frame), - BIRD: (frame: Frame) => toDatabase<BIRD_Schema, BIRD_Database>(BIRD_Schema, frame), - dic: (frame: Frame) => toDatabase<dic_Schema, dic_Database>(dic_Schema, frame), - densityServer: (frame: Frame) => toDatabase<DensityServer_Data_Schema, DensityServer_Data_Database>(DensityServer_Data_Schema, frame) + mmCIF: (frame: CifFrame) => toDatabase<mmCIF_Schema, mmCIF_Database>(mmCIF_Schema, frame), + CCD: (frame: CifFrame) => toDatabase<CCD_Schema, CCD_Database>(CCD_Schema, frame), + BIRD: (frame: CifFrame) => toDatabase<BIRD_Schema, BIRD_Database>(BIRD_Schema, frame), + dic: (frame: CifFrame) => toDatabase<dic_Schema, dic_Database>(dic_Schema, frame), + densityServer: (frame: CifFrame) => toDatabase<DensityServer_Data_Schema, DensityServer_Data_Database>(DensityServer_Data_Schema, frame) } } diff --git a/src/mol-io/reader/cif/binary/field.ts b/src/mol-io/reader/cif/binary/field.ts index dd5758f519c895f28ea88801cc627692cb29cd42..af596d70a9909fd459d077a4c1aa4c21a22b0afd 100644 --- a/src/mol-io/reader/cif/binary/field.ts +++ b/src/mol-io/reader/cif/binary/field.ts @@ -10,12 +10,12 @@ import * as Data from '../data-model' import { EncodedColumn, decode } from '../../../common/binary-cif' import { parseInt as fastParseInt, parseFloat as fastParseFloat } from '../../common/text/number-parser' -export default function Field(column: EncodedColumn): Data.Field { +export default function Field(column: EncodedColumn): Data.CifField { const mask = column.mask ? decode(column.mask) as number[] : void 0; const data = decode(column.data); const isNumeric = ColumnHelpers.isTypedArray(data); - const str: Data.Field['str'] = isNumeric + const str: Data.CifField['str'] = isNumeric ? mask ? row => mask[row] === Column.ValueKind.Present ? '' + data[row] : '' : row => '' + data[row] @@ -23,15 +23,15 @@ export default function Field(column: EncodedColumn): Data.Field { ? row => mask[row] === Column.ValueKind.Present ? data[row] : '' : row => data[row]; - const int: Data.Field['int'] = isNumeric + const int: Data.CifField['int'] = isNumeric ? row => data[row] : row => { const v = data[row]; return fastParseInt(v, 0, v.length); }; - const float: Data.Field['float'] = isNumeric + const float: Data.CifField['float'] = isNumeric ? row => data[row] : row => { const v = data[row]; return fastParseFloat(v, 0, v.length); }; - const valueKind: Data.Field['valueKind'] = mask + const valueKind: Data.CifField['valueKind'] = mask ? row => mask[row] : row => Column.ValueKind.Present; diff --git a/src/mol-io/reader/cif/binary/parser.ts b/src/mol-io/reader/cif/binary/parser.ts index bd53cd0ad2d569049d6145eb35481bb3187e1fae..8a5f0ea1a8133aed0254577b2791d75952042183 100644 --- a/src/mol-io/reader/cif/binary/parser.ts +++ b/src/mol-io/reader/cif/binary/parser.ts @@ -18,7 +18,7 @@ function checkVersions(min: number[], current: number[]) { return true; } -function Category(data: EncodedCategory): Data.Category { +function Category(data: EncodedCategory): Data.CifCategory { const map = Object.create(null); const cache = Object.create(null); for (const col of data.columns) map[col.name] = col; @@ -37,22 +37,22 @@ function Category(data: EncodedCategory): Data.Category { } export default function parse(data: Uint8Array) { - return Task.create<Result<Data.File>>('Parse BinaryCIF', async ctx => { + return Task.create<Result<Data.CifFile>>('Parse BinaryCIF', async ctx => { const minVersion = [0, 3]; try { const unpacked = decodeMsgPack(data) as EncodedFile; if (!checkVersions(minVersion, unpacked.version.match(/(\d)\.(\d)\.\d/)!.slice(1).map(v => +v))) { - return Result.error<Data.File>(`Unsupported format version. Current ${unpacked.version}, required ${minVersion.join('.')}.`); + return Result.error<Data.CifFile>(`Unsupported format version. Current ${unpacked.version}, required ${minVersion.join('.')}.`); } - const file = Data.File(unpacked.dataBlocks.map(block => { + const file = Data.CifFile(unpacked.dataBlocks.map(block => { const cats = Object.create(null); for (const cat of block.categories) cats[cat.name.substr(1)] = Category(cat); - return Data.Block(block.categories.map(c => c.name.substr(1)), cats, block.header); + return Data.CifBlock(block.categories.map(c => c.name.substr(1)), cats, block.header); })); return Result.success(file); } catch (e) { - return Result.error<Data.File>('' + e); + return Result.error<Data.CifFile>('' + e); } }) } \ No newline at end of file diff --git a/src/mol-io/reader/cif/data-model.ts b/src/mol-io/reader/cif/data-model.ts index 4a090d35163ec693de14ff65e0c4e559a6dbaf89..30bc1c43e06cad7e26e412159f7468124f55d60c 100644 --- a/src/mol-io/reader/cif/data-model.ts +++ b/src/mol-io/reader/cif/data-model.ts @@ -8,49 +8,49 @@ import { Column } from 'mol-data/db' import { Tensor } from 'mol-math/linear-algebra' -export interface File { +export interface CifFile { readonly name?: string, - readonly blocks: ReadonlyArray<Block> + readonly blocks: ReadonlyArray<CifBlock> } -export function File(blocks: ArrayLike<Block>, name?: string): File { +export function CifFile(blocks: ArrayLike<CifBlock>, name?: string): CifFile { return { name, blocks: blocks as any }; } -export interface Frame { +export interface CifFrame { readonly header: string, // Category names stored separately so that the ordering can be preserved. readonly categoryNames: ReadonlyArray<string>, - readonly categories: Categories + readonly categories: CifCategories } -export interface Block extends Frame { - readonly saveFrames: Frame[] +export interface CifBlock extends CifFrame { + readonly saveFrames: CifFrame[] } -export function Block(categoryNames: string[], categories: Categories, header: string, saveFrames: Frame[] = []): Block { +export function CifBlock(categoryNames: string[], categories: CifCategories, header: string, saveFrames: CifFrame[] = []): CifBlock { return { categoryNames, header, categories, saveFrames }; } -export function SafeFrame(categoryNames: string[], categories: Categories, header: string): Frame { +export function CifSafeFrame(categoryNames: string[], categories: CifCategories, header: string): CifFrame { return { categoryNames, header, categories }; } -export type Categories = { readonly [name: string]: Category } +export type CifCategories = { readonly [name: string]: CifCategory } -export interface Category { +export interface CifCategory { readonly rowCount: number, readonly name: string, readonly fieldNames: ReadonlyArray<string>, - getField(name: string): Field | undefined + getField(name: string): CifField | undefined } -export function Category(name: string, rowCount: number, fieldNames: string[], fields: { [name: string]: Field }): Category { +export function CifCategory(name: string, rowCount: number, fieldNames: string[], fields: { [name: string]: CifField }): CifCategory { return { rowCount, name, fieldNames: [...fieldNames], getField(name) { return fields[name]; } }; } -export namespace Category { - export function empty(name: string): Category { +export namespace CifCategory { + export function empty(name: string): CifCategory { return { rowCount: 0, name, fieldNames: [], getField(name: string) { return void 0; } }; }; } @@ -60,7 +60,7 @@ export namespace Category { * Always implement without using "this." in any of the interface functions. * This is to ensure that the functions can invoked without having to "bind" them. */ -export interface Field { +export interface CifField { readonly '@array': ArrayLike<any> | undefined readonly isDefined: boolean, readonly rowCount: number, @@ -78,7 +78,7 @@ export interface Field { toFloatArray(params?: Column.ToArrayParams<number>): ReadonlyArray<number> } -export function getTensor(category: Category, field: string, space: Tensor.Space, row: number, zeroIndexed: boolean): Tensor.Data { +export function getTensor(category: CifCategory, field: string, space: Tensor.Space, row: number, zeroIndexed: boolean): Tensor.Data { const ret = space.create(); const offset = zeroIndexed ? 0 : 1; diff --git a/src/mol-io/reader/cif/schema.ts b/src/mol-io/reader/cif/schema.ts index c7710132f758dc4ec0509b0f2089c9f336289544..8efa52ab41b2a0c2047ad7cb9149751d44fd7610 100644 --- a/src/mol-io/reader/cif/schema.ts +++ b/src/mol-io/reader/cif/schema.ts @@ -10,7 +10,7 @@ import { Tensor } from 'mol-math/linear-algebra' import { arrayEqual } from 'mol-util' import * as Data from './data-model' -export function toDatabaseCollection<Schema extends Database.Schema>(schema: Schema, file: Data.File): DatabaseCollection<Schema> { +export function toDatabaseCollection<Schema extends Database.Schema>(schema: Schema, file: Data.CifFile): DatabaseCollection<Schema> { const dbc: DatabaseCollection<Schema> = {} for (const data of file.blocks) { dbc[data.header] = toDatabase(schema, data) @@ -18,15 +18,15 @@ export function toDatabaseCollection<Schema extends Database.Schema>(schema: Sch return dbc; } -export function toDatabase<Schema extends Database.Schema, Frame extends Database<Schema> = Database<Schema>>(schema: Schema, frame: Data.Frame): Frame { +export function toDatabase<Schema extends Database.Schema, Frame extends Database<Schema> = Database<Schema>>(schema: Schema, frame: Data.CifFrame): Frame { return createDatabase(schema, frame) as Frame; } -export function toTable<Schema extends Table.Schema, R extends Table<Schema> = Table<Schema>>(schema: Schema, category: Data.Category): R { +export function toTable<Schema extends Table.Schema, R extends Table<Schema> = Table<Schema>>(schema: Schema, category: Data.CifCategory): R { return new CategoryTable(category, schema, true) as any; } -type ColumnCtor = (field: Data.Field, category: Data.Category, key: string) => Column<any> +type ColumnCtor = (field: Data.CifField, category: Data.CifCategory, key: string) => Column<any> function getColumnCtor(t: Column.Schema): ColumnCtor { switch (t.valueType) { @@ -38,7 +38,7 @@ function getColumnCtor(t: Column.Schema): ColumnCtor { } } -function createColumn<T>(schema: Column.Schema, field: Data.Field, value: (row: number) => T, toArray: Column<T>['toArray']): Column<T> { +function createColumn<T>(schema: Column.Schema, field: Data.CifField, value: (row: number) => T, toArray: Column<T>['toArray']): Column<T> { return { schema, '@array': field['@array'], @@ -51,7 +51,7 @@ function createColumn<T>(schema: Column.Schema, field: Data.Field, value: (row: }; } -function createListColumn<T extends number|string>(schema: Column.Schema.List<T>, category: Data.Category, key: string): Column<(number|string)[]> { +function createListColumn<T extends number|string>(schema: Column.Schema.List<T>, category: Data.CifCategory, key: string): Column<(number|string)[]> { const separator = schema.separator; const itemParse = schema.itemParse; @@ -71,7 +71,7 @@ function createListColumn<T extends number|string>(schema: Column.Schema.List<T> }; } -function createTensorColumn(schema: Column.Schema.Tensor, category: Data.Category, key: string): Column<Tensor.Data> { +function createTensorColumn(schema: Column.Schema.Tensor, category: Data.CifCategory, key: string): Column<Tensor.Data> { const space = schema.space; const zeroOffset = category.fieldNames.indexOf(`${key}[0]`) >= 0; const fst = zeroOffset ? 0 : 1; @@ -105,7 +105,7 @@ class CategoryTable implements Table<any> { // tslint:disable-line:class-name _schema: any; [k: string]: any; - constructor(category: Data.Category, schema: Table.Schema, public _isDefined: boolean) { + constructor(category: Data.CifCategory, schema: Table.Schema, public _isDefined: boolean) { const fieldKeys = Object.keys(schema); this._rowCount = category.rowCount; this._columns = fieldKeys; @@ -134,7 +134,7 @@ class CategoryTable implements Table<any> { // tslint:disable-line:class-name } } -function createDatabase(schema: Database.Schema, frame: Data.Frame): Database<any> { +function createDatabase(schema: Database.Schema, frame: Data.CifFrame): Database<any> { const tables = Object.create(null); for (const k of Object.keys(schema)) { tables[k] = createTable(k, (schema as any)[k], frame); @@ -142,7 +142,7 @@ function createDatabase(schema: Database.Schema, frame: Data.Frame): Database<an return Database.ofTables(frame.header, schema, tables); } -function createTable(key: string, schema: Table.Schema, frame: Data.Frame) { +function createTable(key: string, schema: Table.Schema, frame: Data.CifFrame) { const cat = frame.categories[key]; - return new CategoryTable(cat || Data.Category.empty(key), schema, !!cat); + return new CategoryTable(cat || Data.CifCategory.empty(key), schema, !!cat); } \ No newline at end of file diff --git a/src/mol-io/reader/cif/text/field.ts b/src/mol-io/reader/cif/text/field.ts index 8b6daa2b88a07aa7adc1d4eedbf8d0c83234b394..32af80a85747eb0c8499d1a76b58689a86b721ed 100644 --- a/src/mol-io/reader/cif/text/field.ts +++ b/src/mol-io/reader/cif/text/field.ts @@ -11,24 +11,24 @@ import { Tokens } from '../../common/text/tokenizer' import * as Data from '../data-model' import { parseInt as fastParseInt, parseFloat as fastParseFloat } from '../../common/text/number-parser' -export default function CifTextField(tokens: Tokens, rowCount: number): Data.Field { +export default function CifTextField(tokens: Tokens, rowCount: number): Data.CifField { const { data, indices } = tokens; - const str: Data.Field['str'] = row => { + const str: Data.CifField['str'] = row => { const ret = data.substring(indices[2 * row], indices[2 * row + 1]); if (ret === '.' || ret === '?') return ''; return ret; }; - const int: Data.Field['int'] = row => { + const int: Data.CifField['int'] = row => { return fastParseInt(data, indices[2 * row], indices[2 * row + 1]) || 0; }; - const float: Data.Field['float'] = row => { + const float: Data.CifField['float'] = row => { return fastParseFloat(data, indices[2 * row], indices[2 * row + 1]) || 0; }; - const valueKind: Data.Field['valueKind'] = row => { + const valueKind: Data.CifField['valueKind'] = row => { const s = indices[2 * row]; if (indices[2 * row + 1] - s !== 1) return Column.ValueKind.Present; const v = data.charCodeAt(s); diff --git a/src/mol-io/reader/cif/text/parser.ts b/src/mol-io/reader/cif/text/parser.ts index bbd4b39c83edaaa82b95b720511b9fc0baac7cb1..610c7b0f8eb780224f4fb822295887bded4d50c6 100644 --- a/src/mol-io/reader/cif/text/parser.ts +++ b/src/mol-io/reader/cif/text/parser.ts @@ -413,7 +413,7 @@ interface CifCategoryResult { type FrameContext = { categoryNames: string[], - categories: { [name: string]: Data.Category } + categories: { [name: string]: Data.CifCategory } } function FrameContext(): FrameContext { @@ -451,7 +451,7 @@ function handleSingle(tokenizer: TokenizerState, ctx: FrameContext): CifCategory } const catName = name.substr(1); - ctx.categories[catName] = Data.Category(catName, 1, fieldNames, fields); + ctx.categories[catName] = Data.CifCategory(catName, 1, fieldNames, fields); ctx.categoryNames.push(catName); return { @@ -533,7 +533,7 @@ async function handleLoop(tokenizer: TokenizerState, ctx: FrameContext): Promise } const catName = name.substr(1); - ctx.categories[catName] = Data.Category(catName, rowCount, fieldNames, fields); + ctx.categories[catName] = Data.CifCategory(catName, rowCount, fieldNames, fields); ctx.categoryNames.push(catName); return { @@ -547,13 +547,13 @@ async function handleLoop(tokenizer: TokenizerState, ctx: FrameContext): Promise * Creates an error result. */ function error(line: number, message: string) { - return Result.error<Data.File>(message, line); + return Result.error<Data.CifFile>(message, line); } /** * Creates a data result. */ -function result(data: Data.File) { +function result(data: Data.CifFile) { return Result.success(data); } @@ -563,7 +563,7 @@ function result(data: Data.File) { * @returns CifParserResult wrapper of the result. */ async function parseInternal(data: string, runtimeCtx: RuntimeContext) { - const dataBlocks: Data.Block[] = []; + const dataBlocks: Data.CifBlock[] = []; const tokenizer = createTokenizer(data, runtimeCtx); let blockHeader = ''; @@ -572,9 +572,9 @@ async function parseInternal(data: string, runtimeCtx: RuntimeContext) { let inSaveFrame = false; // the next three initial values are never used in valid files - let saveFrames: Data.Frame[] = []; + let saveFrames: Data.CifFrame[] = []; let saveCtx = FrameContext(); - let saveFrame: Data.Frame = Data.SafeFrame(saveCtx.categoryNames, saveCtx.categories, ''); + let saveFrame: Data.CifFrame = Data.CifSafeFrame(saveCtx.categoryNames, saveCtx.categories, ''); runtimeCtx.update({ message: 'Parsing...', current: 0, max: data.length }); @@ -588,7 +588,7 @@ async function parseInternal(data: string, runtimeCtx: RuntimeContext) { return error(tokenizer.lineNumber, 'Unexpected data block inside a save frame.'); } if (blockCtx.categoryNames.length > 0) { - dataBlocks.push(Data.Block(blockCtx.categoryNames, blockCtx.categories, blockHeader, saveFrames)); + dataBlocks.push(Data.CifBlock(blockCtx.categoryNames, blockCtx.categories, blockHeader, saveFrames)); } blockHeader = data.substring(tokenizer.tokenStart + 5, tokenizer.tokenEnd); blockCtx = FrameContext(); @@ -609,7 +609,7 @@ async function parseInternal(data: string, runtimeCtx: RuntimeContext) { inSaveFrame = true; const safeHeader = data.substring(tokenizer.tokenStart + 5, tokenizer.tokenEnd); saveCtx = FrameContext(); - saveFrame = Data.SafeFrame(saveCtx.categoryNames, saveCtx.categories, safeHeader); + saveFrame = Data.CifSafeFrame(saveCtx.categoryNames, saveCtx.categories, safeHeader); } moveNext(tokenizer); // Loop @@ -636,14 +636,14 @@ async function parseInternal(data: string, runtimeCtx: RuntimeContext) { } if (blockCtx.categoryNames.length > 0) { - dataBlocks.push(Data.Block(blockCtx.categoryNames, blockCtx.categories, blockHeader, saveFrames)); + dataBlocks.push(Data.CifBlock(blockCtx.categoryNames, blockCtx.categories, blockHeader, saveFrames)); } - return result(Data.File(dataBlocks)); + return result(Data.CifFile(dataBlocks)); } export default function parse(data: string) { - return Task.create<Result<Data.File>>('Parse CIF', async ctx => { + return Task.create<Result<Data.CifFile>>('Parse CIF', async ctx => { return await parseInternal(data, ctx); }); } \ No newline at end of file diff --git a/src/mol-io/reader/csv/data-model.ts b/src/mol-io/reader/csv/data-model.ts index 86f50f077574d8fd4e7fa5b511bdb6773821ccae..401c7aa2d5855cd530131ac22345bc0052823f9f 100644 --- a/src/mol-io/reader/csv/data-model.ts +++ b/src/mol-io/reader/csv/data-model.ts @@ -4,32 +4,32 @@ * @author Alexander Rose <alexander.rose@weirdbyte.de> */ -import { Field as Column } from '../cif/data-model' +import { CifField as CsvColumn } from '../cif/data-model' -export { Column } +export { CsvColumn } -export interface File { +export interface CsvFile { readonly name?: string, - readonly table: Table + readonly table: CsvTable } -export function File(table: Table, name?: string): File { +export function CsvFile(table: CsvTable, name?: string): CsvFile { return { name, table }; } -export interface Table { +export interface CsvTable { readonly rowCount: number, readonly columnNames: ReadonlyArray<string>, - getColumn(name: string): Column | undefined + getColumn(name: string): CsvColumn | undefined } -export function Table(rowCount: number, columnNames: string[], columns: Columns): Table { +export function CsvTable(rowCount: number, columnNames: string[], columns: CsvColumns): CsvTable { return { rowCount, columnNames: [...columnNames], getColumn(name) { return columns[name]; } }; } -export type Columns = { [name: string]: Column } +export type CsvColumns = { [name: string]: CsvColumn } -// export namespace Table { +// export namespace CsvTable { // export function empty(name: string): Table { // return { rowCount: 0, name, fieldNames: [], getColumn(name: string) { return void 0; } }; // }; diff --git a/src/mol-io/reader/csv/parser.ts b/src/mol-io/reader/csv/parser.ts index 66a7a9e1783c3107a9b618cbb0fbbaf0fdd4a6e2..d5bc68535344ff6c9d7aed63445b26e19a9220c7 100644 --- a/src/mol-io/reader/csv/parser.ts +++ b/src/mol-io/reader/csv/parser.ts @@ -248,24 +248,24 @@ function init(state: State) { } } -async function handleRecords(state: State): Promise<Data.Table> { +async function handleRecords(state: State): Promise<Data.CsvTable> { init(state) await readRecordsChunks(state) - const columns: Data.Columns = Object.create(null); + const columns: Data.CsvColumns = Object.create(null); for (let i = 0; i < state.columnCount; ++i) { columns[state.columnNames[i]] = Field(state.tokens[i], state.recordCount); } - return Data.Table(state.recordCount, state.columnNames, columns) + return Data.CsvTable(state.recordCount, state.columnNames, columns) } -async function parseInternal(data: string, ctx: RuntimeContext, opts: CsvOptions): Promise<Result<Data.File>> { +async function parseInternal(data: string, ctx: RuntimeContext, opts: CsvOptions): Promise<Result<Data.CsvFile>> { const state = State(data, ctx, opts); ctx.update({ message: 'Parsing...', current: 0, max: data.length }); const table = await handleRecords(state) - const result = Data.File(table) + const result = Data.CsvFile(table) return Result.success(result); } @@ -278,7 +278,7 @@ interface CsvOptions { export function parse(data: string, opts?: Partial<CsvOptions>) { const completeOpts = Object.assign({}, { quote: '"', comment: '#', delimiter: ',', noColumnNames: false }, opts) - return Task.create<Result<Data.File>>('Parse CSV', async ctx => { + return Task.create<Result<Data.CsvFile>>('Parse CSV', async ctx => { return await parseInternal(data, ctx, completeOpts); }); } diff --git a/src/mol-io/reader/gro/parser.ts b/src/mol-io/reader/gro/parser.ts index eee90135b4a588a565b25ee1f5354f150d987f3b..afd469111309c7d3ef6626844ddcd431e520a0ff 100644 --- a/src/mol-io/reader/gro/parser.ts +++ b/src/mol-io/reader/gro/parser.ts @@ -14,12 +14,12 @@ import { Task, RuntimeContext } from 'mol-task' interface State { tokenizer: Tokenizer, - header: Schema.Header, + header: Schema.GroHeader, numberOfAtoms: number, runtimeCtx: RuntimeContext } -function createEmptyHeader(): Schema.Header { +function createEmptyHeader(): Schema.GroHeader { return { title: '', timeInPs: 0, @@ -88,7 +88,7 @@ function handleNumberOfAtoms(state: State) { * position (in nm, x y z in 3 columns, each 8 positions with 3 decimal places) * velocity (in nm/ps (or km/s), x y z in 3 columns, each 8 positions with 4 decimal places) */ -async function handleAtoms(state: State): Promise<Schema.Atoms> { +async function handleAtoms(state: State): Promise<Schema.GroAtoms> { const { tokenizer, numberOfAtoms } = state; const lines = await Tokenizer.readLinesAsync(tokenizer, numberOfAtoms, state.runtimeCtx, 100000); @@ -137,11 +137,11 @@ function handleBoxVectors(state: State) { state.header.box = [+values[0], +values[1], +values[2]]; } -async function parseInternal(data: string, ctx: RuntimeContext): Promise<Result<Schema.File>> { +async function parseInternal(data: string, ctx: RuntimeContext): Promise<Result<Schema.GroFile>> { const tokenizer = Tokenizer(data); ctx.update({ message: 'Parsing...', current: 0, max: data.length }); - const structures: Schema.Structure[] = []; + const structures: Schema.GroStructure[] = []; while (tokenizer.position < data.length) { const state = State(tokenizer, ctx); handleTitleString(state); @@ -151,12 +151,12 @@ async function parseInternal(data: string, ctx: RuntimeContext): Promise<Result< structures.push({ header: state.header, atoms }); } - const result: Schema.File = { structures }; + const result: Schema.GroFile = { structures }; return Result.success(result); } export function parse(data: string) { - return Task.create<Result<Schema.File>>('Parse GRO', async ctx => { + return Task.create<Result<Schema.GroFile>>('Parse GRO', async ctx => { return await parseInternal(data, ctx); }); } diff --git a/src/mol-io/reader/gro/schema.d.ts b/src/mol-io/reader/gro/schema.d.ts index 9a0b1ca550718e16822320091b9500b5cef0597c..ddc84303d6295939188f377c7dde653f0243ba76 100644 --- a/src/mol-io/reader/gro/schema.d.ts +++ b/src/mol-io/reader/gro/schema.d.ts @@ -7,7 +7,7 @@ import { Column } from 'mol-data/db' -export interface Header { +export interface GroHeader { title: string, timeInPs: number, /** number of decimal places */ @@ -16,7 +16,7 @@ export interface Header { box: [number, number, number] } -export interface Atoms { +export interface GroAtoms { count: number, residueNumber: Column<number>, residueName: Column<string>, @@ -30,11 +30,11 @@ export interface Atoms { vz: Column<number> } -export interface Structure { - header: Readonly<Header>, - atoms: Readonly<Atoms> +export interface GroStructure { + header: Readonly<GroHeader>, + atoms: Readonly<GroAtoms> } -export interface File { - structures: Structure[] +export interface GroFile { + structures: GroStructure[] } \ No newline at end of file diff --git a/src/mol-io/reader/mol2/parser.ts b/src/mol-io/reader/mol2/parser.ts index 32126acb9050dbf33e490f2d9ffed2ca98a5f76c..297e1502618329594b0966427649ba7b014aaa87 100644 --- a/src/mol-io/reader/mol2/parser.ts +++ b/src/mol-io/reader/mol2/parser.ts @@ -22,11 +22,11 @@ const { skipWhitespace, eatValue, markLine, getTokenString, readLine } = Tokeniz interface State { tokenizer: Tokenizer, - molecule: Schema.Molecule, + molecule: Schema.Mol2Molecule, runtimeCtx: RuntimeContext } -function createEmptyMolecule(): Schema.Molecule { +function createEmptyMolecule(): Schema.Mol2Molecule { return { mol_name: '', num_atoms: 0, @@ -93,7 +93,7 @@ function isStatus_bit(aString: String): Boolean { return false; } -async function handleAtoms(state: State): Promise<Schema.Atoms> { +async function handleAtoms(state: State): Promise<Schema.Mol2Atoms> { const { tokenizer, molecule } = state; let hasSubst_id = false; let hasSubst_name = false; @@ -239,7 +239,7 @@ async function handleAtoms(state: State): Promise<Schema.Atoms> { return ret; } -async function handleBonds(state: State): Promise<Schema.Bonds> { +async function handleBonds(state: State): Promise<Schema.Mol2Bonds> { const { tokenizer, molecule } = state; let hasStatus_bit = false; @@ -324,11 +324,11 @@ async function handleBonds(state: State): Promise<Schema.Bonds> { return ret; } -async function parseInternal(data: string, ctx: RuntimeContext): Promise<Result<Schema.File>> { +async function parseInternal(data: string, ctx: RuntimeContext): Promise<Result<Schema.Mol2File>> { const tokenizer = Tokenizer(data); ctx.update({ message: 'Parsing...', current: 0, max: data.length }); - const structures: Schema.Structure[] = []; + const structures: Schema.Mol2Structure[] = []; while (tokenizer.position < data.length) { const state = State(tokenizer, ctx); handleMolecule(state); @@ -337,12 +337,12 @@ async function parseInternal(data: string, ctx: RuntimeContext): Promise<Result< structures.push({ molecule: state.molecule, atoms, bonds }); } - const result: Schema.File = { structures }; + const result: Schema.Mol2File = { structures }; return Result.success(result); } export function parse(data: string) { - return Task.create<Result<Schema.File>>('Parse MOL2', async ctx => { + return Task.create<Result<Schema.Mol2File>>('Parse MOL2', async ctx => { return await parseInternal(data, ctx); }); } diff --git a/src/mol-io/reader/mol2/schema.d.ts b/src/mol-io/reader/mol2/schema.d.ts index 374b52d0d8063e5b0995336fb43590b0c5beae14..21205a2673f9675340daa94c7a9d4b9ae3d158ab 100644 --- a/src/mol-io/reader/mol2/schema.d.ts +++ b/src/mol-io/reader/mol2/schema.d.ts @@ -14,7 +14,7 @@ import { Column } from 'mol-data/db' // // note that the format is not a fixed column format but white space separated -export interface Molecule { +export interface Mol2Molecule { mol_name: string num_atoms: number num_bonds: number @@ -27,7 +27,7 @@ export interface Molecule { mol_comment: string } -export interface Atoms { +export interface Mol2Atoms { count: number, atom_id: Column<number>, @@ -44,7 +44,7 @@ export interface Atoms { status_bit: Column<string> } -export interface Bonds { +export interface Mol2Bonds { count: number, bond_id: Column<number>, @@ -56,12 +56,12 @@ export interface Bonds { status_bits: Column<string> } -export interface Structure { - molecule: Readonly<Molecule>, - atoms: Readonly<Atoms>, - bonds: Readonly<Bonds> +export interface Mol2Structure { + molecule: Readonly<Mol2Molecule>, + atoms: Readonly<Mol2Atoms>, + bonds: Readonly<Mol2Bonds> } -export interface File { - structures: Structure[] +export interface Mol2File { + structures: Mol2Structure[] } \ No newline at end of file diff --git a/src/mol-model/structure/model/format.ts b/src/mol-model/structure/model/format.ts index d463c30d70af897445b2293aca0726a14d53119a..59f96c2a79fd06f442d5a439196d3fe54576a9f6 100644 --- a/src/mol-model/structure/model/format.ts +++ b/src/mol-model/structure/model/format.ts @@ -4,7 +4,7 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import { File as GroFile } from 'mol-io/reader/gro/schema' +import { GroFile as GroFile } from 'mol-io/reader/gro/schema' import { mmCIF_Database } from 'mol-io/reader/cif/schema/mmcif' type Format = diff --git a/src/mol-model/structure/model/formats/gro.ts b/src/mol-model/structure/model/formats/gro.ts index cb868ff96ee01927a9fb29f36640b62967e549bc..6e365bb3506446712b08ab3573ea92d8d66fc934 100644 --- a/src/mol-model/structure/model/formats/gro.ts +++ b/src/mol-model/structure/model/formats/gro.ts @@ -7,7 +7,7 @@ import UUID from 'mol-util/uuid' import { Column, Table } from 'mol-data/db' import { Interval, Segmentation } from 'mol-data/int' -import { Atoms } from 'mol-io/reader/gro/schema' +import { GroAtoms } from 'mol-io/reader/gro/schema' import Format from '../format' import Model from '../model' import * as Hierarchy from '../properties/hierarchy' @@ -24,7 +24,7 @@ import { Entities } from '../properties/common'; type HierarchyOffsets = { residues: ArrayLike<number>, chains: ArrayLike<number> } -function findHierarchyOffsets(atomsData: Atoms, bounds: Interval) { +function findHierarchyOffsets(atomsData: GroAtoms, bounds: Interval) { const start = Interval.start(bounds), end = Interval.end(bounds); const residues = [start], chains = [start]; @@ -44,7 +44,7 @@ function guessElementSymbol (value: string) { return ElementSymbol(guessElement(value)); } -function createHierarchyData(atomsData: Atoms, offsets: HierarchyOffsets): Hierarchy.Data { +function createHierarchyData(atomsData: GroAtoms, offsets: HierarchyOffsets): Hierarchy.Data { console.log(atomsData.atomName) const atoms = Table.ofColumns(Hierarchy.AtomsSchema, { type_symbol: Column.ofArray({ array: Column.mapToArray(atomsData.atomName, guessElementSymbol), schema: Column.Schema.Aliased<ElementSymbol>(Column.Schema.str) }), @@ -77,7 +77,7 @@ function createHierarchyData(atomsData: Atoms, offsets: HierarchyOffsets): Hiera return { atoms, residues, chains }; } -function getConformation(atoms: Atoms): AtomSiteConformation { +function getConformation(atoms: GroAtoms): AtomSiteConformation { return { id: UUID.create(), atomId: atoms.atomNumber, diff --git a/src/mol-util/download.ts b/src/mol-util/download.ts new file mode 100644 index 0000000000000000000000000000000000000000..2e820378ce72dafdb060d158a04fe0a0bdc8eec5 --- /dev/null +++ b/src/mol-util/download.ts @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +function openUrl (url: string) { + const opened = window.open(url, '_blank') + if (!opened) { + window.location.href = url + } +} + +export function download (data: Blob|string, downloadName = 'download') { + // using ideas from https://github.com/eligrey/FileSaver.js/blob/master/FileSaver.js + + if (!data) return + + const ua = window.navigator.userAgent + const isSafari = /Safari/i.test(ua) + const isChromeIos = /CriOS\/[\d]+/.test(ua) + + const a = document.createElement('a') + + function open (str: string) { + openUrl(isChromeIos ? str : str.replace(/^data:[^;]*;/, 'data:attachment/file;')) + } + + if (typeof navigator !== 'undefined' && navigator.msSaveOrOpenBlob) { + // native saveAs in IE 10+ + navigator.msSaveOrOpenBlob(data, downloadName) + } else if ((isSafari || isChromeIos) && FileReader) { + if (data instanceof Blob) { + // no downloading of blob urls in Safari + const reader = new FileReader() + reader.onloadend = () => open(reader.result) + reader.readAsDataURL(data) + } else { + open(data) + } + } else { + let objectUrlCreated = false + if (data instanceof Blob) { + data = URL.createObjectURL(data) + objectUrlCreated = true + } + + if ('download' in a) { + // download link available + a.style.display = 'hidden' + document.body.appendChild(a) + a.href = data + a.download = downloadName + a.target = '_blank' + a.click() + document.body.removeChild(a) + } else { + openUrl(data) + } + + if (objectUrlCreated) { + window.URL.revokeObjectURL(data) + } + } +} \ No newline at end of file diff --git a/src/mol-util/file-info.ts b/src/mol-util/file-info.ts new file mode 100644 index 0000000000000000000000000000000000000000..193c2091ed8a124ca56bc458183c5c6d67c3b4e5 --- /dev/null +++ b/src/mol-util/file-info.ts @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +/** A File or Blob object or a URL string */ +export type FileInput = File | Blob | string + +// TODO only support compressed files for which uncompression support is available??? +// TODO store globally with decompression plugins? +const compressedExtList = [ 'gz', 'zip' ] + +// TODO store globally with parser plugins? +const binaryExtList = [ 'bcif', 'ccp4', 'dcd' ] + +export interface FileInfo { + path: string + name: string + ext: string + base: string + dir: string + compressed: string | boolean + binary: boolean + protocol: string + query: string + src: FileInput +} + +export function getFileInfo (file: FileInput): FileInfo { + let path: string + let compressed: string|false + let protocol = '' + + if (file instanceof File) { + path = file.name + } else if (file instanceof Blob) { + path = '' + } else { + path = file + } + const queryIndex = path.lastIndexOf('?') + const query = queryIndex !== -1 ? path.substring(queryIndex) : '' + path = path.substring(0, queryIndex === -1 ? path.length : queryIndex) + + const name = path.replace(/^.*[\\/]/, '') + let base = name.substring(0, name.lastIndexOf('.')) + + const nameSplit = name.split('.') + let ext = nameSplit.length > 1 ? (nameSplit.pop() || '').toLowerCase() : '' + + const protocolMatch = path.match(/^(.+):\/\/(.+)$/) + if (protocolMatch) { + protocol = protocolMatch[ 1 ].toLowerCase() + path = protocolMatch[ 2 ] || '' + } + + const dir = path.substring(0, path.lastIndexOf('/') + 1) + + if (compressedExtList.includes(ext)) { + compressed = ext + const n = path.length - ext.length - 1 + ext = (path.substr(0, n).split('.').pop() || '').toLowerCase() + const m = base.length - ext.length - 1 + base = base.substr(0, m) + } else { + compressed = false + } + + const binary = binaryExtList.includes(ext) + + return { path, name, ext, base, dir, compressed, binary, protocol, query, src: file } +} \ No newline at end of file diff --git a/src/mol-util/id-factory.ts b/src/mol-util/id-factory.ts index 1f0a939fc28075255d36e854cfd948412731ea69..0de5654ff7194cf31ac15dd50e1d08d190e49cd9 100644 --- a/src/mol-util/id-factory.ts +++ b/src/mol-util/id-factory.ts @@ -4,7 +4,7 @@ * @author Alexander Rose <alexander.rose@weirdbyte.de> */ -export function idFactory() { - let _nextId = 0 +export function idFactory(firstId = 0) { + let _nextId = firstId return () => _nextId++ } \ No newline at end of file diff --git a/src/mol-util/index.ts b/src/mol-util/index.ts index 8cb116ef3a21bb50e51bffc14530f58647a1d0a2..94dcad374a72eb65156bd784042ca59f69a04443 100644 --- a/src/mol-util/index.ts +++ b/src/mol-util/index.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> * @author Alexander Rose <alexander.rose@weirdbyte.de> @@ -9,10 +9,16 @@ import BitFlags from './bit-flags' import StringBuilder from './string-builder' import UUID from './uuid' import Mask from './mask' +import { Progress } from 'mol-task'; export * from './value-cell' export { BitFlags, StringBuilder, UUID, Mask } +export function round(n: number, d: number) { + let f = Math.pow(10, d) + return Math.round(f * n) / f +} + export function arrayEqual<T>(arr1: T[], arr2: T[]) { const length = arr1.length if (length !== arr2.length) return false @@ -69,6 +75,111 @@ export function deepEqual(a: any, b: any) { return false } +const hasOwnProperty = Object.prototype.hasOwnProperty; + +export function shallowEqual<T>(a: T, b: T) { + if (!a) { + if (!b) return true; + return false; + } + if (!b) return false; + + let keys = Object.keys(a); + if (Object.keys(b).length !== keys.length) return false; + for (let k of keys) { + if (!hasOwnProperty.call(a, k) || (a as any)[k] !== (b as any)[k]) return false; + } + + return true; +} + export function defaults(value: any, defaultValue: any) { return value !== undefined ? value : defaultValue +} + +export function extend<S, T, U>(object: S, source: T, guard?: U): S & T & U { + let v: any; + + let s = <any>source; + let o = <any>object; + let g = <any>guard; + for (let k of Object.keys(source)) { + v = s[k]; + if (v !== void 0) o[k] = v; + else if (guard) o[k] = g[k]; + } + + if (guard) { + for (let k of Object.keys(guard)) { + v = o[k]; + if (v === void 0) o[k] = g[k]; + } + } + + return <any>object; +} + +export function shallowClone<T>(o: T): T { + return extend({}, o) as T; +} + +function _assign<T>(target: T): T { + for (let s = 1; s < arguments.length; s++) { + let from = arguments[s]; + for (let key of Object.keys(from)) { + if (hasOwnProperty.call(from, key)) { + (target as any)[key] = from[key]; + } + } + } + return target; +} + +export declare function _assignType<T>(o: T, ...from: any[]): T; +export const assign: (<T>(o: T, ...from: any[]) => T) = (Object as any).assign || _assign; + +function _shallowMerge1<T>(source: T, update: T) { + let changed = false; + for (let k of Object.keys(update)) { + if (!hasOwnProperty.call(update, k)) continue; + + if ((update as any)[k] !== (source as any)[k]) { + changed = true; + break; + } + } + + if (!changed) return source; + return assign(shallowClone(source), update); +} + +function _shallowMerge<T>(source: T) { + let ret = source; + + for (let s = 1; s < arguments.length; s++) { + if (!arguments[s]) continue; + ret = _shallowMerge1(source, arguments[s]); + if (ret !== source) { + for (let i = s + 1; i < arguments.length; i++) { + ret = assign(ret, arguments[i]); + } + break; + } + } + return ret; +} + +export const merge: (<T>(source: T, ...rest: Partial<T>[]) => T)= _shallowMerge; + +function padTime(n: number) { return (n < 10 ? '0' : '') + n } +export function formatTime(d: Date) { + const h = d.getHours(), m = d.getMinutes(), s = d.getSeconds(); + return `${h}:${padTime(m)}:${padTime(s)}`; +} + +export function formatProgress(p: Progress) { + const tp = p.root.progress + if (tp.isIndeterminate) return tp.message; + const x = (100 * tp.current / tp.max).toFixed(2); + return `${tp.message} ${x}%`; } \ No newline at end of file diff --git a/src/mol-util/parse-unit.ts b/src/mol-util/parse-unit.ts index 4071eec017d1810712f2a0e77438908337f97a36..e0e0652b1ef34104ea50bc7037e621c91aaa739b 100644 --- a/src/mol-util/parse-unit.ts +++ b/src/mol-util/parse-unit.ts @@ -11,6 +11,7 @@ const reUnit = /[\d.\-\+]*\s*(.*)/ +/** Parsing value of, for example, CSS unit strings */ export default function parseUnit(str: string, out: [number, string] = [ 0, '' ]) { str = String(str) const num = parseFloat(str) diff --git a/src/mol-util/performance-monitor.ts b/src/mol-util/performance-monitor.ts new file mode 100644 index 0000000000000000000000000000000000000000..614b19a29930bb8eda3424b5e7a5f731acb8493c --- /dev/null +++ b/src/mol-util/performance-monitor.ts @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * Adapted from LiteMol + * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info. + */ + +import { now } from 'mol-task/util/now' + +export class PerformanceMonitor { + private starts = new Map<string, number>(); + private ends = new Map<string, number>(); + + static currentTime() { + return now(); + } + + start(name: string) { + this.starts.set(name, now()); + } + + end(name: string) { + this.ends.set(name, now()); + } + + static format(t: number) { + if (isNaN(t)) return 'n/a'; + + let h = Math.floor(t / (60 * 60 * 1000)), + m = Math.floor(t / (60 * 1000) % 60), + s = Math.floor(t / 1000 % 60), + ms = Math.floor(t % 1000).toString(); + + while (ms.length < 3) ms = '0' + ms; + + if (h > 0) return `${h}h${m}m${s}.${ms}s`; + if (m > 0) return `${m}m${s}.${ms}s`; + if (s > 0) return `${s}.${ms}s`; + return `${t.toFixed(0)}ms`; + } + + formatTime(name: string) { + return PerformanceMonitor.format(this.time(name)); + } + + formatTimeSum(...names: string[]) { + return PerformanceMonitor.format(this.timeSum(...names)); + } + + /** Returns the time in milliseconds and removes them from the cache. */ + time(name: string): number { + let start = this.starts.get(name)!, end = this.ends.get(name)!; + + this.starts.delete(name); + this.ends.delete(name); + + return end - start; + } + + timeSum(...names: string[]) { + let t = 0; + for (let m of names.map(n => this.ends.get(n)! - this.starts.get(n)!)) t += m; + return t; + } +} \ No newline at end of file diff --git a/src/mol-util/read.ts b/src/mol-util/read.ts new file mode 100644 index 0000000000000000000000000000000000000000..b88abd9ebfb5587d9800b7eea9af00d0ce832401 --- /dev/null +++ b/src/mol-util/read.ts @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +export const readFileAs = (file: File, isBinary = false) => { + const fileReader = new FileReader() + return new Promise<string | Uint8Array>((resolve, reject) => { + fileReader.onerror = () => { + fileReader.abort() + reject(new DOMException('Error parsing file.')) + } + fileReader.onload = () => { + resolve(isBinary ? new Uint8Array(fileReader.result) : fileReader.result) + } + if (isBinary) { + fileReader.readAsArrayBuffer(file) + } else { + fileReader.readAsText(file) + } + }) +} + +export function readFileAsText(file: File) { + return readFileAs(file, false) as Promise<string> +} + +export function readFileAsBuffer(file: File) { + return readFileAs(file, true) as Promise<Uint8Array> +} + +export async function readUrlAs(url: string, isBinary: boolean) { + const response = await fetch(url); + return isBinary ? new Uint8Array(await response.arrayBuffer()) : await response.text(); +} + +export function readUrlAsText(url: string) { + return readUrlAs(url, false) as Promise<string> +} + +export function readUrlAsBuffer(url: string) { + return readUrlAs(url, true) as Promise<Uint8Array> +} \ No newline at end of file diff --git a/src/mol-view/stage.ts b/src/mol-view/stage.ts new file mode 100644 index 0000000000000000000000000000000000000000..fbc3556642efd844813f690223c385097431553d --- /dev/null +++ b/src/mol-view/stage.ts @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import Viewer from 'mol-view/viewer' +import { StateContext } from './state/context'; +import { Progress } from 'mol-task'; +import { MmcifUrlToSpacefill } from './state/transform'; +import { UrlEntity } from './state/entity'; +import { SpacefillProps } from 'mol-geo/representation/structure/spacefill'; + +// export const ColorTheme = { +// 'atom-index': {}, +// 'chain-id': {}, +// 'element-symbol': {}, +// 'instance-index': {}, +// 'uniform': {} +// } +// export type ColorTheme = keyof typeof ColorTheme + +const spacefillProps: SpacefillProps = { + doubleSided: true, + detail: 2, + colorTheme: { name: 'element-symbol' } +} + +export class Stage { + viewer: Viewer + ctx = new StateContext(Progress.format) + + constructor() { + + } + + async initRenderer (canvas: HTMLCanvasElement, container: HTMLDivElement) { + this.viewer = Viewer.create(canvas, container) + this.viewer.animate() + this.ctx.viewer = this.viewer + this.loadPdbid('1crn') + } + + async loadPdbid (pdbid: string) { + const urlEntity = UrlEntity.ofUrl(this.ctx, `https://files.rcsb.org/download/${pdbid}.cif`) + MmcifUrlToSpacefill.apply(this.ctx, urlEntity, spacefillProps) + } + + dispose () { + // TODO + } +} \ No newline at end of file diff --git a/src/mol-view/state/context.ts b/src/mol-view/state/context.ts new file mode 100644 index 0000000000000000000000000000000000000000..2893a5e37fa78d807888cee2cf4effee7a2efcf6 --- /dev/null +++ b/src/mol-view/state/context.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { BehaviorSubject } from 'rxjs'; +import { UUID } from 'mol-util' +import { AnyEntity } from './entity'; +import Viewer from '../viewer'; +import { Progress } from 'mol-task'; + +// TODO +export type StateTree = {} + +export class StateContext { + id = UUID.create() + change = new BehaviorSubject(0) + + tree: StateTree = {} + entities: Set<AnyEntity> = new Set() + + viewer: Viewer + + constructor(readonly log: (p: Progress) => void) { + + } +} diff --git a/src/mol-view/state/entity.ts b/src/mol-view/state/entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..b77e5b82215d3f1823fd4821b379ca0561feb945 --- /dev/null +++ b/src/mol-view/state/entity.ts @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { readFileAs, readUrlAs } from 'mol-util/read' +import { idFactory } from 'mol-util/id-factory' +import { StateContext } from './context'; +import { getFileInfo } from 'mol-util/file-info'; +import { CifFile } from 'mol-io/reader/cif'; +import { mmCIF_Database } from 'mol-io/reader/cif/schema/mmcif'; +import { Model, Structure } from 'mol-model/structure'; +import { StructureRepresentation } from 'mol-geo/representation/structure'; +import { SpacefillProps } from 'mol-geo/representation/structure/spacefill'; + +const getNextId = idFactory(1) + +export interface StateEntity<T, K extends string> { + id: number + kind: K + value: T +} +export namespace StateEntity { + export function create<T, K extends string>(ctx: StateContext, kind: K, value: T): StateEntity<T, K> { + const entity = { id: getNextId(), kind, value } + ctx.entities.add(entity) + ctx.change.next(ctx.change.getValue() + 1) + return entity + } +} + +export type AnyEntity = StateEntity<any, any> + +export const RootEntity: StateEntity<null, 'root'> = { id: 0, kind: 'root', value: null } + +export interface UrlProps { + url: string + name: string + type: string + getData: () => Promise<string | Uint8Array> +} + +export type UrlEntity = StateEntity<UrlProps, 'url'> +export namespace UrlEntity { + export function ofUrl(ctx: StateContext, url: string, isBinary?: boolean): UrlEntity { + const { name, ext: type, compressed, binary } = getFileInfo(url) + return StateEntity.create(ctx, 'url', { + url, name, type, + getData: () => readUrlAs(url, isBinary || !!compressed || binary) + }) + } +} + +export interface FileProps { + name: string + type: string + getData: () => Promise<string | Uint8Array> +} + +export type FileEntity = StateEntity<FileProps, 'file'> +export namespace FileEntity { + export function ofFile(ctx: StateContext, file: File, isBinary?: boolean): FileEntity { + const { name, ext: type, compressed, binary } = getFileInfo(file) + return StateEntity.create(ctx, 'file', { + name, type, + getData: () => readFileAs(file, isBinary || !!compressed || binary) + }) + } +} + +export interface DataProps { + type: string + data: string | Uint8Array +} + +export type DataEntity = StateEntity<DataProps, 'data'> +export namespace DataEntity { + export function ofData<T>(ctx: StateContext, data: string | Uint8Array, type: string): DataEntity { + return StateEntity.create(ctx, 'data', { + type, + data + }) + } +} + +export type CifEntity = StateEntity<CifFile, 'cif'> +export namespace CifEntity { + export function ofCifFile(ctx: StateContext, file: CifFile): CifEntity { + return StateEntity.create(ctx, 'cif', file) + } +} + +export type MmcifEntity = StateEntity<mmCIF_Database, 'mmcif'> +export namespace MmcifEntity { + export function ofMmcifDb(ctx: StateContext, db: mmCIF_Database): MmcifEntity { + return StateEntity.create(ctx, 'mmcif', db) + } +} + +export type ModelEntity = StateEntity<ReadonlyArray<Model>, 'model'> +export namespace ModelEntity { + export function ofModels(ctx: StateContext, models: ReadonlyArray<Model>): ModelEntity { + return StateEntity.create(ctx, 'model', models) + } +} + +export type StructureEntity = StateEntity<Structure, 'structure'> +export namespace StructureEntity { + export function ofStructure(ctx: StateContext, structure: Structure): StructureEntity { + return StateEntity.create(ctx, 'structure', structure) + } +} + +export type SpacefillEntity = StateEntity<StructureRepresentation<SpacefillProps>, 'spacefill'> +export namespace SpacefillEntity { + export function ofRepr(ctx: StateContext, repr: StructureRepresentation<SpacefillProps>): SpacefillEntity { + return StateEntity.create(ctx, 'spacefill', repr ) + } +} \ No newline at end of file diff --git a/src/mol-view/state/transform.ts b/src/mol-view/state/transform.ts new file mode 100644 index 0000000000000000000000000000000000000000..fe02007907f7b1dbbf84790599ec9c25b7e5edaa --- /dev/null +++ b/src/mol-view/state/transform.ts @@ -0,0 +1,152 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import CIF from 'mol-io/reader/cif' +import { FileEntity, DataEntity, UrlEntity, CifEntity, MmcifEntity, ModelEntity, StructureEntity, SpacefillEntity, AnyEntity } from './entity'; +import { Run } from 'mol-task'; +import { Model, Structure, Symmetry } from 'mol-model/structure'; + +import { StateContext } from './context'; +import Spacefill, { SpacefillProps } from 'mol-geo/representation/structure/spacefill'; +import { StructureRepresentation } from 'mol-geo/representation/structure'; + +type transformer<I extends AnyEntity, O extends AnyEntity, P extends {}> = (ctx: StateContext, inputEntity: I, props?: P) => Promise<O> + +export interface StateTransform<I extends AnyEntity, O extends AnyEntity, P extends {}> { + inputKind: I['kind'] + outputKind: O['kind'] + kind: string + apply: transformer<I, O, P> +} + +export namespace StateTransform { + export function create<I extends AnyEntity, O extends AnyEntity, P extends {}>(inputKind: I['kind'], outputKind: O['kind'], kind: string, transformer: transformer<I, O, P>) { + return { inputKind, outputKind, kind, apply: transformer } + } +} + +export type AnyTransform = StateTransform<AnyEntity, AnyEntity, {}> + +export type UrlToData = StateTransform<UrlEntity, DataEntity, {}> +export const UrlToData: UrlToData = StateTransform.create('url', 'data', 'url-to-data', + async function (ctx: StateContext, urlEntity: UrlEntity) { + return DataEntity.ofData(ctx, await urlEntity.value.getData(), urlEntity.value.type) + }) + +export type FileToData = StateTransform<FileEntity, DataEntity, {}> +export const FileToData: FileToData = StateTransform.create('file', 'data', 'file-to-data', + async function (ctx: StateContext, fileEntity: FileEntity) { + return DataEntity.ofData(ctx, await fileEntity.value.getData(), fileEntity.value.type) + }) + +export type DataToCif = StateTransform<DataEntity, CifEntity, {}> +export const DataToCif: DataToCif = StateTransform.create('data', 'cif', 'data-to-cif', + async function (ctx: StateContext, dataEntity: DataEntity) { + const comp = CIF.parse(dataEntity.value.data) + const parsed = await Run(comp, ctx.log) + if (parsed.isError) throw parsed + return CifEntity.ofCifFile(ctx, parsed.result) + }) + +export type CifToMmcif = StateTransform<CifEntity, MmcifEntity, {}> +export const CifToMmcif: CifToMmcif = StateTransform.create('cif', 'mmcif', 'cif-to-mmcif', + async function (ctx: StateContext, cifEntity: CifEntity) { + return MmcifEntity.ofMmcifDb(ctx, CIF.schema.mmCIF(cifEntity.value.blocks[0])) + }) + +export type MmcifToModel = StateTransform<MmcifEntity, ModelEntity, {}> +export const MmcifToModel: MmcifToModel = StateTransform.create('mmcif', 'model', 'mmcif-to-model', + async function (ctx: StateContext, mmcifEntity: MmcifEntity) { + return ModelEntity.ofModels(ctx, Model.create({ kind: 'mmCIF', data: mmcifEntity.value })) + }) + +export interface StructureProps { + assembly?: string +} + +export type ModelToStructure = StateTransform<ModelEntity, StructureEntity, StructureProps> +export const ModelToStructure: ModelToStructure = StateTransform.create('model', 'structure', 'model-to-structure', + async function (ctx: StateContext, modelEntity: ModelEntity, props: StructureProps = {}) { + const model = modelEntity.value[0] + const assembly = props.assembly + let structure: Structure + const assemblies = model.symmetry.assemblies + if (assemblies.length) { + structure = await Run(Symmetry.buildAssembly(Structure.ofModel(model), assembly || '1'), ctx.log) + } else { + structure = Structure.ofModel(model) + } + return StructureEntity.ofStructure(ctx, structure) + }) + +export type StructureToSpacefill = StateTransform<StructureEntity, SpacefillEntity, SpacefillProps> +export const StructureToSpacefill: StructureToSpacefill = StateTransform.create('structure', 'spacefill', 'structure-to-spacefill', + async function (ctx: StateContext, structureEntity: StructureEntity, props: SpacefillProps = {}) { + const spacefillRepr = StructureRepresentation(Spacefill) + await Run(spacefillRepr.create(structureEntity.value, props), ctx.log) + ctx.viewer.add(spacefillRepr) + ctx.viewer.requestDraw() + console.log(ctx.viewer.stats) + return SpacefillEntity.ofRepr(ctx, spacefillRepr) + }) + +export type SpacefillUpdate = StateTransform<SpacefillEntity, SpacefillEntity, SpacefillProps> +export const SpacefillUpdate: SpacefillUpdate = StateTransform.create('spacefill', 'spacefill', 'spacefill-update', + async function (ctx: StateContext, spacefillEntity: SpacefillEntity, props: SpacefillProps = {}) { + console.log('fopbar') + const spacefillRepr = spacefillEntity.value + await Run(spacefillRepr.update(props), ctx.log) + ctx.viewer.add(spacefillRepr) + ctx.viewer.update() + ctx.viewer.requestDraw() + console.log(ctx.viewer.stats) + return spacefillEntity + }) + +// composed + +export type MmcifUrlToModel = StateTransform<UrlEntity, ModelEntity, {}> +export const MmcifUrlToModel: MmcifUrlToModel = StateTransform.create('url', 'model', 'url-to-model', + async function (ctx: StateContext, urlEntity: UrlEntity) { + const dataEntity = await UrlToData.apply(ctx, urlEntity) + return DataToModel.apply(ctx, dataEntity) + }) + +export type MmcifFileToModel = StateTransform<FileEntity, ModelEntity, {}> +export const MmcifFileToModel: MmcifFileToModel = StateTransform.create('file', 'model', 'file-to-model', + async function (ctx: StateContext, fileEntity: FileEntity) { + const dataEntity = await FileToData.apply(ctx, fileEntity) + return DataToModel.apply(ctx, dataEntity) + }) + +export type DataToModel = StateTransform<DataEntity, ModelEntity, {}> +export const DataToModel: DataToModel = StateTransform.create('data', 'model', 'data-to-model', + async function getModelFromData(ctx: StateContext, dataEntity: DataEntity) { + const cifEntity = await DataToCif.apply(ctx, dataEntity) + const mmcifEntity = await CifToMmcif.apply(ctx, cifEntity) + return MmcifToModel.apply(ctx, mmcifEntity) + }) + +export type ModelToSpacefill = StateTransform<ModelEntity, SpacefillEntity, SpacefillProps> +export const ModelToSpacefill: ModelToSpacefill = StateTransform.create('model', 'spacefill', 'model-to-spacefill', + async function (ctx: StateContext, modelEntity: ModelEntity, props: SpacefillProps = {}) { + const structureEntity = await ModelToStructure.apply(ctx, modelEntity) + return StructureToSpacefill.apply(ctx, structureEntity, props) + }) + +export type MmcifUrlToSpacefill = StateTransform<UrlEntity, SpacefillEntity, SpacefillProps> +export const MmcifUrlToSpacefill: MmcifUrlToSpacefill = StateTransform.create('url', 'spacefill', 'url-to-spacefill', + async function (ctx: StateContext, urlEntity: UrlEntity, props: SpacefillProps = {}) { + const modelEntity = await MmcifUrlToModel.apply(ctx, urlEntity) + return ModelToSpacefill.apply(ctx, modelEntity, props) + }) + +export type MmcifFileToSpacefill = StateTransform<FileEntity, SpacefillEntity, SpacefillProps> +export const MmcifFileToSpacefill: MmcifFileToSpacefill = StateTransform.create('file', 'spacefill', 'file-to-spacefill', + async function (ctx: StateContext, fileEntity: FileEntity, props: SpacefillProps = {}) { + const modelEntity = await MmcifFileToModel.apply(ctx, fileEntity) + return ModelToSpacefill.apply(ctx, modelEntity, props) + }) \ No newline at end of file diff --git a/src/mol-view/util.ts b/src/mol-view/util.ts index 294907ad30cdd987b883f7b95848829de48a611e..cf9343d2450dfca85f2045008d054f939240c0eb 100644 --- a/src/mol-view/util.ts +++ b/src/mol-view/util.ts @@ -4,6 +4,31 @@ * @author Alexander Rose <alexander.rose@weirdbyte.de> */ +import CIF from 'mol-io/reader/cif' +import { Run, Progress } from 'mol-task' +import { VolumeData, parseDensityServerData } from 'mol-model/volume' +import { DensityServer_Data_Database } from 'mol-io/reader/cif/schema/density-server'; + +export async function downloadCif(url: string, isBinary: boolean) { + const data = await fetch(url); + return parseCif(isBinary ? new Uint8Array(await data.arrayBuffer()) : await data.text()); +} + +export async function parseCif(data: string|Uint8Array) { + const comp = CIF.parse(data) + const parsed = await Run(comp, Progress.format); + if (parsed.isError) throw parsed; + return parsed.result +} + +export type Volume = { source: DensityServer_Data_Database, volume: VolumeData } + +export async function getVolumeFromEmdId(emdid: string): Promise<Volume> { + const cif = await downloadCif(`https://webchem.ncbr.muni.cz/DensityServer/em/emd-${emdid}/cell?detail=4`, true) + const data = CIF.schema.densityServer(cif.blocks[1]) + return { source: data, volume: await Run(parseDensityServerData(data)) } +} + export function resizeCanvas (canvas: HTMLCanvasElement, container: Element) { let w = window.innerWidth let h = window.innerHeight diff --git a/src/mol-view/viewer.ts b/src/mol-view/viewer.ts index 4e8d9c26230f39a4407a14e79c137811f6bb9cb2..c4384837d32afff409180b28d77f5267fe8091f4 100644 --- a/src/mol-view/viewer.ts +++ b/src/mol-view/viewer.ts @@ -4,6 +4,8 @@ * @author Alexander Rose <alexander.rose@weirdbyte.de> */ +import { BehaviorSubject } from 'rxjs'; + import { Vec3, Mat4, EPSILON } from 'mol-math/linear-algebra' import InputObserver from 'mol-util/input/input-observer' import * as SetUtils from 'mol-util/set' @@ -29,8 +31,11 @@ interface Viewer { draw: (force?: boolean) => void requestDraw: () => void animate: () => void + reprCount: BehaviorSubject<number> handleResize: () => void + resetCamera: () => void + downloadScreenshot: () => void stats: RendererStats dispose: () => void @@ -50,6 +55,7 @@ function getWebGLContext(canvas: HTMLCanvasElement, contextAttributes?: WebGLCon namespace Viewer { export function create(canvas: HTMLCanvasElement, container: Element): Viewer { const reprMap = new Map<Representation<any>, Set<RenderObject>>() + const reprCount = new BehaviorSubject(0) const input = InputObserver.create(canvas) input.resize.subscribe(handleResize) @@ -124,10 +130,15 @@ namespace Viewer { repr.renderObjects.forEach(o => renderer.add(o)) } reprMap.set(repr, newRO) + reprCount.next(reprMap.size) }, remove: (repr: Representation<any>) => { const renderObjectSet = reprMap.get(repr) - if (renderObjectSet) renderObjectSet.forEach(o => renderer.remove(o)) + if (renderObjectSet) { + renderObjectSet.forEach(o => renderer.remove(o)) + reprMap.delete(repr) + reprCount.next(reprMap.size) + } }, update: () => renderer.update(), clear: () => { @@ -140,6 +151,13 @@ namespace Viewer { animate, handleResize, + resetCamera: () => { + // TODO + }, + downloadScreenshot: () => { + // TODO + }, + reprCount, get stats() { return renderer.stats diff --git a/tsconfig.json b/tsconfig.json index 7e025fd23915e407f1caf32627bad388cd7817cd..eedd1592ae0767790c69a6dbeece0b25e46483fd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,6 +14,7 @@ "outDir": "build/node_modules", "baseUrl": "src", "paths": { + "mol-app": ["./mol-app"], "mol-data": ["./mol-data", "./mol-data/index.ts"], "mol-geo": ["./mol-geo"], "mol-gl": ["./mol-gl"], diff --git a/web/render-test/index.html b/web/render-test/index.html deleted file mode 100644 index 2e144b29791e36ed9926e7d0785e4ffae563cc7d..0000000000000000000000000000000000000000 --- a/web/render-test/index.html +++ /dev/null @@ -1,12 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> - <head> - <meta charset="utf-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=1" /> - <title>Mol* Render Test</title> - </head> - <body> - <div id="app"></div> - <script src="./index.js"></script> - </body> -</html> \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 8ace2768dd73a4664e387ad55298b3f5a184c0ee..0a6bc7e12386ac49423d6771d2b950530348dc67 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,17 +1,34 @@ const path = require('path'); const ExtraWatchWebpackPlugin = require('extra-watch-webpack-plugin'); +const ExtractTextPlugin = require('extract-text-webpack-plugin'); module.exports = { module: { rules: [ { loader: 'raw-loader', test: /\.(glsl|frag|vert)$/, - include: [ path.resolve(__dirname, "build/node_modules/") ], + include: [ path.resolve(__dirname, 'build/node_modules/') ], }, { loader: 'glslify-loader', test: /\.(glsl|frag|vert)$/, - include: [ path.resolve(__dirname, "build/node_modules/") ] + include: [ path.resolve(__dirname, 'build/node_modules/') ] + }, + + { + loader: 'file-loader', + test: /\.(woff2?|ttf|otf|eot|svg|html)$/, + include: [ path.resolve(__dirname, 'build/node_modules/') ], + options: { + name: '[name].[ext]' + } + }, + { + test:/\.(s*)css$/, + use: ExtractTextPlugin.extract({ + fallback:'style-loader', + use:['css-loader', 'resolve-url-loader', 'sass-loader'], + }) } ] }, @@ -20,8 +37,11 @@ module.exports = { files: [ './build/node_modules/**/*.vert', './build/node_modules/**/*.frag', - './build/node_modules/**/*.glsl' + './build/node_modules/**/*.glsl', + './build/node_modules/**/*.scss', + './build/node_modules/**/*.html' ], }), + new ExtractTextPlugin({ filename:'app.css' }), ], } \ No newline at end of file