
Dva režimy selhání click-through overlaye
Okno GeekBye se vznáší nade vším a nechává tvoje kliknutí procházet skrz — kromě míst, kde má tlačítka. To je oboustranná smlouva a v1.8.5 a v1.8.14 ukazují, jak to vypadá, když se každá strana zlomí: jedno vydání, kde overlay spolkl systémový dialog, a jedno, kde ti ukradl stisky kláves. Vítězná oprava toho druhého bylo smazání kódu.
Hlavní okno GeekBye není normální okno. Je bezrámové, průhledné a vždy navrchu — tabule skla vznášející se nad čímkoli, co zrovna děláš, s pár ovládacími prvky namalovanými na ní. Zajímavá inženýrská část není v tom, že se vznáší; je v tom, že tvoje myš má projít rovnou skrz sklo všude kromě ovládacích prvků. Klikneš na tlačítko na overlayi a overlay dostane kliknutí. Klikneš na prázdné místo vedle něj a kliknutí přistane na tvé aplikaci pro schůzky pod ním, jako by tam overlay vůbec nebyl.
To je smlouva se dvěma stranami: přijímej vstup přesně tam, kde je UI, a buď zcela neviditelný pro vstup všude jinde. Obě strany musí platit v každém okamžiku a každý bug v této části aplikace je porušením jedné nebo druhé strany. Dvě vydání, dva měsíce od sebe, jsou nejčistší možná ilustrace — protože každé z nich zlomilo jinou stranu.
Smlouva, mechanicky
Implementace je hook, useMousePassthrough, a je malá. Okno je ve výchozím stavu click-through. Globální posluchač mousemove klade při každém pohybu jednu otázku: je kurzor nad ovládacím prvkem? Ovládací prvky jsou označené atributem data-ui-region, takže test je doslova target.closest('[data-ui-region]'). Nad ovládacím prvkem zavolá setIgnoreMouseEvents(false) a okno zachytává kliknutí. Nad průhledným prostorem zavolá setIgnoreMouseEvents(true, { forward: true }) a kliknutí propadají skrz.
Ono forward: true je nosný detail. Jakmile okno ignoruje události myši, normálně je přestane úplně přijímat — včetně událostí mousemove, které hook potřebuje, aby si všiml kurzoru vracejícího se zpět nad ovládací prvek. forward: true říká Electronu, aby přesto dál přeposílal pohyby ignorujícímu oknu, aby se mohlo znovu vyzbrojit. Bez něj by se overlay stal click-through jednou a už nikdy by se nevrátil. Hook navíc volá přes hranici IPC jen při přechodech stavu — kurzor překračující hranici mezi UI a sklem — ne při každém pixelu pohybu. Zapamatuj si tuhle optimalizaci; má díru a v1.8.5 do ní spadl.
v1.8.5: když se zlomí strana průhlednosti
Strana průhlednosti té smlouvy je „buď neviditelný pro vstup všude, kde není UI." v1.8.5 ukazuje, jak to vypadá, když je overlay příliš přítomný — když zachytává nebo blokuje vstup, který měl pustit dál. Spouštěčem byly dialogy oprávnění macOS a byla tu dvě odlišná selhání naskládaná na sobě.
Overlay byl fyzicky nad dialogem. Always-on-top okno má úroveň a overlay GeekBye seděl na té nejvyšší, kterou macOS nabízí — screen-saver. Ta je nad téměř vším, včetně vlastních dialogů oprávnění Microphone a Screen Recording systému. Takže během onboardingu, přesně ve chvíli, kdy macOS žádal o oprávnění, se dialog mohl vykreslit pod overlayem a být neviditelný. Oprava, setPermissionMode, je jen pro macOS a dělá tu neintuitivní věc: na dobu trvání žádosti o oprávnění overlay sníží na úroveň floating a poté ho obnoví. A jeho obnovení potřebovalo specifický trik, zdokumentovaný přímo v commitu: macOS "doesn't always visually re-raise a window when setting the same level it was previously at," takže kód protáhne okno mezilehlou úrovní — floating → status → screen-saver —, aby přinutil okenní server přepočítat pořadí vrstvení. Nenastavíš úroveň, kterou chceš; dojdeš k ní krok za krokem.
Overlay požíral kliknutí dialogu. Onboarding maluje tmavé pozadí přes celé okno a passthrough hook to řeší jako zvláštní případ: dokud je to pozadí nahoře, vynucuje zachytávání kliknutí na celém okně, aby byl průvodce plně interaktivní. Ale „zachyť každé kliknutí" je přesně špatně, když nějaké chce sesbírat systémový dialog. Oprava odstraní značkovací atribut pozadí během žádosti o oprávnění, takže hook vypadne z režimu zachytávání a tlačítka dialogu dostanou svá kliknutí. Overlay si udrží vlastní ovládací prvky interaktivní; jen přestane předstírat, že prázdné sklo je tlačítko.
Díra v optimalizaci. Pamatuj, že hook jedná jen při přechodech. Když okno opustilo režim zachytávání, znovu spustilo hover test — ale pokud tvůj kurzor v tu chvíli už seděl na ovládacím prvku, odpověď na „jsme nad UI?" už byla „ano," takže se nespustil žádný přechod a click-through se nikdy nevypnul. Ovládací prvek vypadal živý a byl mrtvý. Oprava (1b6eb20, citováno) přestane přechodu důvěřovat: "when exiting onboarding mode, directly evaluate whether cursor is over a UI region and make the correct IPC call." Když měníš režimy, nečekáš, až ti další pohyb řekne, kde je kurzor — podíváš se.
A udělej postoupení vratným. Jemné riziko v tom všem je, že se overlay teď dočasně vzdává svého z-orderu a zachytávání kliknutí a něco mu je musí vrátit zpět. Pokud volání obnovy vyhodí chybu — zádrhel IPC, okno zbourané uprostřed žádosti — aplikace by mohla uvíznout s isRequestingPermission zaseknutým na true a tmavým pozadím navždy skrývajícím celou tvoji obrazovku. Takže výstupní cesta byla obalena tak, aby se reset stavu „vždy provede," plus několikaminutový bezpečnostní časový limit a úklid při zničení okna. Pravidlo, které z toho plyne: pokaždé, když overlay postoupí vstup, vrácení postoupeného musí být ve finally, ne na šťastné cestě.
v1.8.14: když se zlomí strana zachytávání
Druhá strana smlouvy je „přijímej vstup tam, kde je UI." v1.8.14 ukazuje, jak to vypadá, když je overlay příliš hrabivý — když tě interakce s ním stojí vstupní fokus aplikace, na které ti ve skutečnosti záleží.
Symptom v changelogu je kradení fokusu a tady je upřímná verze, protože git historie je poučnější než poznámka k vydání. Obsluhy zkratek overlaye — přepni viditelnost přes Cmd+B, otevři okno chatu, roluj v něm — každá dřív volala mainWindow.focus(), a na macOS app.focus({ steal: true }), aby zajistila, že overlay byl vepředu, když ho spustíš. Problém je, že fokusování overlaye deaktivuje cokoli, co bylo vepředu předtím — tvoji schůzku, tvůj editor — takže stisk klávesy určený pro ně mohl přistát nikde, nebo na špatném místě. Overlay hrabal po něčem, co ve skutečnosti nepotřeboval.
Ta poslední část je celá lekce a tým ji našel tvrdou cestou. Větev, ze které se stala v1.8.14, se nejdřív pokusila vyřešit konkrétně tažení — protože tažení má svou vlastní krásnou verzi tohoto bugu: když táhneš overlay přes celou obrazovku, okno se pohybuje pod nehybným kurzorem, takže si hover model myslí, že ukazatel „opustil" ovládací prvek, a přepne se na click-through uprostřed tažení, čímž tažení zahodí. Tak přidali dragLock. Pak zkusili udělat celé okno nefokusovatelné — což rozbilo psaní a tažení, takže obnovili fokusovatelnost, pak zúžili nefokusovatelnost jen na textová pole, pak přepínač vyrvali ven. Postavili custom drag IPC, aby nahradili nativní tažení Electronu. Přidali blur-při-nečekaném-fokusu. Týden tohohle.
Pak skoro všechno smazali. Commit, který byl odeslán, vrátil custom drag, přepínač fokusovatelnosti a blur obsluhu a ponechal přesně jednu myšlenku: přestaň volat focus(). Prozření, zapsané do komentáře v kódu, je, že overlay vlastně nikdy fokus OS nepotřeboval — "IPC messages (webContents.send) work without window focus." Overlay řídí svůj vlastní renderer přímo; fokus byl čistá vedlejší škoda. Odeslatelná oprava nebyla chytré přepsání custom drag postavené za den. Bylo to odečtení volání fokusu, která tam neměla být. (Stojí za to říct to na rovinu: changelog říká "while dragging it," ale co se ve skutečnosti odeslalo, bylo odstranění kradení fokusu z obsluh klávesových zkratek — mašinerie specifická pro tažení byla prozkoumána a vrácena ve stejném týdnu.)
Sázka, vzatá doslova
Pokud chceš důkaz, že tato smlouva není akademická, podívej se, co se stalo o měsíce později, když overlay uvízl na špatné straně. V mnohem pozdějším vydání se renderer proces zhroutil, zatímco overlay byl v režimu zachytávání — setIgnoreMouseEvents(false), okno hrabající všechna kliknutí — a bez živého UI, které by ho uvolnilo, neviditelné okno přes celý desktop tam sedělo a požíralo každé kliknutí na uživatelově stroji, dokud nevynutil ukončení. Průhledné okno zaseknuté v režimu zachytávání není mrtvé tlačítko; je to past na kliknutí přes celý desktop. (To dostalo vlastní diagnózu — viz když tvůj AI zapisovatel poznámek přestane nahrávat uprostřed schůzky, která ho spojuje s velmi odlišným bugem ve stejném vydání.) Je to nejostřejší formulace celého problému: zasekni přepínač jedním směrem a tvoje UI je mrtvé; zaseknutý druhým směrem a je jím celý tvůj desktop.
Tři věci, které nás tato vydání naučila
- Click-through overlay je oboustranná smlouva a obě strany selhávají jinak. Příliš průhledný a tvoje vlastní ovládací prvky odumřou; příliš hrabivý a spolkneš vstup jiných aplikací. Většina overlay bugů je jen jeden z těch dvou a pojmenování, na které straně jsi, ti řekne, kam se dívat.
- Nejvyšší always-on-top úroveň není výhra. Sedět nade vším znamená sedět nad systémovými dialogy, které potřebuješ, aby uživatel viděl. Navrhuj pro chvíli, kdy by měl tvůj overlay zdvořile ustoupit — a udělej návrat nahoru vratným ve
finally, protože napůl dokončené postoupení je horší než nikdy neustoupit. - Odečtení poráží chytrost. Vítězná oprava v1.8.14 bylo smazání volání
focus(), která overlay nikdy nepotřeboval, nalezené až po týdnu stavění a vracení custom drag obsluhy. Když funkce pořád bojuje s platformou, první otázka, kterou si položit, je, jestli můžeš odstranit to, co bojuje.
Předchozí kapitolu příběhu v1 najdete v vložení vydání do CI, dvakrát (v1.8.4); a celý oblouk v anatomii dodávání softwaru k dokonalosti.