// wx-dom.js — shim additions for the WASM DOM port (non-universal build). // // Loaded as a second --pre-js AFTER wx.js, only in DOM-port bundles. // wx.js keeps owning window management, the element registry, clipboard, // file dialogs and the Canvas2D drawing used by owner-drawn widgets // (canvas islands). This file owns native-control DOM elements: creation // (type-based factory, composites where a wx control maps to more than one // element), property sync, clone-based intrinsic measurement, and event // wiring back into WASM via wx_dom_event (src/wasm/domevents.cpp). // // All functions are no-ops/guarded in Web Workers (-pthread evaluates // pre-js there too) — see the wx.js worker guard precedent. (function () { if (typeof window === 'undefined' || typeof document === 'undefined') { return; } var nextControlId = 1; var controls = new Map(); // domId -> root HTMLElement var inputs = new Map(); // domId -> value-bearing element (if != root) var labels = new Map(); // domId -> label text target (if != root) // Flag read by the C++ keyboard callback (src/wasm/app.cpp): while a DOM // editable owns browser focus, wx must not swallow/preventDefault keys. window.wxDomEditableFocused = 0; // Mirror of wxDomEventKind in include/wx/wasm/window.h. var EVT = { CLICK: 1, INPUT: 2, CHANGE: 3, FOCUSIN: 4, FOCUSOUT: 5, ENTER: 6, SPIN_UP: 7, SPIN_DOWN: 8, MENU: 9, TOOL: 10, TAB: 11, SCROLL: 12 }; // Last pointer position in viewport (clientX/Y) coordinates — read by // wxShowContextMenu when DoPopupMenu is invoked "at the mouse" (the KiCad // canvas right-click case passes wxDefaultCoord). Tracked on every pointer // event below, including over the GAL canvas (where forwardTarget is null). var lastPointerClientX = 0; var lastPointerClientY = 0; // Marks the bundle as DOM-port for tests/boot code. window.wxDomPort = true; window.wxDomControls = controls; function dispatch(domId, kind) { try { Module['ccall']('wx_dom_event', null, ['number', 'number'], [domId, kind]); } catch (e) { // Surfaces in test logs; must never throw back into DOM event handlers. console.error('wx_dom_event(' + domId + ',' + kind + ') failed:', e); } } function isEditable(el) { return el && (el.tagName === 'TEXTAREA' || (el.tagName === 'INPUT' && ['text', 'password', 'number', 'search'].indexOf(el.type) >= 0)); } function flexCenter(el) { el.dataset.wxDisplay = 'flex'; el.style.display = 'flex'; el.style.alignItems = 'center'; } // Builds the element structure for a logical wx control type. Plain HTML // tags ("span", "button", "textarea", "input"+typeAttr) pass through for // the simple controls; composite types build wrappers. // Returns { root, input, label } (input/label optional). function buildControl(type, typeAttr) { var root, input, label; switch (type) { case 'checkbox': case 'radio': { root = document.createElement('label'); input = document.createElement('input'); input.type = type; input.style.margin = '0 3px 0 0'; label = document.createElement('span'); label.className = 'wx-label'; root.appendChild(input); root.appendChild(label); flexCenter(root); break; } case 'toggle': { root = document.createElement('button'); root.setAttribute('aria-pressed', 'false'); flexCenter(root); root.style.justifyContent = 'center'; root.style.padding = '1px 9px'; break; } case 'statbox': { // Visual chrome only: the wx children of a wxStaticBox are SIBLING // DOM controls, so the fieldset must never intercept their input. root = document.createElement('fieldset'); root.style.border = '1px solid #b5b2aa'; root.style.borderRadius = '2px'; label = document.createElement('legend'); label.className = 'wx-label'; label.style.padding = '0 3px'; root.appendChild(label); root.dataset.wxChrome = '1'; break; } case 'statline': { root = document.createElement('div'); root.style.background = '#909090'; root.dataset.wxChrome = '1'; break; } case 'gauge': { root = document.createElement('progress'); root.max = 100; root.value = 0; break; } case 'slider': { root = document.createElement('input'); root.type = 'range'; break; } case 'scrollbar': { // Custom track+thumb (a native can't express a // proportional thumb). Drag handlers are wired in wxDomCreateControl // once the domId is known; metrics arrive via wxDomSetScrollbar. // typeAttr "v" = vertical, "h" = horizontal. root = document.createElement('div'); root.dataset.wxScrollbar = '1'; var sbVertical = (typeAttr === 'v'); root.style.background = '#e8e8e8'; root.style.userSelect = 'none'; var sbTrack = document.createElement('div'); sbTrack.className = 'wx-sb-track'; sbTrack.style.cssText = 'position:absolute;left:0;top:0;right:0;bottom:0;'; var sbThumb = document.createElement('div'); sbThumb.className = 'wx-sb-thumb'; sbThumb.style.cssText = 'position:absolute;background:#a0a0a0;border:1px solid #808080;' + 'border-radius:2px;box-sizing:border-box;touch-action:none;' + 'display:none;' + (sbVertical ? 'left:0;right:0;top:0;height:0;' : 'top:0;bottom:0;left:0;width:0;'); sbTrack.appendChild(sbThumb); root.appendChild(sbTrack); root._wxSb = { pos: 0, thumb: 0, range: 0, page: 0, vertical: sbVertical, dragging: false, phase: 1, dragStart: 0, dragStartOffset: 0 }; break; } case 'choice': { root = document.createElement('select'); break; } case 'listbox': { root = document.createElement('select'); root.multiple = true; break; } case 'spinbutton': { // vertical up/down button pair; clicks dispatch SPIN_UP/SPIN_DOWN root = document.createElement('div'); root.dataset.wxSpin = '1'; var mk = function (txt, cls) { var b = document.createElement('button'); b.textContent = txt; b.className = cls; b.style.cssText = 'flex:1;padding:0;margin:0;font-size:7px;' + 'line-height:1;min-height:0;overflow:hidden;'; root.appendChild(b); return b; }; root.style.display = 'flex'; root.dataset.wxDisplay = 'flex'; root.style.flexDirection = 'column'; mk('▲', 'wx-spin-up'); mk('▼', 'wx-spin-down'); break; } case 'radiobox': { // owns its item rows (unlike statbox chrome): fieldset + legend + // one per item, // filled by wxDomSetItems. root = document.createElement('fieldset'); root.style.border = '1px solid #b5b2aa'; root.style.borderRadius = '2px'; label = document.createElement('legend'); label.className = 'wx-label'; label.style.padding = '0 3px'; root.appendChild(label); root.dataset.wxRadioBox = '1'; break; } case 'image': { root = document.createElement('img'); root.dataset.wxChrome = '1'; // non-interactive like statbmp break; } case 'combobox': { // Editable combo: text input + datalist autocomplete (HTML has no // native editable select). wxDomSetItems fills the datalist. root = document.createElement('input'); root.type = 'text'; var dl = document.createElement('datalist'); dl.id = 'wx-datalist-' + nextControlId; // == the domId assigned below root.setAttribute('list', dl.id); document.body.appendChild(dl); root.dataset.wxDatalist = dl.id; break; } case 'checklistbox': { // Scrollable list of checkbox rows; row checkbox toggles dispatch // CHANGE with the row index retrievable via wxDomGetIntValue. root = document.createElement('div'); root.dataset.wxCheckList = '1'; root.style.overflowY = 'auto'; root.style.background = '#ffffff'; root.style.border = '1px solid #b5b2aa'; break; } case 'menubar': { // Horizontal strip of menu-title buttons; menus open as popup divs // (built by wxDomMenuSetStructure). root = document.createElement('div'); root.dataset.wxMenuBar = '1'; root.style.background = '#d4d0c8'; flexCenter(root); break; } case 'toolbar': { // Horizontal strip of tool buttons (built by wxDomToolbarSetTools). root = document.createElement('div'); root.dataset.wxToolBar = '1'; root.style.background = '#d4d0c8'; flexCenter(root); break; } case 'notebook': { // The root box covers the whole page area, so it is CHROME — it // must never swallow mouse events meant for page content (canvas // islands and sibling DOM controls). Only the tab strip inside is // interactive (built by wxDomNotebookSetTabs). root = document.createElement('div'); root.dataset.wxNotebook = '1'; root.dataset.wxChrome = '1'; var strip = document.createElement('div'); strip.className = 'wx-tab-strip'; strip.setAttribute('role', 'tablist'); strip.style.cssText = 'position:absolute;left:0;top:0;right:0;display:flex;' + 'align-items:flex-end;background:#d4d0c8;' + 'border-bottom:1px solid #808080;pointer-events:auto;' + 'overflow:hidden;'; root.appendChild(strip); break; } default: { root = document.createElement(type); if (typeAttr) root.setAttribute('type', typeAttr); if (type === 'span' || type === 'button' || type === 'label') { flexCenter(root); } if (type === 'button') { root.style.padding = '1px 9px'; root.style.justifyContent = 'center'; } break; } } // Passive controls have no interactive DOM behavior of their own — // left clicks on them belong to wx (KiCad binds LEFT_DOWN on labels). if (root && (type === 'span' || type === 'gauge')) { root.dataset.wxPassive = '1'; } return { root: root, input: input, label: label }; } window.wxDomCreateControl = function (tlwCssId, type, typeAttr) { var container = window.__wxGetWindowElement(tlwCssId); if (!container) { console.error('wxDomCreateControl: no window element for css id ' + tlwCssId); return 0; } var built = buildControl(type, typeAttr); var el = built.root; var domId = nextControlId++; el.dataset.wxDomId = String(domId); el.classList.add('wx-dom-control'); el.style.position = 'absolute'; el.style.left = '0px'; el.style.top = '0px'; el.style.boxSizing = 'border-box'; el.style.margin = '0'; if (!el.style.padding) el.style.padding = '0'; el.style.overflow = type === 'statbox' ? 'visible' : 'hidden'; if (el.dataset.wxDisplay === undefined) el.dataset.wxDisplay = ''; // wx labels never soft-wrap (the wx sizer sizes them exactly), but // multiline wxStaticText labels DO contain hard \n breaks — 'pre' // preserves those while preventing wrapping.