Skip to content

widget_sample

Jan Boon edited this page Jul 24, 2026 · 20 revisions

title: GUI Widget Sample description: Standalone NeL GUI showcase sample (nl_sample_gui) using the legacy w_ interface skin published: true editor: markdown

This page documents the sample itself. The general interface documentation extracted from this work lives under Interface (authoring: layout, widgets, database, Lua) and Embedding NeL GUI (the host-application contract). {.is-info}

nl_sample_gui (nel/samples/gui/) is a standalone showcase of the NLGUI widget library. It boots the widget manager, view renderer and interface parser outside the Ryzom client, parses a self-contained interface definition, and renders it with the legacy w_ interface skin — the older beveled reference style whose window frames (w_header_*, w_l0_*, w_l1_*, w_modal_*, w_scroll_l0_*) were later replaced name-for-name by the shipped skin_* set (non-frame w_ art — gauges, tab buttons, brick slots and more — still ships alongside). The sample keeps the otherwise-unused frame set exercised and doubles as a minimal reference for embedding NLGUI in any NeL application.

Layout

  • main.cpp — bootstrap: UDriver + text context, CViewRenderer::setDriver/setTextContext + init, atlas loadTextures, parseInterface, updateAllLocalisedElements, activateMasterGroup("ui:sample"), then the pump/draw loop (EventServer.pumpsendClockTickEventcheckCoordsdrawViews). A NLGUI::CEventListener routes driver mouse events into the widget manager.
  • data/config_sample.xml — master <root id="sample">, database defaults, and the w_ skin options blocks (layer0, skin_modal, container_move_opt, container_insertion_opt, context_menu_back, …).
  • data/widgets_sample.xml — shared widget styles (text_button_16, button_ok, tab_button) on the w_text_button_* / w_tab_* textures.
  • data/main_sample.xml — the showcase windows, the generic_pointer view, and the <tree> nodes that register windows.
  • data/minesweeper_sample.xml + data/minesweeper.lua — the Minesweeper demo app (see below).

Textures — packed on the fly

No interface texture data lives in the code repository. At build time, CMake runs the build_interface tool over interfaces/v3/ of a ryzomcore_graphics checkout (NL_GRAPHICS_DIR, default sibling of the source tree) and packs gui_sample_atlas.png/.txt into the build directory — the same pattern the WebGL water sample uses for its assets. build_interface gained a -k/--keep-going flag for this: input files that cannot be loaded as bitmaps (e.g. unmaterialized git-lfs pointer files) are skipped with a warning instead of aborting the pack. The font comes from fonts/ingame.ttf in the same checkout — the pixel font whose metrics the interface textures assume (basic.ttf renders ~30% taller at the same sizes).

The Emscripten build preloads the generated atlas, the XML and the font into the .data bundle; when cross-compiling, point NL_BUILD_INTERFACE_TOOL at a host-built build_interface.

Building and running

Native (needs WITH_GUI=ON, which needs Lua and ryzom/luabind; build both into a local prefix and pass LUA_*/LUABIND_* cache variables if the distribution does not package them):

cmake -DWITH_GUI=ON -DWITH_NEL_SAMPLES=ON ...
ninja nl_sample_gui
bin/nl_sample_gui                          # interactive, ESC quits
bin/nl_sample_gui --screenshot out.png --frames 10   # headless witness (works under xvfb-run)
bin/nl_sample_gui -d dir --xml extra.xml   # parse additional interface XML on top of the showcase

Sample-side action handlers

