Skip to content
Permalink
Browse files
Web Inspector: Color picker should allow picking a color from any pix…
…el on screen

https://bugs.webkit.org/show_bug.cgi?id=124357
rdar://15469621

Reviewed by Devin Rousso.

Add the long-missing "eyedropper" for picking a color from the screen to the Color Picker in Web Inspector. This
implementation supports both sRGB color space as well as Display P3 (the two colorspaces currently supported by the Web
Inspector frontend). The existing value format and gamut is preserved as best as possible while not clamping the color
(unless interacting with a color swatch/picker that does not allow changing the format). For example, on macOS the color
picker will return a Display-P3 color on supported displays, but the color itself may be representable in sRGB. If the
existing color you are overwriting is already in sRGB, that is preserved by converting the system's Display-P3 color
into sRGB. If the sampled color can not be represented in sRGB, we update the CSS value to support the new wider gamut
of the selected color in order to provide the best fidelity in color matching.

The picker is implemented in two places. The first is the Color Picker popovers used in Web Inspector. The second place
is as an Option-Click action for inline swatches to allow you to quickly begin color selection without opening the
picker itself.

* Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js:
* Source/WebInspectorUI/UserInterface/Images/Pipette.svg: Added.

* Source/WebInspectorUI/UserInterface/Models/Color.js:
(WI.Color.prototype.fromStringBestMatchingSuggestedFormatAndGamut):
- New convencience "constructor" for creating a color from a String while attempting (or forcing) preservation of an
existing format and gamut.

* Source/WebInspectorUI/UserInterface/Views/ColorPicker.css:
(.color-picker > .color-inputs-wrapper):
(.color-picker > .color-inputs-wrapper > .color-inputs):
(.color-picker > .color-inputs-wrapper > .color-inputs > div):
(.color-picker > .color-inputs-wrapper > .color-inputs > div + div):
(.color-picker > .color-inputs-wrapper > .color-inputs input):
(.color-picker > .color-inputs-wrapper > .pick-color-from-screen):
(.color-picker > .color-inputs-wrapper > .pick-color-from-screen.active):
(.color-picker > .color-inputs): Deleted.
(.color-picker > .color-inputs > div): Deleted.
(.color-picker > .color-inputs > div + div): Deleted.
(.color-picker > .color-inputs input): Deleted.
* Source/WebInspectorUI/UserInterface/Views/ColorPicker.js:
(WI.ColorPicker.async pickColorFromScreen):
- Add new static method to picking a color from the screen (so that InlineSwatch can use it), and add a new Pipette icon
to begin the modal color picking mode.

* Source/WebInspectorUI/UserInterface/Views/InlineSwatch.js:
(WI.InlineSwatch.prototype._updateSwatch):
- Add Option-Click for color swatches to immediately enter the modal color picking mode.

* Source/WebCore/inspector/InspectorFrontendClient.h:
* Source/WebCore/inspector/InspectorFrontendClientLocal.h:
* Source/WebCore/inspector/InspectorFrontendHost.cpp:
(WebCore::InspectorFrontendHost::canPickColorFromScreen):
(WebCore::InspectorFrontendHost::pickColorFromScreen):
* Source/WebCore/inspector/InspectorFrontendHost.h:
* Source/WebCore/inspector/InspectorFrontendHost.idl:
* Source/WebKit/UIProcess/Inspector/RemoteWebInspectorUIProxy.cpp:
(WebKit::RemoteWebInspectorUIProxy::pickColorFromScreen):
(WebKit::RemoteWebInspectorUIProxy::platformPickColorFromScreen):
* Source/WebKit/UIProcess/Inspector/RemoteWebInspectorUIProxy.h:
* Source/WebKit/UIProcess/Inspector/RemoteWebInspectorUIProxy.messages.in:
* Source/WebKit/UIProcess/Inspector/WebInspectorUIProxy.cpp:
(WebKit::WebInspectorUIProxy::pickColorFromScreen):
(WebKit::WebInspectorUIProxy::platformPickColorFromScreen):
* Source/WebKit/UIProcess/Inspector/WebInspectorUIProxy.h:
* Source/WebKit/UIProcess/Inspector/WebInspectorUIProxy.messages.in:
* Source/WebKit/WebProcess/Inspector/RemoteWebInspectorUI.cpp:
(WebKit::RemoteWebInspectorUI::pickColorFromScreen):
* Source/WebKit/WebProcess/Inspector/RemoteWebInspectorUI.h:
* Source/WebKit/WebProcess/Inspector/WebInspectorUI.cpp:
(WebKit::WebInspectorUI::pickColorFromScreen):
(WebKit::WebInspectorUI::canPickColorFromScreen):
* Source/WebKit/WebProcess/Inspector/WebInspectorUI.h:
- Add plubming for picking a color from the screen.

