rem This file contains a simple GUI system written using the DX11 Plugin.
rem As such the functions in this file demonstrate some use of 2D with the DX11 plugin.
rem At the current point in time it only contains two types of components; checkboxes and sliders
rem (labels are already provided as an integral part of the plugin itself). More components could 
rem theoretically be added to extend the system if desired.
rem The GUI system uses a kind of pseudo-object oriented style with backwards dependencies where 
rem parent "classes" (the components) have to be aware of all its subclasses in order to delegate
rem work. This is implemented using arrays and UDT's so only the rendering is dependent on the DX11 plugin.
rem
rem                                                                 - Joel Sjöqvist ("Rudolpho") 2014-07-24


rem Component types
#constant GUI_CTYPE_BASE            0
#constant GUI_CTYPE_CHECKBOX        1
#constant GUI_CTYPE_SLIDER          2


rem **********************************
rem * Globals                        *
rem **********************************
global gGui_Image                   as dword
global gGui_Layer                   as dword
global gGui_ActiveComponent         as integer
global gGui_LastMousePosX           as integer
global gGui_LastMousePosY           as integer

rem Master component type; contains data that is used by all specialized components and keeps track of the associated
rem specialized component's type and array index for instance-specific data
type sGuiComponent
    rem These fields are common to all gui components
    posX                as integer
    posY                as integer
    width               as dword
    height              as integer
    rem Tells what type of component this is and indexes into its respective array
    componentType       as byte
    arrId               as dword
endtype

rem Specialized data for checkbox components
type sGuiCheckbox
    spr                 as dword
    lbl                 as dword
    isChecked           as boolean
endtype

rem Specialized data for slider components
type sGuiSlider
    minValue            as integer
    maxValue            as integer
    value               as integer
    actualWidth         as dword
    
    sprBGL              as dword
    sprBGM              as dword
    sprBGR              as dword
    sprFGL              as dword
    sprFGM              as dword
    sprFGR              as dword
    sprFill             as dword
    sprThumb            as dword
    
    lbl                 as dword
    txt                 as string
endtype