NLGUI ships the widget-level handlers (set, tab_select, ic_* container buttons, combo box, select number, …) but some conventional handlers live in the Ryzom client. The sample registers its own equivalents: proc (run an interface procedure), enter_modal / leave_modal (CWidgetManager::enableModalWindow), push_modal/pop_modal, active_menu, lua, browse/browse_undo/browse_redo (drive a group_html by its name= param — html links emit browse with name=<full id>|url=<href>), a no-op submit_quick_help (CAHManager forwards every handler run to that sink for the client's quick-help system and warns when it is missing), and sample_quit.

proc contract trap: runProcedure must receive the whole split param list, procedure name still at [0]CProcAction::eval maps @0 to paramList[1] by design. The sample originally erased the name first, so every proc_toggle|window silently built the target ui:sample::active and no launcher toggle had ever worked; the Lua console's runAH probes isolated it in minutes.

Widget coverage (M2)

A launcher window toggles one showcase window per family; root windows all use layer0, and the deeper container layer skins (w_l1_*/w_l2_* frame sets) appear where the engine actually selects them — by popin nesting depth inside the contacts rollout. Covered and interaction-verified under Xvfb with xdotool: text views (sizes, colors, case modes, multi-line), bitmaps (scaled/tiled), text_number/digit/text_quantity, bar/bar3 gauges, push/toggle/radio buttons, text buttons, sliders on both axes bound to UI:TEMP:* database entries, edit boxes (typed input works), scroll_text (which contains the runtime-filled group_list), group_tree with arbo_* fold arrows, menus (checkable, grayed, separator, nested submenu — a submenu is a nested <action> inside an <action>, not a <menu> element), the combo box with its dropdown, select_number, the color picker (palette + a makeRGB(...) link recoloring a swatch), tab groups, stacked modals (push_modal/pop_modal), a <link> showing a warning icon above a threshold, and the contacts-style popin rollout with the original anatomy: window contents live in the header groups, the section below is the open rollout where child containers dock via nested <tree> nodes, and a contact entry is a closed container — just its title bar with icons in header_closed. With the child list open, the original layout draws the top block, the m_open left rail (with the list scrollbar riding it), and the floating el/em/er_open ending strip; the frame stops below the header and content, the child bevel bars carry the open section, and the right edge of the rollout is genuinely open (the shipped skin_ layers instead patch a single repeated right-edge texture over it, e.g. skin_l1_r for tx_tr/tx_r/tx_br). The engine had always stretched the frame over the whole open list — which degenerates the open-section geometry (negative rail height, b_open landing on the ending-strip row) — so the sample introduced the frame_covers_open_list layer option (default true, shipped skins unchanged); the sample's w_ layer blocks set it to false for the original behavior. Lua-scripted UI is exercised by the demo apps below; group_html and group_table by the help viewer (tables in html render through group_table). Not exercised: edit_box_decor, group_frame/header.

Note on container layers: standalone windows can force a deeper skin with options="layer1..3", but the engine normally derives the layer from popin nesting depth, and the sample no longer forces it. Unlike the shipped client — where only layer0 draws a header band (skin_header_*; deeper layers use no_bord) and each layer therefore has its own title size and offsets — the w_ set has a single 24px w_header_* band and a single 14px w_win_close button, so all four sample layer blocks share one set of measured-centered title metrics (13pt at y-4, button at y-5). Importing the client's per-layer offsets into identical band geometry is what title/close-button misalignment looks like.

Minesweeper — a complete game in interface data

The showcase includes a playable Minesweeper window (toggled from the launcher) with no game code in the binary: the 9x9 board is 81 instances of a plain cell template stamped out by <vector> (_firstpos/_nextpos/_firstindex with $i substitution — note posref order is parent-hotspot child-hotspot, so a left-to-right chain is _nextpos="TR TL"), and all rules live in minesweeper.lua, driving the widgets through the reflection system. A cell is a flat floor bitmap + number text underneath, a w_slot_brick cover button on top (w_slot_brick_selected as hover), and a w_warning flag marker above; revealing is just cover.active = false, mines show w_death. First-click-safe mine placement, flood fill, right-click flags (onclick_r/params_r) plus a flag-mode toggle button for environments without right-click, a live timer via setOnDraw + nltime.getPreciseLocalTime, and win/boom end states.

What it demonstrates about the Lua embedding contract:

  • The embedder must call parser->initLUA() before parseInterface — it registers the stock NLGUI Lua API (setOnDraw, addOnDbChange, runAH, runExpr, nltime.*, …) and the parser does not call it itself (the client calls it explicitly too). Without it, <lua file="..."> scripts fail on the first stock-API call at parse time.
  • getUI is not part of NLGUI — widget lookup from Lua is left to the embedder (the client registers its own richer version). The sample registers a minimal getUI(id) (CWidgetManager::getElementFromId + CLuaIHM::pushUIOnStack) before parsing; registrations made on the CLuaManager state before parseInterface survive, since initLUA registers into the same state without resetting it.
  • Lua sees widgets through their reflected properties (active, hardtext, color, texture, pushed, frozen, x/y/w/h, …) and child widgets as fields (cell.cover.active = false), so a game needs no dedicated C++ surface at all.
  • A group's setOnDraw script runs through the lua action handler — which also lives client-side, so the sample's registered equivalent covers it.

Lua console — a REPL / UI inspector in interface data

A console window (toggled from the launcher) evaluates Lua against the live widget tree: the log is a <vector> stack of plain text views repainted from a history table, the input a stock edit_box whose onenter runs the evaluator. Set enter_loose_focus="false" on a console-style edit box — the template default drops keyboard focus on Enter and every other command silently goes nowhere. Evaluation tries return <line> first so plain expressions print their values, then falls back to statement form; errors render in red; print is redirected into the log; max_historic gives up-arrow history for free.

The inspector half comes from the __next metamethod NLGUI installs on reflected userdata: redefine global next/pairs to consult getmetatable(t).__next (exactly what the Ring scripts do in r2_misc.lua) and pairs(widget) enumerates children groups/views/ctrls (_Group n keys, values are the child widgets) followed by every exported property (keys stringify to the property names). The sample ships ls() (children by bucket) and props() (property dump) on top, plus ui(path) prefixing ui:sample: for brevity. Reflected x_real/y_real/w_real/h_real are readable, which is what makes Lua-side hit-testing (inventory below) possible.

Drag-and-drop inventory — real gestures in interface data

An armor paper-doll strip (six typed equip slots with labels) over a 6x2 bag, using the w_ar_* armor icons and w_slot_item floors from the atlas. All logic in inventory.lua; the binary only gained two Lua hooks and one listener:

  • getMousePos() / getMouseDown() — registered by the sample with the client's exact signatures (CTool semantics), reading the widget manager's pointer. Coordinates are interface pixels, the same space as reflected *_real geometry.
  • Nothing in NLGUI ever calls setPointerDown — the widget manager updates pointer position only; button state is mirrored into the pointer by the client's input handler. Standalone apps must do the same (the sample adds a small IEventListener for mouse down/up/focus-lost on the driver's event server). Note the parser auto-creates a bare generic_pointer when none is parsed; the parser only wires a parsed view as the pointer for type="pointer", which is the client's registered class, so a type="generic_pointer" view in XML is decorative.

