diff --git a/src/apps/rednatco/controls.tsx b/src/apps/rednatco/controls.tsx index f2f8977c3de0010f184ee27ae9a228c70bf0ecb8..5c720989e87a9eb47959b79fdc2775e699001344 100644 --- a/src/apps/rednatco/controls.tsx +++ b/src/apps/rednatco/controls.tsx @@ -1,18 +1,29 @@ import React from 'react'; -import { numDecimals, stof } from './util'; +import { fuzzyCmp, numDecimals, reduceDecimals, stof } from './util'; const Zero = '0'.charCodeAt(0); const Nine = '9'.charCodeAt(0); const Minus = '-'.charCodeAt(0); const Period = '.'.charCodeAt(0); -function maybeNumeric(s: string) { +function isAllowedNumericInput(s: string, maxNumDecimals?: number) { + let havePeriod = false; for (let idx = 0; idx < s.length; idx++) { const cc = s.charCodeAt(idx); - if (!((cc >= Zero && cc <= Nine) || cc === Minus || cc === Period)) + if (cc === Period) { + if (havePeriod) + return false; + else + havePeriod = true; + } else if (cc === Minus) { + if (idx > 0) + return false; + } else if (!(cc >= Zero && cc <= Nine)) return false; } + if (maxNumDecimals !== undefined) + return numDecimals(s) <= maxNumDecimals; return true; } @@ -109,9 +120,7 @@ export class SpinBox extends React.Component<SpinBox.Props, SpinBoxState> { super(props); this.state = { - displayedValue: this.props.formatter - ? this.props.formatter(this.props.value.toString()) - : this.props.value.toString(), + displayedValue: this.formatValue(this.props.value), }; } @@ -129,7 +138,21 @@ export class SpinBox extends React.Component<SpinBox.Props, SpinBoxState> { return; const nv = n - this.props.step; if (nv >= this.props.min) - this.props.onChange(n); + this.notifyChange(nv); + } + + private formatValue(n: number, padToDecimals?: number) { + if (this.props.maxNumDecimals !== undefined) { + if (padToDecimals !== undefined) { + const padded = n.toFixed(padToDecimals); + if (fuzzyCmp(n, stof(padded)!)) + return padded; + } + + const fn = n.toFixed(this.props.maxNumDecimals); + return reduceDecimals(fn); + } + return n.toString(); } private increase() { @@ -137,11 +160,11 @@ export class SpinBox extends React.Component<SpinBox.Props, SpinBoxState> { if (n === undefined) return; const nv = n + this.props.step; - if (nv >= this.props.min) - this.props.onChange(n); + if (nv <= this.props.max) + this.notifyChange(nv); } - private handleChange(value: string) { + private maybeNotifyUpdate(value: string) { if (this.props.maxNumDecimals !== undefined && numDecimals(value) > this.props.maxNumDecimals) return; @@ -149,23 +172,33 @@ export class SpinBox extends React.Component<SpinBox.Props, SpinBoxState> { if ( n !== undefined && - n !== this.props.value && - (this.props.min <= n && n <= this.props.max) && - value[value.length - 1] !== '.' + !fuzzyCmp(n, this.props.value) && + (this.props.min <= n && n <= this.props.max) ) { - this.props.onChange(n); - } else { - if (maybeNumeric(value)) - this.setState({ ...this.state, displayedValue: value }); + this.notifyChange(n); } } - componentDidUpdate(prevProps: SpinBox.Props) { - if (this.props !== prevProps) { - const displayedValue = this.props.formatter - ? this.props.formatter(this.props.value.toString()) - : this.props.value.toString(); - this.setState({ ...this.state, displayedValue }); + private notifyChange(n: number) { + if (this.props.maxNumDecimals !== undefined) { + const Factor = Math.pow(10, this.props.maxNumDecimals); + this.props.onChange(Math.round(n * Factor) / Factor); + } else + this.props.onChange(n); + } + + componentDidUpdate(prevProps: SpinBox.Props, prevState: SpinBoxState) { + if (this.props !== prevProps && this.props.value !== prevProps.value) { + const padToDecimals = stof(this.state.displayedValue) !== undefined ? numDecimals(this.state.displayedValue) : void 0; + const displayedValue = this.formatValue(this.props.value, padToDecimals); + if ( + displayedValue !== this.state.displayedValue && + displayedValue !== this.state.displayedValue.substring(0, this.state.displayedValue.length - 1) + ) + this.setState({ ...this.state, displayedValue }); + } else { + if (this.state.displayedValue !== prevState.displayedValue) + this.maybeNotifyUpdate(this.state.displayedValue); } } @@ -176,7 +209,11 @@ export class SpinBox extends React.Component<SpinBox.Props, SpinBoxState> { type='text' className={this.props.disabled ? this.clsDisabled() : this.clsEnabled()} value={this.state.displayedValue} - onChange={evt => this.handleChange(evt.currentTarget.value)} + onChange={evt => { + const v = evt.currentTarget.value; + if (isAllowedNumericInput(v, this.props.maxNumDecimals)) + this.setState({ ...this.state, displayedValue: v }); + }} onWheel={evt => { evt.stopPropagation(); const n = stof(this.state.displayedValue); @@ -185,11 +222,11 @@ export class SpinBox extends React.Component<SpinBox.Props, SpinBoxState> { if (evt.deltaY < 0) { const nv = n + this.props.step; if (nv <= this.props.max) - this.props.onChange(nv); + this.notifyChange(nv); } else if (evt.deltaY > 0) { const nv = n - this.props.step; if (nv >= this.props.min) - this.props.onChange(nv); + this.notifyChange(nv); } }} /> @@ -209,7 +246,7 @@ export class SpinBox extends React.Component<SpinBox.Props, SpinBoxState> { } export namespace SpinBox { export interface Formatter { - (v: string): string; + (v: number): string; } export interface OnChange { @@ -226,7 +263,6 @@ export namespace SpinBox { disabled?: boolean; className?: string; classNameDisabled?: string; - formatter?: Formatter; maxNumDecimals?: number; } } diff --git a/src/apps/rednatco/density-map-controls.tsx b/src/apps/rednatco/density-map-controls.tsx index ef6a10ba8eeb794abd2f5bd124427e2d9652ae96..750a3066e1ec638b1d54ddd9e1262325f734a8d7 100644 --- a/src/apps/rednatco/density-map-controls.tsx +++ b/src/apps/rednatco/density-map-controls.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { CollapsibleVertical, RangeSlider, SpinBox, ToggleButton } from './controls'; -import { isoToFixed, numDecimals, stof } from './util'; +import { isoToFixed } from './util'; export class DensityMapControls extends React.Component<DensityMapControls.Props> { render() { @@ -65,28 +65,22 @@ export class DensityMapControls extends React.Component<DensityMapControls.Props <div className='rmsp-control-item'> <RangeSlider min={0} - max={1} - step={0.1} - value={(1.0 - this.props.alpha)} - onChange={(v) => this.props.changeAlpha(1.0 - v!)} + max={100} + step={1} + value={(1.0 - this.props.alpha) * 100} + onChange={(n) => this.props.changeAlpha(1.0 - (n! / 100))} /> </div> <div className='rmsp-control-item'> <div style={{ display: 'grid', gridTemplateColumns: '4em 1fr' }}> <SpinBox min={0} - max={1} - step={0.1} - maxNumDecimals={1} - value={(1.0 - this.props.alpha)} - onChange={(n) => this.props.changeAlpha(1.0 - n)} + max={100} + step={1} + maxNumDecimals={0} + value={(1.0 - this.props.alpha) * 100} + onChange={(n) => this.props.changeAlpha(1.0 - (n / 100))} pathPrefix='' - formatter={v => { - const n = stof(v); - if (n !== undefined && numDecimals(v) > 1) - return n.toFixed(1); - return v; - }} /> </div> </div> diff --git a/src/apps/rednatco/util.ts b/src/apps/rednatco/util.ts index c943be480fc90fd766fae94bcfd8d4ff16d0d2e5..d16a9cff9a1d4fe024735458e4122508f259f35a 100644 --- a/src/apps/rednatco/util.ts +++ b/src/apps/rednatco/util.ts @@ -1,6 +1,21 @@ import { Color } from '../../mol-util/color'; import { parseInt as parseIntMS, parseFloat as parseFloatMS } from '../../mol-io/reader/common/text/number-parser'; +const Zero = '0'.charCodeAt(0); +const Period = '.'.charCodeAt(0); + +export function clampDecimals(s: string, maxNumDecimals: number) { + const idx = s.lastIndexOf('.'); + if (idx < 0) + return s; + return maxNumDecimals === 0 ? s.substring(0, idx) : s.substring(0, idx + maxNumDecimals + 1); +} + +export function fuzzyCmp(a: number, b: number, relativeTolerance = 0.00001) { + const TOL = a * relativeTolerance; + return Math.abs(a - b) <= TOL; +} + export function isoBounds(min: number, max: number): { min: number, max: number, step: number } { let diff = max - min; if (diff <= 0.0) @@ -41,6 +56,23 @@ export function prettyIso(iso: number, step: number) { return Math.floor((iso - step) / step) * step + step; } +export function reduceDecimals(s: string) { + const delimIdx = s.lastIndexOf('.'); + if (delimIdx < 0) + return s; + else if (delimIdx === s.length - 1) + return s.substring(0, s.length - 1); + + let idx = s.length - 1; + for (; idx > delimIdx; idx--) { + if (s.charCodeAt(idx) !== Zero) + break; + } + const noDot = s.charCodeAt(idx) === Period ? 0 : 1 + + return s.substring(0, idx + noDot); +} + export function stof(s: string) { if (s.length === 0) return void 0;