--[[--------------------------------------------------------------------
  PaladinHolyPower – ui/components/textbox.lua  (lean row-based browser)
  Textbox with clickable icon that opens a right-side icon browser.
  NOTE: The header divider is now drawn *behind* the browser frame.
----------------------------------------------------------------------]]

local PHP = _G.PaladinHolyPower
if not PHP then return end

PHP.Textbox = PHP.Textbox or {}
local M = PHP.Textbox

-- Theme (source of truth)
local Theme = PHP.Theme
local WHITE_TEX   = (Theme and Theme.WhiteTex and Theme.WhiteTex()) or "Interface\\Buttons\\WHITE8x8"
local DIV_THICK   = (Theme and Theme.DividerThickness and Theme.DividerThickness()) or 1
local HEADER_H    = 44

-- Simple tint getter with fallbacks (matches panel.lua defaults)
local function Tint(name, fb)
  if Theme and Theme.GetTint then
    local t = Theme.GetTint(name)
    if t then return t end
  end
  return fb
end

-- Fallback content tints if Theme absent
local TINT_ROOT_FB    = { 0.98, 0.98, 0.99, 0.75 }
local TINT_HEADER_FB  = { 0.95, 0.90, 0.55, 0.75 }
local TINT_CONTENT_FB = { 0.95, 0.90, 0.55, 0.70 }

-- Divider helpers
local function NewDivider(parent, layer)
  if Theme and Theme.NewDivider then
    return Theme.NewDivider(parent, layer or "OVERLAY")
  end
  local t = parent:CreateTexture(nil, layer or "OVERLAY")
  t:SetTexture(WHITE_TEX)
  local c = PHP.ACCENT_COLOR or { 1,1,1,1 }
  t:SetColorTexture(c[1], c[2], c[3], c[4])
  if t.SetSnapToPixelGrid then t:SetSnapToPixelGrid(true); t:SetTexelSnappingBias(0) end
  return t
end
local function RecolorDivider(tex)
  if Theme and Theme.RecolorDivider then
    Theme.RecolorDivider(tex); return
  end
  local c = PHP.ACCENT_COLOR or { 1,1,1,1 }
  tex:SetColorTexture(c[1], c[2], c[3], c[4])
end
local PixelUtil = _G.PixelUtil
local function DrawH(line, frame, yFromTop)
  if Theme and Theme.DrawH then Theme.DrawH(line, frame, yFromTop); return end
  line:ClearAllPoints()
  if PixelUtil and PixelUtil.SetPoint then
    PixelUtil.SetPoint(line, "TOPLEFT",  frame, "TOPLEFT",  0, -yFromTop)
    PixelUtil.SetPoint(line, "TOPRIGHT", frame, "TOPRIGHT", 0, -yFromTop)
    PixelUtil.SetHeight(line, DIV_THICK)
  else
    line:SetPoint("TOPLEFT",  frame, "TOPLEFT",  0, -yFromTop)
    line:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 0, -yFromTop)
    line:SetHeight(DIV_THICK)
  end
end

-- ------------------ style ------------------
local C = {
  WIDTH=160, HEIGHT=22, PAD=6, GAP=4, ICON=28,
  BG=WHITE_TEX,
  BORDER=WHITE_TEX,
  BG_COLOR={0.22, 0.20, 0.14, 1},
  BORDER_COLOR={0,0,0,1},

  TITLE_FONT="GameFontNormal",
  LIST_FONT="GameFontHighlightSmall",
  TRACK_COLOR={0.22, 0.20, 0.14, 1},
  THUMB_COLOR={1.00, 0.95, 0.75, 1},
  DEFAULT_ICON=1445240,
}

local function SetWhite(fs, size)
  local f = fs:GetFont()
  fs:SetFont(f or "Fonts\\FRIZQT__.TTF", size or 12, "")
  fs:SetTextColor(1,1,1)
end

