From f5e7fa24d6b8a87b6d4da89e2c4c9589cf361cd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=BCller?= Date: Sat, 11 Nov 2023 18:10:52 +0100 Subject: [PATCH] Add color palette and pipette to editor Add color palette with up to 8 colors to editor toolbar. The palette colors work like regular color picker buttons, so they open a color picker popup on click and the value can be copied and pasted with Ctrl+Right click and Ctrl+Left click respectively. Less palette colors are shown when not enough space is available (with 5:4 resolutions). Add color pipette which allows selecting any color displayed on the screen. Selecting a color with the pipette adds the new color to the palette and shifts the other colors to the right. The selected color is also copied to the clipboard immediately. The hotkey Ctrl+Shift+C is added to toggle the color pipette, which allows using the color pipette in popups and dialogs. The implement this, the function `IGraphics::ReadPixel` and the command `SCommand_TrySwapAndReadPixel` are added to read a specified pixel's color from the backbuffer. Like the screenshot command, this command also requires a swap operation to be performed before the correct pixel color can be read with the Vulkan backend. The `ReadPixel` function therefore accepts a pointer to a `ColorRGBA` that will be filled after the next swap operation. Closes #7430. --- .../client/backend/opengl/backend_opengl.cpp | 29 +++- .../client/backend/opengl/backend_opengl.h | 1 + .../client/backend/vulkan/backend_vulkan.cpp | 82 ++++++++--- src/engine/client/graphics_threaded.cpp | 57 +++++--- src/engine/client/graphics_threaded.h | 20 ++- src/engine/graphics.h | 9 ++ src/engine/textrender.h | 1 + src/game/editor/editor.cpp | 134 +++++++++++++++++- src/game/editor/editor.h | 14 +- 9 files changed, 294 insertions(+), 53 deletions(-) diff --git a/src/engine/client/backend/opengl/backend_opengl.cpp b/src/engine/client/backend/opengl/backend_opengl.cpp index a599a9903..d2aab9bef 100644 --- a/src/engine/client/backend/opengl/backend_opengl.cpp +++ b/src/engine/client/backend/opengl/backend_opengl.cpp @@ -313,8 +313,9 @@ bool CCommandProcessorFragment_OpenGL::InitOpenGL(const SCommand_Init *pCommand) { m_IsOpenGLES = pCommand->m_RequestedBackend == BACKEND_TYPE_OPENGL_ES; - TGLBackendReadPresentedImageData &ReadPresentedImgDataFunc = *pCommand->m_pReadPresentedImageDataFunc; - ReadPresentedImgDataFunc = [this](uint32_t &Width, uint32_t &Height, CImageInfo::EImageFormat &Format, std::vector &vDstData) { return GetPresentedImageData(Width, Height, Format, vDstData); }; + *pCommand->m_pReadPresentedImageDataFunc = [this](uint32_t &Width, uint32_t &Height, CImageInfo::EImageFormat &Format, std::vector &vDstData) { + return GetPresentedImageData(Width, Height, Format, vDstData); + }; const char *pVendorString = (const char *)glGetString(GL_VENDOR); dbg_msg("opengl", "Vendor string: %s", pVendorString); @@ -983,10 +984,27 @@ void CCommandProcessorFragment_OpenGL::Cmd_Render(const CCommandBuffer::SCommand #endif } +void CCommandProcessorFragment_OpenGL::Cmd_ReadPixel(const CCommandBuffer::SCommand_TrySwapAndReadPixel *pCommand) +{ + // get size of viewport + GLint aViewport[4] = {0, 0, 0, 0}; + glGetIntegerv(GL_VIEWPORT, aViewport); + const int h = aViewport[3]; + + // fetch the pixel + uint8_t aPixelData[3]; + GLint Alignment; + glGetIntegerv(GL_PACK_ALIGNMENT, &Alignment); + glPixelStorei(GL_PACK_ALIGNMENT, 1); + glReadPixels(pCommand->m_Position.x, h - 1 - pCommand->m_Position.y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, aPixelData); + glPixelStorei(GL_PACK_ALIGNMENT, Alignment); + + // fill in the information + *pCommand->m_pColor = ColorRGBA(aPixelData[0] / 255.0f, aPixelData[1] / 255.0f, aPixelData[2] / 255.0f, 1.0f); +} + void CCommandProcessorFragment_OpenGL::Cmd_Screenshot(const CCommandBuffer::SCommand_TrySwapAndScreenshot *pCommand) { - *pCommand->m_pSwapped = false; - // fetch image data GLint aViewport[4] = {0, 0, 0, 0}; glGetIntegerv(GL_VIEWPORT, aViewport); @@ -1068,6 +1086,9 @@ ERunCommandReturnTypes CCommandProcessorFragment_OpenGL::RunCommand(const CComma case CCommandBuffer::CMD_RENDER_TEX3D: Cmd_RenderTex3D(static_cast(pBaseCommand)); break; + case CCommandBuffer::CMD_TRY_SWAP_AND_READ_PIXEL: + Cmd_ReadPixel(static_cast(pBaseCommand)); + break; case CCommandBuffer::CMD_TRY_SWAP_AND_SCREENSHOT: Cmd_Screenshot(static_cast(pBaseCommand)); break; diff --git a/src/engine/client/backend/opengl/backend_opengl.h b/src/engine/client/backend/opengl/backend_opengl.h index 2d339c0fd..d7bba4235 100644 --- a/src/engine/client/backend/opengl/backend_opengl.h +++ b/src/engine/client/backend/opengl/backend_opengl.h @@ -99,6 +99,7 @@ protected: virtual void Cmd_Clear(const CCommandBuffer::SCommand_Clear *pCommand); virtual void Cmd_Render(const CCommandBuffer::SCommand_Render *pCommand); virtual void Cmd_RenderTex3D(const CCommandBuffer::SCommand_RenderTex3D *pCommand) { dbg_assert(false, "Call of unsupported Cmd_RenderTex3D"); } + virtual void Cmd_ReadPixel(const CCommandBuffer::SCommand_TrySwapAndReadPixel *pCommand); virtual void Cmd_Screenshot(const CCommandBuffer::SCommand_TrySwapAndScreenshot *pCommand); virtual void Cmd_Update_Viewport(const CCommandBuffer::SCommand_Update_Viewport *pCommand); diff --git a/src/engine/client/backend/vulkan/backend_vulkan.cpp b/src/engine/client/backend/vulkan/backend_vulkan.cpp index a7ebc6537..c895739b0 100644 --- a/src/engine/client/backend/vulkan/backend_vulkan.cpp +++ b/src/engine/client/backend/vulkan/backend_vulkan.cpp @@ -15,26 +15,22 @@ #include #include -#include -#include -#include -#include - #include - +#include +#include #include +#include #include #include +#include #include -#include - -#include #include +#include +#include +#include #include - -#include - #include +#include #include #include @@ -904,6 +900,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase uint32_t m_MinUniformAlign; + std::vector m_vReadPixelHelper; std::vector m_vScreenshotHelper; SDeviceMemoryBlock m_GetPresentedImgDataHelperMem; @@ -1278,6 +1275,7 @@ protected: m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_VSYNC)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { return Cmd_VSync(static_cast(pBaseCommand)); }}; m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_MULTISAMPLING)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { return Cmd_MultiSampling(static_cast(pBaseCommand)); }}; + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_TRY_SWAP_AND_READ_PIXEL)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { return Cmd_ReadPixel(static_cast(pBaseCommand)); }}; m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_TRY_SWAP_AND_SCREENSHOT)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { return Cmd_Screenshot(static_cast(pBaseCommand)); }}; m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_UPDATE_VIEWPORT)] = {false, [this](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) { Cmd_Update_Viewport_FillExecuteBuffer(ExecBuffer, static_cast(pBaseCommand)); }, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { return Cmd_Update_Viewport(static_cast(pBaseCommand)); }}; @@ -1378,15 +1376,29 @@ protected: } } - [[nodiscard]] bool GetPresentedImageDataImpl(uint32_t &Width, uint32_t &Height, CImageInfo::EImageFormat &Format, std::vector &vDstData, bool ResetAlpha) + [[nodiscard]] bool GetPresentedImageDataImpl(uint32_t &Width, uint32_t &Height, CImageInfo::EImageFormat &Format, std::vector &vDstData, bool ResetAlpha, std::optional PixelOffset) { bool IsB8G8R8A8 = m_VKSurfFormat.format == VK_FORMAT_B8G8R8A8_UNORM; bool UsesRGBALikeFormat = m_VKSurfFormat.format == VK_FORMAT_R8G8B8A8_UNORM || IsB8G8R8A8; if(UsesRGBALikeFormat && m_LastPresentedSwapChainImageIndex != std::numeric_limits::max()) { auto Viewport = m_VKSwapImgAndViewportExtent.GetPresentedImageViewport(); - Width = Viewport.width; - Height = Viewport.height; + VkOffset3D SrcOffset; + if(PixelOffset.has_value()) + { + SrcOffset.x = PixelOffset.value().x; + SrcOffset.y = PixelOffset.value().y; + Width = 1; + Height = 1; + } + else + { + SrcOffset.x = 0; + SrcOffset.y = 0; + Width = Viewport.width; + Height = Viewport.height; + } + SrcOffset.z = 0; Format = CImageInfo::FORMAT_RGBA; const size_t ImageTotalSize = (size_t)Width * Height * CImageInfo::PixelSize(Format); @@ -1414,9 +1426,11 @@ protected: BlitSize.x = Width; BlitSize.y = Height; BlitSize.z = 1; + VkImageBlit ImageBlitRegion{}; ImageBlitRegion.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; ImageBlitRegion.srcSubresource.layerCount = 1; + ImageBlitRegion.srcOffsets[0] = SrcOffset; ImageBlitRegion.srcOffsets[1] = BlitSize; ImageBlitRegion.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; ImageBlitRegion.dstSubresource.layerCount = 1; @@ -1436,6 +1450,7 @@ protected: VkImageCopy ImageCopyRegion{}; ImageCopyRegion.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; ImageCopyRegion.srcSubresource.layerCount = 1; + ImageCopyRegion.srcOffset = SrcOffset; ImageCopyRegion.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; ImageCopyRegion.dstSubresource.layerCount = 1; ImageCopyRegion.extent.width = Width; @@ -1458,7 +1473,6 @@ protected: VkSubmitInfo SubmitInfo{}; SubmitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - SubmitInfo.commandBufferCount = 1; SubmitInfo.pCommandBuffers = &CommandBuffer; @@ -1525,7 +1539,7 @@ protected: [[nodiscard]] bool GetPresentedImageData(uint32_t &Width, uint32_t &Height, CImageInfo::EImageFormat &Format, std::vector &vDstData) override { - return GetPresentedImageDataImpl(Width, Height, Format, vDstData, false); + return GetPresentedImageDataImpl(Width, Height, Format, vDstData, false, {}); } /************************ @@ -6523,8 +6537,9 @@ public: m_MultiSamplingCount = (g_Config.m_GfxFsaaSamples & 0xFFFFFFFE); // ignore the uneven bit, only even multi sampling works - TGLBackendReadPresentedImageData &ReadPresentedImgDataFunc = *pCommand->m_pReadPresentedImageDataFunc; - ReadPresentedImgDataFunc = [this](uint32_t &Width, uint32_t &Height, CImageInfo::EImageFormat &Format, std::vector &vDstData) { return GetPresentedImageData(Width, Height, Format, vDstData); }; + *pCommand->m_pReadPresentedImageDataFunc = [this](uint32_t &Width, uint32_t &Height, CImageInfo::EImageFormat &Format, std::vector &vDstData) { + return GetPresentedImageData(Width, Height, Format, vDstData); + }; m_pWindow = pCommand->m_pWindow; @@ -6748,18 +6763,39 @@ public: return RenderStandard(ExecBuffer, pCommand->m_State, pCommand->m_PrimType, pCommand->m_pVertices, pCommand->m_PrimCount); } - [[nodiscard]] bool Cmd_Screenshot(const CCommandBuffer::SCommand_TrySwapAndScreenshot *pCommand) + [[nodiscard]] bool Cmd_ReadPixel(const CCommandBuffer::SCommand_TrySwapAndReadPixel *pCommand) { - if(!NextFrame()) + if(!*pCommand->m_pSwapped && !NextFrame()) return false; *pCommand->m_pSwapped = true; uint32_t Width; uint32_t Height; CImageInfo::EImageFormat Format; - if(GetPresentedImageDataImpl(Width, Height, Format, m_vScreenshotHelper, true)) + if(GetPresentedImageDataImpl(Width, Height, Format, m_vReadPixelHelper, false, pCommand->m_Position)) { - size_t ImgSize = (size_t)Width * (size_t)Height * (size_t)4; + *pCommand->m_pColor = ColorRGBA(m_vReadPixelHelper[0] / 255.0f, m_vReadPixelHelper[1] / 255.0f, m_vReadPixelHelper[2] / 255.0f, 1.0f); + } + else + { + *pCommand->m_pColor = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f); + } + + return true; + } + + [[nodiscard]] bool Cmd_Screenshot(const CCommandBuffer::SCommand_TrySwapAndScreenshot *pCommand) + { + if(!*pCommand->m_pSwapped && !NextFrame()) + return false; + *pCommand->m_pSwapped = true; + + uint32_t Width; + uint32_t Height; + CImageInfo::EImageFormat Format; + if(GetPresentedImageDataImpl(Width, Height, Format, m_vScreenshotHelper, true, {})) + { + const size_t ImgSize = (size_t)Width * (size_t)Height * CImageInfo::PixelSize(Format); pCommand->m_pImage->m_pData = malloc(ImgSize); mem_copy(pCommand->m_pImage->m_pData, m_vScreenshotHelper.data(), ImgSize); } diff --git a/src/engine/client/graphics_threaded.cpp b/src/engine/client/graphics_threaded.cpp index 64d2885ee..1f1edb29c 100644 --- a/src/engine/client/graphics_threaded.cpp +++ b/src/engine/client/graphics_threaded.cpp @@ -784,19 +784,21 @@ public: } }; -bool CGraphics_Threaded::ScreenshotDirect() +void CGraphics_Threaded::ScreenshotDirect(bool *pSwapped) { - // add swap command - CImageInfo Image; + if(!m_DoScreenshot) + return; + m_DoScreenshot = false; + if(!WindowActive()) + return; - bool DidSwap = false; + CImageInfo Image; CCommandBuffer::SCommand_TrySwapAndScreenshot Cmd; Cmd.m_pImage = &Image; - Cmd.m_pSwapped = &DidSwap; + Cmd.m_pSwapped = pSwapped; AddCmd(Cmd); - // kick the buffer and wait for the result KickCommandBuffer(); WaitForIdle(); @@ -804,8 +806,6 @@ bool CGraphics_Threaded::ScreenshotDirect() { m_pEngine->AddJob(std::make_shared(m_pStorage, m_pConsole, m_aScreenshotName, Image.m_Width, Image.m_Height, Image.m_pData)); } - - return DidSwap; } void CGraphics_Threaded::TextureSet(CTextureHandle TextureID) @@ -2755,6 +2755,32 @@ void CGraphics_Threaded::NotifyWindow() return m_pBackend->NotifyWindow(); } +void CGraphics_Threaded::ReadPixel(ivec2 Position, ColorRGBA *pColor) +{ + dbg_assert(Position.x >= 0 && Position.x < ScreenWidth(), "ReadPixel position x out of range"); + dbg_assert(Position.y >= 0 && Position.y < ScreenHeight(), "ReadPixel position y out of range"); + + m_ReadPixelPosition = Position; + m_pReadPixelColor = pColor; +} + +void CGraphics_Threaded::ReadPixelDirect(bool *pSwapped) +{ + if(m_pReadPixelColor == nullptr) + return; + + CCommandBuffer::SCommand_TrySwapAndReadPixel Cmd; + Cmd.m_Position = m_ReadPixelPosition; + Cmd.m_pColor = m_pReadPixelColor; + Cmd.m_pSwapped = pSwapped; + AddCmd(Cmd); + + KickCommandBuffer(); + WaitForIdle(); + + m_pReadPixelColor = nullptr; +} + void CGraphics_Threaded::TakeScreenshot(const char *pFilename) { // TODO: screenshot support @@ -2781,23 +2807,16 @@ void CGraphics_Threaded::Swap() } } - bool TookScreenshotAndSwapped = false; + bool Swapped = false; + ScreenshotDirect(&Swapped); + ReadPixelDirect(&Swapped); - if(m_DoScreenshot) + if(!Swapped) { - if(WindowActive()) - TookScreenshotAndSwapped = ScreenshotDirect(); - m_DoScreenshot = false; - } - - if(!TookScreenshotAndSwapped) - { - // add swap command CCommandBuffer::SCommand_Swap Cmd; AddCmd(Cmd); } - // kick the command buffer KickCommandBuffer(); // TODO: Remove when https://github.com/libsdl-org/SDL/issues/5203 is fixed #ifdef CONF_PLATFORM_MACOS diff --git a/src/engine/client/graphics_threaded.h b/src/engine/client/graphics_threaded.h index 99892a495..114c573d9 100644 --- a/src/engine/client/graphics_threaded.h +++ b/src/engine/client/graphics_threaded.h @@ -130,6 +130,7 @@ public: // misc CMD_MULTISAMPLING, CMD_VSYNC, + CMD_TRY_SWAP_AND_READ_PIXEL, CMD_TRY_SWAP_AND_SCREENSHOT, CMD_UPDATE_VIEWPORT, @@ -462,12 +463,21 @@ public: void *m_pOffset; }; + struct SCommand_TrySwapAndReadPixel : public SCommand + { + SCommand_TrySwapAndReadPixel() : + SCommand(CMD_TRY_SWAP_AND_READ_PIXEL) {} + ivec2 m_Position; + SColorf *m_pColor; // processor will fill this out + bool *m_pSwapped; // processor may set this to true, must be initialized to false by the caller + }; + struct SCommand_TrySwapAndScreenshot : public SCommand { SCommand_TrySwapAndScreenshot() : SCommand(CMD_TRY_SWAP_AND_SCREENSHOT) {} CImageInfo *m_pImage; // processor will fill this out, the one who adds this command must free the data as well - bool *m_pSwapped; + bool *m_pSwapped; // processor may set this to true, must be initialized to false by the caller }; struct SCommand_Swap : public SCommand @@ -917,6 +927,11 @@ class CGraphics_Threaded : public IEngineGraphics void AdjustViewport(bool SendViewportChangeToBackend); + ivec2 m_ReadPixelPosition = ivec2(0, 0); + ColorRGBA *m_pReadPixelColor = nullptr; + void ReadPixelDirect(bool *pSwapped); + void ScreenshotDirect(bool *pSwapped); + int IssueInit(); int InitWindow(); @@ -975,8 +990,6 @@ public: void CopyTextureBufferSub(uint8_t *pDestBuffer, uint8_t *pSourceBuffer, size_t FullWidth, size_t FullHeight, size_t PixelSize, size_t SubOffsetX, size_t SubOffsetY, size_t SubCopyWidth, size_t SubCopyHeight) override; void CopyTextureFromTextureBufferSub(uint8_t *pDestBuffer, size_t DestWidth, size_t DestHeight, uint8_t *pSourceBuffer, size_t SrcWidth, size_t SrcHeight, size_t PixelSize, size_t SrcSubOffsetX, size_t SrcSubOffsetY, size_t SrcSubCopyWidth, size_t SrcSubCopyHeight) override; - bool ScreenshotDirect(); - void TextureSet(CTextureHandle TextureID) override; void Clear(float r, float g, float b, bool ForceClearNow = false) override; @@ -1243,6 +1256,7 @@ public: int Init() override; void Shutdown() override; + void ReadPixel(ivec2 Position, ColorRGBA *pColor) override; void TakeScreenshot(const char *pFilename) override; void TakeCustomScreenshot(const char *pFilename) override; void Swap() override; diff --git a/src/engine/graphics.h b/src/engine/graphics.h index 86d5fc9a2..a25c8ba6d 100644 --- a/src/engine/graphics.h +++ b/src/engine/graphics.h @@ -513,6 +513,15 @@ public: virtual void ChangeColorOfCurrentQuadVertices(float r, float g, float b, float a) = 0; virtual void ChangeColorOfQuadVertices(size_t QuadOffset, unsigned char r, unsigned char g, unsigned char b, unsigned char a) = 0; + /** + * Reads the color at the specified position from the backbuffer once, + * after the next swap operation. + * + * @param Position The pixel position to read. + * @param pColor Pointer that will receive the read pixel color. + * The pointer must be valid until the next swap operation. + */ + virtual void ReadPixel(ivec2 Position, ColorRGBA *pColor) = 0; virtual void TakeScreenshot(const char *pFilename) = 0; virtual void TakeCustomScreenshot(const char *pFilename) = 0; virtual int GetVideoModes(CVideoMode *pModes, int MaxModes, int Screen) = 0; diff --git a/src/engine/textrender.h b/src/engine/textrender.h index af920ea5f..8f486ccda 100644 --- a/src/engine/textrender.h +++ b/src/engine/textrender.h @@ -132,6 +132,7 @@ MAYBE_UNUSED static const char *FONT_ICON_CIRCLE_PLAY = "\xEF\x85\x84"; MAYBE_UNUSED static const char *FONT_ICON_BORDER_ALL = "\xEF\xA1\x8C"; MAYBE_UNUSED static const char *FONT_ICON_EYE = "\xEF\x81\xAE"; MAYBE_UNUSED static const char *FONT_ICON_EYE_SLASH = "\xEF\x81\xB0"; +MAYBE_UNUSED static const char *FONT_ICON_EYE_DROPPER = "\xEF\x87\xBB"; MAYBE_UNUSED static const char *FONT_ICON_DICE_ONE = "\xEF\x94\xA5"; MAYBE_UNUSED static const char *FONT_ICON_DICE_TWO = "\xEF\x94\xA8"; diff --git a/src/game/editor/editor.cpp b/src/game/editor/editor.cpp index 435e22dee..98960463c 100644 --- a/src/game/editor/editor.cpp +++ b/src/game/editor/editor.cpp @@ -1248,6 +1248,38 @@ void CEditor::DoToolbarLayers(CUIRect ToolBar) pLayer->BrushRotate(s_RotationAmount / 360.0f * pi * 2); } } + + // Color pipette and palette + { + const float PipetteButtonWidth = 30.0f; + const float ColorPickerButtonWidth = 20.0f; + const float Spacing = 2.0f; + const size_t NumColorsShown = clamp(round_to_int((TB_Top.w - PipetteButtonWidth - 40.0f) / (ColorPickerButtonWidth + Spacing)), 1, std::size(m_aSavedColors)); + + CUIRect ColorPalette; + TB_Top.VSplitRight(NumColorsShown * (ColorPickerButtonWidth + Spacing) + PipetteButtonWidth, &TB_Top, &ColorPalette); + + // Pipette button + static char s_PipetteButton; + ColorPalette.VSplitLeft(PipetteButtonWidth, &Button, &ColorPalette); + ColorPalette.VSplitLeft(Spacing, nullptr, &ColorPalette); + if(DoButton_FontIcon(&s_PipetteButton, FONT_ICON_EYE_DROPPER, m_ColorPipetteActive ? 1 : 0, &Button, 0, "[Ctrl+Shift+C] Color pipette. Pick a color from the screen by clicking on it.", IGraphics::CORNER_ALL) || + (CLineInput::GetActiveInput() == nullptr && ModPressed && ShiftPressed && Input()->KeyPress(KEY_C))) + { + m_ColorPipetteActive = !m_ColorPipetteActive; + } + + // Palette color pickers + for(size_t i = 0; i < NumColorsShown; ++i) + { + ColorPalette.VSplitLeft(ColorPickerButtonWidth, &Button, &ColorPalette); + ColorPalette.VSplitLeft(Spacing, nullptr, &ColorPalette); + const auto &&SetColor = [&](ColorRGBA NewColor) { + m_aSavedColors[i] = NewColor; + }; + DoColorPickerButton(&m_aSavedColors[i], &Button, m_aSavedColors[i], SetColor); + } + } } // Bottom line buttons @@ -8202,9 +8234,17 @@ void CEditor::Render() MapView()->UpdateZoom(); + // Cancel color pipette with escape before closing popup menus with escape + if(m_ColorPipetteActive && UI()->ConsumeHotkey(CUI::HOTKEY_ESCAPE)) + { + m_ColorPipetteActive = false; + } + UI()->RenderPopupMenus(); FreeDynamicPopupMenus(); + UpdateColorPipette(); + if(m_Dialog == DIALOG_NONE && !m_PopupEventActivated && UI()->ConsumeHotkey(CUI::HOTKEY_ESCAPE)) { OnClose(); @@ -8274,22 +8314,105 @@ void CEditor::FreeDynamicPopupMenus() } } +void CEditor::UpdateColorPipette() +{ + if(!m_ColorPipetteActive) + return; + + static char s_PipetteScreenButton; + if(UI()->HotItem() == &s_PipetteScreenButton) + { + // Read color one pixel to the top and left as we would otherwise not read the correct + // color due to the cursor sprite being rendered over the current mouse position. + const int PixelX = clamp(round_to_int((UI()->MouseX() - 1.0f) / UI()->Screen()->w * Graphics()->ScreenWidth()), 0, Graphics()->ScreenWidth() - 1); + const int PixelY = clamp(round_to_int((UI()->MouseY() - 1.0f) / UI()->Screen()->h * Graphics()->ScreenHeight()), 0, Graphics()->ScreenHeight() - 1); + Graphics()->ReadPixel(ivec2(PixelX, PixelY), &m_PipetteColor); + } + + // Simulate button overlaying the entire screen to intercept all clicks for color pipette. + const int ButtonResult = DoButton_Editor_Common(&s_PipetteScreenButton, "", 0, UI()->Screen(), 0, "Left click to pick a color from the screen. Right click to cancel pipette mode."); + // Don't handle clicks if we are panning, so the pipette stays active while panning. + // Checking m_pContainerPanned alone is not enough, as this variable is reset when + // panning ends before this function is called. + if(m_pContainerPanned == nullptr && m_pContainerPannedLast == nullptr) + { + if(ButtonResult == 1) + { + char aClipboard[9]; + str_format(aClipboard, sizeof(aClipboard), "%08X", m_PipetteColor.PackAlphaLast()); + Input()->SetClipboardText(aClipboard); + + // Check if any of the saved colors is equal to the picked color and + // bring it to the front of the list instead of adding a duplicate. + int ShiftEnd = (int)std::size(m_aSavedColors) - 1; + for(int i = 0; i < (int)std::size(m_aSavedColors); ++i) + { + if(m_aSavedColors[i].Pack() == m_PipetteColor.Pack()) + { + ShiftEnd = i; + break; + } + } + for(int i = ShiftEnd; i > 0; --i) + { + m_aSavedColors[i] = m_aSavedColors[i - 1]; + } + m_aSavedColors[0] = m_PipetteColor; + } + if(ButtonResult > 0) + { + m_ColorPipetteActive = false; + } + } +} + void CEditor::RenderMousePointer() { if(!m_ShowMousePointer) return; + constexpr float CursorSize = 16.0f; + + // Cursor Graphics()->WrapClamp(); Graphics()->TextureSet(m_aCursorTextures[m_CursorType]); Graphics()->QuadsBegin(); if(m_CursorType == CURSOR_RESIZE_V) - Graphics()->QuadsSetRotation(pi / 2); + { + Graphics()->QuadsSetRotation(pi / 2.0f); + } if(ms_pUiGotContext == UI()->HotItem()) - Graphics()->SetColor(1, 0, 0, 1); - IGraphics::CQuadItem QuadItem(UI()->MouseX(), UI()->MouseY(), 16.0f, 16.0f); + { + Graphics()->SetColor(1.0f, 0.0f, 0.0f, 1.0f); + } + IGraphics::CQuadItem QuadItem(UI()->MouseX(), UI()->MouseY(), CursorSize, CursorSize); Graphics()->QuadsDrawTL(&QuadItem, 1); Graphics()->QuadsEnd(); Graphics()->WrapNormal(); + + // Pipette color + if(m_ColorPipetteActive) + { + CUIRect PipetteRect = {UI()->MouseX() + CursorSize, UI()->MouseY() + CursorSize, 80.0f, 20.0f}; + if(PipetteRect.x + PipetteRect.w + 2.0f > UI()->Screen()->w) + { + PipetteRect.x = UI()->MouseX() - PipetteRect.w - CursorSize / 2.0f; + } + if(PipetteRect.y + PipetteRect.h + 2.0f > UI()->Screen()->h) + { + PipetteRect.y = UI()->MouseY() - PipetteRect.h - CursorSize / 2.0f; + } + PipetteRect.Draw(ColorRGBA(0.2f, 0.2f, 0.2f, 0.7f), IGraphics::CORNER_ALL, 3.0f); + + CUIRect Pipette, Label; + PipetteRect.VSplitLeft(PipetteRect.h, &Pipette, &Label); + Pipette.Margin(2.0f, &Pipette); + Pipette.Draw(m_PipetteColor, IGraphics::CORNER_ALL, 3.0f); + + char aLabel[8]; + str_format(aLabel, sizeof(aLabel), "#%06X", m_PipetteColor.PackAlphaLast(false)); + UI()->DoLabel(&Label, aLabel, 10.0f, TEXTALIGN_MC); + } } void CEditor::Reset(bool CreateDefault) @@ -8320,6 +8443,7 @@ void CEditor::Reset(bool CreateDefault) m_MouseDeltaWx = 0; m_MouseDeltaWy = 0; m_pContainerPanned = nullptr; + m_pContainerPannedLast = nullptr; m_Map.m_Modified = false; m_Map.m_ModifiedAuto = false; @@ -8658,6 +8782,8 @@ void CEditor::OnUpdate() Reset(); } + m_pContainerPannedLast = m_pContainerPanned; + // handle key presses for(size_t i = 0; i < Input()->NumEvents(); i++) { @@ -8735,6 +8861,8 @@ void CEditor::OnWindowResize() void CEditor::OnClose() { + m_ColorPipetteActive = false; + if(m_ToolbarPreviewSound >= 0 && Sound()->IsPlaying(m_ToolbarPreviewSound)) Sound()->Pause(m_ToolbarPreviewSound); if(m_FilePreviewSound >= 0 && Sound()->IsPlaying(m_FilePreviewSound)) diff --git a/src/game/editor/editor.h b/src/game/editor/editor.h index 56a91b311..372c084a9 100644 --- a/src/game/editor/editor.h +++ b/src/game/editor/editor.h @@ -396,6 +396,11 @@ public: m_QuadKnifeCount = 0; mem_zero(m_aQuadKnifePoints, sizeof(m_aQuadKnifePoints)); + for(size_t i = 0; i < std::size(m_aSavedColors); ++i) + { + m_aSavedColors[i] = color_cast(ColorHSLA(i / (float)std::size(m_aSavedColors), 1.0f, 0.5f)); + } + m_CheckerTexture.Invalidate(); m_BackgroundTexture.Invalidate(); for(int i = 0; i < NUM_CURSORS; i++) @@ -467,6 +472,7 @@ public: void RenderPressedKeys(CUIRect View); void RenderSavingIndicator(CUIRect View); void FreeDynamicPopupMenus(); + void UpdateColorPipette(); void RenderMousePointer(); std::vector GetSelectedQuads(); @@ -686,7 +692,8 @@ public: float m_MouseDeltaY; float m_MouseDeltaWx; float m_MouseDeltaWy; - void *m_pContainerPanned; + const void *m_pContainerPanned; + const void *m_pContainerPannedLast; enum EShowTile { @@ -745,6 +752,11 @@ public: int m_QuadKnifeCount; vec2 m_aQuadKnifePoints[4]; + // Color palette and pipette + ColorRGBA m_aSavedColors[8]; + ColorRGBA m_PipetteColor = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f); + bool m_ColorPipetteActive = false; + IGraphics::CTextureHandle m_CheckerTexture; IGraphics::CTextureHandle m_BackgroundTexture;