;
; ============================================================
;  NeoOverlay - Rectangle-to-Rectangle Layer & Blend Plug-In
;  for VisualNEO Win (PureBasic x86 Shared DLL)
;
;  Renders PNG layers into VisualNEO Rectangle hosts using
;  layered child windows and UpdateLayeredWindow.  True blend
;  modes (Screen / Overlay / Multiply ...) composite one layer
;  buffer over another (rectangle-to-rectangle).
;
;  Tint presets preserved exactly from NeoDX.
;
;  Build:
;    PureBasic  -> Shared DLL, 32-bit (x86), #PB_Ascii
;    Rename .dll -> .nbp   for VisualNEO Install Plug-Ins.
; ============================================================

EnableExplicit

; ============================================================
;  Constants
; ============================================================

; VisualNEO action parameter types
#ACTIONPARAM_NONE    = 0
#ACTIONPARAM_ALPHA   = 1
#ACTIONPARAM_NUMERIC = 3
#ACTIONPARAM_FILENAME = 5

; VisualNEO message codes
#NBMSG_ENTER_RUNMODE = 1
#NBMSG_EXIT_RUNMODE  = 2
#NBMSG_DEACTIVATE    = 3
#NBMSG_ACTIVATE      = 4
#NBMSG_MOVESIZE      = 5
#NBMSG_PAGECHANGE    = 6
#NBMSG_MINIMIZE      = 7
#NBMSG_RESTORE       = 8

; Layout modes (blueprint: Original / Stretch / Fit / Fill / Tile)
#LAYOUT_ORIGINAL = 0
#LAYOUT_STRETCH  = 1
#LAYOUT_FIT      = 2
#LAYOUT_FILL     = 3
#LAYOUT_TILE     = 4

; Blend modes
#BLEND_NORMAL   = 0
#BLEND_MULTIPLY = 1
#BLEND_SCREEN   = 2
#BLEND_OVERLAY  = 3
#BLEND_DARKEN   = 4
#BLEND_LIGHTEN  = 5
#BLEND_ADD      = 6
#BLEND_SUBTRACT = 7

; GDI+ interpolation
#InterpolationModeHighQualityBicubic = 7

; ============================================================
;  Action IDs
; ============================================================
Enumeration 1
  #Action_Layer_CreateUpdate    ; 1
  #Action_Layer_ShowHide        ; 2
  #Action_Layer_Destroy         ; 3
  #Action_Layer_SetTint         ; 4
  #Action_Blend_Link            ; 5
  #Action_Blend_Unlink          ; 6
  #Action_Layer_SetOpacity      ; 7
  #Action_Overlay_Pause         ; 8
  #Action_Layer_ScreenKey       ; 9
EndEnumeration


; ============================================================
;  GDI+ imports
; ============================================================
Structure GdiplusStartupInput
  GdiplusVersion.l
  DebugEventCallback.i
  SuppressBackgroundThread.l
  SuppressExternalCodecs.l
EndStructure

Import "gdiplus.lib"
  GdiplusStartup(*token, *input.GdiplusStartupInput, *output)
  GdiplusShutdown(token)
  GdipCreateBitmapFromFile(*filenameW, *bitmap)
  GdipDisposeImage(image)
  GdipGetImageWidth(image, *width)
  GdipGetImageHeight(image, *height)
  GdipCreateFromHDC(hdc, *graphics)
  GdipDeleteGraphics(graphics)
  GdipSetInterpolationMode(graphics, interpolationMode)
  GdipDrawImageRectI(graphics, image, x, y, width, height)
  GdipCreateMatrix(*matrix)
  GdipDeleteMatrix(matrix)
  GdipTranslateMatrix(matrix, offsetX.f, offsetY.f, order.l)
  GdipRotateMatrix(matrix, angle.f, order.l)
  GdipSetWorldTransform(graphics, matrix)
  GdipResetWorldTransform(graphics)
EndImport

; GDI+ matrix order constants
#MatrixOrderPrepend = 0
#MatrixOrderAppend  = 1

; Custom window messages for inter-plugin communication (Animator <-> Overlay)
#WM_NEOOVERLAY_OPACITY  = $8001  ; WM_APP + 1: wParam = opacity 0-255
#WM_NEOOVERLAY_ROTATION = $8002  ; WM_APP + 2: wParam = angle in tenths of degrees (signed)

; ============================================================
;  Global state
; ============================================================
Global g_nbWinHandle.i
Global g_nbAddAction.i
Global g_nbAddFile.i
Global g_nbGetVar.i
Global g_nbSetVar.i
Global g_nbInterface.i
Global g_nbPlayAction.i
Global g_hInstance.i

Global g_gdiplusToken.i
Global g_gdiplusStarted.i

; Metadata string pointers
Global *gPluginTitle, *gPluginAuthor, *gPluginHint
Global *gAct1_Name, *gAct1_Hint
Global *gAct2_Name, *gAct2_Hint
Global *gAct3_Name, *gAct3_Hint
Global *gAct4_Name, *gAct4_Hint
Global *gAct5_Name, *gAct5_Hint
Global *gAct6_Name, *gAct6_Hint
Global *gAct7_Name, *gAct7_Hint
Global *gAct8_Name, *gAct8_Hint
Global *gAct9_Name, *gAct9_Hint

Structure TActionParams
  p.b[32]
EndStructure

; ============================================================
;  Layer structure - one per VisualNEO Rectangle
; ============================================================
Structure NeoLayer
  rectName.s             ; VisualNEO Rectangle name
  hHost.i                ; host Rectangle HWND
  hOverlay.i             ; layered overlay popup HWND

  ; PNG source
  pngPath.s
  gdipImg.i              ; GDI+ Image handle
  imgW.i                 ; natural PNG width
  imgH.i                 ; natural PNG height

  ; Layout
  layoutMode.i           ; LAYOUT_ORIGINAL / STRETCH / FIT / FILL / TILE
  offsetX.i
  offsetY.i

  ; Opacity
  opacity.i              ; 0..255

  ; Tint
  tintPreset.s           ; preset name (or "Custom")
  tintR.i                ; 0..255  (255=neutral for multiplicative)
  tintG.i
  tintB.i
  tintStrength.i         ; 0..255  (0=no tint)

  ; Cached ARGB DIB (layer buffer)
  hDIB.i                 ; HBITMAP  (32-bit top-down DIB section)
  *dibBits               ; pointer to pixel data
  dibW.i
  dibH.i
  dirty.i                ; 1 = needs re-render

  ; Size tracking (for resize detection in _nbMessage)
  lastHostW.i
  lastHostH.i

  visible.i              ; 0/1
  blendHidden.i          ; 1 = hidden by BlendRender (source of a blend link)
  origHostProc.i         ; original WndProc of host Rectangle (for subclass removal)
  rotation.f             ; rotation angle in degrees (set by Animator via WM_NEOOVERLAY_ROTATION)
EndStructure

; ============================================================
;  BlendLink structure - one per output rectangle
; ============================================================
Structure NeoBlendLink
  topRectName.s          ; source layer rect (effect/overlay)
  blendMode.i            ; BLEND_*
  outputRectName.s       ; resolved output rect name
  opacityOverride.i      ; -1 = use top layer opacity, 0..255 = override
  dirty.i
EndStructure

; ============================================================
;  Global maps
; ============================================================
Global NewMap gLayers.NeoLayer()          ; key = LCase(RectName)
Global NewMap gBlendLinks.NeoBlendLink()  ; key = LCase(OutputRectName)
Global gRestoring.i = 0                   ; flag to skip host visibility check during restore
Global gReplaying.i = 0                   ; flag to suppress history recording during replay

; Command history for restore replay
Structure CmdRecord
  action.i          ; action enum
  p0.s : p1.s : p2.s : p3.s : p4.s : p5.s : p6.s
EndStructure
Global NewList gCmdHistory.CmdRecord()

; Forward declarations
Declare LayerRender(*layer.NeoLayer)
Declare LayerPresent(*layer.NeoLayer)
Declare LayerReposition(*layer.NeoLayer)
Declare BlendRender(outputKey.s)

; ============================================================
;  Command history - record/prune/replay for RESTORE
; ============================================================

Procedure RecordCmd(action.i, p0.s, p1.s="", p2.s="", p3.s="", p4.s="", p5.s="", p6.s="")
  If gReplaying : ProcedureReturn : EndIf

  Protected key.s = LCase(Trim(p0))

  Select action
    Case #Action_Layer_Destroy
      ; Remove ALL commands for this layer
      ForEach gCmdHistory()
        If LCase(Trim(gCmdHistory()\p0)) = key
          DeleteElement(gCmdHistory())
        EndIf
      Next
      ; Also remove blend links referencing this layer as effect (p2)
      ForEach gCmdHistory()
        If gCmdHistory()\action = #Action_Blend_Link And LCase(Trim(gCmdHistory()\p2)) = key
          DeleteElement(gCmdHistory())
        EndIf
      Next
      ProcedureReturn

    Case #Action_Blend_Unlink
      ; Remove matching BlendLink by output OR effect layer
      ForEach gCmdHistory()
        If gCmdHistory()\action = #Action_Blend_Link
          If LCase(Trim(gCmdHistory()\p0)) = key Or LCase(Trim(gCmdHistory()\p2)) = key
            DeleteElement(gCmdHistory())
          EndIf
        EndIf
      Next
      ProcedureReturn

    Case #Action_Layer_CreateUpdate
      ; Remove ALL previous commands for this layer (fresh start)
      ForEach gCmdHistory()
        If LCase(Trim(gCmdHistory()\p0)) = key
          DeleteElement(gCmdHistory())
        EndIf
      Next
      ; Also remove blend links referencing this layer as effect
      ForEach gCmdHistory()
        If gCmdHistory()\action = #Action_Blend_Link And LCase(Trim(gCmdHistory()\p2)) = key
          DeleteElement(gCmdHistory())
        EndIf
      Next

    Case #Action_Overlay_Pause
      ; Don't record transient commands
      ProcedureReturn

    Default
      ; Replace previous same action + same primary rect
      ForEach gCmdHistory()
        If gCmdHistory()\action = action And LCase(Trim(gCmdHistory()\p0)) = key
          DeleteElement(gCmdHistory())
          Break
        EndIf
      Next
  EndSelect

  ; Add new record
  LastElement(gCmdHistory())
  AddElement(gCmdHistory())
  gCmdHistory()\action = action
  gCmdHistory()\p0 = p0 : gCmdHistory()\p1 = p1 : gCmdHistory()\p2 = p2
  gCmdHistory()\p3 = p3 : gCmdHistory()\p4 = p4 : gCmdHistory()\p5 = p5
  gCmdHistory()\p6 = p6
EndProcedure

Declare LayerCreateUpdate(rectName.s, pngPath.s, layoutMode.i, offsetX.i, offsetY.i, opacity.i, tintPreset.s)
Declare LayerSetTint(rectName.s, tintPreset.s, customR.i, customG.i, customB.i, customStrength.i)
Declare LayerSetOpacity(rectName.s, opacity.i)
Declare LayerShowHide(rectName.s, visible.i)
Declare BlendLinkCreate(topRect.s, blendMode.i, outputRect.s, opacityOverride.i)
Declare ScreenKeyLayer(rectName.s)
Declare.i LayoutNameToIndex(name.s)
Declare.i BlendNameToIndex(name.s)
Declare LayerDestroy(rectName.s)

Procedure ReplayHistory()
  ; Destroy all current layers cleanly
  Protected Dim destroyKeys.s(0)
  Protected dCount.i = 0
  ForEach gLayers()
    If dCount >= ArraySize(destroyKeys()) : ReDim destroyKeys.s(dCount + 8) : EndIf
    destroyKeys(dCount) = MapKey(gLayers())
    dCount + 1
  Next
  Protected di.i
  For di = 0 To dCount - 1
    LayerDestroy(destroyKeys(di))
  Next

  ; Replay all recorded commands
  gReplaying = 1
  ForEach gCmdHistory()
    Protected *p.CmdRecord = @gCmdHistory()
    Select *p\action
      Case #Action_Layer_CreateUpdate
        LayerCreateUpdate(*p\p0, *p\p1, LayoutNameToIndex(*p\p2), Val(*p\p3), Val(*p\p4), Val(*p\p5), *p\p6)
      Case #Action_Layer_SetOpacity
        LayerSetOpacity(*p\p0, Val(*p\p1))
      Case #Action_Layer_SetTint
        LayerSetTint(*p\p0, *p\p1, Val(*p\p2), Val(*p\p3), Val(*p\p4), Val(*p\p5))
      Case #Action_Layer_ScreenKey
        ScreenKeyLayer(*p\p0)
      Case #Action_Layer_ShowHide
        LayerShowHide(*p\p0, Val(*p\p1))
      Case #Action_Blend_Link
        ; p0=outputRect, p1=modeStr, p2=topRect, p3=opacity (user-facing order)
        Protected opOv.i = Val(*p\p3)
        If opOv < 0 : opOv = 0 : ElseIf opOv > 255 : opOv = 255 : EndIf
        BlendLinkCreate(*p\p2, BlendNameToIndex(*p\p1), *p\p0, opOv)
    EndSelect
  Next
  gReplaying = 0
EndProcedure

; ============================================================
;  VisualNEO string helpers (ANSI, GlobalAlloc_)
; ============================================================

