1export const DESKTOP_LAYOUT_SCHEMA = 2; 2export const DESKTOP_LAYOUT_STORAGE_KEY = 'jrda-desktop-layout'; 3 4const MAX_PERSISTED_WINDOWS = 12; 5const LEGACY_DESKTOP_LAYOUT_SCHEMA = 1; 6const MAX_POSITION_OFFSET = 4096; 7const CASCADE_STEP = 32; 8const CASCADE_COLUMNS = 4; 9 10// Some destinations are alternate presentations of one underlying surface. 11// Keeping each pair in one family prevents duplicate control/heading IDs and 12// mirrors the standard archive's existing layout and guided-path switches. 13export const desktopWindowFamily = (id) => { 14 if (id === 'archive' || id === 'folders') return 'archive'; 15 if (id === 'start' || id === 'findings') return 'start'; 16 return id; 17}; 18 19export const emptyDesktopLayout = () => ({ 20 schema: DESKTOP_LAYOUT_SCHEMA, 21 windows: [], 22 zOrder: [], 23}); 24 25const allowedIdSet = (allowedIds) => new Set( 26 Array.isArray(allowedIds) ? allowedIds.filter((id) => typeof id === 'string') : [], 27); 28 29const cascadePosition = (index) => { 30 const safeIndex = Math.max(0, index); 31 return { 32 x: (safeIndex % CASCADE_COLUMNS) * CASCADE_STEP, 33 y: safeIndex * CASCADE_STEP, 34 }; 35}; 36 37const nextCascadePosition = (windows) => { 38 const occupied = new Set( 39 windows.map(({ position }) => `${position.x},${position.y}`), 40 ); 41 for (let index = 0; index <= MAX_PERSISTED_WINDOWS; index += 1) { 42 const candidate = cascadePosition(index); 43 if (!occupied.has(`${candidate.x},${candidate.y}`)) return candidate; 44 } 45 return cascadePosition(windows.length); 46}; 47 48const normalizedPosition = (position) => { 49 if ( 50 !position 51 || typeof position.x !== 'number' 52 || typeof position.y !== 'number' 53 || !Number.isFinite(position.x) 54 || !Number.isFinite(position.y) 55 || Math.abs(position.x) > MAX_POSITION_OFFSET 56 || Math.abs(position.y) > MAX_POSITION_OFFSET 57 ) { 58 return null; 59 } 60 return { x: Math.round(position.x), y: Math.round(position.y) }; 61}; 62 63export function normalizeDesktopLayout(candidate, allowedIds) { 64 const allowed = allowedIdSet(allowedIds); 65 const supportedSchema = candidate?.schema === LEGACY_DESKTOP_LAYOUT_SCHEMA 66 || candidate?.schema === DESKTOP_LAYOUT_SCHEMA; 67 if (!supportedSchema || !Array.isArray(candidate.windows)) { 68 return emptyDesktopLayout(); 69 } 70 71 const windowsByFamily = new Map(); 72 for (const entry of candidate.windows.slice(0, MAX_PERSISTED_WINDOWS)) { 73 if (!entry || !allowed.has(entry.id)) continue; 74 windowsByFamily.set(desktopWindowFamily(entry.id), { 75 id: entry.id, 76 minimized: entry.minimized === true, 77 candidatePosition: candidate.schema === DESKTOP_LAYOUT_SCHEMA 78 ? normalizedPosition(entry.position) 79 : null, 80 }); 81 } 82 const candidateWindows = [...windowsByFamily.values()]; 83 const openIds = new Set(candidateWindows.map((entry) => entry.id)); 84 const zOrder = []; 85 const seen = new Set(); 86 87 if (Array.isArray(candidate.zOrder)) { 88 for (const id of candidate.zOrder) { 89 if (!openIds.has(id) || seen.has(id)) continue; 90 seen.add(id); 91 zOrder.push(id); 92 } 93 } 94 for (const { id } of candidateWindows) { 95 if (seen.has(id)) continue; 96 seen.add(id); 97 zOrder.push(id); 98 } 99 100 const cascadeIndexById = new Map(zOrder.map((id, index) => [id, index])); 101 const windows = candidateWindows.map(({ id, minimized, candidatePosition }) => ({ 102 id, 103 minimized, 104 position: candidatePosition || cascadePosition(cascadeIndexById.get(id) || 0), 105 })); 106 107 return { schema: DESKTOP_LAYOUT_SCHEMA, windows, zOrder }; 108} 109 110export function parseDesktopLayout(rawValue, allowedIds) { 111 if (typeof rawValue !== 'string' || rawValue === '') return emptyDesktopLayout(); 112 try { 113 return normalizeDesktopLayout(JSON.parse(rawValue), allowedIds); 114 } catch { 115 return emptyDesktopLayout(); 116 } 117} 118 119export const serializeDesktopLayout = (layout, allowedIds) => ( 120 JSON.stringify(normalizeDesktopLayout(layout, allowedIds)) 121); 122 123export function activateDesktopWindow(layout, id, allowedIds) { 124 const allowed = allowedIdSet(allowedIds); 125 if (!allowed.has(id)) return normalizeDesktopLayout(layout, allowedIds); 126 127 const current = normalizeDesktopLayout(layout, allowedIds); 128 const family = desktopWindowFamily(id); 129 const familyIndex = current.windows.findIndex( 130 (entry) => desktopWindowFamily(entry.id) === family, 131 ); 132 const windows = familyIndex >= 0 133 ? current.windows.map((entry, index) => ( 134 index === familyIndex ? { ...entry, id, minimized: false } : entry 135 )) 136 : current.windows.concat({ 137 id, 138 minimized: false, 139 position: nextCascadePosition(current.windows), 140 }); 141 const retainedIds = new Set(windows.map((entry) => entry.id)); 142 const zOrder = current.zOrder 143 .filter((entryId) => retainedIds.has(entryId) && desktopWindowFamily(entryId) !== family) 144 .concat(id); 145 146 return { schema: DESKTOP_LAYOUT_SCHEMA, windows, zOrder }; 147} 148 149export function moveDesktopWindow(layout, id, position, allowedIds) { 150 const allowed = allowedIdSet(allowedIds); 151 const nextPosition = normalizedPosition(position); 152 const current = normalizeDesktopLayout(layout, allowedIds); 153 if (!allowed.has(id) || !nextPosition || !current.windows.some((entry) => entry.id === id)) { 154 return current; 155 } 156 return { 157 ...current, 158 windows: current.windows.map((entry) => ( 159 entry.id === id ? { ...entry, position: nextPosition } : entry 160 )), 161 }; 162} 163 164const finiteRect = (rect, fields) => ( 165 rect && fields.every((field) =>
165 typeof rect[field] === 'number' && Number.isFinite(rect[field])) 166); 167 168export function clampDesktopWindowPosition(position, geometry = {}) { 169 const candidate = normalizedPosition(position); 170 const currentPosition = normalizedPosition(geometry.currentPosition); 171 const { 172 frameRect, 173 titlebarRect, 174 workArea, 175 margin = 8, 176 minimumVisibleHeight = 128, 177 } = geometry; 178 if ( 179 !candidate 180 || !currentPosition 181 || !finiteRect(frameRect, ['left', 'right', 'top', 'bottom']) 182 || !finiteRect(titlebarRect, ['left', 'right', 'top', 'bottom']) 183 || !finiteRect(workArea, ['left', 'right', 'top', 'bottom']) 184 || !Number.isFinite(margin) 185 || !Number.isFinite(minimumVisibleHeight) 186 ) { 187 return currentPosition || { x: 0, y: 0 }; 188 } 189 190 let minX = currentPosition.x + workArea.left + margin - titlebarRect.left; 191 let maxX = currentPosition.x + workArea.right - margin - titlebarRect.right; 192 if (minX > maxX) { 193 const centered = currentPosition.x 194 + ((workArea.left + workArea.right) / 2) 195 - ((titlebarRect.left + titlebarRect.right) / 2); 196 minX = centered; 197 maxX = centered; 198 } 199 200 const visibleHeight = Math.min( 201 Math.max(titlebarRect.bottom - frameRect.top, minimumVisibleHeight), 202 frameRect.bottom - frameRect.top, 203 ); 204 const minY = currentPosition.y + workArea.top + margin - titlebarRect.top; 205 let maxY = currentPosition.y 206 + workArea.bottom 207 - margin 208 - visibleHeight 209 - frameRect.top; 210 // When extreme zoom or browser chrome leaves less room than the requested 211 // visible body height, the title bar is the non-negotiable recovery handle. 212 if (minY > maxY) maxY = minY; 213 214 return { 215 x: Math.round(Math.min(maxX, Math.max(minX, candidate.x))), 216 y: Math.round(Math.min(maxY, Math.max(minY, candidate.y))), 217 }; 218} 219 220export function minimizeDesktopWindow(layout, id, allowedIds) { 221 const current = normalizeDesktopLayout(layout, allowedIds); 222 return { 223 ...current, 224 windows: current.windows.map((entry) => ( 225 entry.id === id ? { ...entry, minimized: true } : entry 226 )), 227 }; 228} 229 230export function closeDesktopWindow(layout, id, allowedIds) { 231 const current = normalizeDesktopLayout(layout, allowedIds); 232 return { 233 schema: DESKTOP_LAYOUT_SCHEMA, 234 windows: current.windows.filter((entry) => entry.id !== id), 235 zOrder: current.zOrder.filter((entryId) => entryId !== id), 236 }; 237} 238 239export function nextVisibleDesktopWindow(layout, excludedId = null) { 240 const minimizedById = new Map(layout.windows.map((entry) => [entry.id, entry.minimized])); 241 for (let index = layout.zOrder.length - 1; index >= 0; index -= 1) { 242 const id = layout.zOrder[index]; 243 if (id !== excludedId && minimizedById.get(id) === false) return id; 244 } 245 return null; 246}
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.