A setOnDraw tick polls the hooks and runs the whole gesture: press-edge hit-tests slots against x_real/y_real/w_real/h_real, a ghost bitmap follows the cursor (ghost.x = mx - content.x_real - 20 with posref="BL BL"), item_selection.tga glows on valid targets, release applies move/swap/reject-with-return. Typed slots just compare a kind tag in Lua — acceptance rules cost nothing when the data layer owns them.

Help viewer — group_html over local pages

A browser window on local HTML files: pages resolve through the search path (CGroupHTML::lookupLocalFile accepts bare filenames and file: URLs), so no network and no curl involvement. Anatomy that must be present inside the <group type="html">: a backdrop bitmap named by background_bitmap_view (the classic black2 + blank.tga), the text_list list group, and a scroll_bar ctrl (it extends group_scroll_text). Links emit the browse action handler with name=<full id>|url=<href>; Back/Forward buttons run browse_undo/browse_redo, and pointing the group's browse_undo=/browse_redo= attributes at those buttons makes it freeze/unfreeze them with history state automatically. The page <title> becomes the container window title.

browser.css is load-bearing: the legacy per-attribute styling (link_color, h1_font_size, …) is parsed but the modern CSS engine styles the actual views — without a browser.css on the search path links render as plain gray text and headings collapse to body size, with only a one-line warning in the log. The sample ships the client's stylesheet with the link color adjusted to the w_ palette. Pages exercise headings, lists, <b>/<em>, inline <img> from atlas textures, and bordered tables (group_table).

