Steven
Steven7 dk okuma

Bir Click-Through Overlay'in İki Başarısızlık Modu

GeekBye'nin penceresi her şeyin üstünde yüzer ve tıklamalarının içinden geçmesine izin verir — düğmelerinin olduğu yer hariç. Bu iki taraflı bir sözleşme, ve v1.8.5 ile v1.8.14, her taraf kırıldığında neye benzediğini gösteriyor: overlay'in bir sistem diyaloğunu yuttuğu bir release, tuş vuruşlarını çaldığı bir başkası. İkincisi için kazanan çözüm, kod silmekti.

Mühendislik
Electron
Desktop
GeekBye Releases
Bir Click-Through Overlay'in İki Başarısızlık Modu

GeekBye'nin ana penceresi normal bir pencere değil. Çerçevesiz, saydam ve her zaman üstte — ne yapıyorsan onun üzerinde yüzen bir cam levha, üzerine birkaç kontrol çizilmiş. İlginç mühendislik yüzmesi değil; farenin kontroller dışında her yerde camın içinden dosdoğru geçmesi gerekmesi. Overlay'deki bir düğmeye tıkla, tıklamayı overlay alır. Yanındaki boş alana tıkla, tıklama altındaki toplantı uygulamana iner, sanki overlay hiç orada değilmiş gibi.

Bu, iki taraflı bir sözleşmedir: UI olan yerde tam olarak input al, ve başka her yerde input'a tamamen görünmez ol. Her iki taraf da her an tutmak zorunda, ve uygulamanın bu kısmındaki her bug iki taraftan birinin ihlalidir. İki ay arayla iki release, mümkün olan en temiz örnek — çünkü her biri farklı bir tarafı kırdı.

Sözleşme, mekanik olarak

İmplementasyon bir hook, useMousePassthrough, ve küçük. Pencere varsayılan olarak click-through. Global bir mousemove dinleyicisi her harekette tek bir soru sorar: imleç bir kontrolün üzerinde mi? Kontroller bir data-ui-region niteliğiyle etiketli, yani test tam anlamıyla target.closest('[data-ui-region]'). Bir kontrolün üzerindeyken setIgnoreMouseEvents(false) çağırır ve pencere tıklamaları yakalar. Saydam alanın üzerindeyken setIgnoreMouseEvents(true, { forward: true }) çağırır ve tıklamalar içinden geçer.

O forward: true taşıyıcı detaydır. Bir pencere fare olaylarını yok saymaya başladığında, normalde onları almayı tamamen bırakır — imlecin bir kontrolün üzerine geri geldiğini fark etmek için hook'un ihtiyaç duyduğu mousemove olayları dahil. forward: true, Electron'a hareketleri yine de yok sayan pencereye iletmeyi sürdürmesini söyler, böylece kendini yeniden kurabilir. Onsuz, overlay bir kez click-through olur ve bir daha geri dönmezdi. Hook ayrıca IPC sınırının ötesine yalnızca durum geçişlerinde çağrı yapar — imlecin UI ile cam arasındaki sınırı geçmesi — her piksel hareketinde değil. O optimizasyonu aklında tut; onda bir delik var, ve v1.8.5 içine düştü.

v1.8.5: saydamlık tarafı kırıldığında

Sözleşmenin saydamlık tarafı "UI olmayan her yerde input'a görünmez ol"dur. v1.8.5, overlay fazla baskın olduğunda neye benzediğini gösteriyor — geçmesine izin vermesi gereken input'u yakaladığında ya da engellediğinde. Tetikleyici macOS izin diyaloglarıydı, ve üst üste istiflenmiş iki ayrı başarısızlık vardı.

Overlay fiziksel olarak diyaloğun üstündeydi. Her-zaman-üstte bir pencerenin bir seviyesi vardır, ve GeekBye'nin overlay'i macOS'un sunduğu en yükseğinde oturuyordu — screen-saver. Bu neredeyse her şeyin üstünde, sistemin kendi Microphone ve Screen Recording izin diyalogları dahil. Yani onboarding sırasında, tam da macOS'un izin istediği anda, diyalog overlay'in altında render olup görünmez olabiliyordu. Çözüm, setPermissionMode, yalnızca macOS'a özel ve sezgiye aykırı olanı yapar: bir izin isteği süresince overlay'i floating seviyesine alçaltır, sonra onu geri yükler. Ve onu geri yüklemek belirli bir numara gerektiriyordu, tam da commit'te belgelenmiş: macOS "doesn't always visually re-raise a window when setting the same level it was previously at," yani kod, pencere sunucusunu istifleme sırasını yeniden hesaplamaya zorlamak için pencereyi bir ara seviyeden geçirir — floating → status → screen-saver. İstediğin seviyeyi ayarlamazsın; ona yürürsün.

