no-bug: Add secondary color dot, Rearrange buttons & Editor UI Updates (gh-13708)

Co-authored-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
This commit is contained in:
fen4flo
2026-05-16 02:53:35 +02:00
committed by GitHub
parent 90a6ebe948
commit ba348e04f0
13 changed files with 570 additions and 104 deletions

View File

@@ -11,6 +11,7 @@ zen-boost-edit-reset =
zen-boost-edit-delete =
.label = Delete Boost
zen-boost-size = Size
zen-boost-case = Case
zen-boost-zap = Zap
zen-boost-code = Code
zen-boost-back = Back

View File

@@ -1005,15 +1005,19 @@
fill-opacity: 0.65;
}
#zen-boost-text-case-toggle {
#zen-boost-case[case-mode="none"] {
list-style-image: none;
}
#zen-boost-case[case-mode="capitalize"] {
list-style-image: url("text-title-case.svg");
}
#zen-boost-text-case-toggle[case-mode="uppercase"] {
#zen-boost-case[case-mode="uppercase"] {
list-style-image: url("text-uppercase.svg");
}
#zen-boost-text-case-toggle[case-mode="lowercase"] {
#zen-boost-case[case-mode="lowercase"] {
list-style-image: url("text-lowercase.svg");
}

View File

@@ -1,30 +1,35 @@
diff --git a/docshell/base/BrowsingContext.h b/docshell/base/BrowsingContext.h
index 4e20d7b602932621baf9082f6d28911701b7aa5b..8a59b81f0c82c94bac9d5c536a4b12f69f260dcf 100644
index 4e20d7b602932621baf9082f6d28911701b7aa5b..d6e141b235ce60be5db86bc40578d0741b79b014 100644
--- a/docshell/base/BrowsingContext.h
+++ b/docshell/base/BrowsingContext.h
@@ -265,6 +265,8 @@ struct EmbedderColorSchemes {
@@ -265,6 +265,9 @@ struct EmbedderColorSchemes {
FIELD(HistoryEntryCount, uint32_t) \
FIELD(HasRestoreData, bool) \
FIELD(SessionStoreEpoch, uint32_t) \
+ FIELD(ZenBoostsData, nscolor) \
+ FIELD(ZenBoostsComplementaryRotation, float) \
+ FIELD(IsZenBoostsInverted, bool) \
/* Whether we can execute scripts in this BrowsingContext. Has no effect \
* unless scripts are also allowed in the parent WindowContext. */ \
FIELD(AllowJavascript, bool) \
@@ -680,6 +682,8 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache {
@@ -680,6 +683,11 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache {
bool FullscreenAllowed() const;
+ auto ZenBoostsData() const { return GetZenBoostsData(); }
+ auto ZenBoostsComplementaryRotation() const {
+ return GetZenBoostsComplementaryRotation();
+ }
+ auto IsZenBoostsInverted() const { return GetIsZenBoostsInverted(); }
float FullZoom() const { return GetFullZoom(); }
float TextZoom() const { return GetTextZoom(); }
@@ -1284,6 +1288,8 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache {
@@ -1284,6 +1292,9 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache {
}
void DidSet(FieldIndex<IDX_SessionStoreEpoch>, uint32_t aOldValue);
+ void DidSet(FieldIndex<IDX_ZenBoostsData>, nscolor aOldValue);
+ void DidSet(FieldIndex<IDX_ZenBoostsComplementaryRotation>, float aOldValue);
+ void DidSet(FieldIndex<IDX_IsZenBoostsInverted>, bool aOldValue);
using CanSetResult = syncedcontext::CanSetResult;

View File

@@ -1,12 +1,13 @@
diff --git a/dom/chrome-webidl/BrowsingContext.webidl b/dom/chrome-webidl/BrowsingContext.webidl
index 43f28a6aad9d0dd3e3c908e8472244fc6aa32a74..60bfd83a9da2923ab3811f363320db474323e636 100644
index 43f28a6aad9d0dd3e3c908e8472244fc6aa32a74..a6d47522f89589c1e69a8e7570c17b7e05330bb6 100644
--- a/dom/chrome-webidl/BrowsingContext.webidl
+++ b/dom/chrome-webidl/BrowsingContext.webidl
@@ -179,6 +179,9 @@ interface BrowsingContext {
@@ -179,6 +179,10 @@ interface BrowsingContext {
[SetterThrows] attribute float textZoom;
+ [SetterThrows] attribute long zenBoostsData;
+ [SetterThrows] attribute float zenBoostsComplementaryRotation;
+ [SetterThrows] attribute boolean isZenBoostsInverted;
+
// Override the dots-per-CSS-pixel scaling factor in this BrowsingContext

View File

@@ -39,6 +39,7 @@ export class nsZenBoostEditor {
this.isMouseDown = false;
this.wasDragging = false;
this.dragTarget = "";
this.mouseDownPosition = { x: 0, y: 0 };
this.lastDotSetPos = { x: 0, y: 0 };
this.currentBoostData = null;
@@ -79,8 +80,11 @@ export class nsZenBoostEditor {
.addEventListener("input", this.onColorOptionChange.bind(this));
this.doc
.getElementById("zen-boost-text-case-toggle")
.getElementById("zen-boost-case")
.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", this.onZapButtonPressed.bind(this));
@@ -277,6 +281,11 @@ export class nsZenBoostEditor {
"Impact",
"Palatino Linotype",
"Tahoma",
"Helvetica",
"Garamond",
"Century Gothic",
"Arial Black",
"Papyrus",
];
return cFonts;
}
@@ -291,7 +300,7 @@ export class nsZenBoostEditor {
const fontButtonGroup = this.doc.getElementById("zen-boost-font-grid");
const fontList = this.doc.getElementById("zen-boost-font-select");
const buttonCount = 10;
const buttonCount = 15;
for (let i = 0; i < Math.min(commonFonts.length, buttonCount); i++) {
let font = fonts[i]; // Fallback
@@ -561,7 +570,9 @@ ${cssSelector} {
this.wasDragging = true;
event.preventDefault();
if (event.target.id != "zen-boost-magic-theme") {
if (this.dragTarget == "zen-boost-color-picker-dot-secondary") {
this.setSecondaryDotPos(event.clientX, event.clientY);
} else if (event.target.id != "zen-boost-magic-theme") {
this.setDotPos(event.clientX, event.clientY, false);
}
}
@@ -579,6 +590,7 @@ ${cssSelector} {
this.mouseDownPosition = { x: event.clientX, y: event.clientY };
this.isMouseDown = true;
this.dragTarget = event.target.id;
}
/**
@@ -600,18 +612,40 @@ ${cssSelector} {
* (none, lower, upper) and updating the UI accordingly.
*/
onBoostCasePressed() {
if (this.currentBoostData.textCaseOverride == "lowercase") {
this.currentBoostData.textCaseOverride = "uppercase";
} else if (this.currentBoostData.textCaseOverride == "uppercase") {
if (this.currentBoostData.textCaseOverride == "uppercase") {
this.currentBoostData.textCaseOverride = "lowercase";
} else if (this.currentBoostData.textCaseOverride == "lowercase") {
this.currentBoostData.textCaseOverride = "capitalize";
} else if (this.currentBoostData.textCaseOverride == "capitalize") {
this.currentBoostData.textCaseOverride = "none";
} else {
this.currentBoostData.textCaseOverride = "lowercase";
this.currentBoostData.textCaseOverride = "uppercase";
}
this.updateCaseButtonVisuals();
this.updateCurrentBoost();
}
/**
* Handles the size toggle button press, cycling through size override options
*/
onBoostSizePressed() {
if (this.currentBoostData.sizeOverride == 1) {
this.currentBoostData.sizeOverride = 1.1;
} else if (this.currentBoostData.sizeOverride == 1.1) {
this.currentBoostData.sizeOverride = 1.25;
} else if (this.currentBoostData.sizeOverride == 1.25) {
this.currentBoostData.sizeOverride = 1.5;
} else if (this.currentBoostData.sizeOverride == 1.5) {
this.currentBoostData.sizeOverride = 0.9;
} else if (this.currentBoostData.sizeOverride == 0.9) {
this.currentBoostData.sizeOverride = 1;
}
this.updateSizeButtonVisuals();
this.updateCurrentBoost();
}
/**
* Handles changes to color option sliders (contrast, brightness, saturation)
* and updates the current boost data accordingly.
@@ -649,6 +683,13 @@ ${cssSelector} {
this.setDotPos(null, null);
}
/**
* Resets the secondary color picker dot to the center position (default state).
*/
resetSecondaryDotPosition() {
this.setSecondaryDotPos(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.
@@ -665,7 +706,7 @@ ${cssSelector} {
this.currentBoostData.autoTheme = !this.currentBoostData.autoTheme;
this.updateButtonToggleVisuals();
this.updateCurrentBoost();
} else {
} else if (this.dragTarget != "zen-boost-color-picker-dot-secondary") {
this.setDotPos(event.clientX, event.clientY, !this.wasDragging);
}
this.wasDragging = false;
@@ -681,7 +722,10 @@ ${cssSelector} {
*/
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 dot = this.doc.querySelector("#zen-boost-color-picker-dot-primary");
const dotSec = this.doc.querySelector(
"#zen-boost-color-picker-dot-secondary"
);
const rect = gradient.getBoundingClientRect();
const padding = 50;
@@ -690,6 +734,9 @@ ${cssSelector} {
const centerY = rect.top + rect.height / 2;
const radius = (rect.width - padding) / 2;
let pixelXSec = pixelX;
let pixelYSec = pixelY;
if (!animate) {
let nDistance = Math.sqrt(
(pixelX - this.lastDotSetPos.x) ** 2 +
@@ -710,6 +757,8 @@ ${cssSelector} {
if (pixelX == null || pixelY == null) {
pixelX = centerX;
pixelY = centerY;
pixelXSec = centerX;
pixelYSec = centerY;
this.currentBoostData.dotAngleDeg = 0;
this.currentBoostData.dotDistance = 0;
@@ -719,8 +768,9 @@ ${cssSelector} {
);
distance = Math.min(distance, radius); // Clamp distance
const angle = Math.atan2(pixelY - centerY, pixelX - centerX);
// Primary dot
const angle = Math.atan2(pixelY - centerY, pixelX - centerX);
pixelX = centerX + Math.cos(angle) * distance;
pixelY = centerY + Math.sin(angle) * distance;
@@ -736,6 +786,15 @@ ${cssSelector} {
// Map to 0-1 range
this.currentBoostData.dotDistance = distance / radius;
// Secondary dot
const angleSec =
(angle +
(this.currentBoostData.secondaryDotAngleDegDelta * Math.PI) / 180) %
(Math.PI * 2);
pixelXSec = centerX + Math.cos(angleSec) * distance;
pixelYSec = centerY + Math.sin(angleSec) * distance;
// Enable color boosting again
if (!this.currentBoostData.enableColorBoost) {
this.onToggleDisable(false);
@@ -745,18 +804,26 @@ ${cssSelector} {
const relativeX = pixelX - rect.left;
const relativeY = pixelY - rect.top;
const relativeXSec = pixelXSec - rect.left;
const relativeYSec = pixelYSec - rect.top;
// Capture normalized position of dot for restoring it correctly later
this.currentBoostData.dotPos.x = relativeX / rect.width;
this.currentBoostData.dotPos.y = relativeY / rect.height;
this.currentBoostData.secondaryDotPos ||= {};
this.currentBoostData.secondaryDotPos.x = relativeXSec / rect.width;
this.currentBoostData.secondaryDotPos.y = relativeYSec / rect.height;
dot.setAttribute("animated", animate ? "true" : "false");
dot.style.left = `${relativeX}px`;
dot.style.top = `${relativeY}px`;
dotSec.setAttribute("animated", animate ? "true" : "false");
dotSec.style.left = `${relativeXSec}px`;
dotSec.style.top = `${relativeYSec}px`;
this.updateButtonToggleVisuals();
this.updateDot();
this.updateCircleRadius(animate);
this.updateCircleRadius();
this.updateCurrentBoost();
}
@@ -765,29 +832,209 @@ ${cssSelector} {
* based on the current boost data's angle and distance values.
*/
updateDot() {
const dot = this.doc.querySelector(".zen-boost-color-picker-dot");
const dot = this.doc.querySelector("#zen-boost-color-picker-dot-primary");
const dotSec = this.doc.querySelector(
"#zen-boost-color-picker-dot-secondary"
);
dot.style.setProperty(
"--zen-theme-picker-dot-color",
`hsl(${this.currentBoostData.dotAngleDeg}deg, ${this.currentBoostData.dotDistance * 100}%, 55%)`
);
dotSec.style.setProperty(
"--zen-theme-picker-dot-color",
`hsl(${this.currentBoostData.dotAngleDeg + this.currentBoostData.secondaryDotAngleDegDelta}deg, ${this.currentBoostData.dotDistance * 100}%, 20%)`
);
}
/**
* Sets the position of the secondary color picker dot on the gradient and updates
* the boost data with the corresponding angle values.
*
* @param {number|null} pixelX - The X coordinate in pixels.
* @param {number|null} pixelY - The Y coordinate in pixels.
*/
setSecondaryDotPos(pixelX, pixelY) {
const gradient = this.doc.querySelector(".zen-boost-color-picker-gradient");
const dotSec = this.doc.querySelector(
"#zen-boost-color-picker-dot-secondary"
);
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;
let angle = null;
if (!pixelX || !pixelY) {
pixelX = centerX;
pixelY = centerY;
angle = 32; // Default angle
} else {
angle = Math.atan2(pixelY - centerY, pixelX - centerX);
pixelX =
centerX + Math.cos(angle) * this.currentBoostData.dotDistance * radius;
pixelY =
centerY + Math.sin(angle) * this.currentBoostData.dotDistance * radius;
}
// Rad to degree
this.currentBoostData.secondaryDotAngleDegDelta =
((angle * 180) / Math.PI + 100 - this.currentBoostData.dotAngleDeg) % 360;
if (this.currentBoostData.secondaryDotAngleDegDelta < 0) {
this.currentBoostData.secondaryDotAngleDegDelta += 360;
}
const relativeX = pixelX - rect.left;
const relativeY = pixelY - rect.top;
// Capture normalized position of dot for restoring it correctly later
this.currentBoostData.secondaryDotPos.x = relativeX / rect.width;
this.currentBoostData.secondaryDotPos.y = relativeY / rect.height;
dotSec.setAttribute("animated", "false");
dotSec.style.left = `${relativeX}px`;
dotSec.style.top = `${relativeY}px`;
this.updateButtonToggleVisuals();
this.updateDot();
this.updateCircleRadius();
this.updateCurrentBoost();
}
/**
* Updates the radius of the circle based on the dot's position.
*
* @param {boolean} animate - Whether to animate the radius change (default: true).
*/
updateCircleRadius(animate = true) {
updateCircleRadius() {
const gradient = this.doc.querySelector(".zen-boost-color-picker-gradient");
const rect = gradient.getBoundingClientRect();
const padding = 50;
const radius = (rect.width - padding) / 2;
const cx = rect.width / 2;
const cy = rect.height / 2;
// Updating the circle size to match the distance of the point
const circle = this.doc.querySelector(".zen-boost-color-picker-circle");
circle.setAttribute("animated", animate ? "true" : "false");
circle.setAttribute("animated", "false");
circle.style.width = `${this.currentBoostData.dotDistance * radius * 2}px`;
circle.style.height = `${this.currentBoostData.dotDistance * radius * 2}px`;
const dotColor = `hsl(${this.currentBoostData.dotAngleDeg}deg, ${this.currentBoostData.dotDistance * 100}%, 55%)`;
const dotColorSec = `hsl(${this.currentBoostData.dotAngleDeg + this.currentBoostData.secondaryDotAngleDegDelta}deg, ${this.currentBoostData.dotDistance * 100}%, 20%)`;
this.updateArcFill(cx, cy, radius, dotColor, dotColorSec);
}
/**
* Updates the filled gradient arc between both color dots
*
* @param {number} cx - Half width of the gradient area
* @param {number} cy - Half height of the gradient area
* @param {number} radius - The target radius of the circle
* @param {string} color1 - Primary css color
* @param {string} color2 - Secondary css color
*/
updateArcFill(cx, cy, radius, color1, color2) {
const svg = this.doc.querySelector(".zen-boost-color-picker-arc-svg");
// Create SVG if it doesn't exist
if (!svg) {
this.initArcSVG();
this.updateArcFill(cx, cy, radius, color1, color2);
return;
}
const angle1 = this.currentBoostData.dotAngleDeg;
const angle2 =
this.currentBoostData.dotAngleDeg +
this.currentBoostData.secondaryDotAngleDegDelta;
const dist = this.currentBoostData.dotDistance;
const r = dist * radius;
const thickness = 2;
const toXY = (deg, ra) => {
const rad = ((deg - 90) * Math.PI) / 180;
return [cx + ra * Math.cos(rad), cy + ra * Math.sin(rad)];
};
const [x1, y1] = toXY(angle1, r);
const [x2, y2] = toXY(angle2, r);
// Gradient endpoints for matched dot positions
const grad = svg.querySelector("#arc-gradient");
grad.querySelector("#ag-stop1").setAttribute("stop-color", color1);
grad.querySelector("#ag-stop2").setAttribute("stop-color", color2);
grad.setAttribute("x1", x1);
grad.setAttribute("y1", y1);
grad.setAttribute("x2", x2);
grad.setAttribute("y2", y2);
// Ring sector path
const outerR = r + thickness / 2;
const innerR = Math.max(r - thickness / 2, 1);
const delta = (angle2 - angle1 + 360) % 360;
const large = delta > 180 ? 1 : 0;
const [ox1, oy1] = toXY(angle1, outerR);
const [ox2, oy2] = toXY(angle2, outerR);
const [ix2, iy2] = toXY(angle2, innerR);
const [ix1, iy1] = toXY(angle1, innerR);
const d = `M ${ox1} ${oy1} A ${outerR} ${outerR} 0 ${large} 1 ${ox2} ${oy2} L ${ix2} ${iy2} A ${innerR} ${innerR} 0 ${large} 0 ${ix1} ${iy1} Z`;
svg.querySelector(".arc-fill").setAttribute("d", d);
}
/**
* Initializes the filled gradient arc between both color picker dots in form of a svg
*/
initArcSVG() {
const NS = "http://www.w3.org/2000/svg";
const container = this.doc.querySelector(
".zen-boost-color-picker-gradient"
);
if (!container.clientWidth || !container.clientHeight) {
return;
}
const w = container.clientWidth;
const h = container.clientHeight;
const svg = this.doc.createElementNS(NS, "svg");
svg.classList.add("zen-boost-color-picker-arc-svg");
svg.setAttribute("width", w);
svg.setAttribute("height", h);
svg.setAttribute("viewBox", `0 0 ${w} ${h}`);
svg.style.cssText =
"position:absolute; top:0; left:0; pointer-events:none; z-index:3;";
const defs = this.doc.createElementNS(NS, "defs");
const grad = this.doc.createElementNS(NS, "linearGradient");
grad.setAttribute("id", "arc-gradient");
grad.setAttribute("gradientUnits", "userSpaceOnUse");
const stop1 = this.doc.createElementNS(NS, "stop");
stop1.setAttribute("id", "ag-stop1");
stop1.setAttribute("offset", "0%");
const stop2 = this.doc.createElementNS(NS, "stop");
stop2.setAttribute("id", "ag-stop2");
stop2.setAttribute("offset", "100%");
grad.appendChild(stop1);
grad.appendChild(stop2);
defs.appendChild(grad);
svg.appendChild(defs);
// Arc fill path
const arcFill = this.doc.createElementNS(NS, "path");
arcFill.classList.add("arc-fill");
arcFill.setAttribute("fill", "url(#arc-gradient)");
arcFill.setAttribute("opacity", "0.65");
svg.appendChild(arcFill);
container.style.position = "relative";
container.appendChild(svg);
}
/**
@@ -829,8 +1076,72 @@ ${cssSelector} {
* text case override value (none, upper, or lower).
*/
updateCaseButtonVisuals() {
const sizeValue = this.doc.getElementById("zen-boost-text-case-toggle");
sizeValue.setAttribute("case-mode", this.currentBoostData.textCaseOverride);
const caseButton = this.doc.getElementById("zen-boost-case");
const caseText = this.doc.getElementById("zen-boost-case-text");
caseButton.setAttribute(
"case-mode",
this.currentBoostData.textCaseOverride
);
switch (this.currentBoostData.textCaseOverride) {
case "uppercase":
caseButton.setAttribute("mode", "orange");
caseText.style.display = "none";
break;
case "lowercase":
caseButton.setAttribute("mode", "orange-red");
caseText.style.display = "none";
break;
case "capitalize":
caseButton.setAttribute("mode", "red");
caseText.style.display = "none";
break;
default:
caseButton.setAttribute("mode", "none");
caseText.style.display = "initial";
break;
}
}
/**
* Updates the visual state of the text case toggle button based on the current
* text case override value (none, upper, or lower).
*/
updateSizeButtonVisuals() {
const sizeButton = this.doc.getElementById("zen-boost-size");
const sizeText = this.doc.getElementById("zen-boost-size-text");
const sizeValue = this.doc.getElementById("zen-boost-size-value");
switch (this.currentBoostData.sizeOverride) {
case 1:
sizeButton.setAttribute("mode", "none");
sizeText.style.display = "initial";
sizeValue.style.display = "none";
break;
case 1.1:
sizeButton.setAttribute("mode", "orange");
sizeText.style.display = "none";
sizeValue.style.display = "initial";
break;
case 1.25:
sizeButton.setAttribute("mode", "orange-red");
sizeText.style.display = "none";
sizeValue.style.display = "initial";
break;
case 1.5:
sizeButton.setAttribute("mode", "red");
sizeText.style.display = "none";
sizeValue.style.display = "initial";
break;
case 0.9:
sizeButton.setAttribute("mode", "blue");
sizeText.style.display = "none";
sizeValue.style.display = "initial";
break;
}
sizeValue.setHTML(
`${Math.round(this.currentBoostData.sizeOverride * 100)}%`
);
}
/**
@@ -1199,7 +1510,7 @@ ${cssSelector} {
Math.round(rect.top + Math.random() * rect.height),
true
);
this.currentBoostData.secondaryDotAngleDegDelta = Math.random() * 360;
this.currentBoostData.changeWasMade = true;
this.updateCurrentBoost();
@@ -1260,7 +1571,22 @@ ${cssSelector} {
updateAllVisuals() {
this.doc.getElementById("zen-boost-name-text").textContent =
this.currentBoostData.boostName;
const dot = this.doc.querySelector(".zen-boost-color-picker-dot");
const dot = this.doc.querySelector("#zen-boost-color-picker-dot-primary");
const dotSec = this.doc.querySelector(
"#zen-boost-color-picker-dot-secondary"
);
if (!this.currentBoostData.sizeOverride) {
this.currentBoostData.sizeOverride = 1;
}
if (
!this.currentBoostData.secondaryDotPos ||
!this.currentBoostData.secondaryDotPos.x ||
!this.currentBoostData.secondaryDotPos.y
) {
this.resetSecondaryDotPosition();
}
if (
this.currentBoostData.dotPos.x == null ||
@@ -1290,16 +1616,22 @@ ${cssSelector} {
// Convert normalized position to relative position
const xPos = this.currentBoostData.dotPos.x * rect.width;
const yPos = this.currentBoostData.dotPos.y * rect.height;
const xPosSec = this.currentBoostData.secondaryDotPos.x * rect.width;
const yPosSec = this.currentBoostData.secondaryDotPos.y * rect.height;
dot.setAttribute("animated", "true");
dot.style.left = `${xPos}px`;
dot.style.top = `${yPos}px`;
dotSec.setAttribute("animated", "true");
dotSec.style.left = `${xPosSec}px`;
dotSec.style.top = `${yPosSec}px`;
}
this.editorWindow._editor.setText(this.currentBoostData.customCSS || "");
this.updateFontButtonVisuals();
this.updateCaseButtonVisuals();
this.updateSizeButtonVisuals();
this.updateColorControlSliderVisuals();
this.updateButtonToggleVisuals();
this.updateDot();

View File

@@ -118,6 +118,9 @@ class nsZenBoostsManager {
dotPos: { x: null, y: null },
dotDistance: 0,
secondaryDotAngleDegDelta: 32,
secondaryDotPos: { x: null, y: null },
brightness: 0.5,
saturation: 0.5,
contrast: 0.75,
@@ -131,6 +134,7 @@ class nsZenBoostsManager {
autoTheme: false,
textCaseOverride: "none",
sizeOverride: 1,
zapSelectors: [],
customCSS: "",

View File

@@ -96,6 +96,25 @@ export class ZenBoostsChild extends JSWindowActorChild {
return ((contrast << 24) | (b << 16) | (g << 8) | r) >>> 0;
}
/**
* Builds the packed primary accent NSColor for a boost, with the boost
* contrast stored in the alpha byte. The complementary accent is not a
* separate color: the backend derives it by rotating this accent's hue by
* the boost's `secondaryDotAngleDegDelta`, which is sent separately.
*
* @param {number} hueDeg - Primary hue in degrees.
* @param {number} sat - Saturation in [0, 1].
* @param {number} light - Lightness in [0, 1].
* @param {object} boostData - The current boost data.
* @returns {number} The packed primary NSColor.
*/
#buildBoostColor(hueDeg, sat, light, boostData) {
return this.#rgbToNSColor(
this.#hslToRgb(hueDeg / 360, sat, light),
(1 - boostData.contrast) * 255
);
}
/**
* From ZenGradientGenerator.mjs
* Converts an HSL color value to RGB. Conversion formula
@@ -345,6 +364,7 @@ export class ZenBoostsChild extends JSWindowActorChild {
browsingContext.isZenBoostsInverted = boostData.smartInvert;
if (boostData.enableColorBoost) {
let primaryColor;
if (boostData.autoTheme) {
// Workspace color is converted to the HSL color space
let primaryGradientColor = boost.workspaceGradient[0]?.c ?? [
@@ -362,40 +382,34 @@ export class ZenBoostsChild extends JSWindowActorChild {
// Workspace color is converted back to rgb
// using the same modifiers as the color above
primaryGradientColor = this.#hslToRgb(
primaryGradientColor[0] / 360,
primaryColor = this.#buildBoostColor(
primaryGradientColor[0],
primaryGradientColor[1] * (1 - boostData.saturation),
0.1 + primaryGradientColor[2] * 0.9 * boostData.brightness
0.1 + primaryGradientColor[2] * 0.9 * boostData.brightness,
boostData
);
const rgbColor = primaryGradientColor;
const nsColor = this.#rgbToNSColor(
rgbColor,
(1 - boostData.contrast) * 255
);
browsingContext.zenBoostsData = nsColor;
} else {
let colorWheelColor = this.#hslToRgb(
boostData.dotAngleDeg / 360,
primaryColor = this.#buildBoostColor(
boostData.dotAngleDeg,
/* already is [0, 1] */
boostData.dotDistance * (1 - boostData.saturation),
/* lightness range from [0.1, 0.9] */
0.1 + boostData.dotDistance * 0.8 * boostData.brightness
0.1 + boostData.dotDistance * 0.8 * boostData.brightness,
boostData
);
const rgbColor = colorWheelColor;
const nsColor = this.#rgbToNSColor(
rgbColor,
(1 - boostData.contrast) * 255
);
browsingContext.zenBoostsData = nsColor;
}
browsingContext.zenBoostsData = primaryColor;
// The complementary accent is derived in the backend by rotating the
// primary accent's hue by this delta (in degrees).
browsingContext.zenBoostsComplementaryRotation =
boostData.secondaryDotAngleDegDelta ?? 0;
return;
}
} else {
browsingContext.isZenBoostsInverted = false;
}
browsingContext.zenBoostsData = 0;
browsingContext.zenBoostsComplementaryRotation = 0;
}
/**

View File

@@ -66,6 +66,24 @@ void BrowsingContext::DidSet(FieldIndex<IDX_ZenBoostsData>,
TRIGGER_PRES_CONTEXT_RESTYLE();
}
/**
* @brief Called when the ZenBoostsComplementaryRotation field is set on a
* browsing context. This is the hue rotation (in degrees) applied to the base
* accent to derive the complementary accent that light page colors are tinted
* toward. Triggers a restyle if it has changed.
* @param aOldValue The previous rotation value.
*/
void BrowsingContext::DidSet(FieldIndex<IDX_ZenBoostsComplementaryRotation>,
float aOldValue) {
MOZ_ASSERT(IsTop());
if (ZenBoostsComplementaryRotation() == aOldValue) {
return;
}
RefreshBoostCacheIfMatchesCurrent(this);
PresContextAffectingFieldChanged();
TRIGGER_PRES_CONTEXT_RESTYLE();
}
/**
* @brief Called when the IsZenBoostsInverted field is set on a browsing
* context. Triggers a restyle if the value has changed.

View File

@@ -47,6 +47,8 @@
namespace zen {
nsZenAccentOklab nsZenBoostsBackend::mCachedAccent{0};
nsZenAccentOklab nsZenBoostsBackend::mCachedComplementary{0};
float nsZenBoostsBackend::mCachedComplementaryRotationDeg = 0.0f;
namespace {
@@ -128,17 +130,48 @@ inline static auto zenPrecomputeAccent(nscolor aAccentColor) {
}
/**
* @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.
* @brief Derives the complementary accent from the base accent by rotating its
* hue in the Oklab a/b plane by the given angle. Lightness, contrast and the
* source nscolor are kept; only the hue changes. A zero rotation returns the
* base accent unchanged so the duotone collapses to a single-accent tint.
* @param aBase The precomputed base accent.
* @param aRotationDeg The hue rotation to apply, in degrees.
* @return The complementary accent.
*/
ZEN_HOT_FUNCTION
inline static nsZenAccentOklab zenRotateAccent(const nsZenAccentOklab& aBase,
float aRotationDeg) {
constexpr float kDegToRad = 3.14159265358979323846f / 180.0f;
const float angle = aRotationDeg * kDegToRad;
const float cosR = std::cos(angle);
const float sinR = std::sin(angle);
return nsZenAccentOklab{
.accentNS = aBase.accentNS,
.accL = aBase.accL,
.accA = aBase.accA * cosR - aBase.accB * sinR,
.accB = aBase.accA * sinR + aBase.accB * cosR,
.contrastFactor = aBase.contrastFactor,
};
}
/**
* @brief Applies a duotone color filter to transform an original color toward
* one of two accent colors. The original color's perceived lightness decides
* which accent it is tinted toward: dark colors are pulled to the base accent,
* light colors to the complementary accent, with a smooth crossfade between
* them. The contrast value (stored in the accent's alpha channel) controls both
* the overall tint strength and how hard that dark/light split is. The
* original color's perceived luminance is otherwise preserved.
* @param aOriginalColor The original color to filter.
* @param aAccentColor The accent color to filter toward (alpha channel contains
* contrast value).
* @param aAccent The base accent, tinted toward by dark colors (alpha channel
* contains the contrast value).
* @param aComplementary The complementary accent, tinted toward by light
* colors.
* @return The filtered color with transformations applied.
*/
[[nodiscard]] ZEN_HOT_FUNCTION static inline nscolor zenFilterColorChannel(
nscolor aOriginalColor, const nsZenAccentOklab& aAccent) {
nscolor aOriginalColor, const nsZenAccentOklab& aAccent,
const nsZenAccentOklab& aComplementary) {
const uint8_t oL = NS_GET_A(aOriginalColor);
const uint8_t contrast = NS_GET_CONTRAST(aAccent.accentNS);
if (oL == 0) {
@@ -168,23 +201,40 @@ inline static auto zenPrecomputeAccent(nscolor aAccentColor) {
const float origB =
0.0259040371f * l_ + 0.7827717662f * m_ - 0.8086757660f * s_;
// Blend chroma toward accent
const float bA = origA + (aAccent.accA - origA) * blendFactor;
const float bB = origB + (aAccent.accB - origB) * blendFactor;
// Duotone selection. origL is the original color's Oklab lightness (~0..1).
// A smoothstep around a fixed mid-lightness pivot crossfades from the base
// accent (dark colors, t=0) to the complementary accent (light colors, t=1).
// A stronger tint (higher blendFactor) narrows the crossfade band toward a
// hard two-tone split; a weaker one keeps it a gentle gradient.
constexpr float kPivot = 0.5f;
const float halfWidth = std::clamp(0.5f - blendFactor * 0.45f, 0.05f, 0.5f);
float t = std::clamp((origL - (kPivot - halfWidth)) / (2.0f * halfWidth),
0.0f, 1.0f);
t = t * t * (3.0f - 2.0f * t);
const float selA = aAccent.accA + (aComplementary.accA - aAccent.accA) * t;
const float selB = aAccent.accB + (aComplementary.accB - aAccent.accB) * t;
const float selL = aAccent.accL + (aComplementary.accL - aAccent.accL) * t;
const float selContrastFactor =
aAccent.contrastFactor +
(aComplementary.contrastFactor - aAccent.contrastFactor) * t;
// Blend chroma toward the selected accent
const float bA = origA + (selA - origA) * blendFactor;
const float bB = origB + (selB - origB) * blendFactor;
// Luminance: at low contrast stay near the original, the higher the contrast,
// the more we shift toward the accent luminance, but we never go fully to
// the accent luminance to preserve some of the original color's character.
const float lumDelta = aAccent.accL - origL;
const float fL =
origL + lumDelta * (blendFactor * aAccent.contrastFactor * 0.5f);
const float lumDelta = selL - origL;
const float fL = origL + lumDelta * (blendFactor * selContrastFactor * 0.5f);
// Rotate hue in the Oklab a/b plane. Direction follows the luminance shift:
// pushing darker rotates clockwise ("right"), pushing lighter rotates the
// other way. Magnitude scales with blend strength so subtle accents stay
// subtle.
const float rotAngle = (lumDelta > 0.0f ? -1.0f : 1.0f) * blendFactor *
aAccent.contrastFactor * 0.25f;
selContrastFactor * 0.25f;
const float cosR = std::cos(rotAngle);
const float sinR = std::sin(rotAngle);
const float fA = bA * cosR - bB * sinR;
@@ -267,7 +317,7 @@ inline static nscolor zenInvertColorChannel(nscolor aColor) {
*/
ZEN_HOT_FUNCTION
inline static void GetZenBoostsDataFromBrowsingContext(
ZenBoostData* aData, bool* aIsInverted,
ZenBoostData* aData, float* aComplementaryRotation, bool* aIsInverted,
nsPresContext* aPresContext = nullptr) {
auto zenBoosts = nsZenBoostsBackend::GetInstance();
if (!zenBoosts || (zenBoosts->mCurrentFrameIsAnonymousContent &&
@@ -276,6 +326,7 @@ inline static void GetZenBoostsDataFromBrowsingContext(
}
if (!aPresContext) {
*aData = zenBoosts->mCachedCurrentAccent;
*aComplementaryRotation = zenBoosts->mCachedCurrentComplementaryRotation;
*aIsInverted = zenBoosts->mCachedCurrentInverted;
return;
}
@@ -288,6 +339,7 @@ inline static void GetZenBoostsDataFromBrowsingContext(
}
browsingContext = browsingContext->Top();
*aData = browsingContext->ZenBoostsData();
*aComplementaryRotation = browsingContext->ZenBoostsComplementaryRotation();
*aIsInverted = browsingContext->IsZenBoostsInverted();
}
@@ -329,11 +381,13 @@ auto nsZenBoostsBackend::onPresShellEntered(mozilla::dom::Document* aDocument)
auto nsZenBoostsBackend::RefreshCachedBoostState() -> void {
if (!mCurrentBrowsingContext) {
mCachedCurrentAccent = 0;
mCachedCurrentComplementaryRotation = 0.0f;
mCachedCurrentInverted = false;
return;
}
auto top = mCurrentBrowsingContext->Top();
mCachedCurrentAccent = top->ZenBoostsData();
mCachedCurrentComplementaryRotation = top->ZenBoostsComplementaryRotation();
mCachedCurrentInverted = top->IsZenBoostsInverted();
}
@@ -342,19 +396,31 @@ nsZenBoostsBackend::FilterColorFromPresContext(nscolor aColor,
nsPresContext* aPresContext)
-> nscolor {
ZenBoostData accentNS = 0;
float complementaryRotation = 0.0f;
bool invertColors = false;
GetZenBoostsDataFromBrowsingContext(&accentNS, &invertColors, aPresContext);
GetZenBoostsDataFromBrowsingContext(&accentNS, &complementaryRotation,
&invertColors, aPresContext);
if (accentNS) {
if (mCachedAccent.accentNS != accentNS) {
mCachedAccent = zenPrecomputeAccent(accentNS);
// Trigger a recompute of the complementary accent since
/ it depends on the base accent.mCachedComplementary.accentNS = 0;
}
// Derive the complementary accent by rotating the base accent's hue by the
// boost's complementary rotation. Cached so the per-color hot path only
// recomputes it when the base accent or rotation changes.
if (mCachedComplementary.accentNS != accentNS ||
mCachedComplementaryRotationDeg != complementaryRotation) {
mCachedComplementary =
zenRotateAccent(mCachedAccent, complementaryRotation);
mCachedComplementaryRotationDeg = complementaryRotation;
}
// 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
// - Map hue/chroma toward the base or complementary accent depending on
// the original color's lightness
// - Keep the original alpha
// Convert both colors to nscolor to access channels
aColor = zenFilterColorChannel(aColor, mCachedAccent);
aColor = zenFilterColorChannel(aColor, mCachedAccent, mCachedComplementary);
}
if (invertColors) {
aColor = zenInvertColorChannel(aColor);

View File

@@ -83,6 +83,10 @@ class nsZenBoostsBackend final {
* resolve.
*/
ZenBoostData mCachedCurrentAccent = 0;
// Hue rotation in degrees applied to the base accent to derive the
// complementary accent. Zero means the complementary accent equals the base
// accent (the duotone collapses to a single-accent tint).
float mCachedCurrentComplementaryRotation = 0.0f;
bool mCachedCurrentInverted = false;
private:
@@ -92,6 +96,10 @@ class nsZenBoostsBackend final {
RefPtr<mozilla::dom::BrowsingContext> mCurrentBrowsingContext;
static nsZenAccentOklab mCachedAccent;
// Base accent with its Oklab hue rotated by mCachedComplementaryRotationDeg,
// recomputed only when the base accent or rotation changes.
static nsZenAccentOklab mCachedComplementary;
static float mCachedComplementaryRotationDeg;
public:
/**

View File

@@ -57,7 +57,8 @@
<vbox flex="1" id="zen-boost-filter-wrapper">
<hbox class="zen-boost-color-picker-gradient zen-boost-panel-disabled">
<button data-l10n-id="zen-boost-magic-theme" id="zen-boost-magic-theme" class="subviewbutton mod-button"></button>
<html:div class="zen-boost-color-picker-dot"></html:div>
<html:div id="zen-boost-color-picker-dot-primary" class="zen-boost-color-picker-dot"></html:div>
<html:div id="zen-boost-color-picker-dot-secondary" class="zen-boost-color-picker-dot"></html:div>
<html:div class="zen-boost-color-picker-circle"></html:div>
</hbox>
@@ -69,17 +70,26 @@
<html:div id="zen-boost-font-wrapper">
<vbox id="zen-boost-font-grid">
<!-- Font buttons will be injected here -->
# Font buttons will be injected here
</vbox>
<html:div class="visible-separator"></html:div>
<hbox flex="1" id="zen-boost-font-toolbar">
<html:select name="font" id="zen-boost-font-select" class="mod-button">
<!-- Additional font options will be injected here -->
# Additional font options will be injected here
</html:select>
<button data-l10n-id="zen-boost-text-case-toggle" id="zen-boost-text-case-toggle" class="subviewbutton mod-button"></button>
</hbox>
</html:div>
<hbox flex="1" id="zen-boost-toolbar-wrapper">
<button id="zen-boost-size" class="subviewbutton mod-button big-button med">
<html:p data-l10n-id="zen-boost-size" id="zen-boost-size-text"></html:p>
<html:p id="zen-boost-size-value"></html:p>
</button>
<button id="zen-boost-case" class="subviewbutton mod-button big-button toggleable-button med">
<html:p data-l10n-id="zen-boost-case" id="zen-boost-case-text"></html:p>
</button>
</hbox>
<button id="zen-boost-zap" class="subviewbutton mod-button big-button toggleable-button">
<html:p data-l10n-id="zen-boost-zap" id="zen-boost-zap-text"></html:p>
<html:p id="zen-boost-zap-value"></html:p>
@@ -89,11 +99,11 @@
<html:p data-l10n-id="zen-boost-code" id="zen-boost-code-text"></html:p>
</button>
<hbox flex="1" id="zen-boost-toolbar-wrapper">
</vbox>
<hbox flex="1" class="footer" id="zen-boost-toolbar-wrapper">
<button data-l10n-id="zen-boost-save" id="zen-boost-save" class="subviewbutton mod-button med"></button>
<button data-l10n-id="zen-boost-load" id="zen-boost-load" class="subviewbutton mod-button med"></button>
</hbox>
</vbox>
</hbox>
</vbox>
<vbox flex="1" id="zen-boost-code-editor-root">
<hbox id="zen-boost-code-top-bar">

View File

@@ -302,17 +302,6 @@ body {
}
}
#zen-boost-size {
list-style-type: none;
& #zen-boost-size-value {
text-align: right;
right: 8px;
margin: auto;
position: relative;
}
}
.zen-boost-panel-disabled {
filter: grayscale(1);
}
@@ -348,18 +337,19 @@ body {
}
}
#zen-boost-text-case-toggle[case-mode="none"] {
opacity: 0.5;
&:hover {
opacity: 0.6 !important;
}
}
#zen-boost-text-case-toggle:not([case-mode="none"]) {
#zen-boost-case:not([case-mode="none"]) {
background-color: #ebebed;
font-weight: 600 !important;
}
#zen-boost-case[case-mode="uppercase"] {
text-transform: uppercase;
}
#zen-boost-case[case-mode="lowercase"] {
text-transform: lowercase;
}
#zen-boost-code-top-bar .mod-button {
height: auto !important;
}
@@ -434,6 +424,11 @@ body {
}
}
.footer {
background-color: #F6F6F8;
padding: 20px;
}
#zen-boost-toolbar-wrapper,
#zen-boost-toolbar-wrapper-colors {
width: 100%;
@@ -450,11 +445,18 @@ body {
& .med {
margin: 0;
flex: 1 1 50%;
& p {
text-align: center;
width: 100%;
text-indent: initial;
margin: auto;
}
}
}
#zen-boost-font-select {
width: 95px;
width: 120px;
height: 20px !important;
transition: 0.2s opacity ease-in-out;
@@ -693,10 +695,15 @@ body {
border-radius: 100%;
}
& .zen-boost-color-picker-arc-svg {
opacity: 0;
transition: opacity 0.4s ease;
}
& .zen-boost-color-picker-dot {
box-shadow: 0 2px 4px #00000022;
position: absolute;
z-index: 4;
z-index: 5;
width: 24px;
height: 24px;
border-radius: 50%;
@@ -707,16 +714,15 @@ body {
cursor: pointer;
border: 3px solid #ffffff;
transform: translate(-50%, -50%);
pointer-events: none;
transform-origin: top left;
pointer-events: all;
&:first-of-type {
width: 32px;
height: 32px;
border-width: 3px;
pointer-events: all;
transition: transform 0.2s;
z-index: 999;
z-index: 4;
&:hover {
transform: scale(1.05) translate(-50%, -50%);
}
@@ -729,12 +735,11 @@ body {
}
#zen-boost-editor-root:hover {
& .zen-boost-color-picker-circle {
& .zen-boost-color-picker-circle, .zen-boost-color-picker-arc-svg {
opacity: 0.4;
}
}
.zen-boost-font-button-active {
background-color: #5454572f !important;
}

View File

@@ -36,8 +36,6 @@ skip-if = [
["browser_sandbox_test.js"]
skip-if = [
"os == 'linux' && os_version == '22.04' && arch == 'x86_64' && display == 'wayland' && artifact && debug", # bug 1945658
"os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && artifact && debug", # bug 1945658
"os == 'win' && os_version == '11.26200' && arch == 'x86' && debug", # bug 2028636
"os == 'win' && os_version == '11.26200' && arch == 'x86_64' && debug", # bug 2028636
]