* Source/WebKit/UIProcess/Inspector/mac/RemoteWebInspectorUIProxyMac.mm:
(WebKit::RemoteWebInspectorUIProxy::platformPickColorFromScreen):
* Source/WebKit/UIProcess/Inspector/mac/WebInspectorUIProxyMac.mm:
(WebKit::WebInspectorUIProxy::platformPickColorFromScreen):
- Use NSColorSampler to get the system "pick a color from the screen" UI.

* Source/WebKit/UIProcess/Inspector/gtk/RemoteWebInspectorUIProxyGtk.cpp:
(WebKit::RemoteWebInspectorUIProxy::platformPickColorFromScreen):
* Source/WebKit/UIProcess/Inspector/gtk/WebInspectorUIProxyGtk.cpp:
(WebKit::WebInspectorUIProxy::platformPickColorFromScreen):
* Source/WebKit/UIProcess/Inspector/win/RemoteWebInspectorUIProxyWin.cpp:
(WebKit::RemoteWebInspectorUIProxy::platformPickColorFromScreen):
* Source/WebKit/UIProcess/Inspector/win/WebInspectorUIProxyWin.cpp:
(WebKit::WebInspectorUIProxy::platformPickColorFromScreen):
* Source/WebKit/WebProcess/Inspector/gtk/WebInspectorUIGtk.cpp:
(WebKit::WebInspectorUI::canPickColorFromScreen):
* Source/WebKit/WebProcess/Inspector/mac/WebInspectorUIMac.mm:
(WebKit::WebInspectorUI::canPickColorFromScreen):
* Source/WebKit/WebProcess/Inspector/win/WebInspectorUIWin.cpp:
(WebKit::WebInspectorUI::canPickColorFromScreen):
- Add stubs for Windows/GTK implementations.

Canonical link: https://commits.webkit.org/251236@main
git-svn-id: https://svn.webkit.org/repository/webkit/trunk@295147 268f45cc-cd09-0410-ab3c-d52691b4dbfc
  • Loading branch information
patrickangle committed Jun 2, 2022
1 parent 8fd6837 commit 279b6e75d10ebaa7c646aab86685736c8b93e744
Show file tree
Hide file tree
Showing 30 changed files with 352 additions and 19 deletions.
@@ -32,6 +32,7 @@
#pragma once