rem Initiates the global GUI components
function InitGUI()
    rem Use the gui image sheet as a check of whether the GUI system has already been set up
    if gGui_Image <> nullptr then exitfunction
    
    gGui_Image = dx11 load image("media/gui.png", false)
    gGui_Layer = dx11 get gui sprite layer()
    
    gGui_ActiveComponent    = -1    ` None yet
    gGui_LastMousePosX      = dx11 get mouseX()
    gGui_LastMousePosY      = dx11 get mouseY()
    
    rem Component lists
    global dim gGui_Components(-1) as sGuiComponent
    global dim gGui_Checkboxes(-1) as sGuiCheckbox
    global dim gGui_Sliders(-1)    as sGuiSlider
endfunction

rem ******************************************
rem * Functions global to all GUI components *
rem ******************************************
rem Called for each new Component creation; responsible for setting up data for it in the gGui_Components list
function guiCreateComponent(compType as byte, id as dword, posX as integer, posY as integer, width as dword, height as dword)
    array insert at bottom gGui_Components()
    cId = array count(gGui_Components())
    
    gGui_Components(cId).componentType  = compType
    gGui_Components(cId).arrId          = id
    gGui_Components(cId).posX           = posX
    gGui_Components(cId).posY           = posY
    gGui_Components(cId).width          = width
    gGui_Components(cId).height         = height
    
    rem The rest of the component-specific setup should be done by its corresponding initialization function
endfunction cId

rem Returns true if the given screen coordinates are within the bounding box of the specified component, or false otherwise
function guiComponent_Contains(id as dword, x as integer, y as integer)
    rem Can specialize this for individual component types
    select gGui_Components(id).componentType
        case GUI_CTYPE_SLIDER
            state = guiSlider_Contains(id, x, y)
        endcase
        
        case default
            rem This behaviour will do for other component types
            state = x >= gGui_Components(id).posX and x <= gGui_Components(id).posX + gGui_Components(id).width and y >= gGui_Components(id).posY and y <= gGui_Components(id).posY + gGui_Components(id).height
        endcase
    endselect
endfunction state

rem OnClick-event; will delegate this event to the appropriate component-specific function if available
function guiComponent_OnClick(id as dword, x as integer, y as integer)
    rem Delegate to the appropriate OnClick()-function based on component type
    select gGui_Components(id).componentType
        case GUI_CTYPE_CHECKBOX
            guiCheckbox_OnClick(id, x, y)
        endcase
        
        case GUI_CTYPE_SLIDER
            guiSlider_OnClick(id, x, y)
        endcase
    endselect
endfunction

rem OnMouseDrag-event; will delegate this event to the appropriate component-specific function if available
function guiComponent_OnMouseDrag(id as dword, deltaX as integer, deltaY as integer)
    select gGui_Components(id).componentType
        case GUI_CTYPE_CHECKBOX
            rem The checkbox component currently doesn't use this event
        endcase
        
        case GUI_CTYPE_SLIDER
            guiSlider_OnMouseDrag(id, deltaX, deltaY)
        endcase
    endselect
endfunction

rem OnMouseRelease-event; will delegate this event to the appropriate component-specific function if available
function guiComponent_OnMouseRelease(id as dword, x as integer, y as integer)
    select gGui_Components(id).componentType
        case GUI_CTYPE_CHECKBOX
            rem The checkbox component currently doesn't use this event
        endcase
        
        case GUI_CTYPE_SLIDER
            rem Neither does the slider component
        endcase
    endselect
endfunction


rem Updates all existing GUI components
function UpdateGUI(acceptInput as boolean)
    mx = dx11 get mouseX()
    my = dx11 get mouseY()
    
    rem If the left mouse button is held down or released the events should go to the currently active component
    if dx11 is mouse button released(0) and gGui_ActiveComponent >= 0
        rem The LMB was just released; inform the active component and inactivate it (active here means it is being currently interacted with, not "has focus")
        guiComponent_OnMouseRelease(gGui_ActiveComponent, mx, my)
        gGui_ActiveComponent = -1
    endif
    if dx11 is mouse button down(0) = true and dx11 is mouse button hit(0) = false and gGui_ActiveComponent >= 0
        rem The LMB is held down and it wasn't first pressed down in this frame; update mouse dragging info for the acvtive component
        guiComponent_OnMouseDrag(gGui_ActiveComponent, mx - gGuiLastMousePosX, my - gGuiLastMousePosY)
    endif
    
    rem Traverse all existing gui components
    rem If no component is active one can be activated by clicking within its bounding box (or whatever areA its Contains()- function denotes).
    count = array count(gGui_Components())
    for c = 0 to count
        if acceptInput
            rem Clicking on the current component; raise an OnClick event and set it as active
            if dx11 is mouse button hit(0) and guiComponent_Contains(c, mx, my)
                gGui_ActiveComponent = c
                guiComponent_OnClick(c, mx, my)
            endif
        endif
    next c
    
    rem Store mouse positions so that we can determine delta values in the next call
    gGui_LastMousePosX = mx
    gGui_LastMousePosY = my
endfunction


rem ******************************************
rem * Checkbox-related functions             *
rem ******************************************
rem Creates a new Checkbox component and returns its id
function guiCreateCheckbox(posX as integer, posY as integer, prompt as string)
    array insert at bottom gGui_Checkboxes()
    id  = array count(gGui_Checkboxes())
    cId = guiCreateComponent(GUI_CTYPE_CHECKBOX, id, posX, posY, 18, 18)    ` The size is that of the used graphics for the checkbox
    
    rem Setup local data
    gGui_Checkboxes(id).spr         = dx11 create sprite(gGui_Image, posX, posY, 2, gGui_Layer)
    gGui_Checkboxes(id).lbl         = dx11 create label(prompt, posX + 24, posY + 6)
    gGui_Checkboxes(id).isChecked   = false
    rem Set to show the correct part of the gui master image sheet
    dx11 set sprite uv gGui_Checkboxes(id).spr, 0, 0, GetImageU(gGui_Image, 18), 0, 0, GetImageV(gGui_Image, 18), GetImageU(gGui_Image, 18), GetImageV(gGui_Image, 18)
    dx11 set sprite size gGui_Checkboxes(id).spr, 18, 18
endfunction cId

rem Checks / unchecks the given checkbox component
function guiCheckbox_SetChecked(id as dword, check as boolean)
    sId = gGui_Components(id).arrId
    gGui_Checkboxes(sId).isChecked = check
    left#   = GetImageU(gGui_Image, 20 * gGui_Checkboxes(sId).isChecked)
    right#  = GetImageU(gGui_Image, (20 * gGui_Checkboxes(sId).isChecked) + 18)
    top#    = 0.0
    bottom# = GetImageV(gGui_Image, 18)
    dx11 set sprite uv gGui_Checkboxes(sId).spr, left#, top#, right#, top#, left#, bottom#, right#, bottom#
endfunction

rem Returns whether the given checkbox component is checked
function guiCheckbox_IsChecked(id as dword)
    checked = gGui_Checkboxes(gGui_Components(id).arrId).isChecked
endfunction checked


rem Checkbox component-specific OnClick-event
function guiCheckbox_OnClick(id as dword, x as integer, y as integer)
    rem Toggle checked state
    prevState = gGui_Checkboxes(gGui_Components(id).arrId).isChecked = false
    guiCheckbox_SetChecked(id, prevState)
endfunction

