Steven
Steven4 นาทีที่อ่าน

สองโหมดความล้มเหลวของ overlay แบบคลิกทะลุ

หน้าต่างของ GeekBye ลอยอยู่เหนือทุกอย่าง และปล่อยให้คลิกของคุณผ่านทะลุมันไป — ยกเว้นตรงที่มันมีปุ่ม นั่นคือสัญญาสองด้าน และ v1.8.5 กับ v1.8.14 คือหน้าตาของตอนที่แต่ละด้านพัง: รีลีสหนึ่ง overlay กลืน dialog ของระบบไป, อีกรีลีสหนึ่งมันขโมยการกดปุ่มของคุณ ทางแก้ที่ชนะสำหรับอันที่สองคือการลบโค้ดทิ้ง

วิศวกรรม
Electron
Desktop
GeekBye รีลีส
สองโหมดความล้มเหลวของ overlay แบบคลิกทะลุ

หน้าต่างหลักของ GeekBye ไม่ใช่หน้าต่างธรรมดา มันไม่มีกรอบ, โปร่งใส, และอยู่บนสุดเสมอ — แผ่นกระจกหนึ่งแผ่นที่ลอยอยู่เหนืออะไรก็ตามที่คุณกำลังทำ โดยมี control ไม่กี่อันวาดอยู่บนมัน วิศวกรรมที่น่าสนใจไม่ใช่ที่มันลอยได้ แต่คือที่เมาส์ของคุณควรจะ_ผ่านทะลุกระจกไปตรง ๆ_ ในทุกที่ยกเว้นตรง control คลิกปุ่มบน overlay แล้ว overlay ได้คลิกนั้นไป คลิกพื้นที่ว่างข้าง ๆ มัน แล้วคลิกนั้นก็ลงไปตกบนแอปประชุมของคุณที่อยู่ข้างล่าง ราวกับว่า overlay ไม่ได้อยู่ตรงนั้นเลย

นั่นคือสัญญาที่มีสองด้าน: รับ input ตรงที่มี UI เป๊ะ ๆ, และมองไม่เห็น input อย่างสิ้นเชิงในทุกที่ที่เหลือ ทั้งสองด้านต้องเป็นจริงในทุกขณะ และทุก bug ในส่วนนี้ของแอปคือการละเมิดด้านใดด้านหนึ่ง สองรีลีสที่ห่างกันสองเดือนคือตัวอย่างที่สะอาดที่สุดเท่าที่จะเป็นไปได้ — เพราะแต่ละอันพังคนละด้านกัน

สัญญานั้น ในเชิงกลไก

การ implement คือ hook หนึ่งตัวชื่อ useMousePassthrough และมันเล็ก หน้าต่างเป็นคลิกทะลุ_โดยดีฟอลต์_ listener mousemove แบบ global ถามคำถามเดียวในทุกการขยับ: cursor อยู่บน control ไหม? control ถูกติดแท็กด้วย attribute data-ui-region ดังนั้นการทดสอบจึงคือ target.closest('[data-ui-region]') ตรงตัวเลย ถ้าอยู่บน control มันเรียก setIgnoreMouseEvents(false) และหน้าต่างจับคลิก ถ้าอยู่บนพื้นที่โปร่งใส มันเรียก setIgnoreMouseEvents(true, { forward: true }) และคลิกก็ตกทะลุลงไป

forward: true ตัวนั้นคือรายละเอียดที่รับน้ำหนัก พอหน้าต่างเริ่มเมิน event เมาส์ ปกติมันจะเลิกรับมันทั้งหมด — รวมถึง event mousemove ที่ hook ต้องใช้เพื่อสังเกตว่า cursor กำลังกลับ_เข้ามา_บน control forward: true บอก Electron ให้ยังส่งต่อการขยับไปยังหน้าต่างที่กำลังเมินอยู่นั้นต่อไปอยู่ดี เพื่อให้มันเตรียมตัวใหม่ได้ ถ้าไม่มีมัน overlay ก็จะกลายเป็นคลิกทะลุครั้งเดียวแล้วไม่มีวันกลับมา hook ยังเรียกข้ามเขต IPC เฉพาะตอน_เปลี่ยนสถานะ_เท่านั้น — ตอน cursor ข้ามเส้นแบ่งระหว่าง UI กับกระจก — ไม่ใช่ทุกพิกเซลของการขยับ จำการ optimize นั้นไว้ให้ดี; มันมีรู และ v1.8.5 ตกลงไปในรูนั้น

