ImGui color space controls

Adds radio buttons for switching among legacy, sRGB and F16.
Also adds a list of primaries you can pick from, as well as
a gamut diagram showing the primaries. The primaries can be
dragged around to alter the working space.

BUG=skia:

Change-Id: Ibd8c67dfe085594c0d7462f0efe4d79d73999919
Reviewed-on: https://skia-review.googlesource.com/8311
Reviewed-by: Matt Sarett <msarett@google.com>
Commit-Queue: Brian Osman <brianosman@google.com>
diff --git a/tools/viewer/Viewer.cpp b/tools/viewer/Viewer.cpp
index c7517cc..535ece8 100644
--- a/tools/viewer/Viewer.cpp
+++ b/tools/viewer/Viewer.cpp
@@ -15,6 +15,7 @@
 
 #include "SkATrace.h"
 #include "SkCanvas.h"
+#include "SkColorSpace_Base.h"
 #include "SkCommandLineFlags.h"
 #include "SkCommonFlagsPathRenderer.h"
 #include "SkDashPathEffect.h"
@@ -161,6 +162,44 @@
     }
 }
 
+static SkColorSpacePrimaries gSrgbPrimaries = {
+    0.64f, 0.33f,
+    0.30f, 0.60f,
+    0.15f, 0.06f,
+    0.3127f, 0.3290f };
+
+static SkColorSpacePrimaries gAdobePrimaries = {
+    0.64f, 0.33f,
+    0.21f, 0.71f,
+    0.15f, 0.06f,
+    0.3127f, 0.3290f };
+
+static SkColorSpacePrimaries gP3Primaries = {
+    0.680f, 0.320f,
+    0.265f, 0.690f,
+    0.150f, 0.060f,
+    0.3127f, 0.3290f };
+
+static SkColorSpacePrimaries gRec2020Primaries = {
+    0.708f, 0.292f,
+    0.170f, 0.797f,
+    0.131f, 0.046f,
+    0.3127f, 0.3290f };
+
+struct NamedPrimaries {
+    const char* fName;
+    SkColorSpacePrimaries* fPrimaries;
+} gNamedPrimaries[] = {
+    { "sRGB", &gSrgbPrimaries },
+    { "AdobeRGB", &gAdobePrimaries },
+    { "P3", &gP3Primaries },
+    { "Rec. 2020", &gRec2020Primaries },
+};
+
+static bool primaries_equal(const SkColorSpacePrimaries& a, const SkColorSpacePrimaries& b) {
+    return memcmp(&a, &b, sizeof(SkColorSpacePrimaries)) == 0;
+}
+
 const char* kName = "name";
 const char* kValue = "value";
 const char* kOptions = "options";
@@ -184,7 +223,8 @@
     , fLastImage(nullptr)
     , fBackendType(sk_app::Window::kNativeGL_BackendType)
     , fColorType(kN32_SkColorType)
-    , fColorSpace(nullptr)
+    , fColorManaged(false)
+    , fColorSpacePrimaries(gSrgbPrimaries)
     , fZoomCenterX(0.0f)
     , fZoomCenterY(0.0f)
     , fZoomLevel(0.0f)
