diff --git a/crowdin.yml b/crowdin.yml index c2ccb1c7d5..cb0af5ec02 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -18,3 +18,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 diff --git a/locales/en-US/browser/browser/zen-boosts.ftl b/locales/en-US/browser/browser/zen-boosts.ftl new file mode 100644 index 0000000000..8c252b4761 --- /dev/null +++ b/locales/en-US/browser/browser/zen-boosts.ftl @@ -0,0 +1,34 @@ +zen-panel-ui-boost-create = + .label = New Boost + +zen-boost-invert = + .tooltiptext = Smart Invert Colors +zen-boost-zap = + .tooltiptext = Zap Elements +zen-boost-disable = + .tooltiptext = Disable Color Adjustments + +zen-boost-font-arial = + .tooltiptext = Arial +zen-boost-font-serif = + .tooltiptext = Sans Serif +zen-boost-font-mono = + .tooltiptext = Monospace +zen-boost-font-georgia = + .tooltiptext = Georgia +zen-boost-font-comic = + .tooltiptext = Comic Sans MS +zen-boost-font-tahoma = + .tooltiptext = Tahoma +zen-boost-font-verdana = + .tooltiptext = Verdana +zen-boost-font-corsiva = + .tooltiptext = Corsiva + +zen-boost-name = + .tooltiptext = Change Boost Name +zen-boost-delete = + .tooltiptext = Delete Boost + +zen-panel-ui-boosts-saved-message = Successfully saved the boost! +zen-panel-ui-boosts-deleted-message = Deleted the boost! \ No newline at end of file diff --git a/prefs/zen/zen-boosts.yaml b/prefs/zen/zen-boosts.yaml new file mode 100644 index 0000000000..95d321cd70 --- /dev/null +++ b/prefs/zen/zen-boosts.yaml @@ -0,0 +1,6 @@ +# 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: true diff --git a/src/browser/base/content/zen-assets.jar.inc.mn b/src/browser/base/content/zen-assets.jar.inc.mn index d8ee81f6ed..a6b80e5151 100644 --- a/src/browser/base/content/zen-assets.jar.inc.mn +++ b/src/browser/base/content/zen-assets.jar.inc.mn @@ -72,6 +72,11 @@ content/browser/zen-styles/zen-download-arc-animation.css (../../zen/downloads/zen-download-arc-animation.css) content/browser/zen-styles/zen-download-box-animation.css (../../zen/downloads/zen-download-box-animation.css) + content/browser/zen-styles/zen-boosts.css (../../zen/boosts/zen-boosts.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.xhtml) # Images content/browser/zen-images/brand-header.svg (../../zen/images/brand-header.svg) @@ -80,6 +85,7 @@ content/browser/zen-images/layouts/single-toolbar.png (../../zen/images/layouts/single-toolbar.png) content/browser/zen-images/grain-bg.png (../../zen/images/grain-bg.png) content/browser/zen-images/note-indicator.svg (../../zen/images/note-indicator.svg) + content/browser/zen-images/boost-indicator.svg (../../zen/images/boost-indicator.svg) content/browser/zen-images/downloads/download.svg (../../zen/images/downloads/download.svg) content/browser/zen-images/downloads/archive.svg (../../zen/images/downloads/archive.svg) diff --git a/src/browser/base/content/zen-locales.inc.xhtml b/src/browser/base/content/zen-locales.inc.xhtml index 247fbbc518..d6c5a2d4e5 100644 --- a/src/browser/base/content/zen-locales.inc.xhtml +++ b/src/browser/base/content/zen-locales.inc.xhtml @@ -8,4 +8,5 @@ + diff --git a/src/browser/base/content/zen-panels/site-data.inc b/src/browser/base/content/zen-panels/site-data.inc index e149df52ed..c66a7ad30a 100644 --- a/src/browser/base/content/zen-panels/site-data.inc +++ b/src/browser/base/content/zen-panels/site-data.inc @@ -87,6 +87,8 @@ + \ No newline at end of file diff --git a/src/browser/themes/shared/zen-icons/common/selectable/block.svg b/src/browser/themes/shared/zen-icons/common/selectable/block.svg new file mode 100644 index 0000000000..68825ac937 --- /dev/null +++ b/src/browser/themes/shared/zen-icons/common/selectable/block.svg @@ -0,0 +1,2 @@ +#filter dumbComments emptyLines substitution + \ No newline at end of file diff --git a/src/browser/themes/shared/zen-icons/common/selectable/bolt.svg b/src/browser/themes/shared/zen-icons/common/selectable/bolt.svg new file mode 100644 index 0000000000..0ded312ec8 --- /dev/null +++ b/src/browser/themes/shared/zen-icons/common/selectable/bolt.svg @@ -0,0 +1,2 @@ +#filter dumbComments emptyLines substitution + \ No newline at end of file diff --git a/src/browser/themes/shared/zen-icons/common/selectable/boost.svg b/src/browser/themes/shared/zen-icons/common/selectable/boost.svg new file mode 100644 index 0000000000..9fb906dcbc --- /dev/null +++ b/src/browser/themes/shared/zen-icons/common/selectable/boost.svg @@ -0,0 +1,2 @@ +#filter dumbComments emptyLines substitution + \ No newline at end of file diff --git a/src/browser/themes/shared/zen-icons/common/selectable/brackets-curly.svg b/src/browser/themes/shared/zen-icons/common/selectable/brackets-curly.svg new file mode 100644 index 0000000000..53b939cfef --- /dev/null +++ b/src/browser/themes/shared/zen-icons/common/selectable/brackets-curly.svg @@ -0,0 +1,2 @@ +#filter dumbComments emptyLines substitution + \ No newline at end of file diff --git a/src/browser/themes/shared/zen-icons/common/selectable/brush.svg b/src/browser/themes/shared/zen-icons/common/selectable/brush.svg deleted file mode 100644 index 9bf05281fa..0000000000 --- a/src/browser/themes/shared/zen-icons/common/selectable/brush.svg +++ /dev/null @@ -1,5 +0,0 @@ -#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/. - \ No newline at end of file diff --git a/src/browser/themes/shared/zen-icons/common/selectable/close-filled-round.svg b/src/browser/themes/shared/zen-icons/common/selectable/close-filled-round.svg new file mode 100644 index 0000000000..88fe91a301 --- /dev/null +++ b/src/browser/themes/shared/zen-icons/common/selectable/close-filled-round.svg @@ -0,0 +1,2 @@ +#filter dumbComments emptyLines substitution + \ No newline at end of file diff --git a/src/browser/themes/shared/zen-icons/common/selectable/lightbulb.svg b/src/browser/themes/shared/zen-icons/common/selectable/lightbulb.svg new file mode 100644 index 0000000000..0df3ba4544 --- /dev/null +++ b/src/browser/themes/shared/zen-icons/common/selectable/lightbulb.svg @@ -0,0 +1,2 @@ +#filter dumbComments emptyLines substitution + \ No newline at end of file diff --git a/src/browser/themes/shared/zen-icons/common/selectable/paintbrush-fill.svg b/src/browser/themes/shared/zen-icons/common/selectable/paintbrush-fill.svg new file mode 100644 index 0000000000..eb5bebb415 --- /dev/null +++ b/src/browser/themes/shared/zen-icons/common/selectable/paintbrush-fill.svg @@ -0,0 +1,2 @@ +#filter dumbComments emptyLines substitution + \ No newline at end of file diff --git a/src/browser/themes/shared/zen-icons/common/selectable/paintbrush.svg b/src/browser/themes/shared/zen-icons/common/selectable/paintbrush.svg new file mode 100644 index 0000000000..935eda10f4 --- /dev/null +++ b/src/browser/themes/shared/zen-icons/common/selectable/paintbrush.svg @@ -0,0 +1,2 @@ +#filter dumbComments emptyLines substitution + \ No newline at end of file diff --git a/src/browser/themes/shared/zen-icons/common/selectable/sliders.svg b/src/browser/themes/shared/zen-icons/common/selectable/sliders.svg new file mode 100644 index 0000000000..7f5e255ebc --- /dev/null +++ b/src/browser/themes/shared/zen-icons/common/selectable/sliders.svg @@ -0,0 +1,2 @@ +#filter dumbComments emptyLines substitution + \ No newline at end of file diff --git a/src/browser/themes/shared/zen-icons/common/selectable/square-wand-sparkle.svg b/src/browser/themes/shared/zen-icons/common/selectable/square-wand-sparkle.svg new file mode 100644 index 0000000000..d9d3d2c6a8 --- /dev/null +++ b/src/browser/themes/shared/zen-icons/common/selectable/square-wand-sparkle.svg @@ -0,0 +1,2 @@ +#filter dumbComments emptyLines substitution + \ No newline at end of file diff --git a/src/browser/themes/shared/zen-icons/common/selectable/text-lowercase.svg b/src/browser/themes/shared/zen-icons/common/selectable/text-lowercase.svg new file mode 100644 index 0000000000..7165501ab1 --- /dev/null +++ b/src/browser/themes/shared/zen-icons/common/selectable/text-lowercase.svg @@ -0,0 +1,2 @@ +#filter dumbComments emptyLines substitution + \ No newline at end of file diff --git a/src/browser/themes/shared/zen-icons/common/selectable/text-size.svg b/src/browser/themes/shared/zen-icons/common/selectable/text-size.svg new file mode 100644 index 0000000000..39659f9338 --- /dev/null +++ b/src/browser/themes/shared/zen-icons/common/selectable/text-size.svg @@ -0,0 +1,2 @@ +#filter dumbComments emptyLines substitution + \ No newline at end of file diff --git a/src/browser/themes/shared/zen-icons/common/selectable/text-title-case.svg b/src/browser/themes/shared/zen-icons/common/selectable/text-title-case.svg new file mode 100644 index 0000000000..211cd86440 --- /dev/null +++ b/src/browser/themes/shared/zen-icons/common/selectable/text-title-case.svg @@ -0,0 +1,2 @@ +#filter dumbComments emptyLines substitution + \ No newline at end of file diff --git a/src/browser/themes/shared/zen-icons/common/selectable/text-uppercase.svg b/src/browser/themes/shared/zen-icons/common/selectable/text-uppercase.svg new file mode 100644 index 0000000000..dd643f8e40 --- /dev/null +++ b/src/browser/themes/shared/zen-icons/common/selectable/text-uppercase.svg @@ -0,0 +1,2 @@ +#filter dumbComments emptyLines substitution + \ No newline at end of file diff --git a/src/browser/themes/shared/zen-icons/common/selectable/wand-sparkle.svg b/src/browser/themes/shared/zen-icons/common/selectable/wand-sparkle.svg new file mode 100644 index 0000000000..62777c2db3 --- /dev/null +++ b/src/browser/themes/shared/zen-icons/common/selectable/wand-sparkle.svg @@ -0,0 +1,2 @@ +#filter dumbComments emptyLines substitution + \ No newline at end of file diff --git a/src/browser/themes/shared/zen-icons/icons.css b/src/browser/themes/shared/zen-icons/icons.css index 71416c647d..b18f0df69e 100644 --- a/src/browser/themes/shared/zen-icons/icons.css +++ b/src/browser/themes/shared/zen-icons/icons.css @@ -33,15 +33,18 @@ .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 { +#PanelUI-zen-emojis-picker-none, +#zen-emojis-picker-none { list-style-image: url('trash.svg'); } -#PanelUI-zen-gradient-generator-color-remove { +#PanelUI-zen-gradient-generator-color-remove, +#zen-gradient-generator-color-remove { list-style-image: url('unpin.svg') !important; } @@ -84,7 +87,8 @@ #appMenu-zoom-controls, #PanelUI-zen-gradient-generator-color-add, -#zen-site-data-new-addon-button { +#zen-site-data-new-addon-button, +.zen-create-new-boost { list-style-image: url('plus.svg') !important; } @@ -520,6 +524,27 @@ &:is([open], [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; +} + +@media not (prefers-reduced-motion: reduce) { + #zen-site-data-icon-button[boosting]::after { + content: ''; + position: absolute; + width: 100%; + height: 100%; + opacity: 1; + color: var(--color-accent-primary); + background: url('chrome://browser/content/zen-images/boost-indicator.svg') no-repeat; + transform: translateX(-20%); + z-index: 0; + pointer-events: none; + } } .geo-icon { @@ -890,6 +915,42 @@ fill: currentColor; } +#zen-site-data-boost { + -moz-context-properties: fill, fill-opacity; + fill: currentColor; + border-radius: 6px; + appearance: none; + padding: 6px 10px 6px 10px; + + position: relative; + + list-style-image: url('chrome://browser/skin/zen-icons/selectable/paintbrush.svg'); + + & .toolbarbutton-text { + display: none; + } + + @media not (-moz-pref('zen.boosts.enabled', true)) { + display: none; + } +} + +#zen-site-data-boost[boosting] { + list-style-image: url('chrome://browser/skin/zen-icons/selectable/paintbrush-fill.svg'); +} + +#zen-site-data-boost[boosting]::after { + content: ''; + position: absolute; + width: 90%; + height: 90%; + opacity: 1; + translate: -5px 0px; + background: url('chrome://browser/content/zen-images/boost-indicator.svg') no-repeat; + z-index: 0; + pointer-events: none; +} + #zen-site-data-security-info { -moz-context-properties: fill, fill-opacity; fill: currentColor; @@ -949,3 +1010,47 @@ list-style-image: url('link.svg'); fill-opacity: 0.7; } + +#zen-boost-text-case-toggle { + list-style-image: url('chrome://browser/skin/zen-icons/selectable/text-title-case.svg'); +} + +#zen-boost-text-case-toggle[mode='uppercase'] { + list-style-image: url('chrome://browser/skin/zen-icons/selectable/text-uppercase.svg'); +} + +#zen-boost-text-case-toggle[mode='lowercase'] { + list-style-image: url('chrome://browser/skin/zen-icons/selectable/text-lowercase.svg'); +} + +#zen-boost-code { + list-style-image: url('chrome://browser/skin/zen-icons/selectable/brackets-curly.svg'); +} + +#zen-boost-zap { + list-style-image: url('chrome://browser/skin/zen-icons/selectable/bolt.svg'); +} + +#zen-boost-controls { + list-style-image: url('chrome://browser/skin/zen-icons/selectable/sliders.svg'); +} + +#zen-boost-invert { + list-style-image: url('chrome://browser/skin/zen-icons/selectable/lightbulb.svg'); +} + +#zen-boost-disable { + list-style-image: url('chrome://browser/skin/zen-icons/selectable/block.svg'); +} + +#zen-boost-close { + list-style-image: url('chrome://browser/skin/zen-icons/selectable/close-filled-round.svg'); +} + +#zen-boost-magic-theme { + list-style-image: url('chrome://browser/skin/zen-icons/selectable/square-wand-sparkle.svg'); +} + +#zen-boost-shuffle { + list-style-image: url('chrome://browser/skin/zen-icons/selectable/arrow-rotate-anticlockwise.svg'); +} diff --git a/src/browser/themes/shared/zen-icons/jar.inc.mn b/src/browser/themes/shared/zen-icons/jar.inc.mn index d52bde3b58..8353b4e2e3 100644 --- a/src/browser/themes/shared/zen-icons/jar.inc.mn +++ b/src/browser/themes/shared/zen-icons/jar.inc.mn @@ -464,14 +464,18 @@ * skin/classic/browser/zen-icons/urlbar-arrow.svg (../shared/zen-icons/common/urlbar-arrow.svg) * skin/classic/browser/zen-icons/selectable/airplane.svg (../shared/zen-icons/common/selectable/airplane.svg) * skin/classic/browser/zen-icons/selectable/american-football.svg (../shared/zen-icons/common/selectable/american-football.svg) +* skin/classic/browser/zen-icons/selectable/arrow-rotate-anticlockwise.svg (../shared/zen-icons/common/selectable/arrow-rotate-anticlockwise.svg) * skin/classic/browser/zen-icons/selectable/baseball.svg (../shared/zen-icons/common/selectable/baseball.svg) * skin/classic/browser/zen-icons/selectable/basket.svg (../shared/zen-icons/common/selectable/basket.svg) * skin/classic/browser/zen-icons/selectable/bed.svg (../shared/zen-icons/common/selectable/bed.svg) * skin/classic/browser/zen-icons/selectable/bell.svg (../shared/zen-icons/common/selectable/bell.svg) +* skin/classic/browser/zen-icons/selectable/block.svg (../shared/zen-icons/common/selectable/block.svg) +* skin/classic/browser/zen-icons/selectable/bolt.svg (../shared/zen-icons/common/selectable/bolt.svg) * skin/classic/browser/zen-icons/selectable/book.svg (../shared/zen-icons/common/selectable/book.svg) * skin/classic/browser/zen-icons/selectable/bookmark.svg (../shared/zen-icons/common/selectable/bookmark.svg) +* skin/classic/browser/zen-icons/selectable/boost.svg (../shared/zen-icons/common/selectable/boost.svg) +* skin/classic/browser/zen-icons/selectable/brackets-curly.svg (../shared/zen-icons/common/selectable/brackets-curly.svg) * skin/classic/browser/zen-icons/selectable/briefcase.svg (../shared/zen-icons/common/selectable/briefcase.svg) -* skin/classic/browser/zen-icons/selectable/brush.svg (../shared/zen-icons/common/selectable/brush.svg) * skin/classic/browser/zen-icons/selectable/bug.svg (../shared/zen-icons/common/selectable/bug.svg) * skin/classic/browser/zen-icons/selectable/build.svg (../shared/zen-icons/common/selectable/build.svg) * skin/classic/browser/zen-icons/selectable/cafe.svg (../shared/zen-icons/common/selectable/cafe.svg) @@ -480,6 +484,7 @@ * skin/classic/browser/zen-icons/selectable/chat.svg (../shared/zen-icons/common/selectable/chat.svg) * skin/classic/browser/zen-icons/selectable/checkbox.svg (../shared/zen-icons/common/selectable/checkbox.svg) * skin/classic/browser/zen-icons/selectable/circle.svg (../shared/zen-icons/common/selectable/circle.svg) +* skin/classic/browser/zen-icons/selectable/close-filled-round.svg (../shared/zen-icons/common/selectable/close-filled-round.svg) * skin/classic/browser/zen-icons/selectable/cloud.svg (../shared/zen-icons/common/selectable/cloud.svg) * skin/classic/browser/zen-icons/selectable/code.svg (../shared/zen-icons/common/selectable/code.svg) * skin/classic/browser/zen-icons/selectable/coins.svg (../shared/zen-icons/common/selectable/coins.svg) @@ -506,6 +511,7 @@ * skin/classic/browser/zen-icons/selectable/key.svg (../shared/zen-icons/common/selectable/key.svg) * skin/classic/browser/zen-icons/selectable/layers.svg (../shared/zen-icons/common/selectable/layers.svg) * skin/classic/browser/zen-icons/selectable/leaf.svg (../shared/zen-icons/common/selectable/leaf.svg) +* skin/classic/browser/zen-icons/selectable/lightbulb.svg (../shared/zen-icons/common/selectable/lightbulb.svg) * skin/classic/browser/zen-icons/selectable/lightning.svg (../shared/zen-icons/common/selectable/lightning.svg) * skin/classic/browser/zen-icons/selectable/location.svg (../shared/zen-icons/common/selectable/location.svg) * skin/classic/browser/zen-icons/selectable/lock-closed.svg (../shared/zen-icons/common/selectable/lock-closed.svg) @@ -519,6 +525,8 @@ * skin/classic/browser/zen-icons/selectable/navigate.svg (../shared/zen-icons/common/selectable/navigate.svg) * skin/classic/browser/zen-icons/selectable/nuclear.svg (../shared/zen-icons/common/selectable/nuclear.svg) * skin/classic/browser/zen-icons/selectable/page.svg (../shared/zen-icons/common/selectable/page.svg) +* skin/classic/browser/zen-icons/selectable/paintbrush-fill.svg (../shared/zen-icons/common/selectable/paintbrush-fill.svg) +* skin/classic/browser/zen-icons/selectable/paintbrush.svg (../shared/zen-icons/common/selectable/paintbrush.svg) * skin/classic/browser/zen-icons/selectable/palette.svg (../shared/zen-icons/common/selectable/palette.svg) * skin/classic/browser/zen-icons/selectable/paw.svg (../shared/zen-icons/common/selectable/paw.svg) * skin/classic/browser/zen-icons/selectable/people.svg (../shared/zen-icons/common/selectable/people.svg) @@ -530,6 +538,8 @@ * skin/classic/browser/zen-icons/selectable/shapes.svg (../shared/zen-icons/common/selectable/shapes.svg) * skin/classic/browser/zen-icons/selectable/shirt.svg (../shared/zen-icons/common/selectable/shirt.svg) * skin/classic/browser/zen-icons/selectable/skull.svg (../shared/zen-icons/common/selectable/skull.svg) +* skin/classic/browser/zen-icons/selectable/sliders.svg (../shared/zen-icons/common/selectable/sliders.svg) +* skin/classic/browser/zen-icons/selectable/square-wand-sparkle.svg (../shared/zen-icons/common/selectable/square-wand-sparkle.svg) * skin/classic/browser/zen-icons/selectable/square.svg (../shared/zen-icons/common/selectable/square.svg) * skin/classic/browser/zen-icons/selectable/squares.svg (../shared/zen-icons/common/selectable/squares.svg) * skin/classic/browser/zen-icons/selectable/star-1.svg (../shared/zen-icons/common/selectable/star-1.svg) @@ -538,6 +548,10 @@ * skin/classic/browser/zen-icons/selectable/sun.svg (../shared/zen-icons/common/selectable/sun.svg) * skin/classic/browser/zen-icons/selectable/tada.svg (../shared/zen-icons/common/selectable/tada.svg) * skin/classic/browser/zen-icons/selectable/terminal.svg (../shared/zen-icons/common/selectable/terminal.svg) +* skin/classic/browser/zen-icons/selectable/text-lowercase.svg (../shared/zen-icons/common/selectable/text-lowercase.svg) +* skin/classic/browser/zen-icons/selectable/text-size.svg (../shared/zen-icons/common/selectable/text-size.svg) +* skin/classic/browser/zen-icons/selectable/text-title-case.svg (../shared/zen-icons/common/selectable/text-title-case.svg) +* skin/classic/browser/zen-icons/selectable/text-uppercase.svg (../shared/zen-icons/common/selectable/text-uppercase.svg) * skin/classic/browser/zen-icons/selectable/ticket.svg (../shared/zen-icons/common/selectable/ticket.svg) * skin/classic/browser/zen-icons/selectable/time.svg (../shared/zen-icons/common/selectable/time.svg) * skin/classic/browser/zen-icons/selectable/trash.svg (../shared/zen-icons/common/selectable/trash.svg) @@ -545,6 +559,7 @@ * skin/classic/browser/zen-icons/selectable/video.svg (../shared/zen-icons/common/selectable/video.svg) * skin/classic/browser/zen-icons/selectable/volume-high.svg (../shared/zen-icons/common/selectable/volume-high.svg) * skin/classic/browser/zen-icons/selectable/wallet.svg (../shared/zen-icons/common/selectable/wallet.svg) +* skin/classic/browser/zen-icons/selectable/wand-sparkle.svg (../shared/zen-icons/common/selectable/wand-sparkle.svg) * skin/classic/browser/zen-icons/selectable/warning.svg (../shared/zen-icons/common/selectable/warning.svg) * skin/classic/browser/zen-icons/selectable/water.svg (../shared/zen-icons/common/selectable/water.svg) * skin/classic/browser/zen-icons/selectable/weight.svg (../shared/zen-icons/common/selectable/weight.svg) diff --git a/src/docshell/base/BrowsingContext-h.patch b/src/docshell/base/BrowsingContext-h.patch new file mode 100644 index 0000000000..8f61b93180 --- /dev/null +++ b/src/docshell/base/BrowsingContext-h.patch @@ -0,0 +1,28 @@ +diff --git a/docshell/base/BrowsingContext.h b/docshell/base/BrowsingContext.h +index 2d3a14d59922045197e474f5d097d7e16ce6ad8c..2e86f53edcd1288d3ca2e0292409aec8ee5926da 100644 +--- a/docshell/base/BrowsingContext.h ++++ b/docshell/base/BrowsingContext.h +@@ -258,6 +258,7 @@ struct EmbedderColorSchemes { + FIELD(IsInBFCache, bool) \ + FIELD(HasRestoreData, bool) \ + FIELD(SessionStoreEpoch, uint32_t) \ ++ FIELD(ZenBoostsData, nscolor) \ + /* Whether we can execute scripts in this BrowsingContext. Has no effect \ + * unless scripts are also allowed in the parent WindowContext. */ \ + FIELD(AllowJavascript, bool) \ +@@ -652,6 +653,7 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache { + + bool FullscreenAllowed() const; + ++ auto ZenBoostsData() const { return GetZenBoostsData(); } + float FullZoom() const { return GetFullZoom(); } + float TextZoom() const { return GetTextZoom(); } + +@@ -1175,6 +1177,7 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache { + } + + void DidSet(FieldIndex, uint32_t aOldValue); ++ void DidSet(FieldIndex, nscolor aOldValue); + + using CanSetResult = syncedcontext::CanSetResult; + diff --git a/src/dom/chrome-webidl/BrowsingContext-webidl.patch b/src/dom/chrome-webidl/BrowsingContext-webidl.patch new file mode 100644 index 0000000000..88b82cb040 --- /dev/null +++ b/src/dom/chrome-webidl/BrowsingContext-webidl.patch @@ -0,0 +1,13 @@ +diff --git a/dom/chrome-webidl/BrowsingContext.webidl b/dom/chrome-webidl/BrowsingContext.webidl +index 9730cea647caedaeca4dcbe703e209d79a3440a7..ba0952428625b6a0f0e2f9546ccec5a7c6af0faf 100644 +--- a/dom/chrome-webidl/BrowsingContext.webidl ++++ b/dom/chrome-webidl/BrowsingContext.webidl +@@ -169,6 +169,8 @@ interface BrowsingContext { + + [SetterThrows] attribute float textZoom; + ++ [SetterThrows] attribute unsigned long zenBoostsData; ++ + // 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. diff --git a/src/layout/base/nsPresContext-cpp.patch b/src/layout/base/nsPresContext-cpp.patch new file mode 100644 index 0000000000..26768bf1d5 --- /dev/null +++ b/src/layout/base/nsPresContext-cpp.patch @@ -0,0 +1,26 @@ +diff --git a/layout/base/nsPresContext.cpp b/layout/base/nsPresContext.cpp +index edae3bacba3a029cfb4038e1de4b36b366488385..40b7b7b66edeeb99a5208d989077e6a39162c7a4 100644 +--- a/layout/base/nsPresContext.cpp ++++ b/layout/base/nsPresContext.cpp +@@ -20,6 +20,7 @@ + #include "base/basictypes.h" + #include "gfxPlatform.h" + #include "gfxTextRun.h" ++#include "mozilla/nsZenBoostsBackend.h" + #include "mozilla/AnimationEventDispatcher.h" + #include "mozilla/ContentBlockingAllowList.h" + #include "mozilla/CycleCollectedJSContext.h" +@@ -917,6 +918,13 @@ void nsPresContext::RecomputeBrowsingContextDependentData() { + } + + auto* top = browsingContext->Top(); ++ ++ nsCOMPtr zenBackend( ++ do_GetService(ZEN_BOOSTS_BACKEND_CONTRACTID)); ++ if (zenBackend) { ++ zenBackend->RecomputeBrowsingContextDependentData(this, top); ++ } ++ + SetColorSchemeOverride([&] { + auto overriden = top->PrefersColorSchemeOverride(); + if (overriden != PrefersColorSchemeOverride::None) { diff --git a/src/layout/base/nsPresContext-h.patch b/src/layout/base/nsPresContext-h.patch new file mode 100644 index 0000000000..51ec6cbf66 --- /dev/null +++ b/src/layout/base/nsPresContext-h.patch @@ -0,0 +1,12 @@ +diff --git a/layout/base/nsPresContext.h b/layout/base/nsPresContext.h +index 659c2a237280794c19de2fbb54b3b3070590cc30..b47ef9d53ddcdd904c1383fd136cf13a2224f347 100644 +--- a/layout/base/nsPresContext.h ++++ b/layout/base/nsPresContext.h +@@ -566,6 +566,7 @@ class nsPresContext : public nsISupports, + void UpdateForcedColors(bool aNotify = true); + + public: ++ nscolor mZenBoostsPresContextData; + float GetFullZoom() { return mFullZoom; } + /** + * Device full zoom differs from full zoom because it gets the zoom from diff --git a/src/layout/painting/nsDisplayList-cpp.patch b/src/layout/painting/nsDisplayList-cpp.patch new file mode 100644 index 0000000000..33a5d40eb5 --- /dev/null +++ b/src/layout/painting/nsDisplayList-cpp.patch @@ -0,0 +1,41 @@ +diff --git a/layout/painting/nsDisplayList.cpp b/layout/painting/nsDisplayList.cpp +index 43532357fc9ff86813af8b7bd2ec267b2c23b9c1..da905de5dfcea1c42271dd7c7d0bde52e8704cd7 100644 +--- a/layout/painting/nsDisplayList.cpp ++++ b/layout/painting/nsDisplayList.cpp +@@ -85,6 +85,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" +@@ -1046,6 +1047,12 @@ void nsDisplayListBuilder::EnterPresShell(const nsIFrame* aReferenceFrame, + state->mFirstFrameMarkedForDisplay = mFramesMarkedForDisplay.Length(); + state->mFirstFrameWithOOFData = mFramesWithOOFData.Length(); + ++ nsCOMPtr zenBackend( ++ do_GetService(ZEN_BOOSTS_BACKEND_CONTRACTID)); ++ if (zenBackend) { ++ zenBackend->onPressShellEntered(state->mPresShell->GetPresContext()); ++ } ++ + ScrollContainerFrame* sf = state->mPresShell->GetRootScrollContainerFrame(); + if (sf && IsInSubdocument()) { + // We are forcing a rebuild of nsDisplayCanvasBackgroundColor to make sure +@@ -1231,6 +1238,15 @@ void nsDisplayListBuilder::LeavePresShell(const nsIFrame* aReferenceFrame, + ResetMarkedFramesForDisplayList(aReferenceFrame); + mPresShellStates.RemoveLastElement(); + ++ nsCOMPtr zenBackend( ++ do_GetService(ZEN_BOOSTS_BACKEND_CONTRACTID)); ++ if (zenBackend) { ++ zenBackend->onPressShellLeave( ++ mPresShellStates.IsEmpty() ++ ? nullptr ++ : CurrentPresShellState()->mPresShell->GetPresContext()); ++ } ++ + if (!mPresShellStates.IsEmpty()) { + nsPresContext* pc = CurrentPresContext(); + nsIDocShell* docShell = pc->GetDocShell(); diff --git a/src/layout/style/StyleColor-cpp.patch b/src/layout/style/StyleColor-cpp.patch new file mode 100644 index 0000000000..461e9f0835 --- /dev/null +++ b/src/layout/style/StyleColor-cpp.patch @@ -0,0 +1,30 @@ +diff --git a/layout/style/StyleColor.cpp b/layout/style/StyleColor.cpp +index 5fd5f7efba9bcb2febdc9a6b8f8812df673513d8..200e9bf82aacbf38a50f5b99671e403de5b4f61b 100644 +--- a/layout/style/StyleColor.cpp ++++ b/layout/style/StyleColor.cpp +@@ -10,6 +10,7 @@ + #include "mozilla/dom/BindingDeclarations.h" + #include "nsIFrame.h" + #include "nsStyleStruct.h" ++#include "mozilla/nsZenBoostsBackend.h" + + namespace mozilla { + +@@ -23,6 +24,8 @@ bool StyleColor::MaybeTransparent() const { + template <> + StyleAbsoluteColor StyleColor::ResolveColor( + const StyleAbsoluteColor& aForegroundColor) const { ++ auto ResolveColorInner = [this, ++ &aForegroundColor]() -> StyleAbsoluteColor { + if (IsAbsolute()) { + return AsAbsolute(); + } +@@ -32,6 +35,8 @@ StyleAbsoluteColor StyleColor::ResolveColor( + } + + return Servo_ResolveColor(this, &aForegroundColor); ++ }; ++ return zen::nsZenBoostsBackend::ResolveStyleColor(ResolveColorInner()); + } + + template <> diff --git a/src/zen/boosts/ZenBoostStyles.sys.mjs b/src/zen/boosts/ZenBoostStyles.sys.mjs new file mode 100644 index 0000000000..6dddf69912 --- /dev/null +++ b/src/zen/boosts/ZenBoostStyles.sys.mjs @@ -0,0 +1,95 @@ +/* -*- 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, + }, +}); + +ChromeUtils.defineESModuleGetters(lazy, { + gZenBoostsManager: 'resource:///modules/ZenBoostsManager.sys.mjs', +}); + +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. + * @returns {string} The generated CSS style string. + */ + getStyleForBoost(boostData) { + const { domain } = boostData; + if (this.#stylesCache.has(domain)) { + return this.#stylesCache.get(domain); + } + + const rawStyle = this.#generateStyleString(boostData); + if (!rawStyle) return null; + + const styleUri = this.#convertStyleToDataUri(rawStyle); + this.#cacheStyle(styleUri, domain); + return this.getStyleForBoost(boostData); + } + + 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) { + const boost = lazy.gZenBoostsManager.loadBoostFromStore(boostData); + console.log(boost); + + let style = 'body, p, h1, h2, h3, h4, h5, a, span, textarea, input, span {'; + + if (boost.fontFamily != '') style += `font-family: ${boost.fontFamily} !important;`; + style += `text-transform: ${boost.textCaseOverride} !important;`; + + 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. + * @returns {string} The cached style sheet URI. + * @private + */ + #cacheStyle(styleUri, domain) { + this.#stylesCache.set(domain, { + uuid: Services.uuid.generateUUID().toString(), + uri: styleUri.spec, + }); + } +} diff --git a/src/zen/boosts/ZenBoostsEditor.sys.mjs b/src/zen/boosts/ZenBoostsEditor.sys.mjs new file mode 100644 index 0000000000..d418ad1372 --- /dev/null +++ b/src/zen/boosts/ZenBoostsEditor.sys.mjs @@ -0,0 +1,689 @@ +/* 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 { gZenBoostsManager } from './ZenBoostsManager.sys.mjs'; + +export class nsZenBoostEditor { + doc = null; + window = null; + + /** + * Creates a new boost editor instance for the specified domain. + * @param {Document} doc - The document object for the editor window. + * @param {string} domain - The domain for which to edit the boost. + * @param {Window} window - The window object for the editor. + */ + constructor(doc, domain, window) { + this.doc = doc; + this.window = window; + + this.isMouseDown = false; + this.wasDragging = false; + this.lastDotSetPos = { x: 0, y: 0 }; + this.currentBoostData = null; + + this.killOtherEditorInstances(); + Services.obs.addObserver(this, 'zen-boosts-kill-editor'); + + this.init(); + this.initColorPicker(); + this.initFonts(); + this.loadBoost(domain); + } + + /** + * Initializes the boost editor by setting up event listeners for all UI controls. + */ + init() { + this.window.addEventListener('unload', () => this.handleClose(), { once: true }); + + this.doc + .getElementById('zen-boost-color-contrast') + .addEventListener('input', this.onColorOptionChange.bind(this)); + this.doc + .getElementById('zen-boost-color-brightness') + .addEventListener('input', this.onColorOptionChange.bind(this)); + this.doc + .getElementById('zen-boost-color-saturation') + .addEventListener('input', this.onColorOptionChange.bind(this)); + + this.doc + .getElementById('zen-boost-text-case-toggle') + .addEventListener('click', this.onBoostCasePressed.bind(this)); + this.doc + .getElementById('zen-boost-size') + .addEventListener('click', this.onBoostSizePressed.bind(this)); + this.doc + .getElementById('zen-boost-zap') + .addEventListener('click', () => console.error('Not implemented')); + this.doc + .getElementById('zen-boost-disable') + .addEventListener('click', this.onToggleDisable.bind(this)); + this.doc + .getElementById('zen-boost-invert') + .addEventListener('click', this.onToggleInvert.bind(this)); + this.doc + .getElementById('zen-boost-controls') + .addEventListener('click', (event) => this.openAdvancedColorOptions(event)); + + this.doc + .getElementById('zen-boost-name') + .addEventListener('input', (e) => (this.currentBoostData.boostName = e.target.value)); + + this.doc + .getElementById('zen-boost-close') + .addEventListener('click', this.onClosePressed.bind(this)); + + this.doc.addEventListener('keydown', (event) => { + if (event.key === 'Escape' || (event.key === 'w' && (event.ctrlKey || event.metaKey))) { + this.onClosePressed(); + } + }); + + this.initialized = true; + } + + /** + * Uninitializes the boost editor by cleaning up event listeners and observers. + */ + uninit() { + this.uninitColorPicker(); + Services.obs.removeObserver(this, 'zen-boosts-kill-editor'); + } + + /** + * Kills other editor instances by sending a notification to close them. + * This ensures only one editor instance is open at a time. + */ + killOtherEditorInstances() { + Services.obs.notifyObservers(null, 'zen-boosts-kill-editor'); + } + + /** + * Observer callback that handles notifications from the observer service. + * Closes the editor window when a 'zen-boosts-kill-editor' notification is received. + * @param {Object} subject - The subject of the notification. + * @param {string} topic - The topic of the notification. + */ + observe(subject, topic) { + if (topic === 'zen-boosts-kill-editor') { + this.window.close(); + } + } + + /** + * Registers an event listener to close the editor when the active tab changes + * to a different domain than the one being edited. + */ + registerTabChangedEvent() { + this.window.gBrowser.tabContainer.addEventListener('TabSelect', (event) => { + const tab = event.target; + const domain = new URL(tab.linkedBrowser.currentURI.spec).hostname; + + if (domain != this.currentBoostData.domain) this.window.close(); + }); + } + + /** + * Initializes the font selection UI by creating font buttons and dropdown options + * for the available font families. + */ + initFonts() { + const commonFonts = [ + 'Arial', + 'Times New Roman', + 'Courier New', + 'Georgia', + 'Comic Sans MS', + 'Verdana', + 'Trebuchet MS', + 'Impact', + 'Palatino Linotype', + 'Tahoma', + ]; + const fonts = this.fetchFontList(); + + const fontButtonGroup = this.doc.getElementById('zen-boost-font-grid'); + const fontList = this.doc.getElementById('zen-boost-font-select'); + const buttonCount = 10; + + for (let i = 0; i < Math.min(commonFonts.length, buttonCount); i++) { + let font = fonts[i]; // Fallback + if (fonts.includes(commonFonts[i])) { + font = commonFonts[i]; + } + + const fontButton = this.doc.createElement('button'); + fontButton.setAttribute('font-data', `${font}`); + fontButton.classList.add('subviewbutton'); + fontButton.style.fontFamily = `'${font}'`; + fontButton.innerHTML = 'Aa'; + fontButton.addEventListener('click', this.onFontButtonClick.bind(this)); + + fontButtonGroup.appendChild(fontButton); + } + + // Add default value + const defaultOption = this.doc.createElement('option'); + defaultOption.value = ''; // Use default font of site + defaultOption.label = 'Default'; + fontList.appendChild(defaultOption); + + for (let j = 0; j < fonts.length; j++) { + const font = fonts[j]; + const option = this.doc.createElement('option'); + option.style.fontFamily = `'${font}'`; + option.value = font; + option.label = font; + fontList.appendChild(option); + } + + fontList.addEventListener('change', this.onFontDropdownSelect.bind(this)); + } + + /** + * Fetches a list of all available system fonts. + * @returns {Array} An array with names of available fonts. + */ + fetchFontList() { + const enumerator = Cc['@mozilla.org/gfx/fontenumerator;1'].createInstance(Ci.nsIFontEnumerator); + + return enumerator.EnumerateFonts(null, null); + } + + /** + * Initializes the color picker by setting up mouse event listeners for + * interactive color selection on the gradient picker. + */ + initColorPicker() { + const themePicker = this.doc.querySelector('.zen-boost-color-picker-gradient'); + this._onMouseMove = this.onMouseMove.bind(this); + this._onMouseUp = this.onMouseUp.bind(this); + this._onMouseDown = this.onMouseDown.bind(this); + this._onThemePickerClick = this.onThemePickerClick.bind(this); + this.doc.addEventListener('mousemove', this._onMouseMove); + this.doc.addEventListener('mouseup', this._onMouseUp); + themePicker.addEventListener('mousedown', this._onMouseDown); + themePicker.addEventListener('click', this._onThemePickerClick); + } + + /** + * Uninitializes the color picker by removing all mouse event listeners. + */ + uninitColorPicker() { + const themePicker = this.doc.querySelector('.zen-boost-color-picker-gradient'); + this.doc.removeEventListener('mousemove', this._onMouseMove); + this.doc.removeEventListener('mouseup', this._onMouseUp); + themePicker.removeEventListener('mousedown', this._onMouseDown); + themePicker.removeEventListener('click', this._onThemePickerClick); + this._onThemePickerClick = null; + this._onMouseMove = null; + this._onMouseUp = null; + this._onMouseDown = null; + } + + /** + * Handles mouse move events to update the color picker dot position while dragging. + * @param {MouseEvent} event - The mouse move event. + */ + onMouseMove(event) { + if (this.isMouseDown) { + this.wasDragging = true; + event.preventDefault(); + + if (event.target.id != 'zen-boost-magic-theme') + this.setDotPos(event.clientX, event.clientY, false); + } + } + + /** + * Handles mouse down events to initiate color picker dragging. + * @param {MouseEvent} event - The mouse down event. + */ + onMouseDown(event) { + if (event.button === 2) { + return; + } + + this.isMouseDown = true; + } + + /** + * Handles mouse up events to end color picker dragging. + * @param {MouseEvent} event - The mouse up event. + */ + onMouseUp(event) { + if (event.button === 2) { + return; + } + + this.isMouseDown = false; + this.wasDragging = false; + } + + /** + * Handles the boost size button press, cycling through size override values + * (0.9, 1.0, 1.1, 1.25, 1.5) and updating the UI accordingly. + */ + onBoostSizePressed() { + const sizeValue = this.doc.getElementById('zen-boost-size-value'); + + if (this.currentBoostData.siteSizeOverride >= 1.5) this.currentBoostData.siteSizeOverride = 0.9; + else if (this.currentBoostData.siteSizeOverride >= 1.25) + this.currentBoostData.siteSizeOverride = 1.5; + else if (this.currentBoostData.siteSizeOverride >= 1.1) + this.currentBoostData.siteSizeOverride = 1.25; + else if (this.currentBoostData.siteSizeOverride >= 1) + this.currentBoostData.siteSizeOverride = 1.1; + else if (this.currentBoostData.siteSizeOverride >= 0.9) + this.currentBoostData.siteSizeOverride = 1; + else this.currentBoostData.siteSizeOverride = 1.1; + + sizeValue.innerHTML = `${Math.round(this.currentBoostData.siteSizeOverride * 100)}%`; + + this.updateSizeButtonVisuals(); + this.updateCurrentBoost(); + } + + /** + * Handles the text case toggle button press, cycling through case override options + * (none, lower, upper) and updating the UI accordingly. + */ + onBoostCasePressed() { + if (this.currentBoostData.textCaseOverride == 'lowercase') + this.currentBoostData.textCaseOverride = 'uppercase'; + else if (this.currentBoostData.textCaseOverride == 'uppercase') + this.currentBoostData.textCaseOverride = 'capitalize'; + else if (this.currentBoostData.textCaseOverride == 'capitalize') + this.currentBoostData.textCaseOverride = 'none'; + else this.currentBoostData.textCaseOverride = 'lowercase'; + + this.updateCaseButtonVisuals(); + this.updateCurrentBoost(); + } + + /** + * Handles changes to color option sliders (contrast, brightness, saturation) + * and updates the current boost data accordingly. + */ + onColorOptionChange() { + this.currentBoostData.contrast = this.doc.getElementById('zen-boost-color-contrast').value; + this.currentBoostData.brightness = this.doc.getElementById('zen-boost-color-brightness').value; + this.currentBoostData.saturation = this.doc.getElementById('zen-boost-color-saturation').value; + + this.updateCurrentBoost(); + } + + /** + * Opens the advanced color options popup panel. + * @param {Event} event - The click event that triggered this action. + */ + openAdvancedColorOptions(event) { + const panel = this.doc.getElementById('zen-boost-advanced-color-options-panel'); + panel.openPopup(event.target, 'bottomcenter topcenter', 0, 2); + } + + /** + * Resets the color picker dot to the center position (default state). + */ + resetDotPosition() { + this.setDotPos(null, null); + } + + /** + * Handles clicks on the theme picker gradient or magic theme button. + * Updates the dot position or toggles auto-theme mode based on the click target. + * @param {MouseEvent} event - The click event. + */ + onThemePickerClick(event) { + event.preventDefault(); + + this.currentBoostData.changeWasMade = true; + + if (event.target.id == 'zen-boost-magic-theme') { + this.currentBoostData.autoTheme = !this.currentBoostData.autoTheme; + this.updateButtonToggleVisuals(); + this.updateCurrentBoost(); + } else this.setDotPos(event.clientX, event.clientY, !this.wasDragging); + this.wasDragging = false; + } + + /** + * Sets the position of the color picker dot on the gradient and updates + * the boost data with the corresponding angle and distance values. + * @param {number|null} pixelX - The X coordinate in pixels, or null to center the dot. + * @param {number|null} pixelY - The Y coordinate in pixels, or null to center the dot. + * @param {boolean} animate - Whether to animate the dot movement (currently not implemented). + */ + setDotPos(pixelX, pixelY, animate = true) { + const gradient = this.doc.querySelector('.zen-boost-color-picker-gradient'); + const dot = this.doc.querySelector('.zen-boost-color-picker-dot'); + + const rect = gradient.getBoundingClientRect(); + const padding = 50; + + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + const radius = (rect.width - padding) / 2; + + if (!animate) { + let nDistance = Math.sqrt( + (pixelX - this.lastDotSetPos.x) ** 2 + (pixelY - this.lastDotSetPos.y) ** 2 + ); + + if (nDistance > 15) { + // Optional haptic feedback + // Services.zen.playHapticFeedback(); + + this.lastDotSetPos = { + x: pixelX, + y: pixelY, + }; + } + } + + if (pixelX == null || pixelY == null) { + pixelX = centerX; + pixelY = centerY; + + this.currentBoostData.dotAngleDeg = 0; + this.currentBoostData.dotDistance = 0; + } else { + let distance = Math.sqrt((pixelX - centerX) ** 2 + (pixelY - centerY) ** 2); + distance = Math.min(distance, radius); // Clamp distance + + const angle = Math.atan2(pixelY - centerY, pixelX - centerX); + + pixelX = centerX + Math.cos(angle) * distance; + pixelY = centerY + Math.sin(angle) * distance; + + // Rad to degree + this.currentBoostData.dotAngleDeg = + ((Math.atan2(pixelY - centerY, pixelX - centerX) * 180) / Math.PI + 100) % 360; + if (this.currentBoostData.dotAngleDeg < 0) this.currentBoostData.dotAngleDeg += 360; + + // Map to 0-1 range + this.currentBoostData.dotDistance = distance / radius; + } + + const relativeX = pixelX - rect.left; + const relativeY = pixelY - rect.top; + + // Capture position of dot for restoring it correctly later + this.currentBoostData.dotPos.x = relativeX; + this.currentBoostData.dotPos.y = relativeY; + + // TODO: Fix animation + // if (animate) { + // this.window.motion.animate( + // dot, + // { + // left: `${relativeX}px`, + // top: `${relativeY}px`, + // }, + // { + // duration: 0.4, + // type: 'spring', + // bounce: 0.3, + // } + // ); + // } else { + dot.style.left = `${relativeX}px`; + dot.style.top = `${relativeY}px`; + // } + + // Enable color boosting again + if (!this.currentBoostData.enableColorBoost) this.onToggleDisable(false); + this.currentBoostData.autoTheme = false; + + this.updateButtonToggleVisuals(); + this.updateDot(); + this.updateCurrentBoost(); + } + + /** + * Updates the visual appearance of the color picker dot and circle + * based on the current boost data's angle and distance values. + */ + updateDot() { + const dot = this.doc.querySelector('.zen-boost-color-picker-dot'); + dot.style.setProperty( + '--zen-theme-picker-dot-color', + `hsl(${this.currentBoostData.dotAngleDeg}deg, ${this.currentBoostData.dotDistance * 100}%, 55%)` + ); + + const gradient = this.doc.querySelector('.zen-boost-color-picker-gradient'); + const rect = gradient.getBoundingClientRect(); + const padding = 50; + const radius = (rect.width - padding) / 2; + + const circle = this.doc.querySelector('.zen-boost-color-picker-circle'); + circle.style.width = `${this.currentBoostData.dotDistance * radius * 2}px`; + circle.style.height = `${this.currentBoostData.dotDistance * radius * 2}px`; + } + + /** + * Toggles the color boost enable/disable state. + * @param {boolean} userAction - Whether this was triggered by a user action (default: true). + */ + onToggleDisable(userAction = true) { + this.currentBoostData.enableColorBoost = !this.currentBoostData.enableColorBoost; + + if (userAction) this.currentBoostData.changeWasMade = true; + + this.updateButtonToggleVisuals(); + this.updateCurrentBoost(); + } + + /** + * Toggles the smart invert feature, which automatically inverts colors + * based on the window's color scheme. + * @param {boolean} userAction - Whether this was triggered by a user action (default: true). + */ + onToggleInvert(userAction = true) { + this.currentBoostData.enableColorBoost = true; + this.currentBoostData.smartInvert = !this.currentBoostData.smartInvert; + + if (userAction) this.currentBoostData.changeWasMade = true; + + this.updateButtonToggleVisuals(); + this.updateCurrentBoost(); + } + + /** + * Updates the visual state of the size button based on the current + * site size override value, setting appropriate color modes. + */ + updateSizeButtonVisuals() { + const sizeValue = this.doc.getElementById('zen-boost-size'); + + if (this.currentBoostData.siteSizeOverride >= 1.5) sizeValue.setAttribute('mode', 'red'); + else if (this.currentBoostData.siteSizeOverride >= 1.25) + sizeValue.setAttribute('mode', 'orange-red'); + else if (this.currentBoostData.siteSizeOverride >= 1.1) + sizeValue.setAttribute('mode', 'orange'); + else if (this.currentBoostData.siteSizeOverride >= 1) sizeValue.setAttribute('mode', 'none'); + else if (this.currentBoostData.siteSizeOverride >= 0.9) sizeValue.setAttribute('mode', 'blue'); + else sizeValue.setAttribute('mode', 'none'); + } + + /** + * Updates the visual state of the text case toggle button based on the current + * text case override value (none, upper, or lower). + */ + updateCaseButtonVisuals() { + const sizeValue = this.doc.getElementById('zen-boost-text-case-toggle'); + sizeValue.setAttribute('mode', this.currentBoostData.textCaseOverride); + } + + /** + * Updates the visual state of all toggle buttons (invert, disable, auto-theme) + * and applies grayscale effect to the gradient when color boosting is disabled. + */ + updateButtonToggleVisuals() { + const invertButton = this.doc.getElementById('zen-boost-invert'); + const disableButton = this.doc.getElementById('zen-boost-disable'); + const autoThemeButton = this.doc.getElementById('zen-boost-magic-theme'); + const gradient = this.doc.querySelector('.zen-boost-color-picker-gradient'); + + if (this.currentBoostData.autoTheme) autoThemeButton.classList.add('zen-boost-button-active'); + else autoThemeButton.classList.remove('zen-boost-button-active'); + + if (this.currentBoostData.smartInvert) invertButton.classList.add('zen-boost-button-active'); + else invertButton.classList.remove('zen-boost-button-active'); + + if (this.currentBoostData.smartInvert) invertButton.classList.add('zen-boost-button-active'); + else invertButton.classList.remove('zen-boost-button-active'); + + if (!this.currentBoostData.enableColorBoost) + disableButton.classList.add('zen-boost-button-active'); + else disableButton.classList.remove('zen-boost-button-active'); + + // Give the gradient a grayscale effect + // when the color boosting is disabled + // or the theme is set automatically + if (!this.currentBoostData.enableColorBoost || this.currentBoostData.autoTheme) + gradient.classList.add('zen-boost-panel-disabled'); + else gradient.classList.remove('zen-boost-panel-disabled'); + } + + /** + * Handles font button clicks to change the selected font family. + * @param {Event} event - The click event from a font button. + */ + onFontButtonClick(event) { + const font = event?.target?.getAttribute('font-data') ?? ''; + this.onFontChange(font); + } + + /** + * Handles font dropdown selection changes to change the selected font family. + * @param {Event} event - The change event from the font dropdown. + */ + onFontDropdownSelect(event) { + const select = event.target; + this.onFontChange(select.value); + } + + /** + * Changes the font family for the boost. If the same font is selected again, + * it clears the font override (sets to empty string). + * @param {string} font - The font family string to apply. + */ + onFontChange(font) { + if (this.currentBoostData.fontFamily == font) this.currentBoostData.fontFamily = ''; + else this.currentBoostData.fontFamily = font; + this.updateFontButtonVisuals(); + + this.currentBoostData.changeWasMade = true; + this.updateCurrentBoost(); + } + + /** + * Updates the visual state of font selection buttons and dropdown + * to reflect the currently selected font family. + */ + updateFontButtonVisuals() { + const fontButtonGroup = this.doc.getElementById('zen-boost-font-grid'); + for (let i = 0; i < fontButtonGroup.children.length; i++) { + const fontButton = fontButtonGroup.children[i]; + if (fontButton.getAttribute('font-data') == this.currentBoostData.fontFamily) + fontButton.classList.add('zen-boost-font-button-active'); + else fontButton.classList.remove('zen-boost-font-button-active'); + } + + const fontSelect = this.doc.getElementById('zen-boost-font-select'); + for (let i = 0; i < fontSelect.options.length; i++) { + const option = fontSelect.options[i]; + if (option.value == this.currentBoostData.fontFamily) { + fontSelect.value = option.value; + break; + } + } + } + + /** + * Updates the boost data in the boosts manager with the current boost data. + * This triggers notifications to observers but does not persist to disk. + */ + updateCurrentBoost() { + gZenBoostsManager.updateBoost(this.currentBoostData); + } + + /** + * Deletes the current boost for the domain and closes the editor window. + */ + onDeleteBoost() { + gZenBoostsManager.deleteBoost(this.currentBoostData.domain); + this.currentBoostData = null; + this.window.gZenUIManager.showToast('zen-panel-ui-boosts-deleted-message'); + + this.window.close(); + } + + /** + * Handles the close button press by closing the editor window. + */ + onClosePressed() { + this.window.close(); + } + + /** + * Handles the editor window close event. Saves the boost if changes were made, + * or deletes it if no changes were made (temporary boost). + */ + handleClose() { + this.uninit(); + if (this.currentBoostData != null && this.currentBoostData.changeWasMade) this.saveBoost(); + else if (this.currentBoostData != null && !this.currentBoostData.changeWasMade) + gZenBoostsManager.deleteBoost(this.currentBoostData.domain); + } + + /** + * Loads boost data for the specified domain and initializes the editor UI + * with the boost settings (dot position, sliders, buttons, etc.). + * @param {string} domain - The domain for which to load the boost. + */ + loadBoost(domain) { + this.currentBoostData = gZenBoostsManager.loadBoostFromStore(domain); + + // Initial save to register the boost + gZenBoostsManager.saveBoostToStore(this.currentBoostData); + + this.doc.getElementById('zen-boost-name-text').innerHTML = domain; + + const dot = this.doc.querySelector('.zen-boost-color-picker-dot'); + const contrastSlider = this.doc.getElementById('zen-boost-color-contrast'); + const brightnessSlider = this.doc.getElementById('zen-boost-color-brightness'); + const saturationSlider = this.doc.getElementById('zen-boost-color-saturation'); + + if (this.currentBoostData.dotPos.x == null || this.currentBoostData.dotPos.y == null) + this.resetDotPosition(); + else { + dot.style.left = `${this.currentBoostData.dotPos.x}px`; + dot.style.top = `${this.currentBoostData.dotPos.y}px`; + this.updateFontButtonVisuals(); + this.updateSizeButtonVisuals(); + this.updateCaseButtonVisuals(); + + contrastSlider.value = this.currentBoostData.contrast; + brightnessSlider.value = this.currentBoostData.brightness; + saturationSlider.value = this.currentBoostData.saturation; + } + + this.updateDot(); + this.updateButtonToggleVisuals(); + } + + /** + * Saves the current boost data to persistent storage if changes were made. + * @param {boolean} showToast - Whether to show a toast notification on save (default: true). + */ + saveBoost(showToast = true) { + if (this.currentBoostData == null || !this.currentBoostData.changeWasMade) return; + + gZenBoostsManager.saveBoostToStore(this.currentBoostData); + if (showToast) this.window.gZenUIManager.showToast('zen-panel-ui-boosts-saved-message'); + } +} diff --git a/src/zen/boosts/ZenBoostsManager.sys.mjs b/src/zen/boosts/ZenBoostsManager.sys.mjs new file mode 100644 index 0000000000..655e0dc27f --- /dev/null +++ b/src/zen/boosts/ZenBoostsManager.sys.mjs @@ -0,0 +1,183 @@ +/* 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 { nsZenBoostStyles } from 'resource:///modules/ZenBoostStyles.sys.mjs'; + +class nsZenBoostsManager { + registeredBoosts = new Map(); + + #stylesManager = new nsZenBoostStyles(); + #saveFilename = 'zen-boosts.jsonlz4'; + + constructor() { + this.#init(); + } + + /** + * Initializes the boosts manager by reading boosts from persistent storage. + * @private + */ + #init() { + this.#readBoostsFromStore(this.notify); + } + + /** + * Deletes a boost for the specified domain and persists the change to disk. + * @param {string} domain - The domain for which to delete the boost. + */ + deleteBoost(domain) { + if (this.registeredBoosts.has(domain)) this.registeredBoosts.delete(domain); + this.#writeToDisk(this.registeredBoosts); + this.notify(); + } + + /** + * Loads a boost configuration for the specified domain from storage. + * If no boost exists for the domain, creates and returns a new default boost configuration. + * @param {string} domain - The domain for which to load the boost. + * @returns {Object} The boost data object containing all boost settings for the domain. + */ + loadBoostFromStore(domain) { + if (!domain) console.error('[ZenBoostsManager] Domain expected but got null.'); + + let boostData = { + domain, + boostName: 'My Boost', + + dotAngleDeg: 0, + dotPos: { x: null, y: null }, + dotDistance: 0, + + brightness: 0.5, + saturation: 0.5, + contrast: 0.5, + + fontFamily: '', + + enableColorBoost: false, + smartInvert: false, + + // Choses theme based on Zen's workspace theme + autoTheme: false, + + // Default to 100% scale + siteSizeOverride: 1, + textCaseOverride: 'none', + + changeWasMade: false, + }; + + if (this.registeredBoosts.has(domain)) { + boostData = this.registeredBoosts.get(domain); + } else { + this.registeredBoosts.set(domain, boostData); + } + + return boostData; + } + + /** + * Updates the boost data for a domain in memory and notifies observers of the change. + * @param {Object} boostData - The boost data object to update. + */ + updateBoost(boostData) { + this.registeredBoosts.set(boostData.domain, boostData); + this.#stylesManager.invalidateStyleForDomain(boostData.domain); + this.notify(); + } + + /** + * Notifies all observers that boost data has been updated. + * This triggers a 'zen-boosts-update' notification event. + */ + notify(unloadStyles = false) { + Services.obs.notifyObservers(null, 'zen-boosts-update', { unloadStyles }); + } + + /** + * Saves a boost configuration to persistent storage and notifies observers. + * @param {Object|null} boostData - The boost data object to save. If null, only saves existing boosts. + */ + saveBoostToStore(boostData) { + if (boostData != null) this.registeredBoosts.set(boostData.domain, boostData); + this.#writeToDisk(this.registeredBoosts); + 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((map) => { + this.registeredBoosts = map; + 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); + } + + /** + * Reads boost data from disk, decompresses it, and converts it to a Map. + * @returns {Promise} A promise that resolves to a Map of domain to boost data. + * @private + */ + async #readFromDisk() { + const savePath = this.#storePath; + + if (!(await IOUtils.exists(savePath))) return new Map(); + + const array = await IOUtils.readJSON(savePath, { decompress: true }); + return new Map(array); + } + + /** + * 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 array = Array.from(map.entries()); + IOUtils.writeJSON(this.#storePath, array, { compress: true }); + } + + /** + * Checks if a boost is registered for the specified domain. + * @param {string} domain - The domain to check for a registered boost. + * @returns {boolean} True if a boost exists for the domain, false otherwise. + */ + registeredBoostForDomain(domain) { + return this.registeredBoosts.has(domain); + } + + /** + * 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) { + return uri.schemeIs('http') || uri.schemeIs('https'); + } + + /** + * @brief Gets from cache or creates and caches a new style sheet for the given boost data. + * @param {Object} boostData - The boost data object containing all boost settings for the domain. + * @returns {nsIStyleSheet} The style sheet corresponding to the boost data. + */ + getStyleSheetForBoost(boostData) { + return this.#stylesManager.getStyleForBoost(boostData); + } +} + +export const gZenBoostsManager = new nsZenBoostsManager(); diff --git a/src/zen/boosts/actors/ZenBoostsChild.sys.mjs b/src/zen/boosts/actors/ZenBoostsChild.sys.mjs new file mode 100644 index 0000000000..1c38a516d6 --- /dev/null +++ b/src/zen/boosts/actors/ZenBoostsChild.sys.mjs @@ -0,0 +1,172 @@ +// 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; + +export class ZenBoostsChild extends JSWindowActorChild { + #currentSheet = null; + + /** + * Creates a new ZenBoostsChild actor instance. + */ + constructor() { + super(); + } + + /** + * 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 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} 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. + return (contrast << 24) | (b << 16) | (g << 8) | r; + } + + /** + * 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 + * @return {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)]; + } + + /** + * 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 'DOMDocElementInserted': + this.#applyBoostForPageIfAvailable(); + break; + default: + } + } + + /** + * 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); + } + } + } + + /** + * From ZenGradientGenerator.mjs + * Helper function for hslToRgb conversion + */ + #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; + } + + /** + * 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 null; + } + + const domain = browsingContext.topWindow?.location?.host; + if (!domain) { + return null; + } + + const boost = await this.sendQuery('ZenBoost:GetBoostForDomain', domain); + + if (unloadStyles || !boost?.enableColorBoost) { + this.#unloadCurrentStyleSheet(); + } + + if (boost) { + if (boost.enableColorBoost) { + let prefersColorSchemeOverride = 'none'; + if (boost.smartInvert) { + prefersColorSchemeOverride = boost.topWindowIsDarkMode ? 'light' : 'dark'; + } + browsingContext.prefersColorSchemeOverride = prefersColorSchemeOverride; + // Has to be a finite value for zoom to work correctly + // TODO: Figure out something better for site size override + // browsingContext.fullZoom = boost.siteSizeOverride; + if (boost.styleSheet) { + const { styleSheet } = boost; + styleSheet.uri = Services.io.newURI(styleSheet.uri); + if (this.#currentSheet?.uuid !== styleSheet.uuid) { + browsingContext.window.windowUtils.loadSheet(styleSheet.uri, AGENT_SHEET); + this.#currentSheet = styleSheet; + } + } + const rgbColor = this.#hslToRgb( + boost.dotAngleDeg / 360, + /* already is [0, 1] */ + boost.dotDistance * (1 - boost.saturation), + /* lightness range from [0.2, 0.6] */ + 0.1 + boost.dotDistance * 0.6 * boost.brightness + ); + const nsColor = this.#rgbToNSColor(rgbColor, (1 - boost.contrast) * 255); + browsingContext.zenBoostsData = nsColor; + return; + } + } + + browsingContext.prefersColorSchemeOverride = 'none'; + browsingContext.zenBoostsData = 0; + } + + #unloadCurrentStyleSheet() { + const browsingContext = this.browsingContext; + if (this.#currentSheet && browsingContext) { + browsingContext.window.windowUtils.removeSheet(this.#currentSheet.uri, AGENT_SHEET); + this.#currentSheet = null; + } + } +} diff --git a/src/zen/boosts/actors/ZenBoostsParent.sys.mjs b/src/zen/boosts/actors/ZenBoostsParent.sys.mjs new file mode 100644 index 0000000000..53c80e2521 --- /dev/null +++ b/src/zen/boosts/actors/ZenBoostsParent.sys.mjs @@ -0,0 +1,68 @@ +// 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/ZenBoostsManager.sys.mjs', +}); + +export class ZenBoostsParent extends JSWindowActorParent { + /** + * Creates a new ZenBoostsParent actor instance and sets up an observer + * for boost update notifications. + */ + constructor() { + super(); + + this._observe = this.observe.bind(this); + Services.obs.addObserver(this._observe, 'zen-boosts-update'); + } + + /** + * Called when the actor is destroyed. Cleans up the observer. + */ + didDestroy() { + Services.obs.removeObserver(this._observe, 'zen-boosts-update'); + } + + /** + * 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) { + if (topic === 'zen-boosts-update') { + this.sendQuery('ZenBoost:BoostDataUpdated', { unloadStyles: true }); + } + } + + /** + * 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} A promise that resolves to the boost data or null. + */ + async receiveMessage(message) { + switch (message.name) { + case 'ZenBoost:GetBoostForDomain': { + const domain = message.data; + const embedder = this.browsingContext.top.embedderElement; + if (!embedder || !domain) return null; + const exists = lazy.gZenBoostsManager.registeredBoostForDomain(domain); + if (!exists) return null; + const topWindowIsDarkMode = + embedder.ownerGlobal.getComputedStyle(embedder).colorScheme === 'dark'; + return { + ...lazy.gZenBoostsManager.loadBoostFromStore(domain), + topWindowIsDarkMode, + styleSheet: await lazy.gZenBoostsManager.getStyleSheetForBoost(domain), + }; + } + default: + console.warn(`[ZenBoostsParent]: Unknown message: ${message.name}`); + } + } +} diff --git a/src/zen/boosts/components.conf b/src/zen/boosts/components.conf new file mode 100644 index 0000000000..d6b81daa17 --- /dev/null +++ b/src/zen/boosts/components.conf @@ -0,0 +1,15 @@ +# 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/. + +Classes = [ + { + 'cid': '{7d46aa4e-7486-4f77-ab47-81125f1a5723}', + 'interfaces': ['nsIZenBoostsBackend'], + 'contract_ids': ['@mozilla.org/zen/boosts-backend;1'], + 'type': 'zen::nsZenBoostsBackend', + 'headers': ['mozilla/nsZenBoostsBackend.h'], + 'js_name': 'boosts', + 'processes': ProcessSelector.CONTENT_PROCESS_ONLY, + }, +] diff --git a/src/zen/boosts/moz.build b/src/zen/boosts/moz.build new file mode 100644 index 0000000000..68d62ac632 --- /dev/null +++ b/src/zen/boosts/moz.build @@ -0,0 +1,33 @@ +# 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 += [ + "ZenBoostsEditor.sys.mjs", + "ZenBoostsManager.sys.mjs", + "ZenBoostStyles.sys.mjs", +] + +FINAL_TARGET_FILES.actors += [ + "actors/ZenBoostsChild.sys.mjs", + "actors/ZenBoostsParent.sys.mjs", +] + +XPIDL_SOURCES += [ + "nsIZenBoostsBackend.idl", +] + +EXPORTS.mozilla += [ + "nsZenBoostsBackend.h", +] + +SOURCES += [ + "nsZenBoostsBackend.cpp", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +FINAL_LIBRARY = "xul" +XPIDL_MODULE = "zen_boosts" diff --git a/src/zen/boosts/nsIZenBoostsBackend.idl b/src/zen/boosts/nsIZenBoostsBackend.idl new file mode 100644 index 0000000000..a7b1aa612c --- /dev/null +++ b/src/zen/boosts/nsIZenBoostsBackend.idl @@ -0,0 +1,31 @@ +/* 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 "nsISupports.idl" + +/** + * @brief Interface for Zen boosts backend. + */ +[scriptable, uuid(7d46aa4e-7486-4f77-ab47-81125f1a5723)] +interface nsIZenBoostsBackend : nsISupports { + %{C++ + /* + * @brief Called when the presshell is entered. See nsDisplayListBuilder::EnterPresShell + * for context. + */ + auto onPressShellEntered(nsPresContext* aPresContext) -> void; + + /* + * @brief Called when the presshell is exited. + */ + auto onPressShellLeave(nsPresContext* aPresContext) -> void; + + /* + * Recomputes the data dependent on the browsing context, like zoom and text + * zoom. We use it to store Zen boosts related data too. + */ + void RecomputeBrowsingContextDependentData(nsPresContext* aPresContext, + mozilla::dom::BrowsingContext* aBrowsingContext); + %} +}; diff --git a/src/zen/boosts/nsZenBoostsBackend.cpp b/src/zen/boosts/nsZenBoostsBackend.cpp new file mode 100644 index 0000000000..6b1fb07ddc --- /dev/null +++ b/src/zen/boosts/nsZenBoostsBackend.cpp @@ -0,0 +1,186 @@ +/* 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/RefPtr.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/BrowsingContext.h" + +#define COLOR_CHANNEL_MIDPOINT 128 + +using BrowsingContext = mozilla::dom::BrowsingContext; +using BoostData = nscolor; // For now, Zen boosts data is just a color. + +/** + * @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, + BoostData aOldValue) { + MOZ_ASSERT(IsTop()); + if (ZenBoostsData() == aOldValue) { + return; + } + PresContextAffectingFieldChanged(); +} + +namespace zen { +namespace { + +/** + * @brief Clamps a value to the range [0, 255] using branchless operations. + * @param v The value to clamp. + * @return The clamped value in the range [0, 255]. + */ +static __inline int32_t clamp255(int32_t v) { + // llvm x86 is poor at ternary operator, so use branchless min/max. + return (((255 - (v)) >> 31) | (v)) & 255; +} + +/** + * @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. + */ +static nscolor zenFilterColorChannel(nscolor aOriginalColor, nscolor aAccentColor) { + auto r1 = NS_GET_R(aOriginalColor); + auto g1 = NS_GET_G(aOriginalColor); + auto b1 = NS_GET_B(aOriginalColor); + + auto r2 = NS_GET_R(aAccentColor); + auto g2 = NS_GET_G(aAccentColor); + auto b2 = NS_GET_B(aAccentColor); + + // 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. + auto contrast = NS_GET_A(aAccentColor); + + // Approximate perceived luminance in sRGB space + // Coefficients per Rec.709; gamma correction ignored for speed + double origLum = 0.2126 * r1 + 0.7152 * g1 + 0.0722 * b1; + double accentLum = 0.2126 * r2 + 0.7152 * g2 + 0.0722 * b2; + + double scale = accentLum > 0.0 ? (origLum / accentLum) : 1.0; + + double fr = r2 * scale; + double fg = g2 * scale; + double fb = b2 * scale; + + // Apply contrast adjustment: map contrast from 0–255 to -1.0–+1.0 + // contrast = 0: maximum darkening (mix toward black) + // contrast = 127.5: no change + // contrast = 255: maximum lightening (mix toward white) + double contrastFactor = (contrast - 128.0) / 128.0; + + // Compute perceived luminance for the filtered color + double lum = 0.2126 * fr + 0.7152 * fg + 0.0722 * fb; + + // If it's bright, mix toward white; if dark, mix toward black + if (lum >= COLOR_CHANNEL_MIDPOINT) { + double mix = (lum - COLOR_CHANNEL_MIDPOINT) / COLOR_CHANNEL_MIDPOINT; + double amount = contrastFactor * mix; + fr = fr + (255.0 - fr) * amount; + fg = fg + (255.0 - fg) * amount; + fb = fb + (255.0 - fb) * amount; + } else { + double mix = (COLOR_CHANNEL_MIDPOINT - lum) / COLOR_CHANNEL_MIDPOINT; + double amount = -contrastFactor * mix; + fr = fr * (1.0 - amount); + fg = fg * (1.0 - amount); + fb = fb * (1.0 - amount); + } + + // Clamp to [0,255] using fast branchless clamp + uint8_t fr8 = clamp255(fr); + uint8_t fg8 = clamp255(fg); + uint8_t fb8 = clamp255(fb); + + return NS_RGB(fr8, fg8, fb8); +} + +} // namespace + +// Use the macro to inject all of the definitions for nsISupports. +NS_IMPL_ISUPPORTS(nsZenBoostsBackend, nsIZenBoostsBackend) +nsZenBoostsBackend::nsZenBoostsBackend() {}; + +auto nsZenBoostsBackend::onPressShellEntered(nsPresContext* aPresContext) -> void { + if (!aPresContext) { + return; + } + + mCurrentPresContext = aPresContext; +} + +auto nsZenBoostsBackend::onPressShellLeave(nsPresContext* aPresContext) -> void { + // TODO: We should set it as a null as well, but this prevents borders and shadows + // from being drawn into our Zen boosts modifications. + if (!aPresContext) { + return; + } + mCurrentPresContext = aPresContext; +} + +auto nsZenBoostsBackend::RecomputeBrowsingContextDependentData( + nsPresContext* aPresContext, mozilla::dom::BrowsingContext* aBrowsingContext) -> void { + if (!aPresContext || aPresContext->IsChrome()) { + return; + } + + auto previousData = aPresContext->mZenBoostsPresContextData; + aPresContext->mZenBoostsPresContextData = aBrowsingContext->ZenBoostsData(); + if (previousData != aPresContext->mZenBoostsPresContextData) { + // Lets ask the prescontext to restyle the document + aPresContext->MediaFeatureValuesChanged( + {mozilla::RestyleHint::RecascadeSubtree(), NS_STYLE_HINT_VISUAL, + mozilla::MediaFeatureChangeReason::PreferenceChange}, + mozilla::MediaFeatureChangePropagation::JustThisDocument); + } +} + +auto nsZenBoostsBackend::ResolveStyleColor( + mozilla::StyleAbsoluteColor aColor) -> mozilla::StyleAbsoluteColor { + static nsCOMPtr zenBoosts( + do_GetService(ZEN_BOOSTS_BACKEND_CONTRACTID)); + + if (zenBoosts) { + if (auto presContext = zenBoosts->mCurrentPresContext) { + if (auto accentNS = presContext->mZenBoostsPresContextData) { + // 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 + nscolor originalNS = aColor.ToColor(); + nscolor filteredNS = zenFilterColorChannel(originalNS, accentNS); + + auto filtered = mozilla::StyleAbsoluteColor::FromColor(filteredNS); + filtered.alpha = aColor.alpha; + return filtered; + } + } + } + + return aColor; +} + +} // namespace zen diff --git a/src/zen/boosts/nsZenBoostsBackend.h b/src/zen/boosts/nsZenBoostsBackend.h new file mode 100644 index 0000000000..861e22c73f --- /dev/null +++ b/src/zen/boosts/nsZenBoostsBackend.h @@ -0,0 +1,65 @@ +/* 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 "nsIZenBoostsBackend.h" + +#include "mozilla/RefPtr.h" + +#define ZEN_BOOSTS_BACKEND_CONTRACTID \ + "@mozilla.org/zen/boosts-backend;1" + +namespace zen { + +class nsZenBoostsBackend final : public nsIZenBoostsBackend { + NS_DECL_ISUPPORTS + + public: + explicit nsZenBoostsBackend(); + + /** + * @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; + + /** + * @brief Called when a presshell is entered during rendering. + * @param aPresContext The presentation context that was entered. + */ + auto onPressShellEntered(nsPresContext* aPresContext) -> void; + + /** + * @brief Called when a presshell is left during rendering. + * @param aPresContext The presentation context that was left. + */ + auto onPressShellLeave(nsPresContext* aPresContext) -> void; + + /** + * @brief Recomputes browsing context dependent data, including Zen boost data. + * Triggers a restyle if the boost data has changed. + * @param aPresContext The presentation context to update. + * @param aBrowsingContext The browsing context containing the boost data. + */ + void RecomputeBrowsingContextDependentData(nsPresContext* aPresContext, + mozilla::dom::BrowsingContext* aBrowsingContext); + NS_DECL_NSIZENBOOSTSBACKEND + private: + ~nsZenBoostsBackend() = default; + + /** + * The presshell of the current document being rendered. + */ + RefPtr mCurrentPresContext; +}; + +} // namespace zen + +#endif diff --git a/src/zen/boosts/zen-advanced-color-options.css b/src/zen/boosts/zen-advanced-color-options.css new file mode 100644 index 0000000000..aa89016bf7 --- /dev/null +++ b/src/zen/boosts/zen-advanced-color-options.css @@ -0,0 +1,54 @@ +/* + * 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; + -zen-window-transform-origin: 50% 0%; + padding: 8px 8px; + + & p { + color: #3a3a3b; + } + + & input { + width: 185px; + appearance: none !important; + height: 4px; + + background-color: #b5b5b9; + border-radius: 2px; + + margin-top: 10px; + } + + & separator { + height: 24px !important; + } + + @media (-moz-platform: macos) { + &[animate='open'] { + animation: zen-color-options-panel-animation-macos 0.35s cubic-bezier(0.29, 1.37, 0.87, 1) + forwards !important; + } + } +} + +#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; +} + +#zen-boost-advanced-color-options-container { + display: flex !important; + flex-direction: column !important; +} diff --git a/src/zen/boosts/zen-boost-editor.xhtml b/src/zen/boosts/zen-boost-editor.xhtml new file mode 100644 index 0000000000..795a964ec9 --- /dev/null +++ b/src/zen/boosts/zen-boost-editor.xhtml @@ -0,0 +1,107 @@ +#filter substitution + +# -*- 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/. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + + + + +
+ + + +
+ + + + + + +
+ + + + + + +
+
+ + + +
+

