Merge svghmi
authorEdouard Tisserant
Mon, 10 Aug 2020 11:30:06 +0200
branchsvghmi
changeset 3018 22b969b409b0
parent 3008 dabad70db1bf (diff)
parent 3016 085a678715d0 (current diff)
child 3019 497aac6522a3
Merge
svghmi/gen_index_xhtml.xslt
svghmi/svghmi.js
svghmi/widget_button.ysl2
svghmi/widget_circularslider.ysl2
svghmi/widget_multistate.ysl2
svghmi/widget_slider.ysl2
svghmi/widget_tooglebutton.ysl2
--- a/svghmi/detachable_pages.ysl2	Wed Aug 05 15:20:10 2020 +0200
+++ b/svghmi/detachable_pages.ysl2	Mon Aug 10 11:30:06 2020 +0200
@@ -138,14 +138,14 @@
             warning > Page id="«$page/@id»" : No match for path "«$desc/path/@value»" in HMI tree
     |     page_index: «$desc/path/@index»,
     }
-    |     relative_widgets: [
-    foreach "$page_relative_widgets" {
-    |         hmi_widgets["«@id»"]`if "position()!=last()" > ,`
-    }
-    |     ],
-    |     absolute_widgets: [
-    foreach "$page_managed_widgets[not(@id = $page_relative_widgets/@id)]" {
-    |         hmi_widgets["«@id»"]`if "position()!=last()" > ,`
+    |     widgets: [
+    foreach "$page_managed_widgets" {
+        const "widget_paths_relativeness" 
+            foreach "func:widget(@id)/path" {
+                value "func:is_descendant_path(@value, $desc/path/@value)";
+                if "position()!=last()" > ,
+            }
+    |         [hmi_widgets["«@id»"], [«$widget_paths_relativeness»]]`if "position()!=last()" > ,`
     }
     |     ],
     |     jumps: [
@@ -168,7 +168,7 @@
     |   }`if "position()!=last()" > ,`
 }
 
-emit "declarations:page-desc" {
+emit "definitions:page-desc" {
     |
     | var page_desc = {
     apply "$hmi_pages", mode="page_desc";
--- a/svghmi/gen_index_xhtml.xslt	Wed Aug 05 15:20:10 2020 +0200
+++ b/svghmi/gen_index_xhtml.xslt	Mon Aug 10 11:30:06 2020 +0200
@@ -497,26 +497,22 @@
       <xsl:text>,
 </xsl:text>
     </xsl:if>
-    <xsl:text>    relative_widgets: [
-</xsl:text>
-    <xsl:for-each select="$page_relative_widgets">
-      <xsl:text>        hmi_widgets["</xsl:text>
+    <xsl:text>    widgets: [
+</xsl:text>
+    <xsl:for-each select="$page_managed_widgets">
+      <xsl:variable name="widget_paths_relativeness">
+        <xsl:for-each select="func:widget(@id)/path">
+          <xsl:value-of select="func:is_descendant_path(@value, $desc/path/@value)"/>
+          <xsl:if test="position()!=last()">
+            <xsl:text>,</xsl:text>
+          </xsl:if>
+        </xsl:for-each>
+      </xsl:variable>
+      <xsl:text>        [hmi_widgets["</xsl:text>
       <xsl:value-of select="@id"/>
-      <xsl:text>"]</xsl:text>
-      <xsl:if test="position()!=last()">
-        <xsl:text>,</xsl:text>
-      </xsl:if>
-      <xsl:text>
-</xsl:text>
-    </xsl:for-each>
-    <xsl:text>    ],
-</xsl:text>
-    <xsl:text>    absolute_widgets: [
-</xsl:text>
-    <xsl:for-each select="$page_managed_widgets[not(@id = $page_relative_widgets/@id)]">
-      <xsl:text>        hmi_widgets["</xsl:text>
-      <xsl:value-of select="@id"/>
-      <xsl:text>"]</xsl:text>
+      <xsl:text>"], [</xsl:text>
+      <xsl:value-of select="$widget_paths_relativeness"/>
+      <xsl:text>]]</xsl:text>
       <xsl:if test="position()!=last()">
         <xsl:text>,</xsl:text>
       </xsl:if>
@@ -573,8 +569,8 @@
     <xsl:text>
 </xsl:text>
   </xsl:template>
-  <declarations:page-desc/>
-  <xsl:template match="declarations:page-desc">
+  <definitions:page-desc/>
+  <xsl:template match="definitions:page-desc">
     <xsl:text>
 </xsl:text>
     <xsl:text>/* </xsl:text>
@@ -947,89 +943,161 @@
 </xsl:text>
     <xsl:text>        /* remove subsribers */
 </xsl:text>
+    <xsl:text>        if(!this.unsubscribable)
+</xsl:text>
+    <xsl:text>            for(let i = 0; i &lt; this.indexes.length; i++) {
+</xsl:text>
+    <xsl:text>                let index = this.indexes[i];
+</xsl:text>
+    <xsl:text>                if(this.relativeness[i])
+</xsl:text>
+    <xsl:text>                    index += this.offset;
+</xsl:text>
+    <xsl:text>                subscribers[index].delete(this);
+</xsl:text>
+    <xsl:text>            }
+</xsl:text>
+    <xsl:text>        this.offset = 0;
+</xsl:text>
+    <xsl:text>        this.relativeness = undefined;
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>    sub(new_offset=0, relativeness){
+</xsl:text>
+    <xsl:text>        this.offset = new_offset;
+</xsl:text>
+    <xsl:text>        this.relativeness = relativeness;
+</xsl:text>
+    <xsl:text>        /* add this's subsribers */
+</xsl:text>
+    <xsl:text>        if(!this.unsubscribable)
+</xsl:text>
+    <xsl:text>            for(let i = 0; i &lt; this.indexes.length; i++) {
+</xsl:text>
+    <xsl:text>                let index = this.indexes[i];
+</xsl:text>
+    <xsl:text>                if(relativeness[i])
+</xsl:text>
+    <xsl:text>                    index += new_offset;
+</xsl:text>
+    <xsl:text>                subscribers[index].add(this);
+</xsl:text>
+    <xsl:text>            }
+</xsl:text>
+    <xsl:text>        need_cache_apply.push(this); 
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>    apply_cache() {
+</xsl:text>
     <xsl:text>        if(!this.unsubscribable) for(let index of this.indexes){
 </xsl:text>
-    <xsl:text>            let idx = index + this.offset;
-</xsl:text>
-    <xsl:text>            subscribers[idx].delete(this);
+    <xsl:text>            /* dispatch current cache in newly opened page widgets */
+</xsl:text>
+    <xsl:text>            let realindex = index+this.offset;
+</xsl:text>
+    <xsl:text>            let cached_val = cache[realindex];
+</xsl:text>
+    <xsl:text>            if(cached_val != undefined)
+</xsl:text>
+    <xsl:text>                this.new_hmi_value(realindex, cached_val, cached_val);
 </xsl:text>
     <xsl:text>        }
 </xsl:text>
-    <xsl:text>        this.offset = 0;
-</xsl:text>
     <xsl:text>    }
 </xsl:text>
     <xsl:text>
 </xsl:text>
-    <xsl:text>    sub(new_offset=0){
-</xsl:text>
-    <xsl:text>        /* set the offset because relative */
-</xsl:text>
-    <xsl:text>        this.offset = new_offset;
-</xsl:text>
-    <xsl:text>        /* add this's subsribers */
-</xsl:text>
-    <xsl:text>        if(!this.unsubscribable) for(let index of this.indexes){
-</xsl:text>
-    <xsl:text>            subscribers[index + new_offset].add(this);
+    <xsl:text>    get_idx(index) {
+</xsl:text>
+    <xsl:text>         let orig = this.indexes[index];
+</xsl:text>
+    <xsl:text>         return this.relativeness[index] ? orig + this.offset : orig;
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>    change_hmi_value(index,opstr) {
+</xsl:text>
+    <xsl:text>        return change_hmi_value(this.get_idx(index), opstr);
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>    apply_hmi_value(index, new_val) {
+</xsl:text>
+    <xsl:text>        return apply_hmi_value(this.get_idx(0), new_val);
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>    new_hmi_value(index, value, oldval) {
+</xsl:text>
+    <xsl:text>        try {
+</xsl:text>
+    <xsl:text>            // TODO avoid searching, store index at sub()
+</xsl:text>
+    <xsl:text>            for(let i = 0; i &lt; this.indexes.length; i++) {
+</xsl:text>
+    <xsl:text>                let refindex = this.indexes[i];
+</xsl:text>
+    <xsl:text>                if(this.relativeness[i])
+</xsl:text>
+    <xsl:text>                    refindex += this.offset;
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>                if(index == refindex) {
+</xsl:text>
+    <xsl:text>                    let d = this.dispatch;
+</xsl:text>
+    <xsl:text>                    if(typeof(d) == "function"){
+</xsl:text>
+    <xsl:text>                        d.call(this, value, oldval, i);
+</xsl:text>
+    <xsl:text>                    }
+</xsl:text>
+    <xsl:text>                    else if(typeof(d) == "object"){
+</xsl:text>
+    <xsl:text>                        d[i].call(this, value, oldval);
+</xsl:text>
+    <xsl:text>                    }
+</xsl:text>
+    <xsl:text>                    /* else dispatch_0, ..., dispatch_n ? */
+</xsl:text>
+    <xsl:text>                    /*else {
+</xsl:text>
+    <xsl:text>                        throw new Error("Dunno how to dispatch to widget at index = " + index);
+</xsl:text>
+    <xsl:text>                    }*/
+</xsl:text>
+    <xsl:text>                    break;
+</xsl:text>
+    <xsl:text>                }
+</xsl:text>
+    <xsl:text>            }
+</xsl:text>
+    <xsl:text>        } catch(err) {
+</xsl:text>
+    <xsl:text>            console.log(err);
 </xsl:text>
     <xsl:text>        }
 </xsl:text>
-    <xsl:text>        need_cache_apply.push(this); 
-</xsl:text>
     <xsl:text>    }
 </xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>    apply_cache() {
-</xsl:text>
-    <xsl:text>        if(!this.unsubscribable) for(let index of this.indexes){
-</xsl:text>
-    <xsl:text>            /* dispatch current cache in newly opened page widgets */
-</xsl:text>
-    <xsl:text>            let realindex = index+this.offset;
-</xsl:text>
-    <xsl:text>            let cached_val = cache[realindex];
-</xsl:text>
-    <xsl:text>            if(cached_val != undefined)
-</xsl:text>
-    <xsl:text>                dispatch_value_to_widget(this, realindex, cached_val, cached_val);
-</xsl:text>
-    <xsl:text>        }
-</xsl:text>
-    <xsl:text>    }
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>    get_idx(index) {
-</xsl:text>
-    <xsl:text>         let orig = this.indexes[index];
-</xsl:text>
-    <xsl:text>         return this.offset ? orig + this.offset : orig;
-</xsl:text>
-    <xsl:text>    }
-</xsl:text>
-    <xsl:text>    change_hmi_value(index,opstr) {
-</xsl:text>
-    <xsl:text>        return change_hmi_value(this.get_idx(index), opstr);
-</xsl:text>
-    <xsl:text>    }
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>    apply_hmi_value(index, new_val) {
-</xsl:text>
-    <xsl:text>        return apply_hmi_value(this.get_idx(0), new_val);
-</xsl:text>
-    <xsl:text>    }
-</xsl:text>
     <xsl:text>}
 </xsl:text>
     <xsl:text>
 </xsl:text>
   </xsl:template>
-  <preamble:hmi-classes/>
-  <xsl:template match="preamble:hmi-classes">
+  <declarations:hmi-classes/>
+  <xsl:template match="declarations:hmi-classes">
     <xsl:text>
 </xsl:text>
     <xsl:text>/* </xsl:text>
@@ -1057,8 +1125,8 @@
   </xsl:template>
   <xsl:variable name="excluded_types" select="str:split('Page Lang')"/>
   <xsl:variable name="excluded_ids" select="$parsed_widgets/widget[not(@type = $excluded_types)]/@id"/>
-  <preamble:hmi-elements/>
-  <xsl:template match="preamble:hmi-elements">
+  <declarations:hmi-elements/>
+  <xsl:template match="declarations:hmi-elements">
     <xsl:text>
 </xsl:text>
     <xsl:text>/* </xsl:text>
@@ -1690,9 +1758,13 @@
 </xsl:text>
     <xsl:text>    frequency = 5;
 </xsl:text>
-    <xsl:text>    dispatch(value) {
-</xsl:text>
-    <xsl:text>        this.element.textContent = String(value);
+    <xsl:text>    dispatch(value, oldval, index) {
+</xsl:text>
+    <xsl:text>        this.fields[index] = value;    
+</xsl:text>
+    <xsl:text>        console.log(value, index);
+</xsl:text>
+    <xsl:text>        this.element.textContent = this.args.length == 1 ? vsprintf(this.args[0],this.fields) : this.fields.join(' ');
 </xsl:text>
     <xsl:text>    }
 </xsl:text>
@@ -1708,6 +1780,487 @@
         <xsl:text>" is not a svg::text element</xsl:text>
       </xsl:message>
     </xsl:if>
+    <xsl:text>    fields: [],
+</xsl:text>
+  </xsl:template>
+  <preamble:display/>
+  <xsl:template match="preamble:display">
+    <xsl:text>
+</xsl:text>
+    <xsl:text>/* </xsl:text>
+    <xsl:value-of select="local-name()"/>
+    <xsl:text> */
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>/* https://github.com/alexei/sprintf.js/blob/master/src/sprintf.js */
+</xsl:text>
+    <xsl:text>/* global window, exports, define */
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>!function() {
+</xsl:text>
+    <xsl:text>    'use strict'
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>    var re = {
+</xsl:text>
+    <xsl:text>        not_string: /[^s]/,
+</xsl:text>
+    <xsl:text>        not_bool: /[^t]/,
+</xsl:text>
+    <xsl:text>        not_type: /[^T]/,
+</xsl:text>
+    <xsl:text>        not_primitive: /[^v]/,
+</xsl:text>
+    <xsl:text>        number: /[diefg]/,
+</xsl:text>
+    <xsl:text>        numeric_arg: /[bcdiefguxX]/,
+</xsl:text>
+    <xsl:text>        json: /[j]/,
+</xsl:text>
+    <xsl:text>        not_json: /[^j]/,
+</xsl:text>
+    <xsl:text>        text: /^[^%]+/,
+</xsl:text>
+    <xsl:text>        modulo: /^%{2}/,
+</xsl:text>
+    <xsl:text>        placeholder: /^%(?:([1-9]\d*)\$|\(([^)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-gijostTuvxX])/,
+</xsl:text>
+    <xsl:text>        key: /^([a-z_][a-z_\d]*)/i,
+</xsl:text>
+    <xsl:text>        key_access: /^\.([a-z_][a-z_\d]*)/i,
+</xsl:text>
+    <xsl:text>        index_access: /^\[(\d+)\]/,
+</xsl:text>
+    <xsl:text>        sign: /^[+-]/
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>    function sprintf(key) {
+</xsl:text>
+    <xsl:text>        // </xsl:text>
+    <arguments/>
+    <xsl:text> is not an array, but should be fine for this call
+</xsl:text>
+    <xsl:text>        return sprintf_format(sprintf_parse(key), arguments)
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>    function vsprintf(fmt, argv) {
+</xsl:text>
+    <xsl:text>        return sprintf.apply(null, [fmt].concat(argv || []))
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>    function sprintf_format(parse_tree, argv) {
+</xsl:text>
+    <xsl:text>        var cursor = 1, tree_length = parse_tree.length, arg, output = '', i, k, ph, pad, pad_character, pad_length, is_positive, sign
+</xsl:text>
+    <xsl:text>        for (i = 0; i &lt; tree_length; i++) {
+</xsl:text>
+    <xsl:text>            if (typeof parse_tree[i] === 'string') {
+</xsl:text>
+    <xsl:text>                output += parse_tree[i]
+</xsl:text>
+    <xsl:text>            }
+</xsl:text>
+    <xsl:text>            else if (typeof parse_tree[i] === 'object') {
+</xsl:text>
+    <xsl:text>                ph = parse_tree[i] // convenience purposes only
+</xsl:text>
+    <xsl:text>                if (ph.keys) { // keyword argument
+</xsl:text>
+    <xsl:text>                    arg = argv[cursor]
+</xsl:text>
+    <xsl:text>                    for (k = 0; k &lt; ph.keys.length; k++) {
+</xsl:text>
+    <xsl:text>                        if (arg == undefined) {
+</xsl:text>
+    <xsl:text>                            throw new Error(sprintf('[sprintf] Cannot access property "%s" of undefined value "%s"', ph.keys[k], ph.keys[k-1]))
+</xsl:text>
+    <xsl:text>                        }
+</xsl:text>
+    <xsl:text>                        arg = arg[ph.keys[k]]
+</xsl:text>
+    <xsl:text>                    }
+</xsl:text>
+    <xsl:text>                }
+</xsl:text>
+    <xsl:text>                else if (ph.param_no) { // positional argument (explicit)
+</xsl:text>
+    <xsl:text>                    arg = argv[ph.param_no]
+</xsl:text>
+    <xsl:text>                }
+</xsl:text>
+    <xsl:text>                else { // positional argument (implicit)
+</xsl:text>
+    <xsl:text>                    arg = argv[cursor++]
+</xsl:text>
+    <xsl:text>                }
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>                if (re.not_type.test(ph.type) &amp;&amp; re.not_primitive.test(ph.type) &amp;&amp; arg instanceof Function) {
+</xsl:text>
+    <xsl:text>                    arg = arg()
+</xsl:text>
+    <xsl:text>                }
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>                if (re.numeric_arg.test(ph.type) &amp;&amp; (typeof arg !== 'number' &amp;&amp; isNaN(arg))) {
+</xsl:text>
+    <xsl:text>                    throw new TypeError(sprintf('[sprintf] expecting number but found %T', arg))
+</xsl:text>
+    <xsl:text>                }
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>                if (re.number.test(ph.type)) {
+</xsl:text>
+    <xsl:text>                    is_positive = arg &gt;= 0
+</xsl:text>
+    <xsl:text>                }
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>                switch (ph.type) {
+</xsl:text>
+    <xsl:text>                    case 'b':
+</xsl:text>
+    <xsl:text>                        arg = parseInt(arg, 10).toString(2)
+</xsl:text>
+    <xsl:text>                        break
+</xsl:text>
+    <xsl:text>                    case 'c':
+</xsl:text>
+    <xsl:text>                        arg = String.fromCharCode(parseInt(arg, 10))
+</xsl:text>
+    <xsl:text>                        break
+</xsl:text>
+    <xsl:text>                    case 'd':
+</xsl:text>
+    <xsl:text>                    case 'i':
+</xsl:text>
+    <xsl:text>                        arg = parseInt(arg, 10)
+</xsl:text>
+    <xsl:text>                        break
+</xsl:text>
+    <xsl:text>                    case 'j':
+</xsl:text>
+    <xsl:text>                        arg = JSON.stringify(arg, null, ph.width ? parseInt(ph.width) : 0)
+</xsl:text>
+    <xsl:text>                        break
+</xsl:text>
+    <xsl:text>                    case 'e':
+</xsl:text>
+    <xsl:text>                        arg = ph.precision ? parseFloat(arg).toExponential(ph.precision) : parseFloat(arg).toExponential()
+</xsl:text>
+    <xsl:text>                        break
+</xsl:text>
+    <xsl:text>                    case 'f':
+</xsl:text>
+    <xsl:text>                        arg = ph.precision ? parseFloat(arg).toFixed(ph.precision) : parseFloat(arg)
+</xsl:text>
+    <xsl:text>                        break
+</xsl:text>
+    <xsl:text>                    case 'g':
+</xsl:text>
+    <xsl:text>                        arg = ph.precision ? String(Number(arg.toPrecision(ph.precision))) : parseFloat(arg)
+</xsl:text>
+    <xsl:text>                        break
+</xsl:text>
+    <xsl:text>                    case 'o':
+</xsl:text>
+    <xsl:text>                        arg = (parseInt(arg, 10) &gt;&gt;&gt; 0).toString(8)
+</xsl:text>
+    <xsl:text>                        break
+</xsl:text>
+    <xsl:text>                    case 's':
+</xsl:text>
+    <xsl:text>                        arg = String(arg)
+</xsl:text>
+    <xsl:text>                        arg = (ph.precision ? arg.substring(0, ph.precision) : arg)
+</xsl:text>
+    <xsl:text>                        break
+</xsl:text>
+    <xsl:text>                    case 't':
+</xsl:text>
+    <xsl:text>                        arg = String(!!arg)
+</xsl:text>
+    <xsl:text>                        arg = (ph.precision ? arg.substring(0, ph.precision) : arg)
+</xsl:text>
+    <xsl:text>                        break
+</xsl:text>
+    <xsl:text>                    case 'T':
+</xsl:text>
+    <xsl:text>                        arg = Object.prototype.toString.call(arg).slice(8, -1).toLowerCase()
+</xsl:text>
+    <xsl:text>                        arg = (ph.precision ? arg.substring(0, ph.precision) : arg)
+</xsl:text>
+    <xsl:text>                        break
+</xsl:text>
+    <xsl:text>                    case 'u':
+</xsl:text>
+    <xsl:text>                        arg = parseInt(arg, 10) &gt;&gt;&gt; 0
+</xsl:text>
+    <xsl:text>                        break
+</xsl:text>
+    <xsl:text>                    case 'v':
+</xsl:text>
+    <xsl:text>                        arg = arg.valueOf()
+</xsl:text>
+    <xsl:text>                        arg = (ph.precision ? arg.substring(0, ph.precision) : arg)
+</xsl:text>
+    <xsl:text>                        break
+</xsl:text>
+    <xsl:text>                    case 'x':
+</xsl:text>
+    <xsl:text>                        arg = (parseInt(arg, 10) &gt;&gt;&gt; 0).toString(16)
+</xsl:text>
+    <xsl:text>                        break
+</xsl:text>
+    <xsl:text>                    case 'X':
+</xsl:text>
+    <xsl:text>                        arg = (parseInt(arg, 10) &gt;&gt;&gt; 0).toString(16).toUpperCase()
+</xsl:text>
+    <xsl:text>                        break
+</xsl:text>
+    <xsl:text>                }
+</xsl:text>
+    <xsl:text>                if (re.json.test(ph.type)) {
+</xsl:text>
+    <xsl:text>                    output += arg
+</xsl:text>
+    <xsl:text>                }
+</xsl:text>
+    <xsl:text>                else {
+</xsl:text>
+    <xsl:text>                    if (re.number.test(ph.type) &amp;&amp; (!is_positive || ph.sign)) {
+</xsl:text>
+    <xsl:text>                        sign = is_positive ? '+' : '-'
+</xsl:text>
+    <xsl:text>                        arg = arg.toString().replace(re.sign, '')
+</xsl:text>
+    <xsl:text>                    }
+</xsl:text>
+    <xsl:text>                    else {
+</xsl:text>
+    <xsl:text>                        sign = ''
+</xsl:text>
+    <xsl:text>                    }
+</xsl:text>
+    <xsl:text>                    pad_character = ph.pad_char ? ph.pad_char === '0' ? '0' : ph.pad_char.charAt(1) : ' '
+</xsl:text>
+    <xsl:text>                    pad_length = ph.width - (sign + arg).length
+</xsl:text>
+    <xsl:text>                    pad = ph.width ? (pad_length &gt; 0 ? pad_character.repeat(pad_length) : '') : ''
+</xsl:text>
+    <xsl:text>                    output += ph.align ? sign + arg + pad : (pad_character === '0' ? sign + pad + arg : pad + sign + arg)
+</xsl:text>
+    <xsl:text>                }
+</xsl:text>
+    <xsl:text>            }
+</xsl:text>
+    <xsl:text>        }
+</xsl:text>
+    <xsl:text>        return output
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>    var sprintf_cache = Object.create(null)
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>    function sprintf_parse(fmt) {
+</xsl:text>
+    <xsl:text>        if (sprintf_cache[fmt]) {
+</xsl:text>
+    <xsl:text>            return sprintf_cache[fmt]
+</xsl:text>
+    <xsl:text>        }
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>        var _fmt = fmt, match, parse_tree = [], arg_names = 0
+</xsl:text>
+    <xsl:text>        while (_fmt) {
+</xsl:text>
+    <xsl:text>            if ((match = re.text.exec(_fmt)) !== null) {
+</xsl:text>
+    <xsl:text>                parse_tree.push(match[0])
+</xsl:text>
+    <xsl:text>            }
+</xsl:text>
+    <xsl:text>            else if ((match = re.modulo.exec(_fmt)) !== null) {
+</xsl:text>
+    <xsl:text>                parse_tree.push('%')
+</xsl:text>
+    <xsl:text>            }
+</xsl:text>
+    <xsl:text>            else if ((match = re.placeholder.exec(_fmt)) !== null) {
+</xsl:text>
+    <xsl:text>                if (match[2]) {
+</xsl:text>
+    <xsl:text>                    arg_names |= 1
+</xsl:text>
+    <xsl:text>                    var field_list = [], replacement_field = match[2], field_match = []
+</xsl:text>
+    <xsl:text>                    if ((field_match = re.key.exec(replacement_field)) !== null) {
+</xsl:text>
+    <xsl:text>                        field_list.push(field_match[1])
+</xsl:text>
+    <xsl:text>                        while ((replacement_field = replacement_field.substring(field_match[0].length)) !== '') {
+</xsl:text>
+    <xsl:text>                            if ((field_match = re.key_access.exec(replacement_field)) !== null) {
+</xsl:text>
+    <xsl:text>                                field_list.push(field_match[1])
+</xsl:text>
+    <xsl:text>                            }
+</xsl:text>
+    <xsl:text>                            else if ((field_match = re.index_access.exec(replacement_field)) !== null) {
+</xsl:text>
+    <xsl:text>                                field_list.push(field_match[1])
+</xsl:text>
+    <xsl:text>                            }
+</xsl:text>
+    <xsl:text>                            else {
+</xsl:text>
+    <xsl:text>                                throw new SyntaxError('[sprintf] failed to parse named argument key')
+</xsl:text>
+    <xsl:text>                            }
+</xsl:text>
+    <xsl:text>                        }
+</xsl:text>
+    <xsl:text>                    }
+</xsl:text>
+    <xsl:text>                    else {
+</xsl:text>
+    <xsl:text>                        throw new SyntaxError('[sprintf] failed to parse named argument key')
+</xsl:text>
+    <xsl:text>                    }
+</xsl:text>
+    <xsl:text>                    match[2] = field_list
+</xsl:text>
+    <xsl:text>                }
+</xsl:text>
+    <xsl:text>                else {
+</xsl:text>
+    <xsl:text>                    arg_names |= 2
+</xsl:text>
+    <xsl:text>                }
+</xsl:text>
+    <xsl:text>                if (arg_names === 3) {
+</xsl:text>
+    <xsl:text>                    throw new Error('[sprintf] mixing positional and named placeholders is not (yet) supported')
+</xsl:text>
+    <xsl:text>                }
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>                parse_tree.push(
+</xsl:text>
+    <xsl:text>                    {
+</xsl:text>
+    <xsl:text>                        placeholder: match[0],
+</xsl:text>
+    <xsl:text>                        param_no:    match[1],
+</xsl:text>
+    <xsl:text>                        keys:        match[2],
+</xsl:text>
+    <xsl:text>                        sign:        match[3],
+</xsl:text>
+    <xsl:text>                        pad_char:    match[4],
+</xsl:text>
+    <xsl:text>                        align:       match[5],
+</xsl:text>
+    <xsl:text>                        width:       match[6],
+</xsl:text>
+    <xsl:text>                        precision:   match[7],
+</xsl:text>
+    <xsl:text>                        type:        match[8]
+</xsl:text>
+    <xsl:text>                    }
+</xsl:text>
+    <xsl:text>                )
+</xsl:text>
+    <xsl:text>            }
+</xsl:text>
+    <xsl:text>            else {
+</xsl:text>
+    <xsl:text>                throw new SyntaxError('[sprintf] unexpected placeholder')
+</xsl:text>
+    <xsl:text>            }
+</xsl:text>
+    <xsl:text>            _fmt = _fmt.substring(match[0].length)
+</xsl:text>
+    <xsl:text>        }
+</xsl:text>
+    <xsl:text>        return sprintf_cache[fmt] = parse_tree
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>    /**
+</xsl:text>
+    <xsl:text>     * export to either browser or node.js
+</xsl:text>
+    <xsl:text>     */
+</xsl:text>
+    <xsl:text>    /* eslint-disable quote-props */
+</xsl:text>
+    <xsl:text>    if (typeof exports !== 'undefined') {
+</xsl:text>
+    <xsl:text>        exports['sprintf'] = sprintf
+</xsl:text>
+    <xsl:text>        exports['vsprintf'] = vsprintf
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>    if (typeof window !== 'undefined') {
+</xsl:text>
+    <xsl:text>        window['sprintf'] = sprintf
+</xsl:text>
+    <xsl:text>        window['vsprintf'] = vsprintf
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>        if (typeof define === 'function' &amp;&amp; define['amd']) {
+</xsl:text>
+    <xsl:text>            define(function() {
+</xsl:text>
+    <xsl:text>                return {
+</xsl:text>
+    <xsl:text>                    'sprintf': sprintf,
+</xsl:text>
+    <xsl:text>                    'vsprintf': vsprintf
+</xsl:text>
+    <xsl:text>                }
+</xsl:text>
+    <xsl:text>            })
+</xsl:text>
+    <xsl:text>        }
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>    /* eslint-enable quote-props */
+</xsl:text>
+    <xsl:text>}(); // eslint-disable-line    
+</xsl:text>
+    <xsl:text>
+</xsl:text>
   </xsl:template>
   <xsl:template match="widget[@type='DropDown']" mode="widget_defs">
     <xsl:param name="hmi_element"/>
@@ -2224,6 +2777,20 @@
   </xsl:template>
   <xsl:template match="widget[@type='ForEach']" mode="widget_defs">
     <xsl:param name="hmi_element"/>
+    <xsl:if test="count(path) != 1">
+      <xsl:message terminate="yes">
+        <xsl:text>ForEach widget </xsl:text>
+        <xsl:value-of select="$hmi_element/@id"/>
+        <xsl:text> must have one HMI path given.</xsl:text>
+      </xsl:message>
+    </xsl:if>
+    <xsl:if test="count(arg) != 1">
+      <xsl:message terminate="yes">
+        <xsl:text>ForEach widget </xsl:text>
+        <xsl:value-of select="$hmi_element/@id"/>
+        <xsl:text> must have one argument given : a class name.</xsl:text>
+      </xsl:message>
+    </xsl:if>
     <xsl:variable name="class" select="arg[1]/@value"/>
     <xsl:variable name="base_path" select="path/@value"/>
     <xsl:variable name="hmi_index_base" select="$indexed_hmitree/*[@hmipath = $base_path]"/>
@@ -2325,97 +2892,125 @@
   <xsl:template match="widget[@type='ForEach']" mode="widget_class">
     <xsl:text>class ForEachWidget extends Widget{
 </xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>    unsub_items(){
+</xsl:text>
+    <xsl:text>        for(let item of this.items){
+</xsl:text>
+    <xsl:text>            for(let widget of item) {
+</xsl:text>
+    <xsl:text>                widget.unsub();
+</xsl:text>
+    <xsl:text>            }
+</xsl:text>
+    <xsl:text>        }
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>
+</xsl:text>
     <xsl:text>    unsub(){
 </xsl:text>
-    <xsl:text>        for(let item of this.items){
+    <xsl:text>        this.unsub_items();
+</xsl:text>
+    <xsl:text>        this.offset = 0;
+</xsl:text>
+    <xsl:text>        this.relativeness = undefined;
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>    sub_items(){
+</xsl:text>
+    <xsl:text>        for(let i = 0; i &lt; this.items.length; i++) {
+</xsl:text>
+    <xsl:text>            let item = this.items[i];
+</xsl:text>
+    <xsl:text>            let orig_item_index = this.index_pool[i];
+</xsl:text>
+    <xsl:text>            let item_index = this.index_pool[i+this.item_offset];
+</xsl:text>
+    <xsl:text>            let item_index_offset = item_index - orig_item_index;
+</xsl:text>
+    <xsl:text>            if(this.relativeness[0])
+</xsl:text>
+    <xsl:text>                item_index_offset += this.offset;
 </xsl:text>
     <xsl:text>            for(let widget of item) {
 </xsl:text>
-    <xsl:text>                widget.unsub();
+    <xsl:text>                /* all variables of all widgets in a ForEach are all relative. 
+</xsl:text>
+    <xsl:text>                   Really.
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>                   TODO: allow absolute variables in ForEach widgets
+</xsl:text>
+    <xsl:text>                */
+</xsl:text>
+    <xsl:text>                widget.sub(item_index_offset, widget.indexes.map(_=&gt;true));
 </xsl:text>
     <xsl:text>            }
 </xsl:text>
     <xsl:text>        }
 </xsl:text>
-    <xsl:text>        this.offset = 0;
-</xsl:text>
     <xsl:text>    }
 </xsl:text>
     <xsl:text>
 </xsl:text>
-    <xsl:text>    foreach_widgets_do(todo){
-</xsl:text>
-    <xsl:text>        for(let i = 0; i &lt; this.items.length; i++) {
-</xsl:text>
-    <xsl:text>            let item = this.items[i];
-</xsl:text>
-    <xsl:text>            let orig_item_index = this.index_pool[i];
-</xsl:text>
-    <xsl:text>            let item_index = this.index_pool[i+this.item_offset];
-</xsl:text>
-    <xsl:text>            let item_index_offset = item_index - orig_item_index;
-</xsl:text>
-    <xsl:text>            for(let widget of item) {
-</xsl:text>
-    <xsl:text>                todo(widget).call(widget, this.offset + item_index_offset);
-</xsl:text>
-    <xsl:text>            }
+    <xsl:text>    sub(new_offset=0, relativeness=[]){
+</xsl:text>
+    <xsl:text>        this.offset = new_offset;
+</xsl:text>
+    <xsl:text>        this.relativeness = relativeness;
+</xsl:text>
+    <xsl:text>        this.sub_items();
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>    apply_cache() {
+</xsl:text>
+    <xsl:text>        this.items.forEach(item=&gt;item.forEach(widget=&gt;widget.apply_cache()));
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>    on_click(opstr, evt) {
+</xsl:text>
+    <xsl:text>        let new_item_offset = eval(String(this.item_offset)+opstr);
+</xsl:text>
+    <xsl:text>        if(new_item_offset + this.items.length &gt; this.index_pool.length) {
+</xsl:text>
+    <xsl:text>            if(this.item_offset + this.items.length == this.index_pool.length)
+</xsl:text>
+    <xsl:text>                new_item_offset = 0;
+</xsl:text>
+    <xsl:text>            else
+</xsl:text>
+    <xsl:text>                new_item_offset = this.index_pool.length - this.items.length;
+</xsl:text>
+    <xsl:text>        } else if(new_item_offset &lt; 0) {
+</xsl:text>
+    <xsl:text>            if(this.item_offset == 0)
+</xsl:text>
+    <xsl:text>                new_item_offset = this.index_pool.length - this.items.length;
+</xsl:text>
+    <xsl:text>            else
+</xsl:text>
+    <xsl:text>                new_item_offset = 0;
 </xsl:text>
     <xsl:text>        }
 </xsl:text>
-    <xsl:text>    }
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>    sub(new_offset=0){
-</xsl:text>
-    <xsl:text>        this.offset = new_offset;
-</xsl:text>
-    <xsl:text>        this.foreach_widgets_do(w=&gt;w.sub);
-</xsl:text>
-    <xsl:text>    }
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>    apply_cache() {
-</xsl:text>
-    <xsl:text>        this.foreach_widgets_do(w=&gt;w.apply_cache);
-</xsl:text>
-    <xsl:text>    }
-</xsl:text>
-    <xsl:text>
-</xsl:text>
-    <xsl:text>    on_click(opstr, evt) {
-</xsl:text>
-    <xsl:text>        let new_item_offset = eval(String(this.item_offset)+opstr);
-</xsl:text>
-    <xsl:text>        if(new_item_offset + this.items.length &gt; this.index_pool.length) {
-</xsl:text>
-    <xsl:text>            if(this.item_offset + this.items.length == this.index_pool.length)
-</xsl:text>
-    <xsl:text>                new_item_offset = 0;
-</xsl:text>
-    <xsl:text>            else
-</xsl:text>
-    <xsl:text>                new_item_offset = this.index_pool.length - this.items.length;
-</xsl:text>
-    <xsl:text>        } else if(new_item_offset &lt; 0) {
-</xsl:text>
-    <xsl:text>            if(this.item_offset == 0)
-</xsl:text>
-    <xsl:text>                new_item_offset = this.index_pool.length - this.items.length;
-</xsl:text>
-    <xsl:text>            else
-</xsl:text>
-    <xsl:text>                new_item_offset = 0;
-</xsl:text>
-    <xsl:text>        }
-</xsl:text>
     <xsl:text>        this.item_offset = new_item_offset;
 </xsl:text>
-    <xsl:text>        this.unsub();
-</xsl:text>
-    <xsl:text>        this.sub(this.offset);
+    <xsl:text>        this.unsub_items();
+</xsl:text>
+    <xsl:text>        this.sub_items();
 </xsl:text>
     <xsl:text>        update_subscriptions();
 </xsl:text>
@@ -3917,655 +4512,613 @@
 </xsl:text>
           <xsl:text>
 </xsl:text>
-          <xsl:text>function dispatch_value_to_widget(widget, index, value, oldval) {
+          <xsl:text>
+</xsl:text>
+          <xsl:text>function dispatch_value(index, value) {
+</xsl:text>
+          <xsl:text>    let widgets = subscribers[index];
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>    let oldval = cache[index];
+</xsl:text>
+          <xsl:text>    cache[index] = value;
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>    if(widgets.size &gt; 0) {
+</xsl:text>
+          <xsl:text>        for(let widget of widgets){
+</xsl:text>
+          <xsl:text>            widget.new_hmi_value(index, value, oldval);
+</xsl:text>
+          <xsl:text>        }
+</xsl:text>
+          <xsl:text>    }
+</xsl:text>
+          <xsl:text>};
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>function init_widgets() {
+</xsl:text>
+          <xsl:text>    Object.keys(hmi_widgets).forEach(function(id) {
+</xsl:text>
+          <xsl:text>        let widget = hmi_widgets[id];
+</xsl:text>
+          <xsl:text>        let init = widget.init;
+</xsl:text>
+          <xsl:text>        if(typeof(init) == "function"){
+</xsl:text>
+          <xsl:text>            try {
+</xsl:text>
+          <xsl:text>                init.call(widget);
+</xsl:text>
+          <xsl:text>            } catch(err) {
+</xsl:text>
+          <xsl:text>                console.log(err);
+</xsl:text>
+          <xsl:text>            }
+</xsl:text>
+          <xsl:text>        }
+</xsl:text>
+          <xsl:text>    });
+</xsl:text>
+          <xsl:text>};
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>// Open WebSocket to relative "/ws" address
+</xsl:text>
+          <xsl:text>var ws = new WebSocket(window.location.href.replace(/^http(s?:\/\/[^\/]*)\/.*$/, 'ws$1/ws'));
+</xsl:text>
+          <xsl:text>ws.binaryType = 'arraybuffer';
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>const dvgetters = {
+</xsl:text>
+          <xsl:text>    INT: (dv,offset) =&gt; [dv.getInt16(offset, true), 2],
+</xsl:text>
+          <xsl:text>    BOOL: (dv,offset) =&gt; [dv.getInt8(offset, true), 1],
+</xsl:text>
+          <xsl:text>    NODE: (dv,offset) =&gt; [dv.getInt8(offset, true), 1],
+</xsl:text>
+          <xsl:text>    STRING: (dv, offset) =&gt; {
+</xsl:text>
+          <xsl:text>        size = dv.getInt8(offset);
+</xsl:text>
+          <xsl:text>        return [
+</xsl:text>
+          <xsl:text>            String.fromCharCode.apply(null, new Uint8Array(
+</xsl:text>
+          <xsl:text>                dv.buffer, /* original buffer */
+</xsl:text>
+          <xsl:text>                offset + 1, /* string starts after size*/
+</xsl:text>
+          <xsl:text>                size /* size of string */
+</xsl:text>
+          <xsl:text>            )), size + 1]; /* total increment */
+</xsl:text>
+          <xsl:text>    }
+</xsl:text>
+          <xsl:text>};
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>// Apply updates recieved through ws.onmessage to subscribed widgets
+</xsl:text>
+          <xsl:text>function apply_updates() {
+</xsl:text>
+          <xsl:text>    for(let index in updates){
+</xsl:text>
+          <xsl:text>        // serving as a key, index becomes a string
+</xsl:text>
+          <xsl:text>        // -&gt; pass Number(index) instead
+</xsl:text>
+          <xsl:text>        dispatch_value(Number(index), updates[index]);
+</xsl:text>
+          <xsl:text>        delete updates[index];
+</xsl:text>
+          <xsl:text>    }
+</xsl:text>
+          <xsl:text>}
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>// Called on requestAnimationFrame, modifies DOM
+</xsl:text>
+          <xsl:text>var requestAnimationFrameID = null;
+</xsl:text>
+          <xsl:text>function animate() {
+</xsl:text>
+          <xsl:text>    // Do the page swith if any one pending
+</xsl:text>
+          <xsl:text>    if(current_subscribed_page != current_visible_page){
+</xsl:text>
+          <xsl:text>        switch_visible_page(current_subscribed_page);
+</xsl:text>
+          <xsl:text>    }
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>    while(widget = need_cache_apply.pop()){
+</xsl:text>
+          <xsl:text>        widget.apply_cache();
+</xsl:text>
+          <xsl:text>    }
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>    if(jumps_need_update) update_jumps();
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>    apply_updates();
+</xsl:text>
+          <xsl:text>    requestAnimationFrameID = null;
+</xsl:text>
+          <xsl:text>}
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>function requestHMIAnimation() {
+</xsl:text>
+          <xsl:text>    if(requestAnimationFrameID == null){
+</xsl:text>
+          <xsl:text>        requestAnimationFrameID = window.requestAnimationFrame(animate);
+</xsl:text>
+          <xsl:text>    }
+</xsl:text>
+          <xsl:text>}
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>// Message reception handler
+</xsl:text>
+          <xsl:text>// Hash is verified and HMI values updates resulting from binary parsing
+</xsl:text>
+          <xsl:text>// are stored until browser can compute next frame, DOM is left untouched
+</xsl:text>
+          <xsl:text>ws.onmessage = function (evt) {
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>    let data = evt.data;
+</xsl:text>
+          <xsl:text>    let dv = new DataView(data);
+</xsl:text>
+          <xsl:text>    let i = 0;
 </xsl:text>
           <xsl:text>    try {
 </xsl:text>
-          <xsl:text>        let idx = widget.offset ? index - widget.offset : index;
-</xsl:text>
-          <xsl:text>        let idxidx = widget.indexes.indexOf(idx);
-</xsl:text>
-          <xsl:text>        let d = widget.dispatch;
-</xsl:text>
-          <xsl:text>        if(typeof(d) == "function" &amp;&amp; idxidx == 0){
-</xsl:text>
-          <xsl:text>            d.call(widget, value, oldval);
+          <xsl:text>        for(let hash_int of hmi_hash) {
+</xsl:text>
+          <xsl:text>            if(hash_int != dv.getUint8(i)){
+</xsl:text>
+          <xsl:text>                throw new Error("Hash doesn't match");
+</xsl:text>
+          <xsl:text>            };
+</xsl:text>
+          <xsl:text>            i++;
+</xsl:text>
+          <xsl:text>        };
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>        while(i &lt; data.byteLength){
+</xsl:text>
+          <xsl:text>            let index = dv.getUint32(i, true);
+</xsl:text>
+          <xsl:text>            i += 4;
+</xsl:text>
+          <xsl:text>            let iectype = hmitree_types[index];
+</xsl:text>
+          <xsl:text>            if(iectype != undefined){
+</xsl:text>
+          <xsl:text>                let dvgetter = dvgetters[iectype];
+</xsl:text>
+          <xsl:text>                let [value, bytesize] = dvgetter(dv,i);
+</xsl:text>
+          <xsl:text>                updates[index] = value;
+</xsl:text>
+          <xsl:text>                i += bytesize;
+</xsl:text>
+          <xsl:text>            } else {
+</xsl:text>
+          <xsl:text>                throw new Error("Unknown index "+index);
+</xsl:text>
+          <xsl:text>            }
+</xsl:text>
+          <xsl:text>        };
+</xsl:text>
+          <xsl:text>        // register for rendering on next frame, since there are updates
+</xsl:text>
+          <xsl:text>        requestHMIAnimation();
+</xsl:text>
+          <xsl:text>    } catch(err) {
+</xsl:text>
+          <xsl:text>        // 1003 is for "Unsupported Data"
+</xsl:text>
+          <xsl:text>        // ws.close(1003, err.message);
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>        // TODO : remove debug alert ?
+</xsl:text>
+          <xsl:text>        alert("Error : "+err.message+"\nHMI will be reloaded.");
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>        // force reload ignoring cache
+</xsl:text>
+          <xsl:text>        location.reload(true);
+</xsl:text>
+          <xsl:text>    }
+</xsl:text>
+          <xsl:text>};
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>function send_blob(data) {
+</xsl:text>
+          <xsl:text>    if(data.length &gt; 0) {
+</xsl:text>
+          <xsl:text>        ws.send(new Blob([new Uint8Array(hmi_hash)].concat(data)));
+</xsl:text>
+          <xsl:text>    };
+</xsl:text>
+          <xsl:text>};
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>const typedarray_types = {
+</xsl:text>
+          <xsl:text>    INT: (number) =&gt; new Int16Array([number]),
+</xsl:text>
+          <xsl:text>    BOOL: (truth) =&gt; new Int16Array([truth]),
+</xsl:text>
+          <xsl:text>    NODE: (truth) =&gt; new Int16Array([truth]),
+</xsl:text>
+          <xsl:text>    STRING: (str) =&gt; {
+</xsl:text>
+          <xsl:text>        // beremiz default string max size is 128
+</xsl:text>
+          <xsl:text>        str = str.slice(0,128);
+</xsl:text>
+          <xsl:text>        binary = new Uint8Array(str.length + 1);
+</xsl:text>
+          <xsl:text>        binary[0] = str.length;
+</xsl:text>
+          <xsl:text>        for(var i = 0; i &lt; str.length; i++){
+</xsl:text>
+          <xsl:text>            binary[i+1] = str.charCodeAt(i);
 </xsl:text>
           <xsl:text>        }
 </xsl:text>
-          <xsl:text>        else if(typeof(d) == "object" &amp;&amp; d.length &gt;= idxidx){
-</xsl:text>
-          <xsl:text>            d[idxidx].call(widget, value, oldval);
+          <xsl:text>        return binary;
+</xsl:text>
+          <xsl:text>    }
+</xsl:text>
+          <xsl:text>    /* TODO */
+</xsl:text>
+          <xsl:text>};
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>function send_reset() {
+</xsl:text>
+          <xsl:text>    send_blob(new Uint8Array([1])); /* reset = 1 */
+</xsl:text>
+          <xsl:text>};
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>// subscription state, as it should be in hmi server
+</xsl:text>
+          <xsl:text>// hmitree indexed array of integers
+</xsl:text>
+          <xsl:text>var subscriptions =  hmitree_types.map(_ignored =&gt; 0);
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>// subscription state as needed by widget now
+</xsl:text>
+          <xsl:text>// hmitree indexed array of Sets of widgets objects
+</xsl:text>
+          <xsl:text>var subscribers = hmitree_types.map(_ignored =&gt; new Set());
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>// artificially subscribe the watchdog widget to "/heartbeat" hmi variable
+</xsl:text>
+          <xsl:text>// Since dispatch directly calls change_hmi_value,
+</xsl:text>
+          <xsl:text>// PLC will periodically send variable at given frequency
+</xsl:text>
+          <xsl:text>subscribers[heartbeat_index].add({
+</xsl:text>
+          <xsl:text>    /* type: "Watchdog", */
+</xsl:text>
+          <xsl:text>    frequency: 1,
+</xsl:text>
+          <xsl:text>    indexes: [heartbeat_index],
+</xsl:text>
+          <xsl:text>    new_hmi_value: function(index, value, oldval) {
+</xsl:text>
+          <xsl:text>        apply_hmi_value(heartbeat_index, value+1);
+</xsl:text>
+          <xsl:text>    }
+</xsl:text>
+          <xsl:text>});
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>function update_subscriptions() {
+</xsl:text>
+          <xsl:text>    let delta = [];
+</xsl:text>
+          <xsl:text>    for(let index = 0; index &lt; subscribers.length; index++){
+</xsl:text>
+          <xsl:text>        let widgets = subscribers[index];
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>        // periods are in ms
+</xsl:text>
+          <xsl:text>        let previous_period = subscriptions[index];
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>        // subscribing with a zero period is unsubscribing
+</xsl:text>
+          <xsl:text>        let new_period = 0;
+</xsl:text>
+          <xsl:text>        if(widgets.size &gt; 0) {
+</xsl:text>
+          <xsl:text>            let maxfreq = 0;
+</xsl:text>
+          <xsl:text>            for(let widget of widgets){
+</xsl:text>
+          <xsl:text>                let wf = widget.frequency;
+</xsl:text>
+          <xsl:text>                if(wf != undefined &amp;&amp; maxfreq &lt; wf)
+</xsl:text>
+          <xsl:text>                    maxfreq = wf;
+</xsl:text>
+          <xsl:text>            }
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>            if(maxfreq != 0)
+</xsl:text>
+          <xsl:text>                new_period = 1000/maxfreq;
 </xsl:text>
           <xsl:text>        }
 </xsl:text>
-          <xsl:text>        /* else dispatch_0, ..., dispatch_n ? */
-</xsl:text>
-          <xsl:text>        /*else {
-</xsl:text>
-          <xsl:text>            throw new Error("Dunno how to dispatch to widget at index = " + index);
-</xsl:text>
-          <xsl:text>        }*/
-</xsl:text>
-          <xsl:text>    } catch(err) {
-</xsl:text>
-          <xsl:text>        console.log(err);
+          <xsl:text>
+</xsl:text>
+          <xsl:text>        if(previous_period != new_period) {
+</xsl:text>
+          <xsl:text>            subscriptions[index] = new_period;
+</xsl:text>
+          <xsl:text>            delta.push(
+</xsl:text>
+          <xsl:text>                new Uint8Array([2]), /* subscribe = 2 */
+</xsl:text>
+          <xsl:text>                new Uint32Array([index]),
+</xsl:text>
+          <xsl:text>                new Uint16Array([new_period]));
+</xsl:text>
+          <xsl:text>        }
 </xsl:text>
           <xsl:text>    }
 </xsl:text>
+          <xsl:text>    send_blob(delta);
+</xsl:text>
+          <xsl:text>};
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>function send_hmi_value(index, value) {
+</xsl:text>
+          <xsl:text>    let iectype = hmitree_types[index];
+</xsl:text>
+          <xsl:text>    let tobinary = typedarray_types[iectype];
+</xsl:text>
+          <xsl:text>    send_blob([
+</xsl:text>
+          <xsl:text>        new Uint8Array([0]),  /* setval = 0 */
+</xsl:text>
+          <xsl:text>        new Uint32Array([index]),
+</xsl:text>
+          <xsl:text>        tobinary(value)]);
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>    // DON'T DO THAT unless read_iterator in svghmi.c modifies wbuf as well, not only rbuf
+</xsl:text>
+          <xsl:text>    // cache[index] = value;
+</xsl:text>
+          <xsl:text>};
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>function apply_hmi_value(index, new_val) {
+</xsl:text>
+          <xsl:text>    let old_val = cache[index]
+</xsl:text>
+          <xsl:text>    if(new_val != undefined &amp;&amp; old_val != new_val)
+</xsl:text>
+          <xsl:text>        send_hmi_value(index, new_val);
+</xsl:text>
+          <xsl:text>    return new_val;
+</xsl:text>
           <xsl:text>}
 </xsl:text>
           <xsl:text>
 </xsl:text>
-          <xsl:text>function dispatch_value(index, value) {
-</xsl:text>
-          <xsl:text>    let widgets = subscribers[index];
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>    let oldval = cache[index];
-</xsl:text>
-          <xsl:text>    cache[index] = value;
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>    if(widgets.size &gt; 0) {
-</xsl:text>
-          <xsl:text>        for(let widget of widgets){
-</xsl:text>
-          <xsl:text>            dispatch_value_to_widget(widget, index, value, oldval);
+          <xsl:text>quotes = {"'":null, '"':null};
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>function change_hmi_value(index, opstr) {
+</xsl:text>
+          <xsl:text>    let op = opstr[0];
+</xsl:text>
+          <xsl:text>    let given_val;
+</xsl:text>
+          <xsl:text>    if(opstr.length &lt; 2) 
+</xsl:text>
+          <xsl:text>        return undefined; // TODO raise
+</xsl:text>
+          <xsl:text>    if(opstr[1] in quotes){
+</xsl:text>
+          <xsl:text>        if(opstr.length &lt; 3) 
+</xsl:text>
+          <xsl:text>            return undefined; // TODO raise
+</xsl:text>
+          <xsl:text>        if(opstr[opstr.length-1] == opstr[1]){
+</xsl:text>
+          <xsl:text>            given_val = opstr.slice(2,opstr.length-1);
 </xsl:text>
           <xsl:text>        }
 </xsl:text>
+          <xsl:text>    } else {
+</xsl:text>
+          <xsl:text>        given_val = Number(opstr.slice(1));
+</xsl:text>
           <xsl:text>    }
 </xsl:text>
+          <xsl:text>    let old_val = cache[index];
+</xsl:text>
+          <xsl:text>    let new_val;
+</xsl:text>
+          <xsl:text>    switch(op){
+</xsl:text>
+          <xsl:text>      case "=":
+</xsl:text>
+          <xsl:text>        new_val = given_val;
+</xsl:text>
+          <xsl:text>        break;
+</xsl:text>
+          <xsl:text>      case "+":
+</xsl:text>
+          <xsl:text>        new_val = old_val + given_val;
+</xsl:text>
+          <xsl:text>        break;
+</xsl:text>
+          <xsl:text>      case "-":
+</xsl:text>
+          <xsl:text>        new_val = old_val - given_val;
+</xsl:text>
+          <xsl:text>        break;
+</xsl:text>
+          <xsl:text>      case "*":
+</xsl:text>
+          <xsl:text>        new_val = old_val * given_val;
+</xsl:text>
+          <xsl:text>        break;
+</xsl:text>
+          <xsl:text>      case "/":
+</xsl:text>
+          <xsl:text>        new_val = old_val / given_val;
+</xsl:text>
+          <xsl:text>        break;
+</xsl:text>
+          <xsl:text>    }
+</xsl:text>
+          <xsl:text>    if(new_val != undefined &amp;&amp; old_val != new_val)
+</xsl:text>
+          <xsl:text>        send_hmi_value(index, new_val);
+</xsl:text>
+          <xsl:text>    // TODO else raise
+</xsl:text>
+          <xsl:text>    return new_val;
+</xsl:text>
+          <xsl:text>}
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>var current_visible_page;
+</xsl:text>
+          <xsl:text>var current_subscribed_page;
+</xsl:text>
+          <xsl:text>var current_page_index;
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>function prepare_svg() {
+</xsl:text>
+          <xsl:text>    for(let eltid in detachable_elements){
+</xsl:text>
+          <xsl:text>        let [element,parent] = detachable_elements[eltid];
+</xsl:text>
+          <xsl:text>        parent.removeChild(element);
+</xsl:text>
+          <xsl:text>    }
+</xsl:text>
           <xsl:text>};
 </xsl:text>
           <xsl:text>
 </xsl:text>
-          <xsl:text>function init_widgets() {
-</xsl:text>
-          <xsl:text>    Object.keys(hmi_widgets).forEach(function(id) {
-</xsl:text>
-          <xsl:text>        let widget = hmi_widgets[id];
-</xsl:text>
-          <xsl:text>        let init = widget.init;
-</xsl:text>
-          <xsl:text>        if(typeof(init) == "function"){
-</xsl:text>
-          <xsl:text>            try {
-</xsl:text>
-          <xsl:text>                init.call(widget);
-</xsl:text>
-          <xsl:text>            } catch(err) {
-</xsl:text>
-          <xsl:text>                console.log(err);
-</xsl:text>
-          <xsl:text>            }
-</xsl:text>
-          <xsl:text>        }
-</xsl:text>
-          <xsl:text>    });
-</xsl:text>
-          <xsl:text>};
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>// Open WebSocket to relative "/ws" address
-</xsl:text>
-          <xsl:text>var ws = new WebSocket(window.location.href.replace(/^http(s?:\/\/[^\/]*)\/.*$/, 'ws$1/ws'));
-</xsl:text>
-          <xsl:text>ws.binaryType = 'arraybuffer';
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>const dvgetters = {
-</xsl:text>
-          <xsl:text>    INT: (dv,offset) =&gt; [dv.getInt16(offset, true), 2],
-</xsl:text>
-          <xsl:text>    BOOL: (dv,offset) =&gt; [dv.getInt8(offset, true), 1],
-</xsl:text>
-          <xsl:text>    NODE: (dv,offset) =&gt; [dv.getInt8(offset, true), 1],
-</xsl:text>
-          <xsl:text>    STRING: (dv, offset) =&gt; {
-</xsl:text>
-          <xsl:text>        size = dv.getInt8(offset);
-</xsl:text>
-          <xsl:text>        return [
-</xsl:text>
-          <xsl:text>            String.fromCharCode.apply(null, new Uint8Array(
-</xsl:text>
-          <xsl:text>                dv.buffer, /* original buffer */
-</xsl:text>
-          <xsl:text>                offset + 1, /* string starts after size*/
-</xsl:text>
-          <xsl:text>                size /* size of string */
-</xsl:text>
-          <xsl:text>            )), size + 1]; /* total increment */
+          <xsl:text>function switch_page(page_name, page_index) {
+</xsl:text>
+          <xsl:text>    if(current_subscribed_page != current_visible_page){
+</xsl:text>
+          <xsl:text>        /* page switch already going */
+</xsl:text>
+          <xsl:text>        /* TODO LOG ERROR */
+</xsl:text>
+          <xsl:text>        return false;
 </xsl:text>
           <xsl:text>    }
 </xsl:text>
-          <xsl:text>};
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>// Apply updates recieved through ws.onmessage to subscribed widgets
-</xsl:text>
-          <xsl:text>function apply_updates() {
-</xsl:text>
-          <xsl:text>    for(let index in updates){
-</xsl:text>
-          <xsl:text>        // serving as a key, index becomes a string
-</xsl:text>
-          <xsl:text>        // -&gt; pass Number(index) instead
-</xsl:text>
-          <xsl:text>        dispatch_value(Number(index), updates[index]);
-</xsl:text>
-          <xsl:text>        delete updates[index];
+          <xsl:text>
+</xsl:text>
+          <xsl:text>    if(page_name == undefined)
+</xsl:text>
+          <xsl:text>        page_name = current_subscribed_page;
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>    let old_desc = page_desc[current_subscribed_page];
+</xsl:text>
+          <xsl:text>    let new_desc = page_desc[page_name];
+</xsl:text>
+          <xsl:text>
+</xsl:text>
+          <xsl:text>    if(new_desc == undefined){
+</xsl:text>
+          <xsl:text>        /* TODO LOG ERROR */
+</xsl:text>
+          <xsl:text>        return false;
 </xsl:text>
           <xsl:text>    }
 </xsl:text>
-          <xsl:text>}
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>// Called on requestAnimationFrame, modifies DOM
-</xsl:text>
-          <xsl:text>var requestAnimationFrameID = null;
-</xsl:text>
-          <xsl:text>function animate() {
-</xsl:text>
-          <xsl:text>    // Do the page swith if any one pending
-</xsl:text>
-          <xsl:text>    if(current_subscribed_page != current_visible_page){
-</xsl:text>
-          <xsl:text>        switch_visible_page(current_subscribed_page);
+          <xsl:text>
+</xsl:text>
+          <xsl:text>    if(page_index == undefined){
+</xsl:text>
+          <xsl:text>        page_index = new_desc.page_index;
 </xsl:text>
           <xsl:text>    }
 </xsl:text>
           <xsl:text>
 </xsl:text>
-          <xsl:text>    while(widget = need_cache_apply.pop()){
-</xsl:text>
-          <xsl:text>        widget.apply_cache();
+          <xsl:text>    if(old_desc){
+</xsl:text>
+          <xsl:text>        old_desc.widgets.map(([widget,relativeness])=&gt;widget.unsub());
 </xsl:text>
           <xsl:text>    }
 </xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>    if(jumps_need_update) update_jumps();
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>    apply_updates();
-</xsl:text>
-          <xsl:text>    requestAnimationFrameID = null;
-</xsl:text>
-          <xsl:text>}
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>function requestHMIAnimation() {
-</xsl:text>
-          <xsl:text>    if(requestAnimationFrameID == null){
-</xsl:text>
-          <xsl:text>        requestAnimationFrameID = window.requestAnimationFrame(animate);
-</xsl:text>
-          <xsl:text>    }
-</xsl:text>
-          <xsl:text>}
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>// Message reception handler
-</xsl:text>
-          <xsl:text>// Hash is verified and HMI values updates resulting from binary parsing
-</xsl:text>
-          <xsl:text>// are stored until browser can compute next frame, DOM is left untouched
-</xsl:text>
-          <xsl:text>ws.onmessage = function (evt) {
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>    let data = evt.data;
-</xsl:text>
-          <xsl:text>    let dv = new DataView(data);
-</xsl:text>
-          <xsl:text>    let i = 0;
-</xsl:text>
-          <xsl:text>    try {
-</xsl:text>
-          <xsl:text>        for(let hash_int of hmi_hash) {
-</xsl:text>
-          <xsl:text>            if(hash_int != dv.getUint8(i)){
-</xsl:text>
-          <xsl:text>                throw new Error("Hash doesn't match");
-</xsl:text>
-          <xsl:text>            };
-</xsl:text>
-          <xsl:text>            i++;
-</xsl:text>
-          <xsl:text>        };
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>        while(i &lt; data.byteLength){
-</xsl:text>
-          <xsl:text>            let index = dv.getUint32(i, true);
-</xsl:text>
-          <xsl:text>            i += 4;
-</xsl:text>
-          <xsl:text>            let iectype = hmitree_types[index];
-</xsl:text>
-          <xsl:text>            if(iectype != undefined){
-</xsl:text>
-          <xsl:text>                let dvgetter = dvgetters[iectype];
-</xsl:text>
-          <xsl:text>                let [value, bytesize] = dvgetter(dv,i);
-</xsl:text>
-          <xsl:text>                updates[index] = value;
-</xsl:text>
-          <xsl:text>                i += bytesize;
-</xsl:text>
-          <xsl:text>            } else {
-</xsl:text>
-          <xsl:text>                throw new Error("Unknown index "+index);
-</xsl:text>
-          <xsl:text>            }
-</xsl:text>
-          <xsl:text>        };
-</xsl:text>
-          <xsl:text>        // register for rendering on next frame, since there are updates
-</xsl:text>
-          <xsl:text>        requestHMIAnimation();
-</xsl:text>
-          <xsl:text>    } catch(err) {
-</xsl:text>
-          <xsl:text>        // 1003 is for "Unsupported Data"
-</xsl:text>
-          <xsl:text>        // ws.close(1003, err.message);
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>        // TODO : remove debug alert ?
-</xsl:text>
-          <xsl:text>        alert("Error : "+err.message+"\nHMI will be reloaded.");
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>        // force reload ignoring cache
-</xsl:text>
-          <xsl:text>        location.reload(true);
-</xsl:text>
-          <xsl:text>    }
-</xsl:text>
-          <xsl:text>};
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>function send_blob(data) {
-</xsl:text>
-          <xsl:text>    if(data.length &gt; 0) {
-</xsl:text>
-          <xsl:text>        ws.send(new Blob([new Uint8Array(hmi_hash)].concat(data)));
-</xsl:text>
-          <xsl:text>    };
-</xsl:text>
-          <xsl:text>};
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>const typedarray_types = {
-</xsl:text>
-          <xsl:text>    INT: (number) =&gt; new Int16Array([number]),
-</xsl:text>
-          <xsl:text>    BOOL: (truth) =&gt; new Int16Array([truth]),
-</xsl:text>
-          <xsl:text>    NODE: (truth) =&gt; new Int16Array([truth]),
-</xsl:text>
-          <xsl:text>    STRING: (str) =&gt; {
-</xsl:text>
-          <xsl:text>        // beremiz default string max size is 128
-</xsl:text>
-          <xsl:text>        str = str.slice(0,128);
-</xsl:text>
-          <xsl:text>        binary = new Uint8Array(str.length + 1);
-</xsl:text>
-          <xsl:text>        binary[0] = str.length;
-</xsl:text>
-          <xsl:text>        for(var i = 0; i &lt; str.length; i++){
-</xsl:text>
-          <xsl:text>            binary[i+1] = str.charCodeAt(i);
-</xsl:text>
-          <xsl:text>        }
-</xsl:text>
-          <xsl:text>        return binary;
-</xsl:text>
-          <xsl:text>    }
-</xsl:text>
-          <xsl:text>    /* TODO */
-</xsl:text>
-          <xsl:text>};
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>function send_reset() {
-</xsl:text>
-          <xsl:text>    send_blob(new Uint8Array([1])); /* reset = 1 */
-</xsl:text>
-          <xsl:text>};
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>// subscription state, as it should be in hmi server
-</xsl:text>
-          <xsl:text>// hmitree indexed array of integers
-</xsl:text>
-          <xsl:text>var subscriptions =  hmitree_types.map(_ignored =&gt; 0);
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>// subscription state as needed by widget now
-</xsl:text>
-          <xsl:text>// hmitree indexed array of Sets of widgets objects
-</xsl:text>
-          <xsl:text>var subscribers = hmitree_types.map(_ignored =&gt; new Set());
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>// artificially subscribe the watchdog widget to "/heartbeat" hmi variable
-</xsl:text>
-          <xsl:text>// Since dispatch directly calls change_hmi_value,
-</xsl:text>
-          <xsl:text>// PLC will periodically send variable at given frequency
-</xsl:text>
-          <xsl:text>subscribers[heartbeat_index].add({
-</xsl:text>
-          <xsl:text>    /* type: "Watchdog", */
-</xsl:text>
-          <xsl:text>    frequency: 1,
-</xsl:text>
-          <xsl:text>    indexes: [heartbeat_index],
-</xsl:text>
-          <xsl:text>    dispatch: function(value) {
-</xsl:text>
-          <xsl:text>        apply_hmi_value(heartbeat_index, value+1);
-</xsl:text>
-          <xsl:text>    }
-</xsl:text>
-          <xsl:text>});
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>function update_subscriptions() {
-</xsl:text>
-          <xsl:text>    let delta = [];
-</xsl:text>
-          <xsl:text>    for(let index = 0; index &lt; subscribers.length; index++){
-</xsl:text>
-          <xsl:text>        let widgets = subscribers[index];
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>        // periods are in ms
-</xsl:text>
-          <xsl:text>        let previous_period = subscriptions[index];
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>        // subscribing with a zero period is unsubscribing
-</xsl:text>
-          <xsl:text>        let new_period = 0;
-</xsl:text>
-          <xsl:text>        if(widgets.size &gt; 0) {
-</xsl:text>
-          <xsl:text>            let maxfreq = 0;
-</xsl:text>
-          <xsl:text>            for(let widget of widgets){
-</xsl:text>
-          <xsl:text>                let wf = widget.frequency;
-</xsl:text>
-          <xsl:text>                if(wf != undefined &amp;&amp; maxfreq &lt; wf)
-</xsl:text>
-          <xsl:text>                    maxfreq = wf;
-</xsl:text>
-          <xsl:text>            }
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>            if(maxfreq != 0)
-</xsl:text>
-          <xsl:text>                new_period = 1000/maxfreq;
-</xsl:text>
-          <xsl:text>        }
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>        if(previous_period != new_period) {
-</xsl:text>
-          <xsl:text>            subscriptions[index] = new_period;
-</xsl:text>
-          <xsl:text>            delta.push(
-</xsl:text>
-          <xsl:text>                new Uint8Array([2]), /* subscribe = 2 */
-</xsl:text>
-          <xsl:text>                new Uint32Array([index]),
-</xsl:text>
-          <xsl:text>                new Uint16Array([new_period]));
-</xsl:text>
-          <xsl:text>        }
-</xsl:text>
-          <xsl:text>    }
-</xsl:text>
-          <xsl:text>    send_blob(delta);
-</xsl:text>
-          <xsl:text>};
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>function send_hmi_value(index, value) {
-</xsl:text>
-          <xsl:text>    let iectype = hmitree_types[index];
-</xsl:text>
-          <xsl:text>    let tobinary = typedarray_types[iectype];
-</xsl:text>
-          <xsl:text>    send_blob([
-</xsl:text>
-          <xsl:text>        new Uint8Array([0]),  /* setval = 0 */
-</xsl:text>
-          <xsl:text>        new Uint32Array([index]),
-</xsl:text>
-          <xsl:text>        tobinary(value)]);
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>    // DON'T DO THAT unless read_iterator in svghmi.c modifies wbuf as well, not only rbuf
-</xsl:text>
-          <xsl:text>    // cache[index] = value;
-</xsl:text>
-          <xsl:text>};
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>function apply_hmi_value(index, new_val) {
-</xsl:text>
-          <xsl:text>    let old_val = cache[index]
-</xsl:text>
-          <xsl:text>    if(new_val != undefined &amp;&amp; old_val != new_val)
-</xsl:text>
-          <xsl:text>        send_hmi_value(index, new_val);
-</xsl:text>
-          <xsl:text>    return new_val;
-</xsl:text>
-          <xsl:text>}
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>quotes = {"'":null, '"':null};
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>function change_hmi_value(index, opstr) {
-</xsl:text>
-          <xsl:text>    let op = opstr[0];
-</xsl:text>
-          <xsl:text>    let given_val;
-</xsl:text>
-          <xsl:text>    if(opstr.length &lt; 2) 
-</xsl:text>
-          <xsl:text>        return undefined; // TODO raise
-</xsl:text>
-          <xsl:text>    if(opstr[1] in quotes){
-</xsl:text>
-          <xsl:text>        if(opstr.length &lt; 3) 
-</xsl:text>
-          <xsl:text>            return undefined; // TODO raise
-</xsl:text>
-          <xsl:text>        if(opstr[opstr.length-1] == opstr[1]){
-</xsl:text>
-          <xsl:text>            given_val = opstr.slice(2,opstr.length-1);
-</xsl:text>
-          <xsl:text>        }
-</xsl:text>
-          <xsl:text>    } else {
-</xsl:text>
-          <xsl:text>        given_val = Number(opstr.slice(1));
-</xsl:text>
-          <xsl:text>    }
-</xsl:text>
-          <xsl:text>    let old_val = cache[index];
-</xsl:text>
-          <xsl:text>    let new_val;
-</xsl:text>
-          <xsl:text>    switch(op){
-</xsl:text>
-          <xsl:text>      case "=":
-</xsl:text>
-          <xsl:text>        new_val = given_val;
-</xsl:text>
-          <xsl:text>        break;
-</xsl:text>
-          <xsl:text>      case "+":
-</xsl:text>
-          <xsl:text>        new_val = old_val + given_val;
-</xsl:text>
-          <xsl:text>        break;
-</xsl:text>
-          <xsl:text>      case "-":
-</xsl:text>
-          <xsl:text>        new_val = old_val - given_val;
-</xsl:text>
-          <xsl:text>        break;
-</xsl:text>
-          <xsl:text>      case "*":
-</xsl:text>
-          <xsl:text>        new_val = old_val * given_val;
-</xsl:text>
-          <xsl:text>        break;
-</xsl:text>
-          <xsl:text>      case "/":
-</xsl:text>
-          <xsl:text>        new_val = old_val / given_val;
-</xsl:text>
-          <xsl:text>        break;
-</xsl:text>
-          <xsl:text>    }
-</xsl:text>
-          <xsl:text>    if(new_val != undefined &amp;&amp; old_val != new_val)
-</xsl:text>
-          <xsl:text>        send_hmi_value(index, new_val);
-</xsl:text>
-          <xsl:text>    // TODO else raise
-</xsl:text>
-          <xsl:text>    return new_val;
-</xsl:text>
-          <xsl:text>}
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>var current_visible_page;
-</xsl:text>
-          <xsl:text>var current_subscribed_page;
-</xsl:text>
-          <xsl:text>var current_page_index;
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>function prepare_svg() {
-</xsl:text>
-          <xsl:text>    for(let eltid in detachable_elements){
-</xsl:text>
-          <xsl:text>        let [element,parent] = detachable_elements[eltid];
-</xsl:text>
-          <xsl:text>        parent.removeChild(element);
-</xsl:text>
-          <xsl:text>    }
-</xsl:text>
-          <xsl:text>};
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>function switch_page(page_name, page_index) {
-</xsl:text>
-          <xsl:text>    if(current_subscribed_page != current_visible_page){
-</xsl:text>
-          <xsl:text>        /* page switch already going */
-</xsl:text>
-          <xsl:text>        /* TODO LOG ERROR */
-</xsl:text>
-          <xsl:text>        return false;
-</xsl:text>
-          <xsl:text>    }
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>    if(page_name == undefined)
-</xsl:text>
-          <xsl:text>        page_name = current_subscribed_page;
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>    let old_desc = page_desc[current_subscribed_page];
-</xsl:text>
-          <xsl:text>    let new_desc = page_desc[page_name];
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>    if(new_desc == undefined){
-</xsl:text>
-          <xsl:text>        /* TODO LOG ERROR */
-</xsl:text>
-          <xsl:text>        return false;
-</xsl:text>
-          <xsl:text>    }
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>    if(page_index == undefined){
-</xsl:text>
-          <xsl:text>        page_index = new_desc.page_index;
-</xsl:text>
-          <xsl:text>    }
-</xsl:text>
-          <xsl:text>
-</xsl:text>
-          <xsl:text>    if(old_desc){
-</xsl:text>
-          <xsl:text>        old_desc.absolute_widgets.map(w=&gt;w.unsub());
-</xsl:text>
-          <xsl:text>        old_desc.relative_widgets.map(w=&gt;w.unsub());
-</xsl:text>
-          <xsl:text>    }
-</xsl:text>
-          <xsl:text>    new_desc.absolute_widgets.map(w=&gt;w.sub());
-</xsl:text>
           <xsl:text>    var new_offset = page_index == undefined ? 0 : page_index - new_desc.page_index;
 </xsl:text>
-          <xsl:text>    new_desc.relative_widgets.map(w=&gt;w.sub(new_offset));
+          <xsl:text>    new_desc.widgets.map(([widget,relativeness])=&gt;widget.sub(new_offset,relativeness));
 </xsl:text>
           <xsl:text>
 </xsl:text>
--- a/svghmi/svghmi.js	Wed Aug 05 15:20:10 2020 +0200
+++ b/svghmi/svghmi.js	Mon Aug 10 11:30:06 2020 +0200
@@ -4,25 +4,6 @@
 var updates = {};
 var need_cache_apply = []; 
 
-function dispatch_value_to_widget(widget, index, value, oldval) {
-    try {
-        let idx = widget.offset ? index - widget.offset : index;
-        let idxidx = widget.indexes.indexOf(idx);
-        let d = widget.dispatch;
-        if(typeof(d) == "function" && idxidx == 0){
-            d.call(widget, value, oldval);
-        }
-        else if(typeof(d) == "object" && d.length >= idxidx){
-            d[idxidx].call(widget, value, oldval);
-        }
-        /* else dispatch_0, ..., dispatch_n ? */
-        /*else {
-            throw new Error("Dunno how to dispatch to widget at index = " + index);
-        }*/
-    } catch(err) {
-        console.log(err);
-    }
-}
 
 function dispatch_value(index, value) {
     let widgets = subscribers[index];
@@ -32,7 +13,7 @@
 
     if(widgets.size > 0) {
         for(let widget of widgets){
-            dispatch_value_to_widget(widget, index, value, oldval);
+            widget.new_hmi_value(index, value, oldval);
         }
     }
 };
@@ -190,7 +171,7 @@
     /* type: "Watchdog", */
     frequency: 1,
     indexes: [heartbeat_index],
-    dispatch: function(value) {
+    new_hmi_value: function(index, value, oldval) {
         apply_hmi_value(heartbeat_index, value+1);
     }
 });
@@ -323,12 +304,10 @@
     }
 
     if(old_desc){
-        old_desc.absolute_widgets.map(w=>w.unsub());
-        old_desc.relative_widgets.map(w=>w.unsub());
-    }
-    new_desc.absolute_widgets.map(w=>w.sub());
+        old_desc.widgets.map(([widget,relativeness])=>widget.unsub());
+    }
     var new_offset = page_index == undefined ? 0 : page_index - new_desc.page_index;
-    new_desc.relative_widgets.map(w=>w.sub(new_offset));
+    new_desc.widgets.map(([widget,relativeness])=>widget.sub(new_offset,relativeness));
 
     update_subscriptions();
 
--- a/svghmi/widget_button.ysl2	Wed Aug 05 15:20:10 2020 +0200
+++ b/svghmi/widget_button.ysl2	Mon Aug 10 11:30:06 2020 +0200
@@ -13,7 +13,7 @@
                  this.active_elt.setAttribute("style", this.active_style);
                  this.inactive_elt.setAttribute("style", "display:none");
              }
-             change_hmi_value(this.indexes[0], "=1");
+             this.apply_hmi_value(0, 1);
          }
 
          on_mouse_up(evt) {
@@ -21,7 +21,7 @@
                  this.active_elt.setAttribute("style", "display:none");
                  this.inactive_elt.setAttribute("style", this.inactive_style);
              }
-             change_hmi_value(this.indexes[0], "=0");
+             this.apply_hmi_value(0, 0);
          }
 
          init() {
@@ -33,8 +33,8 @@
                 this.inactive_elt.setAttribute("style", this.inactive_style);
             }
 
-            this.element.setAttribute("onmousedown", "hmi_widgets['«$hmi_element/@id»'].on_mouse_down(evt)");
-            this.element.setAttribute("onmouseup", "hmi_widgets['«$hmi_element/@id»'].on_mouse_up(evt)");
+            this.element.setAttribute("onmousedown", "hmi_widgets["+this.element_id+"].on_mouse_down(evt)");
+            this.element.setAttribute("onmouseup", "hmi_widgets["+this.element_id+"].on_mouse_up(evt)");
          }
     }
     ||
--- a/svghmi/widget_circularslider.ysl2	Wed Aug 05 15:20:10 2020 +0200
+++ b/svghmi/widget_circularslider.ysl2	Mon Aug 10 11:30:06 2020 +0200
@@ -89,7 +89,7 @@
                 this.handle_position(svg_dist);
                 if(this.value_elt)
                     this.value_elt.textContent = String(Math.ceil(svg_dist));
-                change_hmi_value(this.indexes[0], "="+Math.ceil(svg_dist));
+                this.apply_hmi_value(0, Math.ceil(svg_dist));
 
                 //reset timer
                 this.enTimer = false;
@@ -156,4 +156,4 @@
     labels("handle range");
     optional_labels("value min max");
     |,
-}
\ No newline at end of file
+}
--- a/svghmi/widget_display.ysl2	Wed Aug 05 15:20:10 2020 +0200
+++ b/svghmi/widget_display.ysl2	Mon Aug 10 11:30:06 2020 +0200
@@ -5,8 +5,10 @@
     ||
     class DisplayWidget extends Widget{
         frequency = 5;
-        dispatch(value) {
-            this.element.textContent = String(value);
+        dispatch(value, oldval, index) {
+            this.fields[index] = value;    
+            console.log(value, index);
+            this.element.textContent = this.args.length == 1 ? vsprintf(this.args[0],this.fields) : this.fields.join(' ');
         }
     }
     ||
@@ -15,4 +17,242 @@
     param "hmi_element";
     if "$hmi_element[not(self::svg:text)]"
         error > Display Widget id="«$hmi_element/@id»" is not a svg::text element
+
+    |     fields: [],
 }
+
+emit "preamble:display"
+||
+/* https://github.com/alexei/sprintf.js/blob/master/src/sprintf.js */
+/* global window, exports, define */
+
+!function() {
+    'use strict'
+
+    var re = {
+        not_string: /[^s]/,
+        not_bool: /[^t]/,
+        not_type: /[^T]/,
+        not_primitive: /[^v]/,
+        number: /[diefg]/,
+        numeric_arg: /[bcdiefguxX]/,
+        json: /[j]/,
+        not_json: /[^j]/,
+        text: /^[^\x25]+/,
+        modulo: /^\x25{2}/,
+        placeholder: /^\x25(?:([1-9]\d*)\$|\(([^)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-gijostTuvxX])/,
+        key: /^([a-z_][a-z_\d]*)/i,
+        key_access: /^\.([a-z_][a-z_\d]*)/i,
+        index_access: /^\[(\d+)\]/,
+        sign: /^[+-]/
+    }
+
+    function sprintf(key) {
+        // `arguments` is not an array, but should be fine for this call
+        return sprintf_format(sprintf_parse(key), arguments)
+    }
+
+    function vsprintf(fmt, argv) {
+        return sprintf.apply(null, [fmt].concat(argv || []))
+    }
+
+    function sprintf_format(parse_tree, argv) {
+        var cursor = 1, tree_length = parse_tree.length, arg, output = '', i, k, ph, pad, pad_character, pad_length, is_positive, sign
+        for (i = 0; i < tree_length; i++) {
+            if (typeof parse_tree[i] === 'string') {
+                output += parse_tree[i]
+            }
+            else if (typeof parse_tree[i] === 'object') {
+                ph = parse_tree[i] // convenience purposes only
+                if (ph.keys) { // keyword argument
+                    arg = argv[cursor]
+                    for (k = 0; k < ph.keys.length; k++) {
+                        if (arg == undefined) {
+                            throw new Error(sprintf('[sprintf] Cannot access property "%s" of undefined value "%s"', ph.keys[k], ph.keys[k-1]))
+                        }
+                        arg = arg[ph.keys[k]]
+                    }
+                }
+                else if (ph.param_no) { // positional argument (explicit)
+                    arg = argv[ph.param_no]
+                }
+                else { // positional argument (implicit)
+                    arg = argv[cursor++]
+                }
+
+                if (re.not_type.test(ph.type) && re.not_primitive.test(ph.type) && arg instanceof Function) {
+                    arg = arg()
+                }
+
+                if (re.numeric_arg.test(ph.type) && (typeof arg !== 'number' && isNaN(arg))) {
+                    throw new TypeError(sprintf('[sprintf] expecting number but found %T', arg))
+                }
+
+                if (re.number.test(ph.type)) {
+                    is_positive = arg >= 0
+                }
+
+                switch (ph.type) {
+                    case 'b':
+                        arg = parseInt(arg, 10).toString(2)
+                        break
+                    case 'c':
+                        arg = String.fromCharCode(parseInt(arg, 10))
+                        break
+                    case 'd':
+                    case 'i':
+                        arg = parseInt(arg, 10)
+                        break
+                    case 'j':
+                        arg = JSON.stringify(arg, null, ph.width ? parseInt(ph.width) : 0)
+                        break
+                    case 'e':
+                        arg = ph.precision ? parseFloat(arg).toExponential(ph.precision) : parseFloat(arg).toExponential()
+                        break
+                    case 'f':
+                        arg = ph.precision ? parseFloat(arg).toFixed(ph.precision) : parseFloat(arg)
+                        break
+                    case 'g':
+                        arg = ph.precision ? String(Number(arg.toPrecision(ph.precision))) : parseFloat(arg)
+                        break
+                    case 'o':
+                        arg = (parseInt(arg, 10) >>> 0).toString(8)
+                        break
+                    case 's':
+                        arg = String(arg)
+                        arg = (ph.precision ? arg.substring(0, ph.precision) : arg)
+                        break
+                    case 't':
+                        arg = String(!!arg)
+                        arg = (ph.precision ? arg.substring(0, ph.precision) : arg)
+                        break
+                    case 'T':
+                        arg = Object.prototype.toString.call(arg).slice(8, -1).toLowerCase()
+                        arg = (ph.precision ? arg.substring(0, ph.precision) : arg)
+                        break
+                    case 'u':
+                        arg = parseInt(arg, 10) >>> 0
+                        break
+                    case 'v':
+                        arg = arg.valueOf()
+                        arg = (ph.precision ? arg.substring(0, ph.precision) : arg)
+                        break
+                    case 'x':
+                        arg = (parseInt(arg, 10) >>> 0).toString(16)
+                        break
+                    case 'X':
+                        arg = (parseInt(arg, 10) >>> 0).toString(16).toUpperCase()
+                        break
+                }
+                if (re.json.test(ph.type)) {
+                    output += arg
+                }
+                else {
+                    if (re.number.test(ph.type) && (!is_positive || ph.sign)) {
+                        sign = is_positive ? '+' : '-'
+                        arg = arg.toString().replace(re.sign, '')
+                    }
+                    else {
+                        sign = ''
+                    }
+                    pad_character = ph.pad_char ? ph.pad_char === '0' ? '0' : ph.pad_char.charAt(1) : ' '
+                    pad_length = ph.width - (sign + arg).length
+                    pad = ph.width ? (pad_length > 0 ? pad_character.repeat(pad_length) : '') : ''
+                    output += ph.align ? sign + arg + pad : (pad_character === '0' ? sign + pad + arg : pad + sign + arg)
+                }
+            }
+        }
+        return output
+    }
+
+    var sprintf_cache = Object.create(null)
+
+    function sprintf_parse(fmt) {
+        if (sprintf_cache[fmt]) {
+            return sprintf_cache[fmt]
+        }
+
+        var _fmt = fmt, match, parse_tree = [], arg_names = 0
+        while (_fmt) {
+            if ((match = re.text.exec(_fmt)) !== null) {
+                parse_tree.push(match[0])
+            }
+            else if ((match = re.modulo.exec(_fmt)) !== null) {
+                parse_tree.push('%')
+            }
+            else if ((match = re.placeholder.exec(_fmt)) !== null) {
+                if (match[2]) {
+                    arg_names |= 1
+                    var field_list = [], replacement_field = match[2], field_match = []
+                    if ((field_match = re.key.exec(replacement_field)) !== null) {
+                        field_list.push(field_match[1])
+                        while ((replacement_field = replacement_field.substring(field_match[0].length)) !== '') {
+                            if ((field_match = re.key_access.exec(replacement_field)) !== null) {
+                                field_list.push(field_match[1])
+                            }
+                            else if ((field_match = re.index_access.exec(replacement_field)) !== null) {
+                                field_list.push(field_match[1])
+                            }
+                            else {
+                                throw new SyntaxError('[sprintf] failed to parse named argument key')
+                            }
+                        }
+                    }
+                    else {
+                        throw new SyntaxError('[sprintf] failed to parse named argument key')
+                    }
+                    match[2] = field_list
+                }
+                else {
+                    arg_names |= 2
+                }
+                if (arg_names === 3) {
+                    throw new Error('[sprintf] mixing positional and named placeholders is not (yet) supported')
+                }
+
+                parse_tree.push(
+                    {
+                        placeholder: match[0],
+                        param_no:    match[1],
+                        keys:        match[2],
+                        sign:        match[3],
+                        pad_char:    match[4],
+                        align:       match[5],
+                        width:       match[6],
+                        precision:   match[7],
+                        type:        match[8]
+                    }
+                )
+            }
+            else {
+                throw new SyntaxError('[sprintf] unexpected placeholder')
+            }
+            _fmt = _fmt.substring(match[0].length)
+        }
+        return sprintf_cache[fmt] = parse_tree
+    }
+
+    /**
+     * export to either browser or node.js
+     */
+    /* eslint-disable quote-props */
+    if (typeof exports !== 'undefined') {
+        exports['sprintf'] = sprintf
+        exports['vsprintf'] = vsprintf
+    }
+    if (typeof window !== 'undefined') {
+        window['sprintf'] = sprintf
+        window['vsprintf'] = vsprintf
+
+        if (typeof define === 'function' && define['amd']) {
+            define(function() {
+                return {
+                    'sprintf': sprintf,
+                    'vsprintf': vsprintf
+                }
+            })
+        }
+    }
+    /* eslint-enable quote-props */
+}(); // eslint-disable-line    
+||
--- a/svghmi/widget_foreach.ysl2	Wed Aug 05 15:20:10 2020 +0200
+++ b/svghmi/widget_foreach.ysl2	Mon Aug 10 11:30:06 2020 +0200
@@ -2,6 +2,9 @@
 template "widget[@type='ForEach']", mode="widget_defs" {
     param "hmi_element";
 
+    if "count(path) != 1" error > ForEach widget «$hmi_element/@id» must have one HMI path given.
+    if "count(arg) != 1" error > ForEach widget «$hmi_element/@id» must have one argument given : a class name.
+
     const "class","arg[1]/@value";
 
     const "base_path","path/@value";
@@ -49,34 +52,48 @@
 template "widget[@type='ForEach']", mode="widget_class"
 ||
 class ForEachWidget extends Widget{
-    unsub(){
+
+    unsub_items(){
         for(let item of this.items){
             for(let widget of item) {
                 widget.unsub();
             }
         }
-        this.offset = 0;
     }
 
-    foreach_widgets_do(todo){
+    unsub(){
+        this.unsub_items();
+        this.offset = 0;
+        this.relativeness = undefined;
+    }
+
+    sub_items(){
         for(let i = 0; i < this.items.length; i++) {
             let item = this.items[i];
             let orig_item_index = this.index_pool[i];
             let item_index = this.index_pool[i+this.item_offset];
             let item_index_offset = item_index - orig_item_index;
+            if(this.relativeness[0])
+                item_index_offset += this.offset;
             for(let widget of item) {
-                todo(widget).call(widget, this.offset + item_index_offset);
+                /* all variables of all widgets in a ForEach are all relative. 
+                   Really.
+
+                   TODO: allow absolute variables in ForEach widgets
+                */
+                widget.sub(item_index_offset, widget.indexes.map(_=>true));
             }
         }
     }
 
-    sub(new_offset=0){
+    sub(new_offset=0, relativeness=[]){
         this.offset = new_offset;
-        this.foreach_widgets_do(w=>w.sub);
+        this.relativeness = relativeness;
+        this.sub_items();
     }
 
     apply_cache() {
-        this.foreach_widgets_do(w=>w.apply_cache);
+        this.items.forEach(item=>item.forEach(widget=>widget.apply_cache()));
     }
 
     on_click(opstr, evt) {
@@ -93,8 +110,8 @@
                 new_item_offset = 0;
         }
         this.item_offset = new_item_offset;
-        this.unsub();
-        this.sub(this.offset);
+        this.unsub_items();
+        this.sub_items();
         update_subscriptions();
         need_cache_apply.push(this);
         jumps_need_update = true;
--- a/svghmi/widget_multistate.ysl2	Wed Aug 05 15:20:10 2020 +0200
+++ b/svghmi/widget_multistate.ysl2	Mon Aug 10 11:30:06 2020 +0200
@@ -35,7 +35,7 @@
             }
 
             //post value to plc
-            change_hmi_value(this.indexes[0], "="+this.state);
+            this.apply_hmi_value(0, this.state);
         }
 
         init() {
@@ -57,4 +57,4 @@
     |         }`if "position()!=last()" > ,`
     }
     |     ],
-}
\ No newline at end of file
+}
--- a/svghmi/widget_slider.ysl2	Wed Aug 05 15:20:10 2020 +0200
+++ b/svghmi/widget_slider.ysl2	Mon Aug 10 11:30:06 2020 +0200
@@ -80,7 +80,7 @@
                 //redraw handle
                 this.handle_position(svg_dist=(html_dist/range_length)*this.range[1]);
                 this.value_elt.textContent = String(Math.ceil(svg_dist));
-                change_hmi_value(this.indexes[0], "="+Math.ceil(svg_dist));
+                this.apply_hmi_value(0, Math.ceil(svg_dist));
                 //reset timer
                 this.enTimer = false;
                 setTimeout("{hmi_widgets['"+this.element_id+"'].enTimer = true;}", 100);
@@ -128,4 +128,4 @@
     labels("handle range");
     optional_labels("value min max");
     |,
-}
\ No newline at end of file
+}
--- a/svghmi/widget_tooglebutton.ysl2	Wed Aug 05 15:20:10 2020 +0200
+++ b/svghmi/widget_tooglebutton.ysl2	Mon Aug 10 11:30:06 2020 +0200
@@ -23,7 +23,7 @@
         }
 
         on_click(evt) {
-            change_hmi_value(this.indexes[0], "="+this.state);
+            this.apply_hmi_value(0, this.state);
         }
 
         init() {
--- a/svghmi/widgets_common.ysl2	Wed Aug 05 15:20:10 2020 +0200
+++ b/svghmi/widgets_common.ysl2	Mon Aug 10 11:30:06 2020 +0200
@@ -78,20 +78,28 @@
 
         unsub(){
             /* remove subsribers */
-            if(!this.unsubscribable) for(let index of this.indexes){
-                let idx = index + this.offset;
-                subscribers[idx].delete(this);
-            }
+            if(!this.unsubscribable)
+                for(let i = 0; i < this.indexes.length; i++) {
+                    let index = this.indexes[i];
+                    if(this.relativeness[i])
+                        index += this.offset;
+                    subscribers[index].delete(this);
+                }
             this.offset = 0;
-        }
-
-        sub(new_offset=0){
-            /* set the offset because relative */
+            this.relativeness = undefined;
+        }
+
+        sub(new_offset=0, relativeness){
             this.offset = new_offset;
+            this.relativeness = relativeness;
             /* add this's subsribers */
-            if(!this.unsubscribable) for(let index of this.indexes){
-                subscribers[index + new_offset].add(this);
-            }
+            if(!this.unsubscribable)
+                for(let i = 0; i < this.indexes.length; i++) {
+                    let index = this.indexes[i];
+                    if(relativeness[i])
+                        index += new_offset;
+                    subscribers[index].add(this);
+                }
             need_cache_apply.push(this); 
         }
 
@@ -101,13 +109,13 @@
                 let realindex = index+this.offset;
                 let cached_val = cache[realindex];
                 if(cached_val != undefined)
-                    dispatch_value_to_widget(this, realindex, cached_val, cached_val);
+                    this.new_hmi_value(realindex, cached_val, cached_val);
             }
         }
 
         get_idx(index) {
              let orig = this.indexes[index];
-             return this.offset ? orig + this.offset : orig;
+             return this.relativeness[index] ? orig + this.offset : orig;
         }
         change_hmi_value(index,opstr) {
             return change_hmi_value(this.get_idx(index), opstr);
@@ -116,11 +124,39 @@
         apply_hmi_value(index, new_val) {
             return apply_hmi_value(this.get_idx(0), new_val);
         }
+
+        new_hmi_value(index, value, oldval) {
+            try {
+                // TODO avoid searching, store index at sub()
+                for(let i = 0; i < this.indexes.length; i++) {
+                    let refindex = this.indexes[i];
+                    if(this.relativeness[i])
+                        refindex += this.offset;
+
+                    if(index == refindex) {
+                        let d = this.dispatch;
+                        if(typeof(d) == "function"){
+                            d.call(this, value, oldval, i);
+                        }
+                        else if(typeof(d) == "object"){
+                            d[i].call(this, value, oldval);
+                        }
+                        /* else dispatch_0, ..., dispatch_n ? */
+                        /*else {
+                            throw new Error("Dunno how to dispatch to widget at index = " + index);
+                        }*/
+                        break;
+                    }
+                }
+            } catch(err) {
+                console.log(err);
+            }
+        }
     }
     ||
 }
 
-emit "preamble:hmi-classes" {
+emit "declarations:hmi-classes" {
     const "used_widget_types", "func:unique_types($parsed_widgets/widget)";
     apply "$used_widget_types", mode="widget_class";
 }
@@ -135,7 +171,7 @@
 const "excluded_types", "str:split('Page Lang')";
 const "excluded_ids","$parsed_widgets/widget[not(@type = $excluded_types)]/@id";
 
-emit "preamble:hmi-elements" {
+emit "declarations:hmi-elements" {
     | var hmi_widgets = {
     apply  "$hmi_elements[@id = $excluded_ids]", mode="hmi_widgets";
     | }
--- a/tests/svghmi/svghmi_0@svghmi/svghmi.svg	Wed Aug 05 15:20:10 2020 +0200
+++ b/tests/svghmi/svghmi_0@svghmi/svghmi.svg	Mon Aug 10 11:30:06 2020 +0200
@@ -167,16 +167,16 @@
      inkscape:pageopacity="0"
      inkscape:pageshadow="2"
      inkscape:document-units="px"
-     inkscape:current-layer="g6077"
+     inkscape:current-layer="hmi0"
      showgrid="false"
      units="px"
-     inkscape:zoom="1.4142136"
-     inkscape:cx="1970.3359"
-     inkscape:cy="368.15797"
+     inkscape:zoom="0.7071068"
+     inkscape:cx="543.82641"
+     inkscape:cy="218.7845"
      inkscape:window-width="2419"
      inkscape:window-height="1266"
-     inkscape:window-x="1197"
-     inkscape:window-y="563"
+     inkscape:window-x="1405"
+     inkscape:window-y="37"
      inkscape:window-maximized="0"
      showguides="true"
      inkscape:guide-bbox="true" />
@@ -4200,4 +4200,28 @@
            sodipodi:role="line">-1</tspan></text>
     </g>
   </g>
+  <text
+     xml:space="preserve"
+     style="font-style:normal;font-weight:normal;font-size:59.01374435px;line-height:125%;font-family:sans-serif;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#82ff77;fill-opacity:1;stroke:none;stroke-width:0.3688359px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+     x="729.9715"
+     y="539.24927"
+     id="text995-6"
+     inkscape:label="HMI:Display:Ploc %d (%d) grmbl !@/PUMP0/PRESSURE@/PUMP0/SLOTH"><tspan
+       sodipodi:role="line"
+       id="tspan993-3"
+       x="729.9715"
+       y="539.24927"
+       style="text-align:center;text-anchor:middle;fill:#82ff77;fill-opacity:1;stroke-width:0.3688359px">8888</tspan></text>
+  <text
+     id="text831-1"
+     y="477.76758"
+     x="621.62634"
+     style="font-style:normal;font-weight:normal;font-size:25.4761734px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;display:inline;fill:#008000;fill-opacity:1;stroke:none;stroke-width:0.63690436px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+     xml:space="preserve"
+     inkscape:label="actual_label"><tspan
+       y="477.76758"
+       x="621.62634"
+       id="tspan829-7"
+       sodipodi:role="line"
+       style="stroke-width:0.63690436px">Multiple variables</tspan></text>
 </svg>