v1.8.5: เมื่อด้านความโปร่งใสพัง

ด้านความโปร่งใสของสัญญาคือ "มองไม่เห็น input ในทุกที่ที่ไม่มี UI" v1.8.5 คือหน้าตาของตอนที่ overlay ออกหน้าออกตามากเกินไป — ตอนที่มันจับหรือขวาง input ที่มันควรจะปล่อยให้ผ่านไป ตัวจุดชนวนคือ permission dialog ของ macOS และมีความล้มเหลวที่ต่างกันชัดเจนสองอันซ้อนกันอยู่

overlay อยู่เหนือ dialog ในเชิงกายภาพ หน้าต่างที่อยู่บนสุดเสมอมี_level_ และ overlay ของ GeekBye นั่งอยู่บน level ที่สูงที่สุดที่ macOS มีให้ — screen-saver นั่นอยู่เหนือเกือบทุกอย่าง รวมถึง permission dialog ของ Microphone และ Screen Recording ของตัวระบบเอง ดังนั้นระหว่าง onboarding ในจังหวะที่ macOS ขอ permission พอดี dialog อาจถูกวาดอยู่_ใต้_ overlay และมองไม่เห็น ทางแก้ setPermissionMode มีเฉพาะบน macOS และทำสิ่งที่สวนสัญชาตญาณ: มัน_ลด_ overlay ลงไปที่ level floating ตลอดช่วงที่มีการขอ permission แล้วค่อยคืนมันกลับหลังจากนั้น และการคืนมันกลับต้องใช้ทริกเฉพาะตัว ซึ่งบันทึกไว้ใน commit เลย: macOS "ไม่ได้ยกหน้าต่างขึ้นใหม่ในเชิงภาพเสมอไปเมื่อตั้ง level เดิมที่มันเคยอยู่" ดังนั้นโค้ดจึงพาหน้าต่างไต่ผ่าน level ตรงกลาง — floating → status → screen-saver — เพื่อบังคับให้ window server คำนวณลำดับการซ้อนใหม่ คุณไม่ได้ตั้ง level ที่ต้องการตรง ๆ; คุณเดินไปหามัน

overlay กำลังกินคลิกของ dialog onboarding วาด backdrop มืดเต็มหน้าต่าง และ hook passthrough จัดการมันเป็นกรณีพิเศษ: ระหว่างที่ backdrop นั้นขึ้นอยู่ มันบังคับให้ทั้งหน้าต่างจับคลิก เพื่อให้ wizard โต้ตอบได้เต็มที่ แต่ "จับทุกคลิก" คือสิ่งที่ผิดพอดีเมื่อ dialog ของระบบกำลังพยายามเก็บคลิกไปสักอัน ทางแก้ถอด attribute เครื่องหมายของ backdrop ออกระหว่างการขอ permission ดังนั้น hook จึงหลุดออกจากโหมดจับ และปุ่มของ dialog ก็ได้คลิกของมันไป overlay ยังคง control ของตัวเองให้โต้ตอบได้; มันแค่เลิกแสร้งว่ากระจกเปล่า ๆ เป็นปุ่ม

รูในการ optimize จำไว้ว่า hook ทำงานเฉพาะตอนเปลี่ยนสถานะ ตอนหน้าต่างออกจากโหมดจับ มันรันการทดสอบ hover ใหม่ — แต่ถ้าในจังหวะนั้น cursor ของคุณ_กำลังนั่ง_อยู่บน control อยู่แล้ว คำตอบของ "เราอยู่บน UI ไหม?" ก็เป็น "ใช่" อยู่แล้ว ดังนั้นไม่มีการเปลี่ยนสถานะเกิดขึ้น และคลิกทะลุก็ไม่เคยถูกปิด control ดูเหมือนมีชีวิตแต่ตายไปแล้ว ทางแก้ (1b6eb20, ยกมา) เลิกเชื่อการเปลี่ยนสถานะ: "when exiting onboarding mode, directly evaluate whether cursor is over a UI region and make the correct IPC call." เมื่อคุณเปลี่ยนโหมด คุณไม่รอให้การขยับครั้งถัดไปมาบอกว่า cursor อยู่ไหน — คุณไปดูเอง

