
Dva režimy zlyhania prekrytia prepúšťajúceho kliknutia
Okno GeekBye pláva nad všetkým a púšťa tvoje kliknutia naskrz — okrem miest, kde má tlačidlá. To je dvojstranná zmluva a v1.8.5 a v1.8.14 ukazujú, ako vyzerá zlyhanie každej zo strán: jedno vydanie, kde prekrytie prehltlo systémový dialóg, a druhé, kde ti ukradlo stlačenia klávesov. Víťazným liekom na to druhé bolo zmazanie kódu.
Hlavné okno GeekBye nie je normálne okno. Je bez rámu, priehľadné a vždy navrchu — tabuľa skla plávajúca nad tým, čo práve robíš, s pár ovládacími prvkami namaľovanými na nej. Zaujímavá inžinierina nie je v tom, že pláva; je v tom, že tvoja myš má prejsť rovno cez sklo všade okrem ovládacích prvkov. Klikni tlačidlo na prekrytí a kliknutie dostane prekrytie. Klikni prázdne miesto vedľa neho a kliknutie pristane na tvojej aplikácii na stretnutia pod ním, akoby tam prekrytie vôbec nebolo.
To je zmluva s dvoma stranami: prijímaj vstup presne tam, kde je UI, a buď úplne neviditeľné pre vstup všade inde. Obe strany musia držať v každom okamihu a každý bug v tejto časti aplikácie je porušením jednej alebo druhej. Dve vydania, dva mesiace od seba, sú najčistejšou možnou ilustráciou — pretože každé zlomilo inú stranu.
Zmluva, mechanicky
Implementáciou je hook, useMousePassthrough, a je malý. Okno predvolene prepúšťa kliknutia. Globálny poslucháč mousemove sa pri každom pohybe pýta jednu otázku: je kurzor nad ovládacím prvkom? Ovládacie prvky sú označené atribútom data-ui-region, takže test je doslova target.closest('[data-ui-region]'). Nad ovládacím prvkom volá setIgnoreMouseEvents(false) a okno zachytáva kliknutia. Nad priehľadným priestorom volá setIgnoreMouseEvents(true, { forward: true }) a kliknutia prepadnú.
To forward: true je nosný detail. Keď okno ignoruje udalosti myši, normálne ich prestane dostávať úplne — vrátane udalostí mousemove, ktoré hook potrebuje, aby si všimol, že kurzor sa vracia späť nad ovládací prvok. forward: true hovorí Electronu, nech aj tak ďalej preposiela pohyby ignorujúcemu oknu, aby sa mohlo znovu natiahnuť. Bez toho by prekrytie prešlo do režimu prepúšťania raz a nikdy by sa nevrátilo. Hook navyše prechádza cez hranicu IPC len pri prechodoch stavu — keď kurzor prekračuje hranicu medzi UI a sklom — nie pri každom pixeli pohybu. Zapamätaj si tú optimalizáciu; má dieru a v1.8.5 do nej spadlo.
v1.8.5: keď sa láme strana priehľadnosti
Strana priehľadnosti zmluvy znie „buď neviditeľné pre vstup všade tam, kde nie je UI". v1.8.5 ukazuje, ako vyzerá priveľmi prítomné prekrytie — keď zachytáva alebo blokuje vstup, ktorý mal pustiť ďalej. Spúšťačom boli dialógy povolení macOS a boli tam dve odlišné zlyhania naskladané na sebe.
Prekrytie bolo fyzicky nad dialógom. Okno vždy-navrchu má úroveň a prekrytie GeekBye sedelo na najvyššej, akú macOS ponúka — screen-saver. To je nad takmer všetkým, vrátane vlastných dialógov povolení systému Microphone a Screen Recording. Takže počas onboardingu, presne v momente, keď macOS žiadal povolenie, dialóg sa mohol vykresliť pod prekrytím a byť neviditeľný. Oprava, setPermissionMode, je len pre macOS a robí neintuitívnu vec: znižuje prekrytie na úroveň floating na čas žiadosti o povolenie a potom ho obnoví. A obnovenie potrebovalo konkrétny trik, zdokumentovaný priamo v commite: macOS „doesn't always visually re-raise a window when setting the same level it was previously at", takže kód prevedie okno cez medziúroveň — floating → status → screen-saver — aby prinútil okenný server prepočítať poradie vrstvenia. Nenastavíš úroveň, ktorú chceš; dôjdeš k nej krok za krokom.
Prekrytie zjedalo kliknutia dialógu. Onboarding maľuje tmavé pozadie na celé okno a hook prepúšťania ho rieši ako osobitný prípad: kým je to pozadie hore, vynucuje zachytávanie kliknutí na celom okne, aby bol sprievodca plne interaktívny. Ale „zachytávaj každé kliknutie" je presne nesprávne, keď sa ho systémový dialóg snaží pozbierať. Oprava sťahuje značkový atribút pozadia na čas žiadosti o povolenie, takže hook vypadne z režimu zachytávania a tlačidlá dialógu dostanú svoje kliknutia. Prekrytie si drží vlastné ovládacie prvky interaktívne; len prestáva predstierať, že prázdne sklo je tlačidlo.
Diera v optimalizácii. Pamätaj, že hook koná len pri prechodoch. Keď okno opúšťalo režim zachytávania, znovu spustilo test nájazdu — ale ak tvoj kurzor už sedel na ovládacom prvku v tom momente, odpoveď na otázku „sme nad UI?" bola už „áno", takže žiadny prechod sa nespustil a prepúšťanie kliknutí nikdy nebolo vypnuté. Ovládací prvok vyzeral živý a bol mŕtvy. Oprava (1b6eb20, citovaná) prestáva dôverovať prechodu: „when exiting onboarding mode, directly evaluate whether cursor is over a UI region and make the correct IPC call". Keď meníš režim, nečakáš, kým ti ďalší pohyb povie, kde je kurzor — pozrieš sa.
A sprav odovzdanie vratným. Jemné riziko v tomto všetkom je, že prekrytie teraz dočasne vzdáva svoj z-order a svoje zachytávanie kliknutí a niečo im ich musí dať späť. Ak volanie obnovy vyhodí výnimku — čkanie IPC, okno zbúrané uprostred žiadosti — aplikácia by mohla uviaznuť s isRequestingPermission zaseknutým na true a tmavým pozadím skrývajúcim celú tvoju obrazovku, natrvalo. Takže cesta výstupu sa obalila tak, aby sa reset stavu „vždy vykonal", plus viacminútový poistný timeout a upratanie pri zničení okna. Pravidlo, ktoré z toho vypadne: zakaždým, keď prekrytie vzdáva vstup, jeho vrátenie musí byť v finally, nie na šťastnej ceste.
v1.8.14: keď sa láme strana zachytávania
Druhá strana zmluvy je „prijímaj vstup tam, kde je UI". v1.8.14 ukazuje, ako vyzerá priveľmi chňapavé prekrytie — keď ťa interakcia s ním stojí vstupný fokus aplikácie, na ktorej ti naozaj záleží.
Symptóm v changelogu je kradnutie fokusu a tu je úprimná verzia, pretože história git je poučnejšia než poznámka k vydaniu. Obsluhy skratiek prekrytia — prepnutie viditeľnosti na Cmd+B, otvorenie okna chatu, jeho posúvanie — každá kedysi volala mainWindow.focus(), a na macOS app.focus({ steal: true }), aby bolo prekrytie navrchu, keď ho spustíš. Problém je, že fokusovanie prekrytia deaktivuje to, čo bolo navrchu predtým — tvoje stretnutie, tvoj editor — takže stlačenie klávesu určené pre ne mohlo pristáť nikde alebo na nesprávnom mieste. Prekrytie chňaplo vec, ktorú v skutočnosti nepotrebovalo.
Tá posledná časť je celá lekcia a tím na ňu prišiel ťažkou cestou. Vetva, ktorá sa stala v1.8.14, sa najprv pokúsila vyriešiť konkrétne ťahanie — pretože ťahanie má vlastnú krásnu verziu tohto bugu: keď ťaháš celoobrazovkové prekrytie, okno sa pohybuje pod nehybným kurzorom, takže model nájazdu si myslí, že ukazovateľ „opustil" ovládací prvok, a prepne sa do režimu prepúšťania uprostred ťahania, čím ťahanie zhodí. Tak pridali dragLock. Potom skúsili spraviť celé okno nefokusovateľným — čo zlomilo písanie a ťahanie, tak obnovili fokusovateľnosť, potom zúžili nefokusovateľnosť len na textové polia, potom ten prepínač vytrhli. Postavili vlastné IPC ťahania, aby nahradili natívne ťahanie Electronu. Pridali rozostrenie-pri-nečakanom-fokuse. Týždeň tohto.
Potom takmer všetko z toho zmazali. Commit, ktorý expedoval, vrátil vlastné ťahanie, prepínač fokusovateľnosti a obsluhu rozostrenia a zachoval presne jednu myšlienku: prestať volať focus(). Uvedomenie, vpísané do komentára v kóde, je, že prekrytie od začiatku nikdy nepotrebovalo fokus OS — „IPC messages (webContents.send) work without window focus". Prekrytie riadi vlastný renderer priamo; fokus bol čistá vedľajšia škoda. Liekom vhodným na expedíciu nebolo šikovné prepísanie vlastného ťahania postavené za deň. Bolo to odčítanie volaní fokusu, ktoré tam nemali byť. (Za povšimnutie stojí povedať priamo: changelog hovorí „while dragging it", ale to, čo naozaj expedovalo, bolo odstránenie kradnutia fokusu z obslúh klávesových skratiek — mašinéria špecifická pre ťahanie bola preskúmaná a vrátená v tom istom týždni.)
Stávka, doslovne
Ak chceš dôkaz, že táto zmluva nie je akademická, pozri sa, čo sa stalo o mesiace neskôr, keď prekrytie uviazlo na jej nesprávnej strane. V oveľa neskoršom vydaní spadol proces renderera, kým bolo prekrytie v režime zachytávania — setIgnoreMouseEvents(false), okno chytá všetky kliknutia — a bez živého UI, ktoré by ho uvoľnilo, neviditeľné okno na celý desktop tam sedelo a zjedalo každé kliknutie na stroji používateľa, kým to násilne neukončil. Priehľadné okno zaseknuté v režime zachytávania nie je mŕtve tlačidlo; je to pasca na kliknutia cez celý desktop. (Tá dostala vlastnú diagnózu — pozri keď tvoj AI zapisovateľ prestane nahrávať uprostred stretnutia, ktorá ju spája s veľmi odlišným bugom v tom istom vydaní.) Je to najostrejšie vyjadrenie celého problému: zasekni prepínač jedným smerom a tvoje UI je mŕtve; zaseknutý druhým a mŕtvy je celý tvoj desktop.
Tri veci, ktoré nás tieto vydania naučili
- Prekrytie prepúšťajúce kliknutia je dvojstranná zmluva a obe strany zlyhávajú inak. Priveľmi priehľadné a tvoje vlastné ovládacie prvky odumrú; priveľmi chňapavé a prehltneš vstup iných aplikácií. Väčšina bugov prekrytia je len jedna z týchto dvoch a pomenovať, na ktorej strane si, ti povie, kde hľadať.
- Najvyššia úroveň always-on-top nie je cena. Sedieť nad všetkým znamená sedieť nad systémovými dialógmi, ktoré potrebuješ, aby používateľ videl. Navrhuj s ohľadom na chvíľu, keď má tvoje prekrytie zdvorilo ustúpiť — a sprav návrat hore vratným v
finally, pretože spolovice dokončené odovzdanie je horšie než neodovzdať vôbec. - Odčítanie poráža šikovnosť. Víťaznou opravou v1.8.14 bolo zmazanie volaní
focus(), ktoré prekrytie nikdy nepotrebovalo, nájdené až po týždni stavania a vracania vlastného spracovania ťahania. Keď funkcia stále bojuje s platformou, prvá otázka je, či vieš odstrániť to, čo ten boj vedie.
Pre predchádzajúcu kapitolu príbehu v1 — presun vydaní do CI, dvakrát (v1.8.4); a pre celý oblúk — anatómiu dodávania softvéru do dokonalosti.