local function toFileID(v)
  if type(v)=="number" then return v end
  if type(v)=="string" and v:match("^%s*%d+%s*$") then return tonumber(v) end
  return nil
end

local function Valid(v, fallback) return (v and v>10) and v or fallback end

-- --------------- icon DB (only while browser is open) ---------------
local DB = { list={}, paths={}, filter={}, ready=false, heavy=false }
local EMPTY = {}

local function WipeDB()
  DB.ready=false
  DB.list, DB.paths, DB.filter = nil, nil, nil
  collectgarbage("collect"); collectgarbage("collect")
end

local function AddIcon(seen, fid, path)
  fid = tonumber(fid)
  if not fid or seen[fid] then return end
  seen[fid]=true
  DB.list[#DB.list+1]=fid
  DB.paths[fid]=path or ("file:"..fid)
end

-- Build light catalog (no Blizzard_IconSelector)
local function BuildLightCatalog(seen)
  pcall(function()
    if type(GetMacroIcons)=="function" then
      local t = {}; GetMacroIcons(t)
      for _,fid in ipairs(t) do AddIcon(seen, fid) end
    end
    if type(GetMacroItemIcons)=="function" then
      local t = {}; GetMacroItemIcons(t)
      for _,fid in ipairs(t) do AddIcon(seen, fid) end
    end
  end)
  if #DB.list==0 then
    for _,fid in ipairs({134400,134401,134402,236689,237299,458717,571585,613954,632353,803763,1100022,C.DEFAULT_ICON}) do
      AddIcon(seen,fid)
    end
  end
end

-- Expand to heavy catalog (loads Blizzard_IconSelector ONCE when user opts in)
local function ExpandToHeavyCatalog(seen)
  if DB.heavy then return end
  DB.heavy = true
  pcall(function()
    if not _G.IconDataProviderMixin then LoadAddOn("Blizzard_IconSelector") end
    local Mix = _G.IconDataProviderMixin
    if not Mix then return end
    local provider = (CreateFromMixins and CreateFromMixins(Mix))
                 or (CreateAndInitFromMixin and CreateAndInitFromMixin(Mix, nil))
    if not provider then return end
    local function Harvest()
      if provider.GenerateIconFileIDs then provider:GenerateIconFileIDs() end
      local n = (provider.GetNumIcons and provider:GetNumIcons()) or 0
      for i=1,n do
        local fid, path = provider:GetIconByIndex(i)
        AddIcon(seen, fid, path)
      end
    end
    Harvest()
    local IconSet = _G.IconDataProviderIconSet
    if IconSet then
      for _,setVal in pairs(IconSet) do
        if provider.SetIconSet then provider:SetIconSet(setVal) end
        Harvest()
      end
    end
    local Extra = _G.IconDataProviderExtraType
    if Extra then
      for _,extraVal in pairs(Extra) do
        if provider.Init then provider:Init(extraVal) end
        Harvest()
      end
    end
  end)
end

local function EnsureDB(useHeavy)
  if not DB.list   then DB.list   = {} end
  if not DB.paths  then DB.paths  = {} end
  if not DB.filter then DB.filter = {} end
  if DB.ready and (not useHeavy or DB.heavy) then return end

  local seen = {}
  if useHeavy then
    if not DB.ready then BuildLightCatalog(seen) end
    ExpandToHeavyCatalog(seen)
  else
    BuildLightCatalog(seen)
  end

  for k in pairs(DB.filter) do DB.filter[k]=nil end
  for i=1,#DB.list do DB.filter[i]=DB.list[i] end
  DB.ready=true
end

-- --------------- browser (single instance, row-based) ---------------
M.Browser = M.Browser or {}

local function FreeCells()
  local B = M.Browser
  if not B or not B.cells then return end
  for i=1,#B.cells do
    local btn = B.cells[i]
    if btn then
      if btn.tex then btn.tex:SetTexture(nil) end
      btn:SetScript("OnClick", nil)
      btn:SetScript("OnEnter", nil)
      btn:SetScript("OnLeave", nil)
      btn:Hide()
      btn:ClearAllPoints()
      btn:SetParent(nil)
      B.cells[i]=nil
    end
  end
end

local function DestroyBrowser()
  local B = M.Browser
  if not B or not B.frame then return end

  if B.view   then B.view:SetScript("OnMouseWheel", nil); B.view:SetScript("OnSizeChanged", nil) end
  if B.slider then B.slider:SetScript("OnValueChanged", nil) end
  if B.search then B.search:SetScript("OnTextChanged", nil) end
  if B.moreBtn then B.moreBtn:SetScript("OnClick", nil) end

  FreeCells()
  B.cells = nil
  WipeDB()

  local f = B.frame
  if f then
    f:SetScript("OnShow", nil)
    f:SetScript("OnHide", nil)
    if f.UnregisterAllEvents then f:UnregisterAllEvents() end
    f:Hide()
    f:SetParent(nil)
  end
  if _G["PHP_IconBrowser"] then _G["PHP_IconBrowser"] = nil end

  B.frame, B.view, B.slider, B.search, B.moreBtn = nil,nil,nil,nil,nil
  B.onPick = nil
  B.cols, B.rowsVisible, B.rowH = nil,nil,nil
  B.firstRow, B.maxFirstRow, B.RebuildFilter = nil,nil,nil

  collectgarbage("collect"); collectgarbage("collect")
end

local function CloseBrowser()
  local B = M.Browser
  if not B.frame then return end
  B.frame:Hide()
  if C_Timer and C_Timer.After then
    C_Timer.After(0, DestroyBrowser)
  else
    DestroyBrowser()
  end
end

local function EnsureBrowser(host, php, onPick)
  local B = M.Browser
  if B.frame then B.onPick = onPick; return B.frame end

  local panel = php and php.SettingsPanel or host or UIParent
  local hostW = Valid(panel.GetWidth and panel:GetWidth(), 391)
  local hostH = Valid(panel.GetHeight and panel:GetHeight(), 400)

  -- Root frame (NO outer border)
  local f = CreateFrame("Frame","PHP_IconBrowser",UIParent,"BackdropTemplate")
  if Theme and Theme.ApplyRootBG then
    Theme.ApplyRootBG(f)
  else
    f:SetBackdrop({ bgFile = (Theme and Theme.Texture and Theme.Texture()) or "Interface\\AddOns\\PaladinHolyPower\\media\\icons\\background\\panel_1.png" })
    local t = Tint("root", TINT_ROOT_FB)
    f:SetBackdropColor(t[1], t[2], t[3], t[4])
  end
  f:SetPoint("TOPLEFT", panel, "TOPRIGHT", 8, 0)
  f:SetSize(hostW, hostH)
  f:SetFrameStrata("DIALOG")

  -- Header
  local header = CreateFrame("Frame", nil, f, "BackdropTemplate")
  header:SetPoint("TOPLEFT", 0, 0)
  header:SetPoint("TOPRIGHT", 0, 0)
  header:SetHeight(HEADER_H)
  if Theme and Theme.ApplyHeaderBG then
    Theme.ApplyHeaderBG(header)
  else
    local tex = (Theme and Theme.Texture and Theme.Texture()) or "Interface\\AddOns\\PaladinHolyPower\\media\\icons\\background\\panel_1.png"
    local tint = Tint("header", TINT_HEADER_FB)
    header:SetBackdrop({ bgFile = tex }); header:SetBackdropColor(tint[1], tint[2], tint[3], tint[4])
  end

  -- Title
  local title = header:CreateFontString(nil,"OVERLAY","GameFontNormalLarge")
  title:SetPoint("TOPLEFT", 10, -10)
  title:SetText("Select Icon"); SetWhite(title, 20)

  -- Close (on root)
  local close = CreateFrame("Frame", nil, f)
  close:SetSize(14,14); close:SetPoint("TOPRIGHT",-6,-6); close:EnableMouse(true)
  local l1=close:CreateTexture(nil,"ARTWORK"); l1:SetTexture(WHITE_TEX); l1:SetSize(2,14); l1:SetPoint("CENTER"); l1:SetRotation(math.rad(45))
  local l2=close:CreateTexture(nil,"ARTWORK"); l2:SetTexture(WHITE_TEX); l2:SetSize(2,14); l2:SetPoint("CENTER"); l2:SetRotation(math.rad(-45))
  close:SetScript("OnEnter", function() l1:SetVertexColor(.8,.8,.8); l2:SetVertexColor(.8,.8,.8) end)
  close:SetScript("OnLeave", function() l1:SetVertexColor(1,1,1); l2:SetVertexColor(1,1,1) end)
  close:SetScript("OnMouseUp", CloseBrowser)

  -- Single horizontal divider under header (BEHIND all content)
  local back = CreateFrame("Frame", nil, f)
  back:SetAllPoints(f)
  back:EnableMouse(false)
  back:SetFrameStrata(f:GetFrameStrata())
  local lvl = (f:GetFrameLevel() or 1) - 1
  if lvl < 0 then lvl = 0 end
  back:SetFrameLevel(lvl)
  back._line = back._line or NewDivider(back, "BACKGROUND")
  local function RedrawDivider()
    RecolorDivider(back._line)
    DrawH(back._line, back, HEADER_H)
  end
  RedrawDivider()
  f:HookScript("OnSizeChanged", RedrawDivider)

  -- Body (content area)
  local body = CreateFrame("Frame", nil, f, "BackdropTemplate")
  body:SetPoint("TOPLEFT", 0, -HEADER_H)
  body:SetPoint("BOTTOMRIGHT", 0, 0)
  if Theme and Theme.ApplyContentBG then
    Theme.ApplyContentBG(body)
  else
    local tex = (Theme and Theme.Texture and Theme.Texture()) or "Interface\\AddOns\\PaladinHolyPower\\media\\icons\\background\\panel_1.png"
    local tint = Tint("content", TINT_CONTENT_FB)
    body:SetBackdrop({ bgFile = tex }); body:SetBackdropColor(tint[1], tint[2], tint[3], tint[4])
  end

  -- Soft top gradient like panel.lua content
  local grad = body:CreateTexture(nil, "BACKGROUND")
  grad:SetPoint("TOPLEFT"); grad:SetPoint("TOPRIGHT"); grad:SetHeight(96)
  if grad.SetGradient and CreateColor then
    grad:SetGradient("VERTICAL", CreateColor(1,1,1,0.06), CreateColor(1,1,1,0.00))
  end

  -- Search (now anchored to BODY)
  local search = CreateFrame("EditBox", nil, body, "BackdropTemplate")
  search:SetPoint("TOPLEFT",  C.PAD, -10)
  search:SetPoint("TOPRIGHT", -C.PAD-84, -10)  -- leave room for "More…" button
  search:SetHeight(22)
  search:SetBackdrop({ bgFile=C.BG, edgeFile=C.BORDER, edgeSize=1 })
  search:SetBackdropColor(unpack(C.BG_COLOR))
  search:SetBackdropBorderColor(unpack(C.BORDER_COLOR))
  search:SetAutoFocus(false)
  search:SetJustifyH("LEFT")
  search:SetTextInsets(6,6,0,0)
  SetWhite(search, 12)

  -- "More icons…" button (loads Blizzard_IconSelector on demand)
  local moreBtn = CreateFrame("Button", nil, body, "BackdropTemplate")
  moreBtn:SetPoint("TOPRIGHT", -C.PAD, -10)
  moreBtn:SetSize(74, 22)
  moreBtn:SetBackdrop({ bgFile=C.BG, edgeFile=C.BORDER, edgeSize=1 })
  moreBtn:SetBackdropColor(unpack(C.BG_COLOR))
  moreBtn:SetBackdropBorderColor(unpack(C.BORDER_COLOR))
  local moreFS = moreBtn:CreateFontString(nil, "OVERLAY", C.LIST_FONT)
  moreFS:SetPoint("CENTER"); SetWhite(moreFS, 11); moreFS:SetText("More…")

  -- Viewport + slider (inside BODY)
  local view = CreateFrame("Frame", nil, body)
  view:SetPoint("TOPLEFT", C.PAD, -(10+22+C.GAP))
  view:SetPoint("BOTTOMRIGHT", -C.PAD-10, C.PAD)
  view:EnableMouseWheel(true)
  view:SetClipsChildren(true)

  local sb = CreateFrame("Slider", nil, body)
  sb:SetOrientation("VERTICAL")
  sb:SetPoint("TOPRIGHT", -C.PAD, -(10+22+C.GAP))
  sb:SetPoint("BOTTOMRIGHT", -C.PAD, C.PAD)
  sb:SetWidth(8)
  local track = sb:CreateTexture(nil,"BACKGROUND"); track:SetAllPoints(); track:SetColorTexture(unpack(C.TRACK_COLOR))
  local thumb  = sb:CreateTexture(nil,"ARTWORK");   thumb:SetColorTexture(unpack(C.THUMB_COLOR)); thumb:SetSize(8,32)
  sb:SetThumbTexture(thumb)

  -- state
  B.frame, B.view, B.slider, B.search, B.moreBtn = f, view, sb, search, moreBtn
  B.onPick = onPick
  B.cells = {}
  B.cols, B.rowsVisible, B.rowH = 0, 0, (C.ICON + C.GAP)
  B.firstRow, B.maxFirstRow = 0, 0

  -- build just enough cells for visible area (+1 row buffer)
  local function EnsureCells()
    if not B.cells then B.cells = {} end
    local need = (B.rowsVisible + 1) * math.max(B.cols,1)
    while #B.cells < need do
      local btn = CreateFrame("Button", nil, view)
      btn:SetSize(C.ICON, C.ICON)
      btn.tex = btn:CreateTexture(nil,"ARTWORK"); btn.tex:SetAllPoints()
      local hl = btn:CreateTexture(nil,"HIGHLIGHT"); hl:SetAllPoints(); hl:SetColorTexture(1,1,1,0.10)
      btn:SetScript("OnClick", function(self)
        local fid = self._fid
        if B.onPick and fid then
          local path = (DB.paths or EMPTY)[fid]
          B.onPick(fid, path)
        end
        CloseBrowser()
      end)
      B.cells[#B.cells+1] = btn
    end
  end

  local function ComputeGrid()
    local w = Valid(view:GetWidth(), 320)
    local h = Valid(view:GetHeight(), 260)
    B.cols = math.max(4, math.floor((w + C.GAP) / (C.ICON + C.GAP)))
    B.rowsVisible = math.max(1, math.floor((h + C.GAP) / (C.ICON + C.GAP)))
  end

  local function SyncRange()
    local filter = DB.filter or EMPTY
    local total = #filter
    local cols  = math.max(1, B.cols)
    local rowsTotal = math.ceil(total / cols)
    B.maxFirstRow = math.max(0, rowsTotal - B.rowsVisible)
    B.firstRow    = math.min(B.firstRow or 0, B.maxFirstRow)
    sb:SetMinMaxValues(0, B.maxFirstRow)
    sb:SetValueStep(1)
    thumb:SetShown(B.maxFirstRow > 0)
  end

  local function Render()
    local cols  = math.max(1, B.cols)
    local start = B.firstRow * cols + 1
    local filter = DB.filter or EMPTY
    EnsureCells()
    for i=1,#B.cells do
      local btn = B.cells[i]
      local idx = start + (i-1)
      local fid = filter[idx]
      if fid then
        local row = math.floor((i-1)/cols)
        local col = (i-1)%cols
        btn:ClearAllPoints()
        btn:SetPoint("TOPLEFT", view, "TOPLEFT", col*(C.ICON+C.GAP), -row*(C.ICON+C.GAP))
        btn._fid = fid
        btn.tex:SetTexture(fid)
        btn:Show()
      else
        btn._fid = nil
        btn:Hide()
      end
    end
  end

  -- filter (robust against early calls before EnsureDB)
  local function RebuildFilter(q)
    if not DB.list or not DB.filter or not DB.paths or not DB.ready then
      EnsureDB(DB.heavy)
    end

    local list   = DB.list   or EMPTY
    local paths  = DB.paths  or EMPTY
    local filter = DB.filter or {}

    for k in pairs(filter) do filter[k] = nil end

    if not q or q == "" then
      for i = 1, #list do filter[i] = list[i] end
    else
      local isNum  = q:match("^%s*%d+%s*$")
      local needle = q:lower()
      local n = 0
      if isNum then
        for i = 1, #list do
          local fid = list[i]
          if tostring(fid):find(needle, 1, true) then
            n = n + 1; filter[n] = fid
          end
        end
      else
        for i = 1, #list do
          local fid  = list[i]
          local path = (paths[fid] or ""):lower()
          if path:find(needle, 1, true) then
            n = n + 1; filter[n] = fid
          end
        end
      end
    end

    DB.filter = filter
    B.firstRow = 0
    sb:SetValue(0)
    SyncRange()
    Render()
  end
  B.RebuildFilter = RebuildFilter

  -- handlers
  sb:SetScript("OnValueChanged", function(_, v) B.firstRow = math.floor(v + 0.5); Render() end)
  view:SetScript("OnMouseWheel", function(_, d)
    local cur, minV, maxV = sb:GetValue(), sb:GetMinMaxValues()
    local step = (d > 0) and -3 or 3
    sb:SetValue(math.max(minV, math.min(maxV, cur + step)))
  end)
  search:SetScript("OnTextChanged", function(self) RebuildFilter(self:GetText()) end)

  -- First layout (deferred)
  local function FirstLayout(lightOnly)
    EnsureDB(not lightOnly and DB.heavy)
    if panel and panel.GetWidth  then f:SetWidth(panel:GetWidth())  end
    if panel and panel.GetHeight then f:SetHeight(panel:GetHeight()) end
    ComputeGrid()
    EnsureCells()
    RebuildFilter(search:GetText() or "")
  end

  -- “More…” loads heavy catalog (one-time)
  moreBtn:SetScript("OnClick", function()
    EnsureDB(true)
    RebuildFilter(search:GetText() or "")
    SetWhite(moreFS, 11); moreFS:SetText("All ✓")
    moreBtn:Disable()
  end)

  f:SetScript("OnShow", function() C_Timer.After(0, function() FirstLayout(true) end) end)
  f:SetScript("OnHide", function() WipeDB() end)

  f:Hide()
  return f
end

-- --------------- textbox builder ---------------
function M.AddIconTextbox(parent, PHPobj, S, x, y, opts)
  local labelText = (opts and opts.label) or "Custom Icon"
  local tip       = opts and opts.tooltip
  local curID     = toFileID(opts and opts.value or S.customIcon) or C.DEFAULT_ICON

  local box = CreateFrame("Frame", nil, parent, "BackdropTemplate")
  box:SetSize(C.WIDTH, C.HEIGHT)
  box:SetPoint("TOPLEFT", parent, "TOPLEFT", x or 0, y or 0)
  box:SetBackdrop({ bgFile=C.BG, edgeFile=C.BORDER, edgeSize=1 })
  box:SetBackdropColor(unpack(C.BG_COLOR))
  box:SetBackdropBorderColor(unpack(C.BORDER_COLOR))

  local title = box:CreateFontString(nil,"OVERLAY",C.TITLE_FONT)
  title:SetPoint("BOTTOMLEFT", box, "TOPLEFT", 0, 2)
  title:SetText(labelText); SetWhite(title, 12)

  local edit = CreateFrame("EditBox", nil, box, "BackdropTemplate")
  edit:SetAutoFocus(false)
  edit:SetPoint("TOPLEFT", 6, -2)
  edit:SetPoint("BOTTOMRIGHT", -26, 2)
  edit:SetJustifyH("LEFT")
  SetWhite(edit, 12)
  edit:SetText(tostring(curID))

  local ibtn = CreateFrame("Button", nil, box)
  ibtn:SetPoint("RIGHT", -1, 0)
  ibtn:SetSize(C.HEIGHT - 2, C.HEIGHT - 2)

  ibtn.tex = ibtn:CreateTexture(nil, "ARTWORK")
  ibtn.tex:SetPoint("TOPLEFT", 0, 0)
  ibtn.tex:SetPoint("BOTTOMRIGHT", 0, 0)
  ibtn.tex:SetTexture(curID ~= 0 and curID or C.DEFAULT_ICON)
  ibtn.tex:SetTexCoord(0.10, 0.90, 0.10, 0.90)
  ibtn.tex:SetVertexColor(1, 1, 1, 1)

  ibtn.highlight = ibtn:CreateTexture(nil, "HIGHLIGHT")
  ibtn.highlight:SetAllPoints(ibtn.tex)
  ibtn.highlight:SetColorTexture(1, 1, 1, 0.15)
  ibtn.highlight:Hide()

  ibtn.pushed = ibtn:CreateTexture(nil, "OVERLAY")
  ibtn.pushed:SetAllPoints(ibtn.tex)
  ibtn.pushed:SetColorTexture(0.95, 0.90, 0.60, 0.4)
  ibtn.pushed:Hide()

  ibtn:SetScript("OnEnter", function(self) self.highlight:Show() end)
  ibtn:SetScript("OnLeave", function(self) self.highlight:Hide(); self.pushed:Hide() end)
  ibtn:SetScript("OnMouseDown", function(self) self.highlight:Hide(); self.pushed:Show() end)
  ibtn:SetScript("OnMouseUp",   function(self) self.pushed:Hide(); self.highlight:Show() end)

  if PHPobj and PHPobj.Tooltip and PHPobj.Tooltip.Attach then
    PHPobj.Tooltip.Attach(ibtn, labelText, tip or "Click the icon to pick. Paste a file ID to set manually.")
  end

  local function Apply(fileID, path)
    fileID = toFileID(fileID) or C.DEFAULT_ICON
    ibtn.tex:SetTexture(fileID)
    edit:SetText(tostring(fileID))
    if opts and type(opts.onChange)=="function" then
      opts.onChange(fileID, path or ("file:"..fileID))
    end
  end

  edit:SetScript("OnEnterPressed", function(self)
    local v = toFileID(self:GetText())
    if v then Apply(v, (DB.paths or EMPTY)[v]) end
    self:ClearFocus()
  end)
  edit:SetScript("OnEscapePressed", function(self)
    self:ClearFocus()
    self:SetText(tostring(curID))
  end)

  ibtn:SetScript("OnClick", function()
    local browser = EnsureBrowser(PHPobj and PHPobj.SettingsPanel or parent, PHPobj, Apply)
    if browser:IsShown() then
      browser:Hide()
    else
      browser:Show()
      if M.Browser and M.Browser.search then M.Browser.search:SetText("") end
    end
  end)

  function box:SetShownSafe(show)
    if self.SetShown then self:SetShown(not not show)
    elseif show then self:Show() else self:Hide() end
  end

  return box
end