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;