What the application must provide

NLGUI expects several things from the embedding application that the Ryzom client normally supplies:

  • CInterfaceLink::CInterfaceLinkUpdater — instantiate one after creating the widget manager; without it, <link> elements never evaluate (database observer flushes are what pump triggered links). Call CInterfaceLink::updateAllLinks() once after parsing for the initial evaluation.
  • CDBGroupComboBox::selectMenu / selectMenuOut / measureMenu — static ids the combo dropdown machinery resolves; point them at your master group's menus.
  • Action handlers with client-side conventions: proc, enter_modal/leave_modal, push_modal/pop_modal, active_menu, lua, browse/browse_undo/browse_redo, submit_quick_help.
  • Lua environment hooks beyond initLUA: getUI, and for gesture-driven UI getMousePos/getMouseDown plus the pointer button-state mirroring described above.

Bootstrap pitfalls (learned the hard way)

  • Container windows need the global options blocks. CGroupContainer::setup dereferences container_move_opt unchecked — without that options block the first updateAllLocalisedElements crashes.
  • Fresh database leaves default to 0. UI:SAVE:CONTENT_ALPHA, UI:SAVE:CONTAINER_ALPHA and friends must be declared (255) in the XML or every window renders fully transparent. Likewise UI:SAVE:*_ROLLOVER_FACTOR are inverted factors: 255 = no fade-out when the mouse is not over the window, 0 = fade to invisible.
  • Root-level plain <group>s are parsed but never drawn — only windows registered on the master group (containers via <tree>, modals via enableModalWindow) render. Put HUD-style elements inside a registered window.
  • %case_upper etc. are interface defines, not built-ins — declare case_normalcase_first_word_letter_up (0…5).
  • The texture lookup normalizes .png.tga on both the atlas-load and lookup sides, so interface XML keeps the canonical .tga names while the atlas is packed from .png sources. The color picker is the exception: it reads its palette as a loose file (it needs pixel access), so the palette png is staged next to the atlas and CPath::remapExtension("png", "tga", true) is set before the search paths are indexed.
  • Container windows compute their height from the content group unless resizer="true"; a fixed-size window with resizer="false" and an empty-sized content collapses to its title bar.
  • dbview_quantity's registered class name is text_quantity (not quantity).
  • Call setKeep800x600Ratio(false) on the text context (the client does this in resetTextContext). CTextContext defaults the ratio ON, which silently scales every font size by windowHeight/600 — at 1600x1200 all text renders at 2×, overflowing the texture bands no matter which font sizes the XML asks for. With true pixel metrics the client's per-layer title sizes (13/11/10) match their header_h bands (16/14/12). The sample's --font-probe flag prints the renderer's real per-size string metrics (via UTextContext::getStringInfo) for checking this.
  • w= does not limit multi-line text wrapping. A multi_line="true" view wraps at line_maxw clamped by the parent's inner width (getCurrentMultiLineMaxW); a plain w attribute is ignored for wrapping. Use line_maxw for a narrower paragraph column.
  • Two NLGUI bugs fixed while building this: CEventListener never subscribed keyboard events (edit boxes were deaf outside the client), and CGroupTree::SNode::parse crashed on a <node> with opened= but no show= (wrong pointer guarded).