@@ -243,15 +283,15 @@
         fWindow->inval();
     });
     fCommands.addCommand('c', "Modes", "Cycle color mode", [this]() {
-        if (!fColorSpace) {
-            // Legacy -> sRGB
-            this->setColorMode(kN32_SkColorType, SkColorSpace::MakeSRGB());
+        if (!fColorManaged) {
+            // Legacy -> Color correct 8888
+            this->setColorMode(kN32_SkColorType, true);
         } else if (kN32_SkColorType == fColorType) {
-            // sRGB -> F16 sRGB
-            this->setColorMode(kRGBA_F16_SkColorType, SkColorSpace::MakeSRGBLinear());
+            // Color correct 8888 -> Color correct F16
+            this->setColorMode(kRGBA_F16_SkColorType, true);
         } else {
-            // F16 sRGB -> Legacy
-            this->setColorMode(kN32_SkColorType, nullptr);
+            // Color correct F16 -> Legacy
+            this->setColorMode(kN32_SkColorType, false);
         }
     });
     fCommands.addCommand(Window::Key::kRight, "Right", "Navigation", "Next slide", [this]() {
@@ -373,6 +413,15 @@
     fImGuiFontPaint.setFilterQuality(kLow_SkFilterQuality);
     io.Fonts->TexID = &fImGuiFontPaint;
 
+    auto gamutImage = GetResourceAsImage("gamut.png");
+    if (gamutImage) {
+        auto gamutShader = gamutImage->makeShader(SkShader::kClamp_TileMode,
+                                                  SkShader::kClamp_TileMode);
+        fImGuiGamutPaint.setShader(gamutShader);
+    }
+    fImGuiGamutPaint.setColor(SK_ColorWHITE);
+    fImGuiGamutPaint.setFilterQuality(kLow_SkFilterQuality);
+
     fWindow->show();
 }
 
@@ -467,9 +516,17 @@
 
     title.appendf(" %s", sk_tool_utils::colortype_name(fColorType));
 
-    // TODO: Find a short string to describe the gamut of the color space?
-    if (fColorSpace) {
+    if (fColorManaged) {
         title.append(" ColorManaged");
+
+        int curPrimaries = -1;
+        for (size_t i = 0; i < SK_ARRAY_COUNT(gNamedPrimaries); ++i) {
+            if (primaries_equal(*gNamedPrimaries[i].fPrimaries, fColorSpacePrimaries)) {
+                curPrimaries = i;
+                break;
+            }
+        }
+        title.appendf(" %s", curPrimaries >= 0 ? gNamedPrimaries[curPrimaries].fName : "Custom");
     }
 
     title.append(kBackendTypeStrings[fBackendType]);
@@ -579,15 +636,15 @@
     return m;
 }
 
-void Viewer::setColorMode(SkColorType colorType, sk_sp<SkColorSpace> colorSpace) {
+void Viewer::setColorMode(SkColorType colorType, bool colorManaged) {
     fColorType = colorType;
-    fColorSpace = std::move(colorSpace);
+    fColorManaged = colorManaged;
 
     // When we're in color managed mode, we tag our window surface as sRGB. If we've switched into
     // or out of legacy mode, we need to update our window configuration.
     DisplayParams params = fWindow->getDisplayParams();
-    if (SkToBool(fColorSpace) != SkToBool(params.fColorSpace)) {
-        params.fColorSpace = fColorSpace ? SkColorSpace::MakeSRGB() : nullptr;
+    if (fColorManaged != SkToBool(params.fColorSpace)) {
+        params.fColorSpace = fColorManaged ? SkColorSpace::MakeSRGB() : nullptr;
         fWindow->setDisplayParams(params);
     }
 
@@ -611,9 +668,17 @@
     // If we're in F16, or the gamut isn't sRGB, or we're zooming, we need to render offscreen
     sk_sp<SkSurface> offscreenSurface = nullptr;
     if (kRGBA_F16_SkColorType == fColorType || fShowZoomWindow ||
-        (fColorSpace && fColorSpace != SkColorSpace::MakeSRGB())) {
+        (fColorManaged && !primaries_equal(fColorSpacePrimaries, gSrgbPrimaries))) {
+        sk_sp<SkColorSpace> cs = nullptr;
+        if (fColorManaged) {
+            SkColorSpace::RenderTargetGamma transferFn = (kRGBA_F16_SkColorType == fColorType)
+                ? SkColorSpace::kLinear_RenderTargetGamma : SkColorSpace::kSRGB_RenderTargetGamma;
+            SkMatrix44 toXYZ;
+            SkAssertResult(fColorSpacePrimaries.toXYZD50(&toXYZ));
+            cs = SkColorSpace::MakeRGB(transferFn, toXYZ);
+        }
         SkImageInfo info = SkImageInfo::Make(fWindow->width(), fWindow->height(), fColorType,
-                                             kPremul_SkAlphaType, fColorSpace);
+                                             kPremul_SkAlphaType, std::move(cs));
         offscreenSurface = canvas->makeSurface(info);
         slideCanvas = offscreenSurface->getCanvas();
     }
@@ -769,14 +834,79 @@
     canvas->restore();
 }
 
+static ImVec2 ImGui_DragPrimary(const char* label, float* x, float* y,
+                                const ImVec2& pos, const ImVec2& size) {
+    // Transform primaries ([0, 0] - [0.8, 0.9]) to screen coords (including Y-flip)
+    ImVec2 center(pos.x + (*x / 0.8f) * size.x, pos.y + (1.0f - (*y / 0.9f)) * size.y);
+
+    // Invisible 10x10 button
+    ImGui::SetCursorScreenPos(ImVec2(center.x - 5, center.y - 5));
+    ImGui::InvisibleButton(label, ImVec2(10, 10));
+
+    if (ImGui::IsItemActive() && ImGui::IsMouseDragging()) {
+        ImGuiIO& io = ImGui::GetIO();
+        // Normalized mouse position, relative to our gamut box
+        ImVec2 mousePosXY((io.MousePos.x - pos.x) / size.x, (io.MousePos.y - pos.y) / size.y);
+        // Clamp to edge of box, convert back to primary scale
+        *x = SkTPin(mousePosXY.x, 0.0f, 1.0f) * 0.8f;
+        *y = SkTPin(1 - mousePosXY.y, 0.0f, 1.0f) * 0.9f;
+    }
+
+    if (ImGui::IsItemHovered()) {
+        ImGui::SetTooltip("x: %.3f\ny: %.3f", *x, *y);
+    }
+
+    // Return screen coordinates for the caller. We could just return center here, but we'd have
+    // one frame of lag during drag.
+    return ImVec2(pos.x + (*x / 0.8f) * size.x, pos.y + (1.0f - (*y / 0.9f)) * size.y);
+}
+
+static void ImGui_Primaries(SkColorSpacePrimaries* primaries, SkPaint* gamutPaint) {
+    ImDrawList* drawList = ImGui::GetWindowDrawList();
+
+    // The gamut image covers a (0.8 x 0.9) shaped region, so fit our image/canvas to the available
+    // width, and scale the height to maintain aspect ratio.
+    float canvasWidth = SkTMax(ImGui::GetContentRegionAvailWidth(), 50.0f);
+    ImVec2 size = ImVec2(canvasWidth, canvasWidth * (0.9f / 0.8f));
+    ImVec2 pos = ImGui::GetCursorScreenPos();
+
+    // Background image. Only draw a subset of the image, to avoid the regions less than zero.
+    // Simplifes re-mapping math, clipping behavior, and increases resolution in the useful area.
+    // Magic numbers are pixel locations of the origin and upper-right corner.
+    drawList->AddImage(gamutPaint, pos, ImVec2(pos.x + size.x, pos.y + size.y),
+                       ImVec2(242, 61), ImVec2(1897, 1922));
+    ImVec2 endPos = ImGui::GetCursorPos();
+
+    // Primary markers
+    ImVec2 r = ImGui_DragPrimary("R", &primaries->fRX, &primaries->fRY, pos, size);
+    ImVec2 g = ImGui_DragPrimary("G", &primaries->fGX, &primaries->fGY, pos, size);
+    ImVec2 b = ImGui_DragPrimary("B", &primaries->fBX, &primaries->fBY, pos, size);
+    ImVec2 w = ImGui_DragPrimary("W", &primaries->fWX, &primaries->fWY, pos, size);
+
+    // Gamut triangle
+    drawList->AddCircle(r, 5.0f, 0xFF000040);
+    drawList->AddCircle(g, 5.0f, 0xFF004000);
+    drawList->AddCircle(b, 5.0f, 0xFF400000);
+    drawList->AddCircle(w, 5.0f, 0xFFFFFFFF);
+    drawList->AddTriangle(r, g, b, 0xFFFFFFFF);
+
+    // Re-position cursor immediate after the diagram for subsequent controls
+    ImGui::SetCursorPos(endPos);
+}
+
 void Viewer::drawImGui(SkCanvas* canvas) {
     // Support drawing the ImGui demo window. Superfluous, but gives a good idea of what's possible
     if (fShowImGuiTestWindow) {
         ImGui::ShowTestWindow(&fShowImGuiTestWindow);
     }
 
+    SkPaint gamutImagePaint;
     if (fShowImGuiDebugWindow) {
-        if (ImGui::Begin("Debug", &fShowImGuiDebugWindow)) {
+        // We have some dynamic content that sizes to fill available size. If the scroll bar isn't
+        // always visible, we can end up in a layout feedback loop.
+        ImGui::SetNextWindowSize(ImVec2(400, 400), ImGuiSetCond_FirstUseEver);
+        if (ImGui::Begin("Tools", &fShowImGuiDebugWindow,
+                         ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
             if (ImGui::CollapsingHeader("Slide")) {
                 static ImGuiTextFilter filter;
                 filter.Draw();
@@ -796,6 +926,37 @@
                     fCurrentSlide = previousSlide;
                 }
             }
+
+            if (ImGui::CollapsingHeader("Color Mode")) {
+                int oldMode = fColorManaged ? (kRGBA_F16_SkColorType == fColorType) ? 2 : 1 : 0;
+                int newMode = oldMode;
+                ImGui::RadioButton("Legacy", &newMode, 0);
+                ImGui::RadioButton("Color Managed 8888", &newMode, 1);
+                ImGui::RadioButton("Color Managed F16", &newMode, 2);
+                if (newMode != oldMode) {
+                    this->setColorMode(2 == newMode ? kRGBA_F16_SkColorType : kN32_SkColorType,
+                                       0 != newMode);
+                }
+
+                // Pick from common gamuts:
+                int primariesIdx = 4; // Default: Custom
+                for (size_t i = 0; i < SK_ARRAY_COUNT(gNamedPrimaries); ++i) {
+                    if (primaries_equal(*gNamedPrimaries[i].fPrimaries, fColorSpacePrimaries)) {
+                        primariesIdx = i;
+                        break;
+                    }
+                }
+
+                if (ImGui::Combo("Primaries", &primariesIdx,
+                                 "sRGB\0AdobeRGB\0P3\0Rec. 2020\0Custom\0\0")) {
+                    if (primariesIdx >= 0 && primariesIdx <= 3) {
+                        fColorSpacePrimaries = *gNamedPrimaries[primariesIdx].fPrimaries;
+                    }
+                }
+
+                // Allow direct editing of gamut
+                ImGui_Primaries(&fColorSpacePrimaries, &fImGuiGamutPaint);
+            }
         }
 
         ImGui::End();
diff --git a/tools/viewer/Viewer.h b/tools/viewer/Viewer.h
index f4847e5..b17bd25 100644
--- a/tools/viewer/Viewer.h
+++ b/tools/viewer/Viewer.h
@@ -33,7 +33,7 @@
 private:
     void initSlides();
     void updateTitle();
-    void setColorMode(SkColorType, sk_sp<SkColorSpace>);
+    void setColorMode(SkColorType, bool colorManaged);
     void setStartupSlide();
     void setupCurrentSlide(int previousSlide);
     void listNames();
@@ -64,6 +64,7 @@
     bool                   fRefresh; // whether to continuously refresh for measuring render time
 
     SkPaint                fImGuiFontPaint;
+    SkPaint                fImGuiGamutPaint;
     bool                   fShowImGuiDebugWindow;
     bool                   fShowImGuiTestWindow;
 
@@ -74,7 +75,8 @@
 
     // Color properties for slide rendering
     SkColorType            fColorType;
-    sk_sp<SkColorSpace>    fColorSpace;
+    bool                   fColorManaged;
+    SkColorSpacePrimaries  fColorSpacePrimaries;
 
     // transform data
     SkScalar               fZoomCenterX;