Index: ps/trunk/source/graphics/Canvas2D.cpp =================================================================== --- ps/trunk/source/graphics/Canvas2D.cpp (revision 25592) +++ ps/trunk/source/graphics/Canvas2D.cpp (revision 25593) @@ -1,100 +1,151 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "Canvas2D.h" #include "graphics/Color.h" #include "graphics/ShaderManager.h" #include "graphics/TextureManager.h" #include "gui/GUIMatrix.h" #include "maths/Rect.h" #include "maths/Vector2D.h" #include "ps/CStrInternStatic.h" #include "renderer/Renderer.h" #include +namespace +{ + +// Array of 2D elements unrolled into 1D array. +using PlaneArray2D = std::array; + +void DrawTextureImpl(CTexturePtr texture, + const PlaneArray2D& vertices, const PlaneArray2D& uvs, + const CColor& multiply, const CColor& add) +{ + CShaderDefines defines; + CShaderTechniquePtr tech = g_Renderer.GetShaderManager().LoadEffect( + str_canvas2d, g_Renderer.GetSystemShaderDefines(), defines); + tech->BeginPass(); + CShaderProgramPtr shader = tech->GetShader(); + + shader->BindTexture(str_tex, texture); + shader->Uniform(str_transform, GetDefaultGuiMatrix()); + shader->Uniform(str_colorAdd, add); + shader->Uniform(str_colorMul, multiply); + shader->VertexPointer(2, GL_FLOAT, 0, vertices.data()); + shader->TexCoordPointer(GL_TEXTURE0, 2, GL_FLOAT, 0, uvs.data()); + shader->AssertPointersBound(); + + if (!g_Renderer.DoSkipSubmit()) + glDrawArrays(GL_TRIANGLE_FAN, 0, vertices.size() / 2); + + tech->EndPass(); +} + +} // anonymous namespace + void CCanvas2D::DrawLine(const std::vector& points, const float width, const CColor& color) { std::vector vertices; vertices.reserve(points.size() * 3); for (const CVector2D& point : points) { vertices.emplace_back(point.X); vertices.emplace_back(point.Y); vertices.emplace_back(0.0f); } // Setup the render state CMatrix3D transform = GetDefaultGuiMatrix(); CShaderDefines lineDefines; CShaderTechniquePtr tech = g_Renderer.GetShaderManager().LoadEffect(str_gui_solid, g_Renderer.GetSystemShaderDefines(), lineDefines); tech->BeginPass(); CShaderProgramPtr shader = tech->GetShader(); shader->Uniform(str_transform, transform); shader->Uniform(str_color, color); shader->VertexPointer(3, GL_FLOAT, 0, vertices.data()); shader->AssertPointersBound(); #if !CONFIG2_GLES glEnable(GL_LINE_SMOOTH); #endif glLineWidth(width); if (!g_Renderer.DoSkipSubmit()) glDrawArrays(GL_LINE_STRIP, 0, vertices.size() / 3); glLineWidth(1.0f); #if !CONFIG2_GLES glDisable(GL_LINE_SMOOTH); #endif tech->EndPass(); } void CCanvas2D::DrawRect(const CRect& rect, const CColor& color) { - const std::array uvs = { + const PlaneArray2D uvs = { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f }; - const std::array vertices = { + const PlaneArray2D vertices = { rect.left, rect.bottom, rect.right, rect.bottom, rect.right, rect.top, rect.left, rect.top }; - CShaderDefines defines; - CShaderTechniquePtr tech = g_Renderer.GetShaderManager().LoadEffect( - str_canvas2d, g_Renderer.GetSystemShaderDefines(), defines); - tech->BeginPass(); - CShaderProgramPtr shader = tech->GetShader(); + DrawTextureImpl( + g_Renderer.GetTextureManager().GetTransparentTexture(), + vertices, uvs, CColor(0.0f, 0.0f, 0.0f, 0.0f), color); +} - shader->BindTexture(str_tex, g_Renderer.GetTextureManager().GetTransparentTexture()); - shader->Uniform(str_transform, GetDefaultGuiMatrix()); - shader->Uniform(str_colorAdd, color); - shader->Uniform(str_colorMul, CColor(0.0f, 0.0f, 0.0f, 0.0f)); - shader->VertexPointer(2, GL_FLOAT, 0, vertices.data()); - shader->TexCoordPointer(GL_TEXTURE0, 2, GL_FLOAT, 0, uvs.data()); - shader->AssertPointersBound(); +void CCanvas2D::DrawTexture(CTexturePtr texture, const CRect& destination) +{ + DrawTexture(texture, + destination, CRect(0, 0, texture->GetWidth(), texture->GetHeight()), + CColor(1.0f, 1.0f, 1.0f, 1.0f), CColor(0.0f, 0.0f, 0.0f, 0.0f)); +} - if (!g_Renderer.DoSkipSubmit()) - glDrawArrays(GL_TRIANGLE_FAN, 0, vertices.size() / 2); +void CCanvas2D::DrawTexture( + CTexturePtr texture, const CRect& destination, const CRect& source, + const CColor& multiply, const CColor& add) +{ + PlaneArray2D uvs = { + source.left, source.bottom, + source.right, source.bottom, + source.right, source.top, + source.left, source.top + }; + for (size_t idx = 0; idx < uvs.size() / 2; idx += 2) + { + uvs[idx + 0] /= texture->GetWidth(); + uvs[idx + 1] /= texture->GetHeight(); + } + const PlaneArray2D vertices = { + destination.left, destination.bottom, + destination.right, destination.bottom, + destination.right, destination.top, + destination.left, destination.top + }; - tech->EndPass(); -} + DrawTextureImpl( + g_Renderer.GetTextureManager().GetTransparentTexture(), + vertices, uvs, multiply, add); +} \ No newline at end of file Index: ps/trunk/source/graphics/Canvas2D.h =================================================================== --- ps/trunk/source/graphics/Canvas2D.h (revision 25592) +++ ps/trunk/source/graphics/Canvas2D.h (revision 25593) @@ -1,39 +1,63 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_CANVAS2D #define INCLUDED_CANVAS2D +#include "graphics/Texture.h" #include "maths/Vector2D.h" #include class CRect; struct CColor; // Encapsulates 2D drawing functionality to hide and optimize // low level API calls. class CCanvas2D { public: - void DrawLine(const std::vector& points, const float width, const CColor& color); - + /** + * Draws a line by the given points with the width and color. + */ + void DrawLine( + const std::vector& points, + const float width, const CColor& color); + + /** + * Draws the rect filled with the color. + */ void DrawRect(const CRect& rect, const CColor& color); + + /** + * Draws a piece of the texture from the source rect into the + * destination rect. The result color is set by the following formula: + * TEXTURE_COLOR * COLOR_MULTIPLY + COLOR_ADD + */ + void DrawTexture(CTexturePtr texture, + const CRect& destination, const CRect& source, + const CColor& multiply, const CColor& add); + + /** + * A simpler version of the previous one, draws the texture into the + * destination rect without color modifications. + */ + void DrawTexture(CTexturePtr texture, const CRect& destination); }; #endif // INCLUDED_CANVAS2D Index: ps/trunk/source/gui/GUIRenderer.cpp =================================================================== --- ps/trunk/source/gui/GUIRenderer.cpp (revision 25592) +++ ps/trunk/source/gui/GUIRenderer.cpp (revision 25593) @@ -1,415 +1,415 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "GUIRenderer.h" #include "graphics/Canvas2D.h" #include "graphics/ShaderManager.h" #include "graphics/TextureManager.h" #include "gui/CGUI.h" #include "gui/CGUISprite.h" #include "gui/GUIMatrix.h" #include "gui/SettingTypes/CGUIColor.h" #include "i18n/L10n.h" #include "lib/ogl.h" #include "lib/res/h_mgr.h" #include "lib/tex/tex.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/CStrInternStatic.h" #include "ps/Filesystem.h" #include "renderer/Renderer.h" using namespace GUIRenderer; DrawCalls::DrawCalls() { } // DrawCalls needs to be copyable, so it can be used in other copyable types. // But actually copying data is hard, since we'd need to avoid losing track of // who owns various pointers, so instead we just return an empty list. // The list should get filled in again (by GUIRenderer::UpdateDrawCallCache) // before it's used for rendering. (TODO: Is this class actually used safely // in practice?) DrawCalls::DrawCalls(const DrawCalls&) : std::vector() { } DrawCalls& DrawCalls::operator=(const DrawCalls&) { return *this; } void GUIRenderer::UpdateDrawCallCache(const CGUI& pGUI, DrawCalls& Calls, const CStr& SpriteName, const CRect& Size, std::map& Sprites) { // This is called only when something has changed (like the size of the // sprite), so it doesn't need to be particularly efficient. // Clean up the old data Calls.clear(); // If this object has zero size, there's nothing to render. (This happens // with e.g. tooltips that have zero size before they're first drawn, so // it isn't necessarily an error.) if (Size.left == Size.right && Size.top == Size.bottom) return; std::map::iterator it(Sprites.find(SpriteName)); if (it == Sprites.end()) { /* * Sprite not found. Check whether this a special sprite, * and if so create a new sprite: * "stretched:filename.ext" - stretched image * "stretched:grayscale:filename.ext" - stretched grayscale image. * "cropped:0.5, 0.25" - stretch this ratio (x,y) of the top left of the image * "color:r g b a" - solid color * > "textureAsMask" - when using color, use the (optional) texture alpha channel as mask. * These can be combined, but they must be separated by a ":" * so you can have a white overlay over an stretched grayscale image with: * "grayscale:color:255 255 255 100:stretched:filename.ext" */ // Check that this can be a special sprite. if (SpriteName.ReverseFind(":") == -1 && SpriteName.Find("color(") == -1) { LOGERROR("Trying to use a sprite that doesn't exist (\"%s\").", SpriteName.c_str()); return; } CGUISprite* Sprite = new CGUISprite; VfsPath TextureName = VfsPath("art/textures/ui") / wstring_from_utf8(SpriteName.AfterLast(":")); if (SpriteName.Find("stretched:") != -1) { // TODO: Should check (nicely) that this is a valid file? SGUIImage* Image = new SGUIImage(); Image->m_TextureName = TextureName; if (SpriteName.Find("grayscale:") != -1) { Image->m_Effects = std::make_shared(); Image->m_Effects->m_Greyscale = true; } Sprite->AddImage(Image); Sprites[SpriteName] = Sprite; } else if (SpriteName.Find("cropped:") != -1) { // TODO: Should check (nicely) that this is a valid file? SGUIImage* Image = new SGUIImage(); const bool centered = SpriteName.Find("center:") != -1; CStr info = SpriteName.AfterLast("cropped:").BeforeFirst(":"); double xRatio = info.BeforeFirst(",").ToDouble(); double yRatio = info.AfterLast(",").ToDouble(); const CRect percentSize = centered ? CRect(50 - 50 / xRatio, 50 - 50 / yRatio, 50 + 50 / xRatio, 50 + 50 / yRatio) : CRect(0, 0, 100 / xRatio, 100 / yRatio); Image->m_TextureSize = CGUISize(CRect(0, 0, 0, 0), percentSize); Image->m_TextureName = TextureName; if (SpriteName.Find("grayscale:") != -1) { Image->m_Effects = std::make_shared(); Image->m_Effects->m_Greyscale = true; } Sprite->AddImage(Image); Sprites[SpriteName] = Sprite; } if (SpriteName.Find("color:") != -1) { CStrW value = wstring_from_utf8(SpriteName.AfterLast("color:").BeforeFirst(":")); SGUIImage* Image = new SGUIImage(); CGUIColor* color; // If we are using a mask, this is an effect. // Otherwise we can fallback to the "back color" attribute // TODO: we are assuming there is a filename here. if (SpriteName.Find("textureAsMask:") != -1) { Image->m_TextureName = TextureName; Image->m_Effects = std::make_shared(); color = &Image->m_Effects->m_SolidColor; } else color = &Image->m_BackColor; // Check color is valid if (!CGUI::ParseString(&pGUI, value, *color)) { LOGERROR("GUI: Error parsing sprite 'color' (\"%s\")", utf8_from_wstring(value)); return; } Sprite->AddImage(Image); Sprites[SpriteName] = Sprite; } it = Sprites.find(SpriteName); // Otherwise, just complain and give up: if (it == Sprites.end()) { SAFE_DELETE(Sprite); LOGERROR("Trying to use a sprite that doesn't exist (\"%s\").", SpriteName.c_str()); return; } } Calls.reserve(it->second->m_Images.size()); // Iterate through all the sprite's images, loading the texture and // calculating the texture coordinates std::vector::const_iterator cit; for (cit = it->second->m_Images.begin(); cit != it->second->m_Images.end(); ++cit) { SDrawCall Call(*cit); // pointers are safe since we never modify sprites/images after startup CRect ObjectSize = (*cit)->m_Size.GetSize(Size); if (ObjectSize.GetWidth() == 0.0 || ObjectSize.GetHeight() == 0.0) { // Zero sized object. Don't report as an error, since it's common for e.g. hitpoint bars. continue; // i.e. don't continue with this image } Call.m_Vertices = ObjectSize; if ((*cit)->m_RoundCoordinates) { // Round the vertex coordinates to integers, to avoid ugly filtering artifacts Call.m_Vertices.left = (int)(Call.m_Vertices.left + 0.5f); Call.m_Vertices.right = (int)(Call.m_Vertices.right + 0.5f); Call.m_Vertices.top = (int)(Call.m_Vertices.top + 0.5f); Call.m_Vertices.bottom = (int)(Call.m_Vertices.bottom + 0.5f); } if (!(*cit)->m_TextureName.empty()) { CTextureProperties textureProps(g_L10n.LocalizePath((*cit)->m_TextureName)); textureProps.SetWrap((*cit)->m_WrapMode); CTexturePtr texture = g_Renderer.GetTextureManager().CreateTexture(textureProps); texture->Prefetch(); Call.m_HasTexture = true; Call.m_Texture = texture; - Call.m_EnableBlending = false; // will be overridden if the texture has an alpha channel - Call.m_ObjectSize = ObjectSize; } else { Call.m_HasTexture = false; - // Enable blending if it's transparent (allowing a little error in the calculations) - Call.m_EnableBlending = !(fabs((*cit)->m_BackColor.a - 1.0f) < 0.0000001f); + Call.m_EnableBlending = true; } Call.m_BackColor = &(*cit)->m_BackColor; - if (!Call.m_HasTexture) { - Call.m_Shader = g_Renderer.GetShaderManager().LoadEffect(str_gui_solid); Call.m_Material = str_gui_solid; + Call.m_ColorAdd = *Call.m_BackColor; + Call.m_ColorMultiply = CColor(0.0f, 0.0f, 0.0f, 0.0f); + Call.m_Texture = g_Renderer.GetTextureManager().GetTransparentTexture(); } else if ((*cit)->m_Effects) { if ((*cit)->m_Effects->m_AddColor != CGUIColor()) { Call.m_Shader = g_Renderer.GetShaderManager().LoadEffect(str_gui_add); Call.m_Material = str_gui_add; Call.m_ShaderColorParameter = (*cit)->m_Effects->m_AddColor; // Always enable blending if something's being subtracted from // the alpha channel if ((*cit)->m_Effects->m_AddColor.a < 0.f) Call.m_EnableBlending = true; } else if ((*cit)->m_Effects->m_Greyscale) { Call.m_Shader = g_Renderer.GetShaderManager().LoadEffect(str_gui_grayscale); Call.m_Material = str_gui_grayscale; } else if ((*cit)->m_Effects->m_SolidColor != CGUIColor()) { Call.m_Shader = g_Renderer.GetShaderManager().LoadEffect(str_gui_solid_mask); Call.m_Material = str_gui_solid_mask; Call.m_ShaderColorParameter = (*cit)->m_Effects->m_SolidColor; Call.m_EnableBlending = !(fabs((*cit)->m_Effects->m_SolidColor.a - 1.0f) < 0.0000001f); } else /* Slight confusion - why no effects? */ { Call.m_Shader = g_Renderer.GetShaderManager().LoadEffect(str_gui_basic); Call.m_Material = str_gui_basic; } } else { Call.m_Shader = g_Renderer.GetShaderManager().LoadEffect(str_gui_basic); Call.m_Material = str_gui_basic; } Calls.push_back(Call); } } CRect SDrawCall::ComputeTexCoords() const { float TexWidth = m_Texture->GetWidth(); float TexHeight = m_Texture->GetHeight(); if (!TexWidth || !TexHeight) return CRect(0, 0, 1, 1); // Textures are positioned by defining a rectangular block of the // texture (usually the whole texture), and a rectangular block on // the screen. The texture is positioned to make those blocks line up. // Get the screen's position/size for the block CRect BlockScreen = m_Image->m_TextureSize.GetSize(m_ObjectSize); if (m_Image->m_FixedHAspectRatio) BlockScreen.right = BlockScreen.left + BlockScreen.GetHeight() * m_Image->m_FixedHAspectRatio; // Get the texture's position/size for the block: CRect BlockTex; // "real_texture_placement" overrides everything if (m_Image->m_TexturePlacementInFile != CRect()) BlockTex = m_Image->m_TexturePlacementInFile; // Use the whole texture else BlockTex = CRect(0, 0, TexWidth, TexHeight); // When rendering, BlockTex will be transformed onto BlockScreen. // Also, TexCoords will be transformed onto ObjectSize (giving the // UV coords at each vertex of the object). We know everything // except for TexCoords, so calculate it: CVector2D translation(BlockTex.TopLeft()-BlockScreen.TopLeft()); float ScaleW = BlockTex.GetWidth()/BlockScreen.GetWidth(); float ScaleH = BlockTex.GetHeight()/BlockScreen.GetHeight(); CRect TexCoords ( // Resize (translating to/from the origin, so the // topleft corner stays in the same place) (m_ObjectSize-m_ObjectSize.TopLeft()) .Scale(ScaleW, ScaleH) + m_ObjectSize.TopLeft() // Translate from BlockTex to BlockScreen + translation ); // The tex coords need to be scaled so that (texwidth,texheight) is // mapped onto (1,1) TexCoords.left /= TexWidth; TexCoords.right /= TexWidth; TexCoords.top /= TexHeight; TexCoords.bottom /= TexHeight; return TexCoords; } void GUIRenderer::Draw(DrawCalls& Calls, CCanvas2D& canvas) { if (Calls.empty()) return; // Called every frame, to draw the object (based on cached calculations) // TODO: batching by shader/texture/etc would be nice CMatrix3D matrix = GetDefaultGuiMatrix(); glDisable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // Iterate through each DrawCall, and execute whatever drawing code is being called for (DrawCalls::const_iterator cit = Calls.begin(); cit != Calls.end(); ++cit) { if (cit->m_HasTexture) { cit->m_Shader->BeginPass(); CShaderProgramPtr shader = cit->m_Shader->GetShader(); shader->Uniform(str_transform, matrix); shader->Uniform(str_color, cit->m_ShaderColorParameter); shader->BindTexture(str_tex, cit->m_Texture); // Shouldn't call HasAlpha before BindTexture. const bool needsBlend = cit->m_EnableBlending || cit->m_Texture->HasAlpha(); if (needsBlend) glEnable(GL_BLEND); CRect TexCoords = cit->ComputeTexCoords(); // Ensure the quad has the correct winding order, and update texcoords to match CRect Verts = cit->m_Vertices; if (Verts.right < Verts.left) { std::swap(Verts.right, Verts.left); std::swap(TexCoords.right, TexCoords.left); } if (Verts.bottom < Verts.top) { std::swap(Verts.bottom, Verts.top); std::swap(TexCoords.bottom, TexCoords.top); } std::vector data; #define ADD(u, v, x, y, z) STMT(data.push_back(u); data.push_back(v); data.push_back(x); data.push_back(y); data.push_back(z)) ADD(TexCoords.left, TexCoords.bottom, Verts.left, Verts.bottom, 0.0f); ADD(TexCoords.right, TexCoords.bottom, Verts.right, Verts.bottom, 0.0f); ADD(TexCoords.right, TexCoords.top, Verts.right, Verts.top, 0.0f); ADD(TexCoords.right, TexCoords.top, Verts.right, Verts.top, 0.0f); ADD(TexCoords.left, TexCoords.top, Verts.left, Verts.top, 0.0f); ADD(TexCoords.left, TexCoords.bottom, Verts.left, Verts.bottom, 0.0f); #undef ADD shader->TexCoordPointer(GL_TEXTURE0, 2, GL_FLOAT, 5*sizeof(float), &data[0]); shader->VertexPointer(3, GL_FLOAT, 5*sizeof(float), &data[2]); glDrawArrays(GL_TRIANGLES, 0, 6); if (needsBlend) glDisable(GL_BLEND); cit->m_Shader->EndPass(); } else { if (cit->m_EnableBlending) glEnable(GL_BLEND); // Ensure the quad has the correct winding order CRect rect = cit->m_Vertices; if (rect.right < rect.left) std::swap(rect.right, rect.left); if (rect.bottom < rect.top) std::swap(rect.bottom, rect.top); - canvas.DrawRect(rect, *(cit->m_BackColor)); + canvas.DrawTexture(cit->m_Texture, + rect, CRect(0, 0, cit->m_Texture->GetWidth(), cit->m_Texture->GetHeight()), + cit->m_ColorMultiply, cit->m_ColorAdd); if (cit->m_EnableBlending) glDisable(GL_BLEND); } } } Index: ps/trunk/source/gui/GUIRenderer.h =================================================================== --- ps/trunk/source/gui/GUIRenderer.h (revision 25592) +++ ps/trunk/source/gui/GUIRenderer.h (revision 25593) @@ -1,79 +1,82 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_GUIRENDERER #define INCLUDED_GUIRENDERER #include "graphics/Color.h" #include "graphics/ShaderTechniquePtr.h" #include "graphics/Texture.h" #include "lib/res/handle.h" #include "maths/Rect.h" #include "ps/CStrIntern.h" #include #include class CCanvas2D; class CGUI; class CGUISprite; class CStr8; struct CGUIColor; struct SGUIImage; namespace GUIRenderer { struct SDrawCall { SDrawCall(const SGUIImage* image) : m_Image(image) {} CRect ComputeTexCoords() const; const SGUIImage* m_Image; bool m_HasTexture; CTexturePtr m_Texture; CRect m_ObjectSize; bool m_EnableBlending; CShaderTechniquePtr m_Shader; CColor m_ShaderColorParameter; CRect m_Vertices; CGUIColor* m_BackColor; // Temporary type to make a soft transition to canvas rendering. CStrIntern m_Material; + + CColor m_ColorAdd; + CColor m_ColorMultiply; }; class DrawCalls : public std::vector { public: DrawCalls(); // Copy/assignment results in an empty list, not an actual copy DrawCalls(const DrawCalls&); DrawCalls& operator=(const DrawCalls&); }; void UpdateDrawCallCache(const CGUI& pGUI, DrawCalls& Calls, const CStr8& SpriteName, const CRect& Size, std::map& Sprites); void Draw(DrawCalls& Calls, CCanvas2D& canvas); } #endif // INCLUDED_GUIRENDERER Index: ps/trunk/source/ps/CConsole.cpp =================================================================== --- ps/trunk/source/ps/CConsole.cpp (revision 25592) +++ ps/trunk/source/ps/CConsole.cpp (revision 25593) @@ -1,725 +1,727 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ /* * Implements the in-game console with scripting support. */ #include "precompiled.h" #include #include "CConsole.h" #include "graphics/Canvas2D.h" #include "graphics/FontMetrics.h" #include "graphics/ShaderManager.h" #include "graphics/TextRenderer.h" #include "gui/CGUI.h" #include "gui/GUIManager.h" #include "gui/GUIMatrix.h" #include "lib/ogl.h" #include "lib/timer.h" #include "lib/utf8.h" #include "maths/MathUtil.h" #include "network/NetClient.h" #include "network/NetServer.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/CStrInternStatic.h" #include "ps/Filesystem.h" #include "ps/GameSetup/Config.h" #include "ps/Globals.h" #include "ps/Hotkey.h" #include "ps/Profile.h" #include "ps/Pyrogenesis.h" #include "renderer/Renderer.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/JSON.h" #include namespace { // For text being typed into the console. constexpr int CONSOLE_BUFFER_SIZE = 1024; const char* CONSOLE_FONT = "mono-10"; } // anonymous namespace CConsole* g_Console = 0; CConsole::CConsole() { m_bToggle = false; m_bVisible = false; m_fVisibleFrac = 0.0f; m_szBuffer = new wchar_t[CONSOLE_BUFFER_SIZE]; FlushBuffer(); m_iMsgHistPos = 1; m_charsPerPage = 0; m_prevTime = 0.0; m_bCursorVisState = true; m_cursorBlinkRate = 0.5; InsertMessage("[ 0 A.D. Console v0.14 ]"); InsertMessage(""); } CConsole::~CConsole() { delete[] m_szBuffer; } void CConsole::Init() { // Initialise console history file m_MaxHistoryLines = 200; CFG_GET_VAL("console.history.size", m_MaxHistoryLines); m_sHistoryFile = L"config/console.txt"; LoadHistory(); UpdateScreenSize(g_xres, g_yres); // Calculate and store the line spacing const CFontMetrics font{CStrIntern(CONSOLE_FONT)}; m_iFontHeight = font.GetLineSpacing(); m_iFontWidth = font.GetCharacterWidth(L'C'); m_charsPerPage = static_cast(g_xres / m_iFontWidth); // Offset by an arbitrary amount, to make it fit more nicely m_iFontOffset = 7; m_cursorBlinkRate = 0.5; CFG_GET_VAL("gui.cursorblinkrate", m_cursorBlinkRate); } void CConsole::UpdateScreenSize(int w, int h) { m_fX = 0; m_fY = 0; float height = h * 0.6f; m_fWidth = w / g_GuiScale; m_fHeight = height / g_GuiScale; } void CConsole::ToggleVisible() { m_bToggle = true; m_bVisible = !m_bVisible; // TODO: this should be based on input focus, not visibility if (m_bVisible) SDL_StartTextInput(); else SDL_StopTextInput(); } void CConsole::SetVisible(bool visible) { if (visible != m_bVisible) m_bToggle = true; m_bVisible = visible; if (visible) { m_prevTime = 0.0; m_bCursorVisState = false; } } void CConsole::FlushBuffer() { // Clear the buffer and set the cursor and length to 0 memset(m_szBuffer, '\0', sizeof(wchar_t) * CONSOLE_BUFFER_SIZE); m_iBufferPos = m_iBufferLength = 0; } void CConsole::Update(const float deltaRealTime) { if(m_bToggle) { const float AnimateTime = .30f; const float Delta = deltaRealTime / AnimateTime; if(m_bVisible) { m_fVisibleFrac += Delta; if(m_fVisibleFrac > 1.0f) { m_fVisibleFrac = 1.0f; m_bToggle = false; } } else { m_fVisibleFrac -= Delta; if(m_fVisibleFrac < 0.0f) { m_fVisibleFrac = 0.0f; m_bToggle = false; } } } } void CConsole::Render() { if (! (m_bVisible || m_bToggle) ) return; PROFILE3_GPU("console"); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); DrawWindow(); CShaderTechniquePtr textTech = g_Renderer.GetShaderManager().LoadEffect(str_gui_text); textTech->BeginPass(); CTextRenderer textRenderer(textTech->GetShader()); textRenderer.Font(CStrIntern(CONSOLE_FONT)); // animation: slide in from top of screen CMatrix3D transform = GetDefaultGuiMatrix(); const float DeltaY = (1.0f - m_fVisibleFrac) * m_fHeight; transform.PostTranslate(m_fX, m_fY - DeltaY, 0.0f); // move to window position textRenderer.SetTransform(transform); DrawHistory(textRenderer); DrawBuffer(textRenderer); textRenderer.Render(); textTech->EndPass(); glDisable(GL_BLEND); } void CConsole::DrawWindow() { std::vector points = { CVector2D{m_fWidth, 0.0f}, CVector2D{1.0f, 0.0f}, CVector2D{1.0f, m_fHeight - 1.0f}, CVector2D{m_fWidth, m_fHeight - 1.0f}, CVector2D{m_fWidth, 0.0f} }; for (CVector2D& point : points) point += CVector2D{m_fX, m_fY - (1.0f - m_fVisibleFrac) * m_fHeight}; CCanvas2D canvas; canvas.DrawRect(CRect(points[1], points[3]), CColor(0.0f, 0.0f, 0.5f, 0.6f)); canvas.DrawLine(points, 1.0f, CColor(0.5f, 0.5f, 0.0f, 0.6f)); if (m_fHeight > m_iFontHeight + 4) { points = { CVector2D{0.0f, m_fHeight - static_cast(m_iFontHeight) - 4.0f}, CVector2D{m_fWidth, m_fHeight - static_cast(m_iFontHeight) - 4.0f} }; + for (CVector2D& point : points) + point += CVector2D{m_fX, m_fY - (1.0f - m_fVisibleFrac) * m_fHeight}; canvas.DrawLine(points, 1.0f, CColor(0.5f, 0.5f, 0.0f, 0.6f)); } } void CConsole::DrawHistory(CTextRenderer& textRenderer) { int i = 1; std::deque::iterator Iter; //History iterator std::lock_guard lock(m_Mutex); // needed for safe access to m_deqMsgHistory textRenderer.Color(1.0f, 1.0f, 1.0f); for (Iter = m_deqMsgHistory.begin(); Iter != m_deqMsgHistory.end() && (((i - m_iMsgHistPos + 1) * m_iFontHeight) < m_fHeight); ++Iter) { if (i >= m_iMsgHistPos) textRenderer.Put(9.0f, m_fHeight - (float)m_iFontOffset - (float)m_iFontHeight * (i - m_iMsgHistPos + 1), Iter->c_str()); i++; } } // Renders the buffer to the screen. void CConsole::DrawBuffer(CTextRenderer& textRenderer) { if (m_fHeight < m_iFontHeight) return; CMatrix3D savedTransform = textRenderer.GetTransform(); textRenderer.Translate(2.0f, m_fHeight - (float)m_iFontOffset + 1.0f, 0.0f); textRenderer.Color(1.0f, 1.0f, 0.0f); textRenderer.PutAdvance(L"]"); textRenderer.Color(1.0f, 1.0f, 1.0f); if (m_iBufferPos == 0) DrawCursor(textRenderer); for (int i = 0; i < m_iBufferLength; i++) { textRenderer.PrintfAdvance(L"%lc", m_szBuffer[i]); if (m_iBufferPos-1 == i) DrawCursor(textRenderer); } textRenderer.SetTransform(savedTransform); } void CConsole::DrawCursor(CTextRenderer& textRenderer) { if (m_cursorBlinkRate > 0.0) { // check if the cursor visibility state needs to be changed double currTime = timer_Time(); if ((currTime - m_prevTime) >= m_cursorBlinkRate) { m_bCursorVisState = !m_bCursorVisState; m_prevTime = currTime; } } else { // Should always be visible m_bCursorVisState = true; } if(m_bCursorVisState) { // Slightly translucent yellow textRenderer.Color(1.0f, 1.0f, 0.0f, 0.8f); // Cursor character is chosen to be an underscore textRenderer.Put(0.0f, 0.0f, L"_"); // Revert to the standard text color textRenderer.Color(1.0f, 1.0f, 1.0f); } } bool CConsole::IsEOB() const { return m_iBufferPos == m_iBufferLength; } bool CConsole::IsBOB() const { return m_iBufferPos == 0; } bool CConsole::IsFull() const { return m_iBufferLength == CONSOLE_BUFFER_SIZE; } bool CConsole::IsEmpty() const { return m_iBufferLength == 0; } //Inserts a character into the buffer. void CConsole::InsertChar(const int szChar, const wchar_t cooked) { static int iHistoryPos = -1; if (!m_bVisible) return; switch (szChar) { case SDLK_RETURN: iHistoryPos = -1; m_iMsgHistPos = 1; ProcessBuffer(m_szBuffer); FlushBuffer(); return; case SDLK_TAB: // Auto Complete return; case SDLK_BACKSPACE: if (IsEmpty() || IsBOB()) return; if (m_iBufferPos == m_iBufferLength) m_szBuffer[m_iBufferPos - 1] = '\0'; else { for (int j = m_iBufferPos-1; j < m_iBufferLength-1; j++) m_szBuffer[j] = m_szBuffer[j+1]; // move chars to left m_szBuffer[m_iBufferLength-1] = '\0'; } m_iBufferPos--; m_iBufferLength--; return; case SDLK_DELETE: if (IsEmpty() || IsEOB()) return; if (m_iBufferPos == m_iBufferLength-1) { m_szBuffer[m_iBufferPos] = '\0'; m_iBufferLength--; } else { if (g_scancodes[SDL_SCANCODE_LCTRL] || g_scancodes[SDL_SCANCODE_RCTRL]) { // Make Ctrl-Delete delete up to end of line m_szBuffer[m_iBufferPos] = '\0'; m_iBufferLength = m_iBufferPos; } else { // Delete just one char and move the others left for(int j=m_iBufferPos; j lock(m_Mutex); // needed for safe access to m_deqMsgHistory int linesShown = (int)m_fHeight/m_iFontHeight - 4; m_iMsgHistPos = Clamp(static_cast(m_deqMsgHistory.size()) - linesShown, 1, static_cast(m_deqMsgHistory.size())); } else { m_iBufferPos = 0; } return; case SDLK_END: if (g_scancodes[SDL_SCANCODE_LCTRL] || g_scancodes[SDL_SCANCODE_RCTRL]) { m_iMsgHistPos = 1; } else { m_iBufferPos = m_iBufferLength; } return; case SDLK_LEFT: if (m_iBufferPos) m_iBufferPos--; return; case SDLK_RIGHT: if (m_iBufferPos != m_iBufferLength) m_iBufferPos++; return; // BEGIN: Buffer History Lookup case SDLK_UP: if (m_deqBufHistory.size() && iHistoryPos != (int)m_deqBufHistory.size() - 1) { iHistoryPos++; SetBuffer(m_deqBufHistory.at(iHistoryPos).c_str()); m_iBufferPos = m_iBufferLength; } return; case SDLK_DOWN: if (m_deqBufHistory.size()) { if (iHistoryPos > 0) { iHistoryPos--; SetBuffer(m_deqBufHistory.at(iHistoryPos).c_str()); m_iBufferPos = m_iBufferLength; } else if (iHistoryPos == 0) { iHistoryPos--; FlushBuffer(); } } return; // END: Buffer History Lookup // BEGIN: Message History Lookup case SDLK_PAGEUP: { std::lock_guard lock(m_Mutex); // needed for safe access to m_deqMsgHistory if (m_iMsgHistPos != (int)m_deqMsgHistory.size()) m_iMsgHistPos++; return; } case SDLK_PAGEDOWN: if (m_iMsgHistPos != 1) m_iMsgHistPos--; return; // END: Message History Lookup default: //Insert a character if (IsFull()) return; if (cooked == 0) return; if (IsEOB()) //are we at the end of the buffer? m_szBuffer[m_iBufferPos] = cooked; //cat char onto end else { //we need to insert int i; for(i=m_iBufferLength; i>m_iBufferPos; i--) m_szBuffer[i] = m_szBuffer[i-1]; // move chars to right m_szBuffer[i] = cooked; } m_iBufferPos++; m_iBufferLength++; return; } } void CConsole::InsertMessage(const std::string& message) { // (TODO: this text-wrapping is rubbish since we now use variable-width fonts) //Insert newlines to wraparound text where needed std::wstring wrapAround = wstring_from_utf8(message.c_str()); std::wstring newline(L"\n"); size_t oldNewline=0; size_t distance; //make sure everything has been initialized if ( m_charsPerPage != 0 ) { while ( oldNewline+m_charsPerPage < wrapAround.length() ) { distance = wrapAround.find(newline, oldNewline) - oldNewline; if ( distance > m_charsPerPage ) { oldNewline += m_charsPerPage; wrapAround.insert( oldNewline++, newline ); } else oldNewline += distance+1; } } // Split into lines and add each one individually oldNewline = 0; { std::lock_guard lock(m_Mutex); // needed for safe access to m_deqMsgHistory while ( (distance = wrapAround.find(newline, oldNewline)) != wrapAround.npos) { distance -= oldNewline; m_deqMsgHistory.push_front(wrapAround.substr(oldNewline, distance)); oldNewline += distance+1; } m_deqMsgHistory.push_front(wrapAround.substr(oldNewline)); } } const wchar_t* CConsole::GetBuffer() { m_szBuffer[m_iBufferLength] = 0; return( m_szBuffer ); } void CConsole::SetBuffer(const wchar_t* szMessage) { int oldBufferPos = m_iBufferPos; // remember since FlushBuffer will set it to 0 FlushBuffer(); wcsncpy(m_szBuffer, szMessage, CONSOLE_BUFFER_SIZE); m_szBuffer[CONSOLE_BUFFER_SIZE-1] = 0; m_iBufferLength = static_cast(wcslen(m_szBuffer)); m_iBufferPos = std::min(oldBufferPos, m_iBufferLength); } void CConsole::ProcessBuffer(const wchar_t* szLine) { if (!szLine || wcslen(szLine) <= 0) return; ENSURE(wcslen(szLine) < CONSOLE_BUFFER_SIZE); m_deqBufHistory.push_front(szLine); SaveHistory(); // Do this each line for the moment; if a script causes // a crash it's a useful record. // Process it as JavaScript std::shared_ptr pScriptInterface = g_GUI->GetActiveGUI()->GetScriptInterface(); ScriptRequest rq(*pScriptInterface); JS::RootedValue rval(rq.cx); pScriptInterface->Eval(CStrW(szLine).ToUTF8().c_str(), &rval); if (!rval.isUndefined()) InsertMessage(Script::ToString(rq, &rval)); } void CConsole::LoadHistory() { // note: we don't care if this file doesn't exist or can't be read; // just don't load anything in that case. // do this before LoadFile to avoid an error message if file not found. if (!VfsFileExists(m_sHistoryFile)) return; std::shared_ptr buf; size_t buflen; if (g_VFS->LoadFile(m_sHistoryFile, buf, buflen) < 0) return; CStr bytes ((char*)buf.get(), buflen); CStrW str (bytes.FromUTF8()); size_t pos = 0; while (pos != CStrW::npos) { pos = str.find('\n'); if (pos != CStrW::npos) { if (pos > 0) m_deqBufHistory.push_front(str.Left(str[pos-1] == '\r' ? pos - 1 : pos)); str = str.substr(pos + 1); } else if (str.length() > 0) m_deqBufHistory.push_front(str); } } void CConsole::SaveHistory() { WriteBuffer buffer; const int linesToSkip = (int)m_deqBufHistory.size() - m_MaxHistoryLines; std::deque::reverse_iterator it = m_deqBufHistory.rbegin(); if(linesToSkip > 0) std::advance(it, linesToSkip); for (; it != m_deqBufHistory.rend(); ++it) { CStr8 line = CStrW(*it).ToUTF8(); buffer.Append(line.data(), line.length()); static const char newline = '\n'; buffer.Append(&newline, 1); } g_VFS->CreateFile(m_sHistoryFile, buffer.Data(), buffer.Size()); } static bool isUnprintableChar(SDL_Keysym key) { switch (key.sym) { // We want to allow some, which are handled specially case SDLK_RETURN: case SDLK_TAB: case SDLK_BACKSPACE: case SDLK_DELETE: case SDLK_HOME: case SDLK_END: case SDLK_LEFT: case SDLK_RIGHT: case SDLK_UP: case SDLK_DOWN: case SDLK_PAGEUP: case SDLK_PAGEDOWN: return true; // Ignore the others default: return false; } } InReaction conInputHandler(const SDL_Event_* ev) { if (!g_Console) return IN_PASS; if (static_cast(ev->ev.type) == SDL_HOTKEYPRESS) { std::string hotkey = static_cast(ev->ev.user.data1); if (hotkey == "console.toggle") { ResetActiveHotkeys(); g_Console->ToggleVisible(); return IN_HANDLED; } else if (g_Console->IsActive() && hotkey == "copy") { std::string text = utf8_from_wstring(g_Console->GetBuffer()); SDL_SetClipboardText(text.c_str()); return IN_HANDLED; } else if (g_Console->IsActive() && hotkey == "paste") { char* utf8_text = SDL_GetClipboardText(); if (!utf8_text) return IN_HANDLED; std::wstring text = wstring_from_utf8(utf8_text); SDL_free(utf8_text); for (wchar_t c : text) g_Console->InsertChar(0, c); return IN_HANDLED; } } if (!g_Console->IsActive()) return IN_PASS; // In SDL2, we no longer get Unicode wchars via SDL_Keysym // we use text input events instead and they provide UTF-8 chars if (ev->ev.type == SDL_TEXTINPUT) { // TODO: this could be more efficient with an interface to insert UTF-8 strings directly std::wstring wstr = wstring_from_utf8(ev->ev.text.text); for (size_t i = 0; i < wstr.length(); ++i) g_Console->InsertChar(0, wstr[i]); return IN_HANDLED; } // TODO: text editing events for IME support if (ev->ev.type != SDL_KEYDOWN && ev->ev.type != SDL_KEYUP) return IN_PASS; int sym = ev->ev.key.keysym.sym; // Stop unprintable characters (ctrl+, alt+ and escape). if (ev->ev.type == SDL_KEYDOWN && isUnprintableChar(ev->ev.key.keysym) && !HotkeyIsPressed("console.toggle")) { g_Console->InsertChar(sym, 0); return IN_HANDLED; } // We have a probably printable key - we should return HANDLED so it can't trigger hotkeys. // However, if Ctrl/Meta modifiers are active (or it's escape), just pass it through instead, // assuming that we are indeed trying to trigger hotkeys (e.g. copy/paste). // Also ignore the key if we are trying to toggle the console off. // See also similar logic in CInput.cpp if (EventWillFireHotkey(ev, "console.toggle") || g_scancodes[SDL_SCANCODE_LCTRL] || g_scancodes[SDL_SCANCODE_RCTRL] || g_scancodes[SDL_SCANCODE_LGUI] || g_scancodes[SDL_SCANCODE_RGUI]) return IN_PASS; return IN_HANDLED; }