NUI is the Nuklear User Interface that NWN:EE has. This is now partially exposed to modders to pop up windows on compatible clients.

The NUI system is driven primarily by JSON, but usually you use helper functions such as those in nw_inc_nui.nss

This page will cover some of the non-scripting side of NUI - for the actual functions check out the Lexicon.

NUI Hardcoded Elements

The look and feel of NUI elements are hardcoded, such as the colour scheme and general graphical files (arrows, checkboxes, buttons, outlines) used by the game for it. These can be overriden if applied in /override or /development on a client but needs to be present before a module is loaded, so not possible to put into nwsync or a hakpack.

Notably these are also used for the in game Options, New Module and NWscript/Debug panels.

These are generally in the bif file bd_nui.bif all prefixed "nui_" TGA files and some other graphics which are JPG and PNG which are used for the module panel.

New Image File Types

NUI allows loading of new file types previously unavailable, that can be added to hakpacks and nwsync:

TGA can be loaded as normal, PLT is not supported however (if a DDS version of a TGA file exists, for instance, it just won't ever find the DDS version and loads the TGA one instead).

The order of priority for same-named files are: JPG > TGA > PNG > GIF > WBM > DDS

Layout Information

The cassowary implementation in use: https://github.com/nucleic/kiwi

The nwn-side constraints it sets up for layouting are not particularly efficient, but at least they work: https://gist.github.com/niv/9bf60ca649fdf6709c17886a1a467755

std::vector<Constraint> Layoutable::BuildConstraints(const LayoutExpression& layout) const
{
    INSTR_SCOPE();
    INSTR_SCOPE_PROP_STR("m_id", m_id.c_str());

    std::vector<Constraint> ret;
    for (auto& c : m_constraint_applicators)
    {
        const auto mod = c(*this, layout);
        assert(!mod.empty());
        ret.insert(ret.end(), mod.begin(), mod.end());
    }

    ret.insert(ret.end(), m_custom_constraints.begin(), m_custom_constraints.end());
    ret.insert(ret.end(), m_padding_constraints.begin(), m_padding_constraints.end());
    ret.insert(ret.end(), m_margin_constraints.begin(), m_margin_constraints.end());

    const auto dep = BuildConstraintDependants(layout);
    ret.insert(ret.end(), dep.begin(), dep.end());

    return ret;
}

void LayoutGroup::SetChildren(const std::vector<LayoutableRef>& widget)
{
    if (m_children == widget)
        return;

    INSTR_SCOPE();
    INSTR_SCOPE_PROP_STR("m_id", m_id.c_str());

    for (auto q : m_children)
    {
        q->OnDetach(q->m_group);
    }

    Layoutable::SetChildren(widget);

    for (auto q : m_children)
    {
        q->OnAttached(this);
    }

    // This LG received new children: We need to redo the contained layout.
    // Currently, the fastest way to redo the constraints is to rebuild the solver from scratch.
    // Possibly later: Optimise this by only removing/adding the constraints of the children that
    //      have actually changed. This can only be a win when swapping out part of the layout, and
    //      that's currently not supported.
    m_solver.reset();

#ifdef WIP_CHILD_LG_ADVISES_PARENT
    assert(!"Resetting the solver clears out all child associations. Have to redo them.");
#endif

    // This is the inner layout for the solver. It always starts at (0, 0) and is
    // the width and height as suggested by the parent layout group.
    // This dummy layout here binds the layout to the solver wh without actually exposing
    // the solver parent variables.
    const LayoutExpression childLayout = {{0}, {0}, {gw}, {gh}, s_guiscale};

    // We suggest layouting to the children of this group by giving them constraints
    // on the inner layout. We do this via edit variables, which are updated every time
    // geometry changes. The child layout can, of course, still overflow; in which case
    // it will either be clipped or scrollable.
    m_solver.addEditVariable(gw, medium);
    m_solver.addEditVariable(gh, medium);

    m_layout_constraints.clear();
    m_layout_gui_scale_applied = nk_get_gui_scale();
    m_layout_constraints = AddConstraints({s_guiscale == m_layout_gui_scale_applied | required});

    m_child_constraints.clear();
    for (auto& entry : m_children)
    {

        const auto cc = entry->BuildConstraints(childLayout);
        m_child_constraints.insert(m_child_constraints.end(),
                cc.begin(), cc.end());
    }
    AddConstraints(m_child_constraints);

    // Reset cached data so the child entries can reflow. We use the current
    // layout values here that, while not entirely accurate, are hopefully
    // accurate enough to prevent a scrollbar flicker.
    m_cached_content_pane_size = nk_vec2(layout.w.value(), layout.h.value());

    m_solver.updateVariables();
}


std::vector<Constraint> Span::BuildConstraintDependants(const LayoutExpression& layout) const
{
    INSTR_SCOPE();
    INSTR_SCOPE_PROP_STR("m_id", m_id.c_str());

    std::vector<Constraint> ret;

    const auto primary             = m_direction;
    const auto secondary           = m_direction == Vertical ? Horizontal : Vertical;

    const auto layout_position     = [layout](Direction axis) { return axis == Vertical ? layout.y : layout.x; };
    const auto layout_size         = [layout](Direction axis) { return axis == Vertical ? layout.h : layout.w; };

    const auto widget_size         = [](Direction axis, LayoutableRef q) { return axis == Vertical ? q->h : q->w; };
    const auto widget_position     = [](Direction axis, LayoutableRef q) { return axis == Vertical ? q->y : q->x; };
    const auto widget_margin_front = [](Direction axis, LayoutableRef q) { return axis == Vertical ? q->margin.t : q->margin.l; };
    const auto widget_margin_back  = [](Direction axis, LayoutableRef q) { return axis == Vertical ? q->margin.b : q->margin.r; };

    // The span strength is medium, and the widget strengths are strong. This means that
    // a span will overflow/scrollbar, if a span with some strong widgets exceeds the layout.
    const auto span_strength = medium;

    // Build the child dependencies. We do this first, because we do depth-first layouting.
    // Since constraints are applied all in one go, this shouldn't matter; HOWEVER,
    // all mutable fields on Layoutable can be changed by this call chain. Sorry!
    // This can be improved later by passing this state alongside the constraints; for now, this
    // will have to do.
    // These dependencies include things like |width() modifiers, which apply with optional strength.
    {
        for (auto& entry : m_children)
        {
            const auto mods = entry->BuildConstraints(entry->layout);
            ret.insert(ret.end(), mods.begin(), mods.end());
        }
    }

    // This set of constraints places the widgets within the span next to each other.
    // Not negotiable, since spans have have fixed requirements:
    // - Widgets dont overlap.
    // - Widgets as a sum fill the whole span in both dimensions (though they can overflow).
    {
        for (unsigned i = 0; i < m_children.size(); i++)
        {
            auto& entry = m_children[i];

            if (i == 0)
            {
                // First starts at layout offset
                ret.push_back({
                        widget_position(primary, entry)
                        ==
                        layout_position(primary) +
                        widget_margin_front(primary, entry)
                });
            }
            else
            {
                // All further parent off the first
                auto& prev = m_children[i - 1];
                ret.push_back({
                        widget_position(primary, entry)
                        ==
                        widget_position(primary, prev) +
                        widget_margin_front(primary, entry) +
                        widget_size(primary, prev) +
                        widget_margin_back(primary, prev)
                });
            }

        }
    }

    // Since the secondary domain is also filled, all widgets start at the beginning
    // of that layout axis.
    {
        for (auto& entry : m_children)
        {
            ret.push_back({
                    widget_position(secondary, entry)
                    ==
                    layout_position(secondary) +
                    widget_margin_front(secondary, entry)
            });
        }
    }

    // This set of constraints ensures that the primary domain collectively
    // never exceeds the layout size.
    {
        Expression total_primary = 0;
        for (auto& entry : m_children)
        {
            total_primary = total_primary +
                            widget_size(primary, entry) +
                            widget_margin_front(primary, entry) +
                            widget_margin_back(primary, entry);

            // Widgets should not outgrow the parent, if possible.
            ret.push_back({
                    layout_size(primary)
                    >=
                    widget_size(primary, entry) +
                    widget_margin_front(primary, entry) +
                    widget_margin_back(primary, entry)
                    | span_strength
            });
        }

        // We suggest that the span is the combined total of all widgets.
        ret.push_back({ total_primary == layout_size(primary) | span_strength });

        // Also, we suggest the total span size should not outgrow the parent.
        ret.push_back({ total_primary <= layout_size(primary) | span_strength });
    }

    // As a convenience, we collect all spaced-equally widgets and suggest they are the same size.
    // Spacers are most often used to pad-align rows; but this also works for any other element.
    // These suggestions are weak: anything from widget or user overrides it.
    {
        std::vector<LayoutableRef> spacers;
        for (auto& entry : m_children)
        {
            if (entry->m_spaced_equally)
            {
                spacers.push_back(entry);
            }
        }
        if (spacers.size() > 1)
        {
            for (unsigned i = 1; i < spacers.size(); i++)
            {
                ret.push_back({
                        widget_size(primary, spacers[i])
                        ==
                        widget_size(primary, spacers[i - 1])
                        | weak
                });
            }
        }
    }

    // We do the same for the secondary domain: It can never outgrow the available layout.
    // Since the seconday domain only has one entry to consider for the layout size, we
    // can use it directly, instead of adding them all up like above.
    {
        for (auto& entry : m_children)
        {
            // Each widget starts at the front of the secondary axis.
            // This is not negotiable.
            ret.push_back({
                    widget_position(secondary, entry)
                    ==
                    layout_position(secondary) +
                    widget_margin_front(secondary, entry)
            });

            // Widgets can never outgrow the parent. Not negotiable.
            ret.push_back({
                    layout_size(secondary)
                    >=
                    widget_size(secondary, entry) +
                    widget_margin_front(secondary, entry) +
                    widget_margin_back(secondary, entry)
            });

            // And then we offer the solver the secondary for each widget, to select one
            // he likes.
            ret.push_back({
                    layout_size(secondary)
                    ==
                    widget_size(secondary, entry) +
                    widget_margin_front(secondary, entry) +
                    widget_margin_back(secondary, entry)
                    | span_strength
            });
        }
    }

    return ret;
}