1// Copyright (C) 2024 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import m from 'mithril'; 16import {ProbeSetting} from '../../config/config_interfaces'; 17import {assertTrue} from '../../../../base/logging'; 18import {exists} from '../../../../base/utils'; 19 20export interface SliderAttrs { 21 title: string; 22 values: number[]; 23 default?: number; 24 icon?: string; 25 cssClass?: string; 26 isTime?: boolean; 27 unit: string; 28 min?: number; 29 description?: string; 30 disabled?: boolean; 31 zeroIsDefault?: boolean; 32 onChange?: (value: number) => void; 33} 34 35export class Slider implements ProbeSetting { 36 private _value: number; 37 38 constructor(readonly attrs: SliderAttrs) { 39 assertTrue(attrs.values.length > 0); 40 this._value = this.setValue(undefined); 41 } 42 43 serialize() { 44 return this._value; 45 } 46 47 deserialize(state: unknown): void { 48 if (typeof state === 'number') { 49 this._value = state; 50 } 51 } 52 53 get value(): number { 54 return this._value; 55 } 56 57 setValue(value: number | null | undefined) { 58 // Logic if value is null/undefined: try first the .default, if provided, 59 // otherwise fall back on the first value of the fixed range... otherwise 0. 60 this._value = exists(value) 61 ? value 62 : this.attrs.default ?? this.attrs.values[0] ?? 0; 63 return this._value; 64 } 65 66 private onValueChange(newVal: number) { 67 this._value = newVal; 68 this.attrs.onChange?.(newVal); 69 } 70 71 onTimeValueChange(hms: string) { 72 try { 73 const date = new Date(`1970-01-01T${hms}.000Z`); 74 if (isNaN(date.getTime())) return; 75 this.onValueChange(date.getTime()); 76 } catch {} 77 } 78 79 onSliderChange(newIdx: number) { 80 this.onValueChange(this.attrs.values[newIdx]); 81 } 82 83 render() { 84 const attrs = this.attrs; 85 const id = attrs.title.replace(/[^a-z0-9]/gim, '_').toLowerCase(); 86 const maxIdx = attrs.values.length - 1; 87 const val = this._value; 88 let min = attrs.min ?? 1; 89 if (attrs.zeroIsDefault) { 90 min = Math.min(0, min); 91 } 92 const description = attrs.description; 93 const disabled = attrs.disabled; 94 95 // Find the index of the closest value in the slider. 96 let idx = 0; 97 for (; idx < attrs.values.length && attrs.values[idx] < val; idx++) {} 98 99 let spinnerCfg = {}; 100 if (attrs.isTime) { 101 spinnerCfg = { 102 type: 'text', 103 pattern: '(0[0-9]|1[0-9]|2[0-3])(:[0-5][0-9]){2}', // hh:mm:ss 104 value: new Date(val).toISOString().substring(11, 11 + 8), 105 oninput: (e: InputEvent) => { 106 this.onTimeValueChange((e.target as HTMLInputElement).value); 107 }, 108 }; 109 } else { 110 const isDefault = attrs.zeroIsDefault && val === 0; 111 spinnerCfg = { 112 type: 'number', 113 value: isDefault ? '' : val, 114 placeholder: isDefault ? '(default)' : '', 115 oninput: (e: InputEvent) => { 116 this.onValueChange(+(e.target as HTMLInputElement).value); 117 }, 118 }; 119 } 120 return m( 121 '.slider' + (attrs.cssClass ?? ''), 122 m('header', attrs.title), 123 description ? m('header.descr', attrs.description) : '', 124 attrs.icon !== undefined ? m('i.material-icons', attrs.icon) : [], 125 m(`input[id="${id}"][type=range][min=0][max=${maxIdx}][value=${idx}]`, { 126 disabled, 127 oninput: (e: InputEvent) => { 128 this.onSliderChange(+(e.target as HTMLInputElement).value); 129 }, 130 }), 131 m(`input.spinner[min=${min}][for=${id}]`, spinnerCfg), 132 m('.unit', attrs.unit), 133 ); 134 } 135} 136 137export const POLL_INTERVAL_SLIDER: SliderAttrs = { 138 title: 'Poll interval', 139 values: [250, 500, 1000, 2500, 5000, 30000, 60000], 140 cssClass: '.thin', 141 unit: 'ms', 142}; 143