Files
cadhub/app/web/src/components/Customizer/Customizer.tsx
2021-08-21 11:07:09 +10:00

262 lines
8.0 KiB
TypeScript

import { useRender } from 'src/components/IdeWrapper/useRender'
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
import { Switch } from '@headlessui/react'
import Svg from 'src/components/Svg/Svg'
import {
CadhubStringParam,
CadhubBooleanParam,
CadhubNumberParam,
} from './customizerConverter'
import { debounce } from 'lodash'
const Customizer = () => {
const [open, setOpen] = React.useState(false)
const [shouldLiveUpdate, setShouldLiveUpdate] = React.useState(false)
const { state, thunkDispatch } = useIdeContext()
const customizerParams = state?.customizerParams
const currentParameters = state?.currentParameters || {}
const handleRender = useRender()
const updateCustomizerParam = (paramName: string, paramValue: any) => {
const payload = {
...currentParameters,
[paramName]: paramValue,
}
thunkDispatch({ type: 'setCurrentCustomizerParams', payload })
shouldLiveUpdate && setTimeout(() => handleRender())
}
if (!customizerParams?.length) return null
return (
<div
className={`absolute inset-x-0 bottom-0 bg-ch-gray-600 bg-opacity-60 text-ch-gray-300 text-lg font-fira-sans ${
open ? 'h-2/3' : ''
}`}
>
<div className="flex justify-between px-6 py-2 items-center">
<div className="grid grid-flow-col-dense gap-6 items-center">
<button className="px-2" onClick={() => setOpen(!open)}>
<Svg
name="chevron-down"
className={`h-8 w-8 ${!open && 'transform rotate-180'}`}
/>
</button>
<div>Parameters</div>
</div>
{open && (
<>
<div className="flex items-center">
<div className="font-fira-sans text-sm mr-4">Auto Update</div>
<Switch
checked={shouldLiveUpdate}
onChange={(newValue) => {
if (newValue) handleRender()
setShouldLiveUpdate(newValue)
}}
className={`${
shouldLiveUpdate ? 'bg-ch-purple-600' : 'bg-ch-gray-300'
} relative inline-flex items-center h-6 rounded-full w-11 mr-6`}
>
<span
className={`${
shouldLiveUpdate ? 'translate-x-6' : 'translate-x-1'
} inline-block w-4 h-4 transform bg-white rounded-full`}
/>
</Switch>
<button
className={`px-4 py-1 rounded bg-ch-gray-300 text-ch-gray-800 ${
shouldLiveUpdate && 'bg-opacity-30 cursor-default'
}`}
onClick={handleRender}
disabled={shouldLiveUpdate}
>
Update
</button>
</div>
</>
)}
</div>
<div className={`${open ? 'h-full pb-32' : 'h-0'} overflow-y-auto px-12`}>
<div>
{customizerParams.map((param, index) => {
const otherProps = {
value: currentParameters[param.name],
onChange: (value) => updateCustomizerParam(param.name, value),
}
if (param.type === 'string') {
return <StringParam key={index} param={param} {...otherProps} />
} else if (param.type === 'number') {
return <NumberParam key={index} param={param} {...otherProps} />
} else if (param.type === 'boolean') {
return <BooleanParam key={index} param={param} {...otherProps} />
}
return <div key={index}>{JSON.stringify(param)}</div>
})}
</div>
</div>
</div>
)
}
export default Customizer
function CustomizerParamBase({
name,
caption,
children,
}: {
name: string
caption: string
children: React.ReactNode
}) {
return (
<li
className="grid items-center my-2"
style={{ gridTemplateColumns: 'auto 8rem' }}
>
<div className=" text-sm font-fira-sans">
<div className="font-bold text-base">{name}</div>
<div>{caption}</div>
</div>
<div className="w-full">{children}</div>
</li>
)
}
function BooleanParam({
param,
value,
onChange,
}: {
param: CadhubBooleanParam
value: any
onChange: Function
}) {
return (
<CustomizerParamBase name={param.name} caption={param.caption}>
<Switch
checked={value}
onChange={(newValue) => {
onChange(newValue)
}}
className={`${
value ? 'bg-ch-gray-300' : 'bg-ch-gray-600'
} relative inline-flex items-center h-6 rounded-full w-11 mr-6 border border-ch-gray-300`}
>
<span
className={`${
value ? 'translate-x-6' : 'translate-x-1'
} inline-block w-4 h-4 transform bg-white rounded-full`}
/>
</Switch>
</CustomizerParamBase>
)
}
function StringParam({
param,
value,
onChange,
}: {
param: CadhubStringParam
value: any
onChange: Function
}) {
return (
<CustomizerParamBase name={param.name} caption={param.caption}>
<input
className="bg-transparent h-8 border border-ch-gray-300 px-2 text-sm w-full"
type="text"
value={value}
placeholder={param.placeholder}
onChange={({ target }) => onChange(target?.value)}
/>
</CustomizerParamBase>
)
}
function NumberParam({
param,
value,
onChange,
}: {
param: CadhubNumberParam
value: any
onChange: Function
}) {
const [isFocused, isFocusedSetter] = React.useState(false)
const [localValue, localValueSetter] = React.useState(0)
const [isLocked, isLockedSetter] = React.useState(false)
const [pixelsDragged, pixelsDraggedSetter] = React.useState(0)
const handleRender = useRender()
const liveRenderHandler = debounce((a) => handleRender(a), 250)
const step = param.step || 1
const commitChange = () => {
let num = localValue
if (typeof param.step === 'number') {
num = Math.round(num / step) * step
}
if (typeof param.min === 'number') {
num = Math.max(param.min, num)
}
if (typeof param.max === 'number') {
num = Math.min(param.max, num)
}
num = Number(num.toFixed(2))
localValueSetter(num)
onChange(num)
}
React.useEffect(() => {
if (!isFocused) commitChange()
}, [isFocused])
React.useEffect(() => localValueSetter(value), [value])
return (
<CustomizerParamBase name={param.name} caption={param.caption}>
<div className="flex h-8 border border-ch-gray-300">
<input
className={`bg-transparent px-2 text-sm w-full ${
(param.max && param.max < localValue) ||
(param.min && param.min > localValue)
? 'text-red-500'
: ''
}`}
type="number"
value={localValue}
onFocus={() => isFocusedSetter(true)}
onBlur={() => isFocusedSetter(false)}
onKeyDown={({ key }) => key === 'Enter' && commitChange()}
onChange={({ target }) => {
const num = Number(target?.value)
localValueSetter(num)
}}
max={param.max}
min={param.min}
step={step}
/>
<div
className="w-6 border-l border-ch-gray-500 items-center hidden md:flex"
style={{ cursor: 'ew-resize' }}
onMouseDown={({ target }) => {
isLockedSetter(true)
target?.requestPointerLock?.()
pixelsDraggedSetter(localValue)
}}
onMouseUp={() => {
isLockedSetter(false)
document?.exitPointerLock?.()
commitChange()
}}
onMouseMove={({ movementX }) => {
if (isLocked && movementX) {
pixelsDraggedSetter(pixelsDragged + (movementX * step) / 8) // one step per 8 pixels
localValueSetter(Number(pixelsDragged.toFixed(2)))
liveRenderHandler({[param.name]: Number(pixelsDragged.toFixed(2))})
}
}}
>
<Svg className="w-6" name="switch-horizontal" />
</div>
</div>
</CustomizerParamBase>
)
}