Compare commits

..

51 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
cbd9381997 Merge remote-tracking branch 'origin/dev' into little-zen
# Conflicts:
#	src/zen/kbs/ZenKeyboardShortcuts.sys.mjs

Co-authored-by: mr-cheffy <91018726+mr-cheffy@users.noreply.github.com>
2026-05-07 18:40:04 +00:00
mr. m
9c88e3a84f no-bug: Sync upstream Firefox to version 150.0.2 (gh-13601) 2026-05-07 18:28:25 +02:00
mr. m
b999a932ac no-bug: Improve swipe feel and avoid big jumps while swiping (gh-13603) 2026-05-07 17:08:50 +02:00
dependabot[bot]
e9a0beae20 no-bug: bump axios from 1.15.0 to 1.16.0 (gh-13590)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-06 23:12:02 +02:00
Daniel Richard G.
b93c2054c4 no-bug: A few quality-of-life tweaks (build tooling) (gh-13565) 2026-05-06 21:38:16 +02:00
mr. m
dcf272f620 gh-13516: Show the loading indicator ontop of the webview for toolbar mode (gh-13588) 2026-05-06 16:23:05 +02:00
mr. m
3ffdf6b299 gh-13583: Fixed Previous Space in menu bar not working (gh-13587) 2026-05-06 16:10:02 +02:00
mr. m
b052c1d804 gh-13584: Add click tab to split config (gh-13586) 2026-05-06 16:02:41 +02:00
mr. m
818c448dfc gh-10649: Update upstream patchs for session store (gh-13581) 2026-05-06 12:26:53 +02:00
mr. m
5b16bb36d9 no-bug: Dont recalculate urlbar overflow if its opened (gh-13571) 2026-05-05 11:59:50 +02:00
mr. m
5b790a431a gh-12699: Fixed switching windoww focus causing blinks (gh-13568) 2026-05-05 11:37:46 +02:00
Zander Otavka
149419d1fd no-bug: Add shortcut to create a new Space (gh-13552)
Co-authored-by: Zander Otavka <AlexanderOtavka@users.noreply.github.com>
2026-05-05 00:14:24 +02:00
mr. m
60a77413df no-bug: Use a different blur for the urlbar, depending on the theme (gh-13562) 2026-05-05 00:13:14 +02:00
mr. m
ce1ee9228b Merge commit from fork 2026-05-04 20:20:47 +02:00
sporocyst
1bea3d401c gh-13545: Fix regression from gh-13483 (gh-13554)
Co-authored-by: mr. m <mr.m@tuta.com>
2026-05-04 12:15:19 +02:00
Zander Otavka
b6624612cd no-bug: Submit workspace creation form on Enter and cancel on Escape (gh-13549)
Co-authored-by: Zander Otavka <AlexanderOtavka@users.noreply.github.com>
Co-authored-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
2026-05-04 10:21:43 +02:00
mr. m
aa463e2cb0 no-bug: Fix patch sets (gh-13557) 2026-05-04 10:07:22 +02:00
Zander Otavka
6552f0b3c3 no-bug: Add focus indicators to space creation form (gh-13550)
Co-authored-by: Zander Otavka <AlexanderOtavka@users.noreply.github.com>
2026-05-04 02:35:10 +02:00
mr. m
9950679f2c no-bug: Boosts optimization and enable acrylic elements for twilight (gh-13551) 2026-05-04 02:34:41 +02:00
mr. m
a9a582afae gh-13522: Fixed search engines not being visible in settings (gh-13547) 2026-05-03 23:35:14 +02:00
mr. m
607551f394 Merge commit from fork 2026-05-03 22:39:44 +02:00
mr. m
3278a43751 gh-13294: Fix macos crash with native popovers (gh-13546) 2026-05-03 22:09:17 +02:00
mr. m
c82d314913 no-bug: Make addons appear on the urlbar (gh-13535) 2026-05-03 14:53:37 +02:00
mr. m
a71a66c00b no-bug: Force boosts colors to fit in 32 bit ints (gh-13532) 2026-05-02 11:36:15 +02:00
mr. m
c0cd45bf1d gh-13530: Apply translations to desktop files (gh-13531) 2026-05-02 11:08:08 +02:00
mr. m
685cddf7c2 no-bug: Add space button 2026-05-01 19:11:12 +02:00
mr. m
562e6e5c3a gh-13488: Fixed auto hide not working after installing an extension (gh-13498) 2026-04-29 20:31:46 +02:00
mr. m
27f40393d5 no-bug: Simplify stylings 2026-04-29 20:11:05 +02:00
mr. m
21f7fdaef4 no-bug: Increase tab hover scaling (gh-13487) 2026-04-29 12:20:47 +02:00
mr. m
04c5bc3478 no-bug: Bump release version (gh-13486) 2026-04-29 12:06:58 +02:00
mr. m
f99b8af86d no-bug: Collapse top buttons for little zen 2026-04-29 12:04:32 +02:00
sporocyst
092ee7bc2a gh-7094: Fix webcontent shift when bookmark + title bar is active (gh-13483) 2026-04-29 10:00:14 +02:00
mr. m
e3b0295e36 no-bug: Continue work 2026-04-29 02:21:22 +02:00
Ezhik
18c15ac0b8 no-bug: Add workspaces search (gh-12745)
Co-authored-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
Co-authored-by: jababda <jababda@hotmail.com>
2026-04-28 20:56:16 +02:00
mr. m
c0620a95ef no-bug: Sync upstream Firefox to version 150.0.1 (gh-13472) 2026-04-28 20:37:46 +02:00
sporocyst
c939ee4a3d gh-7094: Fixed topbar dropping down when titlebar is enabled (gh-13468) 2026-04-28 20:37:19 +02:00
Andre Nijman
709691225a gh-12142: Clear pending attribute on synced tabs (gh-13460)
Co-authored-by: mr. m <mr.m@tuta.com>
2026-04-28 20:36:32 +02:00
mr. m
bb14e5d66f no-bug: Make checks if boost css is undefined (gh-13451) 2026-04-28 00:49:18 +02:00
mr. m
d15f5331ff Merge branch 'dev' into little-zen 2026-04-28 00:38:39 +02:00
mr. m
cccbcf662e no-bug: Start working on little zen 2026-04-28 00:38:09 +02:00
mr. m
92dfe0babd gh-13439: Add Boosts implementation (gh-12586)
Co-authored-by: fen4flo <75260616+FlorianButz@users.noreply.github.com>
Co-authored-by: fen4flo <fb8599422@gmail.com>
2026-04-27 13:24:30 +02:00
mr. m
4a7b517460 no-bug: Always show initial url if empty tab is selected (gh-13435) 2026-04-27 09:52:01 +02:00
mr. m
102dcb1993 no-bug: Fixed startup cacresh when no initial tab is found (gh-13434) 2026-04-27 09:44:32 +02:00
mr. m
040bed5ce5 gh-8643: Fixed dragging tabs between windows not hiding sidebar (gh-13430) 2026-04-27 00:32:12 +02:00
mr. m
6d3a59ac04 gh-10594: Fixed video controls being off screen in compact mode (gh-13428) 2026-04-27 00:29:20 +02:00
mr. m
4d26b4d597 no-bug: Make transparent urlbar work again (gh-13424) 2026-04-26 19:20:36 +02:00
mr. m
6aafd8cb34 gh-13414: Fixed browser startup not being responsive (gh-13418) 2026-04-26 12:25:44 +02:00
mr. m
2028a9b288 gh-13390: Fixed splitting out a single tab ending up non-functional (gh-13419) 2026-04-26 12:25:26 +02:00
mr. m
cd956d34b1 gh-13400: Fixed modal text being cutoff (gh-13410) 2026-04-26 00:00:44 +02:00
Ivaon
0cfc25ccb0 no-bug: contain of .zen-glance-sidebar-container too strict (gh-13401) 2026-04-25 16:47:59 +02:00
mr. m
0199e256d6 gh-13393: Fixed print dialog appearing off the screen (gh-13397) 2026-04-25 11:01:00 +02:00
187 changed files with 12863 additions and 1390 deletions

View File

@@ -34,8 +34,8 @@ Zen is a firefox-based browser with the aim of pushing your productivity to a ne
### Firefox Versions
- [`Release`](https://zen-browser.app/download) - Is currently built using Firefox version `150.0`! 🚀
- [`Twilight`](https://zen-browser.app/download?twilight) - Is currently built using Firefox version `RC 150.0`!
- [`Release`](https://zen-browser.app/download) - Is currently built using Firefox version `150.0.2`! 🚀
- [`Twilight`](https://zen-browser.app/download?twilight) - Is currently built using Firefox version `RC 150.0.2`!
### Contributing

View File

@@ -1,31 +1,698 @@
[Desktop Entry]
Name=Zen Browser
Comment=Experience tranquillity while browsing the web without people tracking you!
Comment=A fast, private and secure web browser built to improve your day-to-day experience.
Exec=zen %u
Icon=zen
Type=Application
MimeType=text/html;text/xml;application/xhtml+xml;x-scheme-handler/http;x-scheme-handler/https;application/x-xpinstall;application/pdf;application/json;
MimeType=application/json;application/pdf;application/rdf+xml;application/rss+xml;application/x-xpinstall;application/xhtml+xml;application/xml;audio/flac;audio/ogg;audio/webm;image/avif;image/gif;image/jpeg;image/png;image/svg+xml;image/webp;text/html;text/xml;video/ogg;video/webm;x-scheme-handler/chrome;x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/mailto;
StartupWMClass=zen
Categories=Network;WebBrowser;
StartupNotify=true
Terminal=false
X-MultipleArgs=false
GenericName=Web Browser
GenericName[ach]=Web Browser
GenericName[af]=Web Browser
GenericName[an]=Web Browser
GenericName[ar]=متصفح الوِب
GenericName[ast]=Web Browser
GenericName[az]=Web Browser
GenericName[be]=Вэб-браўзер
GenericName[bg]=Уеб браузър
GenericName[bn]=ওয়েব ব্রাউজার
GenericName[bqi]=گشت گر وب
GenericName[br]=Merdeer Web
GenericName[brx]=Web Browser
GenericName[bs]=Web pretraživač
GenericName[ca]=Navegador web
GenericName[ca_valencia]=Web Browser
GenericName[cak]=Web Okik'amaya'l
GenericName[ckb]=Web Browser
GenericName[cs]=Webový prohlížeč
GenericName[cy]=Porwr Gwe
GenericName[da]=Webbrowser
GenericName[de]=Internet-Browser
GenericName[dsb]=Webwobglědowak
GenericName[el]=Πρόγραμμα περιήγησης
GenericName[en_CA]=Web Browser
GenericName[en_GB]=Web Browser
GenericName[eo]=Retumilo
GenericName[es_AR]=Navegador web
GenericName[es_CL]=Navegador Web
GenericName[es_ES]=Navegador web
GenericName[es_MX]=Navegador Web
GenericName[et]=Web Browser
GenericName[eu]=Web nabigatzailea
GenericName[fa]=مرورگر وب
GenericName[ff]=Web Browser
GenericName[fi]=Verkkoselain
GenericName[fr]=Navigateur web
GenericName[fur]=Navigadôr Web
GenericName[fy_NL]=Webbrowser
GenericName[ga_IE]=Web Browser
GenericName[gd]=Brabhsair-lìn
GenericName[gl]=Navegador web
GenericName[gn]=Ñanduti Kundahára
GenericName[gu_IN]=Web Browser
GenericName[he]=דפדפן אינטרנט
GenericName[hi_IN]=Web Browser
GenericName[hr]=Web preglednik
GenericName[hsb]=Webwobhladowak
GenericName[hu]=Webböngésző
GenericName[hy_AM]=Վեբ դիտարկիչ
GenericName[hye]=Web Browser
GenericName[ia]=Navigator web
GenericName[id]=Peramban Web
GenericName[is]=Vafri
GenericName[it]=Browser web
GenericName[ja]=ウェブブラウザー
GenericName[ka]=ბრაუზერი
GenericName[kab]=Iminig web
GenericName[kk]=Веб-браузері
GenericName[km]=Web Browser
GenericName[kn]=Web Browser
GenericName[ko]=웹 브라우저
GenericName[lij]=Navegatô Web
GenericName[lo]=ຕົວ​ທ່ອງ​ເວັບ​ເວັບ​ໄຊ​ຕ​໌
GenericName[lt]=Web Browser
GenericName[ltg]=Web Browser
GenericName[lv]=Tīmekļa pārlūks
GenericName[meh]=Web Browser
GenericName[mk]=Web Browser
GenericName[ml]=ഗോളാന്തരവല അന്വേഷിയന്ത്രം
GenericName[mr]=Web Browser
GenericName[ms]=Web Browser
GenericName[my]=Web Browser
GenericName[nb_NO]=Nettleser
GenericName[ne_NP]=वेब ब्राउजर
GenericName[nl]=Webbrowser
GenericName[nn_NO]=Nettlesar
GenericName[oc]=Navegador Web
GenericName[pa_IN]=ਵੈੱਬ ਬਰਾਊਜ਼ਰ
GenericName[pl]=Przeglądarka internetowa
GenericName[pt_BR]=Navegador web
GenericName[pt_PT]=Navegador Web
GenericName[rm]=Navigatur web
GenericName[ro]=Browser web
GenericName[ru]=Веб-браузер
GenericName[sat]=ᱣᱮᱵᱽ ᱵᱽᱨᱟᱣᱡᱚᱨ
GenericName[sc]=Navigadore web
GenericName[sco]=Web Browser
GenericName[si]=වියමන අතිරික්සුව
GenericName[sk]=Webový prehliadač
GenericName[skr]=ویب براؤزر
GenericName[sl]=Spletni brskalnik
GenericName[son]=Web Browser
GenericName[sq]=Shfletues
GenericName[sr]=Веб прегледач
GenericName[sv_SE]=Webbläsare
GenericName[szl]=Web Browser
GenericName[ta]=Web Browser
GenericName[te]=జాల విహారిణి
GenericName[tg]=Браузери веб
GenericName[th]=เว็บเบราว์เซอร์
GenericName[tl]=Web Browser
GenericName[tr]=Web Tarayıcısı
GenericName[trs]=Web riña gāchē nu
GenericName[uk]=Браузер
GenericName[ur]=Web Browser
GenericName[uz]=Web Browser
GenericName[vi]=Trình duyệt web
GenericName[wo]=Web Browser
GenericName[xh]=Web Browser
GenericName[zh_CN]=Web 浏览器
GenericName[zh_TW]=網頁瀏覽器
Keywords=Internet;WWW;Browser;Web;Explorer;
Keywords[ach]=Internet;WWW;Browser;Web;Explorer;
Keywords[af]=Internet;WWW;Browser;Web;Explorer;
Keywords[an]=Internet;WWW;Browser;Web;Explorer;
Keywords[ar]=إنترنت;WWW;متصفح;ويب;مستكشف;
Keywords[ast]=Internet;WWW;Browser;Web;Explorer;
Keywords[az]=Internet;WWW;Browser;Web;Explorer;
Keywords[be]=Internet;WWW;Browser;Web;Explorer;
Keywords[bg]=Internet;WWW;Browser;Web;Explorer;
Keywords[bn]=ইন্টারনেট;WWW;ব্রাউজার;ওয়েব;এক্সপ্লোরার;
Keywords[bqi]=Internet;WWW;Browser;Web;Explorer;
Keywords[br]=Internet;WWW;Merdeer;Web;Ergerzhout;
Keywords[brx]=Internet;WWW;Browser;Web;Explorer;
Keywords[bs]=Internet;WWW;Pretraživač;Web;Istraživač;
Keywords[ca]=Internet;WWW;Browser;Web;Explorador;Navegador;
Keywords[ca_valencia]=Internet;WWW;Browser;Web;Explorer;
Keywords[cak]=K'amaya'l;WWW;Okik'amaya'l;Kanob'äl;
Keywords[ckb]=Internet;WWW;Browser;Web;Explorer;
Keywords[cs]=internet;WWW;prohlížeč;web;
Keywords[cy]=Rhyngrwyd;WWW;Porwr;Gwe;Archwiliwr;
Keywords[da]=Internet;WWW;Browser;Nettet;Explorer;
Keywords[de]=Internet;WWW;Browser;Web;Explorer;
Keywords[dsb]=Internet;WWW;wobglědowak;Web;Explorer;
Keywords[el]=Internet;WWW;Browser;Web;Explorer;Διαδίκτυο;Ιστός;Ίντερνετ;
Keywords[en_CA]=Internet;WWW;Browser;Web;Explorer;
Keywords[en_GB]=Internet;WWW;Browser;Web;Explorer;
Keywords[eo]=Interreto;Retumilo;TTT;Teksaĵo;Reto;Internet;WWW;Browser;Web;Explorer;
Keywords[es_AR]=Internet;WWW;Navegador;Web;Explorador;
Keywords[es_CL]=Internet;WWW;Navegador;Web;Explorador;
Keywords[es_ES]=Internet;WWW;Navegador;Web;Explorador;
Keywords[es_MX]=Internet;WWW;Navegador;Web;Explorador;
Keywords[et]=Internet;WWW;Browser;Web;Explorer;
Keywords[eu]=Internet;WWW;Nabigatzailea;Web;Arakatzailea;
Keywords[fa]=Internet;WWW;Browser;Web;Explorer;
Keywords[ff]=Internet;WWW;Browser;Web;Explorer;
Keywords[fi]=Internet;WWW;Browser;Web;Explorer;netti;webbi;selain;
Keywords[fr]=Internet;WWW;Navigateur;Web;Explorer;
Keywords[fur]=Internet;WWW;Browser;Navigadôr;Web;Esploradôr;Explorer;
Keywords[fy_NL]=Ynternet;WWW;Browser;Web;Ferkenner;
Keywords[ga_IE]=Internet;WWW;Browser;Web;Explorer;
Keywords[gd]=Internet;WWW;Browser;Web;Explorer;eadar-lìon;brabhsair;brobhsair;lìon;taisgealaiche;
Keywords[gl]=Internet;WWW;Navegador;Web;Explorador;
Keywords[gn]=Internet;WWW;Browser;Web;Explorer;
Keywords[gu_IN]=Internet;WWW;Browser;Web;Explorer;
Keywords[he]=אינטרנט;WWW;דפדפן;רשת;סייר;מרשתת;
Keywords[hi_IN]=Internet;WWW;Browser;Web;Explorer;
Keywords[hr]=Internet;WWW;Preglednik;Web;Istraživač;
Keywords[hsb]=Internet;WWW;wobhladowak;Web;Explorer;
Keywords[hu]=Internet;WWW;Böngésző;Web;Világháló;
Keywords[hy_AM]=Համացանց,WWW,Զննիչ,Վեբ,Ցանցախույզ:
Keywords[hye]=Internet;WWW;Browser;Web;Explorer;
Keywords[ia]=Internet;WWW;Navigator;Web;Explorator;
Keywords[id]=Internet;WWW;Browser;Web;Explorer;
Keywords[is]=Internet;WWW; Vafri; Vefur; Explorer;
Keywords[it]=Internet;WWW;Browser;Web;Explorer;Navigatore;
Keywords[ja]=Internet;WWW;Browser;Web;Explorer;インターネット;ブラウザー;ウェブ;
Keywords[ka]=ინტერნეტი;WWW;ბრაუზერი;ქსელი;ქსელთან წვდომა;
Keywords[kab]=Internet;WWW;Browser;Web;Explorer;
Keywords[kk]=Internet;WWW;Browser;Web;Explorer;Интернет;Ғаламтор;Браузер;Желі;Шолғыш;
Keywords[km]=Internet;WWW;Browser;Web;Explorer;
Keywords[kn]=Internet;WWW;Browser;Web;Explorer;
Keywords[ko]=인터넷;브라우저;웹;탐색기;Internet;WWW;Browser;Web;Explorer;
Keywords[lij]=Internet;WWW;Browser;Web;Explorer;Navegatô;
Keywords[lo]=Internet;WWW;Browser;Web;Explorer;
Keywords[lt]=Internet;WWW;Browser;Web;Explorer;
Keywords[ltg]=Internet;WWW;Browser;Web;Explorer;
Keywords[lv]=Internets;WWW;Pārlūkprogramma;Tīmeklis;
Keywords[meh]=Internet;WWW;Browser;Web;Explorer;
Keywords[mk]=Internet;WWW;Browser;Web;Explorer;
Keywords[ml]=ഗോളാന്തരവല;WWW;അന്വേഷിയന്ത്രം;ഗോളാന്തരവല;ആരായൻ;
Keywords[mr]=Internet;WWW;Browser;Web;Explorer;
Keywords[ms]=Internet;WWW;Browser;Web;Explorer;
Keywords[my]=Internet;WWW;Browser;Web;Explorer;
Keywords[nb_NO]=Internett;WWW;Nettleser;Web;Utforsker;
Keywords[ne_NP]=Internet;WWW;Browser;Web;Explorer;
Keywords[nl]=Internet;WWW;Browser;Web;Verkenner;
Keywords[nn_NO]=Internett;WWW;Nettlesar;Web;Utforskar;
Keywords[oc]=Internet;WWW;Navegador;Navigador;Navegator;Navigator;Web;Explorer;
Keywords[pa_IN]=ਇੰਟਰਨੈੱਟ;WWW;ਬਰਾਊਜ਼ਰ;ਵੈੱਬ;ਐਕਸਪਲਰੋਰ;ਵੈਬ;ਇੰਟਰਨੈਟ;
Keywords[pl]=Internet;WWW;Przeglądarka;Browser;Wyszukiwarka;Web;Sieć;Explorer;Eksplorer;Strony;Witryny;internetowe;
Keywords[pt_BR]=Internet;WWW;Browser;Web;Explorer;Navegador;
Keywords[pt_PT]=Internet;WWW;Navegador;Web;Explorador;
Keywords[rm]=Internet;WWW;Browser;Web;Explorer;navigatur;
Keywords[ro]=Internet; WWW; Browser; Web; Explorer;
Keywords[ru]=Сеть;Интернет;Браузер;Доступ в Интернет;
Keywords[sat]=Internet;WWW;Browser;Web;Explorer;
Keywords[sc]=Internet;WWW;Navigadore;Web;Explorer;
Keywords[sco]=Internet;WWW;Browser;Web;Explorer;
Keywords[si]=අන්තර්ජාලය;අතිරික්සුව;පිරික්සන්න;ගවේශකය;Internet;WWW;Browser;Web;Explorer;
Keywords[sk]=Internet;WWW;Prehliadač;Web;Prieskumník;
Keywords[skr]=Internet;WWW;Browser;Web;Explorer;
Keywords[sl]=internet;www;brskalnik;splet;
Keywords[son]=Internet;WWW;Browser;Web;Explorer;
Keywords[sq]=Internet;WWW;Shfletues;Web;Eksplorues;
Keywords[sr]=Internet;WWW;Browser;Web;Explorer;интернет;њњњ;веб;мрежа;прегледач;експлорер;internet;pregledač;veb;mreža;pregledač;eksplorer;
Keywords[sv_SE]=Internet;WWW;Webbläsare;Webb;Utforskare;
Keywords[szl]=Internet;WWW;Browser;Web;Explorer;
Keywords[ta]=Internet;WWW;Browser;Web;Explorer;
Keywords[te]=Internet;WWW;Browser;Web;Explorer;
Keywords[tg]=Интернет;WWW;Браузер;Сомона;Ҷустуҷӯгар;
Keywords[th]=อินเทอร์เน็ต;เบราว์เซอร์;เว็บ;Internet;WWW;Browser;Web;Explorer;
Keywords[tl]=Internet;WWW;Browser;Web;Explorer;
Keywords[tr]=Internet;WWW;Browser;Web;Explorer;İnternet;Tarayıcı;
Keywords[trs]=Internet;WWW;Browser;Web;Explorer;
Keywords[uk]=Інтернет;WWW;Браузер;Веб;Переглядач;
Keywords[ur]=Internet;WWW;Browser;Web;Explorer;
Keywords[uz]=Internet;WWW;Browser;Web;Explorer;
Keywords[vi]=Internet;WWW;Trình duyệt;Web;Duyệt web;
Keywords[wo]=Internet;WWW;Browser;Web;Explorer;
Keywords[xh]=Internet;WWW;Browser;Web;Explorer;
Keywords[zh_CN]=Internet;WWW;Browser;Web;Explorer;
Keywords[zh_TW]=網際網路;網路;瀏覽器;網頁;上網;Internet;WWW;Browser;Web;Explorer;
Actions=new-window;new-blank-window;new-private-window;profilemanager;
X-AppImage-Version=$VERSION
[Desktop Action new-window]
Name=Open a New Window
Exec=zen %u
Name=New Window
Name[ach]=New Window
Name[af]=New Window
Name[an]=New Window
Name[ar]=نافذة جديدة
Name[ast]=New Window
Name[az]=New Window
Name[be]=Новае акно
Name[bg]=Нов прозорец
Name[bn]=নতুন উইন্ডো
Name[bqi]=نیمدری نۊ
Name[br]=Prenestr nevez
Name[brx]=New Window
Name[bs]=Novi prozor
Name[ca]=Finestra nova
Name[ca_valencia]=New Window
Name[cak]=K'ak'a' Tzuwäch
Name[ckb]=New Window
Name[cs]=Nové okno
Name[cy]=Ffenestr Newydd
Name[da]=Nyt vindue
Name[de]=Neues Fenster
Name[dsb]=Nowe wokno
Name[el]=Νέο παράθυρο
Name[en_CA]=New Window
Name[en_GB]=New Window
Name[eo]=Nova fenestro
Name[es_AR]=Nueva ventana
Name[es_CL]=Nueva ventana
Name[es_ES]=Nueva ventana
Name[es_MX]=Nueva ventana
Name[et]=New Window
Name[eu]=Leiho berria
Name[fa]=پنجره جدید‌
Name[ff]=New Window
Name[fi]=Uusi ikkuna
Name[fr]=Nouvelle fenêtre
Name[fur]=Gnûf barcon
Name[fy_NL]=Nij finster
Name[ga_IE]=New Window
Name[gd]=Uinneag ùr
Name[gl]=Nova xanela
Name[gn]=Ovetã pyahu
Name[gu_IN]=New Window
Name[he]=חלון חדש
Name[hi_IN]=New Window
Name[hr]=Novi prozor
Name[hsb]=Nowe wokno
Name[hu]=Új ablak
Name[hy_AM]=Նոր պատուհան
Name[hye]=New Window
Name[ia]=Nove fenestra
Name[id]=Jendela Baru
Name[is]=Nýr gluggi
Name[it]=Nuova finestra
Name[ja]=新しいウィンドウ
Name[ka]=ახალი ფანჯარა
Name[kab]=Asfaylu amaynut
Name[kk]=Жаңа терезе
Name[km]=បង្អួច​​​ថ្មី
Name[kn]=New Window
Name[ko]=새 창
Name[lij]=Neuvo Barcon
Name[lo]=ວິນໂດໃໝ່
Name[lt]=New Window
Name[ltg]=New Window
Name[lv]=Jauns logs
Name[meh]=New Window
Name[mk]=New Window
Name[ml]=പുതിയ ജാലകം
Name[mr]=New Window
Name[ms]=New Window
Name[my]=New Window
Name[nb_NO]=Nytt vindu
Name[ne_NP]=नयाँ सञ्झ्याल
Name[nl]=Nieuw venster
Name[nn_NO]=Nytt vindauge
Name[oc]=Fenèstra novèla
Name[pa_IN]=ਨਵੀਂ ਵਿੰਡੋ
Name[pl]=Nowe okno
Name[pt_BR]=Nova janela
Name[pt_PT]=Nova janela
Name[rm]=Nova fanestra
Name[ro]=Fereastră nouă
Name[ru]=Новое окно
Name[sat]=ᱱᱟᱶᱟ ᱣᱤᱱᱰᱳ
Name[sc]=Ventana noa
Name[sco]=New Window
Name[si]=නව කවුළුව
Name[sk]=Nové okno
Name[skr]=نویں ونڈو
Name[sl]=Novo okno
Name[son]=New Window
Name[sq]=Dritare e Re
Name[sr]=Нови прозор
Name[sv_SE]=Nytt fönster
Name[szl]=New Window
Name[ta]=New Window
Name[te]=కొత్త కిటికీ
Name[tg]=Равзанаи нав
Name[th]=หน้าต่างใหม่
Name[tl]=New Window
Name[tr]=Yeni pencere
Name[trs]=Bēntanâ nākàa
Name[uk]=Нове вікно
Name[ur]=New Window
Name[uz]=New Window
Name[vi]=Cửa sổ mới
Name[wo]=New Window
Name[xh]=New Window
Name[zh_CN]=新建窗口
Name[zh_TW]=開新視窗
[Desktop Action new-blank-window]
Name=Open a New Blank Window
Exec=zen --blank-window %u
Name=New Blank Window
Name[ach]=New Blank Window
Name[af]=New Blank Window
Name[an]=New Blank Window
Name[ar]=نافذة فارغة جديدة
Name[ast]=New Blank Window
Name[az]=New Blank Window
Name[be]=Новае пустое акно
Name[bg]=Нов празен прозорец
Name[bn]=নতুন ফাঁকা উইন্ডো
Name[bqi]=نیمدری نۊ خالی
Name[br]=Prenestr goulloù nevez
Name[brx]=New Blank Window
Name[bs]=Novi prazni prozor
Name[ca]=Finestra en blanc nova
Name[ca_valencia]=New Blank Window
Name[cak]=K'ak'a' Tzuwäch K'axk'ol
Name[ckb]=New Blank Window
Name[cs]=Nové prázdné okno
Name[cy]=Ffenestr Wag Newydd
Name[da]=Nyt tomt vindue
Name[de]=Neues leeres Fenster
Name[dsb]=Nowe prázdne wokno
Name[el]=Νέο κενό παράθυρο
Name[en_CA]=New Blank Window
Name[en_GB]=New Blank Window
Name[eo]=Nova malplena fenestro
Name[es_AR]=Nueva ventana en blanco
Name[es_CL]=Nueva ventana en blanco
Name[es_ES]=Nueva ventana en blanco
Name[es_MX]=Nueva ventana en blanco
Name[et]=New Blank Window
Name[eu]=Leiho huts berri
Name[fa]=پنجره خالی جدید
Name[ff]=New Blank Window
Name[fi]=Uusi tyhjä ikkuna
Name[fr]=Nouvelle fenêtre vierge
Name[fur]=Gnûf barcon vuot
Name[fy_NL]=Nij leeg finster
Name[ga_IE]=New Blank Window
Name[gd]=Uinneag bhàn ùr
Name[gl]=Nova xanela en branco
Name[gn]=Ovetã ñemi pyahu
Name[gu_IN]=New Blank Window
Name[he]=חלון ריק חדש
Name[hi_IN]=New Blank Window
Name[hr]=Novi prazni prozor
Name[hsb]=Nowe prázdne wokno
Name[hu]=Új üres ablak
Name[hy_AM]=Նոր դատարկ պատուհան
Name[hye]=New Blank Window
Name[ia]=Nove fenestra vacue
Name[id]=Jendela Kosong Baru
Name[is]=Nýr tómur gluggi
Name[it]=Nuova finestra vuota
Name[ja]=新しい空白のウィンドウ
Name[ka]=ახალი ცარიელი ფანჯარა
Name[kab]=Asfaylu amaynut n tunigin tusligt
Name[kk]=Жаңа бос терезе
Name[km]=បង្អួច​ថ្មី​ឯកជន
Name[kn]=New Blank Window
Name[ko]=새 빈 창
Name[lij]=Neuvo Barcon Vuot
Name[lo]=ວິນໂດແອ່ງໃໝ່
Name[lt]=New Blank Window
Name[ltg]=New Blank Window
Name[lv]=Jauns tukšais logs
Name[meh]=New Blank Window
Name[mk]=New Blank Window
Name[ml]=പുതിയ ശൂന്യ ജാലകം
Name[mr]=New Blank Window
Name[ms]=New Blank Window
Name[my]=New Blank Window
Name[nb_NO]=Nytt tomt vindu
Name[ne_NP]=नयाँ खाली सञ्झ्याल
Name[nl]=Nieuw leeg venster
Name[nn_NO]=Nytt tomt vindauge
Name[oc]=Fenèstra en blanc novèla
Name[pa_IN]=ਨਵੀਂ ਖਾਲੀ ਵਿੰਡੋ
Name[pl]=Nowe puste okno
Name[pt_BR]=Nova janela em branco
Name[pt_PT]=Nova janela em branco
Name[rm]=Nova fanestra vacue
Name[ro]=Fereastră nouă și goală
Name[ru]=Новое пустое окно
Name[sat]=ᱱᱟᱶᱟ ᱣᱤᱱᱰᱳ ᱵᱽᱨᱟᱣᱡᱚᱨ
Name[sc]=Ventana en blanc noa
Name[sco]=New Blank Window
Name[si]=නව හිස් කවුළුව
Name[sk]=Nové prázdné okno
Name[skr]=نویں خالی ونڈو
Name[sl]=Novo prazno okno
Name[son]=New Blank Window
Name[sq]=Dritare e Re e Pafajshme
Name[sr]=Нови празни прозор
Name[sv_SE]=Nytt tomt fönster
Name[szl]=New Blank Window
Name[ta]=New Blank Window
Name[te]=కొత్త ఖాళీ కిటికీ
Name[tg]=Равзанаи холӣ нав
Name[th]=หน้าต่างว่างเปล่าใหม่
Name[tl]=New Blank Window
Name[tr]=Yeni boş pencere
Name[trs]=Bēntanâ huì nākàa
Name[uk]=Нове порожнє вікно
Name[ur]=New Blank Window
Name[uz]=New Blank Window
Name[vi]=Cửa sổ trống mới
Name[wo]=New Blank Window
Name[xh]=New Blank Window
Name[zh_CN]=新建空白窗口
Name[zh_TW]=開新空白視窗
[Desktop Action new-private-window]
Name=Open a New Private Window
Exec=zen --private-window %u
Name=New Private Window
Name[ach]=New Private Window
Name[af]=New Private Window
Name[an]=New Private Window
Name[ar]=نافذة خاصة جديدة
Name[ast]=New Private Window
Name[az]=New Private Window
Name[be]=Новае прыватнае акно
Name[bg]=Нов личен прозорец
Name[bn]=নতুন ব্যক্তিগত উইন্ডো
Name[bqi]=نیمدری سیخومی نۊ
Name[br]=Prenestr prevez nevez
Name[brx]=New Private Window
Name[bs]=Novi privatni prozor
Name[ca]=Finestra privada nova
Name[ca_valencia]=New Private Window
Name[cak]=K'ak'a' Ichinan Tzuwäch
Name[ckb]=New Private Window
Name[cs]=Nové anonymní okno
Name[cy]=Ffenestr Breifat Newydd
Name[da]=Nyt privat vindue
Name[de]=Neues privates Fenster
Name[dsb]=Nowe priwatne wokno
Name[el]=Νέο ιδιωτικό παράθυρο
Name[en_CA]=New Private Window
Name[en_GB]=New Private Window
Name[eo]=Nova privata fenestro
Name[es_AR]=Nueva ventana privada
Name[es_CL]=Nueva ventana privada
Name[es_ES]=Nueva ventana privada
Name[es_MX]=Nueva ventana privada
Name[et]=New Private Window
Name[eu]=Leiho pribatu berria
Name[fa]=پنجره ناشناس جدید
Name[ff]=New Private Window
Name[fi]=Uusi yksityinen ikkuna
Name[fr]=Nouvelle fenêtre privée
Name[fur]=Gnûf barcon privât
Name[fy_NL]=Nij priveefinster
Name[ga_IE]=New Private Window
Name[gd]=Uinneag phrìobhaideach ùr
Name[gl]=Nova xanela privada
Name[gn]=Ovetã ñemi pyahu
Name[gu_IN]=New Private Window
Name[he]=חלון פרטי חדש
Name[hi_IN]=New Private Window
Name[hr]=Novi privatni prozor
Name[hsb]=Nowe priwatne wokno
Name[hu]=Új privát ablak
Name[hy_AM]=Նոր գաղտնի պատուհան
Name[hye]=New Private Window
Name[ia]=Nove fenestra private
Name[id]=Jendela Mode Pribadi Baru
Name[is]=Nýr huliðsgluggi
Name[it]=Nuova finestra anonima
Name[ja]=新しいプライベートウィンドウ
Name[ka]=ახალი პირადი ფანჯარა
Name[kab]=Asfaylu amaynut n tunigin tusligt
Name[kk]=Жаңа жекелік терезе
Name[km]=បង្អួច​ឯកជន​ថ្មី
Name[kn]=New Private Window
Name[ko]=새 사생활 보호 창
Name[lij]=Neuvo Barcon Privòu
Name[lo]=ວິນໂດສ່ວນຕົວໃໝ່
Name[lt]=New Private Window
Name[ltg]=New Private Window
Name[lv]=Jauns privātais logs
Name[meh]=New Private Window
Name[mk]=New Private Window
Name[ml]=പുതിയ സ്വകാര്യ ജാലകം
Name[mr]=New Private Window
Name[ms]=New Private Window
Name[my]=New Private Window
Name[nb_NO]=Nytt privat vindu
Name[ne_NP]=नयाँ निजी सञ्झ्याल
Name[nl]=Nieuw privévenster
Name[nn_NO]=Nytt privat vindauge
Name[oc]=Fenèstra privada novèla
Name[pa_IN]=ਨਵੀਂ ਪ੍ਰਾਈਵੇਟ ਵਿੰਡੋ
Name[pl]=Nowe okno prywatne
Name[pt_BR]=Nova janela privativa
Name[pt_PT]=Nova janela privada
Name[rm]=Nova fanestra privata
Name[ro]=Fereastră privată nouă
Name[ru]=Новое приватное окно
Name[sat]=ᱱᱟᱶᱟ ᱱᱤᱡᱮᱨᱟᱜ ᱣᱤᱱᱰᱳ
Name[sc]=Ventana privada noa
Name[sco]=New Private Window
Name[si]=නව පෞද්. කවුළුව
Name[sk]=Nové súkromné okno
Name[skr]=نویں نجی ونڈو
Name[sl]=Novo zasebno okno
Name[son]=New Private Window
Name[sq]=Dritare e Re Private
Name[sr]=Нови приватни прозор
Name[sv_SE]=Nytt privat fönster
Name[szl]=New Private Window
Name[ta]=New Private Window
Name[te]=కొత్త ఆంతరంగిక కిటికీ
Name[tg]=Равзанаи хусусии нав
Name[th]=หน้าต่างส่วนตัวใหม่
Name[tl]=New Private Window
Name[tr]=Yeni gizli pencere
Name[trs]=Bēntanâ huì nākàa
Name[uk]=Приватне вікно
Name[ur]=New Private Window
Name[uz]=New Private Window
Name[vi]=Cửa sổ riêng tư mới
Name[wo]=New Private Window
Name[xh]=New Private Window
Name[zh_CN]=新建隐私窗口
Name[zh_TW]=開新隱私視窗
[Desktop Action profilemanager]
Name=Open the Profile Manager
Exec=zen --ProfileManager %u
Name=Open Profile Manager
Name[ach]=Open Profile Manager
Name[af]=Open Profile Manager
Name[an]=Open Profile Manager
Name[ar]=افتح مدير الملف الشخصي
Name[ast]=Open Profile Manager
Name[az]=Open Profile Manager
Name[be]=Адкрыць менеджар профіляў
Name[bg]=Отваряне на мениджъра на профили
Name[bn]=Open Profile Manager
Name[bqi]=گۊشیڌن دؽوۉداری پوروفایل
Name[br]=Digeriñ an ardoer aeladoù
Name[brx]=Open Profile Manager
Name[bs]=Otvori Menadžera profila
Name[ca]=Obre el gestor de perfils
Name[ca_valencia]=Open Profile Manager
Name[cak]=Open Profile Manager
Name[ckb]=Open Profile Manager
Name[cs]=Otevřít Správce profilů
Name[cy]=Agorwch y Rheolwr Proffil
Name[da]=Åbn profilhåndtering
Name[de]=Profilverwaltung öffnen
Name[dsb]=Profilowy zastojnik wócyniś
Name[el]=Άνοιγμα Διαχείρισης προφίλ
Name[en_CA]=Open Profile Manager
Name[en_GB]=Open Profile Manager
Name[eo]=Malfermi administranton de profiloj
Name[es_AR]=Abrir administrador de perfiles
Name[es_CL]=Abrir administrador de perfiles
Name[es_ES]=Abrir administrador de perfiles
Name[es_MX]=Abrir administrador de perfiles
Name[et]=Open Profile Manager
Name[eu]=Ireki profilen kudeatzailea
Name[fa]=گشودن مدیر نمایه
Name[ff]=Open Profile Manager
Name[fi]=Avaa profiilien hallinta
Name[fr]=Ouvrir le gestionnaire de profils
Name[fur]=Vierç gjestôr profîi
Name[fy_NL]=Profylbehearder iepenje
Name[ga_IE]=Open Profile Manager
Name[gd]=Fosgail manaidsear nam pròifilean
Name[gl]=Abrir o xestor de perfís
Name[gn]=Embojuruja mbaete ñangarekoha
Name[gu_IN]=Open Profile Manager
Name[he]=פתיחת מנהל הפרופילים
Name[hi_IN]=Open Profile Manager
Name[hr]=Otvori upravljač profila
Name[hsb]=Zrjadowak profilow wočinić
Name[hu]=Profilkezelő megnyitása
Name[hy_AM]=Բացեք պրոֆիլի կառավարիչը
Name[hye]=Open Profile Manager
Name[ia]=Aperir le gestor de profilo
Name[id]=Buka Pengelola Profil
Name[is]=Opna umsýslu notandasniða
Name[it]=Apri gestore profili
Name[ja]=プロファイルマネージャーを開く
Name[ka]=პროფილის მმართველის გახსნა
Name[kab]=Ldi amsefrak n umaɣnu
Name[kk]=Профильдер бақарушысын ашу
Name[km]=Open Profile Manager
Name[kn]=Open Profile Manager
Name[ko]=프로필 관리자 열기
Name[lij]=Open Profile Manager
Name[lo]=ເປີດຕົວຈັດການໂປຣໄຟລ໌
Name[lt]=Open Profile Manager
Name[ltg]=Open Profile Manager
Name[lv]=Atvērt profilu pārvaldnieku
Name[meh]=Open Profile Manager
Name[mk]=Open Profile Manager
Name[ml]=രൂപരേഖ മാനേചർ
Name[mr]=Open Profile Manager
Name[ms]=Open Profile Manager
Name[my]=Open Profile Manager
Name[nb_NO]=Åpne profilbehandler
Name[ne_NP]=Open Profile Manager
Name[nl]=Profielbeheerder openen
Name[nn_NO]=Opne profilhandsaming
Name[oc]=Dobrir lo gestionari de perfils
Name[pa_IN]=ਪਰੋਫ਼ਾਈਲ ਮੈਨੇਜਰ ਖੋਲ੍ਹੋ
Name[pl]=Menedżer profili
Name[pt_BR]=Abrir gerenciador de perfis
Name[pt_PT]=Abrir o Gestor de Perfis
Name[rm]=Avrir l'administraziun da profils
Name[ro]=Deschide managerul de profiluri
Name[ru]=Открыть менеджер профилей
Name[sat]=ᱢᱮᱫᱦᱟᱸ ᱢᱮᱱᱮᱡᱚᱨ ᱠᱷᱩᱞᱟᱹᱭ ᱢᱮ
Name[sc]=Aberi su gestore de profilos
Name[sco]=Open Profile Manager
Name[si]=පැතිකඩ කළමනාකරු අරින්න
Name[sk]=Otvoriť Správcu profilov
Name[skr]=پروفائل منیجر کھولو
Name[sl]=Odpri upravitelja profilov
Name[son]=Open Profile Manager
Name[sq]=Hapni Përgjegjës Profilesh
Name[sr]=Отворите управљач профила
Name[sv_SE]=Öppna Profilhanteraren
Name[szl]=Open Profile Manager
Name[ta]=Open Profile Manager
Name[te]=Open Profile Manager
Name[tg]=Кушодани мудири профилҳо
Name[th]=เปิดตัวจัดการโปรไฟล์
Name[tl]=Open Profile Manager
Name[tr]=Profil yöneticisini aç
Name[trs]=Sa nīkāj ñuūnj nej perfî huā nìnï̀nj ïn
Name[uk]=Відкрити менеджер профілів
Name[ur]=Open Profile Manager
Name[uz]=Open Profile Manager
Name[vi]=Mở trình quản lý hồ sơ
Name[wo]=Open Profile Manager
Name[xh]=Open Profile Manager
Name[zh_CN]=打开配置文件管理器
Name[zh_TW]=開啟設定檔管理員

View File

@@ -1 +1 @@
fb55808f9cdd2172649e551705008af4f98038fe
73901ca17f4a2159dd4488cea8684e9abbfdcc89

View File

@@ -20,3 +20,5 @@ files:
translation: browser/browser/preferences/zen-preferences.ftl
- source: en-US/browser/browser/zen-folders.ftl
translation: browser/browser/zen-folders.ftl
- source: en-US/browser/browser/zen-boosts.ftl
translation: browser/browser/zen-boosts.ftl

View File

@@ -321,6 +321,7 @@ zen-workspace-shortcut-switch-9 = Switch to Workspace 9
zen-workspace-shortcut-switch-10 = Switch to Workspace 10
zen-workspace-shortcut-forward = Forward Workspace
zen-workspace-shortcut-backward = Backward Workspace
zen-workspace-shortcut-create = Create New Workspace
zen-sidebar-shortcut-toggle = Toggle Sidebar's Width
zen-pinned-tab-shortcut-reset = Reset Pinned Tab to Pinned URL
zen-split-view-shortcut-grid = Toggle Split View Grid

View File

@@ -0,0 +1,58 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
zen-boost-edit-rename =
.label = Rename Boost
zen-boost-edit-shuffle =
.label = Shuffle Vibes
zen-boost-edit-reset =
.label = Reset All Edits
zen-boost-edit-delete =
.label = Delete Boost
zen-boost-size = Size
zen-boost-zap = Zap
zen-boost-code = Code
zen-boost-back = Back
zen-boost-shuffle =
.tooltiptext = Shuffle Boost Settings
zen-boost-invert =
.tooltiptext = Smart Invert Colors
zen-boost-controls =
.tooltiptext = Advanced Color Controls
zen-boost-disable =
.tooltiptext = Disable Color Adjustments
zen-boost-text-case-toggle =
.tooltiptext = Toggle Text Case
zen-boost-css-picker =
.tooltiptext = Pick Selector
zen-boost-css-inspector =
.tooltiptext = Open Inspector
zen-bootst-color-contrast = Contrast
zen-bootst-color-brightness = Brightness
zen-bootst-color-original-saturation = Original Saturation
zen-add-zap-helper = Click elements on the page to <b>Zap</b> them
zen-remove-zap-helper = ← Click to Unzap
zen-select-this = Insert selector for this
zen-select-related = Insert selector for related
zen-select-cancel = Cancel
zen-zap-this = Zap this
zen-zap-related = Zap all related elements
zen-zap-cancel = Cancel
zen-zap-done = Done
zen-unzap-tooltip =
{
$elementCount ->
[0] No elements zapped
[1] { $elementCount } element zapped
*[other] { $elementCount } elements zapped
}
zen-boost-save =
.tooltiptext = Export Boost
zen-boost-load =
.tooltiptext = Import Boost
zen-panel-ui-boosts-exported-message = Boost exported!
zen-site-data-boosts = Boosts
zen-site-data-create-boost =
.tooltiptext = Create new boost
zen-boost-rename-boost-prompt = Rename Boost?

View File

@@ -151,3 +151,5 @@ zen-window-sync-migration-dialog-accept = Got It
zen-appmenu-new-blank-window =
.label = New blank window
zen-spaces-search-placeholder =
.placeholder = Search your spaces...

8
package-lock.json generated
View File

@@ -1005,13 +1005,13 @@
"license": "MIT"
},
"node_modules/axios": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
"integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz",
"integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==",
"dev": true,
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"follow-redirects": "^1.16.0",
"form-data": "^4.0.5",
"proxy-from-env": "^2.1.0"
}

View File

@@ -27,7 +27,7 @@
"surfer": "surfer",
"test": "python3 scripts/run_tests.py",
"test:dbg": "python3 scripts/run_tests.py --jsdebugger --debug-on-failure",
"ffprefs": "cd tools/ffprefs && cargo run --bin ffprefs -- ../../",
"ffprefs": "${CARGO:-cargo} run --manifest-path tools/ffprefs/Cargo.toml --bin ffprefs -- prefs engine",
"lc": "surfer license-check",
"lc:fix": "surfer license-check --fix",
"use-moz-src": "cd engine && ./mach use-moz-src",

22
prefs/zen/boosts.yaml Normal file
View File

@@ -0,0 +1,22 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
- name: zen.boosts.enabled
value: "@IS_TWILIGHT@"
- name: zen.boosts.dissolve-on-zap
value: true
- name: zen.boosts.invert-channel-floor
value: 15
cpptype: uint32_t
mirror: once
type: static
- name: zen.boosts.disable-on-anonymous-content
value: true
cpptype: bool
mirror: once
type: static

View File

@@ -7,3 +7,6 @@
- name: zen.keyboard.shortcuts.disable-mainkeyset-clear
value: false # for debugging
- name: zen.keyboard.shortcuts.global.enabled
value: true

View File

@@ -2,4 +2,5 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
content/browser/zen-components/ZenKeyboardShortcuts.mjs (../../zen/kbs/ZenKeyboardShortcuts.mjs)
- name: zen.live-folders.github.skip-new-pr-ui-check
value: false

View File

@@ -5,6 +5,9 @@
- name: zen.splitView.enable-tab-drop
value: true
- name: zen.splitView.enable-tab-click-split
value: true
- name: zen.splitView.min-resize-width
value: 7

View File

@@ -18,7 +18,7 @@
value: true
- name: zen.theme.acrylic-elements
value: false
value: "@IS_TWILIGHT@"
- name: zen.theme.disable-lightweight
value: true

View File

@@ -61,5 +61,8 @@
- name: zen.view.overflow-webext-toolbar
value: "@IS_TWILIGHT@"
- name: zen.view.overflow-webext-toolbar-threshold
value: 60
- name: zen.view.enable-loading-indicator
value: true

View File

@@ -17,6 +17,12 @@
- name: zen.workspaces.swipe-actions
value: true
- name: zen.workspaces.swipe-actions.delta-multiplier
value: 90
- name: zen.workspaces.switch-animation-duration
value: 200
- name: zen.workspaces.wrap-around-navigation
value: true

View File

@@ -39,3 +39,6 @@
- name: zen.urlbar.suggestions.quick-actions
value: true
- name: browser.urlbar.shortcuts.workspaces
value: true

View File

@@ -60,4 +60,7 @@ def main():
if __name__ == "__main__":
import sys
if len(sys.argv) == 3:
_, DUMPS_FOLDER, ENGINE_DUMPS_FOLDER = sys.argv
main()

View File

@@ -1,5 +1,5 @@
diff --git a/browser/base/content/navigator-toolbox.inc.xhtml b/browser/base/content/navigator-toolbox.inc.xhtml
index edeb473e46b3aa4b12eb4b59ce62e5ae48edd2a1..d2c6c8c150e732b77d65420520ca4905a9d3ea4d 100644
index edeb473e46b3aa4b12eb4b59ce62e5ae48edd2a1..99210f8bb5633d50d2cba24f1e13ca866c5b6959 100644
--- a/browser/base/content/navigator-toolbox.inc.xhtml
+++ b/browser/base/content/navigator-toolbox.inc.xhtml
@@ -2,7 +2,7 @@
@@ -11,7 +11,7 @@ index edeb473e46b3aa4b12eb4b59ce62e5ae48edd2a1..d2c6c8c150e732b77d65420520ca4905
<script src="chrome://browser/content/navigator-toolbox.js" />
<!-- Menu -->
@@ -18,9 +18,12 @@
@@ -18,9 +18,13 @@
#include browser-menubar.inc
</toolbaritem>
<spacer flex="1" skipintoolbarset="true" style="order: 1000;"/>
@@ -22,14 +22,14 @@ index edeb473e46b3aa4b12eb4b59ce62e5ae48edd2a1..d2c6c8c150e732b77d65420520ca4905
+ <html:div id="zen-toolbar-background" class="zen-toolbar-background zen-browser-generic-background">
+ <html:div class="zen-browser-grain" />
+ </html:div>
+ <box id="zen-overflow-extensions-list" skipintoolbarset="true" contextmenu="toolbar-context-menu" />
<toolbar id="TabsToolbar"
class="browser-toolbar browser-titlebar"
fullscreentoolbar="true"
@@ -62,6 +65,9 @@
@@ -62,6 +66,8 @@
<html:sidebar-pins-promo id="drag-to-pin-promo-card"></html:sidebar-pins-promo>
<arrowscrollbox id="pinned-tabs-container" orient="horizontal" clicktoscroll=""></arrowscrollbox>
<splitter orient="vertical" id="vertical-pinned-tabs-splitter" resizebefore="sibling" resizeafter="none" hidden="true"/>
+<html:div id="zen-overflow-extensions-list" skipintoolbarset="true"></html:div>
+<html:div id="zen-essentials" skipintoolbarset="true"></html:div>
+<html:div id="zen-tabs-wrapper">
<hbox class="tab-drop-indicator" hidden="true"/>

View File

@@ -28,6 +28,8 @@
<link rel="stylesheet" type="text/css" href="chrome://browser/content/zen-styles/zen-welcome.css" />
<link rel="stylesheet" type="text/css" href="chrome://browser/content/zen-styles/zen-media-controls.css" />
<link rel="stylesheet" type="text/css" href="chrome://browser/content/zen-styles/zen-download-box-animation.css" />
<link rel="stylesheet" type="text/css" href="chrome://browser/content/zen-styles/zen-little-window.css" />
</linkset>
# Startup "preloaded" scripts that requre globals such as gBrowser and gURLBar

View File

@@ -9,7 +9,6 @@
#include ../../../zen/mods/jar.inc.mn
#include ../../../zen/spaces/jar.inc.mn
#include ../../../zen/tabs/jar.inc.mn
#include ../../../zen/kbs/jar.inc.mn
#include ../../../zen/glance/jar.inc.mn
#include ../../../zen/folders/jar.inc.mn
#include ../../../zen/welcome/jar.inc.mn
@@ -18,4 +17,6 @@
#include ../../../zen/images/jar.inc.mn
#include ../../../zen/vendor/jar.inc.mn
#include ../../../zen/fonts/jar.inc.mn
#include ../../../zen/boosts/jar.inc.mn
#include ../../../zen/live-folders/jar.inc.mn
#include ../../../zen/little-window/jar.inc.mn

View File

@@ -68,4 +68,6 @@
<command id="cmd_zenNewLiveFolder" />
<command id="cmd_zenDuplicateTab" />
<command id="cmd_zenNewLittleWindow" />
</commandset>

View File

@@ -9,5 +9,6 @@
<link rel="localization" href="browser/zen-menubar.ftl"/>
<link rel="localization" href="browser/zen-vertical-tabs.ftl"/>
<link rel="localization" href="browser/zen-folders.ftl"/>
<link rel="localization" href="browser/zen-boosts.ftl"/>
<link rel="localization" href="browser/zen-live-folders.ftl"/>
</linkset>

View File

@@ -75,6 +75,14 @@
data-l10n-id="unified-extensions-manage-extensions"
hidden="true" />
</vbox>
<vbox class="zen-site-data-section">
<hbox class="zen-site-data-section-header">
<label data-l10n-id="zen-site-data-boosts" flex="1" />
</hbox>
<vbox id="zen-site-data-boost-list">
<!-- settings will be inserted here -->
</vbox>
</vbox>
<vbox class="zen-site-data-section">
<hbox class="zen-site-data-section-header">
<label data-l10n-id="zen-site-data-settings" flex="1" />
@@ -87,6 +95,8 @@
<hbox id="zen-site-data-footer">
<toolbarbutton id="zen-site-data-security-info"
class="subviewbutton zen-interactive-button" />
<toolbarbutton id="zen-site-data-boost" data-l10n-id="zen-site-data-create-boost"
class="subviewbutton zen-interactive-button" />
<toolbarbutton id="zen-site-data-actions"
class="subviewbutton zen-interactive-button"
closemenu="none"

View File

@@ -0,0 +1,23 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<panel id="zen-spaces-popup"
nonnativepopover="true"
type="arrow"
orient="vertical"
side="bottom"
hidden="true"
consumeoutsideclicks="never">
<hbox class="zen-spaces-list-header" flex="1">
<image class="zen-spaces-list-search-icon" src="chrome://global/skin/icons/search-glass.svg"/>
<html:input id="zen-spaces-list-search"
data-l10n-id="zen-spaces-search-placeholder"
type="search" />
</hbox>
<scrollbox class="zen-spaces-list-scrollbox" flex="1">
<vbox id="zen-spaces-list"></vbox>
<hbox id="zen-spaces-search-no-results" hidden="true" flex="1"
data-l10n-id="zen-spaces-search-no-results" />
</scrollbox>
</panel>

View File

@@ -5,6 +5,7 @@
#include zen-panels/theme-picker.inc
#include zen-panels/emojis-picker.inc
#include zen-panels/folders-search.inc
#include zen-panels/spaces-search.inc
#include zen-panels/site-data.inc
#include zen-panels/popups.inc

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/customizableui/CustomizableUI.sys.mjs b/browser/components/customizableui/CustomizableUI.sys.mjs
index db617c65b89643015d91b0f6a20eab7d7a1b598f..9acef640800bdc75f477a8e14e73f08f535e9d9e 100644
index db617c65b89643015d91b0f6a20eab7d7a1b598f..53598363bfbc2c5de4c6b4de712d22d89ee6ce1d 100644
--- a/browser/components/customizableui/CustomizableUI.sys.mjs
+++ b/browser/components/customizableui/CustomizableUI.sys.mjs
@@ -14,6 +14,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
@@ -186,11 +186,34 @@ index db617c65b89643015d91b0f6a20eab7d7a1b598f..9acef640800bdc75f477a8e14e73f08f
});
lazy.log.debug(
@@ -7933,7 +7934,14 @@ class OverflowableToolbar {
@@ -7933,7 +7934,37 @@ class OverflowableToolbar {
Math.max(targetWidth, targetChildrenWidth)
);
totalAvailWidth = Math.ceil(totalAvailWidth);
- let isOverflowing = targetContentWidth > totalAvailWidth;
+ if (this.#target.id == 'nav-bar-customization-target' &&
+ win.gZenVerticalTabsManager._hasSetSingleToolbar &&
+ Services.prefs.getBoolPref("zen.view.overflow-webext-toolbar", true) &&
+ !win.gURLBar.hasAttribute("breakout-extend")) {
+ const availSelectors = ":is(#page-action-buttons, #zen-copy-url-button, .unified-extensions-item)";
+ const width = [
+ ...win.gURLBar._inputContainer.querySelectorAll(availSelectors),
+ ...win.document.getElementById("zen-overflow-extensions-list").querySelectorAll(availSelectors)
+ ].length * 26;
+ const urlbarWidth = win.document.getElementById("urlbar-container").getBoundingClientRect().width;
+ let overflowing = width > urlbarWidth * (Services.prefs.getIntPref("zen.view.overflow-webext-toolbar-threshold", 60) / 100);
+ let wasOverflowing = win.gURLBar._isOverflowingItems;
+ win.gURLBar._isOverflowingItems = overflowing;
+ if (wasOverflowing !== overflowing) {
+ const items = gPlacements.get("nav-bar");
+ for (let item of items) {
+ let [, node] = CustomizableUIInternal.getWidgetNode(item, win);
+ if (node?.hasAttribute("data-extensionid")) {
+ win.gZenVerticalTabsManager.appendCustomizableItem(win.document.getElementById("zen-sidebar-top-buttons-customization-target"), node);
+ }
+ }
+ }
+ }
+ if (win.gZenVerticalTabsManager._hasSetSingleToolbar && this.#toolbar.id == 'nav-bar') return { isOverflowing: false, targetContentWidth, totalAvailWidth };
+ let isOverflowing = targetContentWidth + (win.gZenVerticalTabsManager._hasSetSingleToolbar ? 0.1 : 0) > totalAvailWidth;
+ if (win.gZenVerticalTabsManager._hasSetSingleToolbar &&
@@ -202,7 +225,7 @@ index db617c65b89643015d91b0f6a20eab7d7a1b598f..9acef640800bdc75f477a8e14e73f08f
return { isOverflowing, targetContentWidth, totalAvailWidth };
}
@@ -7994,7 +8002,11 @@ class OverflowableToolbar {
@@ -7994,7 +8025,11 @@ class OverflowableToolbar {
return;
}
}
@@ -215,7 +238,7 @@ index db617c65b89643015d91b0f6a20eab7d7a1b598f..9acef640800bdc75f477a8e14e73f08f
lazy.log.debug(
`Need ${minSize} but width is ${totalAvailWidth} so bailing`
);
@@ -8027,7 +8039,7 @@ class OverflowableToolbar {
@@ -8027,7 +8062,7 @@ class OverflowableToolbar {
}
}
if (!inserted) {
@@ -224,7 +247,7 @@ index db617c65b89643015d91b0f6a20eab7d7a1b598f..9acef640800bdc75f477a8e14e73f08f
}
child.removeAttribute("cui-anchorid");
child.removeAttribute("overflowedItem");
@@ -8153,6 +8165,9 @@ class OverflowableToolbar {
@@ -8153,6 +8188,9 @@ class OverflowableToolbar {
* if no such list exists.
*/
get #webExtList() {
@@ -234,7 +257,7 @@ index db617c65b89643015d91b0f6a20eab7d7a1b598f..9acef640800bdc75f477a8e14e73f08f
if (!this.#webExtListRef) {
let targetID = this.#toolbar.getAttribute("addon-webext-overflowtarget");
if (!targetID) {
@@ -8164,6 +8179,9 @@ class OverflowableToolbar {
@@ -8164,6 +8202,9 @@ class OverflowableToolbar {
let win = this.#toolbar.ownerGlobal;
let { panel } = win.gUnifiedExtensions;
this.#webExtListRef = panel.querySelector(`#${targetID}`);
@@ -244,7 +267,7 @@ index db617c65b89643015d91b0f6a20eab7d7a1b598f..9acef640800bdc75f477a8e14e73f08f
}
return this.#webExtListRef;
}
@@ -8372,7 +8390,7 @@ class OverflowableToolbar {
@@ -8372,7 +8413,7 @@ class OverflowableToolbar {
break;
}
case "mousedown": {

View File

@@ -8,13 +8,16 @@ const { nsZenMultiWindowFeature } = ChromeUtils.importESModule(
{ global: "current" }
);
const { nsKeyShortcutModifiers } = ChromeUtils.importESModule(
"chrome://browser/content/zen-components/ZenKeyboardShortcuts.mjs",
{
global: "current",
}
const {
nsKeyShortcutModifiers,
ZenKeyboardShortcuts,
VALID_SHORTCUT_GROUPS,
} = ChromeUtils.importESModule(
"resource:///modules/zen/ZenKeyboardShortcuts.sys.mjs"
);
const gZenKeyboardShortcutsManager = ZenKeyboardShortcuts.manager;
var gZenMarketplaceManager = {
async init() {
const checkForUpdates = document.getElementById("zenThemeMarketplaceCheckForUpdates");

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/drag-and-drop.js b/browser/components/tabbrowser/content/drag-and-drop.js
index b93a8fcc8b76ed888524959cbe7d6865fe830f2f..fa192837d06e28bb847acbbd462ad3229ebdc2d2 100644
index b93a8fcc8b76ed888524959cbe7d6865fe830f2f..9f39316e33daa8a272e48c84d9e07ad9df197586 100644
--- a/browser/components/tabbrowser/content/drag-and-drop.js
+++ b/browser/components/tabbrowser/content/drag-and-drop.js
@@ -35,6 +35,9 @@
@@ -47,7 +47,7 @@ index b93a8fcc8b76ed888524959cbe7d6865fe830f2f..fa192837d06e28bb847acbbd462ad322
event.stopPropagation();
+ if (draggedTab && dropEffect == "move") {
+ this.handle_drop_transition?.(draggedTab._dragData.dropElement, draggedTab, movingTabs, draggedTab._dragData.dropBefore);
+ gZenPinnedTabManager.moveToAnotherTabContainerIfNecessary(event, draggedTab, movingTabs, this._getDropIndex(event));
+ [draggedTab, movingTabs] = gZenPinnedTabManager.moveToAnotherTabContainerIfNecessary(event, draggedTab, movingTabs, this._getDropIndex(event));
+ }
if (draggedTab && dropEffect == "copy") {
let duplicatedDraggedTab;

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/tab.js b/browser/components/tabbrowser/content/tab.js
index 2e02bad1a7c89b4c3b5aee1e14c13bb953a64eb6..3ac8d2facb224ae39d2199f2c5ec08b77e0c1dfd 100644
index 2e02bad1a7c89b4c3b5aee1e14c13bb953a64eb6..fb9ec4deb5871bc0ba57c323a413f07440e9aa42 100644
--- a/browser/components/tabbrowser/content/tab.js
+++ b/browser/components/tabbrowser/content/tab.js
@@ -21,6 +21,7 @@
@@ -151,7 +151,7 @@ index 2e02bad1a7c89b4c3b5aee1e14c13bb953a64eb6..3ac8d2facb224ae39d2199f2c5ec08b7
on_click(event) {
if (event.button != 0) {
return;
@@ -617,14 +656,23 @@
@@ -617,14 +656,31 @@
trigger: "alt_click",
});
}
@@ -159,9 +159,17 @@ index 2e02bad1a7c89b4c3b5aee1e14c13bb953a64eb6..3ac8d2facb224ae39d2199f2c5ec08b7
+ !event.target.classList.contains("tab-close-button") &&
+ !event.target.classList.contains("tab-icon-overlay") &&
+ !event.target.classList.contains("tab-audio-button") &&
+ !this.splitView
+ Services.prefs.getBoolPref("zen.splitView.enable-tab-click-split", false)
+ ) {
+ gZenViewSplitter.contextSplitTabs(this);
+ if (!this.splitView) {
+ gZenViewSplitter.contextSplitTabs(this);
+ } else {
+ gBrowser.selectedTab = this;
+ gZenViewSplitter.removeTabFromGroup(this, undefined, {
+ forUnsplit: true,
+ changeTab: false
+ });
+ }
+ }
return;
}
@@ -176,7 +184,7 @@ index 2e02bad1a7c89b4c3b5aee1e14c13bb953a64eb6..3ac8d2facb224ae39d2199f2c5ec08b7
gBrowser.multiSelectedTabsCount > 0 &&
!event.target.classList.contains("tab-close-button") &&
!event.target.classList.contains("tab-icon-overlay") &&
@@ -636,8 +684,9 @@
@@ -636,8 +692,9 @@
}
if (
@@ -188,7 +196,7 @@ index 2e02bad1a7c89b4c3b5aee1e14c13bb953a64eb6..3ac8d2facb224ae39d2199f2c5ec08b7
) {
if (this.activeMediaBlocked) {
if (this.multiselected) {
@@ -655,7 +704,7 @@
@@ -655,7 +712,7 @@
return;
}
@@ -197,7 +205,7 @@ index 2e02bad1a7c89b4c3b5aee1e14c13bb953a64eb6..3ac8d2facb224ae39d2199f2c5ec08b7
if (this.multiselected) {
gBrowser.removeMultiSelectedTabs(
lazy.TabMetrics.userTriggeredContext(
@@ -675,6 +724,14 @@
@@ -675,6 +732,14 @@
// (see tabbrowser-tabs 'click' handler).
gBrowser.tabContainer._blockDblClick = true;
}
@@ -212,7 +220,7 @@ index 2e02bad1a7c89b4c3b5aee1e14c13bb953a64eb6..3ac8d2facb224ae39d2199f2c5ec08b7
}
on_dblclick(event) {
@@ -698,6 +755,8 @@
@@ -698,6 +763,8 @@
animate: true,
triggeringEvent: event,
});

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js
index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff606d98f1e 100644
index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..3a21c40eb70621968b5fbfc4b9b6d8412310b7b7 100644
--- a/browser/components/tabbrowser/content/tabbrowser.js
+++ b/browser/components/tabbrowser/content/tabbrowser.js
@@ -413,6 +413,7 @@
@@ -466,7 +466,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
);
tabsFragment.appendChild(tabGroup.node);
}
@@ -4598,9 +4729,23 @@
@@ -4598,9 +4729,21 @@
// to remove the old selected tab.
if (tabToSelect) {
let leftoverTab = this.selectedTab;
@@ -482,15 +482,13 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
+ gZenWorkspaces._initialTab._shouldRemove = true;
+ }
+ }
+ } else {
+ gZenWorkspaces._keepSelectedTab = !window.__isNewZenWindow;
}
+ delete window.__isNewZenWindow;
+ this._hasAlreadyInitializedZenSessionStore = true;
if (tabs.length > 1 || !tabs[0].selected) {
this._updateTabsAfterInsert();
@@ -4791,11 +4936,14 @@
@@ -4791,11 +4934,14 @@
if (ownerTab) {
tab.owner = ownerTab;
}
@@ -506,7 +504,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
if (
!bulkOrderedOpen &&
((openerTab &&
@@ -4807,7 +4955,7 @@
@@ -4807,7 +4953,7 @@
let lastRelatedTab =
openerTab && this._lastRelatedTabMap.get(openerTab);
let previousTab = lastRelatedTab || openerTab || this.selectedTab;
@@ -515,7 +513,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
tabGroup = previousTab.group;
}
if (
@@ -4823,7 +4971,7 @@
@@ -4823,7 +4969,7 @@
previousTab.splitview
) + 1;
} else if (previousTab.visible) {
@@ -524,7 +522,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
} else if (previousTab == FirefoxViewHandler.tab) {
elementIndex = 0;
}
@@ -4851,14 +4999,14 @@
@@ -4851,14 +4997,14 @@
}
// Ensure index is within bounds.
if (tab.pinned) {
@@ -543,7 +541,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
if (pinned && !itemAfter?.pinned) {
itemAfter = null;
@@ -4875,7 +5023,7 @@
@@ -4875,7 +5021,7 @@
this.tabContainer._invalidateCachedTabs();
@@ -552,7 +550,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
if (
(this.isTab(itemAfter) && itemAfter.group == tabGroup) ||
this.isSplitViewWrapper(itemAfter)
@@ -4906,7 +5054,11 @@
@@ -4906,7 +5052,11 @@
const tabContainer = pinned
? this.tabContainer.pinnedTabsContainer
: this.tabContainer;
@@ -564,7 +562,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
}
if (tab.group?.collapsed) {
@@ -4921,6 +5073,7 @@
@@ -4921,6 +5071,7 @@
if (pinned) {
this._updateTabBarForPinnedTabs();
}
@@ -572,7 +570,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
TabBarVisibility.update();
}
@@ -5469,6 +5622,7 @@
@@ -5469,6 +5620,7 @@
telemetrySource,
} = {}
) {
@@ -580,7 +578,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
// When 'closeWindowWithLastTab' pref is enabled, closing all tabs
// can be considered equivalent to closing the window.
if (
@@ -5558,6 +5712,7 @@
@@ -5558,6 +5710,7 @@
if (lastToClose) {
this.removeTab(lastToClose, aParams);
}
@@ -588,7 +586,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
} catch (e) {
console.error(e);
}
@@ -5603,6 +5758,14 @@
@@ -5603,6 +5756,14 @@
return;
}
@@ -603,7 +601,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
let isVisibleTab = aTab.visible;
// We have to sample the tab width now, since _beginRemoveTab might
// end up modifying the DOM in such a way that aTab gets a new
@@ -5610,6 +5773,9 @@
@@ -5610,6 +5771,9 @@
// state).
let tabWidth = window.windowUtils.getBoundsWithoutFlushing(aTab).width;
let isLastTab = this.#isLastTabInWindow(aTab);
@@ -613,7 +611,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
if (
!this._beginRemoveTab(aTab, {
closeWindowFastpath: true,
@@ -5621,13 +5787,14 @@
@@ -5621,13 +5785,14 @@
telemetrySource,
})
) {
@@ -629,7 +627,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
let lockTabSizing =
!this.tabContainer.verticalMode &&
!aTab.pinned &&
@@ -5658,7 +5825,13 @@
@@ -5658,7 +5823,13 @@
// We're not animating, so we can cancel the animation stopwatch.
Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId);
aTab._closeTimeAnimTimerId = null;
@@ -644,7 +642,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
return;
}
@@ -5792,7 +5965,7 @@
@@ -5792,7 +5963,7 @@
closeWindowWithLastTab != null
? closeWindowWithLastTab
: !window.toolbar.visible ||
@@ -653,7 +651,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
if (closeWindow) {
// We've already called beforeunload on all the relevant tabs if we get here,
@@ -5816,6 +5989,7 @@
@@ -5816,6 +5987,7 @@
newTab = true;
}
@@ -661,7 +659,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
aTab._endRemoveArgs = [closeWindow, newTab];
// swapBrowsersAndCloseOther will take care of closing the window without animation.
@@ -5856,13 +6030,7 @@
@@ -5856,13 +6028,7 @@
aTab._mouseleave();
if (newTab) {
@@ -676,7 +674,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
} else {
TabBarVisibility.update();
}
@@ -5995,6 +6163,7 @@
@@ -5995,6 +6161,7 @@
this.tabs[i]._tPos = i;
}
@@ -684,7 +682,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
if (!this._windowIsClosing) {
// update tab close buttons state
this.tabContainer._updateCloseButtons();
@@ -6180,6 +6349,7 @@
@@ -6180,6 +6347,7 @@
memory_after: await getTotalMemoryUsage(),
time_to_unload_in_ms: timeElapsed,
});
@@ -692,7 +690,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
}
/**
@@ -6225,6 +6395,7 @@
@@ -6225,6 +6393,7 @@
}
let excludeTabs = new Set(aExcludeTabs);
@@ -700,7 +698,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
// If this tab has a successor, it should be selectable, since
// hiding or closing a tab removes that tab as a successor.
@@ -6237,15 +6408,22 @@
@@ -6237,15 +6406,22 @@
!excludeTabs.has(aTab.owner) &&
Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose")
) {
@@ -725,7 +723,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
let tab = this.tabContainer.findNextTab(aTab, {
direction: 1,
filter: _tab => remainingTabs.includes(_tab),
@@ -6259,7 +6437,7 @@
@@ -6259,7 +6435,7 @@
}
if (tab) {
@@ -734,7 +732,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
}
// If no qualifying visible tab was found, see if there is a tab in
@@ -6280,7 +6458,7 @@
@@ -6280,7 +6456,7 @@
});
}
@@ -743,7 +741,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
}
_blurTab(aTab) {
@@ -6291,7 +6469,7 @@
@@ -6291,7 +6467,7 @@
* @returns {boolean}
* False if swapping isn't permitted, true otherwise.
*/
@@ -752,7 +750,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
// Do not allow transfering a private tab to a non-private window
// and vice versa.
if (
@@ -6345,6 +6523,7 @@
@@ -6345,6 +6521,7 @@
// fire the beforeunload event in the process. Close the other
// window if this was its last tab.
if (
@@ -760,7 +758,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
!remoteBrowser._beginRemoveTab(aOtherTab, {
adoptedByTab: aOurTab,
closeWindowWithLastTab: true,
@@ -6356,7 +6535,7 @@
@@ -6356,7 +6533,7 @@
// If this is the last tab of the window, hide the window
// immediately without animation before the docshell swap, to avoid
// about:blank being painted.
@@ -769,7 +767,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
if (closeWindow) {
let win = aOtherTab.ownerGlobal;
win.windowUtils.suppressAnimation(true);
@@ -6484,11 +6663,13 @@
@@ -6484,11 +6661,13 @@
}
// Finish tearing down the tab that's going away.
@@ -783,7 +781,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
this.setTabTitle(aOurTab);
@@ -6690,10 +6871,10 @@
@@ -6690,10 +6869,10 @@
SessionStore.deleteCustomTabValue(aTab, "hiddenBy");
}
@@ -796,7 +794,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
aTab.selected ||
aTab.closing ||
// Tabs that are sharing the screen, microphone or camera cannot be hidden.
@@ -6753,7 +6934,8 @@
@@ -6753,7 +6932,8 @@
* @param {object} [aOptions={}]
* Key-value pairs that will be serialized into the features string.
*/
@@ -806,7 +804,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
if (this.tabs.length == 1) {
return null;
}
@@ -6770,7 +6952,7 @@
@@ -6770,7 +6950,7 @@
// tell a new window to take the "dropped" tab
let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
args.appendElement(aTab.splitview ?? aTab);
@@ -815,7 +813,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
private: PrivateBrowsingUtils.isWindowPrivate(window),
features: Object.entries(aOptions)
.map(([key, value]) => `${key}=${value}`)
@@ -6778,6 +6960,8 @@
@@ -6778,6 +6958,8 @@
openerWindow: window,
args,
});
@@ -824,7 +822,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
}
/**
@@ -6890,7 +7074,7 @@
@@ -6890,7 +7072,7 @@
* `true` if element is a `<tab-group>`
*/
isTabGroup(element) {
@@ -833,7 +831,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
}
/**
@@ -6975,8 +7159,8 @@
@@ -6975,8 +7157,8 @@
}
// Don't allow mixing pinned and unpinned tabs.
@@ -844,7 +842,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
} else {
tabIndex = Math.max(tabIndex, this.pinnedTabCount);
}
@@ -7005,13 +7189,19 @@
@@ -7005,13 +7187,19 @@
this.#handleTabMove(
element,
() => {
@@ -866,7 +864,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
let useAfter = false;
if (this.isTab(element)) {
useAfter = neighbor && tabIndex > element._tPos;
@@ -7076,23 +7266,31 @@
@@ -7076,23 +7264,31 @@
#moveTabNextTo(element, targetElement, moveBefore = false, metricsContext) {
if (this.isTabGroupLabel(targetElement)) {
targetElement = targetElement.group;
@@ -904,7 +902,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
} else if (!element.pinned && targetElement && targetElement.pinned) {
// If the caller asks to move an unpinned element next to a pinned
// tab, move the unpinned element to be the first unpinned element
@@ -7105,12 +7303,35 @@
@@ -7105,12 +7301,35 @@
// move the tab group right before the first unpinned tab.
// 4. Moving a tab group and the first unpinned tab is grouped:
// move the tab group right before the first unpinned tab's tab group.
@@ -941,7 +939,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
// We want to include the splitview wrapper if it's the targetElement, but
// not in the case where we want to reverse tabs within the same splitview.
@@ -7119,6 +7340,7 @@
@@ -7119,6 +7338,7 @@
}
let getContainer = () =>
@@ -949,7 +947,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
element.pinned
? this.tabContainer.pinnedTabsContainer
: this.tabContainer;
@@ -7127,11 +7349,15 @@
@@ -7127,11 +7347,15 @@
element,
() => {
if (moveBefore) {
@@ -966,7 +964,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
}
},
metricsContext
@@ -7205,11 +7431,15 @@
@@ -7205,11 +7429,15 @@
* @param {TabMetricsContext} [metricsContext]
*/
moveTabToExistingGroup(aTab, aGroup, metricsContext) {
@@ -985,7 +983,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
}
if (aTab.group && aTab.group.id === aGroup.id) {
return;
@@ -7281,6 +7511,7 @@
@@ -7281,6 +7509,7 @@
let state = {
tabIndex: tab._tPos,
@@ -993,7 +991,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
};
if (tab.visible) {
state.elementIndex = tab.elementIndex;
@@ -7312,7 +7543,7 @@
@@ -7312,7 +7541,7 @@
let changedSplitView =
previousTabState.splitViewId != currentTabState.splitViewId;
@@ -1002,7 +1000,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
tab.dispatchEvent(
new CustomEvent("TabMove", {
bubbles: true,
@@ -7354,6 +7585,10 @@
@@ -7354,6 +7583,10 @@
moveActionCallback();
@@ -1013,7 +1011,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
// Clear tabs cache after moving nodes because the order of tabs may have
// changed.
this.tabContainer._invalidateCachedTabs();
@@ -7404,7 +7639,22 @@
@@ -7404,7 +7637,22 @@
* @returns {object}
* The new tab in the current window, null if the tab couldn't be adopted.
*/
@@ -1037,7 +1035,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
// Swap the dropped tab with a new one we create and then close
// it in the other window (making it seem to have moved between
// windows). We also ensure that the tab we create to swap into has
@@ -7447,6 +7697,8 @@
@@ -7447,6 +7695,8 @@
}
params.skipLoad = true;
let newTab = this.addWebTab("about:blank", params);
@@ -1046,7 +1044,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
aTab.container.tabDragAndDrop.finishAnimateTabMove();
@@ -8149,7 +8401,7 @@
@@ -8149,7 +8399,7 @@
// preventDefault(). It will still raise the window if appropriate.
return;
}
@@ -1055,7 +1053,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
window.focus();
aEvent.preventDefault();
}
@@ -8166,7 +8418,6 @@
@@ -8166,7 +8416,6 @@
on_TabGroupCollapse(aEvent) {
aEvent.target.tabs.forEach(tab => {
@@ -1063,7 +1061,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
});
}
@@ -8500,7 +8751,9 @@
@@ -8500,7 +8749,9 @@
let filter = this._tabFilters.get(tab);
if (filter) {
@@ -1073,7 +1071,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
let listener = this._tabListeners.get(tab);
if (listener) {
@@ -9306,6 +9559,7 @@
@@ -9306,6 +9557,7 @@
aWebProgress.isTopLevel
) {
this.mTab.setAttribute("busy", "true");
@@ -1081,7 +1079,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
gBrowser._tabAttrModified(this.mTab, ["busy"]);
this.mTab._notselectedsinceload = !this.mTab.selected;
}
@@ -9386,6 +9640,7 @@
@@ -9386,6 +9638,7 @@
// known defaults. Note we use the original URL since about:newtab
// redirects to a prerendered page.
const shouldRemoveFavicon =
@@ -1089,7 +1087,7 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
!this.mBrowser.mIconURL &&
!ignoreBlank &&
!(originalLocation.spec in FAVICON_DEFAULTS);
@@ -9560,13 +9815,6 @@
@@ -9560,13 +9813,6 @@
this.mBrowser.originalURI = aRequest.originalURI;
}
@@ -1103,11 +1101,12 @@ index 2643e1a2aa14ba5cb4a64a92e1c2dfa5f07d242f..d16fe62c8a67913dd1499b2665a22ff6
}
let userContextId = this.mBrowser.getAttribute("usercontextid") || 0;
@@ -10450,7 +10698,7 @@ var TabContextMenu = {
@@ -10450,7 +10696,8 @@ var TabContextMenu = {
);
contextUnpinSelectedTabs.hidden =
!this.contextTab.pinned || !this.multiselected;
-
+ gZenViewSplitter.updateContextMenuItems();
+ gZenPinnedTabManager.updatePinnedTabContextMenu(this.contextTab);
// Build Ask Chat items
TabContextMenu.GenAI.buildTabMenu(

View File

@@ -1,12 +1,21 @@
diff --git a/browser/components/urlbar/UrlbarPrefs.sys.mjs b/browser/components/urlbar/UrlbarPrefs.sys.mjs
index 2d21248256c6c2bfb8dac958133c10e3251ef564..6645211ef09518b41cb737e3186fbd0162ecf700 100644
index 2d21248256c6c2bfb8dac958133c10e3251ef564..f788bd10ec2c08e4b27b77cd3bb0489fb04e8b7a 100644
--- a/browser/components/urlbar/UrlbarPrefs.sys.mjs
+++ b/browser/components/urlbar/UrlbarPrefs.sys.mjs
@@ -799,6 +799,7 @@ function makeDefaultResultGroups({ showSearchSuggestionsFirst }) {
@@ -462,6 +462,7 @@ const PREF_URLBAR_DEFAULTS = /** @type {PreferenceDefinition[]} */ ([
["shortcuts.tabs", true],
["shortcuts.history", true],
["shortcuts.actions", true],
+ ["shortcuts.workspaces", true],
// Boolean to determine if the providers defined in `exposureResults`
// should be displayed in search results. This can be set by a
@@ -799,6 +800,8 @@ function makeDefaultResultGroups({ showSearchSuggestionsFirst }) {
*/
let rootGroup = {
children: [
+ { children: [{ group: lazy.UrlbarUtils.RESULT_GROUP.ZEN_ACTION }] },
+ { children: [{ group: lazy.UrlbarUtils.RESULT_GROUP.ZEN_WORKSPACE }] },
// heuristic
{
maxResultCount: 1,

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/urlbar/UrlbarProvidersManager.sys.mjs b/browser/components/urlbar/UrlbarProvidersManager.sys.mjs
index 08455d8d5da233639ccebc0e77c0810fb4f674c3..78d0e875978b568b79646489c28b125a44ea79fa 100644
index 08455d8d5da233639ccebc0e77c0810fb4f674c3..da8092b561c3dd8864e57f5a52a1a643db29ace1 100644
--- a/browser/components/urlbar/UrlbarProvidersManager.sys.mjs
+++ b/browser/components/urlbar/UrlbarProvidersManager.sys.mjs
@@ -913,6 +913,7 @@ export class Query {
@@ -10,3 +10,26 @@ index 08455d8d5da233639ccebc0e77c0810fb4f674c3..78d0e875978b568b79646489c28b125a
(!this.context.trimmedSearchString ||
(!this.context.searchMode.engineName && !result.autofill))
) {
@@ -1043,6 +1044,7 @@ function updateSourcesIfEmpty(context) {
lazy.UrlbarTokenizer.TYPE.RESTRICT_TITLE,
lazy.UrlbarTokenizer.TYPE.RESTRICT_URL,
lazy.UrlbarTokenizer.TYPE.RESTRICT_ACTION,
+ lazy.UrlbarTokenizer.TYPE.RESTRICT_WORKSPACE,
].includes(t.type)
);
@@ -1100,6 +1102,14 @@ function updateSourcesIfEmpty(context) {
acceptedSources.push(source);
}
break;
+ case lazy.UrlbarUtils.RESULT_SOURCE.WORKSPACES:
+ if (
+ restrictTokenType === lazy.UrlbarTokenizer.TYPE.RESTRICT_WORKSPACE ||
+ !restrictTokenType
+ ) {
+ acceptedSources.push(source);
+ }
+ break;
case lazy.UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL:
case lazy.UrlbarUtils.RESULT_SOURCE.ADDON:
default:

View File

@@ -0,0 +1,28 @@
diff --git a/browser/components/urlbar/UrlbarTokenizer.sys.mjs b/browser/components/urlbar/UrlbarTokenizer.sys.mjs
index d4af0ee5138a69139b94d898fb07e2345172f025..f750aae3f9f0a849ca009784510575b2b7119e6d 100644
--- a/browser/components/urlbar/UrlbarTokenizer.sys.mjs
+++ b/browser/components/urlbar/UrlbarTokenizer.sys.mjs
@@ -66,6 +66,7 @@ export var UrlbarTokenizer = {
// `looksLikeOrigin()` returned `LOOKS_LIKE_ORIGIN.OTHER` for this token. It
// may or may not be an origin.
POSSIBLE_ORIGIN_BUT_SEARCH_ALLOWED: 12,
+ RESTRICT_WORKSPACE: 13,
}),
// The special characters below can be typed into the urlbar to restrict
@@ -83,6 +84,7 @@ export var UrlbarTokenizer = {
TITLE: "#",
URL: "$",
ACTION: ">",
+ WORKSPACE: "`",
}),
// The keys of characters in RESTRICT that will enter search mode.
@@ -97,6 +99,7 @@ export var UrlbarTokenizer = {
if (lazy.UrlbarPrefs.get("scotchBonnet.enableOverride")) {
keys.push(this.RESTRICT.ACTION);
}
+ keys.push(this.RESTRICT.WORKSPACE);
return new Set(keys);
},

View File

@@ -1,29 +1,50 @@
diff --git a/browser/components/urlbar/UrlbarUtils.sys.mjs b/browser/components/urlbar/UrlbarUtils.sys.mjs
index 64afd613f454edd7786fcc1e2f307a582e4d5f51..b4af9cc2fbddba2c5229e8ffee7b9c0c06c3e1d0 100644
index 64afd613f454edd7786fcc1e2f307a582e4d5f51..92f91379f43785cf5417c96b178884c860db6bd3 100644
--- a/browser/components/urlbar/UrlbarUtils.sys.mjs
+++ b/browser/components/urlbar/UrlbarUtils.sys.mjs
@@ -85,6 +85,7 @@ export var UrlbarUtils = {
@@ -85,6 +85,8 @@ export var UrlbarUtils = {
RESTRICT_SEARCH_KEYWORD: "restrictSearchKeyword",
SUGGESTED_INDEX: "suggestedIndex",
TAIL_SUGGESTION: "tailSuggestion",
+ ZEN_ACTION: "zenAction",
+ ZEN_WORKSPACE: "zenWorkspace",
}),
// Defines provider types.
@@ -146,6 +147,7 @@ export var UrlbarUtils = {
@@ -146,6 +148,8 @@ export var UrlbarUtils = {
OTHER_NETWORK: 6,
ADDON: 7,
ACTIONS: 8,
+ ZEN_ACTIONS: 9,
+ WORKSPACES: 10,
}),
// Per-result exposure telemetry.
@@ -587,6 +589,8 @@ export var UrlbarUtils = {
@@ -295,6 +299,14 @@ export var UrlbarUtils = {
telemetryLabel: "actions",
uiLabel: "urlbar-searchmode-actions",
},
+ {
+ source: this.RESULT_SOURCE.WORKSPACES,
+ restrict: lazy.UrlbarTokenizer.RESTRICT.WORKSPACE,
+ icon: "chrome://browser/skin/zen-icons/selectable/layers.svg",
+ pref: "shortcuts.workspaces",
+ telemetryLabel: "workspaces",
+ uiLabel: "urlbar-search-mode-workspaces",
+ },
]);
},
@@ -587,6 +599,12 @@ export var UrlbarUtils = {
return this.RESULT_GROUP.HEURISTIC_FALLBACK;
case "UrlbarProviderHistoryUrlHeuristic":
return this.RESULT_GROUP.HEURISTIC_HISTORY_URL;
+ case "ZenUrlbarProviderGlobalActions":
+ return this.RESULT_GROUP.ZEN_ACTION;
+ if (result.source == this.RESULT_SOURCE.WORKSPACES) {
+ return this.RESULT_GROUP.ZEN_WORKSPACE;
+ } else {
+ return this.RESULT_GROUP.ZEN_ACTION;
+ }
case "UrlbarProviderOmnibox":
return this.RESULT_GROUP.HEURISTIC_OMNIBOX;
case "UrlbarProviderRestrictKeywordsAutofill":

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/urlbar/UrlbarView.sys.mjs b/browser/components/urlbar/UrlbarView.sys.mjs
index b665adb1a1ce8bbae8df4cbea6c3248c3e4fb431..7a73907957d38caba5b650749abf80a01b51559d 100644
index b665adb1a1ce8bbae8df4cbea6c3248c3e4fb431..1de7f9461b8ccbd4680b917e6dd5ba3c02f69a94 100644
--- a/browser/components/urlbar/UrlbarView.sys.mjs
+++ b/browser/components/urlbar/UrlbarView.sys.mjs
@@ -640,7 +640,7 @@ export class UrlbarView {
@@ -11,7 +11,18 @@ index b665adb1a1ce8bbae8df4cbea6c3248c3e4fb431..7a73907957d38caba5b650749abf80a0
// Try to reuse the cached top-sites context. If it's not cached, then
// there will be a gap of time between when the input is focused and
// when the view opens that can be perceived as flicker.
@@ -2988,6 +2988,8 @@ export class UrlbarView {
@@ -777,10 +777,6 @@ export class UrlbarView {
}
// If search mode isn't active, close the view.
- if (!this.input.searchMode) {
- this.close();
- return;
- }
// Search mode is active. If the one-offs should be shown, make sure they
// are enabled and show the view.
@@ -2988,6 +2984,8 @@ export class UrlbarView {
if (row?.hasAttribute("row-selectable")) {
row?.toggleAttribute("selected", true);
}
@@ -20,7 +31,7 @@ index b665adb1a1ce8bbae8df4cbea6c3248c3e4fb431..7a73907957d38caba5b650749abf80a0
if (element != row) {
row?.toggleAttribute("descendant-selected", true);
}
@@ -3477,7 +3479,7 @@ export class UrlbarView {
@@ -3477,7 +3475,7 @@ export class UrlbarView {
}
#enableOrDisableRowWrap() {

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/urlbar/content/UrlbarInput.mjs b/browser/components/urlbar/content/UrlbarInput.mjs
index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f5c811a82 100644
index b23244f9d3278918b016bb3fcab19687bc2e292a..e5de81b3060a1ee76b1a6aff2e4ae1ca50f2caa9 100644
--- a/browser/components/urlbar/content/UrlbarInput.mjs
+++ b/browser/components/urlbar/content/UrlbarInput.mjs
@@ -90,6 +90,13 @@ const lazy = XPCOMUtils.declareLazy({
@@ -75,7 +75,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
}
if (isCanonized) {
@@ -2696,6 +2728,42 @@ export class UrlbarInput extends HTMLElement {
@@ -2696,6 +2728,45 @@ export class UrlbarInput extends HTMLElement {
await this.#updateLayoutBreakoutDimensions();
}
@@ -84,7 +84,10 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
+ }
+
+ get zenUrlbarBehavior() {
+ if (this.document.documentElement.hasAttribute("inDOMFullscreen")) {
+ if (
+ this.document.documentElement.hasAttribute("inDOMFullscreen") ||
+ this.document.documentElement.hasAttribute("zen-little-window")
+ ) {
+ return "float";
+ }
+ return lazy.ZEN_URLBAR_BEHAVIOR;
@@ -118,7 +121,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
startLayoutExtend() {
if (!this.#allowBreakout || this.hasAttribute("breakout-extend")) {
// Do not expand if the Urlbar does not support being expanded or it is
@@ -2710,6 +2778,13 @@ export class UrlbarInput extends HTMLElement {
@@ -2710,6 +2781,13 @@ export class UrlbarInput extends HTMLElement {
this.setAttribute("breakout-extend", "true");
@@ -132,7 +135,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
// Enable the animation only after the first extend call to ensure it
// doesn't run when opening a new window.
if (!this.hasAttribute("breakout-extend-animate")) {
@@ -2729,6 +2804,27 @@ export class UrlbarInput extends HTMLElement {
@@ -2729,6 +2807,27 @@ export class UrlbarInput extends HTMLElement {
return;
}
@@ -160,7 +163,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
this.removeAttribute("breakout-extend");
this.#updateTextboxPosition();
}
@@ -2759,7 +2855,7 @@ export class UrlbarInput extends HTMLElement {
@@ -2759,7 +2858,7 @@ export class UrlbarInput extends HTMLElement {
forceUnifiedSearchButtonAvailable = false
) {
let prevState = this.getAttribute("pageproxystate");
@@ -169,7 +172,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
this.setAttribute("pageproxystate", state);
this._inputContainer.setAttribute("pageproxystate", state);
this._identityBox?.setAttribute("pageproxystate", state);
@@ -3031,10 +3127,12 @@ export class UrlbarInput extends HTMLElement {
@@ -3031,10 +3130,12 @@ export class UrlbarInput extends HTMLElement {
return;
}
this.style.top = px(
@@ -182,7 +185,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
);
}
@@ -3093,9 +3191,10 @@ export class UrlbarInput extends HTMLElement {
@@ -3093,9 +3194,10 @@ export class UrlbarInput extends HTMLElement {
return;
}
@@ -194,7 +197,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
);
this.style.setProperty(
"--urlbar-height",
@@ -3597,6 +3696,7 @@ export class UrlbarInput extends HTMLElement {
@@ -3597,6 +3699,7 @@ export class UrlbarInput extends HTMLElement {
}
_toggleActionOverride(event) {
@@ -202,7 +205,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
if (
event.keyCode == KeyEvent.DOM_VK_SHIFT ||
event.keyCode == KeyEvent.DOM_VK_ALT ||
@@ -3709,8 +3809,8 @@ export class UrlbarInput extends HTMLElement {
@@ -3709,8 +3812,8 @@ export class UrlbarInput extends HTMLElement {
if (!this.#isAddressbar) {
return val;
}
@@ -213,7 +216,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
: val;
// Only trim value if the directionality doesn't change to RTL and we're not
// showing a strikeout https protocol.
@@ -4006,6 +4106,7 @@ export class UrlbarInput extends HTMLElement {
@@ -4006,6 +4109,7 @@ export class UrlbarInput extends HTMLElement {
resultDetails = null,
browser = this.window.gBrowser.selectedBrowser
) {
@@ -221,7 +224,19 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
if (this.#isAddressbar) {
this.#prepareAddressbarLoad(
url,
@@ -4117,6 +4218,10 @@ export class UrlbarInput extends HTMLElement {
@@ -4088,6 +4192,11 @@ export class UrlbarInput extends HTMLElement {
* @returns {"current" | "tabshifted" | "tab" | "save" | "window"}
*/
_whereToOpen(event) {
+ if (this.document.documentElement.hasAttribute("zen-little-window")) {
+ // Little windows are single-tab popups -- never spawn extra tabs
+ // or new windows from the urlbar.
+ return "current";
+ }
let isKeyboardEvent = KeyboardEvent.isInstance(event);
let reuseEmpty = isKeyboardEvent;
let where = undefined;
@@ -4117,6 +4226,10 @@ export class UrlbarInput extends HTMLElement {
}
reuseEmpty = true;
}
@@ -232,7 +247,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
if (
where == "tab" &&
reuseEmpty &&
@@ -4124,6 +4229,9 @@ export class UrlbarInput extends HTMLElement {
@@ -4124,6 +4237,9 @@ export class UrlbarInput extends HTMLElement {
) {
where = "current";
}
@@ -242,7 +257,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
return where;
}
@@ -4378,6 +4486,7 @@ export class UrlbarInput extends HTMLElement {
@@ -4378,6 +4494,7 @@ export class UrlbarInput extends HTMLElement {
this.setResultForCurrentValue(null);
this.handleCommand();
this.controller.clearLastQueryContextCache();
@@ -250,7 +265,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
this._suppressStartQuery = false;
});
@@ -4385,7 +4494,6 @@ export class UrlbarInput extends HTMLElement {
@@ -4385,7 +4502,6 @@ export class UrlbarInput extends HTMLElement {
contextMenu.addEventListener("popupshowing", () => {
// Close the results pane when the input field contextual menu is open,
// because paste and go doesn't want a result selection.
@@ -258,7 +273,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
let controller =
this.document.commandDispatcher.getControllerForCommand("cmd_paste");
@@ -4541,7 +4649,11 @@ export class UrlbarInput extends HTMLElement {
@@ -4541,7 +4657,11 @@ export class UrlbarInput extends HTMLElement {
if (!engineName && !source && !this.hasAttribute("searchmode")) {
return;
}
@@ -271,7 +286,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
if (this._searchModeIndicatorTitle) {
this._searchModeIndicatorTitle.textContent = "";
this._searchModeIndicatorTitle.removeAttribute("data-l10n-id");
@@ -4851,6 +4963,7 @@ export class UrlbarInput extends HTMLElement {
@@ -4851,6 +4971,7 @@ export class UrlbarInput extends HTMLElement {
this.document.l10n.setAttributes(
this.inputField,
@@ -279,7 +294,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
l10nId,
l10nId == "urlbar-placeholder-with-name"
? { name: engineName }
@@ -4964,6 +5077,11 @@ export class UrlbarInput extends HTMLElement {
@@ -4964,6 +5085,11 @@ export class UrlbarInput extends HTMLElement {
}
_on_click(event) {
@@ -291,7 +306,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
switch (event.target) {
case this.inputField:
case this._inputContainer:
@@ -5042,7 +5160,7 @@ export class UrlbarInput extends HTMLElement {
@@ -5042,7 +5168,7 @@ export class UrlbarInput extends HTMLElement {
}
}
@@ -300,7 +315,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
this.view.autoOpen({ event });
} else {
if (this._untrimOnFocusAfterKeydown) {
@@ -5082,9 +5200,16 @@ export class UrlbarInput extends HTMLElement {
@@ -5082,9 +5208,16 @@ export class UrlbarInput extends HTMLElement {
}
_on_mousedown(event) {
@@ -318,7 +333,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
if (
event.composedTarget != this.inputField &&
event.composedTarget != this._inputContainer
@@ -5094,6 +5219,10 @@ export class UrlbarInput extends HTMLElement {
@@ -5094,6 +5227,10 @@ export class UrlbarInput extends HTMLElement {
this.focusedViaMousedown = !this.focused;
this._preventClickSelectsAll = this.focused;
@@ -329,7 +344,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
// Keep the focus status, since the attribute may be changed
// upon calling this.focus().
@@ -5129,7 +5258,7 @@ export class UrlbarInput extends HTMLElement {
@@ -5129,7 +5266,7 @@ export class UrlbarInput extends HTMLElement {
}
// Don't close the view when clicking on a tab; we may want to keep the
// view open on tab switch, and the TabSelect event arrived earlier.
@@ -338,7 +353,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
break;
}
@@ -5411,7 +5540,7 @@ export class UrlbarInput extends HTMLElement {
@@ -5411,7 +5548,7 @@ export class UrlbarInput extends HTMLElement {
// When we are in actions search mode we can show more results so
// increase the limit.
let maxResults =

View File

@@ -0,0 +1,14 @@
diff --git a/browser/components/urlbar/content/enUS-searchFeatures.ftl b/browser/components/urlbar/content/enUS-searchFeatures.ftl
index a1fb86058d1f6f015160163f75999b4a429bf1fd..83098291e21a8239513a4105a4604394a1dfabc0 100644
--- a/browser/components/urlbar/content/enUS-searchFeatures.ftl
+++ b/browser/components/urlbar/content/enUS-searchFeatures.ftl
@@ -2,6 +2,9 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+urlbar-search-mode-workspaces = Spaces
+urlbar-search-mode-workspaces-en = Spaces
+
### These strings are related to the Firefox Suggest feature. Firefox Suggest
### shows recommended and sponsored third-party results in the address bar
### panel. It also shows headings/labels above different groups of results. For

View File

@@ -1,22 +1,37 @@
diff --git a/browser/modules/BrowserWindowTracker.sys.mjs b/browser/modules/BrowserWindowTracker.sys.mjs
index 9aecab66d8f23fac9f16cea2120a5fe903ae1122..692f2bfe3899a58925789503a6bb2a547cdbf7f3 100644
index 9aecab66d8f23fac9f16cea2120a5fe903ae1122..e023c27bcb027d29ba9b3469eca5957d42040c46 100644
--- a/browser/modules/BrowserWindowTracker.sys.mjs
+++ b/browser/modules/BrowserWindowTracker.sys.mjs
@@ -330,6 +330,7 @@ export const BrowserWindowTracker = {
@@ -210,7 +210,8 @@ export const BrowserWindowTracker = {
!win.closed &&
(options.allowPopups || win.toolbar.visible) &&
(options.allowTaskbarTabs ||
- !win.document.documentElement.hasAttribute("taskbartab")) &&
+ (!win.document.documentElement.hasAttribute("taskbartab") &&
+ !win.document.documentElement.hasAttribute("zen-little-window"))) &&
(!("private" in options) ||
lazy.PrivateBrowsingUtils.permanentPrivateBrowsing ||
lazy.PrivateBrowsingUtils.isWindowPrivate(win) == options.private)
@@ -330,6 +331,8 @@ export const BrowserWindowTracker = {
args = null,
remote = undefined,
fission = undefined,
+ zenSyncedWindow = true,
+ zenLittleWindow = false,
} = options;
args = lazy.AIWindow.handleAIWindowOptions(options);
@@ -386,6 +387,12 @@ export const BrowserWindowTracker = {
@@ -386,6 +389,16 @@ export const BrowserWindowTracker = {
windowFeatures,
args
);
+ win._zenStartupSyncFlag = Services.prefs.getBoolPref("zen.window-sync.prefer-unsynced-windows")
+ ? (zenSyncedWindow ? 'unsynced' : 'synced')
+ : (zenSyncedWindow ? 'synced' : 'unsynced');
+ if (zenLittleWindow) {
+ win._zenStartupLittleWindow = true;
+ win._zenStartupSyncFlag = 'unsynced';
+ }
+ if (win._zenStartupSyncFlag === 'unsynced' && openerWindow) {
+ win._zenStartupUnsyncedUserContextId = openerWindow.gZenWorkspaces.getCurrentSpaceContainerId();
+ }

View File

@@ -1,5 +1,5 @@
diff --git a/browser/modules/URILoadingHelper.sys.mjs b/browser/modules/URILoadingHelper.sys.mjs
index a005dbdf84609622ef8054f73f78c0c290e76125..d5bf6fb51c9af5e60f69a73612ee91598080730a 100644
index a005dbdf84609622ef8054f73f78c0c290e76125..2d347ac12d53ae97b61750d421a489ce10af3376 100644
--- a/browser/modules/URILoadingHelper.sys.mjs
+++ b/browser/modules/URILoadingHelper.sys.mjs
@@ -224,6 +224,7 @@ function openInWindow(url, params, sourceWindow) {
@@ -19,7 +19,17 @@ index a005dbdf84609622ef8054f73f78c0c290e76125..d5bf6fb51c9af5e60f69a73612ee9159
where = "tab";
targetBrowser = null;
} else if (
@@ -974,7 +975,7 @@ export const URILoadingHelper = {
@@ -724,7 +725,8 @@ export const URILoadingHelper = {
"navigator:browser" &&
(!skipPopups || top.toolbar.visible) &&
(!skipTaskbarTabs ||
- !top.document.documentElement.hasAttribute("taskbartab")) &&
+ (!top.document.documentElement.hasAttribute("taskbartab") &&
+ !top.document.documentElement.hasAttribute("zen-little-window"))) &&
(!forceNonPrivate || !PrivateBrowsingUtils.isWindowPrivate(top))
) {
return top;
@@ -974,7 +976,7 @@ export const URILoadingHelper = {
ignoreQueryString || replaceQueryString,
ignoreFragmentWhenComparing
);
@@ -28,7 +38,7 @@ index a005dbdf84609622ef8054f73f78c0c290e76125..d5bf6fb51c9af5e60f69a73612ee9159
for (let i = 0; i < browsers.length; i++) {
let browser = browsers[i];
let browserCompare = cleanURL(
@@ -1030,7 +1031,7 @@ export const URILoadingHelper = {
@@ -1030,7 +1032,7 @@ export const URILoadingHelper = {
);
aSplitView.ownerGlobal.focus();
} else {

View File

@@ -8,7 +8,8 @@
#zen-welcome-start-button,
.zen-toast button,
.zen-current-workspace-indicator-chevron,
.pinned-tabs-container-separator toolbarbutton {
.pinned-tabs-container-separator toolbarbutton,
.zen-permission-popup-boost-editor-button {
-moz-context-properties: fill, fill-opacity !important;
fill: currentColor !important;
}
@@ -39,10 +40,21 @@
.close-icon,
.zen-glance-sidebar-close,
.zen-theme-picker-custom-list-item-remove,
#appMenu-quit-button2 {
#appMenu-quit-button2,
#zen-boost-delete {
list-style-image: url("close.svg") !important;
}
#PanelUI-zen-emojis-picker-none,
#zen-emojis-picker-none {
list-style-image: url("trash.svg");
}
#PanelUI-zen-gradient-generator-color-remove,
#zen-gradient-generator-color-remove {
list-style-image: url("unpin.svg") !important;
}
#PanelUI-zen-emojis-picker-none {
list-style-image: url("trash.svg");
}
@@ -117,7 +129,8 @@
}
.zen-current-workspace-indicator-chevron,
#PanelUI-zen-gradient-generator-color-page-right {
#PanelUI-zen-gradient-generator-color-page-right,
.zen-permission-popup-boost-editor-button {
list-style-image: url("arrow-right.svg");
}
@@ -412,7 +425,8 @@
}
.panel-header > .subviewbutton-back,
#PanelUI-zen-gradient-generator-color-page-left {
#PanelUI-zen-gradient-generator-color-page-left,
#zen-boost-back {
list-style-image: url("arrow-left.svg") !important;
}
@@ -504,6 +518,30 @@
&:where([starred]) image {
list-style-image: url("permissions-fill.svg");
}
&[boosting] image {
color: var(--color-accent-primary);
list-style-image: url("permissions-fill.svg");
}
position: relative;
& .zen-site-data-boost-animation {
display: none;
}
}
@media not (prefers-reduced-motion: reduce) {
#zen-site-data-icon-button[boosting] .zen-site-data-boost-animation {
position: absolute;
display: flex;
width: 100%;
height: 100%;
color: var(--color-accent-primary);
list-style-image: url("chrome://browser/content/zen-images/boost-indicator.svg");
transform: translateX(-20%);
z-index: 0;
pointer-events: none;
}
}
.geo-icon {
@@ -758,7 +796,8 @@
--fp-enabled: 1;
}
#alltabs-button {
#alltabs-button,
#urlbar-engine-one-off-item-workspaces {
list-style-image: url("chrome://browser/skin/tabs.svg") !important;
}
@@ -878,6 +917,34 @@
fill: currentColor;
}
#zen-site-data-boost {
border-radius: 6px;
appearance: none;
padding: 6px 10px 6px 10px;
position: relative;
list-style-image: url("paintbrush.svg");
& .toolbarbutton-text {
display: none;
}
@media -moz-pref("zen.boosts.enabled", false) {
display: none;
}
& image {
-moz-context-properties: fill, fill-opacity;
fill: currentColor;
}
}
.boost-brush {
list-style-image: url("boost.svg") !important;
padding: 10px;
}
#zen-site-data-security-info {
-moz-context-properties: fill, fill-opacity;
fill: currentColor;
@@ -937,3 +1004,79 @@
list-style-image: url("link.svg");
fill-opacity: 0.65;
}
#zen-boost-text-case-toggle {
list-style-image: url("text-title-case.svg");
}
#zen-boost-text-case-toggle[case-mode="uppercase"] {
list-style-image: url("text-uppercase.svg");
}
#zen-boost-text-case-toggle[case-mode="lowercase"] {
list-style-image: url("text-lowercase.svg");
}
#zen-boost-code {
list-style-image: url("brackets-curly.svg");
}
#zen-boost-zap {
list-style-image: url("bolt.svg");
}
#zen-boost-controls {
list-style-image: url("sliders.svg");
}
#zen-boost-invert {
list-style-image: url("lightbulb.svg");
}
#zen-boost-disable {
list-style-image: url("block.svg");
}
#zen-boost-close {
list-style-image: url("close.svg");
@media (-moz-platform: macos) {
list-style-image: url("close-filled-round.svg");
}
@media (-moz-platform: windows) {
list-style-image: none;
list-style: none;
display: inline-flex;
&::before {
font-family: "Segoe Fluent Icons", "Segoe MDL2 Assets";
content: "\e8bb";
font-size: 12px;
}
}
}
#zen-boost-magic-theme {
list-style-image: url("sparkles.svg");
}
#zen-boost-shuffle {
list-style-image: url("arrow-rotate-anticlockwise.svg");
}
#zen-boost-css-picker {
list-style-image: url("eyedropper.svg");
}
#zen-boost-css-inspector {
list-style-image: url("hammer.svg");
}
#zen-boost-save {
list-style-image: url("downloads.svg");
}
#zen-boost-load {
list-style-image: url("open.svg");
}

View File

@@ -7,20 +7,27 @@
* skin/classic/browser/zen-icons/arrow-down.svg (../shared/zen-icons/nucleo/arrow-down.svg)
* skin/classic/browser/zen-icons/arrow-left.svg (../shared/zen-icons/nucleo/arrow-left.svg)
* skin/classic/browser/zen-icons/arrow-right.svg (../shared/zen-icons/nucleo/arrow-right.svg)
* skin/classic/browser/zen-icons/arrow-rotate-anticlockwise.svg (../shared/zen-icons/nucleo/arrow-rotate-anticlockwise.svg)
* skin/classic/browser/zen-icons/arrow-up.svg (../shared/zen-icons/nucleo/arrow-up.svg)
* skin/classic/browser/zen-icons/autoplay-media-blocked.svg (../shared/zen-icons/nucleo/autoplay-media-blocked.svg)
* skin/classic/browser/zen-icons/autoplay-media-fill.svg (../shared/zen-icons/nucleo/autoplay-media-fill.svg)
* skin/classic/browser/zen-icons/autoplay-media.svg (../shared/zen-icons/nucleo/autoplay-media.svg)
* skin/classic/browser/zen-icons/back.svg (../shared/zen-icons/nucleo/back.svg)
* skin/classic/browser/zen-icons/block.svg (../shared/zen-icons/nucleo/block.svg)
* skin/classic/browser/zen-icons/bolt.svg (../shared/zen-icons/nucleo/bolt.svg)
* skin/classic/browser/zen-icons/bookmark-hollow.svg (../shared/zen-icons/nucleo/bookmark-hollow.svg)
* skin/classic/browser/zen-icons/bookmark-star-on-tray.svg (../shared/zen-icons/nucleo/bookmark-star-on-tray.svg)
* skin/classic/browser/zen-icons/bookmark.svg (../shared/zen-icons/nucleo/bookmark.svg)
* skin/classic/browser/zen-icons/boost.svg (../shared/zen-icons/nucleo/boost.svg)
* skin/classic/browser/zen-icons/blocked-element.svg (../shared/zen-icons/nucleo/blocked-element.svg)
* skin/classic/browser/zen-icons/brackets-curly.svg (../shared/zen-icons/nucleo/brackets-curly.svg)
* skin/classic/browser/zen-icons/camera-blocked.svg (../shared/zen-icons/nucleo/camera-blocked.svg)
* skin/classic/browser/zen-icons/camera-fill.svg (../shared/zen-icons/nucleo/camera-fill.svg)
* skin/classic/browser/zen-icons/camera.svg (../shared/zen-icons/nucleo/camera.svg)
* skin/classic/browser/zen-icons/canvas-blocked.svg (../shared/zen-icons/nucleo/canvas-blocked.svg)
* skin/classic/browser/zen-icons/canvas.svg (../shared/zen-icons/nucleo/canvas.svg)
* skin/classic/browser/zen-icons/chevron.svg (../shared/zen-icons/nucleo/chevron.svg)
* skin/classic/browser/zen-icons/close-filled-round.svg (../shared/zen-icons/nucleo/close-filled-round.svg)
* skin/classic/browser/zen-icons/close.svg (../shared/zen-icons/nucleo/close.svg)
* skin/classic/browser/zen-icons/container-tab.svg (../shared/zen-icons/nucleo/container-tab.svg)
* skin/classic/browser/zen-icons/cookies-fill.svg (../shared/zen-icons/nucleo/cookies-fill.svg)
@@ -44,6 +51,7 @@
* skin/classic/browser/zen-icons/extension-blocked.svg (../shared/zen-icons/nucleo/extension-blocked.svg)
* skin/classic/browser/zen-icons/extension-fill.svg (../shared/zen-icons/nucleo/extension-fill.svg)
* skin/classic/browser/zen-icons/extension.svg (../shared/zen-icons/nucleo/extension.svg)
* skin/classic/browser/zen-icons/eyedropper.svg (../shared/zen-icons/nucleo/eyedropper.svg)
* skin/classic/browser/zen-icons/face-sun.svg (../shared/zen-icons/nucleo/face-sun.svg)
* skin/classic/browser/zen-icons/folder.svg (../shared/zen-icons/nucleo/folder.svg)
* skin/classic/browser/zen-icons/forget.svg (../shared/zen-icons/nucleo/forget.svg)
@@ -53,12 +61,14 @@
* skin/classic/browser/zen-icons/geo-blocked.svg (../shared/zen-icons/nucleo/geo-blocked.svg)
* skin/classic/browser/zen-icons/geo-fill.svg (../shared/zen-icons/nucleo/geo-fill.svg)
* skin/classic/browser/zen-icons/geo.svg (../shared/zen-icons/nucleo/geo.svg)
* skin/classic/browser/zen-icons/hammer.svg (../shared/zen-icons/nucleo/hammer.svg)
* skin/classic/browser/zen-icons/heart-circle-fill.svg (../shared/zen-icons/nucleo/heart-circle-fill.svg)
* skin/classic/browser/zen-icons/help.svg (../shared/zen-icons/nucleo/help.svg)
* skin/classic/browser/zen-icons/history.svg (../shared/zen-icons/nucleo/history.svg)
* skin/classic/browser/zen-icons/home.svg (../shared/zen-icons/nucleo/home.svg)
* skin/classic/browser/zen-icons/info.svg (../shared/zen-icons/nucleo/info.svg)
* skin/classic/browser/zen-icons/library.svg (../shared/zen-icons/nucleo/library.svg)
* skin/classic/browser/zen-icons/lightbulb.svg (../shared/zen-icons/nucleo/lightbulb.svg)
* skin/classic/browser/zen-icons/link.svg (../shared/zen-icons/nucleo/link.svg)
* skin/classic/browser/zen-icons/mail.svg (../shared/zen-icons/nucleo/mail.svg)
* skin/classic/browser/zen-icons/manage.svg (../shared/zen-icons/nucleo/manage.svg)
@@ -78,6 +88,8 @@
* skin/classic/browser/zen-icons/new-tab-image.svg (../shared/zen-icons/nucleo/new-tab-image.svg)
* skin/classic/browser/zen-icons/open.svg (../shared/zen-icons/nucleo/open.svg)
* skin/classic/browser/zen-icons/page-portrait.svg (../shared/zen-icons/nucleo/page-portrait.svg)
* skin/classic/browser/zen-icons/paintbrush-fill.svg (../shared/zen-icons/nucleo/paintbrush-fill.svg)
* skin/classic/browser/zen-icons/paintbrush.svg (../shared/zen-icons/nucleo/paintbrush.svg)
* skin/classic/browser/zen-icons/palette.svg (../shared/zen-icons/nucleo/palette.svg)
* skin/classic/browser/zen-icons/passwords.svg (../shared/zen-icons/nucleo/passwords.svg)
* skin/classic/browser/zen-icons/permissions-fill.svg (../shared/zen-icons/nucleo/permissions-fill.svg)
@@ -110,13 +122,19 @@
* skin/classic/browser/zen-icons/sidebar-right.svg (../shared/zen-icons/nucleo/sidebar-right.svg)
* skin/classic/browser/zen-icons/sidebar.svg (../shared/zen-icons/nucleo/sidebar.svg)
* skin/classic/browser/zen-icons/sidebars-right.svg (../shared/zen-icons/nucleo/sidebars-right.svg)
* skin/classic/browser/zen-icons/sliders.svg (../shared/zen-icons/nucleo/sliders.svg)
* skin/classic/browser/zen-icons/sparkles.svg (../shared/zen-icons/nucleo/sparkles.svg)
* skin/classic/browser/zen-icons/spell-check.svg (../shared/zen-icons/nucleo/spell-check.svg)
* skin/classic/browser/zen-icons/split.svg (../shared/zen-icons/nucleo/split.svg)
* skin/classic/browser/zen-icons/square-wand-sparkle.svg (../shared/zen-icons/nucleo/square-wand-sparkle.svg)
* skin/classic/browser/zen-icons/tab-audio-blocked-small.svg (../shared/zen-icons/nucleo/tab-audio-blocked-small.svg)
* skin/classic/browser/zen-icons/tab-audio-muted-small.svg (../shared/zen-icons/nucleo/tab-audio-muted-small.svg)
* skin/classic/browser/zen-icons/tab-audio-playing-small.svg (../shared/zen-icons/nucleo/tab-audio-playing-small.svg)
* skin/classic/browser/zen-icons/tab.svg (../shared/zen-icons/nucleo/tab.svg)
* skin/classic/browser/zen-icons/text-lowercase.svg (../shared/zen-icons/nucleo/text-lowercase.svg)
* skin/classic/browser/zen-icons/text-size.svg (../shared/zen-icons/nucleo/text-size.svg)
* skin/classic/browser/zen-icons/text-title-case.svg (../shared/zen-icons/nucleo/text-title-case.svg)
* skin/classic/browser/zen-icons/text-uppercase.svg (../shared/zen-icons/nucleo/text-uppercase.svg)
* skin/classic/browser/zen-icons/tool-profiler.svg (../shared/zen-icons/nucleo/tool-profiler.svg)
* skin/classic/browser/zen-icons/tracking-protection-fill.svg (../shared/zen-icons/nucleo/tracking-protection-fill.svg)
* skin/classic/browser/zen-icons/tracking-protection.svg (../shared/zen-icons/nucleo/tracking-protection.svg)
@@ -125,6 +143,7 @@
* skin/classic/browser/zen-icons/unpin.svg (../shared/zen-icons/nucleo/unpin.svg)
* skin/classic/browser/zen-icons/video-blocked-fill.svg (../shared/zen-icons/nucleo/video-blocked-fill.svg)
* skin/classic/browser/zen-icons/video-fill.svg (../shared/zen-icons/nucleo/video-fill.svg)
* skin/classic/browser/zen-icons/wand-sparkle.svg (../shared/zen-icons/nucleo/wand-sparkle.svg)
* skin/classic/browser/zen-icons/window.svg (../shared/zen-icons/nucleo/window.svg)
* skin/classic/browser/zen-icons/xr-blocked.svg (../shared/zen-icons/nucleo/xr-blocked.svg)
* skin/classic/browser/zen-icons/xr-fill.svg (../shared/zen-icons/nucleo/xr-fill.svg)
@@ -136,20 +155,27 @@
* skin/classic/browser/zen-icons/arrow-down.svg (../shared/zen-icons/nucleo/arrow-down.svg)
* skin/classic/browser/zen-icons/arrow-left.svg (../shared/zen-icons/nucleo/arrow-left.svg)
* skin/classic/browser/zen-icons/arrow-right.svg (../shared/zen-icons/nucleo/arrow-right.svg)
* skin/classic/browser/zen-icons/arrow-rotate-anticlockwise.svg (../shared/zen-icons/nucleo/arrow-rotate-anticlockwise.svg)
* skin/classic/browser/zen-icons/arrow-up.svg (../shared/zen-icons/nucleo/arrow-up.svg)
* skin/classic/browser/zen-icons/autoplay-media-blocked.svg (../shared/zen-icons/nucleo/autoplay-media-blocked.svg)
* skin/classic/browser/zen-icons/autoplay-media-fill.svg (../shared/zen-icons/nucleo/autoplay-media-fill.svg)
* skin/classic/browser/zen-icons/autoplay-media.svg (../shared/zen-icons/nucleo/autoplay-media.svg)
* skin/classic/browser/zen-icons/back.svg (../shared/zen-icons/nucleo/back.svg)
* skin/classic/browser/zen-icons/block.svg (../shared/zen-icons/nucleo/block.svg)
* skin/classic/browser/zen-icons/bolt.svg (../shared/zen-icons/nucleo/bolt.svg)
* skin/classic/browser/zen-icons/bookmark-hollow.svg (../shared/zen-icons/nucleo/bookmark-hollow.svg)
* skin/classic/browser/zen-icons/bookmark-star-on-tray.svg (../shared/zen-icons/nucleo/bookmark-star-on-tray.svg)
* skin/classic/browser/zen-icons/bookmark.svg (../shared/zen-icons/nucleo/bookmark.svg)
* skin/classic/browser/zen-icons/boost.svg (../shared/zen-icons/nucleo/boost.svg)
* skin/classic/browser/zen-icons/blocked-element.svg (../shared/zen-icons/nucleo/blocked-element.svg)
* skin/classic/browser/zen-icons/brackets-curly.svg (../shared/zen-icons/nucleo/brackets-curly.svg)
* skin/classic/browser/zen-icons/camera-blocked.svg (../shared/zen-icons/nucleo/camera-blocked.svg)
* skin/classic/browser/zen-icons/camera-fill.svg (../shared/zen-icons/nucleo/camera-fill.svg)
* skin/classic/browser/zen-icons/camera.svg (../shared/zen-icons/nucleo/camera.svg)
* skin/classic/browser/zen-icons/canvas-blocked.svg (../shared/zen-icons/nucleo/canvas-blocked.svg)
* skin/classic/browser/zen-icons/canvas.svg (../shared/zen-icons/nucleo/canvas.svg)
* skin/classic/browser/zen-icons/chevron.svg (../shared/zen-icons/nucleo/chevron.svg)
* skin/classic/browser/zen-icons/close-filled-round.svg (../shared/zen-icons/nucleo/close-filled-round.svg)
* skin/classic/browser/zen-icons/close.svg (../shared/zen-icons/nucleo/close.svg)
* skin/classic/browser/zen-icons/container-tab.svg (../shared/zen-icons/nucleo/container-tab.svg)
* skin/classic/browser/zen-icons/cookies-fill.svg (../shared/zen-icons/nucleo/cookies-fill.svg)
@@ -173,6 +199,7 @@
* skin/classic/browser/zen-icons/extension-blocked.svg (../shared/zen-icons/nucleo/extension-blocked.svg)
* skin/classic/browser/zen-icons/extension-fill.svg (../shared/zen-icons/nucleo/extension-fill.svg)
* skin/classic/browser/zen-icons/extension.svg (../shared/zen-icons/nucleo/extension.svg)
* skin/classic/browser/zen-icons/eyedropper.svg (../shared/zen-icons/nucleo/eyedropper.svg)
* skin/classic/browser/zen-icons/face-sun.svg (../shared/zen-icons/nucleo/face-sun.svg)
* skin/classic/browser/zen-icons/folder.svg (../shared/zen-icons/nucleo/folder.svg)
* skin/classic/browser/zen-icons/forget.svg (../shared/zen-icons/nucleo/forget.svg)
@@ -182,12 +209,14 @@
* skin/classic/browser/zen-icons/geo-blocked.svg (../shared/zen-icons/nucleo/geo-blocked.svg)
* skin/classic/browser/zen-icons/geo-fill.svg (../shared/zen-icons/nucleo/geo-fill.svg)
* skin/classic/browser/zen-icons/geo.svg (../shared/zen-icons/nucleo/geo.svg)
* skin/classic/browser/zen-icons/hammer.svg (../shared/zen-icons/nucleo/hammer.svg)
* skin/classic/browser/zen-icons/heart-circle-fill.svg (../shared/zen-icons/nucleo/heart-circle-fill.svg)
* skin/classic/browser/zen-icons/help.svg (../shared/zen-icons/nucleo/help.svg)
* skin/classic/browser/zen-icons/history.svg (../shared/zen-icons/nucleo/history.svg)
* skin/classic/browser/zen-icons/home.svg (../shared/zen-icons/nucleo/home.svg)
* skin/classic/browser/zen-icons/info.svg (../shared/zen-icons/nucleo/info.svg)
* skin/classic/browser/zen-icons/library.svg (../shared/zen-icons/nucleo/library.svg)
* skin/classic/browser/zen-icons/lightbulb.svg (../shared/zen-icons/nucleo/lightbulb.svg)
* skin/classic/browser/zen-icons/link.svg (../shared/zen-icons/nucleo/link.svg)
* skin/classic/browser/zen-icons/mail.svg (../shared/zen-icons/nucleo/mail.svg)
* skin/classic/browser/zen-icons/manage.svg (../shared/zen-icons/nucleo/manage.svg)
@@ -207,6 +236,8 @@
* skin/classic/browser/zen-icons/new-tab-image.svg (../shared/zen-icons/nucleo/new-tab-image.svg)
* skin/classic/browser/zen-icons/open.svg (../shared/zen-icons/nucleo/open.svg)
* skin/classic/browser/zen-icons/page-portrait.svg (../shared/zen-icons/nucleo/page-portrait.svg)
* skin/classic/browser/zen-icons/paintbrush-fill.svg (../shared/zen-icons/nucleo/paintbrush-fill.svg)
* skin/classic/browser/zen-icons/paintbrush.svg (../shared/zen-icons/nucleo/paintbrush.svg)
* skin/classic/browser/zen-icons/palette.svg (../shared/zen-icons/nucleo/palette.svg)
* skin/classic/browser/zen-icons/passwords.svg (../shared/zen-icons/nucleo/passwords.svg)
* skin/classic/browser/zen-icons/permissions-fill.svg (../shared/zen-icons/nucleo/permissions-fill.svg)
@@ -239,13 +270,19 @@
* skin/classic/browser/zen-icons/sidebar-right.svg (../shared/zen-icons/nucleo/sidebar-right.svg)
* skin/classic/browser/zen-icons/sidebar.svg (../shared/zen-icons/nucleo/sidebar.svg)
* skin/classic/browser/zen-icons/sidebars-right.svg (../shared/zen-icons/nucleo/sidebars-right.svg)
* skin/classic/browser/zen-icons/sliders.svg (../shared/zen-icons/nucleo/sliders.svg)
* skin/classic/browser/zen-icons/sparkles.svg (../shared/zen-icons/nucleo/sparkles.svg)
* skin/classic/browser/zen-icons/spell-check.svg (../shared/zen-icons/nucleo/spell-check.svg)
* skin/classic/browser/zen-icons/split.svg (../shared/zen-icons/nucleo/split.svg)
* skin/classic/browser/zen-icons/square-wand-sparkle.svg (../shared/zen-icons/nucleo/square-wand-sparkle.svg)
* skin/classic/browser/zen-icons/tab-audio-blocked-small.svg (../shared/zen-icons/nucleo/tab-audio-blocked-small.svg)
* skin/classic/browser/zen-icons/tab-audio-muted-small.svg (../shared/zen-icons/nucleo/tab-audio-muted-small.svg)
* skin/classic/browser/zen-icons/tab-audio-playing-small.svg (../shared/zen-icons/nucleo/tab-audio-playing-small.svg)
* skin/classic/browser/zen-icons/tab.svg (../shared/zen-icons/nucleo/tab.svg)
* skin/classic/browser/zen-icons/text-lowercase.svg (../shared/zen-icons/nucleo/text-lowercase.svg)
* skin/classic/browser/zen-icons/text-size.svg (../shared/zen-icons/nucleo/text-size.svg)
* skin/classic/browser/zen-icons/text-title-case.svg (../shared/zen-icons/nucleo/text-title-case.svg)
* skin/classic/browser/zen-icons/text-uppercase.svg (../shared/zen-icons/nucleo/text-uppercase.svg)
* skin/classic/browser/zen-icons/tool-profiler.svg (../shared/zen-icons/nucleo/tool-profiler.svg)
* skin/classic/browser/zen-icons/tracking-protection-fill.svg (../shared/zen-icons/nucleo/tracking-protection-fill.svg)
* skin/classic/browser/zen-icons/tracking-protection.svg (../shared/zen-icons/nucleo/tracking-protection.svg)
@@ -254,6 +291,7 @@
* skin/classic/browser/zen-icons/unpin.svg (../shared/zen-icons/nucleo/unpin.svg)
* skin/classic/browser/zen-icons/video-blocked-fill.svg (../shared/zen-icons/nucleo/video-blocked-fill.svg)
* skin/classic/browser/zen-icons/video-fill.svg (../shared/zen-icons/nucleo/video-fill.svg)
* skin/classic/browser/zen-icons/wand-sparkle.svg (../shared/zen-icons/nucleo/wand-sparkle.svg)
* skin/classic/browser/zen-icons/window.svg (../shared/zen-icons/nucleo/window.svg)
* skin/classic/browser/zen-icons/xr-blocked.svg (../shared/zen-icons/nucleo/xr-blocked.svg)
* skin/classic/browser/zen-icons/xr-fill.svg (../shared/zen-icons/nucleo/xr-fill.svg)
@@ -265,20 +303,27 @@
* skin/classic/browser/zen-icons/arrow-down.svg (../shared/zen-icons/nucleo/arrow-down.svg)
* skin/classic/browser/zen-icons/arrow-left.svg (../shared/zen-icons/nucleo/arrow-left.svg)
* skin/classic/browser/zen-icons/arrow-right.svg (../shared/zen-icons/nucleo/arrow-right.svg)
* skin/classic/browser/zen-icons/arrow-rotate-anticlockwise.svg (../shared/zen-icons/nucleo/arrow-rotate-anticlockwise.svg)
* skin/classic/browser/zen-icons/arrow-up.svg (../shared/zen-icons/nucleo/arrow-up.svg)
* skin/classic/browser/zen-icons/autoplay-media-blocked.svg (../shared/zen-icons/nucleo/autoplay-media-blocked.svg)
* skin/classic/browser/zen-icons/autoplay-media-fill.svg (../shared/zen-icons/nucleo/autoplay-media-fill.svg)
* skin/classic/browser/zen-icons/autoplay-media.svg (../shared/zen-icons/nucleo/autoplay-media.svg)
* skin/classic/browser/zen-icons/back.svg (../shared/zen-icons/nucleo/back.svg)
* skin/classic/browser/zen-icons/block.svg (../shared/zen-icons/nucleo/block.svg)
* skin/classic/browser/zen-icons/bolt.svg (../shared/zen-icons/nucleo/bolt.svg)
* skin/classic/browser/zen-icons/bookmark-hollow.svg (../shared/zen-icons/nucleo/bookmark-hollow.svg)
* skin/classic/browser/zen-icons/bookmark-star-on-tray.svg (../shared/zen-icons/nucleo/bookmark-star-on-tray.svg)
* skin/classic/browser/zen-icons/bookmark.svg (../shared/zen-icons/nucleo/bookmark.svg)
* skin/classic/browser/zen-icons/boost.svg (../shared/zen-icons/nucleo/boost.svg)
* skin/classic/browser/zen-icons/blocked-element.svg (../shared/zen-icons/nucleo/blocked-element.svg)
* skin/classic/browser/zen-icons/brackets-curly.svg (../shared/zen-icons/nucleo/brackets-curly.svg)
* skin/classic/browser/zen-icons/camera-blocked.svg (../shared/zen-icons/nucleo/camera-blocked.svg)
* skin/classic/browser/zen-icons/camera-fill.svg (../shared/zen-icons/nucleo/camera-fill.svg)
* skin/classic/browser/zen-icons/camera.svg (../shared/zen-icons/nucleo/camera.svg)
* skin/classic/browser/zen-icons/canvas-blocked.svg (../shared/zen-icons/nucleo/canvas-blocked.svg)
* skin/classic/browser/zen-icons/canvas.svg (../shared/zen-icons/nucleo/canvas.svg)
* skin/classic/browser/zen-icons/chevron.svg (../shared/zen-icons/nucleo/chevron.svg)
* skin/classic/browser/zen-icons/close-filled-round.svg (../shared/zen-icons/nucleo/close-filled-round.svg)
* skin/classic/browser/zen-icons/close.svg (../shared/zen-icons/nucleo/close.svg)
* skin/classic/browser/zen-icons/container-tab.svg (../shared/zen-icons/nucleo/container-tab.svg)
* skin/classic/browser/zen-icons/cookies-fill.svg (../shared/zen-icons/nucleo/cookies-fill.svg)
@@ -302,6 +347,7 @@
* skin/classic/browser/zen-icons/extension-blocked.svg (../shared/zen-icons/nucleo/extension-blocked.svg)
* skin/classic/browser/zen-icons/extension-fill.svg (../shared/zen-icons/nucleo/extension-fill.svg)
* skin/classic/browser/zen-icons/extension.svg (../shared/zen-icons/nucleo/extension.svg)
* skin/classic/browser/zen-icons/eyedropper.svg (../shared/zen-icons/nucleo/eyedropper.svg)
* skin/classic/browser/zen-icons/face-sun.svg (../shared/zen-icons/nucleo/face-sun.svg)
* skin/classic/browser/zen-icons/folder.svg (../shared/zen-icons/nucleo/folder.svg)
* skin/classic/browser/zen-icons/forget.svg (../shared/zen-icons/nucleo/forget.svg)
@@ -311,12 +357,14 @@
* skin/classic/browser/zen-icons/geo-blocked.svg (../shared/zen-icons/nucleo/geo-blocked.svg)
* skin/classic/browser/zen-icons/geo-fill.svg (../shared/zen-icons/nucleo/geo-fill.svg)
* skin/classic/browser/zen-icons/geo.svg (../shared/zen-icons/nucleo/geo.svg)
* skin/classic/browser/zen-icons/hammer.svg (../shared/zen-icons/nucleo/hammer.svg)
* skin/classic/browser/zen-icons/heart-circle-fill.svg (../shared/zen-icons/nucleo/heart-circle-fill.svg)
* skin/classic/browser/zen-icons/help.svg (../shared/zen-icons/nucleo/help.svg)
* skin/classic/browser/zen-icons/history.svg (../shared/zen-icons/nucleo/history.svg)
* skin/classic/browser/zen-icons/home.svg (../shared/zen-icons/nucleo/home.svg)
* skin/classic/browser/zen-icons/info.svg (../shared/zen-icons/nucleo/info.svg)
* skin/classic/browser/zen-icons/library.svg (../shared/zen-icons/nucleo/library.svg)
* skin/classic/browser/zen-icons/lightbulb.svg (../shared/zen-icons/nucleo/lightbulb.svg)
* skin/classic/browser/zen-icons/link.svg (../shared/zen-icons/nucleo/link.svg)
* skin/classic/browser/zen-icons/mail.svg (../shared/zen-icons/nucleo/mail.svg)
* skin/classic/browser/zen-icons/manage.svg (../shared/zen-icons/nucleo/manage.svg)
@@ -336,6 +384,8 @@
* skin/classic/browser/zen-icons/new-tab-image.svg (../shared/zen-icons/nucleo/new-tab-image.svg)
* skin/classic/browser/zen-icons/open.svg (../shared/zen-icons/nucleo/open.svg)
* skin/classic/browser/zen-icons/page-portrait.svg (../shared/zen-icons/nucleo/page-portrait.svg)
* skin/classic/browser/zen-icons/paintbrush-fill.svg (../shared/zen-icons/nucleo/paintbrush-fill.svg)
* skin/classic/browser/zen-icons/paintbrush.svg (../shared/zen-icons/nucleo/paintbrush.svg)
* skin/classic/browser/zen-icons/palette.svg (../shared/zen-icons/nucleo/palette.svg)
* skin/classic/browser/zen-icons/passwords.svg (../shared/zen-icons/nucleo/passwords.svg)
* skin/classic/browser/zen-icons/permissions-fill.svg (../shared/zen-icons/nucleo/permissions-fill.svg)
@@ -368,13 +418,19 @@
* skin/classic/browser/zen-icons/sidebar-right.svg (../shared/zen-icons/nucleo/sidebar-right.svg)
* skin/classic/browser/zen-icons/sidebar.svg (../shared/zen-icons/nucleo/sidebar.svg)
* skin/classic/browser/zen-icons/sidebars-right.svg (../shared/zen-icons/nucleo/sidebars-right.svg)
* skin/classic/browser/zen-icons/sliders.svg (../shared/zen-icons/nucleo/sliders.svg)
* skin/classic/browser/zen-icons/sparkles.svg (../shared/zen-icons/nucleo/sparkles.svg)
* skin/classic/browser/zen-icons/spell-check.svg (../shared/zen-icons/nucleo/spell-check.svg)
* skin/classic/browser/zen-icons/split.svg (../shared/zen-icons/nucleo/split.svg)
* skin/classic/browser/zen-icons/square-wand-sparkle.svg (../shared/zen-icons/nucleo/square-wand-sparkle.svg)
* skin/classic/browser/zen-icons/tab-audio-blocked-small.svg (../shared/zen-icons/nucleo/tab-audio-blocked-small.svg)
* skin/classic/browser/zen-icons/tab-audio-muted-small.svg (../shared/zen-icons/nucleo/tab-audio-muted-small.svg)
* skin/classic/browser/zen-icons/tab-audio-playing-small.svg (../shared/zen-icons/nucleo/tab-audio-playing-small.svg)
* skin/classic/browser/zen-icons/tab.svg (../shared/zen-icons/nucleo/tab.svg)
* skin/classic/browser/zen-icons/text-lowercase.svg (../shared/zen-icons/nucleo/text-lowercase.svg)
* skin/classic/browser/zen-icons/text-size.svg (../shared/zen-icons/nucleo/text-size.svg)
* skin/classic/browser/zen-icons/text-title-case.svg (../shared/zen-icons/nucleo/text-title-case.svg)
* skin/classic/browser/zen-icons/text-uppercase.svg (../shared/zen-icons/nucleo/text-uppercase.svg)
* skin/classic/browser/zen-icons/tool-profiler.svg (../shared/zen-icons/nucleo/tool-profiler.svg)
* skin/classic/browser/zen-icons/tracking-protection-fill.svg (../shared/zen-icons/nucleo/tracking-protection-fill.svg)
* skin/classic/browser/zen-icons/tracking-protection.svg (../shared/zen-icons/nucleo/tracking-protection.svg)
@@ -383,6 +439,7 @@
* skin/classic/browser/zen-icons/unpin.svg (../shared/zen-icons/nucleo/unpin.svg)
* skin/classic/browser/zen-icons/video-blocked-fill.svg (../shared/zen-icons/nucleo/video-blocked-fill.svg)
* skin/classic/browser/zen-icons/video-fill.svg (../shared/zen-icons/nucleo/video-fill.svg)
* skin/classic/browser/zen-icons/wand-sparkle.svg (../shared/zen-icons/nucleo/wand-sparkle.svg)
* skin/classic/browser/zen-icons/window.svg (../shared/zen-icons/nucleo/window.svg)
* skin/classic/browser/zen-icons/xr-blocked.svg (../shared/zen-icons/nucleo/xr-blocked.svg)
* skin/classic/browser/zen-icons/xr-fill.svg (../shared/zen-icons/nucleo/xr-fill.svg)

View File

@@ -0,0 +1,31 @@
#filter dumbComments emptyLines substitution
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
x="0px"
y="0px"
width="18px"
height="18px"
viewBox="0 0 18 18"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
stroke-linecap="round"
stroke-width="1.5"
fill="none"
stroke="context-fill"
stroke-opacity="context-fill-opacity"
stroke-linejoin="round"
class="nc-icon-wrapper"
id="g1">
<path
id="polyline1"
d="M 1.88,3.305 2.288,6.25 5.232,5.843 M 3,13.071 c 1.304,1.919 3.505,3.179 6,3.179 4.004,0 7.25,-3.246 7.25,-7.25 0,-4.004 -3.246,-7.25 -7.25,-7.25 -3.031,0 -5.627,1.86 -6.71,4.5" />
</g>
</svg>

View File

@@ -0,0 +1,31 @@
#filter dumbComments emptyLines substitution
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
x="0px"
y="0px"
width="18px"
height="18px"
viewBox="0 0 18 18"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
stroke-linecap="round"
stroke-width="1.5"
fill="none"
stroke="context-fill"
stroke-opacity="context-fill-opacity"
stroke-linejoin="round"
class="nc-icon-wrapper"
id="g1">
<path
id="circle1"
d="M 16.25,9 A 7.25,7.25 0 0 1 9,16.25 7.25,7.25 0 0 1 1.75,9 7.25,7.25 0 0 1 9,1.75 7.25,7.25 0 0 1 16.25,9 Z M 14.127,3.8729999 3.8729999,14.127" />
</g>
</svg>

View File

@@ -0,0 +1,5 @@
#filter dumbComments emptyLines substitution
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<svg xmlns="http://www.w3.org/2000/svg" height="18" width="18" viewBox="0 0 18 18"><g stroke-linecap="round" stroke-width="1.5" fill="none" stroke="context-fill" stroke-opacity="context-fill-opacity" stroke-linejoin="round" class="nc-icon-wrapper"><polyline points="14 6.25 16.25 4 14 1.75" data-color="color-2"></polyline><path d="M6.596,2.75h-1.846c-1.105,0-2,.896-2,2V13.25c0,1.104,.895,2,2,2H13.25c1.105,0,2-.896,2-2v-4.846"></path><polyline points="11 6.25 8.75 4 11 1.75" data-color="color-2"></polyline></g></svg>

View File

@@ -0,0 +1,31 @@
#filter dumbComments emptyLines substitution
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
x="0px"
y="0px"
width="18px"
height="18px"
viewBox="0 0 18 18"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
stroke-linecap="round"
stroke-width="1.5"
fill="none"
stroke="context-fill"
stroke-opacity="context-fill-opacity"
stroke-linejoin="round"
class="nc-icon-wrapper"
id="g1">
<path
id="path1"
d="M 14.7505,7.25 H 9.49905 L 9.81065,1.9868 C 9.82325,1.7732 9.55085,1.6734 9.42255,1.8446 l -6.3729,8.5055 C 2.92615,10.5149 3.04376,10.75 3.24976,10.75 h 5.25139 l -0.3116,5.2632 c -0.0126,0.2136 0.2598,0.3134 0.3881,0.1422 L 14.9506,7.6499 C 15.0741,7.4851 14.9565,7.25 14.7505,7.25 Z" />
</g>
</svg>

View File

@@ -0,0 +1,28 @@
#filter dumbComments emptyLines substitution
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 14.7 16.72"
version="1.1"
id="svg3"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs3" />
<g
stroke-linecap="round"
stroke-width="1.5"
fill="none"
stroke="context-fill"
stroke-opacity="context-fill-opacity"
stroke-linejoin="round"
class="nc-icon-wrapper"
id="g3">
<path
id="circle3"
class="cls-1"
d="M 3.4,14.17 A 0.86000001,0.86000001 0 0 1 2.54,15.03 0.86000001,0.86000001 0 0 1 1.6799999,14.17 0.86000001,0.86000001 0 0 1 2.54,13.31 0.86000001,0.86000001 0 0 1 3.4,14.17 Z M 10.53,11.6 C 11.76,10.89 13.88,8.39 14.12,7.61 14.46,6.52 13.63,5.97 12.86,5.52 12.22,5.13 10.95,6.95 10.25,7.72 m -0.01,0 C 10.79,7.17 11.75,5.7 11.75,5.18 11.75,2.67 6.38,-0.99 6.8,1.16 7.09,2.63 5.72,5.24 5.17,6.11 M 5.08,6.35 C 4.76,6.03 4.24,6.02 3.91,6.33 L 3.22,6.98 C 2.88,7.3 2.87,7.85 3.2,8.18 l 1.69,1.69 -3.54,2.65 c -1.02,0.73 -1.15,2.2 -0.27,3.09 v 0 c 0.87,0.88 2.31,0.79 3.06,-0.18 l 2.71,-3.63 1.6,1.6 c 0.33,0.33 0.85,0.33 1.18,0 l 0.64,-0.63 c 0.33,-0.33 0.33,-0.86 0,-1.19 L 5.06,6.33 Z" />
</g>
</svg>

View File

@@ -0,0 +1,31 @@
#filter dumbComments emptyLines substitution
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
x="0px"
y="0px"
width="18px"
height="18px"
viewBox="0 0 18 18"
version="1.1"
id="svg2"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<g
stroke-linecap="round"
stroke-width="1.5"
fill="none"
stroke="context-fill"
stroke-opacity="context-fill-opacity"
stroke-linejoin="round"
class="nc-icon-wrapper"
id="g2">
<path
id="path2"
d="m 11.75,15.25 h 1 c 1.105,0 2,-0.895 2,-2 V 10.625 C 14.75,9.728 15.478,9 16.375,9 15.478,9 14.75,8.272 14.75,7.375 V 4.75 c 0,-1.105 -0.895,-2 -2,-2 h -1 m -5.5,12.5 h -1 c -1.105,0 -2,-0.895 -2,-2 V 10.625 C 3.25,9.728 2.522,9 1.625,9 2.522,9 3.25,8.272 3.25,7.375 V 4.75 c 0,-1.105 0.895,-2 2,-2 h 1" />
</g>
</svg>

View File

@@ -0,0 +1,28 @@
#filter dumbComments emptyLines substitution
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="20px"
viewBox="0 -960 960 960"
width="20px"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
stroke-linecap="round"
stroke-width="1.5"
fill="context-fill"
stroke="none"
stroke-linejoin="round"
class="nc-icon-wrapper"
id="g1">
<path
id="path1"
d="m 480,-424 116,116 q 11,11 28,11 17,0 28,-11 11,-11 11,-28 0,-17 -11,-28 L 536,-480 652,-596 q 11,-11 11,-28 0,-17 -11,-28 -11,-11 -28,-11 -17,0 -28,11 L 480,-536 364,-652 q -11,-11 -28,-11 -17,0 -28,11 -11,11 -11,28 0,17 11,28 l 116,116 -116,116 q -11,11 -11,28 0,17 11,28 11,11 28,11 17,0 28,-11 z m 0,344 Q 397,-80 324,-111.5 251,-143 197,-197 143,-251 111.5,-324 80,-397 80,-480 q 0,-83 31.5,-156 31.5,-73 85.5,-127 54,-54 127,-85.5 73,-31.5 156,-31.5 83,0 156,31.5 73,31.5 127,85.5 54,54 85.5,127 31.5,73 31.5,156 0,83 -31.5,156 -31.5,73 -85.5,127 -54,54 -127,85.5 Q 563,-80 480,-80 Z" />
</g>
</svg>

View File

@@ -0,0 +1,5 @@
#filter dumbComments emptyLines substitution
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" stroke="context-fill" stroke-opacity="context-fill-opacity"><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m9.235 5.735-6.107 6.107a2.142 2.142 0 1 0 3.03 3.03l6.107-6.107M3.128 14.872 1.75 16.25"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m12.265 8.765 2.857-2.857a2.142 2.142 0 1 0-3.03-3.03L9.235 5.735M7.75 4.25l6 6"/></svg>

View File

@@ -0,0 +1,5 @@
#filter dumbComments emptyLines substitution
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" stroke="context-fill" stroke-opacity="context-fill-opacity"><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m11.529 8.721-6.563 6.563a1.59 1.59 0 0 1-2.25 0h0a1.59 1.59 0 0 1 0-2.25l6.563-6.563"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m16.207 8.944-1.521 1.521a1 1 0 0 1-1.414 0l-7.53-7.531L6.676 2l4.191.493a1 1 0 0 1 .59.286l4.75 4.75a1 1 0 0 1 0 1.414Z"/></svg>

View File

@@ -0,0 +1,31 @@
#filter dumbComments emptyLines substitution
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
x="0px"
y="0px"
width="18px"
height="18px"
viewBox="0 0 18 18"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
stroke-linecap="round"
stroke-width="1.5"
fill="none"
stroke="context-fill"
stroke-opacity="context-fill-opacity"
stroke-linejoin="round"
class="nc-icon-wrapper"
id="g1">
<path
id="path1"
d="M 14,6.75 C 14,3.637 11.154,1.188 7.922,1.863 5.99,2.266 4.447,3.856 4.088,5.796 3.654,8.14 4.859,10.255 6.75,11.211 v 3.039 c 0,1.105 0.895,2 2,2 h 0.5 c 1.105,0 2,-0.895 2,-2 V 11.211 C 12.88,10.387 14,8.701 14,6.75 Z m -7.25,6.5 h 4.5" />
</g>
</svg>

View File

@@ -0,0 +1,27 @@
#filter dumbComments emptyLines substitution
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
x="0px"
y="0px"
width="16px"
height="16px"
viewBox="0 0 18 18"
version="1.1"
id="svg2"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<g
fill="context-fill"
fill-opacity="context-fill-opacity"
class="nc-icon-wrapper"
id="g2">
<path
id="path2"
d="M 8.97,9.291 C 8.301,8.62 7.416,8.25 6.476,8.25 H 6.47 C 5.534,8.252 4.653,8.622 3.99,9.293 c -0.952,0.962 -1.119,1.933 -1.252,2.712 -0.149,0.869 -0.247,1.442 -1.347,2.043 -0.266,0.145 -0.418,0.435 -0.387,0.736 0.032,0.301 0.241,0.554 0.53,0.641 1.058,0.318 2.185,0.58 3.306,0.58 1.438,0 2.867,-0.43 4.13,-1.724 1.371,-1.375 1.371,-3.614 0,-4.989 z m 7.348,-7.36 c -0.79,-0.789 -2.166,-0.79 -2.957,0 L 8.216,7.076 c 0.673,0.25 1.294,0.631 1.817,1.156 0.525,0.527 0.897,1.145 1.14,1.802 l 5.146,-5.146 c 0.815,-0.815 0.815,-2.142 0,-2.958 z" />
</g>
</svg>

View File

@@ -0,0 +1,36 @@
#filter dumbComments emptyLines substitution
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
x="0px"
y="0px"
width="16px"
height="16px"
viewBox="0 0 18 18"
version="1.1"
id="svg3"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs3" />
<g
stroke-linecap="round"
stroke-width="1.5"
fill="none"
stroke="context-fill"
stroke-opacity="context-fill-opacity"
stroke-linejoin="round"
class="nc-icon-wrapper"
id="g2">
<path
id="path2"
d="m 1.75,14.706 c 2.703,0.812 4.896,0.88 6.689,-0.955 C 9.52,12.666 9.52,10.906 8.439,9.82 7.358,8.734 5.613,8.718 4.523,9.82 2.75,11.612 4.298,13.314 1.75,14.706 Z M 6.956,9.044 13.534,2.466 c 0.621,-0.621 1.629,-0.621 2.25,0 v 0 c 0.621,0.621 0.621,1.629 0,2.25 l -6.578,6.578" />
</g>
<g
fill="context-fill"
fill-opacity="context-fill-opacity"
class="nc-icon-wrapper"
id="g3" />
</svg>

View File

@@ -0,0 +1,31 @@
#filter dumbComments emptyLines substitution
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
x="0px"
y="0px"
width="18px"
height="18px"
viewBox="0 0 18 18"
version="1.1"
id="svg8"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<g
stroke-linecap="round"
stroke-width="1.5"
fill="none"
stroke="context-fill"
stroke-opacity="context-fill-opacity"
stroke-linejoin="round"
class="nc-icon-wrapper"
id="g8">
<path
id="circle8"
d="M 8.5,14.25 A 1.75,1.75 0 0 1 6.75,16 1.75,1.75 0 0 1 5,14.25 1.75,1.75 0 0 1 6.75,12.5 1.75,1.75 0 0 1 8.5,14.25 Z m 0,-10.5 A 1.75,1.75 0 0 1 6.75,5.5 1.75,1.75 0 0 1 5,3.75 1.75,1.75 0 0 1 6.75,2 1.75,1.75 0 0 1 8.5,3.75 Z M 12.75,9 A 1.75,1.75 0 0 1 11,10.75 1.75,1.75 0 0 1 9.25,9 1.75,1.75 0 0 1 11,7.25 1.75,1.75 0 0 1 12.75,9 Z m 3.5,5.25 h -5 M 5,14.25 H 1.75 m 14.5,-10.5 h -5 M 5,3.75 H 1.75 M 1.75,9 H 9 m 6.25,0 h 1" />
</g>
</svg>

View File

@@ -0,0 +1,36 @@
#filter dumbComments emptyLines substitution
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
x="0px"
y="0px"
width="18px"
height="18px"
viewBox="0 0 18 18"
version="1.1"
id="svg7"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs7" />
<g
fill="context-fill"
fill-opacity="context-fill-opacity"
class="nc-icon-wrapper"
id="g3" />
<g
stroke-linecap="round"
stroke-width="1.5"
fill="none"
stroke="context-fill"
stroke-opacity="context-fill-opacity"
stroke-linejoin="round"
class="nc-icon-wrapper"
id="g7">
<path
id="path7"
d="m 6.25,9.75 2,2 m -1.74799,-9 H 4.75 c -1.105,0 -2,0.896 -2,2 V 9.00732 M 8.992,15.25 h 4.258 c 1.105,0 2,-0.896 2,-2 V 10.616 M 7.95696,8.04302 2.04396,13.956 c -0.39,0.39 -0.39,1.023 0,1.414 l 0.586,0.585 c 0.39,0.391 1.024,0.391 1.414,0 l 5.913,-5.912 c 0.39004,-0.39108 0.39004,-1.02402 0,-1.41502 l -0.586,-0.58496 c -0.39,-0.3911 -1.023,-0.3911 -1.414,0 z M 15.25,2.5 C 15.664,2.5 16,2.164 16,1.75 16,1.336 15.664,1 15.25,1 14.836,1 14.5,1.336 14.5,1.75 c 0,0.414 0.336,0.75 0.75,0.75 z M 16.659,5.98996 15.396,5.56894 14.975,4.30606 c -0.137,-0.408 -0.812,-0.408 -0.949,0 l -0.421,1.26288 -1.263,0.42102 c -0.204,0.068 -0.342,0.259 -0.342,0.474 0,0.215 0.138,0.406 0.342,0.474 l 1.263,0.42102 0.421,1.263 c 0.068,0.204 0.26,0.34198 0.475,0.34198 0.215,0 0.406,-0.13798 0.475,-0.34198 l 0.421,-1.263 1.263,-0.42102 c 0.204,-0.068 0.342,-0.259 0.342,-0.474 0,-0.215 -0.139,-0.406 -0.343,-0.474 z M 11.744,2.49201 10.798,2.17701 10.482,1.22999 c -0.102,-0.306002 -0.609,-0.306002 -0.711,0 L 9.45499,2.17701 8.509,2.49201 c -0.153,0.051 -0.25699,0.19402 -0.25699,0.35602 0,0.162 0.10399,0.30496 0.25699,0.35596 l 0.94599,0.315 0.31601,0.94702 c 0.051,0.153 0.19401,0.25598 0.355,0.25598 0.161,0 0.305,-0.10398 0.355,-0.25598 l 0.316,-0.94702 0.946,-0.315 C 11.896,3.15299 12,3.01003 12,2.84803 12,2.68603 11.897,2.54301 11.744,2.49201 Z" />
</g>
</svg>

View File

@@ -0,0 +1,31 @@
#filter dumbComments emptyLines substitution
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
x="0px"
y="0px"
width="18px"
height="18px"
viewBox="0 0 18 18"
version="1.1"
id="svg3"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs3" />
<g
stroke-linecap="round"
stroke-width="1.5"
fill="none"
stroke="context-fill"
stroke-opacity="context-fill-opacity"
stroke-linejoin="round"
class="nc-icon-wrapper"
id="g3">
<path
id="path3"
d="m 14.75,6.75 v 6.303 c 0,1.214 -0.984,2.197 -2.197,2.197 v 0 c -0.728,0 -1.373,-0.354 -1.773,-0.899 M 12.45,6.75 h 0.204999 C 13.81563,6.75 14.75,7.68437 14.75,8.845 v 0.81 c 0,1.16063 -0.93437,2.095 -2.095001,2.095 H 12.45 c -1.16063,0 -2.095,-0.93437 -2.095,-2.095 v -0.81 c 0,-1.16063 0.93437,-2.095 2.095,-2.095 z M 7.25,9.5 c 0,1.518 -1.232,2.75 -2.75,2.75 H 4.3386 C 3.5993,12.25 3,11.6507 3,10.9114 v 0 C 3,10.2678 3.458,9.7153 4.0904,9.596 L 6.3677,9.1961 C 6.9062,9.0945 7.2576,8.581 7.1689,8.0499 M 3.234,7.234 C 3.72,6.711 4.412,6.5 5.115,6.5 v 0 c 1.179,0 2.134,0.956 2.134,2.134 v 3.616" />
</g>
</svg>

View File

@@ -0,0 +1,31 @@
#filter dumbComments emptyLines substitution
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
x="0px"
y="0px"
width="18px"
height="18px"
viewBox="0 0 18 18"
version="1.1"
id="svg2"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<g
stroke-linecap="round"
stroke-width="1.5"
fill="none"
stroke="context-fill"
stroke-opacity="context-fill-opacity"
stroke-linejoin="round"
class="nc-icon-wrapper"
id="g2">
<path
id="line2"
d="M 1.38,11.75 H 5.7160001 M 6.346,13.25 3.616,6.75 H 3.48 l -2.73,6.5 m 9.49,-2.5 h 6.040001 M 17.25,13.25 13.565,3.75 h -0.61 l -3.685,9.5" />
</g>
</svg>

View File

@@ -0,0 +1,31 @@
#filter dumbComments emptyLines substitution
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
x="0px"
y="0px"
width="18px"
height="18px"
viewBox="0 0 18 18"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
stroke-linecap="round"
stroke-width="1.5"
fill="none"
stroke="context-fill"
stroke-opacity="context-fill-opacity"
stroke-linejoin="round"
class="nc-icon-wrapper"
id="g1">
<path
id="path1"
d="m 15.75,6.75 v 6.303 c 0,1.214 -0.984,2.197 -2.197,2.197 v 0 c -0.728,0 -1.373,-0.354 -1.773,-0.899 M 13.45,6.75 h 0.204999 C 14.81563,6.75 15.75,7.68437 15.75,8.845 v 0.81 c 0,1.16063 -0.93437,2.095 -2.095001,2.095 H 13.45 c -1.16063,0 -2.095,-0.93437 -2.095,-2.095 v -0.81 c 0,-1.16063 0.93437,-2.095 2.095,-2.095 z M 2.5899999,10.25 H 8.2279997 M 9.068,12.25 5.498,3.75 H 5.32 l -3.57,8.5" />
</g>
</svg>

View File

@@ -0,0 +1,31 @@
#filter dumbComments emptyLines substitution
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
x="0px"
y="0px"
width="18px"
height="18px"
viewBox="0 0 18 18"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
stroke-linecap="round"
stroke-width="1.5"
fill="none"
stroke="context-fill"
stroke-opacity="context-fill-opacity"
stroke-linejoin="round"
class="nc-icon-wrapper"
id="g1">
<path
id="path1"
d="M 16.53,6.048 C 16.201,5.615 15.361,4.75 13.905,4.75 c -2.136,0 -3.655,1.522 -3.655,4.304 0,2.444 1.492,4.196 3.439,4.196 1.125,0 3.266,-0.173 3.266,-3.958 H 13.797 M 1.84,11.25 H 7.4780002 M 8.318,13.25 4.748,4.75 H 4.57 L 1,13.25" />
</g>
</svg>

View File

@@ -0,0 +1,36 @@
#filter dumbComments emptyLines substitution
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
x="0px"
y="0px"
width="18px"
height="18px"
viewBox="0 0 18 18"
version="1.1"
id="svg5"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs5" />
<g
stroke-linecap="round"
stroke-width="1.5"
fill="none"
stroke="context-fill"
stroke-opacity="context-fill-opacity"
stroke-linejoin="round"
class="nc-icon-wrapper"
id="g2" />
<g
fill="context-fill"
fill-opacity="context-fill-opacity"
class="nc-icon-wrapper"
id="g5">
<path
id="path5"
d="M 9.25,2.5 C 9.664,2.5 10,2.16 10,1.75 10,1.34 9.664,1 9.25,1 8.836,1 8.5,1.34 8.5,1.75 8.5,2.16 8.836,2.5 9.25,2.5 Z m 7.408,9.49 -1.263,-0.42 -0.421,-1.2599 c -0.137,-0.41004 -0.812,-0.41004 -0.949,0 l -0.421,1.2599 -1.263,0.42 c -0.204,0.0701 -0.342,0.26 -0.342,0.47 0,0.2201 0.138,0.4099 0.342,0.4799 l 1.263,0.4201 0.421,1.26 c 0.068,0.21 0.26,0.34 0.475,0.34 0.215,0 0.406,-0.13 0.475,-0.34 l 0.421,-1.26 1.263,-0.4201 c 0.204,-0.0699 0.342,-0.2598 0.342,-0.4799 0,-0.21 -0.139,-0.3999 -0.343,-0.47 z M 7.24297,3.48999 6.29697,3.18006 5.98097,2.22998 c -0.102,-0.3099 -0.609,-0.3099 -0.711,0 L 4.95396,3.18006 4.00797,3.48999 c -0.153,0.05 -0.25699,0.19999 -0.25699,0.35999 0,0.16 0.10399,0.29997 0.25699,0.34997 L 4.95396,4.52014 5.26997,5.4701 c 0.051,0.15 0.194,0.25 0.355,0.25 0.161,0 0.305,-0.1 0.355,-0.25 l 0.316,-0.94996 0.946,-0.32019 c 0.153,-0.0499 0.25699,-0.18987 0.25699,-0.34997 0,-0.1601 -0.10299,-0.30989 -0.25599,-0.35999 z m 3.14403,1.87 2.25,2.25 M 12.5717,2.92528 2.91893,12.583 c -0.3899,0.39 -0.3903,1.0221 -0.0011,1.4127 l 1.08521,1.0892 c 0.391,0.39 1.02399,0.39 1.41499,0 L 15.0701,5.42687 c 0.3898,-0.3901 0.3903,-1.02212 0.0011,-1.41272 L 13.9874,2.92638 C 13.597,2.53458 12.9627,2.53408 12.5717,2.92528 Z" />
</g>
</svg>

View File

@@ -0,0 +1,31 @@
diff --git a/docshell/base/BrowsingContext.h b/docshell/base/BrowsingContext.h
index dc64d5c13628cb7393705ebf96110705b045471f..497c2e646e2bde4d7732fa28efe7496264d58fa0 100644
--- a/docshell/base/BrowsingContext.h
+++ b/docshell/base/BrowsingContext.h
@@ -263,6 +263,8 @@ struct EmbedderColorSchemes {
FIELD(HistoryEntryCount, uint32_t) \
FIELD(HasRestoreData, bool) \
FIELD(SessionStoreEpoch, uint32_t) \
+ FIELD(ZenBoostsData, nscolor) \
+ FIELD(IsZenBoostsInverted, bool) \
/* Whether we can execute scripts in this BrowsingContext. Has no effect \
* unless scripts are also allowed in the parent WindowContext. */ \
FIELD(AllowJavascript, bool) \
@@ -674,6 +676,8 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache {
bool FullscreenAllowed() const;
+ auto ZenBoostsData() const { return GetZenBoostsData(); }
+ auto IsZenBoostsInverted() const { return GetIsZenBoostsInverted(); }
float FullZoom() const { return GetFullZoom(); }
float TextZoom() const { return GetTextZoom(); }
@@ -1274,6 +1278,8 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache {
}
void DidSet(FieldIndex<IDX_SessionStoreEpoch>, uint32_t aOldValue);
+ void DidSet(FieldIndex<IDX_ZenBoostsData>, nscolor aOldValue);
+ void DidSet(FieldIndex<IDX_IsZenBoostsInverted>, bool aOldValue);
using CanSetResult = syncedcontext::CanSetResult;

View File

@@ -0,0 +1,14 @@
diff --git a/dom/chrome-webidl/BrowsingContext.webidl b/dom/chrome-webidl/BrowsingContext.webidl
index 3bb17157c1dba1ca75ad9cfee2c21fcafb2dd0ee..a43f8aa1e4c808418f1cc7ccc69ced12c9ebfef0 100644
--- a/dom/chrome-webidl/BrowsingContext.webidl
+++ b/dom/chrome-webidl/BrowsingContext.webidl
@@ -169,6 +169,9 @@ interface BrowsingContext {
[SetterThrows] attribute float textZoom;
+ [SetterThrows] attribute long zenBoostsData;
+ [SetterThrows] attribute boolean isZenBoostsInverted;
+
// Override the dots-per-CSS-pixel scaling factor in this BrowsingContext
// and all of its descendants. May only be set on the top BC, and should
// only be set from the parent process.

View File

@@ -0,0 +1,172 @@
diff --git a/gfx/webrender_bindings/WebRenderAPI.cpp b/gfx/webrender_bindings/WebRenderAPI.cpp
--- a/gfx/webrender_bindings/WebRenderAPI.cpp
+++ b/gfx/webrender_bindings/WebRenderAPI.cpp
@@ -298,11 +298,13 @@
panic_on_gl_error, picTileWidth, picTileHeight,
gfx::gfxVars::WebRenderRequiresHardwareDriver(),
StaticPrefs::gfx_webrender_low_quality_pinch_zoom_AtStartup(),
StaticPrefs::gfx_webrender_max_shared_surface_size_AtStartup(),
StaticPrefs::gfx_webrender_enable_subpixel_aa_AtStartup(),
- compositor->ShouldUseLayerCompositor())) {
+ compositor->ShouldUseLayerCompositor(),
+ StaticPrefs::
+ gfx_webrender_opaque_backdrop_fallback_AtStartup())) {
// wr_window_new puts a message into gfxCriticalNote if it returns
// false
MOZ_ASSERT(errorMessage);
error.AssignASCII(errorMessage);
wr_api_free_error_msg(errorMessage);
diff --git a/gfx/webrender_bindings/src/bindings.rs b/gfx/webrender_bindings/src/bindings.rs
--- a/gfx/webrender_bindings/src/bindings.rs
+++ b/gfx/webrender_bindings/src/bindings.rs
@@ -1998,10 +1998,11 @@
reject_software_rasterizer: bool,
low_quality_pinch_zoom: bool,
max_shared_surface_size: i32,
enable_subpixel_aa: bool,
use_layer_compositor: bool,
+ opaque_backdrop_fallback: bool,
) -> bool {
assert!(unsafe { is_in_render_thread() });
// Ensure the WR profiler callbacks are hooked up to the Gecko profiler.
set_profiler_hooks(Some(&PROFILER_HOOKS));
@@ -2164,10 +2165,11 @@
texture_cache_config,
reject_software_rasterizer,
low_quality_pinch_zoom,
max_shared_surface_size,
enable_dithering,
+ opaque_backdrop_fallback,
precise_linear_gradients,
..Default::default()
};
let window_size = DeviceIntSize::new(window_width, window_height);
diff --git a/gfx/wr/webrender/src/device/gl.rs b/gfx/wr/webrender/src/device/gl.rs
--- a/gfx/wr/webrender/src/device/gl.rs
+++ b/gfx/wr/webrender/src/device/gl.rs
@@ -3982,10 +3982,14 @@
pub fn disable_color_write(&self) {
self.gl.color_mask(false, false, false, false);
}
+ pub fn set_color_mask(&self, r: bool, g: bool, b: bool, a: bool) {
+ self.gl.color_mask(r, g, b, a);
+ }
+
pub fn set_blend(&mut self, enable: bool) {
if enable {
self.gl.enable(gl::BLEND);
} else {
self.gl.disable(gl::BLEND);
diff --git a/gfx/wr/webrender/src/renderer/init.rs b/gfx/wr/webrender/src/renderer/init.rs
--- a/gfx/wr/webrender/src/renderer/init.rs
+++ b/gfx/wr/webrender/src/renderer/init.rs
@@ -204,10 +204,12 @@
pub low_quality_pinch_zoom: bool,
pub max_shared_surface_size: i32,
/// If true, open a debug socket to listen for remote debugger.
/// Relies on `debugger` cargo feature being enabled.
pub enable_debugger: bool,
+ /// See explanation of `gfx.webrender.opaque-backdrop-fallback`.
+ pub opaque_backdrop_fallback: bool,
/// Use a more precise method for sampling gradients.
pub precise_linear_gradients: bool,
}
@@ -277,10 +279,11 @@
enable_instancing: true,
reject_software_rasterizer: false,
low_quality_pinch_zoom: false,
max_shared_surface_size: 2048,
enable_debugger: true,
+ opaque_backdrop_fallback: false,
precise_linear_gradients: false,
}
}
}
@@ -802,10 +805,11 @@
allocated_native_surfaces: FastHashSet::default(),
debug_overlay_state: DebugOverlayState::new(),
buffer_damage_tracker: BufferDamageTracker::default(),
max_primitive_instance_count,
enable_instancing: options.enable_instancing,
+ opaque_backdrop_fallback: options.opaque_backdrop_fallback,
consecutive_oom_frames: 0,
target_frame_publish_id: None,
pending_result_msg: None,
layer_compositor_frame_state_in_prev_frame: None,
external_composite_debug_items: Vec::new(),
diff --git a/gfx/wr/webrender/src/renderer/mod.rs b/gfx/wr/webrender/src/renderer/mod.rs
--- a/gfx/wr/webrender/src/renderer/mod.rs
+++ b/gfx/wr/webrender/src/renderer/mod.rs
@@ -867,10 +867,12 @@
buffer_damage_tracker: BufferDamageTracker,
max_primitive_instance_count: usize,
enable_instancing: bool,
+ opaque_backdrop_fallback: bool,
+
/// Count consecutive oom frames to detectif we are stuck unable to render
/// in a loop.
consecutive_oom_frames: u32,
/// update() defers processing of ResultMsg, if frame_publish_id of
@@ -2787,18 +2789,29 @@
let read_target = ReadTarget::from_texture(cache_texture);
// Should always be drawing to picture cache tiles or off-screen surface!
debug_assert!(!draw_target.is_default());
let device_to_framebuffer = Scale::new(1i32);
+ let dest_fb_rect = dest * device_to_framebuffer;
self.device.blit_render_target(
read_target,
src * device_to_framebuffer,
draw_target,
- dest * device_to_framebuffer,
+ dest_fb_rect,
TextureFilter::Linear,
);
+
+ if self.opaque_backdrop_fallback {
+ self.device.set_color_mask(false, false, false, true);
+ self.device.clear_target(
+ Some([0.0, 0.0, 0.0, 1.0]),
+ None,
+ Some(dest_fb_rect),
+ );
+ self.device.set_color_mask(true, true, true, true);
+ }
}
}
}
fn draw_picture_cache_target(
diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml
--- a/modules/libpref/init/StaticPrefList.yaml
+++ b/modules/libpref/init/StaticPrefList.yaml
@@ -8439,10 +8439,17 @@
#else
value: false
#endif
mirror: once
+# Make backdrop-filter treat its captured backdrop as if it had been
+# composited over an opaque-black background. (See bug 2036640)
+- name: gfx.webrender.opaque-backdrop-fallback
+ type: bool
+ value: true
+ mirror: once
+
# Disable wait of GPU execution completion
- name: gfx.webrender.wait-gpu-finished.disabled
type: bool
value: false
mirror: once

View File

@@ -1,7 +1,7 @@
diff --git a/browser/base/content/main-popupset.inc.xhtml b/browser/base/content/main-popupset.inc.xhtml
--- a/browser/base/content/main-popupset.inc.xhtml
+++ b/browser/base/content/main-popupset.inc.xhtml
@@ -196,10 +196,11 @@
@@ -192,10 +192,11 @@
<!-- Starting point for selection actions -->
<panel class="panel-no-padding"
id="selection-shortcut-action-panel"
@@ -13,7 +13,7 @@ diff --git a/browser/base/content/main-popupset.inc.xhtml b/browser/base/content
<html:moz-button id="ai-action-button"/>
</hbox>
</panel>
@@ -207,10 +208,11 @@
@@ -203,10 +204,11 @@
<!-- Shortcut options for Gen AI action -->
<panel class="panel-no-padding"
@@ -25,7 +25,7 @@ diff --git a/browser/base/content/main-popupset.inc.xhtml b/browser/base/content
</panel>
<html:template id="screenshotsPagePanelTemplate">
@@ -560,10 +562,11 @@
@@ -610,10 +612,11 @@
type="arrow"
orient="vertical"
noautofocus="true"
@@ -40,7 +40,7 @@ diff --git a/browser/base/content/main-popupset.inc.xhtml b/browser/base/content
diff --git a/browser/components/asrouter/modules/FeatureCallout.sys.mjs b/browser/components/asrouter/modules/FeatureCallout.sys.mjs
--- a/browser/components/asrouter/modules/FeatureCallout.sys.mjs
+++ b/browser/components/asrouter/modules/FeatureCallout.sys.mjs
@@ -1046,10 +1046,11 @@
@@ -1054,10 +1054,11 @@
noautofocus="true"
flip="slide"
type="arrow"
@@ -70,22 +70,22 @@ diff --git a/browser/components/customizableui/content/panelUI.inc.xhtml b/brows
diff --git a/dom/xul/XULPopupElement.cpp b/dom/xul/XULPopupElement.cpp
--- a/dom/xul/XULPopupElement.cpp
+++ b/dom/xul/XULPopupElement.cpp
@@ -82,10 +82,14 @@
@@ -80,10 +80,14 @@
}
void XULPopupElement::OpenPopupAtScreen(int32_t aXPos, int32_t aYPos,
bool aIsContextMenu,
Event* aTriggerEvent) {
nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
+ // TODO(cheff): At nsCocoaWindow::Show but we check for ShouldShowAsNSPopover
+ // to determine whether to use a native popover or not. This should sort of
+ // "replicate" that logic here, but it's a bit of a hacky way.
+ SetAttr(kNameSpaceID_None, nsGkAtoms::nonnativepopover, u"true"_ns, true);
nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
if (pm) {
pm->ShowPopupAtScreen(this, aXPos, aYPos, aIsContextMenu, aTriggerEvent);
}
}
@@ -94,10 +98,14 @@
@@ -93,10 +97,14 @@
int32_t aWidth, int32_t aHeight,
bool aIsContextMenu,
bool aAttributesOverride,
@@ -103,7 +103,7 @@ diff --git a/dom/xul/XULPopupElement.cpp b/dom/xul/XULPopupElement.cpp
diff --git a/layout/xul/nsMenuPopupFrame.h b/layout/xul/nsMenuPopupFrame.h
--- a/layout/xul/nsMenuPopupFrame.h
+++ b/layout/xul/nsMenuPopupFrame.h
@@ -530,18 +530,10 @@
@@ -528,18 +528,10 @@
// Move the popup to the position specified in its |left| and |top|
// attributes.
@@ -122,7 +122,7 @@ diff --git a/layout/xul/nsMenuPopupFrame.h b/layout/xul/nsMenuPopupFrame.h
public:
/**
* Return whether the popup direction should be RTL.
@@ -550,10 +542,18 @@
@@ -548,10 +540,18 @@
*
* Return whether the popup direction should be RTL.
*/
@@ -144,7 +144,7 @@ diff --git a/layout/xul/nsMenuPopupFrame.h b/layout/xul/nsMenuPopupFrame.h
diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml
--- a/modules/libpref/init/StaticPrefList.yaml
+++ b/modules/libpref/init/StaticPrefList.yaml
@@ -19477,10 +19477,19 @@
@@ -19672,10 +19672,19 @@
value: true
mirror: always
@@ -167,7 +167,7 @@ diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/Sta
diff --git a/toolkit/themes/shared/global-shared.css b/toolkit/themes/shared/global-shared.css
--- a/toolkit/themes/shared/global-shared.css
+++ b/toolkit/themes/shared/global-shared.css
@@ -85,10 +85,22 @@
@@ -72,10 +72,22 @@
--menuitem-border-radius: var(--arrowpanel-menuitem-border-radius);
--menuitem-padding: var(--arrowpanel-menuitem-padding);
--menuitem-margin: var(--arrowpanel-menuitem-margin);
@@ -251,7 +251,7 @@ diff --git a/widget/cocoa/nsCocoaWindow.h b/widget/cocoa/nsCocoaWindow.h
diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm
--- a/widget/cocoa/nsCocoaWindow.mm
+++ b/widget/cocoa/nsCocoaWindow.mm
@@ -3,10 +3,13 @@
@@ -4,10 +4,13 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsCocoaWindow.h"
@@ -265,7 +265,7 @@ diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm
#include "nsIDOMWindowUtils.h"
#include "nsILocalFileMac.h"
#include "CocoaCompositorWidget.h"
@@ -4973,10 +4976,15 @@
@@ -5031,10 +5034,15 @@
if (mWindowType == WindowType::Popup) {
SetPopupWindowLevel();
mWindow.backgroundColor = NSColor.clearColor;
@@ -281,7 +281,7 @@ diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm
// the active space. Does not work with multiple displays. See
// NeedsRecreateToReshow() for multi-display with multi-space workaround.
mWindow.collectionBehavior = mWindow.collectionBehavior |
@@ -5178,10 +5186,57 @@
@@ -5236,10 +5244,57 @@
NS_OBJC_END_TRY_IGNORE_BLOCK;
}
@@ -339,54 +339,58 @@ diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm
NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
if (!mWindow) {
@@ -5242,10 +5297,54 @@
@@ -5300,10 +5355,58 @@
mWindow.contentView.needsDisplay = YES;
if (!nativeParentWindow || mPopupLevel != PopupLevel::Parent) {
[mWindow orderFront:nil];
}
NS_OBJC_END_TRY_IGNORE_BLOCK;
+ if (ShouldShowAsNSPopover()) {
+ if (ShouldShowAsNSPopover() && nativeParentWindow) {
+ nsMenuPopupFrame* popupFrame = GetPopupFrame();
+ if (nativeParentWindow) {
+ NSRectEdge preferredEdge =
+ AlignmentPositionToNSRectEdge(popupFrame->GetAlignmentPosition());
+ nsRect anchorRectAppUnits = popupFrame->GetUntransformedAnchorRect();
+ nsPresContext* pc = popupFrame->PresContext();
+ int32_t appUnitsPerDevPixel = pc->AppUnitsPerDevPixel();
+ mozilla::DesktopToLayoutDeviceScale desktopToLayoutScale =
+ pc->DeviceContext()->GetDesktopToDeviceScale();
+ mozilla::DesktopIntRect popupAnchorRectScaled =
+ mozilla::DesktopIntRect::RoundOut(
+ mozilla::LayoutDeviceRect::FromAppUnits(anchorRectAppUnits,
+ appUnitsPerDevPixel) /
+ desktopToLayoutScale);
+ // Taking the now correctly scaled anchor rect and turning it into a
+ // gecko rect this accounts for the y-axis inversion that cocoa needs,
+ // as the origin is in the bottom left. This rect is in screen space
+ NSRect cocoaScreenRect =
+ nsCocoaUtils::GeckoRectToCocoaRect(popupAnchorRectScaled);
+ // We take the screen space rect and convert it to window space
+ // coordinates, as NSPopover requires the coordinates to be in view
+ // space and inside the view. If the coordinates are outside our view,
+ // the popover will fail silently
+ NSRect windowRect =
+ [nativeParentWindow convertRectFromScreen:cocoaScreenRect];
+ NSView* parentView = [nativeParentWindow contentView];
+ // We take the window space rect and convert it to view space for the
+ // specific parent view
+ NSRect positioningRect = [parentView convertRect:windowRect
+ fromView:nil];
+ BOOL shouldHideAnchor = NO;
+ auto& element = popupFrame->PopupElement();
+ if (element.GetBoolAttr(nsGkAtoms::hidepopovertail)) {
+ shouldHideAnchor = YES;
+ }
+ [(PopupWindow*)mWindow showPopoverRelativeToRect:positioningRect
+ ofView:parentView
+ preferredEdge:preferredEdge
+ hiddenAnchor:shouldHideAnchor];
+ SyncPopoverBounds([(PopupWindow*)mWindow popover], popupFrame);
+ NSRectEdge preferredEdge =
+ AlignmentPositionToNSRectEdge(popupFrame->GetAlignmentPosition());
+ nsRect anchorRectAppUnits = popupFrame->GetUntransformedAnchorRect();
+ nsPresContext* pc = popupFrame->PresContext();
+ int32_t appUnitsPerDevPixel = pc->AppUnitsPerDevPixel();
+ mozilla::DesktopToLayoutDeviceScale desktopToLayoutScale =
+ pc->DeviceContext()->GetDesktopToDeviceScale();
+ mozilla::DesktopIntRect popupAnchorRectScaled =
+ mozilla::DesktopIntRect::RoundOut(
+ mozilla::LayoutDeviceRect::FromAppUnits(anchorRectAppUnits,
+ appUnitsPerDevPixel) /
+ desktopToLayoutScale);
+ // Taking the now correctly scaled anchor rect and turning it into a
+ // gecko rect this accounts for the y-axis inversion that cocoa needs,
+ // as the origin is in the bottom left. This rect is in screen space
+ NSRect cocoaScreenRect =
+ nsCocoaUtils::GeckoRectToCocoaRect(popupAnchorRectScaled);
+ // We take the screen space rect and convert it to window space
+ // coordinates, as NSPopover requires the coordinates to be in view
+ // space and inside the view. If the coordinates are outside our view,
+ // the popover will fail silently
+ NSRect windowRect =
+ [nativeParentWindow convertRectFromScreen:cocoaScreenRect];
+ NSView* parentView = [nativeParentWindow contentView];
+ // We take the window space rect and convert it to view space for the
+ // specific parent view
+ NSRect positioningRect = [parentView convertRect:windowRect
+ fromView:nil];
+ BOOL shouldHideAnchor = NO;
+ auto& element = popupFrame->PopupElement();
+ if (element.GetBoolAttr(nsGkAtoms::hidepopovertail)) {
+ shouldHideAnchor = YES;
+ }
+ [(PopupWindow*)mWindow showPopoverRelativeToRect:positioningRect
+ ofView:parentView
+ preferredEdge:preferredEdge
+ hiddenAnchor:shouldHideAnchor];
+ SyncPopoverBounds([(PopupWindow*)mWindow popover], popupFrame);
+ if (mPopupLevel == PopupLevel::Parent) {
+ [nativeParentWindow addChildWindow:mWindow ordered:NSWindowAbove];
+ }
+
+ // Exit early here since the popover is now shown.
+ mWindow.isBeingShown = NO;
+ return;
+ }
// If our popup window is a non-native context menu, tell the OS (and
@@ -394,12 +398,13 @@ diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm
// close other programs' context menus when ours open.
if ([mWindow isKindOfClass:[PopupWindow class]] &&
[(PopupWindow*)mWindow isContextMenu]) {
@@ -5316,10 +5415,15 @@
@@ -5373,11 +5476,15 @@
// unhook it here before ordering it out. When you order out the child
// of a window it hides the parent window.
if (mWindowType == WindowType::Popup && nativeParentWindow) {
[nativeParentWindow removeChildWindow:mWindow];
}
-
+ // Handle NSPopover hiding or traditional window hiding
+ if ([mWindow isKindOfClass:[PopupWindow class]] &&
+ [(PopupWindow*)mWindow usePopover]) {
@@ -410,7 +415,7 @@ diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm
// other programs) that a menu has closed.
if ([mWindow isKindOfClass:[PopupWindow class]] &&
[(PopupWindow*)mWindow isContextMenu]) {
@@ -5366,10 +5470,28 @@
@@ -5424,10 +5531,28 @@
return false;
}
return nsIWidget::ShouldUseOffMainThreadCompositing();
@@ -439,12 +444,12 @@ diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm
return mWindow.isOpaque ? TransparencyMode::Opaque
: TransparencyMode::Transparent;
@@ -6328,10 +6450,20 @@
@@ -6378,10 +6503,19 @@
// We ignore aRepaint -- we have to call display:YES, otherwise the
// title bar doesn't immediately get repainted and is displayed in
// the wrong place, leading to a visual jump.
[mWindow setFrame:newFrame display:YES];
+ if (ShouldUseNSPopover() && [(PopupWindow*)mWindow usePopover]) {
+ [(PopupWindow*)mWindow updatePopoverContent];
+ // A popover won't resize by setting the frame
@@ -454,18 +459,18 @@ diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm
+ [[(PopupWindow*)mWindow popover] setContentSize:contentSize];
+ SyncPopoverBounds([(PopupWindow*)mWindow popover], GetPopupFrame());
+ }
+
NS_OBJC_END_TRY_IGNORE_BLOCK;
}
void nsCocoaWindow::Resize(const DesktopRect& aRect, bool aRepaint) {
DoResize(aRect.x, aRect.y, aRect.width, aRect.height, aRepaint, false);
@@ -8314,17 +8446,26 @@
@@ -8393,18 +8527,31 @@
backing:bufferingType
defer:deferCreation];
if (!self) {
return nil;
}
-
+ mPopover = nil;
+ mPopoverViewController = nil;
+ mUsePopover = NO;
@@ -477,6 +482,11 @@ diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm
}
+- (void)dealloc {
+ if (mPopover) {
+ ChildViewMouseTracker::OnDestroyWindow(
+ mPopover.contentViewController.view.window);
+ }
+
+ [mPopover release];
+ [mPopoverViewController release];
+ [super dealloc];
@@ -487,7 +497,7 @@ diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm
// Return 0 in order to match what the system does for sheet windows and
// _NSPopoverWindows.
- (CGFloat)_backdropBleedAmount {
@@ -8378,10 +8519,122 @@
@@ -8460,10 +8607,122 @@
- (void)setIsContextMenu:(BOOL)flag {
mIsContextMenu = flag;
@@ -613,7 +623,7 @@ diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm
diff --git a/widget/nsIWidget.h b/widget/nsIWidget.h
--- a/widget/nsIWidget.h
+++ b/widget/nsIWidget.h
@@ -843,10 +843,15 @@
@@ -829,10 +829,15 @@
virtual void SuppressAnimation(bool aSuppress) {}
/** Sets windows-specific mica backdrop on this widget. */
@@ -644,7 +654,7 @@ diff --git a/xpcom/ds/StaticAtoms.py b/xpcom/ds/StaticAtoms.py
Atom("highest", "highest"),
Atom("horizontal", "horizontal"),
Atom("hover", "hover"),
@@ -757,10 +758,11 @@
@@ -759,10 +760,11 @@
Atom("nohref", "nohref"),
Atom("noinitialselection", "noinitialselection"),
Atom("nomodule", "nomodule"),

View File

@@ -1,251 +0,0 @@
diff --git a/dom/chrome-webidl/IOUtils.webidl b/dom/chrome-webidl/IOUtils.webidl
--- a/dom/chrome-webidl/IOUtils.webidl
+++ b/dom/chrome-webidl/IOUtils.webidl
@@ -94,23 +94,23 @@
* otherwise rejects with a DOMException.
*/
[NewObject]
Promise<unsigned long long> writeUTF8(DOMString path, UTF8String string, optional WriteOptions options = {});
/**
- * Attempts to serialize |value| into a JSON string and encode it as into a
- * UTF-8 string, then safely write the result to a file at |path|. Works
- * exactly like |write|.
+ * Attempts to serialize |value| into a JSON string and encode it as a UTF-8
+ * string, then safely write the result to a file at |path|. Works exactly
+ * like |write|.
*
* @param path An absolute file path
* @param value The value to be serialized.
* @param options Options for writing the file. The "append" mode is not supported.
*
* @return Resolves with the number of bytes successfully written to the file,
* otherwise rejects with a DOMException.
*/
[NewObject]
- Promise<unsigned long long> writeJSON(DOMString path, any value, optional WriteOptions options = {});
+ Promise<WriteJSONResult> writeJSON(DOMString path, any value, optional WriteJSONOptions options = {});
/**
* Moves the file from |sourcePath| to |destPath|, creating necessary parents.
* If |destPath| is a directory, then the source file will be moved into the
* destination directory.
*
@@ -567,10 +567,39 @@
* If true, compress the data with LZ4-encoding before writing to the file.
*/
boolean compress = false;
};
+/**
+ * Options to be passed to the |IOUtils.writeJSON| method.
+ */
+dictionary WriteJSONOptions: WriteOptions {
+ /**
+ * An optional length hint that will be used to pre-allocate the buffer that
+ * will hold the stringified JSON.
+ *
+ * This is the *length* and not the size (i.e., it is the number of UTF-16
+ * codepoints and not the number of bytes).
+ */
+ unsigned long long lengthHint = 0;
+};
+
+/**
+ * Information about a WriteJSON operation.
+ */
+dictionary WriteJSONResult {
+ /**
+ * The number of bytes written.
+ */
+ required unsigned long long size;
+
+ /**
+ * The length of the stringified JSON (in UTF-16 codepoints).
+ */
+ required unsigned long long jsonLength;
+};
+
/**
* Options to be passed to the |IOUtils.move| method.
*/
dictionary MoveOptions {
/**
diff --git a/xpcom/ioutils/IOUtils.h b/xpcom/ioutils/IOUtils.h
--- a/xpcom/ioutils/IOUtils.h
+++ b/xpcom/ioutils/IOUtils.h
@@ -94,11 +94,11 @@
const nsACString& aString, const dom::WriteOptions& aOptions,
ErrorResult& aError);
static already_AddRefed<dom::Promise> WriteJSON(
dom::GlobalObject& aGlobal, const nsAString& aPath,
- JS::Handle<JS::Value> aValue, const dom::WriteOptions& aOptions,
+ JS::Handle<JS::Value> aValue, const dom::WriteJSONOptions& aOptions,
ErrorResult& aError);
static already_AddRefed<dom::Promise> Move(dom::GlobalObject& aGlobal,
const nsAString& aSourcePath,
const nsAString& aDestPath,
@@ -736,13 +736,16 @@
RefPtr<nsIFile> mBackupFile;
RefPtr<nsIFile> mTmpFile;
dom::WriteMode mMode;
bool mFlush = false;
bool mCompress = false;
+ size_t mLengthHint = 0;
static Result<InternalWriteOpts, IOUtils::IOError> FromBinding(
const dom::WriteOptions& aOptions);
+ static Result<InternalWriteOpts, IOUtils::IOError> FromBinding(
+ const dom::WriteJSONOptions& aOptions);
};
/**
* Re-implements the file compression and decompression utilities found
* in toolkit/components/lz4/lz4.js
diff --git a/xpcom/ioutils/IOUtils.cpp b/xpcom/ioutils/IOUtils.cpp
--- a/xpcom/ioutils/IOUtils.cpp
+++ b/xpcom/ioutils/IOUtils.cpp
@@ -589,15 +589,21 @@
return WriteSync(file, AsBytes(Span(str)), opts);
});
});
}
+static bool AppendJSON(const char16_t* aBuf, uint32_t aLen, void* aStr) {
+ nsAString* str = static_cast<nsAString*>(aStr);
+
+ return str->Append(aBuf, aLen, fallible);
+}
+
/* static */
already_AddRefed<Promise> IOUtils::WriteJSON(GlobalObject& aGlobal,
const nsAString& aPath,
JS::Handle<JS::Value> aValue,
- const WriteOptions& aOptions,
+ const WriteJSONOptions& aOptions,
ErrorResult& aError) {
return WithPromiseAndState(
aGlobal, aError, [&](Promise* promise, auto& state) {
nsCOMPtr<nsIFile> file = new nsLocalFile();
REJECT_IF_INIT_PATH_FAILED(file, aPath, promise,
@@ -623,14 +629,15 @@
file->HumanReadablePath().get()));
return;
}
JSContext* cx = aGlobal.Context();
- JS::Rooted<JS::Value> rootedValue(cx, aValue);
+ JS::Rooted<JS::Value> value(cx, aValue);
nsString string;
- if (!nsContentUtils::StringifyJSON(cx, aValue, string,
- UndefinedIsNullStringLiteral)) {
+ if (!JS_StringifyWithLengthHint(cx, &value, nullptr,
+ JS::NullHandleValue, AppendJSON,
+ &string, opts.mLengthHint)) {
JS::Rooted<JS::Value> exn(cx, JS::UndefinedValue());
if (JS_GetPendingException(cx, &exn)) {
JS_ClearPendingException(cx);
promise->MaybeReject(exn);
} else {
@@ -639,22 +646,29 @@
"Could not serialize object to JSON"_ns));
}
return;
}
- DispatchAndResolve<uint32_t>(
+ DispatchAndResolve<dom::WriteJSONResult>(
state->mEventQueue, promise,
[file = std::move(file), string = std::move(string),
- opts = std::move(opts)]() -> Result<uint32_t, IOError> {
+ opts = std::move(opts)]() -> Result<WriteJSONResult, IOError> {
nsAutoCString utf8Str;
if (!CopyUTF16toUTF8(string, utf8Str, fallible)) {
return Err(IOError(
NS_ERROR_OUT_OF_MEMORY,
"Failed to write to `%s': could not allocate buffer",
file->HumanReadablePath().get()));
}
- return WriteSync(file, AsBytes(Span(utf8Str)), opts);
+
+ uint32_t size =
+ MOZ_TRY(WriteSync(file, AsBytes(Span(utf8Str)), opts));
+
+ dom::WriteJSONResult result;
+ result.mSize = size;
+ result.mJsonLength = static_cast<uint32_t>(string.Length());
+ return result;
});
});
}
/* static */
@@ -2840,10 +2854,20 @@
opts.mCompress = aOptions.mCompress;
return opts;
}
+Result<IOUtils::InternalWriteOpts, IOUtils::IOError>
+IOUtils::InternalWriteOpts::FromBinding(const WriteJSONOptions& aOptions) {
+ InternalWriteOpts opts =
+ MOZ_TRY(FromBinding(static_cast<const WriteOptions&>(aOptions)));
+
+ opts.mLengthHint = aOptions.mLengthHint;
+
+ return opts;
+}
+
/* static */
Result<IOUtils::JsBuffer, IOUtils::IOError> IOUtils::JsBuffer::Create(
IOUtils::BufferKind aBufferKind, size_t aCapacity) {
JsBuffer buffer(aBufferKind, aCapacity);
if (aCapacity != 0 && !buffer.mBuffer) {
diff --git a/xpcom/ioutils/tests/test_ioutils_read_write_json.html b/xpcom/ioutils/tests/test_ioutils_read_write_json.html
--- a/xpcom/ioutils/tests/test_ioutils_read_write_json.html
+++ b/xpcom/ioutils/tests/test_ioutils_read_write_json.html
@@ -140,10 +140,43 @@
);
await cleanup(filename);
});
+ add_task(async function test_writeJSON_return() {
+ const filename = PathUtils.join(PathUtils.tempDir, "test_ioutils_writeJSON_return.tmp");
+
+ const obj = { emoji: "☕️ ⚧️ 😀 🖖🏿 🤠 🏳️‍🌈 🥠 🏴‍☠️ 🪐" };
+
+ const expectedJson = JSON.stringify(obj);
+ const size = new TextEncoder().encode(expectedJson).byteLength;
+
+ {
+ const result = await IOUtils.writeJSON(filename, obj, { lengthHint: 0 });
+
+ is(await IOUtils.readUTF8(filename), expectedJson, "should have written expected JSON");
+
+ is(typeof result, "object", "writeJSON returns an object");
+ ok(result !== null, "writeJSON returns non-null");
+
+ ok(Object.hasOwn(result, "size"), "result has size property");
+ ok(Object.hasOwn(result, "jsonLength"), "result has jsonLength property");
+
+ is(result.size, size, "Should have written the expected number of bytes");
+ is(result.jsonLength, expectedJson.length, "Should have written the expected number of UTF-16 codepoints");
+ }
+
+ {
+ const result = await IOUtils.writeJSON(filename, obj, { lengthHint: expectedJson.length, compress: true });
+
+ isnot(result.size, size, "Should have written a different number of bytes due to compression");
+ is(result.jsonLength, expectedJson.length, "Should have written the same number of UTF-16 codepoints");
+ }
+
+ await cleanup(filename);
+ });
+
add_task(async function test_append_json() {
const filename = PathUtils.join(PathUtils.tempDir, "test_ioutils_append_json.tmp");
await IOUtils.writeJSON(filename, OBJECT);

View File

@@ -1,279 +0,0 @@
diff --git a/browser/components/sessionstore/SessionFile.sys.mjs b/browser/components/sessionstore/SessionFile.sys.mjs
--- a/browser/components/sessionstore/SessionFile.sys.mjs
+++ b/browser/components/sessionstore/SessionFile.sys.mjs
@@ -503,10 +503,12 @@
if (isFinalWrite) {
Services.obs.notifyObservers(
null,
"sessionstore-final-state-write-complete"
);
+
+ lazy.SessionWriter.deinit();
}
});
},
async wipe() {
diff --git a/browser/components/sessionstore/SessionWriter.sys.mjs b/browser/components/sessionstore/SessionWriter.sys.mjs
--- a/browser/components/sessionstore/SessionWriter.sys.mjs
+++ b/browser/components/sessionstore/SessionWriter.sys.mjs
@@ -6,10 +6,12 @@
ChromeUtils.defineESModuleGetters(lazy, {
sessionStoreLogger: "resource:///modules/sessionstore/SessionLogger.sys.mjs",
});
+const BROWSER_PURGE_SESSION_HISTORY = "browser:purge-session-history";
+
/**
* We just started (we haven't written anything to disk yet) from
* `Paths.clean`. The backup directory may not exist.
*/
const STATE_CLEAN = "clean";
@@ -58,10 +60,14 @@
export const SessionWriter = {
init(origin, useOldExtension, paths, prefs = {}) {
return SessionWriterInternal.init(origin, useOldExtension, paths, prefs);
},
+ deinit() {
+ return SessionWriterInternal.deinit();
+ },
+
/**
* Write the contents of the session file.
*
* @param state - May get changed on shutdown.
*/
@@ -80,10 +86,17 @@
return await SessionWriterInternal.wipe();
} finally {
unlock();
}
},
+
+ /**
+ * *Test Only* Return the SessionWriter's length hint for writing JSON.
+ */
+ get _jsonLengthHint() {
+ return SessionWriterInternal.jsonLengthHint;
+ },
};
const SessionWriterInternal = {
// Path to the files used by the SessionWriter
Paths: null,
@@ -104,10 +117,19 @@
/**
* Number of old upgrade backups that are being kept
*/
maxUpgradeBackups: null,
+ /**
+ * The size of the last write with IOUtils.writeJSON.
+ *
+ * Because SessionWriter writes such a large object graph we will otherwise
+ * spend a large portion of `write()` doing memory allocations and memcpy
+ * when serializing the session file to disk.
+ */
+ jsonLengthHint: 0,
+
/**
* Initialize (or reinitialize) the writer.
*
* @param {string} origin Which of sessionstore.js or its backups
* was used. One of the `STATE_*` constants defined above.
@@ -136,13 +158,20 @@
this.Paths = paths;
this.maxUpgradeBackups = prefs.maxUpgradeBackups;
this.maxSerializeBack = prefs.maxSerializeBack;
this.maxSerializeForward = prefs.maxSerializeForward;
this.upgradeBackupNeeded = paths.nextUpgradeBackup != paths.upgradeBackup;
+
+ Services.obs.addObserver(this, BROWSER_PURGE_SESSION_HISTORY);
+
return { result: true };
},
+ deinit() {
+ Services.obs.removeObserver(this, BROWSER_PURGE_SESSION_HISTORY);
+ },
+
/**
* Write the session to disk.
* Write the session to disk, performing any necessary backup
* along the way.
*
@@ -208,36 +237,42 @@
// We are shutting down. At this stage, we know that
// $Paths.clean is either absent or corrupted. If it was
// originally present and valid, it has been moved to
// $Paths.cleanBackup a long time ago. We can therefore write
// with the guarantees that we erase no important data.
- await IOUtils.writeJSON(this.Paths.clean, state, {
+ const result = await IOUtils.writeJSON(this.Paths.clean, state, {
tmpPath: this.Paths.clean + ".tmp",
compress: true,
+ lengthHint: this.jsonLengthHint,
});
+ this.jsonLengthHint = result.jsonLength;
fileStat = await IOUtils.stat(this.Paths.clean);
} else if (this.state == STATE_RECOVERY) {
// At this stage, either $Paths.recovery was written >= 15
// seconds ago during this session or we have just started
// from $Paths.recovery left from the previous session. Either
// way, $Paths.recovery is good. We can move $Path.backup to
// $Path.recoveryBackup without erasing a good file with a bad
// file.
- await IOUtils.writeJSON(this.Paths.recovery, state, {
+ const result = await IOUtils.writeJSON(this.Paths.recovery, state, {
tmpPath: this.Paths.recovery + ".tmp",
backupFile: this.Paths.recoveryBackup,
compress: true,
+ lengthHint: this.jsonLengthHint,
});
+ this.jsonLengthHint = result.jsonLength;
fileStat = await IOUtils.stat(this.Paths.recovery);
} else {
// In other cases, either $Path.recovery is not necessary, or
// it doesn't exist or it has been corrupted. Regardless,
// don't backup $Path.recovery.
- await IOUtils.writeJSON(this.Paths.recovery, state, {
+ const result = await IOUtils.writeJSON(this.Paths.recovery, state, {
tmpPath: this.Paths.recovery + ".tmp",
compress: true,
+ lengthHint: this.jsonLengthHint,
});
+ this.jsonLengthHint = result.jsonLength;
fileStat = await IOUtils.stat(this.Paths.recovery);
}
telemetry.writeFileMs = Date.now() - startWriteMs;
telemetry.fileSizeBytes = fileStat.size;
@@ -420,6 +455,18 @@
if (exn) {
throw exn;
}
},
+
+ observe(_subject, topic, _data) {
+ switch (topic) {
+ case BROWSER_PURGE_SESSION_HISTORY:
+ this._onPurgeSessionHistory();
+ break;
+ }
+ },
+
+ _onPurgeSessionHistory() {
+ this.jsonLengthHint = 0;
+ },
};
diff --git a/browser/components/sessionstore/test/unit/test_write_json_length_hint.js b/browser/components/sessionstore/test/unit/test_write_json_length_hint.js
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/unit/test_write_json_length_hint.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { updateAppInfo } = ChromeUtils.importESModule(
+ "resource://testing-common/AppInfo.sys.mjs"
+);
+
+const profile = do_get_profile();
+
+updateAppInfo({
+ name: "SessionRestoreTest",
+ ID: "{230de50e-4cd1-11dc-8314-0800200c9a66}",
+ version: "1",
+ platformVersion: "",
+});
+
+const { SessionFile } = ChromeUtils.importESModule(
+ "resource:///modules/sessionstore/SessionFile.sys.mjs"
+);
+const { SessionWriter } = ChromeUtils.importESModule(
+ "resource:///modules/sessionstore/SessionWriter.sys.mjs"
+);
+
+add_setup(async function setup() {
+ const source = do_get_file("data/sessionstore_valid.js");
+ source.copyTo(profile, "sessionstore.js");
+
+ await writeCompressedFile(
+ SessionFile.Paths.clean.replace("jsonlz4", "js"),
+ SessionFile.Paths.clean
+ );
+
+ await SessionFile.read();
+});
+
+add_task(async function test_json_length_hint() {
+ await IOUtils.writeJSON(PathUtils.join(PathUtils.profileDir, "dingus"), {
+ gunk: true,
+ });
+
+ Assert.equal(
+ SessionWriter._jsonLengthHint,
+ 0,
+ "SessionWriter length hint starts at 0"
+ );
+
+ await SessionFile.write({});
+
+ const lengthHint = SessionWriter._jsonLengthHint;
+
+ Assert.equal(
+ SessionWriter._jsonLengthHint,
+ JSON.stringify({}).length,
+ "SessionWriter should cache length hint"
+ );
+
+ const contents = await IOUtils.readJSON(
+ PathUtils.join(do_get_cwd().path, "data", "sessionstore_complete.json")
+ );
+ await SessionFile.write(contents);
+
+ Assert.notEqual(
+ SessionWriter._jsonLengthHint,
+ lengthHint,
+ "SessionWriter length hint updated"
+ );
+
+ Assert.greater(
+ SessionWriter._jsonLengthHint,
+ lengthHint,
+ "SessionWriteLength hint is now larger"
+ );
+
+ Services.obs.notifyObservers(null, "browser:purge-session-history");
+
+ Assert.equal(
+ SessionWriter._jsonLengthHint,
+ 0,
+ "browser:purge-session-history notification cleans length hint"
+ );
+
+ await SessionFile.write(contents);
+
+ Assert.notEqual(
+ SessionWriter._jsonLengthHint,
+ lengthHint,
+ "SessionWriter length hint updated"
+ );
+});
diff --git a/browser/components/sessionstore/test/unit/xpcshell.toml b/browser/components/sessionstore/test/unit/xpcshell.toml
--- a/browser/components/sessionstore/test/unit/xpcshell.toml
+++ b/browser/components/sessionstore/test/unit/xpcshell.toml
@@ -39,5 +39,7 @@
skip-if = [
"condprof", # Bug 1769154
]
["test_startup_session_async.js"]
+
+["test_write_json_length_hint.js"]

View File

@@ -0,0 +1,280 @@
diff --git a/browser/components/sessionstore/SessionWriter.sys.mjs b/browser/components/sessionstore/SessionWriter.sys.mjs
--- a/browser/components/sessionstore/SessionWriter.sys.mjs
+++ b/browser/components/sessionstore/SessionWriter.sys.mjs
@@ -80,10 +80,14 @@
return await SessionWriterInternal.wipe();
} finally {
unlock();
}
},
+
+ get _jsonLengthHint() {
+ return SessionWriterInternal._lastJsonLength;
+ },
};
const SessionWriterInternal = {
// Path to the files used by the SessionWriter
Paths: null,
@@ -104,10 +108,14 @@
/**
* Number of old upgrade backups that are being kept
*/
maxUpgradeBackups: null,
+ // Estimated JSON string length from the previous write, used to pre-size
+ // the serialization buffer and avoid incremental reallocations.
+ _lastJsonLength: 0,
+
/**
* Initialize (or reinitialize) the writer.
*
* @param {string} origin Which of sessionstore.js or its backups
* was used. One of the `STATE_*` constants defined above.
@@ -201,48 +209,60 @@
}
}
let startWriteMs = Date.now();
let fileStat;
+ // Add 5% headroom to the hint so small growth between saves doesn't
+ // cause reallocs. The compressed-size-based estimate already has
+ // sufficient margin from the 4x multiplier.
+ let jsonLengthHint = Math.ceil(this._lastJsonLength * 1.05);
+
+ let uncompressedBytes;
if (options.isFinalWrite) {
// We are shutting down. At this stage, we know that
// $Paths.clean is either absent or corrupted. If it was
// originally present and valid, it has been moved to
// $Paths.cleanBackup a long time ago. We can therefore write
// with the guarantees that we erase no important data.
- await IOUtils.writeJSON(this.Paths.clean, state, {
+ uncompressedBytes = await IOUtils.writeJSON(this.Paths.clean, state, {
tmpPath: this.Paths.clean + ".tmp",
compress: true,
+ jsonLengthHint,
});
fileStat = await IOUtils.stat(this.Paths.clean);
} else if (this.state == STATE_RECOVERY) {
// At this stage, either $Paths.recovery was written >= 15
// seconds ago during this session or we have just started
// from $Paths.recovery left from the previous session. Either
// way, $Paths.recovery is good. We can move $Path.backup to
// $Path.recoveryBackup without erasing a good file with a bad
// file.
- await IOUtils.writeJSON(this.Paths.recovery, state, {
+ uncompressedBytes = await IOUtils.writeJSON(this.Paths.recovery, state, {
tmpPath: this.Paths.recovery + ".tmp",
backupFile: this.Paths.recoveryBackup,
compress: true,
+ jsonLengthHint,
});
fileStat = await IOUtils.stat(this.Paths.recovery);
} else {
// In other cases, either $Path.recovery is not necessary, or
// it doesn't exist or it has been corrupted. Regardless,
// don't backup $Path.recovery.
- await IOUtils.writeJSON(this.Paths.recovery, state, {
+ uncompressedBytes = await IOUtils.writeJSON(this.Paths.recovery, state, {
tmpPath: this.Paths.recovery + ".tmp",
compress: true,
+ jsonLengthHint,
});
fileStat = await IOUtils.stat(this.Paths.recovery);
}
telemetry.writeFileMs = Date.now() - startWriteMs;
telemetry.fileSizeBytes = fileStat.size;
+ // Use the actual pre-compression size from this write as the hint
+ // for the next write's buffer allocation.
+ this._lastJsonLength = uncompressedBytes;
lazy.sessionStoreLogger.debug(
`SessionWriter.write wrote ${telemetry.fileSizeBytes} bytes in ${telemetry.writeFileMs}ms`
);
} catch (ex) {
// Don't throw immediately
@@ -375,10 +395,11 @@
} catch (ex) {
exn = exn || ex;
}
this.state = STATE_EMPTY;
+ this._lastJsonLength = 0;
if (exn) {
throw exn;
}
return { result: true };
diff --git a/browser/components/sessionstore/test/unit/test_write_json_length_hint.js b/browser/components/sessionstore/test/unit/test_write_json_length_hint.js
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/unit/test_write_json_length_hint.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { SessionWriter } = ChromeUtils.importESModule(
+ "resource:///modules/sessionstore/SessionWriter.sys.mjs"
+);
+
+const profd = do_get_profile();
+const { SessionFile } = ChromeUtils.importESModule(
+ "resource:///modules/sessionstore/SessionFile.sys.mjs"
+);
+
+const { updateAppInfo } = ChromeUtils.importESModule(
+ "resource://testing-common/AppInfo.sys.mjs"
+);
+updateAppInfo({
+ name: "SessionRestoreTest",
+ ID: "{230de50e-4cd1-11dc-8314-0800200c9a66}",
+ version: "1",
+ platformVersion: "",
+});
+
+add_setup(async function () {
+ let source = do_get_file("data/sessionstore_valid.js");
+ source.copyTo(profd, "sessionstore.js");
+ await writeCompressedFile(
+ SessionFile.Paths.clean.replace("jsonlz4", "js"),
+ SessionFile.Paths.clean
+ );
+ await SessionFile.read();
+});
+
+add_task(async function test_length_hint_updates_after_write() {
+ Assert.equal(
+ SessionWriter._jsonLengthHint,
+ 0,
+ "Length hint starts at 0"
+ );
+
+ await SessionFile.write({});
+
+ let hintAfterSmall = SessionWriter._jsonLengthHint;
+ Assert.equal(
+ hintAfterSmall,
+ JSON.stringify({}).length,
+ "Hint matches the uncompressed JSON byte length"
+ );
+
+ let largerState = await IOUtils.readJSON(
+ PathUtils.join(do_get_cwd().path, "data", "sessionstore_complete.json")
+ );
+ await SessionFile.write(largerState);
+
+ Assert.greater(
+ SessionWriter._jsonLengthHint,
+ hintAfterSmall,
+ "Hint grows after writing a larger state"
+ );
+});
+
+add_task(async function test_length_hint_resets_on_wipe() {
+ await SessionFile.write({ windows: [{ tabs: [{ entries: [] }] }] });
+ Assert.greater(SessionWriter._jsonLengthHint, 0, "Hint is nonzero");
+
+ await SessionFile.wipe();
+ Assert.equal(
+ SessionWriter._jsonLengthHint,
+ 0,
+ "Hint resets to 0 after wipe"
+ );
+});
diff --git a/browser/components/sessionstore/test/unit/xpcshell.toml b/browser/components/sessionstore/test/unit/xpcshell.toml
--- a/browser/components/sessionstore/test/unit/xpcshell.toml
+++ b/browser/components/sessionstore/test/unit/xpcshell.toml
@@ -39,5 +39,10 @@
skip-if = [
"condprof", # Bug 1769154
]
["test_startup_session_async.js"]
+
+["test_write_json_length_hint.js"]
+support-files = [
+ "data/sessionstore_complete.json",
+]
diff --git a/dom/chrome-webidl/IOUtils.webidl b/dom/chrome-webidl/IOUtils.webidl
--- a/dom/chrome-webidl/IOUtils.webidl
+++ b/dom/chrome-webidl/IOUtils.webidl
@@ -101,12 +101,12 @@
*
* @param path An absolute file path
* @param value The value to be serialized.
* @param options Options for writing the file. The "append" mode is not supported.
*
- * @return Resolves with the number of bytes successfully written to the file,
- * otherwise rejects with a DOMException.
+ * @return Resolves with the pre-compression size of the serialized JSON in
+ * bytes (UTF-8), otherwise rejects with a DOMException.
*/
[NewObject]
Promise<unsigned long long> writeJSON(DOMString path, any value, optional WriteOptions options = {});
/**
* Moves the file from |sourcePath| to |destPath|, creating necessary parents.
@@ -564,10 +564,16 @@
boolean flush = false;
/**
* If true, compress the data with LZ4-encoding before writing to the file.
*/
boolean compress = false;
+ /**
+ * For |writeJSON|, a hint for the expected JSON string length in UTF-16 code
+ * units. When provided, the JSON serializer pre-allocates a buffer of this
+ * size to avoid incremental reallocations.
+ */
+ unsigned long long jsonLengthHint = 0;
};
/**
* Options to be passed to the |IOUtils.move| method.
*/
diff --git a/xpcom/ioutils/IOUtils.cpp b/xpcom/ioutils/IOUtils.cpp
--- a/xpcom/ioutils/IOUtils.cpp
+++ b/xpcom/ioutils/IOUtils.cpp
@@ -622,13 +622,22 @@
return;
}
JSContext* cx = aGlobal.Context();
JS::Rooted<JS::Value> rootedValue(cx, aValue);
+ size_t lengthHint = aOptions.mJsonLengthHint;
nsString string;
- if (!nsContentUtils::StringifyJSON(cx, aValue, string,
- UndefinedIsNullStringLiteral)) {
+ if (lengthHint) {
+ string.SetCapacity(lengthHint);
+ }
+ if (!JS_StringifyWithLengthHint(
+ cx, &rootedValue, nullptr, JS::NullHandleValue,
+ [](const char16_t* aBuf, uint32_t aLen, void* aData) -> bool {
+ return static_cast<nsAString*>(aData)->Append(aBuf, aLen,
+ fallible);
+ },
+ &string, lengthHint)) {
JS::Rooted<JS::Value> exn(cx, JS::UndefinedValue());
if (JS_GetPendingException(cx, &exn)) {
JS_ClearPendingException(cx);
promise->MaybeReject(exn);
} else {
@@ -648,11 +657,13 @@
return Err(IOError(
NS_ERROR_OUT_OF_MEMORY,
"Failed to write to `%s': could not allocate buffer",
file->HumanReadablePath().get()));
}
- return WriteSync(file, AsBytes(Span(utf8Str)), opts);
+ uint32_t uncompressedSize = utf8Str.Length();
+ MOZ_TRY(WriteSync(file, AsBytes(Span(utf8Str)), opts));
+ return uncompressedSize;
});
});
}
/* static */

View File

@@ -10,7 +10,19 @@
// Specifically trying to target FeatureCallout.sys.mjs's change.
// IMPORTANT: Make sure Feature callouts STILL use native popopvers when
// syncing from upstream, as this is a critical part of the patch.
"+ nonnativepopover=\"true\"": "+ "
"+ nonnativepopover=\"true\"": "+ ",
// Fix conflicts with upstream changes.
"--menuitem-border-radius: var(--panel-menuitem-border-radius)": "--menuitem-border-radius: var(--arrowpanel-menuitem-border-radius)",
"--menuitem-padding: var(--panel-menuitem-padding)": "--menuitem-padding: var(--arrowpanel-menuitem-padding)",
"--menuitem-margin: var(--panel-menuitem-margin)": "--menuitem-margin: var(--arrowpanel-menuitem-margin)",
" \n #include \"nsCocoaWindow.h\"\n \n #include \"nsISupportsPrimitives.h\"\n #include \"nsArrayUtils.h\"":
" * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n \n #include \"nsCocoaWindow.h\"\n \n #include \"nsISupportsPrimitives.h\"\n #include \"nsArrayUtils.h\"",
" #include \"nsISupportsPrimitives.h\"\n": "",
" Atom(\"nonnative\", \"nonnative\"),\n": "",
"Atom(\"noscript\", \"noscript\"),": "Atom(\"noscript\", \"noscript\"),\n Atom(\"noshade\", \"noshade\"),"
}
},
{
@@ -21,6 +33,12 @@
"\n\n": "\n // may want to figure out a more robust way to detect abandonment."
}
},
{
"type": "local",
// TODO: Convert into https://phabricator.services.mozilla.com/D298079
// once it gets accepted.
"path": "firefox/allow_backdrop_to_work_on_transparency.patch"
},
{
"type": "local",
"path": "firefox/no_liquid_glass_icon.patch"
@@ -44,8 +62,7 @@
"type": "phabricator",
"ids": [
"D247145",
"D247215",
"D247217"
"D298708"
],
"name": "Session store use size hint"
},

View File

@@ -0,0 +1,21 @@
diff --git a/gfx/layers/AnimationInfo.cpp b/gfx/layers/AnimationInfo.cpp
index dd67af463d383dd98da80ce44f41b395e4a9a7cf..9da3849630a80fea2b254659a218e5ed38b44bc0 100644
--- a/gfx/layers/AnimationInfo.cpp
+++ b/gfx/layers/AnimationInfo.cpp
@@ -16,6 +16,7 @@
#include "mozilla/MotionPathUtils.h"
#include "mozilla/PresShell.h"
#include "mozilla/ScrollContainerFrame.h"
+#include "mozilla/nsZenBoostsBackend.h"
#include "nsIContent.h"
#include "nsLayoutUtils.h"
#include "nsRefreshDriver.h"
@@ -344,7 +345,7 @@ static void SetAnimatable(NonCustomCSSPropertyId aProperty,
// resolve currentColor at this moment.
nscolor foreground =
aFrame->Style()->GetVisitedDependentColor(&nsStyleText::mColor);
- aAnimatable = aAnimationValue.GetColor(foreground);
+ aAnimatable = zen::nsZenBoostsBackend::FilterColorFromPresContext(aAnimationValue.GetColor(foreground), aFrame->PresContext());
break;
}
case eCSSProperty_opacity:

View File

@@ -0,0 +1,21 @@
diff --git a/layout/base/PresShell.cpp b/layout/base/PresShell.cpp
index dd4df91b14137c2aa1ffd978faf1f4c91e234f8a..99976e9416d8a5b814bbb3a5421899f450ceeee5 100644
--- a/layout/base/PresShell.cpp
+++ b/layout/base/PresShell.cpp
@@ -136,6 +136,7 @@
#include "mozilla/layers/WebRenderLayerManager.h"
#include "mozilla/layers/WebRenderUserData.h"
#include "mozilla/layout/ScrollAnchorContainer.h"
+#include "mozilla/nsZenBoostsBackend.h"
#include "nsAnimationManager.h"
#include "nsAutoLayoutPhase.h"
#include "nsCOMArray.h"
@@ -5546,7 +5547,7 @@ nscolor PresShell::GetDefaultBackgroundColorToDraw() const {
if (!mPresContext) {
return NS_RGB(255, 255, 255);
}
- return mPresContext->DefaultBackgroundColor();
+ return zen::nsZenBoostsBackend::FilterColorFromPresContext(mPresContext->DefaultBackgroundColor(), mPresContext);
}
void PresShell::UpdateCanvasBackground() {

View File

@@ -0,0 +1,33 @@
diff --git a/layout/generic/ViewportFrame.cpp b/layout/generic/ViewportFrame.cpp
index ea018fff9f549cd8e07b20f4c8073391a6fdf40d..acb9428cf2276ed04064484600024d9e2c7e2bb7 100644
--- a/layout/generic/ViewportFrame.cpp
+++ b/layout/generic/ViewportFrame.cpp
@@ -24,6 +24,7 @@
#include "nsLayoutUtils.h"
#include "nsPlaceholderFrame.h"
#include "nsSubDocumentFrame.h"
+#include "mozilla/nsZenBoostsBackend.h"
using namespace mozilla;
@@ -299,15 +300,20 @@ ViewportFrame::BuildDisplayListForViewTransitionsAndNACTopLayer(
}
}
+ bool isAnonContent = false;
if (dom::Element* container = doc->GetCustomContentContainer()) {
if (nsIFrame* frame = container->GetPrimaryFrame()) {
MOZ_ASSERT(frame->StyleDisplay()->mTopLayer != StyleTopLayer::None,
"ua.css should ensure this");
MOZ_ASSERT(frame->HasAnyStateBits(NS_FRAME_OUT_OF_FLOW));
+ isAnonContent = frame->ContentIsRootOfNativeAnonymousSubtree();
BuildDisplayListForTopLayerFrame(aBuilder, frame, &topLayerList);
}
}
+ if (auto zenBackend = zen::nsZenBoostsBackend::GetInstance()) {
+ zenBackend->mCurrentFrameIsAnonymousContent = isAnonContent;
+ }
return MaybeWrapTopLayerList(
aBuilder, uint16_t(TopLayerIndex::ViewTransitionsAndAnonymousContent),
topLayerList);

View File

@@ -0,0 +1,23 @@
diff --git a/layout/painting/nsDisplayList.cpp b/layout/painting/nsDisplayList.cpp
index c045de789d924d0fd652b81f4fef37cb0bc19112..ac3d3904626132257a1ffce4a8d7bd6b8c12728e 100644
--- a/layout/painting/nsDisplayList.cpp
+++ b/layout/painting/nsDisplayList.cpp
@@ -81,6 +81,7 @@
#include "mozilla/layers/WebRenderLayerManager.h"
#include "mozilla/layers/WebRenderMessages.h"
#include "mozilla/layers/WebRenderScrollData.h"
+#include "mozilla/nsZenBoostsBackend.h"
#include "nsCSSProps.h"
#include "nsCSSRendering.h"
#include "nsCSSRenderingGradients.h"
@@ -1252,6 +1253,10 @@ void nsDisplayListBuilder::EnterPresShell(const nsIFrame* aReferenceFrame,
docShell->GetWindowDraggingAllowed(&mWindowDraggingAllowed);
}
+ if (auto zenBackend = zen::nsZenBoostsBackend::GetInstance(); zenBackend && !mIsInChromePresContext) {
+ zenBackend->onPresShellEntered(pc->Document());
+ }
+
state->mTouchEventPrefEnabledDoc = dom::TouchEvent::PrefEnabled(docShell);
if (auto* vt = pc->Document()->GetActiveViewTransition()) {

View File

@@ -0,0 +1,44 @@
diff --git a/layout/style/StyleColor.cpp b/layout/style/StyleColor.cpp
index 95c7ae6abea5032bef0466e8d59d212374d7a4d0..8dbfbb846b786d51af288989163aacfae12e787c 100644
--- a/layout/style/StyleColor.cpp
+++ b/layout/style/StyleColor.cpp
@@ -8,6 +8,7 @@
#include "mozilla/dom/BindingDeclarations.h"
#include "nsIFrame.h"
#include "nsStyleStruct.h"
+#include "mozilla/nsZenBoostsBackend.h"
namespace mozilla {
@@ -21,6 +22,8 @@ bool StyleColor::MaybeTransparent() const {
template <>
StyleAbsoluteColor StyleColor::ResolveColor(
const StyleAbsoluteColor& aForegroundColor) const {
+ auto ResolveColorInner = [this,
+ &aForegroundColor]() -> StyleAbsoluteColor {
if (IsAbsolute()) {
return AsAbsolute();
}
@@ -30,6 +33,8 @@ StyleAbsoluteColor StyleColor::ResolveColor(
}
return Servo_ResolveColor(this, &aForegroundColor);
+ };
+ return zen::nsZenBoostsBackend::ResolveStyleColor(ResolveColorInner());
}
template <>
@@ -68,10 +73,11 @@ nscolor StyleAbsoluteColor::ToColor() const {
auto green = std::clamp(srgb.components._1, 0.0f, 1.0f);
auto blue = std::clamp(srgb.components._2, 0.0f, 1.0f);
- return NS_RGBA(nsStyleUtil::FloatToColorComponent(red),
+ return zen::nsZenBoostsBackend::ResolveStyleColor(
+ NS_RGBA(nsStyleUtil::FloatToColorComponent(red),
nsStyleUtil::FloatToColorComponent(green),
nsStyleUtil::FloatToColorComponent(blue),
- nsStyleUtil::FloatToColorComponent(srgb.alpha));
+ nsStyleUtil::FloatToColorComponent(srgb.alpha)));
}
} // namespace mozilla

View File

@@ -1,5 +1,5 @@
diff --git a/toolkit/components/prompts/content/commonDialog.css b/toolkit/components/prompts/content/commonDialog.css
index d811fb62d502cf6fc0bf8163f11e1d264dee9e82..9fd96c560342b3417299ee3c6afda30ddcec76c0 100644
index d811fb62d502cf6fc0bf8163f11e1d264dee9e82..79e44af5d6d37bdb756bc1737d279e8769abb50e 100644
--- a/toolkit/components/prompts/content/commonDialog.css
+++ b/toolkit/components/prompts/content/commonDialog.css
@@ -3,7 +3,8 @@
@@ -12,7 +12,15 @@ index d811fb62d502cf6fc0bf8163f11e1d264dee9e82..9fd96c560342b3417299ee3c6afda30d
}
dialog[insecureauth] {
@@ -91,11 +92,12 @@ dialog[insecureauth] {
@@ -49,7 +50,6 @@ dialog[insecureauth] {
.sizeDetermined #infoRow,
.sizeDetermined #infoContainer {
/* Allow stuff to shrink */
- min-height: 0;
}
.sizeDetermined #infoRow {
@@ -91,11 +91,12 @@ dialog[insecureauth] {
--grid-padding: 16px;
/* All the inner items should have 4px inline margin, leading to 1.16em spacing
* between the dialog and its contents, and 8px horizontal spacing between items. */

View File

@@ -1,10 +1,20 @@
diff --git a/toolkit/content/widgets/panel.js b/toolkit/content/widgets/panel.js
index dc3e34847f1b6dfd58f5e036fd7d714ef51c1380..8d8e370ca8549d8208669d4fb344fc8abb54fadd 100644
index dc3e34847f1b6dfd58f5e036fd7d714ef51c1380..e88ab308c5eba01ccf82a0d1b9477555fcb9a5de 100644
--- a/toolkit/content/widgets/panel.js
+++ b/toolkit/content/widgets/panel.js
@@ -137,6 +137,9 @@
@@ -136,7 +136,19 @@
let anchorRoot =
this.anchorNode.closest("toolbarbutton, .anchor-root") ||
this.anchorNode;
+ let toolbox = anchorRoot.closest("#navigator-toolbox");
+ if (toolbox) {
+ // Disable transitions for now, see gh-11667
+ toolbox.style.transition = "none";
+ toolbox.setAttribute("zen-compact-mode-active", "true");
+ anchorRoot.ownerGlobal.setTimeout(() => {
+ toolbox.style.transition = "";
+ }, 0);
+ }
anchorRoot.setAttribute("open", "true");
+ if (anchorRoot.closest("#urlbar") && window.gURLBar) {
+ gURLBar.setAttribute("has-popup-open", "true");
@@ -12,7 +22,7 @@ index dc3e34847f1b6dfd58f5e036fd7d714ef51c1380..8d8e370ca8549d8208669d4fb344fc8a
}
if (this.getAttribute("animate") != "false") {
@@ -183,6 +186,9 @@
@@ -183,6 +195,9 @@
this.anchorNode.closest("toolbarbutton, .anchor-root") ||
this.anchorNode;
anchorRoot.removeAttribute("open");

View File

@@ -1,5 +1,5 @@
diff --git a/toolkit/modules/JSONFile.sys.mjs b/toolkit/modules/JSONFile.sys.mjs
index 397991e4af8f49b6365d729fc11267b5c1113400..1955b7ff1d428e891f5ef066e7a4ac25aa5ec9b4 100644
index 397991e4af8f49b6365d729fc11267b5c1113400..9b1d6fd3850b239000a3c4d2a2d5799a0989f4e3 100644
--- a/toolkit/modules/JSONFile.sys.mjs
+++ b/toolkit/modules/JSONFile.sys.mjs
@@ -132,6 +132,7 @@ export function JSONFile(config) {
@@ -16,14 +16,14 @@ index 397991e4af8f49b6365d729fc11267b5c1113400..1955b7ff1d428e891f5ef066e7a4ac25
try {
- await IOUtils.writeJSON(
+ if (this._useSizeHints && this._lastSavedSize) {
+ this._options.lengthHint = this._lastSavedSize;
+ this._options.jsonLengthHint = Math.ceil(this._lastSavedSize * 1.05);
+ }
+ const result = await IOUtils.writeJSON(
this.path,
this._data,
Object.assign({ tmpPath: this.path + ".tmp" }, this._options)
);
+ this._lastSavedSize = this._useSizeHints ? result.jsonLength : null;
+ this._lastSavedSize = this._useSizeHints ? result : null;
} catch (ex) {
if (typeof this._data.toJSONSafe == "function") {
// If serialization fails, try fallback safe JSON converter.

View File

@@ -1,5 +1,5 @@
diff --git a/widget/SwipeTracker.cpp b/widget/SwipeTracker.cpp
index 887d06d3bd9cdaa934880e0ae7a11ec8b737fb61..e2bf27c0130701f1d50990b60a5ef76e93c5a6bf 100644
index 887d06d3bd9cdaa934880e0ae7a11ec8b737fb61..7225d4264c9ec71ef1e5e717a1a62c4ef0aff0b7 100644
--- a/widget/SwipeTracker.cpp
+++ b/widget/SwipeTracker.cpp
@@ -3,6 +3,7 @@
@@ -10,7 +10,17 @@ index 887d06d3bd9cdaa934880e0ae7a11ec8b737fb61..e2bf27c0130701f1d50990b60a5ef76e
#include "InputData.h"
#include "mozilla/FlushType.h"
@@ -90,7 +91,7 @@ bool SwipeTracker::ComputeSwipeSuccess() const {
@@ -67,6 +68,9 @@ double SwipeTracker::SwipeSuccessTargetValue() const {
}
double SwipeTracker::ClampToAllowedRange(double aGestureAmount) const {
+ if (StaticPrefs::zen_swipe_is_fast_swipe()) {
+ return aGestureAmount;
+ }
// gestureAmount needs to stay between -1 and 0 when swiping right and
// between 0 and 1 when swiping left.
double min =
@@ -90,7 +94,7 @@ bool SwipeTracker::ComputeSwipeSuccess() const {
return (mGestureAmount * targetValue +
mCurrentVelocity * targetValue *
@@ -19,3 +29,13 @@ index 887d06d3bd9cdaa934880e0ae7a11ec8b737fb61..e2bf27c0130701f1d50990b60a5ef76e
kSwipeSuccessThreshold;
}
@@ -141,7 +145,8 @@ nsEventStatus SwipeTracker::ProcessEvent(
// display the UI as if we were at the success threshold as that would
// give a false indication that navigation would happen.
if (!computedSwipeSuccess && (eventAmount >= kSwipeSuccessThreshold ||
- eventAmount <= -kSwipeSuccessThreshold)) {
+ eventAmount <= -kSwipeSuccessThreshold)
+ && !StaticPrefs::zen_swipe_is_fast_swipe()) {
eventAmount = 0.999 * kSwipeSuccessThreshold;
if (mGestureAmount < 0.f) {
eventAmount = -eventAmount;

View File

@@ -0,0 +1,27 @@
diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm
index 515177a3a97094142593a98fb1b3023acf1ccb87..fb6f932547f9523a240f95915b7440dcdfa16975 100644
--- a/widget/cocoa/nsCocoaWindow.mm
+++ b/widget/cocoa/nsCocoaWindow.mm
@@ -5240,7 +5240,7 @@ static unsigned int WindowMaskForBorderStyle(BorderStyle aBorderStyle) {
// calls to ...orderFront: in TRY blocks. See bmo bug 470864.
NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
mWindow.contentView.needsDisplay = YES;
- if (!nativeParentWindow || mPopupLevel != PopupLevel::Parent) {
+ if (!mZenShowLocked && (!nativeParentWindow || mPopupLevel != PopupLevel::Parent)) {
[mWindow orderFront:nil];
}
NS_OBJC_END_TRY_IGNORE_BLOCK;
@@ -5287,7 +5287,12 @@ static unsigned int WindowMaskForBorderStyle(BorderStyle aBorderStyle) {
// We don't want most alwaysontop / alert windows to pull focus when
// they're opened, as these tend to be for peripheral indicators and
// displays.
- if ((mAlwaysOnTop && mPiPType != PiPType::DocumentPiP) || mIsAlert) {
+ if (mZenShowLocked) {
+ // Zen: window-control service has this widget locked-hidden;
+ // skip the native order-front but let the rest of Show()'s
+ // bookkeeping run normally.
+ } else if ((mAlwaysOnTop && mPiPType != PiPType::DocumentPiP) ||
+ mIsAlert) {
[mWindow orderFront:nil];
} else {
[mWindow makeKeyAndOrderFront:nil];

View File

@@ -0,0 +1,18 @@
diff --git a/widget/gtk/nsWindow.cpp b/widget/gtk/nsWindow.cpp
index 89950bd72c77ed961e59faa2fadb8f21a78fd23a..8c74bfa08af3c16cc05e11ea34902cbcdde3c64a 100644
--- a/widget/gtk/nsWindow.cpp
+++ b/widget/gtk/nsWindow.cpp
@@ -1076,7 +1076,12 @@ void nsWindow::Show(bool aState) {
}
#endif
- NativeShow(aState);
+ // Zen: skip the actual GTK show when window-control has the widget
+ // locked-hidden; mIsShown is already set above so Mozilla's
+ // bookkeeping treats the widget as shown.
+ if (!(aState && mZenShowLocked)) {
+ NativeShow(aState);
+ }
RefreshWindowClass();
}

View File

@@ -0,0 +1,28 @@
diff --git a/widget/nsIWidget.h b/widget/nsIWidget.h
index c22e055b9254ed1c8943c232a8339c574563dffc..0a0cb7cad4e878f93c03078564dac051dde1d014 100644
--- a/widget/nsIWidget.h
+++ b/widget/nsIWidget.h
@@ -698,6 +698,14 @@ class nsIWidget : public nsSupportsWeakReference {
*/
virtual void Show(bool aState) = 0;
+ /**
+ * Zen: when set to true, Show(true) calls on this widget become
+ * no-ops until SetZenShowLocked(false) is called. Used to keep a
+ * window invisible while it's being set up.
+ */
+ void SetZenShowLocked(bool aLocked) { mZenShowLocked = aLocked; }
+ bool IsZenShowLocked() const { return mZenShowLocked; }
+
/**
* Whether or not a widget must be recreated after being hidden to show
* again properly.
@@ -2419,6 +2427,8 @@ class nsIWidget : public nsSupportsWeakReference {
mozilla::Maybe<FullscreenSavedState> mSavedBounds;
bool mUpdateCursor;
+ // Zen: when true, Show(true) is a no-op. See SetZenShowLocked().
+ bool mZenShowLocked = false;
bool mIMEHasFocus;
bool mIMEHasQuit;
// if the window is fully occluded (rendering may be paused in response)

View File

@@ -0,0 +1,59 @@
diff --git a/widget/windows/nsWindow.cpp b/widget/windows/nsWindow.cpp
index baed7d09e31291b26a1d0a1ddfd63b57f0ce67d0..9daa1140e9538427d002c6f8ff99cd68265249cc 100644
--- a/widget/windows/nsWindow.cpp
+++ b/widget/windows/nsWindow.cpp
@@ -1761,28 +1761,33 @@ void nsWindow::Show(bool aState) {
// cursor.
SetCursor(Cursor{eCursor_standard});
- switch (mFrameState->GetSizeMode()) {
- case nsSizeMode_Fullscreen:
- ::ShowWindow(mWnd, SW_SHOW);
- break;
- case nsSizeMode_Maximized:
- ::ShowWindow(mWnd, SW_SHOWMAXIMIZED);
- break;
- case nsSizeMode_Minimized:
- ::ShowWindow(mWnd, SW_SHOWMINIMIZED);
- break;
- default:
- if (CanTakeFocus() &&
- (!mAlwaysOnTop || mPiPType == PiPType::DocumentPiP)) {
- ::ShowWindow(mWnd, SW_SHOWNORMAL);
- } else {
- ::ShowWindow(mWnd, SW_SHOWNOACTIVATE);
- // Don't flicker the window if we're restoring session
- if (!sIsRestoringSession) {
- (void)GetAttention(2);
+ // Zen: skip the actual ShowWindow() call when window-control
+ // has the widget locked-hidden; mIsVisible is already set above
+ // so Mozilla's bookkeeping treats the widget as shown.
+ if (!mZenShowLocked) {
+ switch (mFrameState->GetSizeMode()) {
+ case nsSizeMode_Fullscreen:
+ ::ShowWindow(mWnd, SW_SHOW);
+ break;
+ case nsSizeMode_Maximized:
+ ::ShowWindow(mWnd, SW_SHOWMAXIMIZED);
+ break;
+ case nsSizeMode_Minimized:
+ ::ShowWindow(mWnd, SW_SHOWMINIMIZED);
+ break;
+ default:
+ if (CanTakeFocus() &&
+ (!mAlwaysOnTop || mPiPType == PiPType::DocumentPiP)) {
+ ::ShowWindow(mWnd, SW_SHOWNORMAL);
+ } else {
+ ::ShowWindow(mWnd, SW_SHOWNOACTIVATE);
+ // Don't flicker the window if we're restoring session
+ if (!sIsRestoringSession) {
+ (void)GetAttention(2);
+ }
}
- }
- break;
+ break;
+ }
}
if (!mHasBeenShown) {

View File

@@ -16,3 +16,5 @@ category app-startup nsBrowserGlue @mozilla.org/browser/browserglue;1 applicatio
#include common/Components.manifest
#include sessionstore/SessionComponents.manifest
#include live-folders/LiveFoldersComponents.manifest
#include kbs/KbsComponents.manifest
#include little-window/LittleWindowComponents.manifest

View File

@@ -0,0 +1,124 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = XPCOMUtils.declareLazy({
styleSheetService: {
service: "@mozilla.org/content/style-sheet-service;1",
iid: Ci.nsIStyleSheetService,
},
});
const AGENT_SHEET = Ci.nsIStyleSheetService.AGENT_SHEET;
export class nsZenBoostStyles {
#stylesCache = new Map();
/**
* Retrieves the CSS style string for a given boost configuration.
* Caches styles to optimize performance.
*
* @param {object} boostData - The boost configuration data.
* @param {string} domain - The domain associated with the boost.
* @returns {string} The generated CSS style string.
*/
getStyleForBoost(boostData, domain) {
if (this.#stylesCache.has(domain)) {
return this.#stylesCache.get(domain);
}
const rawStyle = this.#generateStyleString(boostData);
if (!rawStyle || rawStyle.trim() === "") {
return null;
}
const styleUri = this.#convertStyleToDataUri(rawStyle);
this.#cacheStyle(styleUri, domain);
return this.getStyleForBoost(boostData, domain);
}
invalidateStyleForDomain(domain) {
if (this.#stylesCache.has(domain)) {
const { uri } = this.#stylesCache.get(domain);
lazy.styleSheetService.unregisterSheet(uri, AGENT_SHEET);
this.#stylesCache.delete(domain);
}
}
/**
* Generates a CSS style string based on the boost configuration.
*
* @param {object} boostData - The boost configuration data.
* @returns {string} The generated CSS style string.
* @private
*/
#generateStyleString(boostData) {
let style = ``;
const fontFamily =
boostData.fontFamily != ""
? `font-family: ${boostData.fontFamily} !important;`
: ``;
const fontCase =
boostData.textCaseOverride != "none"
? `text-transform: ${boostData.textCaseOverride} !important;`
: ``;
let zapBlocks = "";
if (boostData.zapSelectors) {
for (const selector of boostData.zapSelectors) {
zapBlocks += `${selector}:not([zen-zap-unhide]){ display: none !important; }\n`;
}
if (zapBlocks != "") {
style += `/* Zen-Zaps */\n`;
style += `${zapBlocks}\n`;
}
}
if (fontCase != "" || fontFamily != "") {
style += `/* Text Format */\n`;
style += `body * {\n`;
style += `${fontFamily}\n`;
style += `${fontCase}\n`;
style += `}\n`;
}
if ((boostData.customCSS || "").trim() != "") {
style += `/* USER CSS */\n`;
style += `${boostData.customCSS || ""}\n`;
}
return style;
}
/**
* Converts a raw CSS style string into a data URI.
*
* @param {string} rawStyle - The raw CSS style string.
* @returns {string} The data URI representing the CSS style.
* @private
*/
#convertStyleToDataUri(rawStyle) {
const encodedStyle = encodeURIComponent(rawStyle);
return Services.io.newURI(`data:text/css;charset=utf-8,${encodedStyle}`);
}
/**
* Prefetches the style from the data URI and caches it.
*
* @param {string} styleUri - The data URI of the CSS style.
* @param {string} domain - The domain associated with the boost.
* @private
*/
#cacheStyle(styleUri, domain) {
this.#stylesCache.set(domain, {
uuid: Services.uuid.generateUUID().toString(),
uri: styleUri,
});
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,773 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { JSONFile } from "resource://gre/modules/JSONFile.sys.mjs";
import { nsZenBoostStyles } from "resource:///modules/zen/boosts/ZenBoostStyles.sys.mjs";
class nsZenBoostsManager {
registeredDomains = new Map(); // <domain, { boosts: <id, boostEntry>, activeBoostID: null }>
#stylesManager = new nsZenBoostStyles();
#saveFilename = "zen-boosts.jsonlz4";
#file = null;
constructor() {
this.#readBoostsFromStore(this.notify);
}
/**
* @returns {object} New domain entry with empty boost map and active boost id
*/
#createDomainEntry() {
return {
boostEntries: new Map(), // <id, boostEntry>
activeBoostId: null,
};
}
/**
* Will get or create a domain entry for the given domain
*
* @param {string} domain - The domain of which the data will be fetched
* @returns {object} The domain entry
*/
#getOrCreateDomainEntry(domain) {
if (!this.registeredDomains.has(domain)) {
this.registeredDomains.set(domain, this.#createDomainEntry());
}
return this.registeredDomains.get(domain);
}
/**
* Will get a domain entry for the given domain
*
* @param {string} domain - The domain of which the data will be fetched
* @returns {object|null} The domain entry
*/
#getDomainEntry(domain) {
if (!this.registeredDomains.has(domain)) {
return null;
}
return this.registeredDomains.get(domain);
}
/**
* Will delete the domain entry for a domain
*
* @param {string} domain - The given domain
*/
#deleteDomainEntry(domain) {
if (this.registeredDomains.has(domain)) {
this.registeredDomains.delete(domain);
}
}
/**
* Gets the active boost id for a given domain
*
* @param {string} domain - The target domain
* @returns {string | null} Will return the active boost id or null
*/
getActiveBoostId(domain) {
const domainEntry = this.#getDomainEntry(domain);
if (domainEntry) {
return domainEntry.activeBoostId;
}
return null;
}
/**
* Deletes a boost for the specified domain and persists the change to disk.
*
* @param {object} boost - The targeted boost.
*/
deleteBoost(boost) {
const { domain, id } = boost;
if (this.registeredDomains.has(domain)) {
let domainEntry = this.#getOrCreateDomainEntry(domain);
if (domainEntry.boostEntries.has(id)) {
domainEntry.boostEntries.delete(id);
}
if (domainEntry.activeBoostId == id) {
domainEntry.activeBoostId = null;
}
if (domainEntry.boostEntries.size === 0) {
this.#deleteDomainEntry(domain);
}
}
this.#stylesManager.invalidateStyleForDomain(domain);
this.notify(true);
this.#writeToDisk(this.registeredDomains);
}
/**
* @returns {object} Returns a new empty boost entry
*/
getEmptyBoostEntry() {
return {
boostData: {
boostName: "My Boost",
dotAngleDeg: 0,
dotPos: { x: null, y: null },
dotDistance: 0,
brightness: 0.5,
saturation: 0.5,
contrast: 0.75,
fontFamily: "",
enableColorBoost: false,
smartInvert: false,
// Choses theme based on Zen's workspace theme
autoTheme: false,
textCaseOverride: "none",
zapSelectors: [],
customCSS: "",
changeWasMade: false,
},
};
}
/**
* Will create a new boost
*
* @param {string} domain - The domain which will be affected by the boost
* @returns {object|null} The created boost with { id, domain, boostEntry: { boostData } } or null
*/
createNewBoost(domain) {
if (!domain) {
console.error("[ZenBoostsManager] Domain expected but got null.");
return null;
}
const id = crypto.randomUUID();
const boostEntry = this.getEmptyBoostEntry(domain);
const domainEntry = this.#getOrCreateDomainEntry(domain);
domainEntry.boostEntries.set(id, boostEntry);
const boost = { id, domain, boostEntry };
return boost;
}
/**
* Loads the boost configuration for the specified domain from storage.
*
* @param {string} domain - The domain for which to load the boost
* @returns {object[] | null} All boosts for the domain or null
*/
loadBoostsFromStore(domain) {
if (!domain) {
console.error("[ZenBoostsManager] Domain expected but got null.");
}
const boosts = [];
const domainEntry = this.#getDomainEntry(domain);
if (domainEntry) {
domainEntry.boostEntries.forEach((value, key) => {
const boost = { id: key, domain, boostEntry: value };
boosts.push(boost);
});
return boosts;
}
return null;
}
/**
* Loads the boost for the specified domain and id from storage.
* If no boost is present, a new one will be created.
*
* @param {string} domain - The domain of the boost
* @param {string} id - The id of the boost
* @returns {object} Returns the boost with { id, domain, boostEntry: { boostData } }
*/
loadBoostFromStore(domain, id) {
if (!domain) {
console.error("[ZenBoostsManager] Domain expected but got null.");
}
if (!id) {
console.error("[ZenBoostsManager] ID expected but got null.");
}
const domainEntry = this.#getOrCreateDomainEntry(domain);
if (domainEntry.boostEntries.has(id)) {
const boostEntry = domainEntry.boostEntries.get(id);
return { id, domain, boostEntry };
}
const boost = this.createNewBoost(domain);
return boost;
}
/**
* Loads the active boost for the specified domain from storage.
*
* @param {string} domain - The domain of the boost
* @returns {object | null} Returns the boost with { id, domain, boostEntry: { boostData } } or null
*/
loadActiveBoostFromStore(domain) {
if (!domain) {
console.error("[ZenBoostsManager] Domain expected but got null.");
}
const domainEntry = this.#getDomainEntry(domain);
if (domainEntry) {
if (domainEntry.boostEntries.size === 0) {
return this.createNewBoost(domain);
}
if (domainEntry.boostEntries.has(domainEntry.activeBoostId)) {
const boostEntry = domainEntry.boostEntries.get(
domainEntry.activeBoostId
);
return { id: domainEntry.activeBoostId, domain, boostEntry };
}
}
return null;
}
/**
* Adds the zap selector to the selectors list and updates the website.
*
* @param {string} selector - Selector which will hide the elements
* @param {string} domain - Domain of the target boost
*/
addZapSelectorToActive(selector, domain) {
const boost = this.loadActiveBoostFromStore(domain);
if (!boost) {
console.error("[ZenBoostsManager] Active boost is null");
return;
}
const { boostData } = boost.boostEntry;
if (!boostData.zapSelectors) {
boostData.zapSelectors = [];
}
if (!boostData.zapSelectors.includes(selector)) {
boostData.zapSelectors.push(selector);
}
this.updateBoost(boost);
}
/**
* Removes the zap selector to the selectors list and updates the website.
*
* @param {string} selector - Selector which will no longer hide the elements
* @param {string} domain - Domain of the target boost
*/
removeZapSelectorToActive(selector, domain) {
const boost = this.loadActiveBoostFromStore(domain);
if (!boost) {
console.error("[ZenBoostsManager] Active boost is null");
return;
}
const { boostData } = boost.boostEntry;
if (boostData.zapSelectors && boostData.zapSelectors.includes(selector)) {
const i = boostData.zapSelectors.indexOf(selector);
if (i !== -1) {
boostData.zapSelectors.splice(i, 1);
}
}
this.updateBoost(boost);
}
/**
* Makes the boost at the domain with the id active
*
* @param {string} domain The target domain
* @param {string} id The target boost id
*/
makeBoostActiveForDomain(domain, id) {
const domainEntry = this.#getDomainEntry(domain);
if (domainEntry) {
if (domainEntry.boostEntries.has(id)) {
domainEntry.activeBoostId = id;
}
}
Services.obs.notifyObservers(null, "zen-boosts-active-change", { id });
this.#stylesManager.invalidateStyleForDomain(domain);
this.notify();
}
/**
* Toggles the boost activeness at the domain with the id active
*
* @param {string} domain The target domain
* @param {string} id The target boost id
*/
toggleBoostActiveForDomain(domain, id) {
const domainEntry = this.#getDomainEntry(domain);
if (domainEntry) {
if (domainEntry.boostEntries.has(id)) {
if (domainEntry.activeBoostId === id) {
domainEntry.activeBoostId = null;
Services.obs.notifyObservers(null, "zen-boosts-active-change", {
id: null,
});
this.#stylesManager.invalidateStyleForDomain(domain);
this.notify(true);
} else {
domainEntry.activeBoostId = id;
Services.obs.notifyObservers(null, "zen-boosts-active-change", {
id,
});
this.#stylesManager.invalidateStyleForDomain(domain);
this.notify();
}
}
}
}
/**
* Clears all zap selectors from a boost
*
* @param {string} domain - Target boost domain
*/
clearZapSelectorsForActive(domain) {
const boost = this.loadActiveBoostFromStore(domain);
if (!boost) {
console.error("[ZenBoostsManager] Active boost is null");
return;
}
const { boostData } = boost.boostEntry;
boostData.zapSelectors = [];
this.updateBoost(boost);
}
/**
* Updates the boost for a domain in memory and notifies observers of the change.
*
* @param {object} boost - The target boost
*/
updateBoost(boost) {
const { domain, id, boostEntry } = boost;
const domainEntry = this.#getOrCreateDomainEntry(domain);
domainEntry.boostEntries.set(id, boostEntry);
this.#stylesManager.invalidateStyleForDomain(domain);
this.notify();
}
/**
* Notifies all observers that boost data has been updated.
* This triggers a 'zen-boosts-update' notification event.
*
* @param {boolean} unloadStyles - Whether to unload styles during the update.
*/
notify(unloadStyles = false) {
Services.obs.notifyObservers(null, "zen-boosts-update", { unloadStyles });
}
/**
* Saves a boost configuration to persistent storage and notifies observers.
*
* @param {object | null} boost - The boost data object to save. If null, only saves existing boosts.
*/
saveBoostToStore(boost) {
if (boost) {
this.updateBoost(boost);
}
this.#writeToDisk(this.registeredDomains);
this.notify();
}
/**
* Reads all boosts from persistent storage and updates the registered boosts map.
*
* @param {Function} done - Callback function to execute after reading is complete.
* @private
*/
#readBoostsFromStore(done) {
this.#readFromDisk().then(data => {
this.registeredDomains = data;
done();
});
}
/**
* Gets the file path where boost data is stored in the user's profile directory.
*
* @returns {string} The full path to the boost storage file.
* @private
*/
get #storePath() {
const profilePath = PathUtils.profileDir;
return PathUtils.join(profilePath, this.#saveFilename);
}
/**
* Gets the directory path where user css is stored in the user's profile directory.
*
* @returns {string} The full path to the boost userCSS directory.
* @private
*/
get #cssPath() {
const profilePath = PathUtils.profileDir;
return PathUtils.join(profilePath, "zen-boosts");
}
/**
* Reads boost data from disk, decompresses it, and converts it to a Map.
*
* @returns {Promise<Map>} A promise that resolves to a Map of domain to boost data.
* @private
*/
async #readFromDisk() {
this.#file = new JSONFile({
path: this.#storePath,
compression: "lz4",
});
await this.#file.load();
const raw = this.#file.data ?? {};
const map = new Map();
for (const [domain, entry] of Object.entries(raw)) {
const boostsMap = new Map();
for (const [id, boostEntry] of Object.entries(entry.boostEntries ?? {})) {
// Reuinite the user css with the boost data if any exists
const userCSS = await this.#readBoostCSS(id);
if (userCSS) {
boostEntry.boostData.customCSS = userCSS;
}
boostsMap.set(id, boostEntry);
}
map.set(domain, {
activeBoostId: entry.activeBoostId ?? null,
boostEntries: boostsMap,
});
}
return map;
}
/**
* Reads the user CSS of a boost with the given id from the dedicated folder.
* Returns null if the file doesn't exist.
*
* @param {string} id - The id of the boost
* @returns {string|null} Returns the user CSS or null
*/
async #readBoostCSS(id) {
const fileName = `${id}.css`;
const directoryPath = this.#cssPath;
const savePath = PathUtils.join(directoryPath, fileName);
await IOUtils.makeDirectory(directoryPath, { createAncestors: true });
if (await IOUtils.exists(savePath)) {
const css = await IOUtils.readUTF8(savePath);
return css;
}
return null;
}
/**
* Writes boost data to disk by converting the Map to JSON and compressing it.
*
* @param {Map} map - The Map of domain to boost data to write to disk.
* @private
*/
#writeToDisk(map) {
const obj = {};
for (const [domain, entry] of map) {
const boostsObj = {};
for (const [id, boostEntry] of entry.boostEntries) {
// Split the user css from the boost data
boostsObj[id] = structuredClone(boostEntry);
delete boostsObj[id].boostData.customCSS;
this.#writeBoostCSS(id, boostEntry.boostData.customCSS);
}
obj[domain] = {
activeBoostId: entry.activeBoostId ?? null,
boostEntries: boostsObj,
};
}
this.#file.data = obj;
this.#file.saveSoon();
}
/**
* Writes the user CSS of a boost with the given id to a dedicated folder.
*
* @param {string} id - The id of the boost
* @param {string} css - The user CSS
*/
async #writeBoostCSS(id, css) {
const fileName = `${id}.css`;
const directoryPath = this.#cssPath;
const savePath = PathUtils.join(directoryPath, fileName);
await IOUtils.makeDirectory(directoryPath, { createAncestors: true });
await IOUtils.writeUTF8(savePath, css);
}
/**
* Checks if any boost is registered and active for the specified domain.
*
* @param {string} domain - The domain to check for any registered and active boost.
* @returns {boolean} True if a boost exists for the domain and is active, false otherwise.
*/
registeredBoostForDomain(domain) {
const domainEntry = this.#getDomainEntry(domain);
if (domainEntry) {
return domainEntry.boostEntries.has(domainEntry.activeBoostId);
}
return false;
}
/**
* Determines if a boost can be created for the given URI.
* Only HTTP and HTTPS schemes are supported for boosting.
*
* @param {nsIURI} uri - The URI to check for boost eligibility.
* @returns {boolean} True if the URI scheme is http or https, false otherwise.
*/
canBoostSite(uri) {
if (!uri || !uri.schemeIs) {
return false;
}
return uri.schemeIs("http") || uri.schemeIs("https");
}
/**
* Gets from cache or creates and caches a new style sheet for the given boost data.
*
* @param {string} domain - The domain of the boosts.
* @returns {nsIStyleSheet} The style sheet corresponding to the boost data.
*/
getStyleSheetForBoost(domain) {
const boost = this.loadActiveBoostFromStore(domain);
if (!boost) {
return null;
}
const { boostData } = boost.boostEntry;
return this.#stylesManager.getStyleForBoost(boostData, domain);
}
/**
* Opens the boost editor in a new popup window.
*
* @param {Window} parentWindow - The parent browser window
* @param {Boost} boost - The boost which will be edited
* @param {nsIURI} domainUri - The boost which will be edited
* @returns {Window|null} The instanced editor window
*/
openBoostWindow(parentWindow, boost, domainUri) {
if (!this.canBoostSite(domainUri)) {
console.error(
"[ZenBoostsManager] Cannot open editor for boost with invalid domain."
);
return null;
}
const domain = boost.domain;
const { availLeft, availWidth } = parentWindow.screen;
const screenX = parentWindow.screenX;
const screenY = parentWindow.screenY;
const width = parentWindow.outerWidth;
const height = parentWindow.outerHeight;
const editorWidth = 185;
const editorHeight = 565;
const pad = 20;
let left = screenX + width + pad;
if (this.#areTabsOnRightSide()) {
left = screenX - (editorWidth + pad);
}
let top = screenY + height / 2 - editorHeight / 2;
if (left + editorWidth > availLeft + availWidth || left < availLeft) {
left = screenX + width - (editorWidth + pad);
if (this.#areTabsOnRightSide()) {
left = screenX + pad;
}
}
const editor = Services.ww.openWindow(
parentWindow,
"chrome://browser/content/zen-components/windows/zen-boost-editor.xhtml",
null,
`left=${left},top=${top},chrome,alwaysontop,resizable=no,minimizable=no,dependent,dialog=yes`,
null
);
// Close the editor if the tab is switched
parentWindow.gBrowser.tabContainer.addEventListener(
"TabSelect",
editor.close.bind(editor),
{
once: true,
}
);
const progressListener = {
onLocationChange: webProgress => {
if (webProgress.isTopLevel) {
editor.close();
parentWindow.gBrowser.removeTabsProgressListener(progressListener);
}
},
};
parentWindow.gBrowser.addProgressListener(progressListener);
// Give the domain
editor.domain = domain;
editor.openerWindow = parentWindow;
editor.focus();
// Make boost active
this.makeBoostActiveForDomain(domain, boost.id);
return editor;
}
/**
* Will spawn a file save dialog and export the selected boost
*
* @param {Window} parentWindow The window that will instance the file picker
* @param {object} boostData The data of the boost to be exported
* @returns {Promise<void>} Returns a promise which will be resolved after the export action is complete
*/
exportBoost(parentWindow, boostData) {
// From: firefox-main/browser/base/content/browser-commands.js:354
// https://searchfox.org/firefox-main/source/browser/base/content/browser-commands.js#355:~:text=try%20%7B-,const,fp%2Eopen%28fpCallback%29%3B
const nsIFilePicker = Ci.nsIFilePicker;
const fp = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
fp.init(
parentWindow.browsingContext,
`Exporting Boost ${boostData.boostName}...`,
nsIFilePicker.modeSave
);
// Sanitizing filename
// From: https://gist.github.com/barbietunnie/7bc6d48a424446c44ff4#:~:text=bytes%22%29%3B-,var,%7D
const illegalRe = /[\/\?<>\\:\*\|":]/g;
// eslint-disable-next-line no-control-regex
const controlRe = /[\x00-\x1f\x80-\x9f]/g;
const reservedRe = /^\.+$/;
const windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i;
let sanitized = boostData.boostName
.replace(illegalRe, "")
.replace(controlRe, "")
.replace(reservedRe, "")
.replace(windowsReservedRe, "");
// Replace if resulting filename is empty
if (!sanitized) {
sanitized = "New Boost";
}
fp.defaultString = sanitized;
fp.defaultExtension = "json";
fp.appendFilters(nsIFilePicker.filterAll);
return new Promise(resolve => {
fp.open(async result => {
if (result === nsIFilePicker.returnOK && fp.file) {
try {
const boostJSON = JSON.stringify(boostData);
await IOUtils.writeUTF8(fp.file.path, boostJSON);
resolve(true);
} catch (ex) {
console.error("Export failed:", ex);
resolve(false);
}
} else {
resolve(false);
}
});
});
}
/**
* Will spawn a file open dialog and import the selected boost
*
* @param {Window} parentWindow The window that will instance the file picker
* @returns {Promise<object | null>} Returns a promise with the boost data or null
*/
importBoost(parentWindow) {
const nsIFilePicker = Ci.nsIFilePicker;
const fp = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
fp.init(
parentWindow.browsingContext,
"Importing Boost from JSON",
nsIFilePicker.modeOpen
);
fp.appendFilters(nsIFilePicker.filterAll);
return new Promise(resolve => {
fp.open(async result => {
if (result === nsIFilePicker.returnOK && fp.file) {
try {
const fileContent = await IOUtils.readUTF8(fp.file.path);
resolve(JSON.parse(fileContent));
} catch (e) {
console.error("Import failed:", e);
resolve(null);
}
} else {
resolve(null);
}
});
});
}
/**
* Helper function to determine if tabs are on the right side.
* From: ZenDownloadAnimation.mjs
*/
#areTabsOnRightSide() {
return Services.prefs.getBoolPref("zen.tabs.vertical.right-side");
}
}
export const gZenBoostsManager = new nsZenBoostsManager();

View File

@@ -0,0 +1,787 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const lazy = {};
ChromeUtils.defineLazyGetter(lazy, "overlayLocalization", () => {
return new Localization(["browser/zen-boosts.ftl"], true);
});
export class SelectorComponent {
document = null;
window = null;
#initialized = false;
#content = null;
#currentState = null;
#selectedElement = null;
#lastOverElement = null;
#relatedValueIndex = 0;
static STATES = {
SELECTING: "selecting",
SELECTED: "selected",
};
#zenContentIDs = [];
#onSelect = null;
#localizationArray = [
{ id: "zen-select-this" },
{ id: "zen-select-related" },
{ id: "zen-select-cancel" },
];
safeAreaPadding = { left: 0, right: 0, top: 0, bottom: 0 };
/**
* @param {Document} document Webpage document
* @param {ZenBoostsChild} zenBoostsChild Boost JSActor child
* @param {string[]} additionalZenContentIDs Additional IDs that will be considered as non website content
* @param {Function<string>} onSelect Callback for when a selection was made. The parameter is the css selector
* @param {object[]} localizationArray An array of 3 { id: 'l10n-id' } fluent IDs for localization going from the inputs left to right
*/
constructor(
document,
zenBoostsChild,
additionalZenContentIDs,
onSelect,
localizationArray = null
) {
this.document = document;
this.window = document.ownerGlobal;
this.zenBoostsChild = zenBoostsChild;
this.#onSelect = onSelect;
if (localizationArray != null) {
this.#localizationArray = localizationArray;
}
const baseSelectorIDs = ["select-controls", "select-controls-container"];
this.#zenContentIDs = [...additionalZenContentIDs, ...baseSelectorIDs];
}
/**
* Initializes the zap mode and inserts anonymous content
*/
async initialize() {
if (this.#initialized) {
return;
}
this.#content = this.document.insertAnonymousContent();
this.#content.root.appendChild(this.fragment);
this.#initializeElements();
this.setState(SelectorComponent.STATES.SELECTING);
this.#initialized = true;
}
/**
* Initializes all anonymous content and events
*/
#initializeElements() {
this.hoverDiv = this.getElementById("hover-div");
this.selectorComponent = this.getElementById("select-component");
this.cancelButton = this.getElementById("select-cancel");
this.cancelButton.addEventListener("click", this.#cancelSelect.bind(this));
this.selectThisButton = this.getElementById("select-this");
this.selectThisButton.addEventListener(
"click",
this.#handleSelect.bind(this)
);
this.selectRelatedSlider = this.getElementById("select-related");
this.selectRelatedSlider.addEventListener(
"click",
this.#handleSelect.bind(this)
);
// Initialize the related elements button
this.selectRelatedSlider.addEventListener("mousemove", e => {
const r = e.currentTarget.getBoundingClientRect();
const mouseX = e.clientX;
let value = (mouseX - r.left) / r.width;
value = Math.max(0, Math.min(1, value));
e.target.style = `--related-elements-value: ${value * 100}%;`;
const lastIndex = this.#relatedValueIndex;
this.#relatedValueIndex = Math.round(value * 8);
if (lastIndex != this.#relatedValueIndex) {
this.updateHighlight();
this.#updatePathTextField();
}
});
this.selectRelatedSlider.addEventListener("mouseout", e => {
e.currentTarget.style.removeProperty("--related-elements-value");
this.#relatedValueIndex = 0;
this.updateHighlight();
this.#updatePathTextField();
});
}
/**
* Sets the state of the zap mode
*
* @param {STATES} newState New state
* @param {*} data Optional additional data
*/
setState(newState, data = null) {
this.#currentState = newState;
switch (newState) {
case SelectorComponent.STATES.SELECTED:
this.#selectedElement = data;
this.#relatedValueIndex = 0; // Reset index
this.#hideHoverDiv();
this.#showSelectorComponent();
this.updateHighlight();
this.#updatePathTextField();
break;
case SelectorComponent.STATES.SELECTING:
this.#selectedElement = null;
this.#showHoverDiv();
this.#hideSelectorComponent();
this.removeHighlight();
break;
}
}
get content() {
if (!this.#content || Cu.isDeadWrapper(this.#content)) {
return null;
}
return this.#content;
}
/**
* Helper for getting an anonymous element by id
*
* @param {string} id The id of the element
*/
getElementById(id) {
return this.content.root.getElementById(id);
}
get markup() {
// Fetch localizations
let [thisElement, relatedElements, cancelAction] =
lazy.overlayLocalization.formatMessagesSync(this.#localizationArray);
return `
<template>
<link rel="stylesheet" href="chrome://browser/content/zen-styles/content/zen-selector.css" />
<div id="select-component">
<div id="select-controls">
<input type="button" id="select-this" value="${thisElement.value}"/>
<input type="button" id="select-related" value="${relatedElements.value}"/>
<input type="button" id="select-cancel" value="${cancelAction.value}"/>
</div>
<div id="selector-preview">
<p id="selector-element-preview-text"></p>
</div>
</div>
<div id="hover-div"></div>
<div id="highlight-container"></div>
<div id="highlight-shadow" style="display:none;"></div>
</template>
`;
}
get fragment() {
if (!this.template) {
let parser = new DOMParser();
let doc = parser.parseFromString(this.markup, "text/html");
this.template = this.document.importNode(
doc.querySelector("template"),
true
);
}
let fragment = this.template.content.cloneNode(true);
return fragment;
}
/**
* Handles the addition of the current zap selector
*/
#handleSelect() {
const cssPath = this.getSelectionPath(
this.document,
this.#relatedValueIndex,
this.#selectedElement
);
this.removeHighlight();
this.#resetHoverDiv();
// The highlight should be gone before the onSelect is called
this.window.requestAnimationFrame(() => {
this.setState(SelectorComponent.STATES.SELECTING);
if (cssPath) {
this.#onSelect(cssPath);
}
});
}
/**
* Cancles the current selection operation
*/
#cancelSelect() {
this.setState(SelectorComponent.STATES.SELECTING);
}
/**
* Rebuilds the selection highlight
*/
updateHighlight() {
this.removeHighlight();
this.showHighlight(this.getSelection());
}
/**
* Highlights a selection of elements on the website
*
* @param {List} selection A list of the web elements that should be highlighted
*/
showHighlight(selection) {
const highlightContainerDiv = this.getElementById("highlight-container");
highlightContainerDiv.style.display = "initial";
let counter = 0;
for (const element of selection) {
if (counter >= 100) {
break;
} // Avoid too many instanced objects
counter++;
const padding = 5;
const elementMeasurement = element?.getBoundingClientRect() ?? undefined;
if (elementMeasurement == undefined) {
continue;
}
const highlightDiv = this.document.createElement("div");
highlightDiv.classList.add("highlight");
Object.assign(highlightDiv.style, {
left: `${elementMeasurement.left - padding}px`,
top: `${elementMeasurement.top - padding}px`,
width: `${elementMeasurement.width + padding * 2}px`,
height: `${elementMeasurement.height + padding * 2}px`,
});
highlightContainerDiv.appendChild(highlightDiv);
}
this.getElementById("highlight-shadow").style.display = "initial";
}
/**
* Clears the highlight
*/
removeHighlight() {
const highlightContainerDiv = this.getElementById("highlight-container");
highlightContainerDiv.style.display = "none";
// Clear all children elements
highlightContainerDiv.innerHTML = "";
this.getElementById("highlight-shadow").style.display = "none";
}
/**
* Updates the path display text on the selector component
* based on the current selection
*/
#updatePathTextField() {
const maxPathLength = 64;
const selection = this.getSelection();
const selectionPath = this.getSelectionPath(
this.document,
this.#relatedValueIndex,
this.#selectedElement
);
if (!selectionPath) {
return;
}
const pathText = `<b>[${selection.length}]</b> ${selectionPath.substring(0, Math.min(maxPathLength, selectionPath.length))}`;
this.getElementById("selector-element-preview-text").setHTML(pathText);
}
/**
* Removes all event listeners and removes the overlay from the Anonymous Content
*/
tearDown() {
if (this.#content) {
try {
this.document.removeAnonymousContent(this.#content);
} catch {
/* This might fail but that's not an issue */
}
}
this.window = null;
this.document = null;
this.#initialized = false;
}
/**
* Hides the hover selection box
*/
#hideHoverDiv() {
this.hoverDiv.style.display = "none";
}
/**
* Shows the hover selection box
*/
#showHoverDiv() {
this.hoverDiv.style.display = "initial";
}
/**
* Resets the hover selection box bounds
*/
#resetHoverDiv() {
Object.assign(this.getElementById("hover-div").style, {
top: `0px`,
left: `0px`,
width: `0px`,
height: `0px`,
});
}
/**
* Hides the selector component
*/
#hideSelectorComponent() {
this.selectorComponent.style.visibility = "hidden";
this.selectorComponent.setAttribute("is-appearing", "false");
}
/**
* Shows the selector component
*/
#showSelectorComponent() {
this.selectorComponent.style.visibility = "visible";
this.#setSelectorComponentPosition();
}
/**
* Sets the aligned and clamped position for the zap component on the document
* relative to #selectedElement
*/
#setSelectorComponentPosition() {
const bounds = this.#selectedElement.getBoundingClientRect();
const distance = 8;
const rect = this.selectorComponent.getBoundingClientRect();
const zapComponentWidth = rect.width;
const zapComponentHeight = rect.height;
const windowWidth = this.window.innerWidth;
const windowHeight = this.window.innerHeight;
const windowPadding = 10;
this.selectorComponent.setAttribute("is-appearing", "false");
// This clamps the position so the zap component never goes out of the client bounds and adds a small padding
const top = this.clamp(
bounds.top + bounds.height + distance,
windowPadding + this.safeAreaPadding.top,
windowHeight -
zapComponentHeight -
windowPadding -
this.safeAreaPadding.bottom
);
const left = this.clamp(
bounds.left + bounds.width / 2 - zapComponentWidth / 2,
windowPadding + this.safeAreaPadding.left,
windowWidth -
zapComponentWidth -
windowPadding -
this.safeAreaPadding.right
);
Object.assign(this.selectorComponent.style, {
top: `${top}px`,
left: `${left}px`,
});
// Adjust transform origin for animation
const targetCenterX = bounds.left + bounds.width / 2;
const targetBottomY = bounds.top + bounds.height;
let originX = this.clamp(targetCenterX - left, 0, zapComponentWidth);
let originY = this.clamp(targetBottomY - top, 0, zapComponentHeight);
this.selectorComponent.style.transformOrigin = `${originX}px ${originY}px`;
this.window.requestAnimationFrame(() => {
this.selectorComponent.setAttribute("is-appearing", "true");
});
}
/**
* This function handles page events while the overlay is active
*
* @param {Event} event The event which will be handled by the overlay
* @param {boolean} prevent True if the event should be prevented
*/
handleEvent(event, prevent) {
let isZenContent = false;
if (event?.originalTarget?.closest) {
const closestID = event.originalTarget.closest("div")?.id ?? "";
isZenContent = this.#zenContentIDs.includes(closestID);
}
switch (event.type) {
case "click":
this.#handleClick(event, isZenContent);
break;
case "mousemove":
this.#handleMouseMove(event, isZenContent);
break;
case "scroll":
this.#handlePageChange(event);
return;
case "resize":
this.#handlePageChange(event);
return;
}
// Let the interactable ids pass through
if (isZenContent) {
return;
}
if (prevent) {
// From ScreenshotsComponentChild.sys.mjs:103
// Preventing a pointerdown event throws an error in debug builds.
// See https://searchfox.org/mozilla-central/rev/b41bb321fe4bd7d03926083698ac498ebec0accf/widget/WidgetEventImpl.cpp#566-572
// Don't prevent the default context menu.
if (!["contextmenu", "pointerdown"].includes(event.type)) {
event.preventDefault();
}
event.stopImmediatePropagation();
}
}
/**
* Called after a page change to update the highlight and selector component position
*/
#handlePageChange() {
if (this.#currentState !== SelectorComponent.STATES.SELECTED) {
return;
}
this.updateHighlight();
this.#setSelectorComponentPosition();
}
/**
* Handles the mouse move event
*
* @param {Event} event Mouse move event params
* @param {boolean} isZenContent Flag if the target element is a zen related element
*/
#handleMouseMove(event, isZenContent) {
if (this.#lastOverElement === event.target) {
return;
}
if (!isZenContent) {
this.#lastOverElement = event.target;
if (this.#currentState === SelectorComponent.STATES.SELECTING) {
this.#showHoverDiv();
}
} else {
this.#hideHoverDiv();
}
if (
this.#currentState !== SelectorComponent.STATES.SELECTING ||
!event.target
) {
return;
}
const bounds = event.target.getBoundingClientRect();
const padding = 5;
Object.assign(this.getElementById("hover-div").style, {
top: `${bounds.top - padding}px`,
left: `${bounds.left - padding}px`,
width: `${bounds.width + padding * 2}px`,
height: `${bounds.height + padding * 2}px`,
});
}
/**
* Handles the mouse click event
*
* @param {Event} event Mouse move event params
* @param {boolean} isZenContent Flag if the target element is a zen related element
*/
#handleClick(event, isZenContent) {
// Safeguards for protecting anonymous content from being zapped
if (
event.target === this.document.documentElement ||
event.target === this.document.body ||
!this.document.documentElement.contains(event.target)
) {
return;
}
if (
this.#currentState === SelectorComponent.STATES.SELECTING &&
!isZenContent
) {
this.setState(SelectorComponent.STATES.SELECTED, event.target);
}
}
/**
* @param {number} x Value
* @param {number} min Minimum limit
* @param {number} max Maximum limit
* @returns {number} A value which always lies between min and max
*/
clamp(x, min, max) {
return Math.min(Math.max(x, min), max);
}
/**
* When selecting an area to zap there can be a set of zapped elements
* since related elements can be included.
* This method returns all targeted elements for the zapping process.
*
* @returns {Element[]} An array of selected elements
*/
getSelection() {
const selector = this.getSelectionPath(
this.document,
this.#relatedValueIndex,
this.#selectedElement
);
if (!selector) {
return [];
}
return this.document.querySelectorAll(selector);
}
/**
* Used for retreiving the css path from the selected element and taking
* the related objects into account
*
* @param {Element} document
* @param {Element} relatedValueIndex
* @param {Element} selectedElement
*/
getSelectionPath(document, relatedValueIndex, selectedElement) {
let path = [];
const cssescape = str => CSS.escape(str);
// Body and Html nodes are not considered valid here
const isValidNode = element => {
if (!element) {
return false;
} else if (element.tagName.toLowerCase() === "body") {
return false;
} else if (element.tagName.toLowerCase() === "html") {
return false;
}
return true;
};
const nthChild = element => {
if (!element) {
return "";
}
if (!element.parentNode) {
return "";
}
const parent = element.parentNode;
const index = Array.prototype.indexOf.call(parent.children, element) + 1;
if (index === 1) {
return ":first-child";
}
if (index === parent.children.length) {
return ":last-child";
}
return `:nth-child(${index})`;
};
const getIdentification = (element, specifity = 0) => {
if (!element) {
return "";
}
const id = specifity < 2 && element.id ? `#${cssescape(element.id)}` : "";
const cls =
specifity < 1 && element.classList.length
? "." + [...element.classList].map(c => cssescape(c)).join(".")
: "";
const tag = element.tagName ? element.tagName.toLowerCase() : "";
return `${tag}${id}${cls}`;
};
const traverse = (element, specifity = 0, pathArray) => {
let currentElement = element;
if (!isValidNode(currentElement)) {
return;
}
pathArray.push(nthChild(currentElement));
pathArray.push(getIdentification(currentElement, specifity));
if (currentElement && currentElement.parentNode) {
pathArray.push(" > ");
pathArray.push(getIdentification(currentElement.parentNode, 0));
const tempBuild = build(pathArray);
while (
tempBuild &&
document.querySelectorAll(tempBuild).length > 1 &&
isValidNode(currentElement.parentNode)
) {
currentElement = currentElement.parentNode;
pathArray.push(" > ");
pathArray.push(nthChild(currentElement.parentNode));
pathArray.push(getIdentification(currentElement.parentNode, 0));
}
}
};
const build = pathArray => pathArray.toReversed().join("");
const findBestExactSelector = (element, doc) => {
let buildMap = new Map();
let pathExactElement = [];
traverse(element, 0, pathExactElement);
const pathExactElementBuilt = build(pathExactElement);
const pathExactElementLength = (
pathExactElementBuilt ? doc.querySelectorAll(pathExactElementBuilt) : []
).length;
buildMap.set(pathExactElementLength, pathExactElementBuilt);
let pathTypeElement = [];
pathTypeElement.push(getIdentification(element, 2));
if (isValidNode(element.parentNode)) {
pathTypeElement.push(" > ");
traverse(element.parentNode, 0, pathTypeElement);
}
const pathTypeElementBuilt = build(pathTypeElement);
const pathTypeElementLength = (
pathTypeElementBuilt ? doc.querySelectorAll(pathTypeElementBuilt) : []
).length;
if (!buildMap.has(pathTypeElementLength)) {
buildMap.set(pathTypeElementLength, pathTypeElementBuilt);
}
let parentExactElement = [];
if (isValidNode(element.parentNode)) {
traverse(element.parentNode, 0, parentExactElement);
}
const pathParentElementBuilt = build(parentExactElement);
const pathParentElementLength = (
pathParentElementBuilt
? doc.querySelectorAll(pathParentElementBuilt)
: []
).length;
if (!buildMap.has(pathParentElementLength)) {
buildMap.set(pathParentElementLength, pathParentElementBuilt);
}
let smallestLength = Number.MAX_VALUE;
let smallestLengthPath = null;
buildMap.forEach((buildPath, length) => {
if (length < smallestLength) {
smallestLength = length;
smallestLengthPath = buildPath;
}
});
return smallestLengthPath;
};
if (!isValidNode(selectedElement)) {
return null;
}
switch (relatedValueIndex) {
// Sometimes getting the exact element we want is not guaranteed by
// one specific path builder. It is best to build multiple possible paths
// and decide which one is best.
default:
case 0:
return findBestExactSelector(selectedElement, document);
// Getting the exact parent element of selected element
case 1:
if (!isValidNode(selectedElement.parentNode)) {
return null;
}
traverse(selectedElement.parentNode, 0, path);
break;
// Getting the type of selected element with same exact parent
case 2:
path.push(getIdentification(selectedElement, 2));
if (isValidNode(selectedElement.parentNode)) {
path.push(" > ");
path.push(getIdentification(selectedElement.parentNode, 0));
}
break;
// Getting the same type of selected element with same parent type
case 3:
path.push(getIdentification(selectedElement, 2));
if (isValidNode(selectedElement.parentNode)) {
path.push(" > ");
path.push(getIdentification(selectedElement.parentNode, 2));
}
break;
// Getting the same type of selected element
case 4:
path.push(getIdentification(selectedElement, 2));
break;
// Getting the same type of the parent
case 5:
if (!isValidNode(selectedElement.parentNode)) {
return null;
}
path.push(getIdentification(selectedElement.parentNode, 2));
break;
// Get any element with same parent
case 6:
if (!isValidNode(selectedElement.parentNode)) {
return null;
}
path.push("*");
path.push(" > ");
traverse(selectedElement.parentNode, 0, path);
break;
// Get elements with similar classes
case 7:
path.push(getIdentification(selectedElement, 1));
break;
}
return build(path);
}
}

View File

@@ -0,0 +1,636 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
export class ZapDissolve {
FRAG = `
precision mediump float;
uniform sampler2D u_DissolveTexture;
varying vec2 v_PCoord;
varying float v_PLifetime;
varying float v_POpacity;
void main() {
if(v_PLifetime >= 1.0) discard;
vec4 sampled = texture2D(u_DissolveTexture, v_PCoord);
if(sampled.a == 0.0) discard;
float distToCenter = distance(vec2(0.5, 0.5), gl_PointCoord);
if(distToCenter > 0.5) discard;
float alpha = sampled.a * v_POpacity;
float brightnessFactor = 1.5;
gl_FragColor = vec4(sampled.rgb * brightnessFactor, alpha);
}
`;
VERT = `
precision highp float;
uniform float u_AnimationDuration;
uniform float u_ParticleSize;
uniform float u_ElapsedTime;
uniform float u_ViewportWidth;
uniform float u_ViewportHeight;
uniform float u_TextureWidth;
uniform float u_TextureHeight;
uniform float u_TextureLeft;
uniform float u_TextureTop;
attribute float a_ParticleIndex;
varying vec2 v_PCoord;
varying float v_PLifetime;
varying float v_POpacity;
float hash1(float n) { return fract(sin(n) * 100000.0); }
float hash2(vec2 p) { return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453123); }
float hash3(vec2 p, float offset) {
return fract(sin(dot(p, vec2(12.9898 + offset, 78.233 - offset))) * 43758.5453123);
}
/* coordinates */
vec2 toNDC(vec2 pixel) {
vec2 ndc;
ndc.x = (pixel.x / u_ViewportWidth) * 2.0 - 1.0;
ndc.y = 1.0 - (pixel.y / u_ViewportHeight) * 2.0;
return ndc;
}
float mix1(float a, float b, float t) { return a + (b - a) * t; }
/* particle layout */
vec2 particlePixelPos(float index) {
float cols = floor(u_TextureWidth / u_ParticleSize);
if(cols < 1.0) cols = 1.0;
float row = floor(index / cols);
float col = index - row * cols;
return vec2(
(col + 0.5) * u_ParticleSize + u_TextureLeft,
(row + 0.5) * u_ParticleSize + u_TextureTop
);
}
mat2 rotate2D(float angle) {
float s = sin(angle);
float c = cos(angle);
return mat2(c, -s, s, c);
}
/* motion */
vec4 animatedPosition(vec2 pixelPos, float seed, float seed2, float seed3, float t) {
vec2 base = toNDC(pixelPos);
// Random rotation
float rotation1 = (seed - 0.5) * 0.52; // 15 * 2 * (π/180)
float rotation2 = (seed2 - 0.5) * 0.52;
// Random translation
float randomRadian = 6.28318 * (seed3 - 0.5);
float translateX = 60.0 * cos(randomRadian) / u_ViewportWidth * 2.0;
float translateY = 30.0 * sin(randomRadian) / u_ViewportHeight * 2.0;
vec2 rotated1 = rotate2D(rotation1 * t) * base;
vec2 translated = vec2(
mix1(rotated1.x, rotated1.x + translateX, t),
mix1(rotated1.y, rotated1.y + translateY, t)
);
vec2 finalPos = rotate2D(rotation2 * t) * (translated - base) + base;
return vec4(finalPos, 0.0, 1.0);
}
void main() {
vec2 pixelPos = particlePixelPos(a_ParticleIndex);
// Multiple seeds for different random values
float seed = hash2(pixelPos);
float seed2 = hash3(pixelPos, 1.0);
float seed3 = hash3(pixelPos, 2.0);
float normalizedX = (pixelPos.x - u_TextureLeft) / u_TextureWidth;
float frameAssignment = (seed + 2.0 * normalizedX) / 3.0;
float maxDelay = u_AnimationDuration * 0.42;
float delay = frameAssignment * maxDelay;
float particleDuration = u_AnimationDuration - delay;
float lifetime = clamp((u_ElapsedTime - delay) / particleDuration, 0.0, 1.0);
float easedT = 1.0 - pow(1.0 - lifetime, 3.0);
gl_Position = animatedPosition(pixelPos, seed, seed2, seed3, easedT);
gl_PointSize = u_ParticleSize;
v_PLifetime = lifetime;
v_POpacity = 1.0 - lifetime;
// Texture coordinates
float cols = floor(u_TextureWidth / u_ParticleSize);
if(cols < 1.0) cols = 1.0;
float row = floor(a_ParticleIndex / cols);
float col = a_ParticleIndex - row * cols;
v_PCoord = vec2(
(col + 0.5) / cols,
(row + 0.5) / floor(u_TextureHeight / u_ParticleSize)
);
}
`;
window = null;
document = null;
#initialized = false;
#content = null;
#webglContext = null;
#program;
#animationStartTime = -1;
#particlesCount = 0;
#duration = 1400;
#texture = null;
#buffer = null;
#hasTriggered = false;
#rafId = null;
#onComplete = null;
/**
* @param {Document} document Webpage document
*/
constructor(document) {
if (!document) {
return;
}
this.document = document;
this.window = document.ownerGlobal;
}
/**
* Initializes the zap mode and inserts anonymous content
*/
async initialize() {
if (this.#initialized) {
return;
}
this.#content = this.document.insertAnonymousContent();
this.#content.root.appendChild(this.fragment);
await this.#initializeElements();
this.#initialized = true;
}
/**
* Initializes the elements and contexts required for the rendering
*/
async #initializeElements() {
const canvas = this.getElementById("zen-zap-dissolve-canvas");
const gl = canvas.getContext("webgl", {
alpha: true,
antialias: true,
premultipliedAlpha: false,
});
this.#webglContext = gl;
if (!gl) {
console.error("WebGL is not supported.");
return;
}
this.#resizeCanvasToClientSize(canvas);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.clearColor(0, 0, 0, 0);
// Ensure viewport is set correctly
gl.viewport(0, 0, canvas.width, canvas.height);
this.#program = await this.#createProgram(gl, this.VERT, this.FRAG);
}
/**
* Resizes the canvas to the correct size
*
* @param {Element} canvas
*/
#resizeCanvasToClientSize(canvas) {
const width = this.window.innerWidth;
const height = this.window.innerHeight;
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
// Set the WebGL viewport to match the canvas size
if (this.#webglContext) {
this.#webglContext.viewport(0, 0, width, height);
}
}
}
/**
* Creates the canvas program
*
* @param {object} gl WebGL context
* @param {string} vertContent Content of the vertex shader
* @param {string} fragContent Content of the fragment shader
* @returns {*} The WebGL program
*/
async #createProgram(gl, vertContent, fragContent) {
const vertexShader = this.#compileShader(gl, vertContent, gl.VERTEX_SHADER);
const fragmentShader = this.#compileShader(
gl,
fragContent,
gl.FRAGMENT_SHADER
);
if (!vertexShader || !fragmentShader) {
console.error(
"Program creation aborted: One or more shaders failed to compile."
);
return null;
}
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.detachShader(program, vertexShader);
gl.detachShader(program, fragmentShader);
gl.deleteShader(vertexShader);
gl.deleteShader(fragmentShader);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error(
"Shader program initializiation failure:",
gl.getProgramInfoLog(program)
);
}
return program;
}
/**
* Compiles a shader
*
* @param {object} gl WebGL context
* @param {string} source Shader source
* @param {object} type Shader type
* @returns {*} The compiled shader
*/
#compileShader(gl, source, type) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error("Shader compilation failure:", gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
/**
* Retreives image data from a given image
*
* @param {Image} image The image
* @returns {ImageData} Image data
*/
#getImageData(image) {
const canvas = this.getElementById(
"zen-zap-dissolve-canvas"
).ownerDocument.createElement("canvas");
canvas.width = image.width;
canvas.height = image.height;
const context = canvas.getContext("2d");
context.drawImage(image, 0, 0);
return context.getImageData(0, 0, canvas.width, canvas.height);
}
/**
* Creates and loads a WebGL texture from an Image
*
* @param {Image} image The target image
*/
#loadTexture(image) {
if (this.#texture) {
this.#webglContext.deleteTexture(this.#texture);
}
const texture = this.#webglContext.createTexture();
this.#webglContext.activeTexture(this.#webglContext.TEXTURE0);
this.#webglContext.bindTexture(this.#webglContext.TEXTURE_2D, texture);
this.#webglContext.texParameteri(
this.#webglContext.TEXTURE_2D,
this.#webglContext.TEXTURE_WRAP_S,
this.#webglContext.CLAMP_TO_EDGE
);
this.#webglContext.texParameteri(
this.#webglContext.TEXTURE_2D,
this.#webglContext.TEXTURE_WRAP_T,
this.#webglContext.CLAMP_TO_EDGE
);
this.#webglContext.texParameteri(
this.#webglContext.TEXTURE_2D,
this.#webglContext.TEXTURE_MIN_FILTER,
this.#webglContext.NEAREST
);
this.#webglContext.texParameteri(
this.#webglContext.TEXTURE_2D,
this.#webglContext.TEXTURE_MAG_FILTER,
this.#webglContext.NEAREST
);
if (
image &&
image instanceof Ci.nsIImageLoadingContent &&
image.width &&
image.height
) {
this.#webglContext.texImage2D(
this.#webglContext.TEXTURE_2D,
0,
this.#webglContext.RGBA,
this.#webglContext.RGBA,
this.#webglContext.UNSIGNED_BYTE,
image
);
} else {
const imageData = this.#getImageData(image);
this.#webglContext.texImage2D(
this.#webglContext.TEXTURE_2D,
0,
this.#webglContext.RGBA,
imageData.width,
imageData.height,
0,
this.#webglContext.RGBA,
this.#webglContext.UNSIGNED_BYTE,
imageData.data
);
}
const textureLocation = this.#webglContext.getUniformLocation(
this.#program,
"u_DissolveTexture"
);
this.#webglContext.uniform1i(textureLocation, 0);
this.#texture = texture;
}
/**
* Binds the parameters to the program for a given Element
*
* @param {Element} element
*/
#bindParameters(element) {
const gl = this.#webglContext;
const rect = element.getBoundingClientRect();
const particleSize = 1;
const textureWidth = rect.width;
const textureHeight = rect.height;
const cols = Math.floor(textureWidth / particleSize);
const rows = Math.floor(textureHeight / particleSize);
this.#particlesCount = cols * rows;
gl.useProgram(this.#program);
this.#setUniform1f("u_AnimationDuration", this.#duration);
this.#setUniform1f("u_ParticleSize", particleSize);
this.#setUniform1f("u_ViewportWidth", this.window.innerWidth);
this.#setUniform1f("u_ViewportHeight", this.window.innerHeight);
this.#setUniform1f("u_TextureWidth", textureWidth);
this.#setUniform1f("u_TextureHeight", textureHeight);
this.#setUniform1f("u_TextureLeft", rect.left);
this.#setUniform1f("u_TextureTop", rect.top);
const indices = new Float32Array(this.#particlesCount);
for (let i = 0; i < this.#particlesCount; i++) {
indices[i] = i;
}
if (this.#buffer) {
gl.deleteBuffer(this.#buffer);
}
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, indices, gl.STATIC_DRAW);
const aLoc = gl.getAttribLocation(this.#program, "a_ParticleIndex");
gl.enableVertexAttribArray(aLoc);
gl.vertexAttribPointer(aLoc, 1, gl.FLOAT, false, 0, 0);
this.#buffer = buffer;
}
/**
* Requests an animation frame
*/
#requestDraw() {
this.#rafId = this.window.requestAnimationFrame(t => this.#draw(t));
}
/**
* Helper function for setting a uniform in the WebGL context
*
* @param {string} name The property name
* @param {*} value The property value
*/
#setUniform1f(name, value) {
const loc = this.#webglContext.getUniformLocation(this.#program, name);
this.#webglContext.uniform1f(loc, value);
}
/**
* Renders the output to a canvas
*
* @param {number} time The frametime
*/
#draw(time) {
const gl = this.#webglContext;
gl.useProgram(this.#program);
if (this.#animationStartTime === -1) {
this.#animationStartTime = time;
}
const elapsed = time - this.#animationStartTime;
if (elapsed > this.#duration) {
this.#resetForNextRun();
return;
}
gl.clear(gl.COLOR_BUFFER_BIT);
this.#setUniform1f("u_ElapsedTime", elapsed);
gl.drawArrays(gl.POINTS, 0, this.#particlesCount);
this.#requestDraw();
}
/**
* Displays a dissolve effect for the element
*
* @param {Element} element The element to dissolve
* @param {Function} onComplete Callback for when the animation is complete
*/
dissolve(element, onComplete) {
if (!this.#initialized || this.#hasTriggered || !element) {
return;
}
this.#hasTriggered = true;
this.#onComplete = onComplete;
const rect = element.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
console.warn("[ZapDissolve]: element has zero size. Skipping dissolve");
return;
}
const captureCanvas = this.document.createElement("canvas");
captureCanvas.width = rect.width;
captureCanvas.height = rect.height;
const ctx = captureCanvas.getContext("2d");
const canvas = this.getElementById("zen-zap-dissolve-canvas");
this.#resizeCanvasToClientSize(canvas);
ctx.drawWindow(
this.window,
rect.left + this.window.scrollX,
rect.top + this.window.scrollY,
rect.width,
rect.height,
"rgba(0,0,0,0)"
);
this.#loadTexture(captureCanvas);
this.#bindParameters(element);
this.#animationStartTime = -1;
this.#requestDraw();
}
get content() {
if (!this.#content || Cu.isDeadWrapper(this.#content)) {
return null;
}
return this.#content;
}
/**
* Helper for getting an anonymous element by id
*
* @param {string} id The id of the element
*/
getElementById(id) {
return this.content.root.getElementById(id);
}
get markup() {
return `
<template>
<link rel="stylesheet" href="chrome://browser/content/zen-styles/content/zen-zap.css" />
<canvas id="zen-zap-dissolve-canvas"></canvas>
</template>
`;
}
get fragment() {
if (!this.template) {
let parser = new DOMParser();
let doc = parser.parseFromString(this.markup, "text/html");
this.template = this.document.importNode(
doc.querySelector("template"),
true
);
}
let fragment = this.template.content.cloneNode(true);
return fragment;
}
/**
* Makes the class ready for reusage
*/
#resetForNextRun() {
const gl = this.#webglContext;
if (gl) {
if (this.#texture) {
gl.deleteTexture(this.#texture);
this.#texture = null;
}
if (this.#buffer) {
gl.deleteBuffer(this.#buffer);
this.#buffer = null;
}
gl.clear(gl.COLOR_BUFFER_BIT);
}
this.#hasTriggered = false;
this.#animationStartTime = -1;
if (this.#rafId) {
this.window.cancelAnimationFrame(this.#rafId);
this.#rafId = null;
}
if (this.#onComplete) {
this.#onComplete();
this.#onComplete = null;
}
}
/**
* Removes all event listeners and removes the overlay from the Anonymous Content
*/
tearDown() {
const gl = this.#webglContext;
if (gl) {
if (this.#texture) {
gl.deleteTexture(this.#texture);
this.#texture = null;
}
if (this.#buffer) {
gl.deleteBuffer(this.#buffer);
this.#buffer = null;
}
if (this.#program) {
gl.deleteProgram(this.#program);
this.#program = null;
}
const loseCtx = gl.getExtension("WEBGL_lose_context");
loseCtx?.loseContext();
}
if (this.window != null) {
this.window.cancelAnimationFrame(this.#rafId);
}
this.#rafId = null;
if (this.#content) {
try {
this.document.removeAnonymousContent(this.#content);
} catch {
/* This might fail but that's not an issue */
}
}
this.#content = null;
this.#initialized = false;
}
}

View File

@@ -0,0 +1,424 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ZapDissolve: "resource:///modules/zen/boosts/ZenZapDissolve.sys.mjs",
SelectorComponent:
"resource:///modules/zen/boosts/ZenSelectorComponent.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "overlayLocalization", () => {
return new Localization(["browser/zen-boosts.ftl"], true);
});
export class ZapOverlay {
document = null;
window = null;
#initialized = false;
#content = null;
#zapContentIDs = ["zap-list", "zap-controls-container"];
#selectorComponent = null;
#dissolvePoolSize = 5;
#dissolveEffectPool = [];
#currentDissolveIndex = 0;
/**
* @param {*} document Webpage document
* @param {*} zenBoostsChild Boost JSActor child
*/
constructor(document, zenBoostsChild) {
this.document = document;
this.window = document.ownerGlobal;
this.zenBoostsChild = zenBoostsChild;
this.#selectorComponent = new lazy.SelectorComponent(
document,
zenBoostsChild,
this.#zapContentIDs,
this.handleSelectComponentSelect.bind(this),
[
{ id: "zen-zap-this" },
{ id: "zen-zap-related" },
{ id: "zen-zap-cancel" },
]
);
// Remove the bottom unzap bar to the safe area
this.#selectorComponent.safeAreaPadding.bottom = 65;
}
/**
* Initializes the zap mode and inserts anonymous content
*/
async initialize() {
if (this.#initialized) {
console.warn(
"[ZenZapOverlayChild]: Skipping initialize because initialized."
);
return;
}
this.#selectorComponent.initialize();
this.#content = this.document.insertAnonymousContent();
this.#content.root.appendChild(this.fragment);
this.#initializeElements();
this.#initialized = true;
}
/**
* Initializes all anonymous content and events
*/
#initializeElements() {
this.zapDoneButton = this.getElementById("zap-done");
this.zapDoneButton.addEventListener(
"click",
this.#disableZapMode.bind(this)
);
this.#updateZappedList();
}
/**
* Lazily loads the next available dissolve effect.
* The returned effect might not currently be ready to trigger again.
*
* @returns {Promise<ZapDissolve>} Dissolve effect
*/
async #getNextDissolveEffect() {
// Effect does not exist yet, create and initialize
if (this.#currentDissolveIndex >= this.#dissolveEffectPool.length) {
const dissolveEffect = new lazy.ZapDissolve(this.document);
await dissolveEffect.initialize();
this.#dissolveEffectPool.push(dissolveEffect);
}
// Capture current index and increment for next call
const returnIndex = this.#currentDissolveIndex;
this.#currentDissolveIndex =
(this.#currentDissolveIndex + 1) % this.#dissolvePoolSize;
return this.#dissolveEffectPool[returnIndex];
}
get content() {
if (!this.#content || Cu.isDeadWrapper(this.#content)) {
return null;
}
return this.#content;
}
/**
* Helper for getting an anonymous element by id
*
* @param {string} id The id of the element
*/
getElementById(id) {
return this.content.root.getElementById(id);
}
get markup() {
// Fetch localizations
let [done] = lazy.overlayLocalization.formatMessagesSync([
{ id: "zen-zap-done" },
]);
return `
<template>
<link rel="stylesheet" href="chrome://browser/content/zen-styles/content/zen-zap.css" />
<div id="zap-controls-container">
<div id="zap-list">
</div>
<input type="button" id="zap-done" value="${done.value}"/>
</div>
<div id="zap-border"></div>
</template>
`;
}
get fragment() {
if (!this.template) {
let parser = new DOMParser();
let doc = parser.parseFromString(this.markup, "text/html");
this.template = this.document.importNode(
doc.querySelector("template"),
true
);
}
let fragment = this.template.content.cloneNode(true);
return fragment;
}
/**
* Handles the onSelect callback from the SelectComponent
*
* @param {string} cssSelector The CSS selector of the selected element
*/
handleSelectComponentSelect(cssSelector) {
this.#handleZap(cssSelector);
}
/**
* Notifies listeners for an update in the zap list
*/
onZapUpdate() {
this.#updateZappedList();
this.zenBoostsChild.sendNotify("zap-list-update");
}
/**
* Handles the addition of the given zap selector
*
* @param {string} cssPath The css selector of the zap
*/
#handleZap(cssPath) {
const useDissolve = Services.prefs.getBoolPref(
"zen.boosts.dissolve-on-zap"
);
if (!this.window.gReduceMotion && useDissolve) {
const elements = this.document.querySelectorAll(cssPath);
let counter = 0;
elements.forEach(async element => {
if (counter >= this.#dissolvePoolSize) {
return;
}
counter++;
this.#getNextDissolveEffect().then(dissolve => {
dissolve.dissolve(element, () => {
this.zenBoostsChild.addZapSelector(cssPath);
this.onZapUpdate();
this.window.requestAnimationFrame(() => {
element.style.removeProperty("opacity");
});
});
element.style.setProperty("opacity", "0", "important");
});
});
} else {
this.zenBoostsChild.addZapSelector(cssPath);
this.onZapUpdate();
}
}
/**
* Handles the removal of a zap selector
*
* @param {string} cssPath The css selector of the zap
*/
#handleUnzap(cssPath) {
this.zenBoostsChild.removeZapSelector(cssPath);
this.onZapUpdate();
}
/**
* Cancles the current zap operation
*/
#cancelZap() {
this.#selectorComponent.setState(lazy.SelectorComponent.STATES.SELECTING);
}
/**
* Helper function for leaving the zap mode
*/
#disableZapMode() {
this.zenBoostsChild.disableZapMode();
}
/**
* Rebuilds the unzap button list at the bottom of the website
*/
async #updateZappedList() {
const zapList = this.getElementById("zap-list");
zapList.innerHTML = "";
const boost = await this.zenBoostsChild.getWebsiteBoost();
const { boostData } = boost.boostEntry;
boostData.zapSelectors.forEach(selector => {
const unzapButton = zapList.ownerDocument.createElement("input");
unzapButton.type = "button";
unzapButton.id = "zen-zap-unzap";
const index = boostData.zapSelectors.indexOf(selector) + 1;
const zappedElementsCount =
selector == "" ? 0 : this.document.querySelectorAll(selector).length;
const [tooltip] = lazy.overlayLocalization.formatMessagesSync([
{
id: "zen-unzap-tooltip",
args: { elementCount: zappedElementsCount },
},
]);
unzapButton.value = index;
unzapButton.title = tooltip.value;
unzapButton.setAttribute("index", index);
unzapButton.setAttribute("selector", selector);
zapList.appendChild(unzapButton);
});
// Fetch localizations
let [addZapHelper, removeZapHelper] =
lazy.overlayLocalization.formatMessagesSync([
{ id: "zen-add-zap-helper" },
{ id: "zen-remove-zap-helper" },
]);
if (!boostData.zapSelectors.length) {
const addZapHelperText = zapList.ownerDocument.createElement("p");
addZapHelperText.setHTML(addZapHelper.value);
addZapHelperText.classList.add("pcenter");
zapList.appendChild(addZapHelperText);
} else {
const removeZapHelperText = zapList.ownerDocument.createElement("p");
removeZapHelperText.setHTML(removeZapHelper.value);
zapList.appendChild(removeZapHelperText);
}
}
/**
* Handles the mouse enter event for the unzap buttons
*
* @param {Event} event
*/
#unzapButtonHover(event) {
const button = event.originalTarget;
const selector = button.getAttribute("selector");
this.zenBoostsChild.tempShowZappedElement(selector);
button.value = "×";
// This has to run with later, as the elements we are trying to highlight do not exist yet.
// The css has to load first and calculate the bounding boxes for the elements before we can highlight.
this.window.requestAnimationFrame(() => {
const selection = this.document.querySelectorAll(selector);
if (selection.length) {
this.#selectorComponent.showHighlight(selection);
}
});
// Cancle an ongoing select action
this.#cancelZap();
}
/**
* Handles the mouse exit event for the unzap buttons
*
* @param {Event} event
*/
#unzapButtonUnhover(event) {
const button = event.originalTarget;
button.value = button.getAttribute("index");
this.zenBoostsChild.tempHideZappedElement();
this.#selectorComponent.removeHighlight();
}
/**
* Handles button clicks from the unzap list
*
* @param {Event} event
*/
#unzapButtonClick(event) {
const button = event.originalTarget;
const selector = button.getAttribute("selector");
this.zenBoostsChild.tempHideZappedElement();
this.#selectorComponent.removeHighlight();
this.#cancelZap();
// In order to avoid the clicked element being null when the
// SelectorComponent receives it, push the list re-creation to the next frame
this.window.requestAnimationFrame(() => {
this.#handleUnzap(selector);
});
}
/**
* Removes all event listeners and removes the overlay from the Anonymous Content
*/
tearDown() {
this.#selectorComponent.tearDown();
this.#selectorComponent = null;
this.#dissolveEffectPool.forEach(dissolve => {
dissolve.tearDown();
});
if (this.#content) {
try {
this.document.removeAnonymousContent(this.#content);
} catch {
/* This might fail but that's not an issue */
}
}
this.window = null;
this.document = null;
this.#initialized = false;
}
/**
* This function handles page events while the overlay is active
*
* @param {Event} event The event which will be handled by the overlay
* @param {boolean} prevent True if the event should be prevented
*/
handleEvent(event, prevent) {
switch (event.type) {
case "click":
this.#handleClick(event);
break;
case "mouseover":
this.#handleHoverDelegation(event);
break;
case "mouseout":
this.#handleUnhoverDelegation(event);
}
this.#selectorComponent.handleEvent(event, prevent);
}
/**
* Handles the mouse click event
*
* @param {Event} event Mouse move event params
*/
#handleClick(event) {
if (event.originalTarget.id == "zen-zap-unzap") {
this.#unzapButtonClick(event);
}
}
/**
* Handles the mouse enter event
*
* @param {Event} event Mouse enter event params
*/
#handleHoverDelegation(event) {
if (event.originalTarget.id == "zen-zap-unzap") {
this.#unzapButtonHover(event);
}
}
/**
* Handles the mouse leave event
*
* @param {Event} event Mouse leave event params
*/
#handleUnhoverDelegation(event) {
if (event.originalTarget.id == "zen-zap-unzap") {
this.#unzapButtonUnhover(event);
}
}
}

View File

@@ -0,0 +1,539 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
const AGENT_SHEET = Ci.nsIStyleSheetService.AGENT_SHEET;
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ZapOverlay: "resource:///modules/zen/boosts/ZenZapOverlayChild.sys.mjs",
SelectorComponent:
"resource:///modules/zen/boosts/ZenSelectorComponent.sys.mjs",
});
export class ZenBoostsChild extends JSWindowActorChild {
#currentSheet = null;
#currentState = ZenBoostsChild.STATES.NONE;
#preventableEventsAdded = false;
#zappedElementsTempShown = [];
#overlay = null;
static STATES = {
NONE: "none",
ZAP: "zap",
PICKER: "picker",
};
static OVERLAY_EVENTS = [
"click",
"pointerdown",
"pointermove",
"pointerup",
"scroll",
"resize",
];
// A list of events that will be prevented from
// reaching the document
static PREVENTABLE_EVENTS = [
"click",
"pointerdown",
"pointermove",
"pointerup",
"mousemove",
"mousedown",
"mouseup",
"mouseenter",
"mouseover",
"mouseout",
"mouseleave",
"touchstart",
"touchmove",
"touchend",
"dblclick",
"auxclick",
"keypress",
"contextmenu",
"pointerenter",
"pointerover",
"pointerout",
"pointerleave",
];
// Caching the events in sets for performance
static ALL_EVENTS_SET = new Set([
...ZenBoostsChild.OVERLAY_EVENTS,
...ZenBoostsChild.PREVENTABLE_EVENTS,
]);
static PREVENTABLE_SET = new Set(ZenBoostsChild.PREVENTABLE_EVENTS);
/**
* Inverse of https://searchfox.org/firefox-main/rev/1a8c62b86277005f907151bc5389cf5c5091e76f/gfx/src/nsColor.h#23-27
*
* > #define NS_RGBA(_r, _g, _b, _a) \
* > ((nscolor)(((_a) << 24) | ((_b) << 16) | ((_g) << 8) | (_r)))
*
* Converts [r, g, b] array to (uint32_t) NSColor
* Make a color out of r,g,b,a values. This assumes that the r,g,b,a
* values are properly constrained to 0-255.
*
* @param {Array} rgb - Array of red, green, blue values [0, 255]
* @param {number} rgb.0 - Red color value [0, 255]
* @param {number} rgb.1 - Green color value [0, 255]
* @param {number} rgb.2 - Blue color value [0, 255]
* @param {number} contrast - Contrast value (default 255)
* @returns {number} NSColor integer representation
*/
#rgbToNSColor([r, g, b], contrast = 255) {
// Note will be using the alpha channel for contrast, since the colors will always
// be fully opaque and we need an extra byte to store the contrast value. This allows
// us to still use an nscolor as parameter instead of having to deal with WebIDL structs
// shenanigans.
// Take into account that its an unsigned int
return ((contrast << 24) | (b << 16) | (g << 8) | r) >>> 0;
}
/**
* From ZenGradientGenerator.mjs
* Converts an HSL color value to RGB. Conversion formula
* adapted from https://en.wikipedia.org/wiki/HSL_color_space.
* Assumes h, s, and l are contained in the set [0, 1] and
* returns r, g, and b in the set [0, 255].
*
* @param {number} h The hue
* @param {number} s The saturation
* @param {number} l The lightness
* @returns {Array} The RGB representation
*/
#hslToRgb(h, s, l) {
const { round } = Math;
let r, g, b;
if (s === 0) {
r = g = b = l; // achromatic
} else {
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = this.#hueToRgb(p, q, h + 1 / 3);
g = this.#hueToRgb(p, q, h);
b = this.#hueToRgb(p, q, h - 1 / 3);
}
return [round(r * 255), round(g * 255), round(b * 255)];
}
/**
* From ZenGradientGenerator.mjs
* Inverse of hslToRgb
* Converts an RGB color value to HSL. Conversion formula
* adapted from https://en.wikipedia.org/wiki/HSL_color_space.
* Assumes r, g, and b are contained in the set [0, 255] and
* returns h, s, and l in the set [0, 1].
*
* @param {number} r The red value
* @param {number} g The green value
* @param {number} b The blue value
* @returns {Array} The HSL representation
*/
#rgbToHsl(r, g, b) {
r /= 255;
g /= 255;
b /= 255;
let max = Math.max(r, g, b);
let min = Math.min(r, g, b);
let d = max - min;
let h;
if (d === 0) {
h = 0;
} else if (max === r) {
h = ((g - b) / d) % 6;
} else if (max === g) {
h = (b - r) / d + 2;
} else if (max === b) {
h = (r - g) / d + 4;
}
let l = (min + max) / 2;
let s = d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1));
return [h * 60, s, l];
}
/**
* Handles DOM events for the actor. Applies boost settings when a document
* element is inserted.
*
* @param {Event} event - The DOM event to handle.
*/
handleEvent(event) {
switch (event.type) {
case "unload":
if (this.#currentState === ZenBoostsChild.STATES.ZAP) {
this.disableZapMode();
}
this.#removeEventListeners();
break;
case "DOMWindowCreated":
this.#applyBoostForPageIfAvailable();
break;
default:
break;
}
}
handleZapEvent(event) {
if (ZenBoostsChild.ALL_EVENTS_SET.has(event.type)) {
this.#overlay.handleEvent(
event,
ZenBoostsChild.PREVENTABLE_SET.has(event.type)
);
}
}
/**
* Adds necessary event listeners to the document
* to prevent content interactions
*/
#addEventListeners() {
this._handleZapEvent = this.handleZapEvent.bind(this);
this._disableZapMode = this.disableZapMode.bind(this);
for (let event of ZenBoostsChild.OVERLAY_EVENTS) {
this.document.addEventListener(event, this._handleZapEvent, true);
}
for (let event of ZenBoostsChild.PREVENTABLE_EVENTS) {
this.document.addEventListener(event, this._handleZapEvent, true);
}
this.#preventableEventsAdded = true;
}
/**
* Removes the event listeners from the document
*/
#removeEventListeners() {
for (let event of ZenBoostsChild.OVERLAY_EVENTS) {
this.document.removeEventListener(event, this._handleZapEvent, true);
}
if (this.#preventableEventsAdded) {
for (let event of ZenBoostsChild.PREVENTABLE_EVENTS) {
this.document.removeEventListener(event, this._handleZapEvent, true);
}
}
this.#preventableEventsAdded = false;
}
/**
* Handles messages received from the parent actor.
*
* @param {object} message - The message object containing name and data.
*/
async receiveMessage(message) {
switch (message.name) {
case "ZenBoost:BoostDataUpdated": {
const { unloadStyles = false } = message.data || {};
this.#applyBoostForPageIfAvailable(unloadStyles);
break;
}
case "ZenBoost:DisableZapMode":
if (this.#currentState === ZenBoostsChild.STATES.ZAP) {
this.disableZapMode();
}
break;
case "ZenBoost:DisablePickerMode":
if (this.#currentState === ZenBoostsChild.STATES.PICKER) {
this.disablePickerMode();
}
break;
case "ZenBoost:ToggleZapMode":
if (this.#currentState === ZenBoostsChild.STATES.NONE) {
this.#startZappingOverlay();
} else if (this.#currentState === ZenBoostsChild.STATES.ZAP) {
this.disableZapMode();
}
break;
case "ZenBoost:TogglePickerMode":
if (this.#currentState === ZenBoostsChild.STATES.NONE) {
this.#startPickingOverlay();
} else if (this.#currentState === ZenBoostsChild.STATES.PICKER) {
this.disablePickerMode();
}
break;
case "ZenBoost:ZapModeEnabled":
return this.#currentState === ZenBoostsChild.STATES.ZAP;
case "ZenBoost:ZapModeAny": {
const { boostData } = (await this.getWebsiteBoost())?.boostEntry || {};
return !!boostData?.zapSelectors?.length;
}
case "ZenBoost:SelectorPickerModeEnabled":
return this.#currentState === ZenBoostsChild.STATES.PICKER;
case "ZenBoost:OpenInspector":
this.sendAsyncMessage("ZenBoost:OpenInspector");
break;
}
return null;
}
/**
* From ZenGradientGenerator.mjs
* Helper function for hslToRgb conversion
*
* @param {number} p
* @param {number} q
* @param {number} t
*/
#hueToRgb(p, q, t) {
if (t < 0) {
t += 1;
}
if (t > 1) {
t -= 1;
}
if (t < 1 / 6) {
return p + (q - p) * 6 * t;
}
if (t < 1 / 2) {
return q;
}
if (t < 2 / 3) {
return p + (q - p) * (2 / 3 - t) * 6;
}
return p;
}
/**
* Aquires the boost data for this website
*
* @returns {object} Boost data for the current website
*/
getWebsiteBoost() {
const domain = this.browsingContext.topWindow?.location?.host;
if (!domain) {
return null;
}
return this.sendQuery("ZenBoost:GetBoostForDomain", domain);
}
/**
* Applies the boost settings for the current page if available.
*
* @param {boolean} unloadStyles - Indicates whether to unload styles.
*/
async #applyBoostForPageIfAvailable(unloadStyles = false) {
const browsingContext = this.browsingContext;
// Prevent applying boosts to iframes or non-top-level browsing contexts.
// It makes the tab crash if we try to load stylesheets into an iframe's
if (!browsingContext || browsingContext.parent !== null) {
return;
}
const boost = await this.getWebsiteBoost();
if (unloadStyles) {
this.#unloadCurrentStyleSheet();
}
if (boost) {
const { boostData } = boost.boostEntry;
if (boost.styleSheet) {
this.#loadStyleSheet(boost.styleSheet);
}
browsingContext.isZenBoostsInverted = boostData.smartInvert;
if (boostData.enableColorBoost) {
if (boostData.autoTheme) {
// Workspace color is converted to the HSL color space
let primaryGradientColor = boost.workspaceGradient[0]?.c ?? [
0, 0, 0.6,
];
boost.workspaceGradient.forEach(color => {
if (color.isPrimary) {
primaryGradientColor = this.#rgbToHsl(
color.c[0],
color.c[1],
color.c[2]
);
}
});
// Workspace color is converted back to rgb
// using the same modifiers as the color above
primaryGradientColor = this.#hslToRgb(
primaryGradientColor[0] / 360,
primaryGradientColor[1] * (1 - boostData.saturation),
0.1 + primaryGradientColor[2] * 0.9 * boostData.brightness
);
const rgbColor = primaryGradientColor;
const nsColor = this.#rgbToNSColor(
rgbColor,
(1 - boostData.contrast) * 255
);
browsingContext.zenBoostsData = nsColor;
} else {
let colorWheelColor = this.#hslToRgb(
boostData.dotAngleDeg / 360,
/* already is [0, 1] */
boostData.dotDistance * (1 - boostData.saturation),
/* lightness range from [0.1, 0.9] */
0.1 + boostData.dotDistance * 0.8 * boostData.brightness
);
const rgbColor = colorWheelColor;
const nsColor = this.#rgbToNSColor(
rgbColor,
(1 - boostData.contrast) * 255
);
browsingContext.zenBoostsData = nsColor;
}
return;
}
} else {
browsingContext.isZenBoostsInverted = false;
}
browsingContext.zenBoostsData = 0;
}
/**
* Loads the given stylesheet into the website
*
* @param {object} styleSheet The stylesheet
*/
#loadStyleSheet(styleSheet) {
const browsingContext = this.browsingContext;
styleSheet.uri = Services.io.newURI(styleSheet.uri);
if (this.#currentSheet?.uuid !== styleSheet.uuid) {
if (this.#currentSheet) {
this.#unloadCurrentStyleSheet();
}
browsingContext.window.windowGlobalChild.browsingContext.window.windowUtils.loadSheet(
styleSheet.uri,
AGENT_SHEET
);
this.#currentSheet = styleSheet;
}
}
/**
* Unloads the currently loaded stylesheet
*/
#unloadCurrentStyleSheet() {
const browsingContext = this.browsingContext;
if (this.#currentSheet && browsingContext) {
browsingContext.window.windowGlobalChild.browsingContext.window.windowUtils.removeSheet(
this.#currentSheet.uri,
AGENT_SHEET
);
this.#currentSheet = null;
}
}
async #startZappingOverlay() {
if (this.#currentState === ZenBoostsChild.STATES.ZAP) {
return;
}
this.#currentState = ZenBoostsChild.STATES.ZAP;
this.#overlay = new lazy.ZapOverlay(this.document, this);
this.#overlay.initialize();
this.#addEventListeners();
this.sendNotify("zap-state-update");
}
async #startPickingOverlay() {
if (this.#currentState === ZenBoostsChild.STATES.PICKER) {
return;
}
this.#currentState = ZenBoostsChild.STATES.PICKER;
this.#overlay = new lazy.SelectorComponent(
this.document,
this,
[], // No additional IDs needed
this.onPickerSelection.bind(this)
);
this.#overlay.initialize();
this.#addEventListeners();
this.sendNotify("selector-picker-state-update", "onenable");
}
onPickerSelection(cssSelector) {
this.sendNotify("selector-picker-picked", cssSelector);
}
addZapSelector(selector) {
const domain = this.browsingContext.topWindow?.location?.host;
this.sendQuery("ZenBoost:ZapSelector", {
action: "add",
selector,
domain,
});
}
removeZapSelector(selector) {
const domain = this.browsingContext.topWindow?.location?.host;
this.sendQuery("ZenBoost:ZapSelector", {
action: "remove",
selector,
domain,
});
}
async tempShowZappedElement(selector) {
this.document.querySelectorAll(selector).forEach(element => {
element.setAttribute("zen-zap-unhide", "true");
});
if (!this.#zappedElementsTempShown.includes(selector)) {
this.#zappedElementsTempShown.push(selector);
}
}
async tempHideZappedElement() {
this.#zappedElementsTempShown.forEach(selector => {
this.document.querySelectorAll(selector).forEach(element => {
element.removeAttribute("zen-zap-unhide");
});
});
this.#zappedElementsTempShown = [];
}
disableZapMode() {
if (this.#currentState === ZenBoostsChild.STATES.NONE) {
return;
}
this.#currentState = ZenBoostsChild.STATES.NONE;
this.#overlay?.tearDown();
this.#overlay = null;
this.#removeEventListeners();
this.sendNotify("zap-state-update");
}
disablePickerMode() {
if (this.#currentState === ZenBoostsChild.STATES.NONE) {
return;
}
this.#currentState = ZenBoostsChild.STATES.NONE;
this.#overlay?.tearDown();
this.#overlay = null;
this.#removeEventListeners();
this.sendNotify("selector-picker-state-update", "ondisable");
}
sendNotify(topic, msg = null) {
this.sendAsyncMessage("ZenBoost:Notify", { topic, msg });
}
}

View File

@@ -0,0 +1,185 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
gZenBoostsManager: "resource:///modules/zen/boosts/ZenBoostsManager.sys.mjs",
});
export class ZenBoostsParent extends JSWindowActorParent {
static OBSERVERS = [
"zen-boosts-update",
"zen-space-gradient-update",
"zen-boosts-disable-zap",
"zen-boosts-disable-picker",
];
// Topics the content child is allowed to forward to the observer service.
// Anything outside this set is rejected to prevent content-triggered
// notifications on unrelated chrome observers.
static ALLOWED_NOTIFY_TOPICS = new Set([
"zap-state-update",
"zap-list-update",
"selector-picker-state-update",
"selector-picker-picked",
]);
/**
* Creates a new ZenBoostsParent actor instance and sets up an observer
* for boost update notifications.
*/
constructor() {
super();
this._observe = this.observe.bind(this);
ZenBoostsParent.OBSERVERS.forEach(observe => {
Services.obs.addObserver(this._observe, observe);
});
}
/**
* Called when the actor is destroyed. Cleans up the observer.
*/
didDestroy() {
ZenBoostsParent.OBSERVERS.forEach(observe => {
Services.obs.removeObserver(this._observe, observe);
});
}
/**
* Observer callback that handles boost update notifications.
* Sends a message to child actors when boosts are updated.
*
* @param {object} subject - The subject of the notification.
* @param {string} topic - The topic of the notification.
*/
observe(subject, topic) {
switch (topic) {
case "zen-boosts-update":
case "zen-space-gradient-update":
this.sendAsyncMessage("ZenBoost:BoostDataUpdated", {
unloadStyles: true,
});
break;
case "zen-boosts-disable-zap":
this.sendAsyncMessage("ZenBoost:DisableZapMode");
break;
case "zen-boosts-disable-picker":
this.sendAsyncMessage("ZenBoost:DisablePickerMode");
break;
}
}
/**
* Handles messages received from child actors.
* Retrieves boost data for a domain when requested.
*
* @param {object} message - The message object containing name and data.
* @returns {Promise<object | null>} A promise that resolves to the boost data or null.
*/
async receiveMessage(message) {
switch (message.name) {
case "ZenBoost:OpenInspector": {
const { require } = ChromeUtils.importESModule(
"resource://devtools/shared/loader/Loader.sys.mjs"
);
const { gDevTools } = require("devtools/client/framework/devtools");
let win = Services.wm.getMostRecentWindow("navigator:browser");
let tab = win.gBrowser.selectedTab;
let toolbox = gDevTools.getToolboxForTab(tab);
if (toolbox) {
await gDevTools.closeToolboxForTab(tab);
} else {
await gDevTools.showToolboxForTab(tab, "inspector");
}
break;
}
case "ZenBoost:Notify": {
const { topic, msg } = message.data ?? {};
if (!ZenBoostsParent.ALLOWED_NOTIFY_TOPICS.has(topic)) {
console.warn(
`[ZenBoostsParent]: Rejected notify for disallowed topic: ${topic}`
);
break;
}
Services.obs.notifyObservers(null, topic, msg);
break;
}
case "ZenBoost:ZapSelector": {
const data = message.data;
if (!data.action) {
break;
}
if (!data.selector) {
break;
}
if (!data.domain) {
break;
}
if (data.action == "add") {
lazy.gZenBoostsManager.addZapSelectorToActive(
data.selector,
data.domain
);
} else if (data.action == "remove") {
lazy.gZenBoostsManager.removeZapSelectorToActive(
data.selector,
data.domain
);
} else if (data.action == "clear") {
lazy.gZenBoostsManager.clearZapSelectorsForActive(data.domain);
}
break;
}
case "ZenBoost:GetBoostForDomain": {
const domain = message.data;
const embedder = this.browsingContext.top.embedderElement;
if (!embedder || !domain) {
break;
}
const exists = lazy.gZenBoostsManager.registeredBoostForDomain(domain);
if (!exists) {
break;
}
const boost = lazy.gZenBoostsManager.loadActiveBoostFromStore(domain);
let workspaceGradient = [];
if (boost.boostEntry.boostData.autoTheme) {
const currentWorkspace =
await this.browsingContext.topChromeWindow.gZenWorkspaces.getActiveWorkspace();
workspaceGradient = currentWorkspace.theme.gradientColors;
}
const styleData =
await lazy.gZenBoostsManager.getStyleSheetForBoost(domain);
return {
...boost,
workspaceGradient,
styleSheet: styleData
? {
uuid: styleData.uuid,
uri: styleData.uri.spec,
}
: null,
};
}
default: {
console.warn(`[ZenBoostsParent]: Unknown message: ${message.name}`);
break;
}
}
return null;
}
}

14
src/zen/boosts/jar.inc.mn Normal file
View File

@@ -0,0 +1,14 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
content/browser/zen-styles/zen-boosts.css (../../zen/boosts/zen-boosts.css)
content/browser/zen-styles/content/zen-zap.css (../../zen/boosts/zen-zap.css)
content/browser/zen-styles/content/zen-selector.css (../../zen/boosts/zen-selector.css)
content/browser/zen-styles/zen-advanced-color-options.css (../../zen/boosts/zen-advanced-color-options.css)
# Windows
* content/browser/zen-components/windows/zen-boost-editor.xhtml (../../zen/boosts/zen-boost-editor.inc.xhtml)
# Images
content/browser/zen-images/boost-indicator.svg (../../zen/images/boost-indicator.svg)

28
src/zen/boosts/moz.build Normal file
View File

@@ -0,0 +1,28 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
EXTRA_JS_MODULES.zen.boosts += [
"ZenBoostsEditor.mjs",
"ZenBoostsManager.sys.mjs",
"ZenBoostStyles.sys.mjs",
"ZenSelectorComponent.sys.mjs",
"ZenZapDissolve.sys.mjs",
"ZenZapOverlayChild.sys.mjs",
]
FINAL_TARGET_FILES.actors += [
"actors/ZenBoostsChild.sys.mjs",
"actors/ZenBoostsParent.sys.mjs",
]
EXPORTS.mozilla += [
"nsZenBoostsBackend.h",
]
SOURCES += [
"nsZenBCOverrides.cpp",
"nsZenBoostsBackend.cpp",
]
FINAL_LIBRARY = "xul"

View File

@@ -0,0 +1,83 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsZenBoostsBackend.h"
#include "nsIXULRuntime.h"
#include "nsPresContext.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/MediaFeatureChange.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/DocumentInlines.h"
#include "mozilla/dom/BrowsingContext.h"
#define MARK_MEDIA_FEATURE_CHANGED(_pc) \
(_pc)->MediaFeatureValuesChanged( \
{mozilla::RestyleHint::RecascadeSubtree(), NS_STYLE_HINT_VISUAL, \
mozilla::MediaFeatureChangeReason::PreferenceChange}, \
mozilla::MediaFeatureChangePropagation::All);
#define TRIGGER_PRES_CONTEXT_RESTYLE() \
WalkPresContexts( \
[&](nsPresContext* aPc) { MARK_MEDIA_FEATURE_CHANGED(aPc); });
using BrowsingContext = mozilla::dom::BrowsingContext;
template <typename Callback>
void BrowsingContext::WalkPresContexts(Callback&& aCallback) {
PreOrderWalk([&](BrowsingContext* aContext) {
if (nsIDocShell* shell = aContext->GetDocShell()) {
if (RefPtr pc = shell->GetPresContext()) {
aCallback(pc.get());
}
}
});
}
static void RefreshBoostCacheIfMatchesCurrent(BrowsingContext* aChanged) {
auto* backend = zen::nsZenBoostsBackend::GetInstance();
if (!backend) {
return;
}
auto current = backend->GetCurrentBrowsingContext();
if (!current || current->Top() != aChanged) {
return;
}
backend->RefreshCachedBoostState();
}
/**
* @brief Called when the ZenBoostsData field is set on a browsing context.
* Triggers a restyle if the boost data has changed.
* @param aOldValue The previous value of the boost data.
*/
void BrowsingContext::DidSet(FieldIndex<IDX_ZenBoostsData>,
ZenBoostData aOldValue) {
MOZ_ASSERT(IsTop());
if (ZenBoostsData() == aOldValue) {
return;
}
RefreshBoostCacheIfMatchesCurrent(this);
PresContextAffectingFieldChanged();
TRIGGER_PRES_CONTEXT_RESTYLE();
}
/**
* @brief Called when the IsZenBoostsInverted field is set on a browsing
* context. Triggers a restyle if the value has changed.
* @param aOldValue The previous value of the IsZenBoostsInverted flag.
*/
void BrowsingContext::DidSet(FieldIndex<IDX_IsZenBoostsInverted>,
bool aOldValue) {
MOZ_ASSERT(IsTop());
if (IsZenBoostsInverted() == aOldValue) {
return;
}
RefreshBoostCacheIfMatchesCurrent(this);
PresContextAffectingFieldChanged();
TRIGGER_PRES_CONTEXT_RESTYLE();
}

View File

@@ -0,0 +1,382 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include <cmath>
#include <algorithm>
#include <cstdint>
#include "nsZenBoostsBackend.h"
#include "nsIXULRuntime.h"
#include "nsPresContext.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/ServoStyleConsts.h"
#include "mozilla/ServoStyleConstsInlines.h"
#include "mozilla/MediaFeatureChange.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/DocumentInlines.h"
#include "mozilla/dom/BrowsingContext.h"
#include "mozilla/StaticPrefs_zen.h"
// Lower bound applied to inverted channels so that pure white doesn't invert
// all the way to pure black, which makes inverted pages feel too dark.
#define INVERT_CHANNEL_FLOOR() \
(mozilla::StaticPrefs::zen_boosts_invert_channel_floor_AtStartup())
#define SHOULD_APPLY_BOOSTS_TO_ANONYMOUS_CONTENT() \
(!mozilla::StaticPrefs::zen_boosts_disable_on_anonymous_content_AtStartup())
#if defined(__clang__) || defined(__GNUC__)
# define ZEN_HOT_FUNCTION __attribute__((hot))
#else
# define ZEN_HOT_FUNCTION
#endif
// It's a bit of a hacky solution, but instead of using alpha as what it is
// (opacity), we use it to store contrast information for now.
// We do this primarily to avoid having to deal with WebIDL structs and
// serialization/deserialization between parent and content processes.
#define NS_GET_CONTRAST(_c) NS_GET_A(_c)
namespace zen {
nsZenAccentOklab nsZenBoostsBackend::mCachedAccent{0};
namespace {
/**
* @brief Converts an sRGB color component to linear space.
* @param c The sRGB color component value (0.0 to 1.0).
* @return The linear color component value.
*/
static inline float srgbToLinear(float c) {
return c <= 0.04045f ? c * (1.0f / 12.92f)
: std::pow((c + 0.055f) * (1.0f / 1.055f), 2.4f);
}
/**
* @brief Converts a linear color component to sRGB space.
* @param c The linear color component value.
* @return The sRGB color component value (0.0 to 1.0).
*/
static inline float linearToSrgb(float c) {
c = std::max(0.0f, c);
return c <= 0.0031308f ? 12.92f * c
: 1.055f * std::pow(c, 1.0f / 2.4f) - 0.055f;
}
/*
* @brief Fast approximation of the cube root of a number.
* @param x The input value.
* @return The approximate cube root of the input value.
*/
static inline float fastCbrt(float x) {
if (x == 0.0f) return 0.0f;
float a = std::abs(x);
union {
float f;
uint32_t i;
} u = {a};
u.i = u.i / 3 + 0x2a504a2e;
float y = u.f;
y = (2.0f * y + a / (y * y)) * (1.0f / 3.0f);
y = (2.0f * y + a / (y * y)) * (1.0f / 3.0f);
return x < 0.0f ? -y : y;
}
/**
* @brief Precomputes the Oklab values for a given accent color. This allows us
* to efficiently apply the accent color as a filter to other colors without
* having to convert the accent color from sRGB to Oklab space on every filter
* operation.
* @param aAccentColor The accent color in nscolor format.
* @return A struct containing the precomputed Oklab values and contrast factor
* for the accent color.
*/
ZEN_HOT_FUNCTION
inline static auto zenPrecomputeAccent(nscolor aAccentColor) {
constexpr float inv255 = 1.0f / 255.0f;
const float r = NS_GET_R(aAccentColor) * inv255;
const float g = NS_GET_G(aAccentColor) * inv255;
const float b = NS_GET_B(aAccentColor) * inv255;
const float lr = srgbToLinear(r);
const float lg = srgbToLinear(g);
const float lb = srgbToLinear(b);
const float l_ =
fastCbrt(0.4122214708f * lr + 0.5363325363f * lg + 0.0514459929f * lb);
const float m_ =
fastCbrt(0.2119034982f * lr + 0.6806995451f * lg + 0.1073969566f * lb);
const float s_ =
fastCbrt(0.0883024619f * lr + 0.2817188376f * lg + 0.6299787005f * lb);
return nsZenAccentOklab{
.accentNS = aAccentColor,
.accL = 0.2104542553f * l_ + 0.7936177850f * m_ - 0.0040720468f * s_,
.accA = 1.9779984951f * l_ - 2.4285922050f * m_ + 0.4505937099f * s_,
.accB = 0.0259040371f * l_ + 0.7827717662f * m_ - 0.8086757660f * s_,
.contrastFactor = NS_GET_CONTRAST(aAccentColor) * inv255,
};
}
/**
* @brief Applies a color filter to transform an original color toward an accent
* color. Preserves the original color's perceived luminance while shifting
* hue/chroma toward the accent. Uses the alpha channel of the accent color to
* store contrast information.
* @param aOriginalColor The original color to filter.
* @param aAccentColor The accent color to filter toward (alpha channel contains
* contrast value).
* @return The filtered color with transformations applied.
*/
[[nodiscard]] ZEN_HOT_FUNCTION static inline nscolor zenFilterColorChannel(
nscolor aOriginalColor, const nsZenAccentOklab& aAccent) {
const uint8_t oL = NS_GET_A(aOriginalColor);
const uint8_t contrast = NS_GET_CONTRAST(aAccent.accentNS);
if (oL == 0) {
return aOriginalColor;
}
constexpr float inv255 = 1.0f / 255.0f;
const float blendFactor = contrast * inv255;
// sRGB -> linear
const float lr = srgbToLinear(NS_GET_R(aOriginalColor) * inv255);
const float lg = srgbToLinear(NS_GET_G(aOriginalColor) * inv255);
const float lb = srgbToLinear(NS_GET_B(aOriginalColor) * inv255);
// Linear RGB -> LMS -> cube root -> Oklab (fused)
const float l_ =
fastCbrt(0.4122214708f * lr + 0.5363325363f * lg + 0.0514459929f * lb);
const float m_ =
fastCbrt(0.2119034982f * lr + 0.6806995451f * lg + 0.1073969566f * lb);
const float s_ =
fastCbrt(0.0883024619f * lr + 0.2817188376f * lg + 0.6299787005f * lb);
const float origL =
0.2104542553f * l_ + 0.7936177850f * m_ - 0.0040720468f * s_;
const float origA =
1.9779984951f * l_ - 2.4285922050f * m_ + 0.4505937099f * s_;
const float origB =
0.0259040371f * l_ + 0.7827717662f * m_ - 0.8086757660f * s_;
// Blend chroma toward accent
const float bA = origA + (aAccent.accA - origA) * blendFactor;
const float bB = origB + (aAccent.accB - origB) * blendFactor;
// Luminance: at low contrast stay near the original, the higher the contrast,
// the more we shift toward the accent luminance, but we never go fully to
// the accent luminance to preserve some of the original color's character.
const float lumDelta = aAccent.accL - origL;
const float fL =
origL + lumDelta * (blendFactor * aAccent.contrastFactor * 0.5f);
// Rotate hue in the Oklab a/b plane. Direction follows the luminance shift:
// pushing darker rotates clockwise ("right"), pushing lighter rotates the
// other way. Magnitude scales with blend strength so subtle accents stay
// subtle.
const float rotAngle = (lumDelta > 0.0f ? -1.0f : 1.0f) * blendFactor *
aAccent.contrastFactor * 0.25f;
const float cosR = std::cos(rotAngle);
const float sinR = std::sin(rotAngle);
const float fA = bA * cosR - bB * sinR;
const float fB = bA * sinR + bB * cosR;
// Oklab -> LMS
const float fl_ = fL + 0.3963377774f * fA + 0.2158037573f * fB;
const float fm_ = fL - 0.1055613458f * fA - 0.0638541728f * fB;
const float fs_ = fL - 0.0894841775f * fA - 1.2914855480f * fB;
// Cube
const float fl = fl_ * fl_ * fl_;
const float fm = fm_ * fm_ * fm_;
const float fs = fs_ * fs_ * fs_;
// LMS -> linear RGB
const float rF = 4.0767416621f * fl - 3.3077115913f * fm + 0.2309699292f * fs;
const float gF =
-1.2684380046f * fl + 2.6097574011f * fm - 0.3413193965f * fs;
const float bF =
-0.0041960863f * fl - 0.7034186147f * fm + 1.7076147010f * fs;
// Linear -> sRGB -> uint8
return NS_RGBA(static_cast<uint8_t>(std::clamp(
linearToSrgb(rF) * 255.0f + 0.5f, 0.0f, 255.0f)),
static_cast<uint8_t>(std::clamp(
linearToSrgb(gF) * 255.0f + 0.5f, 0.0f, 255.0f)),
static_cast<uint8_t>(std::clamp(
linearToSrgb(bF) * 255.0f + 0.5f, 0.0f, 255.0f)),
oL);
}
/**
* @brief Inverts a color by inverting each RGB channel while preserving
* perceived luminance. This is done by inverting the color and then shifting it
* based on the sum of the inverted channels.
* @param aColor The color to invert.
* @return The inverted color with luminance preservation.
*/
ZEN_HOT_FUNCTION
inline static nscolor zenInvertColorChannel(nscolor aColor) {
const auto r = NS_GET_R(aColor);
const auto g = NS_GET_G(aColor);
const auto b = NS_GET_B(aColor);
const auto a = NS_GET_A(aColor);
if (a == 0) {
// Skip processing fully transparent colors since they won't be visible and
// we want to avoid unnecessary computations.
return aColor;
}
const auto rInv = 255 - r;
const auto gInv = 255 - g;
const auto bInv = 255 - b;
const auto max = std::max({rInv, gInv, bInv});
const auto min = std::min({rInv, gInv, bInv});
const auto sum = max + min;
const auto rShifted = sum - rInv;
const auto gShifted = sum - gInv;
const auto bShifted = sum - bInv;
// Compress the channel range into [FLOOR, 255] so dark inversions are
// lifted while light inversions are left untouched. This preserves hue
// since all three channels are scaled by the same factor.
const auto channelFloor = INVERT_CHANNEL_FLOOR();
const uint32_t range = 255 - channelFloor;
const auto lift = [channelFloor, range](uint8_t c) -> uint8_t {
return static_cast<uint8_t>(channelFloor + (c * range) / 255);
};
return NS_RGBA(lift(rShifted), lift(gShifted), lift(bShifted), a);
}
/**
* @brief Retrieves the current boost data from the browsing context. When
* called without aPresContext, reads the precomputed cache populated on
* presshell entry; otherwise resolves from the supplied PresContext.
*/
ZEN_HOT_FUNCTION
inline static void GetZenBoostsDataFromBrowsingContext(
ZenBoostData* aData, bool* aIsInverted,
nsPresContext* aPresContext = nullptr) {
auto zenBoosts = nsZenBoostsBackend::GetInstance();
if (!zenBoosts || (zenBoosts->mCurrentFrameIsAnonymousContent &&
!SHOULD_APPLY_BOOSTS_TO_ANONYMOUS_CONTENT())) {
return;
}
if (!aPresContext) {
*aData = zenBoosts->mCachedCurrentAccent;
*aIsInverted = zenBoosts->mCachedCurrentInverted;
return;
}
mozilla::dom::BrowsingContext* browsingContext = nullptr;
if (auto document = aPresContext->Document()) {
browsingContext = document->GetBrowsingContext();
}
if (!browsingContext) {
return;
}
browsingContext = browsingContext->Top();
*aData = browsingContext->ZenBoostsData();
*aIsInverted = browsingContext->IsZenBoostsInverted();
}
} // namespace
static mozilla::StaticAutoPtr<nsZenBoostsBackend> sZenBoostsBackend;
auto nsZenBoostsBackend::GetInstance() -> nsZenBoostsBackend* {
if (!XRE_IsContentProcess()) {
// Zen boosts are only supported in content, so if we're in the parent
// process, just return null.
return nullptr;
}
if (!sZenBoostsBackend) {
sZenBoostsBackend = new nsZenBoostsBackend();
mozilla::ClearOnShutdown(&sZenBoostsBackend);
}
return sZenBoostsBackend.get();
}
auto nsZenBoostsBackend::onPresShellEntered(mozilla::dom::Document* aDocument)
-> void {
if (auto displayDoc = aDocument->GetDisplayDocument()) {
onPresShellEntered(displayDoc);
return;
}
// Note that aDocument can be null when entering anonymous content frames.
// We explicitly do this to prevent applying boosts to anonymous content, such
// as devtools or screenshots.
mozilla::dom::BrowsingContext* browsingContext =
aDocument ? aDocument->GetBrowsingContext() : nullptr;
if (!browsingContext) {
return;
}
mCurrentBrowsingContext = browsingContext;
RefreshCachedBoostState();
}
auto nsZenBoostsBackend::RefreshCachedBoostState() -> void {
if (!mCurrentBrowsingContext) {
mCachedCurrentAccent = 0;
mCachedCurrentInverted = false;
return;
}
auto top = mCurrentBrowsingContext->Top();
mCachedCurrentAccent = top->ZenBoostsData();
mCachedCurrentInverted = top->IsZenBoostsInverted();
}
[[nodiscard]] ZEN_HOT_FUNCTION auto
nsZenBoostsBackend::FilterColorFromPresContext(nscolor aColor,
nsPresContext* aPresContext)
-> nscolor {
ZenBoostData accentNS = 0;
bool invertColors = false;
GetZenBoostsDataFromBrowsingContext(&accentNS, &invertColors, aPresContext);
if (accentNS) {
if (mCachedAccent.accentNS != accentNS) {
mCachedAccent = zenPrecomputeAccent(accentNS);
}
// Apply a filter-like tint:
// - Preserve the original color's perceived luminance
// - Map hue/chroma toward the accent by scaling the accent's RGB
// to match the original luminance
// - Keep the original alpha
// Convert both colors to nscolor to access channels
aColor = zenFilterColorChannel(aColor, mCachedAccent);
}
if (invertColors) {
aColor = zenInvertColorChannel(aColor);
}
return aColor;
}
[[nodiscard]] ZEN_HOT_FUNCTION auto nsZenBoostsBackend::ResolveStyleColor(
mozilla::StyleAbsoluteColor aColor) -> mozilla::StyleAbsoluteColor {
const auto resultColor = FilterColorFromPresContext(aColor.ToColor());
return mozilla::StyleAbsoluteColor::FromColor(resultColor);
}
[[nodiscard]] ZEN_HOT_FUNCTION auto nsZenBoostsBackend::ResolveStyleColor(
nscolor aColor) -> nscolor {
if (NS_GET_A(aColor) == 0) {
// Skip processing fully transparent colors since they won't be visible and
// we want to avoid unnecessary computations. This also prevents issues with
// using the alpha channel for contrast information in the accent color.
return aColor;
}
return FilterColorFromPresContext(aColor);
}
} // namespace zen

View File

@@ -0,0 +1,106 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef mozilla_ZenBoostsBackend_h_
#define mozilla_ZenBoostsBackend_h_
#include "nsColor.h"
#include "nsPresContext.h"
#include "mozilla/RefPtr.h"
#define ZEN_BOOSTS_BACKEND_CONTRACTID "@mozilla.org/zen/boosts-backend;1"
using ZenBoostData = nscolor; // For now, Zen boosts data is just a color.
namespace zen {
struct nsZenAccentOklab {
nscolor accentNS;
float accL, accA, accB;
float contrastFactor;
};
class nsZenBoostsBackend final {
public:
explicit nsZenBoostsBackend() = default;
~nsZenBoostsBackend() = default;
/**
* Indicates whether the current frame being rendered is for anonymous
* content.
*/
bool mCurrentFrameIsAnonymousContent = false;
/**
* @brief Resolve a StyleAbsoluteColor to take into account Zen boosts.
* @param aColor The color to resolve.
* @return The resolved color with Zen boost filters applied, or the original
* color if no boost is active.
* @see StyleColor::ResolveColor for reference.
*/
static auto ResolveStyleColor(mozilla::StyleAbsoluteColor aColor)
-> mozilla::StyleAbsoluteColor;
/**
* @see ResolveStyleColor for reference.
*/
static auto ResolveStyleColor(nscolor aColor) -> nscolor;
/**
* @brief Filter a color based on the current Zen boost settings.
* @param aColor The color to filter.
* @param aPresContext The presentation context to use for filtering.
* @return The filtered color.
*/
static auto FilterColorFromPresContext(nscolor aColor,
nsPresContext* aPresContext = nullptr)
-> nscolor;
/**
* @brief Called when a presshell is entered during rendering.
* @param aDocument The document associated with the presshell being entered.
*/
auto onPresShellEntered(mozilla::dom::Document* aDocument) -> void;
/**
* @brief Refresh the cached boost state from the current top BrowsingContext.
* Called from onPresShellEntered and from BrowsingContext::DidSet hooks when
* the underlying boost fields change.
*/
auto RefreshCachedBoostState() -> void;
[[nodiscard]]
inline auto GetCurrentBrowsingContext() const {
return mCurrentBrowsingContext;
}
/**
* Cached boost data for the current top BrowsingContext, refreshed on
* presshell entry and on DidSet hooks. Read by the per-color hot path so
* that boost-off pages don't pay for a BrowsingContext walk on every color
* resolve.
*/
ZenBoostData mCachedCurrentAccent = 0;
bool mCachedCurrentInverted = false;
private:
/**
* The presshell of the current document being rendered.
*/
RefPtr<mozilla::dom::BrowsingContext> mCurrentBrowsingContext;
static nsZenAccentOklab mCachedAccent;
public:
/**
* @brief Get the singleton instance of the ZenBoostsBackend.
* @return The singleton instance.
*/
static auto GetInstance() -> nsZenBoostsBackend*;
};
} // namespace zen
#endif

View File

@@ -0,0 +1,43 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#zen-boost-advanced-color-options-panel {
color-scheme: light;
--panel-padding: 15px;
& p {
color: #3a3a3b;
}
& input {
width: 185px;
appearance: none !important;
height: 4px;
background-color: #b5b5b9;
border-radius: 2px;
margin-top: 10px;
}
&::part(content) {
display: flex;
flex-direction: column;
gap: 24px;
}
}
#zen-boost-advanced-color-options-panel input::-moz-range-thumb {
width: 18px;
height: 18px;
background-color: #e6e5ea;
border: solid 1px #d1d0d5;
border-radius: 100%;
}
#zen-boost-advanced-color-options-panel input::-moz-range-thumb:active {
background-color: #cbcad0;
}

View File

@@ -0,0 +1,153 @@
#filter substitution
<?xml version="1.0"?>
# -*- Mode: HTML -*-
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<!DOCTYPE window>
<html
xmlns:html="http://www.w3.org/1999/xhtml"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns="http://www.w3.org/1999/xhtml"
id="zenBoostWindow"
windowtype="Zen:BoostEditor"
alwaysontop="true"
customtitlebar="true"
sizemode="normal"
scrolling="false"
macanimationtype="document"
windowsmica="true"
data-l10n-sync="true">
<head>
<link rel="stylesheet" href="chrome://browser/skin/" />
<link rel="stylesheet" href="chrome://global/skin/global.css" />
<link rel="stylesheet" href="chrome://browser/content/zen-styles/zen-boosts.css" />
<link rel="stylesheet" href="chrome://browser/content/zen-styles/zen-buttons.css" />
<link rel="stylesheet" href="chrome://browser/content/zen-styles/zen-theme.css" />
<link rel="stylesheet" href="chrome://browser/content/zen-styles/zen-animations.css" />
<link rel="stylesheet" href="chrome://browser/content/zen-styles/zen-panel-ui.css" />
<link rel="stylesheet" href="chrome://browser/skin/zen-icons/icons.css" />
<link rel="stylesheet" href="chrome://browser/content/zen-styles/zen-advanced-color-options.css" />
<link rel="localization" href="browser/zen-boosts.ftl"/>
<!-- Loading in the window module -->
<script>
const { nsZenBoostEditor } = ChromeUtils.importESModule( "resource:///modules/zen/boosts/ZenBoostsEditor.mjs" );
window.addEventListener("load", () => {
window.boostEditor = new nsZenBoostEditor(document, window.domain, window, window.openerWindow);
});
</script>
</head>
<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<vbox flex="1" id="zen-boost-editor-root">
<hbox id="zen-boost-head-wrapper">
<button id="zen-boost-close" class="subviewbutton mod-button title-button"></button>
<hbox id="zen-boost-name" flex="1">
<hbox id="zen-boost-name-container">
<html:p id="zen-boost-name-text"></html:p>
</hbox>
</hbox>
<button data-l10n-id="zen-boost-shuffle" id="zen-boost-shuffle" class="subviewbutton mod-button title-button"></button>
</hbox>
<vbox flex="1" id="zen-boost-filter-wrapper">
<hbox class="zen-boost-color-picker-gradient zen-boost-panel-disabled">
<button data-l10n-id="zen-boost-magic-theme" id="zen-boost-magic-theme" class="subviewbutton mod-button"></button>
<html:div class="zen-boost-color-picker-dot"></html:div>
<html:div class="zen-boost-color-picker-circle"></html:div>
</hbox>
<hbox flex="1" id="zen-boost-toolbar-wrapper-colors">
<button data-l10n-id="zen-boost-invert" id="zen-boost-invert" class="subviewbutton mod-button small"></button>
<button data-l10n-id="zen-boost-controls" id="zen-boost-controls" class="subviewbutton mod-button small"></button>
<button data-l10n-id="zen-boost-disable" id="zen-boost-disable" class="subviewbutton mod-button small"></button>
</hbox>
<html:div id="zen-boost-font-wrapper">
<vbox id="zen-boost-font-grid">
<!-- Font buttons will be injected here -->
</vbox>
<html:div class="visible-separator"></html:div>
<hbox flex="1" id="zen-boost-font-toolbar">
<html:select name="font" id="zen-boost-font-select" class="mod-button">
<!-- Additional font options will be injected here -->
</html:select>
<button data-l10n-id="zen-boost-text-case-toggle" id="zen-boost-text-case-toggle" class="subviewbutton mod-button"></button>
</hbox>
</html:div>
<button id="zen-boost-zap" class="subviewbutton mod-button big-button toggleable-button">
<html:p data-l10n-id="zen-boost-zap" id="zen-boost-zap-text"></html:p>
<html:p id="zen-boost-zap-value"></html:p>
</button>
<button id="zen-boost-code" class="subviewbutton mod-button big-button">
<html:p data-l10n-id="zen-boost-code" id="zen-boost-code-text"></html:p>
</button>
<hbox flex="1" id="zen-boost-toolbar-wrapper">
<button data-l10n-id="zen-boost-save" id="zen-boost-save" class="subviewbutton mod-button med"></button>
<button data-l10n-id="zen-boost-load" id="zen-boost-load" class="subviewbutton mod-button med"></button>
</hbox>
</vbox>
</vbox>
<vbox flex="1" id="zen-boost-code-editor-root">
<hbox id="zen-boost-code-top-bar">
<button id="zen-boost-back" class="subviewbutton mod-button big-button">
<html:p data-l10n-id="zen-boost-back" id="zen-boost-back-text"></html:p>
</button>
</hbox>
<vbox flex="1" id="zen-boost-code-editor">
</vbox>
<hbox id="zen-boost-code-bottom-bar">
<button data-l10n-id="zen-boost-css-picker" id="zen-boost-css-picker" class="subviewbutton mod-button big-button toggleable-button"></button>
<button data-l10n-id="zen-boost-css-inspector" id="zen-boost-css-inspector" class="subviewbutton mod-button big-button"></button>
</hbox>
</vbox>
<popupset id="mainPopupSet">
<panel type="arrow" popupalign="topmiddle" id="zen-boost-advanced-color-options-panel">
<vbox>
<p data-l10n-id="zen-bootst-color-contrast"></p>
<html:input id="zen-boost-color-contrast" type="range" min="0.05" max="0.9" value="0.75" step="0.01"/>
</vbox>
<vbox>
<p data-l10n-id="zen-bootst-color-brightness"></p>
<html:input id="zen-boost-color-brightness" type="range" min="0" max="1" value="0.5" step="0.01"/>
</vbox>
<vbox>
<p data-l10n-id="zen-bootst-color-original-saturation"></p>
<html:input id="zen-boost-color-saturation" type="range" min="0" max="1" value="0.5" step="0.01"/>
</vbox>
</panel>
</popupset>
<menupopup id="zenBoostContextMenu">
<menuitem data-l10n-id="zen-boost-edit-rename" id="zen-boost-edit-rename" command="cmd_zenBoostEditName" />
<menuitem data-l10n-id="zen-boost-edit-shuffle" id="zen-boost-edit-shuffle" command="cmd_zenBoostShuffle" />
<menuitem data-l10n-id="zen-boost-edit-reset" id="zen-boost-edit-reset" command="cmd_zenBoostReset" />
<menuseparator/>
<menuitem data-l10n-id="zen-boost-edit-delete" id="zen-boost-edit-delete" command="cmd_zenBoostDelete" />
</menupopup>
<commandset id="zenBoostCommandSet">
<command id="cmd_zenBoostEditName"
oncommand="window.boostEditor.editBoostName();" />
<command id="cmd_zenBoostShuffle"
oncommand="window.boostEditor.shuffleBoost();" />
<command id="cmd_zenBoostReset"
oncommand="window.boostEditor.resetBoost();" />
<command id="cmd_zenBoostDelete"
oncommand="window.boostEditor.deleteBoost();" />
</commandset>
</html:body>
</html>

View File

@@ -0,0 +1,805 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#zenBoostWindow {
/* For the mica effect we want a white tint */
@media not (-moz-platform: linux) {
color-scheme: light;
}
appearance: none;
border: none;
}
.source-editor-frame {
max-width: 1000px;
max-height: 1000px;
width: 100%;
height: 100%;
border: none;
}
#zen-boost-code-top-bar {
top: 0;
z-index: 10;
width: 100%;
height: 40px;
background-color: #f6f6f8;
@media (-moz-windows-mica) {
background-color: #f6f6f8c0;
}
border: solid 0 #ededef;
border-bottom-width: 1px;
}
#zen-boost-name-container {
background-image: url("chrome://global/skin/icons/arrow-down.svg");
background-repeat: no-repeat;
background-position: calc(100% - 2px) calc(50% + 2px);
background-size: 12px 12px;
padding: 4px 18px 4px 2px;
margin-left: 4px;
border-radius: 8px;
opacity: 0.75;
background-color: transparent;
transition:
0.25s opacity cubic-bezier(0.075, 0.82, 0.165, 1),
0.25s background-color cubic-bezier(0.075, 0.82, 0.165, 1);
-moz-window-dragging: no-drag;
}
#zen-boost-name-container:hover {
opacity: 0.8;
background-color: #aaaaaa22;
}
#zen-boost-code-bottom-bar {
bottom: 0;
z-index: 10;
width: 100%;
height: 60px;
background-color: #f6f6f8;
@media (-moz-windows-mica) {
background-color: #f6f6f8c0;
}
border: solid 0 #ededef;
border-top-width: 1px;
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
}
#zen-boost-code-editor {
width: 100%;
height: calc(100% - 100px);
margin-top: 0;
margin-bottom: 0;
}
body {
overflow: clip;
display: flex;
margin: 0;
width: 100%;
}
#zen-boost-editor-root,
#zen-boost-code-editor-root {
user-select: none;
width: 100%;
height: 100%;
padding: 0;
margin: 0;
& button {
border-radius: 6px;
@media (-moz-platform: macos) {
border-radius: 10px;
}
padding: 0 !important;
min-width: fit-content !important;
border: solid 6px transparent;
transition: background 0.1s cubic-bezier(0.075, 0.82, 0.165, 1);
appearance: none;
&:active {
transform: none !important;
}
&:hover {
opacity: 0.8;
}
}
& .button-text {
display: none;
}
&[disabled] {
opacity: 0.5;
cursor: not-allowed;
}
}
.subviewbutton {
color: #3a3a3b;
}
#zen-boost-editor-view {
gap: 10px;
}
#zen-boost-code-top-bar {
-moz-window-dragging: drag; /* Allow dragging the window with the custom titlebar for the code view */
}
#zen-boost-head-wrapper {
-moz-window-dragging: drag; /* Allow dragging the window with the custom titlebar */
height: 40px;
min-height: 40px;
max-height: 40px;
align-items: center;
background-color: #f6f6f8;
border: solid 1px #e7e7e7ab;
@media (-moz-windows-mica) {
background-color: #f6f6f8c0;
border: none;
}
display: flex;
flex-direction: row-reverse;
@media (-moz-platform: macos) {
flex-direction: row;
}
& button {
height: 24px !important;
width: 24px !important;
aspect-ratio: 1/1 !important;
background-color: transparent;
opacity: 0.45;
transition: 0.25s opacity cubic-bezier(0.075, 0.82, 0.165, 1);
&:hover {
opacity: 0.6;
}
}
& #zen-boost-name {
transition: color 0.2s ease-in-out;
background-color: transparent;
border: none;
outline: none;
appearance: none;
display: flex;
align-items: center;
}
}
#zen-boost-name-text {
width: 100%;
justify-content: center;
font-size: 100%;
font-weight: 600;
max-width: 100px;
overflow: hidden;
}
#zen-boost-name-text {
text-align: center;
margin: auto !important;
height: min-content;
}
#zen-boost-font-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 0;
width: 100%;
padding: 2px;
& button {
padding: auto;
margin: auto;
background-color: transparent;
}
}
#zen-boost-filter-wrapper {
padding: 20px;
gap: 14px;
padding-top: 10px;
background-color: #fcfcfe;
-moz-window-dragging: drag;
& > * {
-moz-window-dragging: no-drag;
}
}
.big-button {
width: 100% !important;
margin: auto;
text-indent: 6px;
vertical-align: middle;
font-size: 10pt;
list-style-position: right;
& p {
width: 75%;
margin: auto;
}
}
#zen-boost-back {
width: 60px !important;
margin: 5px;
left: 5px;
display: flex;
flex-direction: row-reverse;
& hbox {
scale: 0.75;
margin-right: -7px;
margin-top: 2px;
}
}
#zen-boost-code-bottom-bar button {
content: "";
width: auto !important;
aspect-ratio: 1/1 !important;
margin: 5px;
left: 5px;
}
#zen-boost-zap {
& #zen-boost-zap-value {
text-align: right;
right: 8px;
margin: auto;
position: relative;
}
& hbox {
margin-right: 8px;
}
}
#zen-boost-zap[hideicon="true"] {
list-style: none;
list-style-type: none;
& hbox {
margin-right: 0;
}
}
#zen-boost-size {
list-style-type: none;
& #zen-boost-size-value {
text-align: right;
right: 8px;
margin: auto;
position: relative;
}
}
.zen-boost-panel-disabled {
filter: grayscale(1);
}
.title-button {
scale: 0.9;
}
.mod-button {
height: 38px !important;
border-radius: 6px;
@media (-moz-platform: macos) {
border-radius: 12px;
}
background-color: #ebebed;
transition:
0.4s background-color cubic-bezier(0.075, 0.82, 0.165, 1),
0.4s opacity cubic-bezier(0.075, 0.82, 0.165, 1),
0.4s filter cubic-bezier(0.075, 0.82, 0.165, 1) !important;
&:hover {
opacity: 0.85;
}
&:active {
transform: none !important;
opacity: 0.9;
filter: brightness(0.9);
}
}
#zen-boost-text-case-toggle[case-mode="none"] {
opacity: 0.5;
&:hover {
opacity: 0.6 !important;
}
}
#zen-boost-text-case-toggle:not([case-mode="none"]) {
background-color: #ebebed;
font-weight: 600 !important;
}
#zen-boost-code-top-bar .mod-button {
height: auto !important;
}
#zen-boost-code-bottom-bar button {
margin: 0;
}
#zen-boost-name {
height: 26px;
font-size: 8pt;
text-indent: 2px;
vertical-align: middle;
color: #3a3a3b;
justify-content: center;
}
#zen-boost-close {
margin-left: 0;
margin-right: 6px;
@media (-moz-platform: macos) {
margin-left: 6px;
margin-right: 0;
}
}
#zen-boost-shuffle {
margin-right: 0;
margin-left: 4px;
@media (-moz-platform: macos) {
margin-right: 10px;
margin-left: 0;
}
}
.zen-boost-button-active {
background-color: #3a3a3a;
color: #fcfcfe;
}
.zen-boost-button-active:hover {
background-color: #5b5b5c;
}
.zen-boost-button-active-transparent {
opacity: 0.35;
}
.zen-boost-button-active-transparent:hover {
opacity: 0.5 !important;
}
#zen-boost-magic-theme {
width: 18px;
height: 24px !important;
margin-top: 12px;
z-index: 4;
box-shadow: 0 2px 4px #00000010;
position: relative;
margin-left: auto;
margin-right: auto;
& image {
width: 12px;
}
&:not(.zen-boost-button-active) {
background: white;
}
}
#zen-boost-toolbar-wrapper,
#zen-boost-toolbar-wrapper-colors {
width: 100%;
margin: auto;
justify-content: center;
align-items: center;
gap: 8px;
& .small {
margin: 0;
flex: 1 1 30%;
}
& .med {
margin: 0;
flex: 1 1 50%;
}
}
#zen-boost-font-select {
width: 95px;
height: 20px !important;
transition: 0.2s opacity ease-in-out;
color: #727272;
background: none;
font-size: 9pt;
padding: 0;
text-indent: 2px;
margin-left: 8px;
margin-right: 3px;
margin-top: 8px;
opacity: 0.75;
&:hover {
opacity: 1;
}
&[has-selection="true"] {
opacity: 1;
background-color: #ebebed;
}
}
#zen-boost-font-toolbar {
display: flex;
flex-direction: row;
justify-content: space-between;
& button {
border-radius: 6px;
min-width: fit-content;
height: 20px !important;
margin-left: 8px;
margin-right: 8px;
margin-top: 8px;
background-color: transparent;
}
}
#zen-boost-font-wrapper {
box-shadow: 0 2px 6px rgba(0,0,0,.15);
background-color: #ffffff;
border-radius: 6px;
@media (-moz-platform: macos) {
border-radius: 8px;
}
padding-top: 4px;
padding-bottom: 4px;
display: flex;
flex-direction: column;
& .visible-separator {
content: "";
height: 1px;
width: auto;
margin-right: 12px;
margin-left: 12px;
margin-top: 2px;
opacity: 0.25;
border-top: solid 1px #9a9a9a;
}
& button {
filter: none;
color: rgba(0, 0, 0, 0.6);
border-radius: 50px;
transition: 0.1s transform ease;
font-size: 12px;
padding: 2px;
align-items: center;
justify-content: center;
display: flex;
height: 26px;
width: 26px;
transform-origin: center center;
&:hover {
background-color: #9a9a9a30;
transform: scale(1.1);
}
}
}
@property --mod-button-c1 {
syntax: "<color>";
inherits: false;
initial-value: #ebebed;
}
@property --mod-button-c2 {
syntax: "<color>";
inherits: false;
initial-value: #ebebed;
}
.mod-button[mode="orange"] {
color: #e3e9e4;
--mod-button-c1: #ffbb5d;
--mod-button-c2: #ffa01d;
}
.mod-button[mode="orange-red"] {
color: #e3e9e4;
--mod-button-c1: #ff8758;
--mod-button-c2: #ff5b1b;
}
.mod-button[mode="red"] {
color: #e3e9e4;
--mod-button-c1: #ff595f;
--mod-button-c2: #ff121b;
}
.mod-button[mode="blue"] {
color: #e3e9e4;
--mod-button-c1: #6650fc;
--mod-button-c2: #4125ff;
}
.mod-button[mode] {
background: linear-gradient(180deg, var(--mod-button-c1) 0%, var(--mod-button-c2) 100%) border-box;
transition:
0.4s background-color cubic-bezier(0.075, 0.82, 0.165, 1),
0.4s opacity cubic-bezier(0.075, 0.82, 0.165, 1),
0.4s filter cubic-bezier(0.075, 0.82, 0.165, 1),
0.75s --mod-button-c1 cubic-bezier(0.075, 0.82, 0.165, 1),
0.55s --mod-button-c2 cubic-bezier(0.075, 0.82, 0.165, 1) !important;
}
#zen-boost-back {
background-color: transparent;
&:hover {
background-color: #e3e3e6;
}
}
#zen-boost-code-editor-root button {
opacity: 0.6;
}
.toggleable-button[enabled="true"] {
filter: invert();
opacity: 0.9;
&:active {
filter: invert();
opacity: 0.8;
}
}
.zen-boost-color-picker-gradient::after {
content: "";
position: absolute;
inset: 0;
border-radius: 8px;
margin: 8px;
z-index: 2;
background: #fbfbfdea;
background-position: -23px -23px;
backdrop-filter: saturate(2) blur(15px);
background-size: 6px 6px;
background-image: radial-gradient(#e3e9e4, 1px, transparent 0);
@media (-moz-platform: macos) {
background-size: 4px 4px;
background-image: radial-gradient(#e3e9e4 0.5px, transparent 0);
}
}
.zen-boost-color-picker-gradient::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
z-index: 1;
background: conic-gradient(
rgba(255, 0, 0, 1) 0%,
rgba(255, 162, 0, 1) 10%,
rgba(255, 242, 0, 1) 20%,
rgba(89, 255, 0, 1) 30%,
rgba(0, 255, 128, 1) 40%,
rgba(0, 255, 247, 1) 50%,
rgba(0, 89, 255, 1) 60%,
rgba(8, 0, 255, 1) 70%,
rgba(204, 0, 255, 1) 80%,
rgba(255, 0, 144, 1) 90%,
rgba(255, 0, 8, 1) 100%
)
border-box;
pointer-events: none;
}
.zen-boost-color-picker-gradient {
position: relative;
overflow: hidden;
border-radius: 16px;
box-shadow: 0 4px 12px #00000021;
width: 100%;
aspect-ratio: 1 /1;
margin: 10px 0 4px 0;
min-height: calc(var(--panel-width) - var(--panel-padding) * 2 - 2px);
& [animated="true"] {
transition: all 0.4s cubic-bezier(0.14, 1.43, 0.56, 1.01) !important;
}
& .zen-boost-color-picker-circle {
position: absolute;
z-index: 3;
transform-origin: center center;
transform: translate(-50%, -50%);
opacity: 0;
transition: opacity 0.4s ease;
left: 50%;
top: 50%;
outline: solid 1px #9a9a9a88;
border-radius: 100%;
}
& .zen-boost-color-picker-dot {
box-shadow: 0 2px 4px #00000022;
position: absolute;
z-index: 4;
width: 24px;
height: 24px;
border-radius: 50%;
background: var(--zen-theme-picker-dot-color);
@media (-prefers-color-scheme: dark) {
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
}
cursor: pointer;
border: 3px solid #ffffff;
transform: translate(-50%, -50%);
pointer-events: none;
transform-origin: top left;
&:first-of-type {
width: 32px;
height: 32px;
border-width: 3px;
pointer-events: all;
transition: transform 0.2s;
z-index: 999;
&:hover {
transform: scale(1.05) translate(-50%, -50%);
}
}
&[dragging="true"] {
transform: scale(1.2) translate(-50%, -50%);
}
}
}
#zen-boost-editor-root:hover {
& .zen-boost-color-picker-circle {
opacity: 0.4;
}
}
.zen-boost-font-button-active {
background-color: #5454572f !important;
}
.zen-boost-font-button-active:hover {
background-color: #5b5b5c28 !important;
}
/** Import animation */
/** From zen-single-components.css:211 */
@property --background-top {
syntax: "<percentage>";
inherits: false;
initial-value: -50%;
}
#import-animation {
position: fixed;
width: 100%;
height: 100%;
background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5) 40%, rgba(255, 255, 255, 0.6) 60%, transparent 100%);
left: 0;
z-index: 999;
opacity: 0;
transform: translateY(-50%);
pointer-events: none;
}
#import-animation-shadow {
position: fixed;
mix-blend-mode: multiply;
background: #00000040;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 998;
opacity: 0;
pointer-events: none;
}
#import-animation-border {
position: absolute;
border-radius: var(--zen-border-radius);
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 1000;
}
#import-animation-border::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
padding: 4px; /* thickness of border */
z-index: 1000;
background: radial-gradient(ellipse 100% 50% at center var(--background-top), rgba(255, 255, 255, 0.9) 80%, transparent);
/* Punch out the inside once (not animated) */
mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask-composite: exclude;
}

View File

@@ -0,0 +1,200 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
@keyframes select-component-in {
0% {
scale: 0;
opacity: 0;
}
100% {
scale: 1;
opacity: 1;
}
}
#select-component {
width: min-content;
font-family: system-ui;
height: 98px;
pointer-events: auto;
transform-origin: top;
overflow: hidden;
position: fixed;
z-index: 9999;
background-color: #f5f7fb;
margin: 4px;
border-radius: 12px;
@media (-moz-platform: macos) {
border-radius: 24px;
}
box-shadow: 0 0 20px#00000050;
scale: 1;
opacity: 1;
@media not (-moz-platform: windows) {
&[is-appearing="true"] {
scale: 0;
opacity: 0;
animation: 0.4s select-component-in cubic-bezier(0.14, 1.43, 0.56, 1.01) forwards;
}
}
& input {
width: fit-content;
min-width: 75px;
height: 40px;
border-radius: 6px;
@media (-moz-platform: macos) {
border-radius: 12px;
}
padding-left: 10px;
padding-right: 10px;
font-size: 11pt;
transition:
0.15s filter ease,
0.1s box-shadow ease-out,
scale 0.35s cubic-bezier(0.11, 1.6, 0.63, 1);
&:hover {
filter: brightness(1.3);
}
&:active {
scale: 0.95;
}
}
}
#highlight-shadow {
z-index: 999;
position: fixed;
width: 100%;
height: 100%;
content: "";
background-color: #00000044;
}
#highlight-container {
z-index: 1000;
position: fixed;
width: 100%;
height: 100%;
content: "";
& .highlight {
position: fixed;
border: 1px solid #e9dd38;
background-color: #c2b72046;
border-radius: 6px;
content: "";
}
}
#select-controls {
display: flex;
gap: 10px;
padding: 10px;
@media not (-moz-platform: windows) {
gap: 12px;
padding: 12px;
}
}
#select-related {
appearance: none;
text-align: left;
width: 250px !important;
--related-elements-value: 100%;
background: linear-gradient(to top, rgb(247, 66, 0), rgb(245, 134, 86));
border: none;
color: #dadada;
box-shadow: 0 0 15px #00000052;
&:hover {
box-shadow: 0 0 14px #00000066;
background:
linear-gradient(to right, transparent var(--related-elements-value), gray var(--related-elements-value)),
linear-gradient(to top, rgb(247, 66, 0), rgb(245, 134, 86));
@media not (-moz-platform: windows) {
box-shadow: 0 0 20px #00000077;
}
}
}
#select-this {
appearance: none;
background: linear-gradient(0deg, rgba(246, 27, 25, 1) 0%, rgba(254, 67, 59, 1) 100%);
border: none;
color: #dadada;
box-shadow: 0 0 15px #00000052;
}
#select-cancel {
appearance: none;
background: rgb(90, 94, 100);
@media (-moz-platform: macos) {
background: linear-gradient(0deg, rgba(81, 83, 85, 1) 0%, rgba(108, 110, 112, 1) 100%);
}
border: none;
color: #dadada;
box-shadow: 0 0 15px #00000052;
}
#selector-preview {
background-color: #e0e2e63d;
outline: 1px solid #e0e2e6ae;
width: 100%;
height: 34px;
text-indent: 18px;
display: flex;
align-items: center;
}
#selector-element-preview-text {
font-family: Arial, Helvetica, sans-serif;
font-size: 9pt;
color: rgb(76, 78, 80);
}
#hover-div {
position: fixed;
z-index: 9999;
content: "";
outline: 1px solid #e9dd38;
border-radius: 6px;
background-color: #c2b72046;
transition: 0.1s all ease-out;
will-change: left, top, width, height;
}

Some files were not shown because too many files have changed in this diff Show More