และทำให้การสละคลายกลับได้ ความเสี่ยงแนบเนียนในเรื่องทั้งหมดนี้คือ ตอนนี้ overlay กำลังยอมปล่อย z-order และการจับคลิกของมันไปชั่วคราว และต้องมีอะไรสักอย่าง_คืน_มันกลับมา ถ้า call คืนค่านั้น throw ออกมา — IPC สะอึก, หน้าต่างถูกรื้อกลางคัน — แอปอาจค้างอยู่โดยที่ isRequestingPermission ติดเป็น true และ backdrop มืดบังหน้าจอทั้งหมดของคุณไว้อย่างถาวร ดังนั้นเส้นทางออกจึงถูกห่อไว้เพื่อให้การรีเซ็ตสถานะ "ทำงานเสมอ" บวกกับ safety timeout หลายนาทีและการ cleanup ตอนหน้าต่างถูกทำลาย กฎที่ร่วงออกมา: ทุกครั้งที่ overlay สละ input การเลิกสละนั้นต้องอยู่ใน finally ไม่ใช่บนเส้นทางที่ทุกอย่างราบรื่น

v1.8.14: เมื่อด้านการจับ input พัง

อีกด้านของสัญญาคือ "รับ input ตรงที่มี UI" v1.8.14 คือหน้าตาของตอนที่ overlay ชอบคว้ามากเกินไป — ตอนที่การโต้ตอบกับมันทำให้คุณเสีย input focus ของแอปที่คุณแคร์จริง ๆ ไป

อาการใน changelog คือการแย่ง focus และนี่คือเวอร์ชันที่ซื่อสัตย์ เพราะประวัติ git ให้บทเรียนมากกว่า release note handler ของ shortcut ใน overlay — สลับการมองเห็นด้วย Cmd+B, เปิดหน้าต่างแชท, เลื่อนมัน — แต่ละอันเคยเรียก mainWindow.focus() และบน macOS ก็เรียก app.focus({ steal: true }) เพื่อให้แน่ใจว่า overlay อยู่หน้าสุดตอนคุณสั่งมัน ปัญหาคือการ focus overlay ทำให้อะไรก็ตามที่อยู่หน้าสุดก่อนหน้านั้นถูกปิดการทำงาน — การประชุมของคุณ, editor ของคุณ — ดังนั้นการกดปุ่มที่ตั้งใจส่งให้พวกมันจึงอาจไปไม่ตกที่ไหนเลย หรือตกในที่ที่ผิด overlay กำลังคว้าสิ่งที่จริง ๆ แล้วมันไม่ต้องการ

ส่วนสุดท้ายนั้นแหละคือบทเรียนทั้งหมด และทีมค้นมันเจอด้วยวิธีที่ยากลำบาก branch ที่กลายมาเป็น v1.8.14 แรกสุดพยายามแก้เรื่อง_การลาก_โดยเฉพาะ — เพราะการลากมีเวอร์ชันสวย ๆ ของ bug นี้เป็นของตัวเอง: เมื่อคุณลาก overlay เต็มจอ หน้าต่างขยับไปใต้ cursor ที่หยุดนิ่ง ดังนั้นโมเดล hover จึงคิดว่า pointer "ออก" จาก control ไปแล้ว และพลิกเป็นคลิกทะลุ_กลางการลาก_ ทำให้การลากหลุด พวกเขาจึงเพิ่ม dragLock จากนั้นลองทำให้ทั้งหน้าต่าง non-focusable — ซึ่งทำให้การพิมพ์และการลากพัง พวกเขาจึงคืนความ focus ได้กลับมา แล้วหด non-focusable ให้เหลือแค่ช่อง text input จากนั้นก็ฉีก toggle นั้นออก พวกเขาสร้าง custom drag IPC ขึ้นมาแทน native drag ของ Electron เพิ่ม blur ตอน focus แบบไม่คาดคิด แบบนี้อยู่หนึ่งสัปดาห์

