import type { Fn0 , IClear , IToHiccup } from "@thi.ng/api" ; import type { Vec } from "@thi.ng/vectors" ; import { set2 } from "@thi.ng/vectors/set" ; import { DEFAULT_THEME , GUITheme , Hash , IMGUIOpts , Key , KeyModifier , MouseButton , NONE , } from "./api.js" ; export class IMGUI implements IClear , IToHiccup { attribs ! : any ; layers : any [ ] ; mouse : Vec ; buttons : number ; key ! : string ; modifiers : number ; prevMouse : Vec ; prevButtons : number ; prevKey ! : string ; prevModifiers : number ; hotID : string ; activeID : string ; focusID : string ; lastID : string ; cursor ! : string ; t0 : number ; time ! : number ; draw : boolean ; protected currIDs : Set < string > ; protected prevIDs : Set < string > ; protected themeStack ! : GUITheme [ ] ; protected disabledStack ! : boolean [ ] ; protected resources : Map < string , Map < Hash , any > > ; protected states : Map < string , any > ; protected sizes : Map < string , any > ; constructor ( opts ) { this . mouse = [ - 1e3 , - 1e3 ] ; this . prevMouse = [ - 1e3 , - 1e3 ] ; this . key = this . prevKey = "" ; this . buttons = this . prevButtons = this . modifiers = this . prevModifiers = 0 ; this . hotID = this . activeID = this . focusID = this . lastID = "" ; this . currIDs = new Set < string > ( ) ; this . prevIDs = new Set < string > ( ) ; this . resources = new Map < string , Map < Hash , any > > ( ) ; this . sizes = new Map < string , Hash > ( ) ; this . states = new Map < string , any > ( ) ; this . layers = [ [ ] , [ ] ] ; this . attribs = { } ; this . disabledStack = [ false ] ; this . setTheme ( opts . theme || { } ) ; this . draw = true ; this . t0 = Date . now ( ) ; } get theme ( ) { const stack = this . themeStack ; return stack [ stack . length - 1 ] ; } get disabled ( ) { const stack = this . disabledStack ; return stack [ stack . length - 1 ] ; } /**
     * Clears all shape layers and resets theme / disabled stacks.
     */ clear ( ) { this . layers [ 0 ] . length = 0 ; this . layers [ 1 ] . length = 0 ; this . themeStack . length = 1 ; this . disabledStack . length = 1 ; } param p param buttons setMouse ( p , buttons ) { set2 ( this . prevMouse , this . mouse ) ; set2 ( this . mouse , p ) ; this . prevButtons = this . buttons ; this . buttons = buttons ; return this ; } param e setKey ( e ) { if ( e . type === "keydown" ) { this . prevKey = this . key ; this . key = e . key ; } this . prevModifiers = this . modifiers ; this . modifiers = ( ~ ~ e . shiftKey * KeyModifier . SHIFT ) | ( ~ ~ e . ctrlKey * KeyModifier . CONTROL ) | ( ~ ~ e . metaKey * KeyModifier . META ) | ( ~ ~ e . altKey * KeyModifier . ALT ) ; return this ; } /**
     * Merges given theme settings with  DEFAULT_THEME and resets theme
     * stack.
     *
     *  param theme setTheme ( theme ) { this . themeStack = [ { ... DEFAULT_THEME , ... theme } ] ; } param theme beginTheme ( theme ) { const stack = this . themeStack ; stack . push ( { ... stack [ stack . length - 1 ] , ... theme } ) ; } /**
     * Removes current theme from stack (unless only one theme left).
     */ endTheme ( ) { popIfNotLast ( this . themeStack ) ; } param theme param comp withTheme < T > ( theme , comp ) { this . beginTheme ( theme ) ; const res = comp ( ) ; this . themeStack . pop ( ) ; return res ; } param disabled beginDisabled ( disabled = true ) { this . disabledStack . push ( disabled ) ; } /**
     * Removes current disabled flag from stack (unless only one theme left).
     */ endDisabled ( ) { popIfNotLast ( this . disabledStack ) ; } param disabled param comp withDisabled < T > ( disabled , comp ) { this . disabledStack . push ( disabled ) ; const res = comp ( ) ; this . disabledStack . pop ( ) ; return res ; } param id requestFocus ( id ) { if ( this . disabled ) return false ; if ( this . focusID === "" || this . activeID === id ) { this . focusID = id ; return true ; } return this . focusID === id ; } /**
     * Attempts to switch focus to next, or if Shift is pressed, to
     * previous component. This is meant be called ONLY from component
     * key handlers.
     */ switchFocus ( ) { this . focusID = this . isShiftDown ( ) ? this . lastID : "" ; this . key = "" ; } /**
     * Returns true if left mouse button is pressed.
     */ isMouseDown ( ) { return ( this . buttons & MouseButton . LEFT ) > 0 ; } isShiftDown ( ) { return ( this . modifiers & KeyModifier . SHIFT ) > 0 ; } isControlDown ( ) { return ( this . modifiers & KeyModifier . CONTROL ) > 0 ; } isMetaDown ( ) { return ( this . modifiers & KeyModifier . META ) > 0 ; } isAltDown ( ) { return ( this . modifiers & KeyModifier . ALT ) > 0 ; } isPrevMouseDown ( ) { return ( this . prevButtons & MouseButton . LEFT ) > 0 ; } isPrevShiftDown ( ) { return ( this . prevModifiers & KeyModifier . SHIFT ) > 0 ; } isPrevControlDown ( ) { return ( this . prevModifiers & KeyModifier . CONTROL ) > 0 ; } isPrevMetaDown ( ) { return ( this . prevModifiers & KeyModifier . META ) > 0 ; } isPrevAltDown ( ) { return ( this . prevModifiers & KeyModifier . ALT ) > 0 ; } param draw begin ( draw = true ) { this . hotID = "" ; this . cursor = "default" ; this . draw = draw ; this . clear ( ) ; this . time = ( Date . now ( ) - this . t0 ) * 1e-3 ; } /**
     * Performs end-of-frame handling & component cache cleanup. Also
     * removes cached state and resources of all unused components.
     */ end ( ) { if ( ! this . buttons ) { this . activeID = "" ; } else { if ( this . activeID === "" ) { this . activeID = NONE ; this . focusID = NONE ; this . lastID = "" ; } } this . key === Key . TAB && ( this . focusID = "" ) ; this . key = "" ; this . gc ( ) ; } /**
     * Garbage collect unused component state / resources.
     */ gc ( ) { const { currIDs , prevIDs } = this ; for ( let id of prevIDs ) { if ( ! currIDs . has ( id ) ) { this . resources . delete ( id ) ; this . sizes . delete ( id ) ; this . states . delete ( id ) ; } } this . prevIDs = currIDs ; this . currIDs = prevIDs ; prevIDs . clear ( ) ; } bgColor ( hover ) { return this . disabled ? this . theme . bgDisabled : hover ? this . theme . bgHover : this . theme . bg ; } fgColor ( hover ) { return this . disabled ? this . theme . fgDisabled : hover ? this . theme . fgHover : this . theme . fg ; } textColor ( hover ) { return this . disabled ? this . theme . textDisabled : hover ? this . theme . textHover : this . theme . text ; } focusColor ( id ) { return this . focusID === id ? this . theme . focus : undefined ; } param txt textWidth ( txt ) { return this . theme . charWidth * txt . length ; } param id param hash registerID ( id , hash ) { this . currIDs . add ( id ) ; if ( this . sizes . get ( id ) !== hash ) { this . sizes . set ( id , hash ) ; this . resources . delete ( id ) ; } } /**
     * Attempts to retrieve cached resource for given component `id` and
     * resource `hash`. If unsuccessful, calls resource `ctor` function
     * to create it, caches result and returns it.
     *
     *  IMGUI . registerID param id param hash param ctor resource < T > ( id , hash , ctor ) { let res ; let c = this . resources . get ( id ) ; ! c && this . resources . set ( id , ( c = new Map ( ) ) ) ; return c . get ( hash ) || ( c . set ( hash , ( res = ctor ( ) ) ) , res ) ; } param id param ctor state < T > ( id , ctor ) { let res = this . states . get ( id ) ; return res !== undefined ? res : ( this . states . set ( id , ( res = ctor ( ) ) ) , res ) ; } param id param state setState ( id , state ) { this . states . set ( id , state ) ; } param id setCursor ( id ) { this . cursor = id ; } add ( ... els ) { this . layers [ 0 ] . push ( ... els ) ; } addOverlay ( ... els ) { this . layers [ 1 ] . push ( ... els ) ; } /**
     * Returns hiccup representation of all shapes/text primitives
     * created by components in the current frame.
     */ toHiccup ( ) { return [ "g" , { font : this . theme . font } , ... this . layers [ 0 ] , ... this . layers [ 1 ] , ] ; } } const popIfNotLast = ( stack ) => stack . length > 1 && stack . pop ( ) ;