Lewati ke konten
Rafi Akmal Widikta

ui-ux

Dynamic Island for Hyprland

Notch bergaya perangkat keras untuk desktop Hyprland di atas AGS dan GTK3: ringkas saat diam, memanjang saat disorot, dengan media, lirik tersinkron, pomodoro, rak drag-and-drop, dan waktu salat.

Peran
Desktop Shell & Rekayasa UI (GTK3 / AGS) + Integrasi & Installer
Durasi
19–28 Oktober 2026
Asal
Dibuat dari nol
Tanggal
Oktober 2026

Teknologi

  • gjs
  • ags
  • gtk3
  • gtk-layer-shell
  • cairo
  • glib
  • dbus
  • mpris
  • bluez
  • hyprland
  • python
  • bash

Dampak

28 modul fitur dan 6 modul inti (5.805 baris GJS di 34 berkas), 13 halaman yang dirender oleh satu state machine, 107 kelas CSS di 956 baris, installer idempoten 924 baris, dan harness input Python 179 baris, dibangun dalam 13 commit.

Dibangun dari nol, dengan Nusa (rama2555.github.io/nusa) sebagai referensi gerak dan visual. Semua yang ada di repositori ini ditulis di sini: mesin geometri, seluruh halaman, theming, pipeline notifikasi, lapisan persistensi, dan installer-nya.

Masalah

Membuat widget desktop berperilaku seperti perangkat keras terlihat mudah sampai tiga hal muncul. Pertama, platform tidak bisa menganimasikan apa yang dibutuhkan desain: GTK3 tidak punya transform dan tidak menganimasikan width, height, atau border-radius dari CSS, jadi hasil jujur dari implementasi naif adalah kartu yang melar saat bergerak, radius sudut yang melompat antara 14 dan 32, animasi penutup yang kurang sampai 141×26 sebelum memantul balik, dan pil navigasi yang menabrak jam saat ringkas. Kedua, input pointer tidak bisa dipercaya apa adanya: GTK3 memancarkan leave-notify-event ketika pointer sekadar melintas ke child yang memiliki GdkWindow, dan build GJS ini sama sekali tidak mengekspos struct event-nya, sehingga event.detail, event.button, dan event.x bernilai undefined; notch pun menutup begitu pointer menyentuh kontennya sendiri, dan setiap klik panah diam-diam meleset. Ketiga, jendelanya tidak bertahan terhadap compositor: setelah resume atau hotplug monitor, gtk-layer-shell mengubah zwlr_layer_surface_v1.closed menjadi gtk_window_close(), GTK menghancurkan jendelanya, dan visible = true tidak lagi bisa menghidupkannya, sementara handler Variable global terus menembak widget yang sudah tidak ada.

Permukaannya punya batasan kerasnya sendiri: ia adalah jendela layer-shell yang tingginya persis setinggi notch, jadi apa pun yang digambar di luar kotak widget, entah bayangan atau glow, terpotong, dan kontennya harus bisa menyusut di bawah ukuran alaminya. Batasan tunggal itulah alasan latarnya dicat dengan Cairo alih-alih diberi style, dan alasan mesin geometri, aturan hover, serta jalur rebuild tidak bisa dirancang terpisah satu sama lain.

Solusi

Gerak adalah mesin JavaScript, bukan CSS. Solver cubic-bézier sungguhan (bezierTween di spring.js, cubic-bezier(.34,1.32,.5,1), 620 ms) menggerakkan set_size_request setiap frame, dengan radius sudut diinterpolasi dalam langkah diskret dan “ears” cekung digambar ulang per frame; nilai saat menutup dikurung ke target supaya overshoot kurvanya tidak pernah menyusutkan notch melewati tujuannya. Width lock dengan sisa 16px menghentikan permukaan layer agar tidak mengubah ukuran setiap frame, yang kalau tidak akan menelan klik mouse, dan wrapper-nya memakai CenterBox alih-alih Box, jadi titik tengah bingkai tetap presisi ke piksel sementara kartunya menyusut. Ruang navigasi dikurangi dari konten (stack.margin_bottom) dan dihaluskan mengikuti tinggi kartu, sehingga jam saat ringkas tidak pernah melompat.