Overlay diyaloğun tıklamalarını yiyordu. Onboarding, pencerenin tamamını dolduran karanlık bir arka plan çizer, ve passthrough hook'u bunu özel durum olarak ele alır: o arka plan yukarıdayken, sihirbaz tam etkileşimli olsun diye tüm pencerede tıklama-yakalamayı zorlar. Ama bir sistem diyaloğu bir tıklama toplamaya çalışırken "her tıklamayı yakala" tam da yanlış olandır. Çözüm, bir izin isteği sırasında arka planın işaretleyici niteliğini soyar, böylece hook yakalama modundan düşer ve diyaloğun düğmeleri tıklamalarını alır. Overlay kendi kontrollerini etkileşimli tutar; sadece boş camın bir düğme olduğunu iddia etmeyi bırakır.

Optimizasyondaki delik. Hook'un yalnızca geçişlerde davrandığını hatırla. Pencere yakalama modundan çıktığında, hover testini yeniden çalıştırdı — ama o anda imlecin zaten bir kontrolün üzerinde oturuyorsa, "UI üzerinde miyiz?" cevabı zaten "evet"ti, yani hiçbir geçiş tetiklenmedi ve click-through hiç devre dışı bırakılmadı. Kontrol canlı görünüyordu ve ölüydü. Çözüm (1b6eb20, alıntılanan) geçişe güvenmeyi bırakır: "when exiting onboarding mode, directly evaluate whether cursor is over a UI region and make the correct IPC call." Mod değiştirdiğinde, imlecin nerede olduğunu sana söylemesi için bir sonraki hareketi beklemezsin — bakarsın.

Ve terk edişi geri alınabilir kıl. Tüm bunlardaki ince risk, overlay'in artık geçici olarak z-sırasından ve tıklama-yakalamasından vazgeçmesi, ve bir şeyin onları geri vermesi gerekmesi. Geri yükleme çağrısı hata fırlatırsa — bir IPC tökezlemesi, istek ortasında yıkılan bir pencere — uygulama, isRequestingPermission true'da sıkışmış ve karanlık bir arka plan tüm ekranını gizler halde, kalıcı olarak, mahsur kalabilir. Yani çıkış yolu, durum-sıfırlaması "her zaman çalışacak" şekilde sarıldı, artı çok-dakikalık bir güvenlik zaman aşımı ve pencere yok edildiğinde bir temizlik. Ortaya çıkan kural: bir overlay her input terk ettiğinde, terk-etmeyi-geri-alma bir mutlu yolda değil, bir finally içinde olmalı.

v1.8.14: yakalama tarafı kırıldığında

Sözleşmenin diğer tarafı "UI olan yerde input al"dır. v1.8.14, overlay fazla açgözlü olduğunda neye benzediğini gösteriyor — onunla etkileşmek sana gerçekten önemsediğin uygulamanın input odağına mal olduğunda.

Changelog'daki belirti odak-çalma, ve işte dürüst versiyon, çünkü git geçmişi release notundan daha öğretici. Overlay'in kısayol işleyicileri — Cmd+B ile görünürlüğü aç/kapat, chat penceresini aç, onu kaydır — her biri mainWindow.focus() çağırırdı, ve macOS'ta app.focus({ steal: true }), tetiklediğinde overlay'in en önde olduğundan emin olmak için. Sorun şu ki overlay'i odaklamak daha önce en önde ne varsa onu devre dışı bırakır — toplantın, editörün — böylece onlar için tasarlanmış bir tuş vuruşu hiçbir yere ya da yanlış yere inebilirdi. Overlay aslında ihtiyaç duymadığı bir şeyi kapıyordu.

