1/** 2 * CitrusOS browser-zoom compensation â keep shell chrome and wallpaper at a stable 3 * visual size when the visitor uses Ctrl+scroll or pinch zoom. 4 * 5 * Desktop Ctrl+/- does NOT update visualViewport.scale (pinch only). Page zoom is 6 * tracked from devicePixelRatio + innerWidth moving together. 7 */ 8(function (global) { 9 'use strict'; 10 11 if (global.CitrusBrowserZoom) return; 12 13 var pageZoom = 1; 14 var baselineInnerWidth = 0; 15 var baselineDpr = 0; 16 var rafId = 0; 17 var initialized = false; 18 var dprMedia = null; 19 20 function isCitrusPage() { 21 return document.body && document.body.classList.contains('page-citrusos'); 22 } 23 24 function clampScale(scale) { 25 if (!Number.isFinite(scale) || scale <= 0) return 1; 26 return Math.min(5, Math.max(0.25, scale)); 27 } 28 29 function recalibrateBaseline() { 30 baselineInnerWidth = global.innerWidth; 31 baselineDpr = global.devicePixelRatio || 1; 32 pageZoom = 1; 33 } 34 35 /** 36 * @returns {number} browser/page zoom factor (1 = 100%) 37 */ 38 function readPageZoom() { 39 var innerWidth = global.innerWidth; 40 var dpr = global.devicePixelRatio || 1;
41 var pinch = global.visualViewport && typeof global.visualViewport.scale === 'number' 42 ? global.visualViewport.scale 43 : 1; 44 45 if (Math.abs(pinch - 1) > 0.001) { 46 baselineInnerWidth = innerWidth; 47 baselineDpr = dpr; 48 pageZoom = clampScale(pinch); 49 return pageZoom; 50 } 51 52 if (!baselineInnerWidth) { 53 recalibrateBaseline(); 54 return 1; 55 } 56 57 var zoomByWidth = baselineInnerWidth / innerWidth; 58 var zoomByDpr = dpr / baselineDpr; 59 60 if (Math.abs(zoomByWidth - zoomByDpr) < 0.05) { 61 pageZoom = clampScale(zoomByWidth); 62 if (Math.abs(pageZoom - 1) < 0.02) { 63 recalibrateBaseline(); 64 return 1; 65 } 66 return pageZoom; 67 } 68 69 if (Math.abs(zoomByDpr - 1) < 0.01) { 70 baselineInnerWidth = innerWidth; 71 baselineDpr = dpr; 72 return pageZoom; 73 } 74 75 recalibrateBaseline(); 76 return 1; 77 } 78 79 function isCompensating(scale) { 80 return Math.abs(scale - 1) > 0.001; 81 } 82 83 function apply() { 84 if (!isCitrusPage()) return; 85 86 var prevScale = pageZoom; 87 var scale = readPageZoom(); 88 var changed = Math.abs(scale - prevScale) > 0.001; 89 var root = document.documentElement; 90 var compensate = 1 / scale; 91 92 root.style.setProperty('--citrusos-browser-zoom', String(scale)); 93 root.style.setProperty('--citrusos-zoom-compensate', String(compensate)); 94 document.body.classList.toggle('citrusos-browser-zoom-active', isCompensating(scale)); 95 96 if (!changed) return; 97 98 try { 99 global.dispatchEvent(new CustomEvent('pupwife:citrus-browser-zoom', { 100 detail: { scale: scale, compensate: compensate } 101 })); 102 } catch (e) { /* ignore */ } 103 try { 104 global.dispatchEvent(new Event('resize')); 105 } catch (err) { /* ignore */ } 106 } 107 108 function scheduleApply() { 109 if (rafId) return; 110 rafId = global.requestAnimationFrame(function () { 111 rafId = 0; 112 apply(); 113 }); 114 } 115 116 function onWheel(ev) { 117 if (!ev.ctrlKey && !ev.metaKey) return; 118 scheduleApply(); 119 } 120 121 function onKeyDown(ev) { 122 if (!ev.ctrlKey && !ev.metaKey) return; 123 if (ev.key === '0' || ev.key === '=' || ev.key === '+' || ev.key === '-' || ev.key === '_') { 124 scheduleApply(); 125 } 126 } 127 128 function bindDprWatcher() { 129 function attach() { 130 if (dprMedia) { 131 dprMedia.removeEventListener('change', onDprMediaChange); 132 } 133 var query = '(resolution: ' + (global.devicePixelRatio || 1) + 'dppx)'; 134 dprMedia = global.matchMedia(query); 135 dprMedia.addEventListener('change', onDprMediaChange); 136 } 137 138 function onDprMediaChange() { 139 scheduleApply(); 140 attach(); 141 } 142 143 attach(); 144 } 145 146 function ensureZoomLayerClasses() { 147 ['page-background', 'aurora-overlay', 'webgl-overlay'].forEach(function (id) { 148 var el = document.getElementById(id); 149 if (!el) return; 150 el.classList.add('citrusos-browser-zoom-layer', 'citrusos-browser-zoom-layer--viewport'); 151 }); 152 } 153 154 function bind() { 155 if (initialized || !isCitrusPage()) return; 156 initialized = true; 157 ensureZoomLayerClasses(); 158 apply(); 159 global.addEventListener('resize', scheduleApply, { passive: true }); 160 global.addEventListener('wheel', onWheel, { passive: true }); 161 global.addEventListener('keydown', onKeyDown);
162 if (global.visualViewport) { 163 global.visualViewport.addEventListener('resize', scheduleApply); 164 } 165 bindDprWatcher(); 166 } 167 168 function toLogicalDelta(dx, dy) { 169 if (!isCompensating(pageZoom)) { 170 return { dx: dx, dy: dy }; 171 } 172 return { dx: dx * pageZoom, dy: dy * pageZoom }; 173 } 174 175 function init() { 176 bind(); 177 } 178 179 global.CitrusBrowserZoom = { 180 init: init, 181 refresh: apply, 182 recalibrate: function () { 183 recalibrateBaseline(); 184 apply(); 185 }, 186 getScale: function () { return pageZoom; }, 187 getCompensate: function () { return isCompensating(pageZoom) ? 1 / pageZoom : 1; }, 188 isCompensating: function () { return isCompensating(pageZoom); }, 189 toLogicalDelta: toLogicalDelta 190 }; 191 192 if (document.readyState === 'loading') { 193 document.addEventListener('DOMContentLoaded', bind); 194 } else { 195 bind(); 196 } 197})(typeof window !== 'undefined' ? window : this);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.