
De to feilmodusene til et klikk-gjennom-overlegg
GeekByes vindu svever oppå alt og lar klikkene dine passere gjennom det — bortsett fra der det har knapper. Det er en tosidig kontrakt, og v1.8.5 og v1.8.14 er hvordan det ser ut når hver side ryker: én release der overlegget slukte en systemdialog, én der det stjal tastetrykkene dine. Den vinnende fiksen for den andre var å slette kode.
GeekByes hovedvindu er ikke et normalt vindu. Det er rammeløst, transparent og alltid øverst — en glassplate som svever over hva enn du gjør, med et par kontroller malt på seg. Den interessante ingeniørkunsten er ikke at det svever; det er at musen din skal gå rett gjennom glasset overalt bortsett fra på kontrollene. Klikk på en knapp på overlegget, og overlegget får klikket. Klikk på det tomme rommet ved siden av, og klikket lander i møteappen din under, som om overlegget ikke var der i det hele tatt.
Det er en kontrakt med to sider: motta inndata nøyaktig der det finnes UI, og vær fullstendig usynlig for inndata alle andre steder. Begge sider må holde i hvert øyeblikk, og hver bug i denne delen av appen er et brudd på den ene eller andre siden. To releaser, to måneder fra hverandre, er den renest mulige illustrasjonen — fordi hver av dem brøt en forskjellig side.
Kontrakten, mekanisk
Implementasjonen er en hook, useMousePassthrough, og den er liten. Vinduet er klikk-gjennom som standard. En global mousemove-lytter stiller ett spørsmål ved hver bevegelse: er markøren over en kontroll? Kontroller er tagget med et data-ui-region-attributt, så testen er bokstavelig talt target.closest('[data-ui-region]'). Over en kontroll kaller den setIgnoreMouseEvents(false), og vinduet fanger klikk. Over transparent rom kaller den setIgnoreMouseEvents(true, { forward: true }), og klikkene faller gjennom.
Det forward: true er den bærende detaljen. Når et vindu først ignorerer musehendelser, slutter det normalt å motta dem helt — inkludert de mousemove-hendelsene som hooken trenger for å merke markøren komme tilbake over en kontroll. forward: true ber Electron om å fortsette å videresende bevegelser til det ignorerende vinduet likevel, så det kan stille seg inn på nytt. Uten den ville overlegget gå klikk-gjennom én gang og aldri komme tilbake. Hooken kaller også bare over IPC-grensen ved tilstands_overganger_ — markøren som krysser grensen mellom UI og glass — ikke ved hver piksel av bevegelse. Husk den optimaliseringen; den har et hull, og v1.8.5 falt i det.
v1.8.5: når transparenssiden ryker
Kontraktens transparensside er "vær usynlig for inndata alle steder der det ikke finnes UI." v1.8.5 er hvordan det ser ut når overlegget er for nærværende — når det fanger eller blokkerer inndata det burde ha latt passere. Utløseren var macOS tillatelsesdialoger, og det var to distinkte feil stablet oppå hverandre.
Overlegget var fysisk over dialogen. Et alltid-øverst-vindu har et nivå, og GeekByes overlegg satt på det høyeste macOS tilbyr — screen-saver. Det er over nesten alt, inkludert systemets egne Microphone- og Screen Recording-tillatelsesdialoger. Så under onboarding, i akkurat det øyeblikket macOS ba om tillatelse, kunne dialogen rendres under overlegget og være usynlig. Fiksen, setPermissionMode, er kun macOS og gjør det uintuitive: den senker overlegget til floating-nivået under en tillatelsesforespørsel, og gjenoppretter det etterpå. Og å gjenopprette det krevde et bestemt triks, dokumentert rett i commiten: macOS "doesn't always visually re-raise a window when setting the same level it was previously at," så koden fører vinduet steg for steg gjennom et mellomnivå — floating → status → screen-saver — for å tvinge vindusserveren til å beregne stablingsrekkefølgen på nytt. Du setter ikke nivået du vil ha; du går bort til det.
Overlegget spiste dialogens klikk. Onboarding maler en mørk fullvindus-bakgrunn, og passthrough-hooken særbehandler den: mens den bakgrunnen er oppe, tvinger den klikk-fangst på hele vinduet slik at veiviseren er fullt interagerbar. Men "fang hvert klikk" er akkurat feil når en systemdialog prøver å samle inn ett. Fiksen fjerner bakgrunnens markørattributt under en tillatelsesforespørsel, så hooken faller ut av fangstmodus og dialogens knapper får klikkene sine. Overlegget holder sine egne kontroller interagerbare; det slutter bare å late som om det tomme glasset er en knapp.
Hullet i optimaliseringen. Husk at hooken bare handler på overganger. Da vinduet forlot fangstmodus, kjørte den hovertesten på nytt — men hvis markøren din allerede satt på en kontroll i det øyeblikket, var svaret på "er vi over UI?" allerede "ja," så ingen overgang ble utløst, og klikk-gjennom ble aldri deaktivert. Kontrollen så levende ut og var død. Fiksen (1b6eb20, sitert) slutter å stole på overgangen: "when exiting onboarding mode, directly evaluate whether cursor is over a UI region and make the correct IPC call." Når du bytter modus, venter du ikke på at neste bevegelse skal fortelle deg hvor markøren er — du ser.
Og gjør avgivelsen reversibel. Den subtile risikoen i alt dette er at overlegget nå midlertidig gir opp sin z-rekkefølge og sin klikk-fangst, og noe må gi dem tilbake. Hvis gjenopprettingskallet kaster — et IPC-hikke, et vindu revet ned midt i forespørselen — kunne appen bli strandet med isRequestingPermission hengende på true og en mørk bakgrunn som skjuler hele skjermen din, permanent. Så utgangsstien ble pakket inn slik at tilstandsnullstillingen "alltid kjøres," pluss en fler-minutters sikkerhetstimeout og en opprydding ved vindusødeleggelse. Regelen som faller ut: hver gang et overlegg avgir inndata, må av-avgivelsen ligge i et finally, ikke i en lykkelig sti.
v1.8.14: når fangstsiden ryker
Kontraktens andre side er "motta inndata der det finnes UI." v1.8.14 er hvordan det ser ut når overlegget er for grådig — når det å interagere med det koster deg inndatafokus for appen du faktisk bryr deg om.
Symptomet i endringsloggen er fokustyveri, og her er den ærlige versjonen, fordi git-historikken er mer lærerik enn release-notatet. Overleggets snarveishåndterere — veksle synlighet på Cmd+B, åpne chatvinduet, scrolle det — kalte hver især før mainWindow.focus(), og på macOS app.focus({ steal: true }), for å sikre at overlegget var fremst når du utløste det. Problemet er at det å fokusere overlegget deaktiverer hva enn som var fremst før — møtet ditt, editoren din — så et tastetrykk ment for dem kunne lande ingensteds, eller på feil sted. Overlegget grep en ting det egentlig ikke trengte.
Den siste delen er hele lærdommen, og teamet fant den på den harde måten. Grenen som ble v1.8.14 prøvde først å løse spesifikt dragging — for dragging har sin egen vakre versjon av denne bugen: når du drar et fullskjerms-overlegg, beveger vinduet seg under en stillestående markør, så hover-modellen tror at pekeren "forlot" kontrollen og slår om til klikk-gjennom midt i draget, og mister draget. Så de la til en dragLock. Så prøvde de å gjøre hele vinduet ikke-fokuserbart — som brøt skriving og dragging, så de gjenopprettet fokuserbarheten, smalnet så inn ikke-fokuserbarheten til bare tekstfelter, rev så vekslingen ut. De bygde en egen dra-IPC for å erstatte Electrons native dragging. De la til uskarphet-ved-uventet-fokus. En uke med dette.
Så slettet de nesten alt. Commiten som ble skippet rullet det egne draget, fokuserbarhets-vekslingen og uskarphetshåndtereren tilbake, og beholdt nøyaktig én idé: slutt å kalle focus(). Erkjennelsen, skrevet inn i en kodekommentar, er at overlegget aldri trengte OS-fokus i utgangspunktet — "IPC messages (webContents.send) work without window focus." Overlegget driver sin egen renderer direkte; fokus var ren følgeskade. Den skipbare fiksen var ikke den smarte egen-dra-omskrivingen bygd på en dag. Det var å trekke fra de fokuskallene som ikke burde ha vært der. (Verdt å si rett ut: endringsloggen sier "while dragging it," men det som faktisk ble skippet var å fjerne fokustyveri fra tastatursnarvei-håndtererne — det drag-spesifikke maskineriet ble utforsket og rullet tilbake samme uke.)
Innsatsene, gjort bokstavelige
Hvis du vil ha bevis på at denne kontrakten ikke er akademisk, se på hva som skjedde måneder senere da overlegget satt fast på feil side av den. I en mye senere release krasjet renderer-prosessen mens overlegget var i fangstmodus — setIgnoreMouseEvents(false), vinduet griper alle klikk — og uten noe levende UI til å frigi det satt det usynlige fullskrivebords-vinduet der og spiste hvert klikk på brukerens maskin til de tvangsavsluttet. Et transparent vindu som sitter fast i fangstmodus er ikke en død knapp; det er en skrivebordsdekkende klikkfelle. (Den fikk sin egen diagnose — se når AI-notatskriveren din slutter å ta opp midt i møtet, som parer den med en veldig annerledes bug i samme release.) Det er det skarpeste uttrykket for hele problemet: få vekslingen til å sitte fast den ene veien og UI-et ditt er dødt; fast den andre veien og hele skrivebordet ditt er det.
Tre ting disse releasene lærte oss
- Et klikk-gjennom-overlegg er en tosidig kontrakt, og begge sider feiler forskjellig. For transparent, og dine egne kontroller går døde; for grådig, og du sluker andre appers inndata. De fleste overlegg-bugs er bare en av de to, og å navngi hvilken side du er på forteller deg hvor du skal se.
- Det høyeste alltid-øverst-nivået er ingen premie. Å sitte over alt betyr å sitte over de systemdialogene du trenger at brukeren ser. Design for når overlegget ditt høflig bør trekke seg tilbake — og gjør det å tre opp igjen reversibelt i et
finally, fordi en halvferdig avgivelse er verre enn å aldri avgi. - Fratrekking slår smarthet. Den vinnende v1.8.14-fiksen var å slette
focus()-kall overlegget aldri trengte, funnet først etter en uke med å bygge og rulle egen dra-håndtering tilbake. Når en funksjon fortsetter å kjempe mot plattformen, er det første spørsmålet om du kan fjerne det som kjemper.
For det forrige kapittelet i v1-historien, å legge releasen i CI, to ganger (v1.8.4); og for hele buen, anatomien i å shippe programvare til perfeksjon.