# Cairn

## [48](https://github.com/ChronicTinkerer/Cairn/tree/48) (2026-05-13)
[Full Changelog](https://github.com/ChronicTinkerer/Cairn/compare/47...48) [Previous Releases](https://github.com/ChronicTinkerer/Cairn/releases)

- release 48  
    Cairn-Gui-Widgets-Standard-2.0 MINOR 12 -> 15:  
    - MINOR 13: TabGroup wraps to multi-row on overflow. Auto-grows  
      strip height + re-anchors content panes; OnSizeChanged repacks  
      rows live on window resize.  
    - MINOR 14: TreeView widget. Generic expand/collapse hierarchical  
      list. Single-click toggles expansion + fires Click event. Frame  
      height auto-grows to fit visible rows.  
    - MINOR 15: TreeView honors node.expandable=true so consumers can  
      mark a branch expandable without pre-building children, populating  
      on the Toggle event.  
    Cairn-Util-1.0 MINOR 32 -> 35:  
    - MINOR 33: String.SafePreview(s, maxLen). pcall-wrapped preview;  
      returns nil on WoW "secret string" values so callers can render  
      a placeholder instead of tainting on :gsub.  
    - MINOR 34: Table.SafeKeys(t, sortFn?). pcall-wrapped pairs() that  
      returns nil on tables protected from addon iteration.  
    - MINOR 35: Table.SafeCount(t). Same protection, count only (no  
      array build).  
    All five Cairn TOC flavors registered the new TreeView.lua file.  
    First consumers: Forge\_APIBrowser v2, Forge\_Tables v2, Forge\_CVars  
    v2 (in the Forge repo).  
- Cairn: safe-introspection helpers + TreeView lazy expansion  
    Cairn-Util-1.0 MINOR 32 -> 35:  
    - String.SafePreview(s, maxLen): pcall-wrapped preview that returns  
      nil on WoW "secret string" values (locale entries, protected  
      globals) so callers can render a placeholder instead of tainting  
      on :gsub.  
    - Table.SafeKeys(t, sortFn?): pcall-wrapped pairs() that returns  
      nil on tables protected from addon iteration (Blizzard internal  
      frames, secure containers).  
    - Table.SafeCount(t): same protection, returns just the count  
      without allocating the key array.  
    All three pulled up from inline pcall code in Forge\_Tables. The  
    hazards are general (any inspector tool walking unknown data hits  
    them); centralizing in Cairn-Util means Forge\_Console, future API  
    introspectors, and anything else avoid re-rolling.  
    Cairn-Gui-Widgets-Standard-2.0 MINOR 14 -> 15:  
    - TreeView honors node.expandable=true even when children is empty.  
      Consumer populates children on the Toggle event and calls Refresh,  
      so walking large trees becomes pay-as-you-go. Hit on Forge\_Tables:  
      eagerly walking \_G at depth 2 did 500k+ ops and froze the client.  
      Lazy expansion via this flag keeps initial build to root keys only.  
    Tested in-game on interface 120005 via Forge\_Tables (separate Forge  
    commit).  
- Cairn-Gui-Widgets-Standard-2.0 MINOR 14: TreeView widget  
    Generic expand/collapse hierarchical list. Consumer provides  
    { id, label, aux, children } nodes; widget renders one row per  
    visible node with indent + indicator. Collapsed branches contribute  
    one row regardless of how many descendants they have, so the  
    widget naturally handles large data sets that overwhelm a flat  
    list (Forge\_APIBrowser has ~3000+ entries).  
    - Single-click toggles expansion on branches and fires "Click" with  
      the original node, so consumers can still do click-to-select.  
      "Toggle" event also fires on expand/collapse for programmatic  
      consumers.  
    - Frame height auto-grows to fit visible rows; wrap in a ScrollFrame  
      for tall trees. Consumer hooks OnSizeChanged to push the tree  
      height into ScrollFrame:SetContentHeight.  
    - SetNodes preserves expansion state by id, so consumers that filter  
      trees by rebuilding don't lose user-expanded branches across  
      filter changes (ids that survive the filter stay expanded).  
    - Registered in all 5 TOCs (Cairn.toc + Mists/TBC/Vanilla/XPTR flavor  
      variants).  
    First consumer: Forge\_APIBrowser v2 (separate Forge commit).  
- Cairn-Gui-Widgets-Standard-2.0 MINOR 13: TabGroup wraps to multi-row on overflow  
    Previously the tab strip extended past the TabGroup frame's right edge  
    when total tab width exceeded the strip width (visible on Forge with  
    18 sub-addons at default window width: "About" clipped past the Forge  
    window's right edge).  
    - relayoutTabs tracks current x and wraps to a new row when the next  
      button won't fit. Skips the wrap when x == STRIP\_PAD so a single  
      oversized button still renders (clipped) rather than wrapping  
      infinitely.  
    - Strip auto-grows in height to fit all rows. Content panes re-anchor  
      to the new strip bottom via reanchorPanes() so tab content always  
      starts directly below the (possibly multi-row) strip.  
    - HookScript("OnSizeChanged") on the TabGroup frame re-runs relayout  
      on resize, so widening the host window repacks tabs into fewer  
      rows live.  
    Header doc updated; "no tabs that scroll horizontally - deferred"  
    removed (multi-row wrap subsumes the use case).  
    Tested in-game on Forge with 18 tabs across two rows; resizing the  
    window repacked them as expected.  
