
Dua Mode Kegagalan Sebuah Overlay Click-Through
Jendela GeekBye mengambang di atas segalanya dan membiarkan klikmu menembusnya — kecuali di tempat ia punya tombol. Itu kontrak dua sisi, dan v1.8.5 serta v1.8.14 adalah wujudnya ketika masing-masing sisi patah: satu release di mana overlay menelan sebuah dialog sistem, satu lagi di mana ia mencuri ketikan tombolmu. Perbaikan yang menang untuk yang kedua adalah menghapus kode.
Jendela utama GeekBye bukan jendela biasa. Ia tanpa bingkai, transparan, dan selalu di atas — sehelai kaca yang mengambang di atas apa pun yang sedang kamu kerjakan, dengan beberapa kontrol tergambar di atasnya. Rekayasa yang menarik bukanlah bahwa ia mengambang; melainkan bahwa mouse-mu seharusnya menembus kaca itu lurus di mana-mana kecuali di kontrol. Klik sebuah tombol di overlay dan overlay mendapatkan kliknya. Klik ruang kosong di sebelahnya dan klik itu mendarat di aplikasi meeting-mu di bawahnya, seolah-olah overlay sama sekali tak ada di sana.
Itu adalah kontrak dengan dua sisi: terima input persis di tempat ada UI, dan jadilah sepenuhnya tak terlihat bagi input di mana pun lainnya. Kedua sisi harus bertahan di setiap saat, dan setiap bug di bagian aplikasi ini adalah pelanggaran satu sisi atau sisi lainnya. Dua release, terpaut dua bulan, adalah ilustrasi sebersih mungkin — karena masing-masing mematahkan sisi yang berbeda.
Kontrak itu, secara mekanis
Implementasinya adalah sebuah hook, useMousePassthrough, dan ia kecil. Jendela bersifat click-through secara default. Sebuah listener mousemove global mengajukan satu pertanyaan pada setiap gerakan: apakah kursor berada di atas sebuah kontrol? Kontrol ditandai dengan atribut data-ui-region, jadi tesnya secara harfiah adalah target.closest('[data-ui-region]'). Di atas sebuah kontrol, ia memanggil setIgnoreMouseEvents(false) dan jendela menangkap klik. Di atas ruang transparan, ia memanggil setIgnoreMouseEvents(true, { forward: true }) dan klik jatuh menembus.
Si forward: true itu adalah detail yang menopang. Begitu sebuah jendela mengabaikan event mouse, ia normalnya berhenti menerimanya sama sekali — termasuk event mousemove yang dibutuhkan hook untuk menyadari kursor datang kembali ke atas sebuah kontrol. forward: true menyuruh Electron untuk tetap meneruskan gerakan ke jendela yang mengabaikan itu bagaimanapun, sehingga ia bisa mengaktifkan dirinya kembali. Tanpanya, overlay akan menjadi click-through sekali dan tak pernah kembali. Hook juga hanya memanggil melintasi batas IPC pada transisi keadaan — kursor melintasi batas antara UI dan kaca — bukan pada setiap piksel gerakan. Ingat optimisasi itu; ada lubang di dalamnya, dan v1.8.5 jatuh ke dalamnya.
v1.8.5: ketika sisi transparansi patah
Sisi transparansi dari kontrak adalah "jadilah tak terlihat bagi input di mana pun tak ada UI." v1.8.5 adalah wujudnya ketika overlay terlalu hadir — ketika ia menangkap atau memblokir input yang seharusnya ia biarkan lewat. Pemicunya adalah dialog izin macOS, dan ada dua kegagalan berbeda yang bertumpuk satu di atas yang lain.
Overlay secara fisik berada di atas dialog. Sebuah jendela selalu-di-atas punya sebuah level, dan overlay GeekBye duduk di yang tertinggi yang ditawarkan macOS — screen-saver. Itu di atas hampir segalanya, termasuk dialog izin Microphone dan Screen Recording milik sistem itu sendiri. Jadi selama onboarding, tepat di saat macOS meminta izin, dialog bisa ter-render di bawah overlay dan tak terlihat. Perbaikannya, setPermissionMode, khusus-macOS dan melakukan hal yang tak intuitif: ia menurunkan overlay ke level floating selama durasi sebuah permintaan izin, lalu memulihkannya setelahnya. Dan memulihkannya membutuhkan sebuah trik khusus, terdokumentasi tepat di commit: macOS "doesn't always visually re-raise a window when setting the same level it was previously at," jadi kode melangkahkan jendela melalui sebuah level perantara — floating → status → screen-saver — untuk memaksa window server menghitung ulang urutan penumpukan. Kamu tidak menyetel level yang kamu mau; kamu berjalan menujunya.
Overlay memakan klik milik dialog. Onboarding menggambar sebuah latar gelap yang memenuhi seluruh jendela, dan hook passthrough memperlakukannya sebagai kasus khusus: selama latar itu terpasang, ia memaksa penangkapan-klik pada seluruh jendela agar wizard sepenuhnya bisa diajak berinteraksi. Tapi "tangkap setiap klik" justru persis salah ketika sebuah dialog sistem sedang mencoba mengumpulkan satu. Perbaikannya melucuti atribut penanda milik latar selama sebuah permintaan izin, sehingga hook keluar dari mode penangkapan dan tombol-tombol dialog mendapatkan klik mereka. Overlay tetap menjaga kontrolnya sendiri bisa diajak berinteraksi; ia hanya berhenti berpura-pura bahwa kaca kosong itu sebuah tombol.
Lubang di dalam optimisasi. Ingat bahwa hook hanya bertindak pada transisi. Ketika jendela meninggalkan mode penangkapan, ia menjalankan ulang tes hover — tapi jika kursormu sudah berada di atas sebuah kontrol pada saat itu, jawaban atas "apakah kita di atas UI?" sudah "ya," jadi tak ada transisi yang terpicu, dan click-through tak pernah dinonaktifkan. Kontrol tampak hidup padahal mati. Perbaikannya (1b6eb20, dikutip) berhenti mempercayai transisi: "when exiting onboarding mode, directly evaluate whether cursor is over a UI region and make the correct IPC call." Ketika kamu berganti mode, kamu tidak menunggu gerakan berikutnya untuk memberitahumu di mana kursor berada — kamu melihat.
Dan buat penyerahan itu dapat dibalik. Risiko halus dalam semua ini adalah bahwa overlay kini untuk sementara melepaskan z-order-nya dan penangkapan-kliknya, dan sesuatu harus mengembalikan keduanya. Jika panggilan pemulihan melempar error — sebuah cegukan IPC, sebuah jendela yang dibongkar di tengah permintaan — aplikasi bisa telantar dengan isRequestingPermission macet true dan sebuah latar gelap menyembunyikan seluruh layarmu, selamanya. Jadi jalur keluar dibungkus agar reset-keadaan "selalu dieksekusi," ditambah sebuah timeout pengaman multi-menit dan sebuah pembersihan saat jendela dihancurkan. Aturan yang keluar dari itu: setiap kali sebuah overlay menyerahkan input, pembatalan-penyerahan itu harus berada di dalam sebuah finally, bukan di jalur bahagia.
v1.8.14: ketika sisi penangkapan patah
Sisi lain dari kontrak adalah "terima input di tempat ada UI." v1.8.14 adalah wujudnya ketika overlay terlalu suka merebut — ketika berinteraksi dengannya merenggut darimu fokus input aplikasi yang sebenarnya kamu pedulikan.
Gejalanya di changelog adalah pencurian-fokus, dan inilah versi jujurnya, karena riwayat git lebih instruktif daripada catatan rilis. Handler pintasan milik overlay — mengalihkan visibilitas dengan Cmd+B, membuka jendela chat, men-scroll-nya — masing-masing dulu memanggil mainWindow.focus(), dan di macOS app.focus({ steal: true }), untuk memastikan overlay berada paling depan saat kamu memicunya. Masalahnya adalah bahwa memfokuskan overlay menonaktifkan apa pun yang tadinya paling depan — meeting-mu, editor-mu — sehingga sebuah ketikan tombol yang ditujukan untuk mereka bisa mendarat di mana-mana, atau di tempat yang salah. Overlay merebut sesuatu yang sebenarnya tak ia butuhkan.
Bagian terakhir itulah seluruh pelajarannya, dan tim menemukannya dengan cara yang sulit. Branch yang menjadi v1.8.14 mula-mula mencoba menyelesaikan dragging secara khusus — karena dragging punya versinya sendiri yang indah dari bug ini: ketika kamu men-drag sebuah overlay layar-penuh, jendela bergerak di bawah kursor yang diam, jadi model hover mengira pointer "meninggalkan" kontrol dan berbalik ke click-through di tengah drag, menjatuhkan drag itu. Maka mereka menambahkan sebuah dragLock. Lalu mereka mencoba membuat seluruh jendela non-focusable — yang mematahkan pengetikan dan dragging, jadi mereka memulihkan focusability, lalu mempersempit non-focusability hanya ke input teks, lalu mencabut toggle-nya. Mereka membangun sebuah IPC drag kustom untuk menggantikan drag native milik Electron. Mereka menambahkan blur-saat-fokus-tak-terduga. Seminggu seperti ini.
Kemudian mereka menghapus hampir semuanya. Commit yang dirilis me-revert custom drag, toggle focusable, dan handler blur, dan menyimpan tepat satu gagasan: berhenti memanggil focus(). Kesadarannya, ditulis ke dalam sebuah komentar kode, adalah bahwa overlay sejak awal tak pernah butuh fokus OS — "IPC messages (webContents.send) work without window focus." Overlay mengendalikan renderer-nya sendiri secara langsung; fokus murni kerusakan sampingan. Perbaikan yang bisa dirilis bukanlah penulisan-ulang custom-drag yang cerdik yang dibangun dalam sehari. Melainkan mengurangi panggilan fokus yang seharusnya tak pernah ada di sana. (Layak dikatakan terang-terangan: changelog berkata "while dragging it," tapi yang sebenarnya dirilis adalah menghapus pencurian-fokus dari handler pintasan-keyboard — mesin khusus-drag itu dijajaki dan di-revert pada minggu yang sama.)
Taruhannya, dibuat harfiah
Kalau kamu mau bukti bahwa kontrak ini tidak akademis, lihat apa yang terjadi berbulan-bulan kemudian ketika overlay tersangkut di sisi yang salah darinya. Pada sebuah release yang jauh lebih belakangan, proses renderer crash selagi overlay berada dalam mode penangkapan — setIgnoreMouseEvents(false), jendela merebut semua klik — dan tanpa UI hidup untuk melepaskannya, jendela tak terlihat yang memenuhi seluruh desktop itu duduk di sana memakan setiap klik di mesin pengguna sampai mereka force-quit. Sebuah jendela transparan yang tersangkut dalam mode penangkapan bukanlah tombol mati; ia adalah perangkap klik selebar desktop. (Yang satu itu mendapat diagnosisnya sendiri — lihat ketika AI notetaker Anda berhenti merekam di tengah meeting, yang memasangkannya dengan sebuah bug yang sangat berbeda di release yang sama.) Ia adalah pernyataan paling tajam dari seluruh masalah: buat toggle tersangkut ke satu arah dan UI-mu mati; tersangkut ke arah lain dan seluruh desktop-mu yang mati.
Tiga hal yang diajarkan release-release ini kepada kami
- Sebuah overlay click-through adalah kontrak dua sisi, dan kedua sisi gagal secara berbeda. Terlalu transparan dan kontrolmu sendiri mati; terlalu suka merebut dan kamu menelan input aplikasi lain. Kebanyakan bug overlay hanyalah salah satu dari dua itu, dan menyebutkan di sisi mana kamu berada memberitahumu di mana harus mencari.
- Level selalu-di-atas tertinggi bukanlah hadiah. Duduk di atas segalanya berarti duduk di atas dialog sistem yang kamu butuh penggunanya melihatnya. Rancang untuk saat overlay-mu seharusnya dengan sopan mundur selangkah — dan buat naik kembali dapat dibalik di dalam sebuah
finally, karena sebuah penyerahan yang setengah-selesai lebih buruk daripada tak pernah menyerahkan. - Pengurangan mengalahkan kepintaran. Perbaikan v1.8.14 yang menang adalah menghapus panggilan
focus()yang tak pernah dibutuhkan overlay, ditemukan hanya setelah seminggu membangun dan me-revert penanganan custom drag. Ketika sebuah fitur terus-menerus melawan platform, pertanyaan pertama yang harus diajukan adalah apakah kamu bisa menghapus benda yang melakukan perlawanan itu.
Untuk bab sebelumnya dalam kisah v1, menaruh release ke dalam CI, dua kali (v1.8.4); dan untuk keseluruhan alurnya, anatomi merilis software sampai sempurna.