Steven
Steven8 min læsning

De to fejltilstande hos et klik-igennem-overlay

GeekByes vindue svæver oven på alt og lader dine klik passere gennem det — undtagen der, hvor det har knapper. Det er en tosidet kontrakt, og v1.8.5 og v1.8.14 er, hvordan det ser ud, når hver side går i stykker: én release, hvor overlayet slugte en systemdialog, én hvor det stjal dine tastetryk. Den vindende rettelse til den anden var at slette kode.

Udvikling
Electron
Desktop
GeekBye-releaser
De to fejltilstande hos et klik-igennem-overlay

GeekByes hovedvindue er ikke et normalt vindue. Det er rammeløst, transparent og altid øverst — en glasplade, der svæver over hvad end du laver, med et par kontroller malet på sig. Den interessante ingeniørkunst er ikke, at det svæver; det er, at din mus skal gå lige gennem glasset alle steder undtagen på kontrollerne. Klik på en knap på overlayet, og overlayet får klikket. Klik på det tomme rum ved siden af, og klikket lander i din mødeapp nedenunder, som om overlayet slet ikke var der.

Det er en kontrakt med to sider: modtag input præcis der, hvor der er UI, og vær fuldstændig usynlig for input alle andre steder. Begge sider skal holde i hvert øjeblik, og hver bug i denne del af appen er en overtrædelse af den ene eller den anden side. To releaser, to måneder fra hinanden, er den renest mulige illustration — fordi hver af dem brød en forskellig side.

Kontrakten, mekanisk

Implementeringen er et hook, useMousePassthrough, og det er lille. Vinduet er klik-igennem som standard. En global mousemove-lytter stiller ét spørgsmål ved hver bevægelse: er markøren over en kontrol? Kontroller er tagget med et data-ui-region-attribut, så testen er bogstaveligt target.closest('[data-ui-region]'). Over en kontrol kalder den setIgnoreMouseEvents(false), og vinduet fanger klik. Over transparent rum kalder den setIgnoreMouseEvents(true, { forward: true }), og klikkene falder igennem.

Det forward: true er den bærende detalje. Når et vindue først ignorerer musehændelser, holder det normalt helt op med at modtage dem — inklusive de mousemove-hændelser, som hooket har brug for til at bemærke markøren komme tilbage over en kontrol. forward: true beder Electron om at blive ved med at videresende bevægelser til det ignorerende vindue alligevel, så det kan genindstille sig selv. Uden det ville overlayet gå klik-igennem én gang og aldrig komme tilbage. Hooket kalder også kun over IPC-grænsen ved tilstands_overgange_ — markøren, der krydser grænsen mellem UI og glas — ikke ved hver pixel af bevægelse. Husk den optimering; den har et hul, og v1.8.5 faldt i det.

v1.8.5: når transparenssiden går i stykker

Kontraktens transparensside er "vær usynlig for input alle steder, hvor der ikke er UI." v1.8.5 er, hvordan det ser ud, når overlayet er for nærværende — når det fanger eller blokerer input, det burde have ladet passere. Udløseren var macOS tilladelsesdialoger, og der var to distinkte fejl stablet oven på hinanden.

Overlayet var fysisk over dialogen. Et altid-øverst-vindue har et niveau, og GeekByes overlay sad på det højeste, macOS tilbyder — screen-saver. Det er over næsten alt, inklusive systemets egne Microphone- og Screen Recording-tilladelsesdialoger. Så under onboarding, i præcis det øjeblik macOS bad om tilladelse, kunne dialogen rendere under overlayet og være usynlig. Rettelsen, setPermissionMode, er kun macOS og gør det uintuitive: den sænker overlayet til floating-niveauet under en tilladelsesanmodning og gendanner det derefter. Og at gendanne det krævede et bestemt trick, dokumenteret direkte i commiten: macOS "doesn't always visually re-raise a window when setting the same level it was previously at," så koden trin-for-trin fører vinduet gennem et mellemniveau — floating → status → screen-saver — for at tvinge vinduesserveren til at genberegne stablingsrækkefølgen. Du sætter ikke det niveau, du vil have; du går hen til det.

Overlayet spiste dialogens klik. Onboarding maler en mørk fuldvindues-baggrund, og passthrough-hooket særbehandler den: mens den baggrund er oppe, tvinger det klik-fangst på hele vinduet, så guiden er fuldt interagerbar. Men "fang hvert klik" er præcis forkert, når en systemdialog forsøger at indsamle ét. Rettelsen fjerner baggrundens markørattribut under en tilladelsesanmodning, så hooket falder ud af fangsttilstand, og dialogens knapper får deres klik. Overlayet holder sine egne kontroller interagerbare; det holder bare op med at lade som om det tomme glas er en knap.

Hullet i optimeringen. Husk, at hooket kun handler på overgange. Da vinduet forlod fangsttilstand, kørte det hovertesten igen — men hvis din markør allerede sad på en kontrol i det øjeblik, var svaret på "er vi over UI?" allerede "ja," så ingen overgang blev udløst, og klik-igennem blev aldrig deaktiveret. Kontrollen så levende ud og var død. Rettelsen (1b6eb20, citeret) holder op med at 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 skifter tilstand, venter du ikke på, at den næste bevægelse fortæller dig, hvor markøren er — du kigger.