İşte bütün ders o son kısım, ve ekip bunu zor yoldan buldu. v1.8.14 olan branch önce özellikle sürüklemeyi çözmeye çalıştı — çünkü sürüklemenin bu bug'ın kendi güzel versiyonu var: tam ekran bir overlay'i sürüklediğinde, pencere sabit bir imlecin altında hareket eder, yani hover modeli işaretçinin kontrolü "terk ettiğini" sanır ve sürükleme ortasında click-through'a döner, sürüklemeyi düşürür. Böylece bir dragLock eklediler. Sonra tüm pencereyi odaklanamaz yapmayı denediler — bu yazmayı ve sürüklemeyi bozdu, böylece odaklanabilirliği geri yüklediler, sonra odaklanamazlığı yalnızca metin girdilerine daralttılar, sonra anahtarı söküp attılar. Electron'un yerel sürüklemesinin yerine geçmek için özel bir sürükleme IPC'si kurdular. Beklenmedik-odakta-bulanıklaştır eklediler. Bunun bir haftası.

Sonra neredeyse hepsini sildiler. Gönderilen commit, özel sürüklemeyi, odaklanabilir anahtarını ve blur işleyicisini geri aldı, ve tam olarak tek bir fikri korudu: focus() çağırmayı bırak. Bir kod yorumuna yazılmış olan kavrayış, overlay'in en baştan OS odağına hiç ihtiyaç duymadığı — "IPC messages (webContents.send) work without window focus." Overlay kendi renderer'ını doğrudan yönetir; odak saf yan hasardı. Gönderilebilir çözüm, bir günde kurulan zekice özel-sürükleme yeniden yazımı değildi. Orada hiç olmaması gereken odak çağrılarını çıkarmaktı. (Açıkça söylemeye değer: changelog "while dragging it," diyor, ama gerçekten gönderilen şey, odak-çalmayı klavye-kısayolu işleyicilerinden kaldırmaktı — sürüklemeye özgü mekanizma aynı hafta keşfedilip geri alındı.)

Riskler, somutlaştırılmış

Bu sözleşmenin akademik olmadığının kanıtını istiyorsan, aylar sonra overlay yanlış tarafında sıkışıp kaldığında ne olduğuna bak. Çok daha sonraki bir release'de, overlay yakalama modundayken renderer süreci çöktü — setIgnoreMouseEvents(false), pencere tüm tıklamaları kapıyor — ve onu serbest bırakacak canlı bir UI olmadan, görünmez tüm-masaüstü penceresi orada, kullanıcı zorla kapatana dek makinesindeki her tıklamayı yiyerek oturdu. Yakalama modunda sıkışmış saydam bir pencere ölü bir düğme değildir; masaüstü-genişliğinde bir tıklama tuzağıdır. (O da kendi teşhisini aldı — bkz. yapay zekâ not asistanınız toplantının ortasında kaydı durdurduğunda, ki bunu aynı release'deki çok farklı bir bug'la eşleştirir.) Bütün sorunun en keskin ifadesi bu: anahtarı bir yöne sıkıştır, UI'n ölür; diğer yöne sıkıştır, tüm masaüstün ölür.

Bu release'lerin bize öğrettiği üç şey

  1. Bir click-through overlay iki taraflı bir sözleşmedir, ve iki taraf farklı biçimde başarısız olur. Fazla saydam, kendi kontrollerin ölür; fazla açgözlü, başka uygulamaların input'unu yutarsın. Çoğu overlay bug'ı bu ikisinden yalnızca biridir, ve hangi tarafta olduğunu adlandırmak sana nereye bakacağını söyler.
  2. En yüksek her-zaman-üstte seviye bir ödül değildir. Her şeyin üstünde oturmak, kullanıcının görmesi gereken sistem diyaloglarının üstünde oturmak demektir. Overlay'inin ne zaman nazikçe geri çekilmesi gerektiğini tasarla — ve geri yükselmeyi bir finally içinde geri alınabilir kıl, çünkü yarım kalmış bir terk ediş, hiç terk etmemekten kötüdür.
  3. Çıkarma, zekâyı yener. Kazanan v1.8.14 çözümü, overlay'in hiç ihtiyaç duymadığı focus() çağrılarını silmekti, ancak bir hafta özel sürükleme işleme kurup geri aldıktan sonra bulundu. Bir özellik platformla dövüşmeyi sürdürdüğünde, sorulacak ilk soru dövüşü yapan şeyi kaldırıp kaldıramayacağındır.

v1 hikâyesinin bir önceki bölümü için, release'i CI'ye koymak, iki kez (v1.8.4); tüm yay için de yazılımı kusursuzluğa kadar sevk etmenin anatomisi.