import { Atom } from "@thi.ng/atom/atom" ; import { History } from "@thi.ng/atom/history" ; import { timedResult } from "@thi.ng/bench/timed" ; import { line } from "@thi.ng/geom/line" ; import { normalizedPath } from "@thi.ng/geom/path" ; import { pathFromSvg } from "@thi.ng/geom/path-from-svg" ; import { canvas } from "@thi.ng/hdom-canvas" ; import { DOWNLOAD } from "@thi.ng/hiccup-carbon-icons/download" ; import { RESTART } from "@thi.ng/hiccup-carbon-icons/restart" ; import { DEFAULT_THEME , Key , NONE , type , GUITheme } from "@thi.ng/imgui/api" ; import { buttonH , buttonV } from "@thi.ng/imgui/components/button" ; import { dialGroup } from "@thi.ng/imgui/components/dial" ; import { dropdown } from "@thi.ng/imgui/components/dropdown" ; import { iconButton } from "@thi.ng/imgui/components/icon-button" ; import { radialMenu } from "@thi.ng/imgui/components/radial-menu" ; import { radio } from "@thi.ng/imgui/components/radio" ; import { ring , ringGroup } from "@thi.ng/imgui/components/ring" ; import { sliderH , sliderHGroup } from "@thi.ng/imgui/components/sliderh" ; import { sliderVGroup } from "@thi.ng/imgui/components/sliderv" ; import { textField } from "@thi.ng/imgui/components/textfield" ; import { textLabel , textLabelRaw } from "@thi.ng/imgui/components/textlabel" ; import { toggle } from "@thi.ng/imgui/components/toggle" ; import { xyPad } from "@thi.ng/imgui/components/xypad" ; import { IMGUI } from "@thi.ng/imgui/gui" ; import { layoutBox } from "@thi.ng/layout/box" ; import { gridLayout , GridLayout } from "@thi.ng/layout/grid-layout" ; import { PI } from "@thi.ng/math/api" ; import { clamp } from "@thi.ng/math/interval" ; import { setInManyUnsafe } from "@thi.ng/paths/set-in-many" ; import { gestureStream } from "@thi.ng/rstream-gestures" ; import { fromAtom } from "@thi.ng/rstream/atom" ; import { fromDOMEvent } from "@thi.ng/rstream/event" ; import { merge } from "@thi.ng/rstream/merge" ; import { sidechainPartitionRAF } from "@thi.ng/rstream/sidechain-partition" ; import { sync } from "@thi.ng/rstream/sync" ; import { float } from "@thi.ng/strings/float" ; import { updateDOM } from "@thi.ng/transducers-hdom" ; import { sma } from "@thi.ng/transducers-stats/sma" ; import { comp } from "@thi.ng/transducers/comp" ; import { iterator } from "@thi.ng/transducers/iterator" ; import { map } from "@thi.ng/transducers/map" ; import { mapcat } from "@thi.ng/transducers/mapcat" ; import { step } from "@thi.ng/transducers/step" ; import { add2 } from "@thi.ng/vectors/add" ; import { type , Vec , ZERO2 } from "@thi.ng/vectors/api" ; import { hash } from "@thi.ng/vectors/hash" ; import { min2 } from "@thi.ng/vectors/min" ; import { setC2 } from "@thi.ng/vectors/setc" ; import { vecOf } from "@thi.ng/vectors/vec-of" ; const THEMES = [ DEFAULT_THEME , { globalBg : "#ccc" , focus : [ 1 , 0.66 , 0 , 1 ] , cursor : [ 0 , 0 , 0 , 1 ] , bg : [ 1 , 1 , 1 , 0.66 ] , bgDisabled : [ 1 , 1 , 1 , 0.33 ] , bgHover : [ 1 , 1 , 1 , 0.9 ] , fg : [ 0.8 , 0 , 0.8 , 1 ] , fgDisabled : [ 0.8 , 0 , 0.8 , 0.5 ] , fgHover : [ 1 , 0 , 1 , 1 ] , text : [ 0.3 , 0.3 , 0.3 , 1 ] , textDisabled : [ 0.3 , 0.3 , 0.3 , 0.5 ] , textHover : [ 0.2 , 0.2 , 0.4 , 1 ] , bgTooltip : [ 1 , 1 , 0.8 , 0.85 ] , textTooltip : [ 0 , 0 , 0 , 1 ] , } , ] ; const F1 = float ( 1 ) ; const F2 = float ( 2 ) ; const FONT = "10px 'IBM Plex Mono'" ; const RADIO_LABELS = [ "Yes" , "No" , "Maybe" ] ; const RGB_LABELS = [ "R" , "G" , "B" ] ; const RGB_TOOLTIPS = [ "Red" , "Green" , "Blue" ] ; const RADIAL_LABELS = [ "Buttons" , "Slider" , "Dials" , "Dropdown" , "Text" ] ; const THEME_IDS = [ "Default" , "Raspberry" ] ; const mkIcon = ( icon ) => [ "g" , { stroke : "none" , scale : 16 / 32 } , ... iterator ( comp ( mapcat ( ( p ) => pathFromSvg ( p [ 1 ] . d ) ) , map ( normalizedPath ) ) , icon . slice ( 2 ) ) , ] ; const ICON1 = mkIcon ( DOWNLOAD ) ; const ICON2 = mkIcon ( RESTART ) ; interface AppState { uiVisible : boolean ; uiMode : number ; theme : number ; radius : number ; gridW : number ; rgb : number [ ] ; pos : Vec ; txt : string ; toggles : boolean [ ] ; flags : boolean [ ] ; radio : number ; } const DB = new History ( new Atom < AppState > ( { uiVisible : true , uiMode : 0 , theme : 0 , radius : 10 , gridW : 15 , rgb : [ 0.9 , 0.45 , 0.5 ] , pos : < Vec > [ 400 , 140 ] , txt : "Hello there! This is a test, do not panic!" , toggles : new Array < boolean > ( 12 ) . fill ( false ) , flags : [ true , false ] , radio : 0 , } ) , 500 ) ; const themeForID = ( theme ) : Partial < GUITheme > => ( { ... THEMES [ theme % THEMES . length ] , font : FONT , cursorBlink : 0 , } ) ; const setRGB = ( gui , res ) => res !== undefined && ( gui . isAltDown ( ) ? DB . resetIn ( [ "rgb" ] , vecOf ( 3 , res [ 1 ] ) ) : DB . resetIn ( [ "rgb" , res [ 0 ] ] , res [ 1 ] ) ) ; const app = ( ) => { let maxW = 240 ; let size = [ window . innerWidth , window . innerHeight ] ; let radialPos = [ 0 , 0 ] ; let radialActive = false ; const gui = new IMGUI ( { theme : themeForID ( DB . deref ( ) . theme ) } ) ; const bench = step ( sma ( 50 ) ) ; const _canvas = { ... canvas , init ( canv ) { main . add ( merge < any , any > ( { src : [ gestureStream ( canv , { } ) . subscribe ( { next ( e ) { gui . setMouse ( e . pos , e . buttons ) ; } , } ) , fromDOMEvent ( window , "keydown" ) . subscribe ( { next ( e ) { if ( e . key === Key . TAB ) { e . preventDefault ( ) ; } if ( ( e . metaKey || e . ctrlKey ) && e . key . toLowerCase ( ) === "z" ) { e . shiftKey ? DB . redo ( ) : DB . undo ( ) ; } else { gui . setKey ( e ) ; } } , } ) , fromDOMEvent ( window , "keyup" ) . subscribe ( { next ( e ) { gui . setKey ( e ) ; } , } ) , fromDOMEvent ( window , "resize" ) . subscribe ( { next ( ) { maxW = Math . min ( maxW , window . innerWidth - 16 ) ; setC2 ( size , window . innerWidth , window . innerHeight ) ; DB . swapIn ( [ "pos" ] , ( pos ) => min2 ( [ ] , pos , size ) ) ; } , } ) , ] , } ) ) ; } , } ; const updateGUI = ( draw ) => { const state = DB . deref ( ) ; const grid = gridLayout ( 10 , 10 , maxW - 20 , 1 , 16 , 4 ) ; gui . setTheme ( themeForID ( state . theme ) ) ; gui . begin ( draw ) ; gui . beginDisabled ( radialActive ) ; if ( buttonH ( gui , grid , "show" , state . uiVisible ? "Hide UI" : "Show UI" ) ) { DB . resetIn ( [ "uiVisible" ] , ! state . uiVisible ) ; } if ( state . uiVisible ) { let inner ; let inner2 ; let res ; switch ( state . uiMode ) { case 0 : grid . next ( ) ; inner = grid . nest ( 2 ) ; iconButton ( gui , inner , "icon" , ICON1 , 14 , 16 , "Download" , "Icon button" ) ; iconButton ( gui , inner , "icon2" , ICON2 , 13 , 16 , "Restart" , "Icon button" ) ; grid . next ( ) ; textLabel ( gui , grid , "Toggles:" ) ; inner = grid . nest ( 8 ) ; if ( buttonV ( gui , inner , "toggleAll" , 3 , "INVERT" ) ) { DB . swapIn ( [ "toggles" ] , ( toggles ) => toggles . map ( ( x ) => ! x ) ) ; } inner2 = inner . nest ( 4 , [ 7 , 1 ] ) ; for ( let i = 0 ; i < state . toggles . length ; i ++ ) { if ( ( res = toggle ( gui , inner2 , `toggle${ i }` , state . toggles [ i ] , false , `${ i }` ) ) !== undefined ) { DB . resetIn ( [ "toggles" , i ] , res ) ; } } inner = grid . nest ( 2 ) ; gui . beginTheme ( themeForID ( state . theme + 1 ) ) ; if ( ( res = toggle ( gui , inner , "opt1" , state . flags [ 0 ] , false , state . flags [ 0 ] ? "ON" : "OFF" , "Unused" ) ) !== undefined ) { DB . resetIn ( [ "flags" , 0 ] , res ) ; } if ( ( res = toggle ( gui , inner , "opt2" , state . flags [ 1 ] , false , state . flags [ 1 ] ? "ON" : "OFF" , "Unused" ) ) !== undefined ) { DB . resetIn ( [ "flags" , 1 ] , res ) ; } gui . endTheme ( ) ; grid . next ( ) ; gui . beginDisabled ( ) ; textLabel ( gui , grid , "Radio (horizontal):" ) ; radio ( gui , grid , "radio1" , true , state . radio , false , RADIO_LABELS ) ; gui . endDisabled ( ) ; grid . next ( ) ; if ( ( res = gui . withTheme ( themeForID ( state . theme + 1 ) , ( ) => radio ( gui , grid , "radio2" , true , state . radio , true , RADIO_LABELS ) ) ) !== undefined ) { DB . resetIn ( [ "radio" ] , res ) ; } grid . next ( ) ; textLabel ( gui , grid , "Radio (vertical):" ) ; if ( ( res = radio ( gui , grid , "radio3" , false , state . radio , false , RADIO_LABELS ) ) !== undefined ) { DB . resetIn ( [ "radio" ] , res ) ; } grid . next ( ) ; if ( ( res = radio ( gui , grid , "radio4" , false , state . radio , true , RADIO_LABELS ) ) !== undefined ) { DB . resetIn ( [ "radio" ] , res ) ; } break ; case 1 : grid . next ( ) ; textLabel ( gui , grid , "Sliders:" ) ; inner = grid . nest ( 2 ) ; if ( ( res = sliderH ( gui , inner , "grid" , 1 , 20 , 1 , state . gridW , "Grid" , undefined , "Grid size" ) ) !== undefined ) { DB . resetIn ( [ "gridW" ] , res ) ; } if ( ( res = sliderH ( gui , inner , "rad" , 2 , 20 , 1 , state . radius , "Radius" , undefined , "Dot radius" ) ) !== undefined ) { DB . resetIn ( [ "radius" ] , res ) ; } textLabel ( gui , grid , "Slider groups:" ) ; textLabel ( gui , grid , "(Alt + drag to adjust all):" ) ; inner = grid . nest ( 4 ) ; res = sliderVGroup ( gui , inner , "col2" , 0 , 1 , 0.05 , state . rgb , 5 , RGB_LABELS , F2 , RGB_TOOLTIPS ) ; res = sliderVGroup ( gui , inner , "col3" , 0 , 1 , 0.05 , state . rgb , 5 , RGB_LABELS , F2 , RGB_TOOLTIPS ) || res ; res = sliderHGroup ( gui , inner . nest ( 1 , [ 2 , 1 ] ) , "col" , 0 , 1 , 0.05 , false , state . rgb , RGB_LABELS , F2 , RGB_TOOLTIPS ) || res ; setRGB ( gui , res ) ; textLabel ( gui , grid , "2D controller:" ) ; inner = grid . nest ( 4 ) ; res = xyPad ( gui , inner , "xy1" , ZERO2 , size , 10 , state . pos , 3 , false , undefined , undefined , "Origin" ) ; res = xyPad ( gui , inner , "xy2" , ZERO2 , size , 10 , state . pos , 4 , false , undefined , undefined , "Origin" ) || res ; res = xyPad ( gui , inner , "xy3" , ZERO2 , size , 10 , state . pos , - 1 , false , undefined , undefined , "Origin" ) || res ; res = xyPad ( gui , inner , "xy4" , ZERO2 , size , 10 , state . pos , - 2 , false , undefined , undefined , "Origin" ) || res ; res !== undefined && DB . resetIn ( [ "pos" ] , res ) ; break ; case 2 : grid . next ( ) ; textLabel ( gui , grid , "Dials:" ) ; inner = grid . nest ( 6 ) ; res = dialGroup ( gui , inner , "dials1" , 0 , 1 , 0.05 , true , state . rgb , [ ] , F1 , RGB_TOOLTIPS ) ; res = dialGroup ( gui , inner , "dials2" , 0 , 1 , 0.05 , true , state . rgb , [ ] , F1 , RGB_TOOLTIPS ) || res ; setRGB ( gui , res ) ; inner = grid . nest ( 6 ) ; if ( ( res = ring ( gui , inner , "small" , 0 , 1 , 0.05 , state . rgb [ 0 ] , PI , 0.5 , "R" , F2 , "Red" ) ) !== undefined ) { DB . resetIn ( [ "rgb" , 0 ] , res ) ; } if ( ( res = ring ( gui , inner . nest ( 1 , [ 2 , 2 ] ) , "medium" , 0 , 1 , 0.05 , state . rgb [ 1 ] , PI , 0.5 , "G" , F2 , "Green" ) ) !== undefined ) { DB . resetIn ( [ "rgb" , 1 ] , res ) ; } if ( ( res = ring ( gui , inner . nest ( 1 , [ 3 , 3 ] ) , "large" , 0 , 1 , 0.05 , state . rgb [ 2 ] , PI , 0.5 , "B" , F2 , "Blue" ) ) !== undefined ) { DB . resetIn ( [ "rgb" , 2 ] , res ) ; } inner = grid . nest ( 3 ) ; res = ringGroup ( gui , inner , "rings1" , 0 , 1 , 0.05 , true , PI * 0.75 , 0.5 , state . rgb , RGB_LABELS , F2 , RGB_TOOLTIPS ) ; res = ringGroup ( gui , inner , "rings2" , 0 , 1 , 0.05 , true , PI * 0.5 , 0.75 , state . rgb , RGB_LABELS , F2 , RGB_TOOLTIPS ) || res ; res = ringGroup ( gui , inner , "rings3" , 0 , 1 , 0.05 , true , PI * 0.25 , 0.9 , state . rgb , RGB_LABELS , F2 , RGB_TOOLTIPS ) || res ; setRGB ( gui , res ) ; break ; case 3 : grid . next ( ) ; textLabel ( gui , grid , "Select theme:" ) ; if ( ( res = dropdown ( gui , grid , "theme" , state . theme , THEME_IDS , "GUI theme" ) ) !== undefined ) { DB . resetIn ( [ "theme" ] , res ) ; } const box = layoutBox ( 10 , 150 , 150 , 120 , 200 , 24 , 0 ) ; if ( ( res = dropdown ( gui , box , "theme2" , state . theme , THEME_IDS , "GUI theme" ) ) !== undefined ) { DB . resetIn ( [ "theme" ] , res ) ; } break ; case 4 : grid . next ( ) ; textLabel ( gui , grid , "Editable textfield:" ) ; if ( ( res = textField ( gui , grid , "txt" , state . txt , undefined , "Type something..." ) ) !== undefined ) { DB . resetIn ( [ "txt" ] , res ) ; } break ; default : } } gui . endDisabled ( ) ; if ( gui . isControlDown ( ) ) { if ( ! radialActive ) { radialPos = [ ... gui . mouse ] ; } gui . add ( gui . resource ( "radial" , hash ( radialPos ) + 1 , ( ) => [ "g" , { } , [ "radialGradient" , { id : "shadow" , from : radialPos , to : radialPos , r1 : 5 , r2 : 300 , } , [ [ 0 , [ 1 , 1 , 1 , 0.8 ] ] , [ 0.5 , [ 1 , 1 , 1 , 0.66 ] ] , [ 1 , [ 1 , 1 , 1 , 0 ] ] , ] , ] , [ "circle" , { fill : "$shadow" } , radialPos , 300 ] , ] ) ) ; let res ; if ( ( res = radialMenu ( gui , "radial" , radialPos [ 0 ] , radialPos [ 1 ] , 100 , RADIAL_LABELS , [ ] ) ) !== undefined ) { DB . swap ( ( db ) => setInManyUnsafe ( db , [ "uiMode" ] , res , [ "uiVisible" ] , true ) ) ; } gui . add ( textLabelRaw ( add2 ( [ ] , radialPos , [ 0 , 120 ] ) , { fill : "#000" , align : "center" } , "Use cursor keys to navigate" ) , textLabelRaw ( add2 ( [ ] , radialPos , [ 0 , 134 ] ) , { fill : "#000" , align : "center" } , "Click or Enter to switch UI" ) ) ; if ( ! radialActive ) { gui . focusID = gui . hotID ; } radialActive = true ; } else { radialActive = false ; } const [ w , h ] = size ; if ( gui . activeID === NONE && gui . isMouseDown ( ) && Math . abs ( gui . mouse [ 0 ] - maxW ) < 80 ) { maxW = clamp ( gui . mouse [ 0 ] , 240 , w - 16 ) ; } const { key , hotID , activeID , focusID , lastID } = gui ; const statLayout = gridLayout ( 10 , h - 10 - 3 * 14 , w , 1 , 14 , 0 ) ; textLabel ( gui , statLayout , `Key: ${ key }` ) ; textLabel ( gui , statLayout , `Focus: ${ focusID } / ${ lastID }` ) ; textLabel ( gui , statLayout , `IDs: ${ hotID || "none" } / ${ activeID || "none" }` ) ; gui . end ( ) ; } ; return ( ) => { const width = window . innerWidth ; const height = window . innerHeight ; const t = < number > bench ( timedResult ( ( ) => { updateGUI ( false ) ; updateGUI ( true ) ; } ) [ 1 ] ) ; t != null && gui . add ( textLabelRaw ( [ 10 , height - 10 - 4 * 14 ] , "#ff0" , `GUI time: ${ F2 ( t ) }ms` ) ) ; return [ _canvas , { width , height , style : { background : gui . theme . globalBg , cursor : gui . cursor } , oncontextmenu : ( e ) => e . preventDefault ( ) , ... gui . attribs , } , line ( [ maxW , 0 ] , [ maxW , height ] , { stroke : "#000" } ) , [ "text" , { transform : [ 0 , - 1 , 1 , 0 , maxW + 12 , height / 2 ] , fill : "#000" , font : FONT , align : "center" , } , [ 0 , 0 ] , "DRAG TO RESIZE" , ] , gui , ] ; } ; } ; const main = sync ( { src : { state : fromAtom ( DB ) , } , } ) ; sidechainPartitionRAF ( main ) . transform ( map ( app ( ) ) , updateDOM ( ) ) ;