Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 32 additions & 5 deletions src/extensionsIntegrated/Terminal/TerminalInstance.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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.
*
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand Down
17 changes: 16 additions & 1 deletion src/phoenix/shell.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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});
}
Expand Down
29 changes: 29 additions & 0 deletions src/styles/brackets.less
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
25 changes: 20 additions & 5 deletions src/view/ViewCommandHandlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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);
}
}

Expand Down
60 changes: 60 additions & 0 deletions src/widgets/NotificationUI.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = $('<div class="hud-overlay">' +
'<i class="' + iconClass + '"></i>' +
'<div class="hud-label">' + label + '</div>' +
'</div>');
$("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;
});
Loading