Hover dan keyboard diverifikasi ke compositor, bukan sekadar dipercaya. Setiap leave memeriksa ulang posisi kursor sungguhan lewat hyprctl -j cursorpos terhadap persegi monitor yang di-cache, dijaga token supaya jawaban basi tidak menutup island yang baru saja dimasuki, ditambah watchdog 100 ms untuk jalur padding yang tidak memancarkan event lanjutan. Seluruh penanganan pointer melewati satu helper (modules/pointer.js) memakai event.get_button() dan event.get_coords(), karena field struct-nya tidak terbaca. Navigasi panah dipasang sebagai submap Hyprland berisi bind yang mengonsumsi, aktif hanya saat nav terlihat, sehingga panahnya tidak bocor ke aplikasi yang sedang fokus.

Jendelanya punya jalur rebuild: mountIsland() membuat ulang permukaannya saat destroy, watchdog dua detik dan sinyal resume logind memeriksa jendela yang sudah dibuang, dan setiap koneksi ke Variable global dilepas di handler destroy. Pemetaan monitor mencocokkan id Hyprland dengan indeks GdkMonitor berdasarkan geometri, karena keduanya bukan namespace yang sama.

Di luar gerak, ini produk sungguhan: kartu media MPRIS dengan sub-tampilan lirik tersinkron (LRCLIB lewat curl, parsing LRC, klik-untuk-melompat, dan auto-scroll yang berhenti delapan detik saat di-scroll manual), pomodoro, asupan air, todo (teksnya didelegasikan ke rofi, karena permukaannya keymode: none), rak drag-and-drop yang menyimpan referensi berkas alih-alih salinannya, halaman detail Bluetooth dengan slider volume yang digambar Cairo, dan halaman salat yang menghitung waktunya secara lokal dengan PrayTimes sehingga tetap berfungsi tanpa jaringan, di atas latar langit yang dicat dengan Cairo. Theming menyulih token wallust ke CSS dengan override danger dan ok yang semantik, plus penjaga hexToRgb yang menolak input bukan hex alih-alih diam-diam mengubah semua aksen menjadi hitam.

Keutuhan data diperlakukan sebagai bagian desain. Persistensi menulis JSON secara atomik lewat GLib.file_set_contents, di-debounce, dan melaporkan kegagalan ke pengguna alih-alih kehilangan data diam-diam; installer-nya idempoten dan berbasis marker, selalu mencadangkan ke .dyn-island.bak, mendukung –dry-run, dan me-restart waybar serta AGS dengan argumen command-line aslinya, jadi tidak pernah menulis ulang konfigurasi pengguna tanpa peringatan. Aksi destruktif pada rak dan todo bisa dikonfirmasi dan dibatalkan, dan popup notifikasi digambar island di layer overlay-nya sendiri, dengan geseran dianimasikan lewat offset scroll manual karena negative layer margin, margin_end, dan Revealer semuanya gagal menghasilkan slide di stack ini.

Catatan jujur: proyek ini tidak punya test suite otomatis konvensional dan tidak ada CI. Verifikasi dilakukan di compositor sungguhan, karena bug mahal di sini bersifat visual, bukan logis: perbaikan navigasi digerakkan lewat /dev/uinput dan dihitung per percobaan, hover dan centring dibaca ulang dengan hyprctl cursorpos dan hyprctl layers, dan geraknya diperiksa dengan mengambil sampel geometri per frame alih-alih menguji nilainya. Batasan yang diketahui dan terdokumentasi masih ada: OSD saat default-sink berubah tidak pernah terpicu karena AGS/Gvc tidak memancarkan speaker-changed untuknya, warna slider volume swaync tidak terjangkau dari style.css, dan equalizer saat ringkas hanya dekoratif, bukan visualisasi puncak audio sungguhan. Frontend di sini adalah GJS dengan target GTK3, bukan basis kode React atau TypeScript; backend-nya 34 berkas sumber GJS dan 5.805 baris, stylesheet-nya 956 baris di 107 kelas, dan seluruh pekerjaan mencakup 13 commit dari 19 sampai 28 Oktober 2026.

Kontak

Tertarik untuk bekerja sama?

Ceritakan apa yang sedang Anda bangun dan apa yang dibutuhkan. Catatan singkat sudah cukup.

Kembali ke atas