diff --git a/src/browser/base/content/zen-panels/gradient-generator.inc b/src/browser/base/content/zen-panels/gradient-generator.inc index ab4893f4..a6e487f6 100644 --- a/src/browser/base/content/zen-panels/gradient-generator.inc +++ b/src/browser/base/content/zen-panels/gradient-generator.inc @@ -37,7 +37,7 @@ - + diff --git a/src/zen/common/styles/zen-popup.css b/src/zen/common/styles/zen-popup.css index 774f5e4c..82c1a421 100644 --- a/src/zen/common/styles/zen-popup.css +++ b/src/zen/common/styles/zen-popup.css @@ -394,7 +394,6 @@ menuseparator { } & button { - color-scheme: dark; width: min-content; padding: 0 10px !important; min-width: unset !important; diff --git a/src/zen/common/styles/zen-theme.css b/src/zen/common/styles/zen-theme.css index dd08af92..2c1c8ab8 100644 --- a/src/zen/common/styles/zen-theme.css +++ b/src/zen/common/styles/zen-theme.css @@ -92,7 +92,7 @@ --focus-outline-color: var(--button-bgcolor) !important; --toolbarbutton-icon-fill-attention: var(--zen-primary-color) !important; - --toolbarbutton-icon-fill: light-dark(rgba(57, 57, 58, 0.6), rgba(251, 251, 254, 0.6)) !important; + --toolbarbutton-icon-fill: currentColor !important; --button-primary-bgcolor: var(--in-content-primary-button-background) !important; --button-primary-hover-bgcolor: var(--in-content-primary-button-background-hover) !important; @@ -121,7 +121,10 @@ --zen-button-border-radius: 5px; --zen-button-padding: 0.6rem 1.2rem; - --zen-toolbar-element-bg: color-mix(in srgb, currentColor 10%, transparent 90%) !important; + --zen-toolbar-element-bg: light-dark( + color-mix(in srgb, currentColor 5%, transparent 5%), + color-mix(in srgb, currentColor 10%, transparent 90%) + ) !important; /* Toolbar */ --zen-toolbar-height: 38px; @@ -177,6 +180,8 @@ --zen-themed-toolbar-bg-transparent: transparent; } + --toolbar-field-color: var(--toolbox-textcolor) !important; + &[zen-private-window='true'] { --zen-main-browser-background: linear-gradient( 130deg, @@ -233,12 +238,20 @@ #main-window:not([chromehidden~='toolbar']) { min-height: 495px !important; - &[zen-should-be-dark-mode='true'] #browser { - color-scheme: dark !important; - } + &[zen-should-be-dark-mode] { + & body { + background: rgba(255, 255, 255, 0.3); + } - &[zen-should-be-dark-mode='false'] #browser { - color-scheme: light !important; + &[zen-should-be-dark-mode='true'] #browser { + color-scheme: dark; + --tab-selected-color-scheme: dark; + } + + &[zen-should-be-dark-mode='false'] #browser { + color-scheme: light; + --tab-selected-color-scheme: light; + } } } diff --git a/src/zen/workspaces/ZenGradientGenerator.mjs b/src/zen/workspaces/ZenGradientGenerator.mjs index 606ebd57..fe43c77a 100644 --- a/src/zen/workspaces/ZenGradientGenerator.mjs +++ b/src/zen/workspaces/ZenGradientGenerator.mjs @@ -315,6 +315,23 @@ return [round(r * 255), round(g * 255), round(b * 255)]; } + rgbToHsl(r, g, b) { + r /= 255; + g /= 255; + b /= 255; + let max = Math.max(r, g, b); + let min = Math.min(r, g, b); + let d = max - min; + let h; + if (d === 0) h = 0; + else if (max === r) h = ((g - b) / d) % 6; + else if (max === g) h = (b - r) / d + 2; + else if (max === b) h = (r - g) / d + 4; + let l = (min + max) / 2; + let s = d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1)); + return [h * 60, s, l]; + } + hueToRgb(p, q, t) { if (t < 0) t += 1; if (t > 1) t -= 1; @@ -324,27 +341,36 @@ return p; } - calculateInitialPosition(color) { - const [r, g, b] = color.c; - const gradient = this.panel.querySelector('.zen-theme-picker-gradient'); - const rect = gradient.getBoundingClientRect(); - const padding = 20; // each side + calculateInitialPosition([r, g, b]) { + // This function is called before the picker is even rendered, so we hard code the dimensions + // important: If any sort of sizing is changed, make sure changes are reflected here + const padding = 20; + const rect = { + width: 308, + height: 308, + }; const centerX = rect.width / 2; const centerY = rect.height / 2; const radius = (rect.width - padding) / 2; - const angle = (Math.atan2(g - centerY, r - centerX) * 180) / Math.PI; // Convert to degrees - const normalizedAngle = (angle + 360) % 360; // Normalize to [0, 360) - const normalizedDistance = Math.sqrt(((r - centerX) ** 2 + (g - centerY) ** 2) / radius ** 2); // Normalize distance to [0, 1] - const hue = (normalizedAngle / 360) * 360; // Normalize angle to [0, 360) - const saturation = normalizedDistance * 100; // Scale distance to [0, 100] - const lightness = this.#currentLightness; // Fixed lightness for simplicity - const rgbColor = this.hslToRgb(hue / 360, saturation / 100, lightness / 100); - const x = ((rgbColor[0] / 255) * radius + centerX - rect.left) / (rect.width - padding); - const y = ((rgbColor[1] / 255) * radius + centerY - rect.top) / (rect.height - padding); - return { - x: Math.min(Math.max(x, 0), 1), // Ensure x is between 0 and 1 - y: Math.min(Math.max(y, 0), 1), // Ensure y is between 0 and 1 - }; + + // Convert RGB to HSL + const [h, s, l] = this.rgbToHsl(r, g, b); + + // We assume lightness is fixed; if not, add a warning or threshold check + // const targetLightness = this.#currentLightness / 100; + // if (Math.abs(l - targetLightness) > 0.01) return null; + + // Convert hue (0-1) to angle in radians + const angleRad = (h * 360 * Math.PI) / 180; + + // Convert saturation to radial distance + const distance = (1 - s) * radius; + + // Convert polar to cartesian + const x = centerX + Math.cos(angleRad) * distance; + const y = centerY + Math.sin(angleRad) * distance; + + return { x, y }; } getColorFromPosition(x, y) { @@ -352,6 +378,8 @@ const gradient = this.panel.querySelector('.zen-theme-picker-gradient'); const rect = gradient.getBoundingClientRect(); const padding = 20; // each side + rect.width += padding * 2; + rect.height += padding * 2; const centerX = rect.width / 2; const centerY = rect.height / 2; const radius = (rect.width - padding) / 2; @@ -387,13 +415,13 @@ dot.style.opacity = 0; dot.style.setProperty('--zen-theme-picker-dot-color', color.c); } else { - const { x, y } = this.calculateInitialPosition(color); + const { x, y } = this.calculateInitialPosition(color.c); const dotPad = this.panel.querySelector('.zen-theme-picker-gradient'); dot.classList.add('zen-theme-picker-dot'); - dot.style.left = `${x * 100}%`; - dot.style.top = `${y * 100}%`; + dot.style.left = `${x}px`; + dot.style.top = `${y}px`; if (this.dots.length < 1) { dot.classList.add('primary'); @@ -930,7 +958,11 @@ } onOpacityChange(event) { - this.currentOpacity = event.target.value; + this.currentOpacity = parseFloat(event.target.value); + // If we reached a whole number (e.g., 0.1, 0.2, etc.), send a haptic feedback + if (Math.round(this.currentOpacity % 0.1) === 0) { + Services.zen.playHapticFeedback(); + } this.updateCurrentWorkspace(); } @@ -945,7 +977,7 @@ if (color.isCustom) { return color.c; } - const opacity = Math.min(0.9, Math.max(0.1, this.currentOpacity)); + const opacity = this.currentOpacity; if (forToolbar) { const toolbarBg = this.getToolbarModifiedBase(); return `color-mix(in srgb, rgb(${color.c[0]}, ${color.c[1]}, ${color.c[2]}) ${opacity * 100}%, ${toolbarBg} ${(1 - opacity) * 100}%)`; @@ -953,6 +985,52 @@ return `rgba(${color.c[0]}, ${color.c[1]}, ${color.c[2]}, ${opacity})`; } + luminance([r, g, b]) { + const a = [r, g, b].map((v) => { + v /= 255; + return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); + }); + return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722; + } + + contrastRatio(rgb1, rgb2) { + const lum1 = this.luminance(rgb1); + const lum2 = this.luminance(rgb2); + const brightest = Math.max(lum1, lum2); + const darkest = Math.min(lum1, lum2); + return (brightest + 0.05) / (darkest + 0.05); + } + + blendColors(rgb1, rgb2, percentage) { + const p = percentage / 100; + return [ + Math.round(rgb1[0] * p + rgb2[0] * (1 - p)), + Math.round(rgb1[1] * p + rgb2[1] * (1 - p)), + Math.round(rgb1[2] * p + rgb2[2] * (1 - p)), + ]; + } + + findOptimalBlend(dominantColor, blendTarget, minContrast = 4.5) { + let low = 0; + let high = 100; + let bestMatch = null; + + for (let i = 0; i < 10; i++) { + const mid = (low + high) / 2; + const blended = this.blendColors(dominantColor, blendTarget, mid); + const contrast = this.contrastRatio(blended, blendTarget); + + if (contrast >= minContrast) { + bestMatch = blended; + high = mid; + } else { + low = mid; + } + } + + return bestMatch || this.blendColors(dominantColor, blendTarget, 10); // fallback + } + getGradient(colors, forToolbar = false) { const themedColors = this.themedColors(colors); this.#useAlgo = themedColors[0]?.algorithm ?? ''; @@ -966,8 +1044,8 @@ return this.getSingleRGBColor(themedColors[0], forToolbar); } else if (themedColors.length === 2) { return [ - `linear-gradient(${this.currentRotation}deg, ${this.getSingleRGBColor(themedColors[0], forToolbar)} -20%, transparent 100%)`, - `linear-gradient(${this.currentRotation + 180}deg, ${this.getSingleRGBColor(themedColors[1], forToolbar)} -20%, transparent 100%)`, + `linear-gradient(${this.currentRotation}deg, ${this.getSingleRGBColor(themedColors[0], forToolbar)} -30%, transparent 100%)`, + `linear-gradient(${this.currentRotation + 180}deg, ${this.getSingleRGBColor(themedColors[1], forToolbar)} -30%, transparent 100%)`, ].join(', '); } else { let color1 = this.getSingleRGBColor(themedColors[2], forToolbar); @@ -982,23 +1060,7 @@ } shouldBeDarkMode(accentColor) { - const luminance = (r, g, b) => { - // Convert RGB from [0, 255] to [0, 1] - r /= 255; - g /= 255; - b /= 255; - // Apply sRGB companding - const toLinear = (c) => { - return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); - }; - r = toLinear(r); - g = toLinear(g); - b = toLinear(b); - // Calculate relative luminance - return 0.2126 * r + 0.7152 * g + 0.0722 * b; - }; - const [r, g, b] = accentColor; - const lum = luminance(r, g, b); + const lum = this.luminance(accentColor); // Return true if background is dark enough that white text is preferred return lum < 0.5; } @@ -1139,6 +1201,16 @@ return color; } + blendColors(rgb1, rgb2, percentage) { + const p = percentage / 100; + const blended = [ + Math.round(rgb1[0] * p + rgb2[0] * (1 - p)), + Math.round(rgb1[1] * p + rgb2[1] * (1 - p)), + Math.round(rgb1[2] * p + rgb2[2] * (1 - p)), + ]; + return `rgb(${blended[0]}, ${blended[1]}, ${blended[2]})`; + } + async onWorkspaceChange(workspace, skipUpdate = false, theme = null) { const uuid = workspace.uuid; // Use theme from workspace object or passed theme @@ -1170,7 +1242,6 @@ } } - const appBackground = browser.document.getElementById('zen-browser-background'); if (!skipUpdate) { browser.document.documentElement.style.setProperty( '--zen-main-browser-background-old', @@ -1275,7 +1346,7 @@ const rotationLine = browser.document.getElementById( 'PanelUI-zen-gradient-generator-rotation-line' ); - if (numberOfColors != 1 && numberOfColors != 3) { + if (numberOfColors > 1 && numberOfColors != 3) { rotationDot.style.opacity = 1; rotationLine.style.opacity = 1; rotationDot.style.removeProperty('pointer-events'); @@ -1329,14 +1400,25 @@ ? dominantColor : `rgb(${dominantColor[0]}, ${dominantColor[1]}, ${dominantColor[2]})` ); + let isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches; if (dominantColor !== this.getNativeAccentColor()) { - browser.document.documentElement.setAttribute( - 'zen-should-be-dark-mode', - browser.gZenThemePicker.shouldBeDarkMode(dominantColor) - ); + isDarkMode = browser.gZenThemePicker.shouldBeDarkMode(dominantColor); + browser.document.documentElement.setAttribute('zen-should-be-dark-mode', isDarkMode); } else { browser.document.documentElement.removeAttribute('zen-should-be-dark-mode'); } + // Set `--toolbox-textcolor` to have a contrast with the primary color + const blendTarget = isDarkMode ? [255, 255, 255] : [0, 0, 0]; + const blendedColor = this.blendColors(dominantColor, blendTarget, 15); // 15% dominantColor, 85% target + await gZenUIManager.motion.animate( + browser.document.documentElement, + { + '--toolbox-textcolor': blendedColor, + }, + { + duration: 0.1, + } + ); } if (!skipUpdate) { diff --git a/src/zen/workspaces/zen-gradient-generator.css b/src/zen/workspaces/zen-gradient-generator.css index cfdd4d22..07c2594c 100644 --- a/src/zen/workspaces/zen-gradient-generator.css +++ b/src/zen/workspaces/zen-gradient-generator.css @@ -5,7 +5,7 @@ */ #PanelUI-zen-gradient-generator { - --panel-width: 320px; + --panel-width: 330px; --panel-padding: 10px; min-width: var(--panel-width); } @@ -194,11 +194,10 @@ .zen-theme-picker-gradient { position: relative; - border-radius: calc(var(--zen-border-radius) - 2px); overflow: hidden; border-radius: var(--zen-border-radius); - min-height: calc(var(--panel-width) - var(--panel-padding) * 2); + min-height: calc(var(--panel-width) - var(--panel-padding) * 2 - 2px); margin-bottom: 20px; background: var(--zen-toolbar-element-bg);