rem ******************************************
rem * Slider-related functions               *
rem ******************************************
rem Creates a new Slider component and returns its id
function guiCreateSlider(posX as integer, posY as integer, width as dword, height as dword, label as string, minValue as integer, maxValue as integer, startValue as integer)
    array insert at bottom gGui_Sliders()
    id  = array count(gGui_Sliders())
    cId = guiCreateComponent(GUI_CTYPE_SLIDER, id, posX, posY, width, height)
    
    rem Setup local data
    gGui_Sliders(id).minValue       = minValue
    gGui_Sliders(id).maxValue       = maxValue
    gGui_Sliders(id).value          = startValue
    gGui_Sliders(id).actualWidth    = width - 8     ` Don't include rounded corners of 4 pixels at each side
    
    gGui_Sliders(id).sprBGL         = dx11 create sprite(gGui_Image, posX, posY + 12, 5, gGui_Layer)
    gGui_Sliders(id).sprBGM         = dx11 create sprite(gGui_Image, posX + 4, posY + 12, 5, gGui_Layer)
    gGui_Sliders(id).sprBGR         = dx11 create sprite(gGui_Image, posX + width - 4 , posY + 12, 5, gGui_Layer)
    gGui_Sliders(id).sprFGL         = dx11 create sprite(gGui_Image, posX, posY + 12, 3, gGui_Layer)
    gGui_Sliders(id).sprFGM         = dx11 create sprite(gGui_Image, posX + 4, posY + 12, 3, gGui_Layer)
    gGui_Sliders(id).sprFGR         = dx11 create sprite(gGui_Image, posX + width - 4, posY + 12, 3, gGui_Layer)
    gGui_Sliders(id).sprFill        = dx11 create sprite(gGui_Image, posX + 4, posY + 12, 4, gGui_Layer)
    gGui_Sliders(id).sprThumb       = dx11 create sprite(gGui_Image, posX + 4, posY + 15, 2, gGui_Layer)
    
    gGui_Sliders(id).lbl            = dx11 create label(label, posX, posY)
    gGui_Sliders(id).txt            = label
    
    rem Setup sprites
    SetSpriteToSubImage(gGui_Sliders(id).sprBGL,    0, 20, 4, 5)
    SetSpriteToSubImage(gGui_Sliders(id).sprBGM,    4, 20, 1, 5)
    SetSpriteToSubImage(gGui_Sliders(id).sprBGR,    5, 20, 4, 5)
    SetSpriteToSubImage(gGui_Sliders(id).sprFGL,    0, 25, 4, 5)
    SetSpriteToSubImage(gGui_Sliders(id).sprFGM,    4, 25, 1, 5)
    SetSpriteToSubImage(gGui_Sliders(id).sprFGR,    5, 25, 4, 5)
    SetSpriteToSubImage(gGui_Sliders(id).sprFill,   0, 30, 1, 5)
    SetSpriteToSubImage(gGui_Sliders(id).sprThumb,  20, 20, 15, 15)
    
    rem Rescale indicator sprites horizontally
    dx11 set sprite size gGui_Sliders(id).sprBGM, gGui_Sliders(id).actualWidth, dx11 get sprite height(gGui_Sliders(id).sprBGM)
    dx11 set sprite size gGui_Sliders(id).sprFGM, gGui_Sliders(id).actualWidth, dx11 get sprite height(gGui_Sliders(id).sprFGM)
    dx11 set sprite size gGui_Sliders(id).sprFill, gGui_Sliders(id).actualWidth, dx11 get sprite height(gGui_Sliders(id).sprFill)
    
    rem Calling guiSlider_SetValue() with the given start value will update the size and positions of the fill and thumb sprites accordingly
    guiSlider_SetValue(cId, startValue)
endfunction cId

rem Sets the current value of a slider component
function guiSlider_SetValue(id as dword, v as integer)
    cId     = gGui_Components(id).arrId
    maxVal  = gGui_Sliders(cId).maxValue
    minVal  = gGui_Sliders(cId).minValue
    
    rem Ensure the value is in range and set it
    v = max(minVal, min(v, maxVal))
    gGui_Sliders(cId).value = v
    rem Percentage value in the given range
    p# = (v - minVal) / (1.0 * (maxVal - minVal))
    
    rem Update visuals
    hPos = p# * gGui_Sliders(cId).actualWidth
    dx11 set sprite size gGui_Sliders(cId).sprFill, max(hPos, 1), dx11 get sprite height(gGui_Sliders(cId).sprFill)     ` Sprite sizes must be > 0, hence the MAX() call
    dx11 set sprite position gGui_Sliders(cId).sprThumb, dx11 get sprite position x(gGui_Sliders(cId).sprFill) + hPos, dx11 get sprite position y(gGui_Sliders(cId).sprThumb), true
    dx11 set label text gGui_Sliders(cId).lbl, gGui_Sliders(cId).txt + ": " + str$(v)