จากนั้นพวกเขาลบมันออกเกือบทั้งหมด commit ที่ ship revert custom drag, focusable toggle, และ blur handler แล้วเก็บไอเดียไว้เพียงหนึ่งเดียว: เลิกเรียก focus() สิ่งที่ตระหนักได้ ซึ่งเขียนไว้ใน comment ในโค้ด คือ overlay ไม่เคยต้องการ OS focus ตั้งแต่แรก — "IPC messages (webContents.send) work without window focus." overlay ขับเคลื่อน renderer ของตัวเองโดยตรง; focus เป็นความเสียหายพลอยได้ล้วน ๆ ทางแก้ที่ ship ได้ไม่ใช่การเขียน custom-drag ใหม่แบบฉลาด ๆ ที่สร้างในวันเดียว มันคือการลบ call ของ focus ที่ไม่ควรอยู่ตรงนั้นตั้งแต่แรกออกไป (ควรพูดกันตรง ๆ: changelog บอกว่า "while dragging it" แต่สิ่งที่ ship ออกไปจริง ๆ คือการเอาการแย่ง focus ออกจาก handler ของ keyboard shortcut — กลไกเฉพาะสำหรับการลากนั้นถูกลองและถูก revert ในสัปดาห์เดียวกัน)

เดิมพัน ที่ทำให้เห็นเป็นรูปธรรม

ถ้าคุณอยากได้หลักฐานว่าสัญญานี้ไม่ใช่เรื่องในตำรา ลองดูสิ่งที่เกิดขึ้นหลายเดือนต่อมาตอนที่ overlay ติดค้างอยู่ผิด_ด้าน_ของมัน ในรีลีสที่หลังจากนั้นมาก process ของ renderer แครชขณะที่ overlay อยู่ในโหมดจับ — setIgnoreMouseEvents(false), หน้าต่างคว้าทุกคลิกไว้ — และเมื่อไม่มี UI ที่มีชีวิตมาปล่อยมัน หน้าต่างที่มองไม่เห็นซึ่งกินเต็มทั้งเดสก์ท็อปก็นั่งค้างอยู่ตรงนั้น กินทุกคลิกบนเครื่องของผู้ใช้จนกว่าเขาจะ force-quit หน้าต่างโปร่งใสที่ติดค้างในโหมดจับไม่ใช่ปุ่มที่ตายแล้ว; มันคือกับดักคลิกที่กินทั้งเดสก์ท็อป (อันนั้นได้การวินิจฉัยเป็นของตัวเอง — ดูเมื่อ AI ผู้จดบันทึกของคุณหยุดบันทึกกลางการประชุม ซึ่งจับคู่มันกับ bug ที่ต่างกันมากในรีลีสเดียวกัน) มันคือคำแถลงที่คมที่สุดของทั้งปัญหา: ทำให้ toggle ติดค้างไปทางหนึ่ง แล้ว UI ของคุณตาย; ติดค้างไปอีกทาง แล้วเดสก์ท็อปทั้งหมดของคุณตาย

