From e5ce9ca90a9f1cac9773a09fe18730ef17440b51 Mon Sep 17 00:00:00 2001 From: locphamtran Date: Wed, 19 Aug 2026 10:22:35 +0700 Subject: [PATCH] fix(vst-gui): borderless fx-gui window + anchored re-show (mouse stable) - RenderFxJob: offscreen fx-gui window now WS_POPUP (borderless) so GetWindowRect == client rect; anchored placement over the embed panel is exact. WS_OVERLAPPEDWINDOW put the client ~31px lower/right -> cursor jumped on hover and clicks hit the webview (relay rejected). - AdjustWindowRect sizes to the window's own style (no caption for POPUP). - FxGuiServer: mousemove only SetCursorPos while dragging (buttons!=0); hover no longer teleports the cursor. - FxGuiServer /show: skip SetWindowPos/resize when rect unchanged (frontend now re-POSTs /show every ~500ms to re-anchor on scroll/resize). - app.jsx VstGuiEmbed: drop shownRef one-shot; re-POST /show each refresh. - Rebuilt bridge (6 locations) + Tauri release (embedded sidecar) + v=202608190002. --- app/static/js/app.jsx | 9 ++++----- app/static/js/app.precompiled.js | 7 ++++--- app/templates/index.html | 2 +- install/daw_vst_bridge.exe | Bin 1701376 -> 1701376 bytes install/sonicforge-daw.exe | Bin 11185152 -> 11189760 bytes native_bridge/src/FxGuiServer.cpp | 25 ++++++++++++++++++------- native_bridge/src/RenderFxJob.cpp | 10 ++++++++-- 7 files changed, 35 insertions(+), 18 deletions(-) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 0d2e85c..43c1089 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -11909,7 +11909,6 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor const boxRef = React.useRef(null); const deadRef = React.useRef(false); const screenRectRef = React.useRef(null); - const shownRef = React.useRef(false); const [dead, setDead] = React.useState(false); const post = (payload) => { try { fetch(url + '/input', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }).catch(() => {}); } catch (e) {} }; React.useEffect(() => { @@ -11936,10 +11935,10 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor let alive = true; const maybeShow = () => { const r = screenRectRef.current; - if (!shownRef.current && r && r.w >= 4 && r.h >= 4) { - shownRef.current = true; - // Show the native window anchored over the panel (bridge stays - // alive; hidden again by /hide on unmount). + if (r && r.w >= 4 && r.h >= 4) { + // Re-POST /show on every refresh (~500ms) so the native window + // re-anchors over the panel on scroll/drag/resize. Idempotent: the + // bridge skips SetWindowPos/resize when nothing moved. try { fetch(url + '/show', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ rect: r }) }).catch(() => {}); } catch (e) {} } }; diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 593f938..f156b36 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -742,14 +742,15 @@ return{hx:Math.max(0,Math.min((canvasRef.current?canvasRef.current.clientWidth:4 // gửi pointer/key về bridge qua /input. Dùng chung MasteringModal + FXRackModal. // url = embed_url từ API fx-gui open (embed:true); onClose khi user đóng; // onDead khi bridge chết (ping fail) → parent trả graph + toast. -const VstGuiEmbed=({url,onClose,onDead})=>{const imgRef=React.useRef(null);const boxRef=React.useRef(null);const deadRef=React.useRef(false);const screenRectRef=React.useRef(null);const shownRef=React.useRef(false);const[dead,setDead]=React.useState(false);const post=payload=>{try{fetch(url+'/input',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)}).catch(()=>{});}catch(e){}};React.useEffect(()=>{deadRef.current=false;setDead(false);const tick=setInterval(()=>{const img=imgRef.current;if(img)img.src=url+'/frame?t='+Date.now();},100);const ping=setInterval(()=>{fetch(url+'/ping').then(r=>{if(!r.ok)throw new Error('bad');}).catch(()=>{if(!deadRef.current){deadRef.current=true;setDead(true);onDead&&onDead();}});},2000);return()=>{clearInterval(tick);clearInterval(ping);// Panel closed -> hide the native window (bridge stays alive so +const VstGuiEmbed=({url,onClose,onDead})=>{const imgRef=React.useRef(null);const boxRef=React.useRef(null);const deadRef=React.useRef(false);const screenRectRef=React.useRef(null);const[dead,setDead]=React.useState(false);const post=payload=>{try{fetch(url+'/input',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)}).catch(()=>{});}catch(e){}};React.useEffect(()=>{deadRef.current=false;setDead(false);const tick=setInterval(()=>{const img=imgRef.current;if(img)img.src=url+'/frame?t='+Date.now();},100);const ping=setInterval(()=>{fetch(url+'/ping').then(r=>{if(!r.ok)throw new Error('bad');}).catch(()=>{if(!deadRef.current){deadRef.current=true;setDead(true);onDead&&onDead();}});},2000);return()=>{clearInterval(tick);clearInterval(ping);// Panel closed -> hide the native window (bridge stays alive so // reopening is fast). The "Đóng GUI" button still POSTs /close (exit). try{fetch(url+'/hide',{method:'POST'}).catch(()=>{});}catch(e){}};// eslint-disable-next-line react-hooks/exhaustive-deps },[url]);// Embed panel screen rect (physical px) for the bridge's anchored real-input // dance: Tauri innerPosition + scaleFactor; window.screenX/Y fallback when // not running under Tauri. Refreshed ~500ms so scroll/resize stays current. -React.useEffect(()=>{let alive=true;const maybeShow=()=>{const r=screenRectRef.current;if(!shownRef.current&&r&&r.w>=4&&r.h>=4){shownRef.current=true;// Show the native window anchored over the panel (bridge stays -// alive; hidden again by /hide on unmount). +React.useEffect(()=>{let alive=true;const maybeShow=()=>{const r=screenRectRef.current;if(r&&r.w>=4&&r.h>=4){// Re-POST /show on every refresh (~500ms) so the native window +// re-anchors over the panel on scroll/drag/resize. Idempotent: the +// bridge skips SetWindowPos/resize when nothing moved. try{fetch(url+'/show',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({rect:r})}).catch(()=>{});}catch(e){}}};const refresh=()=>{const box=boxRef.current;if(!box)return;const r=box.getBoundingClientRect();if(r.width<4||r.height<4)return;try{const tw=window.__TAURI__&&window.__TAURI__.window;if(tw&&tw.getCurrentWindow){Promise.all([tw.getCurrentWindow().innerPosition(),tw.getCurrentWindow().scaleFactor()]).then(([pos,sf])=>{if(!alive||!boxRef.current)return;const rb=boxRef.current.getBoundingClientRect();screenRectRef.current={x:Math.round(pos.x+rb.left*sf),y:Math.round(pos.y+rb.top*sf),w:Math.round(rb.width*sf),h:Math.round(rb.height*sf)};maybeShow();}).catch(()=>{});}else{screenRectRef.current={x:Math.round((window.screenX||0)+r.left),y:Math.round((window.screenY||0)+r.top),w:Math.round(r.width),h:Math.round(r.height)};maybeShow();}}catch(e){}};refresh();const iv=setInterval(refresh,500);return()=>{alive=false;clearInterval(iv);};// eslint-disable-next-line react-hooks/exhaustive-deps },[url]);const pt=e=>{const box=boxRef.current,img=imgRef.current;if(!box||!img||!img.naturalWidth||!img.naturalHeight)return null;const r=box.getBoundingClientRect();if(r.width===0||r.height===0)return null;return{x:Math.round((e.clientX-r.left)/r.width*img.naturalWidth),y:Math.round((e.clientY-r.top)/r.height*img.naturalHeight)};};const handle=e=>{const p=pt(e);if(!p)return;post({type:e.type==='mousedown'?'mousedown':e.type==='mouseup'?'mouseup':'mousemove',x:p.x,y:p.y,button:e.button,buttons:e.buttons,rect:screenRectRef.current||undefined});};const close=()=>{try{fetch(url+'/close',{method:'POST'}).catch(()=>{});}catch(e){}onClose&&onClose();};return/*#__PURE__*/React.createElement("div",{className:"relative w-full h-full flex flex-col"},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between px-3 py-1.5 bg-slate-900/80 border border-slate-800 rounded-t-lg shrink-0"},/*#__PURE__*/React.createElement("span",{className:"text-[10px] text-violet-300 font-mono"},"VST GUI \u2014 nh\xFAng (frame-capture \xB7 ~10fps)"),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},dead&&/*#__PURE__*/React.createElement("span",{className:"text-[9px] text-red-400 font-mono"},"bridge m\u1EA5t k\u1EBFt n\u1ED1i"),/*#__PURE__*/React.createElement("button",{onClick:close,className:"px-2 py-0.5 bg-red-900/60 hover:bg-red-800 text-red-100 text-[10px] font-semibold rounded transition",title:"\u0110\xF3ng GUI v\xE0 tr\u1EA3 v\u1EC1 graph"},"\u2715 \u0110\xF3ng GUI"))),/*#__PURE__*/React.createElement("div",{ref:boxRef,tabIndex:0,className:"flex-1 relative bg-slate-950 border border-slate-800 rounded-b-lg overflow-hidden outline-none",onMouseDown:handle,onMouseUp:handle,onMouseMove:handle,onMouseLeave:()=>post({type:'mouseup',x:-1,y:-1,button:0}),onWheel:e=>{const p=pt(e);if(p)post({type:'wheel',x:p.x,y:p.y,deltaY:e.deltaY,rect:screenRectRef.current||undefined});},onKeyDown:e=>{e.preventDefault();post({type:'keydown',keyCode:e.keyCode||e.which});},onKeyUp:e=>{e.preventDefault();post({type:'keyup',keyCode:e.keyCode||e.which});}},/*#__PURE__*/React.createElement("img",{ref:imgRef,alt:"VST GUI",className:"w-full h-full object-contain select-none pointer-events-none",draggable:false})));};const vstRow=(s,idx,toggleV,changeV,uploadV,moveV,removeV,fxl,chainLen,openGui,showCarla=true,activeIdx=-1,onSelect=null)=>/*#__PURE__*/React.createElement("div",{key:idx,onClick:()=>onSelect&&onSelect(s,idx),className:`flex items-center gap-2 bg-slate-900/60 border rounded-lg px-2 py-1.5 flex-wrap cursor-pointer transition-all ${activeIdx===idx?'border-teal-400 bg-slate-800/80':'border-slate-800 hover:border-slate-600'}`},/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();toggleV(idx);},className:`w-4 h-4 rounded-full flex items-center justify-center text-[9px] font-bold shrink-0 ${!s.bypass?'bg-teal-500 text-slate-950':'bg-slate-700 text-slate-300'}`,title:s.bypass?'Bypass':'Active'},/*#__PURE__*/React.createElement("i",{"data-lucide":"power",className:"w-2.5 h-2.5"})),/*#__PURE__*/React.createElement("span",{className:"text-[10px] font-bold text-slate-300 font-mono w-6 shrink-0"},idx+1),/*#__PURE__*/React.createElement("select",{value:s.path||'',onClick:e=>e.stopPropagation(),onChange:e=>changeV(idx,e.target.value),className:"flex-1 min-w-[200px] bg-slate-950 border border-slate-700 text-teal-300 rounded px-2 py-1 text-[11px] outline-none focus:border-teal-500"},/*#__PURE__*/React.createElement("option",{value:""},"\u2014 Ch\u1ECDn VST FX \u2014"),(fxl||[]).map(f=>/*#__PURE__*/React.createElement("option",{key:f.path,value:f.path},f.name||f.id))),/*#__PURE__*/React.createElement("span",{className:`text-[9px] font-mono ${s.preset_b64?'text-emerald-400':'text-slate-600'}`},s.preset_b64?'✓ preset':'no preset'),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();uploadV(idx);},className:"px-2 py-1 bg-slate-800 hover:bg-slate-700 text-slate-300 text-[10px] font-semibold rounded transition",title:"Upload .vstpreset (Save preset)"},"Preset"),openGui&&/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();openGui(s,idx);},disabled:!s.path,className:"px-2 py-1 bg-violet-800 hover:bg-violet-700 disabled:opacity-30 text-violet-100 text-[10px] font-semibold rounded transition",title:"M\u1EDF GUI c\u1EE7a VST (ch\u1EC9nh th\xF4ng s\u1ED1 tr\u1EF1c ti\u1EBFp)"},/*#__PURE__*/React.createElement("i",{"data-lucide":"monitor",className:"w-3 h-3"})," GUI"),showCarla&&/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();if(!s.path){window.showToast&&window.showToast('Chọn VST FX trước','warning');return;}window.SonicAPI.openInCarla(s.name||s.path,s.path).then(r=>{if(r&&r.success)window.showToast&&window.showToast('Đã mở Carla với '+(s.name||s.path)+' — chỉnh âm trong Carla rồi Save preset','success');else window.showToast&&window.showToast('Không mở được Carla','error');}).catch(err=>window.showToast&&window.showToast('Lỗi mở Carla: '+(err.message||err),'error'));},className:"px-2 py-1 bg-teal-800 hover:bg-teal-700 text-teal-100 text-[10px] font-semibold rounded transition",title:"M\u1EDF native GUI trong Carla"},"Carla"),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();moveV(idx,-1);},disabled:idx===0,className:"w-6 h-6 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded text-xs disabled:opacity-30",title:"L\xEAn"},/*#__PURE__*/React.createElement("i",{"data-lucide":"chevron-up",className:"w-3 h-3"})),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();moveV(idx,1);},disabled:idx===chainLen-1,className:"w-6 h-6 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded text-xs disabled:opacity-30",title:"Xu\u1ED1ng"},/*#__PURE__*/React.createElement("i",{"data-lucide":"chevron-down",className:"w-3 h-3"})),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();removeV(idx);},className:"text-slate-600 hover:text-red-400 text-xs px-0.5",title:"X\xF3a slot"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-3 h-3"})));// ── VST FX Plugin Picker (spec vst_fx_panel_integration.md §II) ────────── // Modal chọn VST FX theo category (heuristic từ backend list_fx.category). diff --git a/app/templates/index.html b/app/templates/index.html index 72e9725..68dea57 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -50,7 +50,7 @@ - +