Og gør afgivelsen reversibel. Den subtile risiko i alt dette er, at overlayet nu midlertidigt opgiver sin z-orden og sin klik-fangst, og noget skal give dem tilbage. Hvis gendannelseskaldet kaster — et IPC-hikke, et vindue revet ned midt i anmodningen — kunne appen blive strandet med isRequestingPermission hængende på true og en mørk baggrund, der skjuler hele din skærm, permanent. Så udgangsstien blev pakket ind, så tilstandsnulstillingen "altid udføres," plus en flere-minutters sikkerhedstimeout og en oprydning ved vinduesødelæggelse. Reglen, der falder ud: hver gang et overlay afgiver input, skal af-afgivelsen ligge i et finally, ikke i en lykkelig sti.

v1.8.14: når fangstsiden går i stykker

Kontraktens anden side er "modtag input, hvor der er UI." v1.8.14 er, hvordan det ser ud, når overlayet er for grådigt — når det at interagere med det koster dig input-fokus for den app, du faktisk går op i.

Symptomet i ændringsloggen er fokustyveri, og her er den ærlige version, fordi git-historikken er mere lærerig end release-noten. Overlayets genvejshåndterere — skift synlighed på Cmd+B, åbn chatvinduet, scroll det — kaldte hver især før mainWindow.focus(), og på macOS app.focus({ steal: true }), for at sikre, at overlayet var forrest, når du udløste det. Problemet er, at det at fokusere overlayet deaktiverer hvad end der var forrest før — dit møde, din editor — så et tastetryk beregnet til dem kunne lande ingen steder eller det forkerte sted. Overlayet greb en ting, det ikke faktisk havde brug for.

Den sidste del er hele lektien, og teamet fandt den på den hårde måde. Grenen, der blev til v1.8.14, forsøgte først at løse specifikt træk — for træk har sin egen smukke version af denne bug: når du trækker et fuldskærms-overlay, bevæger vinduet sig under en stillestående markør, så hover-modellen tror, at pegeren "forlod" kontrollen og skifter til klik-igennem midt i trækket, hvorved trækket tabes. Så de tilføjede en dragLock. Så prøvede de at gøre hele vinduet ikke-fokuserbart — hvilket brød skrivning og træk, så de gendannede fokuserbarheden, indsnævrede derefter ikke-fokuserbarheden til kun tekstfelter og rev derefter omskifteren ud. De byggede en egen træk-IPC for at erstatte Electrons native træk. De tilføjede sløring-ved-uventet-fokus. En uge af dette.

Så slettede de næsten det hele. Commiten, der blev shippet, rullede det egne træk, fokuserbarheds-omskifteren og sløringshåndtereren tilbage og beholdt præcis én idé: hold op med at kalde focus(). Erkendelsen, skrevet ind i en kodekommentar, er, at overlayet aldrig havde brug for OS-fokus i første omgang — "IPC messages (webContents.send) work without window focus." Overlayet driver sin egen renderer direkte; fokus var ren følgeskade. Den skibbare rettelse var ikke den smarte egen-træk-omskrivning bygget på en dag. Det var at fratrække de fokuskald, der ikke burde have været der. (Værd at sige lige ud: ændringsloggen siger "while dragging it," men det, der faktisk blev shippet, var at fjerne fokustyveri fra tastaturgenvejs-håndtererne — det træk-specifikke maskineri blev udforsket og rullet tilbage samme uge.)

Indsatserne, gjort bogstavelige

Hvis du vil have bevis på, at denne kontrakt ikke er akademisk, så se på, hvad der skete måneder senere, da overlayet sad fast på den forkerte side af den. I en meget senere release crashede renderer-processen, mens overlayet var i fangsttilstand — setIgnoreMouseEvents(false), vinduet griber alle klik — og uden nogen levende UI til at frigive det sad det usynlige fuld-skrivebords-vindue der og spiste hvert klik på brugerens maskine, indtil de tvangslukkede. Et transparent vindue, der sidder fast i fangsttilstand, er ikke en død knap; det er en skrivebordsdækkende klikfælde. (Den fik sin egen diagnose — se når din AI-notetager holder op med at optage midt i mødet, der parrer den med en meget anderledes bug i samme release.) Det er det skarpeste udtryk for hele problemet: få omskifteren til at sidde fast den ene vej, og dit UI er dødt; fast den anden vej, og hele dit skrivebord er det.

Tre ting disse releaser lærte os

  1. Et klik-igennem-overlay er en tosidet kontrakt, og begge sider fejler forskelligt. For transparent, og dine egne kontroller går døde; for grådigt, og du sluger andre apps' input. De fleste overlay-bugs er bare en af de to, og at navngive, hvilken side du er på, fortæller dig, hvor du skal kigge.
  2. Det højeste altid-øverst-niveau er ikke en præmie. At sidde over alt betyder at sidde over de systemdialoger, du har brug for, at brugeren ser. Design til, hvornår dit overlay høfligt bør træde ned — og gør det at træde op igen reversibelt i et finally, fordi en halvt gennemført afgivelse er værre end aldrig at afgive.
  3. Fratrækning slår snilde. Den vindende v1.8.14-rettelse var at slette focus()-kald, overlayet aldrig havde brug for, fundet først efter en uge med at bygge og rulle egen trækhåndtering tilbage. Når en feature bliver ved med at kæmpe mod platformen, er det første spørgsmål, om du kan fjerne det, der kæmper.

For det forrige kapitel i v1-historien, at lægge releasen i CI, to gange (v1.8.4); og for hele buen, anatomien i at shippe software til perfektion.