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..7efcfc7265 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,21 @@ Phoenix.app = { if(scaleFactor < .1 || scaleFactor > 2) { throw new Error("zoomWebView scale factor should be between .1 and 2"); } + // 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. + // 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 + }); + } if(window.__TAURI__) { return window.__TAURI__.tauri.invoke("zoom_window", {scaleFactor: scaleFactor}); } diff --git a/src/styles/brackets.less b/src/styles/brackets.less index e855886add..05855333ac 100644 --- a/src/styles/brackets.less +++ b/src/styles/brackets.less @@ -3409,6 +3409,35 @@ 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%); + 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; + 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; + } +} + .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..8ab5529c3e 100644 --- a/src/widgets/NotificationUI.js +++ b/src/widgets/NotificationUI.js @@ -480,9 +480,69 @@ 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; + + notification.close = function (closeType) { + if (!this.$notification) { + return this; + } + this.$notification = null; + _activeHUD = null; + $hud.remove(); + this._result.resolve(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; });