Procedure.i MakeNBString(text.s)
  Protected bytes.i = StringByteLength(text, #PB_Ascii) + 1
  Protected *p = GlobalAlloc_($0040, bytes)
  If *p
    PokeS(*p, text, -1, #PB_Ascii)
  EndIf
  ProcedureReturn *p
EndProcedure

Procedure SetOutPtr_NoFree(*pDest.Long, *src)
  If *pDest
    PokeL(*pDest, *src)
  EndIf
EndProcedure

; Safe pointer helpers
Procedure.i PtrReadable(*p, bytes.i)
  If *p = 0 Or bytes <= 0 : ProcedureReturn #False : EndIf
  ProcedureReturn Bool(IsBadReadPtr_(*p, bytes) = 0)
EndProcedure

Procedure.i PtrWritable(*p, bytes.i)
  If *p = 0 Or bytes <= 0 : ProcedureReturn #False : EndIf
  ProcedureReturn Bool(IsBadWritePtr_(*p, bytes) = 0)
EndProcedure

Procedure.s SanitizeParamString(raw.s)
  If raw = "" : ProcedureReturn "" : EndIf
  Protected i.i, c.i, clean.s = "", len.i = Len(raw)
  If len > 500 : ProcedureReturn "" : EndIf
  For i = 1 To len
    c = Asc(Mid(raw, i, 1))
    If (c >= 32 And c <= 126) Or c = 9 Or c = 10 Or c = 13
      clean + Chr(c)
    Else
      Break
    EndIf
  Next
  ProcedureReturn clean
EndProcedure

Procedure.s ReadParamString(*Params, index.i)
  ; VisualNEO passes ANSI PCHAR pointers. Do not assume GlobalSize_ works.
  If *Params = 0 : ProcedureReturn "" : EndIf

  Protected *pSlot.Integer = *Params + index * 4
  If *pSlot = 0 : ProcedureReturn "" : EndIf

  Protected *val = PeekL(*pSlot)
  If *val = 0 : ProcedureReturn "" : EndIf

  ; Basic pointer sanity check to prevent access violations on bad params
  If IsBadReadPtr_(*val, 1)
    ProcedureReturn ""
  EndIf

  Protected raw.s = PeekS(*val, -1, #PB_Ascii)
  ProcedureReturn SanitizeParamString(raw)
EndProcedure


Procedure.i ReadParamInt(*Params, index.i, defaultValue.i = 0)
  Protected s.s = ReadParamString(*Params, index)
  If s = "" : ProcedureReturn defaultValue : EndIf
  ProcedureReturn Val(s)
EndProcedure

Procedure WriteParamString(*Params, index.i, value.s)
  If *Params = 0 : ProcedureReturn : EndIf
  Protected *slot = *Params + index * 4
  If PtrWritable(*slot, 4) = 0 : ProcedureReturn : EndIf
  PokeL(*slot, MakeNBString(value))
EndProcedure

Procedure WriteParamInt(*Params, index.i, value.i)
  WriteParamString(*Params, index, Str(value))
EndProcedure

; ============================================================
;  VisualNEO variable access
; ============================================================

Procedure SetNBVar(varName.s, value.s)
  If g_nbSetVar
    Protected *pVar = MakeNBString(varName)
    Protected *pVal = MakeNBString(value)
    If *pVar And *pVal
      CallFunctionFast(g_nbSetVar, *pVar, *pVal)
      GlobalFree_(*pVar) : GlobalFree_(*pVal)
    Else
      If *pVar : GlobalFree_(*pVar) : EndIf
      If *pVal : GlobalFree_(*pVal) : EndIf
    EndIf
  EndIf
EndProcedure

Procedure.s GetNBVar(varName.s)
  If g_nbGetVar
    Protected *pVar = MakeNBString(varName)
    If *pVar
      Protected *pRet = CallFunctionFast(g_nbGetVar, *pVar)
      GlobalFree_(*pVar)
      If *pRet And PtrReadable(*pRet, 1)
        ProcedureReturn SanitizeParamString(PeekS(*pRet, -1, #PB_Ascii))
      EndIf
    EndIf
  EndIf
  ProcedureReturn ""
EndProcedure

Procedure SetLastError(msg.s)
  SetNBVar("[NeoOverlay.LastError]", msg)
EndProcedure

; ============================================================
;  nbInterface helpers
; ============================================================

Procedure.i GetObjectHandle(objName.s)
  ; nbInterface ID 7: object name -> HWND (numeric string)
  If g_nbInterface = 0 : ProcedureReturn 0 : EndIf

  Protected p.i = MakeNBString(objName)
  If p = 0 : ProcedureReturn 0 : EndIf

  Protected success.i = CallFunctionFast(g_nbInterface, 7, @p)
  Protected hwnd.i = 0

  If success <> 0 And p <> 0
    If IsBadReadPtr_(p, 1) = 0
      hwnd = Val(SanitizeParamString(PeekS(p, -1, #PB_Ascii)))
    EndIf
  EndIf

  If p : GlobalFree_(p) : EndIf
  ProcedureReturn hwnd
EndProcedure


Procedure.s SelectRectangle(initialName.s)
  If g_nbInterface = 0 : ProcedureReturn "" : EndIf
  Protected *pData.Integer
  *pData = MakeNBString(initialName)
  Protected success.i = CallFunctionFast(g_nbInterface, 8, @*pData)
  Protected result.s = ""
  If success <> 0
    If *pData And IsBadReadPtr_(*pData, 1) = 0
      result = SanitizeParamString(PeekS(*pData, -1, #PB_Ascii))
    EndIf
  EndIf
  If *pData : GlobalFree_(*pData) : EndIf
  ProcedureReturn result
EndProcedure

Procedure.s SelectFile(title.s, currentFile.s, defaultDir.s, mask.s)
  If g_nbInterface = 0 : ProcedureReturn "" : EndIf
  Protected fileData.s = title + #CR$ + currentFile + #CR$ + defaultDir + #CR$ + mask + #CR$ + "1"
  Protected *pData.Integer
  *pData = MakeNBString(fileData)
  Protected success.i = CallFunctionFast(g_nbInterface, 15, @*pData)
  Protected result.s = ""
  If success <> 0
    If *pData And IsBadReadPtr_(*pData, 1) = 0
      result = SanitizeParamString(PeekS(*pData, -1, #PB_Ascii))
    EndIf
  EndIf
  If *pData : GlobalFree_(*pData) : EndIf
  ProcedureReturn result
EndProcedure

; ============================================================
;  GDI+ initialisation
; ============================================================

Procedure.i EnsureGDIPlus()
  If g_gdiplusStarted : ProcedureReturn 1 : EndIf
  Protected si.GdiplusStartupInput
  si\GdiplusVersion = 1
  If GdiplusStartup(@g_gdiplusToken, @si, 0) = 0
    g_gdiplusStarted = 1
    ProcedureReturn 1
  EndIf
  ProcedureReturn 0
EndProcedure

Procedure.i AnsiToWidePtr(text.s)
  Protected lenW.i = Len(text) + 1
  If lenW <= 1 : ProcedureReturn 0 : EndIf
  Protected *w = AllocateMemory(lenW * 2 + 2)
  If *w = 0 : ProcedureReturn 0 : EndIf
  PokeS(*w, text, -1, #PB_Unicode)
  ProcedureReturn *w
EndProcedure

Procedure.i LoadPngToGdipImage(path.s, *outW.Long, *outH.Long)
  If EnsureGDIPlus() = 0
    SetLastError("GDI+ startup failed.")
    ProcedureReturn 0
  EndIf
  Protected *wpath = AnsiToWidePtr(path)
  If *wpath = 0
    SetLastError("Failed converting PNG path to Unicode.")
    ProcedureReturn 0
  EndIf
  Protected img.i = 0, w.i, h.i
  If GdipCreateBitmapFromFile(*wpath, @img) = 0 And img
    GdipGetImageWidth(img, @w)
    GdipGetImageHeight(img, @h)
    *outW\l = w : *outH\l = h
  Else
    img = 0
    SetLastError("Unable to load PNG: " + path)
  EndIf
  FreeMemory(*wpath)
  ProcedureReturn img
EndProcedure

; ============================================================
;  DIB buffer creation & management
; ============================================================

Procedure FreeDIB(*layer.NeoLayer)
  If *layer\hDIB
    DeleteObject_(*layer\hDIB)
    *layer\hDIB = 0
    *layer\dibBits = 0
    *layer\dibW = 0 : *layer\dibH = 0
  EndIf
EndProcedure

Procedure.i CreateARGBDib(w.i, h.i, *ppBits.Integer)
  *ppBits\i = 0
  If w <= 0 Or h <= 0 : ProcedureReturn 0 : EndIf
  Protected bmi.BITMAPINFO
  bmi\bmiHeader\biSize = SizeOf(BITMAPINFOHEADER)
  bmi\bmiHeader\biWidth = w
  bmi\bmiHeader\biHeight = -h  ; top-down
  bmi\bmiHeader\biPlanes = 1
  bmi\bmiHeader\biBitCount = 32
  bmi\bmiHeader\biCompression = #BI_RGB

  Protected *bits = 0
  Protected hdcScreen.i = GetDC_(0)
  Protected hdcMem.i = CreateCompatibleDC_(hdcScreen)
  Protected hDib.i = CreateDIBSection_(hdcMem, @bmi, #DIB_RGB_COLORS, @*bits, 0, 0)
  DeleteDC_(hdcMem)
  ReleaseDC_(0, hdcScreen)
  If hDib And *bits
    *ppBits\i = *bits
  EndIf
  ProcedureReturn hDib
EndProcedure

; ============================================================
;  Tint - exact algorithm from NeoDX
; ============================================================

Procedure ApplyTintToBuffer(*bits, totalPixels.i, tintR.i, tintG.i, tintB.i, strength.i)
  ; strength: 0 = no tint, 255 = full tint
  If *bits = 0 Or strength = 0 Or totalPixels <= 0 : ProcedureReturn : EndIf
  Protected i.i, pixel.l, a.i, r.i, g.i, b.i
  Protected newR.i, newG.i, newB.i
  Protected blend.f = strength / 255.0

  For i = 0 To totalPixels - 1
    pixel = PeekL(*bits + i * 4)
    b = (pixel & $FF)
    g = ((pixel >> 8) & $FF)
    r = ((pixel >> 16) & $FF)
    a = ((pixel >> 24) & $FF)
    If a = 0 : Continue : EndIf  ; skip fully transparent

    newR = (r * tintR) / 255
    newG = (g * tintG) / 255
    newB = (b * tintB) / 255

    r = r + ((newR - r) * blend)
    g = g + ((newG - g) * blend)
    b = b + ((newB - b) * blend)

    If r > 255 : r = 255 : ElseIf r < 0 : r = 0 : EndIf
    If g > 255 : g = 255 : ElseIf g < 0 : g = 0 : EndIf
    If b > 255 : b = 255 : ElseIf b < 0 : b = 0 : EndIf

    PokeL(*bits + i * 4, b | (g << 8) | (r << 16) | (a << 24))
  Next
EndProcedure

; ============================================================
;  Tint presets (exact names & RGB/strength from NeoDX)
; ============================================================
; Returns #True if preset name matched; fills R,G,B,Str
Procedure.i ResolveTintPreset(preset.s, *r.Long, *g.Long, *b.Long, *s.Long)
  Select LCase(Trim(preset))
    Case "", "custom"
      ProcedureReturn #False ; caller uses custom values
    Case "no tint"
      *r\l=255 : *g\l=255 : *b\l=255 : *s\l=0
    Case "warm sunset"
      *r\l=255 : *g\l=180 : *b\l=120 : *s\l=128
    Case "cool night"
      *r\l=150 : *g\l=180 : *b\l=255 : *s\l=128
    Case "green toxic"
      *r\l=180 : *g\l=255 : *b\l=180 : *s\l=128
    Case "sepia old photo"
      *r\l=255 : *g\l=220 : *b\l=170 : *s\l=128
    Case "red danger"
      *r\l=255 : *g\l=150 : *b\l=150 : *s\l=128
    Case "blue ice"
      *r\l=180 : *g\l=220 : *b\l=255 : *s\l=128
    Case "purple magic"
      *r\l=220 : *g\l=180 : *b\l=255 : *s\l=128
    Case "orange fire"
      *r\l=255 : *g\l=200 : *b\l=100 : *s\l=128
    Case "yellow gold"
      *r\l=255 : *g\l=240 : *b\l=150 : *s\l=128
    Case "cyan underwater"
      *r\l=150 : *g\l=240 : *b\l=255 : *s\l=128
    Case "pink dream"
      *r\l=255 : *g\l=200 : *b\l=220 : *s\l=128
    Case "lime radioactive"
      *r\l=200 : *g\l=255 : *b\l=150 : *s\l=128
    Case "amber warm"
      *r\l=255 : *g\l=210 : *b\l=130 : *s\l=128
    Case "teal matrix"
      *r\l=150 : *g\l=255 : *b\l=230 : *s\l=128
    Case "magenta neon"
      *r\l=255 : *g\l=150 : *b\l=255 : *s\l=128
    Case "desaturate 50%"
      *r\l=200 : *g\l=200 : *b\l=200 : *s\l=128
    Case "desaturate 75%"
      *r\l=150 : *g\l=150 : *b\l=150 : *s\l=128
    Case "deep blue night"
      *r\l=120 : *g\l=140 : *b\l=200 : *s\l=128
    Case "sunset pink"
      *r\l=255 : *g\l=180 : *b\l=200 : *s\l=128
    Default
      ProcedureReturn #False
  EndSelect
  ProcedureReturn #True
EndProcedure

; ============================================================
;  Layout computation - draw PNG into buffer
; ============================================================

Procedure DrawPngToBuffer(*layer.NeoLayer)
  ; Draws the PNG (gdipImg) into the layer's DIB using layout mode + offsets
  If *layer\gdipImg = 0 Or *layer\hDIB = 0 Or *layer\dibBits = 0
    ProcedureReturn
  EndIf

  Protected w.i = *layer\dibW
  Protected h.i = *layer\dibH
  Protected imgW.i = *layer\imgW
  Protected imgH.i = *layer\imgH

  ; Clear buffer to transparent
  FillMemory(*layer\dibBits, w * h * 4, 0)

  ; Create GDI+ Graphics from DIB
  Protected hdcScreen.i = GetDC_(0)
  Protected hdcMem.i = CreateCompatibleDC_(hdcScreen)
  Protected oldBmp.i = SelectObject_(hdcMem, *layer\hDIB)

  Protected graphics.i
  If GdipCreateFromHDC(hdcMem, @graphics) = 0 And graphics
    GdipSetInterpolationMode(graphics, #InterpolationModeHighQualityBicubic)

    Protected dx.i, dy.i, dw.i, dh.i
    Protected oX.i = *layer\offsetX
    Protected oY.i = *layer\offsetY

    ; Calculate draw rectangle FIRST (need image center for rotation pivot)
    Select *layer\layoutMode
      Case #LAYOUT_ORIGINAL
        dx = oX : dy = oY : dw = imgW : dh = imgH

      Case #LAYOUT_STRETCH
        dx = oX : dy = oY : dw = w : dh = h

      Case #LAYOUT_FIT
        If imgW > 0 And imgH > 0
          Protected scaleF.f
          Protected scaleX.f = 1.0 * w / imgW
          Protected scaleY.f = 1.0 * h / imgH
          If scaleX < scaleY : scaleF = scaleX : Else : scaleF = scaleY : EndIf
          dw = imgW * scaleF
          dh = imgH * scaleF
          dx = (w - dw) / 2 + oX
          dy = (h - dh) / 2 + oY
        Else
          dx = oX : dy = oY : dw = w : dh = h
        EndIf

      Case #LAYOUT_FILL
        If imgW > 0 And imgH > 0
          scaleX = 1.0 * w / imgW
          scaleY = 1.0 * h / imgH
          If scaleX > scaleY : scaleF = scaleX : Else : scaleF = scaleY : EndIf
          dw = imgW * scaleF
          dh = imgH * scaleF
          dx = (w - dw) / 2 + oX
          dy = (h - dh) / 2 + oY
        Else
          dx = oX : dy = oY : dw = w : dh = h
        EndIf

      Case #LAYOUT_TILE
        ; Tile: apply rotation around buffer center, then draw tiles
        Protected hMatrixT.i = 0
        If *layer\rotation <> 0.0
          If GdipCreateMatrix(@hMatrixT) = 0 And hMatrixT
            Protected tcx.f = w / 2.0
            Protected tcy.f = h / 2.0
            GdipTranslateMatrix(hMatrixT, -tcx, -tcy, #MatrixOrderAppend)
            GdipRotateMatrix(hMatrixT, *layer\rotation, #MatrixOrderAppend)
            GdipTranslateMatrix(hMatrixT, tcx, tcy, #MatrixOrderAppend)
            GdipSetWorldTransform(graphics, hMatrixT)
          EndIf
        EndIf
        If imgW > 0 And imgH > 0
          Protected ty.i = oY
          While ty < h
            Protected tx.i = oX
            While tx < w
              GdipDrawImageRectI(graphics, *layer\gdipImg, tx, ty, imgW, imgH)
              tx + imgW
            Wend
            ty + imgH
          Wend
        EndIf
        If hMatrixT : GdipDeleteMatrix(hMatrixT) : EndIf
        GdipDeleteGraphics(graphics)
        SelectObject_(hdcMem, oldBmp)
        DeleteDC_(hdcMem)
        ReleaseDC_(0, hdcScreen)
        ProcedureReturn  ; tile already drawn
    EndSelect

    ; Apply rotation around center of the DRAWN IMAGE (not buffer center)
    Protected hMatrix.i = 0
    If *layer\rotation <> 0.0
      If GdipCreateMatrix(@hMatrix) = 0 And hMatrix
        Protected rcx.f = dx + dw / 2.0
        Protected rcy.f = dy + dh / 2.0
        GdipTranslateMatrix(hMatrix, -rcx, -rcy, #MatrixOrderAppend)
        GdipRotateMatrix(hMatrix, *layer\rotation, #MatrixOrderAppend)
        GdipTranslateMatrix(hMatrix, rcx, rcy, #MatrixOrderAppend)
        GdipSetWorldTransform(graphics, hMatrix)
      EndIf
    EndIf

    ; Draw single image (non-tile modes)
    If dw < 1 : dw = 1 : EndIf
    If dh < 1 : dh = 1 : EndIf
    GdipDrawImageRectI(graphics, *layer\gdipImg, dx, dy, dw, dh)
    If hMatrix : GdipDeleteMatrix(hMatrix) : EndIf
    GdipDeleteGraphics(graphics)
  EndIf

  SelectObject_(hdcMem, oldBmp)
  DeleteDC_(hdcMem)
  ReleaseDC_(0, hdcScreen)
EndProcedure

; ============================================================
;  Layer render pipeline (blueprint #6)
; ============================================================

Procedure LayerRender(*layer.NeoLayer)
  If *layer = 0 : ProcedureReturn : EndIf
  If *layer\hHost = 0 Or IsWindow_(*layer\hHost) = 0 : ProcedureReturn : EndIf

  ; 1) Determine host client size
  Protected rc.RECT
  GetClientRect_(*layer\hHost, @rc)
  Protected w.i = rc\right - rc\left
  Protected h.i = rc\bottom - rc\top
  If w < 1 : w = 1 : EndIf
  If h < 1 : h = 1 : EndIf

  ; 2) Create/resize cached DIB
  If *layer\hDIB = 0 Or *layer\dibW <> w Or *layer\dibH <> h
    FreeDIB(*layer)
    Protected *bits = 0
    *layer\hDIB = CreateARGBDib(w, h, @*bits)
    *layer\dibBits = *bits
    *layer\dibW = w : *layer\dibH = h
  Else
    ; 3) Clear to transparent
    If *layer\dibBits
      FillMemory(*layer\dibBits, w * h * 4, 0)
    EndIf
  EndIf

  If *layer\hDIB = 0 : ProcedureReturn : EndIf

  ; 4) Draw PNG using layout mode
  If *layer\gdipImg
    DrawPngToBuffer(*layer)
  EndIf

  ; 5) Apply layer opacity (premultiplied alpha - must scale RGB too)
  If *layer\opacity < 255 And *layer\dibBits
    Protected totalPx.i = w * h
    Protected i.i, pixel.l, a.i, r.i, g.i, b.i
    Protected opF.f = *layer\opacity / 255.0
    For i = 0 To totalPx - 1
      pixel = PeekL(*layer\dibBits + i * 4)
      a = ((pixel >> 24) & $FF)
      If a > 0
        b = (pixel & $FF)
        g = ((pixel >> 8) & $FF)
        r = ((pixel >> 16) & $FF)
        a = a * opF : If a > 255 : a = 255 : EndIf
        r = r * opF : If r > 255 : r = 255 : EndIf
        g = g * opF : If g > 255 : g = 255 : EndIf
        b = b * opF : If b > 255 : b = 255 : EndIf
        PokeL(*layer\dibBits + i * 4, b | (g << 8) | (r << 16) | (a << 24))
      EndIf
    Next
  EndIf

  ; 6) Apply tint
  If *layer\tintStrength > 0 And *layer\dibBits
    ApplyTintToBuffer(*layer\dibBits, w * h, *layer\tintR, *layer\tintG, *layer\tintB, *layer\tintStrength)
  EndIf

  *layer\dirty = 0
EndProcedure

; ============================================================
;  Layer present - UpdateLayeredWindow (child of host rect)
; ============================================================

; Clip overlay popup to main app window bounds
Procedure ClipOverlayToApp(*layer.NeoLayer, screenX.i, screenY.i)
  If *layer = 0 Or *layer\hOverlay = 0 Or *layer\hHost = 0
    ProcedureReturn
  EndIf
  Protected hRoot.i = GetAncestor_(*layer\hHost, 2)  ; GA_ROOT
  If hRoot = 0 : ProcedureReturn : EndIf
  
  Protected rcApp.RECT
  GetWindowRect_(hRoot, @rcApp)
  
  ; Overlay rect in screen coords
  Protected oLeft.i   = screenX
  Protected oTop.i    = screenY
  Protected oRight.i  = oLeft + *layer\dibW
  Protected oBottom.i = oTop + *layer\dibH
  
  ; Intersection of overlay with app window (in screen coords)
  Protected cLeft.i   = oLeft   : If rcApp\left   > cLeft   : cLeft   = rcApp\left   : EndIf
  Protected cTop.i    = oTop    : If rcApp\top    > cTop    : cTop    = rcApp\top    : EndIf
  Protected cRight.i  = oRight  : If rcApp\right  < cRight  : cRight  = rcApp\right  : EndIf
  Protected cBottom.i = oBottom : If rcApp\bottom < cBottom : cBottom = rcApp\bottom : EndIf
  
  If cLeft >= cRight Or cTop >= cBottom
    ; Fully outside app window - hide entirely
    SetWindowRgn_(*layer\hOverlay, CreateRectRgn_(0, 0, 0, 0), #True)
  ElseIf cLeft = oLeft And cTop = oTop And cRight = oRight And cBottom = oBottom
    ; Fully inside app window - remove clip region
    SetWindowRgn_(*layer\hOverlay, 0, #True)
  Else
    ; Partially visible - clip to intersection (convert to overlay-local coords)
    Protected hRgn.i = CreateRectRgn_(cLeft - oLeft, cTop - oTop, cRight - oLeft, cBottom - oTop)
    SetWindowRgn_(*layer\hOverlay, hRgn, #True)
    ; Note: Windows takes ownership of hRgn after SetWindowRgn_ - do NOT DeleteObject_
  EndIf
EndProcedure

Procedure LayerPresent(*layer.NeoLayer)
  If *layer = 0 Or *layer\hOverlay = 0 Or *layer\hDIB = 0
    ProcedureReturn
  EndIf

  If *layer\visible = 0 Or *layer\hHost = 0 Or IsWindowVisible_(*layer\hHost) = 0 Or *layer\opacity = 0
    ShowWindow_(*layer\hOverlay, #SW_HIDE)
    ProcedureReturn
  EndIf

  Protected w.i = *layer\dibW
  Protected h.i = *layer\dibH

  ; Get screen position of host rectangle's client area
  Protected ptHost.POINT : ptHost\x = 0 : ptHost\y = 0
  ClientToScreen_(*layer\hHost, @ptHost)

  ; Position popup at host's screen coords
  SetWindowPos_(*layer\hOverlay, 0, ptHost\x, ptHost\y, w, h, #SWP_NOACTIVATE | #SWP_NOZORDER)
  ShowWindow_(*layer\hOverlay, #SW_SHOWNOACTIVATE)

  Protected hdcScreen.i = GetDC_(0)
  Protected hdcMem.i = CreateCompatibleDC_(hdcScreen)
  Protected oldBmp.i = SelectObject_(hdcMem, *layer\hDIB)

  Protected ptDst.POINT : ptDst\x = ptHost\x : ptDst\y = ptHost\y
  Protected ptSrc.POINT : ptSrc\x = 0 : ptSrc\y = 0
  Protected sz.SIZE : sz\cx = w : sz\cy = h
  Protected bf.BLENDFUNCTION
  bf\BlendOp = #AC_SRC_OVER
  bf\BlendFlags = 0
  bf\SourceConstantAlpha = *layer\opacity  ; apply layer opacity (0-255)
  bf\AlphaFormat = #AC_SRC_ALPHA

  UpdateLayeredWindow_(*layer\hOverlay, hdcScreen, @ptDst, @sz, hdcMem, @ptSrc, 0, @bf, #ULW_ALPHA)

  SelectObject_(hdcMem, oldBmp)
  DeleteDC_(hdcMem)
  ReleaseDC_(0, hdcScreen)
  
  ; Clip to app window bounds
  ClipOverlayToApp(*layer, ptHost\x, ptHost\y)
EndProcedure

; ============================================================
;  Layer reposition - move overlay window only (no pixel upload)
;  Used during window drag for smooth performance
; ============================================================

Procedure LayerReposition(*layer.NeoLayer)
  If *layer = 0 Or *layer\hOverlay = 0 Or *layer\hHost = 0
    ProcedureReturn
  EndIf
  If *layer\visible = 0 Or IsWindowVisible_(*layer\hHost) = 0
    ProcedureReturn
  EndIf
  Protected ptHost.POINT : ptHost\x = 0 : ptHost\y = 0
  ClientToScreen_(*layer\hHost, @ptHost)
  SetWindowPos_(*layer\hOverlay, 0, ptHost\x, ptHost\y, *layer\dibW, *layer\dibH, #SWP_NOACTIVATE | #SWP_NOZORDER | #SWP_NOSIZE)
  
  ; Clip to app window bounds
  ClipOverlayToApp(*layer, ptHost\x, ptHost\y)
EndProcedure

; ============================================================
;  Host Rectangle subclass - detect moves from ANY source
;  (e.g. NeoWin_Animator SetWindowPos_ calls)
; ============================================================
Procedure.i HostMoveSubclassProc(hwnd.i, uMsg.i, wParam.i, lParam.i)
  ; Find the layer whose hHost matches this hwnd
  Protected origProc.i = 0
  ForEach gLayers()
    If gLayers()\hHost = hwnd
      origProc = gLayers()\origHostProc
      Break
    EndIf
  Next
  If origProc = 0
    ProcedureReturn DefWindowProc_(hwnd, uMsg, wParam, lParam)
  EndIf
  
  ; Reposition overlay whenever host window position changes
  If uMsg = #WM_WINDOWPOSCHANGED
    Protected result.i = CallWindowProc_(origProc, hwnd, uMsg, wParam, lParam)
    ; Reposition all layers on this host (usually just one)
    ForEach gLayers()
      If gLayers()\hHost = hwnd And gLayers()\visible And gLayers()\hOverlay
        LayerReposition(@gLayers())
      EndIf
    Next
    ProcedureReturn result
  EndIf
  
  ; Suppress host Rectangle painting so no border/background shows behind the overlay
  If uMsg = #WM_ERASEBKGND
    ProcedureReturn 1  ; tell Windows "background erased" (we didn't, so it stays clean)
  EndIf
  
  If uMsg = #WM_PAINT
    Protected ps.PAINTSTRUCT
    BeginPaint_(hwnd, @ps)
    EndPaint_(hwnd, @ps)
    ProcedureReturn 0  ; validated the region, painted nothing
  EndIf
  
  ; Prevent Windows from doing ANY internal repainting/bit-blitting when host moves
  ; (stops the background flash during animation)
  If uMsg = #WM_WINDOWPOSCHANGING
    If lParam
      Protected *wp.WINDOWPOS = lParam
      *wp\flags | #SWP_NOREDRAW
    EndIf
  EndIf
  
  ; Inter-plugin message: Animator sets overlay opacity (0-255)
  If uMsg = #WM_NEOOVERLAY_OPACITY
    ForEach gLayers()
      If gLayers()\hHost = hwnd
        Protected newOpacity.i = wParam
        If newOpacity < 0 : newOpacity = 0 : ElseIf newOpacity > 255 : newOpacity = 255 : EndIf
        gLayers()\opacity = newOpacity
        If gLayers()\visible And gLayers()\hOverlay
          LayerPresent(@gLayers())
        EndIf
        Break
      EndIf
    Next
    ProcedureReturn 0
  EndIf
  
  ; Inter-plugin message: Animator sets overlay rotation (wParam = tenths of degrees, signed)
  If uMsg = #WM_NEOOVERLAY_ROTATION
    ForEach gLayers()
      If gLayers()\hHost = hwnd
        ; wParam is tenths of degrees (e.g. 450 = 45.0 degrees)
        ; Interpret as signed: values > 1800000 are negative
        Protected angleTenths.i = wParam
        gLayers()\rotation = angleTenths / 10.0
        If gLayers()\visible And gLayers()\hOverlay
          gLayers()\dirty = 1
          LayerRender(@gLayers())
          LayerPresent(@gLayers())
        EndIf
        Break
      EndIf
    Next
    ProcedureReturn 0
  EndIf
  
  ProcedureReturn CallWindowProc_(origProc, hwnd, uMsg, wParam, lParam)
EndProcedure

; ============================================================
;  Blend mode computation (blueprint #7)
; ============================================================

; Scale an ARGB pixel buffer (nearest-neighbour, no GDI dependency)
Procedure ScaleARGBBuffer(*src, srcW.i, srcH.i, *dst, dstW.i, dstH.i)
  If *src = 0 Or *dst = 0 Or srcW <= 0 Or srcH <= 0 Or dstW <= 0 Or dstH <= 0
    ProcedureReturn
  EndIf
  Protected x.i, y.i, sX.i, sY.i
  For y = 0 To dstH - 1
    sY = y * srcH / dstH
    For x = 0 To dstW - 1
      sX = x * srcW / dstW
      PokeL(*dst + (y * dstW + x) * 4, PeekL(*src + (sY * srcW + sX) * 4))
    Next
  Next
EndProcedure

Procedure BlendRender(outputKey.s)
  If FindMapElement(gBlendLinks(), outputKey) = 0 : ProcedureReturn : EndIf

  Protected *link.NeoBlendLink = @gBlendLinks()
  Protected tKey.s = LCase(Trim(*link\topRectName))
  Protected oKey.s = outputKey  ; output rect IS the bottom
  Protected bKey.s = oKey

  ; Find source layers (bottom = output rect's layer)
  If FindMapElement(gLayers(), bKey) = 0 : ProcedureReturn : EndIf
  Protected *bottom.NeoLayer = @gLayers()
  If FindMapElement(gLayers(), tKey) = 0 : ProcedureReturn : EndIf
  Protected *top.NeoLayer = @gLayers()

  ; Mark top as blend-hidden (prevents MOVESIZE from re-showing it)
  *top\blendHidden = 1

  ; Ensure both rendered
  ; Bottom IS the output - force re-render to restore original content
  *bottom\dirty = 1
  If *bottom\dirty : LayerRender(*bottom) : EndIf
  ; Top must always render at full opacity - blend's topOp controls the strength.
  ; Without this, opacity 0 bakes zero alpha into pixels and blend can never show fog.
  Protected savedTopOp.i = *top\opacity
  *top\opacity = 255 : *top\dirty = 1
  LayerRender(*top)
  *top\opacity = savedTopOp

  ; Find output layer (must exist)
  If FindMapElement(gLayers(), oKey) = 0 : ProcedureReturn : EndIf
  Protected *out.NeoLayer = @gLayers()

  ; Work at output rect size
  Protected rc.RECT
  GetClientRect_(*out\hHost, @rc)
  Protected w.i = rc\right - rc\left
  Protected h.i = rc\bottom - rc\top
  If w < 1 Or h < 1 : ProcedureReturn : EndIf

  ; Ensure output DIB exists at correct size
  If *out\hDIB = 0 Or *out\dibW <> w Or *out\dibH <> h
    FreeDIB(*out)
    Protected *bits = 0
    *out\hDIB = CreateARGBDib(w, h, @*bits)
    *out\dibBits = *bits
    *out\dibW = w : *out\dibH = h
  EndIf
  If *out\hDIB = 0 Or *out\dibBits = 0 : ProcedureReturn : EndIf

  ; We need temporary scaled copies of bottom/top buffers at output size
  ; For simplicity, if source buffers differ in size from output, we StretchBlt
  ; But if they're the same size we can use directly.

  Protected *bBits = *bottom\dibBits
  Protected *tBits = *top\dibBits
  Protected bW.i = *bottom\dibW, bH.i = *bottom\dibH
  Protected tW.i = *top\dibW, tH.i = *top\dibH

  ; Create temp DIBs if sizes differ from output
  Protected *bTemp = 0, *tTemp = 0
  Protected hBTemp.i = 0, hTTemp.i = 0
  Protected *bTmpBits, *tTmpBits

  If bW <> w Or bH <> h
    hBTemp = CreateARGBDib(w, h, @*bTmpBits)
    If hBTemp And *bTmpBits
      ScaleARGBBuffer(*bottom\dibBits, bW, bH, *bTmpBits, w, h)
      *bBits = *bTmpBits
    EndIf
  EndIf

  If tW <> w Or tH <> h
    hTTemp = CreateARGBDib(w, h, @*tTmpBits)
    If hTTemp And *tTmpBits
      ScaleARGBBuffer(*top\dibBits, tW, tH, *tTmpBits, w, h)
      *tBits = *tTmpBits
    EndIf
  EndIf

  ; Effective top opacity (combine blend override with layer opacity)
  Protected topOp.i
  If *link\opacityOverride >= 0
    topOp = (*link\opacityOverride * *top\opacity) / 255
  Else
    topOp = *top\opacity
  EndIf
  If topOp > 255 : topOp = 255 : EndIf
  If topOp < 0 : topOp = 0 : EndIf

  ; Per-pixel blend
  Protected totalPx.i = w * h
  Protected i.i
  Protected dPixel.l, sPixel.l
  Protected dR.i, dG.i, dB.i, dA.i
  Protected sR.i, sG.i, sB.i, sA.i
  Protected bR.i, bG.i, bB.i       ; blended channel
  Protected effA.i                  ; effective alpha
  Protected outR.i, outG.i, outB.i, outA.i
  Protected mode.i = *link\blendMode

  For i = 0 To totalPx - 1
    dPixel = PeekL(*bBits + i * 4)
    sPixel = PeekL(*tBits + i * 4)

    dB = (dPixel & $FF)
    dG = ((dPixel >> 8) & $FF)
    dR = ((dPixel >> 16) & $FF)
    dA = ((dPixel >> 24) & $FF)

    sB = (sPixel & $FF)
    sG = ((sPixel >> 8) & $FF)
    sR = ((sPixel >> 16) & $FF)
    sA = ((sPixel >> 24) & $FF)

    ; Effective alpha: a = (sA * topOp) / 255
    effA = (sA * topOp) / 255

    If effA = 0
      ; Top is fully transparent - output = bottom pixel
      PokeL(*out\dibBits + i * 4, dPixel)
      Continue
    EndIf

    ; Compute B(S,D) per channel
    Select mode
      Case #BLEND_NORMAL
        bR = sR : bG = sG : bB = sB

      Case #BLEND_MULTIPLY
        bR = (sR * dR) / 255
        bG = (sG * dG) / 255
        bB = (sB * dB) / 255

      Case #BLEND_SCREEN
        bR = 255 - ((255 - sR) * (255 - dR) / 255)
        bG = 255 - ((255 - sG) * (255 - dG) / 255)
        bB = 255 - ((255 - sB) * (255 - dB) / 255)

      Case #BLEND_OVERLAY
        If dR < 128 : bR = (2 * sR * dR) / 255 : Else : bR = 255 - 2 * (255 - sR) * (255 - dR) / 255 : EndIf
        If dG < 128 : bG = (2 * sG * dG) / 255 : Else : bG = 255 - 2 * (255 - sG) * (255 - dG) / 255 : EndIf
        If dB < 128 : bB = (2 * sB * dB) / 255 : Else : bB = 255 - 2 * (255 - sB) * (255 - dB) / 255 : EndIf

      Case #BLEND_DARKEN
        If sR < dR : bR = sR : Else : bR = dR : EndIf
        If sG < dG : bG = sG : Else : bG = dG : EndIf
        If sB < dB : bB = sB : Else : bB = dB : EndIf

      Case #BLEND_LIGHTEN
        If sR > dR : bR = sR : Else : bR = dR : EndIf
        If sG > dG : bG = sG : Else : bG = dG : EndIf
        If sB > dB : bB = sB : Else : bB = dB : EndIf

      Case #BLEND_ADD
        bR = sR + dR : If bR > 255 : bR = 255 : EndIf
        bG = sG + dG : If bG > 255 : bG = 255 : EndIf
        bB = sB + dB : If bB > 255 : bB = 255 : EndIf

      Case #BLEND_SUBTRACT
        bR = dR - sR : If bR < 0 : bR = 0 : EndIf
        bG = dG - sG : If bG < 0 : bG = 0 : EndIf
        bB = dB - sB : If bB < 0 : bB = 0 : EndIf

      Default ; fallback normal
        bR = sR : bG = sG : bB = sB
    EndSelect

    ; Alpha-compose:  outRGB = (B * a + D * (255 - a)) / 255
    outR = (bR * effA + dR * (255 - effA)) / 255
    outG = (bG * effA + dG * (255 - effA)) / 255
    outB = (bB * effA + dB * (255 - effA)) / 255
    outA = effA + dA * (255 - effA) / 255

    If outR > 255 : outR = 255 : ElseIf outR < 0 : outR = 0 : EndIf
    If outG > 255 : outG = 255 : ElseIf outG < 0 : outG = 0 : EndIf
    If outB > 255 : outB = 255 : ElseIf outB < 0 : outB = 0 : EndIf
    If outA > 255 : outA = 255 : ElseIf outA < 0 : outA = 0 : EndIf

    PokeL(*out\dibBits + i * 4, outB | (outG << 8) | (outR << 16) | (outA << 24))
  Next

  ; Cleanup temp DIBs
  If hBTemp : DeleteObject_(hBTemp) : EndIf
  If hTTemp : DeleteObject_(hTTemp) : EndIf

  *link\dirty = 0

  ; Hide top source overlay and present blend output back-to-back
  If tKey <> oKey And *top\hOverlay And IsWindow_(*top\hOverlay)
    ShowWindow_(*top\hOverlay, #SW_HIDE)
  EndIf
  LayerPresent(*out)
EndProcedure

; Mark all blend links that depend on a given layer dirty & re-render
Procedure DirtyBlendLinksFor(rectKey.s)
  ; Collect keys first to avoid corrupting ForEach iterator
  Protected Dim keys.s(0)
  Protected count.i = 0
  ForEach gBlendLinks()
    If LCase(gBlendLinks()\topRectName) = rectKey Or MapKey(gBlendLinks()) = rectKey
      If count >= ArraySize(keys()) : ReDim keys.s(count + 8) : EndIf
      keys(count) = MapKey(gBlendLinks())
      count + 1
    EndIf
  Next
  Protected i.i
  For i = 0 To count - 1
    BlendRender(keys(i))
  Next
EndProcedure

; ============================================================
;  Layer creation / update (blueprint #9 - Layer: Create/Update)
; ============================================================

Procedure.i LayerCreateUpdate(rectName.s, pngPath.s, layoutMode.i, offsetX.i, offsetY.i, opacity.i, tintPreset.s)
  rectName = Trim(rectName)
  If rectName = ""
    SetLastError("Layer:Create - RectName is blank.")
    ProcedureReturn 0
  EndIf

  Protected key.s = LCase(rectName)
  Protected isNew.i = 0

  If FindMapElement(gLayers(), key) = 0
    AddMapElement(gLayers(), key)
    isNew = 1
  EndIf

  Protected *layer.NeoLayer = @gLayers()
  *layer\rectName = rectName

  ; Resolve host HWND
  Protected hHost.i = GetObjectHandle(rectName)
  If hHost = 0
    SetLastError("Layer:Create - Rectangle not found: " + rectName)
    If isNew : DeleteMapElement(gLayers(), key) : EndIf
    ProcedureReturn 0
  EndIf
  *layer\hHost = hHost

  ; Subclass the host Rectangle to detect moves from external sources
  ; (e.g. NeoWin_Animator). Only install once per layer.
  If *layer\origHostProc = 0
    *layer\origHostProc = SetWindowLong_(hHost, #GWL_WNDPROC, @HostMoveSubclassProc())
  EndIf

  ; Clamp opacity
  If opacity < 0 : opacity = 0 : EndIf
  If opacity > 255 : opacity = 255 : EndIf
  *layer\opacity = opacity
  *layer\layoutMode = layoutMode
  *layer\offsetX = offsetX
  *layer\offsetY = offsetY

  ; Tint (preset only - use LayerSetTint for custom RGB)
  *layer\tintPreset = tintPreset
  Protected tR.l, tG.l, tB.l, tS.l
  If ResolveTintPreset(tintPreset, @tR, @tG, @tB, @tS)
    *layer\tintR = tR : *layer\tintG = tG : *layer\tintB = tB : *layer\tintStrength = tS
  ElseIf isNew
    ; New layer with no preset -> neutral (no tint)
    *layer\tintR = 255 : *layer\tintG = 255 : *layer\tintB = 255 : *layer\tintStrength = 0
  EndIf
  ; (If updating existing layer with blank/Custom preset, keep current tint values)

  ; Load PNG (blank keeps existing image)
  pngPath = Trim(pngPath)
  If pngPath <> ""
    ; Dispose previous
    If *layer\gdipImg
      GdipDisposeImage(*layer\gdipImg)
      *layer\gdipImg = 0
    EndIf
    FreeDIB(*layer)
    Protected imgW.l, imgH.l
    *layer\gdipImg = LoadPngToGdipImage(pngPath, @imgW, @imgH)
    *layer\imgW = imgW : *layer\imgH = imgH
    *layer\pngPath = pngPath
  EndIf

  ; Create overlay popup window if needed - owned by VNW main form
  ; Owned popups stay above owner but go behind other apps automatically
  If *layer\hOverlay = 0 Or IsWindow_(*layer\hOverlay) = 0
    Protected ex.i = #WS_EX_LAYERED | #WS_EX_TRANSPARENT | #WS_EX_TOOLWINDOW
    Protected hOwner.i = GetAncestor_(*layer\hHost, 2)  ; GA_ROOT = 2
    *layer\hOverlay = CreateWindowEx_(ex, "STATIC", "", #WS_POPUP, 0, 0, 10, 10, hOwner, 0, g_hInstance, 0)
    If *layer\hOverlay = 0
      SetLastError("Layer:Create - Failed to create overlay window.")
      ProcedureReturn 0
    EndIf
  EndIf

  ; Track host size for resize detection in _nbMessage MOVESIZE
  Protected rcHost.RECT
  GetClientRect_(*layer\hHost, @rcHost)
  *layer\lastHostW = rcHost\right - rcHost\left
  *layer\lastHostH = rcHost\bottom - rcHost\top

  *layer\visible = 1
  *layer\dirty = 1

  ; Render and present
  LayerRender(*layer)

  ; Check if output rect has a blend link
  If FindMapElement(gBlendLinks(), key)
    BlendRender(key)
  Else
    LayerPresent(*layer)
  EndIf

  ; Also update any blend links that reference this as source
  DirtyBlendLinksFor(key)

  ProcedureReturn 1
EndProcedure

; ============================================================
;  Layer: Show/Hide
; ============================================================


; ============================================================
;  Layer: Set Tint (preset or custom)
; ============================================================
Procedure.i LayerSetTint(rectName.s, tintPreset.s, customR.i, customG.i, customB.i, customStrength.i)
  Protected key.s = LCase(Trim(rectName))
  If key = ""
    SetLastError("Layer:SetTint ? RectName is blank.")
    ProcedureReturn 0
  EndIf

  If FindMapElement(gLayers(), key) = 0
    SetLastError("Layer:SetTint ? Layer not found: " + rectName)
    ProcedureReturn 0
  EndIf

  Protected *layer.NeoLayer = @gLayers()
  *layer\tintPreset = tintPreset

  Protected tR.l, tG.l, tB.l, tS.l
  If ResolveTintPreset(tintPreset, @tR, @tG, @tB, @tS)
    *layer\tintR = tR : *layer\tintG = tG : *layer\tintB = tB : *layer\tintStrength = tS
  Else
    If customR < 0 : customR = 0 : ElseIf customR > 255 : customR = 255 : EndIf
    If customG < 0 : customG = 0 : ElseIf customG > 255 : customG = 255 : EndIf
    If customB < 0 : customB = 0 : ElseIf customB > 255 : customB = 255 : EndIf
    If customStrength < 0 : customStrength = 0 : ElseIf customStrength > 255 : customStrength = 255 : EndIf
    *layer\tintR = customR : *layer\tintG = customG : *layer\tintB = customB : *layer\tintStrength = customStrength
  EndIf

  *layer\dirty = 1
  LayerRender(*layer)

  ; If this rect is an output of a blend link, recomposite that instead of presenting standalone
  If FindMapElement(gBlendLinks(), key)
    BlendRender(key)
  Else
    LayerPresent(*layer)
  EndIf

  DirtyBlendLinksFor(key)
  ProcedureReturn 1
EndProcedure

; ============================================================
;  Layer: Set Opacity
; ============================================================

Procedure.i LayerSetOpacity(rectName.s, opacity.i)
  Protected key.s = LCase(Trim(rectName))
  If key = ""
    SetLastError("Layer:SetOpacity - RectName is blank.")
    ProcedureReturn 0
  EndIf

  If FindMapElement(gLayers(), key) = 0
    SetLastError("Layer:SetOpacity - Layer not found: " + rectName)
    ProcedureReturn 0
  EndIf

  Protected *layer.NeoLayer = @gLayers()
  If opacity < 0 : opacity = 0 : ElseIf opacity > 255 : opacity = 255 : EndIf
  *layer\opacity = opacity

  ; Re-render: trigger any blend links that use this layer, or re-present standalone
  DirtyBlendLinksFor(key)
  If FindMapElement(gLayers(), key)
    If gLayers()\blendHidden = 0
      gLayers()\dirty = 1
      LayerRender(@gLayers())
      LayerPresent(@gLayers())
    EndIf
  EndIf

  ProcedureReturn 1
EndProcedure

; ============================================================
;  Layer: Show/Hide
; ============================================================

Procedure.i LayerShowHide(rectName.s, visible.i)
  Protected key.s = LCase(Trim(rectName))
  If FindMapElement(gLayers(), key) = 0
    SetLastError("Layer:ShowHide - Layer not found: " + rectName)
    ProcedureReturn 0
  EndIf

  Protected *layer.NeoLayer = @gLayers()
  *layer\visible = Bool(visible <> 0)

  If *layer\hOverlay
    If *layer\visible
      If *layer\blendHidden = 0
        ShowWindow_(*layer\hOverlay, #SW_SHOWNOACTIVATE)
        LayerPresent(*layer)
      EndIf
    Else
      ShowWindow_(*layer\hOverlay, #SW_HIDE)
    EndIf
  EndIf

  ; If this layer is part of a blend, propagate visibility
  ; Collect keys first to avoid iterator corruption
  Protected Dim shKeys.s(0)
  Protected shCount.i = 0
  ForEach gBlendLinks()
    If LCase(Trim(gBlendLinks()\topRectName)) = key Or MapKey(gBlendLinks()) = key
      If shCount >= ArraySize(shKeys()) : ReDim shKeys.s(shCount + 4) : EndIf
      shKeys(shCount) = MapKey(gBlendLinks())
      shCount + 1
    EndIf
  Next
  Protected shi.i
  For shi = 0 To shCount - 1
    If *layer\visible
      BlendRender(shKeys(shi))
    Else
      If FindMapElement(gLayers(), shKeys(shi))
        If gLayers()\hOverlay And IsWindow_(gLayers()\hOverlay)
          ShowWindow_(gLayers()\hOverlay, #SW_HIDE)
        EndIf
      EndIf
    EndIf
  Next

  ProcedureReturn 1
EndProcedure

; ============================================================
;  Layer: Destroy
; ============================================================

Procedure LayerDestroy(rectName.s)
  Protected key.s = LCase(Trim(rectName))
  If FindMapElement(gLayers(), key) = 0 : ProcedureReturn : EndIf

  Protected *layer.NeoLayer = @gLayers()

  ; Remove host subclass before destroying
  If *layer\origHostProc And *layer\hHost And IsWindow_(*layer\hHost)
    SetWindowLong_(*layer\hHost, #GWL_WNDPROC, *layer\origHostProc)
  EndIf
  *layer\origHostProc = 0

  ; Destroy overlay window
  If *layer\hOverlay And IsWindow_(*layer\hOverlay)
    DestroyWindow_(*layer\hOverlay)
  EndIf
  *layer\hOverlay = 0

  ; Free bitmaps
  FreeDIB(*layer)
  If *layer\gdipImg
    GdipDisposeImage(*layer\gdipImg)
    *layer\gdipImg = 0
  EndIf

  DeleteMapElement(gLayers(), key)

  ; Remove any blend links that reference this layer
  ForEach gBlendLinks()
    If LCase(gBlendLinks()\topRectName) = key Or LCase(gBlendLinks()\outputRectName) = key
      DeleteMapElement(gBlendLinks())
    EndIf
  Next
EndProcedure

; ============================================================
;  Blend: Link (Create/Update)
; ============================================================

Procedure.i BlendLinkCreate(topRect.s, blendMode.i, outputRect.s, opacityOverride.i)
  topRect    = Trim(topRect)
  outputRect = Trim(outputRect)

  If topRect = "" Or outputRect = ""
    SetLastError("Blend:Link - TopRect or OutputRect is blank.")
    ProcedureReturn 0
  EndIf

  Protected oKey.s = LCase(outputRect)

  ; Output rect IS the bottom layer - must exist
  If FindMapElement(gLayers(), oKey) = 0
    SetLastError("Blend:Link - Output/bottom layer not found: " + outputRect)
    ProcedureReturn 0
  EndIf
  ; Top layer must exist
  If FindMapElement(gLayers(), LCase(topRect)) = 0
    SetLastError("Blend:Link - Top layer not found: " + topRect)
    ProcedureReturn 0
  EndIf

  ; Clamp blend mode
  If blendMode < 0 Or blendMode > 7 : blendMode = 0 : EndIf

  ; Create/update blend link
  If FindMapElement(gBlendLinks(), oKey) = 0
    AddMapElement(gBlendLinks(), oKey)
  EndIf

  gBlendLinks()\topRectName    = topRect
  gBlendLinks()\blendMode      = blendMode
  gBlendLinks()\outputRectName = outputRect
  gBlendLinks()\opacityOverride = opacityOverride
  gBlendLinks()\dirty = 1

  ; Render blend output now
  BlendRender(oKey)

  ProcedureReturn 1
EndProcedure

; ============================================================
;  Blend: Unlink
; ============================================================

Procedure.i BlendUnlink(rectName.s)
  Protected oKey.s = LCase(Trim(rectName))

  ; Try direct lookup (output/background rect)
  If FindMapElement(gBlendLinks(), oKey) = 0
    ; Try finding by effect/top rect name
    Protected found.i = 0
    ForEach gBlendLinks()
      If LCase(Trim(gBlendLinks()\topRectName)) = oKey
        oKey = MapKey(gBlendLinks())
        found = 1
        Break
      EndIf
    Next
    If found = 0
      SetLastError("Blend:Unlink - No blend link found for: " + rectName)
      ProcedureReturn 0
    EndIf
    FindMapElement(gBlendLinks(), oKey)
  EndIf

  ; Save top key before deleting link
  Protected tKey.s = LCase(Trim(gBlendLinks()\topRectName))

  DeleteMapElement(gBlendLinks(), oKey)

  ; Restore top layer overlay that was hidden by BlendRender
  If FindMapElement(gLayers(), tKey)
    gLayers()\blendHidden = 0
    If gLayers()\visible And gLayers()\hOverlay
      LayerPresent(@gLayers())
    EndIf
  EndIf

  ; Re-present the output/bottom layer with its original content
  If FindMapElement(gLayers(), oKey)
    Protected *layer.NeoLayer = @gLayers()
    *layer\dirty = 1
    LayerRender(*layer)
    LayerPresent(*layer)
  EndIf

  ProcedureReturn 1
EndProcedure

; ============================================================
;  Layer: Screen Key - convert black pixels to transparent
;  Sets alpha = max(R,G,B) so black becomes invisible and
;  bright pixels stay opaque. Perfect for glow/beam PNGs on
;  black backgrounds. RGB stays as-is (already premultiplied).
; ============================================================

Procedure.i ScreenKeyLayer(rectName.s)
  Protected key.s = LCase(Trim(rectName))
  If key = ""
    SetLastError("Layer:ScreenKey - RectName is blank.")
    ProcedureReturn 0
  EndIf

  If FindMapElement(gLayers(), key) = 0
    SetLastError("Layer:ScreenKey - Layer not found: " + rectName)
    ProcedureReturn 0
  EndIf

  Protected *layer.NeoLayer = @gLayers()

  ; Ensure rendered first
  If *layer\dirty : LayerRender(*layer) : EndIf
  If *layer\dibBits = 0 Or *layer\dibW < 1 Or *layer\dibH < 1
    ProcedureReturn 0
  EndIf

  Protected totalPx.i = *layer\dibW * *layer\dibH
  Protected i.i, pixel.l
  Protected r.i, g.i, b.i, maxC.i

  For i = 0 To totalPx - 1
    pixel = PeekL(*layer\dibBits + i * 4)
    b = (pixel & $FF)
    g = ((pixel >> 8) & $FF)
    r = ((pixel >> 16) & $FF)

    ; Alpha = max channel brightness
    maxC = r
    If g > maxC : maxC = g : EndIf
    If b > maxC : maxC = b : EndIf

    PokeL(*layer\dibBits + i * 4, b | (g << 8) | (r << 16) | (maxC << 24))
  Next

  ; Present updated layer
  LayerPresent(*layer)
  ProcedureReturn 1
EndProcedure

; ============================================================
;  Overlay Pause/Resume - hide/show for modal dialogs
; ============================================================

Procedure.i OverlayPause(pause.i)
  ForEach gLayers()
    If gLayers()\hOverlay And IsWindow_(gLayers()\hOverlay)
      If pause
        ShowWindow_(gLayers()\hOverlay, #SW_HIDE)
      Else
        If gLayers()\visible And gLayers()\blendHidden = 0 And gLayers()\opacity > 0
          LayerPresent(@gLayers())
        EndIf
      EndIf
    EndIf
  Next
  ProcedureReturn 1
EndProcedure

; ============================================================
;  Destroy all layers & blend links (cleanup)
; ============================================================

Procedure DestroyAllOverlays()
  ; Remove blend links first
  ClearMap(gBlendLinks())
  ClearList(gCmdHistory())

  ; Destroy layers
  ForEach gLayers()
    Protected *layer.NeoLayer = @gLayers()
    ; Remove host subclass
    If *layer\origHostProc And *layer\hHost And IsWindow_(*layer\hHost)
      SetWindowLong_(*layer\hHost, #GWL_WNDPROC, *layer\origHostProc)
    EndIf
    *layer\origHostProc = 0
    If *layer\hOverlay And IsWindow_(*layer\hOverlay)
      DestroyWindow_(*layer\hOverlay)
    EndIf
    *layer\hOverlay = 0
    FreeDIB(*layer)
    If *layer\gdipImg
      GdipDisposeImage(*layer\gdipImg)
      *layer\gdipImg = 0
    EndIf
  Next
  ClearMap(gLayers())
EndProcedure

; ============================================================
;  Layout mode name <-> index helpers
; ============================================================

Procedure.i LayoutNameToIndex(name.s)
  Select LCase(Trim(name))
    Case "original", "0" : ProcedureReturn #LAYOUT_ORIGINAL
    Case "stretch", "1"  : ProcedureReturn #LAYOUT_STRETCH
    Case "fit", "2"      : ProcedureReturn #LAYOUT_FIT
    Case "fill", "3"     : ProcedureReturn #LAYOUT_FILL
    Case "tile", "4"     : ProcedureReturn #LAYOUT_TILE
  EndSelect
  ProcedureReturn #LAYOUT_STRETCH ; default
EndProcedure

Procedure.s LayoutIndexToName(idx.i)
  Select idx
    Case #LAYOUT_ORIGINAL : ProcedureReturn "Original"
    Case #LAYOUT_STRETCH  : ProcedureReturn "Stretch"
    Case #LAYOUT_FIT      : ProcedureReturn "Fit"
    Case #LAYOUT_FILL     : ProcedureReturn "Fill"
    Case #LAYOUT_TILE     : ProcedureReturn "Tile"
  EndSelect
  ProcedureReturn "Stretch"
EndProcedure

; Blend mode name <-> index
Procedure.i BlendNameToIndex(name.s)
  Select LCase(Trim(name))
    Case "normal", "0"   : ProcedureReturn #BLEND_NORMAL
    Case "multiply", "1" : ProcedureReturn #BLEND_MULTIPLY
    Case "screen", "2"   : ProcedureReturn #BLEND_SCREEN
    Case "overlay", "3"  : ProcedureReturn #BLEND_OVERLAY
    Case "darken", "4"   : ProcedureReturn #BLEND_DARKEN
    Case "lighten", "5"  : ProcedureReturn #BLEND_LIGHTEN
    Case "add", "6"      : ProcedureReturn #BLEND_ADD
    Case "subtract", "7" : ProcedureReturn #BLEND_SUBTRACT
  EndSelect
  ProcedureReturn #BLEND_NORMAL
EndProcedure

Procedure.s BlendIndexToName(idx.i)
  Select idx
    Case #BLEND_NORMAL   : ProcedureReturn "Normal"
    Case #BLEND_MULTIPLY : ProcedureReturn "Multiply"
    Case #BLEND_SCREEN   : ProcedureReturn "Screen"
    Case #BLEND_OVERLAY  : ProcedureReturn "Overlay"
    Case #BLEND_DARKEN   : ProcedureReturn "Darken"
    Case #BLEND_LIGHTEN  : ProcedureReturn "Lighten"
    Case #BLEND_ADD      : ProcedureReturn "Add"
    Case #BLEND_SUBTRACT : ProcedureReturn "Subtract"
  EndSelect
  ProcedureReturn "Normal"
EndProcedure

; ============================================================
;  Design-time editors (_nbEditAction)
; ============================================================

; ------ Editor: Layer Create/Update ------
Procedure.i Edit_LayerCreateUpdate(*Params.Long)
  Protected hParent.i = GetActiveWindow_()
  Protected ret.i = 0

  ; Read existing params (7 only)
  Protected rectName.s = ReadParamString(*Params, 0)
  Protected pngFile.s  = ReadParamString(*Params, 1)
  Protected layoutStr.s = ReadParamString(*Params, 2)
  Protected oX.i       = ReadParamInt(*Params, 3, 0)
  Protected oY.i       = ReadParamInt(*Params, 4, 0)
  Protected opacity.i  = ReadParamInt(*Params, 5, 255)
  Protected tintPreset.s = ReadParamString(*Params, 6)

  Protected win = OpenWindow(#PB_Any, 0, 0, 480, 280, "NeoOverlay - Layer: Create/Update",
                             #PB_Window_SystemMenu | #PB_Window_ScreenCentered, hParent)
  If win = 0 : ProcedureReturn 0 : EndIf

  ; --- Rectangle ---
  TextGadget(#PB_Any, 10, 12, 90, 20, "Rectangle:")
  Protected gRect = StringGadget(#PB_Any, 100, 10, 280, 22, rectName)
  Protected gBtnRect = ButtonGadget(#PB_Any, 385, 10, 80, 22, "Select...")

  ; --- PNG File ---
  TextGadget(#PB_Any, 10, 42, 90, 20, "PNG File:")
  Protected gPng = StringGadget(#PB_Any, 100, 40, 280, 22, pngFile)
  Protected gBtnPng = ButtonGadget(#PB_Any, 385, 40, 80, 22, "Browse...")

  ; --- Layout ---
  TextGadget(#PB_Any, 10, 72, 90, 20, "Layout:")
  Protected gLayout = ComboBoxGadget(#PB_Any, 100, 70, 150, 22)
  AddGadgetItem(gLayout, -1, "Original")
  AddGadgetItem(gLayout, -1, "Stretch")
  AddGadgetItem(gLayout, -1, "Fit")
  AddGadgetItem(gLayout, -1, "Fill")
  AddGadgetItem(gLayout, -1, "Tile")
  SetGadgetState(gLayout, LayoutNameToIndex(layoutStr))

  ; --- Offsets ---
  TextGadget(#PB_Any, 270, 72, 50, 20, "OffsetX:")
  Protected gOX = StringGadget(#PB_Any, 320, 70, 50, 22, Str(oX))
  TextGadget(#PB_Any, 380, 72, 50, 20, "OffsetY:")
  Protected gOY = StringGadget(#PB_Any, 430, 70, 40, 22, Str(oY))

  ; --- Opacity ---
  TextGadget(#PB_Any, 10, 102, 90, 20, "Opacity (0-255):")
  Protected gOpacity = StringGadget(#PB_Any, 100, 100, 60, 22, Str(opacity))

  ; --- Tint Preset ---
  TextGadget(#PB_Any, 10, 140, 90, 20, "Tint Preset:")
  Protected gPreset = ComboBoxGadget(#PB_Any, 100, 138, 240, 22)
  AddGadgetItem(gPreset, -1, "No Tint")                   ; 0
  AddGadgetItem(gPreset, -1, "Warm Sunset")               ; 1
  AddGadgetItem(gPreset, -1, "Cool Night")                ; 2
  AddGadgetItem(gPreset, -1, "Green Toxic")               ; 3
  AddGadgetItem(gPreset, -1, "Sepia Old Photo")           ; 4
  AddGadgetItem(gPreset, -1, "Red Danger")                ; 5
  AddGadgetItem(gPreset, -1, "Blue Ice")                  ; 6
  AddGadgetItem(gPreset, -1, "Purple Magic")              ; 7
  AddGadgetItem(gPreset, -1, "Orange Fire")               ; 8
  AddGadgetItem(gPreset, -1, "Yellow Gold")               ; 9
  AddGadgetItem(gPreset, -1, "Cyan Underwater")           ; 10
  AddGadgetItem(gPreset, -1, "Pink Dream")                ; 11
  AddGadgetItem(gPreset, -1, "Lime Radioactive")          ; 12
  AddGadgetItem(gPreset, -1, "Amber Warm")                ; 13
  AddGadgetItem(gPreset, -1, "Teal Matrix")               ; 14
  AddGadgetItem(gPreset, -1, "Magenta Neon")              ; 15
  AddGadgetItem(gPreset, -1, "Desaturate 50%")            ; 16
  AddGadgetItem(gPreset, -1, "Desaturate 75%")            ; 17
  AddGadgetItem(gPreset, -1, "Deep Blue Night")           ; 18
  AddGadgetItem(gPreset, -1, "Sunset Pink")               ; 19
  ; Restore saved preset selection
  Protected foundPreset.i = 0
  If tintPreset <> ""
    Protected pi.i
    For pi = 0 To CountGadgetItems(gPreset) - 1
      If LCase(GetGadgetItemText(gPreset, pi)) = LCase(tintPreset)
        SetGadgetState(gPreset, pi)
        foundPreset = 1
        Break
      EndIf
    Next
  EndIf
  If foundPreset = 0
    SetGadgetState(gPreset, 0)  ; Default to No Tint
  EndIf

  TextGadget(#PB_Any, 10, 168, 460, 18, "Use NeoOverlay_LayerSetTint for custom RGB tint values.")

  ; --- OK / Cancel ---
  Protected gOK     = ButtonGadget(#PB_Any, 290, 240, 80, 28, "OK")
  Protected gCancel = ButtonGadget(#PB_Any, 385, 240, 80, 28, "Cancel")

  Protected done.i = 0
  Repeat
    Protected ev.i = WaitWindowEvent()
    If ev = #PB_Event_CloseWindow : done = 1 : EndIf
    If ev = #PB_Event_Gadget
      Protected gid.i = EventGadget()
      Select gid
        Case gBtnRect
          Protected sel.s = SelectRectangle(GetGadgetText(gRect))
          If sel <> "" : SetGadgetText(gRect, sel) : EndIf

        Case gBtnPng
          Protected fsel.s = SelectFile("Select PNG Image", GetGadgetText(gPng), "", "PNG Images|*.png|All Files|*.*")
          If fsel <> "" : SetGadgetText(gPng, fsel) : EndIf

        Case gOK
          ; Write 7 params only
          WriteParamString(*Params, 0, Trim(GetGadgetText(gRect)))
          WriteParamString(*Params, 1, Trim(GetGadgetText(gPng)))
          WriteParamString(*Params, 2, LayoutIndexToName(GetGadgetState(gLayout)))
          WriteParamInt(*Params, 3, Val(GetGadgetText(gOX)))
          WriteParamInt(*Params, 4, Val(GetGadgetText(gOY)))

          Protected opVal.i = Val(GetGadgetText(gOpacity))
          If opVal < 0 : opVal = 0 : ElseIf opVal > 255 : opVal = 255 : EndIf
          WriteParamInt(*Params, 5, opVal)

          ; Tint preset name
          WriteParamString(*Params, 6, GetGadgetItemText(gPreset, GetGadgetState(gPreset)))

          ret = 1 : done = 1

        Case gCancel
          done = 1
      EndSelect
    EndIf
  Until done
  CloseWindow(win)
  ProcedureReturn ret
EndProcedure

; ------ Editor: Layer Set Opacity ------
Procedure.i Edit_LayerSetOpacity(*Params.Long)
  Protected hParent.i = GetActiveWindow_()
  Protected rectName.s = ReadParamString(*Params, 0)
  Protected opStr.s    = ReadParamString(*Params, 1)
  Protected ret.i = 0

  Protected win = OpenWindow(#PB_Any, 0, 0, 360, 130, "NeoOverlay - Layer: Set Opacity",
                             #PB_Window_SystemMenu | #PB_Window_ScreenCentered, hParent)
  If win = 0 : ProcedureReturn 0 : EndIf

  TextGadget(#PB_Any, 10, 12, 90, 20, "Rectangle:")
  Protected gRect = StringGadget(#PB_Any, 100, 10, 160, 22, rectName)
  Protected gBtnRect = ButtonGadget(#PB_Any, 265, 10, 80, 22, "Select...")

  TextGadget(#PB_Any, 10, 42, 90, 20, "Opacity (0-255):")
  Protected gOp = StringGadget(#PB_Any, 100, 40, 60, 22, opStr)

  Protected gOK     = ButtonGadget(#PB_Any, 165, 90, 80, 28, "OK")
  Protected gCancel = ButtonGadget(#PB_Any, 260, 90, 80, 28, "Cancel")

  Protected done.i = 0
  Repeat
    Protected ev.i = WaitWindowEvent()
    If ev = #PB_Event_CloseWindow : done = 1 : EndIf
    If ev = #PB_Event_Gadget
      Select EventGadget()
        Case gBtnRect
          Protected sel.s = SelectRectangle(GetGadgetText(gRect))
          If sel <> "" : SetGadgetText(gRect, sel) : EndIf
        Case gOK
          WriteParamString(*Params, 0, Trim(GetGadgetText(gRect)))
          Protected opVal.i = Val(GetGadgetText(gOp))
          If opVal < 0 : opVal = 0 : ElseIf opVal > 255 : opVal = 255 : EndIf
          WriteParamInt(*Params, 1, opVal)
          ret = 1 : done = 1
        Case gCancel
          done = 1
      EndSelect
    EndIf
  Until done
  CloseWindow(win)
  ProcedureReturn ret
EndProcedure

; ------ Editor: Layer Show/Hide ------
Procedure.i Edit_LayerShowHide(*Params.Long)
  Protected hParent.i = GetActiveWindow_()
  Protected rectName.s = ReadParamString(*Params, 0)
  Protected visStr.s   = ReadParamString(*Params, 1)
  Protected ret.i = 0

  Protected win = OpenWindow(#PB_Any, 0, 0, 360, 130, "NeoOverlay - Layer: Show/Hide",
                             #PB_Window_SystemMenu | #PB_Window_ScreenCentered, hParent)
  If win = 0 : ProcedureReturn 0 : EndIf

  TextGadget(#PB_Any, 10, 12, 90, 20, "Rectangle:")
  Protected gRect = StringGadget(#PB_Any, 100, 10, 160, 22, rectName)
  Protected gBtnRect = ButtonGadget(#PB_Any, 265, 10, 80, 22, "Select...")

  TextGadget(#PB_Any, 10, 42, 90, 20, "Visible:")
  Protected gVis = ComboBoxGadget(#PB_Any, 100, 40, 100, 22)
  AddGadgetItem(gVis, -1, "Hide")    ; index 0
  AddGadgetItem(gVis, -1, "Show")    ; index 1
  If visStr = "0"
    SetGadgetState(gVis, 0)
  Else
    SetGadgetState(gVis, 1)  ; default to Show
  EndIf

  Protected gOK     = ButtonGadget(#PB_Any, 165, 90, 80, 28, "OK")
  Protected gCancel = ButtonGadget(#PB_Any, 260, 90, 80, 28, "Cancel")

  Protected done.i = 0
  Repeat
    Protected ev.i = WaitWindowEvent()
    If ev = #PB_Event_CloseWindow : done = 1 : EndIf
    If ev = #PB_Event_Gadget
      Select EventGadget()
        Case gBtnRect
          Protected sel.s = SelectRectangle(GetGadgetText(gRect))
          If sel <> "" : SetGadgetText(gRect, sel) : EndIf
        Case gOK
          WriteParamString(*Params, 0, Trim(GetGadgetText(gRect)))
          WriteParamString(*Params, 1, Str(GetGadgetState(gVis)))
          ret = 1 : done = 1
        Case gCancel
          done = 1
      EndSelect
    EndIf
  Until done
  CloseWindow(win)
  ProcedureReturn ret
EndProcedure

; ------ Editor: Layer Destroy ------
Procedure.i Edit_LayerDestroy(*Params.Long)
  Protected hParent.i = GetActiveWindow_()
  Protected rectName.s = ReadParamString(*Params, 0)
  Protected ret.i = 0

  Protected win = OpenWindow(#PB_Any, 0, 0, 360, 100, "NeoOverlay - Layer: Destroy",
                             #PB_Window_SystemMenu | #PB_Window_ScreenCentered, hParent)
  If win = 0 : ProcedureReturn 0 : EndIf

  TextGadget(#PB_Any, 10, 12, 90, 20, "Rectangle:")
  Protected gRect = StringGadget(#PB_Any, 100, 10, 160, 22, rectName)
  Protected gBtnRect = ButtonGadget(#PB_Any, 265, 10, 80, 22, "Select...")

  Protected gOK     = ButtonGadget(#PB_Any, 165, 60, 80, 28, "OK")
  Protected gCancel = ButtonGadget(#PB_Any, 260, 60, 80, 28, "Cancel")

  Protected done.i = 0
  Repeat
    Protected ev.i = WaitWindowEvent()
    If ev = #PB_Event_CloseWindow : done = 1 : EndIf
    If ev = #PB_Event_Gadget
      Select EventGadget()
        Case gBtnRect
          Protected sel.s = SelectRectangle(GetGadgetText(gRect))
          If sel <> "" : SetGadgetText(gRect, sel) : EndIf
        Case gOK
          WriteParamString(*Params, 0, Trim(GetGadgetText(gRect)))
          ret = 1 : done = 1
        Case gCancel
          done = 1
      EndSelect
    EndIf
  Until done
  CloseWindow(win)
  ProcedureReturn ret
EndProcedure

; ------ Editor: Blend Link ------

; ============================================================
;  Editor: Layer Set Tint
; ============================================================
Procedure.i Edit_LayerSetTint(*Params.Long)
  Protected hParent.i = GetActiveWindow_()
  Protected rectName.s = ReadParamString(*Params, 0)
  Protected preset.s   = ReadParamString(*Params, 1)
  Protected cR.i       = ReadParamInt(*Params, 2, 255)
  Protected cG.i       = ReadParamInt(*Params, 3, 255)
  Protected cB.i       = ReadParamInt(*Params, 4, 255)
  Protected cS.i       = ReadParamInt(*Params, 5, 0)

  Protected win = OpenWindow(#PB_Any, 0, 0, 520, 250, "NeoOverlay - Set Tint", #PB_Window_SystemMenu | #PB_Window_ScreenCentered, hParent)
  If win = 0 : ProcedureReturn 0 : EndIf

  TextGadget(#PB_Any, 10, 14, 90, 20, "Rectangle:")
  Protected gRect = StringGadget(#PB_Any, 100, 12, 300, 22, rectName)
  Protected gPick = ButtonGadget(#PB_Any, 410, 12, 90, 22, "Pick...")

  TextGadget(#PB_Any, 10, 54, 90, 20, "Tint Preset:")
  Protected gPreset = ComboBoxGadget(#PB_Any, 100, 52, 240, 22)
  AddGadgetItem(gPreset, -1, "Custom")
  AddGadgetItem(gPreset, -1, "No Tint")
  AddGadgetItem(gPreset, -1, "Warm Sunset")
  AddGadgetItem(gPreset, -1, "Cool Night")
  AddGadgetItem(gPreset, -1, "Green Toxic")
  AddGadgetItem(gPreset, -1, "Sepia Old Photo")
  AddGadgetItem(gPreset, -1, "Red Danger")
  AddGadgetItem(gPreset, -1, "Blue Ice")
  AddGadgetItem(gPreset, -1, "Purple Magic")
  AddGadgetItem(gPreset, -1, "Orange Fire")
  AddGadgetItem(gPreset, -1, "Yellow Gold")
  AddGadgetItem(gPreset, -1, "Cyan Underwater")
  AddGadgetItem(gPreset, -1, "Pink Dream")
  AddGadgetItem(gPreset, -1, "Lime Radioactive")
  AddGadgetItem(gPreset, -1, "Amber Warm")
  AddGadgetItem(gPreset, -1, "Teal Matrix")
  AddGadgetItem(gPreset, -1, "Magenta Neon")
  AddGadgetItem(gPreset, -1, "Desaturate 50%")
  AddGadgetItem(gPreset, -1, "Desaturate 75%")
  AddGadgetItem(gPreset, -1, "Deep Blue Night")
  AddGadgetItem(gPreset, -1, "Sunset Pink")

  ; Select preset
  Protected sel = 0
  Protected i.i
  If preset <> ""
    For i = 0 To CountGadgetItems(gPreset)-1
      If LCase(GetGadgetItemText(gPreset, i)) = LCase(preset)
        sel = i : Break
      EndIf
    Next
  EndIf
  SetGadgetState(gPreset, sel)

  TextGadget(#PB_Any, 10, 94, 90, 20, "Custom R:")
  Protected gCR = StringGadget(#PB_Any, 100, 92, 60, 22, Str(cR))
  TextGadget(#PB_Any, 170, 94, 25, 20, "G:")
  Protected gCG = StringGadget(#PB_Any, 200, 92, 60, 22, Str(cG))
  TextGadget(#PB_Any, 270, 94, 25, 20, "B:")
  Protected gCB = StringGadget(#PB_Any, 300, 92, 60, 22, Str(cB))
  TextGadget(#PB_Any, 370, 94, 80, 20, "Strength:")
  Protected gCS = StringGadget(#PB_Any, 440, 92, 60, 22, Str(cS))

  Protected okBtn = ButtonGadget(#PB_Any, 320, 210, 90, 26, "OK")
  Protected cancelBtn = ButtonGadget(#PB_Any, 420, 210, 90, 26, "Cancel")

  Protected ret.i = 0
  Protected done.i = 0
  Repeat
    Protected ev.i = WaitWindowEvent()
    If ev = #PB_Event_CloseWindow : done = 1 : EndIf
    If ev = #PB_Event_Gadget
      Select EventGadget()
        Case gPick
          Protected picked.s = SelectRectangle(GetGadgetText(gRect))
          If picked <> "" : SetGadgetText(gRect, picked) : EndIf
        Case okBtn
          rectName = Trim(GetGadgetText(gRect))
          preset = GetGadgetText(gPreset)
          cR = Val(GetGadgetText(gCR))
          cG = Val(GetGadgetText(gCG))
          cB = Val(GetGadgetText(gCB))
          cS = Val(GetGadgetText(gCS))

          WriteParamString(*Params, 0, rectName)
          WriteParamString(*Params, 1, preset)
          WriteParamString(*Params, 2, Str(cR))
          WriteParamString(*Params, 3, Str(cG))
          WriteParamString(*Params, 4, Str(cB))
          WriteParamString(*Params, 5, Str(cS))

          ret = 1 : done = 1
        Case cancelBtn
          done = 1
      EndSelect
    EndIf
  Until done
  CloseWindow(win)
  ProcedureReturn ret
EndProcedure

Procedure.i Edit_BlendLink(*Params.Long)
  Protected hParent.i = GetActiveWindow_()
  Protected outputR.s = ReadParamString(*Params, 0)
  Protected modeStr.s = ReadParamString(*Params, 1)
  Protected topR.s    = ReadParamString(*Params, 2)
  Protected opOvStr.s = ReadParamString(*Params, 3)
  Protected ret.i = 0

  Protected win = OpenWindow(#PB_Any, 0, 0, 480, 210, "NeoOverlay - Blend: Link",
                             #PB_Window_SystemMenu | #PB_Window_ScreenCentered, hParent)
  If win = 0 : ProcedureReturn 0 : EndIf

  ; Top (effect layer)
  TextGadget(#PB_Any, 10, 12, 100, 20, "Top Rect:")
  Protected gTop = StringGadget(#PB_Any, 120, 10, 250, 22, topR)
  Protected gBtnTop = ButtonGadget(#PB_Any, 380, 10, 80, 22, "Select...")

  ; Blend mode
  TextGadget(#PB_Any, 10, 42, 100, 20, "Blend Mode:")
  Protected gMode = ComboBoxGadget(#PB_Any, 120, 40, 150, 22)
  AddGadgetItem(gMode, -1, "Normal")
  AddGadgetItem(gMode, -1, "Multiply")
  AddGadgetItem(gMode, -1, "Screen")
  AddGadgetItem(gMode, -1, "Overlay")
  AddGadgetItem(gMode, -1, "Darken")
  AddGadgetItem(gMode, -1, "Lighten")
  AddGadgetItem(gMode, -1, "Add")
  AddGadgetItem(gMode, -1, "Subtract")
  SetGadgetState(gMode, BlendNameToIndex(modeStr))

  ; Output rect (also the bottom/background layer)
  TextGadget(#PB_Any, 10, 72, 100, 20, "Output Rect:")
  Protected gOut = StringGadget(#PB_Any, 120, 70, 250, 22, outputR)
  Protected gBtnOut = ButtonGadget(#PB_Any, 380, 70, 80, 22, "Select...")
  TextGadget(#PB_Any, 120, 94, 300, 16, "(background layer - blend result renders here)")

  ; Opacity override
  TextGadget(#PB_Any, 10, 118, 120, 20, "Opacity Override:")
  Protected gOpOv = StringGadget(#PB_Any, 130, 116, 60, 22, opOvStr)
  TextGadget(#PB_Any, 200, 118, 200, 16, "(blank = use Top layer opacity)")

  ; OK / Cancel
  Protected gOK     = ButtonGadget(#PB_Any, 290, 165, 80, 28, "OK")
  Protected gCancel = ButtonGadget(#PB_Any, 385, 165, 80, 28, "Cancel")

  Protected done.i = 0
  Repeat
    Protected ev.i = WaitWindowEvent()
    If ev = #PB_Event_CloseWindow : done = 1 : EndIf
    If ev = #PB_Event_Gadget
      Select EventGadget()
        Case gBtnTop
          Protected sel.s = SelectRectangle(GetGadgetText(gTop))
          If sel <> "" : SetGadgetText(gTop, sel) : EndIf
        Case gBtnOut
          sel = SelectRectangle(GetGadgetText(gOut))
          If sel <> "" : SetGadgetText(gOut, sel) : EndIf
        Case gOK
          WriteParamString(*Params, 0, Trim(GetGadgetText(gOut)))
          WriteParamString(*Params, 1, BlendIndexToName(GetGadgetState(gMode)))
          WriteParamString(*Params, 2, Trim(GetGadgetText(gTop)))
          WriteParamString(*Params, 3, Trim(GetGadgetText(gOpOv)))
          ret = 1 : done = 1
        Case gCancel
          done = 1
      EndSelect
    EndIf
  Until done
  CloseWindow(win)
  ProcedureReturn ret
EndProcedure

; ------ Editor: Blend Unlink ------
Procedure.i Edit_BlendUnlink(*Params.Long)
  Protected hParent.i = GetActiveWindow_()
  Protected outputR.s = ReadParamString(*Params, 0)
  Protected ret.i = 0

  Protected win = OpenWindow(#PB_Any, 0, 0, 360, 100, "NeoOverlay - Blend: Unlink",
                             #PB_Window_SystemMenu | #PB_Window_ScreenCentered, hParent)
  If win = 0 : ProcedureReturn 0 : EndIf

  TextGadget(#PB_Any, 10, 12, 100, 20, "Output Rect:")
  Protected gRect = StringGadget(#PB_Any, 120, 10, 140, 22, outputR)
  Protected gBtnRect = ButtonGadget(#PB_Any, 265, 10, 80, 22, "Select...")

  Protected gOK     = ButtonGadget(#PB_Any, 165, 60, 80, 28, "OK")
  Protected gCancel = ButtonGadget(#PB_Any, 260, 60, 80, 28, "Cancel")

  Protected done.i = 0
  Repeat
    Protected ev.i = WaitWindowEvent()
    If ev = #PB_Event_CloseWindow : done = 1 : EndIf
    If ev = #PB_Event_Gadget
      Select EventGadget()
        Case gBtnRect
          Protected sel.s = SelectRectangle(GetGadgetText(gRect))
          If sel <> "" : SetGadgetText(gRect, sel) : EndIf
        Case gOK
          WriteParamString(*Params, 0, Trim(GetGadgetText(gRect)))
          ret = 1 : done = 1
        Case gCancel
          done = 1
      EndSelect
    EndIf
  Until done
  CloseWindow(win)
  ProcedureReturn ret
EndProcedure

; ------ Editor: Overlay Pause ------
Procedure.i Edit_OverlayPause(*Params.Long)
  Protected hParent.i = GetActiveWindow_()
  Protected pStr.s = ReadParamString(*Params, 0)
  Protected ret.i = 0

  Protected win = OpenWindow(#PB_Any, 0, 0, 300, 100, "NeoOverlay - Overlay: Pause/Resume",
                             #PB_Window_SystemMenu | #PB_Window_ScreenCentered, hParent)
  If win = 0 : ProcedureReturn 0 : EndIf

  TextGadget(#PB_Any, 10, 12, 60, 20, "Action:")
  Protected gCombo = ComboBoxGadget(#PB_Any, 80, 10, 120, 22)
  AddGadgetItem(gCombo, -1, "Pause")
  AddGadgetItem(gCombo, -1, "Resume")
  If Val(pStr) = 0 : SetGadgetState(gCombo, 1) : Else : SetGadgetState(gCombo, 0) : EndIf

  Protected gOK     = ButtonGadget(#PB_Any, 105, 60, 80, 28, "OK")
  Protected gCancel = ButtonGadget(#PB_Any, 200, 60, 80, 28, "Cancel")

  Protected done.i = 0
  Repeat
    Protected ev.i = WaitWindowEvent()
    If ev = #PB_Event_CloseWindow : done = 1 : EndIf
    If ev = #PB_Event_Gadget
      Select EventGadget()
        Case gOK
          If GetGadgetState(gCombo) = 0
            WriteParamInt(*Params, 0, 1)
          Else
            WriteParamInt(*Params, 0, 0)
          EndIf
          ret = 1 : done = 1
        Case gCancel
          done = 1
      EndSelect
    EndIf
  Until done
  CloseWindow(win)
  ProcedureReturn ret
EndProcedure

; ------ Editor: Layer Screen Key ------
Procedure.i Edit_LayerScreenKey(*Params.Long)
  Protected hParent.i = GetActiveWindow_()
  Protected rectStr.s = ReadParamString(*Params, 0)
  Protected ret.i = 0

  Protected win = OpenWindow(#PB_Any, 0, 0, 360, 100, "NeoOverlay - Layer: Screen Key",
                             #PB_Window_SystemMenu | #PB_Window_ScreenCentered, hParent)
  If win = 0 : ProcedureReturn 0 : EndIf

  TextGadget(#PB_Any, 10, 12, 80, 20, "Rectangle:")
  Protected gRect = StringGadget(#PB_Any, 100, 10, 160, 22, rectStr)
  Protected gBtnRect = ButtonGadget(#PB_Any, 265, 10, 80, 22, "Select...")

  Protected gOK     = ButtonGadget(#PB_Any, 165, 60, 80, 28, "OK")
  Protected gCancel = ButtonGadget(#PB_Any, 260, 60, 80, 28, "Cancel")

  Protected done.i = 0
  Repeat
    Protected ev.i = WaitWindowEvent()
    If ev = #PB_Event_CloseWindow : done = 1 : EndIf
    If ev = #PB_Event_Gadget
      Select EventGadget()
        Case gBtnRect
          Protected sel.s = SelectRectangle(GetGadgetText(gRect))
          If sel <> "" : SetGadgetText(gRect, sel) : EndIf
        Case gOK
          WriteParamString(*Params, 0, Trim(GetGadgetText(gRect)))
          ret = 1 : done = 1
        Case gCancel
          done = 1
      EndSelect
    EndIf
  Until done
  CloseWindow(win)
  ProcedureReturn ret
EndProcedure

; ============================================================
;  VisualNEO DLL exports
; ============================================================

ProcedureDLL _nbInitPlugIn(hWnd.i, *pTitle.Long, *pAuthor.Long, *pHint.Long)
  g_nbWinHandle = hWnd

  *gPluginTitle  = MakeNBString("NeoOverlay")
  *gPluginAuthor = MakeNBString("Anthony C - Consciousness Tools")
  *gPluginHint   = MakeNBString("PNG layers with blend modes, tint presets, layout modes for Rectangle objects.")

  SetOutPtr_NoFree(*pTitle,  *gPluginTitle)
  SetOutPtr_NoFree(*pAuthor, *gPluginAuthor)
  SetOutPtr_NoFree(*pHint,   *gPluginHint)
EndProcedure

ProcedureDLL _nbRegisterPlugIn(AddAction.i, AddFile.i, GetVar.i, SetVar.i)
  g_nbAddAction = AddAction
  g_nbAddFile   = AddFile
  g_nbGetVar    = GetVar
  g_nbSetVar    = SetVar

  If g_nbAddAction
    Static p.TActionParams

    ; Action 1: Layer Create/Update (7 params - tint preset only; use SetTint for custom RGB)
    *gAct1_Name = MakeNBString("NeoOverlay_LayerCreate")
    *gAct1_Hint = MakeNBString("Create/update a PNG overlay layer on a Rectangle (layout, opacity, tint preset).")
    p\p[0]=1 : p\p[1]=1 : p\p[2]=1 : p\p[3]=1 : p\p[4]=1
    p\p[5]=1 : p\p[6]=1
    CallFunctionFast(g_nbAddAction, #Action_Layer_CreateUpdate, *gAct1_Name, *gAct1_Hint, @p, 0, 7)

    ; Action 2: Layer Show/Hide (2 params)
    *gAct2_Name = MakeNBString("NeoOverlay_LayerShowHide")
    *gAct2_Hint = MakeNBString("Show or hide a layer's overlay window (does not destroy cache).")
    p\p[0]=1 : p\p[1]=1
    CallFunctionFast(g_nbAddAction, #Action_Layer_ShowHide, *gAct2_Name, *gAct2_Hint, @p, 0, 2)

    ; Action 3: Layer Destroy (1 param)
    *gAct3_Name = MakeNBString("NeoOverlay_LayerDestroy")
    *gAct3_Hint = MakeNBString("Destroy overlay layer, restore host WndProc, free all resources.")
    p\p[0]=1
    CallFunctionFast(g_nbAddAction, #Action_Layer_Destroy, *gAct3_Name, *gAct3_Hint, @p, 0, 1)


    ; Action 4: Layer Set Tint (6 params)
    *gAct4_Name = MakeNBString("NeoOverlay_LayerSetTint")
    *gAct4_Hint = MakeNBString("Set a layer's tint preset (or custom RGB/strength) and re-render.")
    p\p[0]=1 : p\p[1]=1 : p\p[2]=1 : p\p[3]=1 : p\p[4]=1 : p\p[5]=1
    CallFunctionFast(g_nbAddAction, #Action_Layer_SetTint, *gAct4_Name, *gAct4_Hint, @p, 0, 6)

    ; Action 4: Blend Link (5 params)
    ; Action 5: Blend Link (4 params - top rect, blend mode, output rect, opacity)
    *gAct5_Name = MakeNBString("NeoOverlay_BlendLink")
    *gAct5_Hint = MakeNBString("Blend top layer onto output rect with a blend mode.")
    p\p[0]=1 : p\p[1]=1 : p\p[2]=1 : p\p[3]=1
    CallFunctionFast(g_nbAddAction, #Action_Blend_Link, *gAct5_Name, *gAct5_Hint, @p, 0, 4)

    ; Action 5: Blend Unlink (1 param)
    *gAct6_Name = MakeNBString("NeoOverlay_BlendUnlink")
    *gAct6_Hint = MakeNBString("Remove blend link; output rect returns to its own layer.")
    p\p[0]=1
    CallFunctionFast(g_nbAddAction, #Action_Blend_Unlink, *gAct6_Name, *gAct6_Hint, @p, 0, 1)

    ; Action 7: Layer Set Opacity (2 params)
    *gAct7_Name = MakeNBString("NeoOverlay_LayerSetOpacity")
    *gAct7_Hint = MakeNBString("Set a layer's opacity (0-255) after creation.")
    p\p[0]=1 : p\p[1]=1
    CallFunctionFast(g_nbAddAction, #Action_Layer_SetOpacity, *gAct7_Name, *gAct7_Hint, @p, 0, 2)

    ; Action 8: Overlay Pause/Resume (1 param)
    *gAct8_Name = MakeNBString("NeoOverlay_OverlayPause")
    *gAct8_Hint = MakeNBString("Pause (1) hides overlays for dialogs. Resume (0) restores them.")
    p\p[0]=1
    CallFunctionFast(g_nbAddAction, #Action_Overlay_Pause, *gAct8_Name, *gAct8_Hint, @p, 0, 1)

    ; Action 9: Layer Screen Key (1 param)
    *gAct9_Name = MakeNBString("NeoOverlay_LayerScreenKey")
    *gAct9_Hint = MakeNBString("Convert black pixels to transparent (Screen-style keying).")
    p\p[0]=1
    CallFunctionFast(g_nbAddAction, #Action_Layer_ScreenKey, *gAct9_Name, *gAct9_Hint, @p, 0, 1)
  EndIf
EndProcedure

ProcedureDLL.i _nbEditAction(ID.i, *Params.Long)
  Select ID
    Case #Action_Layer_CreateUpdate : ProcedureReturn Edit_LayerCreateUpdate(*Params)
    Case #Action_Layer_ShowHide     : ProcedureReturn Edit_LayerShowHide(*Params)
    Case #Action_Layer_Destroy      : ProcedureReturn Edit_LayerDestroy(*Params)
    Case #Action_Layer_SetTint      : ProcedureReturn Edit_LayerSetTint(*Params)
    Case #Action_Blend_Link         : ProcedureReturn Edit_BlendLink(*Params)
    Case #Action_Blend_Unlink       : ProcedureReturn Edit_BlendUnlink(*Params)
    Case #Action_Layer_SetOpacity   : ProcedureReturn Edit_LayerSetOpacity(*Params)
    Case #Action_Overlay_Pause      : ProcedureReturn Edit_OverlayPause(*Params)
    Case #Action_Layer_ScreenKey    : ProcedureReturn Edit_LayerScreenKey(*Params)
  EndSelect
  ProcedureReturn 0
EndProcedure

ProcedureDLL.i _nbExecAction(ID.i, *Params.Long)
  Protected result.i = 0

  Select ID
    Case #Action_Layer_CreateUpdate
      Protected rectName.s   = ReadParamString(*Params, 0)
      Protected pngFile.s    = ReadParamString(*Params, 1)
      Protected layoutStr.s  = ReadParamString(*Params, 2)
      Protected oX.i         = ReadParamInt(*Params, 3, 0)
      Protected oY.i         = ReadParamInt(*Params, 4, 0)
      Protected opacity.i    = ReadParamInt(*Params, 5, 255)
      Protected tintPreset.s = ReadParamString(*Params, 6)
      result = LayerCreateUpdate(rectName, pngFile, LayoutNameToIndex(layoutStr), oX, oY, opacity, tintPreset)
      RecordCmd(#Action_Layer_CreateUpdate, rectName, pngFile, layoutStr, Str(oX), Str(oY), Str(opacity), tintPreset)
      ProcedureReturn result

    Case #Action_Layer_ShowHide
      Protected shRect.s = ReadParamString(*Params, 0)
      Protected vis.i    = ReadParamInt(*Params, 1, 1)
      result = LayerShowHide(shRect, vis)
      RecordCmd(#Action_Layer_ShowHide, shRect, Str(vis))
      ProcedureReturn result

    Case #Action_Layer_Destroy
      RecordCmd(#Action_Layer_Destroy, ReadParamString(*Params, 0))
      LayerDestroy(ReadParamString(*Params, 0))
      ProcedureReturn 1

    Case #Action_Layer_SetTint
      Protected tr.s  = ReadParamString(*Params, 0)
      Protected tp.s  = ReadParamString(*Params, 1)
      Protected tintCR.i  = ReadParamInt(*Params, 2, 255)
      Protected tintCG.i  = ReadParamInt(*Params, 3, 255)
      Protected tintCB.i  = ReadParamInt(*Params, 4, 255)
      Protected tintCS.i  = ReadParamInt(*Params, 5, 0)
      result = LayerSetTint(tr, tp, tintCR, tintCG, tintCB, tintCS)
      RecordCmd(#Action_Layer_SetTint, tr, tp, Str(tintCR), Str(tintCG), Str(tintCB), Str(tintCS))
      ProcedureReturn result

    Case #Action_Blend_Link
      Protected outRect.s = ReadParamString(*Params, 0)
      Protected modeStr.s = ReadParamString(*Params, 1)
      Protected tRect.s   = ReadParamString(*Params, 2)
      Protected opOvStr.s = Trim(ReadParamString(*Params, 3))
      Protected opOv.i = -1
      If opOvStr <> ""
        opOv = Val(opOvStr)
        If opOv < 0 : opOv = 0 : ElseIf opOv > 255 : opOv = 255 : EndIf
      EndIf
      result = BlendLinkCreate(tRect, BlendNameToIndex(modeStr), outRect, opOv)
      RecordCmd(#Action_Blend_Link, outRect, modeStr, tRect, Str(opOv))
      ProcedureReturn result

    Case #Action_Blend_Unlink
      Protected ulRect.s = ReadParamString(*Params, 0)
      RecordCmd(#Action_Blend_Unlink, ulRect)
      ProcedureReturn BlendUnlink(ulRect)

    Case #Action_Layer_SetOpacity
      Protected opRect.s = ReadParamString(*Params, 0)
      Protected opVal2.i = ReadParamInt(*Params, 1, 255)
      result = LayerSetOpacity(opRect, opVal2)
      RecordCmd(#Action_Layer_SetOpacity, opRect, Str(opVal2))
      ProcedureReturn result

    Case #Action_Overlay_Pause
      Protected pauseVal.i = ReadParamInt(*Params, 0, 1)
      ProcedureReturn OverlayPause(pauseVal)

    Case #Action_Layer_ScreenKey
      Protected skRect.s = ReadParamString(*Params, 0)
      result = ScreenKeyLayer(skRect)
      RecordCmd(#Action_Layer_ScreenKey, skRect)
      ProcedureReturn result

  EndSelect
  ProcedureReturn 0
EndProcedure

ProcedureDLL _nbMessage(MsgCode.i, Reserved.i)
  Select MsgCode
    Case #NBMSG_EXIT_RUNMODE
      DestroyAllOverlays()

    Case #NBMSG_MOVESIZE
      ; Collect all layer keys
      Protected Dim moveKeys.s(0)
      Protected moveCount.i = 0
      ForEach gLayers()
        If gLayers()\hOverlay And gLayers()\hHost
          If moveCount >= ArraySize(moveKeys()) : ReDim moveKeys.s(moveCount + 8) : EndIf
          moveKeys(moveCount) = MapKey(gLayers())
          moveCount + 1
        EndIf
      Next

      ; Check for size changes and flag any resized layers
      Protected anyResized.i = 0
      Protected mi.i
      For mi = 0 To moveCount - 1
        If FindMapElement(gLayers(), moveKeys(mi))
          If IsWindow_(gLayers()\hHost)
            Protected rc.RECT
            GetClientRect_(gLayers()\hHost, @rc)
            Protected curW.i = rc\right - rc\left
            Protected curH.i = rc\bottom - rc\top
            If curW <> gLayers()\lastHostW Or curH <> gLayers()\lastHostH
              gLayers()\lastHostW = curW
              gLayers()\lastHostH = curH
              gLayers()\dirty = 1
              LayerRender(@gLayers())
              anyResized = 1
            EndIf
          EndIf
        EndIf
      Next

      ; Reposition all visible non-blend-hidden layers
      For mi = 0 To moveCount - 1
        If FindMapElement(gLayers(), moveKeys(mi))
          If gLayers()\visible And gLayers()\blendHidden = 0
            If anyResized
              LayerPresent(@gLayers())
            Else
              LayerReposition(@gLayers())
            EndIf
          EndIf
        EndIf
      Next

      ; Only re-run blend links if something actually resized
      If anyResized
        Protected Dim blendKeys.s(0)
        Protected blendCount.i = 0
        ForEach gBlendLinks()
          If blendCount >= ArraySize(blendKeys()) : ReDim blendKeys.s(blendCount + 8) : EndIf
          blendKeys(blendCount) = MapKey(gBlendLinks())
          blendCount + 1
        Next
        Protected bi.i
        For bi = 0 To blendCount - 1
          BlendRender(blendKeys(bi))
        Next
      Else
        ; Just reposition blend output overlays (no pixel work)
        ForEach gBlendLinks()
          Protected boKey.s = LCase(Trim(gBlendLinks()\outputRectName))
          If FindMapElement(gLayers(), boKey)
            If gLayers()\visible And gLayers()\hOverlay
              LayerReposition(@gLayers())
            EndIf
          EndIf
        Next
      EndIf

    Case #NBMSG_MINIMIZE
      ForEach gLayers()
        If gLayers()\hOverlay And IsWindow_(gLayers()\hOverlay)
          ShowWindow_(gLayers()\hOverlay, #SW_HIDE)
        EndIf
      Next

    Case #NBMSG_RESTORE
      ReplayHistory()

    Case #NBMSG_PAGECHANGE
      ForEach gLayers()
        If gLayers()\hOverlay And IsWindow_(gLayers()\hOverlay)
          ShowWindow_(gLayers()\hOverlay, #SW_HIDE)
        EndIf
      Next
  EndSelect
EndProcedure

ProcedureDLL _nbRegisterInterfaceAccess(*Proc)
  g_nbInterface = *Proc
EndProcedure

ProcedureDLL _nbRegisterScriptProcessor(*Proc)
  g_nbPlayAction = *Proc
EndProcedure

ProcedureDLL _nbAbout()
  ; Disabled - causes host freeze in some VisualNEO builds
EndProcedure

ProcedureDLL AttachProcess(instance)
  g_hInstance = instance
EndProcedure

ProcedureDLL DetachProcess(instance)
  DestroyAllOverlays()

  If g_gdiplusStarted
    GdiplusShutdown(g_gdiplusToken)
    g_gdiplusStarted = 0
  EndIf

  If *gPluginTitle  : GlobalFree_(*gPluginTitle)  : EndIf
  If *gPluginAuthor : GlobalFree_(*gPluginAuthor) : EndIf
  If *gPluginHint   : GlobalFree_(*gPluginHint)   : EndIf
  If *gAct1_Name : GlobalFree_(*gAct1_Name) : EndIf
  If *gAct1_Hint : GlobalFree_(*gAct1_Hint) : EndIf
  If *gAct2_Name : GlobalFree_(*gAct2_Name) : EndIf
  If *gAct2_Hint : GlobalFree_(*gAct2_Hint) : EndIf
  If *gAct3_Name : GlobalFree_(*gAct3_Name) : EndIf
  If *gAct3_Hint : GlobalFree_(*gAct3_Hint) : EndIf
  If *gAct4_Name : GlobalFree_(*gAct4_Name) : EndIf
  If *gAct4_Hint : GlobalFree_(*gAct4_Hint) : EndIf
  If *gAct5_Name : GlobalFree_(*gAct5_Name) : EndIf
  If *gAct5_Hint : GlobalFree_(*gAct5_Hint) : EndIf
  If *gAct6_Name : GlobalFree_(*gAct6_Name) : EndIf
  If *gAct6_Hint : GlobalFree_(*gAct6_Hint) : EndIf
  If *gAct7_Name : GlobalFree_(*gAct7_Name) : EndIf
  If *gAct7_Hint : GlobalFree_(*gAct7_Hint) : EndIf
  If *gAct8_Name : GlobalFree_(*gAct8_Name) : EndIf
  If *gAct8_Hint : GlobalFree_(*gAct8_Hint) : EndIf
  If *gAct9_Name : GlobalFree_(*gAct9_Name) : EndIf
  If *gAct9_Hint : GlobalFree_(*gAct9_Hint) : EndIf
EndProcedure

; IDE Options = PureBasic 6.21 (Windows - x86)
; ExecutableFormat = Shared dll
; Folding = ------------
; EnableXP
; DPIAware
; Executable = NeoOverlay.dll
; CompileSourceDirectory
; Compiler = PureBasic 6.21 (Windows - x86)