mergeInto(LibraryManager.library, {
  // ─────────────────────────────────────────────────────────────
  //  VIDÉO HTML5 AU-DESSUS DU CANVAS UNITY
  // ─────────────────────────────────────────────────────────────
  WVO_Create: function (urlPtr, fitPtr, loop, muted) {
    var url = UTF8ToString(urlPtr);
    var fit = UTF8ToString(fitPtr);

    // Calque vidéo (au-dessus du canvas)
    var layer = document.getElementById('video-overlay') || (function () {
      var c = document.getElementById('unity-container');
      var o = document.createElement('div');
      o.id = 'video-overlay';
      o.style.position = 'absolute';
      o.style.inset = '0';
      o.style.zIndex = '5';
      (c || document.body).appendChild(o);
      return o;
    })();

    // Détruire l’ancienne vidéo si présente
    if (Module.WVO_video) {
      try { Module.WVO_video.remove(); } catch (e) {}
      Module.WVO_video = null;
    }

    // Créer l’élément <video>
    var vid = document.createElement('video');
    Object.assign(vid, {
      src: url,
      playsInline: true,
      autoplay: false,
      controls: false,
      loop: !!loop,
      muted: !!muted,
      preload: 'auto',
      crossOrigin: 'anonymous'
    });
    Object.assign(vid.style, {
      position: 'absolute',
      inset: '0',
      width: '100%',
      height: '100%',
      objectFit: fit,
      zIndex: '5',
      pointerEvents: 'none',
      background: '#000'
    });

    layer.appendChild(vid);
    vid.addEventListener('loadedmetadata', function () {
      console.log('[WVO] loadedmetadata', vid.videoWidth + 'x' + vid.videoHeight);
    });
    vid.addEventListener('error', function (e) {
      console.warn('[WVO] video error', e, vid.error);
    });

    // Callback fin de vidéo -> Unity (optionnel)
    // L'objectif est de permettre au code C# de terminer proprement un step vidéo en WebGL
    // sans RenderTexture (prévention abort(OOM)).
    try {
      if (Module.WVO_endGo && Module.WVO_endMethod) {
        var handler = function () {
          try {
            if (typeof SendMessage === 'function') {
              SendMessage(Module.WVO_endGo, Module.WVO_endMethod, "");
            } else if (Module && typeof Module.SendMessage === 'function') {
              Module.SendMessage(Module.WVO_endGo, Module.WVO_endMethod, "");
            } else {
              console.warn('[WVO] ended: SendMessage unavailable');
            }
          } catch (e) {
            console.warn('[WVO] ended callback failed', e);
          }
        };
        vid._wvoEndedHandler = handler;
        vid.addEventListener('ended', handler);
      }
    } catch (e) {
      console.warn('[WVO] cannot attach ended handler', e);
    }

    Module.WVO_video = vid;
  },

  WVO_Play: function () {
    var v = Module.WVO_video;
    if (!v) return;
    var p = v.play();
    if (p && p.catch) p.catch(function (e) { console.warn('[WVO] play blocked', e); });
  },

  WVO_Dispose: function () {
    var v = Module.WVO_video;
    if (v) {
      try {
        if (v._wvoEndedHandler) {
          v.removeEventListener('ended', v._wvoEndedHandler);
          v._wvoEndedHandler = null;
        }
      } catch (e) {}
      try { v.remove(); } catch (e) {}
      Module.WVO_video = null;
    }

    // Supprimer aussi le bouton "skip" s'il existe
    try {
      var btn = document.getElementById('wvo-skip-btn');
      if (btn) btn.remove();
    } catch (e) {}
  },

  // Définit un callback fin de vidéo (utilisé par MapFullscreenVideo en WebGL)
  WVO_SetOnEnded: function (goPtr, methodPtr) {
    Module.WVO_endGo = UTF8ToString(goPtr || 0) || '';
    Module.WVO_endMethod = UTF8ToString(methodPtr || 0) || '';
  },

  // ─────────────────────────────────────────────────────────────
  //  BOUTON "PASSER" (DOM) AU-DESSUS DE LA VIDÉO
  // ─────────────────────────────────────────────────────────────
  WVO_ShowSkipButton: function (labelPtr, goPtr, methodPtr) {
    var label = UTF8ToString(labelPtr || 0) || 'PASSER';
    var go = UTF8ToString(goPtr || 0) || '';
    var method = UTF8ToString(methodPtr || 0) || '';

    // Conteneur overlay vidéo
    var layer = document.getElementById('video-overlay');
    if (!layer) {
      // si pas encore créé, le créer comme WVO_Create
      var c = document.getElementById('unity-container');
      layer = document.createElement('div');
      layer.id = 'video-overlay';
      layer.style.position = 'absolute';
      layer.style.inset = '0';
      layer.style.zIndex = '5';
      (c || document.body).appendChild(layer);
    }

    // Bouton unique
    var btn = document.getElementById('wvo-skip-btn');
    if (!btn) {
      btn = document.createElement('button');
      btn.id = 'wvo-skip-btn';
      layer.appendChild(btn);
    }

    btn.textContent = label;

    // Style "chip" discret en bas à droite
    Object.assign(btn.style, {
      position: 'absolute',
      right: '24px',
      bottom: '24px',
      width: '200px',
      height: '60px',
      border: '0',
      borderRadius: '14px',
      background: 'rgba(0,0,0,0.70)',
      color: '#fff',
      fontSize: '20px',
      fontFamily: 'system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Arial, sans-serif',
      cursor: 'pointer',
      zIndex: '20',
      pointerEvents: 'auto',
      boxShadow: '0 8px 20px rgba(0,0,0,0.25)'
    });

    // Reset handler
    btn.onclick = null;
    btn.onpointerdown = null;

    // Click -> SendMessage Unity
    var handler = function () {
      try {
        if (!go || !method) return;
        if (typeof SendMessage === 'function') {
          SendMessage(go, method, "");
        } else if (Module && typeof Module.SendMessage === 'function') {
          Module.SendMessage(go, method, "");
        } else {
          console.warn('[WVO] skip: SendMessage unavailable');
        }
      } catch (e) {
        console.warn('[WVO] skip callback failed', e);
      }
    };
    btn.onclick = handler;
    btn.onpointerdown = function (e) { e.preventDefault(); e.stopPropagation(); };
  },

  WVO_HideSkipButton: function () {
    try {
      var btn = document.getElementById('wvo-skip-btn');
      if (btn) btn.remove();
    } catch (e) {}
  },

  WVO_SetObjectFit: function (fitPtr) {
    var v = Module.WVO_video;
    if (v) v.style.objectFit = UTF8ToString(fitPtr);
  },

  // ─────────────────────────────────────────────────────────────
  //  OVERLAY TEXTE (DOM) — versions "brut" et "HTML"
  // ─────────────────────────────────────────────────────────────
  UI_SetSpeaker: function (ptr) {
    var s = UTF8ToString(ptr || 0) || "";
    var el = document.getElementById('dom-speaker');
    if (el) el.textContent = s;
  },
  UI_SetSubtitle: function (ptr) {
    var s = UTF8ToString(ptr || 0) || "";
    var el = document.getElementById('dom-subtitle');
    if (el) el.textContent = s;
  },
  UI_SetSpeakerHtml: function (ptr) {
    var s = UTF8ToString(ptr || 0) || "";
    var el = document.getElementById('dom-speaker');
    if (el) el.innerHTML = s;
    // Afficher/cacher le wrap selon le contenu
    var wrap = document.querySelector('.subtitle-wrap');
    if (wrap) {
      var sub = document.getElementById('dom-subtitle');
      var hasContent = s || (sub && sub.innerHTML);
      if (hasContent) {
        wrap.classList.add('has-content');
      } else {
        wrap.classList.remove('has-content');
      }
    }
  },
  UI_SetSubtitleHtml: function (ptr) {
    var s = UTF8ToString(ptr || 0) || "";
    var el = document.getElementById('dom-subtitle');
    if (el) el.innerHTML = s;
    // Afficher/cacher le wrap selon le contenu
    var wrap = document.querySelector('.subtitle-wrap');
    if (wrap) {
      var spk = document.getElementById('dom-speaker');
      var hasContent = s || (spk && spk.innerHTML);
      if (hasContent) {
        wrap.classList.add('has-content');
      } else {
        wrap.classList.remove('has-content');
      }
    }
  },
  UI_SetVisible: function (visible) {
    var ov = document.getElementById('dom-overlay');
    var wrap = document.querySelector('.subtitle-wrap');
    if (ov) {
      if (visible) {
        ov.classList.add('visible');
      } else {
        ov.classList.remove('visible');
        // Vider aussi les textes pour éviter qu'ils restent affichés
        var spk = document.getElementById('dom-speaker');
        var sub = document.getElementById('dom-subtitle');
        if (spk) spk.innerHTML = '';
        if (sub) sub.innerHTML = '';
        // Cacher le wrap
        if (wrap) wrap.classList.remove('has-content');
      }
    }
  },

  // ─────────────────────────────────────────────────────────────
  //  OVERLAY IMAGE (DOM) — robust fallback + cache handling
  // ─────────────────────────────────────────────────────────────
  UI_ShowOverlayImage: function (urlPtr, fadeMs, skipFadeIfSame) {
    var input = UTF8ToString(urlPtr || 0) || "";
    var wrap  = document.getElementById('img-overlay');
    var img   = document.getElementById('dom-img');
    if (!wrap || !img) return;

    // garantir visibilité/z-index
    wrap.style.display = '';
    wrap.style.zIndex  = '12';
    img.crossOrigin    = 'anonymous';

    // base absolue (chemin de l’index)
    function baseOf(href){ return (href||location.href).replace(/[?#].*$/,'').replace(/\/[^/]*$/,'/'); }
    var ROOT = baseOf(document.baseURI);
    function abs(u){ return /^https?:\/\//i.test(u) ? u : (ROOT + u); }

    // Construire la liste des candidats
    var list = [];
    if (!input) {
      wrap.classList.remove('show'); img.dataset.src=''; img.removeAttribute('src'); return;
    }
    if (/^https?:\/\//i.test(input)) {
      list = [ input ];                            // URL absolue
    } else if (input.startsWith('StreamingAssets/')) {
      list = [ abs(input) ];                       // chemin direct depuis StreamingAssets
    } else {
      // nom simple : essaye d’abord dans Images/, puis à la racine de StreamingAssets/
      var base = input.replace(/\.[a-z0-9]+$/i, '');
      var hasExt = /\.[a-z0-9]+$/i.test(input);
      var paths = [
        'StreamingAssets/Images/' + (hasExt ? input : base),
        'StreamingAssets/'        + (hasExt ? input : base)
      ];
      var exts  = hasExt ? [''] : ['.png','.jpg','.jpeg','.webp','.gif'];
      for (var p of paths) for (var e of exts) list.push(abs(p + e));
    }

    console.log('[DOM IMG] request:', input, 'candidates:', list);

    var i = 0, ms = (fadeMs || 220) | 0;

    function apply(url){
  img.dataset.src = url;
  // reset + reflow pour (re)déclencher la transition proprement
  img.style.transition = 'none';
  img.style.opacity = '0';
  void img.offsetWidth;

  // montre l’overlay
  wrap.classList.add('show');

  // et force aussi l’inline à 1 (certains navigateurs gardent l’inline à 0)
  img.style.transition = 'opacity '+ms+'ms ease-out, transform '+ms+'ms ease-out';
  requestAnimationFrame(function(){ img.style.opacity = '1'; });
}


    function tryNext(){
      if (i >= list.length) {
        console.warn('[DOM IMG] not found for', input);
        wrap.classList.remove('show');
        return;
      }
      var url = list[i++];

      // même image que la phrase précédente → pas de fondu si demandé
      if (img.dataset.src === url && !!skipFadeIfSame) {
        wrap.classList.add('show');
        return;
      }

      wrap.classList.remove('show');
      img.style.opacity = '0';

      img.onload = function(){
        console.log('[DOM IMG] loaded:', url, img.naturalWidth+'x'+img.naturalHeight);
        apply(url);
      };
      img.onerror = function(){
        console.warn('[DOM IMG] failed:', url);
        tryNext();
      };

      img.src = url;

      // si l’image est déjà en cache, certains navigateurs ne déclenchent pas onload
      if (img.complete && img.naturalWidth > 0) {
        console.log('[DOM IMG] cache hit:', url);
        apply(url);
      }
    }

    tryNext();
  },

  UI_HideOverlayImage: function (fadeMs) {
    var wrap = document.getElementById('img-overlay');
    var img  = document.getElementById('dom-img');
    if (!wrap || !img) return;
    var ms = (fadeMs || 180) | 0;

    // IMPORTANT:
    // On force aussi l'opacité à 0 car UI_ShowOverlayImage met img.style.opacity = '1' en inline,
    // ce qui peut survivre au retrait de la classe 'show' et laisser visible le fond/gradient (rond).
    try {
      img.style.transition = 'opacity '+ms+'ms ease-out, transform '+ms+'ms ease-out';
      img.style.opacity = '0';
      // Laisser transform centré, mais on peut le "replier" légèrement pour un hide propre.
      // (Si un autre code remet transform, ce n'est pas bloquant.)
      img.style.transform = (img.style.transform || 'translate(-50%, -50%)') + ' scale(0.98)';
    } catch (e) {}

    wrap.classList.remove('show');

    // Après la transition, on nettoie vraiment tout et on cache le wrapper (aucun artefact résiduel).
    setTimeout(function(){
      try { img.dataset.src=''; img.removeAttribute('src'); } catch (e) {}
      try {
        // reset du style visuel (au cas où l'image était vide mais le background restait)
        img.style.background = 'none';
        img.style.backgroundColor = 'transparent';
        img.style.border = '0';
      } catch (e) {}
      try { wrap.style.display = 'none'; } catch (e) {}
    }, ms + 30);
  },

  // Positionner l'image overlay selon les coordonnées JSON (1920x1080)
  // Système Unity : Origine BAS-GAUCHE
  //   X : 0 (gauche) → 1920 (droite)
  //   Y : 0 (BAS) → 1080 (HAUT)
  // Conversion vers CSS : Origine HAUT-GAUCHE (Y inversé)
  UI_SetOverlayImagePosition: function (x, y) {
    var img = document.getElementById('dom-img');
    if (!img) return;
    
    // Convertir les coordonnées Unity (origine bas-gauche) en CSS (origine haut-gauche)
    var percentX = (x / 1920) * 100;
    var percentY = 100 - ((y / 1080) * 100); // Inverser Y
    
    // Appliquer la position
    img.style.left = percentX + '%';
    img.style.top = percentY + '%';
    img.style.right = 'auto';
    img.style.bottom = 'auto';
    img.style.transform = 'translate(-50%, -50%) scale(1)'; // Centrer l'image sur le point
    
    console.log('[DOM IMG] Unity(bas-gauche): x=' + x + ', y=' + y + ' -> CSS(haut-gauche): ' + percentX.toFixed(1) + '%, ' + percentY.toFixed(1) + '%');
  },

  // Appliquer style (taille + bordure + fond) à l'image overlay.
  // sizeW/sizeH/borderW sont exprimés dans la "référence Unity" 1920x1080.
  UI_SetOverlayImageStyle: function (sizeW, sizeH, borderW, borderColorPtr, bgColorPtr, gradStartPtr, gradEndPtr) {
    var img = document.getElementById('dom-img');
    if (!img) return;

    var borderColor = UTF8ToString(borderColorPtr || 0) || '#F4ECE5';
    var bgColor = UTF8ToString(bgColorPtr || 0) || '#FF0000';
    var gradStart = UTF8ToString(gradStartPtr || 0) || '';
    var gradEnd = UTF8ToString(gradEndPtr || 0) || '';

    // Convertir la taille Unity (1920x1080) vers pixels CSS en utilisant la taille réelle du canvas.
    var canvas = document.getElementById('unity-canvas');
    var rect = canvas ? canvas.getBoundingClientRect() : { width: window.innerWidth, height: window.innerHeight };

    var wPx = (sizeW / 1920) * rect.width;
    var hPx = (sizeH / 1080) * rect.height;
    var bPx = (borderW / 1920) * rect.width; // approx: largeur bordure proportionnelle à la largeur

    // Clamp sécurité
    wPx = Math.max(1, wPx);
    hPx = Math.max(1, hPx);
    bPx = Math.max(0, bPx);

    img.style.width = wPx.toFixed(2) + 'px';
    img.style.height = hPx.toFixed(2) + 'px';
    img.style.borderRadius = '50%';
    img.style.overflow = 'hidden';
    if (gradStart && gradEnd) {
      img.style.background = 'linear-gradient(180deg, ' + gradStart + ' 0%, ' + gradEnd + ' 100%)';
    } else {
      img.style.background = 'none';
      img.style.backgroundColor = bgColor;
    }
    img.style.border = bPx.toFixed(2) + 'px solid ' + borderColor;
    img.style.objectFit = 'contain';

    console.log('[DOM IMG] style: size=' + wPx.toFixed(1) + 'x' + hPx.toFixed(1) + 'px, border=' + bPx.toFixed(1) + 'px, borderColor=' + borderColor + ', bg=' + bgColor + ', grad=' + (gradStart && gradEnd ? (gradStart+'->'+gradEnd) : 'none'));
  },

  // ─────────────────────────────────────────────────────────────
  //  CONFIGURATION DU STYLE DES SOUS-TITRES (nouveau design cadre)
  // ─────────────────────────────────────────────────────────────
  UI_SetSubtitleStyle: function (bgColorPtr, bgAlpha, speakerColorPtr, dialogueColorPtr, dialogueSizePtr, speakerSizePtr) {
    var bgColor = UTF8ToString(bgColorPtr || 0) || "#f5ece5";
    var alpha = bgAlpha || 1.0;
    var speakerColor = UTF8ToString(speakerColorPtr || 0) || "#654a7e";
    var dialogueColor = UTF8ToString(dialogueColorPtr || 0) || "#4a4a4a";
    var dialogueSize = UTF8ToString(dialogueSizePtr || 0) || "22px";
    var speakerSize = UTF8ToString(speakerSizePtr || 0) || "24px";

    // Convertir couleur hex en rgba
    function hexToRgba(hex, a) {
      hex = hex.replace(/^#/, '');
      if (hex.length === 3) {
        hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
      }
      var r = parseInt(hex.substring(0, 2), 16);
      var g = parseInt(hex.substring(2, 4), 16);
      var b = parseInt(hex.substring(4, 6), 16);
      return 'rgba(' + r + ', ' + g + ', ' + b + ', ' + a + ')';
    }

    // Appliquer les styles au nouveau design (cadre arrondi)
    var subtitleWrap = document.querySelector('.subtitle-wrap');
    var subtitle = document.querySelector('.subtitle');
    var speaker = document.querySelector('.speaker');
    
    // Style du cadre (fond beige arrondi)
    if (subtitleWrap) {
      subtitleWrap.style.background = hexToRgba(bgColor, alpha);
      subtitleWrap.style.borderRadius = '20px';
    }
    
    // Style du texte de dialogue
    if (subtitle) {
      subtitle.style.background = 'transparent';
      subtitle.style.color = dialogueColor;
      subtitle.style.fontSize = dialogueSize;
      subtitle.style.fontFamily = '"Lato", system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Arial, sans-serif';
    }
    
    // Style du speaker (violet)
    if (speaker) {
      speaker.style.color = speakerColor;
      speaker.style.fontSize = speakerSize;
      speaker.style.fontFamily = '"Lato", system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Arial, sans-serif';
    }

    console.log('[SubtitleStyle] Frame background:', bgColor, 'Alpha:', alpha);
    console.log('[SubtitleStyle] Speaker:', speakerColor, 'Dialogue:', dialogueColor);
  },

  // ─────────────────────────────────────────────────────────────
  //  INPUTS HTML NATIFS POUR MOBILE (clavier virtuel)
  // ─────────────────────────────────────────────────────────────
  
  // Détecter si on est sur un appareil mobile
  Mobile_IsMobile: function () {
    return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
           ('ontouchstart' in window) ||
           (navigator.maxTouchPoints > 0);
  },

  // Créer un input HTML natif positionné au-dessus d'un champ Unity
  Mobile_CreateInput: function (idPtr, x, y, width, height, placeholderPtr, inputTypePtr, fontSize) {
    var id = UTF8ToString(idPtr || 0) || 'unity-input';
    var placeholder = UTF8ToString(placeholderPtr || 0) || '';
    var inputType = UTF8ToString(inputTypePtr || 0) || 'text';
    
    // Supprimer l'input existant s'il y en a un
    var existing = document.getElementById(id);
    if (existing) {
      existing.remove();
    }
    
    // Créer le conteneur
    var container = document.getElementById('unity-container') || document.body;
    
    // Créer l'input HTML
    var input = document.createElement('input');
    input.id = id;
    input.type = inputType;
    input.placeholder = placeholder;
    input.value = '';
    
    // Styles pour positionner l'input au-dessus du canvas Unity
    var canvas = document.getElementById('unity-canvas');
    var canvasRect = canvas ? canvas.getBoundingClientRect() : { left: 0, top: 0 };
    
    Object.assign(input.style, {
      position: 'absolute',
      left: (canvasRect.left + x) + 'px',
      top: (canvasRect.top + y) + 'px',
      width: width + 'px',
      height: height + 'px',
      fontSize: (fontSize || 20) + 'px',
      padding: '10px',
      border: '2px solid #ccc',
      borderRadius: '8px',
      backgroundColor: '#ffffff',
      color: '#333333',
      zIndex: '99999',
      boxSizing: 'border-box',
      fontFamily: 'system-ui, -apple-system, sans-serif',
      outline: 'none'
    });
    
    // Masquer l'input par défaut
    input.style.display = 'none';
    
    container.appendChild(input);
    
    // Stocker la référence
    if (!Module.Mobile_Inputs) {
      Module.Mobile_Inputs = {};
    }
    Module.Mobile_Inputs[id] = input;
    
    console.log('[Mobile Input] Créé:', id, 'à', x, y, width + 'x' + height);
    return 1;
  },

  // Afficher/masquer un input HTML
  Mobile_ShowInput: function (idPtr, show) {
    var id = UTF8ToString(idPtr || 0);
    if (!Module.Mobile_Inputs || !Module.Mobile_Inputs[id]) return 0;
    
    var input = Module.Mobile_Inputs[id];
    input.style.display = show ? 'block' : 'none';
    
    if (show) {
      // Forcer le focus pour afficher le clavier
      setTimeout(function() {
        input.focus();
        input.click();
      }, 100);
    } else {
      input.blur();
    }
    
    return 1;
  },

  // Positionner un input HTML (mise à jour de la position)
  Mobile_SetInputPosition: function (idPtr, x, y, width, height) {
    var id = UTF8ToString(idPtr || 0);
    if (!Module.Mobile_Inputs || !Module.Mobile_Inputs[id]) return 0;
    
    var input = Module.Mobile_Inputs[id];
    var canvas = document.getElementById('unity-canvas');
    var canvasRect = canvas ? canvas.getBoundingClientRect() : { left: 0, top: 0 };
    
    input.style.left = (canvasRect.left + x) + 'px';
    input.style.top = (canvasRect.top + y) + 'px';
    input.style.width = width + 'px';
    input.style.height = height + 'px';
    
    return 1;
  },

  // Obtenir la valeur d'un input HTML
  Mobile_GetInputValue: function (idPtr, valuePtr, maxLen) {
    var id = UTF8ToString(idPtr || 0);
    if (!Module.Mobile_Inputs || !Module.Mobile_Inputs[id]) {
      stringToUTF8('', valuePtr, maxLen);
      return 0;
    }
    
    var value = Module.Mobile_Inputs[id].value || '';
    stringToUTF8(value, valuePtr, maxLen);
    return value.length;
  },

  // Définir la valeur d'un input HTML
  Mobile_SetInputValue: function (idPtr, valuePtr) {
    var id = UTF8ToString(idPtr || 0);
    var value = UTF8ToString(valuePtr || 0) || '';
    
    if (!Module.Mobile_Inputs || !Module.Mobile_Inputs[id]) return 0;
    
    Module.Mobile_Inputs[id].value = value;
    return 1;
  },

  // Supprimer un input HTML
  Mobile_RemoveInput: function (idPtr) {
    var id = UTF8ToString(idPtr || 0);
    if (!Module.Mobile_Inputs || !Module.Mobile_Inputs[id]) return 0;
    
    var input = Module.Mobile_Inputs[id];
    input.remove();
    delete Module.Mobile_Inputs[id];
    
    return 1;
  },

  // Configurer les callbacks pour un input HTML
  Mobile_SetInputCallbacks: function (idPtr, onChangeCallbackPtr, onFocusCallbackPtr, onBlurCallbackPtr) {
    var id = UTF8ToString(idPtr || 0);
    if (!Module.Mobile_Inputs || !Module.Mobile_Inputs[id]) return 0;
    
    var input = Module.Mobile_Inputs[id];
    
    // Supprimer les anciens listeners
    input.onchange = null;
    input.onfocus = null;
    input.onblur = null;
    input.oninput = null;
    
    // Nouveaux listeners
    if (onChangeCallbackPtr) {
      var onChangeCallback = UTF8ToString(onChangeCallbackPtr);
      input.oninput = function() {
        if (Module[onChangeCallback]) {
          var valuePtr = Module._malloc(input.value.length + 1);
          stringToUTF8(input.value, valuePtr, input.value.length + 1);
          Module[onChangeCallback](valuePtr);
          Module._free(valuePtr);
        }
      };
    }
    
    if (onFocusCallbackPtr) {
      var onFocusCallback = UTF8ToString(onFocusCallbackPtr);
      input.onfocus = function() {
        if (Module[onFocusCallback]) {
          Module[onFocusCallback]();
        }
      };
    }
    
    if (onBlurCallbackPtr) {
      var onBlurCallback = UTF8ToString(onBlurCallbackPtr);
      input.onblur = function() {
        if (Module[onBlurCallback]) {
          Module[onBlurCallback]();
        }
      };
    }
    
    return 1;
  }
});