#include "CertificateInfo.h"
#include "Color.h"
#include "DiagnosticLoggingClient.h"
#include "FrameIdentifier.h"
#include "InspectorDebuggableType.h"
@@ -113,6 +114,9 @@ class InspectorFrontendClient : public CanMakeWeakPtr<InspectorFrontendClient> {
virtual bool canLoad() = 0;
virtual void load(const String& path, CompletionHandler<void(const String&)>&&) = 0;

virtual bool canPickColorFromScreen() = 0;
virtual void pickColorFromScreen(CompletionHandler<void(const std::optional<WebCore::Color>&)>&&) = 0;

virtual void inspectedURLChanged(const String&) = 0;
virtual void showCertificate(const CertificateInfo&) = 0;

@@ -39,6 +39,7 @@

namespace WebCore {

class Color;
class FloatRect;
class Frame;
class InspectorController;
@@ -87,6 +88,9 @@ class InspectorFrontendClientLocal : public InspectorFrontendClient {
bool canLoad() override { return false; }
void load(const String&, CompletionHandler<void(const String&)>&& completionHandler) override { completionHandler(nullString()); }

bool canPickColorFromScreen() override { return false; }
void pickColorFromScreen(CompletionHandler<void(const std::optional<WebCore::Color>&)>&& completionHandler) override { completionHandler({ }); }

virtual void attachWindow(DockSide) = 0;
virtual void detachWindow() = 0;

@@ -31,6 +31,9 @@
#include "InspectorFrontendHost.h"

#include "CertificateInfo.h"
#include "ColorConversion.h"
#include "ColorSerialization.h"
#include "ColorSpace.h"
#include "ContextMenu.h"
#include "ContextMenuController.h"
#include "ContextMenuItem.h"
@@ -494,6 +497,40 @@ void InspectorFrontendHost::load(const String& path, Ref<DeferredPromise>&& prom
});
}

bool InspectorFrontendHost::canPickColorFromScreen()
{
if (m_client)
return m_client->canPickColorFromScreen();
return false;
}

void InspectorFrontendHost::pickColorFromScreen(Ref<DeferredPromise>&& promise)
{
if (!m_client) {
promise->reject(InvalidStateError);
return;
}

m_client->pickColorFromScreen([promise = WTFMove(promise)](const std::optional<WebCore::Color>& color) {
if (!color) {
promise->resolve();
return;
}

String serializedColor;
// FIXME: <webkit.org/b/241198> Inspector frontend should support all color function gamuts.
if (color->colorSpace() != ColorSpace::SRGB || color->colorSpace() != ColorSpace::DisplayP3) {
// DisplayP3 is the least-lossy format the frontend currently supports. This conversion will only be lossy
// if the color space the system is providing colors in were to support a wider gamut than DisplayP3.
auto colorForFrontend = color->toColorTypeLossy<DisplayP3<float>>();
serializedColor = serializationForCSS(colorForFrontend);
} else
serializedColor = serializationForCSS(*color);

promise->resolve<IDLDOMString>(serializedColor);
});
}

void InspectorFrontendHost::close(const String&)
{
}
@@ -116,6 +116,9 @@ class InspectorFrontendHost : public RefCounted<InspectorFrontendHost> {
void load(const String& path, Ref<DeferredPromise>&&);
void close(const String& url);

bool canPickColorFromScreen();
void pickColorFromScreen(Ref<DeferredPromise>&&);

String getPath(const File&);

struct ContextMenuItem {
@@ -79,6 +79,9 @@
[NewObject] Promise<DOMString> load(DOMString path);
undefined close(DOMString url);

boolean canPickColorFromScreen();
[NewObject] Promise<DOMString> pickColorFromScreen();

DOMString getPath(File file);

readonly attribute DOMString port;
@@ -333,8 +333,7 @@ localizedStrings["Click to create a Local Override from this content"] = "Click
localizedStrings["Click to import a file and create a Local Override\nShift-click to create a Local Override from this content"] = "Click to import a file and create a Local Override\nShift-click to create a Local Override from this content";
/* Title of text button that resets the gesture controls in the image resource content view. */
localizedStrings["Click to reset @ Image Resource Content View Gesture Controls"] = "Click to reset";
localizedStrings["Click to select a color"] = "Click to select a color";
localizedStrings["Click to select a color\nShift-click to switch color formats"] = "Click to select a color\nShift-click to switch color formats";
localizedStrings["Click to select a color."] = "Click to select a color.";
localizedStrings["Click to show %d error in the Console"] = "Click to show %d error in the Console";
localizedStrings["Click to show %d errors in the Console"] = "Click to show %d errors in the Console";
localizedStrings["Click to show %d warning in the Console"] = "Click to show %d warning in the Console";
@@ -1071,6 +1070,7 @@ localizedStrings["Open"] = "Open";
localizedStrings["Open closed tabs\u2026"] = "Open closed tabs\u2026";
/* Context menu item for opening the target item in a new window. */
localizedStrings["Open in New Window @ Context Menu Item"] = "Open in New Window";
localizedStrings["Option-click to pick color from screen."] = "Option-click to pick color from screen.";
localizedStrings["Option-click to show source"] = "Option-click to show source";
/* Tooltip with instructions on how to show all hidden CSS variables */
localizedStrings["Option-click to show unused CSS variables from all rules @ Styles Sidebar Panel Tooltip"] = "Option-click to show unused CSS variables from all rules";
@@ -1131,6 +1131,8 @@ localizedStrings["Percentage (of audits)"] = "%s%%";
localizedStrings["Periods of high CPU utilization will rapidly drain battery. Strive to keep idle pages under %s average CPU utilization."] = "Periods of high CPU utilization will rapidly drain battery. Strive to keep idle pages under %s average CPU utilization.";
/* Property value for `font-variant-capitals: petite-caps`. */
localizedStrings["Petite Capitals @ Font Details Sidebar Property Value"] = "Petite Capitals";
/* Color picker view tooltip for picking a color from the screen. */
localizedStrings["Pick color from screen"] = "Pick color from screen";
localizedStrings["Ping"] = "Ping";
localizedStrings["Ping Frame"] = "Ping Frame";
localizedStrings["Pings"] = "Pings";
@@ -1374,6 +1376,7 @@ localizedStrings["Shader Programs"] = "Shader Programs";
localizedStrings["Shadow Content"] = "Shadow Content";
localizedStrings["Shadow Content (%s)"] = "Shadow Content (%s)";
localizedStrings["Shared Focus"] = "Shared Focus";
localizedStrings["Shift-click to switch color formats."] = "Shift-click to switch color formats.";
localizedStrings["Shortest property path to %s"] = "Shortest property path to %s";
localizedStrings["Show %d More"] = "Show %d More";
/* Text label for button to reveal one unused CSS variable */
@@ -1805,7 +1808,7 @@ localizedStrings["default prevented"] = "default prevented";
/* Shown in the 'Type' column of the Network Table for document resources. */
localizedStrings["document @ Network Tab Resource Type Column Value"] = "document";
localizedStrings["ensuring that common debugging functions are available on every page via the Console"] = "ensuring that common debugging functions are available on every page via the Console";
/* Shown in the 'Type' column of the Network Table for EventSource resources. */
/* Shown in the 'Type' column of the Network Table for resources loaded via the EventSource API. */
localizedStrings["eventsource @ Network Tab Resource Type Column Value"] = "eventsource";
/* Shown in the 'Type' column of the Network Table for resources loaded via the 'fetch' method. */
localizedStrings["fetch @ Network Tab Resource Type Column Value"] = "fetch";
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2009, 2013 Apple Inc. All rights reserved.
* Copyright (C) 2009-2022 Apple Inc. All rights reserved.
* Copyright (C) 2009 Joseph Pecoraro
*
* Redistribution and use in source and binary forms, with or without
@@ -209,6 +209,75 @@ WI.Color = class Color
return null;
}

static fromStringBestMatchingSuggestedFormatAndGamut(colorString, {suggestedFormat, suggestedGamut, forceSuggestedFormatAndGamut} = {})
{
let newColor = WI.Color.fromString(colorString);

if (forceSuggestedFormatAndGamut) {
newColor.format = suggestedFormat;
newColor.gamut = suggestedGamut;
return newColor;
}

// Match the suggested gamut if we can do so losslessly.
if (suggestedGamut === WI.Color.Gamut.DisplayP3 && newColor.gamut !== WI.Color.Gamut.DisplayP3)
newColor.gamut = WI.Color.Gamut.DisplayP3;
else if (suggestedGamut !== WI.Color.Gamut.DisplayP3 && newColor.gamut === WI.Color.Gamut.DisplayP3 && !newColor.isOutsideSRGB())
newColor.gamut = WI.Color.Gamut.SRGB;

// Non-sRGB gamuts can only be expressed in the Color Function format.
if (newColor.gamut !== WI.Color.Gamut.SRGB)
return newColor;

// Match as closely as possible the suggested format, and progressively adjust the format (e.g. ShortHEX -> HEX
// -> HEXAlpha) if an exact match would be lossy.
switch (suggestedFormat) {
case WI.Color.Format.Original:
console.assert(false, "No color should have a format of 'Original'.");
break;

case WI.Color.Format.Keyword:
// Use the format of the color string as-provided.
break;

case WI.Color.Format.HEX:
newColor.format = newColor.simple ? WI.Color.Format.HEX : WI.Color.Format.HEXAlpha;
break;

case WI.Color.Format.ShortHEX:
if (newColor.canBeSerializedAsShortHEX())
newColor.format = newColor.simple ? WI.Color.Format.ShortHEX : WI.Color.Format.ShortHEXAlpha;
else
newColor.format = newColor.simple ? WI.Color.Format.HEX : WI.Color.Format.HEXAlpha;
break;

case WI.Color.Format.ShortHEXAlpha:
newColor.format = newColor.canBeSerializedAsShortHEX() ? WI.Color.Format.ShortHEXAlpha : WI.Color.Format.HEXAlpha;
break;

case WI.Color.Format.RGB:
newColor.format = newColor.simple ? WI.Color.Format.RGB : WI.Color.Format.RGBA;
break;

case WI.Color.Format.HSL:
newColor.format = newColor.simple ? WI.Color.Format.HSL : WI.Color.Format.HSLA;
break;

case WI.Color.Format.HEXAlpha:
case WI.Color.Format.RGBA:
case WI.Color.Format.HSLA:
case WI.Color.Format.ColorFunction:
newColor.format = suggestedFormat;
break;

default:
console.assert(false, "Should not be reached.", suggestedFormat);
break;
}

return newColor;
}

static rgb2hsl(r, g, b)
{
r = WI.Color._eightBitChannel(r) / 255;
@@ -76,23 +76,39 @@
}
}

.color-picker > .color-inputs {
.color-picker > .color-inputs-wrapper {
display: flex;
align-items: center;
gap: var(--color-inputs-spacing);
margin-top: var(--color-inputs-spacing);

--color-inputs-spacing: 8px;
}

.color-picker > .color-inputs-wrapper > .color-inputs {
display: flex;
justify-content: space-between;
margin-top: 8px;
flex: 1;
}

.color-picker > .color-inputs > div {
.color-picker > .color-inputs-wrapper > .color-inputs > div {
display: flex;
align-items: center;
width: 100%;
}

.color-picker > .color-inputs > div + div {
margin-inline-start: 4px;
}

.color-picker > .color-inputs input {
.color-picker > .color-inputs-wrapper > .color-inputs input {
width: 100%;
margin: 0 0.25em;
}

.color-picker > .color-inputs-wrapper > .pick-color-from-screen {
width: 16px;
height: 16px;
color: var(--glyph-color);
opacity: var(--glyph-opacity);
}

.color-picker > .color-inputs-wrapper > .pick-color-from-screen.active {
color: var(--glyph-color-active);
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
* 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
@@ -25,10 +25,12 @@

WI.ColorPicker = class ColorPicker extends WI.Object
{
constructor()
constructor({preventChangingColorFormats} = {})
{
super();

this._preventChangingColorFormats = !!preventChangingColorFormats;

this._colorSquare = new WI.ColorSquare(this, 200);

this._hueSlider = new WI.Slider;
@@ -56,7 +58,31 @@ WI.ColorPicker = class ColorPicker extends WI.Object
wrapper.appendChild(this._hueSlider.element);
wrapper.appendChild(this._opacitySlider.element);

this._element.appendChild(this._colorInputsContainerElement);
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)";
@@ -68,6 +94,33 @@ WI.ColorPicker = class ColorPicker extends WI.Object
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; }

0 comments on commit 279b6e7

Please sign in to comment.