endfunction

rem Retrieves the current value of a slider component
function guiSlider_GetValue(id as dword)
    v = gGui_Sliders(gGui_Components(id).arrId).value
endfunction v

rem Retrieves the minimum value of a slider component
function guiSlider_GetMinValue(id as dword)
    v = gGui_Sliders(gGui_Components(id).arrId).minValue
endfunction v

rem Retrieves the maximum value of a slider component
function guiSlider_GetMaxValue(id as dword)
    v = gGui_Sliders(gGui_Components(id).arrId).maxValue
endfunction v

rem We need a specialized Contains() function for the slider components due to
rem the difference in thumb and bar sizes
function guiSlider_Contains(id as dword, x as integer, y as integer)
    cId = gGui_Components(id).arrId
    
    rem Over the thumb sprite? That one is higher than the rest of the control
    sprThumb = gGui_Sliders(cId).sprThumb
    tw = dx11 get sprite width(sprThumb) / 2
    th = dx11 get sprite height(sprThumb) / 2
    tx = dx11 get sprite position x(sprThumb)
    ty = dx11 get sprite position y(sprThumb)
    if x >= tx - tw and x <= tx + tw and y >= ty - th and y <= ty + th then exitfunction true
    
    rem Are we over the actualWidth part of the bar then?
    sprFill = gGui_Sliders(cId).sprFill
    xStart = dx11 get sprite position x(sprFill)
    yStart = dx11 get sprite position y(sprFill)
    height = dx11 get sprite height(sprFill)
    result = y >= yStart and y <= yStart + height and x >= xStart and x <= xStart + gGui_Sliders(cId).actualWidth
endfunction result

rem Slider component-specific OnClick event
function guiSlider_OnClick(id as dword, x as integer, y as integer)
    rem This event should only ever be raised if the provided screen coordinates actually fall within this slider's
    rem visual representation (ie. guiSlider_Contains(id, x, y) returns true).
    rem As such we can convert the x-coordinate to a component-relative value that
    rem can be used to set the current value of the slider.
    cId     = gGui_Components(id).arrId
    sprFill = gGui_Sliders(cId).sprFill
    hPos    = x - dx11 get sprite position x(sprFill)
    p#      = hPos / (gGui_Sliders(cId).actualWidth * 1.0)
    
    guiSlider_SetValue(id, gGui_Sliders(cId).minValue + (p# * (gGui_Sliders(cId).maxValue - gGui_Sliders(cId).minValue)))
endfunction

rem The OnMouseDrag event is used by the slider to update its value by dragging the mouse 
rem cursor horizontally while the left mouse button is held down and it was initially clicked
rem over the slider component.
function guiSlider_OnMouseDrag(id as dword, deltaX as integer, deltaY as integer)
    cId      = gGui_Components(id).arrId
    sprFill  = gGui_Sliders(cId).sprFill
    sprThumb = gGui_Sliders(cId).sprThumb
    rem It is perfectly valid to drag the mouse outside the actual width of the slider component; 
    rem we just clamp the x coordinate to the valid pixel range.
    mx = max(dx11 get sprite position x(sprFill), min(dx11 get mouseX(), dx11 get sprite position x(sprFill) + gGui_Sliders(cId).actualWidth))
    
    rem Transform X coordinate into percentage of full width and then convert that to the currently represented value by the visuals
    hPos = mx - dx11 get sprite position x(sprFill)
    p#   = hPos / (1.0 * gGui_Sliders(cId).actualWidth)
    guiSlider_SetValue(id, gGui_Sliders(cId).minValue + (p# * (gGui_Sliders(cId).maxValue - gGui_Sliders(cId).minValue)))
endfunction

rem ******************************************


rem Helper functions for calculating percentage dimensions from pixel dimensions
function GetImageU(img as dword, xPixel as dword)
    u# = xPixel * 1.0 / dx11 get image width(img)
endfunction u#

function GetImageV(img as dword, yPixel as dword)
    v# = yPixel * 1.0 / dx11 get image height(img)
endfunction v#

rem Helper function that sets the given sprite to display the designated pixel area
rem of ots associated image and sets the sprite to be the same pixel size as this 
rem image sub-area.
function SetSpriteToSubImage(spr as dword, x as dword, y as dword, width as dword, height as dword)
    img     = dx11 get sprite image(spr)
    top#    = GetImageV(img, y)
    left#   = GetImageU(img, x)
    bottom# = GetImageV(img, y + height)
    right#  = GetImageU(img, x + width)
    dx11 set sprite uv spr, left#, top#, right#, top#, left#, bottom#, right#, bottom#
    dx11 set sprite size spr, width, height
endfunction
