
Els dos modes de fallada d'un overlay click-through
La finestra de GeekBye flota per damunt de tot i deixa que els teus clics la travessin — excepte on té botons. Això és un contracte de dues cares, i la v1.8.5 i la v1.8.14 mostren com és quan cada cara es trenca: una release on l'overlay es va empassar un diàleg del sistema, una altra on et robava les pulsacions de tecla. La solució guanyadora per a la segona va ser esborrar codi.
La finestra principal de GeekBye no és una finestra normal. És sense marc, transparent i sempre al damunt — un full de vidre flotant per damunt del que sigui que estiguis fent, amb uns quants controls pintats a sobre. L'enginyeria interessant no és que floti; és que se suposa que el teu ratolí travessa el vidre directament a tot arreu excepte als controls. Fes clic a un botó de l'overlay i l'overlay rep el clic. Fes clic a l'espai buit del costat i el clic aterra a la teva app de reunió de sota, com si l'overlay no hi fos gens.
Això és un contracte amb dues cares: rep input exactament on hi ha UI, i sigues completament invisible a l'input a tota la resta. Totes dues cares han d'aguantar a cada instant, i cada bug en aquesta part de l'app és una violació d'una cara o de l'altra. Dues releases, amb dos mesos de diferència, en són la il·lustració més neta possible — perquè cadascuna va trencar una cara diferent.
El contracte, mecànicament
La implementació és un hook, useMousePassthrough, i és petit. La finestra és click-through per defecte. Un listener global de mousemove fa una pregunta a cada moviment: el cursor és sobre un control? Els controls estan etiquetats amb un atribut data-ui-region, així que la prova és literalment target.closest('[data-ui-region]'). Sobre un control, crida setIgnoreMouseEvents(false) i la finestra captura els clics. Sobre espai transparent, crida setIgnoreMouseEvents(true, { forward: true }) i els clics travessen.
Aquell forward: true és el detall de càrrega. Un cop una finestra ignora els esdeveniments del ratolí, normalment deixa de rebre'ls del tot — inclosos els esdeveniments mousemove que el hook necessita per adonar-se que el cursor torna sobre un control. forward: true diu a Electron que continuï reenviant els moviments a la finestra que els ignora igualment, així es pot rearmar. Sense això, l'overlay es tornaria click-through una vegada i no tornaria mai més. El hook també només crida a través de la frontera IPC en les transicions d'estat — el cursor creuant la frontera entre UI i vidre — no a cada píxel de moviment. Recorda aquella optimització; té un forat, i la v1.8.5 hi va caure.
v1.8.5: quan es trenca la cara de la transparència
La cara de la transparència del contracte és "sigues invisible a l'input a tot arreu on no hi ha UI." La v1.8.5 mostra com és quan l'overlay és massa present — quan captura o bloqueja input que hauria d'haver deixat passar. El detonant van ser els diàlegs de permisos de macOS, i hi havia dues fallades diferents apilades l'una sobre l'altra.
L'overlay era físicament per damunt del diàleg. Una finestra sempre-al-damunt té un nivell, i l'overlay de GeekBye seia al més alt que ofereix macOS — screen-saver. Això és per damunt de gairebé tot, inclosos els propis diàlegs de permisos de Microphone i Screen Recording del sistema. Així que durant l'onboarding, en l'instant exacte que macOS demanava permís, el diàleg podia renderitzar-se per sota de l'overlay i ser invisible. La solució, setPermissionMode, és només per a macOS i fa la cosa poc intuïtiva: abaixa l'overlay al nivell floating durant la petició de permís, i el restaura després. I restaurar-lo necessitava un truc específic, documentat justament al commit: macOS "doesn't always visually re-raise a window when setting the same level it was previously at," així que el codi fa passar la finestra per un nivell intermedi — floating → status → screen-saver — per forçar el window server a recalcular l'ordre d'apilament. No poses el nivell que vols; hi camines.
L'overlay s'empassava els clics del diàleg. L'onboarding pinta un fons fosc que omple tota la finestra, i el hook de passthrough el tracta com a cas especial: mentre aquell fons és a dalt, força la captura de clics a tota la finestra perquè l'assistent sigui plenament interactiu. Però "captura cada clic" és exactament el que no toca quan un diàleg del sistema n'està intentant recollir un. La solució treu l'atribut marcador del fons durant una petició de permís, així el hook surt del mode captura i els botons del diàleg reben els seus clics. L'overlay manté interactius els seus propis controls; només deixa de fer veure que el vidre buit és un botó.
El forat de l'optimització. Recorda que el hook només actua en les transicions. Quan la finestra va sortir del mode captura, va tornar a executar la prova de hover — però si el teu cursor ja estava assegut sobre un control en aquell moment, la resposta a "estem sobre UI?" ja era "sí," així que no es va disparar cap transició, i el click-through no es va desactivar mai. El control semblava viu i era mort. La solució (1b6eb20, citada) deixa de fiar-se de la transició: "when exiting onboarding mode, directly evaluate whether cursor is over a UI region and make the correct IPC call." Quan canvies de mode, no esperes que el proper moviment et digui on és el cursor — mires.
I fes la cessió reversible. El risc subtil en tot això és que l'overlay ara està renunciant temporalment al seu ordre-z i a la seva captura de clics, i alguna cosa els ha de tornar. Si la crida de restauració llança una excepció — un ensopec d'IPC, una finestra desmuntada a mitja petició — l'app podria quedar encallada amb isRequestingPermission clavat a true i un fons fosc amagant-te tota la pantalla, permanentment. Així que el camí de sortida es va embolcallar perquè el reset d'estat "s'executi sempre," més un temporitzador de seguretat de diversos minuts i una neteja en destruir la finestra. La regla que en surt: cada cop que un overlay cedeix input, el des-cedir ha d'estar en un finally, no en un camí feliç.
v1.8.14: quan es trenca la cara de la captura
L'altra cara del contracte és "rep input on hi ha UI." La v1.8.14 mostra com és quan l'overlay és massa àvid d'agafar — quan interactuar-hi et costa el focus d'input de l'app que de debò t'importa.
El símptoma al changelog és el robatori de focus, i aquí ve la versió honesta, perquè l'historial de git és més instructiu que la nota de la release. Els gestors de dreceres de l'overlay — commutar la visibilitat amb Cmd+B, obrir la finestra de xat, fer-hi scroll — cadascun cridava mainWindow.focus(), i a macOS app.focus({ steal: true }), per assegurar que l'overlay era al davant quan el disparaves. El problema és que focalitzar l'overlay desactiva el que fos que era al davant abans — la teva reunió, el teu editor — així que una pulsació de tecla pensada per a ells podia aterrar enlloc, o al lloc equivocat. L'overlay agafava una cosa que en realitat no necessitava.
Aquesta última part és tota la lliçó, i l'equip la va trobar a les males. La branca que va esdevenir la v1.8.14 primer va intentar resoldre específicament l'arrossegament — perquè arrossegar té la seva pròpia versió preciosa d'aquest bug: quan arrossegues un overlay a pantalla completa, la finestra es mou sota un cursor immòbil, així que el model de hover creu que el punter va "sortir" del control i canvia a click-through a mig arrossegament, deixant caure l'arrossegament. Així que van afegir un dragLock. Després van provar de fer tota la finestra no-focalitzable — cosa que va trencar l'escriptura i l'arrossegament, així que van restaurar la focalitzabilitat, després van estrènyer la no-focalitzabilitat només als camps de text, i després van arrencar el commutador. Van construir una IPC de drag personalitzada per substituir el drag natiu d'Electron. Van afegir blur-en-focus-inesperat. Una setmana d'això.
Després en van esborrar gairebé tot. El commit que es va lliurar va revertir el drag personalitzat, el commutador de focalitzable i el gestor de blur, i va conservar exactament una idea: deixar de cridar focus(). La constatació, escrita en un comentari de codi, és que l'overlay mai no va necessitar el focus del SO d'entrada — "IPC messages (webContents.send) work without window focus." L'overlay condueix el seu propi renderer directament; el focus era pur dany col·lateral. La solució lliurable no va ser la reescriptura enginyosa del drag personalitzat construïda en un dia. Va ser sostreure les crides de focus que no hi havien d'haver estat mai. (Val la pena dir-ho clarament: el changelog diu "while dragging it," però el que realment es va lliurar va ser treure el robatori de focus dels gestors de dreceres de teclat — la maquinària específica de drag es va explorar i revertir la mateixa setmana.)
El que està en joc, fet literal
Si vols una prova que aquest contracte no és acadèmic, mira què va passar mesos després quan l'overlay va quedar encallat a la cara equivocada. En una release molt posterior, el procés del renderer va petar mentre l'overlay era en mode captura — setIgnoreMouseEvents(false), la finestra agafant tots els clics — i sense cap UI viva per alliberar-lo, la finestra invisible que ocupa tot l'escriptori es va quedar allà empassant-se cada clic de la màquina de l'usuari fins que va forçar-ne la sortida. Una finestra transparent encallada en mode captura no és un botó mort; és un parany de clics per tot l'escriptori. (Aquella va tenir el seu propi diagnòstic — vegeu quan el teu assistent de notes amb IA deixa de gravar a mitja reunió, que l'aparella amb un bug ben diferent de la mateixa release.) És la formulació més afilada de tot el problema: deixa el commutador encallat d'una manera i la teva UI és morta; encallat de l'altra i ho és tot el teu escriptori.
Tres coses que aquestes releases ens van ensenyar
- Un overlay click-through és un contracte de dues cares, i totes dues cares fallen de manera diferent. Massa transparent i els teus propis controls es moren; massa àvid d'agafar i t'empasses l'input d'altres apps. La majoria de bugs d'overlay són només un d'aquests dos, i anomenar en quina cara ets et diu on has de mirar.
- El nivell sempre-al-damunt més alt no és cap premi. Seure per damunt de tot significa seure per damunt dels diàlegs del sistema que necessites que l'usuari vegi. Dissenya per al moment en què el teu overlay hauria de fer un pas enrere educadament — i fes que tornar a pujar sigui reversible en un
finally, perquè una cessió a mig fer és pitjor que no cedir mai. - La sostracció venç la intel·ligència. La solució guanyadora de la v1.8.14 va ser esborrar crides
focus()que l'overlay mai no va necessitar, trobades només després d'una setmana de construir i revertir la gestió de drag personalitzada. Quan una funcionalitat no para de barallar-se amb la plataforma, la primera pregunta a fer-se és si pots eliminar la cosa que fa la baralla.
Per al capítol anterior de la història de la v1, posar la release a CI, dues vegades (v1.8.4); i per a tot l'arc, l'anatomia de publicar programari fins a la perfecció.