สามสิ่งที่รีลีสเหล่านี้สอนเรา

  1. overlay แบบคลิกทะลุคือสัญญาสองด้าน และทั้งสองด้านล้มเหลวคนละแบบ โปร่งใสเกินไป แล้ว control ของคุณเองก็ตาย; ชอบคว้าเกินไป แล้วคุณก็กลืน input ของแอปอื่น bug ของ overlay ส่วนใหญ่ก็แค่หนึ่งในสองอันนั้น และการเรียกชื่อว่าคุณอยู่ด้านไหนก็บอกคุณว่าควรไปดูตรงไหน
  2. level บนสุดของ always-on-top ไม่ใช่รางวัล การนั่งอยู่เหนือ_ทุกอย่าง_หมายถึงการนั่งอยู่เหนือ system dialog ที่คุณต้องการให้ผู้ใช้เห็น ออกแบบเผื่อจังหวะที่ overlay ของคุณควรถอยลงมาหนึ่งก้าวอย่างสุภาพ — และทำให้การก้าวกลับขึ้นไปคลายกลับได้ใน finally เพราะการสละที่ทำเสร็จแค่ครึ่งเดียวแย่กว่าการไม่สละเลย
  3. การลบทิ้งชนะความฉลาด ทางแก้ที่ชนะของ v1.8.14 คือการลบ focus() call ที่ overlay ไม่เคยต้องการ ซึ่งเจอก็ต่อเมื่อผ่านหนึ่งสัปดาห์ของการสร้างและ revert custom drag handling เมื่อฟีเจอร์หนึ่งเอาแต่สู้กับ platform คำถามแรกที่ควรถามคือ คุณเอาสิ่งที่กำลังสู้อยู่นั้นออกไปได้ไหม

สำหรับบทก่อนหน้าในเรื่องยุค v1 ดูเอาการรีลีสใส่ไว้ใน CI, สองครั้ง (v1.8.4); และสำหรับภาพรวมทั้งเส้นเรื่อง ดูกายวิภาคของการส่งมอบซอฟต์แวร์ให้สมบูรณ์แบบ

บทความที่เกี่ยวข้อง

จาก OCR สู่ pixel โดยไม่ทิ้ง fallback
Steven
Steven4 นาทีที่อ่าน

จาก OCR สู่ pixel โดยไม่ทิ้ง fallback

GeekBye เลิกอ่าน screenshot ของคุณด้วย OCR แล้วเริ่มส่งภาพจริง ๆ ไปให้ vision model — ซึ่งเป็นส่วนที่ง่าย ส่วนที่ให้บทเรียนคือการ optimize ที่ข้าม OCR ไปทั้งหมดสำหรับผู้ใช้ vision ซึ่งทำให้ fallback ไปยัง model ที่ไม่รองรับ vision พังไปเงียบ ๆ และต้องถอยกลับไปยังเวอร์ชันที่ตาข่ายนิรภัยทำงานขนานกันและแทบไม่มีต้นทุนเลย

วิศวกรรม
AI
Desktop
เอาการรีลีสใส่ไว้ใน CI, สองครั้ง
Steven
Steven4 นาทีที่อ่าน

เอาการรีลีสใส่ไว้ใน CI, สองครั้ง

GeekBye v1.8.4 ย้าย release build ทั้ง macOS และ Windows เข้าไปใน CI สิ่งที่ changelog ไม่ได้บอกคือ เราเคยลองแบบนี้มาก่อนหนึ่งครั้ง, ลบทิ้งหนึ่งเดือนต่อมาเพราะค่า runner, และเพิ่งทำให้มันอยู่ตัวได้ในครั้งที่สอง — เพราะพอถึงตอนนั้น release script ก็ทำงานด้วยมือได้แล้ว

วิศวกรรม
CI/CD
Desktop
ส่งมอบสามสิบภาษาโดยไม่มีตาข่ายรองรับ
Steven
Steven4 นาทีที่อ่าน

ส่งมอบสามสิบภาษาโดยไม่มีตาข่ายรองรับ

GeekBye v1.8.3 ต่อ react-i18next เข้าไปและแปลทั้งแอปเป็น 30 ภาษา ส่วนที่เป็น library นั้นเป็นงานปกติ ส่วนที่น่าสนใจคือสิ่งที่ขาดหายไป: ไม่มีการเช็ก key parity และไม่มีตัวดึงสตริง — ภาษาอังกฤษที่ hardcode ไว้จึงหลุดออกมา, typo ภาษาเดนมาร์กหลุดขึ้น production, และ changelog เองก็ตกลงกันไม่ได้ว่าแอปพูดได้ 28, 29 หรือ 30 ภาษากันแน่

วิศวกรรม
i18n
Desktop