blob: 7f87db86854847f9a6ef0d0fde446c879155185e [file] [log] [blame]
/*
* Copyright (C) 2013-2022 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
WI.ColorPicker = class ColorPicker extends WI.Object
{
constructor({preventChangingColorFormats} = {})
{
super();
this._preventChangingColorFormats = !!preventChangingColorFormats;
this._colorSquare = new WI.ColorSquare(this, 200);
this._hueSlider = new WI.Slider;
this._hueSlider.delegate = this;
this._hueSlider.element.classList.add("hue");
this._opacitySlider = new WI.Slider;
this._opacitySlider.delegate = this;
this._opacitySlider.element.classList.add("opacity");
this._colorInputs = [];
this._colorInputsFormat = null;
this._colorInputsHasAlpha = null;
this._colorInputsContainerElement = document.createElement("div");
this._colorInputsContainerElement.classList.add("color-inputs");
this._colorInputsContainerElement.addEventListener("input", this._handleColorInputsContainerInput.bind(this));
this._element = document.createElement("div");
this._element.classList.add("color-picker");
let wrapper = this._element.appendChild(document.createElement("div"));
wrapper.className = "wrapper";
wrapper.appendChild(this._colorSquare.element);
wrapper.appendChild(this._hueSlider.element);
wrapper.appendChild(this._opacitySlider.element);
let colorInputsWrapperElement = this._element.appendChild(document.createElement("div"));
colorInputsWrapperElement.classList.add("color-inputs-wrapper");
colorInputsWrapperElement.appendChild(this._colorInputsContainerElement);
if (InspectorFrontendHost.canPickColorFromScreen()) {
let pickColorElement = WI.ImageUtilities.useSVGSymbol("Images/Pipette.svg", "pick-color-from-screen", WI.UIString("Pick color from screen", "Color picker view tooltip for picking a color from the screen."));
pickColorElement.role = "button";
pickColorElement.addEventListener("click", async (event) => {
pickColorElement.classList.add("active");
let pickedColor = await WI.ColorPicker.pickColorFromScreen({
suggestedFormat: this.color.format,
suggestedGamut: this.color.gamut,
forceSuggestedFormatAndGamut: this._preventChangingColorFormats,
});
pickColorElement.classList.remove("active");
if (!pickedColor)
return;
this.color = pickedColor;
this.dispatchEventToListeners(WI.ColorPicker.Event.ColorChanged, {color: this._color});
});
colorInputsWrapperElement.appendChild(pickColorElement);
}
this._opacity = 0;
this._opacityPattern = "url(Images/Checkers.svg)";
this._color = WI.Color.fromString("white");
this._dontUpdateColor = false;
this._enableColorComponentInputs = true;
}
// Static
static async pickColorFromScreen({suggestedFormat, suggestedGamut, forceSuggestedFormatAndGamut} = {})
{
console.assert(InspectorFrontendHost.canPickColorFromScreen());
// There is a brief moment where the frontend page remains interactable before the backend actually begins the
// modal color picking mode. In order to avoid accidentally hovering an element and showing its highlight on the
// page and not being able to hide the highlight while selecting a color, make the document inert so that even
// immediate mouse movement doesn't accidentaly cause any highlighting to occur.
document.body.inert = true;
let pickedColorCSSString = null;
try {
pickedColorCSSString = await InspectorFrontendHost.pickColorFromScreen();
} catch (e) {
WI.reportInternalError(error);
}
document.body.inert = false;
if (!pickedColorCSSString)
return null;
return WI.Color.fromStringBestMatchingSuggestedFormatAndGamut(pickedColorCSSString, {suggestedFormat, suggestedGamut, forceSuggestedFormatAndGamut});
}
// Public
get element() { return this._element; }
get colorSquare() { return this._colorSquare; }
set opacity(opacity)
{
if (opacity === this._opacity)
return;
this._opacity = opacity;
this._updateColor();
}
get color()
{
return this._color;
}
set color(color)
{
console.assert(color instanceof WI.Color);
this._dontUpdateColor = true;
this._color = color;
this._colorSquare.tintedColor = this._color;
this._hueSlider.value = this._color.hsl[0] / 360;
this._opacitySlider.value = this._color.alpha;
this._updateOpacitySlider();
this._showColorComponentInputs();
this._updateColorGamut();
this._dontUpdateColor = false;
}
set enableColorComponentInputs(value)
{
this._enableColorComponentInputs = value;
this._element.classList.toggle("hide-inputs", !this._enableColorComponentInputs);
}
focus()
{
this._colorSquare.element.focus();
}
colorSquareColorDidChange(colorSquare)
{
this._updateColor();
this._updateOpacitySlider();
}
sliderValueDidChange(slider, value)
{
if (slider === this._opacitySlider)
this.opacity = value;
else if (slider === this._hueSlider) {
this._colorSquare.hue = value * 360;
this._updateColor();
this._updateOpacitySlider();
}
}
// Private
_updateColor()
{
if (this._dontUpdateColor)
return;
let opacity = Math.round(this._opacity * 100) / 100;
let format = this._color.format;
let gamut = this._color.gamut;
let components = null;
if (format === WI.Color.Format.HSL || format === WI.Color.Format.HSLA) {
components = this._colorSquare.tintedColor.hsl.concat(opacity);
if (opacity !== 1)
format = WI.Color.Format.HSLA;
} else if (format === WI.Color.Format.ColorFunction)
components = this._colorSquare.tintedColor.normalizedRGB.concat(opacity);
else {
components = this._colorSquare.tintedColor.rgb.concat(opacity);
if (opacity !== 1 && format === WI.Color.Format.RGB)
format = WI.Color.Format.RGBA;
}
this._color = new WI.Color(format, components, gamut);
this._showColorComponentInputs();
this.dispatchEventToListeners(WI.ColorPicker.Event.ColorChanged, {color: this._color});
this._updateColorGamut();
}
_updateOpacitySlider()
{
let color = this._colorSquare.tintedColor;
let rgb = color.format === WI.Color.Format.ColorFunction ? color.normalizedRGB : color.rgb;
let gamut = color.gamut;
let format = gamut === WI.Color.Gamut.DisplayP3 ? WI.Color.Format.ColorFunction : WI.Color.Format.RGBA;
let opaque = new WI.Color(format, rgb.concat(1), gamut).toString();
let transparent = new WI.Color(format, rgb.concat(0), gamut).toString();
this._opacitySlider.element.style.setProperty("background-image", "linear-gradient(0deg, " + transparent + ", " + opaque + "), " + this._opacityPattern);
}
_updateColorGamut()
{
this._element.classList.toggle("gamut-p3", this._color.gamut === WI.Color.Gamut.DisplayP3);
}
_createColorInputsIfNeeded()
{
let hasAlpha = this._color.alpha !== 1;
if ((this._color.format === this._colorInputsFormat) && (hasAlpha === this._colorInputsHasAlpha))
return;
this._colorInputsFormat = this._color.format;
this._colorInputsHasAlpha = hasAlpha;
this._colorInputs = [];
this._colorInputsContainerElement.removeChildren();
let createColorInput = (label, {max, step, units} = {}) => {
let containerElement = this._colorInputsContainerElement.createChild("div");
containerElement.append(label);
let numberInputElement = containerElement.createChild("input");
numberInputElement.type = "number";
numberInputElement.min = 0;
numberInputElement.max = max;
numberInputElement.step = step || 1;
this._colorInputs.push(numberInputElement);
if (units && units.length)
containerElement.append(units);
};
switch (this._color.format) {
case WI.Color.Format.RGB:
case WI.Color.Format.RGBA:
case WI.Color.Format.HEX:
case WI.Color.Format.ShortHEX:
case WI.Color.Format.HEXAlpha:
case WI.Color.Format.ShortHEXAlpha:
case WI.Color.Format.Keyword:
createColorInput("R", {max: 255});
createColorInput("G", {max: 255});
createColorInput("B", {max: 255});
break;
case WI.Color.Format.HSL:
case WI.Color.Format.HSLA:
createColorInput("H", {max: 360});
createColorInput("S", {max: 100, units: "%"});
createColorInput("L", {max: 100, units: "%"});
break;
case WI.Color.Format.ColorFunction:
createColorInput("R", {max: 1, step: 0.01});
createColorInput("G", {max: 1, step: 0.01});
createColorInput("B", {max: 1, step: 0.01});
break;
default:
console.error(`Unknown color format: ${this._color.format}`);
return;
}
if (this._color.alpha !== 1
|| this._color.format === WI.Color.Format.RGBA
|| this._color.format === WI.Color.Format.HSLA
|| this._color.format === WI.Color.Format.HEXAlpha
|| this._color.format === WI.Color.Format.ShortHEXAlpha) {
createColorInput("A", {max: 1, step: 0.01});
}
}
_showColorComponentInputs()
{
if (!this._enableColorComponentInputs)
return;
this._createColorInputsIfNeeded();
let components = [];
switch (this._color.format) {
case WI.Color.Format.RGB:
case WI.Color.Format.RGBA:
case WI.Color.Format.HEX:
case WI.Color.Format.ShortHEX:
case WI.Color.Format.HEXAlpha:
case WI.Color.Format.ShortHEXAlpha:
case WI.Color.Format.Keyword:
components = this._color.rgb.map((value) => Math.round(value));
break;
case WI.Color.Format.HSL:
case WI.Color.Format.HSLA:
components = this._color.hsl.map((value) => value.maxDecimals(2));
break;
case WI.Color.Format.ColorFunction:
components = this._color.normalizedRGB.map((value) => value.maxDecimals(4));
break;
default:
console.error(`Unknown color format: ${this._color.format}`);
}
if (this._color.alpha !== 1)
components.push(this._color.alpha);
console.assert(this._colorInputs.length === components.length);
for (let i = 0; i < components.length; ++i)
this._colorInputs[i].value = components[i];
}
_handleColorInputsContainerInput(event)
{
let components = this._colorInputs.map((input) => {
return isNaN(input.valueAsNumber) ? 0 : input.valueAsNumber;
});
this.color = new WI.Color(this._color.format, components, this._color.gamut);
this.dispatchEventToListeners(WI.ColorPicker.Event.ColorChanged, {color: this._color});
}
};
WI.ColorPicker.Event = {
ColorChanged: "css-color-picker-color-changed",
};