Emscripten / WebGL build (M3)

The GUI stack builds for the web sample target. Recipe (dev-box; prefix paths are examples):

  1. Cross-build the static deps with the emsdk into a local prefix:
    • Lua 5.1.5: make generic CC="emcc -O2" AR="emar rcu" RANLIB=emranlib, make install INSTALL_TOP=<prefix>.
    • libxml2 (2.12.x): emcmake cmake with -DBUILD_SHARED_LIBS=OFF -DLIBXML2_WITH_{PYTHON,ICONV,LZMA,ZLIB,TESTS,PROGRAMS}=OFF.
    • ryzom/luabind: emcmake cmake against the wasm Lua. Boost is headers-only here, but do not pass /usr/include as the Boost include dir — host headers poison the emscripten sysroot; symlink boost/ into a clean directory instead.
  2. curl/openssl are not builtnel/samples/gui/emscripten/nel_wasm_stubs.c (compiled with emcc, archived as libcurl.a/libssl.a/libcrypto.a in the prefix, with real curl headers copied alongside) provides symbol-level stubs whose entry points fail the way the real libraries would. NeL's error paths handle that; a JS missing-function import would abort at the first call instead (CCurlHttpClient::connect dereferences curl_version_info() unconditionally, so that one returns a static struct). SysV IPC stubs cover nelmisc shared memory.
  3. Configure the emscripten tree with WITH_GUI=ON WITH_WEB=ON WITH_LUA51=ON plus LUA_*, LUABIND_*, Boost_INCLUDE_DIR, LibXml2_DIR=<prefix>/lib/cmake/libxml2-<ver>, CURL_INCLUDE_DIR/CURL_LIBRARY, OPENSSL_* pointing at the prefix, and NL_BUILD_INTERFACE_TOOL=<host build>/bin/build_interface for the atlas pack. The root CMake emscripten branch probes these instead of compiling XML support out (nelmisc gains real CIXml on wasm this way).
  4. The sample selects UDriver::OpenGlEs3 under emscripten and defaults to the 1280x720 showcase layout; the html page CSS-scales the canvas. Deploys next to the other WebGL samples (nl_sample_gui.html/.js/.wasm/.data).

Verified in headless chromium over SwiftShader WebGL: rendering matches the native build, including evaluated <link>s and the color picker palette.

Input works in the browser since cc374f902: the driver's CEmscriptenEventEmitter feeds the event server from HTML5 callbacks (mouse/wheel/keyboard; the first touch maps to the left mouse button), normalizing coordinates against the canvas CSS size — so the CSS-scaled host page and mobile touch both work with no sample-side changes. Ctrl/Cmd combos and F-keys are left to the browser; Tab/Space/Backspace/arrows are consumed so the page doesn't scroll. Details on the embedding page.

Status

  • M1 (landed): skeleton — w_ layer0 container window with header/title/buttons, w_modal modal via enter_modal, mouse pointer, frame-capped loop, headless screenshot mode, xdotool-verified event chain under Xvfb.
  • M2 (landed): comprehensive widget coverage as described above, every interaction verified headless.
  • M3 (landed): Emscripten/WebGL build deployed with the other NeL web samples.
  • M4 (landed): Minesweeper demo app — pure XML+Lua game riding the widget set; interaction-verified headless (reveal, flood, flags, flag mode, timer, boom, win, reset).
  • M5 (landed): Lua console / UI inspector — REPL over the live tree, pairs() walking reflected widgets, ls/props helpers.
  • M6 (landed): drag-and-drop inventory — getMousePos/getMouseDown hooks + pointer button mirroring; gestures verified headless (drag, highlight, swap, reject, reset, full-equip).
  • M7 (landed): group_html help viewer over local pages — links, Back/Forward history freezing, tables, atlas images, shipped browser.css; plus the proc @0 fix that made launcher toggles actually work.

Clone this wiki locally