Contrast

+ + +

Brightness

+ + +

Original Saturation

+ +
+
+
+
+ diff --git a/src/zen/boosts/zen-boosts.css b/src/zen/boosts/zen-boosts.css new file mode 100644 index 0000000000..1a3273d416 --- /dev/null +++ b/src/zen/boosts/zen-boosts.css @@ -0,0 +1,512 @@ +/* + * 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/. + */ + +#zenSettingsWindow { + animation: 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94) zen-boost-window-in; + max-width: 185px; + min-width: 185px; + min-height: 528px; +} + +body { + overflow: clip; + display: flex; + margin: 0; + width: 100%; +} + +#zen-boost-editor-root { + background-color: #fcfcfe; + user-select: none; + + & button { + border-radius: 10px !important; + + padding: 0 !important; + min-width: fit-content !important; + + border: solid 6px transparent !important; + width: 26px; + height: 26px; + + transition: background 0.1s cubic-bezier(0.075, 0.82, 0.165, 1); + appearance: none; + + &:active { + scale: 1 !important; + transform: none !important; + } + } + + & .button-text { + display: none; + } + + &[disabled] { + opacity: 0.5; + cursor: not-allowed; + } +} + +@keyframes zen-boost-window-in { + from { + transform: scale(0.9) translateY(20px); + opacity: 0; + filter: blur(6px); + } + to { + transform: scale(1) translateY(0); + opacity: 1; + filter: none; + } +} + +.subviewbutton { + color: #3a3a3b; +} + +#zen-boost-font-arial { + font-family: Arial, sans-serif; +} + +#zen-boost-font-serif { + font-family: serif; +} + +#zen-boost-font-mono { + font-family: monospace, monospace; +} + +#zen-boost-editor-view { + gap: 10px; +} + +#zen-boost-head-wrapper { + -moz-window-dragging: drag; /* Allow dragging the window with the custom titlebar */ + + margin-bottom: -10px; + height: 40px !important; + min-height: 40px; + max-height: 40px; + align-items: center; + background-color: #f6f6f8; + border: solid 1px #ededef; + + display: flex; + + flex-direction: row-reverse; + @media (-moz-platform: macos) { + flex-direction: row; + } + + & button { + height: 24px !important; + width: 24px !important; + aspect-ratio: 1/1; + background-color: transparent !important; + opacity: 0.45; + + &:hover { + opacity: 0.6; + } + } + + & #zen-boost-name { + height: 24px !important; + width: 90px !important; + color: black !important; + background-color: transparent; + border: none; + outline: none; + + display: flex; + align-items: center; + + opacity: 0.85; + + &:hover { + background-color: #ebebed; + opacity: 1; + } + } +} + +#zen-boost-name-text { + width: 100%; + text-align: center; + justify-content: center; + margin: auto; +} + +#zen-boost-font-grid { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 0px; + width: 100%; + + padding: 2px; + + & button { + padding: auto !important; + margin: auto !important; + background-color: transparent; + } +} + +#zen-boost-filter-wrapper { + padding: 20px; + gap: 14px; + -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-size { + list-style-type: none !important; + + & #zen-boost-size-value { + text-align: right; + right: 0; + margin: auto; + } +} + +.zen-boost-panel-disabled { + filter: grayscale(1); +} + +#zen-boost-filter-wrapper separator { + height: 7px !important; +} + +.title-button { + scale: 0.9; +} + +.mod-button { + height: 38px !important; + + transition: + 0.35s opacity cubic-bezier(0.075, 0.82, 0.165, 1), + 0.35s filter cubic-bezier(0.075, 0.82, 0.165, 1) !important; + + background-color: #ebebed; + &:hover { + opacity: 0.9; + } + + &:active { + scale: 1 !important; + transform: none !important; + + opacity: 0.9; + filter: brightness(0.9); + } +} + +#zen-boost-name { + height: 26px !important; + font-size: 8pt !important; + text-indent: 2px; + vertical-align: middle; + color: #3a3a3b !important; + + justify-content: center; + + & #zen-boost-name-text { + translate: 0px -2px; + } +} + +#zen-boost-shuffle { + margin-left: 6px; + margin-right: 6px; +} + +#zen-boost-close { + margin-right: 10px; +} + +.zen-boost-font-button-active { + background-color: #5b5b5c22 !important; +} + +.zen-boost-font-button-active:hover { + background-color: #5b5b5c32 !important; +} + +.zen-boost-button-active { + background-color: #3a3a3a !important; + color: #fcfcfe !important; +} + +.zen-boost-button-active:hover { + background-color: #5b5b5c !important; +} + +#zen-boost-magic-theme { + width: 18px !important; + height: 18px !important; + margin-top: 12px; + padding: 2px; + + z-index: 4; + + position: relative; + margin-left: auto; + margin-right: auto; +} + +#zen-boost-toolbar-wrapper { + width: auto; + margin: auto; + gap: 0.75em; + + & .small { + width: 40px; + margin: auto !important; + padding: 5px 5px 5px 5px !important; + } + + & .med { + width: 70px; + margin: auto !important; + padding: 3px 5px 5px 3px !important; + + display: flex; + text-align: center; + + vertical-align: middle; + justify-content: center; + align-items: center; + font-size: 10pt; + } +} + +#zen-boost-font-select { + width: 85px !important; + height: 20px !important; + + color: #727272 !important; + background-color: transparent; + background: none; + + font-size: 9pt; + + padding: 0px !important; + text-indent: 2px; + + margin-left: 8px !important; + margin-right: 8px !important; + margin-top: 8px !important; + + &:hover { + background-color: #9a9a9a44; + } +} + +#zen-boost-font-toolbar { + display: flex; + flex-direction: row; + + & button { + border-radius: 6px !important; + min-width: fit-content !important; + height: 20px !important; + + margin-left: 8px; + margin-right: 8px; + margin-top: 8px; + background-color: transparent !important; + } +} + +#zen-boost-font-wrapper { + box-shadow: 0px 4px 8px #00000020; + background-color: #ffffff; + 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; + + opacity: 0.25; + + border-top: solid 1px #9a9a9a !important; + } + + & button { + filter: none; + color: #9a9a9a !important; + border-radius: 99px !important; + transition: + 0.15s transform cubic-bezier(0.075, 0.82, 0.165, 1), + 0.15s background-color cubic-bezier(0.075, 0.82, 0.165, 1); + + padding: 2px; + + &:hover { + transform: scale(1.1); + background-color: #9a9a9a30; + } + } +} + +.mod-button[mode='orange'] { + color: #e3e9e4 !important; + background: linear-gradient(180deg, #ffab35 0%, #ffa01d 100%) border-box; +} +.mod-button[mode='orange-red'] { + color: #e3e9e4 !important; + background: linear-gradient(180deg, #ff723b 0%, #ff5b1b 100%) border-box; +} +.mod-button[mode='red'] { + color: #e3e9e4 !important; + background: linear-gradient(180deg, #ff3e45 0%, #ff121b 100%) border-box; +} +.mod-button[mode='blue'] { + color: #e3e9e4 !important; + background: linear-gradient(180deg, #5d45ff 0%, #4125ff 100%) border-box; +} + +#zen-boost-text-case-toggle[mode='none'] { + opacity: 0.5; +} + +.zen-boost-color-picker-gradient::after { + content: ''; + position: absolute; + inset: 0; + border-radius: 8px; + margin: 8px; + + z-index: 2; + + background: #fbfbfdf2; + background-image: radial-gradient(#e3e9e4 0.5px, transparent 0); + background-position: -23px -23px; + background-size: 4px 4px; +} + +.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: 0px 4px 8px #00000021; + width: 100%; + aspect-ratio: 1 /1; + margin: 10px 0px 10px 0px; + transition: filter 0.5s cubic-bezier(0.075, 0.82, 0.165, 1) !important; + min-height: calc(var(--panel-width) - var(--panel-padding) * 2 - 2px); + + & .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: 0px 2px 4px #00000022; + position: absolute; + z-index: 4; + width: 18px; + height: 18px; + 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; + animation: zen-boost-color-picker-dot-animation 0.5s; + transform: translate(-50%, -50%); + pointer-events: none; + transform-origin: center center; + + &:first-of-type { + width: 34px; + height: 34px; + 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%) !important; + } + } +} + +.zen-boost-color-picker-gradient:hover { + & .zen-boost-color-picker-circle { + opacity: 0.4 !important; + } +} diff --git a/src/zen/common/ZenActorsManager.sys.mjs b/src/zen/common/ZenActorsManager.sys.mjs index bfaf184afa..69c0cc0f45 100644 --- a/src/zen/common/ZenActorsManager.sys.mjs +++ b/src/zen/common/ZenActorsManager.sys.mjs @@ -54,6 +54,20 @@ let JSWINDOWACTORS = { matches: ['*://*/*'], enablePreference: 'zen.glance.enabled', }, + ZenBoosts: { + parent: { + esModuleURI: 'resource:///actors/ZenBoostsParent.sys.mjs', + }, + child: { + esModuleURI: 'resource:///actors/ZenBoostsChild.sys.mjs', + events: { + DOMDocElementInserted: { capture: true }, + }, + }, + allFrames: true, + matches: ['*://*/*'], + enablePreference: 'zen.boosts.enabled', + }, }; export let gZenActorsManager = { diff --git a/src/zen/common/ZenUIManager.mjs b/src/zen/common/ZenUIManager.mjs index f8c11d47aa..c76d3d4024 100644 --- a/src/zen/common/ZenUIManager.mjs +++ b/src/zen/common/ZenUIManager.mjs @@ -162,6 +162,61 @@ var gZenUIManager = { }; }, + openBoostWindow() { + const screenX = window.screenX; + const screenY = window.screenY; + const width = window.outerWidth; + const height = window.outerHeight; + + // TODO: This needs to be changed to exact values + const editorWidth = 185; + const editorHeight = 575; + const pad = 20; + + const animationTarget = 25; + + let left = screenX + width + pad; + let top = screenY + height / 2 - editorHeight / 2; + + if (left + editorWidth > screen.availWidth) { + left = screenX + width - (editorWidth + pad); + } + + const editor = Services.ww.openWindow( + window, + 'chrome://browser/content/zen-components/windows/zen-boost-editor.xhtml', + null, + `left=${left},top=${top + animationTarget},chrome,alwaysontop,resizable=no`, + null + ); + + // Close the editor if the tab is switched + window.gBrowser.tabContainer.addEventListener( + 'TabSelect', + (event) => { + // This seems to be a safer way than doing currentURI.host + const url = new URL(event.target.linkedBrowser.currentURI.spec); + const domain = url.hostname; + + // Close if domain doesn't match + if (domain != editor.domain) { + editor.close(); + } + }, + // Remove the event listener after the window closes + { once: true } + ); + + // Give the domain + const domain = window.gBrowser.selectedTab.linkedBrowser.currentURI.host; + editor.domain = domain; + + // Give the animator + editor.gZenUIManager = this; + + return editor; + }, + updateTabsToolbar() { const kUrlbarHeight = 335; gURLBar.textbox.style.setProperty( diff --git a/src/zen/common/styles/zen-animations.css b/src/zen/common/styles/zen-animations.css index ae77e95021..b05610cf23 100644 --- a/src/zen/common/styles/zen-animations.css +++ b/src/zen/common/styles/zen-animations.css @@ -16,6 +16,18 @@ } } +@keyframes zen-color-options-panel-animation-macos { + 0% { + -moz-window-opacity: 0; + -moz-window-transform: scale(0); + } + + 100% { + -moz-window-opacity: 1; + -moz-window-transform: scale(1); + } +} + @keyframes zen-theme-picker-dot-animation { from { transform: scale(0.8) translate(-50%, -50%); @@ -28,6 +40,18 @@ } } +@keyframes zen-boost-color-picker-dot-animation { + from { + transform: scale(0.8) translate(-50%, -50%); + } + 50% { + transform: scale(1.2) translate(-50%, -50%); + } + to { + transform: scale(1) translate(-50%, -50%); + } +} + /* Mark: Zen Glance */ @keyframes zen-glance-overlay-animation { from { diff --git a/src/zen/common/styles/zen-single-components.css b/src/zen/common/styles/zen-single-components.css index 0948413858..8d9a8269d2 100644 --- a/src/zen/common/styles/zen-single-components.css +++ b/src/zen/common/styles/zen-single-components.css @@ -487,6 +487,8 @@ body > #confetti { padding-top: 8px; margin: 2px 8px 8px 8px; + gap: 8px; + & toolbarbutton { margin: 0; } diff --git a/src/zen/glance/actors/ZenGlanceChild.sys.mjs b/src/zen/glance/actors/ZenGlanceChild.sys.mjs index 8ebf72eaaa..0e27b7b26c 100644 --- a/src/zen/glance/actors/ZenGlanceChild.sys.mjs +++ b/src/zen/glance/actors/ZenGlanceChild.sys.mjs @@ -1,6 +1,7 @@ // 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 ZenGlanceChild extends JSWindowActorChild { #activationMethod; diff --git a/src/zen/glance/actors/ZenGlanceParent.sys.mjs b/src/zen/glance/actors/ZenGlanceParent.sys.mjs index 159a5f5598..18ef441ee9 100644 --- a/src/zen/glance/actors/ZenGlanceParent.sys.mjs +++ b/src/zen/glance/actors/ZenGlanceParent.sys.mjs @@ -1,6 +1,7 @@ // 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 ZenGlanceParent extends JSWindowActorParent { constructor() { super(); diff --git a/src/zen/glance/moz.build b/src/zen/glance/moz.build index 9588459dc1..39567ca4cf 100644 --- a/src/zen/glance/moz.build +++ b/src/zen/glance/moz.build @@ -1,4 +1,3 @@ -# # 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/. diff --git a/src/zen/images/boost-indicator.svg b/src/zen/images/boost-indicator.svg new file mode 100644 index 0000000000..4829b24b97 --- /dev/null +++ b/src/zen/images/boost-indicator.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + diff --git a/src/zen/moz.build b/src/zen/moz.build index 21915a69ab..01a9caaea6 100644 --- a/src/zen/moz.build +++ b/src/zen/moz.build @@ -13,4 +13,5 @@ DIRS += [ "tests", "urlbar", "toolkit", + "boosts", ] diff --git a/src/zen/urlbar/ZenSiteDataPanel.sys.mjs b/src/zen/urlbar/ZenSiteDataPanel.sys.mjs index 48b94bace1..5a2e6474ce 100644 --- a/src/zen/urlbar/ZenSiteDataPanel.sys.mjs +++ b/src/zen/urlbar/ZenSiteDataPanel.sys.mjs @@ -8,6 +8,7 @@ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { FeatureCallout: 'resource:///modules/asrouter/FeatureCallout.sys.mjs', + gZenBoostsManager: 'resource:///modules/ZenBoostsManager.sys.mjs', }); export class nsZenSiteDataPanel { @@ -45,6 +46,7 @@ export class nsZenSiteDataPanel { this.#initCopyUrlButton(); this.#initEventListeners(); + this.#initBrowserListeners(); this.#maybeShowFeatureCallout(); } @@ -57,6 +59,7 @@ export class nsZenSiteDataPanel { 'zen-site-data-header-share', 'zen-site-data-header-bookmark', 'zen-site-data-security-info', + 'zen-site-data-boost', 'zen-site-data-actions', 'zen-site-data-new-addon-button', ]; @@ -68,6 +71,50 @@ export class nsZenSiteDataPanel { this.#initContextMenuEventListener(); } + #initBrowserListeners() { + Services.obs.addObserver(this, 'zen-boosts-update'); + this.window.gBrowser.addProgressListener({ + onLocationChange: (aWebProgress) => { + if (aWebProgress.isTopLevel) { + this.checkIfTabIsBoosted(); + } + }, + }); + this.window.addEventListener( + 'unload', + () => { + Services.obs.removeObserver(this, 'zen-boosts-update'); + }, + { once: true } + ); + } + + observe(subject, topic) { + switch (topic) { + case 'zen-boosts-update': + this.checkIfTabIsBoosted(); + break; + } + } + + #getCurrentDomain() { + try { + return this.window.gBrowser.currentURI.host; + } catch { + return ''; + } + } + + checkIfTabIsBoosted() { + const domain = this.#getCurrentDomain(); + const isBoosted = lazy.gZenBoostsManager.registeredBoostForDomain(domain); + if (isBoosted) { + this.anchor.setAttribute('boosting', 'true'); + } else { + this.anchor.removeAttribute('boosting'); + } + } + #initCopyUrlButton() { // This function is a bit out of place, but it's related enough to the panel // that it's easier to do it here than in a separate module. @@ -125,12 +172,29 @@ export class nsZenSiteDataPanel { } #preparePanel() { + this.#resetSiteOptionsList(); + this.#setSiteBoost(); this.#setSitePermissions(); this.#setSiteSecurityInfo(); this.#setSiteHeader(); this.#setAddonsOverflow(); } + #setSiteBoost() { + const boostButton = this.document.getElementById('zen-site-data-boost'); + const domain = this.#getCurrentDomain(); + const uri = this.window.gBrowser.currentURI; + + if (!lazy.gZenBoostsManager.canBoostSite(uri)) { + boostButton.removeAttribute('boosting'); + return; + } + + if (lazy.gZenBoostsManager.registeredBoostForDomain(domain)) + boostButton.setAttribute('boosting', 'true'); + else boostButton.removeAttribute('boosting'); + } + #setAddonsOverflow() { const addons = this.document.getElementById('zen-site-data-addons'); if (addons.getBoundingClientRect().height > 420) { @@ -194,6 +258,11 @@ export class nsZenSiteDataPanel { return uri.scheme.startsWith('http'); } + #resetSiteOptionsList() { + const list = this.document.getElementById('zen-site-data-settings-list'); + list.innerHTML = ''; + } + #setSiteSecurityInfo() { const { gIdentityHandler } = this.window; const button = this.document.getElementById('zen-site-data-security-info'); @@ -326,7 +395,6 @@ export class nsZenSiteDataPanel { } const separator = this.document.createXULElement('toolbarseparator'); - list.innerHTML = ''; list.appendChild(separator); const settingElements = []; const crossSiteCookieElements = []; @@ -463,6 +531,10 @@ export class nsZenSiteDataPanel { this.window.gIdentityHandler._openPopup(event); break; } + case 'zen-site-data-boost': { + this.window.gZenUIManager.openBoostWindow(); + break; + } case 'zen-site-data-actions': { const button = this.document.getElementById('zen-site-data-actions'); const popup = this.document.getElementById('zenSiteDataActions'); @@ -560,7 +632,9 @@ export class nsZenSiteDataPanel { break; } default: { - const item = event.target.closest('.permission-popup-permission-item'); + const item = event.target.closest( + '.permission-popup-permission-item, .permission-popup-generic-item' + ); if (!item) { break; }