From 3cd71b0a901a78d8a8e7c23cf76dcc1b2071bdd9 Mon Sep 17 00:00:00 2001 From: abose Date: Wed, 4 Mar 2026 07:53:53 +0530 Subject: [PATCH 1/4] fix(terminal): render terminal canvas at correct DPR when webview is zoomed Tauri/Electron native zoom does not update window.devicePixelRatio, causing the xterm.js WebGL canvas to render at 1x resolution regardless of zoom level. The native zoom then upscales the bitmap, producing blurry terminal text at zoom levels above 100%. Fix by overriding the devicePixelRatio getter in zoomWebView() to reflect the effective DPR (baseDPR * zoomFactor). The terminal detects DPR changes during fit() and calls clearTextureAtlas() to rebuild glyphs at the new resolution. Also guards _cropDataUrlToRect to use the original base DPR so screenshot cropping is unaffected. --- .../Terminal/TerminalInstance.js | 37 ++++++++++++++++--- src/phoenix/shell.js | 13 ++++++- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/extensionsIntegrated/Terminal/TerminalInstance.js b/src/extensionsIntegrated/Terminal/TerminalInstance.js index 837a90458b..e944900ed8 100644 --- a/src/extensionsIntegrated/Terminal/TerminalInstance.js +++ b/src/extensionsIntegrated/Terminal/TerminalInstance.js @@ -105,6 +105,8 @@ define(function (require, exports, module) { this.$container = null; this._resizeTimeout = null; this._disposed = false; + this._webglAddon = null; + this._lastDpr = null; // Bound event handlers for cleanup this._onTerminalData = this._onTerminalData.bind(this); @@ -144,11 +146,7 @@ define(function (require, exports, module) { this.terminal.open(this.$container[0]); // Load WebGL renderer for better performance - try { - this.terminal.loadAddon(new WebglAddon()); - } catch (e) { - console.warn("Terminal: WebglAddon failed to load, using default renderer:", e); - } + this._loadWebGLAddon(); // Fit to container this._fit(); @@ -270,6 +268,22 @@ define(function (require, exports, module) { return true; // Let xterm handle it }; + /** + * Load (or reload) the WebGL renderer addon, recording the current DPR + * so we can detect changes later and recreate the addon at the correct + * resolution (e.g. after a zoom change). + */ + TerminalInstance.prototype._loadWebGLAddon = function () { + try { + this._webglAddon = new WebglAddon(); + this.terminal.loadAddon(this._webglAddon); + this._lastDpr = window.devicePixelRatio; + } catch (e) { + console.warn("Terminal: WebglAddon failed to load, using default renderer:", e); + this._webglAddon = null; + } + }; + /** * Fit the terminal to its container. * @@ -288,6 +302,15 @@ define(function (require, exports, module) { return; } + // When the effective DPR changes (e.g. after a webview zoom change), + // clear the glyph texture atlas so the WebGL renderer rebuilds it at + // the new resolution. The subsequent fit() will resize the canvas. + const currentDpr = window.devicePixelRatio; + if (this._lastDpr !== null && this._lastDpr !== currentDpr) { + this._lastDpr = currentDpr; + this.terminal.clearTextureAtlas(); + } + try { // Only clear the prompt region when dimensions are actually // changing — i.e. a real reflow will happen. When dimensions @@ -416,6 +439,10 @@ define(function (require, exports, module) { // Dispose xterm clearTimeout(this._resizeTimeout); + if (this._webglAddon) { + this._webglAddon.dispose(); + this._webglAddon = null; + } if (this.terminal) { this.terminal.dispose(); this.terminal = null; diff --git a/src/phoenix/shell.js b/src/phoenix/shell.js index 0daf4fef9b..d19d14d0d7 100644 --- a/src/phoenix/shell.js +++ b/src/phoenix/shell.js @@ -203,7 +203,7 @@ function _cropDataUrlToRect(dataUrl, rect) { const img = new Image(); img.onload = function () { try { - const dpr = window.devicePixelRatio || 1; + const dpr = window._origDevicePixelRatio || window.devicePixelRatio || 1; const canvas = document.createElement("canvas"); const sx = Math.round(rect.x * dpr); const sy = Math.round(rect.y * dpr); @@ -859,6 +859,17 @@ Phoenix.app = { if(scaleFactor < .1 || scaleFactor > 2) { throw new Error("zoomWebView scale factor should be between .1 and 2"); } + // Native webview zoom (Tauri/Electron) does not update + // window.devicePixelRatio, causing canvas-based renderers + // (e.g. xterm.js WebGL) to render at the wrong resolution. + // Override the getter so it reflects the effective DPR. + if(window._origDevicePixelRatio === undefined) { + window._origDevicePixelRatio = window.devicePixelRatio; + } + Object.defineProperty(window, 'devicePixelRatio', { + get() { return window._origDevicePixelRatio * scaleFactor; }, + configurable: true + }); if(window.__TAURI__) { return window.__TAURI__.tauri.invoke("zoom_window", {scaleFactor: scaleFactor}); } From 8570ad9b2e54e9b193633d315930b7af71a8f94b Mon Sep 17 00:00:00 2001 From: abose Date: Wed, 4 Mar 2026 09:22:48 +0530 Subject: [PATCH 2/4] fix(terminal): limit DPR override to macOS Tauri only The devicePixelRatio override breaks on Chrome-based Electron and Tauri on other platforms. Restrict it to macOS + Tauri where the native webview zoom does not update DPR. --- src/phoenix/shell.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/phoenix/shell.js b/src/phoenix/shell.js index d19d14d0d7..7efcfc7265 100644 --- a/src/phoenix/shell.js +++ b/src/phoenix/shell.js @@ -859,17 +859,21 @@ Phoenix.app = { if(scaleFactor < .1 || scaleFactor > 2) { throw new Error("zoomWebView scale factor should be between .1 and 2"); } - // Native webview zoom (Tauri/Electron) does not update + // On macOS + Tauri, native webview zoom does not update // window.devicePixelRatio, causing canvas-based renderers // (e.g. xterm.js WebGL) to render at the wrong resolution. // Override the getter so it reflects the effective DPR. - if(window._origDevicePixelRatio === undefined) { - window._origDevicePixelRatio = window.devicePixelRatio; + // Limited to macOS Tauri — Electron and other platforms + // handle DPR correctly or conflict with this override. + if(window.__TAURI__ && Phoenix.platform === "mac") { + if(window._origDevicePixelRatio === undefined) { + window._origDevicePixelRatio = window.devicePixelRatio; + } + Object.defineProperty(window, 'devicePixelRatio', { + get() { return window._origDevicePixelRatio * scaleFactor; }, + configurable: true + }); } - Object.defineProperty(window, 'devicePixelRatio', { - get() { return window._origDevicePixelRatio * scaleFactor; }, - configurable: true - }); if(window.__TAURI__) { return window.__TAURI__.tauri.invoke("zoom_window", {scaleFactor: scaleFactor}); } From 0319d3884b0d4fa2bf66c38fa445351434b676c9 Mon Sep 17 00:00:00 2001 From: abose Date: Wed, 4 Mar 2026 09:39:29 +0530 Subject: [PATCH 3/4] feat(ui): show HUD overlay with zoom percentage on zoom in/out Add NotificationUI.showHUD() for centered, macOS-style HUD notifications with icon and label. Used by zoom commands to display a magnifying glass icon with the current zoom percentage that auto-dismisses after 1 second. --- src/styles/brackets.less | 35 +++++++++++++++ src/view/ViewCommandHandlers.js | 25 ++++++++--- src/widgets/NotificationUI.js | 75 +++++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 5 deletions(-) diff --git a/src/styles/brackets.less b/src/styles/brackets.less index e855886add..000975f835 100644 --- a/src/styles/brackets.less +++ b/src/styles/brackets.less @@ -3409,6 +3409,41 @@ label input { opacity: 1; } +/* HUD overlay: centered macOS-style notification (zoom, volume, etc.) */ +.hud-overlay { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) scale(0.9); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 140px; + height: 120px; + border-radius: 16px; + background: rgba(30, 30, 30, 0.85); + color: #e0e0e0; + z-index: 10000; + opacity: 0; + transition: opacity 0.2s ease, transform 0.2s ease; + pointer-events: none; + user-select: none; + i { + font-size: 40px; + margin-bottom: 8px; + } + .hud-label { + font-size: 18px; + font-weight: 600; + letter-spacing: 0.5px; + } +} +.hud-overlay.visible { + opacity: 1; + transform: translate(-50%, -50%) scale(1); +} + .inline-toast kbd { display: inline-block; padding: 1px 5px; diff --git a/src/view/ViewCommandHandlers.js b/src/view/ViewCommandHandlers.js index 280d1b6f3b..37822ce8fd 100644 --- a/src/view/ViewCommandHandlers.js +++ b/src/view/ViewCommandHandlers.js @@ -49,7 +49,8 @@ define(function (require, exports, module) { KeyBindingManager = require("command/KeyBindingManager"), WorkspaceManager = require("view/WorkspaceManager"), _ = require("thirdparty/lodash"), - FontRuleTemplate = require("text!view/fontrules/font-based-rules.less"); + FontRuleTemplate = require("text!view/fontrules/font-based-rules.less"), + NotificationUI = require("widgets/NotificationUI"); var prefs = PreferencesManager.getExtensionPrefs("fonts"); @@ -370,14 +371,26 @@ define(function (require, exports, module) { } } + function _showZoomHUD(zoomFactor, zoomingIn) { + const pct = Math.round(zoomFactor * 100); + const icon = zoomingIn + ? "fa-solid fa-magnifying-glass-plus" + : "fa-solid fa-magnifying-glass-minus"; + NotificationUI.showHUD(icon, pct + "%", { + autoCloseTimeS: 1 + }); + } + function _handleZoomIn(event) { if(!Phoenix.isNativeApp) { return _handleBrowserZoom(event); } const currentZoom = prefs.get(PREF_DESKTOP_ZOOM_SCALE); if(currentZoom < MAX_ZOOM_SCALE){ - prefs.set(PREF_DESKTOP_ZOOM_SCALE, currentZoom + 0.1); - PhStore.setItem(PhStore._PHSTORE_BOOT_DESKTOP_ZOOM_SCALE_KEY, currentZoom + 0.1); + const newZoom = currentZoom + 0.1; + prefs.set(PREF_DESKTOP_ZOOM_SCALE, newZoom); + PhStore.setItem(PhStore._PHSTORE_BOOT_DESKTOP_ZOOM_SCALE_KEY, newZoom); + _showZoomHUD(newZoom, true); } } @@ -387,8 +400,10 @@ define(function (require, exports, module) { } const currentZoom = prefs.get(PREF_DESKTOP_ZOOM_SCALE); if(currentZoom > MIN_ZOOM_SCALE){ - prefs.set(PREF_DESKTOP_ZOOM_SCALE, currentZoom - 0.1); - PhStore.setItem(PhStore._PHSTORE_BOOT_DESKTOP_ZOOM_SCALE_KEY, currentZoom - 0.1); + const newZoom = currentZoom - 0.1; + prefs.set(PREF_DESKTOP_ZOOM_SCALE, newZoom); + PhStore.setItem(PhStore._PHSTORE_BOOT_DESKTOP_ZOOM_SCALE_KEY, newZoom); + _showZoomHUD(newZoom, false); } } diff --git a/src/widgets/NotificationUI.js b/src/widgets/NotificationUI.js index 0aab5da8f0..924f862c89 100644 --- a/src/widgets/NotificationUI.js +++ b/src/widgets/NotificationUI.js @@ -480,9 +480,84 @@ define(function (require, exports, module) { return notification; } + let _activeHUD = null; + + /** + * Shows a large, centered HUD overlay (like macOS volume/brightness indicator) with an icon and label. + * The HUD fades in/out and auto-dismisses. Only one HUD is shown at a time — calling this while a + * previous HUD is visible replaces it instantly. + * + * ```js + * NotificationUI.showHUD("fa-solid fa-magnifying-glass-plus", "110%"); + * ``` + * + * @param {string} iconClass Font Awesome class string for the icon (e.g. "fa-solid fa-magnifying-glass-plus"). + * @param {string} label Text to display below the icon (e.g. "110%"). + * @param {Object} [options] optional, supported options: + * * `autoCloseTimeS` - Time in seconds after which the HUD auto-closes. Default is 1. + * @return {Notification} Object with a done handler that resolves when the HUD closes. + * @type {function} + */ + function showHUD(iconClass, label, options = {}) { + const autoCloseTimeS = options.autoCloseTimeS !== undefined ? options.autoCloseTimeS : 1; + + // Close any existing HUD immediately + if (_activeHUD && _activeHUD.$notification) { + _activeHUD.$notification.remove(); + _activeHUD._result.resolve(CLOSE_REASON.TIMEOUT); + _activeHUD.$notification = null; + } + + const $hud = $('
' + + '' + + '
' + label + '
' + + '
'); + $("body").append($hud); + + const notification = new Notification($hud, "hud"); + _activeHUD = notification; + + // Fade in on next frame + requestAnimationFrame(function () { + $hud.addClass("visible"); + }); + + function closeHUD(reason) { + if (!notification.$notification) { + return; + } + notification.$notification = null; + _activeHUD = null; + $hud.removeClass("visible"); + function cleanup() { + $hud.remove(); + notification._result.resolve(reason); + } + $hud.one("transitionend transitioncancel", cleanup); + // Safety fallback + setTimeout(cleanup, 600); + } + + notification.close = function (closeType) { + closeHUD(closeType || CLOSE_REASON.CLICK_DISMISS); + return this; + }; + + if (autoCloseTimeS) { + setTimeout(function () { + if (notification.$notification) { + notification.close(CLOSE_REASON.TIMEOUT); + } + }, autoCloseTimeS * 1000); + } + + return notification; + } + exports.createFromTemplate = createFromTemplate; exports.createToastFromTemplate = createToastFromTemplate; exports.showToastOn = showToastOn; + exports.showHUD = showHUD; exports.CLOSE_REASON = CLOSE_REASON; exports.NOTIFICATION_STYLES_CSS_CLASS = NOTIFICATION_STYLES_CSS_CLASS; }); From f07c203c77d6ede354e57bda4e1f0207a725bc1e Mon Sep 17 00:00:00 2001 From: abose Date: Wed, 4 Mar 2026 09:44:53 +0530 Subject: [PATCH 4/4] fix(ui): remove HUD overlay animations for instant show/hide --- src/styles/brackets.less | 8 +------- src/widgets/NotificationUI.js | 27 ++++++--------------------- 2 files changed, 7 insertions(+), 28 deletions(-) diff --git a/src/styles/brackets.less b/src/styles/brackets.less index 000975f835..05855333ac 100644 --- a/src/styles/brackets.less +++ b/src/styles/brackets.less @@ -3414,7 +3414,7 @@ label input { position: fixed; top: 50%; left: 50%; - transform: translate(-50%, -50%) scale(0.9); + transform: translate(-50%, -50%); display: flex; flex-direction: column; align-items: center; @@ -3425,8 +3425,6 @@ label input { background: rgba(30, 30, 30, 0.85); color: #e0e0e0; z-index: 10000; - opacity: 0; - transition: opacity 0.2s ease, transform 0.2s ease; pointer-events: none; user-select: none; i { @@ -3439,10 +3437,6 @@ label input { letter-spacing: 0.5px; } } -.hud-overlay.visible { - opacity: 1; - transform: translate(-50%, -50%) scale(1); -} .inline-toast kbd { display: inline-block; diff --git a/src/widgets/NotificationUI.js b/src/widgets/NotificationUI.js index 924f862c89..8ab5529c3e 100644 --- a/src/widgets/NotificationUI.js +++ b/src/widgets/NotificationUI.js @@ -517,29 +517,14 @@ define(function (require, exports, module) { const notification = new Notification($hud, "hud"); _activeHUD = notification; - // Fade in on next frame - requestAnimationFrame(function () { - $hud.addClass("visible"); - }); - - function closeHUD(reason) { - if (!notification.$notification) { - return; + notification.close = function (closeType) { + if (!this.$notification) { + return this; } - notification.$notification = null; + this.$notification = null; _activeHUD = null; - $hud.removeClass("visible"); - function cleanup() { - $hud.remove(); - notification._result.resolve(reason); - } - $hud.one("transitionend transitioncancel", cleanup); - // Safety fallback - setTimeout(cleanup, 600); - } - - notification.close = function (closeType) { - closeHUD(closeType || CLOSE_REASON.CLICK_DISMISS); + $hud.remove(); + this._result.resolve(closeType || CLOSE_REASON.CLICK_DISMISS); return this; };