Window.svelte 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749
  1. <script>
  2. import { onDestroy } from 'svelte'
  3. import {
  4. LogicalSize,
  5. UserAttentionType,
  6. PhysicalSize,
  7. PhysicalPosition,
  8. Effect,
  9. EffectState,
  10. ProgressBarStatus
  11. } from '@tauri-apps/api/window'
  12. import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
  13. export let onMessage
  14. const webview = WebviewWindow.getCurrent()
  15. let selectedWebview = webview.label
  16. const webviewMap = {
  17. [webview.label]: webview
  18. }
  19. const cursorIconOptions = [
  20. 'default',
  21. 'crosshair',
  22. 'hand',
  23. 'arrow',
  24. 'move',
  25. 'text',
  26. 'wait',
  27. 'help',
  28. 'progress',
  29. // something cannot be done
  30. 'notAllowed',
  31. 'contextMenu',
  32. 'cell',
  33. 'verticalText',
  34. 'alias',
  35. 'copy',
  36. 'noDrop',
  37. // something can be grabbed
  38. 'grab',
  39. /// something is grabbed
  40. 'grabbing',
  41. 'allScroll',
  42. 'zoomIn',
  43. 'zoomOut',
  44. // edge is to be moved
  45. 'eResize',
  46. 'nResize',
  47. 'neResize',
  48. 'nwResize',
  49. 'sResize',
  50. 'seResize',
  51. 'swResize',
  52. 'wResize',
  53. 'ewResize',
  54. 'nsResize',
  55. 'neswResize',
  56. 'nwseResize',
  57. 'colResize',
  58. 'rowResize'
  59. ]
  60. const windowsEffects = [
  61. 'mica',
  62. 'blur',
  63. 'acrylic',
  64. 'tabbed',
  65. 'tabbedDark',
  66. 'tabbedLight'
  67. ]
  68. const isWindows = navigator.appVersion.includes('Windows')
  69. const isMacOS = navigator.appVersion.includes('Macintosh')
  70. let effectOptions = isWindows
  71. ? windowsEffects
  72. : Object.keys(Effect)
  73. .map((effect) => Effect[effect])
  74. .filter((e) => !windowsEffects.includes(e))
  75. const effectStateOptions = Object.keys(EffectState).map(
  76. (state) => EffectState[state]
  77. )
  78. const progressBarStatusOptions = Object.keys(ProgressBarStatus).map(
  79. (s) => ProgressBarStatus[s]
  80. )
  81. const mainEl = document.querySelector('main')
  82. let newWebviewLabel
  83. let resizable = true
  84. let maximizable = true
  85. let minimizable = true
  86. let closable = true
  87. let maximized = false
  88. let decorations = true
  89. let alwaysOnTop = false
  90. let alwaysOnBottom = false
  91. let contentProtected = false
  92. let fullscreen = false
  93. let width = null
  94. let height = null
  95. let minWidth = null
  96. let minHeight = null
  97. let maxWidth = null
  98. let maxHeight = null
  99. let x = null
  100. let y = null
  101. let scaleFactor = 1
  102. let innerPosition = new PhysicalPosition(x, y)
  103. let outerPosition = new PhysicalPosition(x, y)
  104. let innerSize = new PhysicalSize(width, height)
  105. let outerSize = new PhysicalSize(width, height)
  106. let resizeEventUnlisten
  107. let moveEventUnlisten
  108. let cursorGrab = false
  109. let cursorVisible = true
  110. let cursorX = null
  111. let cursorY = null
  112. /** @type {import('@tauri-apps/api/window').CursorIcon} */
  113. let cursorIcon = 'default'
  114. let cursorIgnoreEvents = false
  115. let windowTitle = 'Awesome Tauri Example!'
  116. /** @type {import('@tauri-apps/api/window').Theme | 'auto'} */
  117. let theme = 'auto'
  118. let effects = []
  119. let selectedEffect
  120. let effectState
  121. let effectRadius
  122. let effectR, effectG, effectB, effectA
  123. /** @type {ProgressBarStatus} */
  124. let selectedProgressBarStatus = ProgressBarStatus.None
  125. let progress = 0
  126. let windowIconPath
  127. function setTitle() {
  128. webviewMap[selectedWebview].setTitle(windowTitle)
  129. }
  130. async function hide() {
  131. let visible = await webviewMap[selectedWebview].isVisible()
  132. onMessage('window is ' + (visible ? 'visible' : 'invisible'))
  133. await webviewMap[selectedWebview].hide()
  134. setTimeout(async () => {
  135. visible = await webviewMap[selectedWebview].isVisible()
  136. onMessage('window is ' + (visible ? 'visible' : 'invisible'))
  137. await webviewMap[selectedWebview].show()
  138. visible = await webviewMap[selectedWebview].isVisible()
  139. onMessage('window is ' + (visible ? 'visible' : 'invisible'))
  140. }, 2000)
  141. }
  142. async function disable() {
  143. let enabled = await webviewMap[selectedWebview].isEnabled()
  144. onMessage('window is ' + (enabled ? 'enabled' : 'disabled'))
  145. await webviewMap[selectedWebview].setEnabled(false)
  146. setTimeout(async () => {
  147. enabled = await webviewMap[selectedWebview].isEnabled()
  148. onMessage('window is ' + (enabled ? 'enabled' : 'disabled'))
  149. await webviewMap[selectedWebview].setEnabled(true)
  150. enabled = await webviewMap[selectedWebview].isEnabled()
  151. onMessage('window is ' + (enabled ? 'enabled' : 'disabled'))
  152. }, 2000)
  153. }
  154. function minimize() {
  155. webviewMap[selectedWebview].minimize()
  156. setTimeout(webviewMap[selectedWebview].unminimize, 2000)
  157. }
  158. function changeIcon() {
  159. webviewMap[selectedWebview].setIcon(windowIconPath)
  160. }
  161. function createWebviewWindow() {
  162. if (!newWebviewLabel) return
  163. const label = `main-${newWebviewLabel}`
  164. const webview = new WebviewWindow(label)
  165. webviewMap[label] = webview
  166. webview.once('tauri://error', function (e) {
  167. onMessage('Error creating new webview ' + JSON.stringify(e))
  168. })
  169. webview.once('tauri://created', function () {
  170. onMessage('webview created')
  171. })
  172. }
  173. function loadWindowSize() {
  174. webviewMap[selectedWebview].innerSize().then((response) => {
  175. innerSize = response
  176. width = innerSize.width
  177. height = innerSize.height
  178. })
  179. webviewMap[selectedWebview].outerSize().then((response) => {
  180. outerSize = response
  181. })
  182. }
  183. function loadWindowPosition() {
  184. webviewMap[selectedWebview].innerPosition().then((response) => {
  185. innerPosition = response
  186. })
  187. webviewMap[selectedWebview].outerPosition().then((response) => {
  188. outerPosition = response
  189. x = outerPosition.x
  190. y = outerPosition.y
  191. })
  192. }
  193. async function addWindowEventListeners(window) {
  194. if (!window) return
  195. resizeEventUnlisten?.()
  196. moveEventUnlisten?.()
  197. moveEventUnlisten = await window.listen('tauri://move', loadWindowPosition)
  198. resizeEventUnlisten = await window.listen('tauri://resize', loadWindowSize)
  199. }
  200. async function requestUserAttention() {
  201. await webviewMap[selectedWebview].minimize()
  202. await webviewMap[selectedWebview].requestUserAttention(
  203. UserAttentionType.Critical
  204. )
  205. await new Promise((resolve) => setTimeout(resolve, 3000))
  206. await webviewMap[selectedWebview].requestUserAttention(null)
  207. }
  208. async function switchTheme() {
  209. switch (theme) {
  210. case 'dark':
  211. theme = 'light'
  212. break
  213. case 'light':
  214. theme = 'auto'
  215. break
  216. case 'auto':
  217. theme = 'dark'
  218. break
  219. }
  220. await webviewMap[selectedWebview].setTheme(theme === 'auto' ? null : theme)
  221. }
  222. async function updateProgressBar() {
  223. webviewMap[selectedWebview]?.setProgressBar({
  224. status: selectedProgressBarStatus,
  225. progress
  226. })
  227. }
  228. async function addEffect() {
  229. if (!effects.includes(selectedEffect)) {
  230. effects = [...effects, selectedEffect]
  231. }
  232. const payload = {
  233. effects,
  234. state: effectState,
  235. radius: effectRadius
  236. }
  237. if (
  238. Number.isInteger(effectR) &&
  239. Number.isInteger(effectG) &&
  240. Number.isInteger(effectB) &&
  241. Number.isInteger(effectA)
  242. ) {
  243. payload.color = [effectR, effectG, effectB, effectA]
  244. }
  245. mainEl.classList.remove('bg-primary')
  246. mainEl.classList.remove('dark:bg-darkPrimary')
  247. await webviewMap[selectedWebview].clearEffects()
  248. await webviewMap[selectedWebview].setEffects(payload)
  249. }
  250. async function clearEffects() {
  251. effects = []
  252. await webviewMap[selectedWebview].clearEffects()
  253. mainEl.classList.add('bg-primary')
  254. mainEl.classList.add('dark:bg-darkPrimary')
  255. }
  256. async function updatePosition() {
  257. webviewMap[selectedWebview]?.setPosition(new PhysicalPosition(x, y))
  258. }
  259. async function updateSize() {
  260. webviewMap[selectedWebview]?.setSize(new PhysicalSize(width, height))
  261. }
  262. $: {
  263. webviewMap[selectedWebview]
  264. loadWindowPosition()
  265. loadWindowSize()
  266. }
  267. $: webviewMap[selectedWebview]?.setResizable(resizable)
  268. $: webviewMap[selectedWebview]?.setMaximizable(maximizable)
  269. $: webviewMap[selectedWebview]?.setMinimizable(minimizable)
  270. $: webviewMap[selectedWebview]?.setClosable(closable)
  271. $: maximized
  272. ? webviewMap[selectedWebview]?.maximize()
  273. : webviewMap[selectedWebview]?.unmaximize()
  274. $: webviewMap[selectedWebview]?.setDecorations(decorations)
  275. $: webviewMap[selectedWebview]?.setAlwaysOnTop(alwaysOnTop)
  276. $: webviewMap[selectedWebview]?.setAlwaysOnBottom(alwaysOnBottom)
  277. $: webviewMap[selectedWebview]?.setContentProtected(contentProtected)
  278. $: webviewMap[selectedWebview]?.setFullscreen(fullscreen)
  279. $: minWidth && minHeight
  280. ? webviewMap[selectedWebview]?.setMinSize(
  281. new LogicalSize(minWidth, minHeight)
  282. )
  283. : webviewMap[selectedWebview]?.setMinSize(null)
  284. $: maxWidth > 800 && maxHeight > 400
  285. ? webviewMap[selectedWebview]?.setMaxSize(
  286. new LogicalSize(maxWidth, maxHeight)
  287. )
  288. : webviewMap[selectedWebview]?.setMaxSize(null)
  289. $: webviewMap[selectedWebview]
  290. ?.scaleFactor()
  291. .then((factor) => (scaleFactor = factor))
  292. $: addWindowEventListeners(webviewMap[selectedWebview])
  293. $: webviewMap[selectedWebview]?.setCursorGrab(cursorGrab)
  294. $: webviewMap[selectedWebview]?.setCursorVisible(cursorVisible)
  295. $: webviewMap[selectedWebview]?.setCursorIcon(cursorIcon)
  296. $: cursorX !== null &&
  297. cursorY !== null &&
  298. webviewMap[selectedWebview]?.setCursorPosition(
  299. new PhysicalPosition(cursorX, cursorY)
  300. )
  301. $: webviewMap[selectedWebview]?.setIgnoreCursorEvents(cursorIgnoreEvents)
  302. onDestroy(() => {
  303. resizeEventUnlisten?.()
  304. moveEventUnlisten?.()
  305. })
  306. </script>
  307. <div class="flex flex-col children:grow gap-8 mb-4">
  308. <div
  309. class="flex flex-wrap items-center gap-4 pb-6 border-b-solid border-b-1 border-code"
  310. >
  311. {#if Object.keys(webviewMap).length >= 1}
  312. <div class="grid gap-1">
  313. <h4 class="my-2">Selected Window</h4>
  314. <select class="input" bind:value={selectedWebview}>
  315. <option value="" disabled selected>Choose a window...</option>
  316. {#each Object.keys(webviewMap) as label}
  317. <option value={label}>{label}</option>
  318. {/each}
  319. </select>
  320. </div>
  321. {/if}
  322. <div class="grid gap-1">
  323. <h4 class="my-2">Create New Window</h4>
  324. <form class="flex gap-2" on:submit|preventDefault={createWebviewWindow}>
  325. <input
  326. class="input"
  327. type="text"
  328. placeholder="New window label.."
  329. bind:value={newWebviewLabel}
  330. />
  331. <button class="btn" type="submit">Create</button>
  332. </form>
  333. </div>
  334. </div>
  335. {#if webviewMap[selectedWebview]}
  336. <div class="flex flex-wrap items-center gap-4">
  337. <div class="grid gap-1 grow">
  338. <h4 class="my-2">Change Window Icon</h4>
  339. <form class="flex gap-2" on:submit|preventDefault={changeIcon}>
  340. <input
  341. class="input flex-1 min-w-10"
  342. placeholder="Window icon path"
  343. bind:value={windowIconPath}
  344. />
  345. <button class="btn" type="submit">Change</button>
  346. </form>
  347. </div>
  348. <div class="grid gap-1 grow">
  349. <h4 class="my-2">Set Window Title</h4>
  350. <form class="flex gap-2" on:submit|preventDefault={setTitle}>
  351. <input class="input flex-1 min-w-10" bind:value={windowTitle} />
  352. <button class="btn" type="submit">Set</button>
  353. </form>
  354. </div>
  355. </div>
  356. <div class="flex flex-wrap gap-2">
  357. <button
  358. class="btn"
  359. title="Unminimizes after 2 seconds"
  360. on:click={() => webviewMap[selectedWebview].center()}
  361. >
  362. Center
  363. </button>
  364. <button
  365. class="btn"
  366. title="Unminimizes after 2 seconds"
  367. on:click={minimize}
  368. >
  369. Minimize
  370. </button>
  371. <button class="btn" title="Visible again after 2 seconds" on:click={hide}>
  372. Hide
  373. </button>
  374. <button
  375. class="btn"
  376. title="Enabled again after 2 seconds"
  377. on:click={disable}
  378. >
  379. Disable
  380. </button>
  381. <button
  382. class="btn"
  383. on:click={requestUserAttention}
  384. title="Minimizes the window, requests attention for 3s and then resets it"
  385. >Request attention</button
  386. >
  387. <button class="btn" on:click={switchTheme}>Switch Theme ({theme})</button>
  388. </div>
  389. <div class="grid cols-[repeat(auto-fill,minmax(180px,1fr))]">
  390. <label>
  391. <input type="checkbox" class="checkbox" bind:checked={resizable} />
  392. Resizable
  393. </label>
  394. <label>
  395. <input type="checkbox" class="checkbox" bind:checked={maximizable} />
  396. Maximizable
  397. </label>
  398. <label>
  399. <input type="checkbox" class="checkbox" bind:checked={minimizable} />
  400. Minimizable
  401. </label>
  402. <label>
  403. <input type="checkbox" class="checkbox" bind:checked={closable} />
  404. Closable
  405. </label>
  406. <label>
  407. <input type="checkbox" class="checkbox" bind:checked={decorations} />
  408. Has decorations
  409. </label>
  410. <label>
  411. <input type="checkbox" class="checkbox" bind:checked={alwaysOnTop} />
  412. Always on top
  413. </label>
  414. <label>
  415. <input type="checkbox" class="checkbox" bind:checked={alwaysOnBottom} />
  416. Always on bottom
  417. </label>
  418. <label>
  419. <input
  420. type="checkbox"
  421. class="checkbox"
  422. bind:checked={contentProtected}
  423. />
  424. Content protected
  425. </label>
  426. <label>
  427. <input type="checkbox" class="checkbox" bind:checked={maximized} />
  428. Maximized
  429. </label>
  430. <label>
  431. <input type="checkbox" class="checkbox" bind:checked={fullscreen} />
  432. Fullscreen
  433. </label>
  434. </div>
  435. <div class="flex flex-wrap children:flex-basis-30 gap-2">
  436. <div class="grid gap-1 children:grid">
  437. <label>
  438. X
  439. <input
  440. class="input"
  441. type="number"
  442. bind:value={x}
  443. on:change={updatePosition}
  444. min="0"
  445. />
  446. </label>
  447. <label>
  448. Y
  449. <input
  450. class="input"
  451. type="number"
  452. bind:value={y}
  453. on:change={updatePosition}
  454. min="0"
  455. />
  456. </label>
  457. </div>
  458. <div class="grid gap-1 children:grid">
  459. <label>
  460. Width
  461. <input
  462. class="input"
  463. type="number"
  464. bind:value={width}
  465. on:change={updateSize}
  466. min="400"
  467. />
  468. </label>
  469. <div>
  470. Height
  471. <input
  472. class="input"
  473. type="number"
  474. bind:value={height}
  475. on:change={updateSize}
  476. min="400"
  477. />
  478. </div>
  479. </div>
  480. <div class="grid gap-1 children:grid">
  481. <label>
  482. Min width
  483. <input class="input" type="number" bind:value={minWidth} />
  484. </label>
  485. <label>
  486. Min height
  487. <input class="input" type="number" bind:value={minHeight} />
  488. </label>
  489. </div>
  490. <div class="grid gap-1 children:grid">
  491. <label>
  492. Max width
  493. <input class="input" type="number" bind:value={maxWidth} min="800" />
  494. </label>
  495. <label>
  496. Max height
  497. <input class="input" type="number" bind:value={maxHeight} min="400" />
  498. </label>
  499. </div>
  500. </div>
  501. <div class="grid grid-cols-2 gap-2 max-inline-2xl">
  502. <div>
  503. <div class="text-accent dark:text-darkAccent font-700 m-block-1">
  504. Inner Size
  505. </div>
  506. <span>Width: {innerSize.width}</span>
  507. <span>Height: {innerSize.height}</span>
  508. </div>
  509. <div>
  510. <div class="text-accent dark:text-darkAccent font-700 m-block-1">
  511. Outer Size
  512. </div>
  513. <span>Width: {outerSize.width}</span>
  514. <span>Height: {outerSize.height}</span>
  515. </div>
  516. <div>
  517. <div class="text-accent dark:text-darkAccent font-700 m-block-1">
  518. Inner Logical Size
  519. </div>
  520. <span>Width: {innerSize.toLogical(scaleFactor).width.toFixed(3)}</span>
  521. <span>Height: {innerSize.toLogical(scaleFactor).height.toFixed(3)}</span
  522. >
  523. </div>
  524. <div>
  525. <div class="text-accent dark:text-darkAccent font-700 m-block-1">
  526. Outer Logical Size
  527. </div>
  528. <span>Width: {outerSize.toLogical(scaleFactor).width.toFixed(3)}</span>
  529. <span>Height: {outerSize.toLogical(scaleFactor).height.toFixed(3)}</span
  530. >
  531. </div>
  532. <div>
  533. <div class="text-accent dark:text-darkAccent font-700 m-block-1">
  534. Inner Position
  535. </div>
  536. <span>x: {innerPosition.x}</span>
  537. <span>y: {innerPosition.y}</span>
  538. </div>
  539. <div>
  540. <div class="text-accent dark:text-darkAccent font-700 m-block-1">
  541. Outer Position
  542. </div>
  543. <span>x: {outerPosition.x}</span>
  544. <span>y: {outerPosition.y}</span>
  545. </div>
  546. <div>
  547. <div class="text-accent dark:text-darkAccent font-700 m-block-1">
  548. Inner Logical Position
  549. </div>
  550. <span>x: {innerPosition.toLogical(scaleFactor).x.toFixed(3)}</span>
  551. <span>y: {innerPosition.toLogical(scaleFactor).y.toFixed(3)}</span>
  552. </div>
  553. <div>
  554. <div class="text-accent dark:text-darkAccent font-700 m-block-1">
  555. Outer Logical Position
  556. </div>
  557. <span>x: {outerPosition.toLogical(scaleFactor).x.toFixed(3)}</span>
  558. <span>y: {outerPosition.toLogical(scaleFactor).y.toFixed(3)}</span>
  559. </div>
  560. </div>
  561. <div class="grid gap-2">
  562. <h4 class="my-2">Cursor</h4>
  563. <div class="flex gap-2">
  564. <label>
  565. <input type="checkbox" class="checkbox" bind:checked={cursorGrab} />
  566. Grab
  567. </label>
  568. <label>
  569. <input
  570. type="checkbox"
  571. class="checkbox"
  572. bind:checked={cursorVisible}
  573. />
  574. Visible
  575. </label>
  576. <label>
  577. <input
  578. type="checkbox"
  579. class="checkbox"
  580. bind:checked={cursorIgnoreEvents}
  581. />
  582. Ignore events
  583. </label>
  584. </div>
  585. <div class="flex gap-2">
  586. <label>
  587. Icon
  588. <select class="input" bind:value={cursorIcon}>
  589. {#each cursorIconOptions as kind}
  590. <option value={kind}>{kind}</option>
  591. {/each}
  592. </select>
  593. </label>
  594. <label>
  595. X position
  596. <input class="input" type="number" bind:value={cursorX} />
  597. </label>
  598. <label>
  599. Y position
  600. <input class="input" type="number" bind:value={cursorY} />
  601. </label>
  602. </div>
  603. </div>
  604. <div class="flex flex-col gap-1">
  605. <div class="flex gap-2">
  606. <label>
  607. Progress Status
  608. <select
  609. class="input"
  610. bind:value={selectedProgressBarStatus}
  611. on:change={updateProgressBar}
  612. >
  613. {#each progressBarStatusOptions as status}
  614. <option value={status}>{status}</option>
  615. {/each}
  616. </select>
  617. </label>
  618. <label>
  619. Progress
  620. <input
  621. class="input"
  622. type="number"
  623. min="0"
  624. max="100"
  625. bind:value={progress}
  626. on:change={updateProgressBar}
  627. />
  628. </label>
  629. </div>
  630. </div>
  631. {#if isWindows || isMacOS}
  632. <div class="flex flex-col gap-2">
  633. <div class="flex items-center gap-2">
  634. <div>
  635. Applied effects: {effects.length ? effects.join(', ') : 'None'}
  636. </div>
  637. <button class="btn" on:click={clearEffects}>Clear</button>
  638. </div>
  639. <div class="flex gap-2">
  640. <label>
  641. Effect
  642. <select class="input" bind:value={selectedEffect}>
  643. {#each effectOptions as effect}
  644. <option value={effect}>{effect}</option>
  645. {/each}
  646. </select>
  647. </label>
  648. <label>
  649. State
  650. <select class="input" bind:value={effectState}>
  651. {#each effectStateOptions as state}
  652. <option value={state}>{state}</option>
  653. {/each}
  654. </select>
  655. </label>
  656. <label>
  657. Radius
  658. <input class="input" type="number" bind:value={effectRadius} />
  659. </label>
  660. </div>
  661. <div class="flex">
  662. <label>
  663. Color
  664. <div class="flex gap-2 children:flex-basis-30">
  665. <input
  666. class="input"
  667. type="number"
  668. placeholder="R"
  669. bind:value={effectR}
  670. />
  671. <input
  672. class="input"
  673. type="number"
  674. placeholder="G"
  675. bind:value={effectG}
  676. />
  677. <input
  678. class="input"
  679. type="number"
  680. placeholder="B"
  681. bind:value={effectB}
  682. />
  683. <input
  684. class="input"
  685. type="number"
  686. placeholder="A"
  687. bind:value={effectA}
  688. />
  689. </div>
  690. </label>
  691. </div>
  692. <div class="flex">
  693. <button class="btn" on:click={addEffect}>Add</button>
  694. </div>
  695. </div>
  696. {/if}
  697. {/if}
  698. </div>